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
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* phases/review-override.js — the sanctioned, logged override of a Story-scope
|
|
3
|
+
* code-review critical blocker.
|
|
4
|
+
*
|
|
5
|
+
* Split out of `phases/review-block.js`, which owns the opposite outcome: the
|
|
6
|
+
* blocker that *holds*. Keeping the two in one module meant one file owning
|
|
7
|
+
* both "park this Story" and "ship it anyway", and the override's audit trail is
|
|
8
|
+
* substantial enough — three write surfaces, each independently best-effort —
|
|
9
|
+
* to be its own reason to change.
|
|
10
|
+
*
|
|
11
|
+
* **Why an override exists at all.** A critical review finding halts
|
|
12
|
+
* `single-story-close.js` before auto-merge, and no flag overrode a review
|
|
13
|
+
* verdict: `--skip-validation` bypasses the gate chain and `--no-auto-merge`
|
|
14
|
+
* declines to arm, but neither touches the review. So an operator who had read a
|
|
15
|
+
* finding and judged it wrong could only land by merging the PR by hand, which
|
|
16
|
+
* bypasses the gate with no record anywhere of what was overridden or why. This
|
|
17
|
+
* module does not weaken the gate; it relocates that escape hatch out of an
|
|
18
|
+
* untraceable hand-merge and into a mandatory-reason audit trail.
|
|
19
|
+
*
|
|
20
|
+
* Live provenance: Story #5007 / PR #5022, where a FALSE critical blocker — an
|
|
21
|
+
* MI finding on files `delivery.quality.gates.maintainability.ignoreGlobs`
|
|
22
|
+
* exempts, which the `check-baselines.js` ratchet correctly ignored in the same
|
|
23
|
+
* run — halted a legitimate delivery with a hand-merge as the only way out.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { Logger } from '../../../Logger.js';
|
|
27
|
+
import {
|
|
28
|
+
emitRuntimeFriction,
|
|
29
|
+
RUNTIME_FRICTION_CATEGORIES,
|
|
30
|
+
} from '../../../observability/runtime-friction.js';
|
|
31
|
+
import {
|
|
32
|
+
postStructuredComment,
|
|
33
|
+
upsertStructuredComment,
|
|
34
|
+
} from '../../ticketing.js';
|
|
35
|
+
|
|
36
|
+
/** Cap on the reason text copied into the friction signal's `details`. */
|
|
37
|
+
const REASON_SIGNAL_LIMIT = 500;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Build the audit record posted when an operator overrides a review blocker.
|
|
41
|
+
* Pure.
|
|
42
|
+
*
|
|
43
|
+
* The body restates the count, the reason, and the fact that auto-merge was
|
|
44
|
+
* armed anyway — an override whose trail says only "overridden" is no better
|
|
45
|
+
* than the hand-merge it replaces.
|
|
46
|
+
*
|
|
47
|
+
* Module-local: an implementation detail of
|
|
48
|
+
* {@link handleOverriddenReviewBlock}, whose posted body is observable through
|
|
49
|
+
* that public entry point. Exporting it would add a public symbol no production
|
|
50
|
+
* path reaches — the exact dead-export shape this repo ratchets against.
|
|
51
|
+
*
|
|
52
|
+
* @param {{ prUrl: string, criticalCount: number, reason: string }} args
|
|
53
|
+
* @returns {string}
|
|
54
|
+
*/
|
|
55
|
+
function buildReviewOverrideBody({ prUrl, criticalCount, reason }) {
|
|
56
|
+
return [
|
|
57
|
+
'### Code-review blocker overridden by operator',
|
|
58
|
+
'',
|
|
59
|
+
`The Story-scope review reported **${criticalCount} critical blocker(s)** on ${prUrl}.`,
|
|
60
|
+
'The operator reviewed and rejected the finding(s) and authorized delivery',
|
|
61
|
+
'with `--override-review-block`; auto-merge was armed.',
|
|
62
|
+
'',
|
|
63
|
+
'**Recorded reason:**',
|
|
64
|
+
'',
|
|
65
|
+
`> ${reason.split('\n').join('\n> ')}`,
|
|
66
|
+
'',
|
|
67
|
+
'The findings comment on the PR is left in place unchanged — this record',
|
|
68
|
+
'sits beside it rather than resolving it.',
|
|
69
|
+
].join('\n');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Post one audit record, swallowing the failure into a warning.
|
|
74
|
+
*
|
|
75
|
+
* Every write here is best-effort by design: an override whose audit trail
|
|
76
|
+
* partly failed must still land the delivery the operator authorized, because
|
|
77
|
+
* the close would otherwise fail for a reason the operator cannot act on. The
|
|
78
|
+
* friction signal is the durable record — it is what makes a rising override
|
|
79
|
+
* count visible to the retro.
|
|
80
|
+
*
|
|
81
|
+
* Module-local: the two call sites below are its only callers.
|
|
82
|
+
*
|
|
83
|
+
* @param {{ post: () => Promise<unknown>, surface: string }} args
|
|
84
|
+
* @returns {Promise<boolean>} true when the record landed.
|
|
85
|
+
*/
|
|
86
|
+
async function postAuditRecord({ post, surface }) {
|
|
87
|
+
try {
|
|
88
|
+
await post();
|
|
89
|
+
return true;
|
|
90
|
+
} catch (err) {
|
|
91
|
+
Logger.warn(
|
|
92
|
+
`[single-story-close] failed to post review-override record on ${surface}: ${err?.message ?? err}`,
|
|
93
|
+
);
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Record an operator-authorized override of a critical code-review blocker,
|
|
100
|
+
* then let close continue to the auto-merge phase.
|
|
101
|
+
*
|
|
102
|
+
* Writes to three surfaces: the Story issue via `upsert` so a re-run does not
|
|
103
|
+
* stack duplicates; the PR via `post` so the trail is append-only on the surface
|
|
104
|
+
* a human reviews, and so a reviewer reading the PR need not open the Story to
|
|
105
|
+
* learn the blocker was overridden; and the friction stream.
|
|
106
|
+
*
|
|
107
|
+
* @param {{
|
|
108
|
+
* provider: object,
|
|
109
|
+
* storyId: number,
|
|
110
|
+
* prUrl: string,
|
|
111
|
+
* prNumber: number|null,
|
|
112
|
+
* criticalCount: number,
|
|
113
|
+
* reason: string,
|
|
114
|
+
* config?: object,
|
|
115
|
+
* emitFrictionFn?: typeof emitRuntimeFriction,
|
|
116
|
+
* }} args
|
|
117
|
+
* @returns {Promise<{ overridden: true, reason: string, criticalCount: number }>}
|
|
118
|
+
*/
|
|
119
|
+
export async function handleOverriddenReviewBlock({
|
|
120
|
+
provider,
|
|
121
|
+
storyId,
|
|
122
|
+
prUrl,
|
|
123
|
+
prNumber,
|
|
124
|
+
criticalCount,
|
|
125
|
+
reason,
|
|
126
|
+
config,
|
|
127
|
+
emitFrictionFn = emitRuntimeFriction,
|
|
128
|
+
}) {
|
|
129
|
+
const body = buildReviewOverrideBody({ prUrl, criticalCount, reason });
|
|
130
|
+
Logger.warn(
|
|
131
|
+
`[single-story-close] ⚠️ Story-scope review reported ${criticalCount} critical blocker(s) on ` +
|
|
132
|
+
`PR ${prUrl} — OVERRIDDEN by operator: ${reason}`,
|
|
133
|
+
);
|
|
134
|
+
await postAuditRecord({
|
|
135
|
+
post: () => upsertStructuredComment(provider, storyId, 'friction', body),
|
|
136
|
+
surface: `Story #${storyId}`,
|
|
137
|
+
});
|
|
138
|
+
if (Number.isInteger(prNumber)) {
|
|
139
|
+
await postAuditRecord({
|
|
140
|
+
post: () =>
|
|
141
|
+
postStructuredComment(provider, prNumber, 'notification', body),
|
|
142
|
+
surface: `PR #${prNumber}`,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
await emitFrictionFn({
|
|
146
|
+
storyId,
|
|
147
|
+
category: RUNTIME_FRICTION_CATEGORIES.REVIEW_BLOCK_OVERRIDDEN,
|
|
148
|
+
tool: 'single-story-close',
|
|
149
|
+
details: {
|
|
150
|
+
prUrl,
|
|
151
|
+
criticalCount,
|
|
152
|
+
reason: reason.slice(0, REASON_SIGNAL_LIMIT),
|
|
153
|
+
},
|
|
154
|
+
config,
|
|
155
|
+
});
|
|
156
|
+
return { overridden: true, reason, criticalCount };
|
|
157
|
+
}
|
|
@@ -16,13 +16,9 @@
|
|
|
16
16
|
* actually reaped and every close said it did. Best-effort means the close
|
|
17
17
|
* still succeeds on refusal; it does not mean the close may claim an
|
|
18
18
|
* outcome it never checked.
|
|
19
|
-
*
|
|
20
|
-
* Also clears the trace-hook env vars so subsequent tooling falls back to
|
|
21
|
-
* the no-op branch instead of pointing at a (now-reaped) worktree.
|
|
22
19
|
*/
|
|
23
20
|
|
|
24
21
|
import { Logger } from '../../../Logger.js';
|
|
25
|
-
import { clearActiveStoryEnv } from '../../../observability/active-story-env.js';
|
|
26
22
|
import { WorktreeManager as DefaultWorktreeManager } from '../../../worktree-manager.js';
|
|
27
23
|
|
|
28
24
|
/**
|
|
@@ -92,15 +88,5 @@ export async function reapWorktreePhase({
|
|
|
92
88
|
}
|
|
93
89
|
}
|
|
94
90
|
|
|
95
|
-
// Clear the trace-hook env vars so subsequent tooling falls back to the
|
|
96
|
-
// no-op branch instead of pointing at a (now-reaped) worktree.
|
|
97
|
-
try {
|
|
98
|
-
clearActiveStoryEnv({
|
|
99
|
-
logger: { warn: (m) => progress('ENV', `⚠️ ${m}`) },
|
|
100
|
-
});
|
|
101
|
-
} catch {
|
|
102
|
-
// Non-fatal.
|
|
103
|
-
}
|
|
104
|
-
|
|
105
91
|
return worktreeReaped;
|
|
106
92
|
}
|
|
@@ -29,6 +29,7 @@ import { parseCloseOptions, resolveWaitForMerge } from './phases/options.js';
|
|
|
29
29
|
import { ensurePullRequestWith } from './phases/pull-request.js';
|
|
30
30
|
import { pushStoryBranch } from './phases/push.js';
|
|
31
31
|
import { handleCriticalReviewBlock } from './phases/review-block.js';
|
|
32
|
+
import { handleOverriddenReviewBlock } from './phases/review-override.js';
|
|
32
33
|
import { reapWorktreePhase } from './phases/worktree-reap.js';
|
|
33
34
|
import { runWrongTreeGuardPhase } from './phases/wrong-tree-guard.js';
|
|
34
35
|
|
|
@@ -201,22 +202,31 @@ async function runPrePushPhases({
|
|
|
201
202
|
|
|
202
203
|
async function openAndReviewPr({
|
|
203
204
|
cwd,
|
|
205
|
+
worktreePath,
|
|
204
206
|
story,
|
|
205
207
|
storyId,
|
|
206
208
|
storyBranch,
|
|
207
209
|
baseBranch,
|
|
208
210
|
provider,
|
|
211
|
+
config,
|
|
212
|
+
overrideReviewBlock,
|
|
209
213
|
injectedGh,
|
|
210
214
|
injectedRunCodeReview,
|
|
211
215
|
setPhase = () => {},
|
|
212
216
|
}) {
|
|
213
217
|
setPhase('push');
|
|
214
|
-
|
|
218
|
+
// Issue #4990 — push from the Story worktree so `pre-push` measures the
|
|
219
|
+
// tree being sent. `gh` and the review's ref-based diffs below keep the
|
|
220
|
+
// caller's `cwd`: they read the shared `.git`, so they resolve identically
|
|
221
|
+
// from either tree.
|
|
222
|
+
pushStoryBranch({ cwd, worktreePath, storyBranch, gitSync, progress });
|
|
215
223
|
setPhase('pull-request');
|
|
216
224
|
const { url: prUrl, alreadyMerged } = await ensurePullRequestWith({
|
|
217
225
|
cwd,
|
|
218
226
|
storyId,
|
|
219
227
|
storyTitle: story.title,
|
|
228
|
+
// Scanned for a declared `BREAKING CHANGE:` footer — see pull-request.js.
|
|
229
|
+
storyBody: story.body,
|
|
220
230
|
storyBranch,
|
|
221
231
|
baseBranch,
|
|
222
232
|
gh: injectedGh,
|
|
@@ -244,6 +254,27 @@ async function openAndReviewPr({
|
|
|
244
254
|
});
|
|
245
255
|
if (reviewOutcome.halted) {
|
|
246
256
|
const criticalCount = reviewOutcome.severity?.critical ?? 0;
|
|
257
|
+
// The sanctioned override. Checked BEFORE the blocked
|
|
258
|
+
// transition so an overridden run never touches `agent::blocked`: the
|
|
259
|
+
// Story is proceeding, and parking it would make the label lie for the
|
|
260
|
+
// rest of the close.
|
|
261
|
+
if (overrideReviewBlock) {
|
|
262
|
+
const override = await handleOverriddenReviewBlock({
|
|
263
|
+
provider,
|
|
264
|
+
storyId,
|
|
265
|
+
prUrl,
|
|
266
|
+
prNumber,
|
|
267
|
+
criticalCount,
|
|
268
|
+
reason: overrideReviewBlock,
|
|
269
|
+
config,
|
|
270
|
+
});
|
|
271
|
+
return {
|
|
272
|
+
prUrl,
|
|
273
|
+
prNumber,
|
|
274
|
+
alreadyMerged: false,
|
|
275
|
+
reviewOverride: override,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
247
278
|
await handleCriticalReviewBlock({
|
|
248
279
|
provider,
|
|
249
280
|
storyId,
|
|
@@ -252,10 +283,12 @@ async function openAndReviewPr({
|
|
|
252
283
|
});
|
|
253
284
|
throw new Error(
|
|
254
285
|
`[single-story-close] Story-scope review reported ${criticalCount} critical blocker(s) on PR ${prUrl}. ` +
|
|
255
|
-
'Auto-merge was not enabled. Remediate the findings posted to the PR and re-run `/deliver`.'
|
|
286
|
+
'Auto-merge was not enabled. Remediate the findings posted to the PR and re-run `/deliver`. ' +
|
|
287
|
+
'If you have reviewed a finding and judged it wrong, re-run with ' +
|
|
288
|
+
'`--override-review-block "<reason>"` rather than merging by hand.',
|
|
256
289
|
);
|
|
257
290
|
}
|
|
258
|
-
return { prUrl, prNumber, alreadyMerged: false };
|
|
291
|
+
return { prUrl, prNumber, alreadyMerged: false, reviewOverride: null };
|
|
259
292
|
}
|
|
260
293
|
|
|
261
294
|
async function releaseLease({
|
|
@@ -291,11 +324,9 @@ async function releaseLease({
|
|
|
291
324
|
* `runBaseSyncPhase`, and a critical-blocker review halt in
|
|
292
325
|
* `openAndReviewPr`) throw before the clean-close lease release at the
|
|
293
326
|
* tail of `runSingleStoryClose`, stranding the operator's lease
|
|
294
|
-
* indefinitely. The standalone lease
|
|
295
|
-
*
|
|
296
|
-
*
|
|
297
|
-
* configured TTL), so a stranded claim is cleared only by `--steal` or
|
|
298
|
-
* de-assignment. That fail-closed-refuses a different operator who picks up
|
|
327
|
+
* indefinitely. The standalone lease has **no** TTL to expire by (Story
|
|
328
|
+
* #5006 deleted it): `acquireLease` refuses any foreign assignee outright,
|
|
329
|
+
* so a stranded claim is cleared only by `--steal` or de-assignment. That fail-closed-refuses a different operator who picks up
|
|
299
330
|
* the blocked Story — exactly the hand-off case. Releasing here closes
|
|
300
331
|
* that gap.
|
|
301
332
|
*
|
|
@@ -378,6 +409,7 @@ export async function runSingleStoryClose({
|
|
|
378
409
|
noWaitForMerge: noWaitForMergeParam,
|
|
379
410
|
maxWaitSeconds: maxWaitSecondsParam,
|
|
380
411
|
mergeWatchMode: mergeWatchModeParam,
|
|
412
|
+
overrideReviewBlock: overrideReviewBlockParam,
|
|
381
413
|
injectedProvider,
|
|
382
414
|
injectedConfig,
|
|
383
415
|
injectedNotify,
|
|
@@ -397,10 +429,11 @@ export async function runSingleStoryClose({
|
|
|
397
429
|
noWaitForMergeParam,
|
|
398
430
|
maxWaitSecondsParam,
|
|
399
431
|
mergeWatchModeParam,
|
|
432
|
+
overrideReviewBlockParam,
|
|
400
433
|
});
|
|
401
434
|
if (!options.storyId) {
|
|
402
435
|
throw new Error(
|
|
403
|
-
'Usage: node single-story-close.js --story <STORY_ID> [--cwd <main-repo>] [--skip-validation] [--skip-sync] [--no-auto-merge] [--wait-merge|--no-wait-merge] [--max-wait-seconds <n>] [--merge-watch-mode <sync|async>]',
|
|
436
|
+
'Usage: node single-story-close.js --story <STORY_ID> [--cwd <main-repo>] [--skip-validation] [--skip-sync] [--no-auto-merge] [--wait-merge|--no-wait-merge] [--max-wait-seconds <n>] [--merge-watch-mode <sync|async>] [--override-review-block <reason>]',
|
|
404
437
|
);
|
|
405
438
|
}
|
|
406
439
|
|
|
@@ -725,21 +758,25 @@ async function runClosePipeline({
|
|
|
725
758
|
leaseArgs,
|
|
726
759
|
);
|
|
727
760
|
|
|
728
|
-
const { prUrl, prNumber, alreadyMerged } =
|
|
729
|
-
(
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
761
|
+
const { prUrl, prNumber, alreadyMerged, reviewOverride } =
|
|
762
|
+
await releaseLeaseOnBlock(
|
|
763
|
+
() =>
|
|
764
|
+
openAndReviewPr({
|
|
765
|
+
cwd: options.cwd,
|
|
766
|
+
worktreePath,
|
|
767
|
+
story,
|
|
768
|
+
storyId: options.storyId,
|
|
769
|
+
storyBranch,
|
|
770
|
+
baseBranch,
|
|
771
|
+
provider,
|
|
772
|
+
config,
|
|
773
|
+
overrideReviewBlock: options.overrideReviewBlock,
|
|
774
|
+
injectedGh,
|
|
775
|
+
injectedRunCodeReview,
|
|
776
|
+
setPhase,
|
|
777
|
+
}),
|
|
778
|
+
leaseArgs,
|
|
779
|
+
);
|
|
743
780
|
// Reap the per-Story worktree BEFORE the arm (Story #4681). Arming runs
|
|
744
781
|
// `gh pr merge --auto --squash --delete-branch`, which — against an
|
|
745
782
|
// already-mergeable PR — merges immediately and then shells out to local
|
|
@@ -837,7 +874,10 @@ async function runClosePipeline({
|
|
|
837
874
|
gates: {
|
|
838
875
|
validation: options.skipValidation ? 'skipped' : 'passed',
|
|
839
876
|
baseSync: options.skipSync ? 'skipped' : 'passed',
|
|
840
|
-
|
|
877
|
+
// An overridden blocker reports `overridden`, never
|
|
878
|
+
// `passed`. The review DID fail; a human authorized shipping anyway, and
|
|
879
|
+
// the envelope is the machine-readable trail that says so.
|
|
880
|
+
codeReview: reviewOverride ? 'overridden' : 'passed',
|
|
841
881
|
},
|
|
842
882
|
};
|
|
843
883
|
|
|
@@ -24,18 +24,17 @@
|
|
|
24
24
|
* operator handle from config and delegates assignee mutation to the pure
|
|
25
25
|
* `ticket-lease.js` primitive.
|
|
26
26
|
*
|
|
27
|
-
* **Fail-closed
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* raced into automatically. Unclaimed and self-held tickets still proceed.
|
|
27
|
+
* **Fail-closed (audit #3513, hard-wired by Story #5006).** The lease
|
|
28
|
+
* primitive used to reclaim a foreign claim whose heartbeat was older than a
|
|
29
|
+
* TTL — and with no heartbeat source, *every* foreign claim looked stale, so
|
|
30
|
+
* the guard silently reclaimed any foreign assignee and was inert as a
|
|
31
|
+
* concurrency guard. The guard worked around that by anchoring the heartbeat
|
|
32
|
+
* to `now`; Story #5006 deleted the TTL outright, so the fail-closed
|
|
33
|
+
* behaviour is now the primitive's own: a foreign assignee refuses the take
|
|
34
|
+
* (naming the current owner) unless the operator passes `--steal` to forcibly
|
|
35
|
+
* transfer it. A genuinely abandoned claim is cleared by hand (or `--steal`)
|
|
36
|
+
* rather than raced into automatically. Unclaimed and self-held tickets still
|
|
37
|
+
* proceed.
|
|
39
38
|
*/
|
|
40
39
|
|
|
41
40
|
import {
|
|
@@ -75,21 +74,19 @@ export function resolveOperator(config) {
|
|
|
75
74
|
}
|
|
76
75
|
|
|
77
76
|
/**
|
|
78
|
-
* Acquire (or re-affirm
|
|
77
|
+
* Acquire (or re-affirm) the Story lease for the standalone path.
|
|
79
78
|
*
|
|
80
|
-
* **Fail-closed:**
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
* transfer it. Unclaimed and self-held tickets proceed without a write.
|
|
79
|
+
* **Fail-closed:** a foreign assignee refuses the take — the guard throws
|
|
80
|
+
* naming the current owner so the operator can coordinate. Pass
|
|
81
|
+
* `steal: true` (`--steal`) to forcibly transfer it. Unclaimed and self-held
|
|
82
|
+
* tickets proceed without a write.
|
|
85
83
|
*
|
|
86
84
|
* @param {object} opts
|
|
87
85
|
* @param {object} opts.provider Ticketing provider (getTicket/updateTicket).
|
|
88
86
|
* @param {number} opts.storyId Story ticket to claim.
|
|
89
|
-
* @param {object} opts.config Resolved config (operator handle
|
|
87
|
+
* @param {object} opts.config Resolved config (operator handle).
|
|
90
88
|
* @param {string} [opts.operator] Override the resolved operator (tests).
|
|
91
89
|
* @param {boolean} [opts.steal=false] Forcibly transfer a foreign claim.
|
|
92
|
-
* @param {number} [opts.now] Injectable clock (epoch ms) for tests.
|
|
93
90
|
* @returns {Promise<{ acquired: boolean, owner: string, previousOwner: string|null, reason: string }>}
|
|
94
91
|
* @throws {Error} When a foreign claim refuses the acquire (no `steal`).
|
|
95
92
|
*/
|
|
@@ -99,27 +96,19 @@ export async function acquireStoryLease({
|
|
|
99
96
|
config,
|
|
100
97
|
operator,
|
|
101
98
|
steal = false,
|
|
102
|
-
now,
|
|
103
99
|
}) {
|
|
104
100
|
const owner = operator ?? resolveOperator(config);
|
|
105
|
-
// Fail closed: with no live-heartbeat source on the standalone path, the
|
|
106
|
-
// shared kernel anchors `heartbeatAt` to the same `now` the primitive
|
|
107
|
-
// evaluates against, so `isClaimLive` returns true for any foreign owner
|
|
108
|
-
// and `acquireLease` refuses unless `steal` is set.
|
|
109
101
|
return acquireLeaseFailClosed({
|
|
110
102
|
provider,
|
|
111
103
|
ticketId: storyId,
|
|
112
104
|
operator: owner,
|
|
113
105
|
steal,
|
|
114
|
-
config,
|
|
115
|
-
now,
|
|
116
|
-
anchorHeartbeatToNow: true,
|
|
117
106
|
renderRefusal: (result) =>
|
|
118
107
|
`single-story lease: Story #${storyId} is currently held by @${result.owner}. ` +
|
|
119
108
|
'Another /deliver run owns this Story. Coordinate with that ' +
|
|
120
109
|
'operator, or re-run with --steal to forcibly transfer the claim once you ' +
|
|
121
|
-
'have confirmed the other run is dead. (
|
|
122
|
-
'
|
|
110
|
+
'have confirmed the other run is dead. (A foreign assignee always ' +
|
|
111
|
+
'blocks unless stolen.)',
|
|
123
112
|
});
|
|
124
113
|
}
|
|
125
114
|
|
|
@@ -142,5 +131,5 @@ export async function releaseStoryLease({
|
|
|
142
131
|
operator,
|
|
143
132
|
}) {
|
|
144
133
|
const owner = operator ?? resolveOperator(config);
|
|
145
|
-
return releaseLease({ provider, ticketId: storyId, operator: owner
|
|
134
|
+
return releaseLease({ provider, ticketId: storyId, operator: owner });
|
|
146
135
|
}
|
|
@@ -6,13 +6,27 @@
|
|
|
6
6
|
* Spec is treated as a sizing smell (the Story should be split or the Spec
|
|
7
7
|
* tightened), not as a reason to write temporary product docs.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
* chars/token)
|
|
9
|
+
* This module also **owns** the §2 FinOps token estimator
|
|
10
|
+
* ({@link estimateTokens}, ~4 chars/token) — the one shared approximation every
|
|
11
|
+
* fixed framework ceiling speaks in (the Spec budget below, the plan-time
|
|
12
|
+
* sizing ceilings in `ticket-validator-sizing.js`, and the audit checklist
|
|
13
|
+
* threading budget). It used to live in a `context-envelope.js` SDK whose
|
|
14
|
+
* remaining surface had no live caller; the estimator is all that survived.
|
|
11
15
|
*
|
|
12
16
|
* @module lib/orchestration/spec-spill
|
|
13
17
|
*/
|
|
14
18
|
|
|
15
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Rough token estimate: ~4 characters per token. Deliberately cheap and
|
|
21
|
+
* deterministic — every ceiling that quotes "tokens" is quoting this number,
|
|
22
|
+
* so the approximation matters far less than every caller sharing one.
|
|
23
|
+
*
|
|
24
|
+
* @param {string} text
|
|
25
|
+
* @returns {number}
|
|
26
|
+
*/
|
|
27
|
+
export function estimateTokens(text) {
|
|
28
|
+
return Math.ceil(String(text ?? '').length / 4);
|
|
29
|
+
}
|
|
16
30
|
|
|
17
31
|
/**
|
|
18
32
|
* Soft budget (estimated tokens) for an inline `## Spec`. ~1500 tokens ≈ 6KB —
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* phases/review-core.js — the shared Story-scope review spine.
|
|
3
3
|
*
|
|
4
|
-
* Extracted from `phases/code-review.js` (Story #4603)
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* v2 path
|
|
8
|
-
*
|
|
9
|
-
*
|
|
4
|
+
* Extracted from the Epic-era `phases/code-review.js` (Story #4603) so the
|
|
5
|
+
* spine belonged to neither close path. `runStoryReviewCore` is the one
|
|
6
|
+
* implementation `runCodeReview` is called through; since Story #5006 retired
|
|
7
|
+
* the Epic-attached phase, its sole caller is the v2 standalone path
|
|
8
|
+
* (`single-story-close/phases/code-review.js#runStoryScopeReview`). It stays
|
|
9
|
+
* here — the shared-spine contract (Story #3653) is what keeps the maker-blind
|
|
10
|
+
* review invocation out of a phase file.
|
|
10
11
|
*/
|
|
11
12
|
|
|
12
13
|
import { countChangedLines } from '../../../audit-suite/index.js';
|
|
@@ -425,7 +425,8 @@ export function persistTerminalEnvelope(
|
|
|
425
425
|
* machine-readable contract — every invocation emits exactly ONE, and a
|
|
426
426
|
* headless caller parses it out of stdout to decide what happened.
|
|
427
427
|
* `Logger.info` is level-gated, so under the documented
|
|
428
|
-
* `AGENT_LOG_LEVEL=silent` (§
|
|
428
|
+
* `AGENT_LOG_LEVEL=silent` (execution-reference § Log-level control) the
|
|
429
|
+
* envelope silently vanished and the
|
|
429
430
|
* caller got a bare exit code: precisely the "no envelope at all" outcome
|
|
430
431
|
* Story #4543 exists to remove. A contract payload must not be suppressible
|
|
431
432
|
* by a verbosity knob.
|
|
@@ -45,7 +45,10 @@
|
|
|
45
45
|
* `body.verify` entries must either name a testing tier in parentheses
|
|
46
46
|
* drawn from `VERIFY_TIER_VALUES` (e.g. `npm run test (unit)`) or be the
|
|
47
47
|
* literal `manual:<reason>` escape hatch when the Story is genuinely
|
|
48
|
-
* unverifiable in isolation.
|
|
48
|
+
* unverifiable in isolation. `verify-tier-repair.js#normalizeVerifyTiers` runs
|
|
49
|
+
* first on the persist path (Story #5005) and **repairs** the entries whose
|
|
50
|
+
* tier `suggestVerifyFix` can infer, so the hard error below is reserved for
|
|
51
|
+
* the entries only the author can resolve.
|
|
49
52
|
*
|
|
50
53
|
* The errors are batched and surfaced as a single thrown Error so the
|
|
51
54
|
* planner can see every offending slug in one pass instead of fixing one
|