devlyn-cli 3.1.2 → 3.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 (33) hide show
  1. package/AGENTS.md +6 -6
  2. package/CLAUDE.md +7 -7
  3. package/README.md +34 -13
  4. package/bin/devlyn.js +44 -65
  5. package/bin/instruction-templates.json +82 -0
  6. package/bin/instructions.js +187 -0
  7. package/config/skills/_shared/adapters/claude.md +1 -5
  8. package/config/skills/_shared/adapters/codex.md +1 -5
  9. package/config/skills/_shared/adapters/omp.md +0 -4
  10. package/config/skills/_shared/archive_run.py +5 -1
  11. package/config/skills/_shared/codex-config.md +3 -14
  12. package/config/skills/_shared/engine-preflight.md +3 -3
  13. package/config/skills/_shared/expected.schema.json +2 -2
  14. package/config/skills/_shared/platform-support.py +4 -1
  15. package/config/skills/_shared/resolve-bootstrap.py +22 -2
  16. package/config/skills/_shared/role-config.py +14 -2
  17. package/config/skills/_shared/spec-verify-check.py +427 -101
  18. package/config/skills/_shared/state-phase-write.py +497 -49
  19. package/config/skills/_shared/task-complete.py +46 -5
  20. package/config/skills/_shared/terminal-claim-check.py +2 -2
  21. package/config/skills/devlyn:ideate/SKILL.md +1 -1
  22. package/config/skills/devlyn:ideate/references/from-spec-mode.md +1 -1
  23. package/config/skills/devlyn:ideate/references/spec-template.md +2 -2
  24. package/config/skills/devlyn:resolve/SKILL.md +29 -51
  25. package/config/skills/devlyn:resolve/references/free-form-mode.md +2 -2
  26. package/config/skills/devlyn:resolve/references/phases/build-gate.md +8 -7
  27. package/config/skills/devlyn:resolve/references/phases/cleanup.md +5 -11
  28. package/config/skills/devlyn:resolve/references/phases/implement.md +1 -0
  29. package/config/skills/devlyn:resolve/references/phases/plan.md +3 -3
  30. package/config/skills/devlyn:resolve/references/state-schema.md +10 -9
  31. package/package.json +2 -1
  32. package/scripts/lint-skills.sh +14 -4
  33. package/scripts/test-owner-phases.py +193 -0
@@ -36,7 +36,7 @@ def require(condition, message):
36
36
  def command(argv, *, cwd=None, ok=(0,)):
37
37
  result = subprocess.run(argv, cwd=cwd, capture_output=True, text=True, encoding="utf-8")
38
38
  require(result.returncode in ok, f"{shlex.join(argv[:4])}: {result.stderr.strip() or result.stdout.strip()}")
39
- return result.stdout.strip()
39
+ return result.stdout.removesuffix("\n")
40
40
 
41
41
 
42
42
  def git(work, *args):
@@ -190,7 +190,7 @@ def allocate(args):
190
190
  receipt["remote_url"] = remote_url(receipt)
191
191
  require("\n" not in receipt["remote_url"]["push"] and receipt["remote_url"]["push"] == receipt["remote_url"]["fetch"], "split/multiple remote URLs are unsupported")
192
192
  require(not ref_sha(receipt, "refs/heads/"+args.branch), "existing branch cannot be adopted")
193
- require(git(work, "symbolic-ref", "--short", "HEAD") == args.base, "allocate from the retained base checkout")
193
+ require(git(work, "symbolic-ref", "HEAD") == "refs/heads/" + args.base, "allocate from the retained base checkout")
194
194
  require(not git(work, "status", "--porcelain", "--untracked-files=all"), "allocation requires clean tracked/untracked contents")
195
195
  receipt["baseline"] = git(work, "rev-parse", "HEAD")
196
196
  target = Path(args.worktree).absolute() if args.worktree else work
@@ -779,6 +779,14 @@ else:
779
779
  raise SystemExit('unexpected gh arguments: '+str(a))
780
780
  '''
781
781
 
782
+ GIT_DISPATCH = r'''#!/bin/sh
783
+ for arg do
784
+ case "$arg" in push|fetch|ls-remote|remove) exec python3 "$0.py" "$@";; esac
785
+ done
786
+ exec "$REAL_GIT" "$@"
787
+ '''
788
+
789
+
782
790
  GIT_WRAPPER = r'''#!/usr/bin/env python3
783
791
  import json, os, pathlib, subprocess, sys
784
792
  a = sys.argv[1:]
@@ -822,7 +830,7 @@ class CompletionTests(unittest.TestCase):
822
830
  for key in list(self.env):
823
831
  if key in {"GIT_DIR", "GIT_WORK_TREE", "GIT_INDEX_FILE", "GIT_COMMON_DIR"}:
824
832
  del self.env[key]
825
- for name, body in (("gh", FAKE_GH), ("git", GIT_WRAPPER)):
833
+ for name, body in (("gh", FAKE_GH), ("git.py", GIT_WRAPPER), ("git", GIT_DISPATCH)):
826
834
  path = self.bin / name
827
835
  path.write_text(body, encoding="utf-8")
828
836
  path.chmod(0o755)
@@ -854,8 +862,8 @@ class CompletionTests(unittest.TestCase):
854
862
  r = self.run_cmd([sys.executable, str(Path(__file__).resolve()), *map(str, args)], success=success, cwd=cwd)
855
863
  return json.loads(r.stdout), r
856
864
 
857
- def allocate(self, linked=False):
858
- args = ["allocate", "--repo", self.work, "--task", "fixture", "--branch", "task/fixture", "--repository", "test/project", "--base", "main"]
865
+ def allocate(self, linked=False, base="main"):
866
+ args = ["allocate", "--repo", self.work, "--task", "fixture", "--branch", "task/fixture", "--repository", "test/project", "--base", base]
859
867
  if linked:
860
868
  args += ["--worktree", self.root / "linked"]
861
869
  result, _ = self.cli(*args)
@@ -863,6 +871,39 @@ class CompletionTests(unittest.TestCase):
863
871
  self.task = Path(result["worktree"])
864
872
  return result
865
873
 
874
+ def test_allocate_base_with_same_named_tag(self):
875
+ self.g("tag", "main")
876
+ result = self.allocate(linked=True)
877
+ self.assertEqual(result["status"], "ALLOCATED")
878
+ self.assertEqual(json.loads(self.receipt.read_text(encoding="utf-8"))["base"], "main")
879
+
880
+ def test_allocate_base_preserves_unicode_whitespace(self):
881
+ base = "main\u00a0"
882
+ self.g("switch", "-c", base)
883
+ result = self.allocate(base=base)
884
+ self.assertEqual(result["status"], "ALLOCATED")
885
+ self.assertEqual(json.loads(self.receipt.read_text(encoding="utf-8"))["base"], base)
886
+
887
+ def assert_base_identity_rejected(self, actual, base):
888
+ result, process = self.cli("allocate", "--repo", self.work, "--task", "fixture",
889
+ "--branch", "task/fixture", "--repository", "test/project", "--base", base, success=False)
890
+ self.assertNotEqual(process.returncode, 0)
891
+ self.assertIn("retained base checkout", result["reason"])
892
+ head = self.run_cmd(["git", "-C", str(self.work), "symbolic-ref", "HEAD"]).stdout
893
+ self.assertEqual(head, "refs/heads/" + actual + "\n")
894
+ self.assertFalse((self.work / ".git/devlyn-completion").exists())
895
+ self.assertNotEqual(self.run_cmd(["git", "-C", str(self.work), "show-ref", "--verify",
896
+ "refs/heads/task/fixture"], success=False).returncode, 0)
897
+
898
+ def test_allocate_rejects_trimmed_base_alias(self):
899
+ self.g("switch", "-c", "main\u00a0")
900
+ self.assert_base_identity_rejected("main\u00a0", "main")
901
+
902
+ def test_allocate_rejects_short_base_alias(self):
903
+ self.g("branch", "heads/main")
904
+ self.g("tag", "main")
905
+ self.assert_base_identity_rejected("main", "heads/main")
906
+
866
907
  @unittest.skipUnless(sys.platform == "darwin" or sys.platform.startswith("linux"), "writer observation requires POSIX")
867
908
  def test_disposable_scratch_cleans_for_local_only_and_preserves_source(self):
868
909
  result = self.allocate()
