devlyn-cli 2.8.0 → 2.9.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.
- package/AGENTS.md +6 -2
- package/CLAUDE.md +7 -1
- package/README.md +14 -2
- package/config/skills/_shared/adapters/claude.md +6 -3
- package/config/skills/_shared/archive_run.py +25 -1
- package/config/skills/_shared/codex-config.md +1 -0
- package/config/skills/_shared/finish-gate.py +517 -0
- package/config/skills/_shared/run-bounded.py +44 -0
- package/config/skills/_shared/spec-verify-check.py +716 -93
- package/config/skills/_shared/state-phase-write.py +113 -15
- package/config/skills/_shared/verify-merge-findings.py +197 -1
- package/config/skills/devlyn:resolve/SKILL.md +37 -20
- package/config/skills/devlyn:resolve/references/free-form-mode.md +7 -7
- package/config/skills/devlyn:resolve/references/phases/build-gate.md +1 -1
- package/config/skills/devlyn:resolve/references/phases/probe-derive.md +14 -3
- package/config/skills/devlyn:resolve/references/phases/verify.md +19 -4
- package/config/skills/devlyn:resolve/references/state-schema.md +16 -9
- package/package.json +1 -1
- package/scripts/lint-skills.sh +1 -1
|
@@ -52,7 +52,7 @@ Each phase routes to an engine and prepends the per-engine adapter header from `
|
|
|
52
52
|
|
|
53
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
54
|
- Claude Code phases: spawn `Agent` (`mode: "bypassPermissions"`); prompt = adapter-header + canonical-body + task-context.
|
|
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. The wrapper closes stdin and emits a heartbeat. No MCP.
|
|
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. The wrapper closes stdin and emits a heartbeat. 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
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
57
|
- Default engine: Claude for PLAN / IMPLEMENT / BUILD_GATE / CLEANUP when the orchestrator has Claude Code's native `Agent` primitive; otherwise the default route is the current CLI's fresh worker (`codex` child process for Codex CLI, native `task` for oh-my-pi). Role resolution (`_shared/engine-preflight.md#role-resolution`): explicit `--engine` flag > machine-local `.devlyn/engines.json` `executor` pin > orchestrator-supported default above. Explicit routes and pins still fail closed when their required engine/spawn channel is unavailable. Pair-mode is conditional-default in VERIFY/JUDGE and selects the OTHER engine for the fresh worker when the trigger policy fires — OTHER = first adapter-valid, non-primary, available entry of `engines.json` `pair_judge_priority`, or the binary claude↔codex complement when unset.
|
|
58
58
|
- Multi-LLM evolution: when a new model adapter ships in `_shared/adapters/`, that engine becomes selectable via `--engine <model>` without further skill changes (NORTH-STAR.md "Multi-LLM evolution direction").
|
|
@@ -61,7 +61,7 @@ Each phase routes to an engine and prepends the per-engine adapter header from `
|
|
|
61
61
|
<modes>
|
|
62
62
|
Three input shapes:
|
|
63
63
|
|
|
64
|
-
1. **Free-form**: `/devlyn:resolve "fix the login bug"` (inline goal) or `/devlyn:resolve --goal-file <path>` (goal text read from a file — the devlynd `ResolveAdapter` launcher path, injection-safe). PHASE 0 runs the complexity classifier and either proceeds with an internal mini-spec (trivial), drafts focused questions for in-prompt resolution (medium), or
|
|
64
|
+
1. **Free-form**: `/devlyn:resolve "fix the login bug"` (inline goal) or `/devlyn:resolve --goal-file <path>` (goal text read from a file — the devlynd `ResolveAdapter` launcher path, injection-safe). PHASE 0 runs the complexity classifier and either proceeds with an internal mini-spec (trivial), drafts focused questions for in-prompt resolution (medium), or synthesizes a best-effort spec with a logged `## Assumptions` block (large; zero-scope-signal goals halt). No mid-pipeline prompts in any branch.
|
|
65
65
|
2. **Spec**: `/devlyn:resolve --spec docs/roadmap/phase-N/X.md`. Spec is read-only. Stage verification commands from sibling `spec.expected.json`; if absent, use the legacy `## Verification` JSON block.
|
|
66
66
|
3. **Verify-only**: `/devlyn:resolve --verify-only <diff-or-PR-ref> --spec <path>`. Skips PHASE 1-4. Runs PHASE 5 (VERIFY) on the supplied diff against the spec.
|
|
67
67
|
</modes>
|
|
@@ -94,20 +94,22 @@ Once `state.implement_passed_sha` is non-null (PHASE 2 returned and produced a d
|
|
|
94
94
|
|
|
95
95
|
3. Initialize `.devlyn/pipeline.state.json` per `references/state-schema.md`. Set `state.run_id`, `started_at`, `engine`, `engine_source` (`"flag"` | `"engines.json"` | `"default"`), `pair_verify: true` only when `--pair-verify` was passed and `false` otherwise, `base_ref.{branch, sha}`, `rounds.{max_rounds, global: 0}`, `bypasses`, empty `phases`, empty `criteria`, and `risk_profile: { high_risk: false, reasons: [], risk_probes_enabled: false, pair_default_enabled: true }`. `risk_profile` is strict typed state: keep it an object, keep the three flags as JSON booleans, and keep `reasons` as a string array; never serialize booleans as strings.
|
|
96
96
|
|
|
97
|
-
4.
|
|
98
|
-
|
|
97
|
+
4. Write `.devlyn/untracked.baseline`: `python3 "$DEVLYN_SHARED_DIR/spec-verify-check.py" --write-untracked-baseline`.
|
|
98
|
+
|
|
99
|
+
5. **Mode-specific init**:
|
|
100
|
+
- **Free-form**: first resolve `goal_text` — the inline positional goal, OR (when `--goal-file <path>` was passed) the raw UTF-8 content of `<path>`, read as opaque goal text (never re-parsed as flags). Fail closed before classifying: empty/whitespace-only file → `BLOCKED:goal-file-empty`; missing/unreadable/not-a-regular-file/invalid-UTF-8 → `BLOCKED:goal-file-unreadable`; an absolute path, a `..` segment, or a path that canonicalizes outside cwd → `BLOCKED:goal-file-invalid-path`. (Like all PHASE-0 halts these are report-level `BLOCKED:<reason>` strings — no `phases.*.verdict` is written, so the bare-enum carrier rule is never violated.) Then read `references/free-form-mode.md` and run the complexity classifier deterministically on `goal_text` (rules over keyword density / file count / spec-shape signals, plus pair-evidence intent). Set `state.complexity ∈ {trivial, medium, large}`. Trivial: write internal mini-spec to `.devlyn/criteria.generated.md` and proceed. Medium: synthesize a minimal spec from the goal + add 1-2 context anchors from the codebase, write to `.devlyn/criteria.generated.md`, proceed. Every free-form branch that writes criteria must set `state.source.type = "generated"`, `state.source.criteria_path = ".devlyn/criteria.generated.md"`, and `state.source.criteria_sha256` from the raw file bytes. Large: follow the Large action in `references/free-form-mode.md` — synthesize a best-effort spec with an `## Assumptions` block, log `recommend: /devlyn:ideate first`, proceed, and flag every assumption in the final report; zero-scope-signal goals halt `BLOCKED:large-needs-ideation`; pair-evidence intent without an actionable solo-headroom hypothesis must halt with `BLOCKED:solo-headroom-hypothesis-required`, and unmeasured pair-candidate intent without solo ceiling avoidance must halt with `BLOCKED:solo-ceiling-avoidance-required`.
|
|
99
101
|
- **Spec**: validate spec exists. If sibling `spec.expected.json` exists, run `--check-expected <expected-path>` to validate both the expected contract, 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`. Then stage `.devlyn/spec-verify.json` from `verification_commands`. Otherwise run `--check <spec-path>` to validate the legacy inline carrier plus supported `complexity` frontmatter and any present actionable solo-headroom hypothesis; if the spec uses an inline `## Verification` JSON carrier, any solo-headroom hypothesis command must match that carrier's `verification_commands[].cmd`. Then stage from the legacy inline carrier. Compute `state.source.spec_sha256`.
|
|
100
102
|
- **Verify-only**: skip to PHASE 5 with `state.source.spec_path` set, the supplied diff captured at `.devlyn/external-diff.patch`.
|
|
101
103
|
|
|
102
|
-
|
|
104
|
+
6. Compute `state.risk_profile` from the user goal plus spec/criteria text. Mark `high_risk: true` when the work touches any of: auth/authz, permissions, security, token/session, payment/money/billing/invoice/pricing/tax/ledger, persistence/data mutation/deletion/migration, idempotency/replay/duplicate, API/webhook/raw-body/signature, allocation/scheduling/inventory/rollback/transaction, or explicit error-priority/output-shape contracts. Explicit `--risk-probes` sets `risk_probes_enabled: true` unconditionally (the OTHER engine is then a promise; unavailability blocks at PHASE 1.5). If high-risk is detected and `--no-risk-probes` is absent, this is an AUTO escalation: set `risk_probes_enabled: true` only when a pair-judge engine is available per role resolution (first adapter-valid, non-primary, available entry of `pair_judge_priority`; binary complement when unset — for a Claude run without a config, `command -v codex`); if none is available, leave `risk_probes_enabled: false`, keep `high_risk: true`, and append a reason such as `auto-risk-probes skipped: codex-unavailable` so the report shows it. `--no-risk-probes` disables only the auto escalation; a single-engine run does not need it to proceed. If `--no-pair` is present, set `pair_default_enabled: false`. Add concise string reasons for the classification, but do not use reasons as substitutes for the boolean fields.
|
|
103
105
|
|
|
104
|
-
|
|
106
|
+
7. Announce one line: `resolve starting — run <run_id> — engine <engine> — mode <mode> — complexity <complexity-or-na> — pair <conditional|disabled> — risk_probes <on|off>`.
|
|
105
107
|
|
|
106
108
|
## PHASE 1: PLAN
|
|
107
109
|
|
|
108
110
|
Skip in verify-only mode. The heaviest phase by design — spec/criteria define non-negotiable invariants; plan formalizes how the implementation hits them.
|
|
109
111
|
|
|
110
|
-
Engine: Claude (PLAN-pair is **
|
|
112
|
+
Engine: Claude (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). Prompt body: `references/phases/plan.md`.
|
|
111
113
|
|
|
112
114
|
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.
|
|
113
115
|
|
|
@@ -199,7 +201,19 @@ After return:
|
|
|
199
201
|
1. Run `python3 "$DEVLYN_SHARED_DIR/spec-verify-check.py" --validate-risk-probes`
|
|
200
202
|
for the artifact boundary before IMPLEMENT; malformed probes halt with
|
|
201
203
|
`BLOCKED:probe-derive-malformed`.
|
|
202
|
-
2.
|
|
204
|
+
2. Compute `python3 "$DEVLYN_SHARED_DIR/spec-verify-check.py" --print-risk-probes-digest` and write the result to top-level `state.risk_probes_digest`:
|
|
205
|
+
```bash
|
|
206
|
+
RISK_PROBES_DIGEST="$(python3 "$DEVLYN_SHARED_DIR/spec-verify-check.py" --print-risk-probes-digest)"
|
|
207
|
+
python3 - "$RISK_PROBES_DIGEST" <<'PY'
|
|
208
|
+
import json, pathlib, sys
|
|
209
|
+
path = pathlib.Path(".devlyn/pipeline.state.json")
|
|
210
|
+
state = json.loads(path.read_text())
|
|
211
|
+
state["risk_probes_digest"] = sys.argv[1]
|
|
212
|
+
path.write_text(json.dumps(state, indent=2) + "\n")
|
|
213
|
+
PY
|
|
214
|
+
```
|
|
215
|
+
Any later legitimate probe regeneration is orchestrator-only and repeats validate plus digest-write.
|
|
216
|
+
3. IMPLEMENT receives `.devlyn/plan.md` plus `.devlyn/risk-probes.jsonl` as
|
|
203
217
|
concrete acceptance obligations. It must not receive the producer engine's
|
|
204
218
|
commentary or any mention of pair/critic/debate.
|
|
205
219
|
|
|
@@ -214,12 +228,12 @@ State write: `phases.implement.{started_at, verdict, completed_at, duration_ms}`
|
|
|
214
228
|
**Single-phase path** (plan.md has no `## Execution phases` section — every trivial/medium run, and any large run PLAN judged atomic): spawn IMPLEMENT once. No phase metadata of any kind appears in the prompt. After return:
|
|
215
229
|
1. `git diff --stat` — empty diff → halt with `BLOCKED:implement-empty`.
|
|
216
230
|
2. Set `state.implement_passed_sha = git rev-parse HEAD` (activates `<post_implement_invariant>`).
|
|
217
|
-
3. Checkpoint: `git add -
|
|
231
|
+
3. Checkpoint (**scoped staging** — this exact shape everywhere a pipeline commit is made): `bash -o pipefail -c 'python3 "$DEVLYN_SHARED_DIR/spec-verify-check.py" --print-authorized-surface | git add --pathspec-from-file=- --pathspec-file-nul' && git commit -m "chore(pipeline): implement"`.
|
|
218
232
|
|
|
219
233
|
**Phase-gated path** (plan.md has `## Execution phases` with >1 phase): definitions are the contract in plan.md; progress is routing truth in `state.phases.implement.exec = { total, current, statuses, commits }` — never route on plan.md checkbox parsing. For each phase k = 1..N:
|
|
220
234
|
1. Spawn IMPLEMENT with the standard prompt plus: this phase's plan.md block only, the current worktree as the working base (overrides the body's `base_ref.sha` framing after phase 1), a `git diff <base_ref.sha>...HEAD --stat` summary, and the prior phase's gate output.
|
|
221
235
|
2. After return: run the phase's `gate:` commands directly — deterministic, exit-code truth, no LLM judgment.
|
|
222
|
-
3. Gate PASS →
|
|
236
|
+
3. Gate PASS → scoped-staging checkpoint with message `chore(pipeline): implement phase <k>/<N>`, write `exec.statuses[k-1] = "PASS"` + commit sha, advance `exec.current`, tick the plan.md checkbox mirror (display only).
|
|
223
237
|
4. Gate FAIL → no commit. One fix respawn for this phase with the gate output (increments `rounds.global`, shares `max_rounds`); second FAIL → `exec.statuses[k-1] = "FAIL"`, halt with `BLOCKED:phase-gate-exhausted`.
|
|
224
238
|
|
|
225
239
|
After the final phase's gate PASS: `git diff <base_ref.sha>...HEAD --stat` — empty → halt with `BLOCKED:implement-empty`; otherwise set `state.implement_passed_sha = git rev-parse HEAD` (phase commits already checkpoint the work — no extra commit).
|
|
@@ -239,7 +253,7 @@ State write: `phases.build_gate.{started_at, verdict, completed_at, duration_ms,
|
|
|
239
253
|
|
|
240
254
|
Branch:
|
|
241
255
|
- `PASS` → PHASE 4.
|
|
242
|
-
- `FAIL` → fix loop. Spawn IMPLEMENT-engine agent with the build_gate findings as input. Increment `state.rounds.global`. On second FAIL with `state.rounds.global >= state.rounds.max_rounds` → halt with verdict `BLOCKED:build-gate-exhausted`.
|
|
256
|
+
- `FAIL` → fix loop. Spawn IMPLEMENT-engine agent with the build_gate findings as input. Increment `state.rounds.global`. After a fix-loop IMPLEMENT return, scoped-staging checkpoint with message `chore(pipeline): implement fix round <n>`. On second FAIL with `state.rounds.global >= state.rounds.max_rounds` → halt with verdict `BLOCKED:build-gate-exhausted`.
|
|
243
257
|
|
|
244
258
|
## PHASE 4: CLEANUP
|
|
245
259
|
|
|
@@ -252,11 +266,12 @@ Engine: per `--engine`. Prompt body: `references/phases/cleanup.md`. Allowlist e
|
|
|
252
266
|
|
|
253
267
|
Before spawn: pass `--pre-sha "$(git rev-parse HEAD)"` to the `state-phase-write.py ... spawn` call for this phase.
|
|
254
268
|
|
|
255
|
-
State write: `phases.cleanup.{started_at, verdict, completed_at, duration_ms}`.
|
|
269
|
+
State write: `phases.cleanup.{started_at, verdict, completed_at, duration_ms, pre_sha, post_sha}`.
|
|
256
270
|
|
|
257
271
|
After return:
|
|
258
|
-
1. Run `git diff --name-only <pre_sha>` — any path outside the cleanup allowlist → revert to `pre_sha
|
|
259
|
-
2. If allowlist honored and diff non-empty: `git add -
|
|
272
|
+
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`.
|
|
273
|
+
2. If allowlist honored and diff non-empty: `git add -u && git commit -m "chore(pipeline): cleanup"`.
|
|
274
|
+
3. Complete cleanup with `--post-sha "$(git rev-parse HEAD)"` (also when the diff was empty).
|
|
260
275
|
|
|
261
276
|
## PHASE 5: VERIFY (fresh subagent, findings-only)
|
|
262
277
|
|
|
@@ -300,7 +315,7 @@ failed availability check evidence, and print setup guidance:
|
|
|
300
315
|
- Claude: install/configure Claude Code, run `claude --version` when available,
|
|
301
316
|
confirm the host can spawn Claude agents, then rerun.
|
|
302
317
|
|
|
303
|
-
Findings written to `.devlyn/verify.findings.jsonl`. **VERIFY agents have no code-mutation tools.** Codex pair-JUDGE is read-only: invoke `codex-monitored.sh` with `CODEX_MONITORED_ISOLATED=1` and `-c model_reasoning_effort=medium`, no `tail`/`head`/`grep` pipes, direct stdout/stderr capture, JSONL findings on stdout, and orchestrator-written `.devlyn/verify.pair.findings.jsonl`. Isolation blocks user config, AGENTS.md, hooks, and project rules from hidden context/tool/transcript side effects. If stdout is captured as `.devlyn/codex-judge.stdout`, run `python3 "$DEVLYN_SHARED_DIR/collect-codex-findings.py"` before merge; raw stdout is diagnostic only. If stdout contains findings or a non-PASS summary while `.devlyn/verify.pair.findings.jsonl` is empty, `verify-merge-findings.py` blocks VERIFY for `verify.pair.emission-contract`. Do not ask Codex to `apply_patch` or edit `.devlyn`. When the OTHER engine is Claude (codex/omp orchestrator), the judge call
|
|
318
|
+
Findings written to `.devlyn/verify.findings.jsonl`. **VERIFY agents have no code-mutation tools.** Codex pair-JUDGE is read-only: invoke `codex-monitored.sh` with `CODEX_MONITORED_ISOLATED=1 CODEX_MONITORED_TIMEOUT_SEC=600` and `-c model_reasoning_effort=medium`, no `tail`/`head`/`grep` pipes, direct stdout/stderr capture, JSONL findings on stdout, and orchestrator-written `.devlyn/verify.pair.findings.jsonl`. Isolation blocks user config, AGENTS.md, hooks, and project rules from hidden context/tool/transcript side effects. If stdout is captured as `.devlyn/codex-judge.stdout`, run `python3 "$DEVLYN_SHARED_DIR/collect-codex-findings.py"` before merge; raw stdout is diagnostic only. If stdout contains findings or a non-PASS summary while `.devlyn/verify.pair.findings.jsonl` is empty, `verify-merge-findings.py` blocks VERIFY for `verify.pair.emission-contract`. Do not ask Codex to `apply_patch` or edit `.devlyn`. When the OTHER engine is Claude (codex/omp orchestrator), the judge call is wrapped as `python3 "$DEVLYN_SHARED_DIR/run-bounded.py" 600 -- claude -p ...` with the same flags as `_shared/adapters/claude.md` `## Invocation`, stdout captured as `.devlyn/claude-judge.stdout`, and the same emission contract applied to that file. On judge subprocess exit 124, the orchestrator writes `.devlyn/verify.pair.timeout.json` (`{"engine": "<codex|claude>", "budget_seconds": 600}`) before running `verify-merge-findings.py`. After primary and pair findings are written, run `python3 "$DEVLYN_SHARED_DIR/verify-merge-findings.py" --write-state`. Branch only on the merged `state.phases.verify.verdict`; a HIGH/CRITICAL finding from either judge must mechanically become `NEEDS_WORK`. Never write `.devlyn/verify-merged.findings.jsonl` or `.devlyn/verify-merge.summary.json` by hand; `verify-merge-findings.py` is their only writer. State write: `phases.verify.{started_at, verdict, completed_at, duration_ms, sub_verdicts: {mechanical, judge, pair_judge?}, artifacts}`.
|
|
304
319
|
|
|
305
320
|
Branch:
|
|
306
321
|
- `PASS` → PHASE 6.
|
|
@@ -311,15 +326,17 @@ Branch:
|
|
|
311
326
|
|
|
312
327
|
State write: `phases.final_report.started_at` at the top of this phase.
|
|
313
328
|
|
|
314
|
-
1.
|
|
329
|
+
1. Kill any dev server PHASE 3 left running.
|
|
330
|
+
|
|
331
|
+
2. **FINISH GATE** — run `python3 "$DEVLYN_SHARED_DIR/finish-gate.py"`; branch only on exit code plus `.devlyn/finish-gate.findings.jsonl` existence: 0 without the file → clean; 0 with it → terminal verdict floors at `PASS_WITH_ISSUES` (report lists the reverted paths); 1 or 2 → `BLOCKED:finish-gate-unclean`.
|
|
315
332
|
|
|
316
|
-
|
|
333
|
+
3. **Terminal verdict** — derive from `state.phases.{plan, implement, 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`.
|
|
317
334
|
|
|
318
|
-
|
|
335
|
+
4. **Render report** — sections: header (run_id, engine, mode, verdict, wall-time), per-phase summary, pair/risk-probe status, findings table (verify + finish-gate findings), follow-up notes (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`).
|
|
319
336
|
|
|
320
|
-
|
|
337
|
+
5. State write: `phases.final_report.{verdict, completed_at, duration_ms}` BEFORE archive runs (archive prune logic skips runs whose `final_report.verdict` is null).
|
|
321
338
|
|
|
322
|
-
|
|
339
|
+
6. **Archive** — invoke the deterministic script: `python3 "$DEVLYN_SHARED_DIR/archive_run.py"`. The script reads `run_id` from `.devlyn/pipeline.state.json`, moves the per-run artifact set (`PER_RUN_PATTERNS` in the script is the single source of truth) into `.devlyn/runs/<run_id>/`, then best-effort prunes to last 10 completed runs. Archive must run; running this step as deterministic-script-not-prose ensures the move actually happens (iter-0033a Smoke 3 caught a case where the agent claimed archive ran without moving the files).
|
|
323
340
|
|
|
324
341
|
## State management
|
|
325
342
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Free-form mode — complexity classifier
|
|
2
2
|
|
|
3
|
-
When `/devlyn:resolve` is invoked with a free-form goal (no `--spec`) — whether the goal is the inline positional text or the content of `--goal-file <path>` (PHASE 0 resolves `goal_text` from either source before classifying) — PHASE 0 runs this classifier to set `state.complexity ∈ {trivial, medium, large}` and either proceeds with an internal mini-spec, drafts focused questions for in-prompt resolution, or
|
|
3
|
+
When `/devlyn:resolve` is invoked with a free-form goal (no `--spec`) — whether the goal is the inline positional text or the content of `--goal-file <path>` (PHASE 0 resolves `goal_text` from either source before classifying) — PHASE 0 runs this classifier to set `state.complexity ∈ {trivial, medium, large}` and either proceeds with an internal mini-spec, drafts focused questions for in-prompt resolution, synthesizes a best-effort spec with logged assumptions, or halts zero-scope-signal goals with `/devlyn:ideate` guidance.
|
|
4
4
|
|
|
5
5
|
The classifier is rules-based / deterministic — not an LLM judgment call. Decision rules below.
|
|
6
6
|
|
|
@@ -57,11 +57,11 @@ Conditions (any one):
|
|
|
57
57
|
- `pair_evidence_intent == true` and `has_actionable_solo_headroom == false`.
|
|
58
58
|
- `unmeasured_pair_candidate_intent == true` and `has_solo_ceiling_avoidance == false`.
|
|
59
59
|
|
|
60
|
-
Action:
|
|
61
|
-
- Default:
|
|
62
|
-
-
|
|
63
|
-
- Exception: if the large classification came from pair-evidence intent without an actionable solo-headroom hypothesis, halt with `BLOCKED:solo-headroom-hypothesis-required
|
|
64
|
-
- Exception: if the large classification came from unmeasured pair-candidate intent without solo ceiling avoidance, halt with `BLOCKED:solo-ceiling-avoidance-required
|
|
60
|
+
Action:
|
|
61
|
+
- Default: synthesize a best-effort spec from the goal with an explicit `## Assumptions` block (every assumption scope-narrowing and reversible — when in doubt, narrower); log `recommend: /devlyn:ideate first` in `.devlyn/criteria.generated.md` AND the final report; proceed to PHASE 1; the final report flags every assumption for user review.
|
|
62
|
+
- Zero-signal exception: if the large classification includes `file_scope_signals == 0` (classifier cannot pick scope), halt with terminal verdict `BLOCKED:large-needs-ideation` — assumptions there would be scope-invention, not narrowing.
|
|
63
|
+
- Exception: if the large classification came from pair-evidence intent without an actionable solo-headroom hypothesis, halt with `BLOCKED:solo-headroom-hypothesis-required`. Do not invent a hypothesis; recommend `/devlyn:ideate` so the user can supply the visible behavior `solo_claude` is expected to miss.
|
|
64
|
+
- Exception: if the large classification came from unmeasured pair-candidate intent without solo ceiling avoidance, halt with `BLOCKED:solo-ceiling-avoidance-required`. Do not invent the note; recommend `/devlyn:ideate` so the user can supply the concrete difference from rejected or solo-saturated controls such as `S2`-`S6`.
|
|
65
65
|
|
|
66
66
|
## Anti-pattern: drift to LLM judgment
|
|
67
67
|
|
|
@@ -71,7 +71,7 @@ When the rules are silent (rare — pathological goal text), default to `medium`
|
|
|
71
71
|
|
|
72
72
|
## Mini-spec quality bar
|
|
73
73
|
|
|
74
|
-
The internal mini-spec written for trivial / medium /
|
|
74
|
+
The internal mini-spec written for trivial / medium / large-assumptions paths must satisfy:
|
|
75
75
|
|
|
76
76
|
- `## Requirements` non-empty, each bullet testable (CLI command, test command, observable file change).
|
|
77
77
|
- `## 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").
|
|
@@ -22,7 +22,7 @@ Run in this order; each emits findings into `.devlyn/build_gate.findings.jsonl`:
|
|
|
22
22
|
1. **Type check** (TypeScript / mypy / etc.). Each error → one finding, severity `HIGH`, rule `correctness.type-check`.
|
|
23
23
|
2. **Lint** (eslint / ruff / clippy / etc.). Each error → finding, severity `MEDIUM`, rule `quality.lint`. Warnings stay LOW unless the spec elevates them.
|
|
24
24
|
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.
|
|
25
|
-
4. **Spec literal verification + risk probes**: `python3 "$DEVLYN_SHARED_DIR/spec-verify-check.py" --include-risk-probes`. The script 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. 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: any changed path outside the declared surface → `scope.out-of-scope-file` CRITICAL — the only sanctioned fix is removing the file
|
|
25
|
+
4. **Spec literal verification + risk probes**: `python3 "$DEVLYN_SHARED_DIR/spec-verify-check.py" --include-risk-probes`. The script 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.
|
|
26
26
|
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`.
|
|
27
27
|
|
|
28
28
|
Append all findings; do not stop on the first failure.
|
|
@@ -42,6 +42,12 @@ When a batch/import operation must be all-or-nothing, the probe must exercise a
|
|
|
42
42
|
mixed valid/invalid batch and prove the externally-visible state is unchanged
|
|
43
43
|
after the failure.
|
|
44
44
|
|
|
45
|
+
Do not modify tracked files, and do not pre-implement the candidate fix to prove
|
|
46
|
+
post-fix success. Validate each probe by executing it against `state.base_ref.sha`
|
|
47
|
+
and classify the base outcome: bug-exposing probes fail on base for the asserted
|
|
48
|
+
reason, while preservation/regression probes may pass on base when they exercise
|
|
49
|
+
unchanged behavior.
|
|
50
|
+
|
|
45
51
|
If the visible spec includes a solo-headroom hypothesis, the first probe must
|
|
46
52
|
target that hypothesis: use the visible command/input shape it names, exercise
|
|
47
53
|
the behavior the spec says `solo_claude` is expected to miss, and assert the
|
|
@@ -105,9 +111,10 @@ must request the same scarce first-line SKU so the probe proves rollback by
|
|
|
105
111
|
observable success, not by internal reasoning.
|
|
106
112
|
|
|
107
113
|
Each probe must run entirely from the worktree with standard shell/Node/Python
|
|
108
|
-
tools already present in the repo.
|
|
109
|
-
|
|
110
|
-
APIs or write to external
|
|
114
|
+
tools already present in the repo. For multiline probes, write a script under
|
|
115
|
+
`.devlyn/probes/<id>.<ext>` and set `cmd` to invoke it (rules below). Probe
|
|
116
|
+
commands must not call external network APIs or write to external
|
|
117
|
+
memory/telemetry services.
|
|
111
118
|
</task>
|
|
112
119
|
|
|
113
120
|
<output>
|
|
@@ -171,6 +178,10 @@ Rules:
|
|
|
171
178
|
paths, hidden oracle files, external URLs, or files outside the worktree.
|
|
172
179
|
Localhost URLs are allowed only when the visible verification command needs a
|
|
173
180
|
local server.
|
|
181
|
+
- `cmd` may invoke `.devlyn/probes/<id>.<ext>`; the script is part of the
|
|
182
|
+
replayed probe and obeys the same hidden-path/external-URL rules as `cmd`.
|
|
183
|
+
Solo-headroom hypothesis probes keep the hypothesis's backticked observable
|
|
184
|
+
command inline in `cmd` for mechanical validation.
|
|
174
185
|
- Match the spec's visible input and output key names literally; do not invent
|
|
175
186
|
aliases such as `stock` for `lots`, `order_id` for `id`, or `warehouse_id`
|
|
176
187
|
for `warehouse`.
|
|
@@ -222,7 +222,8 @@ When eligible and the orchestrator spawns a second VERIFY agent with the OTHER e
|
|
|
222
222
|
(or scheduled) and rejected rows.
|
|
223
223
|
|
|
224
224
|
Codex pair-JUDGE is read-only. Invoke `codex-monitored.sh` directly with
|
|
225
|
-
`CODEX_MONITORED_ISOLATED=1` and
|
|
225
|
+
`CODEX_MONITORED_ISOLATED=1 CODEX_MONITORED_TIMEOUT_SEC=600` and
|
|
226
|
+
`-c model_reasoning_effort=medium`; this is a
|
|
226
227
|
bounded two-probe review, not implementation. Isolation blocks user config,
|
|
227
228
|
AGENTS.md, hooks, and project rules from hidden context/tool
|
|
228
229
|
side effects. Do not pipe it to `tail`, `head`, `grep`, `sed`, or `awk`.
|
|
@@ -230,9 +231,10 @@ Capture stdout/stderr directly. The Codex judge must return JSONL findings on
|
|
|
230
231
|
stdout; the orchestrator writes `.devlyn/verify.pair.findings.jsonl` and merges
|
|
231
232
|
verdicts. Do not ask Codex to `apply_patch` or edit `.devlyn`.
|
|
232
233
|
When the OTHER engine is Claude (codex/omp orchestrator), the judge call
|
|
233
|
-
follows `_shared/adapters/claude.md` `## Invocation`: headless `claude -p`
|
|
234
|
-
|
|
235
|
-
|
|
234
|
+
follows `_shared/adapters/claude.md` `## Invocation`: wrap headless `claude -p`
|
|
235
|
+
as `python3 "$DEVLYN_SHARED_DIR/run-bounded.py" 600 -- claude -p ...` with
|
|
236
|
+
`--permission-mode dontAsk`, an allowlist of `Read,Grep,Glob` plus the repo test
|
|
237
|
+
command, hermetic settings, stdout captured to
|
|
236
238
|
`.devlyn/claude-judge.stdout`. The same bounded-output contract and emission
|
|
237
239
|
rule apply to that stdout file, and the orchestrator writes the same
|
|
238
240
|
canonical `.devlyn/verify.pair.findings.jsonl`.
|
|
@@ -249,6 +251,19 @@ findings or a non-PASS summary while `.devlyn/verify.pair.findings.jsonl` is
|
|
|
249
251
|
empty, VERIFY is `BLOCKED` for `verify.pair.emission-contract`; do not pass or
|
|
250
252
|
silently recover from a broken capture contract.
|
|
251
253
|
|
|
254
|
+
Both pair-judge directions are wall-budgeted at 600s. A judge subprocess exit
|
|
255
|
+
124 is a budget abort: before merge, the orchestrator writes
|
|
256
|
+
`.devlyn/verify.pair.timeout.json` with `{"engine": "<codex|claude>",
|
|
257
|
+
"budget_seconds": 600}`. Three cases are binding: marker plus no canonical pair
|
|
258
|
+
findings and no parseable stdout findings records `pair_judge: "TIMEOUT"`,
|
|
259
|
+
computes the merged verdict from mechanical plus primary judge, and surfaces
|
|
260
|
+
`solo verdict after pair TIMEOUT` in the report header; marker plus canonical
|
|
261
|
+
findings or parseable stdout findings binds those findings exactly as today,
|
|
262
|
+
including the stdout emission contract; no marker preserves the existing
|
|
263
|
+
`BLOCKED` contract for missing, empty, or uncaptured pair output. A budget
|
|
264
|
+
abort is not an availability fallback; explicit-route availability still fails
|
|
265
|
+
closed.
|
|
266
|
+
|
|
252
267
|
After all VERIFY findings files are written, run:
|
|
253
268
|
|
|
254
269
|
```bash
|
|
@@ -14,6 +14,7 @@ Single authoritative verdict source for `/devlyn:resolve`. The orchestrator bran
|
|
|
14
14
|
"pair_verify": false,
|
|
15
15
|
"complexity": null,
|
|
16
16
|
"risk_profile": { "high_risk": false, "reasons": [], "risk_probes_enabled": false, "pair_default_enabled": true },
|
|
17
|
+
"risk_probes_digest": null,
|
|
17
18
|
"base_ref": { "branch": "main", "sha": "abc123..." },
|
|
18
19
|
"rounds": { "max_rounds": 4, "global": 0 },
|
|
19
20
|
"bypasses": [],
|
|
@@ -51,12 +52,14 @@ Single authoritative verdict source for `/devlyn:resolve`. The orchestrator bran
|
|
|
51
52
|
- **engine_source** — `"flag" | "engines.json" | "default"` — provenance of the resolved executor engine (`_shared/engine-preflight.md#role-resolution`). Optional on archived pre-iter-0038 runs; absent means `default`.
|
|
52
53
|
- **source** — provenance for the contract all downstream phases read. Spec and verify-only mode set `type: "spec"`, `spec_path`, and `spec_sha256`. Free-form mode sets `type: "generated"`, leaves `spec_path`/`spec_sha256` null, and must set `criteria_path: ".devlyn/criteria.generated.md"` plus `criteria_sha256` from the generated file's raw bytes. VERIFY re-checks the matching hash before judging.
|
|
53
54
|
- **risk_profile** — PHASE 0 classification for conditional defaults. `high_risk` records durable-risk signals from the goal/spec; `risk_probes_enabled` is true for explicit `--risk-probes` or high-risk specs unless `--no-risk-probes`; `pair_default_enabled` is false only for explicit `--no-pair`. `risk_profile` must remain an object with boolean `high_risk`, `risk_probes_enabled`, and `pair_default_enabled` fields when present, plus `reasons` as a list of strings. Malformed `risk_profile` blocks VERIFY because pair-trigger reasons derive `risk.high` and `risk_probes.enabled` from this state.
|
|
55
|
+
- **risk_probes_digest** — top-level sha256 written by PHASE 1.5 after probe validation; BUILD_GATE/VERIFY replay semantics live in `phases/build-gate.md` step 4.
|
|
54
56
|
- **rounds.global** — incremented every fix-loop pass (BUILD_GATE → fix-loop, VERIFY → fix-loop, OR a phase-gate fix respawn inside phase-gated IMPLEMENT).
|
|
55
57
|
- **phases.implement.exec** — only on phase-gated large runs (plan.md has `## Execution phases` with >1 phase): `{ "total": N, "current": k, "statuses": ["PASS"|"FAIL"|null, ...], "commits": ["<sha>", ...] }`. Phase definitions live in plan.md (immutable contract); progress lives here (routing truth — never route on plan.md checkboxes). Absent on single-phase runs. `cumulative.patch` for such runs is `git diff <base_ref.sha>...HEAD`.
|
|
56
58
|
- **phases.probe_derive** — optional PHASE 1.5 entry when `--risk-probes` is enabled. Artifacts include `.devlyn/risk-probes.jsonl`. Probe failures later surface through BUILD_GATE/VERIFY as `correctness.risk-probe-failed`.
|
|
57
59
|
- **bypasses** — array of phase names from `--bypass`. Valid: `"build-gate" | "cleanup"`. PLAN, IMPLEMENT, VERIFY are non-bypassable (orchestrator rejects at parse time).
|
|
58
60
|
- **implement_passed_sha** — captured at end of PHASE 2; null until then. Activates the post-implement invariant for CLEANUP and VERIFY.
|
|
59
61
|
- **criteria** — generated from spec's `## Requirements` checklist (one per `- [ ]`). `status: pending → implemented` is the legal transition. `failed_by_finding_ids` populates when VERIFY surfaces a finding tied to a criterion.
|
|
62
|
+
- **`.devlyn/untracked.baseline` artifact** — written at PHASE 0 (`spec-verify-check.py --write-untracked-baseline`); BUILD_GATE flags created-during-run unauthorized untracked files against it (`.devlyn/` exempt; semantics in `phases/build-gate.md` step 4).
|
|
60
63
|
- **verify.coverage_failed** — set by VERIFY's JUDGE sub-phase when a spec axis could not be exercised against the diff. Triggers pair-mode escalation when set. Pair-mode also triggers for `state.pair_verify == true`, verify-only mode, high-risk specs, active risk probes, actionable solo-headroom hypotheses, `complexity: high` specs, or current free-form `state.complexity` of `"large"` when MECHANICAL and the primary JUDGE have no verdict-binding blockers. Legacy/external spec `complexity: large` remains accepted for compatibility; new specs use `high`. Legacy `"high"` state remains accepted by the merge validator only for archived run compatibility.
|
|
61
64
|
- **verify.pair_trigger** — VERIFY's trigger decision: `{ "eligible": boolean, "reasons": string[], "skipped_reason": string|null }`. The shape is strict: `eligible: true` requires a non-empty reasons list containing every applicable canonical eligible reason and only canonical eligible reasons, plus `skipped_reason: null`; `eligible: false` requires an empty reasons list and may set only `user_no_pair`, `mechanical_blocker`, `primary_judge_blocker`, `auto_pair_other_engine_unavailable`, or null as the skip cause. Canonical eligible reasons are `mode.verify-only`, `mode.pair-verify`, `complexity.high`, `complexity.large`, `spec.complexity.high`, `spec.complexity.large`, `spec.solo_headroom_hypothesis`, `risk.high`, `risk_probes.enabled`, `risk_probes.present`, `coverage.failed`, `mechanical.warning`, and `judge.warning`. `user_no_pair` is valid only when `risk_profile.pair_default_enabled == false` from an explicit `--no-pair`; `mechanical_blocker` and `primary_judge_blocker` are valid only when the matching source has a verdict-binding finding; `auto_pair_other_engine_unavailable` is valid only when every applicable trigger reason is automatic (none is `mode.pair-verify`) and no pair-judge engine is available per role resolution (pre-iter-0038 SKILL.md already used this skip; the schema previously omitted it — reconciled). If state implies a pair decision is required but `pair_trigger` is missing, if it records `eligible:false` with no supported skip reason, if an eligible trigger omits an applicable reason such as `spec.solo_headroom_hypothesis`, or if any combination is malformed, `verify-merge-findings.py` blocks VERIFY.
|
|
62
65
|
|
|
@@ -75,34 +78,38 @@ Each entry under `phases.<name>` (for `plan`, `probe_derive`, `implement`, `buil
|
|
|
75
78
|
"engine": "claude",
|
|
76
79
|
"model": "<exact model id that ran this phase>",
|
|
77
80
|
"pre_sha": null,
|
|
81
|
+
"post_sha": null,
|
|
78
82
|
"artifacts": { "findings_file": null, "log_file": null },
|
|
79
|
-
"sub_verdicts": null
|
|
83
|
+
"sub_verdicts": null,
|
|
84
|
+
"rounds_history": []
|
|
80
85
|
}
|
|
81
86
|
```
|
|
82
87
|
|
|
83
88
|
- `verdict` — `"PASS" | "PASS_WITH_ISSUES" | "FAIL" | "NEEDS_WORK" | "BLOCKED"`. PHASE 6 (FINAL_REPORT) writes its own verdict per the terminal-verdict precedence.
|
|
84
89
|
- `triggered_by` — null on first run; one of `"build_gate" | "verify"` when the phase is a fix-loop respawn.
|
|
85
90
|
- `pre_sha` — captured by orchestrator before CLEANUP and (if needed) other allowlist-enforced phases. Used to validate the post-spawn diff.
|
|
86
|
-
- `
|
|
91
|
+
- `post_sha` — captured on CLEANUP complete when that phase ran; finish-gate subtracts `pre_sha..post_sha` as the cleanup window.
|
|
92
|
+
- `sub_verdicts` — only populated for VERIFY: `{ "mechanical": "PASS|FAIL", "judge": "PASS|...", "pair_judge": "PASS|..." | "TIMEOUT" | null }`. Values are normalized by `verify-merge-findings.py`; model prose verdicts cannot upgrade or downgrade the deterministic findings-derived verdict. `sub_verdicts.pair_judge` is `"TIMEOUT"` when a pair judge exceeded its wall budget and a valid `.devlyn/verify.pair.timeout.json` marker was read — semantics in `references/phases/verify.md` (pair budget section).
|
|
93
|
+
- `rounds_history` — appended by `spawn` on respawn: the superseded round's `{round, started_at, completed_at, duration_ms, verdict, triggered_by, engine}`. Failed/superseded round wall time is read from here, never lost to the reset.
|
|
87
94
|
- `merged` — only populated for VERIFY after `verify-merge-findings.py --write-state`: `{ "verdict": "...", "findings_file": ".devlyn/verify-merged.findings.jsonl", "summary_file": ".devlyn/verify-merge.summary.json" }`.
|
|
88
95
|
- `pair_trigger` — only populated for VERIFY; same shape as top-level `verify.pair_trigger` when the phase stores it locally.
|
|
89
96
|
- `correctness.risk-probe-failed` — emitted by `spec-verify-check.py --include-risk-probes` when an executable probe derived from the visible `## Verification` section fails.
|
|
90
97
|
|
|
91
98
|
## Write protocol
|
|
92
99
|
|
|
93
|
-
Phase lifecycle (`started_at`/`completed_at`/`duration_ms`/`round`/`triggered_by`/`verdict`) is written by a deterministic script, never hand-edited JSON — a hand-edited fix-loop respawn left `started_at` stale
|
|
100
|
+
Phase lifecycle (`started_at`/`completed_at`/`duration_ms`/`round`/`triggered_by`/`verdict`) is written by a deterministic script, never hand-edited JSON — a prior hand-edited fix-loop respawn left `started_at` stale, corrupting cross-phase ordering because `completed_at`/`round`/`triggered_by` advanced to the new round while `started_at` didn't. Phase workers report their verdict and artifact paths in their reply; they never edit `pipeline.state.json` themselves.
|
|
94
101
|
|
|
95
|
-
1. **Spawn** (before every phase dispatch, including a fix-loop respawn — one call per outer phase-gated IMPLEMENT run, not per inner sub-phase): `python3 "$DEVLYN_SHARED_DIR/state-phase-write.py" --devlyn-dir .devlyn --phase <name> spawn --round <N> [--triggered-by build_gate|verify] [--pre-sha <sha>] [--engine <e>] [--model <m>]`.
|
|
96
|
-
2. **Complete** (after the orchestrator reads the phase's reply and determines the verdict): `python3 "$DEVLYN_SHARED_DIR/state-phase-write.py" --devlyn-dir .devlyn --phase <name> complete --verdict <V> [--findings-file <path>] [--log-file <path>]`. `duration_ms` is derived from the phase's own recorded `started_at` — it cannot drift from `completed_at`. `--verdict` is required for every phase except VERIFY: `verify-merge-findings.py --write-state` is the sole writer of `phases.verify.verdict`, so `complete verify` is always called without `--verdict` (an explicit one is rejected) and preserves what's already on disk.
|
|
97
|
-
3. **Before archive** (PHASE 6 step
|
|
102
|
+
1. **Spawn** (before every phase dispatch, including a fix-loop respawn — one call per outer phase-gated IMPLEMENT run, not per inner sub-phase): `python3 "$DEVLYN_SHARED_DIR/state-phase-write.py" --devlyn-dir .devlyn --phase <name> spawn --round <N> [--triggered-by build_gate|verify] [--pre-sha <sha>] [--engine <e>] [--model <m>]`. If the existing phase record has `started_at`, appends the superseded round to `rounds_history`, then resets `started_at` to now and nulls `completed_at`/`duration_ms`/`verdict`/`artifacts`/`sub_verdicts` — a respawn can never inherit a prior round's stale timing or verdict, and prior round timing remains attributable. A VERIFY spawn also deletes the prior round's on-disk VERIFY artifacts (`verify*.jsonl`, `verify-merge.summary.json`, `*-judge.*`) so a stale pair findings file can never read as current-round spawn evidence in `verify-merge-findings.py`. Fields the script doesn't own (e.g. `phases.implement.exec`, the phase-gated progress tracker) are left untouched.
|
|
103
|
+
2. **Complete** (after the orchestrator reads the phase's reply and determines the verdict): `python3 "$DEVLYN_SHARED_DIR/state-phase-write.py" --devlyn-dir .devlyn --phase <name> complete --verdict <V> [--post-sha <sha>] [--findings-file <path>] [--log-file <path>]`. `duration_ms` is derived from the phase's own recorded `started_at` — it cannot drift from `completed_at`. `--verdict` is required for every phase except VERIFY: `verify-merge-findings.py --write-state` is the sole writer of `phases.verify.verdict`, so `complete verify` is always called without `--verdict` (an explicit one is rejected) and preserves what's already on disk.
|
|
104
|
+
3. **Before archive** (PHASE 6 step 5): `phases.final_report.verdict` must be non-null. Archive prune skips runs whose final_report verdict is null (treated as in-flight).
|
|
98
105
|
|
|
99
106
|
## Terminal verdict (PHASE 6)
|
|
100
107
|
|
|
101
108
|
Precedence:
|
|
102
109
|
|
|
103
|
-
1. `phases.<any>.verdict == "BLOCKED"` → terminal `BLOCKED:<reason>`.
|
|
110
|
+
1. finish-gate exit 1 or 2 → `BLOCKED:finish-gate-unclean`; `phases.<any>.verdict == "BLOCKED"` → terminal `BLOCKED:<reason>`.
|
|
104
111
|
2. `phases.verify.verdict == "NEEDS_WORK"` after fix-loop exhaustion → terminal `NEEDS_WORK`.
|
|
105
|
-
3. `phases.verify.verdict == "PASS_WITH_ISSUES"` → terminal `PASS_WITH_ISSUES`.
|
|
112
|
+
3. `phases.verify.verdict == "PASS_WITH_ISSUES"` or finish-gate findings file present → terminal `PASS_WITH_ISSUES`.
|
|
106
113
|
4. `phases.verify.verdict == "PASS"` → terminal `PASS`.
|
|
107
114
|
5. Verify-only mode: terminal = `phases.verify.verdict` directly (PHASE 1-4 are skipped).
|
|
108
115
|
|
|
@@ -114,7 +121,7 @@ Per-phase summary table: `phase | verdict | duration_ms | round | triggered_by |
|
|
|
114
121
|
|
|
115
122
|
Findings table (post-IMPLEMENT phases only — they are findings-only): each finding's `severity | rule_id | file:line | message | confidence`.
|
|
116
123
|
|
|
117
|
-
Follow-up notes: any
|
|
124
|
+
Follow-up notes: any large-mode assumptions, any pair-judge TIMEOUT surfaced as `solo verdict after pair TIMEOUT`, pair/risk-probe opt-out state, engine setup guidance for `BLOCKED:<engine>-unavailable`, `/devlyn:ideate` guidance for `BLOCKED:solo-headroom-hypothesis-required` that asks for the visible behavior `solo_claude` is expected to miss, `/devlyn:ideate` guidance for `BLOCKED:solo-ceiling-avoidance-required` that asks for the concrete difference from rejected or solo-saturated controls such as `S2`-`S6`, and any `state.verify.coverage_failed` axes.
|
|
118
125
|
|
|
119
126
|
## Archive contract
|
|
120
127
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "devlyn-cli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.9.0",
|
|
4
4
|
"description": "AI development toolkit for Claude Code — ideate, resolve, and ship with context engineering and agent orchestration",
|
|
5
5
|
"homepage": "https://github.com/fysoul17/devlyn-cli#readme",
|
|
6
6
|
"bin": {
|
package/scripts/lint-skills.sh
CHANGED
|
@@ -1451,7 +1451,7 @@ section "Check 10a: Bounded Codex calls use isolated wrapper mode"
|
|
|
1451
1451
|
isolation_missing=0
|
|
1452
1452
|
for needle in \
|
|
1453
1453
|
'CODEX_MONITORED_ISOLATED=1 bash "$CODEX_MONITORED_PATH"' \
|
|
1454
|
-
'CODEX_MONITORED_ISOLATED=1` and `-c model_reasoning_effort=medium' \
|
|
1454
|
+
'CODEX_MONITORED_ISOLATED=1 CODEX_MONITORED_TIMEOUT_SEC=600` and `-c model_reasoning_effort=medium' \
|
|
1455
1455
|
'CODEX_MONITORED_PATH="$DEVLYN_SHARED_DIR/codex-monitored.sh"'
|
|
1456
1456
|
do
|
|
1457
1457
|
if ! grep -RInF "$needle" config/skills >/dev/null 2>&1; then
|