mandrel 2.59.0 → 2.60.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/README.md +11 -9
- package/.agents/agents/acceptance-critic.md +24 -43
- package/.agents/agents/story-worker.md +18 -19
- package/.agents/docs/SDLC.md +6 -6
- package/.agents/docs/agentrc-reference.json +1 -2
- package/.agents/docs/configuration.md +29 -46
- package/.agents/docs/quality-gates.md +8 -4
- package/.agents/docs/workflows.md +1 -1
- package/.agents/instructions.md +4 -5
- package/.agents/rules/ci-remediation.md +41 -8
- package/.agents/rules/known-tooling-behavior.md +65 -15
- package/.agents/schemas/acceptance-eval-verdict.schema.json +1 -1
- package/.agents/schemas/agentrc.schema.json +6 -11
- package/.agents/schemas/story-deliver-terminal.schema.json +3 -3
- package/.agents/scripts/README.md +11 -1
- package/.agents/scripts/acceptance-eval.js +25 -27
- package/.agents/scripts/ceremony-derive.js +15 -10
- package/.agents/scripts/check-context-budget.js +148 -228
- package/.agents/scripts/check-schema-references.js +5 -3
- package/.agents/scripts/check-workflow-citations.js +33 -147
- package/.agents/scripts/coverage-capture.js +7 -4
- package/.agents/scripts/deliver-light.js +41 -100
- package/.agents/scripts/deliver-run.js +631 -0
- package/.agents/scripts/file-ci-gap.js +59 -11
- package/.agents/scripts/lib/baselines/crap-preview-incremental.js +6 -2
- package/.agents/scripts/lib/changed-files.js +30 -0
- package/.agents/scripts/lib/config/delivery-routing.js +5 -4
- package/.agents/scripts/lib/config/explain.js +1 -3
- package/.agents/scripts/lib/config/gates/crap-incremental-coverage.schema.js +1 -1
- package/.agents/scripts/lib/config-resolver.js +1 -0
- package/.agents/scripts/lib/config-settings-schema-delivery.js +28 -21
- package/.agents/scripts/lib/coverage-capture-fullscope.js +10 -2
- package/.agents/scripts/lib/coverage-capture-incremental.js +3 -2
- package/.agents/scripts/lib/coverage-capture-usage.js +4 -1
- package/.agents/scripts/lib/doc-tiers.js +4 -2
- package/.agents/scripts/lib/feedback-loop/graduator-core.js +7 -6
- package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +7 -5
- package/.agents/scripts/lib/generated/agentrc-validator.js +1 -1
- package/.agents/scripts/lib/gh-exec.js +160 -0
- package/.agents/scripts/lib/observability/source-classifier.js +1 -0
- package/.agents/scripts/lib/orchestration/ceremony-routing.js +74 -132
- package/.agents/scripts/lib/orchestration/ci-rerun-guard.js +123 -12
- package/.agents/scripts/lib/orchestration/complexity-gate.js +180 -352
- package/.agents/scripts/lib/orchestration/light-suitability.js +71 -136
- package/.agents/scripts/lib/orchestration/plan-context.js +13 -25
- package/.agents/scripts/lib/orchestration/plan-persist/changes-repair.js +8 -6
- package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +76 -95
- package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +35 -18
- package/.agents/scripts/lib/orchestration/plan-persist/summary.js +11 -11
- package/.agents/scripts/lib/orchestration/plan-persist/supersede-ops.js +63 -29
- package/.agents/scripts/lib/orchestration/review-depth.js +14 -11
- package/.agents/scripts/lib/orchestration/run-epilogue.js +260 -182
- package/.agents/scripts/lib/orchestration/run-scoped-config.js +63 -99
- package/.agents/scripts/lib/orchestration/single-story-close/phases/base-sync.js +3 -3
- package/.agents/scripts/lib/orchestration/single-story-close/phases/graphql-preflight.js +137 -0
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +105 -18
- package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +4 -3
- package/.agents/scripts/lib/orchestration/story-follow-ups.js +156 -39
- package/.agents/scripts/lib/orchestration/story-init-envelope.js +71 -0
- package/.agents/scripts/lib/orchestration/task-body-validator.js +8 -17
- package/.agents/scripts/lib/orchestration/ticket-validator.js +44 -183
- package/.agents/scripts/lib/orchestration/ticketing/reads.js +14 -25
- package/.agents/scripts/lib/story-body/body-format-lints.js +58 -12
- package/.agents/scripts/lib/story-body/story-body.js +83 -29
- package/.agents/scripts/lib/templates/decomposer-prompts.js +7 -15
- package/.agents/scripts/lib/wave-runner/live-probe.js +31 -5
- package/.agents/scripts/merge-baseline.js +4 -5
- package/.agents/scripts/plan-context.js +117 -28
- package/.agents/scripts/plan-persist.js +79 -28
- package/.agents/scripts/plan-run-epilogue.js +11 -8
- package/.agents/scripts/pr-watch-with-update.js +9 -2
- package/.agents/scripts/run-verify.js +13 -6
- package/.agents/scripts/single-story-init.js +7 -57
- package/.agents/scripts/stories-wave-tick.js +160 -26
- package/.agents/skills/core/gates-and-baselines/reference.md +0 -1
- package/.agents/skills/skills.index.json +2 -2
- package/.agents/skills/stack/qa/playwright/SKILL.md +26 -0
- package/.agents/workflows/helpers/acceptance-self-eval.md +84 -157
- package/.agents/workflows/helpers/code-review.md +4 -2
- package/.agents/workflows/helpers/deliver-digest.md +31 -24
- package/.agents/workflows/helpers/deliver-light.md +92 -101
- package/.agents/workflows/helpers/deliver-reference.md +116 -100
- package/.agents/workflows/helpers/deliver-story-reference.md +58 -124
- package/.agents/workflows/helpers/deliver-story.md +17 -18
- package/.agents/workflows/helpers/plan-reference.md +65 -54
- package/.agents/workflows/mandrel-deliver.md +47 -31
- package/.agents/workflows/mandrel-plan.md +22 -21
- package/.agents/workflows/mandrel-update.md +36 -21
- package/docs/CHANGELOG.md +35 -0
- package/lib/cli/update.js +376 -17
- package/lib/migrations/index.js +2 -0
- package/lib/migrations/steps/2.60.0-retire-audit-results-autofile.js +40 -0
- package/package.json +2 -1
- package/.agents/schemas/model-attribution.schema.json +0 -53
- package/.agents/scripts/lib/orchestration/model-attribution.js +0 -418
- package/.agents/scripts/lib/orchestration/story-plan-state.js +0 -33
- package/.agents/scripts/lib/orchestration/structured-comment-parser.js +0 -67
|
@@ -77,20 +77,19 @@ export const STRUCTURED_COMMENT_TYPES = Object.freeze([
|
|
|
77
77
|
'follow-ups',
|
|
78
78
|
// v2 plan-run epilogue artifacts (posted on the primary Story)
|
|
79
79
|
'plan-run-audit-roster',
|
|
80
|
-
'plan-run-sibling-coherence',
|
|
81
80
|
'epic-run-state',
|
|
82
81
|
'epic-run-progress',
|
|
83
82
|
'epic-plan-state',
|
|
84
83
|
// `parked-follow-ons` retired in Story #5114 with the module that was its
|
|
85
|
-
// only writer
|
|
86
|
-
//
|
|
84
|
+
// only writer, and `plan-run-sibling-coherence` in Story #5341 with the
|
|
85
|
+
// epilogue step that was its. Story #5367 retired two more on the same
|
|
86
|
+
// rule: `story-init` (its write went away in #5343 — the receipt is read
|
|
87
|
+
// off disk now) and `model-attribution` (its writer went with the per-Task
|
|
88
|
+
// progress writer in #3157, and its reader modules are deleted). A kind the
|
|
89
|
+
// reader still recognises but nothing emits is the same dead wiring in a
|
|
90
|
+
// new place.
|
|
87
91
|
// Story #566 — per-phase wall-clock summary posted by single-story-close.js.
|
|
88
92
|
'phase-timings',
|
|
89
|
-
// Story #831 — story-init upserts a `story-init` comment that
|
|
90
|
-
// surfaces `dependenciesInstalled` (and the underlying installStatus) so
|
|
91
|
-
// downstream workflow steps don't have to infer install state from
|
|
92
|
-
// node_modules presence.
|
|
93
|
-
'story-init',
|
|
94
93
|
// Story #2128 — Phase 6 Epic Clarity Gate (CLI retired). Historical
|
|
95
94
|
// `clarity-gate-update` comments may still exist on older tickets.
|
|
96
95
|
'clarity-gate-update',
|
|
@@ -100,16 +99,6 @@ export const STRUCTURED_COMMENT_TYPES = Object.freeze([
|
|
|
100
99
|
// operator can correct drift before Phase 8 decomposes from a stale
|
|
101
100
|
// spec. Advisory: the run continues regardless of the report contents.
|
|
102
101
|
'spec-freshness',
|
|
103
|
-
// Story #2813 — the per-Task progress writer (since retired under
|
|
104
|
-
// #3157) upserted a `model-attribution` comment on a Task ticket at
|
|
105
|
-
// the moment it transitioned to `agent::executing`, recording which
|
|
106
|
-
// Claude model was actively executing the work. One entry per Task
|
|
107
|
-
// (upsert is idempotent across resume re-runs). Story- and Epic-level
|
|
108
|
-
// rollups are computed at query time by `rollupModelAttribution` in
|
|
109
|
-
// `lib/orchestration/model-attribution.js` — no Story/Epic-scope
|
|
110
|
-
// emissions are written. Schema:
|
|
111
|
-
// `.agents/schemas/model-attribution.schema.json`.
|
|
112
|
-
'model-attribution',
|
|
113
102
|
// Story #2894 — `finalize/post-handoff-comment.js` upserts an
|
|
114
103
|
// `epic-handoff` comment on the Epic at the end of the bus-owned
|
|
115
104
|
// finalize flow (after `open-or-locate-pr`
|
|
@@ -157,13 +146,13 @@ export const STRUCTURED_COMMENT_TYPES = Object.freeze([
|
|
|
157
146
|
// `graduator="<name>"` attr so independent graduators do not clobber each
|
|
158
147
|
// other's comment; re-runs upsert in place.
|
|
159
148
|
'cross-repo-deferred',
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
//
|
|
163
|
-
//
|
|
164
|
-
|
|
165
|
-
//
|
|
166
|
-
//
|
|
149
|
+
// v2 Stage 3 — the flat Story persist comment on every created Story
|
|
150
|
+
// (replaces epic-plan-state for new plans). Since Story #5343 it is the
|
|
151
|
+
// ONLY comment persist posts: the primary-Story-only `plan-summary` marker
|
|
152
|
+
// was retired and its content — story set, delivery order, deliver command
|
|
153
|
+
// — rides this marker on every Story. Story #5367 deleted the machine
|
|
154
|
+
// checkpoint that used to lead the body; the marker remains, and it is what
|
|
155
|
+
// makes a re-persist upsert in place.
|
|
167
156
|
'story-plan-state',
|
|
168
157
|
// Story #4535 — `plan-persist.js` upserts a `superseded-by` comment on
|
|
169
158
|
// each `/mandrel-plan --tickets` source issue at persist time, naming the single
|
|
@@ -22,7 +22,10 @@
|
|
|
22
22
|
*
|
|
23
23
|
* Story #5312 deleted the `verify-tier-suffix` / `verify-manual-reason` lints
|
|
24
24
|
* with the tier suffix itself: a `verify[]` entry is any command, and the
|
|
25
|
-
* `manual:<reason>` escape is gone with the rule it escaped.
|
|
25
|
+
* `manual:<reason>` escape is gone with the rule it escaped. Story #5342
|
|
26
|
+
* deleted `verify-non-empty`: an empty `verify[]` is a dry-run warning, not a
|
|
27
|
+
* refusal, so it is no longer a rejecting lint — this registry carries only
|
|
28
|
+
* the rules that still refuse.
|
|
26
29
|
*
|
|
27
30
|
* Import hygiene: this module imports only the cycle-free
|
|
28
31
|
* `file-assumption-enum.js` leaf. It must NOT import `story-body.js` or
|
|
@@ -40,6 +43,53 @@ import { FILE_ASSUMPTION_VALUES } from '../orchestration/file-assumption-enum.js
|
|
|
40
43
|
*/
|
|
41
44
|
const DEFAULT_SUGGESTED_ASSUMPTION = 'refactors-existing';
|
|
42
45
|
|
|
46
|
+
/**
|
|
47
|
+
* The bare-path bullet grammar (Story #5342, widened by Story #5361) — the
|
|
48
|
+
* **one** definition of what a `## Changes` / `## References` bullet has to
|
|
49
|
+
* look like to be a path rather than prose.
|
|
50
|
+
*
|
|
51
|
+
* It lives here, in the cycle-free lint leaf, because two consumers need the
|
|
52
|
+
* identical judgment and neither may import the other: the story-body parser
|
|
53
|
+
* (`story-body.js#parsePathEntry`) and the persist repair pass
|
|
54
|
+
* (`plan-persist/changes-repair.js`). They carried a copy each, and the
|
|
55
|
+
* copies drifted.
|
|
56
|
+
*
|
|
57
|
+
* A bullet is a path when it is a single token git could track: any run of
|
|
58
|
+
* non-whitespace, optionally wrapped in a matched pair of backticks. That
|
|
59
|
+
* admits the shapes the earlier `[\w@*-]*[/.][\w@./*-]+` class refused
|
|
60
|
+
* outright — route-segment paths (`app/[slug]/page.tsx`,
|
|
61
|
+
* `app/(marketing)/page.tsx`, `src/routes/$id.svelte`) and extensionless
|
|
62
|
+
* top-level files (`Makefile`) — while still refusing prose, since
|
|
63
|
+
* whitespace is the one thing that reliably marks a sentence.
|
|
64
|
+
*/
|
|
65
|
+
const BARE_PATH_TOKEN_RE = /^(?:`([^\s`]+)`|([^\s`]+))$/;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Read the bare path token out of a bullet, or `null` when the bullet is not
|
|
69
|
+
* one single token.
|
|
70
|
+
*
|
|
71
|
+
* @param {unknown} raw
|
|
72
|
+
* @returns {string|null} The path, with any wrapping backticks peeled.
|
|
73
|
+
*/
|
|
74
|
+
export function matchBarePathToken(raw) {
|
|
75
|
+
if (typeof raw !== 'string') return null;
|
|
76
|
+
const match = raw.trim().match(BARE_PATH_TOKEN_RE);
|
|
77
|
+
return match ? (match[1] ?? match[2]) : null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Whether a rejected bullet is prose — it carries whitespace, so no path
|
|
82
|
+
* grammar could ever have admitted it. The counterpart failure is a
|
|
83
|
+
* single token that still names no usable path; the two get different
|
|
84
|
+
* refusals because they need different fixes.
|
|
85
|
+
*
|
|
86
|
+
* @param {unknown} raw
|
|
87
|
+
* @returns {boolean}
|
|
88
|
+
*/
|
|
89
|
+
export function isProseBullet(raw) {
|
|
90
|
+
return typeof raw === 'string' && /\s/.test(raw.trim());
|
|
91
|
+
}
|
|
92
|
+
|
|
43
93
|
// A token that looks like a file path / glob / module id: it carries a `/` or a
|
|
44
94
|
// `.`-separated segment. Deliberately loose — the suggestion is best-effort, and
|
|
45
95
|
// a false positive only produces an unhelpful (still-valid) fix-it string.
|
|
@@ -114,10 +164,13 @@ export const BODY_FORMAT_LINTS = Object.freeze([
|
|
|
114
164
|
{
|
|
115
165
|
id: 'changes-path-entry-shape',
|
|
116
166
|
summary:
|
|
117
|
-
'Every `## Changes` / `## References` bullet MUST
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
167
|
+
'Every `## Changes` / `## References` bullet MUST name a path — a bare ' +
|
|
168
|
+
'path string is the default form and persist derives its assumption by ' +
|
|
169
|
+
'probing the base branch. Use the `{ path, assumption }` object ' +
|
|
170
|
+
`(assumption ∈ ${FILE_ASSUMPTION_VALUES.join(' | ')}) only to pin one ` +
|
|
171
|
+
'yourself; `deletes` always needs it. Prose bullets are rejected.',
|
|
172
|
+
badExample: '- the routing module and its tests',
|
|
173
|
+
goodExample: '- src/app.js',
|
|
121
174
|
autoFixable: true,
|
|
122
175
|
},
|
|
123
176
|
{
|
|
@@ -127,13 +180,6 @@ export const BODY_FORMAT_LINTS = Object.freeze([
|
|
|
127
180
|
goodExample: '- {"path": "src/app.js", "assumption": "creates"}',
|
|
128
181
|
autoFixable: false,
|
|
129
182
|
},
|
|
130
|
-
{
|
|
131
|
-
id: 'verify-non-empty',
|
|
132
|
-
summary: 'A Story MUST list at least one `verify[]` entry.',
|
|
133
|
-
badExample: '"verify": []',
|
|
134
|
-
goodExample: '"verify": ["npm run validate"]',
|
|
135
|
-
autoFixable: false,
|
|
136
|
-
},
|
|
137
183
|
{
|
|
138
184
|
id: 'acceptance-non-empty',
|
|
139
185
|
summary:
|
|
@@ -41,7 +41,11 @@
|
|
|
41
41
|
*/
|
|
42
42
|
|
|
43
43
|
import { FILE_ASSUMPTION_VALUES } from '../orchestration/file-assumption-enum.js';
|
|
44
|
-
import {
|
|
44
|
+
import {
|
|
45
|
+
isProseBullet,
|
|
46
|
+
matchBarePathToken,
|
|
47
|
+
suggestPathEntryFix,
|
|
48
|
+
} from './body-format-lints.js';
|
|
45
49
|
import { isFooterSeparator, parseFooterBlockedByRefs } from './footer-block.js';
|
|
46
50
|
|
|
47
51
|
// ---------------------------------------------------------------------------
|
|
@@ -161,6 +165,14 @@ function stripListMarker(line) {
|
|
|
161
165
|
// issue bodies are never rewritten).
|
|
162
166
|
const HUMANIZED_PATH_ENTRY_RE = /^`([^`]+)`\s+—\s+(\S+)$/;
|
|
163
167
|
|
|
168
|
+
// Bare path bullet (Story #5342): `src/app.js` or `` `src/app.js` `` with no
|
|
169
|
+
// assumption. The assumption is a fact about the base branch, not a thing the
|
|
170
|
+
// author knows better than a probe does, so the default authored form omits
|
|
171
|
+
// it and persist derives it. The grammar itself — a single whitespace-free
|
|
172
|
+
// token, so prose bullets do not match and are still rejected — is
|
|
173
|
+
// `body-format-lints.js#matchBarePathToken`, the one definition the persist
|
|
174
|
+
// repair pass scores against too (Story #5361).
|
|
175
|
+
|
|
164
176
|
// AC-<n> presentation prefix on acceptance checkboxes (Story #4600). The
|
|
165
177
|
// numbering is a stable 1-based human handle only — parse() strips it so the
|
|
166
178
|
// top-level acceptance[] machine contract round-trips byte-identical.
|
|
@@ -189,8 +201,10 @@ const META_BLOCK_RE = /<!--\s*meta:[\s\S]*?-->/;
|
|
|
189
201
|
/**
|
|
190
202
|
* Parse a single `changes` / `references` bullet into a `PathEntry`.
|
|
191
203
|
*
|
|
192
|
-
* Accepted markdown shapes (
|
|
204
|
+
* Accepted markdown shapes (all parsed indefinitely — live issue bodies
|
|
193
205
|
* are never rewritten):
|
|
206
|
+
* - Bare path (the default authored form since Story #5342):
|
|
207
|
+
* `` `src/x.js` `` or `src/x.js`, parsed with `assumption: null`
|
|
194
208
|
* - Humanized bullet (canonical serialize() output since Story #4600):
|
|
195
209
|
* `` `src/x.js` — refactors-existing ``
|
|
196
210
|
* - Legacy inline-JSON object bullet:
|
|
@@ -209,18 +223,65 @@ function parsePathEntry(raw, warnings) {
|
|
|
209
223
|
return pathEntryFromObject(raw);
|
|
210
224
|
}
|
|
211
225
|
|
|
212
|
-
const str =
|
|
226
|
+
const str = String(raw).trim();
|
|
213
227
|
if (str.length === 0) return null;
|
|
214
228
|
|
|
215
229
|
const entry = pathEntryFromHumanized(str) ?? pathEntryFromInlineJson(str);
|
|
216
230
|
if (entry) return entry;
|
|
231
|
+
return pathEntryFromBare(str);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Parse the bare path bullet — the default authored form since Story #5342 —
|
|
236
|
+
* or refuse the bullet. This is the last shape `parsePathEntry` tries, so it
|
|
237
|
+
* owns the rejection too.
|
|
238
|
+
*
|
|
239
|
+
* `assumption: null` records only what the author said; persist derives the
|
|
240
|
+
* rest by probing the base branch. A `{`-leading string reached here because
|
|
241
|
+
* it failed to parse as the inline JSON object it announced itself as, so it
|
|
242
|
+
* is malformed JSON rather than a path and keeps failing closed.
|
|
243
|
+
*
|
|
244
|
+
* Story #5361: the two failures need different fixes, so they get different
|
|
245
|
+
* refusals — rewrite a sentence as a path, versus fix a token that is not one.
|
|
246
|
+
*
|
|
247
|
+
* @param {string} str
|
|
248
|
+
* @returns {PathEntry}
|
|
249
|
+
*/
|
|
250
|
+
function pathEntryFromBare(str) {
|
|
251
|
+
const bare = str.startsWith('{') ? null : matchBarePathToken(str);
|
|
252
|
+
if (bare !== null) return { path: bare, assumption: null };
|
|
217
253
|
|
|
254
|
+
const shape = isProseBullet(str)
|
|
255
|
+
? 'is prose, not a path'
|
|
256
|
+
: 'names no usable path';
|
|
218
257
|
throw new StoryBodyParseError(
|
|
219
|
-
`changes/references entry
|
|
258
|
+
`changes/references entry ${shape} — a bullet must be a single ` +
|
|
259
|
+
`whitespace-free path token, or a { path, assumption } object: ` +
|
|
260
|
+
`${str.slice(0, 120)}${pathEntryFixIt(str)}`,
|
|
220
261
|
{ field: 'changes', raw: str },
|
|
221
262
|
);
|
|
222
263
|
}
|
|
223
264
|
|
|
265
|
+
/**
|
|
266
|
+
* Read the assumption a raw entry declares, collapsing the three cases the
|
|
267
|
+
* parser has to tell apart into one value:
|
|
268
|
+
*
|
|
269
|
+
* - a canonical `FILE_ASSUMPTION_VALUES` member — the author pinned one;
|
|
270
|
+
* - `null` — the author named no assumption at all. Since Story #5342 that
|
|
271
|
+
* is the **bare form**, and persist derives the value by probing the base
|
|
272
|
+
* branch rather than the author guessing it;
|
|
273
|
+
* - `undefined` — the author named something that is not an assumption.
|
|
274
|
+
* Distinct from absent on purpose: a typo must fail closed where an
|
|
275
|
+
* omission is the default shape.
|
|
276
|
+
*
|
|
277
|
+
* @param {unknown} raw
|
|
278
|
+
* @returns {string|null|undefined}
|
|
279
|
+
*/
|
|
280
|
+
function readAssumption(raw) {
|
|
281
|
+
if (FILE_ASSUMPTION_VALUES.includes(raw)) return raw;
|
|
282
|
+
return raw == null ? null : undefined;
|
|
283
|
+
}
|
|
284
|
+
|
|
224
285
|
/**
|
|
225
286
|
* Validate an already-structured `{ path, assumption }` object. Fails closed
|
|
226
287
|
* on a malformed object.
|
|
@@ -229,12 +290,10 @@ function parsePathEntry(raw, warnings) {
|
|
|
229
290
|
* @returns {PathEntry}
|
|
230
291
|
*/
|
|
231
292
|
function pathEntryFromObject(raw) {
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
) {
|
|
237
|
-
return { path: raw.path.trim(), assumption: raw.assumption };
|
|
293
|
+
const path = typeof raw.path === 'string' ? raw.path.trim() : '';
|
|
294
|
+
const assumption = readAssumption(raw.assumption);
|
|
295
|
+
if (path !== '' && assumption !== undefined) {
|
|
296
|
+
return { path, assumption };
|
|
238
297
|
}
|
|
239
298
|
// Malformed object: fail closed.
|
|
240
299
|
throw new StoryBodyParseError(
|
|
@@ -270,8 +329,9 @@ function pathEntryFromHumanized(str) {
|
|
|
270
329
|
* Parse the legacy inline-JSON object bullet:
|
|
271
330
|
* `{ "path": "...", "assumption": "..." }`. Returns `null` when the line is
|
|
272
331
|
* not a JSON object at all (including a JSON parse failure — the caller then
|
|
273
|
-
*
|
|
274
|
-
*
|
|
332
|
+
* tries the bare-path form and finally rejects the bullet); delegates the
|
|
333
|
+
* field check to {@link pathEntryFromObject}, which is the same judgment on
|
|
334
|
+
* the same shape and fails closed the same way.
|
|
275
335
|
*
|
|
276
336
|
* @param {string} str
|
|
277
337
|
* @returns {PathEntry|null}
|
|
@@ -282,21 +342,12 @@ function pathEntryFromInlineJson(str) {
|
|
|
282
342
|
try {
|
|
283
343
|
parsed = JSON.parse(str);
|
|
284
344
|
} catch {
|
|
285
|
-
// JSON parse failed — the caller
|
|
345
|
+
// JSON parse failed — the caller falls through to the bare-path form.
|
|
286
346
|
return null;
|
|
287
347
|
}
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
FILE_ASSUMPTION_VALUES.includes(parsed.assumption)
|
|
292
|
-
) {
|
|
293
|
-
return { path: parsed.path.trim(), assumption: parsed.assumption };
|
|
294
|
-
}
|
|
295
|
-
// Parsed successfully as JSON object but has invalid fields — fail closed.
|
|
296
|
-
throw new StoryBodyParseError(
|
|
297
|
-
`changes/references entry is a JSON object but not a valid PathEntry: ${str}`,
|
|
298
|
-
{ field: 'changes', raw: str },
|
|
299
|
-
);
|
|
348
|
+
return parsed !== null && typeof parsed === 'object'
|
|
349
|
+
? pathEntryFromObject(parsed)
|
|
350
|
+
: null;
|
|
300
351
|
}
|
|
301
352
|
|
|
302
353
|
/**
|
|
@@ -789,10 +840,13 @@ const STRUCTURED_FIELD_NORMALIZERS = {
|
|
|
789
840
|
*/
|
|
790
841
|
function serializePathEntry(entry) {
|
|
791
842
|
if (typeof entry === 'string') return entry;
|
|
792
|
-
// Canonical object form (Story #4600):
|
|
793
|
-
//
|
|
794
|
-
//
|
|
795
|
-
|
|
843
|
+
// Canonical object form (Story #4600): a human-readable bullet — path in
|
|
844
|
+
// backticks, em-dash, assumption. parsePathEntry recognizes this shape (and
|
|
845
|
+
// the legacy inline-JSON one) for round-trip fidelity. Story #5342: a bare
|
|
846
|
+
// entry — a path the author wrote with no assumption — serializes back bare
|
|
847
|
+
// rather than silently acquiring a derivation they never made; persist
|
|
848
|
+
// fills it in by probing base before it writes a body.
|
|
849
|
+
return [`\`${entry.path}\``, entry.assumption].filter(Boolean).join(' — ');
|
|
796
850
|
}
|
|
797
851
|
|
|
798
852
|
/**
|
|
@@ -117,7 +117,8 @@ The **persisted** \`body\` renders these markdown sections (in order) — you au
|
|
|
117
117
|
<optional technical approach at contract level — do NOT restate Goal / Acceptance / Verify>
|
|
118
118
|
|
|
119
119
|
## Changes
|
|
120
|
-
-
|
|
120
|
+
- <file path>
|
|
121
|
+
- {"path": "<file path>", "assumption": "deletes"}
|
|
121
122
|
- ...
|
|
122
123
|
|
|
123
124
|
## Acceptance <-- synthesized by persist from acceptance[]; do not author
|
|
@@ -137,9 +138,9 @@ The **persisted** \`body\` renders these markdown sections (in order) — you au
|
|
|
137
138
|
- **goal** (in body string): One sentence stating WHY this Story exists.
|
|
138
139
|
- **spec** (optional, in body string as \`## Spec\`): The technical approach at the altitude the SPEC PROSE CONTRACT below fixes — contract and invariants, never implementation narration. Write as much as the work needs and no more; persist keeps Specs inline at any length and never writes them under \`docs/\`.
|
|
139
140
|
- **slicing** (optional): Ordered intra-session checkpoints for one Story, one line each. A checkpoint is a **stage of the work** — a commit boundary the deliverer passes through inside one session, stated as the step it performs. An acceptance item is a **state of the codebase** a PR reviewer confirms once the Story has landed. The same Story therefore carries both: the checkpoints say in what order it is built, \`acceptance[]\` says what must then be true. Never a fan-out table, never a second acceptance list, and never sibling tickets — a broad sweep with many stages is still one Story, sliced here.
|
|
140
|
-
- **changes** (in body string): Each entry is
|
|
141
|
+
- **changes** (in body string): Each entry is a **bare path string** — the default form; persist derives its assumption by probing the base branch and reports the derivation. Use the object form \`{ path, assumption }\` (\`assumption\` one of \`creates | refactors-existing | deletes\`) only to pin one yourself, and always for a \`deletes\`, which a bare path can never express. **Name the files the deliverer authors, and omit generated artifacts** — quality baselines, generated test indexes, migration journals, lockfiles and the like are regenerated by the work itself, the refresh is a close-gate concern, and declaring one needlessly reserves a footprint that serializes sibling Stories at dispatch. Acceptable path shapes include explicit files (\`src/components/Foo.tsx\`), glob patterns (\`tests/e2e/*.spec.ts\`, \`**/*.astro\`), and module identifiers that resolve to files. Pin \`refactors-existing\` for an in-place edit, \`creates\` for a net-new file, \`deletes\` for a removal, whenever the probe would get it wrong. Persist probes every path against the base branch and derives the assumption for you, so a \`creates\` on an existing path or a \`refactors-existing\` on an absent one is a dry-run warning; only a \`deletes\` naming an absent path is refused.
|
|
141
142
|
- **acceptance** (top-level array on the ticket object): Each item is an **outcome a PR reviewer can confirm from the diff and the verify output** — what is true of the codebase once the Story lands, stated at the altitude of the capability (a command that now exits 0 against a named input, a behavior a named test now asserts, a config that now fails validation on a retired key, a document that now records a decision). State as many outcomes as the capability has and no more — the list has no target, floor or ceiling, and a long one is never a reason to split the Story. Push grep-shaped probes, file-exists checks and exit-code tests down into \`verify[]\`; never pin an internal helper name or a private file path into an acceptance item the advisory \`changes[]\` is free to reshape. UNACCEPTABLE: "verify by reading the diff", "looks good", "matches the spec".
|
|
142
|
-
- **verify** (top-level array on the ticket object): The **mechanical checks** — exact commands or test paths the deliverer runs and the acceptance critic consumes as evidence: \`node --test tests/x.test.js\`, \`npm run lint\`, \`npm run validate\`, a scoped grep. Every acceptance item should be confirmable from at least one verify entry's output plus the diff.
|
|
143
|
+
- **verify** (top-level array on the ticket object): The **mechanical checks** — exact commands or test paths the deliverer runs and the acceptance critic consumes as evidence: \`node --test tests/x.test.js\`, \`npm run lint\`, \`npm run validate\`, a scoped grep. Every acceptance item should be confirmable from at least one verify entry's output plus the diff. A Story with no verify entry is warned about, not rejected — but the critic then has nothing to read as evidence, so author the commands.
|
|
143
144
|
- **Bodies record decisions, never questions to the operator.** Never persist an open question ("Flag if…", "TBD", "confirm with the operator") into a Story body — the executing sub-agent is non-interactive and cannot answer it, and the dry-run warns on every one it finds. Triage each unknown by who can resolve it: an AFK-shaped unknown (a fact in docs, a third-party API surface, observable repo behavior) MUST be resolved by your own research before authoring — never restated as an assumption; only a HITL-shaped unknown (a genuine product or architecture call the operator owns) may be restated as a declarative Key Assumption the agent can act on, stating the default chosen (a decision-made-by-default).
|
|
144
145
|
- **non_goals** (OPTIONAL, in body string as the \`## Non-Goals\` section): A short list of capabilities or changes this Story explicitly does NOT deliver — an advisory negative-scope bound that fences the executing agent away from adjacent work. It is **advisory and NON-GATING**: the validator does not require, count, or reject on it, and an absent or empty section renders nothing. Use the EXACT single-word hyphenated heading spelling \`## Non-Goals\` (a space-separated heading like \`## Out of Scope\` is NOT recognized by the parser and will be dropped). Reach for it when a Story's negative boundary is non-obvious from its \`acceptance[]\` alone; omit it otherwise.
|
|
145
146
|
|
|
@@ -180,19 +181,10 @@ ${envelopeFloor}
|
|
|
180
181
|
- **A remediation sweep over one subsystem is one Story.** A batch of findings in the same subsystem shares one reason to exist — the subsystem is wrong — so it arrives as one Story whose \`## Slicing\` checkpoints carry the stages, not as one Story per finding.
|
|
181
182
|
- ${singleConsumerRule}
|
|
182
183
|
|
|
183
|
-
#### UI
|
|
184
|
+
#### UI AND COPY WORK — where the contract is written down:
|
|
184
185
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
- Stories that touch UI (\`*.tsx\`, \`*.astro\`, \`*.svelte\`, \`*.vue\`, components folders) MUST carry the testid contract as a top-level \`acceptance[]\` item, one of:
|
|
188
|
-
- \`"data-testid invariance: <list of testids that MUST be preserved>"\`, or
|
|
189
|
-
- \`"data-testid changes: <old> -> <new>, with the matching tests/e2e/*.spec.ts selector updated"\` — paired with that \`tests/e2e/*.spec.ts\` file in \`changes[]\`, in the same Story or a depends_on Story.
|
|
190
|
-
- State the preserved-testid set in \`## Non-Goals\` prose as well when the Story deliberately renames nothing.
|
|
191
|
-
- Renaming a testid without the matching e2e edit is FORBIDDEN.
|
|
192
|
-
|
|
193
|
-
#### BRAND / COPY / STYLE WORK:
|
|
194
|
-
|
|
195
|
-
- Stories that touch user-visible copy, brand assets, or visual style MUST cite the relevant section of \`docs/style-guide.md\` in \`acceptance\` (e.g. \`"acceptance": ["Hero copy matches docs/style-guide.md §3 (voice & tone)"]\`). If \`docs/style-guide.md\` does not exist or has no relevant section, state that explicitly: \`"acceptance": ["docs/style-guide.md absent — copy reviewed against the inline brand brief in the plan seed"]\`. Silence on style sourcing is a smell.
|
|
186
|
+
- A Story touching UI (\`*.tsx\`, \`*.astro\`, \`*.svelte\`, \`*.vue\`, a components folder) states the \`data-testid\` contract in \`acceptance[]\` per the testid contract in \`.agents/skills/stack/qa/playwright/SKILL.md\`.
|
|
187
|
+
- A Story touching user-visible copy, brand assets or visual style cites the relevant section of \`docs/style-guide.md\` in \`acceptance[]\` when that file exists.
|
|
196
188
|
|
|
197
189
|
CRITICAL: Dependencies should follow execution blockers. There is no parent ticket — never emit a 'parent_slug' field.
|
|
198
190
|
IMPORTANT DEPENDENCY RULE: Story-to-Story dependencies are expressed via \`depends_on\` (one Story depends_on another Story's slug). Use this to express execution ordering across the plan.
|
|
@@ -101,22 +101,37 @@ import { classifyStory, storyIdOf } from './ready-set.js';
|
|
|
101
101
|
* Ids outside the probed set are ignored: they are not part of this run and
|
|
102
102
|
* must not consume its cap.
|
|
103
103
|
*
|
|
104
|
+
* The second arm is also reported on its own, as `unlabelled`. A claimed id
|
|
105
|
+
* that still reads `agent::ready` is in flight either because its init is
|
|
106
|
+
* still running or because the spawn that claimed it never started one, and
|
|
107
|
+
* those two are indistinguishable here — but only the second pins the id in
|
|
108
|
+
* the caller's ledger forever. Live state cannot tell them apart; the operator
|
|
109
|
+
* can, so the fact is surfaced rather than acted on (Story #5363).
|
|
110
|
+
*
|
|
104
111
|
* @param {Array<{id?: number, number?: number, labels?: string[], state?: string}>} storyRecords
|
|
105
112
|
* @param {Iterable<number>} [dispatched] Ids the host has spawned.
|
|
106
|
-
* @returns {Set<number>} In-flight Story
|
|
113
|
+
* @returns {{inFlight: Set<number>, unlabelled: Set<number>}} In-flight Story
|
|
114
|
+
* ids, and the subset of them claimed by the caller that live state still
|
|
115
|
+
* reports as `agent::ready`.
|
|
107
116
|
*/
|
|
108
117
|
function deriveInFlightIds(storyRecords, dispatched = []) {
|
|
109
118
|
const claimed = new Set(dispatched);
|
|
110
|
-
|
|
119
|
+
// Bucketed by live class rather than tested twice. The admission rule is
|
|
120
|
+
// unchanged; it is only that the `ready` bucket IS the claimed-but-unlabelled
|
|
121
|
+
// set, since `ready` is the one class the rule admits on the caller's claim.
|
|
122
|
+
const byClass = { executing: new Set(), ready: new Set() };
|
|
111
123
|
for (const rec of storyRecords) {
|
|
112
124
|
const id = storyIdOf(rec);
|
|
113
125
|
if (id === null) continue;
|
|
114
126
|
const cls = classifyStory(rec);
|
|
115
127
|
if (cls === 'executing' || (claimed.has(id) && cls === 'ready')) {
|
|
116
|
-
|
|
128
|
+
byClass[cls].add(id);
|
|
117
129
|
}
|
|
118
130
|
}
|
|
119
|
-
return
|
|
131
|
+
return {
|
|
132
|
+
inFlight: new Set([...byClass.executing, ...byClass.ready]),
|
|
133
|
+
unlabelled: byClass.ready,
|
|
134
|
+
};
|
|
120
135
|
}
|
|
121
136
|
|
|
122
137
|
/**
|
|
@@ -251,6 +266,7 @@ export function createProbeContext({
|
|
|
251
266
|
* doneIds: Set<number>,
|
|
252
267
|
* inFlight: number,
|
|
253
268
|
* blockedIds: number[],
|
|
269
|
+
* stalledDispatch: number[],
|
|
254
270
|
* foreignHeld: Array<{id: number, holder: string}>
|
|
255
271
|
* }>}
|
|
256
272
|
*/
|
|
@@ -294,7 +310,10 @@ export async function probeLiveState({
|
|
|
294
310
|
// They are consumed in-process by `planReadySet` and never serialized into
|
|
295
311
|
// the beat envelope, which stays a list of ids.
|
|
296
312
|
const bodyById = new Map(stories.map((s) => [s.id, s.body ?? '']));
|
|
297
|
-
const inFlightIds = deriveInFlightIds(
|
|
313
|
+
const { inFlight: inFlightIds, unlabelled } = deriveInFlightIds(
|
|
314
|
+
stories,
|
|
315
|
+
dispatched,
|
|
316
|
+
);
|
|
298
317
|
// A Story another operator's lease holds occupies a (global) dispatch slot
|
|
299
318
|
// just like an in-flight one: fold it into the in-flight set so it is both
|
|
300
319
|
// withheld (via the projected label) and excluded from a false wedge, but
|
|
@@ -322,6 +341,13 @@ export async function probeLiveState({
|
|
|
322
341
|
doneIds: new Set(envelope.done),
|
|
323
342
|
inFlight: inFlightIds.size,
|
|
324
343
|
blockedIds: deriveBlockedIds(stories),
|
|
344
|
+
// Claimed by the caller, still labelled `agent::ready`: a live init window
|
|
345
|
+
// or a spawn that never reached one. Reported, never released here. A
|
|
346
|
+
// foreign lease is its own withhold reason and outranks the claim, so it
|
|
347
|
+
// is subtracted — one Story must not carry two recoveries.
|
|
348
|
+
stalledDispatch: [...unlabelled]
|
|
349
|
+
.filter((id) => !foreignHeld.has(id))
|
|
350
|
+
.sort((a, b) => a - b),
|
|
325
351
|
foreignHeld: [...foreignHeld].map(([id, holder]) => ({ id, holder })),
|
|
326
352
|
};
|
|
327
353
|
}
|
|
@@ -36,9 +36,8 @@
|
|
|
36
36
|
*
|
|
37
37
|
* ## Not every `baselines/*.json` is an envelope
|
|
38
38
|
*
|
|
39
|
-
* That glob also matches arch-cycles, cyclomatic, dead-exports, audit-ledger
|
|
40
|
-
* context-budget
|
|
41
|
-
* row identity. Anything whose `$schema` is not a known per-kind envelope is
|
|
39
|
+
* That glob also matches arch-cycles, cyclomatic, dead-exports, audit-ledger
|
|
40
|
+
* and context-budget — files with their own shapes and no row identity. Anything whose `$schema` is not a known per-kind envelope is
|
|
42
41
|
* handed straight back to `git merge-file`, so registering the driver cannot
|
|
43
42
|
* change their behaviour.
|
|
44
43
|
*/
|
|
@@ -164,8 +163,8 @@ function delegateToGit(basePath, oursPath, theirsPath) {
|
|
|
164
163
|
* `git merge-file` even though `.gitattributes` routes them here, so the
|
|
165
164
|
* attribute promised a row merge the driver never performed.
|
|
166
165
|
*
|
|
167
|
-
* Anything else — arch-cycles, audit-ledger, context-budget
|
|
168
|
-
*
|
|
166
|
+
* Anything else — arch-cycles, audit-ledger, context-budget — resolves `null`
|
|
167
|
+
* and is handed back to git unchanged.
|
|
169
168
|
*
|
|
170
169
|
* @param {unknown} ours
|
|
171
170
|
* @param {unknown} theirs
|