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
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `lib/signals/write.js` — thin re-export of the signals
|
|
2
|
+
* `lib/signals/write.js` — thin re-export of the signals writer.
|
|
3
3
|
*
|
|
4
4
|
* The real implementation lives at `../observability/signals-writer.js`
|
|
5
5
|
* (Epic #1030 / Story #1041). This module exists so callers can converge
|
|
6
|
-
* on `lib/signals/` for
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* from
|
|
10
|
-
*
|
|
11
|
-
*
|
|
6
|
+
* on `lib/signals/` for the write surface. New code should
|
|
7
|
+
* `import { appendSignal } from './lib/signals/index.js'` (or
|
|
8
|
+
* `'./lib/signals/write.js'` for a narrower import); legacy direct imports
|
|
9
|
+
* from `lib/observability/` keep working unchanged.
|
|
10
|
+
*
|
|
11
|
+
* `appendTrace` was removed in Story #5003 together with the tool-trace
|
|
12
|
+
* hook — its only producer — and the viewer that was its only reader.
|
|
12
13
|
*/
|
|
13
14
|
|
|
14
15
|
export {
|
|
15
16
|
appendSignal,
|
|
16
|
-
appendTrace,
|
|
17
17
|
forEachLine,
|
|
18
18
|
} from '../observability/signals-writer.js';
|
|
@@ -21,7 +21,6 @@
|
|
|
21
21
|
* wide: { reason } | null,// declared-wide footprint (optional)
|
|
22
22
|
* reason_to_exist: string | null, // one-sentence cohesion reason (optional)
|
|
23
23
|
* depends_on: string[], // blocker story slugs or #ids
|
|
24
|
-
* estimated_test_files: number | null, // absent → null (informational)
|
|
25
24
|
* }
|
|
26
25
|
* ```
|
|
27
26
|
*
|
|
@@ -79,7 +78,6 @@ import { suggestPathEntryFix } from './body-format-lints.js';
|
|
|
79
78
|
* @property {{ reason: string }|null} wide - Declared-wide footprint (reason), or null.
|
|
80
79
|
* @property {string|null} reason_to_exist - One-sentence cohesion reason ("why this Story exists"), or null.
|
|
81
80
|
* @property {string[]} depends_on - Blocking story slugs / issue refs.
|
|
82
|
-
* @property {number|null} estimated_test_files - Test surface count or null.
|
|
83
81
|
* @property {string|null} mandrel_version - Framework version stamped at authoring, or null.
|
|
84
82
|
* @property {string|null} authored_at - Authoring date (YYYY-MM-DD) stamped at authoring, or null.
|
|
85
83
|
*/
|
|
@@ -334,7 +332,7 @@ const META_BLOCK_RE = /<!--\s*meta:\s*([\s\S]*?)\s*-->/;
|
|
|
334
332
|
const META_OBJECT_RE = /<!--\s*meta:\s*(\{[\s\S]*?\})\s*-->/;
|
|
335
333
|
|
|
336
334
|
/**
|
|
337
|
-
* Extract the `wide` / `
|
|
335
|
+
* Extract the `wide` / `reason_to_exist` fields from the trailing
|
|
338
336
|
* `<!-- meta: {...} -->` comment block written by {@link serialize}. Returns
|
|
339
337
|
* canonical-shaped values (null when absent or malformed) so the parser
|
|
340
338
|
* round-trips the meta block faithfully.
|
|
@@ -350,13 +348,12 @@ const META_OBJECT_RE = /<!--\s*meta:\s*(\{[\s\S]*?\})\s*-->/;
|
|
|
350
348
|
* rather than dropping or re-deriving it.
|
|
351
349
|
*
|
|
352
350
|
* @param {string} markdown
|
|
353
|
-
* @returns {{ wide: { reason: string }|null, reason_to_exist: string|null,
|
|
351
|
+
* @returns {{ wide: { reason: string }|null, reason_to_exist: string|null, mandrel_version: string|null, authored_at: string|null }}
|
|
354
352
|
*/
|
|
355
353
|
function extractMeta(markdown) {
|
|
356
354
|
const result = {
|
|
357
355
|
wide: null,
|
|
358
356
|
reason_to_exist: null,
|
|
359
|
-
estimated_test_files: null,
|
|
360
357
|
mandrel_version: null,
|
|
361
358
|
authored_at: null,
|
|
362
359
|
};
|
|
@@ -377,9 +374,6 @@ function extractMeta(markdown) {
|
|
|
377
374
|
|
|
378
375
|
result.wide = normalizeWide(parsed.wide);
|
|
379
376
|
result.reason_to_exist = normalizeReasonToExist(parsed.reason_to_exist);
|
|
380
|
-
if (typeof parsed.estimated_test_files === 'number') {
|
|
381
|
-
result.estimated_test_files = parsed.estimated_test_files;
|
|
382
|
-
}
|
|
383
377
|
if (
|
|
384
378
|
typeof parsed.mandrel_version === 'string' &&
|
|
385
379
|
parsed.mandrel_version.trim()
|
|
@@ -579,7 +573,6 @@ function isMachineMarkerLine(line) {
|
|
|
579
573
|
function parseUnstructuredBody(input, preamble, footer) {
|
|
580
574
|
const warnings = [
|
|
581
575
|
'unstructured-body: no structured sections found; returning minimal body from preamble text.',
|
|
582
|
-
'test-surface-unestimated: estimated_test_files not present.',
|
|
583
576
|
];
|
|
584
577
|
const body = {
|
|
585
578
|
goal: preamble || input.trim(),
|
|
@@ -593,7 +586,6 @@ function parseUnstructuredBody(input, preamble, footer) {
|
|
|
593
586
|
wide: null,
|
|
594
587
|
reason_to_exist: null,
|
|
595
588
|
depends_on: extractBlockedBy(footer),
|
|
596
|
-
estimated_test_files: null,
|
|
597
589
|
mandrel_version: null,
|
|
598
590
|
authored_at: null,
|
|
599
591
|
};
|
|
@@ -688,11 +680,6 @@ function parseTextListSection(lines) {
|
|
|
688
680
|
* `result.warnings` to detect legacy path entries that should be
|
|
689
681
|
* migrated.
|
|
690
682
|
*
|
|
691
|
-
* Informational finding emitted on `result.warnings`:
|
|
692
|
-
* - `test-surface-unestimated` — when `estimated_test_files` is absent
|
|
693
|
-
* from both a structured body and the markdown. Callers that care about
|
|
694
|
-
* test-surface coverage SHOULD surface this to the operator.
|
|
695
|
-
*
|
|
696
683
|
* @param {string|object} input - Markdown string or already-structured body object.
|
|
697
684
|
* @returns {ParseResult}
|
|
698
685
|
* @throws {StoryBodyParseError} When the body is structurally unrecoverable.
|
|
@@ -765,20 +752,16 @@ export function parse(input) {
|
|
|
765
752
|
const non_goals = parseTextListSection(sections.get('non_goals') ?? []);
|
|
766
753
|
const dependsOn = extractBlockedBy(footer);
|
|
767
754
|
|
|
768
|
-
// --- Recover wide /
|
|
755
|
+
// --- Recover wide / reason_to_exist / provenance from the meta block ---
|
|
769
756
|
// serialize() writes these into a trailing `<!-- meta: {...} -->` comment
|
|
770
757
|
// so round-trips preserve them. Absent meta block → canonical null defaults.
|
|
758
|
+
// Unknown keys are ignored, so a body carrying a retired meta field parses
|
|
759
|
+
// clean and simply drops it.
|
|
771
760
|
const meta = extractMeta(input);
|
|
772
|
-
const estimated_test_files = meta.estimated_test_files;
|
|
773
761
|
const wide = meta.wide;
|
|
774
762
|
const reason_to_exist = meta.reason_to_exist;
|
|
775
763
|
const mandrel_version = meta.mandrel_version;
|
|
776
764
|
const authored_at = meta.authored_at;
|
|
777
|
-
if (estimated_test_files === null) {
|
|
778
|
-
warnings.push(
|
|
779
|
-
'test-surface-unestimated: estimated_test_files not present.',
|
|
780
|
-
);
|
|
781
|
-
}
|
|
782
765
|
|
|
783
766
|
const body = {
|
|
784
767
|
goal,
|
|
@@ -792,7 +775,6 @@ export function parse(input) {
|
|
|
792
775
|
wide,
|
|
793
776
|
reason_to_exist,
|
|
794
777
|
depends_on: dependsOn,
|
|
795
|
-
estimated_test_files,
|
|
796
778
|
mandrel_version,
|
|
797
779
|
authored_at,
|
|
798
780
|
};
|
|
@@ -834,13 +816,6 @@ function parseStructuredObject(obj) {
|
|
|
834
816
|
body[name] = STRUCTURED_FIELD_NORMALIZERS[kind](obj[name], warnings);
|
|
835
817
|
}
|
|
836
818
|
|
|
837
|
-
// estimated_test_files is the one warning-coupled scalar: absent (== null)
|
|
838
|
-
// warns; a non-number, non-null value stays null silently.
|
|
839
|
-
body.estimated_test_files = normalizeEstimatedTestFiles(
|
|
840
|
-
obj.estimated_test_files,
|
|
841
|
-
warnings,
|
|
842
|
-
);
|
|
843
|
-
|
|
844
819
|
// Provenance stamp (preserved verbatim; never re-derived here).
|
|
845
820
|
body.mandrel_version = normalizeProvenanceString(obj.mandrel_version);
|
|
846
821
|
body.authored_at = normalizeProvenanceString(obj.authored_at);
|
|
@@ -917,25 +892,6 @@ const STRUCTURED_FIELD_NORMALIZERS = {
|
|
|
917
892
|
reasonToExist: (raw) => normalizeReasonToExist(raw),
|
|
918
893
|
};
|
|
919
894
|
|
|
920
|
-
/**
|
|
921
|
-
* Normalize `estimated_test_files`: a number passes through; an absent
|
|
922
|
-
* (`null`/`undefined`) value warns `test-surface-unestimated`; any other
|
|
923
|
-
* value stays `null` silently.
|
|
924
|
-
*
|
|
925
|
-
* @param {unknown} raw
|
|
926
|
-
* @param {string[]} warnings
|
|
927
|
-
* @returns {number|null}
|
|
928
|
-
*/
|
|
929
|
-
function normalizeEstimatedTestFiles(raw, warnings) {
|
|
930
|
-
if (typeof raw === 'number') return raw;
|
|
931
|
-
if (raw == null) {
|
|
932
|
-
warnings.push(
|
|
933
|
-
'test-surface-unestimated: estimated_test_files not present.',
|
|
934
|
-
);
|
|
935
|
-
}
|
|
936
|
-
return null;
|
|
937
|
-
}
|
|
938
|
-
|
|
939
895
|
/**
|
|
940
896
|
* Normalize a provenance stamp field (`mandrel_version` / `authored_at`) to
|
|
941
897
|
* a non-empty trimmed string or `null`.
|
|
@@ -1066,11 +1022,11 @@ const SERIALIZE_SECTIONS = [
|
|
|
1066
1022
|
|
|
1067
1023
|
/**
|
|
1068
1024
|
* Build the trailing `<!-- meta: {...} -->` block carrying the fields that
|
|
1069
|
-
* have no human-readable section (`wide`, `reason_to_exist
|
|
1070
|
-
*
|
|
1071
|
-
*
|
|
1025
|
+
* have no human-readable section (`wide`, `reason_to_exist`). Returns the
|
|
1026
|
+
* empty string when no meta field is present so {@link serialize} appends
|
|
1027
|
+
* nothing.
|
|
1072
1028
|
*
|
|
1073
|
-
* Key insertion order (`wide` → `reason_to_exist` →
|
|
1029
|
+
* Key insertion order (`wide` → `reason_to_exist` →
|
|
1074
1030
|
* `mandrel_version` → `authored_at`) is load-bearing: it fixes the serialized
|
|
1075
1031
|
* JSON byte sequence the parser's meta round-trip and the unit suite assert
|
|
1076
1032
|
* against. The provenance stamp keys are appended **last** so every
|
|
@@ -1089,9 +1045,6 @@ function serializeMetaBlock(body) {
|
|
|
1089
1045
|
if (reasonToExist !== null) {
|
|
1090
1046
|
metaFields.reason_to_exist = reasonToExist;
|
|
1091
1047
|
}
|
|
1092
|
-
if (typeof body.estimated_test_files === 'number') {
|
|
1093
|
-
metaFields.estimated_test_files = body.estimated_test_files;
|
|
1094
|
-
}
|
|
1095
1048
|
if (typeof body.mandrel_version === 'string' && body.mandrel_version.trim()) {
|
|
1096
1049
|
metaFields.mandrel_version = body.mandrel_version.trim();
|
|
1097
1050
|
}
|
|
@@ -1153,9 +1106,9 @@ function serializeFooter(body, opts) {
|
|
|
1153
1106
|
* `## Goal`, `## Slicing`, `## Spec`, `## Changes`, `## Acceptance`,
|
|
1154
1107
|
* `## Verify`, `## References`, `## Non-Goals` (each omitted when empty).
|
|
1155
1108
|
*
|
|
1156
|
-
* `wide
|
|
1157
|
-
*
|
|
1158
|
-
*
|
|
1109
|
+
* `wide` and `reason_to_exist` are emitted as a fenced `<!-- meta -->`
|
|
1110
|
+
* comment block so round-trips preserve them without polluting the
|
|
1111
|
+
* human-readable body.
|
|
1159
1112
|
*
|
|
1160
1113
|
* @param {StoryBody} body
|
|
1161
1114
|
* @param {SerializeOptions} [opts]
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
* ## Keep-class
|
|
32
32
|
*
|
|
33
33
|
* `signals.ndjson` is the artifact whose value *starts* when the run ends —
|
|
34
|
-
* `
|
|
34
|
+
* `acceptance-eval` and the loop-health check both read it
|
|
35
35
|
* long after the Story merged. It is excluded twice over: it is not in the
|
|
36
36
|
* evidence basename allowlist, and {@link KEEP_BASENAMES} is re-checked at
|
|
37
37
|
* the deletion site. Defence in depth is warranted for the one file whose
|
|
@@ -109,7 +109,7 @@ You MUST respond ONLY with a valid JSON array of objects. No prose, no markdown
|
|
|
109
109
|
**Slug format**: \`^[a-z0-9][a-z0-9-]*$\` — hyphen-case only. Underscores are rejected by the validator.
|
|
110
110
|
|
|
111
111
|
### STORY BODY SCHEMA (REQUIRED FOR EVERY STORY):
|
|
112
|
-
\`body\` is either the serialized markdown **string** (the section format below) or a **structured object** carrying the same fields (\`goal\`, optional \`slicing\` / \`spec\`, \`changes\`, optional \`non_goals\` / \`wide\` / \`reason_to_exist\`
|
|
112
|
+
\`body\` is either the serialized markdown **string** (the section format below) or a **structured object** carrying the same fields (\`goal\`, optional \`slicing\` / \`spec\`, \`changes\`, optional \`non_goals\` / \`wide\` / \`reason_to_exist\`) — persist parses either shape and serializes the canonical markdown itself, so you never need to read \`story-body.js\` or hand-assemble the markdown (the \`stories.template.json\` file emitted next to the plan-context envelope is a ready-to-fill structured-object skeleton). Stories are consumed by non-interactive sub-agents that must self-verify from the Story ticket alone — so the ticket must carry everything an agent needs to execute and self-verify.
|
|
113
113
|
|
|
114
114
|
The \`acceptance[]\` and \`verify[]\` arrays live at the **top level** of the Story ticket object — that is the machine contract the validator reads. Author each list **once, at top level**, and **omit** the \`## Acceptance\` / \`## Verify\` sections from the authored \`body\` string: persist syncs the top-level arrays into those sections so the GitHub issue stays a complete executable document. The validator resolves both fields from the top level, so an omitted section is the expected shape, not a violation.
|
|
115
115
|
|
|
@@ -151,7 +151,6 @@ The **persisted** \`body\` renders these markdown sections (in order) — you au
|
|
|
151
151
|
- **acceptance** (top-level array on the ticket object): Items MUST be observable from outside the agent. Acceptable shapes: a specific command exits 0, a file exists at a given path, a snapshot test matches, a \`data-testid\` resolves under a given selector, a row count in a fixture matches. UNACCEPTABLE: "verify by reading the diff", "looks good", "matches the spec" — push these down into a \`verify\` command instead.
|
|
152
152
|
- **verify** (top-level array on the ticket object): Each entry MUST name a testing tier in parentheses, drawn from \`unit\` / \`contract\` / \`e2e\` / \`validate\`. Example: \`npm run test -- src/x.test.ts (unit)\`, \`npm run validate (validate)\`. Stories with zero verify entries SHOULD fail validation; if a story is genuinely unverifiable in isolation (e.g., a copy edit auditor will eyeball), the literal entry \`manual:<reason>\` is allowed so the absence is intentional, not lazy. Manual entries without a reason are rejected.
|
|
153
153
|
- **reason to exist** (REQUIRED, encoded as the \`reason_to_exist\` field of the \`<!-- meta: {...} -->\` comment appended to the serialized body string — NOT a top-level ticket field): One sentence stating the single coherent reason this Story exists, distinct from its broader \`## Goal\` prose. Every Story MUST carry a non-empty \`reason_to_exist\`; it is the machine-checkable form of the cohesion rule (**one Story = one coherent change with one reason to exist**) and the \`epic-plan-consolidate\` critic flags any Story whose body carries no non-empty reason to exist. Encode it as \`<!-- meta: {"reason_to_exist": "..."} -->\`.
|
|
154
|
-
- **estimated_test_files** (optional, encoded in the \`<!-- meta: {...} -->\` comment appended to the serialized body string — NOT a top-level ticket field): Integer estimate of how many test files this Story creates or modifies. Omit when the number is not estimable. Informational only — it does not gate the decompose.
|
|
155
154
|
- **Observed-behavior claims open with \`Current state (verified <date>)\`.** Any Spec claim about how the codebase behaves today MUST open with that preamble (e.g. \`Current state (verified 2026-07-17): …\`) so a reader can tell a verified observation from an assumption, and can tell when the observation went stale.
|
|
156
155
|
- **Intent-then-proxy acceptance shape.** When an acceptance item verifies through a proxy check (a grep, a file-exists probe, an exit-code test), state the intent clause before the proxy check — what outcome the check stands in for — so the proxy never becomes the goal (e.g. "the workflow names hygiene findings as re-author input: \`grep -n "textHygiene" …\` exits 0").
|
|
157
156
|
- **Slicing checkpoints are one line each.** Each \`## Slicing\` checkpoint is a single line naming the checkpoint; implementation detail lives in \`## Spec\`, never duplicated into Slicing. A Slicing section outweighing its Spec is a defect the text-hygiene lint flags.
|
|
@@ -171,7 +170,7 @@ The Story is executed by a frontier-model deliverer that reads the codebase itse
|
|
|
171
170
|
|
|
172
171
|
#### DETERMINISTIC BODY-FORMAT LINTS — author lint-clean by construction:
|
|
173
172
|
|
|
174
|
-
Persist enforces the deterministic body-format rules below and **rejects** an authored body that violates any of them. Author every Story to satisfy all of them on the FIRST draft — each rule is stated example-first so there is nothing to discover by trial-and-error.
|
|
173
|
+
Persist enforces the deterministic body-format rules below and **rejects** an authored body that violates any of them. Author every Story to satisfy all of them on the FIRST draft — each rule is stated example-first so there is nothing to discover by trial-and-error. \`verify-tier-suffix\` is the one rule persist repairs for you: when the tier is unambiguously inferable from the command, persist appends it and proceeds; when it is not, the entry is still rejected and you must choose the tier. \`changes-path-entry-shape\` emits the corrected form in the dry-run failure output but is never applied for you.
|
|
175
174
|
|
|
176
175
|
${bodyFormatLintChecklist}
|
|
177
176
|
|
|
@@ -231,35 +230,38 @@ Declaring \`wide\` with a non-empty reason **lifts the hard session-mass rejecti
|
|
|
231
230
|
|
|
232
231
|
#### UI / TESTID INVARIANCE (per CLAUDE.md safety rule):
|
|
233
232
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
233
|
+
Every \`changes[]\` entry is a \`{ path, assumption }\` object — a prose bullet there is rejected by the parser, so the testid contract is carried where prose belongs:
|
|
234
|
+
|
|
235
|
+
- Stories that touch UI (\`*.tsx\`, \`*.astro\`, \`*.svelte\`, \`*.vue\`, components folders) MUST carry the testid contract as a top-level \`acceptance[]\` item, one of:
|
|
236
|
+
- \`"data-testid invariance: <list of testids that MUST be preserved>"\`, or
|
|
237
|
+
- \`"data-testid changes: <old> -> <new>, with the matching tests/e2e/*.spec.ts selector updated"\` — paired with that \`tests/e2e/*.spec.ts\` file in \`changes[]\`, in the same Story or a depends_on Story.
|
|
238
|
+
- State the preserved-testid set in \`## Non-Goals\` prose as well when the Story deliberately renames nothing.
|
|
237
239
|
- Renaming a testid without the matching e2e edit is FORBIDDEN.
|
|
238
240
|
|
|
239
241
|
#### BRAND / COPY / STYLE WORK:
|
|
240
242
|
|
|
241
|
-
- Stories that touch user-visible copy, brand assets, or visual style MUST cite the relevant section of \`docs/style-guide.md\` in \`acceptance\` (e.g. \`"acceptance": ["Hero copy matches docs/style-guide.md §3 (voice & tone)"]\`). If \`docs/style-guide.md\` does not exist or has no relevant section, state that explicitly: \`"acceptance": ["docs/style-guide.md absent — copy reviewed against the inline brand brief in the
|
|
243
|
+
- Stories that touch user-visible copy, brand assets, or visual style MUST cite the relevant section of \`docs/style-guide.md\` in \`acceptance\` (e.g. \`"acceptance": ["Hero copy matches docs/style-guide.md §3 (voice & tone)"]\`). If \`docs/style-guide.md\` does not exist or has no relevant section, state that explicitly: \`"acceptance": ["docs/style-guide.md absent — copy reviewed against the inline brand brief in the plan seed"]\`. Silence on style sourcing is a smell.
|
|
242
244
|
|
|
243
|
-
### WAVE-0 BDD SCAFFOLD STORY (features-first; emit when
|
|
244
|
-
The
|
|
245
|
+
### WAVE-0 BDD SCAFFOLD STORY (features-first; emit when your plan verifies against a scenario that does not exist yet):
|
|
246
|
+
The plan-context envelope's \`bddScenarios\` field is the index of Gherkin scenarios that **already exist on \`main\`** — one row per scenario, carrying its \`.feature\` file path, line, scenario title and tags. It is the live signal for this rule: a \`.feature\` path + scenario your plan needs but that appears in no \`bddScenarios\` row does not exist yet. The framework is features-first: implementing Stories reference those \`.feature\` paths in their \`verify[]\` lines, so the files MUST already exist when those Stories run — otherwise verification fails mid-delivery on a missing file.
|
|
245
247
|
|
|
246
|
-
When
|
|
248
|
+
When **one or more** \`.feature\` scenarios your plan verifies against are absent from \`bddScenarios\`, you MUST emit **exactly one** dedicated wave-0 scaffold Story whose sole job is to create those \`.feature\` files with \`@skip\`-tagged scenarios BEFORE any implementation Story runs:
|
|
247
249
|
|
|
248
250
|
- **goal**: contains the literal token \`bdd-scaffold\` (e.g. "bdd-scaffold: create the @skip-tagged feature files the implementation Stories verify against").
|
|
249
251
|
- **depends_on**: EMPTY (\`[]\`) — it runs first, in wave 0.
|
|
250
|
-
- **changes**: one entry per distinct \`.feature\` file
|
|
252
|
+
- **changes**: one entry per distinct absent \`.feature\` file, each \`{ "path": "<feature file path>", "assumption": "creates" }\`.
|
|
251
253
|
- **acceptance**: MUST assert (a) every new \`.feature\` file exists AND (b) every new scenario within them carries an \`@skip\` tag. Keep these observable (a grep/validate command exits 0, a file exists at a path).
|
|
252
254
|
- **verify**: a grep/validate command (tier \`validate\`), NOT an e2e runner — verifying that a file exists with the required tags needs no browser/playwright run. Example: \`grep -rL '@skip' tests/features/<area>/*.feature (validate)\` paired with an existence check.
|
|
253
255
|
- Each implementation Story whose \`verify[]\` references one of these scaffolded \`.feature\` paths MUST \`depends_on\` the scaffold Story (so the scaffold lands in an earlier wave). Omitting the link trips the soft \`missing-bdd-scaffold\` validator finding.
|
|
254
256
|
|
|
255
|
-
When
|
|
257
|
+
When every scenario your plan verifies against is already present in \`bddScenarios\`, do NOT emit a scaffold Story — there is nothing to create.
|
|
256
258
|
|
|
257
259
|
### SCOPE-OVERLAP FLAGGING (docs/runbook downstream of config work):
|
|
258
|
-
When a "docs update" / "runbook" / "README" Story appears downstream of an earlier Story in the same
|
|
260
|
+
When a "docs update" / "runbook" / "README" Story appears downstream of an earlier Story in the same plan whose AC already covers updating the same document (e.g. a "config + runbook" Story followed by a "docs" Story touching the same runbook), the downstream Story's deliverable may be fully absorbed by the earlier Story. Flag the risk directly in the Story's top-level \`acceptance\` array by appending an item of the form:
|
|
259
261
|
"Scope verification note: this story's deliverable may already be satisfied by Story #<slug-or-id>'s AC — before implementing, \`git diff main -- <path>\` against the upstream Story branch and confirm whether a substantive edit is still required, or whether only a cross-reference remains."
|
|
260
262
|
This prevents the executing agent from redoing work the upstream Story already merged.
|
|
261
263
|
|
|
262
|
-
CRITICAL: Dependencies should follow execution blockers.
|
|
264
|
+
CRITICAL: Dependencies should follow execution blockers. There is no parent ticket — never emit a 'parent_slug' field.
|
|
263
265
|
IMPORTANT DEPENDENCY RULE: Story-to-Story dependencies are expressed via \`depends_on\` (one Story depends_on another Story's slug). Use this to express execution ordering across the plan.
|
|
264
266
|
|
|
265
267
|
### REVIEWABILITY BUDGET (Story #2798):
|
|
@@ -53,11 +53,10 @@ export const TICKET_BODY_SECTIONS = Object.freeze({
|
|
|
53
53
|
export const ACCEPTANCE_TABLE_HEADING = '## Acceptance Table';
|
|
54
54
|
|
|
55
55
|
/**
|
|
56
|
-
* Regex matching the Tech Spec's
|
|
57
|
-
* `
|
|
58
|
-
* source of truth for this pattern — `
|
|
59
|
-
*
|
|
60
|
-
* it rather than each carrying their own copy.
|
|
56
|
+
* Regex matching the Tech Spec's `## Delivery Slicing` heading, tolerating
|
|
57
|
+
* the `Delivery ` qualifier and casing variants. Exported so it is the single
|
|
58
|
+
* source of truth for this pattern — `consolidation-precondition.js` imports
|
|
59
|
+
* it rather than carrying its own copy.
|
|
61
60
|
*/
|
|
62
61
|
export const DELIVERY_SLICING_RE = /^##\s+(?:Delivery\s+)?Slicing\s*$/im;
|
|
63
62
|
|
|
@@ -3,23 +3,25 @@
|
|
|
3
3
|
*
|
|
4
4
|
* The "is the worktree's work integrated upstream?" half of
|
|
5
5
|
* `isSafeToRemove`. Runs the two-phase reachability gate the parent
|
|
6
|
-
* documents: primary `merge-base --is-ancestor HEAD
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* documents: primary `merge-base --is-ancestor HEAD baseRef`, and a
|
|
7
|
+
* `git cherry` patch-equivalence fallback when the ancestry check returns
|
|
8
|
+
* "not an ancestor".
|
|
9
9
|
*
|
|
10
10
|
* The fallback exists because a post-merge rebase or force-push can drop
|
|
11
|
-
* the local branch ref off the merged tip
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
11
|
+
* the local branch ref off the merged tip, so SHA reachability alone
|
|
12
|
+
* under-reports integration.
|
|
13
|
+
*
|
|
14
|
+
* A third phase used to sit between the two: a `git log --merges
|
|
15
|
+
* --grep=resolves #<storyId>` probe for the `(resolves #N)` token on the
|
|
16
|
+
* Epic's `--no-ff` merge commit. Story #5006 removed it — v2 lands every
|
|
17
|
+
* Story as a **squash** merge onto `main`, which never writes that token,
|
|
18
|
+
* and the `--no-ff` emitter (`story-close/merge-runner.js`) went with the
|
|
19
|
+
* Epic pipeline. The probe could only ever return `false`.
|
|
15
20
|
*
|
|
16
21
|
* Pure with respect to the supplied `ctx` bag; the only side effects are
|
|
17
22
|
* the `gitSpawn` calls.
|
|
18
23
|
*/
|
|
19
24
|
|
|
20
|
-
import { parseStoryBranch } from '../../git-utils.js';
|
|
21
|
-
import { resolvesGrepArgs } from '../../orchestration/resolves-token.js';
|
|
22
|
-
|
|
23
25
|
/**
|
|
24
26
|
* Resolve a worktree's `HEAD` to a full commit SHA via
|
|
25
27
|
* `git rev-parse HEAD` (run inside the worktree). Returns
|
|
@@ -70,37 +72,6 @@ export function checkHeadAncestor(ctx, headSha, epicRef) {
|
|
|
70
72
|
};
|
|
71
73
|
}
|
|
72
74
|
|
|
73
|
-
/**
|
|
74
|
-
* Predicate: did the Epic ref accumulate a `--no-ff` merge commit whose
|
|
75
|
-
* subject names this Story (e.g. `... (resolves #1851)`)? Returns `true`
|
|
76
|
-
* when the grep finds at least one matching merge commit, `false` when it
|
|
77
|
-
* returns empty or fails.
|
|
78
|
-
*
|
|
79
|
-
* Returns `false` for branches that do not match the canonical
|
|
80
|
-
* `story-<id>` shape — the merge-commit subject contract is only
|
|
81
|
-
* guaranteed for story branches.
|
|
82
|
-
*
|
|
83
|
-
* @param {object} ctx
|
|
84
|
-
* @param {string} branch Worktree branch (e.g. `story-1851`).
|
|
85
|
-
* @param {string} epicRef Epic branch ref (e.g. `epic/1831`).
|
|
86
|
-
* @returns {boolean}
|
|
87
|
-
*/
|
|
88
|
-
export function hasMergeCommitForStory(ctx, branch, epicRef) {
|
|
89
|
-
const storyId = parseStoryBranch(branch);
|
|
90
|
-
if (storyId === null) return false;
|
|
91
|
-
const grep = ctx.git.gitSpawn(
|
|
92
|
-
ctx.repoRoot,
|
|
93
|
-
'log',
|
|
94
|
-
epicRef,
|
|
95
|
-
'--merges',
|
|
96
|
-
'-n',
|
|
97
|
-
'1',
|
|
98
|
-
'--pretty=%H',
|
|
99
|
-
...resolvesGrepArgs(storyId),
|
|
100
|
-
);
|
|
101
|
-
return grep.status === 0 && grep.stdout.trim().length > 0;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
75
|
/**
|
|
105
76
|
* Predicate: are every commit on `branch` patch-equivalent to a commit
|
|
106
77
|
* already on `epicRef`? Runs `git cherry <epicRef> <branch>` and returns
|
|
@@ -140,7 +111,7 @@ export function hasRebasedEquivalents(ctx, branch, epicRef) {
|
|
|
140
111
|
}
|
|
141
112
|
|
|
142
113
|
/**
|
|
143
|
-
* Run the full
|
|
114
|
+
* Run the full two-phase merge-reachability gate. Returns the same
|
|
144
115
|
* `{ safe, reason }` envelope `isSafeToRemove` does, so callers can chain
|
|
145
116
|
* the verdict directly into the parent return value.
|
|
146
117
|
*
|
|
@@ -165,9 +136,6 @@ export async function checkMergeReachability(ctx, wtPath, branch, epicRef) {
|
|
|
165
136
|
};
|
|
166
137
|
}
|
|
167
138
|
|
|
168
|
-
if (hasMergeCommitForStory(ctx, branch, epicRef)) {
|
|
169
|
-
return { safe: true, reason: 'merge-commit-reachable' };
|
|
170
|
-
}
|
|
171
139
|
if (hasRebasedEquivalents(ctx, branch, epicRef)) {
|
|
172
140
|
return { safe: true, reason: 'rebased-equivalents' };
|
|
173
141
|
}
|
|
@@ -47,11 +47,10 @@ const WINDOWS_CWD_RE =
|
|
|
47
47
|
* (branch advanced to a SHA still reachable from the Epic merge commit).
|
|
48
48
|
*
|
|
49
49
|
* When HEAD is no longer an ancestor (force-push that drops or rewrites
|
|
50
|
-
* the merged tip), the function falls back to a `
|
|
51
|
-
* check:
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
* has diverged, so the worktree is still safe to reap.
|
|
50
|
+
* the merged tip), the function falls back to a `rebased-equivalents`
|
|
51
|
+
* check: `git cherry` proves every commit on the branch is already
|
|
52
|
+
* upstream by patch-id even though the current HEAD has diverged, so the
|
|
53
|
+
* worktree is still safe to reap.
|
|
55
54
|
*
|
|
56
55
|
* `opts.epicBranch` is the integration / base ref the Story must already
|
|
57
56
|
* be merged into (e.g. `main` or a plan-run branch).
|
|
@@ -54,9 +54,8 @@ export class WorktreeManager {
|
|
|
54
54
|
* @param {(phase: 'worktree-create'|'bootstrap'|'install') => void} [opts.onPhase]
|
|
55
55
|
* Optional phase-boundary callback fired from `ensure()` just before each
|
|
56
56
|
* internal phase (git worktree add, bootstrap-file copy, dependency
|
|
57
|
-
* install)
|
|
58
|
-
*
|
|
59
|
-
* sub-phases of worktree provisioning.
|
|
57
|
+
* install), so a caller can attribute wall-clock to the sub-phases of
|
|
58
|
+
* worktree provisioning.
|
|
60
59
|
*/
|
|
61
60
|
constructor({
|
|
62
61
|
repoRoot,
|
|
@@ -38,8 +38,8 @@
|
|
|
38
38
|
import { readdirSync, readFileSync } from 'node:fs';
|
|
39
39
|
import path from 'node:path';
|
|
40
40
|
import { fileURLToPath } from 'node:url';
|
|
41
|
-
|
|
42
41
|
import { runAsCli } from './lib/cli-utils.js';
|
|
42
|
+
import { walkFilesByExtension } from './lib/fs-walk.js';
|
|
43
43
|
|
|
44
44
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
45
45
|
const REPO_ROOT = path.resolve(__dirname, '..', '..');
|
|
@@ -71,28 +71,6 @@ export const KNOWN_AXES = Object.freeze([
|
|
|
71
71
|
'planning',
|
|
72
72
|
]);
|
|
73
73
|
|
|
74
|
-
/**
|
|
75
|
-
* Walk a directory tree synchronously, yielding absolute paths of files
|
|
76
|
-
* matching `.md`. Mirrors the walker shape in `check-lifecycle-lint.js`.
|
|
77
|
-
*/
|
|
78
|
-
function* walkMd(dir) {
|
|
79
|
-
let entries;
|
|
80
|
-
try {
|
|
81
|
-
entries = readdirSync(dir, { withFileTypes: true });
|
|
82
|
-
} catch (err) {
|
|
83
|
-
if (err.code === 'ENOENT') return;
|
|
84
|
-
throw err;
|
|
85
|
-
}
|
|
86
|
-
for (const entry of entries) {
|
|
87
|
-
const p = path.join(dir, entry.name);
|
|
88
|
-
if (entry.isDirectory()) {
|
|
89
|
-
yield* walkMd(p);
|
|
90
|
-
} else if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
91
|
-
yield p;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
74
|
/**
|
|
97
75
|
* Resolve a mixed list of file/directory paths into the flat set of
|
|
98
76
|
* markdown files to scan. Missing paths are silently skipped (the
|
|
@@ -116,7 +94,7 @@ function* resolveTargets(targets) {
|
|
|
116
94
|
for (const entry of stat) {
|
|
117
95
|
const p = path.join(target, entry.name);
|
|
118
96
|
if (entry.isDirectory()) {
|
|
119
|
-
yield*
|
|
97
|
+
yield* walkFilesByExtension(p, '.md');
|
|
120
98
|
} else if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
121
99
|
yield p;
|
|
122
100
|
}
|
|
@@ -6,10 +6,12 @@
|
|
|
6
6
|
*
|
|
7
7
|
* Polls the PR's required checks to a terminal state and auto-recovers
|
|
8
8
|
* from `mergeStateStatus: BEHIND` (via bounded `gh pr update-branch`
|
|
9
|
-
* calls) by delegating to the
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
9
|
+
* calls) by delegating to the `watchPrToTerminal` primitive in
|
|
10
|
+
* `lib/orchestration/pr-watch.js`. That primitive was shared with the
|
|
11
|
+
* `Watcher` bus listener until Story #5006 deleted it (nothing emitted at
|
|
12
|
+
* it); this CLI is now its only caller. Story #5024 retired the bus
|
|
13
|
+
* outright, so there is no bus to create — this is a direct, synchronous
|
|
14
|
+
* watch with a real exit code.
|
|
13
15
|
*
|
|
14
16
|
* Slow-vs-failed semantics (Story #4358):
|
|
15
17
|
* - GREEN — every required check terminal + green → exit 0, unless the
|
|
@@ -100,7 +102,7 @@ import {
|
|
|
100
102
|
retireCiDigest,
|
|
101
103
|
writeCiDigest,
|
|
102
104
|
} from './lib/orchestration/ci-rerun-guard.js';
|
|
103
|
-
import { watchPrToTerminal } from './lib/orchestration/
|
|
105
|
+
import { watchPrToTerminal } from './lib/orchestration/pr-watch.js';
|
|
104
106
|
import { enableAutoMergeWith } from './lib/orchestration/single-story-close/phases/auto-merge.js';
|
|
105
107
|
import { sleep as defaultSleep } from './lib/util/poll-loop.js';
|
|
106
108
|
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* single `GitHubProvider`, shared by dispatcher, reconciler, and cascade.
|
|
6
6
|
* The outer TTL wrapper is deliberately omitted because `peekFresh` already
|
|
7
7
|
* bounds entries by a caller-supplied `maxAgeMs`, and every other reader
|
|
8
|
-
* trusts the ticket mutators (`updateTicket` / `postComment`
|
|
9
|
-
* `
|
|
8
|
+
* trusts the ticket mutators (`updateTicket` / `postComment`) to call
|
|
9
|
+
* `invalidate` explicitly.
|
|
10
10
|
*
|
|
11
11
|
* Surface is intentionally narrow: only the methods the provider itself
|
|
12
12
|
* reaches for live here (`has` / `peek` / `peekFresh` / `set` /
|
|
@@ -7,20 +7,16 @@
|
|
|
7
7
|
* the upstream `upsertStructuredComment` ticketing helper before the body
|
|
8
8
|
* lands here.
|
|
9
9
|
*
|
|
10
|
-
* Extracted from `../github.js` in Story #2462 / Task #2480.
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* Extracted from `../github.js` in Story #2462 / Task #2480. `postComment`,
|
|
11
|
+
* `getTicketComments`, and `deleteComment` on `GitHubProvider` all delegate
|
|
12
|
+
* here. Story #5008 dropped the repo-wide `getRecentComments` feed — the
|
|
13
|
+
* telemetry dashboard that read it was retired and nothing else polls a
|
|
14
|
+
* cross-issue comment stream.
|
|
14
15
|
*
|
|
15
16
|
* @see Story #2462 — Split GitHubProvider god class into seven composed gateways.
|
|
16
17
|
*/
|
|
17
18
|
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
defaultRetryWarn,
|
|
21
|
-
paginateRest,
|
|
22
|
-
parseApiJson,
|
|
23
|
-
} from './request-helpers.js';
|
|
19
|
+
import { paginateRest, parseApiJson } from './request-helpers.js';
|
|
24
20
|
|
|
25
21
|
// Structured-comment badge — preserved verbatim from the legacy
|
|
26
22
|
// `./github/comments.js`. The upstream `upsertStructuredComment` ticketing
|
|
@@ -51,24 +47,6 @@ export class CommentGateway {
|
|
|
51
47
|
this._hooks = hooks;
|
|
52
48
|
}
|
|
53
49
|
|
|
54
|
-
/**
|
|
55
|
-
* Recent comments across all issues in the repo (sorted newest first).
|
|
56
|
-
*
|
|
57
|
-
* @field-manifest /repos/{owner}/{repo}/issues/comments?sort=created:
|
|
58
|
-
* id, body, created_at, user, issue_url
|
|
59
|
-
*/
|
|
60
|
-
async getRecentComments(limit = 100) {
|
|
61
|
-
const result = await withTransientRetry(
|
|
62
|
-
() =>
|
|
63
|
-
this._gh.api({
|
|
64
|
-
method: 'GET',
|
|
65
|
-
endpoint: `/repos/${this.owner}/${this.repo}/issues/comments?sort=created&direction=desc&per_page=${limit}`,
|
|
66
|
-
}),
|
|
67
|
-
{ label: 'getRecentComments', onRetry: defaultRetryWarn },
|
|
68
|
-
);
|
|
69
|
-
return parseApiJson(result) ?? [];
|
|
70
|
-
}
|
|
71
|
-
|
|
72
50
|
/**
|
|
73
51
|
* All comments on a single ticket. Used by the upstream
|
|
74
52
|
* `findStructuredComment` ticketing helper, which greps each comment
|
|
@@ -23,7 +23,6 @@ import { LabelGateway } from './labels.js';
|
|
|
23
23
|
import { MergeMethodsGateway } from './merge-methods.js';
|
|
24
24
|
import { ProjectBoardGateway } from './project-board.js';
|
|
25
25
|
import * as projects from './projects-v2-graphql.js';
|
|
26
|
-
import { PullRequestGateway } from './prs.js';
|
|
27
26
|
import { SubIssueGateway } from './sub-issues.js';
|
|
28
27
|
import { TicketGateway } from './tickets.js';
|
|
29
28
|
|
|
@@ -50,12 +49,6 @@ export function composeGateways(provider) {
|
|
|
50
49
|
ghGraphql: (q, v, o) => p.graphql(q, v, o),
|
|
51
50
|
cache: p._cache,
|
|
52
51
|
classifyGithubError,
|
|
53
|
-
hooks: {
|
|
54
|
-
getTicket: (id, o) => p.getTicket(id, o),
|
|
55
|
-
getTickets: (id) => p.getTickets(id),
|
|
56
|
-
primeTicketCache: (t) => p.primeTicketCache(t),
|
|
57
|
-
invalidateTicket: (id) => p.invalidateTicket(id),
|
|
58
|
-
},
|
|
59
52
|
});
|
|
60
53
|
p.comments = new CommentGateway({
|
|
61
54
|
...ghDeps,
|
|
@@ -64,14 +57,6 @@ export function composeGateways(provider) {
|
|
|
64
57
|
p.labels = new LabelGateway(ghDeps);
|
|
65
58
|
p.branchProtection = new BranchProtectionGateway(ghDeps);
|
|
66
59
|
p.mergeMethods = new MergeMethodsGateway(ghDeps);
|
|
67
|
-
p.pullRequests = new PullRequestGateway({
|
|
68
|
-
gh: p._gh,
|
|
69
|
-
hooks: {
|
|
70
|
-
getTicket: (id) => p.getTicket(id),
|
|
71
|
-
addItemToProject,
|
|
72
|
-
getProjectNumber,
|
|
73
|
-
},
|
|
74
|
-
});
|
|
75
60
|
p.issues = new IssuesGateway({
|
|
76
61
|
...ghDeps,
|
|
77
62
|
hooks: {
|