devlyn-cli 2.9.1 → 2.9.3
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/README.md +5 -5
- package/bin/devlyn.js +13 -0
- package/config/skills/_shared/state-phase-write.py +20 -28
- package/config/skills/devlyn:resolve/SKILL.md +4 -3
- package/config/skills/devlyn:resolve/references/free-form-mode.md +3 -2
- package/config/skills/devlyn:resolve/references/state-schema.md +6 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -27,7 +27,7 @@ If devlyn-cli saved you time, [give it a star](https://github.com/fysoul17/devly
|
|
|
27
27
|
npx devlyn-cli
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
-
That's it. The installer opens with a single **agent selector** — pick any combination of **Claude Code**, **Codex CLI**, **oh-my-pi (omp)**, or **
|
|
30
|
+
That's it. The installer opens with a single **agent selector** — pick any combination of **Claude Code**, **Codex CLI**, **oh-my-pi (omp)**, **Pi**, or **Grok Build CLI**, and devlyn installs into every one you choose in a single pass. Claude Code and any agent already present on your machine are pre-checked, so the common case is just Enter. Skill-capable agents receive the `devlyn:resolve`, `devlyn:ideate`, and `devlyn:design-ui` skills — plus the `devlyn:engines` and `devlyn:queue` utilities — in the directory each one loads from: Codex → `~/.codex/skills/`, Grok → `~/.grok/skills/`, while **omp and Pi share `~/.agents/skills/`** — the cross-agent standard both read — so the bundle is written there once, not duplicated per agent. In Codex / omp / Pi, invoke them as skills (`$devlyn:resolve`, `$devlyn:ideate`, `$devlyn:design-ui`); in Claude Code and Grok Build CLI they're slash commands (`/devlyn:resolve`). Run it again anytime to update. (`npx devlyn-cli -y` installs the Claude core non-interactively; `npx devlyn-cli agents <cli>` adds one agent later.)
|
|
31
31
|
|
|
32
32
|
---
|
|
33
33
|
|
|
@@ -39,7 +39,7 @@ devlyn-cli turns your AI coding agent into a hands-free development pipeline. Th
|
|
|
39
39
|
ideate (optional) → resolve → ship
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
-
Non-Claude agents (Codex / omp / Pi): when one of these is selected, the workflows install as that agent's skills.
|
|
42
|
+
Non-Claude agents (Codex / omp / Pi / Grok): when one of these is selected, the workflows install as that agent's skills. In Codex / omp / Pi, use `$devlyn:ideate`, `$devlyn:resolve`, or `$devlyn:design-ui`; in Grok, use `/devlyn:ideate`, `/devlyn:resolve`, or `/devlyn:design-ui`, the same slash-command form as Claude Code.
|
|
43
43
|
|
|
44
44
|
### Step 1 (optional) — Plan with `/devlyn:ideate`
|
|
45
45
|
|
|
@@ -283,14 +283,14 @@ Selected during install. Run `npx devlyn-cli` again to add more.
|
|
|
283
283
|
devlyn-cli gives your agent a world-class **harness**. Give it a world-class **memory** and the loop compounds — decisions, corrections, and hard-won context survive across sessions instead of resetting every conversation.
|
|
284
284
|
|
|
285
285
|
> [!TIP]
|
|
286
|
-
> ### 🧠 [memory
|
|
287
|
-
> Durable facts, corrections, and project state your agent recalls and reinforces — so every run starts smarter than the last.
|
|
286
|
+
> ### 🧠 [pyx-memory](https://memory.pyxmate.com) — world-class agentic memory (hybrid RAG) for coding agents
|
|
287
|
+
> Durable facts, corrections, and project state your agent recalls and reinforces — so every run starts smarter than the last. → **[memory.pyxmate.com](https://memory.pyxmate.com)**
|
|
288
288
|
|
|
289
289
|
- **Remembers across sessions** — decisions, gotchas, preferences, and project state, not just this conversation
|
|
290
290
|
- **Hybrid retrieval** — semantic *and* graph recall: find by meaning *and* by relationship
|
|
291
291
|
- **Learns the loop** — reinforces what works, records corrections when it doesn't
|
|
292
292
|
|
|
293
|
-
**Harness (devlyn) + Memory (
|
|
293
|
+
**Harness (devlyn) + Memory (pyx-memory) = agents that don't just execute — they improve.** Wire up **pyx-memory** as an MCP server and `/devlyn:resolve` recalls prior decisions before it plans and stores what it learns after it ships.
|
|
294
294
|
|
|
295
295
|
## Support & Attribution
|
|
296
296
|
|
package/bin/devlyn.js
CHANGED
|
@@ -58,6 +58,19 @@ const CLI_TARGETS = {
|
|
|
58
58
|
// Project-scoped only (see omp): machine-level ~/.pi must not auto-trigger.
|
|
59
59
|
detect: () => fs.existsSync(path.join(process.cwd(), '.pi')) || fs.existsSync(path.join(process.cwd(), '.agents')),
|
|
60
60
|
},
|
|
61
|
+
grok: {
|
|
62
|
+
name: 'Grok Build CLI (xAI)',
|
|
63
|
+
instructionsFile: 'AGENTS.md',
|
|
64
|
+
baseInstructionsFile: 'AGENTS.md',
|
|
65
|
+
// Grok discovers same-format SKILL.md skills from ./.grok/skills >
|
|
66
|
+
// <repo_root>/.grok/skills > ~/.grok/skills > ~/.claude/skills
|
|
67
|
+
// (Claude-compatible); skills are slash-invocable in the Grok TUI.
|
|
68
|
+
skillsDir: path.join(os.homedir(), '.grok', 'skills'),
|
|
69
|
+
skillsToInstall: DEVLYN_CORE_SKILLS,
|
|
70
|
+
// Project-scoped only: machine-level ~/.grok must not auto-trigger a project
|
|
71
|
+
// AGENTS.md write via `npx devlyn-cli agents` in an unrelated repo.
|
|
72
|
+
detect: () => fs.existsSync(path.join(process.cwd(), '.grok')),
|
|
73
|
+
},
|
|
61
74
|
};
|
|
62
75
|
|
|
63
76
|
// Files removed in previous versions that should be cleaned up on upgrade
|
|
@@ -94,22 +94,19 @@ def clear_verify_round_artifacts(devlyn: pathlib.Path) -> None:
|
|
|
94
94
|
(devlyn / "verify-merge.summary.json").unlink(missing_ok=True)
|
|
95
95
|
|
|
96
96
|
|
|
97
|
-
def
|
|
97
|
+
def append_phase_history(entry: dict) -> None:
|
|
98
98
|
if entry.get("started_at") is None:
|
|
99
99
|
return
|
|
100
|
-
history = entry.get("
|
|
100
|
+
history = entry.get("history")
|
|
101
101
|
if not isinstance(history, list):
|
|
102
102
|
history = []
|
|
103
103
|
history.append({
|
|
104
|
-
"round": entry.get("round"),
|
|
105
104
|
"started_at": entry.get("started_at"),
|
|
105
|
+
"verdict": entry.get("verdict"),
|
|
106
106
|
"completed_at": entry.get("completed_at"),
|
|
107
107
|
"duration_ms": entry.get("duration_ms"),
|
|
108
|
-
"verdict": entry.get("verdict"),
|
|
109
|
-
"triggered_by": entry.get("triggered_by"),
|
|
110
|
-
"engine": entry.get("engine"),
|
|
111
108
|
})
|
|
112
|
-
entry["
|
|
109
|
+
entry["history"] = history
|
|
113
110
|
|
|
114
111
|
|
|
115
112
|
def do_spawn(state: dict, phase: str, round_: int, triggered_by: str | None,
|
|
@@ -121,7 +118,7 @@ def do_spawn(state: dict, phase: str, round_: int, triggered_by: str | None,
|
|
|
121
118
|
if not isinstance(entry, dict):
|
|
122
119
|
entry = {}
|
|
123
120
|
phases[phase] = entry
|
|
124
|
-
|
|
121
|
+
append_phase_history(entry)
|
|
125
122
|
entry["started_at"] = now_iso()
|
|
126
123
|
entry["completed_at"] = None
|
|
127
124
|
entry["duration_ms"] = None
|
|
@@ -197,6 +194,7 @@ def self_test() -> int:
|
|
|
197
194
|
do_complete(state, "implement", "NEEDS_WORK", None, None, None, None, "test-model-id")
|
|
198
195
|
write_state(state_path, state)
|
|
199
196
|
entry = read_state(state_path)["phases"]["implement"]
|
|
197
|
+
assert "history" not in entry, "history must be absent before re-entry"
|
|
200
198
|
assert entry["completed_at"] is not None
|
|
201
199
|
assert entry["duration_ms"] >= 0
|
|
202
200
|
assert parse_iso(entry["completed_at"]) >= parse_iso(entry["started_at"])
|
|
@@ -216,15 +214,13 @@ def self_test() -> int:
|
|
|
216
214
|
assert respawned["engine"] == "claude", "engine preserved when not re-supplied"
|
|
217
215
|
assert respawned["round"] == 1
|
|
218
216
|
assert respawned["triggered_by"] == "verify"
|
|
219
|
-
assert len(respawned["
|
|
220
|
-
history0 = respawned["
|
|
221
|
-
assert history0["round"] == 0
|
|
217
|
+
assert len(respawned["history"]) == 1
|
|
218
|
+
history0 = respawned["history"][0]
|
|
222
219
|
assert history0["started_at"] == round0_started
|
|
220
|
+
assert history0["verdict"] == "NEEDS_WORK"
|
|
223
221
|
assert history0["completed_at"] == entry["completed_at"]
|
|
224
222
|
assert history0["duration_ms"] == entry["duration_ms"]
|
|
225
|
-
assert history0
|
|
226
|
-
assert history0["triggered_by"] is None
|
|
227
|
-
assert history0["engine"] == "claude"
|
|
223
|
+
assert set(history0) == {"started_at", "verdict", "completed_at", "duration_ms"}
|
|
228
224
|
|
|
229
225
|
time.sleep(0.05)
|
|
230
226
|
state = read_state(state_path)
|
|
@@ -262,9 +258,9 @@ def self_test() -> int:
|
|
|
262
258
|
write_state(state_path, state)
|
|
263
259
|
respawned_fail = read_state(state_path)["phases"]["build_gate"]
|
|
264
260
|
assert respawned_fail["verdict"] is None
|
|
265
|
-
assert len(respawned_fail["
|
|
266
|
-
assert respawned_fail["
|
|
267
|
-
assert respawned_fail["
|
|
261
|
+
assert len(respawned_fail["history"]) == 1
|
|
262
|
+
assert respawned_fail["history"][0]["verdict"] == "FAIL"
|
|
263
|
+
assert respawned_fail["history"][0]["completed_at"] == failed_round["completed_at"]
|
|
268
264
|
|
|
269
265
|
# VERIFY flow: verify-merge-findings.py already wrote verdict; complete()
|
|
270
266
|
# must preserve it when --verdict is omitted.
|
|
@@ -368,8 +364,8 @@ def self_test() -> int:
|
|
|
368
364
|
respawned_exec = read_state(state_path)["phases"]["implement"]
|
|
369
365
|
assert respawned_exec["exec"]["current"] == 3, "spawn must not clobber unowned fields like exec"
|
|
370
366
|
assert respawned_exec["verdict"] is None, "respawn still nulls owned fields even with exec present"
|
|
371
|
-
assert len(respawned_exec["
|
|
372
|
-
assert respawned_exec["
|
|
367
|
+
assert len(respawned_exec["history"]) == 1
|
|
368
|
+
assert respawned_exec["history"][0]["verdict"] == "PASS"
|
|
373
369
|
|
|
374
370
|
# Existing history is append-only; a respawn must not clobber prior
|
|
375
371
|
# entries that were already preserved from older rounds.
|
|
@@ -383,14 +379,11 @@ def self_test() -> int:
|
|
|
383
379
|
"triggered_by": "verify",
|
|
384
380
|
"verdict": "FAIL",
|
|
385
381
|
"engine": "codex",
|
|
386
|
-
"
|
|
387
|
-
"round": 1,
|
|
382
|
+
"history": [{
|
|
388
383
|
"started_at": "2026-01-01T00:00:00.000Z",
|
|
384
|
+
"verdict": "FAIL",
|
|
389
385
|
"completed_at": "2026-01-01T00:00:01.000Z",
|
|
390
386
|
"duration_ms": 1000,
|
|
391
|
-
"verdict": "FAIL",
|
|
392
|
-
"triggered_by": "build_gate",
|
|
393
|
-
"engine": "claude",
|
|
394
387
|
}],
|
|
395
388
|
}
|
|
396
389
|
}
|
|
@@ -398,11 +391,10 @@ def self_test() -> int:
|
|
|
398
391
|
state = read_state(state_path)
|
|
399
392
|
do_spawn(state, "implement", 3, "verify", None, None, None)
|
|
400
393
|
write_state(state_path, state)
|
|
401
|
-
history_preserved = read_state(state_path)["phases"]["implement"]["
|
|
394
|
+
history_preserved = read_state(state_path)["phases"]["implement"]["history"]
|
|
402
395
|
assert len(history_preserved) == 2
|
|
403
|
-
assert history_preserved[0]["
|
|
404
|
-
assert history_preserved[1]["
|
|
405
|
-
assert history_preserved[1]["engine"] == "codex"
|
|
396
|
+
assert history_preserved[0]["started_at"] == "2026-01-01T00:00:00.000Z"
|
|
397
|
+
assert history_preserved[1]["started_at"] == "2026-01-01T00:00:02.000Z"
|
|
406
398
|
|
|
407
399
|
# complete() records a post-state commit when a bounded phase needs an
|
|
408
400
|
# exact diff window for later mechanical checks.
|
|
@@ -92,7 +92,7 @@ Once `state.implement_passed_sha` is non-null (PHASE 2 returned and produced a d
|
|
|
92
92
|
|
|
93
93
|
`--pair-verify` and `--no-pair` are mutually exclusive; if both are present, stop with `BLOCKED:invalid-flags`. Likewise `--goal-file` combined with `--spec`, `--verify-only`, or an inline positional goal — or a missing/repeated `--goal-file` value — stops with `BLOCKED:invalid-flags`. (These `BLOCKED:<reason>` strings are report-level only; the JSON `phases.*.verdict` carrier in `pipeline.state.json` always stays the bare enum `"BLOCKED"`.)
|
|
94
94
|
|
|
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
|
|
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, risk_probes_explicit: false, pair_default_enabled: true }`. `risk_profile` is strict typed state: keep it an object, keep the four flags as JSON booleans, and keep `reasons` as a string array; never serialize booleans as strings.
|
|
96
96
|
|
|
97
97
|
4. Write `.devlyn/untracked.baseline`: `python3 "$DEVLYN_SHARED_DIR/spec-verify-check.py" --write-untracked-baseline`.
|
|
98
98
|
|
|
@@ -101,7 +101,7 @@ Once `state.implement_passed_sha` is non-null (PHASE 2 returned and produced a d
|
|
|
101
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`.
|
|
102
102
|
- **Verify-only**: skip to PHASE 5 with `state.source.spec_path` set, the supplied diff captured at `.devlyn/external-diff.patch`.
|
|
103
103
|
|
|
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.
|
|
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` and `risk_probes_explicit: 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.
|
|
105
105
|
|
|
106
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>`.
|
|
107
107
|
|
|
@@ -118,6 +118,7 @@ State write: `phases.plan.{started_at, verdict, completed_at, duration_ms}`.
|
|
|
118
118
|
After return:
|
|
119
119
|
1. If `.devlyn/plan.md` lists zero files → halt with verdict `BLOCKED:plan-empty`.
|
|
120
120
|
2. If risk list flags an out-of-scope expansion the user did not authorize → re-spawn once with the reminder; second fail → halt.
|
|
121
|
+
3. After any re-spawn above, if `state.risk_profile.risk_probes_enabled == true` and `state.risk_profile.risk_probes_explicit == false`, parse the `authorized_surface` array from the JSON block under `<!-- devlyn:authorized-surface -->`. For a well-formed string array, compute `probe_scale_small := len(authorized_surface) <= 2 AND no entry ends in "/**"`. If true, set `risk_probes_enabled = false`, leave `high_risk` unchanged, and append `auto-risk-probes demoted: plan surface small (<n> paths)` to `reasons` using the actual length. A missing or malformed block leaves probe state unchanged; BUILD_GATE owns its malformed-block failure.
|
|
121
122
|
|
|
122
123
|
## PHASE 1.5: RISK_PROBES
|
|
123
124
|
|
|
@@ -340,4 +341,4 @@ State write: `phases.final_report.started_at` at the top of this phase.
|
|
|
340
341
|
|
|
341
342
|
## State management
|
|
342
343
|
|
|
343
|
-
`.devlyn/pipeline.state.json` is the single authoritative verdict source. Branch on `state.phases.<name>.verdict` directly; never parse `.devlyn/*.findings.jsonl` for routing decisions. Schema and write protocol: `references/state-schema.md`. Every "State write: `phases.X.{...}`" note above describes the resulting shape only — the orchestrator always writes it via `state-phase-write.py spawn`/`complete` (`references/state-schema.md#write-protocol`); phase workers never edit `pipeline.state.json` directly.
|
|
344
|
+
`.devlyn/pipeline.state.json` is the single authoritative verdict source. Branch on `state.phases.<name>.verdict` directly; never parse `.devlyn/*.findings.jsonl` for routing decisions. Schema and write protocol: `references/state-schema.md`. Every "State write: `phases.X.{...}`" note above describes the resulting shape only — the orchestrator always writes it via `state-phase-write.py spawn`/`complete`, whose re-entry path preserves the prior `started_at`, `verdict`, `completed_at`, and `duration_ms` in `history[]` before resetting them (`references/state-schema.md#write-protocol`); phase workers never edit `pipeline.state.json` directly.
|
|
@@ -22,6 +22,8 @@ Compute these signals from the goal text + project state:
|
|
|
22
22
|
phrase `solo ceiling avoidance`, mention `solo_claude`, and name a concrete
|
|
23
23
|
difference from rejected or solo-saturated controls such as `S2`-`S6`?
|
|
24
24
|
|
|
25
|
+
Evaluate Large first, then Medium, then Trivial; stop at the first matching branch.
|
|
26
|
+
|
|
25
27
|
### Trivial branch
|
|
26
28
|
|
|
27
29
|
Conditions (all must hold):
|
|
@@ -37,7 +39,7 @@ Action: synthesize a minimal internal spec from the goal:
|
|
|
37
39
|
### Medium branch
|
|
38
40
|
|
|
39
41
|
Conditions (any one):
|
|
40
|
-
- `goal_length
|
|
42
|
+
- `goal_length > 30` words.
|
|
41
43
|
- `file_scope_signals` between 4 and 10.
|
|
42
44
|
- `verb_class ∈ {refactor, debug, review}` AND scope is a single subsystem.
|
|
43
45
|
- `has_failing_test == false` but the goal implies a runnable acceptance check.
|
|
@@ -50,7 +52,6 @@ Action: synthesize a richer internal spec:
|
|
|
50
52
|
### Large branch
|
|
51
53
|
|
|
52
54
|
Conditions (any one):
|
|
53
|
-
- `goal_length > 80` words.
|
|
54
55
|
- `file_scope_signals > 10` OR zero signals (vague enough that the classifier cannot pick scope).
|
|
55
56
|
- `verb_class ∈ {rewrite, migrate}` and scope is multi-subsystem.
|
|
56
57
|
- The goal mentions a new feature whose surface area requires design decisions the harness cannot make from a one-shot prompt.
|
|
@@ -13,7 +13,7 @@ Single authoritative verdict source for `/devlyn:resolve`. The orchestrator bran
|
|
|
13
13
|
"mode": "spec",
|
|
14
14
|
"pair_verify": false,
|
|
15
15
|
"complexity": null,
|
|
16
|
-
"risk_profile": { "high_risk": false, "reasons": [], "risk_probes_enabled": false, "pair_default_enabled": true },
|
|
16
|
+
"risk_profile": { "high_risk": false, "reasons": [], "risk_probes_enabled": false, "risk_probes_explicit": false, "pair_default_enabled": true },
|
|
17
17
|
"risk_probes_digest": null,
|
|
18
18
|
"base_ref": { "branch": "main", "sha": "abc123..." },
|
|
19
19
|
"rounds": { "max_rounds": 4, "global": 0 },
|
|
@@ -51,7 +51,7 @@ Single authoritative verdict source for `/devlyn:resolve`. The orchestrator bran
|
|
|
51
51
|
- **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
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`.
|
|
53
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.
|
|
54
|
-
- **risk_profile** — PHASE 0 classification for conditional defaults. `high_risk` records durable-risk signals from the goal/spec; `risk_probes_enabled`
|
|
54
|
+
- **risk_profile** — PHASE 0 classification for conditional defaults. `high_risk` records durable-risk signals from the goal/spec; `risk_probes_enabled` starts true for explicit `--risk-probes` or eligible high-risk specs and only an automatic small-surface route may demote it after PLAN; `risk_probes_explicit` is true only for explicit `--risk-probes`, preventing that demotion from breaking the promise; `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 list of strings. Malformed `risk_profile` blocks VERIFY because pair-trigger reasons derive `risk.high` and `risk_probes.enabled` from this state.
|
|
55
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.
|
|
56
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).
|
|
57
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`.
|
|
@@ -80,17 +80,17 @@ Each entry under `phases.<name>` (for `plan`, `probe_derive`, `implement`, `buil
|
|
|
80
80
|
"pre_sha": null,
|
|
81
81
|
"post_sha": null,
|
|
82
82
|
"artifacts": { "findings_file": null, "log_file": null },
|
|
83
|
-
"sub_verdicts": null
|
|
84
|
-
"rounds_history": []
|
|
83
|
+
"sub_verdicts": null
|
|
85
84
|
}
|
|
86
85
|
```
|
|
87
86
|
|
|
87
|
+
`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
|
+
|
|
88
89
|
- `verdict` — `"PASS" | "PASS_WITH_ISSUES" | "FAIL" | "NEEDS_WORK" | "BLOCKED"`. PHASE 6 (FINAL_REPORT) writes its own verdict per the terminal-verdict precedence.
|
|
89
90
|
- `triggered_by` — null on first run; one of `"build_gate" | "verify"` when the phase is a fix-loop respawn.
|
|
90
91
|
- `pre_sha` — captured by orchestrator before CLEANUP and (if needed) other allowlist-enforced phases. Used to validate the post-spawn diff.
|
|
91
92
|
- `post_sha` — captured on CLEANUP complete when that phase ran; finish-gate subtracts `pre_sha..post_sha` as the cleanup window.
|
|
92
93
|
- `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.
|
|
94
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" }`.
|
|
95
95
|
- `pair_trigger` — only populated for VERIFY; same shape as top-level `verify.pair_trigger` when the phase stores it locally.
|
|
96
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.
|
|
@@ -99,7 +99,7 @@ Each entry under `phases.<name>` (for `plan`, `probe_derive`, `implement`, `buil
|
|
|
99
99
|
|
|
100
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.
|
|
101
101
|
|
|
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>]`.
|
|
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>]`. Re-entry applies the `history` rule above, then resets `started_at` to now and nulls `completed_at`/`duration_ms`/`verdict`/`artifacts`/`sub_verdicts`. A VERIFY spawn also deletes the prior round's on-disk VERIFY artifacts (`verify*.jsonl`, `verify-merge.summary.json`, `*-judge.*`) so stale findings cannot read as current-round evidence. Fields the script doesn't own (e.g. `phases.implement.exec`) are left untouched.
|
|
103
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
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).
|
|
105
105
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "devlyn-cli",
|
|
3
|
-
"version": "2.9.
|
|
3
|
+
"version": "2.9.3",
|
|
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": {
|