W Workflow Skills Wiki
Home Wiki Index GitHub

Spec-to-PR Delivery Pipeline (delivery)

From Workflow Skills Wiki, the living architecture & domain knowledge base

> Provenance: .agents/skills/ws-spec-to-pr/SKILL.md, .agents/skills/ws-spec-to-pr-lite/SKILL.md, .agents/skills/ws-spec-to-pr-distributed/SKILL.md, .agents/skills/ws-shared/runtime/scripts/workflow_state.cjs, .agents/skills/ws-spec-to-pr-distributed/scripts/step_coordinator.cjs, .agents/skills/ws-spec-to-pr/WORKER-TURN-RULES.md, .agents/skills/ws-spec-to-pr/scripts/worker_liveness_check.cjs, living synthesis of specs 0006, 0016, 0021โ€“0024, 0029, 0035, 0043, 0047, 0054, 0062, 0068, 0073โ€“0074, 0082, 0084, 0089, 0090, 0092, 0094, 0095, 0097-us-354, 0099-us-352, 0101-us-355, 0105-us-365, 0117-us-386, 0118-us-393, 0120-us-388, 0121-us-395, 0125-us-412-413, 0126-us-415-416, 0127-us-414, 0129-pre-ship-doc-sync, 0129-us-419, 0133-us-430, 0134-default-stay-on-current-branch, 0138-workflow-parallel-writer-compat, 0141-restore-automode-continuous-orchestration, 0142-reduce-spec-to-pr-tokens, 0143-ws-spec-to-pr-distributed, 0146-us-436, 0148-us-448, 0154-dispatch-prompt-audit-trail, 0155-fresh-worker-verifier-step, 0159-per-step-context-budgets, 0164-us-475, 0174-us-502.

Feature [edit]

