mandrel 2.35.0 → 2.37.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.
Files changed (108) hide show
  1. package/.agents/docs/agentrc-reference.json +3 -1
  2. package/.agents/docs/configuration.md +27 -0
  3. package/.agents/rules/testing-standards.md +56 -0
  4. package/.agents/schemas/agentrc.schema.json +11 -0
  5. package/.agents/schemas/lifecycle/merge.unlanded.schema.json +2 -1
  6. package/.agents/schemas/story-deliver-terminal.schema.json +1 -0
  7. package/.agents/scripts/boot-sweep.js +7 -1
  8. package/.agents/scripts/check-context-budget.js +195 -103
  9. package/.agents/scripts/check-cyclomatic.js +112 -42
  10. package/.agents/scripts/check-doc-links.js +23 -2
  11. package/.agents/scripts/check-generated-validator.js +202 -0
  12. package/.agents/scripts/check-lifecycle-lint.js +2 -68
  13. package/.agents/scripts/check-schema-references.js +2 -28
  14. package/.agents/scripts/drain-pending-cleanup.js +18 -1
  15. package/.agents/scripts/git-cleanup.js +2 -0
  16. package/.agents/scripts/lib/baselines/components.js +32 -2
  17. package/.agents/scripts/lib/baselines/env-overrides.js +1 -1
  18. package/.agents/scripts/lib/bootstrap/branch-protection.js +1 -1
  19. package/.agents/scripts/lib/bootstrap/install-ledger.js +1 -1
  20. package/.agents/scripts/lib/bootstrap/prompt.js +1 -1
  21. package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +2 -2
  22. package/.agents/scripts/lib/checks/loop-health.js +1 -1
  23. package/.agents/scripts/lib/cli-args.js +0 -2
  24. package/.agents/scripts/lib/close-validation/gates.js +1 -1
  25. package/.agents/scripts/lib/close-validation/process.js +1 -1
  26. package/.agents/scripts/lib/config/ci.js +18 -0
  27. package/.agents/scripts/lib/config/sync-agentrc.js +1 -1
  28. package/.agents/scripts/lib/config-settings-schema-delivery.js +13 -0
  29. package/.agents/scripts/lib/config-settings-schema.js +46 -6
  30. package/.agents/scripts/lib/coverage-baseline.js +2 -2
  31. package/.agents/scripts/lib/cpu-pool.js +90 -10
  32. package/.agents/scripts/lib/crap-utils.js +6 -2
  33. package/.agents/scripts/lib/cyclomatic-ceiling.js +28 -6
  34. package/.agents/scripts/lib/cyclomatic-scope.js +144 -0
  35. package/.agents/scripts/lib/dependency-version.js +86 -0
  36. package/.agents/scripts/lib/duplicate-search.js +1 -1
  37. package/.agents/scripts/lib/errors/index.js +1 -1
  38. package/.agents/scripts/lib/feedback-loop/graduator-core.js +2 -2
  39. package/.agents/scripts/lib/feedback-loop/prior-feedback-fetcher.js +1 -1
  40. package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +1 -1
  41. package/.agents/scripts/lib/generated/agentrc-validator.js +16 -0
  42. package/.agents/scripts/lib/install-cmd-parser.js +1 -1
  43. package/.agents/scripts/lib/knip-entry-sync.js +1 -57
  44. package/.agents/scripts/lib/maintainability-utils.js +108 -10
  45. package/.agents/scripts/lib/observability/source-classifier.js +1 -1
  46. package/.agents/scripts/lib/orchestration/git-cleanup/phases/branches.js +22 -7
  47. package/.agents/scripts/lib/orchestration/git-cleanup/phases/git-probes.js +22 -14
  48. package/.agents/scripts/lib/orchestration/git-cleanup/phases/merged-tip.js +132 -0
  49. package/.agents/scripts/lib/orchestration/git-cleanup/phases/render.js +56 -11
  50. package/.agents/scripts/lib/orchestration/merge-block-class.js +10 -1
  51. package/.agents/scripts/lib/orchestration/merge-poll.js +164 -0
  52. package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +70 -9
  53. package/.agents/scripts/lib/orchestration/pr-watch.js +1 -30
  54. package/.agents/scripts/lib/orchestration/reassert-status-column.js +2 -2
  55. package/.agents/scripts/lib/orchestration/review-providers/codex.js +3 -3
  56. package/.agents/scripts/lib/orchestration/review-providers/findings-renderer.js +1 -1
  57. package/.agents/scripts/lib/orchestration/review-providers/native.js +9 -2
  58. package/.agents/scripts/lib/orchestration/review-providers/review-depth.js +2 -2
  59. package/.agents/scripts/lib/orchestration/review-providers/security-review.js +3 -3
  60. package/.agents/scripts/lib/orchestration/single-story-close/phases/auto-merge.js +145 -0
  61. package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +96 -5
  62. package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +52 -1
  63. package/.agents/scripts/lib/orchestration/single-story-close/runner.js +9 -1
  64. package/.agents/scripts/lib/orchestration/story-close/format-autofix.js +2 -5
  65. package/.agents/scripts/lib/orchestration/story-follow-ups.js +1 -1
  66. package/.agents/scripts/lib/orchestration/ticket-lease.js +34 -9
  67. package/.agents/scripts/lib/orchestration/ticketing/reads.js +3 -1
  68. package/.agents/scripts/lib/preflight-runner.js +3 -3
  69. package/.agents/scripts/lib/qa/qa-session.js +1 -1
  70. package/.agents/scripts/lib/runtime-deps/manifest.js +1 -7
  71. package/.agents/scripts/lib/runtime-deps/scan-imports.js +3 -58
  72. package/.agents/scripts/lib/single-story-sweep/sweep-lock.js +284 -25
  73. package/.agents/scripts/lib/skills/walk-skill-files.js +1 -1
  74. package/.agents/scripts/lib/source-text/strip-js-comments.js +115 -0
  75. package/.agents/scripts/lib/test-isolate/runner.js +3 -3
  76. package/.agents/scripts/lib/test-runner-contract.js +14 -6
  77. package/.agents/scripts/lib/test-tiers.js +135 -18
  78. package/.agents/scripts/lib/transpile.js +16 -3
  79. package/.agents/scripts/lib/worktree/lifecycle/pending-cleanup.js +169 -31
  80. package/.agents/scripts/lib/worktree/node-modules-strategy.js +2 -5
  81. package/.agents/scripts/notify.js +4 -10
  82. package/.agents/scripts/pr-watch-with-update.js +305 -137
  83. package/.agents/scripts/providers/github/tickets.js +109 -16
  84. package/.agents/scripts/quality-preview.js +162 -70
  85. package/.agents/scripts/run-test-profile.js +8 -5
  86. package/.agents/scripts/run-tests.js +79 -14
  87. package/.agents/scripts/single-story-close.js +0 -2
  88. package/.agents/scripts/single-story-init.js +5 -1
  89. package/.agents/workflows/audit-documentation.md +5 -6
  90. package/docs/CHANGELOG.md +50 -0
  91. package/lib/cli/migrate.js +100 -94
  92. package/lib/migrations/helpers/retire-agentrc-key.js +200 -0
  93. package/lib/migrations/index.js +32 -33
  94. package/lib/migrations/steps/2.1.0-retire-mi-drop-knobs.js +21 -80
  95. package/lib/migrations/steps/2.1.0-retire-verify-concurrency-cap.js +15 -87
  96. package/lib/migrations/steps/2.11.0-retire-max-seed-words.js +13 -71
  97. package/lib/migrations/steps/2.20.0-retire-codebase-snapshot.js +13 -101
  98. package/lib/migrations/steps/2.32.0-retire-lint-baseline-command.js +13 -100
  99. package/package.json +9 -5
  100. package/.agents/scripts/generate-lifecycle-docs.js +0 -237
  101. package/.agents/scripts/lib/audit-suite/frontmatter-lint.js +0 -32
  102. package/.agents/scripts/lib/baselines/maintainability-baseline-save.js +0 -37
  103. package/.agents/scripts/lib/cli/parse-numeric.js +0 -60
  104. package/.agents/scripts/lib/close-validation/telemetry.js +0 -79
  105. package/.agents/scripts/lib/orchestration/label-transitions.js +0 -44
  106. package/.agents/scripts/lib/orchestration/parked-follow-ons.js +0 -147
  107. package/.agents/scripts/lib/orchestration/phase-runner.js +0 -88
  108. package/.agents/scripts/lib/orchestration/recut.js +0 -56
