mandrel 2.31.0 → 2.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/README.md +13 -17
- package/.agents/agents/acceptance-critic.md +1 -2
- package/.agents/docs/SDLC.md +4 -4
- package/.agents/docs/agentrc-reference.json +61 -57
- package/.agents/docs/configuration.md +274 -227
- package/.agents/docs/execution-reference.md +13 -14
- package/.agents/docs/quality-gates.md +195 -23
- package/.agents/instructions.md +2 -5
- package/.agents/rules/git-conventions-reference.md +27 -27
- package/.agents/rules/git-conventions.md +4 -2
- package/.agents/rules/known-tooling-behavior.md +66 -30
- package/.agents/rules/testing-standards.md +35 -71
- package/.agents/runtime-deps.json +0 -1
- package/.agents/schemas/agentrc.schema.json +1939 -1400
- package/.agents/schemas/lifecycle/README.md +21 -14
- package/.agents/schemas/lifecycle/ledger-record.schema.json +76 -22
- package/.agents/schemas/story-deliver-terminal.schema.json +2 -2
- package/.agents/scripts/README.md +7 -29
- package/.agents/scripts/apply-quality-bootstrap.js +27 -34
- package/.agents/scripts/bootstrap.js +28 -26
- package/.agents/scripts/check-baseline-drift.js +73 -13
- package/.agents/scripts/check-baseline-scope.js +362 -0
- package/.agents/scripts/check-dead-exports.js +9 -1
- package/.agents/scripts/check-gherkin-corpus.js +508 -0
- package/.agents/scripts/check-knip-entries.js +136 -0
- package/.agents/scripts/check-lifecycle-lint.js +36 -112
- package/.agents/scripts/check-schema-references.js +1 -1
- package/.agents/scripts/diagnose-friction.js +7 -4
- package/.agents/scripts/generate-config-docs.js +263 -171
- package/.agents/scripts/install-matrix-assert.js +0 -1
- package/.agents/scripts/lib/ITicketingProvider.js +0 -58
- package/.agents/scripts/lib/audit-baselines/staleness.js +6 -6
- package/.agents/scripts/lib/audit-baselines/trend.js +7 -8
- package/.agents/scripts/lib/audit-baselines/weights.js +4 -5
- package/.agents/scripts/lib/audit-suite/checklist-threading.js +1 -1
- package/.agents/scripts/lib/audit-to-stories/build-story-body.js +0 -1
- package/.agents/scripts/lib/baselines/envelope.js +41 -60
- package/.agents/scripts/lib/baselines/git-base.js +30 -37
- package/.agents/scripts/lib/baselines/kinds/_crap-new-method-gate.js +103 -0
- package/.agents/scripts/lib/baselines/kinds/_crap-read.js +150 -0
- package/.agents/scripts/lib/baselines/kinds/crap.js +25 -65
- package/.agents/scripts/lib/baselines/orphan-pruner.js +233 -0
- package/.agents/scripts/lib/baselines/refresh-service.js +6 -8
- package/.agents/scripts/lib/baselines/scope-assert.js +223 -0
- package/.agents/scripts/lib/baselines/scope-inventory.js +314 -0
- package/.agents/scripts/lib/bdd-step-index.js +326 -0
- package/.agents/scripts/lib/bootstrap/install-ledger.js +5 -3
- package/.agents/scripts/lib/bootstrap/issue-forms-template.js +4 -6
- package/.agents/scripts/lib/bootstrap/manifest.js +17 -40
- package/.agents/scripts/lib/bootstrap/project-bootstrap.js +12 -59
- package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +62 -2
- package/.agents/scripts/lib/checks/loop-health.js +9 -37
- package/.agents/scripts/lib/child-exec.js +193 -0
- package/.agents/scripts/lib/cli/standard-args.js +1 -1
- package/.agents/scripts/lib/cli-args.js +64 -0
- package/.agents/scripts/lib/close-validation/gates.js +2 -2
- package/.agents/scripts/lib/close-validation/runner.js +3 -3
- package/.agents/scripts/lib/config/acceptance-eval.js +5 -52
- package/.agents/scripts/lib/config/commands.js +3 -5
- package/.agents/scripts/lib/config/explain.js +5 -7
- package/.agents/scripts/lib/config/gates/bundle-size.schema.js +32 -6
- package/.agents/scripts/lib/config/gates/coverage.schema.js +25 -5
- package/.agents/scripts/lib/config/gates/crap-incremental-coverage.schema.js +12 -2
- package/.agents/scripts/lib/config/gates/crap.schema.js +68 -23
- package/.agents/scripts/lib/config/gates/duplication.schema.js +29 -17
- package/.agents/scripts/lib/config/gates/index.js +5 -2
- package/.agents/scripts/lib/config/gates/lighthouse.schema.js +34 -6
- package/.agents/scripts/lib/config/gates/lint.schema.js +11 -2
- package/.agents/scripts/lib/config/gates/maintainability.schema.js +37 -15
- package/.agents/scripts/lib/config/gates/mutation.schema.js +15 -3
- package/.agents/scripts/lib/config/gates/shared.js +58 -9
- package/.agents/scripts/lib/config/github.js +0 -1
- package/.agents/scripts/lib/config/limits.js +3 -48
- package/.agents/scripts/lib/config/qa.js +105 -0
- package/.agents/scripts/lib/config/temp-paths.js +6 -5
- package/.agents/scripts/lib/config-settings-schema-delivery.js +237 -56
- package/.agents/scripts/lib/config-settings-schema-quality.js +209 -29
- package/.agents/scripts/lib/config-settings-schema.js +386 -39
- package/.agents/scripts/lib/crap-baseline-join.js +126 -9
- package/.agents/scripts/lib/crap-utils.js +84 -520
- package/.agents/scripts/lib/dead-exports-knip.js +79 -10
- package/.agents/scripts/lib/degraded-mode.js +2 -2
- package/.agents/scripts/lib/doc-tiers.js +3 -3
- package/.agents/scripts/lib/feedback-loop/graduator-core.js +46 -104
- package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +10 -8
- package/.agents/scripts/lib/fs-walk.js +52 -0
- package/.agents/scripts/lib/git-branch-lifecycle.js +2 -2
- package/.agents/scripts/lib/git-utils.js +16 -36
- package/.agents/scripts/lib/knip-entry-sync.js +469 -0
- package/.agents/scripts/lib/observability/metrics-ledger.js +1 -1
- package/.agents/scripts/lib/observability/runtime-friction.js +10 -0
- package/.agents/scripts/lib/observability/signal-validator.js +5 -85
- package/.agents/scripts/lib/observability/signals-writer.js +19 -62
- package/.agents/scripts/lib/observability/source-classifier.js +5 -7
- package/.agents/scripts/lib/observability/terse-result.js +3 -3
- package/.agents/scripts/lib/orchestration/behind-recovery.js +114 -0
- package/.agents/scripts/lib/orchestration/ceremony-routing.js +7 -8
- package/.agents/scripts/lib/orchestration/ci-rerun-guard.js +34 -33
- package/.agents/scripts/lib/orchestration/code-review.js +2 -2
- package/.agents/scripts/lib/orchestration/complexity-gate.js +43 -161
- package/.agents/scripts/lib/orchestration/diff-magnitude.js +4 -4
- package/.agents/scripts/lib/orchestration/label-transitions.js +3 -2
- package/.agents/scripts/lib/orchestration/lease-guard-shared.js +12 -38
- package/.agents/scripts/lib/orchestration/lifecycle/emit-merge-unlanded.js +5 -6
- package/.agents/scripts/lib/orchestration/plan-metrics.js +2 -3
- package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +6 -0
- package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +0 -1
- package/.agents/scripts/lib/orchestration/{lifecycle/listeners/watcher.js → pr-watch.js} +58 -208
- package/.agents/scripts/lib/orchestration/resolve-stories.js +5 -15
- package/.agents/scripts/lib/orchestration/review-providers/codex.js +1 -1
- package/.agents/scripts/lib/orchestration/review-providers/mi-exemptions.js +130 -0
- package/.agents/scripts/lib/orchestration/review-providers/native.js +30 -16
- package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +1 -1
- package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +37 -26
- package/.agents/scripts/lib/orchestration/single-story-close/phases/conventional-subject.js +376 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/normalize-pr-title.js +161 -151
- package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +15 -3
- package/.agents/scripts/lib/orchestration/single-story-close/phases/pull-request.js +10 -15
- package/.agents/scripts/lib/orchestration/single-story-close/phases/review-block.js +5 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/review-override.js +157 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/worktree-reap.js +0 -14
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +59 -25
- package/.agents/scripts/lib/orchestration/single-story-lease-guard.js +20 -31
- package/.agents/scripts/lib/orchestration/spec-spill.js +17 -3
- package/.agents/scripts/lib/orchestration/story-close/phases/review-core.js +7 -6
- package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +2 -1
- package/.agents/scripts/lib/orchestration/task-body-validator.js +4 -1
- package/.agents/scripts/lib/orchestration/ticket-lease.js +28 -127
- package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +1 -1
- package/.agents/scripts/lib/orchestration/ticketing/reads.js +5 -5
- package/.agents/scripts/lib/orchestration/ticketing/transition.js +5 -4
- package/.agents/scripts/lib/orchestration/verify-tier-repair.js +107 -0
- package/.agents/scripts/lib/qa/coverage-verdict.js +5 -87
- package/.agents/scripts/lib/signals/detectors/common.js +1 -1
- package/.agents/scripts/lib/signals/index.js +8 -6
- package/.agents/scripts/lib/signals/schema.js +20 -25
- package/.agents/scripts/lib/signals/write.js +8 -8
- package/.agents/scripts/lib/story-body/story-body.js +12 -59
- package/.agents/scripts/lib/temp-retention.js +1 -1
- package/.agents/scripts/lib/templates/decomposer-prompts.js +16 -14
- package/.agents/scripts/lib/ticket-body-sections.js +4 -5
- package/.agents/scripts/lib/worktree/lifecycle/merge-reachability.js +13 -45
- package/.agents/scripts/lib/worktree/lifecycle/reap.js +4 -5
- package/.agents/scripts/lib/worktree-manager.js +2 -3
- package/.agents/scripts/lint-label-vocabulary.js +2 -24
- package/.agents/scripts/pr-watch-with-update.js +7 -5
- package/.agents/scripts/providers/github/cache.js +2 -2
- package/.agents/scripts/providers/github/comments.js +6 -28
- package/.agents/scripts/providers/github/compose.js +0 -15
- package/.agents/scripts/providers/github/errors.js +10 -27
- package/.agents/scripts/providers/github/request-helpers.js +1 -2
- package/.agents/scripts/providers/github/sub-issues.js +10 -218
- package/.agents/scripts/providers/github.js +4 -7
- package/.agents/scripts/prune-baseline-orphans.js +181 -0
- package/.agents/scripts/resolve-stories.js +0 -2
- package/.agents/scripts/run-lint.js +61 -61
- package/.agents/scripts/run-test-profile.js +6 -6
- package/.agents/scripts/run-verify.js +48 -30
- package/.agents/scripts/single-story-close.js +20 -0
- package/.agents/scripts/single-story-init.js +12 -35
- package/.agents/scripts/update-dead-exports-baseline.js +321 -0
- package/.agents/skills/core/gates-and-baselines/SKILL.md +2 -2
- package/.agents/skills/skills.index.json +1 -11
- package/.agents/workflows/audit-documentation.md +5 -6
- package/.agents/workflows/audit-to-stories.md +2 -2
- package/.agents/workflows/helpers/audit-lens-core.md +11 -12
- package/.agents/workflows/helpers/code-quality-guardrails.md +15 -14
- package/.agents/workflows/helpers/code-review.md +3 -8
- package/.agents/workflows/helpers/deliver-reference.md +2 -1
- package/.agents/workflows/helpers/deliver-story-reference.md +27 -16
- package/.agents/workflows/helpers/worktree-lifecycle.md +1 -2
- package/.agents/workflows/mandrel-update.md +10 -10
- package/.agents/workflows/qa-assist.md +15 -20
- package/.agents/workflows/qa-explore.md +9 -8
- package/README.md +1 -1
- package/docs/CHANGELOG.md +42 -0
- package/lib/migrations/index.js +2 -0
- package/lib/migrations/steps/2.32.0-retire-lint-baseline-command.js +127 -0
- package/package.json +12 -3
- package/.agents/schemas/lifecycle/checkpoint.written.schema.json +0 -13
- package/.agents/schemas/lifecycle/close-validate.end.schema.json +0 -18
- package/.agents/schemas/lifecycle/close-validate.start.schema.json +0 -13
- package/.agents/schemas/lifecycle/code-review.end.schema.json +0 -30
- package/.agents/schemas/lifecycle/code-review.start.schema.json +0 -12
- package/.agents/schemas/lifecycle/intervention.recorded.schema.json +0 -15
- package/.agents/schemas/lifecycle/loop.tick.schema.json +0 -20
- package/.agents/schemas/lifecycle/notification.emitted.schema.json +0 -18
- package/.agents/schemas/lifecycle/pr.created.schema.json +0 -14
- package/.agents/schemas/lifecycle/retro.end.schema.json +0 -16
- package/.agents/schemas/lifecycle/retro.start.schema.json +0 -12
- package/.agents/schemas/lifecycle/story.blocked.schema.json +0 -13
- package/.agents/schemas/lifecycle/story.dispatch.end.schema.json +0 -17
- package/.agents/schemas/lifecycle/story.dispatch.start.schema.json +0 -15
- package/.agents/schemas/lifecycle/story.merged.schema.json +0 -13
- package/.agents/scripts/check-gherkin-placeholders.js +0 -663
- package/.agents/scripts/check-lifecycle-doc-drift.js +0 -411
- package/.agents/scripts/lib/audit-suite/cli.js +0 -64
- package/.agents/scripts/lib/bootstrap/baselines-layout-migration.js +0 -202
- package/.agents/scripts/lib/bootstrap/ci-workflow-template.js +0 -212
- package/.agents/scripts/lib/checks/baseline-drift-main-checkout.js +0 -104
- package/.agents/scripts/lib/checks/push-hook-parity.js +0 -106
- package/.agents/scripts/lib/checks/windows-coverage-noise-floor.js +0 -92
- package/.agents/scripts/lib/checks/worktree-bootstrap-env.js +0 -81
- package/.agents/scripts/lib/checks/worktree-residue-biome.js +0 -55
- package/.agents/scripts/lib/crap-baseline-index.js +0 -46
- package/.agents/scripts/lib/crap-utils-incremental.js +0 -113
- package/.agents/scripts/lib/dynamic-workflow/capability.js +0 -396
- package/.agents/scripts/lib/feedback-loop/audit-results-graduator.js +0 -335
- package/.agents/scripts/lib/mutation/baseline-snapshot.js +0 -239
- package/.agents/scripts/lib/mutation/config-detector.js +0 -119
- package/.agents/scripts/lib/mutation/stryker-runner.js +0 -306
- package/.agents/scripts/lib/mutation/survivor-report.js +0 -160
- package/.agents/scripts/lib/observability/active-story-env.js +0 -170
- package/.agents/scripts/lib/observability/tool-trace-hook.js +0 -456
- package/.agents/scripts/lib/orchestration/acceptance-clusters.js +0 -111
- package/.agents/scripts/lib/orchestration/context-envelope.js +0 -277
- package/.agents/scripts/lib/orchestration/detectors-phase.js +0 -194
- package/.agents/scripts/lib/orchestration/lifecycle/bus.js +0 -309
- package/.agents/scripts/lib/orchestration/lifecycle/emit-loop-tick.js +0 -181
- package/.agents/scripts/lib/orchestration/lifecycle/ledger-writer.js +0 -229
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/README.md +0 -54
- package/.agents/scripts/lib/orchestration/lifecycle/trace-logger.js +0 -344
- package/.agents/scripts/lib/orchestration/lint-baseline-service.js +0 -114
- package/.agents/scripts/lib/orchestration/pr-base-guard.js +0 -37
- package/.agents/scripts/lib/orchestration/resolves-token.js +0 -127
- package/.agents/scripts/lib/orchestration/spec-section-validator.js +0 -130
- package/.agents/scripts/lib/orchestration/story-close/emit-blocked.js +0 -55
- package/.agents/scripts/lib/orchestration/story-close/phases/code-review.js +0 -211
- package/.agents/scripts/lib/planning-corpus.js +0 -37
- package/.agents/scripts/lib/qa/coverage-report.js +0 -181
- package/.agents/scripts/lib/qa/propose-missing-test.js +0 -95
- package/.agents/scripts/lib/qa/qa-context-hydrator.js +0 -217
- package/.agents/scripts/lib/signals/detectors/index.js +0 -14
- package/.agents/scripts/lib/signals/detectors/retry.js +0 -253
- package/.agents/scripts/lib/signals/detectors/rework.js +0 -167
- package/.agents/scripts/lib/signals/read.js +0 -268
- package/.agents/scripts/lib/signals/span-tree.js +0 -291
- package/.agents/scripts/lib/story-lifecycle.js +0 -194
- package/.agents/scripts/lib/story-plan.js +0 -379
- package/.agents/scripts/lib/util/phase-timer-state.js +0 -72
- package/.agents/scripts/lib/util/phase-timer.js +0 -163
- package/.agents/scripts/lib/workers/combined-mi-crap-worker.js +0 -169
- package/.agents/scripts/lint-baseline.js +0 -507
- package/.agents/scripts/providers/github/prs.js +0 -103
- package/.agents/scripts/signals-view.js +0 -309
- package/.agents/scripts/story-plan.js +0 -370
- package/.agents/scripts/sync-branch-from-base.js +0 -149
- package/.agents/scripts/validate-docs-freshness.js +0 -314
- package/.agents/skills/core/diagnose-friction/SKILL.md +0 -78
- package/.agents/workflows/helpers/signals.md +0 -112
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
* runner only — set SKIP_PREFLIGHT=0 to include preflight if desired.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { spawnSync } from 'node:child_process';
|
|
12
11
|
import fs from 'node:fs';
|
|
13
12
|
import path from 'node:path';
|
|
14
13
|
import { fileURLToPath } from 'node:url';
|
|
14
|
+
import { spawnChild } from './lib/child-exec.js';
|
|
15
15
|
import { runAsCli } from './lib/cli-utils.js';
|
|
16
16
|
import { buildWebhookSafeTestEnv } from './lib/test-env.js';
|
|
17
17
|
import { parseTapOutput } from './lib/test-profile/parse-tap.js';
|
|
@@ -56,13 +56,14 @@ export function parseProfileArgv(argv) {
|
|
|
56
56
|
* @param {object} [opts]
|
|
57
57
|
* @param {string[]} [opts.argv]
|
|
58
58
|
* @param {string} [opts.cwd]
|
|
59
|
-
* @param {
|
|
59
|
+
* @param {Function} [opts.spawn] - Injected child runner; defaults to the
|
|
60
|
+
* shared surface's real `spawnSync`.
|
|
60
61
|
* @param {typeof fs} [opts.fs]
|
|
61
62
|
*/
|
|
62
63
|
export function runTestProfile({
|
|
63
64
|
argv = process.argv.slice(2),
|
|
64
65
|
cwd = ROOT,
|
|
65
|
-
spawn
|
|
66
|
+
spawn,
|
|
66
67
|
fs: fsLike = fs,
|
|
67
68
|
} = {}) {
|
|
68
69
|
const { outDir, topN, testArgv } = parseProfileArgv(argv);
|
|
@@ -75,7 +76,8 @@ export function runTestProfile({
|
|
|
75
76
|
];
|
|
76
77
|
|
|
77
78
|
const started = Date.now();
|
|
78
|
-
const result =
|
|
79
|
+
const result = spawnChild(process.execPath, nodeArgs, {
|
|
80
|
+
run: spawn,
|
|
79
81
|
cwd,
|
|
80
82
|
encoding: 'utf8',
|
|
81
83
|
env: {
|
|
@@ -84,8 +86,6 @@ export function runTestProfile({
|
|
|
84
86
|
FORCE_COLOR: '0',
|
|
85
87
|
NO_COLOR: '1',
|
|
86
88
|
},
|
|
87
|
-
maxBuffer: 64 * 1024 * 1024,
|
|
88
|
-
shell: false,
|
|
89
89
|
});
|
|
90
90
|
const wallDurationMs = Date.now() - started;
|
|
91
91
|
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
* locally, without epic-scoped MI projection or push semantics.
|
|
7
7
|
*
|
|
8
8
|
* Order: audit (SCA) → lint (includes docs:check + the arch-cycles ratchet) →
|
|
9
|
-
* full test suite → unified baselines → the
|
|
10
|
-
*
|
|
9
|
+
* full test suite → unified baselines → the standalone ratchets
|
|
10
|
+
* (dead-exports ×2, context-budget, cyclomatic, schema-references).
|
|
11
11
|
*
|
|
12
12
|
* The `audit` step runs `npm audit --audit-level=high`, matching CI's
|
|
13
13
|
* "Dependency Vulnerability Audit (SCA)" gate so a local green no longer hides
|
|
@@ -27,42 +27,60 @@
|
|
|
27
27
|
* full-tree scan in check-dead-exports.js) to a command that already carries
|
|
28
28
|
* the full test suite.
|
|
29
29
|
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
30
|
+
* Story #5004 closed the last two mirror gaps that were pure omission.
|
|
31
|
+
* `check-cyclomatic.js` (#4923) and `check-schema-references.js` (#4938) were
|
|
32
|
+
* each added to the `baselines` job's standalone-ratchet slot without ever
|
|
33
|
+
* being added here, so a green `verify` still hid both. Like their neighbours
|
|
34
|
+
* they are pure-Node and cost milliseconds.
|
|
35
|
+
*
|
|
36
|
+
* Still NOT mirrored: `check-workflow-citations.js` and
|
|
37
|
+
* `check-baseline-scope.js` run in CI's `baselines` job only —
|
|
38
|
+
* `.agents/rules/known-tooling-behavior.md` entry 2 carries the current
|
|
39
|
+
* coverage table. (`prune-baseline-orphans.js --check` used to sit in that
|
|
40
|
+
* list; it no longer runs in CI at all — the un-attributed duplicate of the
|
|
41
|
+
* scope gate's `extra` direction reds every open PR on inherited rows.) Nor are the CI gates this command structurally cannot
|
|
42
|
+
* reproduce (action pinning, TruffleHog secret scan) — those are catalogued
|
|
43
|
+
* in docs/ci-contract.md. The nightly full-scope re-score
|
|
44
|
+
* (.github/workflows/baseline-drift.yml) is deliberately outside this
|
|
45
|
+
* mirror too: it re-scores the whole tree, which is the cost `verify` exists
|
|
46
|
+
* to avoid paying on every run.
|
|
33
47
|
*/
|
|
34
48
|
|
|
35
49
|
import { spawnSync } from 'node:child_process';
|
|
36
50
|
import { runAsCli } from './lib/cli-utils.js';
|
|
37
51
|
|
|
52
|
+
/**
|
|
53
|
+
* A gate step: `node .agents/scripts/<script>` plus any extra args. Seven of
|
|
54
|
+
* the ten steps share exactly that shape, so spelling it once leaves the list
|
|
55
|
+
* below readable as what it actually is — a gate *order* — instead of a wall
|
|
56
|
+
* of spawn tuples.
|
|
57
|
+
*
|
|
58
|
+
* @param {string} label reported as `failedStep` when the gate exits non-zero
|
|
59
|
+
* @param {string} script basename under `.agents/scripts/`
|
|
60
|
+
* @param {...string} args extra CLI args
|
|
61
|
+
* @returns {{ label: string, cmd: string, args: string[] }}
|
|
62
|
+
*/
|
|
63
|
+
const gate = (label, script, ...args) => ({
|
|
64
|
+
label,
|
|
65
|
+
cmd: 'node',
|
|
66
|
+
args: [`.agents/scripts/${script}`, ...args],
|
|
67
|
+
});
|
|
68
|
+
|
|
38
69
|
const STEPS = [
|
|
39
|
-
{
|
|
40
|
-
label: 'audit',
|
|
41
|
-
cmd: 'npm',
|
|
42
|
-
args: ['audit', '--audit-level=high'],
|
|
43
|
-
},
|
|
70
|
+
{ label: 'audit', cmd: 'npm', args: ['audit', '--audit-level=high'] },
|
|
44
71
|
{ label: 'lint', cmd: 'npm', args: ['run', 'lint'] },
|
|
45
72
|
{ label: 'test', cmd: 'npm', args: ['test'] },
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
label: 'dead-exports-production',
|
|
58
|
-
cmd: 'node',
|
|
59
|
-
args: ['.agents/scripts/check-dead-exports.js', '--production'],
|
|
60
|
-
},
|
|
61
|
-
{
|
|
62
|
-
label: 'context-budget',
|
|
63
|
-
cmd: 'node',
|
|
64
|
-
args: ['.agents/scripts/check-context-budget.js'],
|
|
65
|
-
},
|
|
73
|
+
gate('baselines', 'check-baselines.js'),
|
|
74
|
+
// Ordered ahead of the dead-exports pair deliberately. When a new CLI is
|
|
75
|
+
// missing from knip.json's entry list, both gates fail — but only this one
|
|
76
|
+
// names the cause. Seeing the ratchet's whole-file diff first is what made
|
|
77
|
+
// "accept the diff" look like the fix during Story #5012.
|
|
78
|
+
gate('knip-entries', 'check-knip-entries.js'),
|
|
79
|
+
gate('dead-exports', 'check-dead-exports.js'),
|
|
80
|
+
gate('dead-exports-production', 'check-dead-exports.js', '--production'),
|
|
81
|
+
gate('context-budget', 'check-context-budget.js'),
|
|
82
|
+
gate('cyclomatic', 'check-cyclomatic.js'),
|
|
83
|
+
gate('schema-references', 'check-schema-references.js'),
|
|
66
84
|
];
|
|
67
85
|
|
|
68
86
|
export function runVerifySteps({
|
|
@@ -44,6 +44,18 @@
|
|
|
44
44
|
* [--no-auto-merge]
|
|
45
45
|
* [--wait-merge | --no-wait-merge]
|
|
46
46
|
* [--merge-watch-mode <sync|async>]
|
|
47
|
+
* [--override-review-block <reason>]
|
|
48
|
+
*
|
|
49
|
+
* `--override-review-block <reason>` is the one sanctioned way
|
|
50
|
+
* past a code-review CRITICAL blocker. A critical finding halts this script
|
|
51
|
+
* before auto-merge, and until this flag existed there was no override at all —
|
|
52
|
+
* so an operator who had read a finding and judged it wrong could only land by
|
|
53
|
+
* running `gh pr merge` themselves, bypassing the gate with nothing written
|
|
54
|
+
* down. The flag does not weaken the gate; it moves that escape hatch into a
|
|
55
|
+
* mandatory-reason audit trail (Story comment + PR comment + a
|
|
56
|
+
* `review-block-overridden` friction signal) and reports
|
|
57
|
+
* `gates.codeReview: "overridden"` on the terminal envelope. A bare or
|
|
58
|
+
* too-short reason fails during option parsing, before any phase runs.
|
|
47
59
|
*
|
|
48
60
|
* `--merge-watch-mode` (Story #4949) overrides `delivery.mergeWatch.mode` for
|
|
49
61
|
* one invocation, on the same explicit-wins-over-config precedence
|
|
@@ -217,6 +229,14 @@ runAsCli(import.meta.url, main, {
|
|
|
217
229
|
'--merge-watch-mode <sync|async>',
|
|
218
230
|
'Override delivery.mergeWatch.mode for this invocation only. `async` caps the merge wait to a short probe window and returns the resumable `pending` terminal instead of holding the foreground slot — pass it on every close of a multi-Story run. An invalid value exits non-zero before any phase runs.',
|
|
219
231
|
],
|
|
232
|
+
[
|
|
233
|
+
'--override-review-block <reason>',
|
|
234
|
+
// Deliberately does not spell the merge CLI invocation: the
|
|
235
|
+
// merge-lockout rule in `check-lifecycle-lint.js` forbids that literal
|
|
236
|
+
// in any string outside `phases/auto-merge.js`, and it is right to —
|
|
237
|
+
// the point of this flag is that arming stays on the one code path.
|
|
238
|
+
'Land despite a Story-scope code-review CRITICAL blocker you have reviewed and judged wrong. The reason is mandatory (≥12 chars) and is recorded on the Story, on the PR, and as a `review-block-overridden` friction signal; the terminal envelope reports `gates.codeReview: "overridden"`. Use this instead of merging the PR by hand with the GitHub CLI — a hand-merge bypasses the gate with no record at all.',
|
|
239
|
+
],
|
|
220
240
|
['--no-evidence', 'Do not reuse or write gate evidence stamps.'],
|
|
221
241
|
['--dry-run', 'Report the plan; mutate nothing.'],
|
|
222
242
|
],
|
|
@@ -54,7 +54,6 @@ import {
|
|
|
54
54
|
import { getStoryBranch, gitSpawn, gitSync } from './lib/git-utils.js';
|
|
55
55
|
import { Logger } from './lib/Logger.js';
|
|
56
56
|
import { TYPE_LABELS } from './lib/label-constants.js';
|
|
57
|
-
import { setActiveStoryEnv } from './lib/observability/active-story-env.js';
|
|
58
57
|
import { emitTerseResult } from './lib/observability/terse-result.js';
|
|
59
58
|
import {
|
|
60
59
|
executeFastForward,
|
|
@@ -492,10 +491,9 @@ export function seedStoryBranch({ cwd, storyBranch, baseBranch, progress }) {
|
|
|
492
491
|
}
|
|
493
492
|
|
|
494
493
|
/**
|
|
495
|
-
* Provision a worktree (or check out the branch in single-tree mode)
|
|
496
|
-
*
|
|
497
|
-
*
|
|
498
|
-
* (owns the worktree/single-tree routing + setActiveStoryEnv call).
|
|
494
|
+
* Provision a worktree (or check out the branch in single-tree mode).
|
|
495
|
+
* Returns the resolved `workCwd`, `worktreeCreated`, and `installStatus`.
|
|
496
|
+
* Exported for testing (owns the worktree/single-tree routing).
|
|
499
497
|
*
|
|
500
498
|
* @param {object} opts
|
|
501
499
|
* @param {object} opts.runtime
|
|
@@ -541,23 +539,6 @@ export async function provisionWorktree({
|
|
|
541
539
|
gitSync(cwd, 'checkout', storyBranch);
|
|
542
540
|
}
|
|
543
541
|
|
|
544
|
-
try {
|
|
545
|
-
// v2 Stories are standalone — no parent Epic. The helper omits
|
|
546
|
-
// CC_EPIC_ID from env + file; the trace hook keys its standalone-trace
|
|
547
|
-
// branch on CC_EPIC_ID being absent.
|
|
548
|
-
setActiveStoryEnv({
|
|
549
|
-
storyId,
|
|
550
|
-
workCwd,
|
|
551
|
-
logger: {
|
|
552
|
-
warn: (m) => progress('ENV', `⚠️ ${m}`),
|
|
553
|
-
},
|
|
554
|
-
});
|
|
555
|
-
} catch (err) {
|
|
556
|
-
Logger.error(
|
|
557
|
-
`[single-story-init] ⚠️ Failed to set active-Story env: ${err?.message ?? err}`,
|
|
558
|
-
);
|
|
559
|
-
}
|
|
560
|
-
|
|
561
542
|
return { workCwd, worktreeCreated, installStatus };
|
|
562
543
|
}
|
|
563
544
|
|
|
@@ -572,13 +553,11 @@ export async function runSingleStoryInit({
|
|
|
572
553
|
injectedConfig,
|
|
573
554
|
injectedSweep,
|
|
574
555
|
// Story #3483: lets tests drive the lease preflight deterministically.
|
|
575
|
-
// `injectedAcquireLease` swaps the guard
|
|
576
|
-
//
|
|
577
|
-
//
|
|
578
|
-
// heartbeat ledger, so a foreign assignee blocks unless stolen.
|
|
556
|
+
// `injectedAcquireLease` swaps the guard. `steal` forcibly transfers a
|
|
557
|
+
// foreign claim — the lease fails closed (audit #3513), so a foreign
|
|
558
|
+
// assignee blocks unless stolen.
|
|
579
559
|
injectedAcquireLease,
|
|
580
560
|
steal = false,
|
|
581
|
-
leaseNow,
|
|
582
561
|
injectedVerifyRemote,
|
|
583
562
|
// Story #4620: swap the git-touching provisioning steps so the
|
|
584
563
|
// early-flip-then-rollback ordering is unit-testable without a real worktree.
|
|
@@ -652,13 +631,12 @@ export async function runSingleStoryInit({
|
|
|
652
631
|
`Standalone Story: "${story.title}" → branch ${storyBranch} from ${baseBranch}.`,
|
|
653
632
|
);
|
|
654
633
|
|
|
655
|
-
// Story #3483 — lease preflight. Take an exclusive
|
|
656
|
-
//
|
|
657
|
-
//
|
|
658
|
-
//
|
|
659
|
-
//
|
|
660
|
-
//
|
|
661
|
-
// proceed. Skipped under --dry-run (no assignee mutation).
|
|
634
|
+
// Story #3483 — lease preflight. Take an exclusive claim on the Story
|
|
635
|
+
// ticket before any git mutation so two concurrent standalone runs cannot
|
|
636
|
+
// both drive the same Story. The guard fails closed (audit #3513): a
|
|
637
|
+
// foreign assignee aborts init (naming the current owner) unless --steal
|
|
638
|
+
// forcibly transfers it. Unclaimed / self-held claims proceed. Skipped
|
|
639
|
+
// under --dry-run (no assignee mutation).
|
|
662
640
|
let workCwd = cwd;
|
|
663
641
|
let worktreeCreated = false;
|
|
664
642
|
let installStatus = { status: 'skipped', reason: 'dry-run' };
|
|
@@ -670,7 +648,6 @@ export async function runSingleStoryInit({
|
|
|
670
648
|
storyId,
|
|
671
649
|
config,
|
|
672
650
|
steal: stealRequested,
|
|
673
|
-
now: leaseNow,
|
|
674
651
|
});
|
|
675
652
|
progress(
|
|
676
653
|
'LEASE',
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* update-dead-exports-baseline.js — the producer behind the dead-export
|
|
5
|
+
* ratchet (Story #5011).
|
|
6
|
+
*
|
|
7
|
+
* `check-dead-exports.js` has shipped since Story #1852 with no way to write
|
|
8
|
+
* the baseline it ratchets against. Every seed and every refresh was therefore
|
|
9
|
+
* a hand-edit: run the checker with `--json`, copy `currentRows` out of the
|
|
10
|
+
* report, re-sort them, and preserve `$schema` / `kernelVersion` / `mode` by
|
|
11
|
+
* hand. That is the gap this CLI closes — dead-exports now sits beside its four
|
|
12
|
+
* `update-*-baseline.js` siblings instead of being the one baseline an operator
|
|
13
|
+
* had to author in a text editor.
|
|
14
|
+
*
|
|
15
|
+
* **Fail closed, unlike the checker.** `check-dead-exports.js` treats a knip
|
|
16
|
+
* spawn or parse failure as advisory (exit 0 + a stderr warning) because it
|
|
17
|
+
* still holds a committed snapshot to compare against — a broken knip install
|
|
18
|
+
* must not redden CI on its own. The producer has no such fallback: the file it
|
|
19
|
+
* is about to write *is* the snapshot. An empty row set persisted from a failed
|
|
20
|
+
* run would silently grandfather every dead export in the repository and blind
|
|
21
|
+
* the ratchet permanently. So this CLI exits non-zero and writes **nothing**
|
|
22
|
+
* whenever knip cannot run, its report cannot be parsed, or the knip version
|
|
23
|
+
* cannot be resolved. That asymmetry with the checker is deliberate.
|
|
24
|
+
*
|
|
25
|
+
* **Envelope, not the shared writer.** Dead-exports does not route through
|
|
26
|
+
* `lib/baselines/writer.js`: that writer admits only the kinds registered in
|
|
27
|
+
* `lib/baselines/envelope.js` and requires a `*` rollup row, while dead-exports
|
|
28
|
+
* is an out-of-band ratchet kind (`lib/audit-baselines/kinds.js`) that carries
|
|
29
|
+
* no rollup. This CLI writes exactly the shape the checker already reads and
|
|
30
|
+
* both committed baselines already carry:
|
|
31
|
+
*
|
|
32
|
+
* { $schema, kernelVersion, generatedAt, [mode], rows: [{ file, symbol }] }
|
|
33
|
+
*
|
|
34
|
+
* `kernelVersion` is knip's own installed version — knip is the scorer, so a
|
|
35
|
+
* knip upgrade is what invalidates the rows. `mode` is stamped on the
|
|
36
|
+
* `--production` pass only, matching the committed production baseline.
|
|
37
|
+
*
|
|
38
|
+
* Rows are de-duplicated and sorted by `(file, symbol)` with the same
|
|
39
|
+
* comparator `check-dead-exports.js` uses for its diff output, so a re-run
|
|
40
|
+
* against an unchanged tree differs only in `generatedAt` and review sees real
|
|
41
|
+
* movement rather than reordering noise.
|
|
42
|
+
*
|
|
43
|
+
* The pass is selected with `--production`, and the baseline/label/mode triple
|
|
44
|
+
* comes from `lib/dead-exports-mode.js` — the same resolver the checker uses.
|
|
45
|
+
* Resolving that pairing independently here is precisely how a producer would
|
|
46
|
+
* end up writing production rows over the default baseline.
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
import fs from 'node:fs';
|
|
50
|
+
import path from 'node:path';
|
|
51
|
+
import process from 'node:process';
|
|
52
|
+
import { runAsCli } from './lib/cli-utils.js';
|
|
53
|
+
import {
|
|
54
|
+
extractRowsFromKnip,
|
|
55
|
+
readKnipOutput,
|
|
56
|
+
runKnip,
|
|
57
|
+
} from './lib/dead-exports-knip.js';
|
|
58
|
+
import { resolveDeadExportsMode } from './lib/dead-exports-mode.js';
|
|
59
|
+
|
|
60
|
+
/** `$schema` ref stamped into every dead-export baseline envelope. */
|
|
61
|
+
export const DEAD_EXPORTS_SCHEMA_REF =
|
|
62
|
+
'https://mandrel.dev/baselines/dead-exports.schema.json';
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Parse argv for `--production`, `--baseline <path>` and `--knip-output
|
|
66
|
+
* <path>`. `--knip-output` is the test seam: it feeds a pre-captured knip JSON
|
|
67
|
+
* report instead of spawning knip, and mirrors the checker's flag of the same
|
|
68
|
+
* name so a captured report drives both sides of the ratchet.
|
|
69
|
+
*
|
|
70
|
+
* @param {string[]} argv
|
|
71
|
+
* @returns {{ baselinePath: string | null, knipOutputPath: string | null, production: boolean }}
|
|
72
|
+
*/
|
|
73
|
+
export function parseArgv(argv = []) {
|
|
74
|
+
const out = { baselinePath: null, knipOutputPath: null, production: false };
|
|
75
|
+
const valueFlags = {
|
|
76
|
+
'--baseline': 'baselinePath',
|
|
77
|
+
'--knip-output': 'knipOutputPath',
|
|
78
|
+
};
|
|
79
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
80
|
+
const arg = argv[i];
|
|
81
|
+
if (arg === '--production') {
|
|
82
|
+
out.production = true;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const field = valueFlags[arg];
|
|
86
|
+
const next = argv[i + 1];
|
|
87
|
+
if (field && next && !next.startsWith('--')) {
|
|
88
|
+
out[field] = next;
|
|
89
|
+
i += 1;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return out;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Resolve the knip version to stamp as `kernelVersion`, read from the
|
|
97
|
+
* installed package rather than recalled. Returns `null` when knip is not
|
|
98
|
+
* installed or its manifest is unreadable — the caller treats that as a
|
|
99
|
+
* fail-closed condition, because an unstamped baseline cannot signal to a
|
|
100
|
+
* later run that the scorer moved underneath it.
|
|
101
|
+
*
|
|
102
|
+
* @param {{ cwd?: string, readFileImpl?: typeof fs.readFileSync }} [opts]
|
|
103
|
+
* @returns {string | null}
|
|
104
|
+
*/
|
|
105
|
+
export function resolveKnipKernelVersion({
|
|
106
|
+
cwd = process.cwd(),
|
|
107
|
+
readFileImpl = fs.readFileSync,
|
|
108
|
+
} = {}) {
|
|
109
|
+
try {
|
|
110
|
+
const manifest = path.resolve(cwd, 'node_modules', 'knip', 'package.json');
|
|
111
|
+
const version = JSON.parse(readFileImpl(manifest, 'utf-8'))?.version;
|
|
112
|
+
return typeof version === 'string' && version.length > 0 ? version : null;
|
|
113
|
+
} catch {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* De-duplicate and sort `{ file, symbol }` rows into the committed order.
|
|
120
|
+
*
|
|
121
|
+
* Identity is `(file, symbol)` — the same identity `diffRows` in
|
|
122
|
+
* `check-dead-exports.js` uses — so two knip issue records naming the same
|
|
123
|
+
* dead file collapse to one row. The comparator is that checker's comparator,
|
|
124
|
+
* which is why re-sorting an already-committed baseline is a no-op.
|
|
125
|
+
*
|
|
126
|
+
* @param {Array<{ file?: unknown, symbol?: unknown }>} rows
|
|
127
|
+
* @returns {Array<{ file: string, symbol: string }>}
|
|
128
|
+
*/
|
|
129
|
+
export function normalizeRows(rows) {
|
|
130
|
+
const seen = new Set();
|
|
131
|
+
const out = [];
|
|
132
|
+
for (const row of rows ?? []) {
|
|
133
|
+
if (typeof row?.file !== 'string' || typeof row?.symbol !== 'string')
|
|
134
|
+
continue;
|
|
135
|
+
const key = `${row.file}\0${row.symbol}`;
|
|
136
|
+
if (seen.has(key)) continue;
|
|
137
|
+
seen.add(key);
|
|
138
|
+
out.push({ file: row.file, symbol: row.symbol });
|
|
139
|
+
}
|
|
140
|
+
return out.sort(
|
|
141
|
+
(a, b) => a.file.localeCompare(b.file) || a.symbol.localeCompare(b.symbol),
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Assemble the baseline envelope. `mode` is stamped on the production pass
|
|
147
|
+
* only: the default baseline has never carried the key, and adding it would
|
|
148
|
+
* churn the committed file for no signal.
|
|
149
|
+
*
|
|
150
|
+
* @param {{ kernelVersion: string, mode: string, rows: Array<object>, generatedAt: string }} args
|
|
151
|
+
* @returns {object}
|
|
152
|
+
*/
|
|
153
|
+
export function buildEnvelope({ kernelVersion, mode, rows, generatedAt }) {
|
|
154
|
+
const envelope = {
|
|
155
|
+
$schema: DEAD_EXPORTS_SCHEMA_REF,
|
|
156
|
+
kernelVersion,
|
|
157
|
+
generatedAt,
|
|
158
|
+
};
|
|
159
|
+
if (mode === 'production') envelope.mode = mode;
|
|
160
|
+
envelope.rows = rows;
|
|
161
|
+
return envelope;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Obtain a knip report, either from the `--knip-output` seam or by spawning
|
|
166
|
+
* knip for the requested pass. Never throws; a failure is reported as
|
|
167
|
+
* `{ ok: false, error }` so the caller can fail closed without a try/catch.
|
|
168
|
+
*
|
|
169
|
+
* @param {{
|
|
170
|
+
* cwd: string,
|
|
171
|
+
* production: boolean,
|
|
172
|
+
* knipOutputPath: string | null,
|
|
173
|
+
* runKnipImpl: typeof runKnip,
|
|
174
|
+
* readKnipOutputImpl: typeof readKnipOutput,
|
|
175
|
+
* }} args
|
|
176
|
+
* @returns {{ ok: true, envelope: unknown } | { ok: false, error: string }}
|
|
177
|
+
*/
|
|
178
|
+
export function collectKnipReport({
|
|
179
|
+
cwd,
|
|
180
|
+
production,
|
|
181
|
+
knipOutputPath,
|
|
182
|
+
runKnipImpl,
|
|
183
|
+
readKnipOutputImpl,
|
|
184
|
+
}) {
|
|
185
|
+
if (knipOutputPath) {
|
|
186
|
+
const envelope = readKnipOutputImpl(path.resolve(cwd, knipOutputPath));
|
|
187
|
+
return envelope == null
|
|
188
|
+
? { ok: false, error: `could not read knip report at ${knipOutputPath}` }
|
|
189
|
+
: { ok: true, envelope };
|
|
190
|
+
}
|
|
191
|
+
return runKnipImpl({ cwd, production });
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Reject a report the extractor cannot read. `extractRowsFromKnip` is total —
|
|
196
|
+
* it answers `[]` for any shape it does not recognise — which is the right
|
|
197
|
+
* posture for an advisory checker and the wrong one for a producer: an
|
|
198
|
+
* unrecognised report and a genuinely clean repository would persist the same
|
|
199
|
+
* empty row set. Returns an error string, or `null` when the report is usable.
|
|
200
|
+
*
|
|
201
|
+
* @param {unknown} envelope
|
|
202
|
+
* @returns {string | null}
|
|
203
|
+
*/
|
|
204
|
+
export function describeUnusableReport(envelope) {
|
|
205
|
+
if (!envelope || typeof envelope !== 'object')
|
|
206
|
+
return 'knip report is not a JSON object';
|
|
207
|
+
if (!Array.isArray(envelope.issues))
|
|
208
|
+
return 'knip report carries no `issues` array';
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Top-level CLI entry, exported so tests drive the whole pipeline through
|
|
214
|
+
* injected hooks without spawning knip or touching a committed baseline.
|
|
215
|
+
*
|
|
216
|
+
* @param {{
|
|
217
|
+
* argv?: string[],
|
|
218
|
+
* cwd?: string,
|
|
219
|
+
* stdout?: { write: (s: string) => void },
|
|
220
|
+
* stderr?: { write: (s: string) => void },
|
|
221
|
+
* runKnipImpl?: typeof runKnip,
|
|
222
|
+
* readKnipOutputImpl?: typeof readKnipOutput,
|
|
223
|
+
* readFileImpl?: typeof fs.readFileSync,
|
|
224
|
+
* writeFileImpl?: typeof fs.writeFileSync,
|
|
225
|
+
* renameImpl?: typeof fs.renameSync,
|
|
226
|
+
* now?: () => string,
|
|
227
|
+
* }} [opts]
|
|
228
|
+
* @returns {Promise<number>} 0 on a written baseline; 1 on any fail-closed path.
|
|
229
|
+
*/
|
|
230
|
+
export async function runCli({
|
|
231
|
+
argv = process.argv.slice(2),
|
|
232
|
+
cwd = process.cwd(),
|
|
233
|
+
stdout = process.stdout,
|
|
234
|
+
stderr = process.stderr,
|
|
235
|
+
runKnipImpl = runKnip,
|
|
236
|
+
readKnipOutputImpl = readKnipOutput,
|
|
237
|
+
readFileImpl = fs.readFileSync,
|
|
238
|
+
writeFileImpl = fs.writeFileSync,
|
|
239
|
+
renameImpl = fs.renameSync,
|
|
240
|
+
now = () => new Date().toISOString(),
|
|
241
|
+
} = {}) {
|
|
242
|
+
const { baselinePath, knipOutputPath, production } = parseArgv(argv);
|
|
243
|
+
const { mode, label, baseline } = resolveDeadExportsMode(production);
|
|
244
|
+
const target = path.resolve(cwd, baselinePath ?? baseline);
|
|
245
|
+
|
|
246
|
+
const kernelVersion = resolveKnipKernelVersion({ cwd, readFileImpl });
|
|
247
|
+
if (!kernelVersion) {
|
|
248
|
+
stderr.write(
|
|
249
|
+
`[${label}] ❌ cannot resolve knip's version from node_modules/knip/package.json — refusing to write ${target}\n`,
|
|
250
|
+
);
|
|
251
|
+
return 1;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const report = collectKnipReport({
|
|
255
|
+
cwd,
|
|
256
|
+
production,
|
|
257
|
+
knipOutputPath,
|
|
258
|
+
runKnipImpl,
|
|
259
|
+
readKnipOutputImpl,
|
|
260
|
+
});
|
|
261
|
+
const failure = report.ok
|
|
262
|
+
? describeUnusableReport(report.envelope)
|
|
263
|
+
: report.error;
|
|
264
|
+
if (failure) {
|
|
265
|
+
stderr.write(
|
|
266
|
+
`[${label}] ❌ ${failure} — refusing to write ${target} (an empty baseline would grandfather every dead export)\n`,
|
|
267
|
+
);
|
|
268
|
+
return 1;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const rows = normalizeRows(extractRowsFromKnip(report.envelope));
|
|
272
|
+
const envelope = buildEnvelope({
|
|
273
|
+
kernelVersion,
|
|
274
|
+
mode,
|
|
275
|
+
rows,
|
|
276
|
+
generatedAt: now(),
|
|
277
|
+
});
|
|
278
|
+
// Write-then-rename, matching `lib/baselines/writer.js`: a crash or a full
|
|
279
|
+
// disk mid-write must not leave a truncated envelope behind. An unparseable
|
|
280
|
+
// baseline reads as empty to `check-dead-exports.js`, which would report
|
|
281
|
+
// every pre-existing row as newly added.
|
|
282
|
+
const tmpTarget = `${target}.tmp`;
|
|
283
|
+
writeFileImpl(tmpTarget, `${JSON.stringify(envelope, null, 2)}\n`, 'utf-8');
|
|
284
|
+
renameImpl(tmpTarget, target);
|
|
285
|
+
stdout.write(
|
|
286
|
+
`[${label}] ✅ wrote ${rows.length} row(s) to ${target} (kernelVersion=${kernelVersion}).\n`,
|
|
287
|
+
);
|
|
288
|
+
return 0;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async function main() {
|
|
292
|
+
return runCli();
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
runAsCli(import.meta.url, main, {
|
|
296
|
+
source: 'dead-exports-baseline',
|
|
297
|
+
propagateExitCode: true,
|
|
298
|
+
errorPrefix: '[dead-exports-baseline] ❌ Fatal error',
|
|
299
|
+
usage: {
|
|
300
|
+
invocation:
|
|
301
|
+
'node .agents/scripts/update-dead-exports-baseline.js [--production] [--baseline <path>] [--knip-output <path>]',
|
|
302
|
+
summary:
|
|
303
|
+
'Scan → write one dead-export baseline. Regenerates the rows `check-dead-exports.js` ratchets against, so the snapshot is produced rather than hand-edited.',
|
|
304
|
+
flags: [
|
|
305
|
+
[
|
|
306
|
+
'--production',
|
|
307
|
+
'Write the production-pass baseline (baselines/dead-exports-production.json) instead of the default one.',
|
|
308
|
+
],
|
|
309
|
+
['--baseline <path>', 'Write to this path instead of the mode default.'],
|
|
310
|
+
[
|
|
311
|
+
'--knip-output <path>',
|
|
312
|
+
'Read a saved knip JSON report instead of running knip.',
|
|
313
|
+
],
|
|
314
|
+
],
|
|
315
|
+
notes: [
|
|
316
|
+
'Fails closed: when knip cannot run or its report cannot be parsed, the CLI exits 1 and writes nothing — the checker is advisory on that failure, the producer must not be.',
|
|
317
|
+
'There is no scope flag: knip scores the whole graph, so a diff-scoped refresh is not expressible. Run `npm run dead-exports:update` to refresh both passes.',
|
|
318
|
+
'Exit codes:\n 0 baseline written\n 1 knip unavailable, unparseable, or version unresolvable',
|
|
319
|
+
],
|
|
320
|
+
},
|
|
321
|
+
});
|
|
@@ -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
|
-
- **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
|
|
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 with both scopes: `coverage`, `crap`, `maintainability`, `duplication`. Dead-exports has a producer but no scope axis — knip scores the whole graph, so `npm run dead-exports:update` (both passes, via `.agents/scripts/update-dead-exports-baseline.js`) is the only sanctioned way to write `baselines/dead-exports.json` / `baselines/dead-exports-production.json`; never hand-edit those files. Lighthouse still has no producer — edit `baselines/lighthouse.json` directly. Run any of them 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.
|
|
@@ -101,7 +101,7 @@ own as the parseable marker for any future reader.)
|
|
|
101
101
|
| Maintainability | `npm run maintainability:update` | `npm run maintainability:reanchor` |
|
|
102
102
|
| Coverage | `npm run coverage:update` | `npm run coverage:reanchor` |
|
|
103
103
|
| Duplication | `npm run duplication:update` | `npm run duplication:reanchor` |
|
|
104
|
-
| Dead-exports |
|
|
104
|
+
| Dead-exports | `npm run dead-exports:update` (regenerates both passes; never hand-edit the JSON) | same — no scope flag applies |
|
|
105
105
|
| Lighthouse | edit `baselines/lighthouse.json` | same — no scope flag applies |
|
|
106
106
|
|
|
107
107
|
1. **Run the matching command for your reason** — left column after a code
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"generatedAt": "2026-08-
|
|
2
|
+
"generatedAt": "2026-08-06T10:26:18.702Z",
|
|
3
3
|
"generator": "generate-skills-index.js@1",
|
|
4
4
|
"skills": [
|
|
5
5
|
{
|
|
@@ -42,16 +42,6 @@
|
|
|
42
42
|
"allowedTools": null,
|
|
43
43
|
"vendor": null
|
|
44
44
|
},
|
|
45
|
-
{
|
|
46
|
-
"name": "diagnose-friction",
|
|
47
|
-
"tier": "core",
|
|
48
|
-
"category": "core",
|
|
49
|
-
"path": ".agents/skills/core/diagnose-friction/SKILL.md",
|
|
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": 9,
|
|
52
|
-
"allowedTools": ["Bash", "Read"],
|
|
53
|
-
"vendor": null
|
|
54
|
-
},
|
|
55
45
|
{
|
|
56
46
|
"name": "documentation-and-adrs",
|
|
57
47
|
"tier": "core",
|