mandrel 2.30.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/ci-remediation.md +44 -1
- 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/push.js +17 -2
- 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 +65 -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 +2 -12
- package/.agents/skills/stack/qa/playwright/SKILL.md +48 -0
- 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 +49 -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
|
@@ -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.
|
|
@@ -109,6 +109,59 @@ function tolerantMergeWatchMode(value) {
|
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
/**
|
|
113
|
+
* Shortest override reason that can plausibly name a rejected finding. Below
|
|
114
|
+
* this, the flag is being used to silence the gate rather than to record a
|
|
115
|
+
* judgement, which is the failure mode the reason requirement exists to stop.
|
|
116
|
+
*/
|
|
117
|
+
const MIN_OVERRIDE_REASON_LENGTH = 12;
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Parse `--override-review-block <reason>` — the sanctioned,
|
|
121
|
+
* logged override of a Story-scope code-review critical blocker.
|
|
122
|
+
*
|
|
123
|
+
* The reason is **mandatory and validated**, because the alternative it
|
|
124
|
+
* replaces is not "no override" but `gh pr merge` run by hand: before this
|
|
125
|
+
* flag, a review blocker the operator had reviewed and rejected left bypassing
|
|
126
|
+
* the gate entirely as the only way to land, and that bypass wrote nothing
|
|
127
|
+
* down anywhere. An override that records why is strictly more auditable than
|
|
128
|
+
* the hand-merge it displaces; an override that records nothing is not, so a
|
|
129
|
+
* bare or blank `--override-review-block` fails closed here — before any phase
|
|
130
|
+
* runs, at no mutation cost — rather than arming a silent one.
|
|
131
|
+
*
|
|
132
|
+
* @param {unknown} value
|
|
133
|
+
* @returns {string|undefined} the trimmed reason, or `undefined` when absent.
|
|
134
|
+
*/
|
|
135
|
+
export function parseOverrideReviewBlock(value) {
|
|
136
|
+
if (value == null) return undefined;
|
|
137
|
+
// `parseArgs` with `strict: false` yields `true` for a bare string flag.
|
|
138
|
+
const reason = typeof value === 'string' ? value.trim() : '';
|
|
139
|
+
if (reason.length >= MIN_OVERRIDE_REASON_LENGTH) return reason;
|
|
140
|
+
throw new Error(
|
|
141
|
+
'--override-review-block requires a reason of at least ' +
|
|
142
|
+
`${MIN_OVERRIDE_REASON_LENGTH} characters naming the finding you reviewed ` +
|
|
143
|
+
`and rejected (got ${JSON.stringify(value)}). The reason is posted to the ` +
|
|
144
|
+
'PR and the Story and recorded as friction telemetry.',
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* {@link parseOverrideReviewBlock} degraded to absent instead of throwing, for
|
|
150
|
+
* the tolerant reporting parse. Same contract as
|
|
151
|
+
* {@link tolerantMergeWatchMode}: safe only because a tolerant parse runs no
|
|
152
|
+
* phase.
|
|
153
|
+
*
|
|
154
|
+
* @param {unknown} value
|
|
155
|
+
* @returns {string|undefined}
|
|
156
|
+
*/
|
|
157
|
+
function tolerantOverrideReviewBlock(value) {
|
|
158
|
+
try {
|
|
159
|
+
return parseOverrideReviewBlock(value);
|
|
160
|
+
} catch {
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
112
165
|
/**
|
|
113
166
|
* Standardized CLI argument parser for sprint scripts.
|
|
114
167
|
* Supports options like --epic, --story, --dry-run, --skip-dashboard.
|
|
@@ -147,6 +200,11 @@ export function parseSprintArgs(
|
|
|
147
200
|
// Absent means "use the config"; see `parseMergeWatchMode` for why an
|
|
148
201
|
// unrecognized value fails closed instead of degrading to absent.
|
|
149
202
|
'merge-watch-mode': { type: 'string' },
|
|
203
|
+
// Sanctioned override of a code-review critical blocker.
|
|
204
|
+
// Absent means "the blocker blocks"; see `parseOverrideReviewBlock` for
|
|
205
|
+
// why a bare or too-short reason fails closed instead of arming a silent
|
|
206
|
+
// override.
|
|
207
|
+
'override-review-block': { type: 'string' },
|
|
150
208
|
executor: { type: 'string' },
|
|
151
209
|
cwd: { type: 'string' },
|
|
152
210
|
'recut-of': { type: 'string' },
|
|
@@ -183,6 +241,12 @@ export function parseSprintArgs(
|
|
|
183
241
|
mergeWatchMode: tolerant
|
|
184
242
|
? tolerantMergeWatchMode(values['merge-watch-mode'])
|
|
185
243
|
: parseMergeWatchMode(values['merge-watch-mode']),
|
|
244
|
+
// The operator's recorded reason for overriding a review
|
|
245
|
+
// blocker. `undefined` when the flag is absent, which is what keeps the
|
|
246
|
+
// blocker blocking by default.
|
|
247
|
+
overrideReviewBlock: tolerant
|
|
248
|
+
? tolerantOverrideReviewBlock(values['override-review-block'])
|
|
249
|
+
: parseOverrideReviewBlock(values['override-review-block']),
|
|
186
250
|
executor: values.executor ?? null,
|
|
187
251
|
// Resolve worktree cwd from flag or env. Empty string/whitespace → null.
|
|
188
252
|
cwd:
|
|
@@ -327,8 +327,8 @@ export function buildDefaultGates({
|
|
|
327
327
|
...buildTestGateEntry(coverageCaptureActive),
|
|
328
328
|
{
|
|
329
329
|
// Gate name kept generic ("format") so the close-orchestrator log line
|
|
330
|
-
//
|
|
331
|
-
//
|
|
330
|
+
// doesn't shift when a repo swaps biome for Prettier / dprint via
|
|
331
|
+
// `project.commands.formatCheck`. The
|
|
332
332
|
// actual command and the remediation hint resolve from config.
|
|
333
333
|
name: 'format',
|
|
334
334
|
cmd: formatCmd,
|
|
@@ -87,9 +87,9 @@ function applyChangedFileScope({ gate, spawnCwd, log }) {
|
|
|
87
87
|
* `<tempRoot>/standalone/stories/story-<id>/validation-evidence.json`
|
|
88
88
|
* keyspace. v2.0.0 removed the Epic tier and its Epic-keyed keyspace.
|
|
89
89
|
*
|
|
90
|
-
* `onGateStart` is invoked immediately before each gate's runner spawn
|
|
91
|
-
*
|
|
92
|
-
*
|
|
90
|
+
* `onGateStart` is invoked immediately before each gate's runner spawn, so a
|
|
91
|
+
* caller can record per-gate wall-clock telemetry. Errors thrown from the
|
|
92
|
+
* hook propagate.
|
|
93
93
|
*
|
|
94
94
|
* Projection advisories (Story #4776): when `baseBranch` and `storyBranch`
|
|
95
95
|
* are both supplied and every gate passed, the maintainability and CRAP
|
|
@@ -38,7 +38,6 @@
|
|
|
38
38
|
*/
|
|
39
39
|
export const ACCEPTANCE_EVAL_DEFAULTS = Object.freeze({
|
|
40
40
|
maxRounds: 2,
|
|
41
|
-
clusterCeiling: 4,
|
|
42
41
|
});
|
|
43
42
|
|
|
44
43
|
/**
|
|
@@ -51,22 +50,6 @@ export const ACCEPTANCE_EVAL_DEFAULTS = Object.freeze({
|
|
|
51
50
|
*/
|
|
52
51
|
export const ACCEPTANCE_EVAL_MAX_ROUNDS_CEILING = 5;
|
|
53
52
|
|
|
54
|
-
/**
|
|
55
|
-
* Hard, undisableable ceiling on `clusterCeiling` — the max ACs one
|
|
56
|
-
* single-delivery acceptance critic scores in a single fresh-context pass
|
|
57
|
-
* (Epic #4475, M4-B, design §S2a). It is the **acceptance-dilution guard**:
|
|
58
|
-
* single delivery collapses the whole Epic into one session, so the only
|
|
59
|
-
* acceptance coverage left is the per-AC-cluster critic fan-out
|
|
60
|
-
* (`ceil(totalACs / clusterCeiling)` independent maker-blind passes). A
|
|
61
|
-
* pathologically large `clusterCeiling` would collapse that fan-out to a
|
|
62
|
-
* single critic scoring every AC at once — exactly the dilution the design
|
|
63
|
-
* forecloses — so a configured value above this ceiling is clamped down to
|
|
64
|
-
* it. Kept small on purpose.
|
|
65
|
-
*
|
|
66
|
-
* @type {number}
|
|
67
|
-
*/
|
|
68
|
-
export const ACCEPTANCE_EVAL_CLUSTER_CEILING_MAX = 8;
|
|
69
|
-
|
|
70
53
|
/**
|
|
71
54
|
* Clamp a candidate round count into the inviolable `[1, ceiling]` range.
|
|
72
55
|
* Non-integer / non-finite inputs fall back to the documented default.
|
|
@@ -85,43 +68,19 @@ function clampRounds(value, fallback) {
|
|
|
85
68
|
return candidate;
|
|
86
69
|
}
|
|
87
70
|
|
|
88
|
-
/**
|
|
89
|
-
* Clamp a candidate cluster ceiling into the inviolable
|
|
90
|
-
* `[1, ACCEPTANCE_EVAL_CLUSTER_CEILING_MAX]` range. Non-integer / non-finite
|
|
91
|
-
* inputs fall back to the documented default. Mirrors `clampRounds` — the
|
|
92
|
-
* anti-dilution guard cannot be disabled (`clusterCeiling: 0` or a negative
|
|
93
|
-
* clamps up to 1; an over-max value clamps down to the hard cap).
|
|
94
|
-
*
|
|
95
|
-
* @param {unknown} value
|
|
96
|
-
* @param {number} fallback
|
|
97
|
-
* @returns {number}
|
|
98
|
-
*/
|
|
99
|
-
function clampClusterCeiling(value, fallback) {
|
|
100
|
-
const candidate =
|
|
101
|
-
typeof value === 'number' && Number.isInteger(value) ? value : fallback;
|
|
102
|
-
if (candidate < 1) return 1;
|
|
103
|
-
if (candidate > ACCEPTANCE_EVAL_CLUSTER_CEILING_MAX) {
|
|
104
|
-
return ACCEPTANCE_EVAL_CLUSTER_CEILING_MAX;
|
|
105
|
-
}
|
|
106
|
-
return candidate;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
71
|
/**
|
|
110
72
|
* Read the merged acceptance-eval block. Returns the canonical shape:
|
|
111
73
|
*
|
|
112
74
|
* {
|
|
113
|
-
* maxRounds: number,
|
|
114
|
-
* ceiling: number
|
|
115
|
-
* clusterCeiling: number, // clamped into [1, clusterCeilingMax]
|
|
116
|
-
* clusterCeilingMax: number // the undisableable hard cap on cluster size
|
|
75
|
+
* maxRounds: number, // clamped into [1, ceiling]
|
|
76
|
+
* ceiling: number // the undisableable hard cap on rounds
|
|
117
77
|
* }
|
|
118
78
|
*
|
|
119
|
-
* `maxRounds` is always a positive integer no greater than `ceiling
|
|
120
|
-
*
|
|
121
|
-
* `clusterCeilingMax`, regardless of what the resolved config carried.
|
|
79
|
+
* `maxRounds` is always a positive integer no greater than `ceiling`,
|
|
80
|
+
* regardless of what the resolved config carried.
|
|
122
81
|
*
|
|
123
82
|
* @param {object | null | undefined} config
|
|
124
|
-
* @returns {{ maxRounds: number, ceiling: number
|
|
83
|
+
* @returns {{ maxRounds: number, ceiling: number }}
|
|
125
84
|
*/
|
|
126
85
|
export function getAcceptanceEval(config) {
|
|
127
86
|
const user = config?.delivery?.acceptanceEval ?? {};
|
|
@@ -129,14 +88,8 @@ export function getAcceptanceEval(config) {
|
|
|
129
88
|
user.maxRounds,
|
|
130
89
|
ACCEPTANCE_EVAL_DEFAULTS.maxRounds,
|
|
131
90
|
);
|
|
132
|
-
const clusterCeiling = clampClusterCeiling(
|
|
133
|
-
user.clusterCeiling,
|
|
134
|
-
ACCEPTANCE_EVAL_DEFAULTS.clusterCeiling,
|
|
135
|
-
);
|
|
136
91
|
return {
|
|
137
92
|
maxRounds,
|
|
138
93
|
ceiling: ACCEPTANCE_EVAL_MAX_ROUNDS_CEILING,
|
|
139
|
-
clusterCeiling,
|
|
140
|
-
clusterCeilingMax: ACCEPTANCE_EVAL_CLUSTER_CEILING_MAX,
|
|
141
94
|
};
|
|
142
95
|
}
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `project.commands` accessor (Epic #1720 Story #1739 — top-level reshape).
|
|
3
3
|
*
|
|
4
|
-
* The surviving
|
|
5
|
-
* `
|
|
4
|
+
* The surviving four command keys are `test`, `typecheck`,
|
|
5
|
+
* `formatCheck`, `formatWrite`.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
export const COMMANDS_DEFAULTS = Object.freeze({
|
|
9
|
-
lintBaseline: 'npx eslint . --format json',
|
|
10
9
|
test: 'npm test',
|
|
11
10
|
typecheck: null,
|
|
12
11
|
formatCheck: 'npx biome format .',
|
|
@@ -19,12 +18,11 @@ export const COMMANDS_DEFAULTS = Object.freeze({
|
|
|
19
18
|
* a bare `{ project }` bag.
|
|
20
19
|
*
|
|
21
20
|
* @param {object | null | undefined} config
|
|
22
|
-
* @returns {{
|
|
21
|
+
* @returns {{ test: string, typecheck: string|null, formatCheck: string, formatWrite: string }}
|
|
23
22
|
*/
|
|
24
23
|
export function getCommands(config) {
|
|
25
24
|
const commands = config?.project?.commands ?? {};
|
|
26
25
|
return {
|
|
27
|
-
lintBaseline: commands.lintBaseline ?? COMMANDS_DEFAULTS.lintBaseline,
|
|
28
26
|
test: commands.test ?? COMMANDS_DEFAULTS.test,
|
|
29
27
|
typecheck:
|
|
30
28
|
commands.typecheck === undefined
|