@@ -1,237 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * .agents/scripts/generate-lifecycle-docs.js — Schema-backed lifecycle docs table
4
- *
5
- * Renders a bounded region inside `docs/LIFECYCLE.md` from every
6
- * `.agents/schemas/lifecycle/*.schema.json`. The region is delimited by:
7
- *
8
- * <!-- BEGIN GENERATED:lifecycle-events -->
9
- * ...generated table...
10
- * <!-- END GENERATED:lifecycle-events -->
11
- *
12
- * Columns: | Event | Schema | Description | Required fields |
13
- *
14
- * - Event = schema filename minus `.schema.json`
15
- * - Schema = relative markdown link to the schema file
16
- * - Description = the schema's top-level `description` property
17
- * - Required fields = comma-joined list from the schema's `required` array
18
- * (rendered as inline code; "—" when empty/absent)
19
- *
20
- * Skips `README.md`. The `ledger-record.schema.json` file is a record
21
- * envelope rather than a lifecycle event, but to keep this generator
22
- * literal-schema driven we still emit a row for it — the surrounding doc
23
- * already calls out the distinction.
24
- *
25
- * Modes:
26
- * (default) — rewrites the bounded region in place.
27
- * --check — exits 0 when the on-disk region matches the freshly
28
- * generated content, exits 1 with a diff hint otherwise.
29
- *
30
- * Per `.agents/rules/orchestration-error-handling.md`, unrecoverable
31
- * failures surface via `throw new Error(...)` so `runAsCli` can map the
32
- * throw to `process.exit(1)` deterministically (no `Logger.fatal`).
33
- */
34
-
35
- import fs from 'node:fs';
36
- import path from 'node:path';
37
- import { fileURLToPath } from 'node:url';
38
- import { parseArgs } from 'node:util';
39
- import { runAsCli } from './lib/cli-utils.js';
40
- import { Logger } from './lib/Logger.js';
41
-
42
- const __filename = fileURLToPath(import.meta.url);
43
- const __dirname = path.dirname(__filename);
44
- const PROJECT_ROOT = path.resolve(__dirname, '..', '..');
45
- const SCHEMA_DIR = path.join(PROJECT_ROOT, '.agents', 'schemas', 'lifecycle');
46
- const DOC_PATH = path.join(PROJECT_ROOT, 'docs', 'LIFECYCLE.md');
47
- const REGION_BEGIN = '<!-- BEGIN GENERATED:lifecycle-events -->';
48
- const REGION_END = '<!-- END GENERATED:lifecycle-events -->';
49
-
50
- /**
51
- * Read and parse every `*.schema.json` under the lifecycle schema dir, in
52
- * ASCII-sorted filename order. The sort is intentional — it produces a
53
- * stable diff regardless of the host filesystem's enumeration order, which
54
- * is what makes `--check` reliable across platforms.
55
- *
56
- * @param {string} dir Absolute path to the schema directory.
57
- * @returns {Array<{event:string, file:string, description:string, required:string[]}>}
58
- */
59
- function readLifecycleSchemas(dir) {
60
- if (!fs.existsSync(dir)) {
61
- throw new Error(`Lifecycle schema directory not found: ${dir}`);
62
- }
63
- const entries = fs
64
- .readdirSync(dir)
65
- .filter((name) => name.endsWith('.schema.json'))
66
- .sort();
67
- if (entries.length === 0) {
68
- throw new Error(`No *.schema.json files found in ${dir}`);
69
- }
70
- return entries.map((file) => {
71
- const abs = path.join(dir, file);
72
- const raw = fs.readFileSync(abs, 'utf8');
73
- let json;
74
- try {
75
- json = JSON.parse(raw);
76
- } catch (err) {
77
- throw new Error(`Failed to parse JSON schema ${file}: ${err.message}`);
78
- }
79
- const event = file.replace(/\.schema\.json$/, '');
80
- const description =
81
- typeof json.description === 'string' && json.description.trim().length > 0
82
- ? json.description.trim()
83
- : '';
84
- const required = Array.isArray(json.required) ? [...json.required] : [];
85
- return { event, file, description, required };
86
- });
87
- }
88
-
89
- /**
90
- * Escape pipe characters so they survive Markdown table cell parsing.
91
- *
92
- * @param {string} text
93
- * @returns {string}
94
- */
95
- function escapeCell(text) {
96
- return text.replace(/\|/g, '\\|');
97
- }
98
-
99
- /**
100
- * Render the bounded-region body (the generated content between the two
101
- * comment markers, exclusive of the markers themselves).
102
- *
103
- * @param {ReturnType<typeof readLifecycleSchemas>} rows
104
- * @returns {string}
105
- */
106
- function renderTable(rows) {
107
- const header = '| Event | Schema | Description | Required fields |';
108
- const sep = '| --- | --- | --- | --- |';
109
- const body = rows.map((row) => {
110
- const eventCell = `\`${row.event}\``;
111
- const schemaLink = `[\`${row.file}\`](../.agents/schemas/lifecycle/${row.file})`;
112
- const description = escapeCell(row.description || '—');
113
- const requiredCell =
114
- row.required.length === 0
115
- ? '—'
116
- : row.required.map((field) => `\`${field}\``).join(', ');
117
- return `| ${eventCell} | ${schemaLink} | ${description} | ${requiredCell} |`;
118
- });
119
- // Surround with blank lines so the markers + table read as a separate block.
120
- return ['', header, sep, ...body, ''].join('\n');
121
- }
122
-
123
- /**
124
- * Substitute the bounded region inside `original`. Throws if either marker
125
- * is missing, or if `BEGIN` appears after `END`. Idempotent — a re-run on
126
- * the same input yields byte-identical output.
127
- *
128
- * @param {string} original
129
- * @param {string} body Region body, already including leading/trailing blank
130
- * lines (as produced by `renderTable`).
131
- * @returns {string}
132
- */
133
- function spliceRegion(original, body) {
134
- const beginIdx = original.indexOf(REGION_BEGIN);
135
- const endIdx = original.indexOf(REGION_END);
136
- if (beginIdx === -1) {
137
- throw new Error(
138
- `Missing region marker "${REGION_BEGIN}" in ${DOC_PATH}. ` +
139
- 'Insert the marker pair before re-running the generator.',
140
- );
141
- }
142
- if (endIdx === -1) {
143
- throw new Error(
144
- `Missing region marker "${REGION_END}" in ${DOC_PATH}. ` +
145
- 'Insert the marker pair before re-running the generator.',
146
- );
147
- }
148
- if (endIdx < beginIdx) {
149
- throw new Error(
150
- `Region markers out of order in ${DOC_PATH}: END appears before BEGIN.`,
151
- );
152
- }
153
- const before = original.slice(0, beginIdx + REGION_BEGIN.length);
154
- const after = original.slice(endIdx);
155
- return `${before}\n${body}\n${after}`;
156
- }
157
-
158
- /**
159
- * Build the canonical post-generation file content for `docs/LIFECYCLE.md`.
160
- *
161
- * @param {string} schemaDir
162
- * @param {string} docPath
163
- * @returns {{ generated: string, original: string }}
164
- */
165
- function buildExpected(schemaDir, docPath) {
166
- if (!fs.existsSync(docPath)) {
167
- throw new Error(`Target doc not found: ${docPath}`);
168
- }
169
- const original = fs.readFileSync(docPath, 'utf8');
170
- const rows = readLifecycleSchemas(schemaDir);
171
- const body = renderTable(rows);
172
- const generated = spliceRegion(original, body);
173
- return { generated, original };
174
- }
175
-
176
- /**
177
- * @param {string[]} argv
178
- */
179
- async function main(argv = process.argv.slice(2)) {
180
- const { values } = parseArgs({
181
- args: argv,
182
- options: {
183
- check: { type: 'boolean', default: false },
184
- },
185
- allowPositionals: false,
186
- });
187
-
188
- const { generated, original } = buildExpected(SCHEMA_DIR, DOC_PATH);
189
-
190
- if (values.check) {
191
- if (generated === original) {
192
- Logger.info(
193
- `generate-lifecycle-docs: ${path.relative(PROJECT_ROOT, DOC_PATH)} is up to date.`,
194
- );
195
- return;
196
- }
197
- const hint =
198
- `${path.relative(PROJECT_ROOT, DOC_PATH)} is out of date. ` +
199
- 'Run `node .agents/scripts/generate-lifecycle-docs.js` to regenerate the bounded region.';
200
- throw new Error(hint);
201
- }
202
-
203
- if (generated === original) {
204
- Logger.info(
205
- `generate-lifecycle-docs: ${path.relative(PROJECT_ROOT, DOC_PATH)} already current — no write.`,
206
- );
207
- return;
208
- }
209
- fs.writeFileSync(DOC_PATH, generated, 'utf8');
210
- Logger.info(
211
- `generate-lifecycle-docs: wrote bounded region into ${path.relative(PROJECT_ROOT, DOC_PATH)}.`,
212
- );
213
- }
214
-
215
- export {
216
- buildExpected,
217
- REGION_BEGIN,
218
- REGION_END,
219
- readLifecycleSchemas,
220
- renderTable,
221
- spliceRegion,
222
- };
223
-
224
- runAsCli(import.meta.url, main, {
225
- source: 'generate-lifecycle-docs',
226
- usage: {
227
- invocation: 'node .agents/scripts/generate-lifecycle-docs.js [--check]',
228
- summary:
229
- 'Regenerate the lifecycle-event table in docs/LIFECYCLE.md from the event schemas. Writes only when the generated content differs.',
230
- flags: [
231
- [
232
- '--check',
233
- 'Verify the doc is current and fail if stale; write nothing.',
234
- ],
235
- ],
236
- },
237
- });
@@ -1,32 +0,0 @@
1
- /**
2
- * lib/audit-suite/frontmatter-lint.js — workflow frontmatter linter.
3
- *
4
- * Lives next to `frontmatter.js` but stays in its own module so adding
5
- * lints doesn't drag the summary helper's maintainability score down.
6
- * Pure: no IO, no provider calls, safe to unit-test in isolation.
7
- *
8
- * Story #1324, Epic #1185 — Dispatch performance pass.
9
- *
10
- * Story #2824, Epic #2815 — Model-hint frontmatter was removed from
11
- * every workflow and from the validator's field list. The function is
12
- * retained as a no-op safety net so callers (and any future frontmatter
13
- * lints) keep a stable entry point.
14
- */
15
-
16
- import { extractFrontmatter } from './frontmatter.js';
17
-
18
- /**
19
- * Pure: lint a frontmatter map (or raw workflow content). Currently a
20
- * no-op — no frontmatter fields are validated after the model-hint
21
- * removal. The signature is preserved so future field-level lints can
22
- * slot in without churning callers.
23
- *
24
- * @param {string | Record<string, string>} input
25
- * @returns {{ ok: boolean, errors: Array<{ field: string, value: string, message: string }> }}
26
- */
27
- export function validateFrontmatter(input) {
28
- // Extract so callers still pay the same "is this parseable?" cost as
29
- // before; we just have no field-level rules to enforce right now.
30
- if (typeof input === 'string') extractFrontmatter(input);
31
- return { ok: true, errors: [] };
32
- }
@@ -1,37 +0,0 @@
1
- import path from 'node:path';
2
- import {
3
- write as writeBaselineEnvelope,
4
- writeFile as writeBaselineFile,
5
- } from './writer.js';
6
-
7
- /**
8
- * Saves a new maintainability baseline to disk at `baselinePath`.
9
- *
10
- * Accepts the legacy flat `{ path: mi }` shape for backwards compatibility
11
- * with existing callers (`regenerateMainFromTree`, refresh helpers). The
12
- * map is transformed into the canonical envelope shape (`$schema`,
13
- * `kernelVersion`, `generatedAt`, `rollup`, `rows`) via the shared
14
- * `lib/baselines/writer.js` pipeline before being persisted, so every
15
- * write produces a file that round-trips through `lib/baselines/reader.js`
16
- * without schema errors.
17
- *
18
- * @param {Record<string, number>} baseline path→MI flat map.
19
- * @param {string} baselinePath Required — caller supplies via getBaselines().
20
- */
21
- export function saveBaseline(baseline, baselinePath) {
22
- if (typeof baselinePath !== 'string' || baselinePath.length === 0) {
23
- throw new TypeError(
24
- 'maintainability-utils.saveBaseline: baselinePath is required.',
25
- );
26
- }
27
- const abs = path.isAbsolute(baselinePath)
28
- ? baselinePath
29
- : path.resolve(process.cwd(), baselinePath);
30
-
31
- const rows = Object.entries(baseline ?? {}).map(([p, mi]) => ({
32
- path: p,
33
- mi,
34
- }));
35
- const envelope = writeBaselineEnvelope({ kind: 'maintainability', rows });
36
- writeBaselineFile(abs, envelope);
37
- }
@@ -1,60 +0,0 @@
1
- /**
2
- * parse-numeric.js — shared CLI integer-flag validators (Story #2993).
3
- *
4
- * Extracted from `lifecycle-emit-story-dispatch.js` so future scripts that
5
- * need a strict positive-integer or non-negative-integer flag value share
6
- * a single validator instead of hand-rolling another `Number.parseInt`
7
- * dance. Both helpers throw with a clear "required" / "must be …" message
8
- * shape so a `runAsCli` boundary surfaces a clean exit-1.
9
- *
10
- * Contract:
11
- * - Empty / `null` / `undefined` raw values throw "is required".
12
- * - Non-integer or out-of-range values throw "must be a … integer".
13
- * - A leading `#` on the raw value is stripped (matches the ticket-ID
14
- * ergonomic on the rest of the framework).
15
- *
16
- * @module lib/cli/parse-numeric
17
- */
18
-
19
- /**
20
- * Parse a flag value as a strict positive integer (≥ 1). Leading `#` is
21
- * stripped to match the `parseTicketId` ergonomic.
22
- *
23
- * @param {string|number|null|undefined} raw
24
- * @param {string} flag Flag label used in the thrown error message.
25
- * @param {string} [tool='cli'] Optional tool prefix for the error message.
26
- * @returns {number}
27
- */
28
- export function parseRequiredPositiveInt(raw, flag, tool = 'cli') {
29
- if (raw === undefined || raw === null || raw === '') {
30
- throw new Error(`${tool}: ${flag} is required`);
31
- }
32
- const n = Number.parseInt(String(raw).replace(/^#/, ''), 10);
33
- if (!Number.isInteger(n) || n < 1) {
34
- throw new Error(`${tool}: ${flag} must be a positive integer (got ${raw})`);
35
- }
36
- return n;
37
- }
38
-
39
- /**
40
- * Parse a flag value as a strict non-negative integer (≥ 0). Leading `#`
41
- * is NOT stripped — non-negative use cases (wave index, attempt offset)
42
- * do not carry the ticket-ID convention.
43
- *
44
- * @param {string|number|null|undefined} raw
45
- * @param {string} flag
46
- * @param {string} [tool='cli']
47
- * @returns {number}
48
- */
49
- export function parseRequiredNonNegativeInt(raw, flag, tool = 'cli') {
50
- if (raw === undefined || raw === null || raw === '') {
51
- throw new Error(`${tool}: ${flag} is required`);
52
- }
53
- const n = Number.parseInt(String(raw), 10);
54
- if (!Number.isInteger(n) || n < 0) {
55
- throw new Error(
56
- `${tool}: ${flag} must be a non-negative integer (got ${raw})`,
57
- );
58
- }
59
- return n;
60
- }
@@ -1,79 +0,0 @@
1
- /**
2
- * close-validation/telemetry.js — gh-spawn telemetry emitter.
3
- */
4
-
5
- import { writeFile as defaultWriteFile } from 'node:fs/promises';
6
- import path from 'node:path';
7
- import { storyTempDir } from '../config/temp-paths.js';
8
- import { getSpawnCount as defaultGetSpawnCount } from '../gh-exec.js';
9
-
10
- /**
11
- * Throw-away ghSpawnCount emitter (Story #1795 / Epic #1788).
12
- *
13
- * Writes the current `gh-exec` spawn counter to
14
- * `temp/run-<id>/stories/story-<sid>/gh-spawn-count.json`.
15
- *
16
- * Story #4545 — its reader is gone: `analyze-execution.js` consumed this file
17
- * to emit a `ghSpawnCount` field on the `story-perf-summary` payload, and both
18
- * that CLI and that payload were deleted with the execution-analysis surface.
19
- * The writer itself already had no production caller before that (the
20
- * `runPostMergeClose` orchestrator named below went in the v2.0.0 cutover), so
21
- * this module is production-dead and kept alive only by its own test — the
22
- * test-importer blind spot the dead-exports ratchet cannot see. It is left in
23
- * place rather than deleted because reviving spawn telemetry against the live
24
- * close path is a decision, not a sweep.
25
- *
26
- * @param {object} opts
27
- * @param {number|string} opts.epicId
28
- * @param {number|string} opts.storyId
29
- * @param {object} [opts.config] - Resolved config bag so `tempRoot`
30
- * resolution honours the consumer's configured path.
31
- * @param {() => number} [opts.getSpawnCountFn=defaultGetSpawnCount] - Test seam.
32
- * @param {typeof defaultWriteFile} [opts.writeFileFn=defaultWriteFile] - Test seam.
33
- * @param {{ warn?: (s: string) => void }} [opts.logger] - Best-effort
34
- * failure-path logger; never throws.
35
- * @returns {Promise<{ status: 'ok'|'failed', path?: string, ghSpawnCount?: number, reason?: string }>}
36
- */
37
- export async function emitGhSpawnCount({
38
- epicId,
39
- storyId,
40
- config,
41
- getSpawnCountFn = defaultGetSpawnCount,
42
- writeFileFn = defaultWriteFile,
43
- logger,
44
- } = {}) {
45
- const eid = Number(epicId);
46
- const sid = Number(storyId);
47
- if (!Number.isInteger(eid) || eid < 1 || !Number.isInteger(sid) || sid < 1) {
48
- return { status: 'failed', reason: 'invalid-ids' };
49
- }
50
- let ghSpawnCount;
51
- try {
52
- ghSpawnCount = getSpawnCountFn();
53
- } catch (err) {
54
- logger?.warn?.(
55
- `[close-validation] gh-spawn-count read failed: ${err?.message ?? err}`,
56
- );
57
- return { status: 'failed', reason: 'counter-read-failed' };
58
- }
59
- const targetPath = path.join(
60
- storyTempDir(eid, sid, config),
61
- 'gh-spawn-count.json',
62
- );
63
- const payload = {
64
- kind: 'gh-spawn-count',
65
- epicId: eid,
66
- storyId: sid,
67
- ghSpawnCount,
68
- capturedAt: new Date().toISOString(),
69
- };
70
- try {
71
- await writeFileFn(targetPath, JSON.stringify(payload, null, 2));
72
- return { status: 'ok', path: targetPath, ghSpawnCount };
73
- } catch (err) {
74
- logger?.warn?.(
75
- `[close-validation] gh-spawn-count emit failed: ${err?.message ?? err}`,
76
- );
77
- return { status: 'failed', reason: 'write-failed' };
78
- }
79
- }
@@ -1,44 +0,0 @@
1
- /**
2
- * label-transitions.js — readability wrappers over `transitionTicketState`.
3
- *
4
- * These are deliberately thin: each helper names the *target* state and
5
- * forwards to the underlying SDK call. They exist so that init / close /
6
- * deliver-tail call sites read as prose ("toExecuting(provider, taskId)")
7
- * instead of forcing readers to parse the `STATE_LABELS.X` constant at
8
- * every call site.
9
- *
10
- * Not an abstraction — the underlying `transitionTicketState` remains the
11
- * authoritative single-ticket transition path. Opts are forwarded verbatim.
12
- */
13
-
14
- import { STATE_LABELS, transitionTicketState } from './ticketing.js';
15
-
16
- /** Transition a ticket to `agent::executing`. */
17
- export function toExecuting(provider, ticketId, opts) {
18
- return transitionTicketState(
19
- provider,
20
- ticketId,
21
- STATE_LABELS.EXECUTING,
22
- opts,
23
- );
24
- }
25
-
26
- /**
27
- * Transition an array of tickets to `agent::done`, in order. Each call
28
- * triggers its own cascade (via `transitionTicketState`). Failures for
29
- * individual tickets propagate — the loop aborts on the first error, so
30
- * callers that need per-ticket tolerance must catch around each id
31
- * themselves.
32
- *
33
- * @param {object} provider
34
- * @param {number[]} ticketIds
35
- * @param {object} [opts]
36
- */
37
- export async function toDone(provider, ticketIds, opts) {
38
- if (!Array.isArray(ticketIds)) {
39
- throw new TypeError('toDone: ticketIds must be an array');
40
- }
41
- for (const id of ticketIds) {
42
- await transitionTicketState(provider, id, STATE_LABELS.DONE, opts);
43
- }
44
- }
@@ -1,147 +0,0 @@
1
- /**
2
- * Classify Stories that exist under an Epic but are absent from the frozen
3
- * dispatch manifest. Two cases:
4
- *
5
- * - "recut" — the Story carries a `<!-- recut-of: #N -->` marker whose
6
- * parent ID matches a manifest entry. These are attributable
7
- * to a manifest Story and must be satisfied alongside it.
8
- *
9
- * - "parked" — the Story is genuinely outside the manifest (carved off
10
- * mid-sprint, no recut lineage). The operator should explicitly
11
- * adopt it into the current Epic or defer it. Surfaced as a
12
- * structured comment so `/deliver` has a single checkpoint.
13
- *
14
- * Both categories are informational at the wave-completeness gate — they do
15
- * not fail closure by themselves. The gate continues to enforce that every
16
- * manifest Story is closed; recuts and parked follow-ons are additional
17
- * transparency.
18
- */
19
-
20
- import { parseRecutMarker } from './recut.js';
21
-
22
- /**
23
- * Partition Stories under an Epic into manifest, recut, and parked buckets.
24
- *
25
- * @param {number[]} manifestStoryIds IDs present in the dispatch manifest.
26
- * @param {Array<{ id: number, title?: string, body?: string, state?: string, labels?: string[] }>} storiesUnderEpic
27
- * All `type::story` tickets under the Epic.
28
- * @returns {{
29
- * manifest: Array<object>,
30
- * recuts: Array<{ storyId: number, parentId: number, title: string, state: string }>,
31
- * parked: Array<{ storyId: number, title: string, state: string }>,
32
- * }}
33
- */
34
- export function classifyStoriesAgainstManifest(
35
- manifestStoryIds,
36
- storiesUnderEpic,
37
- ) {
38
- const manifestSet = new Set(manifestStoryIds.map(Number));
39
- const manifest = [];
40
- const recuts = [];
41
- const parked = [];
42
-
43
- for (const story of storiesUnderEpic) {
44
- if (manifestSet.has(story.id)) {
45
- manifest.push(story);
46
- continue;
47
- }
48
- const marker = parseRecutMarker(story.body);
49
- if (marker && manifestSet.has(marker.parentStoryId)) {
50
- recuts.push({
51
- storyId: story.id,
52
- parentId: marker.parentStoryId,
53
- title: story.title ?? '',
54
- state: story.state ?? 'open',
55
- });
56
- } else {
57
- parked.push({
58
- storyId: story.id,
59
- title: story.title ?? '',
60
- state: story.state ?? 'open',
61
- });
62
- }
63
- }
64
-
65
- return { manifest, recuts, parked };
66
- }
67
-
68
- /**
69
- * Render the structured `parked-follow-ons` comment body for an Epic.
70
- * Idempotent: the same input produces identical output, so repeated upserts
71
- * don't churn comment revisions.
72
- *
73
- * @param {number} epicId
74
- * @param {ReturnType<typeof classifyStoriesAgainstManifest>} classification
75
- * @returns {string}
76
- */
77
- export function renderParkedFollowOnsComment(epicId, classification) {
78
- const { recuts, parked } = classification;
79
-
80
- const lines = [
81
- `## 🪝 Parked Follow-Ons & Recuts — Epic #${epicId}`,
82
- '',
83
- 'Stories created under this Epic that are **not** in the frozen dispatch',
84
- 'manifest. Surfaced here so `/deliver` can gate on them at the',
85
- 'completeness check.',
86
- '',
87
- `- **Recuts** (attributable to a manifest Story): ${recuts.length}`,
88
- `- **Parked follow-ons** (no manifest lineage): ${parked.length}`,
89
- '',
90
- ];
91
-
92
- if (recuts.length > 0) {
93
- lines.push('### Recuts');
94
- lines.push('');
95
- lines.push('| Story | Recut-of | State | Title |');
96
- lines.push('| :--- | :--- | :--- | :--- |');
97
- for (const r of recuts) {
98
- lines.push(
99
- `| #${r.storyId} | #${r.parentId} | ${r.state} | ${r.title} |`,
100
- );
101
- }
102
- lines.push('');
103
- }
104
-
105
- if (parked.length > 0) {
106
- lines.push('### Parked Follow-Ons');
107
- lines.push('');
108
- lines.push('| Story | State | Title |');
109
- lines.push('| :--- | :--- | :--- |');
110
- for (const p of parked) {
111
- lines.push(`| #${p.storyId} | ${p.state} | ${p.title} |`);
112
- }
113
- lines.push('');
114
- lines.push(
115
- '> **Action required**: adopt each Story into the current Epic (by',
116
- '> re-running the dispatcher so the manifest is refreshed), or explicitly',
117
- '> defer by closing the Story with `state_reason=not_planned`.',
118
- );
119
- lines.push('');
120
- }
121
-
122
- if (recuts.length === 0 && parked.length === 0) {
123
- lines.push(
124
- '✅ No out-of-manifest Stories detected — every Story under this Epic is in the dispatch manifest.',
125
- );
126
- lines.push('');
127
- }
128
-
129
- lines.push('```json');
130
- lines.push(
131
- JSON.stringify(
132
- {
133
- recuts: recuts.map((r) => ({
134
- storyId: r.storyId,
135
- parentId: r.parentId,
136
- state: r.state,
137
- })),
138
- parked: parked.map((p) => ({ storyId: p.storyId, state: p.state })),
139
- },
140
- null,
141
- 2,
142
- ),
143
- );
144
- lines.push('```');
145
-
146
- return lines.join('\n');
147
- }