@@ -149,7 +149,7 @@ def classify_state_bytes(
149
149
  return malformed("run state must be a JSON object"), None
150
150
 
151
151
  run_id = state.get("run_id")
152
- if not isinstance(run_id, str) or not SAFE_RUN_ID_RE.fullmatch(run_id):
152
+ if not isinstance(run_id, str) or run_id in (".", "..") or not SAFE_RUN_ID_RE.fullmatch(run_id):
153
153
  return malformed("run state has invalid run_id"), state
154
154
  phases = state.get("phases")
155
155
  if not isinstance(phases, dict):
@@ -197,7 +197,7 @@ def classify_state_bytes(
197
197
  verdict = verify.get("verdict")
198
198
  if verdict is None:
199
199
  return incomplete("verify", "verify completed without verdict", run_id), state
200
- if verdict not in VALID_VERIFY_VERDICTS:
200
+ if not isinstance(verdict, str) or verdict not in VALID_VERIFY_VERDICTS:
201
201
  return malformed("verify has invalid verdict", run_id), state
202
202
  else:
203
203
  verdict = None
@@ -110,7 +110,7 @@ Structural lint (inline check, no script needed):
110
110
  After lint passes:
111
111
  1. Write `<spec-dir>/<id>-<slug>/spec.md` (the spec).
112
112
  2. Generate `<spec-dir>/<id>-<slug>/spec.expected.json` following `references/spec-template.md` § "Sibling file: `spec.expected.json`" for constraint coverage, diff scope and guard controls.
113
- 3. Run `python3 "$DEVLYN_SHARED_DIR/spec-verify-check.py" --check <spec-path>` to validate the verification carrier shape, supported `complexity` frontmatter, and any present actionable solo-headroom hypothesis; if the spec uses a legacy inline `## Verification` JSON carrier, any solo-headroom hypothesis command must match that carrier's `verification_commands[].cmd`. If exit 2, fix the carrier/frontmatter/hypothesis and re-run.
113
+ 3. Run `python3 "$DEVLYN_SHARED_DIR/spec-verify-check.py" --check <spec-path>` to validate the actual spec's carrier shape and supported `complexity` frontmatter. Sibling `spec.expected.json` takes precedence over the legacy inline `## Verification` JSON carrier; any present actionable solo-headroom hypothesis command must match that carrier's `verification_commands[].cmd`. If exit 2, fix the file named in the error and re-run.
114
114
  4. Run `python3 "$DEVLYN_SHARED_DIR/spec-verify-check.py" --check-expected <expected-path>` to validate sibling `spec.expected.json` against `_shared/expected.schema.json` plus sibling spec `complexity` frontmatter and any present actionable solo-headroom hypothesis; if the spec has a solo-headroom hypothesis, its observable command must match `spec.expected.json.verification_commands[].cmd`. If exit 2, fix the JSON/frontmatter/hypothesis and re-run.
115
115
  5. Print: `spec ready — /devlyn:resolve --spec <spec-path>`.
116
116
 
@@ -37,7 +37,7 @@ You must NOT:
37
37
  3. For each missing/malformed piece: apply the smallest allowed fix.
38
38
  4. Write the normalized spec. Default location: `<spec-dir>/<id>-<slug>/spec.md`. With `--in-place` flag: write to `<path>` directly (overwrites the original).
39
39
  5. Generate or fix `spec.expected.json` per the rules above. Same dir as the spec.
40
- 6. Run `python3 "$DEVLYN_SHARED_DIR/spec-verify-check.py" --check <spec-path>` to validate the spec carrier and supported `complexity` frontmatter; if the spec uses a legacy inline `## Verification` JSON carrier, any solo-headroom hypothesis command must match that carrier's `verification_commands[].cmd`.
40
+ 6. Run `python3 "$DEVLYN_SHARED_DIR/spec-verify-check.py" --check <spec-path>` to validate the actual spec's carrier shape and supported `complexity` frontmatter. Sibling `spec.expected.json` takes precedence over the legacy inline `## Verification` JSON carrier; any present actionable solo-headroom hypothesis command must match that carrier's `verification_commands[].cmd`. If exit 2, fix the file named in the error and re-run.
41
41
  7. Run `python3 "$DEVLYN_SHARED_DIR/spec-verify-check.py" --check-expected <expected-path>` to validate sibling `spec.expected.json` plus sibling spec `complexity` frontmatter and any present solo-headroom hypothesis command against `spec.expected.json.verification_commands[].cmd`.
42
42
  8. If lint still fails after allowed fixes (e.g. Requirements section is empty in the source), surface the issue and exit non-zero — do NOT invent Requirements.
43
43
  9. If the preserved Requirements combine state mutation with ordering/priority,
@@ -95,10 +95,10 @@ Schema: `_shared/expected.schema.json`. Required when Requirements have observab
95
95
  python3 "$DEVLYN_SHARED_DIR/spec-verify-check.py" --check-expected <expected-path>
96
96
  ```
97
97
 
98
- Map mechanically decidable rules from both Requirements and Constraints into the existing carriers. Validate each new guard with a violating control that fails and an allowed control that passes, preserving permitted pre-existing occurrences:
98
+ Map mechanically decidable rules from both Requirements and Constraints into the existing carriers. Preserve specific file constraints even when narrower than PLAN's scope. During BUILD_GATE, with `state.base_ref.sha` recorded, the existing `authorized_surface` check enforces PLAN's scope over the run diff and newly created untracked paths (`.devlyn/` exempt). Do not duplicate this general scope check with a whole-checkout scan in `verification_commands`. Validate each new guard with a violating control that fails and an allowed control that passes, preserving permitted pre-existing occurrences:
99
99
  - `verification_commands` ← `## Verification` + commands the conversation surfaced. While drafting both Verification and its carrier, omit a standalone check only when another command in that contract runs the same assertions under the required execution conditions and propagates failure; describe that shared coverage in Verification and keep both carriers aligned. Preserve distinct scenarios and required independent verification; execute approved commands unchanged without reusing results across phases. For semantic annotation or exception-handling rules, use a suitable syntax-aware/executable check, preferring existing checks; explicitly retain uncovered semantics as source-review obligations.
100
100
  - `forbidden_patterns` ← literal syntax constraints only; regex does not prove semantics. It scans the whole file-filtered diff, including context, deletions and headers; additions-only guards must exclude those lines.
101
- - `required_files` / `forbidden_files` ← file requirements and exclusions in Requirements, Constraints and Out of Scope.
101
+ - `required_files` / `forbidden_files` ← literal file paths from Requirements, Constraints and Out of Scope; `forbidden_files` matches exact Git-relative paths (forward slashes, no leading `./`), including both names of a rename. Globs and directory-wide exclusions need a suitable `verification_commands` guard, not wildcard-looking entries in these lists.
102
102
  - `max_deps_added` ← dependency limits in Requirements or Constraints (default 0 unless the spec licenses a new dep).
103
103
  - `required_risk_probe_requirements` ← for specs where a Requirement combines ordering/priority, rollback, idempotency, auth/signature, concurrency, atomic-batch, or exact output shape, declare the risk categories PHASE 1.5 risk-probe derivation must cover directly, e.g. `[{"tag": "rollback_state", "derived_from": "<exact ## Verification bullet>"}]`. This replaces guessing the required categories from prose — declare them explicitly so the check works in any language. `tag` must be one of the risk-probe tags in `_shared/spec-verify-check.py:RISK_PROBE_TAGS`; `derived_from` must be an exact substring of `## Verification`. Omit entirely when the spec has no ordering/rollback/idempotency/auth/concurrency/atomic-batch/exact-shape risk.
104
104
 
@@ -1,9 +1,9 @@
1
1
  ---
2
2
  name: devlyn:resolve
3
- description: Hands-free full pipeline for explicit resolve requests, formal specs, queue drains, material ambiguity, subsystem/design work, security/auth/payment/persistence/concurrency/public-API-contract changes, or no decisive acceptance check. Free-form goal or formal spec input. Plan → Implement → Build-gate → Cleanup → Verify (fresh subagent, findings-only). Mechanical-first verification; Verify dual-judge is default-when-available. Clear, local, reversible, low-risk conversational edits with decisive checks default to direct execution; explicit small resolve keeps the full workflow.
3
+ description: Default to direct work after inspecting scope and verification, including bounded multi-file changes. Use full resolve for explicit resolve/spec-mode requests, queue drains, or concrete interacting requirements or verification too complex for the current context. Domain labels, file count, spec documents and quoted skill paths alone do not trigger it. Investigate or clarify missing intent/access first. Explicit small resolve retains all phases and independent verification. Verify dual-judge is default-when-available.
4
4
  ---
5
5
 
6
- Orchestrator for the 2-skill harness pipeline. One fresh worker per phase; file-based handoff via `.devlyn/pipeline.state.json`. VERIFY spawns a fresh-context worker so independence is structural — not advisory.
6
+ The current CLI owns PHASE 0, state transitions, Git checkpoints and final report/archive. Spawn the canonical phase workers from this process; never delegate the whole run to another parent orchestrator. VERIFY uses a fresh, findings-only worker.
7
7
 
8
8
  <pipeline_config>
9
9
  $ARGUMENTS
@@ -48,13 +48,13 @@ fi
48
48
  </runtime_paths>
49
49
 
50
50
  <engine_routing>
51
- Each phase routes to an engine and prepends the per-engine adapter header from `_shared/adapters/<engine>.md` (e.g. `claude.md`, `codex.md`) to the canonical phase body. Adapter is the per-model delta (Anthropic's prompt-engineering guide for Claude, OpenAI's prompt guidance for Codex). Canonical body is engine-agnostic.
51
+ Each model-invoked phase routes to an engine and prepends the per-engine adapter header from `_shared/adapters/<engine>.md` (e.g. `claude.md`, `codex.md`) to the canonical phase body. Adapter is the per-model delta (Anthropic's prompt-engineering guide for Claude, OpenAI's prompt guidance for Codex). Canonical body is engine-agnostic.
52
52
 
53
- - Phase spawning is mandatory for every orchestrator. Same-context PLAN / IMPLEMENT / BUILD_GATE / CLEANUP / VERIFY is a contract violation. If the current CLI cannot spawn a fresh worker, write the current phase verdict as `"BLOCKED"` and report `BLOCKED:fresh-context-unavailable` with the failed spawn command; do not continue with ad-hoc same-context execution.
54
- - Claude Code phases other than PLAN: spawn `Agent` (`mode: "bypassPermissions"`); prompt = adapter-header + canonical-body + task-context. PLAN uses the native foreground call shape in PHASE 1.
55
- - Codex CLI phases: shell out via `bash "$CODEX_MONITORED_PATH"` with the same compounded prompt. Each `codex exec` child is a new session/fresh context. Write the compounded prompt to a file and set `DEVLYN_CODEX_PROMPT_FILE` with sole prompt argument `-`; the wrapper snapshots exact stdin bytes, seals transport evidence and emits a heartbeat. Without file transport stdin remains DEVNULL. No MCP. The wrapper call is foreground-blocking — never a background shell (`run_in_background`, `&`, `nohup`), never end your message while it runs: headless print-mode wind-down kills backgrounded children (0-byte delivery); the heartbeat is the observability channel.
56
- - oh-my-pi phases: spawn the native `task` tool with a fresh `context` containing adapter-header + canonical-body + task-context. Capture the task result into `.devlyn/<phase>.stdout` and any tool error into `.devlyn/<phase>.stderr` before updating state. If the `task` tool is unavailable for an omp-routed phase, write the current phase verdict as `"BLOCKED"` and report `BLOCKED:fresh-context-unavailable`; do not fall back to same-context execution or a nested `omp -p` subprocess.
57
- - Default engine: Claude when the orchestrator has Claude Code’s native `Agent`; otherwise its own fresh worker. PLAN is orchestrator-fixed and never inherits `--engine`, an executor pin, or `state.engine`. BUILD_GATE and probes retain their existing routes. `_shared/engine-preflight.md#role-resolution` defines the single resolver and explicit `--role-config` / project role precedence. Absent profiles preserve legacy `--engine` / executor / default behavior; VERIFY primary may be independently selected. Configured role/priority pins and flags fail closed on unavailable engine or failed authentication, including failures discovered at dispatch. Unconfigured automatic VERIFY selects an available OTHER engine.
53
+ - PLAN runs in the owner context; BUILD_GATE and CLEANUP run orchestrator commands with no separate model. The selected IMPLEMENT worker also owns code/doc cleanup before its final checkpoint. IMPLEMENT and VERIFY still require fresh workers. If the current CLI cannot spawn a required worker, write the current phase verdict as `"BLOCKED"` and report `BLOCKED:fresh-context-unavailable` with the failed spawn command; do not continue with ad-hoc same-context execution.
54
+ - Claude Code model-invoked phases: spawn `Agent` (`mode: "bypassPermissions"`); prompt = adapter-header + canonical-body + task-context.
55
+ - Codex CLI model-invoked phases: shell out via `bash "$CODEX_MONITORED_PATH"` with the same compounded prompt. Each `codex exec` child is a new session/fresh context. Write the compounded prompt to a file and set `DEVLYN_CODEX_PROMPT_FILE` with sole prompt argument `-`; the wrapper snapshots exact stdin bytes, seals transport evidence and emits a heartbeat. Without file transport stdin remains DEVNULL. No MCP. The wrapper call is foreground-blocking — never a background shell (`run_in_background`, `&`, `nohup`), never end your message while it runs: headless print-mode wind-down kills backgrounded children (0-byte delivery); the heartbeat is the observability channel.
56
+ - oh-my-pi model-invoked phases: spawn the native `task` tool with a fresh `context` containing adapter-header + canonical-body + task-context. Capture the task result into `.devlyn/<phase>.stdout` and any tool error into `.devlyn/<phase>.stderr` before updating state. If the `task` tool is unavailable for an omp-routed phase, write the current phase verdict as `"BLOCKED"` and report `BLOCKED:fresh-context-unavailable`; do not fall back to same-context execution or a nested `omp -p` subprocess.
57
+ - Default engine: Claude when the orchestrator has Claude Code’s native `Agent`; otherwise its own fresh worker. PLAN is orchestrator-fixed and never inherits `--engine`, an executor pin, or `state.engine`. BUILD_GATE runs orchestrator commands; probes retain their existing routes. `_shared/engine-preflight.md#role-resolution` defines the single resolver and explicit `--role-config` / project role precedence. Absent profiles preserve legacy `--engine` / executor / default behavior; VERIFY primary may be independently selected. Configured role/priority pins and flags fail closed on unavailable engine or failed authentication, including failures discovered at dispatch. Unconfigured automatic VERIFY selects an available OTHER engine.
58
58
  - The `--engine` flag does not disable default pairing: the second judge uses the OTHER engine by default when available.
59
59
  - Multi-LLM evolution: when a new model adapter ships in `_shared/adapters/`, that engine becomes selectable for the roles its adapter declares eligible without further skill changes (NORTH-STAR.md "Multi-LLM evolution direction").
60
60
  </engine_routing>
@@ -72,7 +72,7 @@ After `state.implement_passed_sha` is set: SURFACE_CLOSE and CLEANUP are limited
72
72
  </post_implement_invariant>
73
73
 
74
74
  <transition_protocol>
75
- For every direct complete→spawn handoff, call `state-phase-write.py ... --phase <current> transition` with the current phase's normal completion/attestation arguments plus caller-specified `--next-phase`, `--next-round`, `--next-triggered-by`, `--next-engine`, `--next-model`, and any next-phase metadata. The verb validates a legal edge and commits both lifecycle writes atomically; it returns JSON state facts only. It never selects a phase/engine, renders a prompt, or spawns an agent. Use standalone `spawn` only for the initial post-bootstrap dispatch; use standalone `complete` when no next phase opens (including a halt).
75
+ For every direct complete→spawn handoff, call `state-phase-write.py ... --phase <current> transition` with the current phase's normal completion/attestation arguments plus caller-specified `--next-phase`, `--next-round`, `--next-triggered-by`, `--next-engine`, `--next-model`, and any next-phase metadata. For owner PLAN/BUILD_GATE/CLEANUP, omit engine/model/prompt/session arguments. PLAN records `execution_kind: "orchestrator_context"`; BUILD_GATE/CLEANUP record `execution_kind: "orchestrator_commands"`, all with null worker identity. Existing metadata-bearing PLAN/CLEANUP and already-open absent-kind BUILD spans retain historical worker validation; never relabel their evidence. The verb validates a legal edge and commits both lifecycle writes atomically; it returns JSON state facts only. It never selects a phase/engine, renders a prompt, or spawns an agent. Use standalone `spawn` only for the initial post-bootstrap dispatch; use standalone `complete` when no next phase opens (including a halt).
76
76
  </transition_protocol>
77
77
 
78
78
  ## PHASE 0: PARSE + CLASSIFY + ROUTE
@@ -97,7 +97,7 @@ Outer-owner boundary: before normal task writes, follow `references/task-complet
97
97
 
98
98
  5. Announce one line: `resolve starting — run <run_id> — engine <engine> — mode <mode> — complexity <complexity-or-na> — pair <on|solo:auto_pair_other_engine_unavailable|disabled> — risk_probes <on|off>`.
99
99
 
100
- 6. Open the initial post-bootstrap span with standalone `state-phase-write.py ... spawn`: `plan` before its worker dispatch (normal run), or `verify` before MECHANICAL (verify-only). The bootstrap never chooses a phase, engine, branch, prompt, or agent.
100
+ 6. Open the initial post-bootstrap span with standalone `state-phase-write.py ... spawn`: `plan` before owner planning (normal run), or `verify` before MECHANICAL (verify-only). The bootstrap never chooses a phase, engine, branch, prompt, or agent.
101
101
 
102
102
  ### Explicit role dispatch
103
103
 
@@ -111,17 +111,13 @@ After the actual process exits, run `python3 "$DEVLYN_SHARED_DIR/judge-role-evid
111
111
 
112
112
  ## PHASE 1: PLAN
113
113
 
114
- Skip in verify-only mode. The heaviest phase by design — spec/criteria define non-negotiable invariants; plan formalizes how the implementation hits them.
114
+ Skip in verify-only mode. PLAN is orchestrator-fixed and never inherits `--engine`, an executor pin, or `state.engine`. The owner reads `references/phases/plan.md` and the original request/spec, then writes `.devlyn/plan.md` before product edits. Open with `state-phase-write.py ... --phase plan spawn --round 0`, without worker identity or a prompt dispatch. This is owner reasoning, not zero-cost mechanical work.
115
115
 
116
- Engine: PLAN is orchestrator-fixed and never inherits `--engine`, an executor pin, or `state.engine`; Claude Code uses a native Claude `Agent` worker, while Codex CLI / oh-my-pi use their own fresh worker per `_shared/engine-preflight.md` (PLAN-pair is **research-only** — iter-0033d/f/g; unblock conditions in PHASE 1 / iter-0033g §H; iter-0020 falsified Codex-BUILD/IMPLEMENT, NOT PLAN-pair). Before rendering, assign the current orchestrator's literal adapter name (`PLAN_ENGINE=claude`, `PLAN_ENGINE=codex`, or `PLAN_ENGINE=omp`) and assign `PLAN_MODEL=<exact model id passed to that fresh worker>`; replace the metavalue and fail closed if either assignment is empty. Write the data-only task context to `.devlyn/plan.task-context` with `PLAN_WORKING_DIR="$(pwd -P)"`: its first two lines are exactly `Working directory: $PLAN_WORKING_DIR` and `Plan output: $PLAN_WORKING_DIR/.devlyn/plan.md`, followed by the task data. For round `PLAN_ROUND`, render adapter + `references/phases/plan.md` + task context without byte normalization to `PLAN_PROMPT_FILE=.devlyn/plan.prompt.$PLAN_ROUND`, and set `PLAN_PROMPT_SHA256="$(python3 "$DEVLYN_SHARED_DIR/phase-prompt-render.py" --adapter "$DEVLYN_SHARED_DIR/adapters/$PLAN_ENGINE.md" --canonical-body "$DEVLYN_SKILL_DIR/references/phases/plan.md" --task-context .devlyn/plan.task-context --output "$PLAN_PROMPT_FILE")"`. The renderer fails closed before writing the round-scoped prompt if either fixed header field is missing, non-absolute, or mismatches the active repository paths. Open round 0 with `state-phase-write.py ... --phase plan spawn --round 0 --engine "$PLAN_ENGINE" --model "$PLAN_MODEL" --prompt-sha256 "$PLAN_PROMPT_SHA256"`. In Claude Code, invoke the native `Agent` with its `prompt` field set to the exact `PLAN_PROMPT_FILE` bytes without appending a terminal LF (the renderer emits none), explicitly set `run_in_background: false`, and omit `mode`; this is a data field, so a path, `$(cat ...)`, or any variable-reference substitution is not the rendered prompt. If native input validation rejects this call, surface the failure without retrying a different `mode` or background shape and fail closed through the existing `BLOCKED:fresh-context-unavailable` path. The PLAN worker inherits the parent's permission context: the hands-free parent already uses native `bypassPermissions`, while a non-bypass parent's permission prompt remains the user's native boundary and is never silently escalated. A Codex CLI PLAN must pass explicit `-m "$PLAN_MODEL"` and the seven `DEVLYN_INVOCATION_*` values around `codex-monitored.sh`, using phase `plan`, `PLAN_ROUND`, `PLAN_WORKING_DIR`, `PLAN_PROMPT_FILE`, `PLAN_SESSION_FILE=.devlyn/plan.worker-session.$PLAN_ROUND.jsonl`, and `PLAN_RECEIPT_FILE=.devlyn/plan.invocation.$PLAN_ROUND.json`; invoke the wrapper as `DEVLYN_CODEX_PROMPT_FILE="$PLAN_PROMPT_FILE" bash "$CODEX_MONITORED_PATH" --json -C "$PLAN_WORKING_DIR" -s workspace-write -m "$PLAN_MODEL" -c sandbox_workspace_write.network_access=false <other options> - >"$PLAN_SESSION_FILE" 2>.devlyn/plan.stderr`, then pass that same session as `state-phase-write.py ... --engine-session-log "$PLAN_SESSION_FILE"` on completion/transition. The wrapper must write and finish the receipt around that direct capture; this state-bound receipt proves requested-model dispatch and prompt/session binding; effective model remains unknown, and a native model-reroute event blocks completion. oh-my-pi delivers the same prompt through its unchanged route. Product code records rendered intent; it never parses a transcript or intercepts, rewrites, wraps, or forces native delivery.
117
-
118
- Subagent output (writes `.devlyn/plan.md`): file list to touch, risk list (out-of-scope expansions, ambiguous spec sections), acceptance restatement (what `## Verification` actually requires verbatim). For large work only, PLAN may add a fourth section `## Execution phases` under the conditions in `references/phases/plan.md`; a phase block missing a runnable `gate:` line disqualifies the whole section — treat the run as single-phase and log the disqualification.
119
-
120
- State write: each PLAN receipt carries spawn-known `round`, `started_at`, `triggered_by`, `engine`, `model_requested`, and `prompt_sha256`; completion adds `completed_at`, `duration_ms`, `verdict`, nullable `model_effective`, and `output_sha256` under `references/state-schema.md`. Later mutations rehash bound output. Never edit PLAN to widen scope after completion; only the bounded PLAN re-spawn may replace its receipt.
116
+ Keep the file list/authorized surface, risks and verbatim verification requirements. Large work may include the existing execution-phase section only when every boundary has a runnable gate. Completing PLAN binds the exact output digest; all later mutations rehash it. Never widen it after implementation starts.
121
117
 
122
118
  After return:
123
119
  1. If `.devlyn/plan.md` lists zero files → halt with verdict `BLOCKED:plan-empty`.
124
- 2. If risk list flags an out-of-scope expansion the user did not authorize → the sole legal corrective re-spawn is round 1: update the data-only task context, repeat render + digest into `.devlyn/plan.prompt.1`, open PLAN with `--round 1 --triggered-by plan` and the actual engine/model/digest, then deliver those exact prompt bytes using the same native or receipt-bound CLI call shape as round 0. A second failed plan halts; no further PLAN dispatch is authorized.
120
+ 2. If the plan exceeds the authorized scope, the sole correction is owner round 1 with `--triggered-by plan`, before implementation starts. Narrow to the original authorization, complete and bind the new output; a second failed plan halts. No worker dispatch or new permission is implied.
125
121
  3. After any re-spawn above, if `state.risk_profile.risk_probes_enabled == true` and `state.risk_profile.risk_probes_explicit == false`, parse the `authorized_surface` array from the JSON block under `<!-- devlyn:authorized-surface -->`. For a well-formed string array, compute `probe_scale_small := len(authorized_surface) <= 2 AND no entry ends in "/**"`. If true, set `risk_probes_enabled = false`, leave `high_risk` unchanged, and append `auto-risk-probes demoted: plan surface small (<n> paths)` to `reasons` using the actual length. A missing or malformed block leaves probe state unchanged; BUILD_GATE owns its malformed-block failure.
126
122
 
127
123
  ## PHASE 1.5: RISK_PROBES
@@ -228,9 +224,9 @@ After return:
228
224
 
229
225
  Skip in verify-only mode. Constrained design judgment within PLAN's invariants. Writes code, tests, and inline doc-comments. No standalone DOCS phase — what the spec licenses is updated here, what it does not is out of scope.
230
226
 
231
- Engine/model/effort: frozen `role_resolution.roles.worker`; obtain validated argv additions using `role-config.py --state .devlyn/pipeline.state.json --role worker --resolved-model <existing-exact-phase-model>`. Prompt body: `references/phases/implement.md`.
227
+ Engine/model/effort: frozen `role_resolution.roles.worker`, including code/doc cleanup within this same invocation before its final checks; obtain validated argv additions using `role-config.py --state .devlyn/pipeline.state.json --role worker --resolved-model <existing-exact-phase-model>`. Prompt body: `references/phases/implement.md`.
232
228
 
233
- For every Codex-routed IMPLEMENT, BUILD_GATE, or CLEANUP spawn, render the exact
229
+ For every Codex-routed IMPLEMENT spawn, render the exact
234
230
  prompt to `.devlyn/<phase>.prompt.<round>`, pass its SHA-256 to `state-phase-write.py
235
231
  spawn --prompt-sha256`, and invoke only through `codex-monitored.sh` with these
236
232
  seven variables set to the active state identity:
@@ -240,13 +236,14 @@ The session and receipt paths are `.devlyn/<phase>.worker-session.<round>.jsonl`
240
236
  and `.devlyn/<phase>.invocation.<round>.json`. These three paths are round-scoped
241
237
  so a retry cannot overwrite earlier prompt/session evidence. Every invocation
242
238
  must include `--json -m <model_requested>` and `-c sandbox_workspace_write.network_access=<true|false>`: exactly
243
- `true` for BUILD_GATE and exactly `false` for PLAN, IMPLEMENT, and CLEANUP, so
239
+ `false` for IMPLEMENT, so
244
240
  user configuration cannot silently change the phase capability. Redirect wrapper stdout directly
245
241
  to that session path. The wrapper rejects bypass/yolo flags and any sandbox other
246
242
  than `workspace-write`. The receipt seals the requested model, sandbox, phase-scoped
247
243
  network capability, prompt, terminal exit, and session digest; completion passes the
248
244
  same canonical session via `--engine-session-log`. A missing/mismatched receipt
249
245
  or same-round retry blocks. Respawn with a new round instead of replacing it.
246
+ Historical metadata-bearing PLAN/CLEANUP and absent-kind BUILD spans retain their original receipt-bound worker routes (network false for PLAN/CLEANUP, true for BUILD). New owner spans never create worker receipts.
250
247
 
251
248
  State write: `phases.implement.{started_at, verdict, completed_at, duration_ms}`.
252
249
 
@@ -276,56 +273,37 @@ Run once iff `state.source.type == "generated"` and complexity is trivial/medium
276
273
 
277
274
  Freeze `.devlyn/surface-close.input.patch`; assemble Claude adapter + `references/phases/surface-close.md` canonical body VERBATIM + data-only Goal/patch/hashes/surface/commands; write the exact bytes to `.devlyn/surface-close.prompt.<round>`, hash them and spawn with `--tools "Read,Grep,Glob,Edit,Write" --dangerously-skip-permissions --output-format json --strict-mcp-config --mcp-config '{"mcpServers":{}}'`, omitting `--model` at SPW spawn to inherit native Claude model settings. Record the effective model from the native JSON result. Use no positional prompt on the CLI route and retain its generated transport carrier. Bound at 600s (native or `run-bounded.py 600 --stdin-file .devlyn/surface-close.prompt.<round> --record-transport -- claude -p`), else block pre-spawn. Save raw stdout as `.devlyn/surface-close.output.json`, extract its `result` string to `.devlyn/surface-close.stdout`, retain `.devlyn/surface-close.worker-session.<round>.jsonl`, and complete with `.devlyn/surface-close.output.json` as `--engine-session-log`; non-JSON/missing result follows the existing failure path. Workers execute nothing; `surface-check` gates rows, citations, scope, and execution. Empty PASS completes; authorized delta is scoped-staged and committed. Only `BLOCKED:surface-close-adjudication-malformed` runs `surface-adjudication-recover --authorized-surface-json <array>`, whose successful rollback and transcript Edit/Write audit complete the nonterminal skip carrier and continue to BUILD_GATE. Timeout, input/prompt mismatch, attestation failure, execution-audit violation, and out-of-surface delta run `surface-rollback`, complete bare `BLOCKED`, and halt; rollback failure also halts. One shot; no `max_rounds`.
278
275
 
279
- **Common post-fix checkpoint (BUILD_GATE and VERIFY):** After fix IMPLEMENT returns, increment `state.rounds.global`; scoped-stage the authorized surface and commit `chore(pipeline): implement fix round <n>`; run `python3 "$DEVLYN_SHARED_DIR/state-phase-write.py" --devlyn-dir .devlyn --phase implement durability-enforce --round <n> --origin-phase <build_gate|verify>`. Its receipt must PASS before re-entry, which enforces it again before VERIFY artifact clearing or phase spawn.
276
+ **Common post-fix checkpoint (BUILD_GATE and VERIFY):** CLEANUP findings use the same checkpoint with origin `cleanup`. After fix IMPLEMENT returns, increment `state.rounds.global`; scoped-stage the authorized surface and commit `chore(pipeline): implement fix round <n>`; run `python3 "$DEVLYN_SHARED_DIR/state-phase-write.py" --devlyn-dir .devlyn --phase implement durability-enforce --round <n> --origin-phase <build_gate|cleanup|verify>`. Its receipt must PASS before re-entry, which enforces it again before VERIFY artifact clearing or phase spawn.
280
277
 
281
278
  ## PHASE 3: BUILD_GATE
282
279
 
283
280
  Skip in verify-only mode OR when `build-gate` in `state.bypasses`. Deterministic — same commands CI / Docker / production run.
284
281
 
285
- Spawn the resolved BUILD_GATE engine through the fresh-worker route in
286
- `<engine_routing>` with prompt body `references/phases/build-gate.md`. A Codex
287
- route must inherit CI-equivalent capabilities from the parent invocation; it
288
- must not add isolation, narrow the sandbox, or widen permissions inside the
289
- worker. Invoke a Codex BUILD_GATE with
290
- `-s workspace-write -c sandbox_workspace_write.network_access=true`; the
291
- receipt rejects a missing, false, duplicate, or non-BUILD_GATE network override
292
- before the worker starts. This retains the write sandbox while allowing the
293
- loopback and network operations exercised by CI-equivalent gates. If an
294
- authoritative route/tool response denies a required filesystem,
295
- subprocess, loopback, PTY, or network operation, preserve that denial through
296
- the BUILD_GATE process-evidence manifest and complete the phase as
297
- `BLOCKED:build-env-underprovisioned`, not as a product finding or substitute
298
- command. The worker:
282
+ The orchestrator reads `references/phases/build-gate.md` and runs the gates directly: no adapter header, rendered worker prompt or separate model. Open its span through the predecessor transition with no BUILD engine/model/prompt/session arguments. New spans record `execution_kind: "orchestrator_commands"`; historical already-open spans with that field absent retain their worker attestation contract.
283
+
284
+ Follow the BUILD body's capability contract on the parent's actual command route and log that basis; no child receipt attests it. Authoritative denial yields report-level `BLOCKED:build-env-underprovisioned` (bare `BLOCKED` in state). Keep spec/probe capture and its conditional post-spec denial carrier refresh; ordinary gates retain findings and raw logs. The orchestrator:
299
285
  1. Detects language/framework via project files (`package.json`, `pyproject.toml`, etc.).
300
- 2. Runs language-specific gates (tsc / lint / test).
286
+ 2. Runs language-specific gates (tsc / lint / test), deferring exact literal overlaps under `references/phases/build-gate.md`'s current-contract/current-round rule; task-context prompts must preserve that rule.
301
287
  3. Always runs `python3 "$DEVLYN_SHARED_DIR/spec-verify-check.py" --include-risk-probes` (verification_commands literal-match plus `.devlyn/risk-probes.jsonl` when present). If `state.risk_profile.risk_probes_enabled == true`, the script requires `.devlyn/risk-probes.jsonl`; a missing file is a CRITICAL mechanical blocker, not a silent solo run. The script routes each command through `process-evidence.py`; preserve `.devlyn/spec-verify.results.json` and its validated BUILD_GATE carrier for the state-bound archive flow.
302
288
  4. If diff touches web-surface files: run the browser tier with the repo's available toolchain (for example Playwright or curl).
303
289
  5. Emits `.devlyn/build_gate.findings.jsonl` + `.devlyn/build_gate.log.md`.
304
290
 
305
- State write: `phases.build_gate.{started_at, verdict, completed_at, duration_ms, artifacts}`.
291
+ State write: `phases.build_gate.{execution_kind, started_at, verdict, completed_at, duration_ms, artifacts}`; engine/model fields are null, with no worker prompt or receipt. Completion keeps PLAN integrity and process-evidence floors.
306
292
 
307
293
  Branch:
308
294
  - `PASS` → PHASE 4.
309
- - `FAIL` → fix loop. Spawn IMPLEMENT-engine agent with the build_gate findings as input, then run the common post-fix checkpoint with origin `build_gate`. On second FAIL with `state.rounds.global >= state.rounds.max_rounds` → halt with verdict `BLOCKED:build-gate-exhausted`.
295
+ - `FAIL` → before product repair, halt with report-level `BLOCKED:required-tools-unavailable` only if a parent probe confirms the failed invocation's external gate tool is absent, unchanged `state.base_ref.sha` configuration requires it, and cited explicit task/spec constraints prohibit supplying it through any authorized route. Derive the tool from that configuration and invocation, never diagnostic text; bind the probe's interpreter, work root and normalized child environment to the runner call sites and actual gate invocation, not parent PATH or a manifest command string alone. An available authorized tool route, product-module/configuration failure, changed tool configuration, permitted or unspecified supply, or uncertain probe/identity/environment retains the fix loop. A confirmed prohibited-tool blocker wins even with other product findings: leave them unrepaired and preserve phase `FAIL`, findings, raw streams and sealed manifests unchanged. Record the probe and constraint citation separately in parent halt evidence; no repair means no round increment. Otherwise spawn IMPLEMENT-engine agent with the build_gate findings as input, then run the common post-fix checkpoint with origin `build_gate`. On second FAIL with `state.rounds.global >= state.rounds.max_rounds` → halt with verdict `BLOCKED:build-gate-exhausted`.
310
296
  - `BLOCKED:build-env-underprovisioned` → halt immediately with the denied operation, original command, and process-evidence manifest/id; do not enter the product fix loop.
311
297
 
312
298
  ## PHASE 4: CLEANUP
313
299
 
314
- Skip if `cleanup` in `state.bypasses`. Task-scoped pass.
300
+ Skip if `cleanup` in `state.bypasses`. The owner reads `references/phases/cleanup.md`; no separate model invocation. Code/doc cleanup already belongs to the selected IMPLEMENT worker, with its original engine/model/effort and receipt.
315
301
 
316
- Engine/model/effort: the same frozen worker profile, with validated role-config argv additions. Prompt body: `references/phases/cleanup.md`. Allowlist enforced post-spawn:
317
- - Tooling artifacts the spec did not list as deliverables (`test-results/`, `playwright-report/`, `.last-run.json`, coverage HTML).
318
- - Dead code added by this diff (not pre-existing dead code).
319
- - Doc references whose target this diff renamed or removed.
302
+ Open through the predecessor transition with `--next-pre-sha "$(git rev-parse HEAD)"`, omitting worker identity. Remove only observed generated, untracked/ignored artifacts owned by this run and not required deliverables; preserve source, user data, evidence and restart records. For PASS, tracked source and HEAD must equal `pre_sha`; the state writer checks them against `--post-sha` and rejects new untracked files relative to `.devlyn/untracked.baseline` (`.devlyn/` exempt). FAIL/BLOCKED closure preserves the observed delta and cannot enter VERIFY.
320
303
 
321
- Before spawn: pass `--pre-sha "$(git rev-parse HEAD)"` to the `state-phase-write.py ... spawn` call for this phase.
304
+ If a new code/doc cleanup issue appears after BUILD, emit a concrete finding and complete CLEANUP as FAIL into the existing selected IMPLEMENT repair route with `--next-triggered-by cleanup`, within the shared round budget. After repair, checkpoint and rerun BUILD_GATE before CLEANUP and fresh VERIFY; never reuse the earlier green checks. Keep the existing authorized surface, allowlist and durability checks. Do not broaden scope for cleanup.
322
305
 
323
- State write: `phases.cleanup.{started_at, verdict, completed_at, duration_ms, pre_sha, post_sha}`.
324
-
325
- After return:
326
- 1. Run `git diff --name-only <pre_sha>` and compare new untracked files against `.devlyn/untracked.baseline` (`.devlyn/` exempt) — any path outside the cleanup allowlist (including a cleanup-created untracked file) → revert to `pre_sha`, delete the unauthorized untracked file, and emit `invariant.cleanup-out-of-scope` into `.devlyn/cleanup.findings.jsonl`.
327
- 2. If allowlist honored and diff non-empty: `git add -u && git commit -m "chore(pipeline): cleanup"`.
328
- 3. Transition CLEANUP→VERIFY through `<transition_protocol>` with `--post-sha "$(git rev-parse HEAD)"` (also when the diff was empty), preserving completion/session arguments and the next-phase identity below.
306
+ On success transition CLEANUP→VERIFY with `--post-sha "$(git rev-parse HEAD)"` and the next judge identity. VERIFY MECHANICAL runs independently on the final source.
329
307
 
330
308
  ## PHASE 5: VERIFY (fresh subagent, findings-only)
331
309
 
@@ -378,7 +356,7 @@ Open the `final_report` span through the predecessor's `state-phase-write.py --d
378
356
 
379
357
  3. **Terminal verdict** — derive from `state.phases.{plan, implement, surface_close, build_gate, cleanup, verify}.verdict` per the precedence rules in `references/state-schema.md#terminal-verdict`. Verify-only mode short-circuits to `state.phases.verify.verdict`.
380
358
 
381
- 4. **Render report to `.devlyn/final-report.md` before completion** — first line exactly `<!-- devlyn:final-report run_id=<current state.run_id> -->`, once only, followed by a nonempty report body. Sections: header (run_id, engine, mode, verdict, wall-time), per-phase summary (including SURFACE_CLOSE run or skip), pair/risk-probe status, findings table (verify + finish-gate findings), follow-up notes (the explicit line `pipeline continued to BUILD_GATE — surface_close_rolled_back_adjudication_malformed` when `continued_after_block` is set, any large-mode `## Assumptions` block, any pair-judge TIMEOUT (headline: solo verdict after pair TIMEOUT), any `--no-pair` / `--no-risk-probes` opt-out, any engine setup guidance after BLOCKED, `/devlyn:ideate` guidance after `BLOCKED:solo-headroom-hypothesis-required` that asks for the visible behavior `solo_claude` is expected to miss, and `/devlyn:ideate` guidance after `BLOCKED:solo-ceiling-avoidance-required` that asks for the concrete difference from rejected or solo-saturated controls such as `S2`-`S6`). User-facing text may follow archive; it cannot substitute for this file.
359
+ 4. **Render report to `.devlyn/final-report.md` before completion** — first line exactly `<!-- devlyn:final-report run_id=<current state.run_id> -->`, once only, followed by a nonempty report body. Sections: header (run_id, engine, mode, verdict, wall-time), per-phase summary (including SURFACE_CLOSE run or skip, owner PLAN reasoning, and BUILD_GATE/CLEANUP as orchestrator commands with no separate model; code cleanup is included in IMPLEMENT cost), pair/risk-probe status, findings table (verify + finish-gate findings), follow-up notes (the explicit line `pipeline continued to BUILD_GATE — surface_close_rolled_back_adjudication_malformed` when `continued_after_block` is set, any large-mode `## Assumptions` block, any pair-judge TIMEOUT (headline: solo verdict after pair TIMEOUT), any `--no-pair` / `--no-risk-probes` opt-out, any engine setup guidance after BLOCKED, `/devlyn:ideate` guidance after `BLOCKED:solo-headroom-hypothesis-required` that asks for the visible behavior `solo_claude` is expected to miss, and `/devlyn:ideate` guidance after `BLOCKED:solo-ceiling-avoidance-required` that asks for the concrete difference from rejected or solo-saturated controls such as `S2`-`S6`). User-facing text may follow archive; it cannot substitute for this file.
382
360
 
383
361
  5. Complete the span with `state-phase-write.py --devlyn-dir .devlyn --phase final_report complete --verdict <bare enum> --log-file .devlyn/final-report.md` — the enum class of the terminal verdict (`BLOCKED:<reason>` → `BLOCKED`; `NEEDS_WORK` / `PASS_WITH_ISSUES` / `PASS` unchanged) — BEFORE archive runs (archive prune skips runs whose `final_report.verdict` is null). The writer validates the canonical nonsymlink regular file, current run marker and nonempty body, then binds its exact bytes; validation failure leaves the phase open. Never hand-edit lifecycle fields in `pipeline.state.json` (`references/state-schema.md` § Write protocol).
384
362
 
@@ -75,8 +75,8 @@ When the rules are silent (rare — pathological goal text), default to `medium`
75
75
  The internal mini-spec written for trivial / medium / large-assumptions paths must satisfy:
76
76
 
77
77
  - `## Requirements` non-empty, each bullet testable (CLI command, test command, observable file change).
78
- - For executable generated verification, put one fenced `json` object with only `verification_commands` inside the sentinel-marked `## Verification` section. Generated mode reads this inline carrier, not a sibling file. Follow `../../devlyn:ideate/references/spec-template.md` § "Sibling file: `spec.expected.json`" for constraint coverage, diff scope and violating/allowed controls, but express those checks as executable `verification_commands`; sibling-only fields such as `required_files` and `forbidden_patterns` are rejected inline. Preserve uncovered semantics as source-review obligations.
79
- - `## Verification` is preceded by a `<!-- devlyn:verification -->` sentinel on its own line directly above the heading — the machine locator `spec-verify-check.py` uses; the heading text itself is decorative and may be any language. `## Verification` non-empty if the goal implies any runnable acceptance check. Empty Verification is allowed only when all Requirements are pure-design (e.g. "follow existing pattern X").
78
+ - For executable generated verification, put one fenced `json` object with `verification_commands` inside the sentinel-marked `## Verification` section. Generated mode reads this inline carrier, not a sibling file. Follow `../../devlyn:ideate/references/spec-template.md` § "Sibling file: `spec.expected.json`" for constraint coverage, diff scope and violating/allowed controls, but express those checks as executable `verification_commands`; sibling-only fields such as `required_files` and `forbidden_patterns` are rejected inline. Preserve uncovered semantics as source-review obligations.
79
+ - `## Verification` is preceded by a `<!-- devlyn:verification -->` sentinel on its own line directly above the heading — the machine locator `spec-verify-check.py` uses; the heading text itself is decorative and may be any language. When all Requirements are pure-design with no runnable acceptance check, put `{"pure_design": true, "verification_commands": []}` in the fenced `json` block and retain semantic source-review obligations. This explicit declaration is required; a missing block, an unmarked empty list, or `pure_design: true` with commands is invalid.
80
80
  - If a free-form goal includes pair-evidence intent and already includes an actionable solo-headroom hypothesis, preserve that literal hypothesis in `.devlyn/criteria.generated.md` unchanged enough for VERIFY to detect `solo-headroom hypothesis`, `solo_claude`, `miss`, and the backticked observable command line that itself contains `miss`, emit the canonical `spec.solo_headroom_hypothesis` pair trigger reason, and satisfy regenerated-evidence checks such as `benchmark audit --require-hypothesis-trigger`.
81
81
  - If a free-form goal includes unmeasured pair-candidate intent and already includes solo ceiling avoidance, preserve that literal note in `.devlyn/criteria.generated.md` unchanged enough for reviewers to see `solo ceiling avoidance`, `solo_claude`, and the concrete difference from rejected or solo-saturated controls such as `S2`-`S6`.
82
82
  - Free-form mode mini-specs are written to `.devlyn/criteria.generated.md` (not to a roadmap path) — this is run-scoped artifact, not a documented spec.
@@ -1,15 +1,13 @@
1
1
  # PHASE 3 — BUILD_GATE (canonical body)
2
2
 
3
- Per-engine adapter header is prepended at runtime. BUILD_GATE is mechanical / deterministic — same commands CI / Docker / production run.
3
+ The orchestrator reads this body directly, without an adapter header or worker prompt. BUILD_GATE runs the same commands CI / Docker / production run; gate selection and diagnosis remain orchestrator work.
4
4
 
5
5
  <role>
6
- Run language-specific gates and the spec literal-match verification. Emit findings; the orchestrator's fix loop consumes them.
6
+ Run language-specific gates and the spec literal-match verification. Emit findings; the orchestrator's fix loop consumes them. Git staging and commits belong to the parent IMPLEMENT checkpoint; verify any staging prerequisite with read-only Git inspection, never repeat `git add` or forward task staging instructions as worker actions.
7
7
  </role>
8
8
 
9
9
  <capability_contract>
10
- A Codex BUILD_GATE runs on the parent-provided CI-equivalent route. Required
11
- filesystem, subprocess, loopback, PTY, and network capabilities are part of the
12
- gate input, not permissions this worker may widen.
10
+ Before executing gates, inspect the orchestrator command route's effective filesystem, subprocess, loopback, PTY and network capabilities and record the actual basis in `.devlyn/build_gate.log.md`. Use an already authorized CI-equivalent route; do not infer capabilities from an engine name or unrelated command success, narrow the route, or widen permissions. No child receipt attests these parent capabilities.
13
11
 
14
12
  If the parent route or an authoritative tool response explicitly denies a
15
13
  required operation before the command can produce product output:
@@ -24,6 +22,8 @@ required operation before the command can produce product output:
24
22
  3. Do not emit a product finding, run a substitute command, narrow the command,
25
23
  retry it on another route, or treat the denial as test/lint output.
26
24
 
25
+ If a denial is appended after `.devlyn/spec-verify.results.json` was sealed, refresh only that existing object's `process_evidence` and `commands` before completion. Load `$DEVLYN_SHARED_DIR/process-evidence.py` with Python's `runpy.run_path`; using the current work root, `state.run_id`, `"build_gate"`, and its round, call `validate_manifest(work, manifest_relative_path(state, "build_gate"), run_id, "build_gate", round_, require_expectations=False)`, then `bound_carrier_summary_commands(work, carrier)` and `validate_summary_commands(work, commands, carrier)`. Replace only those two result fields with the returned carrier/commands. Preserve all other fields, genuine findings, failed expectations and raw streams; do not rewrite the manifest or replay commands. This is conditional on an actual append, not routine browser resealing. If no results file exists, retain the existing BLOCKED completion path.
26
+
27
27
  The state writer derives the phase verdict floor from this sealed manifest. A
28
28
  capability denial can complete only as `BLOCKED`; a failed product expectation
29
29
  cannot complete as `PASS` or `PASS_WITH_ISSUES` even if a mutable results or
@@ -46,12 +46,12 @@ Detect the project shape from files in `state.base_ref.sha`:
46
46
  </detection>
47
47
 
48
48
  <gates>
49
- Run in this order; each emits findings into `.devlyn/build_gate.findings.jsonl`:
49
+ Resolve the concrete commands for steps 1–3; detection examples are not command identities. Run them in order unless step 4's current valid literal contract, using its source precedence and checker defaults, schedules the exact command with the same source/test inputs, working directory and effective environment. Establish the work root and normalized child environment from the checker/runner call sites and the current invocation; a manifest's command string alone does not establish them. The literal must expect exit 0, cover every gate assertion and use the same effective timeout as the planned gate invocation; uncertain matches run normally. Defer a matching gate to step 4 and derive its usual per-error findings and severity from that invocation's validated current BUILD_GATE run/round raw streams, citing the evidence id. A completed nonzero exit still supplies the gate's usual failure findings. If step 4 leaves no validated exit-kind result for a deferred gate, run that gate normally and append its findings, preserving step 4's failures; capability denials retain the stop contract above. Keep every literal invocation and independent post-CLEANUP VERIFY. Each gate emits findings into `.devlyn/build_gate.findings.jsonl`:
50
50
 
51
51
  1. **Type check** (TypeScript / mypy / etc.). Each error → one finding, severity `HIGH`, rule `correctness.type-check`.
52
52
  2. **Lint**. Run a recognized configured language linter (for example, ESLint with a config, `ruff`, or `clippy`) and/or the exact `package.json` `scripts.lint` command. A declared specialized `lint:*` script (for example, `lint:json`) is not the general language lint gate on the strength of its declaration alone. If the spec's verification commands explicitly name such a specialized command, step 4 executes it and preserves its real failure severity. If no linter is applicable, log an explicit SKIP in `.devlyn/build_gate.log.md` and emit zero lint findings. Each error → finding, severity `MEDIUM`, rule `quality.lint`. Warnings stay LOW unless the spec elevates them.
53
53
  3. **Test suite** (npm test / pytest / go test / cargo test). Each failing test → finding, severity `HIGH`, rule `correctness.test-failure`. Include the failing test's file:line and the assertion.
54
- 4. **Spec literal verification + risk probes**: `python3 "$DEVLYN_SHARED_DIR/spec-verify-check.py" --include-risk-probes`. The script routes every literal command and risk probe through `process-evidence.py`, preserving exact command, outcome, raw stdout/stderr, and digests in the BUILD_GATE run/round manifest. It self-stages from sibling `spec.expected.json` next to `state.source.spec_path`, or the legacy inline carrier when the sibling is absent; benchmark-prestaged `.devlyn/spec-verify.json` still wins. It appends `.devlyn/risk-probes.jsonl` when present, and requires that file when `state.risk_profile.risk_probes_enabled == true`. Malformed `state.risk_profile` is also CRITICAL because it can hide enabled risk probes. When probes are enabled or `.devlyn/risk-probes.jsonl` exists, missing or mismatched top-level `state.risk_probes_digest` emits `correctness.risk-probe-integrity` CRITICAL; the digest binds `.devlyn/risk-probes.jsonl` plus referenced `.devlyn/probes/` script bytes. Command or risk-probe mismatch → CRITICAL finding. Missing required risk probes, missing/malformed generated carrier, or malformed sibling expected file → `correctness.spec-verify-malformed` CRITICAL. The same invocation also enforces PLAN's declared `authorized_surface` (the json block under `.devlyn/plan.md`'s `<!-- devlyn:authorized-surface -->` section, located by that sentinel regardless of the heading's language) against this run's diff and untracked delta: any changed path or created-during-run untracked path outside the declared surface → `scope.out-of-scope-file` CRITICAL (`.devlyn/` exempt) — the only sanctioned fix is removing the file, never widening the declared surface, since that would let the same worker that leaked scope self-authorize the leak; missing sentinel, malformed `plan.md`/surface block, or missing `.devlyn/untracked.baseline` → `scope.authorized-surface-malformed` CRITICAL. This scope check runs only here (BUILD_GATE), not when VERIFY MECHANICAL reuses this script post-CLEANUP, because CLEANUP's own allowlist legitimately touches paths PLAN never declared.
54
+ 4. **Spec literal verification + risk probes**: `python3 "$DEVLYN_SHARED_DIR/spec-verify-check.py" --include-risk-probes`. The script routes every literal command and risk probe through `process-evidence.py`, preserving exact command, outcome, raw stdout/stderr, and digests in the BUILD_GATE run/round manifest. It self-stages from sibling `spec.expected.json` next to `state.source.spec_path`, or the legacy inline carrier when the sibling is absent; benchmark-prestaged `.devlyn/spec-verify.json` still wins. It appends `.devlyn/risk-probes.jsonl` when present, and requires that file when `state.risk_profile.risk_probes_enabled == true`. Malformed `state.risk_profile` is also CRITICAL because it can hide enabled risk probes. When probes are enabled or `.devlyn/risk-probes.jsonl` exists, missing or mismatched top-level `state.risk_probes_digest` emits `correctness.risk-probe-integrity` CRITICAL; the digest binds `.devlyn/risk-probes.jsonl` plus referenced `.devlyn/probes/` script bytes. Command or risk-probe mismatch → CRITICAL finding. Missing required risk probes, missing/malformed generated carrier, or malformed sibling expected file → `correctness.spec-verify-malformed` CRITICAL. The same invocation also enforces PLAN's declared `authorized_surface` (the json block under `.devlyn/plan.md`'s `<!-- devlyn:authorized-surface -->` section, located by that sentinel regardless of the heading's language) against this run's diff and untracked delta: any changed path or created-during-run untracked path outside the declared surface → `scope.out-of-scope-file` CRITICAL (`.devlyn/` exempt) — the only sanctioned fix is removing the file, never widening the declared surface, since that would let the same worker that leaked scope self-authorize the leak; missing sentinel, malformed `plan.md`/surface block, or missing `.devlyn/untracked.baseline` → `scope.authorized-surface-malformed` CRITICAL. This scope check runs only here (BUILD_GATE), not when VERIFY MECHANICAL reuses this script post-CLEANUP, while owner CLEANUP separately enforces unchanged tracked source and the final untracked baseline. Historical worker CLEANUP retains its existing allowlist.
55
55
  5. **Browser** (only when diff touches `*.tsx`, `*.jsx`, `*.vue`, `*.svelte`, `page.*`, `layout.*`, `route.*`, `*.css`, `*.html`): start the dev server and run the repo's existing browser checks, or a minimal curl/HTML check when no browser test harness exists. Each failed check → finding, severity `HIGH`, rule `correctness.browser-flow-failed`.
56
56
 
57
57
  Append all findings; do not stop on the first failure.
@@ -59,6 +59,7 @@ Append all findings; do not stop on the first failure.
59
59
 
60
60
  <output>
61
61
  - `.devlyn/build_gate.findings.jsonl` — JSONL stream, one finding per line. Schema: `{id, rule_id, severity, file, line, message, fix_hint, criterion_ref}`.
62
+ - A completed checker preflight rejection writes `spec-verify.results.json` with `preflight_failure` and the original CRITICAL finding's path/digest. Retain that source (default `.devlyn/spec-verify-findings.jsonl`) and append its finding to `.devlyn/build_gate.findings.jsonl`. Earlier gate observations remain bound, but no spec command is claimed to have run. An absent results file still follows the interrupted-observation `BLOCKED` path.
62
63
  - `.devlyn/build_gate.log.md` — human-readable gate results with raw output or links to complete raw-output files, using sealed artifacts where available. Retain complete inventories/hash maps in `.devlyn/` files; report their paths, digests and relevant deltas instead of dumping them into the conversation.
63
64
  - Report `PASS` if zero CRITICAL/HIGH findings, `FAIL` for genuine product findings, or `BLOCKED:build-env-underprovisioned` only for the evidence-backed capability path above. A capability blocker leaves no product finding. Do not edit `pipeline.state.json` yourself — the orchestrator records it via `state-phase-write.py` from these artifacts.
64
65
  </output>
@@ -1,19 +1,14 @@
1
1
  # PHASE 4 — CLEANUP (canonical body)
2
2
 
3
- Per-engine adapter header is prepended at runtime. Task-scoped pass — only what this diff introduced or invalidated.
4
-
5
- <role>
6
- Remove tooling artifacts, dead code added by this diff, and doc references invalidated by this diff. The cleanup is bounded by an allowlist enforced post-spawn.
7
- </role>
3
+ The selected IMPLEMENT worker performs code/doc cleanup in its existing invocation before its final checks. The owner then uses the CLEANUP span for generated-artifact removal and invariant checks only; no new model call.
8
4
 
9
5
  <input>
10
- - Cumulative diff since `state.base_ref.sha`.
11
- - Spec at `state.source.spec_path` or `state.source.criteria_path`.
12
- - `state.phases.cleanup.pre_sha` (the orchestrator captured this before spawn — your post-cleanup diff against this SHA must stay within the allowlist).
6
+ - Cumulative diff, original spec and immutable PLAN authorized surface.
7
+ - CLEANUP `pre_sha`: tracked source and HEAD must remain unchanged during owner checks.
13
8
  </input>
14
9
 
15
10
  <allowlist>
16
- You may modify or delete:
11
+ Within IMPLEMENT and PLAN authorization, you may modify or delete:
17
12
 
18
13
  1. **Tooling artifacts** the spec did not list as deliverables: `test-results/`, `playwright-report/`, `.last-run.json`, coverage HTML output, build artifacts, runtime caches (`__pycache__/`, `*.pyc`, `.cache/`).
19
14
  2. **Dead code added by this diff** — symbols (functions, classes, types, exports) introduced by this diff that no other code added by this diff references AND that are not part of the spec's required surface. Pre-existing dead code is out of scope.
@@ -24,8 +19,7 @@ Files outside this allowlist must not change. Pre-existing tooling leaks (alread
24
19
  </allowlist>
25
20
 
26
21
  <output>
27
- - Code changes within the allowlist.
28
- - Report your verdict in this reply: `PASS` if changes within allowlist (or no changes needed); `FAIL` if you cannot complete within the allowlist (the orchestrator will revert). Do not edit `pipeline.state.json` yourself — the orchestrator records it via `state-phase-write.py`.
22
+ The owner removes only run-owned untracked/ignored generated artifacts from item 1, preserving deliverables, evidence and recovery inputs. It records PASS when the source checkpoint is unchanged and no cleanup finding remains. A code/doc finding returns through the selected IMPLEMENT repair route and reruns BUILD before fresh VERIFY. Never modify source inside owner CLEANUP or reuse stale checks. Record lifecycle via `state-phase-write.py`.
29
23
  </output>
30
24
 
31
25
  <quality_bar>
@@ -14,6 +14,7 @@ You execute the plan. Constrained design judgment within PLAN's invariants — w
14
14
 
15
15
  <output>
16
16
  - Code changes implementing every Requirement. Verify with `git diff`.
17
+ - In this same selected worker invocation, remove dead code/comments introduced by this diff and repair references it invalidated, within PLAN authorization and `references/phases/cleanup.md`. Finish before the final checks/checkpoint; no separate cleanup model is dispatched.
17
18
  - Tests added or updated for changed behavior. Run the focused development tests needed to establish that behavior; BUILD_GATE and VERIFY own the post-implementation full suite.
18
19
  - For every sibling `spec.expected.json.process_evidence[]` item whose `phase` is `implement`, run `python3 "$DEVLYN_SHARED_DIR/process-evidence.py" --devlyn-dir .devlyn run --phase implement --id '<id>'`. The runner must report `expectation_met: true`; cite its manifest path in the phase reply.
19
20
  - For each criterion satisfied, set `state.criteria[i].status: "implemented"` with an `evidence` record `{"file": "...", "line": N, "note": "brief"}`.
@@ -1,6 +1,6 @@
1
1
  # PHASE 1 — PLAN (canonical body)
2
2
 
3
- The per-engine adapter header from `_shared/adapters/<engine>.md` is prepended at runtime. This file is engine-agnostic.
3
+ The orchestrator reads this body in its existing context; no PLAN worker is dispatched.
4
4
 
5
5
  <role>
6
6
  You translate a spec or generated criteria into a concrete plan: the file list to touch, the risks the implementation must navigate, and a verbatim restatement of what acceptance requires. The plan is the contract IMPLEMENT executes against.
@@ -20,14 +20,14 @@ Write `.devlyn/plan.md` with three sections:
20
20
  3. **Acceptance restatement** — verbatim copy of the spec's `## Verification` block (or generated criteria's equivalent). The plan is wrong if any verification command later fails because of a planning oversight.
21
21
  4. **Execution phases** — conditional; large work only. Emit this section ONLY when ALL hold: (a) spec frontmatter `complexity: high` (legacy `large`) or `state.complexity == "large"`; (b) the work spans multiple subsystems or more than ~8 files; (c) every phase boundary has at least one runnable gate command. Otherwise emit exactly the three sections above — when in doubt, do not decompose. When emitted: 2-5 `### Phase <k> — <title>` blocks, each with task checkboxes (`- [ ]`), a `gate:` line (1-2 commands, exit-code truth, runnable at that boundary), and the files it owns (a subset of section 1). Definitions are written once here and are the contract; the orchestrator later updates checkboxes as a display mirror only — routing truth lives in `pipeline.state.json`.
22
22
 
23
- Report your verdict in this reply: `PASS` if plan is shippable; `BLOCKED` if spec is internally contradictory or cannot be planned without violating constraints. Do not edit `pipeline.state.json` yourself — the orchestrator records it via `state-phase-write.py`.
23
+ Record via `state-phase-write.py`: `PASS` if plan is shippable; `BLOCKED` if spec is internally contradictory or cannot be planned without violating constraints. Never hand-edit lifecycle fields.
24
24
  </output>
25
25
 
26
26
  <quality_bar>
27
27
  - Scope first, then implementation. Decide what files to touch before deciding how to implement. Files not in the list are off-limits to IMPLEMENT.
28
28
  - Tooling artifacts and reporter output are not deliverables unless the spec lists them. Plan to configure tools to emit to gitignored paths.
29
29
  - Existing tests are contract. Plan to extend them; do not plan to remove or weaken them.
30
- - Spec frontmatter is read-only to PLAN and IMPLEMENT. The DOCS-style status flip happens in CLEANUP under a tight allowlist.
30
+ - Spec frontmatter is read-only; cleanup cannot broaden the task contract.
31
31
  - If a Requirement says "match the literal output X", restate the literal in the plan. Paraphrasing the contract here propagates into IMPLEMENT.
32
32
  </quality_bar>
33
33