Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,12 @@ Standalone CLI:
npx @eforge-build/eforge build "Add rate limiting to the API"
npx @eforge-build/eforge build plans/my-feature-prd.md

# Deterministic handoff: enqueue a build that waits for an upstream build to finish
# Use --after <queue-id> to create an explicit dependency on an active build.
# Active upstream (pending/running/waiting): held until upstream completes.
# Completed upstream with artifact: enqueued immediately as an eligible dependent.
npx @eforge-build/eforge build "Add e2e tests for rate limiting" --after q-abc123

# Run a saved playbook
npx @eforge-build/eforge play docs-sync

Expand Down
4 changes: 3 additions & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,9 @@ The **daemon** (`eforge daemon start`) is a long-running process that watches th

**Auto-build** mode (default) automatically processes PRDs on enqueue. The daemon spawns a worker process for each build, tracking progress via SQLite. Failed builds pause auto-build until manually restarted. The daemon shuts down after a configurable idle timeout.

**Piggyback scheduling** — PRDs with a non-empty `dependsOn` array are held in a `waiting` state and not dispatched until each referenced upstream entry reaches a terminal state. On any terminal transition the dispatcher re-evaluates all `waiting` entries: upstream `completed` removes that id from the dependent's unsatisfied set and, once the set is empty, transitions the dependent to `pending` so the normal concurrency limit picks it up; upstream `failed` or `cancelled` transitions the dependent directly to `skipped` with a reason of `upstream <id> <state>`. Skip propagation is recursive - if a `skipped` entry itself has dependents, those also become `skipped`. The `eforge queue list` command renders piggybacked entries indented under their parent (two-space indent with an `↳ ` prefix); the monitor UI applies the same nesting using shadcn primitives. Piggybacked playbooks auto-enqueue their generated plan directly into the build pipeline without an interactive review gate - the user approved the playbook contents at authoring time.
**Explicit deterministic handoff** — Pass `afterQueueId` to the `eforge_build` MCP/Pi tool or `--after <queue-id>` to the CLI to create an explicit dependency on an upstream queue entry. Active upstream items (pending/running/waiting) cause the dependent PRD to be placed in `.eforge/queue/waiting/` and unblocked when the upstream reaches a terminal state. Completed upstream items with a usable artifact cause the dependent PRD to be enqueued immediately as an eligible dependent in the queue root. Failed, skipped, and unknown IDs are rejected at enqueue time. Explicit `afterQueueId` takes precedence over any automatic dependency detection performed during enqueue. Dependency detector inference remains best effort and is used only when no explicit dependency is supplied.

**Piggyback scheduling** — PRDs with unsatisfied active dependencies (upstream entries still in `pending`, `running`, or `waiting/`) are held in a `waiting` state and not dispatched until each such upstream entry reaches a terminal state. PRDs whose dependencies are already completed with usable artifacts are eligible immediately and remain in the queue root rather than `waiting/`. On any terminal transition the dispatcher re-evaluates all `waiting` entries: upstream `completed` removes that id from the dependent's unsatisfied set and, once the set is empty, transitions the dependent to `pending` so the normal concurrency limit picks it up; upstream `failed` or `cancelled` transitions the dependent directly to `skipped` with a reason of `upstream <id> <state>`. Skip propagation is recursive - if a `skipped` entry itself has dependents, those also become `skipped`. The `eforge queue list` command renders piggybacked entries indented under their parent (two-space indent with an `↳ ` prefix); the monitor UI applies the same nesting using shadcn primitives. Piggybacked playbooks auto-enqueue their generated plan directly into the build pipeline without an interactive review gate - the user approved the playbook contents at authoring time.

