devflow-kit 3.2.0 → 3.4.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/CHANGELOG.md +34 -0
- package/README.md +1 -1
- package/dist/agents/code.md +330 -0
- package/{src/assets → dist}/agents/design.md +11 -1
- package/{src/assets → dist}/agents/diagnose.md +17 -13
- package/dist/agents/git.md +29 -56
- package/{src/assets → dist}/agents/knowledge.md +8 -5
- package/{src/assets → dist}/agents/research.md +21 -2
- package/{src/assets → dist}/agents/review.md +18 -7
- package/{src/assets → dist}/agents/scrutinize.md +35 -11
- package/dist/agents/skim.md +148 -0
- package/{src/assets → dist}/agents/triage.md +8 -2
- package/dist/cli/commands/init.js +62 -0
- package/dist/cli/commands/learning.js +63 -8
- package/dist/cli/commands/uninstall.js +42 -1
- package/dist/commands/bug-analysis.md +30 -8
- package/dist/commands/code-review.md +142 -61
- package/dist/commands/debug.md +15 -13
- package/dist/commands/dynamic-build.md +176 -130
- package/dist/commands/dynamic-plan.md +31 -19
- package/dist/commands/dynamic-profile.md +27 -13
- package/dist/commands/dynamic-tickets.md +28 -14
- package/dist/commands/explore.md +16 -14
- package/dist/commands/implement.md +163 -158
- package/dist/commands/plan.md +37 -24
- package/dist/commands/release.md +69 -4
- package/dist/commands/research.md +33 -11
- package/dist/commands/resolve.md +61 -47
- package/dist/commands/self-review.md +50 -32
- package/dist/core/agent-models.js +53 -7
- package/dist/core/assets.js +55 -10
- package/dist/core/claude-md-audit.js +190 -0
- package/dist/core/feature-switch.js +20 -1
- package/dist/core/flags.js +28 -0
- package/dist/core/fs-atomic.js +8 -3
- package/dist/core/learning-tuning-config.js +8 -0
- package/dist/core/learning-variants.js +213 -0
- package/dist/core/manifest.js +62 -0
- package/dist/core/mds-variants.js +38 -1
- package/dist/core/plugins.js +78 -11
- package/{src/assets → dist/learning-off}/agents/code.md +77 -28
- package/dist/learning-off/agents/design.md +119 -0
- package/dist/learning-off/agents/diagnose.md +210 -0
- package/dist/learning-off/agents/knowledge.md +90 -0
- package/dist/learning-off/agents/research.md +149 -0
- package/dist/learning-off/agents/review.md +228 -0
- package/dist/learning-off/agents/scrutinize.md +117 -0
- package/{src/assets → dist/learning-off}/agents/skim.md +5 -10
- package/dist/learning-off/agents/triage.md +163 -0
- package/dist/learning-off/commands/bug-analysis.md +420 -0
- package/dist/learning-off/commands/code-review.md +525 -0
- package/dist/learning-off/commands/debug.md +294 -0
- package/dist/learning-off/commands/dynamic-build.md +1255 -0
- package/dist/learning-off/commands/dynamic-plan.md +424 -0
- package/dist/learning-off/commands/dynamic-profile.md +214 -0
- package/dist/learning-off/commands/dynamic-tickets.md +632 -0
- package/dist/learning-off/commands/explore.md +210 -0
- package/dist/learning-off/commands/implement.md +808 -0
- package/dist/learning-off/commands/plan.md +664 -0
- package/dist/learning-off/commands/release.md +310 -0
- package/dist/learning-off/commands/research.md +222 -0
- package/dist/learning-off/commands/resolve.md +837 -0
- package/dist/learning-off/commands/self-review.md +266 -0
- package/dist/skills/git/references/tracker/_contract.md +33 -0
- package/dist/skills/git/references/tracker/github/fetch-issue.md +2 -0
- package/dist/skills/git/references/tracker/github/fetch-issues-batch.md +2 -0
- package/dist/skills/git/references/tracker/github/gather-release-evidence.md +5 -1
- package/dist/skills/git/references/tracker/github/post-wave-report.md +2 -0
- package/dist/skills/git/references/tracker/github/setup-task.md +12 -0
- package/dist/skills/git/references/tracker/jira/associate-release.md +1 -1
- package/dist/skills/git/references/tracker/jira/fetch-issue.md +2 -0
- package/dist/skills/git/references/tracker/jira/fetch-issues-batch.md +2 -0
- package/dist/skills/git/references/tracker/jira/gather-release-evidence.md +5 -1
- package/dist/skills/git/references/tracker/jira/post-wave-report.md +2 -0
- package/dist/skills/git/references/tracker/jira/setup-task.md +14 -2
- package/dist/skills/git/references/tracker/linear/associate-release.md +1 -1
- package/dist/skills/git/references/tracker/linear/fetch-issue.md +2 -0
- package/dist/skills/git/references/tracker/linear/fetch-issues-batch.md +2 -0
- package/dist/skills/git/references/tracker/linear/gather-release-evidence.md +5 -1
- package/dist/skills/git/references/tracker/linear/post-wave-report.md +2 -0
- package/dist/skills/git/references/tracker/linear/setup-task.md +14 -2
- package/dist/targets/claude-code/installer.js +72 -36
- package/dist/targets/claude-code/language-stamp.js +185 -0
- package/dist/targets/claude-code/learning-install.js +489 -0
- package/package.json +1 -1
- package/src/assets/agents/code.mds +339 -0
- package/src/assets/agents/design.mds +149 -0
- package/src/assets/agents/diagnose.mds +225 -0
- package/src/assets/agents/evaluate.md +12 -25
- package/src/assets/agents/git.mds +29 -56
- package/src/assets/agents/knowledge.mds +125 -0
- package/src/assets/agents/learning.md +4 -6
- package/src/assets/agents/research.mds +176 -0
- package/src/assets/agents/review.mds +286 -0
- package/src/assets/agents/scrutinize.mds +132 -0
- package/src/assets/agents/simplify.md +20 -0
- package/src/assets/agents/skim.mds +161 -0
- package/src/assets/agents/synthesize.md +12 -0
- package/src/assets/agents/test.md +2 -2
- package/src/assets/agents/triage.mds +194 -0
- package/src/assets/agents/validate.md +15 -8
- package/src/assets/commands/_partials/_compliance.mds +5 -4
- package/src/assets/commands/_partials/_decisions.mds +31 -0
- package/src/assets/commands/_partials/_engine.mds +29 -24
- package/src/assets/commands/_partials/_knowledge.mds +26 -13
- package/src/assets/commands/_partials/_plan_contract.mds +1 -1
- package/src/assets/commands/_partials/_preamble.mds +33 -9
- package/src/assets/commands/_partials/_publication.mds +5 -4
- package/src/assets/commands/_partials/_settings.mds +13 -5
- package/src/assets/commands/_partials/_wave.mds +16 -6
- package/src/assets/commands/bug-analysis.mds +24 -2
- package/src/assets/commands/code-review.mds +148 -45
- package/src/assets/commands/debug.mds +17 -1
- package/src/assets/commands/dynamic-build.mds +144 -65
- package/src/assets/commands/dynamic-plan.mds +36 -6
- package/src/assets/commands/dynamic-profile.mds +9 -1
- package/src/assets/commands/dynamic-tickets.mds +16 -2
- package/src/assets/commands/explore.mds +27 -1
- package/src/assets/commands/implement.mds +171 -138
- package/src/assets/commands/plan.mds +47 -8
- package/src/assets/commands/{release.md → release.mds} +27 -24
- package/src/assets/commands/research.mds +28 -4
- package/src/assets/commands/resolve.mds +69 -17
- package/src/assets/commands/self-review.mds +43 -13
- package/src/assets/mds/tracker/_common.mds +1 -1
- package/src/assets/mds/tracker/_contract.mds +72 -0
- package/src/assets/mds/tracker/_github.mds +13 -2
- package/src/assets/mds/tracker/_jira.mds +17 -5
- package/src/assets/mds/tracker/_linear.mds +17 -5
- package/src/assets/mds/tracker/_mcp.mds +2 -2
- package/src/assets/mds/tracker/_steps.mds +97 -0
- package/src/assets/rules/context-economy.md +10 -0
- package/src/assets/rules/go.md +1 -0
- package/src/assets/rules/java.md +1 -0
- package/src/assets/rules/python.md +1 -0
- package/src/assets/rules/rust.md +1 -0
- package/src/assets/rules/typescript.md +1 -0
- package/src/assets/scripts/ci-wait.cjs +636 -0
- package/src/assets/scripts/claude-md-audit.cjs +611 -0
- package/src/assets/scripts/hooks/assets/orchestrator-charter.md +2 -3
- package/src/assets/scripts/hooks/background-memory-update +290 -9
- package/src/assets/scripts/hooks/json-helper.cjs +13 -5
- package/src/assets/scripts/hooks/json-parse +34 -10
- package/src/assets/scripts/hooks/session-start-context +315 -7
- package/src/assets/scripts/release-trace.cjs +27 -10
- package/src/assets/skills/accessibility/SKILL.md +1 -1
- package/src/assets/skills/apply-decisions/SKILL.md +12 -82
- package/src/assets/skills/apply-feature-knowledge/SKILL.md +9 -43
- package/src/assets/skills/architecture/SKILL.md +1 -1
- package/src/assets/skills/boundary-validation/SKILL.md +1 -1
- package/src/assets/skills/complexity/SKILL.md +1 -1
- package/src/assets/skills/compliance/SKILL.md +1 -1
- package/src/assets/skills/consistency/SKILL.md +1 -1
- package/src/assets/skills/database/SKILL.md +1 -1
- package/src/assets/skills/dependencies/SKILL.md +1 -1
- package/src/assets/skills/dependency-research/SKILL.md +3 -6
- package/src/assets/skills/design-review/SKILL.md +1 -1
- package/src/assets/skills/docs-framework/SKILL.md +1 -1
- package/src/assets/skills/documentation/SKILL.md +1 -1
- package/src/assets/skills/feature-knowledge/SKILL.md +43 -12
- package/src/assets/skills/gap-analysis/SKILL.md +1 -1
- package/src/assets/skills/git/SKILL.md +1 -1
- package/src/assets/skills/go/SKILL.md +1 -1
- package/src/assets/skills/java/SKILL.md +1 -1
- package/src/assets/skills/patterns/SKILL.md +1 -1
- package/src/assets/skills/performance/SKILL.md +1 -1
- package/src/assets/skills/python/SKILL.md +1 -1
- package/src/assets/skills/qa/SKILL.md +1 -3
- package/src/assets/skills/quality-gates/SKILL.md +10 -13
- package/src/assets/skills/quality-gates/references/report-template.md +20 -20
- package/src/assets/skills/react/SKILL.md +1 -1
- package/src/assets/skills/regression/SKILL.md +1 -1
- package/src/assets/skills/reliability/SKILL.md +1 -1
- package/src/assets/skills/research-codebase/SKILL.md +1 -1
- package/src/assets/skills/research-competitor/SKILL.md +1 -1
- package/src/assets/skills/research-external/SKILL.md +1 -1
- package/src/assets/skills/research-technology/SKILL.md +1 -1
- package/src/assets/skills/review-methodology/SKILL.md +1 -1
- package/src/assets/skills/rust/SKILL.md +1 -1
- package/src/assets/skills/security/SKILL.md +1 -1
- package/src/assets/skills/software-design/SKILL.md +1 -1
- package/src/assets/skills/test-driven-development/SKILL.md +9 -29
- package/src/assets/skills/testing/SKILL.md +1 -1
- package/src/assets/skills/typescript/SKILL.md +1 -1
- package/src/assets/skills/ui-design/SKILL.md +1 -1
- package/src/assets/skills/worktree-support/SKILL.md +3 -55
- package/src/assets/skills/worktree-support/references/discovery.md +48 -0
- package/src/assets/skills/worktree-support/references/roots.md +2 -2
|
@@ -63,8 +63,34 @@ Then run a cheap syntax gate: write the authored script to a fresh, run-unique s
|
|
|
63
63
|
|
|
64
64
|
The `budget` global governs depth. Scale Review agent roster and verification votes to `budget`. A low-budget run uses a leaner roster and fewer verification votes; a high-budget run expands both. Never hardcode a roster size — let budget guide it.
|
|
65
65
|
|
|
66
|
+
### Handoff convention for sequential Code agents within a ticket
|
|
67
|
+
|
|
68
|
+
When a ticket requires multiple sequential Code agent phases, each Code agent appends its own `## Phase {N} Implementation Summary` section, at most 8,192 bytes, to `{toplevel}/.devflow/docs/handoff-{branch_slug}.md` (branch-scoped to prevent concurrent session clobber), `{toplevel}` being `git rev-parse --show-toplevel` in the checkout the ticket's branch is in — never a subdirectory. It never rewrites an earlier section. The next Code agent reads, via HANDOFF_FILE input, only the section of the phase immediately before its own, not the whole file. PRIOR_PHASE_SUMMARY is the compact in-context form; the handoff file is the durable form that survives context compaction. Code is authoritative, summaries are supplementary.
|
|
69
|
+
|
|
70
|
+
### IRON RULE (LLM-vs-plumbing)
|
|
71
|
+
|
|
72
|
+
**Author ZERO deterministic feature code.** No parsers, no schedulers, no topological-sort, no dependency-graph helpers, no confidence formulas. ALL issue reading, dependency reasoning, and scheduling decisions are LLM judgment at runtime, performed by the workflow's agents. The recipe is instructions. The workflow script Claude authors IS the runtime logic — keep it free of hand-coded feature algorithms.
|
|
73
|
+
|
|
74
|
+
### SAFETY BANNER
|
|
75
|
+
|
|
76
|
+
**NEVER merge to main or master** — the workflow merges to an integration branch only. The user merges to main themselves after reviewing. This rule is absolute and must appear as an `engine_invariants()` note in every workflow that touches git.
|
|
77
|
+
|
|
78
|
+
### Settings line
|
|
79
|
+
|
|
80
|
+
**Resolve the settings line** once per worktree root, reusing a line this run already resolved for the same root. `{root}` is the worktree the values are for — the repository root when the run has one worktree:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
node "$HOME/.devflow/scripts/resolve-settings.cjs" "{root}" 2>/dev/null; echo "exit=$?"
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Accept the output only when it is exactly two lines: `exit=0` last and, before it, one line of the form `TRACKER=<github|jira|linear> TRACKER_SOURCE=<project|personal|machine|default> TRACKER_WARN=<none|mismatch|invalid> SITE=<none|https://<host>> KEY=<none|<key>> REVIEW_PUBLICATION=<off|auto|full> COMPLIANCE=<off|generic|<id>[,<id>…]> MEMORY=<on|off> LEARNING=<on|off> KNOWLEDGE=<on|off>` — these fields, in this order, nothing else, where `<host>` is a lowercase dotted host name alone, `<key>` is 2–10 of `A-Z`, `0-9` and `_` starting with a letter, and each `<id>` is one of `gdpr`, `hipaa`, `pci-dss`, `soc2`, `iso-27001`, `sox`. **Anything else** (a non-zero exit, no line, extra text, or a missing, reordered or unlisted field or value) ⇒ use `TRACKER=github TRACKER_SOURCE=default TRACKER_WARN=invalid SITE=none KEY=none REVIEW_PUBLICATION=off COMPLIANCE=generic MEMORY=on LEARNING=on KNOWLEDGE=off` instead.
|
|
87
|
+
|
|
88
|
+
The accepted line is the only source of these values: the script alone folds the committed `.devflow/project.json`, the personal `.devflow/config.json` and the machine manifest.
|
|
89
|
+
|
|
66
90
|
### DECISIONS_CONTEXT — obtain BEFORE authoring
|
|
67
91
|
|
|
92
|
+
When the settings line says `LEARNING=off`, set `DECISIONS_CONTEXT` to `(none)` and skip this step, locating no ledger and reading no index.
|
|
93
|
+
|
|
68
94
|
The decisions ledger belongs to the repository, not to one checkout: in a linked worktree it lives in the main worktree, and a session started in a subdirectory reads the copy at the repository root. Locate it with ONE git call, run from the start directory — `WORKTREE_PATH` if provided, otherwise cwd (`devflow:worktree-support`):
|
|
69
95
|
|
|
70
96
|
```bash
|
|
@@ -81,19 +107,7 @@ This is the rule the learning hooks apply (D-LEDGER-MAIN-WORKTREE, D-PROMPT-ROOT
|
|
|
81
107
|
|
|
82
108
|
Before you author the workflow script, read `{ledger}/.devflow/learning/index.md`. If the file is absent or empty, set `DECISIONS_CONTEXT` to `(none)`; otherwise use the file content as `DECISIONS_CONTEXT`.
|
|
83
109
|
|
|
84
|
-
The script body cannot perform this read — you (the main model) do it before authoring. Then inject the relevant DECISIONS_CONTEXT into
|
|
85
|
-
|
|
86
|
-
### Handoff convention for sequential Code agents within a ticket
|
|
87
|
-
|
|
88
|
-
When a ticket requires multiple sequential Code agent phases, each Code agent writes `{toplevel}/.devflow/docs/handoff-{branch_slug}.md` (branch-scoped to prevent concurrent session clobber), `{toplevel}` being `git rev-parse --show-toplevel` in the checkout the ticket's branch is in — never a subdirectory. The next Code agent reads it via HANDOFF_FILE input. PRIOR_PHASE_SUMMARY is the compact in-context form; the handoff file is the durable form that survives context compaction. Always read the handoff file directly — code is authoritative, summaries are supplementary.
|
|
89
|
-
|
|
90
|
-
### IRON RULE (LLM-vs-plumbing)
|
|
91
|
-
|
|
92
|
-
**Author ZERO deterministic feature code.** No parsers, no schedulers, no topological-sort, no dependency-graph helpers, no confidence formulas. ALL issue reading, dependency reasoning, and scheduling decisions are LLM judgment at runtime, performed by the workflow's agents. The recipe is instructions. The workflow script Claude authors IS the runtime logic — keep it free of hand-coded feature algorithms.
|
|
93
|
-
|
|
94
|
-
### SAFETY BANNER
|
|
95
|
-
|
|
96
|
-
**NEVER merge to main or master** — the workflow merges to an integration branch only. The user merges to main themselves after reviewing. This rule is absolute and must appear as an `engine_invariants()` note in every workflow that touches git.
|
|
110
|
+
The script body cannot perform this read — you (the main model) do it before authoring. Then inject the relevant DECISIONS_CONTEXT into the prompts of the Code, Design, Knowledge, Review and Scrutinize agents, using the `devflow:apply-decisions` consumption algorithm (scan index → Read relevant entries → cite verbatim IDs in agent prompts). Those are the agent types above whose contract declares it; every other agent type gets no decisions context.
|
|
97
111
|
|
|
98
112
|
---
|
|
99
113
|
|
|
@@ -168,17 +182,7 @@ Author the resolved `ISSUE_REQUIRED` and `APPLY_CONVENTIONS` into the workflow s
|
|
|
168
182
|
|
|
169
183
|
**Produces:** COMPLIANCE_FRAMEWORKS
|
|
170
184
|
|
|
171
|
-
**Resolve the compliance lens** for each worktree root, from its settings line (every framework reference is installed on every machine, so no file check decides it)
|
|
172
|
-
|
|
173
|
-
**Resolve the settings line** once per worktree root, reusing a line this run already resolved for the same root. `{root}` is the worktree the values are for — the repository root when the run has one worktree:
|
|
174
|
-
|
|
175
|
-
```bash
|
|
176
|
-
node "$HOME/.devflow/scripts/resolve-settings.cjs" "{root}" 2>/dev/null; echo "exit=$?"
|
|
177
|
-
```
|
|
178
|
-
|
|
179
|
-
Accept the output only when it is exactly two lines: `exit=0` last and, before it, one line of the form `TRACKER=<github|jira|linear> TRACKER_SOURCE=<project|personal|machine|default> TRACKER_WARN=<none|mismatch|invalid> SITE=<none|https://<host>> KEY=<none|<key>> REVIEW_PUBLICATION=<off|auto|full> COMPLIANCE=<off|generic|<id>[,<id>…]> MEMORY=<on|off> LEARNING=<on|off> KNOWLEDGE=<on|off>` — these fields, in this order, nothing else, where `<host>` is a lowercase dotted host name alone, `<key>` is 2–10 of `A-Z`, `0-9` and `_` starting with a letter, and each `<id>` is one of `gdpr`, `hipaa`, `pci-dss`, `soc2`, `iso-27001`, `sox`. **Anything else** (a non-zero exit, no line, extra text, or a missing, reordered or unlisted field or value) ⇒ use `TRACKER=github TRACKER_SOURCE=default TRACKER_WARN=invalid SITE=none KEY=none REVIEW_PUBLICATION=off COMPLIANCE=generic MEMORY=on LEARNING=on KNOWLEDGE=off` instead.
|
|
180
|
-
|
|
181
|
-
The accepted line is the only source of these values: the script alone folds the committed `.devflow/project.json`, the personal `.devflow/config.json` and the machine manifest.
|
|
185
|
+
**Resolve the compliance lens** for each worktree root, from its settings line — the line resolved above for that root, by the settings block when this run has not yet resolved it (every framework reference is installed on every machine, so no file check decides it).
|
|
182
186
|
|
|
183
187
|
**Set the compliance lens** from that line: `COMPLIANCE_FRAMEWORKS` is the settings line's `COMPLIANCE` with `generic` written `none`: `off`, `none`, or the framework ids the machine and this repository declare.
|
|
184
188
|
|
|
@@ -198,7 +202,7 @@ and using its one-line output. If the command fails (outside a git repository),
|
|
|
198
202
|
|
|
199
203
|
**1. Apply decisions context**
|
|
200
204
|
|
|
201
|
-
Apply the `devflow:apply-decisions` algorithm to the DECISIONS_CONTEXT loaded
|
|
205
|
+
Apply the `devflow:apply-decisions` algorithm to the DECISIONS_CONTEXT loaded by the decisions step above: scan the index, Read relevant entries, note the verbatim ADR/PF IDs you will inject into Code agent prompts.
|
|
202
206
|
|
|
203
207
|
**2. Read budget**
|
|
204
208
|
|
|
@@ -322,7 +326,8 @@ if (BRANCH === "(none)") {
|
|
|
322
326
|
|
|
323
327
|
// Phase 2: Implement
|
|
324
328
|
await phase("implement", () =>
|
|
325
|
-
agent(`
|
|
329
|
+
agent(`OPERATION: implement
|
|
330
|
+
Implement the following ticket on branch ${BRANCH}:
|
|
326
331
|
|
|
327
332
|
${TICKET}
|
|
328
333
|
|
|
@@ -343,39 +348,52 @@ After implementing, commit your changes with a conventional-commit message and r
|
|
|
343
348
|
- Any open questions or blockers`, { agentType: "Code" })
|
|
344
349
|
);
|
|
345
350
|
|
|
346
|
-
// Phase 3: Gate 1 — post-code pipeline
|
|
351
|
+
// Phase 3: Gate 1 #1 — post-code pipeline: Simplify agent → Scrutinize agent → Validate agent.
|
|
352
|
+
// Validate runs last and unconditionally, so its one full run covers every commit the pass made (see gate1_postcode()).
|
|
347
353
|
const gate1 = await phase("gate1", async () => {
|
|
348
|
-
|
|
354
|
+
await agent(`Simplify and reduce complexity of recent changes on branch ${BRANCH}. Commit any improvements.`, { agentType: "Simplify" });
|
|
355
|
+
|
|
356
|
+
const scrutiny = await agent(`9-pillar self-review of recent changes on branch ${BRANCH}. Fix P0/P1 issues and commit the fixes.
|
|
357
|
+
Return: {"status": "PASS" | "FIXED" | "BLOCKED"}`, { agentType: "Scrutinize" });
|
|
358
|
+
// Anything but an explicit PASS or FIXED is a stop: a Scrutinize that returned no status, or one this skeleton does not know, has not accepted the code.
|
|
359
|
+
if (!["PASS", "FIXED"].includes(scrutiny?.status)) {
|
|
360
|
+
return { verdict: "ESCALATED", type: "scrutiny-blocked", reason: scrutiny?.status === "BLOCKED" ? "Scrutinize agent returned BLOCKED: a P0 issue cannot be fixed in scope" : "Scrutinize agent returned no recognised status: counted as BLOCKED" };
|
|
361
|
+
}
|
|
362
|
+
|
|
349
363
|
const validation = await agent(`Run build, typecheck, lint, and tests on branch ${BRANCH}.
|
|
350
364
|
Follow your Running commands block for every build and test command.
|
|
351
|
-
|
|
365
|
+
Return: {"verdict": "PASS" | "FAIL", "details": "..."}`, { agentType: "Validate" });
|
|
352
366
|
|
|
353
|
-
|
|
354
|
-
|
|
367
|
+
// Anything but an explicit PASS is a failure: a Validate that returned no verdict has not validated the branch.
|
|
368
|
+
if (validation?.verdict !== "PASS") {
|
|
369
|
+
let failureDetails = validation?.details || "Validate agent returned no PASS verdict";
|
|
370
|
+
// Up to 2 fix attempts, each followed by a Validate re-run
|
|
355
371
|
for (let attempt = 1; attempt <= 2; attempt++) {
|
|
356
|
-
await agent(`
|
|
357
|
-
${
|
|
372
|
+
await agent(`OPERATION: validation-fix
|
|
373
|
+
Fix the validation failures on branch ${BRANCH}:
|
|
374
|
+
${failureDetails}
|
|
375
|
+
DECISIONS_CONTEXT: ${DECISIONS_CONTEXT}
|
|
358
376
|
ISSUE_NUMBER: ${ISSUE_NUMBER}
|
|
359
377
|
ISSUE_PR_LINK: ${ISSUE_PR_LINK}
|
|
360
378
|
COMPLIANCE_FRAMEWORKS: ${COMPLIANCE_FRAMEWORKS}
|
|
361
379
|
Commit fixes with conventional-commit message.`, { agentType: "Code" });
|
|
362
|
-
const recheck = await agent(`Re-run build, typecheck, lint, tests on branch ${BRANCH}
|
|
363
|
-
|
|
364
|
-
if (
|
|
380
|
+
const recheck = await agent(`Re-run build, typecheck, lint, tests on branch ${BRANCH} after fix attempt ${attempt} (follow your Running commands block).
|
|
381
|
+
Return: {"verdict": "PASS" | "FAIL", "details": "..."}`, { agentType: "Validate" });
|
|
382
|
+
if (recheck?.verdict === "PASS") break;
|
|
383
|
+
failureDetails = recheck?.details || failureDetails;
|
|
384
|
+
if (attempt === 2) return { verdict: "ESCALATED", type: "validation-exhausted", reason: "Validation exhausted after 2 Code agent fix attempts" };
|
|
365
385
|
}
|
|
366
386
|
}
|
|
367
387
|
|
|
368
|
-
await agent(`Simplify and reduce complexity of recent changes on branch ${BRANCH}. Commit any improvements.`, { agentType: "Simplify" });
|
|
369
|
-
|
|
370
|
-
const scrutiny = await agent(`9-pillar self-review of recent changes on branch ${BRANCH}. Report any code you changed and your findings.`, { agentType: "Scrutinize" });
|
|
371
|
-
|
|
372
|
-
if (scrutiny.codeChanged) {
|
|
373
|
-
await agent(`Re-run build, typecheck, lint, tests on branch ${BRANCH} (Scrutinize agent made changes). Report: PASS or FAIL.`, { agentType: "Validate" });
|
|
374
|
-
}
|
|
375
|
-
|
|
376
388
|
return { verdict: "PASS" };
|
|
377
389
|
});
|
|
378
390
|
|
|
391
|
+
// Gate 1 #1 stop, before Gate 2: an exhausted Validate or a BLOCKED Scrutinize leaves a broken or unfinished build,
|
|
392
|
+
// so Gate 2 and the review pass never run on it — a correctness stop, like the ticket-link and branch stops.
|
|
393
|
+
if (gate1.verdict === "ESCALATED") {
|
|
394
|
+
return { ticket: TICKET, branch: BRANCH, verdict: "ESCALATED", issueId: ISSUE_NUMBER, issuePrLink: ISSUE_PR_LINK, escalations: [{ type: gate1.type, description: gate1.reason }] };
|
|
395
|
+
}
|
|
396
|
+
|
|
379
397
|
// Phase 4: Gate 2 — acceptance gate (once, before review pass)
|
|
380
398
|
const gate2 = await phase("gate2", async () => {
|
|
381
399
|
if (!PLAN && !CRITERIA && !TEST_PLAN) {
|
|
@@ -384,21 +402,20 @@ const gate2 = await phase("gate2", async () => {
|
|
|
384
402
|
|
|
385
403
|
let evalVerdict = "SKIPPED";
|
|
386
404
|
if (PLAN) {
|
|
387
|
-
const
|
|
388
|
-
|
|
405
|
+
const evaluation = await agent(`Acceptance and scope check on branch ${BRANCH}. Answer both questions:
|
|
406
|
+
1. Does the implementation satisfy each numbered criterion, INCLUDING negative criteria (what it must NOT do)?
|
|
407
|
+
2. Did the Code agent introduce any unplanned changes, smuggled anti-features, or drift from the plan's intent?
|
|
389
408
|
Plan: ${PLAN}
|
|
390
409
|
Criteria: ${CRITERIA || "see plan"}
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
Plan: ${PLAN}
|
|
394
|
-
Report: PASS or FAIL with rationale.`, { agentType: "Evaluate" }),
|
|
395
|
-
]);
|
|
396
|
-
evalVerdict = panel.every(p => p.verdict === "PASS") ? "PASS" : "FAIL";
|
|
410
|
+
Return: {"verdict": "PASS" | "FAIL", "rationale": "..."} — FAIL if either answer is no, naming the failing criterion or the drift.`, { agentType: "Evaluate" });
|
|
411
|
+
evalVerdict = evaluation?.verdict === "PASS" ? "PASS" : "FAIL";
|
|
397
412
|
if (evalVerdict === "FAIL") {
|
|
398
413
|
// fix-and-continue — no re-evaluate, no inline Gate 1. The final Gate 1 (#2)
|
|
399
414
|
// after the review pass is the build gate before the branch is handed back.
|
|
400
|
-
await agent(`
|
|
401
|
-
|
|
415
|
+
await agent(`OPERATION: alignment-fix
|
|
416
|
+
Fix the alignment issues identified by the Evaluate agent on branch ${BRANCH}:
|
|
417
|
+
${evaluation?.rationale || "see the Evaluate agent's report"}
|
|
418
|
+
DECISIONS_CONTEXT: ${DECISIONS_CONTEXT}
|
|
402
419
|
ISSUE_NUMBER: ${ISSUE_NUMBER}
|
|
403
420
|
ISSUE_PR_LINK: ${ISSUE_PR_LINK}
|
|
404
421
|
COMPLIANCE_FRAMEWORKS: ${COMPLIANCE_FRAMEWORKS}
|
|
@@ -417,8 +434,10 @@ Cover: functionality, API contracts, performance, and cover every TEST_PLAN scen
|
|
|
417
434
|
testVerdict = testResult.verdict;
|
|
418
435
|
if (testVerdict === "FAIL") {
|
|
419
436
|
// fix-and-continue — no re-test, no inline Gate 1.
|
|
420
|
-
await agent(`
|
|
437
|
+
await agent(`OPERATION: qa-fix
|
|
438
|
+
Fix the failing acceptance test scenarios on branch ${BRANCH}:
|
|
421
439
|
${testResult.failures}
|
|
440
|
+
DECISIONS_CONTEXT: ${DECISIONS_CONTEXT}
|
|
422
441
|
ISSUE_NUMBER: ${ISSUE_NUMBER}
|
|
423
442
|
ISSUE_PR_LINK: ${ISSUE_PR_LINK}
|
|
424
443
|
COMPLIANCE_FRAMEWORKS: ${COMPLIANCE_FRAMEWORKS}
|
|
@@ -444,7 +463,7 @@ const reviewResult = await phase("review", async () => {
|
|
|
444
463
|
const chunkSize = 5;
|
|
445
464
|
const reviewThunks = reviewFocuses.map(focus =>
|
|
446
465
|
() => agent(`${focus.charAt(0).toUpperCase() + focus.slice(1)} review. ${reviewScope}
|
|
447
|
-
Focus discipline: ${focus}. Return: {"focus": "${focus}", "reviewed": true, "filesExamined": ["<path>"], "findings": [{"file": "<path-or-null>", "description": "<one-sentence issue>", "severity": "critical|high|medium|low"}]}`, { agentType: "Review" })
|
|
466
|
+
Focus discipline: ${focus}. Return: {"focus": "${focus}", "reviewed": true, "filesExamined": ["<path>"], "findings": [{"file": "<path-or-null>", "line": <number-or-null>, "description": "<one-sentence issue>", "severity": "critical|high|medium|low"}]}`, { agentType: "Review" })
|
|
448
467
|
);
|
|
449
468
|
|
|
450
469
|
let rawReviews = [];
|
|
@@ -461,7 +480,7 @@ Focus discipline: ${focus}. Return: {"focus": "${focus}", "reviewed": true, "fil
|
|
|
461
480
|
const focus = reviewFocuses[i];
|
|
462
481
|
if (!r || r.reviewed !== true) {
|
|
463
482
|
// Dead Review agent: retry once sequentially
|
|
464
|
-
const retry = await agent(`Retry ${focus} review. ${reviewScope} Focus: ${focus} discipline. Return: {"focus": "${focus}", "reviewed": true, "filesExamined": ["<path>"], "findings": [{"file": "<path-or-null>", "description": "<one-sentence issue>", "severity": "critical|high|medium|low"}]}`, { agentType: "Review" });
|
|
483
|
+
const retry = await agent(`Retry ${focus} review. ${reviewScope} Focus: ${focus} discipline. Return: {"focus": "${focus}", "reviewed": true, "filesExamined": ["<path>"], "findings": [{"file": "<path-or-null>", "line": <number-or-null>, "description": "<one-sentence issue>", "severity": "critical|high|medium|low"}]}`, { agentType: "Review" });
|
|
465
484
|
if (!retry || retry.reviewed !== true) {
|
|
466
485
|
coverageGaps.push(focus);
|
|
467
486
|
log(`review coverage incomplete: ${focus}`);
|
|
@@ -526,13 +545,19 @@ ${JSON.stringify(allFindings.map((f, i) => ({ index: i, description: f.descripti
|
|
|
526
545
|
// Distinct-file groups are paced in chunks of FIX_CHUNK — same pacing bar as the Review spawn
|
|
527
546
|
// path — to avoid launching all Code agents at once on a large branch (reliability: explicit bound).
|
|
528
547
|
const FIX_CHUNK = 5;
|
|
529
|
-
|
|
548
|
+
// issue-fix SCOPE per finding: CRITICAL or HIGH takes the Careful protocol (failing regression test first); anything else is Standard.
|
|
549
|
+
const CAREFUL_SEVERITIES = new Set(["critical", "high"]);
|
|
550
|
+
const fixThunks = Object.values(fileGroups).map(findings => async () => {
|
|
530
551
|
const chunkResults = [];
|
|
531
552
|
for (let i = 0; i < findings.length; i += MAX_BATCH) {
|
|
532
553
|
const chunk = findings.slice(i, i + MAX_BATCH);
|
|
533
|
-
const r = await agent(`
|
|
534
|
-
|
|
535
|
-
|
|
554
|
+
const r = await agent(`OPERATION: issue-fix
|
|
555
|
+
Fix the following confirmed review findings on branch ${BRANCH}:
|
|
556
|
+
ISSUES:
|
|
557
|
+
${chunk.map((f, n) => `${n + 1}. ${f.file ? `${f.file}${f.line ? `:${f.line}` : ""} — ` : ""}${f.description} (${f.severity})`).join("\n")}
|
|
558
|
+
SCOPE: ${chunk.map((f, n) => `${n + 1}=${CAREFUL_SEVERITIES.has(String(f.severity).toLowerCase()) ? "Careful" : "Standard"}`).join(", ")}
|
|
559
|
+
PUSH: false
|
|
560
|
+
DECISIONS_CONTEXT: ${DECISIONS_CONTEXT}
|
|
536
561
|
ISSUE_NUMBER: ${ISSUE_NUMBER}
|
|
537
562
|
ISSUE_PR_LINK: ${ISSUE_PR_LINK}
|
|
538
563
|
COMPLIANCE_FRAMEWORKS: ${COMPLIANCE_FRAMEWORKS}
|
|
@@ -571,38 +596,41 @@ Return: {"status": "fixed"|"blocked", "commitShas": ["<sha>"], "unresolved": ["<
|
|
|
571
596
|
return { survivingFindings: notAddressed, fixedFindings, coverageGaps };
|
|
572
597
|
});
|
|
573
598
|
|
|
574
|
-
// Phase 5.5: Gate 1 #2 — FINAL post-fix gate (
|
|
599
|
+
// Phase 5.5: Gate 1 #2 — FINAL post-fix gate (Simplify agent → Scrutinize agent → full Validate agent, Validate last and unconditional).
|
|
575
600
|
// Runs ONCE, after the review pass. Inside the pass the Code agent self-verified its own fixes;
|
|
576
601
|
// this is the build gate before the branch is handed back. See gate1_postcode() cadence.
|
|
577
602
|
const gate1Final = await phase("gate1-final", async () => {
|
|
603
|
+
await agent(`Simplify and reduce complexity of recent changes on branch ${BRANCH}. Commit any improvements.`, { agentType: "Simplify" });
|
|
604
|
+
|
|
605
|
+
const scrutiny = await agent(`9-pillar self-review of recent changes on branch ${BRANCH}. Fix P0/P1 issues and commit the fixes.
|
|
606
|
+
Return: {"status": "PASS" | "FIXED" | "BLOCKED"}`, { agentType: "Scrutinize" });
|
|
607
|
+
if (!["PASS", "FIXED"].includes(scrutiny?.status)) {
|
|
608
|
+
return { verdict: "ESCALATED", type: "scrutiny-blocked", reason: scrutiny?.status === "BLOCKED" ? "Final Gate 1 Scrutinize agent returned BLOCKED: a P0 issue cannot be fixed in scope" : "Final Gate 1 Scrutinize agent returned no recognised status: counted as BLOCKED" };
|
|
609
|
+
}
|
|
610
|
+
|
|
578
611
|
const validation = await agent(`Run build, typecheck, lint, and tests on branch ${BRANCH} (final gate after all fixing).
|
|
579
612
|
Follow your Running commands block for every build and test command.
|
|
580
|
-
|
|
613
|
+
Return: {"verdict": "PASS" | "FAIL", "details": "..."}`, { agentType: "Validate" });
|
|
581
614
|
|
|
582
|
-
if (validation
|
|
583
|
-
let failureDetails = validation
|
|
615
|
+
if (validation?.verdict !== "PASS") {
|
|
616
|
+
let failureDetails = validation?.details || "Validate agent returned no PASS verdict";
|
|
584
617
|
for (let attempt = 1; attempt <= 2; attempt++) {
|
|
585
|
-
await agent(`
|
|
618
|
+
await agent(`OPERATION: validation-fix
|
|
619
|
+
Fix the final validation failures on branch ${BRANCH}:
|
|
586
620
|
${failureDetails}
|
|
621
|
+
DECISIONS_CONTEXT: ${DECISIONS_CONTEXT}
|
|
587
622
|
ISSUE_NUMBER: ${ISSUE_NUMBER}
|
|
588
623
|
ISSUE_PR_LINK: ${ISSUE_PR_LINK}
|
|
589
624
|
COMPLIANCE_FRAMEWORKS: ${COMPLIANCE_FRAMEWORKS}
|
|
590
625
|
Self-verify your fix compiles. Commit fixes with conventional-commit message.`, { agentType: "Code" });
|
|
591
|
-
const recheck = await agent(`Re-run build, typecheck, lint, tests on branch ${BRANCH} (follow your Running commands block).
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
626
|
+
const recheck = await agent(`Re-run build, typecheck, lint, tests on branch ${BRANCH} (final gate, after fix attempt ${attempt}; follow your Running commands block).
|
|
627
|
+
Return: {"verdict": "PASS" | "FAIL", "details": "..."}`, { agentType: "Validate" });
|
|
628
|
+
if (recheck?.verdict === "PASS") break;
|
|
629
|
+
failureDetails = recheck?.details || failureDetails;
|
|
630
|
+
if (attempt === 2) return { verdict: "ESCALATED", type: "validation-exhausted", reason: "Final Gate 1 validation exhausted after 2 Code agent fix attempts" };
|
|
595
631
|
}
|
|
596
632
|
}
|
|
597
633
|
|
|
598
|
-
await agent(`Simplify and reduce complexity of recent changes on branch ${BRANCH}. Commit any improvements.`, { agentType: "Simplify" });
|
|
599
|
-
|
|
600
|
-
const scrutiny = await agent(`9-pillar self-review of recent changes on branch ${BRANCH}. Report any code you changed and your findings.`, { agentType: "Scrutinize" });
|
|
601
|
-
|
|
602
|
-
if (scrutiny.codeChanged) {
|
|
603
|
-
await agent(`Re-run build, typecheck, lint, tests on branch ${BRANCH} (Scrutinize agent made changes; follow your Running commands block). Report: PASS or FAIL.`, { agentType: "Validate" });
|
|
604
|
-
}
|
|
605
|
-
|
|
606
634
|
return { verdict: "PASS" };
|
|
607
635
|
});
|
|
608
636
|
|
|
@@ -631,7 +659,7 @@ Write a concise report. Present only surviving findings as outstanding — never
|
|
|
631
659
|
survivingFindings: reviewResult.survivingFindings || [], fixedFindings: reviewResult.fixedFindings || [],
|
|
632
660
|
reviewCoverage: { failedFocuses: coverageGaps, complete: coverageGaps.length === 0 },
|
|
633
661
|
escalations: [
|
|
634
|
-
...(gate1Final.verdict === "ESCALATED" ? [{ type: "validation-exhausted", description: gate1Final.reason || "final Gate 1 escalated" }] : []),
|
|
662
|
+
...(gate1Final.verdict === "ESCALATED" ? [{ type: gate1Final.type || "validation-exhausted", description: gate1Final.reason || "final Gate 1 escalated" }] : []),
|
|
635
663
|
...coverageGaps.map(focus => ({ type: "review-coverage-incomplete", description: `review coverage incomplete: ${focus}` })),
|
|
636
664
|
],
|
|
637
665
|
gate2, report,
|
|
@@ -679,12 +707,12 @@ Run builds, typechecks, lints and tests in the foreground, each with an explicit
|
|
|
679
707
|
The standard implementation unit for one ticket. Run in order:
|
|
680
708
|
|
|
681
709
|
```
|
|
682
|
-
Code(agentType:"Code", prompt: full task + plan + DECISIONS_CONTEXT + handoff if sequential)
|
|
710
|
+
Code(agentType:"Code", prompt: "OPERATION: implement" first line, then full task + plan + DECISIONS_CONTEXT + COMPLIANCE_FRAMEWORKS + handoff if sequential)
|
|
683
711
|
→ gate1_postcode()
|
|
684
712
|
→ gate2_acceptance() ← Gate 2 runs HERE — before the review pass, not after
|
|
685
713
|
```
|
|
686
714
|
|
|
687
|
-
|
|
715
|
+
Every Code agent prompt opens with `OPERATION: <mode>` as its first line (`implement`, `issue-fix`, `validation-fix`, `alignment-fix` or `qa-fix`). It must include: task description, implementation plan (if one exists), relevant DECISIONS_CONTEXT (the index you loaded before authoring), the compliance lens (`COMPLIANCE_FRAMEWORKS` — every Code prompt carries it, fix prompts included), and any PRIOR_PHASE_SUMMARY / HANDOFF_FILE for sequential multi-phase tickets.
|
|
688
716
|
|
|
689
717
|
Gate 2 runs at implementation acceptance — this matches devflow's deliberate placement: "evaluation is part of implementation acceptance, not post-review" (§6.1).
|
|
690
718
|
|
|
@@ -692,21 +720,21 @@ Gate 2 runs at implementation acceptance — this matches devflow's deliberate p
|
|
|
692
720
|
|
|
693
721
|
ORDER IS LOAD-BEARING. Run exactly in this sequence:
|
|
694
722
|
|
|
695
|
-
1. **
|
|
723
|
+
1. **Simplify agent** — reduce complexity, remove duplication
|
|
724
|
+
2. **Scrutinize agent** — 9-pillar self-review (deep structural analysis); returns `{"status": "PASS" | "FIXED" | "BLOCKED"}`
|
|
725
|
+
- BLOCKED (a P0 issue cannot be fixed in scope), or a missing or unrecognised status, which counts as BLOCKED → stop the pass and escalate as `scrutiny-blocked`; Validate does not run on code Scrutinize could not accept
|
|
726
|
+
3. **Validate agent** — build / typecheck / lint / test; returns `{"verdict": "PASS" | "FAIL", "details": "..."}`. It runs LAST and UNCONDITIONALLY: one full run over the HEAD that Simplify and Scrutinize left, so it covers their commits whether or not Scrutinize changed code
|
|
696
727
|
- Build/test commands follow `build_execution_doctrine()`: the Validate agent's Running commands block, foreground under an explicit Bash timeout.
|
|
697
|
-
- FAIL → Code agent fix (max 2 retries)
|
|
698
|
-
- If still FAIL after 2 retries → escalate (do not loop endlessly)
|
|
699
|
-
2. **Simplify agent** — reduce complexity, remove duplication
|
|
700
|
-
3. **Scrutinize agent** — 9-pillar self-review (deep structural analysis)
|
|
701
|
-
- If Scrutinize agent changed code → re-run Validate agent (verify the Scrutinize agent's edits compile/pass)
|
|
728
|
+
- FAIL → Code agent fix (max 2 retries), each followed by a Validate agent re-run
|
|
729
|
+
- If still FAIL after 2 retries → escalate as `validation-exhausted` (do not loop endlessly)
|
|
702
730
|
|
|
703
731
|
**Gate 1 contains NO Evaluate agent and NO Test agent.** Those are Gate 2 only.
|
|
704
732
|
|
|
705
733
|
Depth scales to change size + budget: a trivial one-line fix warrants a lighter pass; a multi-file refactor warrants the full depth.
|
|
706
734
|
|
|
707
735
|
**Cadence — Gate 1 runs at exactly TWO points per ticket:**
|
|
708
|
-
1. **Gate 1 #1** — immediately after the initial Code agent implementation (inside `implement_bundle()`).
|
|
709
|
-
2. **Gate 1 #2** — the FINAL gate, after ALL Gate-2 fixes AND the entire review pass have completed.
|
|
736
|
+
1. **Gate 1 #1** — immediately after the initial Code agent implementation (inside `implement_bundle()`). An escalated result — Validate exhausted, or Scrutinize BLOCKED — returns early: the ticket's engine returns `ESCALATED` with one escalation, and Gate 2 and the review pass never run on a broken or unfinished build.
|
|
737
|
+
2. **Gate 1 #2** — the FINAL gate, after ALL Gate-2 fixes AND the entire review pass have completed. Scrutinize BLOCKED here is `ESCALATED` as well.
|
|
710
738
|
|
|
711
739
|
It does NOT run inside the review pass, nor after each individual Gate-2 / review / QA fix. At those points the fixing Code agent self-verifies its OWN build compiles (see `review_pass()` and the Code agent's Running commands block). The final Gate 1 #2 is the invariant that all written code passes before merge.
|
|
712
740
|
|
|
@@ -716,30 +744,27 @@ Gate 2 fires ONCE: after the implement-bundle and BEFORE the review pass. It doe
|
|
|
716
744
|
|
|
717
745
|
Gate 2 inputs are produced by `/devflow:dynamic-plan`'s plan-challenge step — the acceptance criteria and test plan written for the Evaluate agent and Test agent.
|
|
718
746
|
|
|
719
|
-
**Evaluate agent
|
|
720
|
-
- Run `evaluator_panel()` — see that block for the
|
|
721
|
-
- If
|
|
747
|
+
**Evaluate agent** (only if a plan exists):
|
|
748
|
+
- Run `evaluator_panel()` — see that block for the one spawn and the two lenses it names
|
|
749
|
+
- If the Evaluate agent returns FAIL (either lens failed): fix-and-continue — the demanded fixes are applied by a Code agent that self-verifies its own build (batched per the review-pass batching doctrine if numerous). The recorded verdict becomes `FAIL-FIXED` (issues found, fixes applied, not re-evaluated by design); Gate 2 then proceeds. In SINGLE mode the run reports it as `UNVERIFIED`, never PASS.
|
|
722
750
|
|
|
723
751
|
**Test agent** (only if acceptance criteria or a test plan exist):
|
|
724
752
|
- Scenario-based acceptance tests covering functionality, API contracts, performance
|
|
725
753
|
- FAIL → fix-and-continue — a Code agent applies the demanded fixes and self-verifies its own build. The recorded verdict becomes `FAIL-FIXED`; Gate 2 then proceeds. In SINGLE mode the run reports it as `UNVERIFIED`, never PASS.
|
|
726
754
|
|
|
727
755
|
**When Gate 2 inputs are absent:**
|
|
728
|
-
- No plan → skip Evaluate agent
|
|
756
|
+
- No plan → skip the Evaluate agent silently (note in output: "Gate 2 Evaluate agent skipped — no plan available")
|
|
729
757
|
- No acceptance criteria and no test plan → skip Test agent silently (note in output: "Gate 2 Test agent skipped — no criteria available")
|
|
730
758
|
- Build proceeds Gate-1-only. Never refuse to build; never force-generate fake criteria. Trust the user.
|
|
731
759
|
|
|
732
|
-
### Evaluate agent
|
|
733
|
-
|
|
734
|
-
Run 2–3 Evaluate agents with DISTINCT lenses. Diversity not redundancy — each agent asks a different question:
|
|
760
|
+
### Evaluate agent spawn (one agent, two lenses)
|
|
735
761
|
|
|
736
|
-
|
|
737
|
-
2. **Scope / intent-drift evaluator** — "Did the Code agent smuggle in unplanned changes, deviate from the plan's intent, or introduce anti-features?"
|
|
738
|
-
3. **Cross-ticket-consistency evaluator** — (wave-only; skip for single-ticket runs) "Does this ticket honor the API contracts and invariants that other wave tickets depend on?"
|
|
762
|
+
Run ONE Evaluate agent whose prompt names both lenses, so a ticket pays for one spawn and one read of the plan:
|
|
739
763
|
|
|
740
|
-
|
|
764
|
+
1. **Acceptance criteria** — "Does the implementation satisfy each numbered acceptance criterion, INCLUDING the negative criteria (what it must NOT do)?"
|
|
765
|
+
2. **Scope / intent drift** — "Did the Code agent smuggle in unplanned changes, deviate from the plan's intent, or introduce anti-features?"
|
|
741
766
|
|
|
742
|
-
|
|
767
|
+
It returns `{"verdict": "PASS" | "FAIL", "rationale": "..."}`: FAIL when either lens fails, the rationale naming the failing criterion or the drift. A FAIL on either lens blocks acceptance. A wave ticket runs this same skeleton through `runSingleTicketEngine`, so it gets the same single spawn.
|
|
743
768
|
|
|
744
769
|
### Acceptance criteria + test plan contract
|
|
745
770
|
|
|
@@ -784,7 +809,7 @@ Every line of a `## Test Plan` section, or of a PR's test-plan block, follows th
|
|
|
784
809
|
|
|
785
810
|
#### Consumption by Gate 2
|
|
786
811
|
|
|
787
|
-
The Evaluate agent
|
|
812
|
+
The Evaluate agent receives: the per-ticket plan + the numbered acceptance criteria (positive and negative).
|
|
788
813
|
|
|
789
814
|
The Test agent receives: the test plan's TP lines, once `check tp` has admitted them.
|
|
790
815
|
|
|
@@ -846,7 +871,7 @@ If survivors remain: batch the confirmed findings for fixing: group findings by
|
|
|
846
871
|
|
|
847
872
|
1. **Code is written ONLY by Code agents.** No other agent type writes code — not Review agent, not Evaluate agent.
|
|
848
873
|
2. **Findings are verified before any fix is written.** The adversarial verification step is not optional; unverified findings are not passed to the Code agent.
|
|
849
|
-
3. **All written code passes Gate 1.** No code merge, commit, or handoff before
|
|
874
|
+
3. **All written code passes Gate 1.** No code merge, commit, or handoff before Simplify agent + Scrutinize agent + Validate agent (in that order — Validate last, so its one run covers the commits of the two before it).
|
|
850
875
|
4. **Gate 2 runs once, at implementation acceptance.** It does not re-run after review-fixes.
|
|
851
876
|
5. **NEVER auto-merge to main or master.** All merges target the integration branch. The user merges to main themselves.
|
|
852
877
|
6. **No unauthorized tracker or remote side-effects.** Sub-agents NEVER create issues/PRs on the tracker, comment on them, or push beyond the ticket-authorized branch unless the ticket, plan, or user explicitly authorizes that exact action. This applies to whatever tracker is resolved, not to one vendor. Proposed follow-ups go in the run report.
|
|
@@ -888,7 +913,7 @@ Each ticket engine run returns a structured result. The Synthesize agent or the
|
|
|
888
913
|
},
|
|
889
914
|
"escalations": [
|
|
890
915
|
{
|
|
891
|
-
"type": "merge-conflict | gate2-fail | validation-exhausted | ambiguous-resolution | review-coverage-incomplete | dependency-blocked | engine-crash | ticket-link-missing | branch-missing",
|
|
916
|
+
"type": "merge-conflict | gate2-fail | validation-exhausted | scrutiny-blocked | ambiguous-resolution | review-coverage-incomplete | dependency-blocked | engine-crash | ticket-link-missing | branch-missing",
|
|
892
917
|
"description": "string"
|
|
893
918
|
}
|
|
894
919
|
],
|
|
@@ -947,8 +972,8 @@ For each ready ticket (sequentially by default; parallel only past the §7.1 bar
|
|
|
947
972
|
- Branch setup: the engine's setup-task creates the ticket's branch off integration HEAD at ready-time (so it already contains merged deps); every later phase, and the merge, uses the branch setup-task created, and a setup-task that reports none stops the ticket before implementing
|
|
948
973
|
- Run the single-ticket engine inside a try/catch — one ticket's crash/stall never kills the wave; catch the exception, quarantine that ticket, and continue with the remaining ready set
|
|
949
974
|
- The engine gets the ticket's own reference, the one the pre-fetch printed, as its setup-task input — never the wave's tracking issue
|
|
950
|
-
- On engine PASS or UNVERIFIED: merge to integration branch,
|
|
951
|
-
-
|
|
975
|
+
- On engine PASS or UNVERIFIED: merge to the integration branch locally through Git (no push, no build or test), then the workflow spawns the Validate agent (build + test) over the merge. The merge counts as kept only when that Validate returns PASS
|
|
976
|
+
- Validate FAIL or no PASS (build red after merge): the workflow spawns Git to undo the merge, quarantines the ticket, marks it as escalated, and continues. If the undo is refused, the wave stops taking merges and rounds
|
|
952
977
|
- On any other verdict (PARTIAL, FAIL, ESCALATED) or none: quarantine ticket, do not block independent siblings
|
|
953
978
|
|
|
954
979
|
**Cascade quarantine:** when a ticket is quarantined for any reason (Gate-1 exhausted, engine crash/stall, build-red after merge, review coverage incomplete after retry), the quarantine cascades to its direct and transitive dependents — each is marked blocked with the named reason, naming the blocker by its `{ISSUE_REF}` (e.g. "blocked: depends on {ISSUE_REF} which failed Gate-1"). Independent siblings are never affected. The quarantined list is injected into every subsequent Design agent reader prompt so the reader never schedules dependents of failed tickets.
|
|
@@ -975,7 +1000,9 @@ MAX_ROUNDS = LLM judgment based on ticket count (heuristic: ticket_count * 2 + 5
|
|
|
975
1000
|
|
|
976
1001
|
**Parallel independent tickets:** each gets its own `git worktree add` + durable branch managed by the Git agent. Use explicit `git worktree add` — NOT the Workflow tool's ephemeral `isolation:'worktree'`. The branch must persist across implement → review → resolve → merge stages; ephemeral worktrees are gone when the agent call ends.
|
|
977
1002
|
|
|
978
|
-
**Post-merge validation:** after EVERY merge into the integration branch,
|
|
1003
|
+
**Post-merge validation:** after EVERY merge into the integration branch, the workflow itself spawns the Validate agent (build + test) over the integration branch; Git runs no build or test. A red build immediately after merge is the cheapest possible conflict detector. It always runs — the merge reports `treeEqual` (whether the merge commit's tree equals the ticket branch head's tree) and the wave row records it, but a true value never skips the Validate.
|
|
1004
|
+
|
|
1005
|
+
**Undo of a red merge:** on FAIL or no PASS verdict the workflow spawns Git to undo the merge. The undo runs only when no remote branch contains the merge commit and the integration HEAD still equals it, as `git reset --keep {mergeSha}^1` on the integration branch — never `--hard`, never a push. It returns `{"undone": true}` or `{"undone": false, "reason": "..."}`. Undone → the row reads `merged: false` and the ticket is quarantined ("post-merge build red; merge undone"). Not undone → the wave stops taking merges and rounds, and its report names the red integration HEAD.
|
|
979
1006
|
|
|
980
1007
|
**Commit discipline:** Git agent creates atomic commits per logical change, conventional-commit format, on the ticket branch before merge.
|
|
981
1008
|
|
|
@@ -986,13 +1013,13 @@ Two parallel sibling tickets can produce real git conflicts. The resolution is *
|
|
|
986
1013
|
**Resolution procedure:**
|
|
987
1014
|
|
|
988
1015
|
1. Git agent detects the conflict and reports the conflicting files + sections
|
|
989
|
-
2. Spawn a Code agent with FULL intent context:
|
|
1016
|
+
2. Spawn a Code agent whose prompt opens with `OPERATION: implement` and carries `COMPLIANCE_FRAMEWORKS` and `DECISIONS_CONTEXT`, with FULL intent context:
|
|
990
1017
|
- Both ticket descriptions and plans
|
|
991
1018
|
- The conflicting diff sections (both sides)
|
|
992
1019
|
- Relevant ADRs from DECISIONS_CONTEXT (loaded by main model before authoring)
|
|
993
1020
|
3. Code agent resolves to PRESERVE BOTH INTENTS — the resolution must honor what both tickets were trying to achieve
|
|
994
1021
|
4. If the correct resolution is NOT UNAMBIGUOUS from the intent context: **do NOT guess** → quarantine + escalate
|
|
995
|
-
5. After any resolution: Validate
|
|
1022
|
+
5. After any resolution: the post-merge Validate covers it — the workflow spawns it after the merge, and no Validate is requested from Git
|
|
996
1023
|
|
|
997
1024
|
**Conservative-or-escalate is absolute.** An LLM silently guessing a wrong merge is the highest-danger failure mode in the whole design. When in doubt: quarantine + surface in report. The user re-runs (resume) with the escalated context.
|
|
998
1025
|
|
|
@@ -1007,7 +1034,7 @@ A workflow cannot pause mid-run (F4). "Escalate" means: quarantine-and-continue
|
|
|
1007
1034
|
- Ticket engine FAIL after max retries (Gate 1 exhausted)
|
|
1008
1035
|
- Gate 2 FAIL after max retries (Evaluate agent/Test agent not satisfied)
|
|
1009
1036
|
- Circular dependency detected (all remaining tickets blocked on each other)
|
|
1010
|
-
- Build red after merge (Validate agent fails
|
|
1037
|
+
- Build red after merge (the post-merge Validate agent fails: the merge is undone and the ticket quarantined; a refused undo halts the wave)
|
|
1011
1038
|
- Review coverage incomplete after retry (a focus area failed to produce a live Review agent result after the retry)
|
|
1012
1039
|
- Ticket engine crash/stall (unrecoverable exception or watchdog kill — quarantine cascades to dependents)
|
|
1013
1040
|
- No ticket link while issues are required (the engine stops before implementing)
|
|
@@ -1025,7 +1052,7 @@ A workflow cannot pause mid-run (F4). "Escalate" means: quarantine-and-continue
|
|
|
1025
1052
|
|
|
1026
1053
|
**Wave workflow structure (author after the SINGLE engine blocks above):**
|
|
1027
1054
|
|
|
1028
|
-
The wave workflow uses the same phases as SINGLE but wraps them in a wave loop. The integration branch is `wave/<initiative>` — the initiative slug, referenced below as `{slug}` — (or the user's current branch). Each ticket works on the branch setup-task created. The Git agent manages worktrees for parallel-eligible tickets. After every merge
|
|
1055
|
+
The wave workflow uses the same phases as SINGLE but wraps them in a wave loop. The integration branch is `wave/<initiative>` — the initiative slug, referenced below as `{slug}` — (or the user's current branch). Each ticket works on the branch setup-task created. The Git agent manages worktrees for parallel-eligible tickets. After every merge the workflow spawns the Validate agent (build + test) and undoes a red merge. Escalations accumulate in a list; the final report lists all of them.
|
|
1029
1056
|
|
|
1030
1057
|
Wave skeleton — compact reference (see `wave_loop()` doctrine for full semantics):
|
|
1031
1058
|
|
|
@@ -1036,11 +1063,12 @@ Wave skeleton — compact reference (see `wave_loop()` doctrine for full semanti
|
|
|
1036
1063
|
const WAVE_TICKETS = [...remainingTickets]; // the pre-fetch's refs, in input order — the wave block's row order
|
|
1037
1064
|
const results = {}; // ticketId → its row of the workflow's return; a ticket with no entry never ran
|
|
1038
1065
|
// A row from an engine result: the fields step 3 after the workflow renders
|
|
1039
|
-
const rowOf = (ticketId, r, merged) => ({ ticket: ticketId, ran: true, verdict: r?.verdict || r?.overallVerdict || null, merged, issuePrLink: r?.issuePrLink || "(none)", evaluateVerdict: r?.gate2?.evaluateVerdict, testVerdict: r?.gate2?.testVerdict, surviving: r?.survivingFindings?.length, coverageComplete: r?.reviewCoverage?.complete });
|
|
1066
|
+
const rowOf = (ticketId, r, merged, treeEqual = null) => ({ ticket: ticketId, ran: true, verdict: r?.verdict || r?.overallVerdict || null, merged, treeEqual, issuePrLink: r?.issuePrLink || "(none)", evaluateVerdict: r?.gate2?.evaluateVerdict, testVerdict: r?.gate2?.testVerdict, surviving: r?.survivingFindings?.length, coverageComplete: r?.reviewCoverage?.complete });
|
|
1040
1067
|
const MAX_ROUNDS = Math.max(10, remainingTickets.length * 2 + 5); // heuristic; always finite
|
|
1041
|
-
const
|
|
1068
|
+
const MERGE_SHA = /^[0-9a-f]{40}$/; // a merge commit as git prints it: the only Git-reported value that reaches a later prompt
|
|
1069
|
+
const waveState = { quarantined: [], round: 0, halted: null };
|
|
1042
1070
|
let reAskedThisDeadlock = false; // re-ask guard: re-ask once on empty ready-set, then escalate
|
|
1043
|
-
while (remainingTickets.length > 0 && waveState.round < MAX_ROUNDS) {
|
|
1071
|
+
while (remainingTickets.length > 0 && waveState.round < MAX_ROUNDS && waveState.halted === null) {
|
|
1044
1072
|
waveState.round++;
|
|
1045
1073
|
|
|
1046
1074
|
// Design agent reader (opus) applies vacuous-truth rule: a ticket with no unmet dependencies is ALWAYS ready.
|
|
@@ -1073,10 +1101,36 @@ Return: {"ready": [...ticket-ids], "blocked": [{"ticket": "id", "namedBlocker":
|
|
|
1073
1101
|
// Check both verdict (engine_output_schema) and overallVerdict (SINGLE skeleton alias).
|
|
1074
1102
|
// PASS and UNVERIFIED merge; PARTIAL, FAIL, ESCALATED (the ticket-link and branch stops included) or no verdict quarantine.
|
|
1075
1103
|
if (["PASS", "UNVERIFIED"].includes(engineResult.verdict || engineResult.overallVerdict)) {
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1104
|
+
// Git merges locally and validates nothing: the workflow owns the post-merge Validate (D-WAVE-MERGE-VALIDATE).
|
|
1105
|
+
const merge = await agent(`Merge ${engineResult.branch} to ${INTEGRATION_BRANCH} locally. Include ticket ID ${ticketId} in the merge commit message. Do not push, and run no build or test.
|
|
1106
|
+
Return: {"merged": true, "mergeSha": "<40-hex merge commit>", "treeEqual": <true when mergeSha^{tree} equals the tree of ${engineResult.branch}'s head>} — or {"merged": false, "reason": "<why>"} when no merge is kept.`, { agentType: "Git" });
|
|
1107
|
+
let kept = false;
|
|
1108
|
+
let reason = merge?.reason || "merge failed";
|
|
1109
|
+
if (merge?.merged === true && !MERGE_SHA.test(String(merge.mergeSha))) {
|
|
1110
|
+
// A merge may sit on the integration branch with no SHA to validate or undo it by: stop taking merges.
|
|
1111
|
+
waveState.halted = { ticket: ticketId, mergeSha: null, reason: "merge reported without a 40-hex mergeSha; the integration branch may hold an unvalidated merge" };
|
|
1112
|
+
reason = waveState.halted.reason;
|
|
1113
|
+
} else if (merge?.merged === true) {
|
|
1114
|
+
// A merge counts as kept only when this Validate returns PASS. It always runs: treeEqual is recorded, never a reason to skip.
|
|
1115
|
+
const post = await agent(`Run build, typecheck, lint, and tests on ${INTEGRATION_BRANCH} after merging ${engineResult.branch} (merge commit ${merge.mergeSha}).
|
|
1116
|
+
Follow your Running commands block for every build and test command.
|
|
1117
|
+
Return: {"verdict": "PASS" | "FAIL", "details": "..."}`, { agentType: "Validate" });
|
|
1118
|
+
if (post?.verdict === "PASS") {
|
|
1119
|
+
kept = true;
|
|
1120
|
+
} else {
|
|
1121
|
+
const undo = await agent(`Undo the merge ${merge.mergeSha} on ${INTEGRATION_BRANCH}, only when no remote branch contains ${merge.mergeSha} and the HEAD of ${INTEGRATION_BRANCH} still equals ${merge.mergeSha}. On the integration branch run exactly this one command and no other that changes a branch or a remote: git reset --keep ${merge.mergeSha}^1
|
|
1122
|
+
Return: {"undone": true} — or {"undone": false, "reason": "<why>"} when you did not undo it.`, { agentType: "Git" });
|
|
1123
|
+
if (undo?.undone === true) {
|
|
1124
|
+
reason = "post-merge build red; merge undone";
|
|
1125
|
+
} else {
|
|
1126
|
+
// A merge that stays red would poison every later ticket: stop taking merges and rounds, and name the red HEAD.
|
|
1127
|
+
reason = `post-merge build red; merge not undone (${undo?.reason || "no reason given"})`;
|
|
1128
|
+
waveState.halted = { ticket: ticketId, mergeSha: merge.mergeSha, reason };
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
results[ticketId] = rowOf(ticketId, engineResult, kept, merge?.merged === true && typeof merge.treeEqual === "boolean" ? merge.treeEqual : null);
|
|
1133
|
+
if (!kept) waveState.quarantined.push({ ticket: ticketId, reason });
|
|
1080
1134
|
} else {
|
|
1081
1135
|
results[ticketId] = rowOf(ticketId, engineResult, false);
|
|
1082
1136
|
waveState.quarantined.push({ ticket: ticketId, reason: engineResult.escalations?.[0]?.description || "engine fail/escalated" });
|
|
@@ -1087,11 +1141,12 @@ Return: {"merged": true} — or {"merged": false, "reason": "<why>"} when the me
|
|
|
1087
1141
|
waveState.quarantined.push({ ticket: ticketId, reason: `engine crash: ${String(err)}` });
|
|
1088
1142
|
}
|
|
1089
1143
|
remainingTickets = remainingTickets.filter(t => t !== ticketId);
|
|
1144
|
+
if (waveState.halted !== null) break; // a red integration HEAD takes no further merge in this round
|
|
1090
1145
|
}
|
|
1091
1146
|
}
|
|
1092
1147
|
|
|
1093
1148
|
// After the wave report is written: one row per wave ticket, in input order. No entry ⇒ never ran (cascade, deadlock, MAX_ROUNDS).
|
|
1094
|
-
return { tickets: WAVE_TICKETS.map(t => results[t] || { ticket: t, ran: false, verdict: null, merged: false, issuePrLink: "(none)" }), quarantined: waveState.quarantined };
|
|
1149
|
+
return { tickets: WAVE_TICKETS.map(t => results[t] || { ticket: t, ran: false, verdict: null, merged: false, treeEqual: null, issuePrLink: "(none)" }), quarantined: waveState.quarantined, halted: waveState.halted };
|
|
1095
1150
|
```
|
|
1096
1151
|
|
|
1097
1152
|
---
|
|
@@ -1115,6 +1170,7 @@ A workflow cannot pause mid-run. After the build/wave workflow returns, you (the
|
|
|
1115
1170
|
In WAVE mode, if no tracking-issue number was resolved in Pre-authoring step 5: state `TRACEABILITY: DEGRADED (no tracking issue for this run)` in the run summary and skip — never skip silently.
|
|
1116
1171
|
3. **Compose the wave PR inputs** (WAVE mode only — skip this step entirely in SINGLE mode). The workflow returned `tickets`: one entry per wave ticket, in its input order, each `{ticket, ran, verdict, merged, issuePrLink, evaluateVerdict, testVerdict, surviving, coverageComplete}`. Every value in it is agent-reported and treated as untrusted: nothing below is repaired, and no reference is ever composed from a number.
|
|
1117
1172
|
- **Branch check.** Run `git -C "{integration worktree root}" branch --show-current`. Only a name matching `^wave/[a-z0-9][a-z0-9-]{0,59}$` opens a wave PR, and its part after `wave/` is the `{slug}` below. Any other name: record `TRACEABILITY: DEGRADED (not a wave branch)` and go to step 4 with no wave PR.
|
|
1173
|
+
- **Halted** (the workflow returned a non-null `halted`: a post-merge build it could not undo, or a merge reported with no SHA): the integration HEAD may be red. Record `Wave PR: skipped (integration HEAD red, wave halted at <halted.ticket>)`, name `halted.mergeSha` (or `none reported`) and `halted.reason` as the red integration HEAD in the run summary, and go to step 4 with no wave PR.
|
|
1118
1174
|
- **Nothing merged** (no entry has `merged: true`): record `Wave PR: skipped (nothing merged)` and go to step 4.
|
|
1119
1175
|
- Otherwise compose (a) and then (b).
|
|
1120
1176
|
|
|
@@ -1210,17 +1266,7 @@ git -C "{integration worktree root}" push origin HEAD; echo "exit=$?"
|
|
|
1210
1266
|
|
|
1211
1267
|
**(d) Refresh.** Resolve the publication value for the integration worktree:
|
|
1212
1268
|
|
|
1213
|
-
**Resolve
|
|
1214
|
-
|
|
1215
|
-
```bash
|
|
1216
|
-
node "$HOME/.devflow/scripts/resolve-settings.cjs" "{root}" 2>/dev/null; echo "exit=$?"
|
|
1217
|
-
```
|
|
1218
|
-
|
|
1219
|
-
Accept the output only when it is exactly two lines: `exit=0` last and, before it, one line of the form `TRACKER=<github|jira|linear> TRACKER_SOURCE=<project|personal|machine|default> TRACKER_WARN=<none|mismatch|invalid> SITE=<none|https://<host>> KEY=<none|<key>> REVIEW_PUBLICATION=<off|auto|full> COMPLIANCE=<off|generic|<id>[,<id>…]> MEMORY=<on|off> LEARNING=<on|off> KNOWLEDGE=<on|off>` — these fields, in this order, nothing else, where `<host>` is a lowercase dotted host name alone, `<key>` is 2–10 of `A-Z`, `0-9` and `_` starting with a letter, and each `<id>` is one of `gdpr`, `hipaa`, `pci-dss`, `soc2`, `iso-27001`, `sox`. **Anything else** (a non-zero exit, no line, extra text, or a missing, reordered or unlisted field or value) ⇒ use `TRACKER=github TRACKER_SOURCE=default TRACKER_WARN=invalid SITE=none KEY=none REVIEW_PUBLICATION=off COMPLIANCE=generic MEMORY=on LEARNING=on KNOWLEDGE=off` instead.
|
|
1220
|
-
|
|
1221
|
-
The accepted line is the only source of these values: the script alone folds the committed `.devflow/project.json`, the personal `.devflow/config.json` and the machine manifest.
|
|
1222
|
-
|
|
1223
|
-
**Resolve `REVIEW_PUBLICATION` per worktree:** take `REVIEW_PUBLICATION` from that worktree's settings line, with `{root}` the worktree's root — multi-worktree repos may resolve different values per worktree. The line already caps the personal choice at the team's (D-PUBLICATION-CEILING), so it is `off`, `auto` or `full`, and `off` when the line was unresolvable.
|
|
1269
|
+
**Resolve `REVIEW_PUBLICATION` per worktree:** take `REVIEW_PUBLICATION` from that worktree's settings line — the line resolved above for `{root}`, the worktree's root, by the settings block when this run has not yet resolved that root; multi-worktree repos may resolve different values per worktree. The line already caps the personal choice at the team's (D-PUBLICATION-CEILING), so it is `off`, `auto` or `full`, and `off` when the line was unresolvable.
|
|
1224
1270
|
|
|
1225
1271
|
**Evidence stub:** only when `EVIDENCE_POLICY` is `required`, a resolved `off` becomes `stub`, so a counts-only record still reaches the PR. `stub` is never a config value: the settings line never carries it.
|
|
1226
1272
|
|