mandrel 2.25.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 +10 -6
- package/.agents/audit-checklists/baselines.md +21 -0
- package/.agents/docs/quality-gates.md +80 -18
- package/.agents/docs/workflows.md +3 -1
- package/.agents/instructions.md +1 -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 +4 -0
- package/.agents/schemas/baselines/crap.schema.json +8 -0
- package/.agents/schemas/model-attribution.schema.json +4 -0
- package/.agents/scripts/acceptance-eval.js +89 -6
- package/.agents/scripts/audit-baselines.js +136 -0
- 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-cyclomatic.js +214 -0
- package/.agents/scripts/check-schema-references.js +392 -0
- package/.agents/scripts/check-test-temp-hygiene.js +38 -1
- package/.agents/scripts/check-workflow-timeouts.js +291 -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-lenses.js +1 -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 +26 -4
- package/.agents/scripts/lib/baselines/kinds/crap.js +112 -15
- 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 -11
- 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/crap-engine.js +32 -13
- package/.agents/scripts/lib/crap-method-identity.js +153 -0
- package/.agents/scripts/lib/crap-utils.js +13 -0
- package/.agents/scripts/lib/cyclomatic-ceiling.js +265 -0
- 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/git-utils.js +136 -80
- package/.agents/scripts/lib/import-graph.js +156 -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/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/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/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 +209 -109
- package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +48 -12
- 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/run-epilogue.js +12 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +29 -3
- 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/push.js +6 -1
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +245 -140
- 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/story-adjacency.js +3 -3
- package/.agents/scripts/lib/test-runner-contract.js +134 -0
- package/.agents/scripts/lib/test-tiers.js +11 -2
- 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/wave-runner/live-probe.js +24 -14
- package/.agents/scripts/lib/wave-runner/ready-set.js +189 -42
- package/.agents/scripts/lib/workers/combined-mi-crap-worker.js +4 -10
- package/.agents/scripts/lib/workers/crap-worker.js +2 -10
- 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/providers/github/tickets.js +33 -10
- package/.agents/scripts/provision-git-hooks.js +85 -0
- package/.agents/scripts/quality-preview.js +112 -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 +26 -36
- package/.agents/scripts/single-story-close.js +28 -2
- package/.agents/scripts/single-story-confirm-merge.js +22 -6
- package/.agents/scripts/stories-wave-tick.js +214 -38
- package/.agents/scripts/update-coverage-baseline.js +34 -4
- package/.agents/scripts/update-duplication-baseline.js +209 -83
- 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/workflows/audit-baselines.md +289 -0
- package/.agents/workflows/audit-navigability.md +5 -4
- package/.agents/workflows/deliver.md +13 -4
- package/.agents/workflows/helpers/acceptance-self-eval.md +47 -10
- 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 +77 -1
- package/.agents/workflows/helpers/deliver-story-reference.md +47 -6
- package/.agents/workflows/helpers/plan-reference.md +15 -5
- package/.agents/workflows/memory-consolidate.md +116 -0
- package/.agents/workflows/plan.md +3 -0
- package/README.md +13 -6
- package/docs/CHANGELOG.md +64 -0
- 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
|
@@ -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
|
});
|
|
@@ -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
|
|
@@ -18,7 +18,7 @@ allowed_tools:
|
|
|
18
18
|
- **No gate may be skipped.** Failing lint means fix lint, not disable the rule; a failing test means fix the code, not `.skip` or delete the test. Gates are ordered shift-left so cheap checks fail first, and CI failure output is fed back verbatim with the directive to reproduce and fix locally before re-pushing.
|
|
19
19
|
- **Introducing a gate that asserts on pre-existing state** (doc-drift, lint-vocabulary, dependency-cycle, missing-coverage) MUST land green at merge: either advisory-first (report-only until the backlog is burned down) or with the populated baseline committed in the same change that turns the gate on. Never wire a gate into `requiredChecks` that lands red on latent findings nobody authored.
|
|
20
20
|
- **Refresh a baseline only when the change is deliberate** — a rename/move, an operator-approved complexity bump, a signed-off perf delta, an intentional API-surface change. Never refresh to paper over an unintentional regression; fix the regression instead.
|
|
21
|
-
-
|
|
21
|
+
- **Pick the refresh that matches why you are refreshing** — the two are different scopes, and the wrong one silently no-ops. `npm run <kind>:update` is **diff-scoped**: it re-scores only files changed in `origin/main..HEAD` and preserves every other row verbatim. That is what you want after changing code. `npm run <kind>:reanchor` is the same script with `--full-scope`: it re-scores every file in every target dir. That is what you want when the baseline is **stale** — a scorer-version bump, a coverage-shape change, accumulated drift — because a diff-scoped run would leave the stale rows exactly as they were and the staleness finding would re-fire. Kinds: `coverage`, `crap`, `maintainability`, `duplication`; dead-exports and lighthouse have no npm script — regenerate the rows and edit `baselines/dead-exports*.json` / `baselines/lighthouse.json` directly. Run either on the **Story branch**, not on `main`.
|
|
22
22
|
- Verify the refresh diff is scoped to the relevant `baselines/<kind>.json` (plus cosmetic `package-lock.json` churn only). If unrelated files appear, STOP — the refresh is contaminated. Stage baseline files **explicitly** (`git add baselines/<kind>.json`); never `git add -A` in a refresh commit.
|
|
23
23
|
- Commit-subject contract: a **Conventional-Commits** subject `chore(baselines): refresh <kind> snapshot for <reason>` — never an ad-hoc leading token like `baseline-refresh:` (commitlint and the planner validator reject it). The body is **mandatory** and non-empty: what changed, why the new floor is correct, and the Story that triggered it.
|
|
24
24
|
- Add the machine-readable trailer `baseline-refresh: true` (git-trailer `Key: value` style) and `Story: #<storyId>` to the body whenever observability classification matters. Never pass `--no-verify`; the `commit-msg` hook (commitlint) MUST run and pass.
|
|
@@ -95,18 +95,24 @@ own as the parseable marker for any future reader.)
|
|
|
95
95
|
|
|
96
96
|
### Procedure
|
|
97
97
|
|
|
98
|
-
| Kind |
|
|
99
|
-
| --------------- |
|
|
100
|
-
| CRAP | `npm run crap:update`
|
|
101
|
-
| Maintainability | `npm run maintainability:update` |
|
|
102
|
-
|
|
|
103
|
-
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
98
|
+
| Kind | After a code change (diff-scoped) | Stale baseline (full re-anchor) |
|
|
99
|
+
| --------------- | ---------------------------------- | ------------------------------------ |
|
|
100
|
+
| CRAP | `npm run crap:update` | `npm run crap:reanchor` |
|
|
101
|
+
| Maintainability | `npm run maintainability:update` | `npm run maintainability:reanchor` |
|
|
102
|
+
| Coverage | `npm run coverage:update` | `npm run coverage:reanchor` |
|
|
103
|
+
| Duplication | `npm run duplication:update` | `npm run duplication:reanchor` |
|
|
104
|
+
| Dead-exports | edit `baselines/dead-exports.json` / `baselines/dead-exports-production.json` (rows are `(file, symbol)`; `check-dead-exports.js --json` prints the current rows) | same — no scope flag applies |
|
|
105
|
+
| Lighthouse | edit `baselines/lighthouse.json` | same — no scope flag applies |
|
|
106
|
+
|
|
107
|
+
1. **Run the matching command for your reason** — left column after a code
|
|
108
|
+
change, right column for a stale baseline — on the Story branch (HEAD must
|
|
109
|
+
already be the Story branch, not `main`).
|
|
107
110
|
2. **Verify the diff is scoped** to the relevant `baselines/<kind>.json` (and
|
|
108
111
|
possibly cosmetic `package-lock.json` churn). Unrelated files → STOP, the
|
|
109
|
-
refresh is contaminated.
|
|
112
|
+
refresh is contaminated. Judge contamination by **which files** changed, not
|
|
113
|
+
how many rows moved inside the baseline: a `:reanchor` legitimately rewrites
|
|
114
|
+
most of its own file, so say in the commit body that the breadth is a
|
|
115
|
+
re-anchor and not a mass regression.
|
|
110
116
|
3. **Author the commit.** Stage the baseline file(s) explicitly, then commit with
|
|
111
117
|
the Conventional-Commits subject + body trailer above:
|
|
112
118
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"generatedAt": "2026-
|
|
2
|
+
"generatedAt": "2026-08-02T11:35:53.522Z",
|
|
3
3
|
"generator": "generate-skills-index.js@1",
|
|
4
4
|
"skills": [
|
|
5
5
|
{
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"category": "core",
|
|
49
49
|
"path": ".agents/skills/core/diagnose-friction/SKILL.md",
|
|
50
50
|
"description": "Wrap a shell command with diagnostic capture. On failure, print static suggestions and append a structured `friction` record to the per-Story signals.ndjson stream. Use whenever a script in the orchestration loop invokes a tool whose failure shape we want the analyzer to attribute.",
|
|
51
|
-
"policyCapsuleBullets":
|
|
51
|
+
"policyCapsuleBullets": 9,
|
|
52
52
|
"allowedTools": ["Bash", "Read"],
|
|
53
53
|
"vendor": null
|
|
54
54
|
},
|