devlyn-cli 2.9.3 → 2.10.1
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 +3 -3
- package/CLAUDE.md +5 -5
- package/README.md +5 -5
- package/benchmark/auto-resolve/fixtures/SCHEMA.md +1 -1
- package/benchmark/auto-resolve/scripts/pair_evidence_contract.py +1 -0
- package/benchmark/auto-resolve/scripts/run-fixture.sh +11 -1
- package/benchmark/auto-resolve/scripts/test-benchmark-arg-parsing.sh +83 -1
- package/bin/devlyn.js +108 -15
- package/config/skills/_shared/adapters/README.md +2 -2
- package/config/skills/_shared/adapters/grok.md +99 -0
- package/config/skills/_shared/archive_run.py +35 -1
- package/config/skills/_shared/collect-codex-findings.py +231 -50
- package/config/skills/_shared/engine-doctor.sh +6 -3
- package/config/skills/_shared/engine-preflight.md +12 -5
- package/config/skills/_shared/expected.schema.json +7 -0
- package/config/skills/_shared/grok-anchor-guard.py +61 -0
- package/config/skills/_shared/phase-prompt-render.py +181 -0
- package/config/skills/_shared/resolve-bootstrap.py +630 -0
- package/config/skills/_shared/resolve-stop-hook.py +325 -0
- package/config/skills/_shared/spec-verify-check.py +308 -31
- package/config/skills/_shared/state-phase-write.py +2966 -27
- package/config/skills/_shared/terminal-claim-check.py +537 -0
- package/config/skills/_shared/verify-merge-findings.py +533 -16
- package/config/skills/devlyn:engines/SKILL.md +2 -2
- package/config/skills/devlyn:ideate/SKILL.md +1 -1
- package/config/skills/devlyn:queue/SKILL.md +12 -0
- package/config/skills/devlyn:resolve/SKILL.md +60 -78
- package/config/skills/devlyn:resolve/references/phases/build-gate.md +2 -2
- package/config/skills/devlyn:resolve/references/phases/implement.md +1 -1
- package/config/skills/devlyn:resolve/references/phases/plan.md +1 -5
- package/config/skills/devlyn:resolve/references/phases/probe-derive.md +4 -2
- package/config/skills/devlyn:resolve/references/phases/surface-close.md +34 -0
- package/config/skills/devlyn:resolve/references/phases/verify.md +70 -97
- package/config/skills/devlyn:resolve/references/state-schema.md +26 -19
- package/optional-skills/devlyn:reap/SKILL.md +1 -1
- package/optional-skills/devlyn:reap/scripts/reap.sh +25 -13
- package/optional-skills/devlyn:reap/scripts/scan.sh +15 -1
- package/package.json +1 -1
- package/scripts/lint-fixtures.sh +4 -1
- package/scripts/lint-skills.sh +269 -77
|
@@ -82,33 +82,36 @@ design/style concerns remain non-binding MEDIUM and produce `PASS_WITH_ISSUES`.
|
|
|
82
82
|
|
|
83
83
|
**Anti-self-filter rule**: report every finding you observe, including ones you consider low-severity or low-confidence. Tag each with `confidence: high|medium|low` and let the harness's downstream filter rank them. Filtering at this stage suppresses recall.
|
|
84
84
|
|
|
85
|
-
### Pair-mode (when
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
When
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
- `
|
|
85
|
+
### Pair-mode (default when OTHER engine is available)
|
|
86
|
+
|
|
87
|
+
MECHANICAL runs first. A verdict-binding blocker routes to the fix loop without
|
|
88
|
+
either judge and records `pair_judge: null`. After it passes, honor `--no-pair`;
|
|
89
|
+
otherwise resolve the OTHER-engine route. An explicit `--pair-verify` is an
|
|
90
|
+
availability promise: an unavailable OTHER engine BLOCKs. An automatic route
|
|
91
|
+
with no OTHER engine records `auto_pair_other_engine_unavailable` and continues
|
|
92
|
+
solo.
|
|
93
|
+
|
|
94
|
+
When the OTHER engine is available, write `pair_trigger` at judge-spawn time
|
|
95
|
+
with `pair.default` plus every applicable outcome-independent canonical reason,
|
|
96
|
+
then dispatch the primary and pair JUDGEs concurrently against the same frozen
|
|
97
|
+
diff via foreground parallel dispatch, never background shells. A primary blocker does not cancel the pair-JUDGE; both finding sets join the same fix
|
|
98
|
+
round and merge by worst source verdict without vote counting. An orchestrator
|
|
99
|
+
without foreground parallel dispatch runs the same two required judges
|
|
100
|
+
sequentially, with no flag or extra state marker. This is a dispatch-shape
|
|
101
|
+
fallback, not outcome-dependent escalation; the primary cannot skip the pair.
|
|
102
|
+
Prefer read-only checks and serialize probes requiring exclusive shared state.
|
|
103
|
+
|
|
104
|
+
Keep these reasons as telemetry; they no longer gate the second spawn:
|
|
105
|
+
`mode.pair-verify`, `mode.verify-only`, `complexity.high`,
|
|
106
|
+
`complexity.large`, `spec.complexity.high`, `spec.complexity.large`,
|
|
107
|
+
`spec.solo_headroom_hypothesis`, `risk.high`, `risk_probes.enabled`,
|
|
108
|
+
`risk_probes.present`, `coverage.failed`, `mechanical.warning`, and
|
|
109
|
+
`judge.warning`.
|
|
106
110
|
|
|
107
111
|
Malformed `state.risk_profile` is a VERIFY contract violation: it must be an
|
|
108
112
|
object, `high_risk` / `risk_probes_enabled` / `pair_default_enabled` must be
|
|
109
|
-
JSON booleans when present, and `reasons` must be a string array.
|
|
110
|
-
|
|
111
|
-
it because it can hide `risk.high` or `risk_probes.enabled` pair triggers.
|
|
113
|
+
JSON booleans when present, and `reasons` must be a string array. The merge
|
|
114
|
+
blocks malformed state because it can hide a required route.
|
|
112
115
|
|
|
113
116
|
If `--no-pair` was set, do not spawn the OTHER-engine judge. Record
|
|
114
117
|
`pair_trigger: { eligible: false, reasons: [], skipped_reason: "user_no_pair" }`
|
|
@@ -116,58 +119,32 @@ and continue with solo VERIFY. This is an explicit user opt-out, not an engine
|
|
|
116
119
|
availability fallback. `--pair-verify` and `--no-pair` are mutually exclusive;
|
|
117
120
|
if both are present, stop with `BLOCKED:invalid-flags`.
|
|
118
121
|
|
|
119
|
-
|
|
120
|
-
spawning the OTHER-engine pair judge:
|
|
122
|
+
When the OTHER engine is available, persist this before either judge starts:
|
|
121
123
|
|
|
122
124
|
```json
|
|
123
125
|
"pair_trigger": {
|
|
124
126
|
"eligible": true,
|
|
125
|
-
"reasons": ["complexity.high"],
|
|
127
|
+
"reasons": ["pair.default", "complexity.high"],
|
|
126
128
|
"skipped_reason": null
|
|
127
129
|
}
|
|
128
130
|
```
|
|
129
131
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
on malformed trigger state. Eligible triggers must contain only canonical
|
|
142
|
-
reasons and at least one reason: `mode.verify-only`, `complexity.high`, `complexity.large`,
|
|
143
|
-
`mode.pair-verify`, `spec.complexity.high`, `spec.complexity.large`,
|
|
144
|
-
`spec.solo_headroom_hypothesis`, `risk.high`, `risk_probes.enabled`,
|
|
145
|
-
`risk_probes.present`, `coverage.failed`, `mechanical.warning`, or
|
|
146
|
-
`judge.warning`.
|
|
132
|
+
After both judges return, append every applicable outcome-dependent reason
|
|
133
|
+
(`coverage.failed`, `mechanical.warning`, `judge.warning`) before merge. Eligible schema-v3 state must contain `pair.default` and every other applicable canonical
|
|
134
|
+
reason. The OTHER-engine judge is mandatory; missing output is a contract
|
|
135
|
+
violation.
|
|
136
|
+
|
|
137
|
+
`pair_trigger` remains strict: eligible state has canonical reasons and no skip;
|
|
138
|
+
ineligible state has empty reasons and one of `user_no_pair`,
|
|
139
|
+
`mechanical_blocker`, `auto_pair_other_engine_unavailable`, or null.
|
|
140
|
+
`primary_judge_blocker` is parser-recognized only for archived v2.0 replay and
|
|
141
|
+
retains the existing rejection when a pre-known reason applied. New runs never
|
|
142
|
+
write it. The `--engine` flag does not disable default pairing.
|
|
147
143
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
skip reason.
|
|
153
|
-
The valid skip reasons after a non-empty eligible trigger are deterministic
|
|
154
|
-
MECHANICAL HIGH/CRITICAL blockers, an explicit `--no-pair`, or — when none of the
|
|
155
|
-
applicable reasons is `mode.pair-verify` (every reason is automatic) — the OTHER
|
|
156
|
-
engine being unavailable. Explicit `--pair-verify` with the OTHER engine absent
|
|
157
|
-
is a promise and stays `BLOCKED:<engine>-unavailable`, never a skip.
|
|
158
|
-
|
|
159
|
-
Before invoking the OTHER-engine judge, run the shared availability pre-flight
|
|
160
|
-
for that engine. If the route is explicit (`--pair-verify`) and Codex is required
|
|
161
|
-
and unavailable, set VERIFY to `BLOCKED:codex-unavailable` and tell the user to
|
|
162
|
-
install/configure the Codex CLI, run the current Codex auth/login flow, verify
|
|
163
|
-
`codex --version`, and rerun. If Claude is required and the host cannot spawn a
|
|
164
|
-
Claude agent, set VERIFY to `BLOCKED:claude-unavailable` and tell the user to
|
|
165
|
-
install/configure Claude Code, verify `claude --version`, and rerun. For an
|
|
166
|
-
explicit route, do not convert this to a solo pass and do not synthesize pair
|
|
167
|
-
findings. When the trigger was AUTO-only and the OTHER engine is unavailable, set
|
|
168
|
-
`eligible: false`, `reasons: []`, `skipped_reason:
|
|
169
|
-
"auto_pair_other_engine_unavailable"`, run solo VERIFY, and report the skipped
|
|
170
|
-
auto escalation — route selection, not a fallback.
|
|
144
|
+
Run the shared availability pre-flight before spawn. Explicit `--pair-verify`
|
|
145
|
+
unavailability stays `BLOCKED:<engine>-unavailable` with setup guidance.
|
|
146
|
+
Automatic unavailability records `auto_pair_other_engine_unavailable` and runs
|
|
147
|
+
solo. Never synthesize pair findings.
|
|
171
148
|
|
|
172
149
|
When eligible and the orchestrator spawns a second VERIFY agent with the OTHER engine's adapter, both judgments are merged:
|
|
173
150
|
- Any HIGH/CRITICAL finding either model surfaces is verdict-binding.
|
|
@@ -200,7 +177,11 @@ When eligible and the orchestrator spawns a second VERIFY agent with the OTHER e
|
|
|
200
177
|
A targeted probe must compare the full externally visible result
|
|
201
178
|
(stdout/stderr/exit and full parsed output object, including accepted/scheduled
|
|
202
179
|
rows, rejected rows, and remaining state when present), not just a single
|
|
203
|
-
property.
|
|
180
|
+
property. The mandatory dominance-loss anchor must run as a single command
|
|
181
|
+
with no shell chain operators. Chains used only to re-capture exit status are
|
|
182
|
+
forbidden because the tool result already carries it; this does not ban
|
|
183
|
+
compound shell generally or multi-step probes issued as separate tool calls.
|
|
184
|
+
When the spec names exact keys, row shapes, JSON object shape, or an
|
|
204
185
|
exact error body, compare parsed key sets/deep equality so aliased keys,
|
|
205
186
|
missing keys, and extra keys are verdict-binding failures. Use the spec's
|
|
206
187
|
visible input key names literally when constructing the probe input. For
|
|
@@ -221,39 +202,31 @@ When eligible and the orchestrator spawns a second VERIFY agent with the OTHER e
|
|
|
221
202
|
and the assertion must cover the complete output ordering for both accepted
|
|
222
203
|
(or scheduled) and rejected rows.
|
|
223
204
|
|
|
224
|
-
|
|
205
|
+
The resolved pair-JUDGE is read-only. Codex keeps the monitored
|
|
206
|
+
`codex-monitored.sh` route with
|
|
225
207
|
`CODEX_MONITORED_ISOLATED=1 CODEX_MONITORED_TIMEOUT_SEC=600` and
|
|
226
|
-
`-c model_reasoning_effort=medium`;
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
Capture
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
|
238
|
-
`.devlyn/claude-judge.stdout`. The same bounded-output contract and emission
|
|
239
|
-
rule apply to that stdout file, and the orchestrator writes the same
|
|
240
|
-
canonical `.devlyn/verify.pair.findings.jsonl`.
|
|
241
|
-
The Codex prompt must include a bounded-output contract: no harness-doc reads,
|
|
208
|
+
`-c model_reasoning_effort=medium`; isolation blocks user config, AGENTS.md,
|
|
209
|
+
hooks, and project rules from hidden context/tool side effects. Do not pipe it
|
|
210
|
+
to `tail`, `head`, `grep`, `sed`, or `awk`; capture stdout/stderr directly.
|
|
211
|
+
Every other resolved OTHER engine follows `_shared/adapters/<name>.md`
|
|
212
|
+
`## Invocation`. Capture the result as `.devlyn/<name>-judge.stdout`, then run
|
|
213
|
+
`python3 "$DEVLYN_SHARED_DIR/collect-codex-findings.py" --devlyn-dir
|
|
214
|
+
"<abs repo>/.devlyn" --stdout-file <name>-judge.stdout` before merge.
|
|
215
|
+
The orchestrator writes the canonical `.devlyn/verify.pair.findings.jsonl`.
|
|
216
|
+
The pair prompt must include a bounded-output contract: no harness-doc reads,
|
|
242
217
|
maximum two targeted probes before first output, stop on the first
|
|
243
218
|
verdict-binding finding, and emit PASS immediately after the bounded checks pass.
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
`.devlyn
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
Both pair-judge directions are wall-budgeted at 600s. A judge subprocess exit
|
|
219
|
+
Raw stdout is diagnostic-only. A non-zero collector exit must write no
|
|
220
|
+
canonical findings file: set the pair source to `BLOCKED` for
|
|
221
|
+
`verify.pair.emission-contract`, and do not merge as though unparsed stdout were
|
|
222
|
+
only a diagnostic. Do not ask the judge to edit `.devlyn`.
|
|
223
|
+
|
|
224
|
+
Both pair-judge directions are wall-budgeted at 600s. The orchestrator records
|
|
225
|
+
`state.phases.verify.judge_durations_ms: {"judge": <int>, "pair_judge": <int|null>}`
|
|
226
|
+
as it collects each judge result; these wall durations are siblings of, never
|
|
227
|
+
nested inside, normalized string `sub_verdicts`. A judge subprocess exit
|
|
255
228
|
124 is a budget abort: before merge, the orchestrator writes
|
|
256
|
-
`.devlyn/verify.pair.timeout.json` with `{"engine": "<
|
|
229
|
+
`.devlyn/verify.pair.timeout.json` with `{"engine": "<resolved name>",
|
|
257
230
|
"budget_seconds": 600}`. Three cases are binding: marker plus no canonical pair
|
|
258
231
|
findings and no parseable stdout findings records `pair_judge: "TIMEOUT"`,
|
|
259
232
|
computes the merged verdict from mechanical plus primary judge, and surfaces
|
|
@@ -6,7 +6,7 @@ Single authoritative verdict source for `/devlyn:resolve`. The orchestrator bran
|
|
|
6
6
|
|
|
7
7
|
```json
|
|
8
8
|
{
|
|
9
|
-
"version": "
|
|
9
|
+
"version": "3.0",
|
|
10
10
|
"run_id": "rs-<UTC-timestamp>-<12-hex>",
|
|
11
11
|
"started_at": "2026-04-30T12:00:00Z",
|
|
12
12
|
"engine": "claude",
|
|
@@ -33,6 +33,7 @@ Single authoritative verdict source for `/devlyn:resolve`. The orchestrator bran
|
|
|
33
33
|
"plan": null,
|
|
34
34
|
"probe_derive": null,
|
|
35
35
|
"implement": null,
|
|
36
|
+
"surface_close": null,
|
|
36
37
|
"build_gate": null,
|
|
37
38
|
"cleanup": null,
|
|
38
39
|
"verify": null,
|
|
@@ -45,27 +46,29 @@ Single authoritative verdict source for `/devlyn:resolve`. The orchestrator bran
|
|
|
45
46
|
## Field rules
|
|
46
47
|
|
|
47
48
|
- **version** — string. Bump major on a breaking schema change.
|
|
49
|
+
- **session_id** — optional string or null stamped from `CLAUDE_CODE_SESSION_ID`; a missing/null value leaves Stop-hook pressure inert.
|
|
48
50
|
- **mode** — `"free-form" | "spec" | "verify-only"`.
|
|
49
|
-
- **pair_verify** — boolean.
|
|
51
|
+
- **pair_verify** — boolean. True only for `--pair-verify`. Pairing is already default-when-available; this flag makes OTHER-engine availability an explicit fail-closed promise and adds `mode.pair-verify` telemetry. It is mutually exclusive with `risk_profile.pair_default_enabled == false` from `--no-pair`.
|
|
50
52
|
- **complexity** — `null | "trivial" | "medium" | "large"`. Free-form mode populates this; spec/verify-only mode leaves it null.
|
|
51
53
|
- **engine** — any engine name with a shipped adapter (`_shared/adapters/<name>.md`); `"claude"` and `"codex"` today. A required unavailable engine stops the run with `BLOCKED:<engine>-unavailable`.
|
|
52
54
|
- **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`.
|
|
53
|
-
- **source** —
|
|
54
|
-
- **risk_profile** — PHASE 0
|
|
55
|
+
- **source** — downstream contract provenance. Spec/verify-only set `type: "spec"`, `spec_path`, and `spec_sha256`. Free-form sets `type: "generated"`, `goal_path: ".devlyn/goal.raw.txt"` + exact-byte `goal_sha256`, and `criteria_path: ".devlyn/criteria.generated.md"` + raw-byte `criteria_sha256`. VERIFY re-checks its contract hash.
|
|
56
|
+
- **risk_profile** — PHASE 0 routing state. `high_risk` controls automatic risk probes; `risk_probes_enabled` / `risk_probes_explicit` preserve their current semantics; `pair_default_enabled` is false only for explicit `--no-pair`. `risk_profile` must remain an object with boolean `high_risk`, `risk_probes_enabled`, `risk_probes_explicit`, and `pair_default_enabled` fields when present, plus `reasons` as a string list. Malformed state blocks VERIFY because it can hide required routes.
|
|
55
57
|
- **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.
|
|
56
58
|
- **rounds.global** — incremented every fix-loop pass (BUILD_GATE → fix-loop, VERIFY → fix-loop, OR a phase-gate fix respawn inside phase-gated IMPLEMENT).
|
|
57
59
|
- **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`.
|
|
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
|
|
60
|
+
- **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`, or as `correctness.verification-timeout` when the probe exceeds its verification budget.
|
|
61
|
+
- **phases.surface_close** — generated trivial/medium one-shot: `pre_sha`, `input_patch_sha256`, assembled-byte `prompt_sha256`, `untracked_before`; spawn requires Claude plus a requested model, and `surface-check` gates PASS. `.devlyn/surface-close.output.json` retains the Claude JSON wrapper used for completion attestation while the round JSONL remains the execution-audit backstop. Automatic Claude unavailability alone records null verdict plus `skipped_reason: "auto_surface_close_claude_unavailable"`. After an adjudication-malformed reply only, successful rollback plus transcript write-audit records the two nonterminal fields `skipped_reason: "surface_close_rolled_back_adjudication_malformed"` and `continued_after_block: true`; neither creates a terminal floor. Other failures store bare `"BLOCKED"`. `durability[]` is append-only: `{round, origin_phase, receipt_path, receipt_sha256, restore_commit_sha}` per `.devlyn/closure-durability.round-<n>.json`.
|
|
59
62
|
- **bypasses** — array of phase names from `--bypass`. Valid: `"build-gate" | "cleanup"`. PLAN, IMPLEMENT, VERIFY are non-bypassable (orchestrator rejects at parse time).
|
|
60
|
-
- **implement_passed_sha** — captured at end of PHASE 2; null until then. Activates the post-implement invariant for CLEANUP and VERIFY.
|
|
63
|
+
- **implement_passed_sha** — captured at end of PHASE 2; null until then. Activates the post-implement invariant for SURFACE_CLOSE, CLEANUP, and VERIFY.
|
|
61
64
|
- **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
65
|
- **`.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).
|
|
63
|
-
- **verify.coverage_failed** — set
|
|
64
|
-
- **verify.pair_trigger** —
|
|
66
|
+
- **verify.coverage_failed** — outcome-dependent telemetry set when JUDGE cannot exercise a spec axis. It no longer gates pair dispatch: after MECHANICAL passes, `pair.default` dispatches both judges concurrently whenever `pair_default_enabled != false` and the OTHER engine is available. An orchestrator without foreground parallel dispatch runs the same two required judges sequentially; a primary blocker never skips the pair. A verdict-binding MECHANICAL blocker still skips both. Legacy complexity values remain accepted only for archived compatibility.
|
|
67
|
+
- **verify.pair_trigger** — strict decision state: `{ "eligible": boolean, "reasons": string[], "skipped_reason": string|null }`. Schema v3.0 eligible state requires `pair.default` plus every applicable telemetry reason and `skipped_reason: null`; outcome-dependent reasons are appended after both judges return. Canonical reasons are `pair.default`, `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`. Ineligible new-run state has empty reasons and only `user_no_pair`, `mechanical_blocker`, `auto_pair_other_engine_unavailable`, or null; `primary_judge_blocker` remains parser-recognized only for archived v2.0 replay and retains its existing pre-known-reason rejection. Explicit `mode.pair-verify` cannot use the unavailable-engine skip. Missing, contradictory, incomplete, or unknown trigger state BLOCKs VERIFY.
|
|
65
68
|
|
|
66
69
|
## Per-phase shape
|
|
67
70
|
|
|
68
|
-
Each entry under `phases.<name>` (for `plan`, `probe_derive`, `implement`, `build_gate`, `cleanup`, `verify`, `final_report`):
|
|
71
|
+
Each entry under `phases.<name>` (for `plan`, `probe_derive`, `implement`, `surface_close`, `build_gate`, `cleanup`, `verify`, `final_report`):
|
|
69
72
|
|
|
70
73
|
```json
|
|
71
74
|
{
|
|
@@ -76,7 +79,8 @@ Each entry under `phases.<name>` (for `plan`, `probe_derive`, `implement`, `buil
|
|
|
76
79
|
"triggered_by": null,
|
|
77
80
|
"verdict": "PASS",
|
|
78
81
|
"engine": "claude",
|
|
79
|
-
"
|
|
82
|
+
"model_requested": "<orchestrator-selected model id or null>",
|
|
83
|
+
"model_effective": "<session-attested model id or null>",
|
|
80
84
|
"pre_sha": null,
|
|
81
85
|
"post_sha": null,
|
|
82
86
|
"artifacts": { "findings_file": null, "log_file": null },
|
|
@@ -87,10 +91,12 @@ Each entry under `phases.<name>` (for `plan`, `probe_derive`, `implement`, `buil
|
|
|
87
91
|
`history` is absent until a phase is re-entered. Immediately before re-entry overwrites the live `started_at`, `verdict`, `completed_at`, and `duration_ms`, `state-phase-write.py` appends those prior values as one object to `history[]`, creating the append-only array on first re-entry.
|
|
88
92
|
|
|
89
93
|
- `verdict` — `"PASS" | "PASS_WITH_ISSUES" | "FAIL" | "NEEDS_WORK" | "BLOCKED"`. PHASE 6 (FINAL_REPORT) writes its own verdict per the terminal-verdict precedence.
|
|
94
|
+
- `model_requested` / `model_effective` — `model_requested` is supplied by the orchestrator through `--model`; `model_effective` is parsed only from completion-time engine session evidence. Codex evidence accepts the `model: <id>` exec header or, for `.jsonl`, rollout `turn_context.payload.model`; Claude JSON output accepts a singleton `modelUsage` key or, for multiple entries, the sole key whose `inputTokens`, `outputTokens`, `cacheReadInputTokens`, and `cacheCreationInputTokens` exactly match top-level `usage.input_tokens`, `usage.output_tokens`, `usage.cache_read_input_tokens`, and `usage.cache_creation_input_tokens`. Missing or malformed counters, zero or multiple matches, and requested/effective mismatch fail closed: the phase verdict becomes `BLOCKED` and the completion command fails. For mutation phases, omission of `--engine-session-log` also fails closed when `.devlyn/<artifact-phase>.worker-session.<round>.jsonl` exists; the error names that retained file. Otherwise omission records `model_effective: null` and attests that the engine class exposed no session log for that invocation.
|
|
90
95
|
- `triggered_by` — null on first run; one of `"build_gate" | "verify"` when the phase is a fix-loop respawn.
|
|
91
|
-
- `pre_sha` — captured
|
|
92
|
-
- `post_sha` — captured
|
|
96
|
+
- `pre_sha` — captured before SURFACE_CLOSE or CLEANUP. It bounds that phase's post-spawn diff and rollback.
|
|
97
|
+
- `post_sha` — captured when SURFACE_CLOSE or CLEANUP completes; finish-gate subtracts CLEANUP's `pre_sha..post_sha` window.
|
|
93
98
|
- `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).
|
|
99
|
+
- `judge_durations_ms` — only populated for VERIFY: `{ "judge": <non-negative int>, "pair_judge": <non-negative int|null> }`. The orchestrator writes each wall duration when it collects that judge's result; values remain a sibling of `sub_verdicts`, whose values stay normalized strings. VERIFY spawn resets this key to null so re-entry cannot retain prior-round timings.
|
|
94
100
|
- `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" }`.
|
|
95
101
|
- `pair_trigger` — only populated for VERIFY; same shape as top-level `verify.pair_trigger` when the phase stores it locally.
|
|
96
102
|
- `correctness.risk-probe-failed` — emitted by `spec-verify-check.py --include-risk-probes` when an executable probe derived from the visible `## Verification` section fails.
|
|
@@ -99,9 +105,10 @@ Each entry under `phases.<name>` (for `plan`, `probe_derive`, `implement`, `buil
|
|
|
99
105
|
|
|
100
106
|
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.
|
|
101
107
|
|
|
102
|
-
1. **Spawn
|
|
103
|
-
2. **
|
|
104
|
-
3. **
|
|
108
|
+
1. **Spawn**: `state-phase-write.py --devlyn-dir .devlyn --phase <name> spawn --round <N> [--triggered-by build_gate|verify] [--pre-sha <sha>] [--engine <e>] [--model <requested-model>]`. Re-entry preserves lifecycle in `history[]`, resets owned fields, and leaves fields such as `phases.implement.exec`; VERIFY also clears prior-round files. SURFACE_CLOSE additionally requires `--model <requested-model> --input-patch-sha256 <hex> --prompt-sha256 <hex> --untracked-before-json <array>`; use `surface-check --authorized-surface-json <array>`, `surface-adjudication-recover --authorized-surface-json <array>` only after its adjudication-malformed result, `surface-rollback` for other failures, or `surface-skip` only for automatic Claude unavailability.
|
|
109
|
+
2. **Transition** (normal direct handoff): `state-phase-write.py ... --phase <current> transition` takes the same completion/attestation arguments plus caller-specified `--next-phase`, `--next-round`, `--next-triggered-by`, `--next-engine`, `--next-model`, and next-phase metadata. It validates a fixed legal edge, applies complete plus spawn to a detached candidate, and replaces state once; either both lifecycle writes land or neither. The verb returns JSON paths/digests/state facts only and never chooses a phase, engine, branch, prompt, or agent.
|
|
110
|
+
3. **Complete** (after the orchestrator reads the phase's reply and determines the verdict, when no next phase opens): `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>] [--engine-session-log <path>]`. Pass the invocation's own session evidence whenever it exists; SURFACE_CLOSE passes `.devlyn/surface-close.output.json` as `--engine-session-log` so its `modelUsage` attests completion. Omission records `model_effective: null` only when the current phase/round has no retained worker-session file. Before completing a mutation phase (`implement`, `surface_close` when present, or `cleanup`), copy the exact session JSONL identified by that phase's dispatch receipt to `.devlyn/<artifact-phase>.worker-session.<round>.jsonl`, where `<artifact-phase>` is `implement`, `surface-close`, or `cleanup`; multiple rounds remain separate. Never discover or copy sessions by scanning an engine-global session directory. If that current-round file exists but `--engine-session-log` is omitted, completion fails closed and names the file. `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 normal 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; model-attestation failure still forces the phase to `BLOCKED`.
|
|
111
|
+
4. **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).
|
|
105
112
|
|
|
106
113
|
## Terminal verdict (PHASE 6)
|
|
107
114
|
|
|
@@ -117,12 +124,12 @@ Precedence:
|
|
|
117
124
|
|
|
118
125
|
Header: `run_id | engine | mode | complexity | verdict | wall_time_s`.
|
|
119
126
|
|
|
120
|
-
Per-phase summary table: `phase | verdict | duration_ms | round | triggered_by | findings_count
|
|
127
|
+
Per-phase summary table: `phase | verdict | duration_ms | round | triggered_by | findings_count`; include SURFACE_CLOSE when run or skipped.
|
|
121
128
|
|
|
122
|
-
Findings table
|
|
129
|
+
Findings table: each emitted finding's `severity | rule_id | file:line | message | confidence`.
|
|
123
130
|
|
|
124
|
-
Follow-up notes: any large-mode assumptions
|
|
131
|
+
Follow-up notes: when `phases.surface_close.continued_after_block` is true, exactly one explicit `pipeline continued to BUILD_GATE — surface_close_rolled_back_adjudication_malformed` line (never a terminal floor); 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.
|
|
125
132
|
|
|
126
133
|
## Archive contract
|
|
127
134
|
|
|
128
|
-
PHASE 6 step 4 moves
|
|
135
|
+
PHASE 6 step 4 moves `PER_RUN_PATTERNS` into `.devlyn/runs/<run_id>/`, including Goal, SURFACE_CLOSE patch/prompt/reply, and mutation worker-session JSONL; it never scans engine-global sessions. Machine config stays; the last 10 completed runs remain. Archive/prune failure does not change verdict.
|
|
@@ -95,7 +95,7 @@ These are the ONLY categories reap.sh will touch:
|
|
|
95
95
|
|
|
96
96
|
| Category | Match | Why safe |
|
|
97
97
|
|---|---|---|
|
|
98
|
-
| `telegram-bun` | `bun server.ts` **AND** cwd
|
|
98
|
+
| `telegram-bun` | worker `bun server.ts` **AND** cwd under `…/claude-plugins-official/…telegram` (matches both the old `plugins/cache/` and current `plugins/marketplaces/…/external_plugins/` layouts); PLUS the runner root `bun run --cwd …/claude-plugins-official/…telegram … start`, verified by its own cmdline (its cwd is the session dir). Workers ignore SIGTERM (measured 2026-07-17: 23/23 survived TERM, died on KILL) — reap.sh sends SIGKILL for this category by default. | Telegram MCP plugin leaks one runner+worker tree per Claude session; at ~24 trees the workers (21-43% CPU each) saturated an 11-core machine (loadavg 100+). |
|
|
99
99
|
| `superset-codex-bash` | `/bin/bash .*/.superset/bin/codex` with PPID=1 | `.superset/bin/codex` wrapper exits without killing its tail child; bash copies left behind. |
|
|
100
100
|
| `superset-codex-tail` | `tail -F .*superset-codex-session-*.jsonl` with PPID=1 | Log tail from the same wrapper, always safe to stop. |
|
|
101
101
|
| `workerd` (opt-in) | `@cloudflare/workerd-darwin-*/bin/workerd serve ` with PPID=1 | moonmaker-engine dev server that survives tab close. Opt-in because the user may have an active dev session. |
|
|
@@ -47,16 +47,23 @@ collect_pids() {
|
|
|
47
47
|
local category="$1"
|
|
48
48
|
case "$category" in
|
|
49
49
|
telegram-bun)
|
|
50
|
-
# cwd-verified — same logic
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
50
|
+
# Worker form: cwd-verified — same logic (and same both-layouts pattern)
|
|
51
|
+
# as scan.sh. Runner form: cmdline names the plugin path = verification
|
|
52
|
+
# (its cwd is the session dir, not the plugin dir).
|
|
53
|
+
{
|
|
54
|
+
printf '%s\n' "$SNAPSHOT" \
|
|
55
|
+
| grep -E '/bun[^ ]* server\.ts( |$)' \
|
|
56
|
+
| awk '{print $1}' \
|
|
57
|
+
| while read -r pid; do
|
|
58
|
+
cwd="$(lsof -a -d cwd -p "$pid" 2>/dev/null | awk 'NR==2 {for(i=9;i<=NF;i++) printf "%s ", $i; print ""}')"
|
|
59
|
+
case "$cwd" in
|
|
60
|
+
*"/claude-plugins-official/"*telegram*) printf '%s\n' "$pid" ;;
|
|
61
|
+
esac
|
|
62
|
+
done
|
|
63
|
+
printf '%s\n' "$SNAPSHOT" \
|
|
64
|
+
| grep -E '( |/)bun[^ ]* run --cwd [^ ]*/claude-plugins-official/[^ ]*telegram[^ ]*( |$)' \
|
|
65
|
+
| awk '{print $1}'
|
|
66
|
+
} | sort -u
|
|
60
67
|
;;
|
|
61
68
|
superset-codex-bash)
|
|
62
69
|
printf '%s\n' "$SNAPSHOT" | grep -E '/bin/bash .*/\.superset/bin/codex( |$)' | awk '{print $1}' ;;
|
|
@@ -108,11 +115,16 @@ for cat in "${CATS_ARR[@]}"; do
|
|
|
108
115
|
TOTAL_SKIPPED=$((TOTAL_SKIPPED+1))
|
|
109
116
|
continue
|
|
110
117
|
fi
|
|
118
|
+
# telegram-bun workers IGNORE SIGTERM (measured 2026-07-17: 23/23 survived
|
|
119
|
+
# TERM, all died on KILL — the leak's root behavior is "never exits").
|
|
120
|
+
# TERM against them is a proven no-op, so this category defaults to KILL.
|
|
121
|
+
sig="$SIGNAL"
|
|
122
|
+
[ "$cat" = "telegram-bun" ] && sig="KILL"
|
|
111
123
|
if [ "$DRY" -eq 1 ]; then
|
|
112
|
-
printf '[%s] %s would SIG%s\n' "$cat" "$pid" "$
|
|
124
|
+
printf '[%s] %s would SIG%s\n' "$cat" "$pid" "$sig"
|
|
113
125
|
else
|
|
114
|
-
if kill -s "$
|
|
115
|
-
printf '[%s] %s SIG%s sent\n' "$cat" "$pid" "$
|
|
126
|
+
if kill -s "$sig" "$pid" 2>/dev/null; then
|
|
127
|
+
printf '[%s] %s SIG%s sent\n' "$cat" "$pid" "$sig"
|
|
116
128
|
TOTAL_KILLED=$((TOTAL_KILLED+1))
|
|
117
129
|
else
|
|
118
130
|
printf '[%s] %s kill failed\n' "$cat" "$pid"
|
|
@@ -21,6 +21,10 @@ SNAPSHOT="$(ps -eo pid=,ppid=,user=,etime=,command= 2>/dev/null | awk -v me="$ME
|
|
|
21
21
|
# Conservative by design — if unsure, leave it UNKNOWN.
|
|
22
22
|
# -----------------------------------------------------------------------------
|
|
23
23
|
match_telegram_bun() { grep -E '/bun[^ ]* server\.ts( |$)'; }
|
|
24
|
+
# Runner form of the same leak (2026-07-17: ~24 found at loadavg 100+): the
|
|
25
|
+
# `bun run --cwd <plugin> … start` root outlives its session; its cmdline
|
|
26
|
+
# NAMES the plugin path, so the cmdline itself is the verification.
|
|
27
|
+
match_telegram_run() { grep -E '( |/)bun[^ ]* run --cwd [^ ]*/claude-plugins-official/[^ ]*telegram[^ ]*( |$)'; }
|
|
24
28
|
match_superset_codex_sh() { grep -E '/bin/bash .*/\.superset/bin/codex( |$)'; }
|
|
25
29
|
match_superset_codex_tl() { grep -E 'tail .*superset-codex-session-.*\.jsonl'; }
|
|
26
30
|
match_workerd_dev() { grep -E '@cloudflare/workerd-darwin-[^/]+/bin/workerd serve '; }
|
|
@@ -50,12 +54,22 @@ if [ -n "$SNAPSHOT" ]; then
|
|
|
50
54
|
BUN_CANDIDATES="$(printf '%s\n' "$SNAPSHOT" | match_telegram_bun | awk '{print $1}')"
|
|
51
55
|
for pid in $BUN_CANDIDATES; do
|
|
52
56
|
cwd="$(lsof -a -d cwd -p "$pid" 2>/dev/null | awk 'NR==2 {for(i=9;i<=NF;i++) printf "%s ", $i; print ""}')"
|
|
57
|
+
# One pattern covers both plugin layouts: old plugins/cache/…/telegram/
|
|
58
|
+
# and current plugins/marketplaces/…/external_plugins/telegram (the old
|
|
59
|
+
# cache-only pattern missed the live layout — 2026-07-17).
|
|
53
60
|
case "$cwd" in
|
|
54
|
-
*"/
|
|
61
|
+
*"/claude-plugins-official/"*telegram*)
|
|
55
62
|
TELEGRAM_PIDS="${TELEGRAM_PIDS}${pid}
|
|
56
63
|
" ;;
|
|
57
64
|
esac
|
|
58
65
|
done
|
|
66
|
+
# Runner roots verify by cmdline (their cwd is the SESSION dir, not the
|
|
67
|
+
# plugin dir — `bun run --cwd X` sets X for the child, not for itself).
|
|
68
|
+
RUN_CANDIDATES="$(printf '%s\n' "$SNAPSHOT" | match_telegram_run | awk '{print $1}')"
|
|
69
|
+
for pid in $RUN_CANDIDATES; do
|
|
70
|
+
TELEGRAM_PIDS="${TELEGRAM_PIDS}${pid}
|
|
71
|
+
"
|
|
72
|
+
done
|
|
59
73
|
fi
|
|
60
74
|
|
|
61
75
|
if [ -n "$TELEGRAM_PIDS" ]; then
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "devlyn-cli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.10.1",
|
|
4
4
|
"description": "Loop & harness engineering for AI coding agents (Claude Code, Codex, and any AGENTS.md-driven agent) — take intent to shipped, engineer-quality software hands-free with phase-gated pipelines and agent orchestration",
|
|
5
5
|
"homepage": "https://github.com/fysoul17/devlyn-cli#readme",
|
|
6
6
|
"bin": {
|
package/scripts/lint-fixtures.sh
CHANGED
|
@@ -161,7 +161,7 @@ def fallback_validate():
|
|
|
161
161
|
if not isinstance(command, dict):
|
|
162
162
|
errors.append(f"verification_commands[{idx}] must be an object")
|
|
163
163
|
continue
|
|
164
|
-
unknown_command = sorted(set(command) - {"cmd", "exit_code", "stdout_contains", "stdout_not_contains", "contract_refs"})
|
|
164
|
+
unknown_command = sorted(set(command) - {"cmd", "exit_code", "timeout_sec", "stdout_contains", "stdout_not_contains", "contract_refs"})
|
|
165
165
|
if unknown_command:
|
|
166
166
|
errors.append(f"verification_commands[{idx}] has unknown key(s): {', '.join(unknown_command)}")
|
|
167
167
|
if not isinstance(command.get("cmd"), str) or not command.get("cmd"):
|
|
@@ -169,6 +169,9 @@ def fallback_validate():
|
|
|
169
169
|
exit_code = command.get("exit_code", 0)
|
|
170
170
|
if isinstance(exit_code, bool) or not isinstance(exit_code, int):
|
|
171
171
|
errors.append(f"verification_commands[{idx}].exit_code must be an integer")
|
|
172
|
+
timeout_sec = command.get("timeout_sec", 60)
|
|
173
|
+
if isinstance(timeout_sec, bool) or not isinstance(timeout_sec, int) or not 1 <= timeout_sec <= 600:
|
|
174
|
+
errors.append(f"verification_commands[{idx}].timeout_sec must be an integer from 1 to 600")
|
|
172
175
|
for key in ("stdout_contains", "stdout_not_contains", "contract_refs"):
|
|
173
176
|
if key in command and not is_string_list(command[key]):
|
|
174
177
|
errors.append(f"verification_commands[{idx}].{key} must be an array of non-empty strings")
|