mandrel 2.24.0 → 2.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/agents/acceptance-critic.md +19 -21
- package/.agents/agents/auditor.md +16 -19
- package/.agents/agents/story-worker.md +41 -54
- package/.agents/audit-checklists/architecture.md +5 -1
- package/.agents/audit-checklists/baselines.md +21 -0
- package/.agents/audit-checklists/quality.md +3 -0
- package/.agents/docs/agentrc-reference.json +2 -1
- package/.agents/docs/configuration.md +2 -1
- package/.agents/docs/quality-gates.md +80 -18
- package/.agents/docs/workflows.md +4 -2
- package/.agents/instructions.md +1 -1
- package/.agents/rules/ci-remediation.md +68 -3
- package/.agents/schemas/agentrc.schema.json +6 -1
- package/.agents/schemas/audit-rules.json +15 -0
- package/.agents/schemas/baselines/audit-baselines-envelope.schema.json +242 -0
- package/.agents/schemas/baselines/baseline-envelope.schema.json +8 -0
- package/.agents/schemas/baselines/crap.schema.json +13 -1
- package/.agents/schemas/crap-report.schema.json +37 -0
- package/.agents/schemas/model-attribution.schema.json +4 -0
- package/.agents/scripts/acceptance-eval.js +124 -15
- package/.agents/scripts/audit-baselines.js +136 -0
- package/.agents/scripts/audit-labels-bootstrap.js +16 -95
- package/.agents/scripts/audit-to-stories.js +33 -3
- package/.agents/scripts/check-arch-cycles.js +12 -93
- package/.agents/scripts/check-baseline-drift.js +16 -3
- package/.agents/scripts/check-baselines.js +19 -3
- package/.agents/scripts/check-context-budget.js +95 -14
- package/.agents/scripts/check-cyclomatic.js +214 -0
- package/.agents/scripts/check-schema-references.js +392 -0
- package/.agents/scripts/check-test-temp-hygiene.js +121 -1
- package/.agents/scripts/check-workflow-timeouts.js +291 -0
- package/.agents/scripts/cleanup-repo-test-temp.js +7 -5
- package/.agents/scripts/deliver-light.js +3 -4
- package/.agents/scripts/deliver-recover.js +13 -0
- package/.agents/scripts/diagnose-friction.js +85 -19
- package/.agents/scripts/lib/audit-baselines/engine.js +177 -0
- package/.agents/scripts/lib/audit-baselines/gate-surface.js +63 -0
- package/.agents/scripts/lib/audit-baselines/headroom.js +72 -0
- package/.agents/scripts/lib/audit-baselines/hotspots.js +69 -0
- package/.agents/scripts/lib/audit-baselines/kinds.js +313 -0
- package/.agents/scripts/lib/audit-baselines/outliers.js +100 -0
- package/.agents/scripts/lib/audit-baselines/read.js +87 -0
- package/.agents/scripts/lib/audit-baselines/staleness.js +123 -0
- package/.agents/scripts/lib/audit-baselines/surface-entry.js +106 -0
- package/.agents/scripts/lib/audit-baselines/trend.js +125 -0
- package/.agents/scripts/lib/audit-baselines/weights.js +193 -0
- package/.agents/scripts/lib/audit-suite/index.js +0 -5
- package/.agents/scripts/lib/audit-suite/selector.js +9 -62
- package/.agents/scripts/lib/audit-to-stories/audit-label-taxonomy.js +177 -0
- package/.agents/scripts/lib/audit-to-stories/audit-lenses.js +1 -0
- package/.agents/scripts/lib/audit-to-stories/build-story-body.js +35 -3
- package/.agents/scripts/lib/audit-to-stories/group-findings.js +20 -11
- package/.agents/scripts/lib/audit-to-stories/parse-audit-md.js +23 -9
- package/.agents/scripts/lib/audit-to-stories/seed-from-findings.js +9 -2
- package/.agents/scripts/lib/baseline-loader.js +0 -0
- package/.agents/scripts/lib/baseline-schema-registry.js +13 -1
- package/.agents/scripts/lib/baselines/diff-scope-cli.js +22 -160
- package/.agents/scripts/lib/baselines/duplication-scanner.js +27 -0
- package/.agents/scripts/lib/baselines/git-base.js +0 -0
- package/.agents/scripts/lib/baselines/kinds/crap.js +499 -57
- package/.agents/scripts/lib/baselines/preview-gates.js +58 -15
- package/.agents/scripts/lib/baselines/reader.js +10 -0
- package/.agents/scripts/lib/baselines/refresh-service.js +69 -11
- package/.agents/scripts/lib/baselines/scope.js +39 -90
- package/.agents/scripts/lib/baselines/writer.js +16 -7
- package/.agents/scripts/lib/changed-files.js +8 -1
- package/.agents/scripts/lib/cli-args.js +115 -1
- package/.agents/scripts/lib/close-validation/runner.js +70 -25
- package/.agents/scripts/lib/config-settings-schema-delivery.js +5 -0
- package/.agents/scripts/lib/crap-engine.js +124 -27
- package/.agents/scripts/lib/crap-method-identity.js +153 -0
- package/.agents/scripts/lib/crap-utils.js +86 -13
- package/.agents/scripts/lib/cyclomatic-ceiling.js +265 -0
- package/.agents/scripts/lib/env-loader.js +46 -16
- package/.agents/scripts/lib/feedback-loop/audit-results-graduator.js +0 -2
- package/.agents/scripts/lib/feedback-loop/prior-feedback-fetcher.js +0 -2
- package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +0 -2
- package/.agents/scripts/lib/findings/route-finding.js +123 -17
- package/.agents/scripts/lib/findings/severity.js +80 -2
- package/.agents/scripts/lib/git/cached-fetch.js +0 -0
- package/.agents/scripts/lib/git-utils.js +136 -80
- package/.agents/scripts/lib/import-graph.js +156 -0
- package/.agents/scripts/lib/label-constants.js +17 -0
- package/.agents/scripts/lib/observability/runtime-friction.js +17 -2
- package/.agents/scripts/lib/observability/source-classifier.js +175 -2
- package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +110 -17
- package/.agents/scripts/lib/orchestration/ceremony-routing.js +17 -12
- package/.agents/scripts/lib/orchestration/check-baselines/phases/compare.js +36 -6
- package/.agents/scripts/lib/orchestration/check-baselines/phases/evaluate.js +5 -0
- package/.agents/scripts/lib/orchestration/check-baselines/phases/floors.js +12 -1
- package/.agents/scripts/lib/orchestration/check-baselines/phases/report.js +8 -1
- package/.agents/scripts/lib/orchestration/ci-rerun-guard.js +548 -0
- package/.agents/scripts/lib/orchestration/deliver-recover.js +2 -2
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/phase-drivers.js +10 -5
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/render.js +39 -3
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/watcher.js +71 -20
- package/.agents/scripts/lib/orchestration/light-backstop.js +20 -3
- package/.agents/scripts/lib/orchestration/light-escalation.js +82 -3
- package/.agents/scripts/lib/orchestration/light-suitability.js +103 -5
- package/.agents/scripts/lib/orchestration/plan-context.js +119 -66
- package/.agents/scripts/lib/orchestration/plan-persist/fan-out-gate.js +31 -5
- package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +216 -109
- package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +61 -13
- package/.agents/scripts/lib/orchestration/plan-persist/supersede-ops.js +79 -22
- package/.agents/scripts/lib/orchestration/plan-text-hygiene.js +51 -20
- package/.agents/scripts/lib/orchestration/planning/authoring-context.js +70 -74
- package/.agents/scripts/lib/orchestration/planning/memory-pool-advisory.js +231 -0
- package/.agents/scripts/lib/orchestration/resolve-stories.js +18 -17
- package/.agents/scripts/lib/orchestration/retro-proposals.js +0 -0
- package/.agents/scripts/lib/orchestration/run-epilogue.js +12 -0
- package/.agents/scripts/lib/orchestration/run-scoped-config.js +276 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/base-sync.js +41 -10
- package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +125 -37
- package/.agents/scripts/lib/orchestration/single-story-close/phases/normalize-pr-title.js +6 -6
- package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +42 -38
- package/.agents/scripts/lib/orchestration/single-story-close/phases/pull-request.js +129 -21
- package/.agents/scripts/lib/orchestration/single-story-close/phases/push.js +6 -1
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +267 -125
- package/.agents/scripts/lib/orchestration/spec-budget.js +16 -5
- package/.agents/scripts/lib/orchestration/story-follow-ups.js +182 -95
- package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +22 -0
- package/.agents/scripts/lib/orchestration/ticket-validator.js +5 -11
- package/.agents/scripts/lib/orchestration/ticketing/reads.js +4 -4
- package/.agents/scripts/lib/reserved-test-ids.js +77 -0
- package/.agents/scripts/lib/story-adjacency.js +3 -3
- package/.agents/scripts/lib/story-plan.js +137 -42
- package/.agents/scripts/lib/templates/decomposer-prompts.js +4 -4
- package/.agents/scripts/lib/test-runner-contract.js +134 -0
- package/.agents/scripts/lib/test-tiers.js +11 -2
- package/.agents/scripts/lib/ticket-body-sections.js +1 -1
- package/.agents/scripts/lib/util/concurrent-map.js +17 -0
- package/.agents/scripts/lib/util/parse-id-list.js +103 -0
- package/.agents/scripts/lib/util/poll-loop.js +18 -2
- package/.agents/scripts/lib/wave-runner/live-probe.js +29 -13
- package/.agents/scripts/lib/wave-runner/ready-set.js +254 -43
- package/.agents/scripts/lib/workers/combined-mi-crap-worker.js +10 -13
- package/.agents/scripts/lib/workers/crap-worker.js +8 -13
- package/.agents/scripts/lib/workers/maintainability-report-worker.js +4 -10
- package/.agents/scripts/lib/workers/maintainability-worker.js +4 -10
- package/.agents/scripts/lib/workers/serve-worker-messages.js +35 -0
- package/.agents/scripts/lib/worktree/git-hooks.js +206 -0
- package/.agents/scripts/lib/worktree/lifecycle/creation.js +6 -0
- package/.agents/scripts/lib/worktree-manager.js +14 -0
- package/.agents/scripts/plan-run-epilogue.js +17 -5
- package/.agents/scripts/pr-watch-with-update.js +568 -213
- package/.agents/scripts/providers/github/tickets.js +33 -10
- package/.agents/scripts/provision-git-hooks.js +85 -0
- package/.agents/scripts/quality-preview.js +137 -28
- package/.agents/scripts/resolve-stories.js +4 -1
- package/.agents/scripts/run-coverage.js +86 -35
- package/.agents/scripts/run-lint.js +20 -0
- package/.agents/scripts/run-tests.js +36 -36
- package/.agents/scripts/single-story-close.js +34 -2
- package/.agents/scripts/single-story-confirm-merge.js +22 -6
- package/.agents/scripts/single-story-init.js +7 -0
- package/.agents/scripts/stories-wave-tick.js +308 -47
- package/.agents/scripts/story-plan.js +65 -9
- package/.agents/scripts/update-coverage-baseline.js +34 -4
- package/.agents/scripts/update-crap-baseline.js +42 -4
- package/.agents/scripts/update-duplication-baseline.js +209 -83
- package/.agents/scripts/update-maintainability-baseline.js +31 -4
- package/.agents/scripts/validate-docs-freshness.js +1 -0
- package/.agents/skills/core/diagnose-friction/SKILL.md +4 -1
- package/.agents/skills/core/gates-and-baselines/SKILL.md +17 -11
- package/.agents/skills/skills.index.json +2 -2
- package/.agents/templates/single-story-body.md +16 -8
- package/.agents/workflows/audit-architecture.md +69 -13
- package/.agents/workflows/audit-baselines.md +289 -0
- package/.agents/workflows/audit-navigability.md +5 -4
- package/.agents/workflows/audit-quality.md +26 -0
- package/.agents/workflows/audit-to-stories.md +30 -4
- package/.agents/workflows/deliver.md +92 -97
- package/.agents/workflows/helpers/acceptance-self-eval.md +47 -10
- package/.agents/workflows/helpers/audit-lens-core.md +25 -4
- package/.agents/workflows/helpers/code-quality-guardrails.md +9 -2
- package/.agents/workflows/helpers/deliver-digest.md +41 -21
- package/.agents/workflows/helpers/deliver-reference.md +95 -4
- package/.agents/workflows/helpers/deliver-story-reference.md +174 -52
- package/.agents/workflows/helpers/deliver-story.md +104 -119
- package/.agents/workflows/helpers/plan-reference.md +49 -5
- package/.agents/workflows/memory-consolidate.md +116 -0
- package/.agents/workflows/plan.md +62 -75
- package/README.md +13 -6
- package/docs/CHANGELOG.md +93 -0
- package/lib/cli/update.js +14 -11
- package/lib/cli/version-check.js +9 -1
- package/package.json +9 -4
- package/.agents/schemas/friction-event.schema.json +0 -56
- package/.agents/scripts/lib/feedback-loop/memory-freshness.js +0 -707
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
rankDuplicateCandidates,
|
|
33
33
|
readTechStackSummary,
|
|
34
34
|
shouldRefine,
|
|
35
|
+
synthesizeContractSections,
|
|
35
36
|
validateStoryBody,
|
|
36
37
|
} from './lib/story-plan.js';
|
|
37
38
|
|
|
@@ -40,7 +41,8 @@ Usage:
|
|
|
40
41
|
story-plan.js --emit-context (--seed "<seed>" | --seed-file <file>) \\
|
|
41
42
|
[--refine | --no-refine] [--pretty]
|
|
42
43
|
|
|
43
|
-
story-plan.js --body <file> [--
|
|
44
|
+
story-plan.js --body <file> [--acceptance <file>] [--verify <file>] \\
|
|
45
|
+
[--dry-run]
|
|
44
46
|
|
|
45
47
|
story-plan.js --help
|
|
46
48
|
|
|
@@ -48,9 +50,18 @@ Modes:
|
|
|
48
50
|
--emit-context Build the host-LLM authoring envelope and print it as
|
|
49
51
|
JSON on stdout. Use this first; the host LLM authors a
|
|
50
52
|
draft body using the envelope and the body template.
|
|
51
|
-
--body <file> Persist a pre-authored body. Validates shape (
|
|
52
|
-
|
|
53
|
-
calls \`gh issue create\` with
|
|
53
|
+
--body <file> Persist a pre-authored body. Validates shape (## Goal
|
|
54
|
+
and ## Changes present, no Epic: ref, an acceptance +
|
|
55
|
+
verify contract) and calls \`gh issue create\` with
|
|
56
|
+
type::story.
|
|
57
|
+
--acceptance <file>
|
|
58
|
+
JSON string[] of the Story's top-level acceptance
|
|
59
|
+
criteria. Authored once here — NOT mirrored into the
|
|
60
|
+
body by hand; the \`## Acceptance\` section is
|
|
61
|
+
synthesized from it, matching the story-author prompt.
|
|
62
|
+
--verify <file> JSON string[] of the Story's top-level verify entries,
|
|
63
|
+
each ending in a (unit|contract|e2e|validate) tier.
|
|
64
|
+
Synthesized into \`## Verify\` the same way.
|
|
54
65
|
--dry-run With --body: print the body and the gh argv that would
|
|
55
66
|
be invoked, then exit 0. No GitHub mutations.
|
|
56
67
|
|
|
@@ -101,8 +112,13 @@ async function fetchOpenStories(provider) {
|
|
|
101
112
|
* array; the caller decides whether to execute it (persist) or print
|
|
102
113
|
* it (--dry-run).
|
|
103
114
|
*/
|
|
104
|
-
function renderGhArgv({ title, bodyPath, labels }) {
|
|
105
|
-
|
|
115
|
+
function renderGhArgv({ title, bodyPath, labels, body = null }) {
|
|
116
|
+
// `--body-file` streams the authored file untouched; an inline `--body` is
|
|
117
|
+
// used only when persist synthesized sections into it (Story #4874), so the
|
|
118
|
+
// file on disk is no longer what gets created.
|
|
119
|
+
const source =
|
|
120
|
+
typeof body === 'string' ? ['--body', body] : ['--body-file', bodyPath];
|
|
121
|
+
const argv = ['issue', 'create', '--title', title, ...source];
|
|
106
122
|
for (const label of labels) {
|
|
107
123
|
argv.push('--label', label);
|
|
108
124
|
}
|
|
@@ -180,6 +196,31 @@ async function runEmitContext({
|
|
|
180
196
|
write(`${json}\n`);
|
|
181
197
|
}
|
|
182
198
|
|
|
199
|
+
/**
|
|
200
|
+
* Read an optional top-level contract array (`--acceptance` / `--verify`)
|
|
201
|
+
* from a JSON file. Absent flag → an empty list, so the flags stay optional
|
|
202
|
+
* for a body that already carries its own sections.
|
|
203
|
+
*
|
|
204
|
+
* @param {string|undefined} filePath
|
|
205
|
+
* @param {'acceptance'|'verify'} label
|
|
206
|
+
* @returns {Promise<string[]>}
|
|
207
|
+
*/
|
|
208
|
+
async function readContractList(filePath, label) {
|
|
209
|
+
if (!filePath) return [];
|
|
210
|
+
let parsed;
|
|
211
|
+
try {
|
|
212
|
+
parsed = JSON.parse(await readFile(filePath, 'utf8'));
|
|
213
|
+
} catch (err) {
|
|
214
|
+
throw new Error(
|
|
215
|
+
`Cannot read --${label} file "${filePath}": ${err.message}`,
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
if (!Array.isArray(parsed) || parsed.some((v) => typeof v !== 'string')) {
|
|
219
|
+
throw new Error(`--${label} file "${filePath}" must be a JSON string[].`);
|
|
220
|
+
}
|
|
221
|
+
return parsed;
|
|
222
|
+
}
|
|
223
|
+
|
|
183
224
|
async function runPersist({
|
|
184
225
|
values,
|
|
185
226
|
provider,
|
|
@@ -193,17 +234,30 @@ async function runPersist({
|
|
|
193
234
|
if (!bodyPath) {
|
|
194
235
|
throw new Error('--body <file> is required in persist mode.');
|
|
195
236
|
}
|
|
196
|
-
const
|
|
197
|
-
const
|
|
237
|
+
const authored = await readFile(bodyPath, 'utf8');
|
|
238
|
+
const contract = {
|
|
239
|
+
acceptance: await readContractList(values.acceptance, 'acceptance'),
|
|
240
|
+
verify: await readContractList(values.verify, 'verify'),
|
|
241
|
+
};
|
|
242
|
+
const validation = validateStoryBody(authored, contract);
|
|
198
243
|
if (!validation.ok) {
|
|
199
244
|
throw new Error(
|
|
200
245
|
`Drafted body failed validation:\n - ${validation.errors.join('\n - ')}`,
|
|
201
246
|
);
|
|
202
247
|
}
|
|
203
248
|
|
|
249
|
+
// Story #4874: the author writes acceptance[] / verify[] once, at the
|
|
250
|
+
// ticket's top level, and persist renders their sections — never the
|
|
251
|
+
// author by hand.
|
|
252
|
+
const body = synthesizeContractSections(authored, contract);
|
|
204
253
|
const title = extractTitle(body);
|
|
205
254
|
const labels = [TYPE_LABELS.STORY];
|
|
206
|
-
const argv = renderGhArgv({
|
|
255
|
+
const argv = renderGhArgv({
|
|
256
|
+
title,
|
|
257
|
+
bodyPath,
|
|
258
|
+
labels,
|
|
259
|
+
body: body === authored ? null : body,
|
|
260
|
+
});
|
|
207
261
|
|
|
208
262
|
if (dryRun) {
|
|
209
263
|
Logger.info('--- DRY RUN ---');
|
|
@@ -255,6 +309,8 @@ async function main() {
|
|
|
255
309
|
seed: { type: 'string' },
|
|
256
310
|
'seed-file': { type: 'string' },
|
|
257
311
|
body: { type: 'string' },
|
|
312
|
+
acceptance: { type: 'string' },
|
|
313
|
+
verify: { type: 'string' },
|
|
258
314
|
refine: { type: 'boolean', default: false },
|
|
259
315
|
'no-refine': { type: 'boolean', default: false },
|
|
260
316
|
pretty: { type: 'boolean', default: false },
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// cli-opt-out: top-level main()-driven CLI invoked via npm run coverage:update; no runAsCli() wrapper required.
|
|
3
2
|
/**
|
|
4
3
|
* Refresh `baselines/coverage.json` from the most recent
|
|
5
4
|
* `coverage/coverage-final.json`. Run this when you intentionally add,
|
|
@@ -22,6 +21,7 @@ import { createRequire } from 'node:module';
|
|
|
22
21
|
import path from 'node:path';
|
|
23
22
|
import { parseDiffScopeFlag } from './lib/baselines/diff-scope-cli.js';
|
|
24
23
|
import { refreshBaseline } from './lib/baselines/refresh-service.js';
|
|
24
|
+
import { runAsCli } from './lib/cli-utils.js';
|
|
25
25
|
import { getBaselineEpsilon } from './lib/config/quality.js';
|
|
26
26
|
import {
|
|
27
27
|
buildScopePredicate,
|
|
@@ -31,6 +31,32 @@ import {
|
|
|
31
31
|
} from './lib/coverage-baseline.js';
|
|
32
32
|
import { Logger } from './lib/Logger.js';
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Usage block for `--help`. This CLI *writes* on invocation, so the help
|
|
36
|
+
* branch must short-circuit before `main` runs rather than inside it —
|
|
37
|
+
* `runAsCli` answers help first, which makes "a usage probe never mutates a
|
|
38
|
+
* baseline" structural instead of a check `main` has to remember.
|
|
39
|
+
*/
|
|
40
|
+
const USAGE = {
|
|
41
|
+
invocation:
|
|
42
|
+
'node .agents/scripts/update-coverage-baseline.js [--full-scope | --diff-scope <ref>]',
|
|
43
|
+
summary:
|
|
44
|
+
'Score → write the coverage baseline from the coverage-final.json already on disk. With no scope flag the refresh is scoped to the files changed in `origin/main..HEAD`; out-of-scope rows are preserved verbatim.',
|
|
45
|
+
flags: [
|
|
46
|
+
[
|
|
47
|
+
'--full-scope',
|
|
48
|
+
'Rescore every file in every target dir (no out-of-scope merge).',
|
|
49
|
+
],
|
|
50
|
+
[
|
|
51
|
+
'--diff-scope <ref>',
|
|
52
|
+
'Scope the refresh to files changed between <ref> and HEAD. Incompatible with --full-scope.',
|
|
53
|
+
],
|
|
54
|
+
],
|
|
55
|
+
notes: [
|
|
56
|
+
'Run `npm run test:coverage` first — this script never runs the suite itself.',
|
|
57
|
+
],
|
|
58
|
+
};
|
|
59
|
+
|
|
34
60
|
const require = createRequire(import.meta.url);
|
|
35
61
|
|
|
36
62
|
function loadC8Scope(cwd) {
|
|
@@ -123,7 +149,11 @@ function main() {
|
|
|
123
149
|
});
|
|
124
150
|
}
|
|
125
151
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
152
|
+
runAsCli(import.meta.url, main, {
|
|
153
|
+
source: 'coverage-baseline',
|
|
154
|
+
usage: USAGE,
|
|
155
|
+
onError: (err) => {
|
|
156
|
+
Logger.error(`[Coverage] ❌ Fatal error: ${err?.message ?? err}`);
|
|
157
|
+
process.exitCode = 1;
|
|
158
|
+
},
|
|
129
159
|
});
|
|
@@ -5,6 +5,7 @@ import './lib/runtime-deps/ensure-installed.js';
|
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { parseDiffScopeFlag } from './lib/baselines/diff-scope-cli.js';
|
|
7
7
|
import { refreshBaseline } from './lib/baselines/refresh-service.js';
|
|
8
|
+
import { runAsCli } from './lib/cli-utils.js';
|
|
8
9
|
import { getBaselineEpsilon } from './lib/config/quality.js';
|
|
9
10
|
import {
|
|
10
11
|
getBaselines,
|
|
@@ -44,6 +45,40 @@ import { Logger } from './lib/Logger.js';
|
|
|
44
45
|
* "no baseline yet".
|
|
45
46
|
*/
|
|
46
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Usage block for `--help` (Story #4872). This CLI *writes* on invocation, so
|
|
50
|
+
* the help branch must short-circuit before `main` runs rather than inside it —
|
|
51
|
+
* `runAsCli` answers help first, which makes "a usage probe never mutates a
|
|
52
|
+
* baseline" structural instead of a check `main` has to remember.
|
|
53
|
+
*/
|
|
54
|
+
const USAGE = {
|
|
55
|
+
invocation:
|
|
56
|
+
'node .agents/scripts/update-crap-baseline.js [--baseline <path>] [--coverage <path>] [--full-scope | --diff-scope <ref>]',
|
|
57
|
+
summary:
|
|
58
|
+
'Scan → score → write the CRAP baseline. With no scope flag the refresh is scoped to the files changed in `origin/main..HEAD`; out-of-scope rows are preserved verbatim.',
|
|
59
|
+
flags: [
|
|
60
|
+
[
|
|
61
|
+
'--baseline <path>',
|
|
62
|
+
'Write to this path instead of `delivery.quality.baselines.crap.path`.',
|
|
63
|
+
],
|
|
64
|
+
[
|
|
65
|
+
'--coverage <path>',
|
|
66
|
+
'Read coverage from this path (default `coverage/coverage-final.json`).',
|
|
67
|
+
],
|
|
68
|
+
[
|
|
69
|
+
'--full-scope',
|
|
70
|
+
'Rescore every file in every target dir (no out-of-scope merge).',
|
|
71
|
+
],
|
|
72
|
+
[
|
|
73
|
+
'--diff-scope <ref>',
|
|
74
|
+
'Scope the refresh to files changed between <ref> and HEAD. Incompatible with --full-scope.',
|
|
75
|
+
],
|
|
76
|
+
],
|
|
77
|
+
notes: [
|
|
78
|
+
'Run `npm run test:coverage` first — without a coverage artifact every file is skipped.',
|
|
79
|
+
],
|
|
80
|
+
};
|
|
81
|
+
|
|
47
82
|
function parseCliArgs(argv = process.argv.slice(2)) {
|
|
48
83
|
const out = {
|
|
49
84
|
baselinePath: undefined,
|
|
@@ -183,8 +218,11 @@ async function main() {
|
|
|
183
218
|
);
|
|
184
219
|
}
|
|
185
220
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
221
|
+
runAsCli(import.meta.url, main, {
|
|
222
|
+
source: 'crap-baseline',
|
|
223
|
+
usage: USAGE,
|
|
224
|
+
onError: (err) => {
|
|
225
|
+
Logger.error(`[CRAP] ❌ Fatal error: ${err?.stack ?? err?.message ?? err}`);
|
|
226
|
+
process.exitCode = 1;
|
|
227
|
+
},
|
|
190
228
|
});
|
|
@@ -1,57 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* update-duplication-baseline.js — manual refresh CLI for the
|
|
3
|
+
* code-duplication (DRY) baseline (Story #3664).
|
|
4
|
+
*
|
|
5
|
+
* Story #4944: this CLI is now a thin wrapper around
|
|
6
|
+
* `refreshBaseline({ kind: 'duplication' })` from
|
|
7
|
+
* `.agents/scripts/lib/baselines/refresh-service.js`, completing the Epic
|
|
8
|
+
* #2173 migration — duplication was the last kind still assembling its own
|
|
9
|
+
* writer call via the legacy `buildWriterScopeArgs` path. Scope resolution,
|
|
10
|
+
* prior-envelope reading, epsilon damping, envelope assembly, and
|
|
11
|
+
* persistence all flow through the unified service.
|
|
12
|
+
*
|
|
13
|
+
* The migration is what makes this CLI's documented surface true. Before it,
|
|
14
|
+
* `--full-scope` was advertised and parsed by nobody, and the no-flag default
|
|
15
|
+
* was a full rewrite rather than the diff-scoped refresh the usage text
|
|
16
|
+
* described.
|
|
17
|
+
*
|
|
18
|
+
* Surface:
|
|
19
|
+
*
|
|
20
|
+
* - `--baseline <path>`: write somewhere other than the configured
|
|
21
|
+
* `delivery.quality.gates.duplication.baselinePath`.
|
|
22
|
+
* - `--diff-scope <ref>`: scope the refresh to files changed between
|
|
23
|
+
* `<ref>` and HEAD. Out-of-scope rows are preserved verbatim from the
|
|
24
|
+
* prior on-disk baseline.
|
|
25
|
+
* - `--full-scope`: regenerate every row (no out-of-scope merge).
|
|
26
|
+
* - With no scope flag: scope is derived from `git diff --name-only
|
|
27
|
+
* origin/main..HEAD` (the service's default `baseRef..headRef`), matching
|
|
28
|
+
* `update-crap-baseline.js` / `update-maintainability-baseline.js` /
|
|
29
|
+
* `update-coverage-baseline.js`.
|
|
30
|
+
*
|
|
31
|
+
* **Scope is a write-side filter here, not a scan-side one.** jscpd detects
|
|
32
|
+
* clones pairwise, so the scan always covers the whole target tree even in
|
|
33
|
+
* diff mode — see `buildDefaultDuplicationScorer` in the refresh service for
|
|
34
|
+
* why narrowing the scan would drop clones between a changed file and an
|
|
35
|
+
* unchanged one. What a scope flag narrows is which rows the refresh is
|
|
36
|
+
* allowed to rewrite.
|
|
37
|
+
*
|
|
38
|
+
* Exits non-zero only when the scanner itself crashes. An empty result (no
|
|
39
|
+
* detected clones) still writes an envelope with `rows: []` so downstream
|
|
40
|
+
* `check-baselines` can tell "intentional empty baseline" apart from "no
|
|
41
|
+
* baseline yet".
|
|
42
|
+
*/
|
|
43
|
+
|
|
1
44
|
// Fail-fast if the framework's runtime deps are not installed — must be the
|
|
2
45
|
// first import so the check runs before any third-party-importing sibling
|
|
3
46
|
// module is evaluated (Story #3432).
|
|
4
47
|
import './lib/runtime-deps/ensure-installed.js';
|
|
5
|
-
import { createRequire } from 'node:module';
|
|
6
48
|
import path from 'node:path';
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
49
|
+
import { parseDiffScopeFlag } from './lib/baselines/diff-scope-cli.js';
|
|
50
|
+
import { refreshBaseline } from './lib/baselines/refresh-service.js';
|
|
51
|
+
import { runAsCli } from './lib/cli-utils.js';
|
|
10
52
|
import { getBaselineEpsilon } from './lib/config/quality.js';
|
|
11
53
|
import { getQuality, resolveConfig } from './lib/config-resolver.js';
|
|
12
54
|
import { Logger } from './lib/Logger.js';
|
|
13
55
|
|
|
14
56
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* `
|
|
19
|
-
* `baselines/duplication.json`), or the path supplied via `--baseline <path>`.
|
|
20
|
-
* Output is a deterministic, kernel-stamped envelope produced by the shared
|
|
21
|
-
* writer — every row path is canonicalised, the per-kind rollup math runs,
|
|
22
|
-
* and the envelope is schema-validated before persisting.
|
|
23
|
-
*
|
|
24
|
-
* Mirrors `update-crap-baseline.js`: thin CLI shell + shared writer funnel.
|
|
25
|
-
* The duplication scan delegates to jscpd's `detectClones` (a pure clone
|
|
26
|
-
* detector with no test coupling), wrapped by `scanDuplication` so the
|
|
27
|
-
* parse→envelope path is unit-testable with the scanner mocked.
|
|
28
|
-
*
|
|
29
|
-
* Exits non-zero only when the scanner itself crashes. An empty result (no
|
|
30
|
-
* detected clones) still writes an envelope with `rows: []` so downstream
|
|
31
|
-
* `check-baselines` can tell "intentional empty baseline" apart from "no
|
|
32
|
-
* baseline yet".
|
|
57
|
+
* Usage block for `--help`. This CLI *writes* on invocation, so the help
|
|
58
|
+
* branch must short-circuit before `main` runs rather than inside it —
|
|
59
|
+
* `runAsCli` answers help first, which makes "a usage probe never mutates a
|
|
60
|
+
* baseline" structural instead of a check `main` has to remember.
|
|
33
61
|
*/
|
|
34
|
-
|
|
35
|
-
|
|
62
|
+
const USAGE = {
|
|
63
|
+
invocation:
|
|
64
|
+
'node .agents/scripts/update-duplication-baseline.js [--baseline <path>] [--full-scope | --diff-scope <ref>]',
|
|
65
|
+
summary:
|
|
66
|
+
'Scan → score → write the code-duplication (DRY) baseline. With no scope flag the refresh is scoped to the files changed in `origin/main..HEAD`; out-of-scope rows are preserved verbatim.',
|
|
67
|
+
flags: [
|
|
68
|
+
[
|
|
69
|
+
'--baseline <path>',
|
|
70
|
+
'Write to this path instead of `delivery.quality.gates.duplication.baselinePath`.',
|
|
71
|
+
],
|
|
72
|
+
[
|
|
73
|
+
'--full-scope',
|
|
74
|
+
'Rescan every file in every target dir (no out-of-scope merge).',
|
|
75
|
+
],
|
|
76
|
+
[
|
|
77
|
+
'--diff-scope <ref>',
|
|
78
|
+
'Scope the refresh to files changed between <ref> and HEAD. Incompatible with --full-scope.',
|
|
79
|
+
],
|
|
80
|
+
],
|
|
81
|
+
notes: [
|
|
82
|
+
'Backed by jscpd; the scan reads the working tree and runs no test suite.',
|
|
83
|
+
'Clone detection is pairwise, so the scan always covers the whole target tree — a scope flag narrows which rows are rewritten, not what is scanned.',
|
|
84
|
+
],
|
|
85
|
+
};
|
|
36
86
|
|
|
37
87
|
/**
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* the rest of the module stays import-pure and testable.
|
|
88
|
+
* Parse `--baseline <path>` — the one flag this CLI does not share with its
|
|
89
|
+
* siblings. The scope flags are parsed by the shared helpers so their
|
|
90
|
+
* contract stays identical across the four update CLIs.
|
|
42
91
|
*
|
|
43
|
-
* @
|
|
92
|
+
* @param {string[]} argv
|
|
93
|
+
* @returns {{ baselinePath: string | undefined }}
|
|
44
94
|
*/
|
|
45
|
-
function resolveDetectClones() {
|
|
46
|
-
const jscpd = require('jscpd');
|
|
47
|
-
if (typeof jscpd.detectClones !== 'function') {
|
|
48
|
-
throw new Error(
|
|
49
|
-
"[Duplication] jscpd.detectClones is not available — run 'npm install'",
|
|
50
|
-
);
|
|
51
|
-
}
|
|
52
|
-
return jscpd.detectClones;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
95
|
function parseCliArgs(argv = process.argv.slice(2)) {
|
|
56
96
|
const out = { baselinePath: undefined };
|
|
57
97
|
for (let i = 0; i < argv.length; i += 1) {
|
|
@@ -63,6 +103,16 @@ function parseCliArgs(argv = process.argv.slice(2)) {
|
|
|
63
103
|
return out;
|
|
64
104
|
}
|
|
65
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Parse `--full-scope` (boolean opt-out flag).
|
|
108
|
+
*
|
|
109
|
+
* @param {string[]} argv
|
|
110
|
+
* @returns {boolean}
|
|
111
|
+
*/
|
|
112
|
+
function parseFullScopeFlag(argv = []) {
|
|
113
|
+
return argv.includes('--full-scope');
|
|
114
|
+
}
|
|
115
|
+
|
|
66
116
|
/**
|
|
67
117
|
* Resolve the duplication gate block from the merged quality config. The
|
|
68
118
|
* flattened legacy bag does not expose a `duplication` accessor (the kind
|
|
@@ -76,59 +126,135 @@ function resolveDuplicationGate(config) {
|
|
|
76
126
|
return gates.duplication ?? {};
|
|
77
127
|
}
|
|
78
128
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
129
|
+
/**
|
|
130
|
+
* Resolve the mutually-exclusive scope selection from argv.
|
|
131
|
+
*
|
|
132
|
+
* Split out of `main` deliberately: nothing in this file is reachable from a
|
|
133
|
+
* test (the CLI is only ever spawned with `--help`, which `runAsCli`
|
|
134
|
+
* short-circuits), so every function here scores CRAP at zero coverage —
|
|
135
|
+
* `c² + c`. A single `main` carrying all the branching lands at c=8 → 72,
|
|
136
|
+
* well over the 30 ceiling. Small single-purpose helpers keep each row far
|
|
137
|
+
* below it without hiding any logic.
|
|
138
|
+
*
|
|
139
|
+
* @param {string[]} argv
|
|
140
|
+
* @returns {{ fullScope: boolean, diffScopeRef: string | null }}
|
|
141
|
+
* @throws {Error} when both scope flags are supplied.
|
|
142
|
+
*/
|
|
143
|
+
function resolveScopeSelection(argv) {
|
|
144
|
+
const diffScopeRef = parseDiffScopeFlag(argv);
|
|
145
|
+
const fullScope = parseFullScopeFlag(argv);
|
|
146
|
+
if (fullScope && diffScopeRef !== null) {
|
|
147
|
+
throw new Error(
|
|
148
|
+
'[Duplication] --full-scope is incompatible with --diff-scope; pick one',
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
return { fullScope, diffScopeRef };
|
|
152
|
+
}
|
|
97
153
|
|
|
98
|
-
|
|
154
|
+
/**
|
|
155
|
+
* Resolve the absolute path the refreshed envelope is written to:
|
|
156
|
+
* `--baseline <path>` wins, then the configured gate path, then the
|
|
157
|
+
* framework default.
|
|
158
|
+
*
|
|
159
|
+
* @param {string[]} argv
|
|
160
|
+
* @param {object} gate resolved duplication gate block
|
|
161
|
+
* @returns {string} absolute path
|
|
162
|
+
*/
|
|
163
|
+
function resolveAbsBaselinePath(argv, gate) {
|
|
164
|
+
const baselinePath =
|
|
165
|
+
parseCliArgs(argv).baselinePath ??
|
|
166
|
+
gate.baselinePath ??
|
|
167
|
+
'baselines/duplication.json';
|
|
168
|
+
return path.isAbsolute(baselinePath)
|
|
99
169
|
? baselinePath
|
|
100
170
|
: path.resolve(process.cwd(), baselinePath);
|
|
171
|
+
}
|
|
101
172
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
173
|
+
/**
|
|
174
|
+
* Announce which scope the refresh resolved to, so an operator reading the
|
|
175
|
+
* log can tell a narrowed refresh from a full rewrite without re-deriving it.
|
|
176
|
+
*
|
|
177
|
+
* @param {{ fullScope: boolean, diffScopeRef: string | null }} selection
|
|
178
|
+
*/
|
|
179
|
+
function logScopeDecision({ fullScope, diffScopeRef }) {
|
|
180
|
+
if (fullScope) {
|
|
181
|
+
Logger.info(
|
|
182
|
+
'[Duplication] --full-scope: regenerating every row (out-of-scope merge disabled).',
|
|
183
|
+
);
|
|
184
|
+
} else if (diffScopeRef) {
|
|
185
|
+
Logger.info(
|
|
186
|
+
`[Duplication] --diff-scope ${diffScopeRef}: narrowing to changed files; out-of-scope rows preserved verbatim.`,
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Assemble the `refreshBaseline` option bag for the resolved selection.
|
|
193
|
+
*
|
|
194
|
+
* No `scorer` is injected — the service resolves the canonical default
|
|
195
|
+
* duplication scorer, which reads `gates.duplication.targetDirs` /
|
|
196
|
+
* `ignoreGlobs` off the same resolved config this CLI reads.
|
|
197
|
+
*
|
|
198
|
+
* @param {{ fullScope: boolean, diffScopeRef: string | null, absBaselinePath: string, epsilon: number }} args
|
|
199
|
+
* @returns {object} options for `refreshBaseline`
|
|
200
|
+
*/
|
|
201
|
+
function buildRefreshOpts({
|
|
202
|
+
fullScope,
|
|
203
|
+
diffScopeRef,
|
|
204
|
+
absBaselinePath,
|
|
205
|
+
epsilon,
|
|
206
|
+
}) {
|
|
207
|
+
const refreshOpts = {
|
|
114
208
|
kind: 'duplication',
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
}
|
|
118
|
-
|
|
209
|
+
writePath: absBaselinePath,
|
|
210
|
+
epsilon,
|
|
211
|
+
};
|
|
212
|
+
if (fullScope) {
|
|
213
|
+
refreshOpts.fullScope = true;
|
|
214
|
+
} else if (diffScopeRef) {
|
|
215
|
+
// The CLI's documented `--diff-scope <ref>` semantics are `<ref>...HEAD`
|
|
216
|
+
// (three-dot). The service derives via two-dot `baseRef..headRef`; pass
|
|
217
|
+
// the ref as `baseRef` so the derivation runs through the same execFile
|
|
218
|
+
// seam the sibling CLIs use.
|
|
219
|
+
refreshOpts.baseRef = diffScopeRef;
|
|
220
|
+
}
|
|
221
|
+
// No flag → scopeFiles=null + fullScope=false → service derives the diff
|
|
222
|
+
// via `origin/main..HEAD` (its default baseRef/headRef).
|
|
223
|
+
return refreshOpts;
|
|
224
|
+
}
|
|
119
225
|
|
|
120
|
-
|
|
121
|
-
|
|
226
|
+
async function main() {
|
|
227
|
+
const argv = process.argv.slice(2);
|
|
228
|
+
const selection = resolveScopeSelection(argv);
|
|
229
|
+
const config = resolveConfig();
|
|
230
|
+
const absBaselinePath = resolveAbsBaselinePath(
|
|
231
|
+
argv,
|
|
232
|
+
resolveDuplicationGate(config),
|
|
122
233
|
);
|
|
234
|
+
|
|
235
|
+
Logger.info('[Duplication] Updating baseline...');
|
|
236
|
+
logScopeDecision(selection);
|
|
237
|
+
|
|
238
|
+
const result = await refreshBaseline(
|
|
239
|
+
buildRefreshOpts({
|
|
240
|
+
...selection,
|
|
241
|
+
absBaselinePath,
|
|
242
|
+
epsilon: getBaselineEpsilon('duplication', config),
|
|
243
|
+
}),
|
|
244
|
+
);
|
|
245
|
+
|
|
123
246
|
Logger.info(
|
|
124
|
-
`[Duplication] ✅ Baseline updated (kernelVersion=${envelope.kernelVersion}). Wrote to ${absBaselinePath}.`,
|
|
247
|
+
`[Duplication] ✅ Baseline updated (kernelVersion=${result.envelope.kernelVersion}, wrote=${result.wrote}, scope=${result.scope.mode}, rows=${result.envelope.rows.length}). Wrote to ${absBaselinePath}.`,
|
|
125
248
|
);
|
|
126
249
|
}
|
|
127
250
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
251
|
+
runAsCli(import.meta.url, main, {
|
|
252
|
+
source: 'duplication-baseline',
|
|
253
|
+
usage: USAGE,
|
|
254
|
+
onError: (err) => {
|
|
255
|
+
Logger.error(
|
|
256
|
+
`[Duplication] ❌ Fatal error: ${err?.stack ?? err?.message ?? err}`,
|
|
257
|
+
);
|
|
258
|
+
process.exitCode = 1;
|
|
259
|
+
},
|
|
134
260
|
});
|
|
@@ -43,10 +43,34 @@ import './lib/runtime-deps/ensure-installed.js';
|
|
|
43
43
|
import path from 'node:path';
|
|
44
44
|
import { parseDiffScopeFlag } from './lib/baselines/diff-scope-cli.js';
|
|
45
45
|
import { refreshBaseline } from './lib/baselines/refresh-service.js';
|
|
46
|
+
import { runAsCli } from './lib/cli-utils.js';
|
|
46
47
|
import { getBaselineEpsilon } from './lib/config/quality.js';
|
|
47
48
|
import { getBaselines, resolveConfig } from './lib/config-resolver.js';
|
|
48
49
|
import { Logger } from './lib/Logger.js';
|
|
49
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Usage block for `--help` (Story #4872). This CLI *writes* on invocation, so
|
|
53
|
+
* the help branch must short-circuit before `main` runs rather than inside it —
|
|
54
|
+
* `runAsCli` answers help first, which makes "a usage probe never mutates a
|
|
55
|
+
* baseline" structural instead of a check `main` has to remember.
|
|
56
|
+
*/
|
|
57
|
+
const USAGE = {
|
|
58
|
+
invocation:
|
|
59
|
+
'node .agents/scripts/update-maintainability-baseline.js [--full-scope | --diff-scope <ref>]',
|
|
60
|
+
summary:
|
|
61
|
+
'Score → write the maintainability baseline. With no scope flag the refresh is scoped to the files changed in `origin/main..HEAD`; out-of-scope rows are preserved verbatim.',
|
|
62
|
+
flags: [
|
|
63
|
+
[
|
|
64
|
+
'--full-scope',
|
|
65
|
+
'Rescore every file in every target dir (no out-of-scope merge).',
|
|
66
|
+
],
|
|
67
|
+
[
|
|
68
|
+
'--diff-scope <ref>',
|
|
69
|
+
'Scope the refresh to files changed between <ref> and HEAD. Incompatible with --full-scope.',
|
|
70
|
+
],
|
|
71
|
+
],
|
|
72
|
+
};
|
|
73
|
+
|
|
50
74
|
/**
|
|
51
75
|
* Parse `--full-scope` (boolean opt-out flag).
|
|
52
76
|
*
|
|
@@ -119,8 +143,11 @@ async function main() {
|
|
|
119
143
|
);
|
|
120
144
|
}
|
|
121
145
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
146
|
+
runAsCli(import.meta.url, main, {
|
|
147
|
+
source: 'maintainability-baseline',
|
|
148
|
+
usage: USAGE,
|
|
149
|
+
onError: (err) => {
|
|
150
|
+
Logger.error(`[Maintainability] ❌ Fatal error: ${err.message}`);
|
|
151
|
+
process.exitCode = 1;
|
|
152
|
+
},
|
|
126
153
|
});
|
|
@@ -15,6 +15,7 @@ allowed_tools:
|
|
|
15
15
|
## Policy Capsule
|
|
16
16
|
|
|
17
17
|
- Invoke via the wrapping CLI `node .agents/scripts/diagnose-friction.js --story <id> [--epic <id>] --cmd <command args...>`; this is the single supported entry point.
|
|
18
|
+
- `--cmd` takes the command as **separate argv words** — the CLI spawns them with no shell. Quoting the whole command as one string (`--cmd "npm run lint"`) makes the entire string the executable name, so the spawn fails ENOENT. That is a usage error in your own invocation, not friction: the CLI refuses it with a usage message and appends **no** ledger row. Re-run it unquoted.
|
|
18
19
|
- Pass the wrapped command's stdout and stderr through **unchanged** — never reformat, redact, or buffer in a way that loses the original failure shape.
|
|
19
20
|
- Never mutate the wrapped command's exit code. The Skill observes; the caller decides whether the failure is fatal.
|
|
20
21
|
- Operate as **best-effort observation**: a write failure on the signals stream MUST NOT halt the runner. A missing signal is preferable to a stalled wave.
|
|
@@ -39,7 +40,9 @@ that want to dispatch via the Skill tool rather than spawn the CLI.
|
|
|
39
40
|
|
|
40
41
|
## Inputs
|
|
41
42
|
|
|
42
|
-
- `--cmd <command args...>` — the command to invoke and observe
|
|
43
|
+
- `--cmd <command args...>` — the command to invoke and observe, passed as
|
|
44
|
+
separate argv words. A single whitespace-containing argument is rejected as
|
|
45
|
+
a quoting mistake before anything is spawned or recorded.
|
|
43
46
|
- `--story <id>` / `--epic <id>` (optional) — when resolved, the Skill
|
|
44
47
|
appends a `friction` signal to
|
|
45
48
|
`temp/run-<eid>/stories/story-<sid>/signals.ndjson` on non-zero exit
|