devlyn-cli 4.1.0 → 4.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/AGENTS.md +10 -94
  2. package/CLAUDE.md +7 -160
  3. package/LICENSE +21 -0
  4. package/README.md +65 -110
  5. package/bin/devlyn.js +95 -116
  6. package/bin/instruction-templates.json +43 -2
  7. package/bin/instructions.js +29 -8
  8. package/config/skills/_shared/adapters/claude.md +8 -85
  9. package/config/skills/_shared/adapters/codex.md +14 -34
  10. package/config/skills/_shared/adapters/omp.md +1 -11
  11. package/config/skills/_shared/codex-monitored.sh +4 -54
  12. package/config/skills/_shared/engine-doctor.sh +18 -58
  13. package/config/skills/_shared/engine-preflight.md +15 -47
  14. package/config/skills/_shared/expected-contract.py +242 -0
  15. package/config/skills/_shared/expected.schema.json +10 -95
  16. package/config/skills/_shared/invocation-receipt.py +26 -757
  17. package/config/skills/_shared/platform-support.py +37 -28
  18. package/config/skills/_shared/role-config.py +100 -421
  19. package/config/skills/_shared/runtime-principles.md +17 -93
  20. package/config/skills/_shared/task-complete.py +490 -285
  21. package/config/skills/_shared/task-completion.md +235 -0
  22. package/config/skills/devlyn-engines/SKILL.md +9 -11
  23. package/config/skills/devlyn-ideate/SKILL.md +57 -107
  24. package/config/skills/devlyn-ideate/references/elicitation.md +18 -99
  25. package/config/skills/devlyn-ideate/references/loop.md +110 -0
  26. package/config/skills/devlyn-ideate/references/package-format.md +128 -0
  27. package/config/skills/devlyn-ideate/scripts/acceptance.py +795 -0
  28. package/config/skills/devlyn-ideate/scripts/queue.py +1780 -0
  29. package/package.json +2 -3
  30. package/config/commit-conventions.md +0 -28
  31. package/config/skills/_shared/adapters/README.md +0 -74
  32. package/config/skills/_shared/adapters/grok.md +0 -100
  33. package/config/skills/_shared/archive_run.py +0 -969
  34. package/config/skills/_shared/codex-config.md +0 -81
  35. package/config/skills/_shared/collect-codex-findings.py +0 -505
  36. package/config/skills/_shared/finish-gate.py +0 -521
  37. package/config/skills/_shared/grok-anchor-guard.py +0 -78
  38. package/config/skills/_shared/judge-output-parser.py +0 -200
  39. package/config/skills/_shared/judge-role-evidence.py +0 -320
  40. package/config/skills/_shared/phase-prompt-render.py +0 -183
  41. package/config/skills/_shared/process-evidence.py +0 -926
  42. package/config/skills/_shared/resolve-bootstrap.py +0 -1410
  43. package/config/skills/_shared/resolve-stop-hook.py +0 -359
  44. package/config/skills/_shared/run-bounded.py +0 -58
  45. package/config/skills/_shared/spec-verify-check.py +0 -5083
  46. package/config/skills/_shared/state-phase-write.py +0 -5989
  47. package/config/skills/_shared/terminal-claim-check.py +0 -777
  48. package/config/skills/_shared/verify-merge-findings.py +0 -3528
  49. package/config/skills/devlyn-design-ui/SKILL.md +0 -650
  50. package/config/skills/devlyn-ideate/references/from-spec-mode.md +0 -61
  51. package/config/skills/devlyn-ideate/references/project-mode.md +0 -80
  52. package/config/skills/devlyn-ideate/references/spec-template.md +0 -121
  53. package/config/skills/devlyn-ideate/references/templates/decision.md +0 -48
  54. package/config/skills/devlyn-ideate/references/templates/roadmap.md +0 -50
  55. package/config/skills/devlyn-ideate/references/templates/vision.md +0 -44
  56. package/config/skills/devlyn-queue/SKILL.md +0 -66
  57. package/config/skills/devlyn-queue/scripts/append.py +0 -94
  58. package/config/skills/devlyn-resolve/SKILL.md +0 -379
  59. package/config/skills/devlyn-resolve/references/free-form-mode.md +0 -71
  60. package/config/skills/devlyn-resolve/references/outer-loop.md +0 -23
  61. package/config/skills/devlyn-resolve/references/phases/build-gate.md +0 -75
  62. package/config/skills/devlyn-resolve/references/phases/cleanup.md +0 -33
  63. package/config/skills/devlyn-resolve/references/phases/implement.md +0 -44
  64. package/config/skills/devlyn-resolve/references/phases/plan.md +0 -39
  65. package/config/skills/devlyn-resolve/references/phases/probe-derive.md +0 -261
  66. package/config/skills/devlyn-resolve/references/phases/surface-close.md +0 -34
  67. package/config/skills/devlyn-resolve/references/phases/verify.md +0 -310
  68. package/config/skills/devlyn-resolve/references/state-schema.md +0 -152
  69. package/config/skills/devlyn-resolve/references/task-completion.md +0 -160
  70. package/config/templates/prompt-templates.md +0 -71
  71. package/config/templates/template-feature.spec.md +0 -255
  72. package/config/templates/template-product-spec.md +0 -680
@@ -1,110 +1,29 @@
1
- # Elicitation flow (canonical body)
1
+ # Planning and elicitation
2
2
 