When a build fails, the queue parent's finalize handler runs the recovery-analyst agent inline (synchronously, before the PRD is moved) against the still-present `state.json`. The PRD is moved to `failed/` and both sidecar files (`<prdId>.recovery.md`, `<prdId>.recovery.json`) are written via filesystem-only operations - there is no window where `failed/` has a PRD without sidecars. Queue state lives under `.eforge/queue/` which is gitignored; no git commits are produced for queue mutations. The recovery-analyst operates with `tools: 'none'` and runs under a 90-second timeout; on any error or timeout, a `manual` verdict sidecar is written anyway. The sidecar schema is v2, which adds optional `partial` and `recoveryError` fields to the verdict for degraded-context cases. Recovery can also be triggered manually via the `eforge_recover` MCP tool (Claude Code plugin), the `recover` Pi tool, or `eforge recover` CLI - useful for backfilling sidecars on PRDs already in `failed/` before this architecture was in place. When `state.json` is missing (manual backfill scenario), `buildFailureSummary` synthesizes a partial summary from monitor.db events and git history, setting `partial: true` on the result. The resulting sidecar can be read back through `eforge_read_recovery_sidecar` / `readRecoverySidecar`. Once a verdict has been reviewed, `applyRecovery(prdId)` on `EforgeEngine` enacts it: `retry` moves the PRD back to the queue root and removes both sidecars (filesystem-only); `split` routes the agent's successor PRD body through `enqueuePrd` with `depends_on: []` — stripping any agent-emitted frontmatter before writing so the successor always starts with clean, dependency-free frontmatter — leaving the failed PRD and sidecars in place; `abandon` removes all three paths under `.eforge/queue/failed/`; `manual` is a no-op that returns `noAction: true`. The apply path is exposed as `POST /api/recover/apply` on the daemon, the `eforge_apply_recovery` MCP tool (Claude Code plugin and Pi), and the `eforge apply-recovery <prdId>` CLI subcommand. The `/api/queue` endpoint post-filters `dependsOn` before responding: terminal items (`failed`, `skipped`) never expose a `dependsOn` field, and live items (`pending`, `running`) expose only `dependsOn` IDs that match other live items in the same response - mirroring `resolveQueueOrder`'s runtime semantics so the UI's view of dependencies is always consistent with what the scheduler actually acts on.

Expand Down
9 changes: 8 additions & 1 deletion docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,13 @@ prdQueue:
dir: .eforge/queue # Where queued PRDs are stored (gitignored — runtime state)
autoBuild: true # Daemon automatically builds after enqueue
watchPollIntervalMs: 5000 # Poll interval for watch mode (ms)
# Explicit build dependency (per-enqueue, not a config key):
# Pass --after <queue-id> to the CLI or afterQueueId to the eforge_build tool
# to create a deterministic dependency on an active or completed queue entry.
# Active upstream items (pending/running/waiting) are held in .eforge/queue/waiting/
# and unblocked when the upstream completes. Completed upstream items with a usable
# artifact are enqueued immediately as eligible dependents. Explicit afterQueueId
# takes precedence over automatic dependency detection (which remains best effort).

daemon:
idleShutdownMs: 7200000 # Idle timeout before auto-shutdown (2 hours). Set to 0 to disable.
Expand Down Expand Up @@ -855,7 +862,7 @@ eforge has two dimensions of parallelism:

Controls the maximum number of PRDs built concurrently when processing the queue (`eforge build --queue` or `eforge queue run`). Default: `2`.

PRDs with `depends_on` frontmatter are held in a `waiting` state until their upstream builds reach a terminal state. When an upstream build completes, its dependents transition from `waiting` to `pending` and are dispatched normally. If an upstream build fails or is cancelled, all transitive dependents transition to `skipped` with a reason recording the upstream id and terminal state. Skip propagation is recursive - if a `skipped` entry itself has dependents, those also become `skipped`.
PRDs with `depends_on` frontmatter whose upstream builds are still active (pending, running, or waiting) are held in a `waiting/` subdirectory until each upstream reaches a terminal state. PRDs whose upstream dependencies have already completed with usable artifacts are eligible immediately and remain in the queue root rather than `waiting/`. When an active upstream build completes, its waiting dependents transition from `waiting` to `pending` and are dispatched normally. If an upstream build fails or is cancelled, all transitive dependents transition to `skipped` with a reason recording the upstream id and terminal state. Skip propagation is recursive - if a `skipped` entry itself has dependents, those also become `skipped`.

CLI override: `--max-concurrent-builds <n>`

Expand Down
4 changes: 4 additions & 0 deletions docs/stacking.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ When a PRD has exactly one `depends_on` entry and stacking is enabled, eforge au

When a PRD has multiple `depends_on` entries, eforge cannot infer the stack parent unambiguously. You must set `stack_parent` explicitly to indicate which dependency is the direct parent layer.

### Explicit handoff and stack parent

When you use `--after <queue-id>` (CLI) or `afterQueueId` (MCP/Pi tool) to create an explicit dependency, the resulting single `depends_on` entry participates in the same stack parent inference described above. If stacking is enabled and the explicit dependency is the only `depends_on` entry, eforge infers `stack_parent` from it at dispatch time — no extra configuration is needed. The explicit handoff is deterministic: dependency detector inference is not used when `afterQueueId` is supplied.

### Multi-dependency ambiguity

