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.
- package/.agents/docs/agentrc-reference.json +3 -1
- package/.agents/docs/configuration.md +27 -0
- package/.agents/rules/testing-standards.md +56 -0
- package/.agents/schemas/agentrc.schema.json +11 -0
- package/.agents/schemas/lifecycle/merge.unlanded.schema.json +2 -1
- package/.agents/schemas/story-deliver-terminal.schema.json +1 -0
- package/.agents/scripts/boot-sweep.js +7 -1
- package/.agents/scripts/check-context-budget.js +195 -103
- package/.agents/scripts/check-cyclomatic.js +112 -42
- package/.agents/scripts/check-doc-links.js +23 -2
- package/.agents/scripts/check-generated-validator.js +202 -0
- package/.agents/scripts/check-lifecycle-lint.js +2 -68
- package/.agents/scripts/check-schema-references.js +2 -28
- package/.agents/scripts/drain-pending-cleanup.js +18 -1
- package/.agents/scripts/git-cleanup.js +2 -0
- package/.agents/scripts/lib/baselines/components.js +32 -2
- package/.agents/scripts/lib/baselines/env-overrides.js +1 -1
- package/.agents/scripts/lib/bootstrap/branch-protection.js +1 -1
- package/.agents/scripts/lib/bootstrap/install-ledger.js +1 -1
- package/.agents/scripts/lib/bootstrap/prompt.js +1 -1
- package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +2 -2
- package/.agents/scripts/lib/checks/loop-health.js +1 -1
- package/.agents/scripts/lib/cli-args.js +0 -2
- package/.agents/scripts/lib/close-validation/gates.js +1 -1
- package/.agents/scripts/lib/close-validation/process.js +1 -1
- package/.agents/scripts/lib/config/ci.js +18 -0
- package/.agents/scripts/lib/config/sync-agentrc.js +1 -1
- package/.agents/scripts/lib/config-settings-schema-delivery.js +13 -0
- package/.agents/scripts/lib/config-settings-schema.js +46 -6
- package/.agents/scripts/lib/coverage-baseline.js +2 -2
- package/.agents/scripts/lib/cpu-pool.js +90 -10
- package/.agents/scripts/lib/crap-utils.js +6 -2
- package/.agents/scripts/lib/cyclomatic-ceiling.js +28 -6
- package/.agents/scripts/lib/cyclomatic-scope.js +144 -0
- package/.agents/scripts/lib/dependency-version.js +86 -0
- package/.agents/scripts/lib/duplicate-search.js +1 -1
- package/.agents/scripts/lib/errors/index.js +1 -1
- package/.agents/scripts/lib/feedback-loop/graduator-core.js +2 -2
- package/.agents/scripts/lib/feedback-loop/prior-feedback-fetcher.js +1 -1
- package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +1 -1
- package/.agents/scripts/lib/generated/agentrc-validator.js +16 -0
- package/.agents/scripts/lib/install-cmd-parser.js +1 -1
- package/.agents/scripts/lib/knip-entry-sync.js +1 -57
- package/.agents/scripts/lib/maintainability-utils.js +108 -10
- package/.agents/scripts/lib/observability/source-classifier.js +1 -1
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/branches.js +22 -7
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/git-probes.js +22 -14
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/merged-tip.js +132 -0
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/render.js +56 -11
- package/.agents/scripts/lib/orchestration/merge-block-class.js +10 -1
- package/.agents/scripts/lib/orchestration/merge-poll.js +164 -0
- package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +70 -9
- package/.agents/scripts/lib/orchestration/pr-watch.js +1 -30
- package/.agents/scripts/lib/orchestration/reassert-status-column.js +2 -2
- package/.agents/scripts/lib/orchestration/review-providers/codex.js +3 -3
- package/.agents/scripts/lib/orchestration/review-providers/findings-renderer.js +1 -1
- package/.agents/scripts/lib/orchestration/review-providers/native.js +9 -2
- package/.agents/scripts/lib/orchestration/review-providers/review-depth.js +2 -2
- package/.agents/scripts/lib/orchestration/review-providers/security-review.js +3 -3
- package/.agents/scripts/lib/orchestration/single-story-close/phases/auto-merge.js +145 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +96 -5
- package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +52 -1
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +9 -1
- package/.agents/scripts/lib/orchestration/story-close/format-autofix.js +2 -5
- package/.agents/scripts/lib/orchestration/story-follow-ups.js +1 -1
- package/.agents/scripts/lib/orchestration/ticket-lease.js +34 -9
- package/.agents/scripts/lib/orchestration/ticketing/reads.js +3 -1
- package/.agents/scripts/lib/preflight-runner.js +3 -3
- package/.agents/scripts/lib/qa/qa-session.js +1 -1
- package/.agents/scripts/lib/runtime-deps/manifest.js +1 -7
- package/.agents/scripts/lib/runtime-deps/scan-imports.js +3 -58
- package/.agents/scripts/lib/single-story-sweep/sweep-lock.js +284 -25
- package/.agents/scripts/lib/skills/walk-skill-files.js +1 -1
- package/.agents/scripts/lib/source-text/strip-js-comments.js +115 -0
- package/.agents/scripts/lib/test-isolate/runner.js +3 -3
- package/.agents/scripts/lib/test-runner-contract.js +14 -6
- package/.agents/scripts/lib/test-tiers.js +135 -18
- package/.agents/scripts/lib/transpile.js +16 -3
- package/.agents/scripts/lib/worktree/lifecycle/pending-cleanup.js +169 -31
- package/.agents/scripts/lib/worktree/node-modules-strategy.js +2 -5
- package/.agents/scripts/notify.js +4 -10
- package/.agents/scripts/pr-watch-with-update.js +305 -137
- package/.agents/scripts/providers/github/tickets.js +109 -16
- package/.agents/scripts/quality-preview.js +162 -70
- package/.agents/scripts/run-test-profile.js +8 -5
- package/.agents/scripts/run-tests.js +79 -14
- package/.agents/scripts/single-story-close.js +0 -2
- package/.agents/scripts/single-story-init.js +5 -1
- package/.agents/workflows/audit-documentation.md +5 -6
- package/docs/CHANGELOG.md +50 -0
- package/lib/cli/migrate.js +100 -94
- package/lib/migrations/helpers/retire-agentrc-key.js +200 -0
- package/lib/migrations/index.js +32 -33
- package/lib/migrations/steps/2.1.0-retire-mi-drop-knobs.js +21 -80
- package/lib/migrations/steps/2.1.0-retire-verify-concurrency-cap.js +15 -87
- package/lib/migrations/steps/2.11.0-retire-max-seed-words.js +13 -71
- package/lib/migrations/steps/2.20.0-retire-codebase-snapshot.js +13 -101
- package/lib/migrations/steps/2.32.0-retire-lint-baseline-command.js +13 -100
- package/package.json +9 -5
- package/.agents/scripts/generate-lifecycle-docs.js +0 -237
- package/.agents/scripts/lib/audit-suite/frontmatter-lint.js +0 -32
- package/.agents/scripts/lib/baselines/maintainability-baseline-save.js +0 -37
- package/.agents/scripts/lib/cli/parse-numeric.js +0 -60
- package/.agents/scripts/lib/close-validation/telemetry.js +0 -79
- package/.agents/scripts/lib/orchestration/label-transitions.js +0 -44
- package/.agents/scripts/lib/orchestration/parked-follow-ons.js +0 -147
- package/.agents/scripts/lib/orchestration/phase-runner.js +0 -88
- 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
|
-
}
|