3
- Per-engine adapter prepended at runtime. This file is engine-agnostic.
3
+ How `plan` applies the question and autonomous policies in SKILL.md; [package-format.md](package-format.md) defines what it writes. No turn target, question quota or confirmation step applies.
4
4
 
5
- <role>
6
- You drive a focused conversation with a user who has an idea but not an engineering spec. Your job is to ask the right questions — not many questions — until a verifiable spec exists. The user does not know context engineering; assume they will under-specify and over-assume. You compensate by asking the specific decisions they did not realize they were making.
7
- </role>
5
+ ## Inspect first
8
6
 
9
- <input>
10
- - The user's initial goal text (free-form, possibly ambiguous).
11
- - The codebase (read-only) — for grounding inferred defaults.
12
- - `references/spec-template.md` — the shape of the output.
13
- - `_shared/expected.schema.json` — the shape of `spec.expected.json`.
14
- </input>
7
+ Read what the project already answers before asking: the code paths, tests and conventions the intent touches, the installed instructions and completion mode, the queue through `queue.py status` and each added loop's capture (`git for-each-ref refs/devlyn/captures`, then `git show <ref>:docs/specs/<loop-id>/meta.md`) rather than package files in the checkout, and any document the user supplied. Infer `kind` from the framing (investigate or explore → `spike`, a rough version → `prototype`, otherwise `feature`) instead of asking.
15
8
 
16
- <conversation_rules>
17
- 1. Ask 1-2 questions per turn. More than that overwhelms the user and produces shallow answers.
18
- 2. Questions are concrete and decision-grade. Bad: "what should the UX look like?" Good: "should `--lang fr` exit 1 with the rejected code in the error message, or fall back silently to English?"
19
- 3. When the answer is obvious from context, infer the default and ask "I'll assume X — okay?" instead of asking the user to choose. Defaults free up the user's attention for decisions that actually matter.
20
- 4. Track what is filled and what is missing in your running draft at `.devlyn/ideate-draft.md`. Update after each turn.
21
- 5. Stop when the structural lint passes AND the user explicitly confirms ("looks good", "ship it", etc.). Hard ceiling: 8 turns total. Beyond that, the task is too large for ideate — recommend `--project` mode and stop.
22
- 6. Do not save the conversation. The output is the spec.
23
- </conversation_rules>
9
+ ## What to ask
24
10
 
25
- <missing_decisions_to_surface>
26
- For most coding tasks, the under-specified blanks are:
11
+ Only an answer that changes authorized behavior, scope, data semantics, acceptance or delivery:
27
12
 
28
- 1. **Input shape**: what does the user invoke to trigger the feature? Exact CLI command, HTTP request, function call?
29
- 2. **Output shape**: what does success look like? Exit code, stdout substring, file existence, JSON shape?
30
- 3. **Failure shape**: what happens on bad input? Exit code, error message format, fallback behavior (silent vs visible)?
31
- 4. **Scope boundary**: which files are in-scope, which are out-of-scope? "Don't touch the auth module" is a boundary worth surfacing.
32
- 5. **Constraints**: dependency policy (new deps allowed?), silent-catch policy, type-system escape policy, test coverage expectations.
33
- 6. **Complexity signal**: set spec frontmatter `complexity` to `high` when
34
- the spec needs a compound scenario crossing state mutation with ordering,
35
- idempotency, auth/error priority, rollback/failure handling, or exact output
36
- shape. This is a downstream VERIFY pair-trigger signal, not a vague
37
- difficulty label.
38
- 7. **Verification**: how does the user know it worked? Pick the smallest concrete check.
39
- If the goal combines state mutation with ordering/priority, idempotency,
40
- auth/error priority, or exact output shape, ask for one concrete compound
41
- scenario that exercises the interaction end-to-end instead of accepting only
42
- isolated happy-path checks.
13
+ - **Behavior** — the exact input, output and failure a user observes. An unspecified user-visible detail alone is not material ambiguity; apply the autonomous policy's boundary.
14
+ - **Scope** — what may change and what must not.
15
+ - **Data semantics** — persisted state, ordering, idempotency, migration.
16
+ - **Acceptance** — the smallest decisive check for each requirement.
17
+ - **Delivery** — local-only, PR or merge.
43
18
 
44
- Walk through these in roughly this order. Skip the ones already clear from the user's initial text.
45
- </missing_decisions_to_surface>
19
+ Lead with the recommendation and its consequence, for example: "Should `--lang fr` exit 1 naming the rejected code? Recommended: yes, so bad input stays visible instead of silently falling back to English." Decide preferences the user has no stake in.
46
20
 
47
- <spec_kind_inference>
48
- Infer `spec.kind` from the user's framing:
21
+ ## Autonomous planning
49
22
 
50
- - "explore", "investigate", "I'm not sure if X is possible", "let's see what works" → spike. Confirm with: "I'll mark this as a spike — deliverable is learning, not production code. Sound right?"
51
- - "prototype", "rough version", "show me what it would look like" → prototype.
52
- - Default: feature. The user wants production-quality work.
23
+ With `--autonomous`, or when a drain plans a legacy row, take only the defaults the autonomous policy allows and record each once under `## Decisions and assumptions`. Material ambiguity stops planning with its concrete question.
53
24
 