Multi-dependency stacking requires explicit `stack_parent`:
Expand Down
2 changes: 1 addition & 1 deletion eforge-plugin/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "eforge",
"description": "Conversational planning and MCP-based daemon integration for eforge — enqueue, run, and monitor builds from within Claude Code",
"version": "0.25.33",
"version": "0.25.34",
"commands": [
"./skills/build/build.md",
"./skills/status/status.md",
Expand Down
7 changes: 4 additions & 3 deletions eforge-plugin/skills/build/build.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
description: Enqueue a source for the eforge daemon to build via MCP tool
argument-hint: "[source] [--infer] [--profile <name>] [--landing-action <action>]"
argument-hint: "[source] [--infer] [--profile <name>] [--landing-action <action>] [--after <queue-id>]"
disable-model-invocation: true
---

Expand All @@ -16,12 +16,13 @@ Enqueue a PRD file or description for the eforge daemon to build. Uses the eforg
- `--landing-action <action>` (optional) - Override the landing action for this build. One of `pr`, `merge`, or `leave`. Precedence: this argument > PRD frontmatter > `landing.action` in `eforge/config.yaml` > engine default (`merge`). If omitted, the project config default applies. Note: `merge` on the trunk branch requires `build.allowLocalMergeToTrunk: true` in `eforge/config.yaml`.
- `--landing-auto-merge` (optional) - Enable GitHub PR auto-merge for this build. Only applies when the effective landing action is `pr`. Sends `landingAutoMerge: true` in the enqueue body, overriding the configured `landing.pr.autoMerge` policy for this run.
- `--no-landing-auto-merge` (optional) - Disable GitHub PR auto-merge for this build. Only applies when the effective landing action is `pr`. Sends `landingAutoMerge: false` in the enqueue body, overriding the configured `landing.pr.autoMerge` policy for this run.
- `--after <queue-id>` (optional) - Explicit upstream dependency. When provided, the enqueued PRD gains `depends_on: [queue-id]`. Active upstream items (pending/running/waiting) are held in `waiting/` and start automatically when the upstream completes. Completed upstream items with a usable artifact are enqueued immediately as eligible dependents. This is a deterministic handoff - it takes precedence over any automatic dependency detection. A single explicit dependency becomes the stack parent when stacking is enabled.

## Workflow

### Step 1: Resolve Source Input

Parse and remember any `--profile <name>` override before resolving the source. Determine the working source from one of four branches:
Parse and remember any `--profile <name>` override and any `--after <queue-id>` flag before resolving the source. Remove both flags from `$ARGUMENTS` before source resolution so they are not mistaken for build content. Keep the remembered queue id to include as `afterQueueId` in the enqueue call at Step 5. Determine the working source from one of four branches:

**Branch A — File path**: If `$ARGUMENTS` is a file path (ends in `.md`, `.txt`, `.yaml`, or contains `/`):
1. Verify the file exists with the Read tool
Expand Down Expand Up @@ -175,7 +176,7 @@ First, validate the project config by calling the `mcp__eforge__eforge_config` t

- If `valid` is `true`, continue silently.

Call the `mcp__eforge__eforge_build` tool with `{ source: "<source>" }`. If the user explicitly specified a profile override, include `profile: "<name>"` in the call. If an explicit landing action was selected (anything other than "Use project default"), include `landingAction: "<value>"` in the call. **Do not include `landingAction` when the user selected "Use project default" or when no landing override is present in `$ARGUMENTS` — omitting the key lets the engine inherit the configured default.** If an explicit auto-merge override was selected (anything other than "Use policy default"), include `landingAutoMerge: <true|false>` in the call. **Do not include `landingAutoMerge` when "Use policy default" was selected or when neither `--landing-auto-merge` nor `--no-landing-auto-merge` is present in `$ARGUMENTS` — omitting the key defers to the `landing.pr.autoMerge` policy.**
Call the `mcp__eforge__eforge_build` tool with `{ source: "<source>" }`. If the user explicitly specified a profile override, include `profile: "<name>"` in the call. If an explicit landing action was selected (anything other than "Use project default"), include `landingAction: "<value>"` in the call. **Do not include `landingAction` when the user selected "Use project default" or when no landing override is present in `$ARGUMENTS` — omitting the key lets the engine inherit the configured default.** If an explicit auto-merge override was selected (anything other than "Use policy default"), include `landingAutoMerge: <true|false>` in the call. **Do not include `landingAutoMerge` when "Use policy default" was selected or when neither `--landing-auto-merge` nor `--no-landing-auto-merge` is present in `$ARGUMENTS` — omitting the key defers to the `landing.pr.autoMerge` policy.** If a queue id was remembered from the `--after` flag parsed in Step 1, include `afterQueueId: "<remembered-queue-id>"` in the call. **Do not include `afterQueueId` when no `--after` flag was present in the original `$ARGUMENTS` — omitting the key means no explicit dependency and automatic dependency detection applies.**

<!-- parity-skip-start -->
Present the landing selector conversationally with the four choices (Use project default, pr, merge, leave) before calling the tool, unless an explicit override is already in `$ARGUMENTS`. On protected trunk with unsafe effective default, exclude merge and project-default-as-merge from the normal options and offer the remediation choices instead (pr, leave, update config if applicable, cancel).
Expand Down
Loading
Loading