mandrel 2.1.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/agents/acceptance-critic.md +11 -2
- package/.agents/agents/story-worker.md +4 -2
- package/.agents/docs/SDLC.md +11 -4
- package/.agents/docs/configuration.md +1 -1
- package/.agents/docs/quality-gates.md +3 -3
- package/.agents/rules/gherkin-standards.md +10 -0
- package/.agents/schemas/acceptance-eval-verdict.schema.json +2 -2
- package/.agents/schemas/agentrc.schema.json +1 -1
- package/.agents/scripts/acceptance-eval.js +2 -2
- package/.agents/scripts/lib/config/acceptance-eval.js +2 -2
- package/.agents/scripts/lib/config-settings-schema-delivery.js +3 -3
- package/.agents/scripts/lib/orchestration/change-set.js +103 -0
- package/.agents/scripts/lib/orchestration/code-review.js +24 -35
- package/.agents/scripts/lib/orchestration/plan-context.js +2 -9
- package/.agents/scripts/lib/orchestration/plan-critic-conditions.js +17 -16
- package/.agents/scripts/lib/orchestration/plan-critics-evaluate.js +28 -15
- package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +0 -25
- package/.agents/scripts/lib/orchestration/plan-text-hygiene.js +230 -0
- package/.agents/scripts/lib/orchestration/planning/decomposer-context.js +1 -2
- package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +1 -1
- package/.agents/scripts/lib/orchestration/story-close/phases/code-review.js +97 -255
- package/.agents/scripts/lib/orchestration/story-close/phases/local-lens-review.js +191 -0
- package/.agents/scripts/lib/orchestration/story-close/phases/review-core.js +120 -0
- package/.agents/scripts/lib/story-body/story-body.js +75 -8
- package/.agents/scripts/lib/templates/decomposer-prompts.js +8 -13
- package/.agents/scripts/lib/wave-runner/live-probe.js +315 -0
- package/.agents/scripts/plan-context.js +0 -1
- package/.agents/scripts/plan-critics.js +203 -0
- package/.agents/scripts/quality-preview.js +13 -6
- package/.agents/scripts/stories-wave-tick.js +307 -55
- package/.agents/workflows/deliver.md +50 -15
- package/.agents/workflows/helpers/acceptance-self-eval.md +14 -5
- package/.agents/workflows/helpers/code-quality-guardrails.md +7 -4
- package/.agents/workflows/helpers/code-review.md +2 -2
- package/.agents/workflows/helpers/deliver-story.md +22 -6
- package/.agents/workflows/plan.md +55 -0
- package/docs/CHANGELOG.md +22 -0
- package/lib/migrations/index.js +6 -1
- package/lib/migrations/steps/2.2.0-retire-epic-ac-tags.js +154 -0
- package/package.json +2 -2
|
@@ -34,7 +34,15 @@ verdicts, their commit-message justifications-as-proof, or any prior verdict
|
|
|
34
34
|
file they authored. You grade the **work product**, not the homework the maker
|
|
35
35
|
turned in about it. Your only trusted inputs are:
|
|
36
36
|
|
|
37
|
-
- the
|
|
37
|
+
- the **change set** your caller hands you: the list of files this Story
|
|
38
|
+
touched, computed **once** per delivery by the shared `computeChangeSet`
|
|
39
|
+
enumerator (`.agents/scripts/lib/orchestration/change-set.js`) and threaded
|
|
40
|
+
into your spawn context. Read those files and inspect their changes to see
|
|
41
|
+
the work product. Do **not** re-derive the set yourself — re-enumerating it
|
|
42
|
+
can pick up commits that landed after your caller routed the ceremony, and
|
|
43
|
+
then you would be scoring a different change than the one you were dispatched
|
|
44
|
+
for (Story #4593). If no change set reached you, say so in your verdict
|
|
45
|
+
rather than substituting your own enumeration.
|
|
38
46
|
- the Story's inline `acceptance[]` and `verify[]` arrays, read from the
|
|
39
47
|
**Story body itself** (`gh issue view <storyId> --json body`) — its `##
|
|
40
48
|
Acceptance` / `## Verify` sections are the SSOT. The `story-init` structured
|
|
@@ -58,7 +66,8 @@ given.
|
|
|
58
66
|
|
|
59
67
|
For each acceptance item in your cluster:
|
|
60
68
|
|
|
61
|
-
1. **Inspect the
|
|
69
|
+
1. **Inspect the change set** — read the files your caller named and look for
|
|
70
|
+
the change that would satisfy the criterion.
|
|
62
71
|
2. **Run the relevant `verify[]` commands** and consume their output as
|
|
63
72
|
**required evidence**. A criterion cannot be scored `met` without the
|
|
64
73
|
supporting `verify[]` evidence where a `verify[]` command is relevant to it.
|
|
@@ -101,8 +101,10 @@ self-eval loop (below) may share `lint` / `typecheck` evidence with close via
|
|
|
101
101
|
After the implementation commits land and **before** flipping to `closing`, run
|
|
102
102
|
the bounded acceptance self-eval loop (see
|
|
103
103
|
[`acceptance-self-eval.md`](../workflows/helpers/acceptance-self-eval.md)). It
|
|
104
|
-
scores the
|
|
105
|
-
|
|
104
|
+
scores the change set you computed once and injected into the critic — never
|
|
105
|
+
one the critic re-derives (Story #4593) — against **each** `acceptance[]` item,
|
|
106
|
+
and consumes the `verify[]` command output as **required evidence**. The gate
|
|
107
|
+
returns one of:
|
|
106
108
|
|
|
107
109
|
- **`proceed`** (every criterion met) → flip to `closing` and close.
|
|
108
110
|
- **`redraft`** (rounds remaining) → fix the flagged criteria, commit, re-eval.
|
package/.agents/docs/SDLC.md
CHANGED
|
@@ -40,8 +40,9 @@ From zero to shipped:
|
|
|
40
40
|
|
|
41
41
|
`/plan` is a **single path** — there is no Epic/Story router, no
|
|
42
42
|
scope-triage `epic|story` verdict, and no `deliveryShape`. All GitHub
|
|
43
|
-
reads happen in `plan-context.js`,
|
|
44
|
-
two HITL gates bracket the authoring middle.
|
|
43
|
+
reads happen in `plan-context.js`, the issue-creating writes in
|
|
44
|
+
`plan-persist.js`, and two HITL gates bracket the authoring middle.
|
|
45
|
+
Duplicate search targets
|
|
45
46
|
open **Stories** (`type::story`), never Epics.
|
|
46
47
|
|
|
47
48
|
1. **Interrogate** — `plan-context.js` emits the single authoring
|
|
@@ -54,6 +55,12 @@ From zero to shipped:
|
|
|
54
55
|
Binding criteria live in top-level `acceptance[]` / `verify[]`;
|
|
55
56
|
changes/references are `{ path, assumption }` objects. Split into
|
|
56
57
|
N>1 only under the default-single split policy.
|
|
58
|
+
2.5. **Critics** — `plan-critics.js` evaluates the consolidation +
|
|
59
|
+
pre-mortem dispatch conditions against the authored draft and ledgers
|
|
60
|
+
every skip. This is the **only** critic gate (#4592 moved it out of
|
|
61
|
+
`plan-persist.js` into workflow prose), so skipping it silently skips
|
|
62
|
+
both critics: run it before Persist, per
|
|
63
|
+
[`/plan`](../workflows/plan.md) step 2.5.
|
|
57
64
|
3. **Persist** — **gate #2** (raised only by an explicit `--force-review`)
|
|
58
65
|
then `plan-persist.js` runs every deterministic gate and
|
|
59
66
|
creates Story issue(s) with `type::story` + `agent::ready`, writing
|
|
@@ -76,8 +83,8 @@ From zero to shipped:
|
|
|
76
83
|
2. **Implement** — the agent delivers the Story in one guarded session
|
|
77
84
|
against its inline `acceptance[]` / `verify[]` contract (optional
|
|
78
85
|
`## Slicing` intra-session checkpoints).
|
|
79
|
-
3. **Acceptance self-eval** — a bounded critic loop scores the
|
|
80
|
-
|
|
86
|
+
3. **Acceptance self-eval** — a bounded critic loop scores the
|
|
87
|
+
caller-injected change set against each acceptance item before close (see
|
|
81
88
|
[`helpers/acceptance-self-eval`](../workflows/helpers/acceptance-self-eval.md)).
|
|
82
89
|
4. **Ceremony** — acceptance critic mode and review depth, both routed off
|
|
83
90
|
the change level derived from the Story's own diff
|
|
@@ -263,7 +263,7 @@ top-level keys are validation errors.
|
|
|
263
263
|
| `codeReview.autoFixSeverity` | No | `"high"` \| `"medium"` | `"medium"` | Severity threshold for on-branch remediation in /deliver Phase 5 (code-review). `medium` (default) routes 🔴/🟠/🟡 findings into the host-LLM focused-fix routing (Mediums batched per lens: one commit per lens, a single validation + rescan at the end) while 🟢 suggestions still graduate to follow-up issues; `high` reproduces the pre-4399 Critical/High-only routing. Hard cutover — no back-compat flag. |
|
|
264
264
|
| `refactorStage` | No | `object` | — | Opt-in, config-gated post-green refactor checkpoint wired into story-deliver (Story #3430, Epic #3418). Strictly additive and default-OFF: when disabled, story-deliver behaves exactly as before. Advisory only — never changes existing close-validation gate semantics. |
|
|
265
265
|
| `refactorStage.enabled` | No | `boolean` | `false` | When true, story-deliver runs an advisory post-green refactor stage (core/code-review-and-quality skill, Post-Green Refactor Pass) after the suite is green. Default false — when unset the stage is skipped and close-validation gate semantics are unchanged. |
|
|
266
|
-
| `acceptanceEval` | No | `object` | — | Story #3819. Bounded per-Story acceptance self-eval loop. After the implementation commits land and before the Story-implementation phase flips to `closing`, an independent (fresh-context) critic pass scores the
|
|
266
|
+
| `acceptanceEval` | No | `object` | — | Story #3819. Bounded per-Story acceptance self-eval loop. After the implementation commits land and before the Story-implementation phase flips to `closing`, an independent (fresh-context) critic pass scores the caller-injected change set against each inline `acceptance[]` item, redrafts the unmet items, and re-evaluates — capped at `maxRounds` redraft rounds, then escalates to `agent::blocked` when criteria remain unmet. There is no `enabled` flag: the loop is a hard cutover (always on). |
|
|
267
267
|
| `acceptanceEval.maxRounds` | No | `integer` | — | Maximum number of redraft rounds before escalation. Default 2; clamped into [1, hard ceiling] by lib/config/acceptance-eval.js so the cap can never be disabled (maxRounds: 0 clamps up to 1). |
|
|
268
268
|
| `acceptanceEval.clusterCeiling` | No | `integer` | — | Epic #4475 (M4-B). Max acceptance criteria one single-delivery acceptance critic scores in a single fresh-context pass. Single delivery clusters the Epic ## Acceptance Table ACs into ceil(totalACs / clusterCeiling) groups and spawns one maker-blind critic per cluster, restoring the distributed acceptance coverage the per-Story critic fan-out gave for free. Default 4; clamped into [1, 8] by lib/config/acceptance-eval.js so a large value cannot collapse the fan-out to a single diluted critic. Ignored on the fan-out route. |
|
|
269
269
|
| `ci` | No | `object` | — | Nested configuration block. |
|
|
@@ -260,9 +260,9 @@ per-round mechanic lives in
|
|
|
260
260
|
with the gate CLI at
|
|
261
261
|
[`.agents/scripts/acceptance-eval.js`](../scripts/acceptance-eval.js)).
|
|
262
262
|
Each round, a fresh-context **critic pass** — independent of the
|
|
263
|
-
implementing agent — scores the
|
|
264
|
-
`acceptance[]` item, using `verify[]`
|
|
265
|
-
of three decisions:
|
|
263
|
+
implementing agent — scores the change set its caller injected (never one
|
|
264
|
+
it re-derives) against every inline `acceptance[]` item, using `verify[]`
|
|
265
|
+
output as evidence, and yields one of three decisions:
|
|
266
266
|
|
|
267
267
|
- **proceed** — all criteria met; the Story continues to close.
|
|
268
268
|
- **redraft** — unmet criteria are redrafted and re-implemented, then
|
|
@@ -23,6 +23,16 @@ scenarios. Use the canonical set below; do not invent ad-hoc tags.
|
|
|
23
23
|
- `@flaky` — operational quarantine tag. Scenarios carrying this tag are
|
|
24
24
|
excluded from the gating suite and run in a dedicated non-blocking job
|
|
25
25
|
until stabilized. Treat `@flaky` as a debt marker, not a permanent label.
|
|
26
|
+
- `@skip` — scaffold-gating tag. Applied to scenarios scaffolded ahead of
|
|
27
|
+
their implementation (e.g. by a wave-0 BDD scaffold Story); the scenario
|
|
28
|
+
is excluded from the gating suite until the implementing Story removes
|
|
29
|
+
the tag (the "de-skip" edit). Unlike `@flaky`, `@skip` marks planned
|
|
30
|
+
not-yet-implemented behavior, never a stability problem.
|
|
31
|
+
|
|
32
|
+
Retired: the `@epic-<id>-ac-N` namespaced AC tag. Its consumer
|
|
33
|
+
(`acceptance-spec-reconciler.js`) was deleted in the v2 Epic removal, so
|
|
34
|
+
the tag is inert — do not apply it to new scenarios. The `mandrel update`
|
|
35
|
+
migration strips surviving instances from consumer feature files.
|
|
26
36
|
|
|
27
37
|
Rules:
|
|
28
38
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
3
|
"$id": "acceptance-eval-verdict",
|
|
4
4
|
"title": "Acceptance Self-Eval Verdict",
|
|
5
|
-
"description": "Structured output of one independent (fresh-context) acceptance self-eval round (Story #3819). Produced by the critic pass in the Story-implementation phase (helpers/deliver-story.md Step 1a), written under the gitignored temp tree, and consumed by acceptance-eval.js to enforce the bounded round cap and decide whether the Story proceeds to `closing`, redrafts, or escalates to `agent::blocked`. The author MUST NOT be the evaluator — the verdict is an independent judgment of the
|
|
5
|
+
"description": "Structured output of one independent (fresh-context) acceptance self-eval round (Story #3819). Produced by the critic pass in the Story-implementation phase (helpers/deliver-story.md Step 1a), written under the gitignored temp tree, and consumed by acceptance-eval.js to enforce the bounded round cap and decide whether the Story proceeds to `closing`, redrafts, or escalates to `agent::blocked`. The author MUST NOT be the evaluator — the verdict is an independent judgment of the caller-injected change set against each inline acceptance[] item.",
|
|
6
6
|
"type": "object",
|
|
7
7
|
"required": ["storyId", "schemaVersion", "round", "criteria"],
|
|
8
8
|
"properties": {
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"verdict": {
|
|
53
53
|
"type": "string",
|
|
54
54
|
"enum": ["met", "partial", "unmet"],
|
|
55
|
-
"description": "Per-criterion judgment of the
|
|
55
|
+
"description": "Per-criterion judgment of the caller-injected change set. `met` clears the criterion; `partial` and `unmet` both trigger a redraft round (subject to the cap)."
|
|
56
56
|
},
|
|
57
57
|
"evidence": {
|
|
58
58
|
"type": "string",
|
|
@@ -1246,7 +1246,7 @@
|
|
|
1246
1246
|
},
|
|
1247
1247
|
"acceptanceEval": {
|
|
1248
1248
|
"type": "object",
|
|
1249
|
-
"description": "Story #3819. Bounded per-Story acceptance self-eval loop. After the implementation commits land and before the Story-implementation phase flips to `closing`, an independent (fresh-context) critic pass scores the
|
|
1249
|
+
"description": "Story #3819. Bounded per-Story acceptance self-eval loop. After the implementation commits land and before the Story-implementation phase flips to `closing`, an independent (fresh-context) critic pass scores the caller-injected change set against each inline `acceptance[]` item, redrafts the unmet items, and re-evaluates — capped at `maxRounds` redraft rounds, then escalates to `agent::blocked` when criteria remain unmet. There is no `enabled` flag: the loop is a hard cutover (always on).",
|
|
1250
1250
|
"properties": {
|
|
1251
1251
|
"maxRounds": {
|
|
1252
1252
|
"type": "integer",
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* acceptance-eval.js — bounded per-Story acceptance self-eval gate (Story #3819).
|
|
5
5
|
*
|
|
6
6
|
* The Story-implementation phase runs an independent (fresh-context)
|
|
7
|
-
* critic pass that scores the
|
|
8
|
-
* `acceptance[]` item, emitting one verdict file per round
|
|
7
|
+
* critic pass that scores the caller-injected change set against each
|
|
8
|
+
* inline `acceptance[]` item, emitting one verdict file per round
|
|
9
9
|
* (`.agents/schemas/acceptance-eval-verdict.schema.json`). This CLI is the
|
|
10
10
|
* deterministic substrate that turns that verdict into the loop's next
|
|
11
11
|
* action:
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Resolves `.agentrc.json → delivery.acceptanceEval` into the canonical
|
|
5
5
|
* shape the per-Story acceptance self-eval loop consumes. The loop scores
|
|
6
|
-
* the
|
|
7
|
-
* unmet items, and re-evaluates — capped at `maxRounds`
|
|
6
|
+
* the caller-injected change set against each inline `acceptance[]` item,
|
|
7
|
+
* redrafts the unmet items, and re-evaluates — capped at `maxRounds` rounds,
|
|
8
8
|
* then escalates to `agent::blocked` when criteria remain unmet.
|
|
9
9
|
*
|
|
10
10
|
* ## The undisableable cap
|
|
@@ -249,9 +249,9 @@ const REFACTOR_STAGE_SCHEMA = {
|
|
|
249
249
|
* `delivery.acceptanceEval` — bounded per-Story acceptance self-eval loop
|
|
250
250
|
* (Story #3819). After the implementation commits land and before the
|
|
251
251
|
* Story-implementation phase flips to `closing`, an independent
|
|
252
|
-
* (fresh-context) critic pass scores the
|
|
253
|
-
* `acceptance[]` item, redrafts the unmet items, and
|
|
254
|
-
* at `maxRounds` redraft rounds.
|
|
252
|
+
* (fresh-context) critic pass scores the caller-injected change set against
|
|
253
|
+
* each inline `acceptance[]` item, redrafts the unmet items, and
|
|
254
|
+
* re-evaluates — capped at `maxRounds` redraft rounds.
|
|
255
255
|
*
|
|
256
256
|
* `maxRounds` is the operator-tunable redraft ceiling (default 2 via
|
|
257
257
|
* `lib/config/acceptance-eval.js`). It is a soft knob inside an
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/orchestration/change-set.js — the **one** Story change-set enumerator
|
|
3
|
+
* (Story #4593).
|
|
4
|
+
*
|
|
5
|
+
* ## Why this exists
|
|
6
|
+
*
|
|
7
|
+
* A single Story delivery used to enumerate `git diff --name-only
|
|
8
|
+
* <base>...<head>` three to four separate times: once for the ceremony
|
|
9
|
+
* derivation in `helpers/deliver-story`, once inside `runCodeReview` (to derive
|
|
10
|
+
* the review depth), once for the Story-scope local-lens roster, and once more
|
|
11
|
+
* per fresh acceptance critic. Every one of those consumers must agree about
|
|
12
|
+
* *what changed* — ceremony level and review depth both flow from
|
|
13
|
+
* `deriveChangeLevel`, so two enumerations straddling a commit could route the
|
|
14
|
+
* same Story two different ways. Computing the set once and injecting it closes
|
|
15
|
+
* that window and drops the redundant git calls.
|
|
16
|
+
*
|
|
17
|
+
* ## Contract
|
|
18
|
+
*
|
|
19
|
+
* {@link computeChangeSet} is **total**: it never throws. A diff it cannot
|
|
20
|
+
* enumerate (git failure, missing ref, spawn error) yields
|
|
21
|
+
* `{ files: null, enumerated: false }` — the neutral "diff unknown" signal that
|
|
22
|
+
* `deriveChangeLevel` / `resolveDepth` already fail safe on (`standard` depth, a
|
|
23
|
+
* `fresh` critic). `null` is deliberately distinct from `[]`: an empty array is
|
|
24
|
+
* the *fact* that nothing changed, whereas `null` is the *absence* of evidence,
|
|
25
|
+
* and only the latter must never buy a change less checking.
|
|
26
|
+
*
|
|
27
|
+
* The returned `files` are trimmed, de-duplicated, and sorted, so two consumers
|
|
28
|
+
* comparing the same change set compare byte-identical lists. The refs the set
|
|
29
|
+
* was computed against ride along on the envelope so a downstream consumer can
|
|
30
|
+
* report (or assert) its provenance rather than re-deriving it.
|
|
31
|
+
*
|
|
32
|
+
* @typedef {{
|
|
33
|
+
* baseRef: string,
|
|
34
|
+
* headRef: string,
|
|
35
|
+
* files: string[]|null,
|
|
36
|
+
* enumerated: boolean,
|
|
37
|
+
* }} ChangeSet
|
|
38
|
+
*
|
|
39
|
+
* @typedef {typeof gitSpawn} GitSpawnFn
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
import { gitSpawn } from '../git-utils.js';
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Trim, drop empties, de-duplicate, and sort raw `git diff --name-only` output.
|
|
46
|
+
* Pure.
|
|
47
|
+
*
|
|
48
|
+
* @param {string} stdout
|
|
49
|
+
* @returns {string[]}
|
|
50
|
+
*/
|
|
51
|
+
function normalizeFileList(stdout) {
|
|
52
|
+
const seen = new Set();
|
|
53
|
+
for (const line of stdout.split('\n')) {
|
|
54
|
+
const file = line.trim();
|
|
55
|
+
if (file.length > 0) seen.add(file);
|
|
56
|
+
}
|
|
57
|
+
return [...seen].sort();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Compute the change set for the `baseRef...headRef` diff **once**, for every
|
|
62
|
+
* consumer that needs to know what a Story touched.
|
|
63
|
+
*
|
|
64
|
+
* Total — never throws; see the module header for the `null` vs `[]` contract.
|
|
65
|
+
*
|
|
66
|
+
* @param {{
|
|
67
|
+
* baseRef: string,
|
|
68
|
+
* headRef: string,
|
|
69
|
+
* cwd?: string,
|
|
70
|
+
* gitSpawnFn?: typeof gitSpawn,
|
|
71
|
+
* }} args
|
|
72
|
+
* @returns {ChangeSet}
|
|
73
|
+
*/
|
|
74
|
+
export function computeChangeSet({
|
|
75
|
+
baseRef,
|
|
76
|
+
headRef,
|
|
77
|
+
cwd = process.cwd(),
|
|
78
|
+
gitSpawnFn = gitSpawn,
|
|
79
|
+
} = {}) {
|
|
80
|
+
const unknown = { baseRef, headRef, files: null, enumerated: false };
|
|
81
|
+
if (typeof baseRef !== 'string' || baseRef.length === 0) return unknown;
|
|
82
|
+
if (typeof headRef !== 'string' || headRef.length === 0) return unknown;
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
const result = gitSpawnFn(
|
|
86
|
+
cwd,
|
|
87
|
+
'diff',
|
|
88
|
+
'--name-only',
|
|
89
|
+
`${baseRef}...${headRef}`,
|
|
90
|
+
);
|
|
91
|
+
if (!result || result.status !== 0 || typeof result.stdout !== 'string') {
|
|
92
|
+
return unknown;
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
baseRef,
|
|
96
|
+
headRef,
|
|
97
|
+
files: normalizeFileList(result.stdout),
|
|
98
|
+
enumerated: true,
|
|
99
|
+
};
|
|
100
|
+
} catch {
|
|
101
|
+
return unknown;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
|
|
42
42
|
import { hasSurvivingCritical } from '../audit-suite/findings.js';
|
|
43
43
|
import { resolveConfig } from '../config-resolver.js';
|
|
44
|
-
import {
|
|
44
|
+
import { computeChangeSet } from './change-set.js';
|
|
45
45
|
import { deriveChangeLevel, resolveDepth } from './review-depth.js';
|
|
46
46
|
import {
|
|
47
47
|
countBySeverity,
|
|
@@ -65,36 +65,6 @@ import { upsertStructuredComment } from './ticketing.js';
|
|
|
65
65
|
* @typedef {import('./review-depth.js').ReviewDepth} ReviewDepth
|
|
66
66
|
*/
|
|
67
67
|
|
|
68
|
-
/**
|
|
69
|
-
* Enumerate the files changed in the `baseRef...headRef` diff via
|
|
70
|
-
* `git diff --name-only`. Returns the file list, or `null` when the diff
|
|
71
|
-
* cannot be enumerated (git failure, missing ref). A `null` list is the neutral
|
|
72
|
-
* "diff unknown" signal both {@link deriveChangeLevel} and {@link resolveDepth}
|
|
73
|
-
* tolerate by failing safe to `standard`. Best-effort — never throws.
|
|
74
|
-
*
|
|
75
|
-
* @param {{ baseRef: string, headRef: string, gitSpawnFn?: typeof gitSpawn }} args
|
|
76
|
-
* @returns {string[]|null}
|
|
77
|
-
*/
|
|
78
|
-
function listChangedFiles({ baseRef, headRef, gitSpawnFn = gitSpawn }) {
|
|
79
|
-
try {
|
|
80
|
-
const result = gitSpawnFn(
|
|
81
|
-
process.cwd(),
|
|
82
|
-
'diff',
|
|
83
|
-
`${baseRef}...${headRef}`,
|
|
84
|
-
'--name-only',
|
|
85
|
-
);
|
|
86
|
-
if (!result || result.status !== 0 || typeof result.stdout !== 'string') {
|
|
87
|
-
return null;
|
|
88
|
-
}
|
|
89
|
-
return result.stdout
|
|
90
|
-
.split('\n')
|
|
91
|
-
.map((line) => line.trim())
|
|
92
|
-
.filter((line) => line.length > 0);
|
|
93
|
-
} catch {
|
|
94
|
-
return null;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
68
|
/**
|
|
99
69
|
* Resolve the project base branch fallback used when a caller omits
|
|
100
70
|
* `baseRef`.
|
|
@@ -192,7 +162,7 @@ function resolveScopeEnvelope(opts, config) {
|
|
|
192
162
|
* changedFileCount?: number|null,
|
|
193
163
|
* storyId?: number|null,
|
|
194
164
|
* reviewProvider?: { runReview: Function },
|
|
195
|
-
* gitSpawnFn?:
|
|
165
|
+
* gitSpawnFn?: import('./change-set.js').GitSpawnFn,
|
|
196
166
|
* resolveConfigFn?: typeof resolveConfig,
|
|
197
167
|
* createReviewProviderFn?: typeof createReviewProvider,
|
|
198
168
|
* upsertCommentFn?: typeof upsertStructuredComment,
|
|
@@ -229,6 +199,27 @@ function resolveProviderName(codeReviewConfig) {
|
|
|
229
199
|
return 'native';
|
|
230
200
|
}
|
|
231
201
|
|
|
202
|
+
/**
|
|
203
|
+
* Resolve the change set the depth derivation reads (Story #4593).
|
|
204
|
+
*
|
|
205
|
+
* `opts.changedFiles` is an injection with three distinct states,
|
|
206
|
+
* and the difference is load-bearing: an **array** is the change set to use
|
|
207
|
+
* verbatim; an explicit **null** is a caller (`runStoryReviewCore`) reporting
|
|
208
|
+
* that it already tried and the diff is unenumerable — re-running git here would
|
|
209
|
+
* only fail again, so it degrades straight to the fail-safe tier; **absent**
|
|
210
|
+
* means no caller enumerated at all, so the shared {@link computeChangeSet}
|
|
211
|
+
* enumerator runs as the fallback (standalone CLI use). On the close path the
|
|
212
|
+
* spine always injects, so the diff is enumerated exactly once per delivery and
|
|
213
|
+
* this pillar can never disagree with the lens pass about what changed.
|
|
214
|
+
*/
|
|
215
|
+
function resolveInjectedChangedFiles({ opts, baseRef, headRef }) {
|
|
216
|
+
if (opts.changedFiles === undefined) {
|
|
217
|
+
return computeChangeSet({ baseRef, headRef, gitSpawnFn: opts.gitSpawnFn })
|
|
218
|
+
.files;
|
|
219
|
+
}
|
|
220
|
+
return Array.isArray(opts.changedFiles) ? opts.changedFiles : null;
|
|
221
|
+
}
|
|
222
|
+
|
|
232
223
|
/**
|
|
233
224
|
* Build the provider `runReview` input, resolving the review depth from the
|
|
234
225
|
* diff under review: its changed files derive the change level (sensitive path
|
|
@@ -238,9 +229,7 @@ function resolveProviderName(codeReviewConfig) {
|
|
|
238
229
|
* Story #4075 — extracted from `runCodeReview`.
|
|
239
230
|
*/
|
|
240
231
|
function buildReviewInput({ opts, scope, ticketId, baseRef, headRef }) {
|
|
241
|
-
const changedFiles =
|
|
242
|
-
? opts.changedFiles
|
|
243
|
-
: listChangedFiles({ baseRef, headRef, gitSpawnFn: opts.gitSpawnFn });
|
|
232
|
+
const changedFiles = resolveInjectedChangedFiles({ opts, baseRef, headRef });
|
|
244
233
|
const changedFileCount =
|
|
245
234
|
typeof opts.changedFileCount === 'number'
|
|
246
235
|
? opts.changedFileCount
|
|
@@ -361,17 +361,12 @@ export function buildDeliveryShapeSignal({ body } = {}) {
|
|
|
361
361
|
* authoritative from day one); the decompose prompt reuses the existing
|
|
362
362
|
* Story #4162 carrier including the risk-heuristics suffix.
|
|
363
363
|
*
|
|
364
|
-
* @param {{ heuristics?: string[], maxTickets?: number
|
|
364
|
+
* @param {{ heuristics?: string[], maxTickets?: number }} args
|
|
365
365
|
* @returns {{ spec: string, acceptance: string, decompose: string }}
|
|
366
366
|
*/
|
|
367
|
-
export function buildSystemPrompts({
|
|
368
|
-
heuristics = [],
|
|
369
|
-
maxTickets,
|
|
370
|
-
epicId = null,
|
|
371
|
-
} = {}) {
|
|
367
|
+
export function buildSystemPrompts({ heuristics = [], maxTickets } = {}) {
|
|
372
368
|
const decompose = buildDecomposerSystemPrompt(heuristics, {
|
|
373
369
|
maxTickets,
|
|
374
|
-
epicId,
|
|
375
370
|
});
|
|
376
371
|
return {
|
|
377
372
|
spec: renderTechSpecSystemPrompt(),
|
|
@@ -500,7 +495,6 @@ async function buildSeedFileModeEnvelope({
|
|
|
500
495
|
systemPrompts: buildSystemPrompts({
|
|
501
496
|
heuristics,
|
|
502
497
|
maxTickets: limits.maxTickets,
|
|
503
|
-
epicId: null,
|
|
504
498
|
}),
|
|
505
499
|
planState: null,
|
|
506
500
|
// N=1 default: author one Story; skip Epic-scale decompose ceremony.
|
|
@@ -654,7 +648,6 @@ async function buildTicketsModeEnvelope({
|
|
|
654
648
|
systemPrompts: buildSystemPrompts({
|
|
655
649
|
heuristics,
|
|
656
650
|
maxTickets: limits.maxTickets,
|
|
657
|
-
epicId: null,
|
|
658
651
|
}),
|
|
659
652
|
planState: null,
|
|
660
653
|
planProfile:
|
|
@@ -2,15 +2,15 @@
|
|
|
2
2
|
* plan-critic-conditions.js — size/heuristic-conditional dispatch decisions for
|
|
3
3
|
* the /plan author-step critics (Epic #4474 PR6, design §4).
|
|
4
4
|
*
|
|
5
|
-
* The collapsed plan flow keeps the consolidation
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
5
|
+
* The collapsed plan flow keeps the consolidation and pre-mortem critics as
|
|
6
|
+
* fresh-context sub-agent dispatches, but makes each dispatch
|
|
7
|
+
* **conditional** instead of unconditional — the dominant plan cost is
|
|
8
|
+
* turns × standing context, and an unconditional critic pays a full
|
|
9
|
+
* sub-agent spawn even when it provably has nothing to find. This module
|
|
10
|
+
* computes those decisions deterministically so the workflow never judges
|
|
11
|
+
* its own dispatch conditions:
|
|
12
12
|
*
|
|
13
|
-
* - **Consolidation
|
|
13
|
+
* - **Consolidation**: dispatch only when the existing
|
|
14
14
|
* `evaluateConsolidationPrecondition` gate says `dispatch: true` AND
|
|
15
15
|
* (the draft has more than `CONSOLIDATION_STORY_THRESHOLD` stories OR
|
|
16
16
|
* the precondition confirmed a divergence from the Delivery Slicing
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* small draft is NOT a confirmed divergence — it skips, because a
|
|
19
19
|
* ≤-threshold draft is small enough for gate #2's single-view review
|
|
20
20
|
* to catch a distorted shape without a dedicated sub-agent.
|
|
21
|
-
* - **Pre-mortem
|
|
21
|
+
* - **Pre-mortem**: dispatch when the ticket count is at least half
|
|
22
22
|
* of `maxTickets`, OR any configured `planning.riskHeuristics` phrase
|
|
23
23
|
* matches the plan text (case-insensitive substring). Story #4542 removed
|
|
24
24
|
* its third condition — the authored risk verdict's overall level — along
|
|
@@ -28,18 +28,19 @@
|
|
|
28
28
|
* Under-firing risk (design PR6 note): the persist validators are
|
|
29
29
|
* unchanged hard gates and G2's cohort re-measures plan quality; every
|
|
30
30
|
* skip decision this module produces is logged to the plan-metrics ledger
|
|
31
|
-
* (`appendCriticSkip`) by the
|
|
31
|
+
* (`appendCriticSkip`) by the caller so under-firing is auditable.
|
|
32
32
|
*
|
|
33
|
-
* Pure, synchronous, no I/O
|
|
34
|
-
* `plan-
|
|
35
|
-
*
|
|
33
|
+
* Pure, synchronous, no I/O. The single caller is `plan-critics-evaluate.js`,
|
|
34
|
+
* driven by the `plan-critics.js` CLI that `/plan` runs between Author and
|
|
35
|
+
* Persist (Story #4592); the CLI owns reading the authored artifacts and the
|
|
36
|
+
* resolved config.
|
|
36
37
|
*/
|
|
37
38
|
|
|
38
39
|
import { evaluateConsolidationPrecondition } from './consolidation-precondition.js';
|
|
39
40
|
|
|
40
41
|
/**
|
|
41
42
|
* Draft-story count above which the consolidation critic fires even
|
|
42
|
-
* without a confirmed slicing divergence (
|
|
43
|
+
* without a confirmed slicing divergence (#4474 PR6: "> 5 stories").
|
|
43
44
|
*/
|
|
44
45
|
export const CONSOLIDATION_STORY_THRESHOLD = 5;
|
|
45
46
|
|
|
@@ -53,7 +54,7 @@ export const CONSOLIDATION_STORY_THRESHOLD = 5;
|
|
|
53
54
|
*/
|
|
54
55
|
|
|
55
56
|
/**
|
|
56
|
-
* Decide the
|
|
57
|
+
* Decide the consolidation dispatch: precondition AND size/divergence.
|
|
57
58
|
*
|
|
58
59
|
* @param {object} input
|
|
59
60
|
* @param {object[]} input.draftStories - The draft `tickets.json` array
|
|
@@ -106,7 +107,7 @@ export function evaluateConsolidationDispatch({ draftStories, specText }) {
|
|
|
106
107
|
}
|
|
107
108
|
|
|
108
109
|
/**
|
|
109
|
-
* Decide the
|
|
110
|
+
* Decide the pre-mortem dispatch: size ≥ ½ budget, or a risk-heuristic
|
|
110
111
|
* phrase match.
|
|
111
112
|
*
|
|
112
113
|
* @param {object} input
|
|
@@ -1,20 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* plan-critics-evaluate.js — shared critic-dispatch evaluation for the
|
|
3
|
-
* collapsed /plan flow (#4496 fix 6
|
|
4
|
-
* CLI so the persist surface folds the same evaluation in as a pre-write
|
|
5
|
-
* phase).
|
|
3
|
+
* collapsed /plan flow (#4496 fix 6).
|
|
6
4
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
5
|
+
* One consumer: the `plan-critics.js` CLI, which `/plan` runs between its
|
|
6
|
+
* Author and Persist steps. The CLI loads the draft artifacts, calls this
|
|
7
|
+
* module, prints the verdict as JSON, and records every skip on the
|
|
8
|
+
* plan-metrics ledger; the workflow dispatches a fresh-context critic
|
|
9
|
+
* sub-agent on a `dispatch: true` verdict and folds the findings into a
|
|
10
|
+
* re-author round before persist.
|
|
11
|
+
*
|
|
12
|
+
* Story #4592 moved that evaluation here from `run-plan-persist.js`, which
|
|
13
|
+
* ran it after authoring was finished and immediately before
|
|
14
|
+
* `createStoryIssues` — the one point where a `dispatch: true` verdict has
|
|
15
|
+
* no re-author loop to route to. Persist no longer evaluates critics; this
|
|
16
|
+
* module has exactly one evaluation point.
|
|
15
17
|
*
|
|
16
18
|
* Pure evaluation: no file I/O, no GitHub calls, no ledger writes — the
|
|
17
|
-
*
|
|
19
|
+
* caller owns artifact loading and skip recording.
|
|
18
20
|
*
|
|
19
21
|
* @module lib/orchestration/plan-critics-evaluate
|
|
20
22
|
*/
|
|
@@ -24,6 +26,7 @@ import {
|
|
|
24
26
|
evaluateConsolidationDispatch,
|
|
25
27
|
evaluatePremortemDispatch,
|
|
26
28
|
} from './plan-critic-conditions.js';
|
|
29
|
+
import { evaluateTextHygiene } from './plan-text-hygiene.js';
|
|
27
30
|
|
|
28
31
|
/**
|
|
29
32
|
* Resolve the planning risk heuristics list from the canonical config
|
|
@@ -41,8 +44,7 @@ function resolveRiskHeuristics(config = {}) {
|
|
|
41
44
|
|
|
42
45
|
/**
|
|
43
46
|
* Evaluate the consolidation + pre-mortem critic dispatch conditions over
|
|
44
|
-
* the authored planning artifacts (
|
|
45
|
-
* unchanged):
|
|
47
|
+
* the authored planning artifacts (#4474 PR6 conditions, unchanged):
|
|
46
48
|
*
|
|
47
49
|
* - Consolidation: skipped outright when `tickets` is null/absent (the
|
|
48
50
|
* single-delivery shape authors no draft tickets); otherwise the
|
|
@@ -50,6 +52,11 @@ function resolveRiskHeuristics(config = {}) {
|
|
|
50
52
|
* - Pre-mortem: ticket count at least half `maxTickets`, OR any
|
|
51
53
|
* `planning.riskHeuristics` phrase matching the plan text. Story #4542
|
|
52
54
|
* retired its authored-risk-level condition with the verdict itself.
|
|
55
|
+
* - Text hygiene (Story #4599, advisory-only): deterministic body lints
|
|
56
|
+
* (dangling-citation / open-question / slicing-mass) over the draft
|
|
57
|
+
* stories. It has no `dispatch` semantics and spawns nothing — its
|
|
58
|
+
* `findings[]` are re-author-round input, and the consolidation /
|
|
59
|
+
* premortem dispatch verdicts are untouched by it.
|
|
53
60
|
*
|
|
54
61
|
* @param {{
|
|
55
62
|
* techSpecContent: string,
|
|
@@ -59,6 +66,7 @@ function resolveRiskHeuristics(config = {}) {
|
|
|
59
66
|
* @returns {{
|
|
60
67
|
* consolidation: { critic: string, dispatch: boolean, reasons: string[] },
|
|
61
68
|
* premortem: { critic: string, dispatch: boolean, reasons: string[] },
|
|
69
|
+
* textHygiene: { critic: string, findings: Array<object> },
|
|
62
70
|
* }}
|
|
63
71
|
*/
|
|
64
72
|
export function evaluatePlanCritics({
|
|
@@ -91,5 +99,10 @@ export function evaluatePlanCritics({
|
|
|
91
99
|
].join('\n'),
|
|
92
100
|
});
|
|
93
101
|
|
|
94
|
-
|
|
102
|
+
const textHygiene = {
|
|
103
|
+
critic: 'text-hygiene',
|
|
104
|
+
findings: evaluateTextHygiene({ draftStories: ticketList }).findings,
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
return { consolidation, premortem, textHygiene };
|
|
95
108
|
}
|
|
@@ -44,7 +44,6 @@ import { anchorTempRoot, tempRootFrom } from '../../config/temp-paths.js';
|
|
|
44
44
|
import { getLimits, PROJECT_ROOT } from '../../config-resolver.js';
|
|
45
45
|
import { gitSpawn } from '../../git-utils.js';
|
|
46
46
|
import { Logger } from '../../Logger.js';
|
|
47
|
-
import { evaluatePlanCritics } from '../plan-critics-evaluate.js';
|
|
48
47
|
import {
|
|
49
48
|
appendCriticSkip,
|
|
50
49
|
readPlanMetrics,
|
|
@@ -438,29 +437,6 @@ export async function runPlanPersist({
|
|
|
438
437
|
);
|
|
439
438
|
}
|
|
440
439
|
|
|
441
|
-
const critics = evaluatePlanCritics({
|
|
442
|
-
techSpecContent: techSpecContent ?? '',
|
|
443
|
-
tickets: rawStories,
|
|
444
|
-
config,
|
|
445
|
-
});
|
|
446
|
-
for (const decision of [critics.consolidation, critics.premortem]) {
|
|
447
|
-
Logger.info(
|
|
448
|
-
`[plan-persist] critic ${decision.critic}: ` +
|
|
449
|
-
`${decision.dispatch ? 'dispatch' : 'skip'} — ` +
|
|
450
|
-
decision.reasons.join('; '),
|
|
451
|
-
);
|
|
452
|
-
if (!decision.dispatch) {
|
|
453
|
-
await appendCriticSkip(
|
|
454
|
-
{
|
|
455
|
-
critic: decision.critic,
|
|
456
|
-
reasons: decision.reasons,
|
|
457
|
-
cli: 'plan-persist',
|
|
458
|
-
},
|
|
459
|
-
config,
|
|
460
|
-
);
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
|
|
464
440
|
// Split policy + inline Spec fold (over-budget Specs fail closed — no docs/).
|
|
465
441
|
const { stories } = assemblePlanStories(rawStories, {
|
|
466
442
|
sharedSpec: techSpecContent,
|
|
@@ -580,7 +556,6 @@ export async function runPlanPersist({
|
|
|
580
556
|
stories: created,
|
|
581
557
|
primaryStoryId: primary.id,
|
|
582
558
|
forceReview,
|
|
583
|
-
critics,
|
|
584
559
|
reachability,
|
|
585
560
|
freshness,
|
|
586
561
|
waveTable,
|