54
- Do not ask "is this a feature, spike, or prototype?" — the user does not know the difference. Infer and confirm in one line.
55
- </spec_kind_inference>
25
+ ## Tasks and checks
56
26
 
57
- <draft_spec>
58
- Maintain `.devlyn/ideate-draft.md` after every user turn. Include:
59
- - Latest version of all 6 sections (Frontmatter, Context, Requirements, Constraints, Out of Scope, Verification).
60
- - "TODO" markers for sections still missing pieces.
61
- - A "decisions log" comment at the bottom listing what each turn settled.
62
-
63
- When you're about to ask the user a question, look at the draft first — if the answer is already discoverable from the initial goal + codebase, infer it instead of asking.
64
- </draft_spec>
65
-
66
- <lint>
67
- Before declaring the spec ready, verify structurally:
68
- - Frontmatter has `id`, `title`, `kind`, `status: planned`, `complexity`.
69
- - All 5 H2 sections present (`## Context`, `## Requirements`, `## Constraints`, `## Out of Scope`, `## Verification`).
70
- - Requirements ≥ 1 bullet.
71
- - Verification has either ≥ 1 named command OR the explicit pure-design escape phrase.
72
-
73
- If the lint fails, fix the missing piece (ask one focused question if needed) before announcing.
74
-
75
- After lint passes, run both mechanical checks:
76
- 1. `python3 "$DEVLYN_SHARED_DIR/spec-verify-check.py" --check <spec-path>` validates the spec's verification carrier shape and supported `complexity` frontmatter.
77
- 2. `python3 "$DEVLYN_SHARED_DIR/spec-verify-check.py" --check-expected <expected-path>` validates sibling `spec.expected.json` against `_shared/expected.schema.json` plus sibling spec `complexity` frontmatter.
78
-
79
- If either exits 2: read the stderr message, fix the malformed carrier or JSON, and re-run the failed command. Both commands must exit 0 before ready.
80
- </lint>
81
-
82
- <output>
83
- Two files in `<spec-dir>/<id>-<slug>/`:
84
- - `spec.md` (per template).
85
- - `spec.expected.json` (per `_shared/expected.schema.json`).
86
-
87
- Final announcement (one line): `spec ready — /devlyn-resolve --spec <spec-path>`.
88
-
89
- Do NOT include the conversation transcript in the output. The spec stands alone.
90
- </output>
91
-
92
- ## Quick mode (1Q) — single-turn assume-and-confirm
93
-
94
- When `--quick` is set:
95
-
96
- 1. AI synthesizes spec from the one-line goal — fill every section with the most reasonable inference.
97
- 2. AI presents the spec to the user with an explicit `## Assumptions made` block listing every inferred decision (one bullet each).
98
- 3. User responds with "go" / "fix X to be Y" / "no, different".
99
- 4. On "go": write the spec + spec.expected.json, run both lint checks, announce.
100
- 5. On "fix X": apply correction, re-present, ask again. Maximum 3 correction rounds before escalating to default mode.
101
-
102
- Quick mode trades thoroughness for speed. Use it for trivial-medium tasks where the user has a clear-enough goal that one round of inference + correction is sufficient.
103
-
104
- ## Anti-patterns
105
-
106
- - Walls of questions in one turn.
107
- - Open-ended "what would you like" questions when the user clearly does not know.
108
- - Adding "for future flexibility" or "just in case" sections to the spec — the user did not ask for those, and the principles binding `/devlyn-resolve` reject them.
109
- - Saving the conversation transcript alongside the spec.
110
- - Stalling when the user gives a vague answer — re-ask with a more specific shape ("should it be A, B, or other?") rather than re-asking the same question.
27
+ - One task per independently verifiable deliverable.
28
+ - A check must fail on the wrong behavior. When requirements interact (state with ordering, idempotency, error priority or an exact output shape), add one compound check that exercises the interaction end to end. Validate each new guard with a violating control that fails and an allowed control that passes.
29
+ - Semantic or source judgments are review requirements, not regular expressions. A requirement that resists a mechanical check becomes a review requirement; it is never weakened into something checkable.
@@ -0,0 +1,110 @@
1
+ # Loop protocol
2
+
3
+ `drain` runs the queue serially. Each eligible task gets an owned worktree, committed inputs, one executor exchange, evidence-derived acceptance, a queue-only terminal commit and delivery. The executor works under the installed methodology (the CLAUDE.md/AGENTS.md instruction block); the loop adds no phase graph, reviewer quota, engine router or restart cycle. Formats: [package-format.md](package-format.md). Allocation, custody, delivery and cleanup follow the completion contract in `$DEVLYN_SHARED_DIR/task-completion.md`.
4
+
5
+ ## Commands
6
+
7
+ ```sh
8
+ python3 "$DEVLYN_SKILL_DIR/scripts/queue.py" status --repo .
9
+ python3 "$DEVLYN_SKILL_DIR/scripts/queue.py" drain --repo . [--local-only] [--worktree-root <dir>] -- <executor argv containing {packet}>
10
+ ```
11
+
12
+ Both print one JSON object; exit 1 is `BLOCKED` with a `reason`.
13
+
14
+ - `status` writes nothing beyond finishing `add`'s cleanup (step 1): reconciled counts (`pending`, `active`, `accepted`, `failed`, `blocked`, `legacy_pending`), the `next` task, `blockers`, deliveries still pending with their resume (drain again until the terminal commit is attached, then `task-complete.py complete --receipt <receipt>`), and under `cleanup` the package copies `add` kept or could not remove.
15
+ - `drain` ends `DRAINED` (no runnable task remains; open deliveries show per task), `WAITING` (a task waits with its reason, or legacy rows need planning) or `BLOCKED` (conflict, invalid input or a recovery blocker). Progress lines `devlyn-loop: <task>: <event>` go to stderr.
16
+ - `--local-only` (alias `--no-push`) keeps the loop local. A manifest `delivery: local-only` and any earlier local receipt of the loop keep that restriction for later drains. A local loop needs no remote; nothing is fetched or pushed. An `auto`/`pr` loop none of whose tasks has a pushed PR runs as a local loop from its capture and the branch it was added on. Once one has, that PR is never rewritten to local and the loop is never run locally: its tasks wait, naming the PR, while other work continues.
17
+ - Worktrees default to `<repo parent>/<repo name>.devlyn/<loop-id>/<task-id>`.
18
+
19
+ The host supplies the executor and maps its configured engine to the argv; the loop has no model preference. Each `{packet}` in the argv is replaced by the task packet path, and each `{worktree_git_dir}` by the task worktree's absolute Git directory. The executor runs with the task worktree as its working directory and the null device as stdin; its stdout and stderr are appended to `executor.stdout` and `executor.stderr` in the packet's `evidence_dir`, never a pipe.
20
+
21
+ `<common Gitdir>/devlyn-loops/drain.lock` is held for the controller's lifetime, so a second drain from any worktree of the repository is refused. `queue.lock` is held only to read the add records and queue files, to add a loop and to make terminal transitions, so `add` works during a drain.
22
+
23
+ ## Steps
24
+
25
+ 1. **Reconcile.** First finish `add`'s cleanup of package copies ([package-format.md](package-format.md) Commands). Then read the queue view: the legacy rows, each replaced row as its loop's rows, then each loop's rows, from its capture or, without a record, from the checkout's tracked queue file ([package-format.md](package-format.md) Queue rows); every `<common Gitdir>/devlyn-completion/*/receipt.json` and their recovery refs. A row's identity, its receipt's branch `devlyn/<loop-id>/<task-id>`, the recovery ref (equal to the receipt's `publish_sha`, or to its validated terminal commit while that attachment is unfinished), the terminal commit's mark and, while active, the committed contract digests must agree. A receipt's terminal result supplements a `[ ]` row. Two receipts for one task or a terminal row contradicting its receipt stop selection with the conflict named; an unfinished allocation makes its task wait (step 10). A captured package never changes; for a loop without a record, an active task whose contract or expected acceptance changed in the checkout, also when that text is now missing or invalid, becomes `[F] inputs-changed` without executing again, its workspace retained, other work continuing while the revision is planned as a new task, and a pending task whose package text fails validation waits with that error, as do its dependents. Unsettled receipts (active, terminal commit unattached, or delivery not final) resume before any new allocation.
26
+ 2. **Select** the earliest pending row whose prerequisites are accepted with a receipt in every mode (a hand-written `[x]` never counts) and, for `auto`/`pr`, delivered (merged). A failed or blocked prerequisite makes its dependent `[F] blocked-prerequisite:<id>` without invoking the executor; that derived mark is proven by the prerequisite's receipt and travels into later checkouts and the report. A prerequisite awaiting delivery leaves its dependent pending while independent work continues; so does an `auto`/`pr` carrier in flight (step 3), or a local loop's active task (a local loop runs one task at a time), for every other task of its loop. Legacy rows wait for `add --materialize`.
27
+ 3. **Allocate** a new owned worktree on the absent branch `devlyn/<loop-id>/<task-id>` with `task-complete.py allocate`. First, the commit the task starts from must ignore `.devlyn/` (a committed `.gitignore` entry there, or `<common Gitdir>/info/exclude`), or nothing is allocated and the drain blocks naming the fix. It must also hold this checkout's `CLAUDE.md` and `AGENTS.md` (each identical, or both absent), so the task runs under the installed instructions, and, where it holds the loop's queue file, every captured package file unchanged, so the task binds its captured contract; otherwise the task waits, naming the fix, while independent work continues.
28
+ - Local: the first task uses `--local-base <HEAD of the branch the loop was added on>`, the add record's branch, never whichever branch is checked out; that HEAD must descend from the manifest `base_sha`, or the task waits naming it. Instructions committed there before this first allocation are therefore used. Its receipt keeps that start, and a later task with no accepted predecessor reuses it, also after the first task failed; a fixed start is never recalculated. Later tasks use `--from-receipt <latest accepted receipt of the loop>`, which starts from that receipt's exact `source_sha`, never its terminal commit, current HEAD or workspace. Every prerequisite's accepted source must be an ancestor of that frontier; divergent dependencies need a planned integration task, because the driver never merges. A failure leaves the frontier unchanged.
29
+ - `auto`/`pr`: `--start` the refreshed remote base, never an anchor-local commit, so no unpushed anchor commit reaches a task PR; push whatever the plan depends on. A task allocated while that base lacks `docs/specs/<loop-id>/` is the loop's carrier; a carrier that fails is never published, and the next task allocated becomes the carrier. The start must contain each prerequisite's merge commit, checked before allocation and again before every execution or resume. A squash merge needs no original-source ancestry.
30
+ 4. **Commit scoped inputs** on the owned branch before any executor write, only what the allocation base lacks. A base without `docs/specs/<loop-id>/` gets the package directory from the capture with the loop's queue file, its settled rows keeping their marks (receipt-proven `[x]` and `[F]`, prerequisite-blocked `[F]`): a local loop's first task, or an `auto`/`pr` carrier, whose PR lands the package before tasks diverge. Otherwise a local task commits the base's queue file with the loop's receipt-proven terminal rows (typically a predecessor's `[x]`), every other byte unchanged, and an `auto`/`pr` task commits nothing, so its PR changes only its own row and its product. A base that already carries them gets no inputs commit; unrelated product commits stay out.
31
+ 5. **Exchange one packet.** Write `<receipt dir>/packet.json`, then run the executor. An executor that exits without writing its submission leaves its task active, waiting with the exit code and `executor.stderr`, and the next drain runs it again; one that cannot start blocks the drain.
32
+ 6. **Derive acceptance** with `acceptance.py accept`. It binds the committed contract to the packet digests; requires the candidate to be the owned branch head with HEAD checked out on that branch, to descend from the inputs, to leave the package (its queue file included) and `docs/specs/queue.md` untouched and to have a clean worktree; executes each declared command on that source, recording raw stdout/stderr and exit, timeout or spawn outcomes (a runner result is reused only when it is wholly clean — no reasons, so no failed check and no source change during its checks — for the same source and contract digests, with unchanged streams); evaluates the file and diff guards against the inputs; confirms the source is unchanged; checks review records; and writes `.devlyn/loop/acceptance.json`. Missing checks, missing review coverage and open binding findings cannot produce `[x]`, and acceptance weakening is not repair. This establishes command outcomes and evidence completeness, not a reviewer's semantic judgment; deliberate evidence forgery is outside the trust model.
33
+ 7. **Bind before terminal metadata.** `task-complete.py accept --receipt <receipt> --acceptance <worktree>/.devlyn/loop/acceptance.json` takes evidence custody and points the recovery ref at the source. Failed results are bound the same way; they never become a frontier or a publishable product.
34
+ 8. **Commit the terminal transition** under the queue lock: one commit whose sole parent is the bound source and whose only change is this task's row in `docs/specs/<loop-id>/queue.md` (`[x]`, or `[F] — <first reason> (receipt <id>)`), validated as the only legal transition. Then `task-complete.py attach --receipt <receipt> --commit <terminal> --file docs/specs/<loop-id>/queue.md` records it separately from the source and moves the recovery ref to it.
35
+ 9. **Deliver** with `task-complete.py complete --receipt <receipt> --writers-stopped`, plus `--local-only` or, until the receipt records a mode, the manifest's `--mode auto|pr`, so a per-task `--mode` persists. Local-only keeps source, custody and the recovery ref with no remote effect. `auto`/`pr` publish the terminal commit under the completion contract; a pending or refused delivery keeps product acceptance, resources and a resume command. Where writer cessation cannot be observed, or a process still uses the tree, a merged delivery settles (`COMPLETE`) with its workspace, task ref and custody retained and reported under workspace cleanup; retained cleanup never blocks the queue. Failed products are never published. The receipt's `delivery` field checkpoints the latest outcome. A local chain is never turned into stacked PRs; publishing it requires integrated acceptance of the actual publication candidate.
36
+ 10. **Recover** from durable state only:
37
+
38
+ | Interrupted | Resume |
39
+ |---|---|
40
+ | During allocation | A receipt without `allocation: owned` is never adopted: its task waits, naming the recovery (remove its worktree and branch if present, delete the receipt directory, drain again), while independent work continues. |
41
+ | Inputs commit | A branch head equal to the expected inputs tree is adopted; anything else blocks. |
42
+ | During execution | Each attempt's start is recorded before its spawn and its exit after it. A start without an exit waits until task-complete observes no process using the worktree, then adopts the submission that executor wrote or runs the executor again; where writers cannot be observed (native Windows) the task becomes `[F] interrupted-unobservable` with its workspace retained, its dependents become prerequisite-blocked and independent work continues. Exactly-once execution is not promised. |
43
+ | During checks | Acceptance reruns; incomplete evidence stays unreferenced. |
44
+ | Accepted, before terminal commit | Only the missing transition is created. |
45
+ | Terminal committed, before attachment | The commit is validated and attached, also when attachment already moved the recovery ref; nothing reruns. |
46
+ | Terminal committed, before delivery | Only the missing delivery effects run. |
47
+ | After delivery, before cleanup | Owned cleanup resumes; the product verdict is unchanged. |
48
+
49
+ 11. **Report** to `<common Gitdir>/devlyn-loops/<loop-id>/drain-report.md` after every drain: per task the product result and reason, receipt, evidence custody and recovery ref, allocation base, accepted (or unaccepted) source, terminal commit, delivery status, PR URL and resume command, assumptions and unresolved questions, the retained worktree and branch, and workspace and scratch cleanup. Include the package's decisions and assumptions once, excluding the manifest and separately from executor assumptions; read the captured package when an add record exists, otherwise use the checkout, and report an unreadable package explicitly. Whole-loop acceptance is reported separately over the manifest's complete task set: every task needs receipt-backed acceptance, including the integration task's assembled-product check on the final frontier; a manifest task missing from the queue is reported as not yet run. `Bring into` names the command that brings the accepted frontier into the branch the loop was added on. For a local loop it is `git merge --ff <final frontier branch>` on that branch, which fast-forwards when it can and merges otherwise, so the commands of several local loops apply in either order. For `auto`/`pr`, once the frontier is delivered, it is `git merge --ff origin/<base>` on `<base>`: the anchor holds no loop commit or copy, so it fast-forwards after merge, squash and rebase delivery alike unless that branch has commits of its own, and merges then.
50
+
51
+ ## Executor exchange
52
+
53
+ The packet (`<receipt dir>/packet.json`) carries everything a fresh executor needs:
54
+
55
+ | Field | Meaning |
56
+ |---|---|
57
+ | `task`, `title`, `kind` | Queue identity and deliverable description. |
58
+ | `worktree`, `branch`, `receipt`, `scratch` | Absolute owned worktree, branch, receipt and disposable build directory. |
59
+ | `allocation_base`, `inputs_sha` | Allocation base and the committed-inputs commit. |
60
+ | `contract`, `expected`, `meta` | `{path, sha256}` of the committed task contract, expected acceptance and meta-prompt. |
61
+ | `requirements`, `review_requirements` | Requirement IDs and those needing review evidence. |
62
+ | `shared_constraints` | The meta-prompt's `Constraints and exclusions`. |
63
+ | `dependencies` | `{task, receipt, source_sha}` of each accepted prerequisite. |
64
+ | `methodology` | `{path, sha256}` of the installed instruction files at the inputs commit. |
65
+ | `delivery` | `local-only`, `auto` or `pr`. |
66
+ | `evidence_dir`, `runner` | Where loop evidence lives (`<worktree>/.devlyn/loop`, ignored by Git) and the runner argv. |
67
+ | `submission`, `obligations` | Where to write the submission, and the completion obligations. |
68
+
69
+ Writable tool caches go under `scratch`, for example `npm_config_cache=<scratch>/npm`: Codex's sandbox keeps caches in HOME read-only.
70
+
71
+ Run `runner` (`acceptance.py run --packet <packet>`) on the committed candidate before review. It prints `{"result": <path>, "passed": ..., "reasons": [...]}`; listing a wholly clean result (empty `reasons`) lets drain reuse its outcomes. Then write the submission:
72
+
73
+ ```json
74
+ {
75
+ "schema_version": 1,
76
+ "task": "<identity>",
77
+ "source_sha": "<candidate commit on the owned branch>",
78
+ "runner_results": ["<worktree>/.devlyn/loop/runs/<n>/result.json"],
79
+ "reviews": ["<worktree>/.devlyn/reviews/<name>.json"],
80
+ "findings": [{"id": "F1", "disposition": "resolved", "evidence": "<what changed>"}],
81
+ "cleanup": "<residue removed, output retained>",
82
+ "handoff": "<state a successor needs>",
83
+ "assumptions": ["<each assumption, recorded once>"],
84
+ "blockers": [],
85
+ "summary": "<informational only>"
86
+ }
87
+ ```
88
+
89
+ `schema_version`, `task` and `source_sha` are required. A blocker `{"kind": "failed" | "needs-review" | "blocked-infrastructure", "detail": "<concrete question or cause>"}` records `[F]` with that reason and runs no checks; material ambiguity becomes `needs-review`, never a weakened acceptance. The summary is never evidence.
90
+
91
+ ## Review records
92
+
93
+ Each `review_requirements` ID needs review evidence: a JSON record under `<worktree>/.devlyn/`, listed in the submission's `reviews`, in this shape:
94
+
95
+ ```json
96
+ {
97
+ "schema_version": 1,
98
+ "kind": "devlyn-review",
99
+ "task": "<identity>",
100
+ "engine": "<engine>",
101
+ "model": "<exact model id>",
102
+ "source_sha": "<reviewed commit>",
103
+ "contract_sha256": "<packet.contract.sha256>",
104
+ "expected_sha256": "<packet.expected.sha256>",
105
+ "requirements": ["R3"],
106
+ "findings": [{"id": "F1", "binding": true, "disposition": "resolved", "requirement": "R3", "summary": "<finding>"}]
107
+ }
108
+ ```
109
+
110
+ A record counts only when it binds this task, the candidate source and both contract digests; any other well-formed record is reported under `ignored_reviews` (a source change invalidates earlier reviews). A listed review or runner record that is unreadable or not this shape fails acceptance with its path named; keys a review record or finding adds beyond this shape are ignored. The counted records' `requirements` must cover `review_requirements`. Each finding's `disposition` is `open`, `resolved` or `rejected` (`rejected` needs a `reason`); an open binding finding blocks acceptance. The loop requires no particular engine or number of reviewers.
@@ -0,0 +1,128 @@
1
+ # Loop package format
2
+
3
+ The formats `scripts/queue.py check|add` validate. One package per intent:
4
+
5
+ ```text
6
+ docs/specs/<loop-id>/meta.md index: intent, shared constraints, manifest
7
+ docs/specs/<loop-id>/<task-id>/spec.md self-contained task contract
8
+ docs/specs/<loop-id>/<task-id>/spec.expected.json mechanical acceptance
9
+ docs/specs/<loop-id>/queue.md the loop's queue rows, generated by add
10
+ docs/specs/queue.md legacy raw intents: read for materialize, never written
11
+ ```
12
+
13
+ IDs match `[a-z0-9][a-z0-9-]{0,62}`. The queue identity `<loop-id>.<task-id>` never changes and is never reused; revised work gets new IDs. Headings below are fixed ASCII identifiers, like the verification sentinel; their content may be in any language.
14
+
15
+ ## meta.md
16
+
17
+ Required `##` sections, each non-empty:
18
+
19
+ | Heading | Content |
20
+ |---|---|
21
+ | `Intent` | Original intent and referenced source documents (input documents are preserved, not rewritten); intended user outcome. |
22
+ | `Constraints and exclusions` | Shared requirements, prohibited changes, applicable delivery restrictions. Sent to every worker. |
23
+ | `Tasks` | Ordered task index, dependency graph and decomposition rationale. No prescribed task count; one task is a valid loop. |
24
+ | `Overall acceptance` | Every overall requirement assigned to a task; cross-task requirements assigned to the integration task's assembled-product check. |
25
+ | `Execution policy` | Baseline, installed-methodology rule, autonomous question policy, delivery policy, stop conditions. |
26
+ | `Decisions and assumptions` | Adopted decisions and defaults with reasons. No elicitation transcript. Put defaults affecting a task's observable behavior in that task's requirements and acceptance checks. |
27
+
28
+ Exactly one fenced `json` block, the manifest. It is authoritative for order and dependencies and never carries status:
29
+
30
+ ```json
31
+ {
32
+ "schema_version": 1,
33
+ "loop_id": "inventory",
34
+ "base_ref": "main",
35
+ "base_sha": "<exact 40- or 64-hex commit in this repository>",
36
+ "delivery": "auto",
37
+ "tasks": [
38
+ {"id": "t01", "spec": "t01/spec.md", "depends_on": []},
39
+ {"id": "t02", "spec": "t02/spec.md", "depends_on": ["t01"]}
40
+ ],
41
+ "integration_task_id": "t02"
42
+ }
43
+ ```
44
+
45
+ - Keys are exactly these; `loop_id` equals its directory; each `spec` is `<id>/spec.md`.
46
+ - `delivery` is the inherited `auto` or `pr`, or an explicit `local-only` ([loop.md](loop.md) step 3).
47
+ - `depends_on` names earlier tasks; unknown dependencies, cycles and forward references are rejected before execution.
48
+ - `integration_task_id` is the last task and depends, directly or transitively, on every other task; its acceptance is the assembled-product check. Assign integration to the final substantive task when its substantive deliverable already depends, directly or transitively, on every other task. Otherwise preserve the substantive tasks' dependencies and append a check-only task that depends on all of them and verifies the assembled product without changing product source; it declares the full-diff guard `{"pattern": ".", "description": "No source changes", "severity": "disqualifier"}` in `forbidden_patterns`.
49
+
50
+ ## Task spec.md
51
+
52
+ ```yaml
53
+ ---
54
+ id: t02
55
+ loop_id: inventory
56
+ title: Reservation behavior
57
+ kind: feature
58
+ review_requirements: [R3]
59
+ ---
60
+ ```
61
+
62
+ `kind` (`feature`, `spike`, `prototype`) describes the deliverable and routes nothing. `review_requirements` lists requirement IDs that need semantic or source judgment (`[]` when none). An optional `complexity` is descriptive only. `status` is rejected: queue state lives in the queue and receipts.
63
+
64
+ Required `##` sections, each non-empty:
65
+
66
+ | Heading | Content |
67
+ |---|---|
68
+ | `Context and goal` | Outcome and why this task exists. |
69
+ | `Requirements` | One list item per obligation with a stable ID: `- R1: <concrete acceptance obligation>`. |
70
+ | `Scope and constraints` | Authorized implementation surface and the shared constraints that apply. |
71
+ | `Out of scope` | Explicit exclusions. |
72
+ | `Prerequisite inputs` | Predecessor interfaces, artifacts and assumptions this task relies on. |
73
+ | `Verification` | Commands, assertions and requirement coverage. The line directly above the heading is `<!-- devlyn:verification -->`. |
74
+ | `Deliverable and cleanup` | Result to retain, change-created residue to remove, permitted disposable output. |
75
+
76
+ A worker receives this file and its task packet only, never the planning conversation or other task specs, so state everything it needs here.
77
+
78
+ ## spec.expected.json
79
+
80
+ Schema: [expected.schema.json](../../_shared/expected.schema.json), parsed strictly (duplicate keys and non-finite numbers are rejected) by `_shared/expected-contract.py`. Loop tasks use:
81
+
82
+ - `verification_commands`, each with exactly one of `argv` (preferred: no shell, portable to Windows) or `cmd` (shell); `exit_code` (default 0); `timeout_sec` (1-600, default 60); `stdout_contains` and `stdout_not_contains` (UTF-8 substrings of stdout plus stderr); and non-empty `contract_refs` naming this task's requirement IDs.
83
+ - `required_files` and `forbidden_files` (literal Git paths), `forbidden_patterns` (regex over the task's own diff; `disqualifier` blocks acceptance, `warning` is recorded), `max_deps_added` (default 0; counts only the root `package.json`, so list other manifests in `forbidden_files`) and `pure_design`.
84
+
85
+ Rejected otherwise:
86
+
87
+ - Every requirement is covered by some command's `contract_refs`, by `review_requirements`, or both.
88
+ - A runtime task has at least one command. `pure_design: true` has no commands and lists every requirement in `review_requirements`.
89
+ - Phase-specific obligations (`process_evidence`, `required_risk_probe_requirements`, `tier_a_waivers`, `spec_output_files`) are rejected with an instruction to translate them into commands or review requirements without weakening them.
90
+
91
+ Coverage proves that every obligation has evidence. Whether the checks express the owner's intent stays a planning and review responsibility.
92
+
93
+ ## Queue rows
94
+
95
+ `add` generates the loop's `docs/specs/<loop-id>/queue.md` into its capture (Commands), never into the checkout: one row per task in manifest order, each line one blank line from the next, ending in one newline.
96
+
97
+ ```markdown
98
+ - [ ] inventory.t01 [Stock model](docs/specs/inventory/t01/spec.md)
99
+
100
+ - [x] inventory.t02 [Reservation behavior](docs/specs/inventory/t02/spec.md)
101
+
102
+ - [F] inventory.t03 [Report](docs/specs/inventory/t03/spec.md) — blocked-prerequisite:inventory.t02 (receipt <id>)
103
+ ```
104
+
105
+ - Links are repository-relative and exactly `docs/specs/<loop-id>/<task-id>/spec.md`; titles escape `\`, `[` and `]`. Packets and reports carry absolute paths.
106
+ - `[ ]` is pending or active (the receipt tells which). `[x]` is an accepted product. `[F]` is failed, infrastructure-blocked, prerequisite-blocked or needs-review and ends with ` — <reason> (receipt <id>)`. Delivery state lives in receipts and the drain report; a pending PR never turns `[x]` into `[F]`.
107
+ - The only legal transition turns one pending row into `[x]` or `[F]` with every other byte unchanged, so a terminal commit changes one line of its loop's file. Terminal rows are never reinterpreted or rerun. The blank lines let transitions of adjacent rows merge, and loops never share a file.
108
+ - Identities are unique; equal titles are fine.
109
+ - `docs/specs/queue.md` is the legacy queue. Its raw rows are intents: readable, never executed, never written (a row there that fully matches this grammar is ignored). `add --materialize <line>` plans one pending legacy row as a loop whose `## Intent` reproduces it verbatim (whitespace may reflow); that loop's queue file starts with `<!-- replaces docs/specs/queue.md row (occurrence <n>): <row> -->`, the row's exact text and its occurrence among equal rows, line endings aside. Every checkout, a fresh clone included, then shows the loop's rows in that row's place. A claim matching no row replaces nothing; a row two loops claim (two clones each materializing it) is hidden, neither loop taking its place, so it is never planned again.
110
+ - Status and drain read the legacy rows in file order, each replaced row as its loop's rows; then the other recorded loops in add order, from their captures; then loops without a record here, as in a fresh clone, from the checkout's tracked queue files in loop-id order.
111
+
112
+ ## Commands
113
+
114
+ ```sh
115
+ python3 "$DEVLYN_SKILL_DIR/scripts/queue.py" check '<repo>/docs/specs/<loop-id>/meta.md'
116
+ python3 "$DEVLYN_SKILL_DIR/scripts/queue.py" add '<repo>/docs/specs/<loop-id>/meta.md' [--materialize <line>]
117
+ ```
118
+
119
+ `check` validates without writing. Both print one JSON object; exit 1 is `BLOCKED` with a `reason`. `add` holds the queue lock and, in every delivery mode, makes no commit on the checked-out branch and changes no branch, index entry or tracked file:
120
+
121
+ 1. It resolves an add record for the loop id first, with or without the package files: the same package (every remaining copy equal to the capture, line endings aside, and the same `--materialize` row) reports `ADDED` and changes nothing; another package is refused as identity reuse. A capture ref a crash left without a record is captured again only when the checkout's package equals it; otherwise add reports that state.
122
+ 2. It validates the package and refuses a loop id that a committed `docs/specs/<loop-id>/queue.md` already uses, package files committed with different content, a detached HEAD, and a HEAD whose `CLAUDE.md` or `AGENTS.md` differs from this checkout's.
123
+ 3. It refuses package changes staged in the index, which it would leave behind; unstage them (`git restore --staged -- docs/specs/<loop-id>`) and add again.
124
+ 4. It builds a tree of the package files and the generated queue file in a temporary index, commits it, and points `refs/devlyn/captures/<loop-id>` at the commit, which keeps it from garbage collection.
125
+ 5. It publishes `<common Gitdir>/devlyn-loops/<loop-id>/added.json` atomically: the loop, its delivery, the capture ref, commit and tree, the checkout and branch it was added on, any materialize target and an add sequence.
126
+ 6. It removes the untracked package files equal to the capture, line endings aside. A changed copy is kept and a failed removal is reported as incomplete, both under `cleanup`; every later `add`, `status` and `drain` finishes the cleanup.
127
+
128
+ The result names the capture: the package is held immutably at `refs/devlyn/captures/<loop-id>` (inspect it with `git show refs/devlyn/captures/<loop-id>:docs/specs/<loop-id>/meta.md`), and a revision needs a new loop id. While a loop has a record, validation, the active-contract check, scoped inputs, packets, reports and whole-loop acceptance read its capture; a loop without a record reads the checkout's tracked files.