mandrel 2.31.0 → 2.33.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 +12 -9
- package/.agents/docs/agentrc-reference.json +63 -58
- package/.agents/docs/configuration.md +275 -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 +2 -2
- package/.agents/schemas/agentrc.schema.json +1955 -1410
- 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 +16 -29
- package/.agents/scripts/apply-quality-bootstrap.js +27 -34
- package/.agents/scripts/audit-to-stories.js +160 -41
- 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 +159 -0
- package/.agents/scripts/check-lifecycle-lint.js +108 -124
- 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 +81 -35
- package/.agents/scripts/lib/audit-to-stories/wire-dependencies.js +185 -0
- 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/runners.js +38 -16
- package/.agents/scripts/lib/config/temp-paths.js +6 -5
- package/.agents/scripts/lib/config-settings-schema-delivery.js +245 -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/dependency-parser.js +20 -7
- 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/findings/provenance-field.js +135 -0
- package/.agents/scripts/lib/findings/route-finding.js +57 -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-config-resolver.js +181 -0
- package/.agents/scripts/lib/knip-entry-sync.js +508 -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 +7 -26
- package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +69 -5
- package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +69 -13
- package/.agents/scripts/lib/orchestration/plan-persist/summary.js +49 -0
- package/.agents/scripts/lib/orchestration/{lifecycle/listeners/watcher.js → pr-watch.js} +58 -208
- package/.agents/scripts/lib/orchestration/resolve-stories.js +77 -50
- 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-conflicts.js +116 -1
- package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +1 -1
- package/.agents/scripts/lib/orchestration/ticket-validator.js +38 -0
- 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/footer-block.js +97 -0
- package/.agents/scripts/lib/story-body/story-body.js +18 -81
- 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/wave-runner/footprint.js +306 -0
- package/.agents/scripts/lib/wave-runner/ready-set.js +198 -181
- 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/blocked-by-add.js +25 -10
- 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 +21 -7
- 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/stories-wave-tick.js +192 -9
- 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 +28 -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 +30 -2
- package/.agents/workflows/helpers/deliver-story-reference.md +84 -16
- package/.agents/workflows/helpers/plan-reference.md +76 -0
- 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 +58 -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 +14 -5
- 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
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* bootstrap/manifest — single mutation-manifest source for the
|
|
3
|
-
*
|
|
2
|
+
* bootstrap/manifest — single mutation-manifest source for the install
|
|
3
|
+
* ledger (Story #3521, Feature #3515, Epic #3438).
|
|
4
4
|
*
|
|
5
5
|
* `buildMutationManifest(ctx)` enumerates every mutation the bootstrap
|
|
6
|
-
* pipeline can perform as a flat, declarative array. Each entry carries
|
|
7
|
-
*
|
|
8
|
-
* before any write lands:
|
|
6
|
+
* pipeline can perform as a flat, declarative array. Each entry carries five
|
|
7
|
+
* fields:
|
|
9
8
|
*
|
|
10
|
-
* - `phaseGroup` — one of the four
|
|
11
|
-
*
|
|
9
|
+
* - `phaseGroup` — one of the four groups (`ide-wiring`, `repo-config`,
|
|
10
|
+
* `github-admin`, `quality-gates`).
|
|
12
11
|
* - `target` — the file path or remote resource the mutation touches.
|
|
13
12
|
* - `action` — the verb describing the mutation (`create`, `merge`,
|
|
14
13
|
* `update`, `run`, `configure`).
|
|
@@ -17,16 +16,18 @@
|
|
|
17
16
|
* (e.g. delete a created file) vs. a remote-admin mutation that is not
|
|
18
17
|
* a simple local revert.
|
|
19
18
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
19
|
+
* Story #3690 replaced the consent-first phased-approval install screen with
|
|
20
|
+
* a plain summary+confirm loop, and Story #5007 removed the vestigial
|
|
21
|
+
* machinery it left behind (the no-write `previewMutationManifest` renderer
|
|
22
|
+
* and the `approvedGroups` set-threading). What survives is load-bearing:
|
|
23
|
+
* `recordLedger` filters these entries into the install ledger, and
|
|
24
|
+
* `mandrel uninstall` walks them — the `phaseGroup` string is the ledger's
|
|
25
|
+
* grouping key, not an approval gate.
|
|
24
26
|
*
|
|
25
27
|
* The manifest describes *intended* mutations deterministically from the
|
|
26
28
|
* project root; it does not read or mutate any file. Whether a given
|
|
27
29
|
* mutation is a no-op on a particular clone (the file already carries the
|
|
28
|
-
* wiring) is decided at execution time by the idempotent `ensure*` steps
|
|
29
|
-
* the manifest always lists the full surface so the preview is complete.
|
|
30
|
+
* wiring) is decided at execution time by the idempotent `ensure*` steps.
|
|
30
31
|
*
|
|
31
32
|
* @module bootstrap/manifest
|
|
32
33
|
*/
|
|
@@ -34,9 +35,9 @@
|
|
|
34
35
|
import path from 'node:path';
|
|
35
36
|
|
|
36
37
|
/**
|
|
37
|
-
* The four
|
|
38
|
-
*
|
|
39
|
-
*
|
|
38
|
+
* The four phase groups. Ledger/uninstall metadata only (Story #5007): every
|
|
39
|
+
* manifest entry MUST carry exactly one of these values so the install ledger
|
|
40
|
+
* can group and the uninstaller can select what a run actually landed.
|
|
40
41
|
*
|
|
41
42
|
* @type {Readonly<{ IDE_WIRING: 'ide-wiring', REPO_CONFIG: 'repo-config',
|
|
42
43
|
* GITHUB_ADMIN: 'github-admin', QUALITY_GATES: 'quality-gates' }>}
|
|
@@ -271,27 +272,3 @@ export function buildMutationManifest(ctx = {}) {
|
|
|
271
272
|
|
|
272
273
|
return entries;
|
|
273
274
|
}
|
|
274
|
-
|
|
275
|
-
/**
|
|
276
|
-
* Render the manifest as a no-write preview report grouped by phase group.
|
|
277
|
-
* Pure helper — derives entirely from {@link buildMutationManifest}, so the
|
|
278
|
-
* preview and the executing pipeline enumerate one identical source.
|
|
279
|
-
*
|
|
280
|
-
* The returned shape is `{ preview: true, groups: { <phaseGroup>: entry[] },
|
|
281
|
-
* entries: entry[] }`: callers that want the flat list read `entries`, and
|
|
282
|
-
* the consent-first screen reads `groups` to render one approvable section
|
|
283
|
-
* per phase group (only groups with at least one entry appear).
|
|
284
|
-
*
|
|
285
|
-
* @param {object} [ctx] — same context as {@link buildMutationManifest}.
|
|
286
|
-
* @returns {{ preview: true, groups: Record<string, MutationManifestEntry[]>,
|
|
287
|
-
* entries: MutationManifestEntry[] }}
|
|
288
|
-
*/
|
|
289
|
-
export function previewMutationManifest(ctx = {}) {
|
|
290
|
-
const entries = buildMutationManifest(ctx);
|
|
291
|
-
const groups = {};
|
|
292
|
-
for (const entry of entries) {
|
|
293
|
-
if (!groups[entry.phaseGroup]) groups[entry.phaseGroup] = [];
|
|
294
|
-
groups[entry.phaseGroup].push(entry);
|
|
295
|
-
}
|
|
296
|
-
return { preview: true, groups, entries };
|
|
297
|
-
}
|
|
@@ -20,7 +20,7 @@ import { isCommandExcluded } from '../command-header.js';
|
|
|
20
20
|
import { detectPackageManager as detectPm } from '../detect-package-manager.js';
|
|
21
21
|
import { LEDGER_RELATIVE_PATH } from './install-ledger.js';
|
|
22
22
|
import { ensureIssueForms } from './issue-forms-template.js';
|
|
23
|
-
import { PHASE_GROUPS
|
|
23
|
+
import { PHASE_GROUPS } from './manifest.js';
|
|
24
24
|
import { applyQualityBootstrap } from './quality-bootstrap.js';
|
|
25
25
|
|
|
26
26
|
export const SYNC_COMMAND = 'node .agents/scripts/sync-claude-commands.js';
|
|
@@ -436,8 +436,7 @@ export function ensureGitignore(ctx) {
|
|
|
436
436
|
* project (Story #4227). Derived from the Story-body SSOT so a human-filed
|
|
437
437
|
* ticket round-trips through `story-body.parse()`. Idempotent and additive,
|
|
438
438
|
* mirroring `ensureGitignore`: byte-identical forms are `unchanged`,
|
|
439
|
-
* operator-edited forms are preserved (`custom-skip`).
|
|
440
|
-
* (no writes) like the other phases.
|
|
439
|
+
* operator-edited forms are preserved (`custom-skip`).
|
|
441
440
|
*
|
|
442
441
|
* Returns the per-form action envelope keyed by ticket type.
|
|
443
442
|
*
|
|
@@ -446,13 +445,9 @@ export function ensureGitignore(ctx) {
|
|
|
446
445
|
*
|
|
447
446
|
* @param {object} ctx
|
|
448
447
|
* @param {string} ctx.projectRoot
|
|
449
|
-
* @param {boolean} [ctx.preview]
|
|
450
448
|
*/
|
|
451
449
|
function ensureIssueFormsPhase(ctx) {
|
|
452
|
-
const { forms } = ensureIssueForms({
|
|
453
|
-
projectRoot: ctx.projectRoot,
|
|
454
|
-
write: !ctx.preview,
|
|
455
|
-
});
|
|
450
|
+
const { forms } = ensureIssueForms({ projectRoot: ctx.projectRoot });
|
|
456
451
|
const outcomes = {};
|
|
457
452
|
for (const form of forms) {
|
|
458
453
|
outcomes[form.type] = { action: form.action, path: form.path };
|
|
@@ -691,12 +686,11 @@ const fatalParity = (result) =>
|
|
|
691
686
|
* bootstrap.
|
|
692
687
|
*
|
|
693
688
|
* Each project-side mutation phase carries a `phaseGroup` matching one of
|
|
694
|
-
* the
|
|
695
|
-
*
|
|
696
|
-
* the
|
|
697
|
-
*
|
|
698
|
-
*
|
|
699
|
-
* dependency install) are always-run infrastructure, never gated.
|
|
689
|
+
* the {@link PHASE_GROUPS}. Since Story #5007 that string is pure
|
|
690
|
+
* ledger/uninstall metadata — every phase runs on every install, and the
|
|
691
|
+
* ledger decides after the fact which groups actually landed. Phases with no
|
|
692
|
+
* `phaseGroup` (the Node-version precondition and the dependency install)
|
|
693
|
+
* are always-run infrastructure.
|
|
700
694
|
*/
|
|
701
695
|
export const BOOTSTRAP_PHASES = Object.freeze([
|
|
702
696
|
{
|
|
@@ -770,24 +764,6 @@ export const BOOTSTRAP_PHASES = Object.freeze([
|
|
|
770
764
|
},
|
|
771
765
|
]);
|
|
772
766
|
|
|
773
|
-
/**
|
|
774
|
-
* Decide whether a phase should run given the approved-phase-group gate.
|
|
775
|
-
* An always-run infrastructure phase (no `phaseGroup`) runs unconditionally.
|
|
776
|
-
* A grouped phase runs only when no gate is supplied (`approvedGroups`
|
|
777
|
-
* absent — the un-gated legacy path) or when its group is in the gate.
|
|
778
|
-
*
|
|
779
|
-
* Exported for unit testing.
|
|
780
|
-
*
|
|
781
|
-
* @param {BootstrapPhase} phase
|
|
782
|
-
* @param {Set<string>|undefined} approvedGroups
|
|
783
|
-
* @returns {boolean}
|
|
784
|
-
*/
|
|
785
|
-
export function isPhaseApproved(phase, approvedGroups) {
|
|
786
|
-
if (!phase.phaseGroup) return true;
|
|
787
|
-
if (!approvedGroups) return true;
|
|
788
|
-
return approvedGroups.has(phase.phaseGroup);
|
|
789
|
-
}
|
|
790
|
-
|
|
791
767
|
/**
|
|
792
768
|
* Throw with the formatted message when the phase is marked fatal and
|
|
793
769
|
* the result indicates an abort. Pure helper so the driver stays a
|
|
@@ -809,12 +785,6 @@ export function throwIfFatal(phase, result) {
|
|
|
809
785
|
* the result on `report[phase.name]`, then route fatal phases through
|
|
810
786
|
* `throwIfFatal`. Returns the accumulated report.
|
|
811
787
|
*
|
|
812
|
-
* When `ctx.approvedGroups` is a `Set`, a grouped phase whose `phaseGroup`
|
|
813
|
-
* is not approved is skipped and recorded as
|
|
814
|
-
* `{ skipped: true, reason: 'phase-group-declined', phaseGroup }` — it never
|
|
815
|
-
* runs, never throws (so a declined `ide-wiring` group also skips its
|
|
816
|
-
* fatal `parity` check), and never short-circuits the remaining phases.
|
|
817
|
-
*
|
|
818
788
|
* Exported for tests so phase ordering and fatal behaviour can be
|
|
819
789
|
* asserted without spawning a full bootstrap.
|
|
820
790
|
*
|
|
@@ -825,14 +795,6 @@ export function throwIfFatal(phase, result) {
|
|
|
825
795
|
export async function runPhases(phases, ctx) {
|
|
826
796
|
const report = {};
|
|
827
797
|
for (const phase of phases) {
|
|
828
|
-
if (!isPhaseApproved(phase, ctx.approvedGroups)) {
|
|
829
|
-
report[phase.name] = {
|
|
830
|
-
skipped: true,
|
|
831
|
-
reason: 'phase-group-declined',
|
|
832
|
-
phaseGroup: phase.phaseGroup,
|
|
833
|
-
};
|
|
834
|
-
continue;
|
|
835
|
-
}
|
|
836
798
|
const result = await phase.run(ctx, report);
|
|
837
799
|
report[phase.name] = result;
|
|
838
800
|
throwIfFatal(phase, result);
|
|
@@ -844,24 +806,16 @@ export async function runPhases(phases, ctx) {
|
|
|
844
806
|
* Compose every step in order. Each returned key is the outcome of one
|
|
845
807
|
* step so the CLI can render a structured summary.
|
|
846
808
|
*
|
|
847
|
-
*
|
|
848
|
-
*
|
|
849
|
-
*
|
|
850
|
-
*
|
|
851
|
-
* consent-first install screen renders. Deriving the preview from
|
|
852
|
-
* `buildMutationManifest` (rather than from a parallel hand-maintained list)
|
|
853
|
-
* guarantees the preview the operator approves and the execution that
|
|
854
|
-
* follows enumerate one identical set of mutations (Story #3521).
|
|
809
|
+
* Story #5007 removed the no-write `ctx.preview` branch: it had zero
|
|
810
|
+
* production callers (bootstrap's own `--dry-run` renders a hand-rolled text
|
|
811
|
+
* plan instead) and it survived only to serve the consent-first install
|
|
812
|
+
* screen Story #3690 replaced with a plain summary+confirm loop.
|
|
855
813
|
*
|
|
856
814
|
* @param {object} ctx
|
|
857
815
|
* @param {string} ctx.projectRoot
|
|
858
816
|
* @param {string} [ctx.agentRoot]
|
|
859
817
|
* @param {{ owner: string, repo: string, baseBranch: string,
|
|
860
818
|
* operatorHandle: string|null }} ctx.answers
|
|
861
|
-
* @param {boolean} [ctx.preview] — no-write preview from the manifest.
|
|
862
|
-
* @param {Set<string>} [ctx.approvedGroups] — when present, only phases
|
|
863
|
-
* whose `phaseGroup` is in this set execute (the consent-first gate from
|
|
864
|
-
* Story #3524); always-run infrastructure phases ignore it.
|
|
865
819
|
* @param {boolean} [ctx.withQuality]
|
|
866
820
|
* @param {boolean} [ctx.skipGithub]
|
|
867
821
|
* @param {boolean} [ctx.skipInstall]
|
|
@@ -871,6 +825,5 @@ export async function runPhases(phases, ctx) {
|
|
|
871
825
|
* @returns {Promise<object>}
|
|
872
826
|
*/
|
|
873
827
|
export async function applyProjectBootstrap(ctx) {
|
|
874
|
-
if (ctx.preview) return previewMutationManifest(ctx);
|
|
875
828
|
return runPhases(BOOTSTRAP_PHASES, ctx);
|
|
876
829
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* bootstrap/quality-bootstrap — Story #1401 (Epic #1386)
|
|
3
3
|
*
|
|
4
4
|
* Idempotent installer for the stabilized-quality-gates surface area on a
|
|
5
|
-
* project clone. Performs
|
|
5
|
+
* project clone. Performs five additive actions, each safe to re-run:
|
|
6
6
|
*
|
|
7
7
|
* 1. Copies the `code-quality-guardrails.md` helper into the project's
|
|
8
8
|
* `.agents/workflows/helpers/` (no-op when the helper is already present).
|
|
@@ -15,6 +15,11 @@
|
|
|
15
15
|
* 4. Seeds `delivery.quality.codingGuardrails` and
|
|
16
16
|
* `delivery.quality.autoRefresh` defaults in `.agentrc.json` when
|
|
17
17
|
* the keys are absent. Existing values are preserved.
|
|
18
|
+
* 5. Prunes a committed pre-v2 `baselines/epic/` tree (Story #5007). The
|
|
19
|
+
* v2 model is Story-only — nothing writes, reads, or reaps per-Epic
|
|
20
|
+
* ratchet snapshots — so an upgrading consumer is left carrying a
|
|
21
|
+
* committed directory no gate consults. Absent on every repo that never
|
|
22
|
+
* ran a pre-v2 install, which is the overwhelmingly common case.
|
|
18
23
|
*
|
|
19
24
|
* Returns a structured summary so the bootstrap and update workflows can
|
|
20
25
|
* surface exactly which actions ran and which were no-ops.
|
|
@@ -25,6 +30,7 @@
|
|
|
25
30
|
* @module bootstrap/quality-bootstrap
|
|
26
31
|
*/
|
|
27
32
|
|
|
33
|
+
import { spawnSync as defaultSpawnSync } from 'node:child_process';
|
|
28
34
|
import fs from 'node:fs';
|
|
29
35
|
import path from 'node:path';
|
|
30
36
|
import { getAgentrcDefaults, lookupPath } from '../config/defaults.js';
|
|
@@ -349,7 +355,59 @@ export function ensureQualityConfigDefaults(ctx) {
|
|
|
349
355
|
}
|
|
350
356
|
|
|
351
357
|
/**
|
|
352
|
-
*
|
|
358
|
+
* Repo-root-relative path of the retired committed per-Epic snapshot tree.
|
|
359
|
+
* Module-private: exporting it would add a production-dead row to the
|
|
360
|
+
* dead-exports ratchet for a two-word constant.
|
|
361
|
+
*/
|
|
362
|
+
const LEGACY_EPIC_BASELINES_RELPATH = 'baselines/epic';
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Step 5 — Prune a committed pre-v2 `baselines/epic/` tree (Story #5007).
|
|
366
|
+
*
|
|
367
|
+
* Story #1396 committed per-Epic ratchet snapshots under
|
|
368
|
+
* `baselines/epic/<id>/`. The v2 Story-only model retired epics along with
|
|
369
|
+
* every reader of those snapshots, and `check-baselines.js` resolves
|
|
370
|
+
* baselines by fixed filename rather than by directory glob — so the tree is
|
|
371
|
+
* inert, and the only remaining value in the retired layout migration was
|
|
372
|
+
* getting it out of version control.
|
|
373
|
+
*
|
|
374
|
+
* `git rm -r --ignore-unmatch` stages the removal when the path is tracked
|
|
375
|
+
* and is a safe no-op when it is not (fresh clone / untracked leftovers);
|
|
376
|
+
* the on-disk residue is then removed so a re-run reports `absent`. The
|
|
377
|
+
* caller commits the resulting working-tree delta.
|
|
378
|
+
*
|
|
379
|
+
* @param {object} ctx
|
|
380
|
+
* @param {string} ctx.projectRoot
|
|
381
|
+
* @param {typeof defaultSpawnSync} [ctx.spawnImpl] — injectable spawn seam.
|
|
382
|
+
* @returns {{ action: 'absent'|'pruned', path: string, gitStatus?: number|null }}
|
|
383
|
+
*/
|
|
384
|
+
export function pruneLegacyEpicBaselines(ctx) {
|
|
385
|
+
const target = path.join(ctx.projectRoot, 'baselines', 'epic');
|
|
386
|
+
if (!fs.existsSync(target)) return { action: 'absent', path: target };
|
|
387
|
+
const spawn = ctx.spawnImpl ?? defaultSpawnSync;
|
|
388
|
+
const rm = spawn(
|
|
389
|
+
'git',
|
|
390
|
+
[
|
|
391
|
+
'rm',
|
|
392
|
+
'-r',
|
|
393
|
+
'--quiet',
|
|
394
|
+
'--ignore-unmatch',
|
|
395
|
+
'--',
|
|
396
|
+
LEGACY_EPIC_BASELINES_RELPATH,
|
|
397
|
+
],
|
|
398
|
+
{
|
|
399
|
+
cwd: ctx.projectRoot,
|
|
400
|
+
encoding: 'utf-8',
|
|
401
|
+
stdio: 'pipe',
|
|
402
|
+
shell: false,
|
|
403
|
+
},
|
|
404
|
+
);
|
|
405
|
+
fs.rmSync(target, { recursive: true, force: true });
|
|
406
|
+
return { action: 'pruned', path: target, gitStatus: rm.status ?? null };
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Run all five steps in order. Composable wrapper used by the bootstrap
|
|
353
411
|
* and update workflows. Each step's outcome is returned under its own key
|
|
354
412
|
* so callers can render a per-action summary.
|
|
355
413
|
*
|
|
@@ -357,6 +415,7 @@ export function ensureQualityConfigDefaults(ctx) {
|
|
|
357
415
|
* @param {string} ctx.projectRoot
|
|
358
416
|
* @param {string} [ctx.frameworkRoot]
|
|
359
417
|
* @param {'framework'|'downstream'} [ctx.variant]
|
|
418
|
+
* @param {typeof defaultSpawnSync} [ctx.spawnImpl]
|
|
360
419
|
*/
|
|
361
420
|
export function applyQualityBootstrap(ctx) {
|
|
362
421
|
return {
|
|
@@ -364,5 +423,6 @@ export function applyQualityBootstrap(ctx) {
|
|
|
364
423
|
hook: ensurePreCommitHook(ctx),
|
|
365
424
|
scripts: ensureQualityNpmScripts(ctx),
|
|
366
425
|
config: ensureQualityConfigDefaults(ctx),
|
|
426
|
+
legacyBaselines: pruneLegacyEpicBaselines(ctx),
|
|
367
427
|
};
|
|
368
428
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* The original feedback-loop drift shipped silently because an empty
|
|
6
6
|
* feedback report is indistinguishable from a healthy one. This standing
|
|
7
7
|
* self-check closes that gap: at retro time it samples the on-disk signal
|
|
8
|
-
* substrate the sibling Stories established and surfaces the
|
|
8
|
+
* substrate the sibling Stories established and surfaces the two ways the
|
|
9
9
|
* repaired loop can regress without anyone noticing:
|
|
10
10
|
*
|
|
11
11
|
* 1. **Schema-invalid signal lines.** It tails the most recent
|
|
@@ -13,11 +13,7 @@
|
|
|
13
13
|
* the run temp tree and validates each against the canonical
|
|
14
14
|
* `signal-event.schema.json` (via `validateSignal`, the same validator
|
|
15
15
|
* the writer uses — no hand-rolled drift).
|
|
16
|
-
* 2. **
|
|
17
|
-
* (`temp/run-<id>/signal-rejects.json`, written by Story #4413's
|
|
18
|
-
* signals-writer) so records that were dropped at write time — and thus
|
|
19
|
-
* never appear in the stream — are still counted.
|
|
20
|
-
* 3. **Un-actioned retro proposals.** It reads the retro mirror
|
|
16
|
+
* 2. **Un-actioned retro proposals.** It reads the retro mirror
|
|
21
17
|
* (`temp/run-<id>/retro.md`, Story #4418) and flags any actionable
|
|
22
18
|
* "Proposed issues" item that carries neither a filed-issue reference
|
|
23
19
|
* (`Filed: [#N](url)`) nor lives under the explicit "One-off /
|
|
@@ -26,7 +22,7 @@
|
|
|
26
22
|
* Contract:
|
|
27
23
|
* - Scope `retro`, `autoCorrect: 'refuse-and-print'` — read-only by
|
|
28
24
|
* construction; the runner refuses `autoFix` under the retro scope.
|
|
29
|
-
* - A clean substrate (valid lines,
|
|
25
|
+
* - A clean substrate (valid lines, every proposal filed or
|
|
30
26
|
* discarded) yields **zero findings**, preserving the compact retro
|
|
31
27
|
* shape. Only when a concern is non-zero does `detect` return a single
|
|
32
28
|
* combined finding naming every non-clean dimension.
|
|
@@ -168,25 +164,6 @@ export function sampleStreamInvalidCount(
|
|
|
168
164
|
return { sampled: tail.length, invalid };
|
|
169
165
|
}
|
|
170
166
|
|
|
171
|
-
/**
|
|
172
|
-
* Read the per-run persisted reject count from `signal-rejects.json`.
|
|
173
|
-
* Returns 0 when the tally is absent or unreadable.
|
|
174
|
-
*
|
|
175
|
-
* @param {string} epicDir
|
|
176
|
-
* @param {{ readImpl?: typeof readFileSync }} [opts]
|
|
177
|
-
* @returns {number}
|
|
178
|
-
*/
|
|
179
|
-
export function readRejectTally(epicDir, { readImpl = readFileSync } = {}) {
|
|
180
|
-
try {
|
|
181
|
-
const parsed = JSON.parse(
|
|
182
|
-
readImpl(path.join(epicDir, 'signal-rejects.json'), 'utf8'),
|
|
183
|
-
);
|
|
184
|
-
return parsed && Number.isFinite(parsed.count) ? parsed.count : 0;
|
|
185
|
-
} catch {
|
|
186
|
-
return 0;
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
167
|
/**
|
|
191
168
|
* Scan a retro mirror body for actionable proposals that were neither filed
|
|
192
169
|
* nor discarded. An actionable proposal is a `- **Title**` item under a
|
|
@@ -207,7 +184,7 @@ export function scanRetroMirror(retroText) {
|
|
|
207
184
|
let inSection = false;
|
|
208
185
|
let current = null;
|
|
209
186
|
const flush = () => {
|
|
210
|
-
if (current
|
|
187
|
+
if (current?.actionable && !current.filed) {
|
|
211
188
|
unfiled.push(current.title);
|
|
212
189
|
}
|
|
213
190
|
current = null;
|
|
@@ -235,8 +212,8 @@ export function scanRetroMirror(retroText) {
|
|
|
235
212
|
|
|
236
213
|
/**
|
|
237
214
|
* Core detection: locate the Epic temp tree under `baseDir`, sample its
|
|
238
|
-
* signal streams,
|
|
239
|
-
*
|
|
215
|
+
* signal streams, and scan its retro mirror. Returns a single combined
|
|
216
|
+
* finding when either dimension is non-clean, else `null`.
|
|
240
217
|
*
|
|
241
218
|
* @param {string} baseDir
|
|
242
219
|
* @param {{
|
|
@@ -277,8 +254,6 @@ export function detectLoopHealth(
|
|
|
277
254
|
sampled += r.sampled;
|
|
278
255
|
}
|
|
279
256
|
|
|
280
|
-
const rejectCount = readRejectTally(epicDir, { readImpl });
|
|
281
|
-
|
|
282
257
|
let retroText = '';
|
|
283
258
|
try {
|
|
284
259
|
retroText = readImpl(path.join(epicDir, 'retro.md'), 'utf8');
|
|
@@ -287,20 +262,17 @@ export function detectLoopHealth(
|
|
|
287
262
|
}
|
|
288
263
|
const unfiledProposals = scanRetroMirror(retroText);
|
|
289
264
|
|
|
290
|
-
const signalConcern = invalidCount > 0
|
|
265
|
+
const signalConcern = invalidCount > 0;
|
|
291
266
|
const proposalConcern = unfiledProposals.length > 0;
|
|
292
267
|
if (!signalConcern && !proposalConcern) return null;
|
|
293
268
|
|
|
294
269
|
const summaryParts = [];
|
|
295
270
|
const detailLines = [];
|
|
296
271
|
if (signalConcern) {
|
|
297
|
-
summaryParts.push(
|
|
298
|
-
`${invalidCount} schema-invalid signal sample(s), ${rejectCount} persisted reject(s)`,
|
|
299
|
-
);
|
|
272
|
+
summaryParts.push(`${invalidCount} schema-invalid signal sample(s)`);
|
|
300
273
|
detailLines.push(
|
|
301
274
|
`Sampled ${sampled} line(s) across ${streams.length} signals.ndjson stream(s) (last ${maxLines} per stream):`,
|
|
302
275
|
` schema-invalid samples: ${invalidCount}`,
|
|
303
|
-
` persisted reject tally (signal-rejects.json): ${rejectCount}`,
|
|
304
276
|
);
|
|
305
277
|
}
|
|
306
278
|
if (proposalConcern) {
|
|
@@ -320,7 +292,7 @@ export function detectLoopHealth(
|
|
|
320
292
|
summary: `Loop-health (run-${epicId}): ${summaryParts.join('; ')}.`,
|
|
321
293
|
detail: detailLines.join('\n'),
|
|
322
294
|
fixCommand:
|
|
323
|
-
'Inspect temp/run-<id>/{signals.ndjson,
|
|
295
|
+
'Inspect temp/run-<id>/{signals.ndjson,retro.md}; fix the signal producer or file/discard the surfaced proposals.',
|
|
324
296
|
autoCorrectable: false,
|
|
325
297
|
};
|
|
326
298
|
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* child-exec.js — the one child-process execution surface (Story #5009).
|
|
3
|
+
*
|
|
4
|
+
* Every synchronous and asynchronous child this framework spawns runs through
|
|
5
|
+
* this module. It owns exactly three policies, and nothing else:
|
|
6
|
+
*
|
|
7
|
+
* 1. **The stdout ceiling.** `MAX_BUFFER_BYTES` is defined here and nowhere
|
|
8
|
+
* else. Node's child-process runners default `maxBuffer` to 1 MB and
|
|
9
|
+
* *kill* the child on overflow — `status: null`, `signal: 'SIGTERM'`,
|
|
10
|
+
* `error.code: 'ENOBUFS'` — so the call fails for a reason that has
|
|
11
|
+
* nothing to do with the command. That failure class has shipped
|
|
12
|
+
* operator-visible breakage more than once (Story #4914's committed
|
|
13
|
+
* baseline read at 1,178,910 bytes; Story #4948's `git push` relaying a
|
|
14
|
+
* 2,166,643-byte `pre-push` envelope, which reddened `phase: push` on
|
|
15
|
+
* every Story close while the gates themselves were green). Both were
|
|
16
|
+
* fixed one call site at a time. This module is why there is no third.
|
|
17
|
+
* 2. **Shell-free argv.** `shell: false` on every invocation, so no argument
|
|
18
|
+
* is ever shell-interpolated (`rules/security-baseline.md` § Output &
|
|
19
|
+
* Rendering). Callers pass argv tokens; they cannot pass a command line.
|
|
20
|
+
* 3. **Result and error normalisation.** {@link spawnCapture} collapses the
|
|
21
|
+
* `spawnSync` return into `{ status, stdout, stderr }` with a non-null
|
|
22
|
+
* status and trimmed streams; {@link formatChildFailure} renders the one
|
|
23
|
+
* failure-message shape so a thrown child error reads the same wherever
|
|
24
|
+
* it came from.
|
|
25
|
+
*
|
|
26
|
+
* Every wrapper takes an optional `run` — the injected child-process runner —
|
|
27
|
+
* so a module keeps its own test seam (`git-base.js`'s `__setSpawnRunner`,
|
|
28
|
+
* `git-utils.js`'s `__setGitRunners`, the `run` / `spawn` parameters threaded
|
|
29
|
+
* through the audit-baselines engine) while still delegating buffer, shell and
|
|
30
|
+
* error policy here. Omit it and the real Node runner is used.
|
|
31
|
+
*
|
|
32
|
+
* **Do not re-import `node:child_process` in new modules.**
|
|
33
|
+
* `tests/enforcement/child-process-imports.test.js` captures the current set
|
|
34
|
+
* of direct importers and fails on any addition outside that allowlist.
|
|
35
|
+
*
|
|
36
|
+
* @module lib/child-exec
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
import { execFile, execFileSync, spawnSync } from 'node:child_process';
|
|
40
|
+
import { promisify } from 'node:util';
|
|
41
|
+
|
|
42
|
+
/** One mebibyte, so the ceilings below read as the units they are quoted in. */
|
|
43
|
+
const MIB = 1024 * 1024;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The stdout ceiling every child in this framework runs under — the single
|
|
47
|
+
* definition of the constant.
|
|
48
|
+
*
|
|
49
|
+
* Deliberately **not** exported. Every wrapper below applies it as its
|
|
50
|
+
* default, so a call site gets the ceiling by importing the wrapper rather
|
|
51
|
+
* than by importing (and possibly forgetting) a number. The only bound a
|
|
52
|
+
* caller ever names explicitly is one that is deliberately *different* —
|
|
53
|
+
* today that is {@link INTERCEPTOR_MAX_BUFFER_BYTES} and nothing else.
|
|
54
|
+
*
|
|
55
|
+
* 64 MiB is not a fresh guess: it is the bound Stories #4914 and #4948 already
|
|
56
|
+
* settled on independently for the git read and git push paths, and the value
|
|
57
|
+
* `run-test-profile.js`, `audit-baselines/trend.js` and
|
|
58
|
+
* `audit-baselines/weights.js` had hand-copied. `maxBuffer` caps a buffer, it
|
|
59
|
+
* does not reserve one, so a generous ceiling costs nothing on the calls that
|
|
60
|
+
* print two lines.
|
|
61
|
+
*
|
|
62
|
+
* @type {number}
|
|
63
|
+
*/
|
|
64
|
+
const MAX_BUFFER_BYTES = 64 * MIB;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The deliberately *lower* ceiling the friction interceptor spawns arbitrary
|
|
68
|
+
* operator commands under (`diagnose-friction.js`).
|
|
69
|
+
*
|
|
70
|
+
* This one is a reported policy bound, not an overflow guard: when it fires,
|
|
71
|
+
* the interceptor records `executionMaxBuffer` on the friction row and tells
|
|
72
|
+
* the operator to quieten the command rather than split it (Story #4915).
|
|
73
|
+
* Raising it to {@link MAX_BUFFER_BYTES} would change that emitted row, so it
|
|
74
|
+
* stays where it was — but it is defined here, alongside the ceiling it is
|
|
75
|
+
* deliberately different from, rather than hand-copied into the interceptor.
|
|
76
|
+
*
|
|
77
|
+
* @type {number}
|
|
78
|
+
*/
|
|
79
|
+
export const INTERCEPTOR_MAX_BUFFER_BYTES = 10 * MIB;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Merge caller options over this module's fixed policy.
|
|
83
|
+
*
|
|
84
|
+
* `maxBuffer` is applied **last** so a caller's spread cannot silently drop it
|
|
85
|
+
* back to Node's 1 MB default — the only way to change the ceiling is to pass
|
|
86
|
+
* `maxBuffer` explicitly, which the two named constants above exist for.
|
|
87
|
+
*
|
|
88
|
+
* @param {{ encoding: string }} defaults - Runner-specific defaults.
|
|
89
|
+
* @param {object} rest - Caller options (`cwd`, `env`, `stdio`, …).
|
|
90
|
+
* @param {number} maxBuffer - Resolved stdout ceiling.
|
|
91
|
+
* @returns {object}
|
|
92
|
+
*/
|
|
93
|
+
function childOptions(defaults, rest, maxBuffer) {
|
|
94
|
+
return { ...defaults, shell: false, ...rest, maxBuffer };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Run a child synchronously via `execFileSync`, returning whatever the runner
|
|
99
|
+
* returns (a UTF-8 string under the default encoding). **Throws** on a
|
|
100
|
+
* non-zero exit, exactly like `execFileSync` — callers that treat failure as a
|
|
101
|
+
* recoverable state want {@link spawnCapture} instead.
|
|
102
|
+
*
|
|
103
|
+
* @param {string} file - Executable name (never a shell command line).
|
|
104
|
+
* @param {string[]} args - Argv tokens.
|
|
105
|
+
* @param {object} [opts] - `cwd` / `env` / `stdio` / `encoding`, plus:
|
|
106
|
+
* @param {Function} [opts.run] - Injected runner; defaults to `execFileSync`.
|
|
107
|
+
* @param {number} [opts.maxBuffer] - Override the ceiling; defaults to {@link MAX_BUFFER_BYTES}.
|
|
108
|
+
* @returns {string}
|
|
109
|
+
*/
|
|
110
|
+
export function execFileCapture(file, args, opts = {}) {
|
|
111
|
+
const { run = execFileSync, maxBuffer = MAX_BUFFER_BYTES, ...rest } = opts;
|
|
112
|
+
return run(file, args, childOptions({ encoding: 'utf8' }, rest, maxBuffer));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Promisified `execFile` — the default runner for {@link execFileCaptureAsync}. */
|
|
116
|
+
const execFileAsync = promisify(execFile);
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* The asynchronous sibling of {@link execFileCapture}. Resolves to
|
|
120
|
+
* `{ stdout, stderr }` and rejects on a non-zero exit.
|
|
121
|
+
*
|
|
122
|
+
* @param {string} file - Executable name.
|
|
123
|
+
* @param {string[]} args - Argv tokens.
|
|
124
|
+
* @param {object} [opts] - As {@link execFileCapture}; `run` defaults to a
|
|
125
|
+
* promisified `execFile`.
|
|
126
|
+
* @returns {Promise<{ stdout: string, stderr: string }>}
|
|
127
|
+
*/
|
|
128
|
+
export function execFileCaptureAsync(file, args, opts = {}) {
|
|
129
|
+
const { run = execFileAsync, maxBuffer = MAX_BUFFER_BYTES, ...rest } = opts;
|
|
130
|
+
return run(file, args, childOptions({ encoding: 'utf8' }, rest, maxBuffer));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Run a child synchronously via `spawnSync`, returning the runner's **raw**
|
|
135
|
+
* result. Never throws on a non-zero exit.
|
|
136
|
+
*
|
|
137
|
+
* Use this when the caller needs the untouched result — untrimmed `stdout`
|
|
138
|
+
* (file contents, TAP output), a `status` of `null` that must stay `null`
|
|
139
|
+
* (`git show`'s 128-vs-killed split), or `result.error`. Callers that only
|
|
140
|
+
* want a normalised `{ status, stdout, stderr }` should use
|
|
141
|
+
* {@link spawnCapture}.
|
|
142
|
+
*
|
|
143
|
+
* @param {string} file - Executable name.
|
|
144
|
+
* @param {string[]} args - Argv tokens.
|
|
145
|
+
* @param {object} [opts] - As {@link execFileCapture}; `run` defaults to `spawnSync`.
|
|
146
|
+
* @returns {import('node:child_process').SpawnSyncReturns<string>}
|
|
147
|
+
*/
|
|
148
|
+
export function spawnChild(file, args, opts = {}) {
|
|
149
|
+
const { run = spawnSync, maxBuffer = MAX_BUFFER_BYTES, ...rest } = opts;
|
|
150
|
+
return run(
|
|
151
|
+
file,
|
|
152
|
+
args,
|
|
153
|
+
childOptions({ encoding: 'utf-8', stdio: 'pipe' }, rest, maxBuffer),
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* {@link spawnChild} with the result normalised: `status` is coerced to `1`
|
|
159
|
+
* when the child did not exit normally (so no caller can read a `null` status
|
|
160
|
+
* as success — `process.exit(null)` exits 0), and both streams are coerced to
|
|
161
|
+
* trimmed strings.
|
|
162
|
+
*
|
|
163
|
+
* @param {string} file - Executable name.
|
|
164
|
+
* @param {string[]} args - Argv tokens.
|
|
165
|
+
* @param {object} [opts] - As {@link spawnChild}.
|
|
166
|
+
* @returns {{ status: number, stdout: string, stderr: string }}
|
|
167
|
+
*/
|
|
168
|
+
export function spawnCapture(file, args, opts = {}) {
|
|
169
|
+
const result = spawnChild(file, args, opts);
|
|
170
|
+
return {
|
|
171
|
+
status: result?.status ?? 1,
|
|
172
|
+
stdout: (result?.stdout ?? '').toString().trim(),
|
|
173
|
+
stderr: (result?.stderr ?? '').toString().trim(),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* The one failure-message shape for a child that exited non-zero.
|
|
179
|
+
*
|
|
180
|
+
* `status` is rendered verbatim — a `null` prints as `status=null`, which is
|
|
181
|
+
* the diagnostic: it means the child was killed (buffer overflow, timeout,
|
|
182
|
+
* signal) rather than having reported an exit code of its own.
|
|
183
|
+
*
|
|
184
|
+
* @param {object} failure
|
|
185
|
+
* @param {string} failure.label - What was being attempted, e.g. `readBaseFromGit: git show main:x`.
|
|
186
|
+
* @param {number|null} failure.status - Raw child exit status.
|
|
187
|
+
* @param {unknown} [failure.stderr] - Raw stderr; coerced and trimmed.
|
|
188
|
+
* @returns {string}
|
|
189
|
+
*/
|
|
190
|
+
export function formatChildFailure({ label, status, stderr }) {
|
|
191
|
+
const detail = (stderr ?? '').toString().trim();
|
|
192
|
+
return `${label} failed (status=${status}): ${detail}`;
|
|
193
|
+
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Replaces the per-CLI hand-rolled flag dispatch with a single
|
|
6
6
|
* declarative entrypoint that the refactored scripts (story-close,
|
|
7
|
-
* epic-deliver, check-baselines
|
|
7
|
+
* epic-deliver, check-baselines) all call. The helper
|
|
8
8
|
* covers the flags every dispatcher CLI shares (`--epic`, `--story`,
|
|
9
9
|
* `--changed-since`, `--json`, `--full-scope`, `--dry-run`)
|
|
10
10
|
* and — via the `extras` schema entry — caller-defined extras (e.g.
|