The Spec-to-PR delivery pipeline turns an approved *.spec.md contract into a merged pull request through two orchestrators that share one project hub and one state contract. The standard orchestrator (ws-spec-to-pr) runs steps 0 through 9: spec registration, plan authoring, plan interview, DAG task breakdown, implementation, verify scoring, adversarial code review, testing, close, ship, and fix-PR convergence. The lite orchestrator (ws-spec-to-pr-lite) compresses the same guarantees into steps 0 through 5 for work that passes ws-classify-complexity thresholds. ws-spec-multi classifies each queued spec and dispatches exactly one pipeline at a time with isolated workflowType and no cross-resume between standard and lite. Batch queue state lives in a per-run {plansDir}/{runId}/ directory; legacy flat {plansDir}/ws-spec-multi/*.state.md files still resume. The opt-in ws-spec-to-pr-distributed workflow (explicit invocation only) drives the same standard 0โ€“9 step set across several CLI processes on one machine via the state-file baton and step_coordinator.cjs; it never auto-selects and does not fork the FSM. Outside Spec-to-PR, ws-task-lifecycle covers prompt-driven work through Intake โ†’ Implementation โ†’ Completion without creating a plan tree.

How it works [edit]

A run begins at bootstrap with a configurable branch strategy. defaults.branchStrategy accepts stay (the seeded default), from-current, from-base, or prompt. With stay on an attached branch, bootstrap records branchStrategy: stay and uses the current branch as state.branch with no gate. prompt or the --prompt-branch switch surfaces the three-choice gate (create feat/{slug} from HEAD, from base, or stay). Detached HEAD always rejects stay. Standard Steps 1 through 3 always execute even in autoMode; autoMode automates only gate index 0, and Step 4 dispatch fails closed when plan artifacts are missing. Step 5 (ws-plan-verify) advances only when the derived score from ac-ledger.json reaches defaults.minVerifyScore (default 9). Uncovered negative scenarios cap the score at 8, and critical stack-invariant violations cap below the bar and force scoreAndRefine back into implementation.

Product files commit before review, never plan directories or secrets. After Step 5 reaches the verify bar, only files_touched paths stage into a product commit. Step 6 (ws-code-review) inspects the committed base...HEAD diff and STOPs when workflow files remain uncommitted. Review fixes get a separate product commit before testing or close. Implementation closes at Step 8 with status: completed and shipStatus: pending while MEMORY, changelog, and optional delivery artifacts persist; ws-ship-pr in workflowMode only pushes and opens the PR after close.

A fresh-worker verifier stage (ws-fresh-verify, Step 6b) runs after Step 6 review and before Step 7 testing: a worker holding only the spec, the plan of record, and the compact handoff re-derives every AC verdict, injects one fault per AC on a scratch worktree (removed after; the primary branch stays byte-identical), and writes step-05b-{slug}.fresh-verify.md with evidence-or-zero scoring. Defects loop through fix plus re-verify (max 3 rounds, then Pause with the residual list); runs without product tree changes write a no-product-changes skip marker instead of verdicts.

Shipping carries an optional non-blocking preview dry-run gate that observes the final pushed tree. Step 4b in ws-ship-pr reuses preview.dryRunCommand verbatim from the consumer repo root immediately after the Step 4 commit and push and before any Step 5 provider authentication or PR creation, recording its summary on the Prepare-to-PR board. The gate runs only when Create PR will actually execute, stays identical across GitHub and Azure DevOps providers, skips with a recorded reason when preview.previewBeforeShip is explicit false or the command is empty, and never blocks ship, merge, or handoff when findings or a non-zero exit appear. Single-session CLI-harness runs bind host capabilities once at bootstrap, run routine shell recipes quietly with non-interactive subject flags under a declared no-approval posture while keeping every step-boundary user-gate, and let ws-ship-pr auto-resolve head, base, remote, provider, commit title, stage set, and consumer prepare obligations before asking about genuine ambiguity only. Created pull requests still carry a GitHub auto-close keyword. ensure_pr_closer.cjs guarantees the PR body holds Closes #N for a numeric tracker id, leaves a body that already closes the issue alone, and skips local specs with a null id and non-GitHub providers without writing. GitHub honors that keyword only when the pull request base is the repository default branch. After a merge with zero active review threads, ship and in-session fix-PR convergence dispatch the close-issue intent so the linked work item reaches CLOSED on any base. comment-issue (alias close-loop) stays a comment and does not change tracker state. A local spec or a null id skips close-issue with exit 0, and a dry-run prints the planned close without calling the host.

Step skills no longer restate shared contracts. gates.md owns the gate tables and the minVerifyScore and scoreAndRefine rules, PROTOCOLS.md and STEP-DISPATCH.md own handoff and dispatch boilerplate, tools.md owns the commit-code G2 hunk-separation algorithm and the subagent model-role chain, config-resolution.md owns start-of-step banners, and artifact-cleanup.md owns the Step 8 combined gate plus Phase A cleanup. Callers keep a one- or two-line Contract pointer plus their own blast-radius delta, and standalone or lite invocations still carry skill-local guardrails instead of relying on dispatch injection.

Skill-owned blast-radius guardrails are explicit: ws-implement-tasks stages only files_touched and never runs git add, commit, or push; ws-plan-verify requires a Shell-capable session, refuses question-only read-only sessions, and never edits product files; ws-plan-interview lets a force_interview: true MEMORY conflict override soft-skip; ws-plan-write never touches git and always plans Section 6 stack-invariant verification; ws-testing never edits product or test sources and treats skipQualityGates as a gate-only bypass; and ws-plan-to-tasks writes no git or state files and invents no stubs.

Model selection supports a per-run preset override: preset=<name> (aliases --preset=<name>, --preset <name>) on the ws-spec-to-pr or ws-spec-to-pr-lite invocation replaces defaults.modelsPreset for that run only, so one execution can target muse-spark, cursor, cheap, or a custom bundle without editing project config. A configured modelsPreset that is absent from defaults.modelPresets fails at run start and names the unknown preset plus the available presets. It never silently resolves to another preset. Invocation overrides that name an unknown preset still warn and fall back. Telemetry records the resolved model id per step and never stores a preset name in a model field. The resolved preset persists as modelsPreset in {workflow-id}.state.json and the .state.md frontmatter, and resume retains it without re-passing the flag; the Step 3 Init banner shows the value and whether it came from the override or config.json. The orchestrator session itself always stays on currentModel; the override governs subagent dispatch and phase resolution, while stepModels entries still beat preset values.

Every dispatch leaves a durable prompt audit pair beside the step artifacts: the exact prompt bytes in step-{NN}-{slug}.prompt.md plus a .prompt.json manifest carrying budget, refs, and sha. The pair is linked from state.stepDispatches[] and the dispatch telemetry event, preserved by cleanup, and verified by the next pre-advance gate, while a re-dispatch bumps revision and preserves the prior sha. Dispatch context itself is budgeted per step through defaults.stepContextBudgets (step number to bytes with an 18000 floor, where a step override wins over the global defaults.contextBudget): the manifest records the effective bytes plus their source, and the harness report audits each step against its own cap.

Fix-PR batches always plan on the reviewer model (fixPrPlan โ†’ reviewerModel) and execute on the fix model (fixPrExec โ†’ executionModel). ws-goal-fix-pr runs as an orchestrator: the skill session owns the wait/fetch convergence loop inline (initialize, convergence check, heartbeat wait, re-check, pre-merge gate, final report) and never authors plan gates or product fixes itself when dispatch is available, while every Act round batch runs in a freshly dispatched worker through the portable dispatch-agent alias. Each batch worker runs one ordered fixPrPlan โ†’ fixPrExec pair with the gate completed before any product or remote mutation, resolves its models through the same stepModels โ†’ preset role โ†’ top-level role โ†’ session-model chain as Step 9 (never numeric "9"), and emits ordered dispatch telemetry with actual versus configured models. Hosts with no bound subagent tool fall back to documented Tier 3 inline-isolated execution with identical gate and learning contracts. The resolveReviewThread mutation runs only after reply and commit verification. Hermes rules require a prior-work sweep, whole-class fixes or recorded exemptions, and sabotage or CI-triage hygiene before resolving threads. Fix-PR also starts from a dirty worktree by design: outer preflight snapshots preExistingDirty (tracked modifications, staged leftovers, untracked paths) and keeps operator-owned local harness helpers on disk for the whole batch, while whole-tree git stash before or after the batch is forbidden. Repository sync fetches first and intersects HEAD..FETCH_HEAD with preExistingDirty, pulling only when the intersection is empty and STOPping with the overlapping paths otherwise, and the batch commit stages only surgical fix paths plus recorded proactive same-class hits. When defaults.enableDag is false (the default), Step 4 tasks dispatch sequentially; when true, at most three parallel subagents may run. Cooperative session leases are retired; parallel work uses git worktrees instead of lease files. Standard dispatch through build_dispatch_context.cjs inlines the enhancing ## Subagent contract sections from ws-senior-developer, ws-tdah, and ws-self-learning (the retired ws-karpathy-guidelines contract is consolidated into ws-senior-developer), capping each section at 40 lines plus a MEMORY slice.

Terminal close is contractual across single runs and batch queues. A run whose steps are all terminal still writes an explicit terminal status with endedAt instead of lingering as active, and the close step refuses to complete while any step reports failure. A batch child that finishes correctly propagates that outcome to its parent queue row so the parent updatedAt advances and the row leaves in_progress, batch ship updates transition existing queue rows instead of appending duplicate pending rows beside shipped rows, superseded batch runs retire atomically as a single canonical Markdown write with full-precision timestamps and deterministic newer-run ordering for lineage ties, and only the active run retires while stale rows stay untouched. Queue mutations are keyed by spec identity so the same item can never gain a second concurrent row and cause double processing.

Unattended autoMode chains Steps 0โ†’9 in one orchestrator session (Step 8 close, ws-ship-pr, Step 9 ws-goal-fix-pr) without voluntary turn stops between steps. When the host forces a mid-step turn end, update_state.cjs checkpoint stores completed unit ids and the remaining count for a step, appends a checkpoint telemetry event, and increments state.revision. pause-turn stores the step, reason, ISO timestamp, and a nextAction that names the resumable unit, then appends turn_paused. finish clears that step's pause marker and checkpoint before rendering state. A later turn resumes from state.turnPause.nextAction instead of looking like a stall.

Workflow scripts are the only sanctioned way to mutate machine state. ac_ledger.cjs and update_state.cjs document each subcommand's flags and one example on --help. score persists scoreState and lists per-row deficiencies; verify is a dry run. validate_state --pre-advance names the expected boundary (step5, pre-step6, ship) and the differing fields. finish never returns ok: true with a non-zero exit for phantom filesTouched paths. Gate copy in STEP-DISPATCH and gates.md prints the copy-paste command at each boundary. Hand-edits of .state.* or ac-ledger.json are tamper evidence, not a supported path. skipReason: "dag" is never written for a step that already produced DAG artifacts.

Ship writeback is part of close. After merge, shipStatus, prNumber, and prUrl land in state and a Step 9 handoff is recorded, so a completed run does not leave ship fields empty. Step 9 emits {reviewsDir}/PR-<N>-round-*.md or records an explicit clean-immediate reason when no review rounds ran. G2 delivery commits and baseline refreshes refresh {plansDir}/index.json stateSha256 immediately after dual-write so the plans index hash matches state before the next state write. A second finish updates the literal ## Step outputs (compact) section.

Staying on the current branch changes how ship resolves refs, because no host accepts a PR from a branch into itself. resolve_ship_refs.cjs classifies the run from explicit inputs: feature (head differs from base, open PR head into base), shared-head (head equals base but the configured base differs, push head and open the PR into the configured base), or push-only (head equals every base, push without a PR).

Shared-head multi-spec batches carry an executable foreign-commit guard. foreign_commit_guard.cjs (record-baseline, check-advance, check-convergence, list-foreign) records the local and remote tips at each dispatch, pauses with Resume/Skip/Abort naming the new commits when the branch advanced, refuses convergence on tip mismatch, and lists the foreign commits for the PR body. A new batch item with no record of its own falls back to the most recent dispatch baseline, so a commit landing between two different slugs still pauses instead of passing silently; exit 2 stays reserved for a genuinely empty store (real first dispatch).

Parallel writers on one worktree are a supported shape, not an accident. The ownership contract in git-ownership.md (stage only own paths, never reset, clean, or stash foreign work, advance the baseline forward) now covers the whole workflow skill surface through a published compatibility matrix. Bootstrap runs concurrency_preflight.cjs, which warns (never blocks) when other active workflow states or foreign dirty paths share the branch, and records the report into state. Shared artifacts (index.PRD, the wiki sync watermark, compiled MEMORY.md, the changelog) update through ownership-scoped, idempotent writers serialized by a cross-process file_lock.cjs, so a second writer cannot silently overwrite another's edit. A concurrent-writer end-to-end test drives a real second commit to the base mid-run.

Pre-ship documentation sync is a close gate when defaults.requirePreShipDocSync is true (the default). Standard Step 8 and lite Step 4 block the ship phase until ws-wiki sync {slug}, ws-spec-index sync {slug}, and the changelog entry are done. A project with no wiki warn-and-skips the wiki leg; index and changelog stay required. Setting the flag to false restores the previous offered wiki behavior.

Completion optionally continues into UI evidence collection. Two boolean switches with one folder setting gate a single opt-in post-completion proof-of-work step that runs after PR creation or fix-PR convergence while status is already completed. defaults.enableOptionalProofOfWork (explicit true only) asks once through user-gate whether to start the evidence collector, and adding defaults.enableAutomaticEvidenceCollectForProofOfWork starts it without asking. The collector writes numbered screenshots plus one Markdown evidence report per user story under defaults.projectRootFolderToSave (default {projectRoot}/.proofOfWork/{slug}) and never commits that folder unless separately asked, and cancellation plus slug sanitization keep the terminal transitions safe.

The opt-in ws-spec-to-pr-distributed workflow owns step-level baton runs, spreading one run across CLI processes on the same machine and repository; the standard ws-spec-to-pr ignores the baton keys entirely and stays single-host. defaults.stepRunners maps step numbers to opaque runner ids and defaults.runners binds each id to a command template with {prompt}, {cwd}, {slug}, and {step} substitutions plus a timeout and environment allowlist; unknown steps, runner ids, or empty commands fail fast before Step 0, and unmapped steps keep single-host dispatch. The state file carries a baton record (holder, step, claimedAt, leaseUntil, revision) as the sole turn signal: a claim succeeds only on matching revision with no live lease, finish releases exactly once while advancing currentStep, expired leases become re-claimable with logged holder, step, and attempt count, and maxAttempts (default 2) consecutive expiries move the workflow to blocked. The deterministic step_coordinator.cjs (plain Node, no LLM) owns the run loop โ€” claim, spawn the worker one-shot with sparse pointers plus a baton envelope, await exit, verify advancement and artifacts โ€” and is the only polling party (default 30s, range 5โ€“300s). Gates surface at the coordinator as pause-and-prompt (index 0 in autoMode); worker gate output is a protocol violation. Every claim, release, expiry, spawn, and exit appends baton_* / runner_* telemetry, ws-monitor snapshots show holder and lease read-only, and an enabled spec-memo integration mirrors released handoff summaries into the vault payload shape. Out of scope by design: IPC between runners, multi-machine orchestration, CLI provisioning, and gate-menu changes.

Backend [edit]

State lives in a single {workflow-id}.state.json SoT with handoffs, acLedger, and shipStatus, plus a rendered .state.md companion and append-only telemetry.jsonl. The first write is the sanctioned update_state.cjs init bootstrap: it requires explicit --slug, --workflow-id, --branch, and --baseline inputs (no inference; HEAD and non-hex baselines refused), writes the pair with the recorded identity through the canonical dual write, refreshes the plans index row so stateSha256 matches, replays idempotently over an untouched revision-0 pair, and refuses a progressed, partial, or mismatched target with no partial pair and no residue, so a single agent driving the steps directly reaches the same observable state as a dispatched run. Shared helpers workflow_state.cjs and validate_state.cjs --pre-advance N enforce the same contracts across standard and lite. Configuration keys that govern delivery include defaults.minVerifyScore, defaults.enableDag, defaults.testingModel, defaults.deliveryCommitArtifacts (refined plan on by default, everything else opt-in), defaults.modelsPreset with modelPresets and stepModels (substep role beats numeric step beats preset beats phase key beats current, including the muse-spark preset where every role resolves to the session model), the invocation-level preset= override persisted as state modelsPreset (workflow-state.schema.json, workflow_state.cjs --preset on dispatch and finish), the fix-PR roles fixPrPlan / fixPrExec, defaults.contextBudget with per-step defaults.stepContextBudgets overrides, defaults.workerLivenessWindowSec (effective worker liveness window, 600 s when absent or invalid), the baton run config defaults.stepRunners / defaults.runners / defaults.stepBaton (owned by ws-spec-to-pr-distributed; pollIntervalSeconds 5โ€“300 default 30, maxAttempts default 2), and preview.previewBeforeShip with preview.dryRunCommand for the optional Step 4b dry-run gate that defaults to enabled and is disabled only by explicit false.

Orchestrator instructions stay lean through enforced single-source-of-truth boundaries and byte budgets. STEP-DISPATCH.md owns step actions, gates.md owns gate transitions, and SKILL.md owns the autoMode table; duplicated model hierarchies, golden-path recipes, and per-step procedures were replaced with pointers, and historical issue-and-commit sediment was pruned. test/test-context-budget.js caps each orchestration document (ws-spec-to-pr/SKILL.md, STEP-DISPATCH.md, PROTOCOLS.md, lite SKILL.md) so prose cannot inflate again, while FSM phases, step numbering, gate semantics, and autoMode chaining keep full behavioral parity.

Branch discipline keeps state.branch as the PR head while project.workingBranch and the base branch never rewrite. Protected branches (main, master, develop) deny-list from cleanup, and Phase A git cleanup runs only at terminal shipStatus. Timesheet proof comes from ws-activity-report, which bills Human Total inclusive of agent supervision (Human โ‰ฅ Agent Running when agent time is positive) and treats idle or AFK gaps over 30 minutes as non-billable.

Worker turns are structurally hardened against premature endings that used to force full re-dispatches. Every dispatch path carries a turn rule stating that the first response must contain both the VerboseMode preview and at least two tool calls, and a response with zero tool calls ends the turn as failed delivery rather than resolving completed. A turn that ends with required step artifacts unwritten emits an explicit non-success signal so the parent fails fast instead of waiting on artifact timeouts. Parent liveness probing of long workers is read-only by contract: no mid-batch ping may inject a terminal message into a working turn, and a continuation worker that recovers an interrupted turn reuses the intact worktree partial progress instead of rebuilding context from scratch. A dispatched batch worker now owes a first liveness marker: {us-dir}/.runtime/worker-started-{step}-{batch}.md, written within the effective worker liveness window after its dispatch, where {step} is the dispatched step and {batch} the round id or DAG node id. The marker carries non-empty step, batch, and dispatchedAt fields, and counts only for the dispatch whose baseline it is at or after, so a stale same-name marker from a superseded dispatch is never read as current. It is a plain runtime file: it adds no state mutation and must not bump the workflow state revision.

The parent resolves exactly one deterministic read-only verdict with node .agents/skills/ws-spec-to-pr/scripts/worker_liveness_check.cjs --us-dir {us-dir} --step <N> --batch <id>, in this order: no-baseline (no dispatch record for the step; the check fails open and is never a wedge), alive (a fresh marker, or durable progress after the baseline), pending (window not elapsed), then wedged (window elapsed with neither a fresh marker nor durable progress), exiting 0/0/0/2. Durable progress outranks marker absence, so a marker-less but advancing worker is never taken over. Only wedged authorizes takeover, and a batch that is not proven wedged never gets a second worker. The sanctioned no-output branches are cancel-then-redispatch, or inline re-execution on the parent when re-dispatch is unavailable, recorded in the round artifact with measured ages (dispatch baseline, marker age, elapsed window). A status ping is never a progress signal; when one is sent anyway, its outcome is recorded as watchdog evidence instead of silence. The window resolves from defaults.workerLivenessWindowSec with a 600 s fallback. Lite, inline, and Tier 3 isolated postures dispatch no batch worker, so they carry no marker obligation and no watchdog takeover branch. The canonical obligation lives in WORKER-TURN-RULES.md ยง Worker liveness contract (batch workers), wired into the ws-goal-fix-pr ยง Loop liveness watchdog.

Step completion is contractual, not decorative. A step marked completed has its mandatory artifact on disk, and a completed mutating step reports a non-empty filesTouched handoff in telemetry unless the step genuinely modified nothing and declares the no-op explicitly. ws-monitor flags both violation classes in its read-only snapshots, with historical runs classified so old batches do not flood the monitor.

The orchestrator carries an opt-in execution observer for the pipeline itself. Workflow state records available agent transcript paths so observation can follow execution, and the autoStartObserver config key (default false) gates a single parallel watcher subagent per run that reports state and flags errors or mistakes in skill instructions. The watcher is strictly read-only and shares its instruction source with ws-monitor instead of duplicating observation semantics, and its findings land in the workflow directory as classified errors with fix proposals scoped to the upstream repo.

Fix-loop execution posture is configurable with one shared key. ws-goal-fix-pr.useSubAgents absent or false runs the whole fix-PR loop inline in the current turn, the historical fast path, with zero subagent dispatches; explicit true restores per-iteration subagent dispatch with identical behavior. The single key gates every subagent-dispatch loop iteration across the fix path that spans ws-fix-pr, ws-goal-fix-pr, and the pre-ship loop inside ws-ship-pr, and schema, GUI config editor, and docs change atomically with the runtime gate.

Standard children persist the same state contract as single runs. Every dispatched child writes its own machine {workflow-id}.state.json plus the rendered .state.md companion, per-step artifacts, and telemetry.jsonl under its {plansDir}/{slug}/ directory so the batch stays observable and resumable with delivery evidence intact. The child-exit guard fails closed on unparseable state, missing identity, slug mismatch, or unsafe queue slugs, and it treats the queue transition as an executable step rather than a silent alias. The monitor computes a real expected-artifact set for multi-spec children instead of an empty list, so absent child state now surfaces as a finding through record_child_outcome.cjs, verify_child_artifacts.cjs, and retire_superseded_run.cjs.