mandrel 2.58.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 +17 -12
- package/.agents/agents/acceptance-critic.md +24 -43
- package/.agents/agents/story-worker.md +18 -19
- package/.agents/docs/SDLC.md +12 -13
- package/.agents/docs/agentrc-reference.json +1 -2
- package/.agents/docs/configuration.md +29 -46
- package/.agents/docs/quality-gates.md +9 -5
- package/.agents/docs/workflows.md +1 -1
- package/.agents/instructions.md +5 -7
- package/.agents/rules/ci-remediation.md +41 -8
- package/.agents/rules/known-tooling-behavior.md +65 -15
- package/.agents/runtime-deps.json +7 -2
- package/.agents/schemas/acceptance-eval-verdict.schema.json +1 -1
- package/.agents/schemas/agentrc.schema.json +6 -11
- package/.agents/schemas/crap-baseline.schema.json +1 -1
- package/.agents/schemas/crap-report.schema.json +1 -1
- 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/install-matrix-assert.js +48 -3
- package/.agents/scripts/lib/audit-to-stories/seed-from-findings.js +51 -33
- package/.agents/scripts/lib/baselines/crap-preview-incremental.js +6 -2
- package/.agents/scripts/lib/baselines/kinds/_crap-read.js +0 -8
- package/.agents/scripts/lib/baselines/kinds/crap.js +35 -18
- 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/crap-engine.js +2 -2
- package/.agents/scripts/lib/crap-utils.js +21 -5
- package/.agents/scripts/lib/doc-tiers.js +4 -2
- package/.agents/scripts/lib/escomplex-ast-compat.js +39 -17
- package/.agents/scripts/lib/escomplex-kernel.js +298 -0
- 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/maintainability-engine.js +3 -3
- 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 +44 -50
- package/.agents/scripts/lib/orchestration/plan-persist/changes-repair.js +8 -6
- package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +104 -119
- package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +41 -25
- 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/plan-persist/wave-collision-gate.js +107 -0
- 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-conflicts.js +25 -209
- package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +8 -5
- package/.agents/scripts/lib/orchestration/ticket-validator.js +44 -183
- package/.agents/scripts/lib/orchestration/ticketing/reads.js +14 -25
- package/.agents/scripts/lib/runtime-deps/dep-resolution.js +155 -0
- package/.agents/scripts/lib/runtime-deps/ensure-installed.js +44 -9
- package/.agents/scripts/lib/runtime-deps/parser-major.js +110 -0
- package/.agents/scripts/lib/runtime-deps/preflight.js +6 -25
- package/.agents/scripts/lib/runtime-deps/scan-imports.js +46 -1
- package/.agents/scripts/lib/skills/walk-skill-files.js +1 -1
- 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 +28 -33
- 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 -39
- 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 -12
- package/.agents/skills/stack/qa/playwright/SKILL.md +26 -0
- package/.agents/workflows/audit-to-stories.md +14 -11
- 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 +82 -60
- package/.agents/workflows/mandrel-deliver.md +47 -31
- package/.agents/workflows/mandrel-plan.md +32 -30
- package/.agents/workflows/mandrel-update.md +36 -21
- package/README.md +3 -3
- package/docs/CHANGELOG.md +43 -0
- package/lib/cli/registry.js +45 -25
- 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 +8 -2
- package/.agents/schemas/model-attribution.schema.json +0 -53
- package/.agents/scripts/lib/orchestration/model-attribution.js +0 -418
- package/.agents/scripts/lib/orchestration/split-policy-validator.js +0 -188
- package/.agents/scripts/lib/orchestration/story-plan-state.js +0 -33
- package/.agents/scripts/lib/orchestration/structured-comment-parser.js +0 -67
- package/.agents/scripts/lib/templates/spec-author-prompts.js +0 -76
- package/.agents/skills/core/scope-triage/SKILL.md +0 -48
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* runtime-deps/parser-major — which `@babel/parser` major the complexity
|
|
3
|
+
* kernel can actually parse with, and how to say so when it is wrong.
|
|
4
|
+
*
|
|
5
|
+
* Its own module because two very different callers need the same answer and
|
|
6
|
+
* neither should drag the other in: the kernel asserts it at load, and
|
|
7
|
+
* `mandrel doctor` reports it to a consumer. Importing the kernel into doctor
|
|
8
|
+
* to ask one version question would pull the whole metric core and install the
|
|
9
|
+
* AST compatibility patch as a side effect.
|
|
10
|
+
*
|
|
11
|
+
* `.agents/` materializes into the consumer's repository root, so
|
|
12
|
+
* `@babel/parser` resolves from *their* `node_modules`. A range in
|
|
13
|
+
* `runtime-deps.json` documents the requirement; it cannot enforce it. This is
|
|
14
|
+
* the enforcement.
|
|
15
|
+
*
|
|
16
|
+
* @module lib/runtime-deps/parser-major
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import fs from 'node:fs';
|
|
20
|
+
import { createRequire } from 'node:module';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The only `@babel/parser` major the kernel supports.
|
|
24
|
+
*
|
|
25
|
+
* Module-local, with `describeParserMajorError` as the single public door:
|
|
26
|
+
* every caller wants the verdict and the remedy, not the number.
|
|
27
|
+
*
|
|
28
|
+
* 8.x removed several plugin names from the kernel's fixed list (they became
|
|
29
|
+
* default syntax), so it does not merely warn — it throws on the plugin list
|
|
30
|
+
* itself. Adopting it is a deliberate change with a baseline recut attached,
|
|
31
|
+
* not something to absorb from a consumer's resolution.
|
|
32
|
+
*/
|
|
33
|
+
const SUPPORTED_PARSER_MAJOR = 7;
|
|
34
|
+
|
|
35
|
+
/** Package whose resolved major gates the kernel. */
|
|
36
|
+
const PARSER_PACKAGE = '@babel/parser';
|
|
37
|
+
|
|
38
|
+
/** Memoised resolved parser version: `undefined` unread, `null` unresolvable. */
|
|
39
|
+
let parserVersion;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Read the resolved `@babel/parser` version from its own manifest.
|
|
43
|
+
*
|
|
44
|
+
* The package exports no version, so its `package.json` is the only source.
|
|
45
|
+
* This is the one non-static resolution in the file and it deliberately
|
|
46
|
+
* targets a manifest rather than code: `@babel/parser` itself is reached by a
|
|
47
|
+
* static import above, so it is declared and preflighted like every other
|
|
48
|
+
* dependency.
|
|
49
|
+
*
|
|
50
|
+
* @returns {string|null} The resolved version, or `null` when the manifest
|
|
51
|
+
* cannot be read (a layout that hides `package.json` behind `exports`, say).
|
|
52
|
+
*/
|
|
53
|
+
function resolveParserVersion() {
|
|
54
|
+
if (parserVersion !== undefined) return parserVersion;
|
|
55
|
+
try {
|
|
56
|
+
const require = createRequire(import.meta.url);
|
|
57
|
+
const manifest = require.resolve(`${PARSER_PACKAGE}/package.json`);
|
|
58
|
+
const parsed = JSON.parse(fs.readFileSync(manifest, 'utf-8'));
|
|
59
|
+
parserVersion = typeof parsed?.version === 'string' ? parsed.version : null;
|
|
60
|
+
} catch {
|
|
61
|
+
parserVersion = null;
|
|
62
|
+
}
|
|
63
|
+
return parserVersion;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The resolved parser's major version.
|
|
68
|
+
*
|
|
69
|
+
* @returns {number|null} `null` when the version could not be resolved or
|
|
70
|
+
* does not lead with an integer.
|
|
71
|
+
*/
|
|
72
|
+
function resolveParserMajor() {
|
|
73
|
+
const version = resolveParserVersion();
|
|
74
|
+
if (version === null) return null;
|
|
75
|
+
const major = Number.parseInt(version, 10);
|
|
76
|
+
return Number.isInteger(major) ? major : null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Describe the resolved-parser problem, if there is one.
|
|
81
|
+
*
|
|
82
|
+
* Single-sourced so the load-time assertion below and the preflight guard
|
|
83
|
+
* (`runtime-deps/ensure-installed.js`) emit the *same* named, actionable
|
|
84
|
+
* message — the point of AC-5 is that a consumer never meets this as a
|
|
85
|
+
* plugin-list syntax error mid-scan.
|
|
86
|
+
*
|
|
87
|
+
* An unresolvable version is **not** a problem: a consumer layout that hides
|
|
88
|
+
* the manifest still resolves the parser itself, and refusing to score would
|
|
89
|
+
* be a worse answer than scoring with an unverified parser. Only a
|
|
90
|
+
* *known-wrong* major is reported.
|
|
91
|
+
*
|
|
92
|
+
* @param {{major?: number|null, version?: string|null}} [resolved] Overrides
|
|
93
|
+
* the resolved parser, so the message a consumer on an unsupported major
|
|
94
|
+
* would read is assertable without installing one.
|
|
95
|
+
* @returns {string|null} The message, or `null` when the resolved parser is
|
|
96
|
+
* supported (or its version is unknowable).
|
|
97
|
+
*/
|
|
98
|
+
export function describeParserMajorError(resolved = {}) {
|
|
99
|
+
const { major = resolveParserMajor(), version = resolveParserVersion() } =
|
|
100
|
+
resolved;
|
|
101
|
+
if (major === null || major === SUPPORTED_PARSER_MAJOR) return null;
|
|
102
|
+
return (
|
|
103
|
+
`unsupported ${PARSER_PACKAGE} major: resolved ${version}, ` +
|
|
104
|
+
`the complexity kernel requires ${SUPPORTED_PARSER_MAJOR}.x. It parses ` +
|
|
105
|
+
`with a fixed plugin list that later majors reject, so CRAP and ` +
|
|
106
|
+
`maintainability scoring would fail mid-scan with an opaque plugin-list ` +
|
|
107
|
+
`error. Declare "${PARSER_PACKAGE}": "^${SUPPORTED_PARSER_MAJOR}" in ` +
|
|
108
|
+
`your package.json (see .agents/runtime-deps.json).`
|
|
109
|
+
);
|
|
110
|
+
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* runtime-deps/preflight — pure helpers for the dependency-presence check
|
|
2
|
+
* runtime-deps/preflight — pure helpers for the dependency-presence check's
|
|
3
|
+
* *messaging* half.
|
|
3
4
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* The check itself moved to `dep-resolution.js`, which owns resolving a
|
|
6
|
+
* declared dependency, judging its major, and explaining a mismatch. What is
|
|
7
|
+
* left here holds no side effects and stays unit-testable in isolation:
|
|
8
|
+
* `detectPackageManager` takes an injected `exists` seam and
|
|
7
9
|
* `formatMissingDepsMessage` is a pure string builder. The side-effecting
|
|
8
10
|
* guard that wires them to the real process lives in `ensure-installed.js`.
|
|
9
11
|
*
|
|
@@ -14,27 +16,6 @@
|
|
|
14
16
|
import fs from 'node:fs';
|
|
15
17
|
import { detectPackageManager as detectPm } from '../detect-package-manager.js';
|
|
16
18
|
|
|
17
|
-
/**
|
|
18
|
-
* Resolve each required package via the injected `resolve` seam and collect
|
|
19
|
-
* the ones that fail. `resolve` is typically `require.resolve` bound to the
|
|
20
|
-
* framework module location; it throws `MODULE_NOT_FOUND` when a package is
|
|
21
|
-
* absent from the resolvable `node_modules`.
|
|
22
|
-
*
|
|
23
|
-
* @param {{ required: string[], resolve: (specifier: string) => string }} opts
|
|
24
|
-
* @returns {{ ok: boolean, missing: string[] }}
|
|
25
|
-
*/
|
|
26
|
-
export function checkRuntimeDeps({ required, resolve }) {
|
|
27
|
-
const missing = [];
|
|
28
|
-
for (const dep of required) {
|
|
29
|
-
try {
|
|
30
|
-
resolve(dep);
|
|
31
|
-
} catch {
|
|
32
|
-
missing.push(dep);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
return { ok: missing.length === 0, missing };
|
|
36
|
-
}
|
|
37
|
-
|
|
38
19
|
/**
|
|
39
20
|
* Detect the consumer's package manager from lockfile presence so the
|
|
40
21
|
* remediation message names the right install command. Defaults to `npm`.
|
|
@@ -71,6 +71,51 @@ const STATIC_FROM =
|
|
|
71
71
|
const SIDE_EFFECT = /^\s*import\s*['"]([^'"]+)['"]/gm;
|
|
72
72
|
// `require(...)` and dynamic `import(...)` may appear mid-expression.
|
|
73
73
|
const CALL_FORM = /\b(?:require|import)\s*\(\s*['"]([^'"]+)['"]/g;
|
|
74
|
+
// Names bound to a `createRequire(...)` result, e.g.
|
|
75
|
+
// `const fromReader = createRequire(x)`. Such a binding is a require function
|
|
76
|
+
// under a different name, so calls through it are real runtime imports that
|
|
77
|
+
// `CALL_FORM` cannot see — it matches the literal callees `require`/`import`.
|
|
78
|
+
// A module reached only that way would be an undeclared, unpreflighted
|
|
79
|
+
// dependency that this scanner reported as absent.
|
|
80
|
+
const REQUIRE_ALIAS =
|
|
81
|
+
/(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*createRequire\s*\(/g;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Build a matcher for calls through `createRequire`-bound identifiers.
|
|
85
|
+
*
|
|
86
|
+
* Returns `null` when the source binds none, so the common case adds no pass.
|
|
87
|
+
* Aliases named `require` need no entry — `CALL_FORM` already covers them.
|
|
88
|
+
*
|
|
89
|
+
* @param {string} cleaned Comment-stripped source.
|
|
90
|
+
* @returns {RegExp|null}
|
|
91
|
+
*/
|
|
92
|
+
function aliasedRequireMatcher(cleaned) {
|
|
93
|
+
REQUIRE_ALIAS.lastIndex = 0;
|
|
94
|
+
const names = new Set();
|
|
95
|
+
let m = REQUIRE_ALIAS.exec(cleaned);
|
|
96
|
+
while (m !== null) {
|
|
97
|
+
if (m[1] !== 'require') names.add(m[1]);
|
|
98
|
+
m = REQUIRE_ALIAS.exec(cleaned);
|
|
99
|
+
}
|
|
100
|
+
if (names.size === 0) return null;
|
|
101
|
+
const alternation = [...names]
|
|
102
|
+
.map((n) => n.replace(/[$]/g, '\\$$'))
|
|
103
|
+
.join('|');
|
|
104
|
+
return new RegExp(`\\b(?:${alternation})\\s*\\(\\s*['"]([^'"]+)['"]`, 'g');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* The specifier patterns to run over one source: the three fixed forms, plus
|
|
109
|
+
* an alias matcher when the source binds a `createRequire` result.
|
|
110
|
+
*
|
|
111
|
+
* @param {string} cleaned Comment-stripped source.
|
|
112
|
+
* @returns {RegExp[]}
|
|
113
|
+
*/
|
|
114
|
+
function specifierMatchers(cleaned) {
|
|
115
|
+
const aliased = aliasedRequireMatcher(cleaned);
|
|
116
|
+
if (!aliased) return [STATIC_FROM, SIDE_EFFECT, CALL_FORM];
|
|
117
|
+
return [STATIC_FROM, SIDE_EFFECT, CALL_FORM, aliased];
|
|
118
|
+
}
|
|
74
119
|
|
|
75
120
|
/**
|
|
76
121
|
* Extract the set of third-party top-level package names imported by a
|
|
@@ -82,7 +127,7 @@ const CALL_FORM = /\b(?:require|import)\s*\(\s*['"]([^'"]+)['"]/g;
|
|
|
82
127
|
export function extractThirdPartyImports(source) {
|
|
83
128
|
const found = new Set();
|
|
84
129
|
const cleaned = stripJsComments(source);
|
|
85
|
-
for (const re of
|
|
130
|
+
for (const re of specifierMatchers(cleaned)) {
|
|
86
131
|
re.lastIndex = 0;
|
|
87
132
|
let match = re.exec(cleaned);
|
|
88
133
|
while (match !== null) {
|
|
@@ -41,7 +41,7 @@ export const LOCAL_SKILLS_SEGMENTS = Object.freeze([
|
|
|
41
41
|
|
|
42
42
|
/**
|
|
43
43
|
* A skill id is the tier-relative path naming a skill — e.g.
|
|
44
|
-
* `core/
|
|
44
|
+
* `core/test-first` or `stack/qa/playwright`. It is the value that
|
|
45
45
|
* appears in `skills.index.json` minus the root prefix, and the value a
|
|
46
46
|
* `qa.environments.*.signInSeam.skill` seam carries.
|
|
47
47
|
*
|
|
@@ -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
|
/**
|
|
@@ -24,10 +24,11 @@ import { BODY_FORMAT_LINTS } from '../story-body/body-format-lints.js';
|
|
|
24
24
|
* no verify-tier suffix — every one of those either scored a shape the
|
|
25
25
|
* authoring model already judges or prescribed a proxy that became the
|
|
26
26
|
* goal.
|
|
27
|
-
* - **The N>1 rules** ({@link renderStorySplitRules}) — the schedule
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
27
|
+
* - **The N>1 rules** ({@link renderStorySplitRules}) — the schedule rules
|
|
28
|
+
* that only mean anything once a draft has siblings: every Story must
|
|
29
|
+
* earn its slot in the wave schedule, and no same-wave pair may collide
|
|
30
|
+
* on a declared path (Story #5332 replaced the acceptance partition with
|
|
31
|
+
* the dispatcher's own collision predicate, armed as a refusal).
|
|
31
32
|
* - **The tickets-mode rules** ({@link ticketsModePromptField}, Story
|
|
32
33
|
* #5323) — what to re-derive rather than carry when the seed is an
|
|
33
34
|
* existing ticket whose body is already in Story shape.
|
|
@@ -116,7 +117,8 @@ The **persisted** \`body\` renders these markdown sections (in order) — you au
|
|
|
116
117
|
<optional technical approach at contract level — do NOT restate Goal / Acceptance / Verify>
|
|
117
118
|
|
|
118
119
|
## Changes
|
|
119
|
-
-
|
|
120
|
+
- <file path>
|
|
121
|
+
- {"path": "<file path>", "assumption": "deletes"}
|
|
120
122
|
- ...
|
|
121
123
|
|
|
122
124
|
## Acceptance <-- synthesized by persist from acceptance[]; do not author
|
|
@@ -135,10 +137,10 @@ The **persisted** \`body\` renders these markdown sections (in order) — you au
|
|
|
135
137
|
|
|
136
138
|
- **goal** (in body string): One sentence stating WHY this Story exists.
|
|
137
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/\`.
|
|
138
|
-
- **slicing** (optional): Ordered intra-session checkpoints for one Story, one line each.
|
|
139
|
-
- **changes** (in body string): Each entry is
|
|
140
|
-
- **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).
|
|
141
|
-
- **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.
|
|
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.
|
|
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.
|
|
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".
|
|
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.
|
|
142
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).
|
|
143
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.
|
|
144
146
|
|
|
@@ -171,27 +173,18 @@ ${advisoryCaveat}
|
|
|
171
173
|
|
|
172
174
|
**Decompose at deliverable granularity, not module/task level.** ${granularityDefinition}
|
|
173
175
|
|
|
174
|
-
The only sizing question is **cohesion**: *is this one coherent change with one reason to exist?* There is no ceiling on a Story's footprint, Spec length or acceptance count
|
|
176
|
+
The only sizing question is **cohesion**: *is this one coherent change with one reason to exist?* There is no target, floor or ceiling on a Story's footprint, Spec length or acceptance count, and a long acceptance list is a description of a broad capability, never a reason to split. A broad contract cutover is one Story when every changed site changes for the same reason. Frontier models one-shot capability-sized work in a single pass; do not fragment a coherent capability into dependent slices to stay "small", and do not pad a Story with adjacent work to look "complete".
|
|
175
177
|
|
|
176
178
|
${envelopeFloor}
|
|
177
179
|
|
|
178
|
-
- **One Story = one coherent change with one reason to exist.**
|
|
180
|
+
- **One Story = one coherent change with one reason to exist.**
|
|
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.
|
|
179
182
|
- ${singleConsumerRule}
|
|
180
|
-
- **Split independent, parallelizable work** into sibling Stories — but only when the pieces genuinely have separate reasons to exist.
|
|
181
183
|
|
|
182
|
-
#### UI
|
|
184
|
+
#### UI AND COPY WORK — where the contract is written down:
|
|
183
185
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
- Stories that touch UI (\`*.tsx\`, \`*.astro\`, \`*.svelte\`, \`*.vue\`, components folders) MUST carry the testid contract as a top-level \`acceptance[]\` item, one of:
|
|
187
|
-
- \`"data-testid invariance: <list of testids that MUST be preserved>"\`, or
|
|
188
|
-
- \`"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.
|
|
189
|
-
- State the preserved-testid set in \`## Non-Goals\` prose as well when the Story deliberately renames nothing.
|
|
190
|
-
- Renaming a testid without the matching e2e edit is FORBIDDEN.
|
|
191
|
-
|
|
192
|
-
#### BRAND / COPY / STYLE WORK:
|
|
193
|
-
|
|
194
|
-
- 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.
|
|
195
188
|
|
|
196
189
|
CRITICAL: Dependencies should follow execution blockers. There is no parent ticket — never emit a 'parent_slug' field.
|
|
197
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.
|
|
@@ -201,7 +194,7 @@ IMPORTANT DEPENDENCY RULE: Story-to-Story dependencies are expressed via \`depen
|
|
|
201
194
|
/**
|
|
202
195
|
* The rules that only apply once a draft has more than one Story: the
|
|
203
196
|
* delivery-schedule simulation that makes each Story earn its slot, and the
|
|
204
|
-
*
|
|
197
|
+
* same-wave collision refusal persist enforces at N>1.
|
|
205
198
|
*
|
|
206
199
|
* @returns {string}
|
|
207
200
|
*/
|
|
@@ -213,16 +206,18 @@ You are splitting past the default-single policy, so simulate the delivery sched
|
|
|
213
206
|
1. **Build the wave schedule.** A Story runs only after every \`depends_on\` completes, and two Stories that name the same file in \`changes[]\` cannot run in the same wave (the scheduler serializes file-overlapping Stories even when no \`depends_on\` edge links them).
|
|
214
207
|
2. **Every Story must earn its slot** by at least one of:
|
|
215
208
|
- **(a) parallelism** — it actually runs concurrently with a sibling in the schedule you just built ("logically independent" does not count; *schedule*-independent does);
|
|
216
|
-
- **(b)
|
|
217
|
-
|
|
218
|
-
3. **A dependent link with none of those justifications merges into its consumer.** This generalizes the single-consumer merge rule from pairs to chains: N Stories that deliver no faster than one Story pay N delivery sessions (branch, PR, review, CI) for nothing.
|
|
209
|
+
- **(b) cohesion break** — merged into its neighbor it would no longer be one coherent change with one reason to exist.
|
|
210
|
+
3. **A dependent link with neither of those justifications merges into its consumer.** This generalizes the single-consumer merge rule from pairs to chains: N Stories that deliver no faster than one Story pay N delivery sessions (branch, PR, review, CI) for nothing.
|
|
219
211
|
4. **When one file appears in the \`changes[]\` of most of your Stories, the slicing axis cuts across a shared seam** — merge the Stories that co-edit it, or re-slice along the seam so each Story owns its files.
|
|
220
212
|
|
|
221
|
-
####
|
|
213
|
+
#### THE COLLISION REFUSAL (persist-enforced at N>1):
|
|
214
|
+
|
|
215
|
+
Persist runs the **dispatcher's own** collision predicate pairwise over your draft, before it creates a single issue, and **refuses** the plan when any two same-wave Stories collide — both declaring a path in \`changes[]\`, or one declaring a glob that covers the other's path. Such a pair cannot be co-dispatched, so the split buys no parallelism and costs a delivery session. Two remedies, both yours to choose at authoring time:
|
|
216
|
+
|
|
217
|
+
- **Merge the pair** into the one Story they already are, with \`## Slicing\` checkpoints for the stages; or
|
|
218
|
+
- **Order them** with \`depends_on\` so they sit in different waves, when they genuinely have separate reasons to exist.
|
|
222
219
|
|
|
223
|
-
|
|
224
|
-
- Each Story carries its **own** \`## Spec\`; a shared \`techspec.md\` cannot be folded into N>1 Stories.
|
|
225
|
-
- Express ordering with \`depends_on\` (a sibling slug, or \`#<id>\` for an open Story from an earlier plan). A Story whose \`verify[]\` runs against a file a sibling creates MUST \`depends_on\` that sibling, so the file exists when verification runs.`;
|
|
220
|
+
Each Story carries its **own** \`## Spec\`; a shared \`techspec.md\` cannot be folded into N>1 Stories. Express ordering with \`depends_on\` (a sibling slug, or \`#<id>\` for an open Story from an earlier plan). A Story whose \`verify[]\` runs against a file a sibling creates MUST \`depends_on\` that sibling, so the file exists when verification runs.`;
|
|
226
221
|
}
|
|
227
222
|
|
|
228
223
|
/**
|
|
@@ -244,7 +239,7 @@ function renderStoryTicketsRules() {
|
|
|
244
239
|
|
|
245
240
|
You are planning from one or more existing tickets. Read them for **what the work is** — the problem, the constraints, the commands that verify it — and re-derive everything else. Specifically:
|
|
246
241
|
|
|
247
|
-
1. **Re-derive \`acceptance[]\` from the goal.** Do not copy the source's \`## Acceptance\` list, and never carry its \`AC-<n>:\` handles — the body renderer numbers the checkboxes itself, so a copied handle renders doubled. A source ticket carrying fifteen criteria is telling you its acceptance was over-specified, not that yours must be: state the
|
|
242
|
+
1. **Re-derive \`acceptance[]\` from the goal.** Do not copy the source's \`## Acceptance\` list, and never carry its \`AC-<n>:\` handles — the body renderer numbers the checkboxes itself, so a copied handle renders doubled. A source ticket carrying fifteen criteria is telling you its acceptance was over-specified, not that yours must be: state the outcomes a PR reviewer can confirm, and let the mechanical checks fall to \`verify[]\`.
|
|
248
243
|
2. **A mechanical check is a \`verify[]\` command, not an acceptance item.** "Baselines refreshed", "lint exits 0", "the generated index is regenerated", "the quality gate passes" are commands the deliverer runs and the critic reads as evidence. Carrying them as acceptance items inflates the binding contract with work every close already gates.
|
|
249
244
|
3. **Read the source's \`verify[]\` for the commands it names, not for its shape.** Take the test paths and scripts; drop any trailing tier suffix (\`(unit)\`, \`(contract)\`, \`(e2e)\`, \`(validate)\`) and any \`manual:<reason>\` escape — a verify entry is a bare command.
|
|
250
245
|
4. **Re-derive the footprint against the tree as it is now.** The source ticket's \`## Changes\` predicted a repository that has since moved; probe the paths you cite and omit the generated artifacts it listed.
|