mandrel 2.31.0 → 2.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/README.md +13 -17
- package/.agents/agents/acceptance-critic.md +1 -2
- package/.agents/docs/SDLC.md +12 -9
- package/.agents/docs/agentrc-reference.json +63 -58
- package/.agents/docs/configuration.md +275 -227
- package/.agents/docs/execution-reference.md +13 -14
- package/.agents/docs/quality-gates.md +195 -23
- package/.agents/instructions.md +2 -5
- package/.agents/rules/git-conventions-reference.md +27 -27
- package/.agents/rules/git-conventions.md +4 -2
- package/.agents/rules/known-tooling-behavior.md +66 -30
- package/.agents/rules/testing-standards.md +35 -71
- package/.agents/runtime-deps.json +2 -2
- package/.agents/schemas/agentrc.schema.json +1955 -1410
- package/.agents/schemas/lifecycle/README.md +21 -14
- package/.agents/schemas/lifecycle/ledger-record.schema.json +76 -22
- package/.agents/schemas/story-deliver-terminal.schema.json +2 -2
- package/.agents/scripts/README.md +16 -29
- package/.agents/scripts/apply-quality-bootstrap.js +27 -34
- package/.agents/scripts/audit-to-stories.js +160 -41
- package/.agents/scripts/bootstrap.js +28 -26
- package/.agents/scripts/check-baseline-drift.js +73 -13
- package/.agents/scripts/check-baseline-scope.js +362 -0
- package/.agents/scripts/check-dead-exports.js +9 -1
- package/.agents/scripts/check-gherkin-corpus.js +508 -0
- package/.agents/scripts/check-knip-entries.js +159 -0
- package/.agents/scripts/check-lifecycle-lint.js +108 -124
- package/.agents/scripts/check-schema-references.js +1 -1
- package/.agents/scripts/diagnose-friction.js +7 -4
- package/.agents/scripts/generate-config-docs.js +263 -171
- package/.agents/scripts/install-matrix-assert.js +0 -1
- package/.agents/scripts/lib/ITicketingProvider.js +0 -58
- package/.agents/scripts/lib/audit-baselines/staleness.js +6 -6
- package/.agents/scripts/lib/audit-baselines/trend.js +7 -8
- package/.agents/scripts/lib/audit-baselines/weights.js +4 -5
- package/.agents/scripts/lib/audit-suite/checklist-threading.js +1 -1
- package/.agents/scripts/lib/audit-to-stories/build-story-body.js +81 -35
- package/.agents/scripts/lib/audit-to-stories/wire-dependencies.js +185 -0
- package/.agents/scripts/lib/baselines/envelope.js +41 -60
- package/.agents/scripts/lib/baselines/git-base.js +30 -37
- package/.agents/scripts/lib/baselines/kinds/_crap-new-method-gate.js +103 -0
- package/.agents/scripts/lib/baselines/kinds/_crap-read.js +150 -0
- package/.agents/scripts/lib/baselines/kinds/crap.js +25 -65
- package/.agents/scripts/lib/baselines/orphan-pruner.js +233 -0
- package/.agents/scripts/lib/baselines/refresh-service.js +6 -8
- package/.agents/scripts/lib/baselines/scope-assert.js +223 -0
- package/.agents/scripts/lib/baselines/scope-inventory.js +314 -0
- package/.agents/scripts/lib/bdd-step-index.js +326 -0
- package/.agents/scripts/lib/bootstrap/install-ledger.js +5 -3
- package/.agents/scripts/lib/bootstrap/issue-forms-template.js +4 -6
- package/.agents/scripts/lib/bootstrap/manifest.js +17 -40
- package/.agents/scripts/lib/bootstrap/project-bootstrap.js +12 -59
- package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +62 -2
- package/.agents/scripts/lib/checks/loop-health.js +9 -37
- package/.agents/scripts/lib/child-exec.js +193 -0
- package/.agents/scripts/lib/cli/standard-args.js +1 -1
- package/.agents/scripts/lib/cli-args.js +64 -0
- package/.agents/scripts/lib/close-validation/gates.js +2 -2
- package/.agents/scripts/lib/close-validation/runner.js +3 -3
- package/.agents/scripts/lib/config/acceptance-eval.js +5 -52
- package/.agents/scripts/lib/config/commands.js +3 -5
- package/.agents/scripts/lib/config/explain.js +5 -7
- package/.agents/scripts/lib/config/gates/bundle-size.schema.js +32 -6
- package/.agents/scripts/lib/config/gates/coverage.schema.js +25 -5
- package/.agents/scripts/lib/config/gates/crap-incremental-coverage.schema.js +12 -2
- package/.agents/scripts/lib/config/gates/crap.schema.js +68 -23
- package/.agents/scripts/lib/config/gates/duplication.schema.js +29 -17
- package/.agents/scripts/lib/config/gates/index.js +5 -2
- package/.agents/scripts/lib/config/gates/lighthouse.schema.js +34 -6
- package/.agents/scripts/lib/config/gates/lint.schema.js +11 -2
- package/.agents/scripts/lib/config/gates/maintainability.schema.js +37 -15
- package/.agents/scripts/lib/config/gates/mutation.schema.js +15 -3
- package/.agents/scripts/lib/config/gates/shared.js +58 -9
- package/.agents/scripts/lib/config/github.js +0 -1
- package/.agents/scripts/lib/config/limits.js +3 -48
- package/.agents/scripts/lib/config/qa.js +105 -0
- package/.agents/scripts/lib/config/runners.js +38 -16
- package/.agents/scripts/lib/config/temp-paths.js +6 -5
- package/.agents/scripts/lib/config-settings-schema-delivery.js +245 -56
- package/.agents/scripts/lib/config-settings-schema-quality.js +209 -29
- package/.agents/scripts/lib/config-settings-schema.js +386 -39
- package/.agents/scripts/lib/crap-baseline-join.js +126 -9
- package/.agents/scripts/lib/crap-utils.js +84 -520
- package/.agents/scripts/lib/dead-exports-knip.js +79 -10
- package/.agents/scripts/lib/degraded-mode.js +2 -2
- package/.agents/scripts/lib/dependency-parser.js +20 -7
- package/.agents/scripts/lib/doc-tiers.js +3 -3
- package/.agents/scripts/lib/feedback-loop/graduator-core.js +46 -104
- package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +10 -8
- package/.agents/scripts/lib/findings/provenance-field.js +135 -0
- package/.agents/scripts/lib/findings/route-finding.js +57 -8
- package/.agents/scripts/lib/fs-walk.js +52 -0
- package/.agents/scripts/lib/git-branch-lifecycle.js +2 -2
- package/.agents/scripts/lib/git-utils.js +16 -36
- package/.agents/scripts/lib/knip-config-resolver.js +181 -0
- package/.agents/scripts/lib/knip-entry-sync.js +508 -0
- package/.agents/scripts/lib/observability/metrics-ledger.js +1 -1
- package/.agents/scripts/lib/observability/runtime-friction.js +10 -0
- package/.agents/scripts/lib/observability/signal-validator.js +5 -85
- package/.agents/scripts/lib/observability/signals-writer.js +19 -62
- package/.agents/scripts/lib/observability/source-classifier.js +5 -7
- package/.agents/scripts/lib/observability/terse-result.js +3 -3
- package/.agents/scripts/lib/orchestration/behind-recovery.js +114 -0
- package/.agents/scripts/lib/orchestration/ceremony-routing.js +7 -8
- package/.agents/scripts/lib/orchestration/ci-rerun-guard.js +34 -33
- package/.agents/scripts/lib/orchestration/code-review.js +2 -2
- package/.agents/scripts/lib/orchestration/complexity-gate.js +43 -161
- package/.agents/scripts/lib/orchestration/diff-magnitude.js +4 -4
- package/.agents/scripts/lib/orchestration/label-transitions.js +3 -2
- package/.agents/scripts/lib/orchestration/lease-guard-shared.js +12 -38
- package/.agents/scripts/lib/orchestration/lifecycle/emit-merge-unlanded.js +5 -6
- package/.agents/scripts/lib/orchestration/plan-metrics.js +2 -3
- package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +7 -26
- package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +69 -5
- package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +69 -13
- package/.agents/scripts/lib/orchestration/plan-persist/summary.js +49 -0
- package/.agents/scripts/lib/orchestration/{lifecycle/listeners/watcher.js → pr-watch.js} +58 -208
- package/.agents/scripts/lib/orchestration/resolve-stories.js +77 -50
- package/.agents/scripts/lib/orchestration/review-providers/codex.js +1 -1
- package/.agents/scripts/lib/orchestration/review-providers/mi-exemptions.js +130 -0
- package/.agents/scripts/lib/orchestration/review-providers/native.js +30 -16
- package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +1 -1
- package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +37 -26
- package/.agents/scripts/lib/orchestration/single-story-close/phases/conventional-subject.js +376 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/normalize-pr-title.js +161 -151
- package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +15 -3
- package/.agents/scripts/lib/orchestration/single-story-close/phases/pull-request.js +10 -15
- package/.agents/scripts/lib/orchestration/single-story-close/phases/review-block.js +5 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/review-override.js +157 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/worktree-reap.js +0 -14
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +59 -25
- package/.agents/scripts/lib/orchestration/single-story-lease-guard.js +20 -31
- package/.agents/scripts/lib/orchestration/spec-spill.js +17 -3
- package/.agents/scripts/lib/orchestration/story-close/phases/review-core.js +7 -6
- package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +2 -1
- package/.agents/scripts/lib/orchestration/task-body-validator.js +4 -1
- package/.agents/scripts/lib/orchestration/ticket-lease.js +28 -127
- package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +116 -1
- package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +1 -1
- package/.agents/scripts/lib/orchestration/ticket-validator.js +38 -0
- package/.agents/scripts/lib/orchestration/ticketing/reads.js +5 -5
- package/.agents/scripts/lib/orchestration/ticketing/transition.js +5 -4
- package/.agents/scripts/lib/orchestration/verify-tier-repair.js +107 -0
- package/.agents/scripts/lib/qa/coverage-verdict.js +5 -87
- package/.agents/scripts/lib/signals/detectors/common.js +1 -1
- package/.agents/scripts/lib/signals/index.js +8 -6
- package/.agents/scripts/lib/signals/schema.js +20 -25
- package/.agents/scripts/lib/signals/write.js +8 -8
- package/.agents/scripts/lib/story-body/footer-block.js +97 -0
- package/.agents/scripts/lib/story-body/story-body.js +18 -81
- package/.agents/scripts/lib/temp-retention.js +1 -1
- package/.agents/scripts/lib/templates/decomposer-prompts.js +16 -14
- package/.agents/scripts/lib/ticket-body-sections.js +4 -5
- package/.agents/scripts/lib/wave-runner/footprint.js +306 -0
- package/.agents/scripts/lib/wave-runner/ready-set.js +198 -181
- package/.agents/scripts/lib/worktree/lifecycle/merge-reachability.js +13 -45
- package/.agents/scripts/lib/worktree/lifecycle/reap.js +4 -5
- package/.agents/scripts/lib/worktree-manager.js +2 -3
- package/.agents/scripts/lint-label-vocabulary.js +2 -24
- package/.agents/scripts/pr-watch-with-update.js +7 -5
- package/.agents/scripts/providers/github/blocked-by-add.js +25 -10
- package/.agents/scripts/providers/github/cache.js +2 -2
- package/.agents/scripts/providers/github/comments.js +6 -28
- package/.agents/scripts/providers/github/compose.js +0 -15
- package/.agents/scripts/providers/github/errors.js +10 -27
- package/.agents/scripts/providers/github/request-helpers.js +1 -2
- package/.agents/scripts/providers/github/sub-issues.js +10 -218
- package/.agents/scripts/providers/github.js +4 -7
- package/.agents/scripts/prune-baseline-orphans.js +181 -0
- package/.agents/scripts/resolve-stories.js +21 -7
- package/.agents/scripts/run-lint.js +61 -61
- package/.agents/scripts/run-test-profile.js +6 -6
- package/.agents/scripts/run-verify.js +48 -30
- package/.agents/scripts/single-story-close.js +20 -0
- package/.agents/scripts/single-story-init.js +12 -35
- package/.agents/scripts/stories-wave-tick.js +192 -9
- package/.agents/scripts/update-dead-exports-baseline.js +321 -0
- package/.agents/skills/core/gates-and-baselines/SKILL.md +2 -2
- package/.agents/skills/skills.index.json +1 -11
- package/.agents/workflows/audit-documentation.md +5 -6
- package/.agents/workflows/audit-to-stories.md +28 -2
- package/.agents/workflows/helpers/audit-lens-core.md +11 -12
- package/.agents/workflows/helpers/code-quality-guardrails.md +15 -14
- package/.agents/workflows/helpers/code-review.md +3 -8
- package/.agents/workflows/helpers/deliver-reference.md +30 -2
- package/.agents/workflows/helpers/deliver-story-reference.md +84 -16
- package/.agents/workflows/helpers/plan-reference.md +76 -0
- package/.agents/workflows/helpers/worktree-lifecycle.md +1 -2
- package/.agents/workflows/mandrel-update.md +10 -10
- package/.agents/workflows/qa-assist.md +15 -20
- package/.agents/workflows/qa-explore.md +9 -8
- package/README.md +1 -1
- package/docs/CHANGELOG.md +58 -0
- package/lib/migrations/index.js +2 -0
- package/lib/migrations/steps/2.32.0-retire-lint-baseline-command.js +127 -0
- package/package.json +14 -5
- package/.agents/schemas/lifecycle/checkpoint.written.schema.json +0 -13
- package/.agents/schemas/lifecycle/close-validate.end.schema.json +0 -18
- package/.agents/schemas/lifecycle/close-validate.start.schema.json +0 -13
- package/.agents/schemas/lifecycle/code-review.end.schema.json +0 -30
- package/.agents/schemas/lifecycle/code-review.start.schema.json +0 -12
- package/.agents/schemas/lifecycle/intervention.recorded.schema.json +0 -15
- package/.agents/schemas/lifecycle/loop.tick.schema.json +0 -20
- package/.agents/schemas/lifecycle/notification.emitted.schema.json +0 -18
- package/.agents/schemas/lifecycle/pr.created.schema.json +0 -14
- package/.agents/schemas/lifecycle/retro.end.schema.json +0 -16
- package/.agents/schemas/lifecycle/retro.start.schema.json +0 -12
- package/.agents/schemas/lifecycle/story.blocked.schema.json +0 -13
- package/.agents/schemas/lifecycle/story.dispatch.end.schema.json +0 -17
- package/.agents/schemas/lifecycle/story.dispatch.start.schema.json +0 -15
- package/.agents/schemas/lifecycle/story.merged.schema.json +0 -13
- package/.agents/scripts/check-gherkin-placeholders.js +0 -663
- package/.agents/scripts/check-lifecycle-doc-drift.js +0 -411
- package/.agents/scripts/lib/audit-suite/cli.js +0 -64
- package/.agents/scripts/lib/bootstrap/baselines-layout-migration.js +0 -202
- package/.agents/scripts/lib/bootstrap/ci-workflow-template.js +0 -212
- package/.agents/scripts/lib/checks/baseline-drift-main-checkout.js +0 -104
- package/.agents/scripts/lib/checks/push-hook-parity.js +0 -106
- package/.agents/scripts/lib/checks/windows-coverage-noise-floor.js +0 -92
- package/.agents/scripts/lib/checks/worktree-bootstrap-env.js +0 -81
- package/.agents/scripts/lib/checks/worktree-residue-biome.js +0 -55
- package/.agents/scripts/lib/crap-baseline-index.js +0 -46
- package/.agents/scripts/lib/crap-utils-incremental.js +0 -113
- package/.agents/scripts/lib/dynamic-workflow/capability.js +0 -396
- package/.agents/scripts/lib/feedback-loop/audit-results-graduator.js +0 -335
- package/.agents/scripts/lib/mutation/baseline-snapshot.js +0 -239
- package/.agents/scripts/lib/mutation/config-detector.js +0 -119
- package/.agents/scripts/lib/mutation/stryker-runner.js +0 -306
- package/.agents/scripts/lib/mutation/survivor-report.js +0 -160
- package/.agents/scripts/lib/observability/active-story-env.js +0 -170
- package/.agents/scripts/lib/observability/tool-trace-hook.js +0 -456
- package/.agents/scripts/lib/orchestration/acceptance-clusters.js +0 -111
- package/.agents/scripts/lib/orchestration/context-envelope.js +0 -277
- package/.agents/scripts/lib/orchestration/detectors-phase.js +0 -194
- package/.agents/scripts/lib/orchestration/lifecycle/bus.js +0 -309
- package/.agents/scripts/lib/orchestration/lifecycle/emit-loop-tick.js +0 -181
- package/.agents/scripts/lib/orchestration/lifecycle/ledger-writer.js +0 -229
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/README.md +0 -54
- package/.agents/scripts/lib/orchestration/lifecycle/trace-logger.js +0 -344
- package/.agents/scripts/lib/orchestration/lint-baseline-service.js +0 -114
- package/.agents/scripts/lib/orchestration/pr-base-guard.js +0 -37
- package/.agents/scripts/lib/orchestration/resolves-token.js +0 -127
- package/.agents/scripts/lib/orchestration/spec-section-validator.js +0 -130
- package/.agents/scripts/lib/orchestration/story-close/emit-blocked.js +0 -55
- package/.agents/scripts/lib/orchestration/story-close/phases/code-review.js +0 -211
- package/.agents/scripts/lib/planning-corpus.js +0 -37
- package/.agents/scripts/lib/qa/coverage-report.js +0 -181
- package/.agents/scripts/lib/qa/propose-missing-test.js +0 -95
- package/.agents/scripts/lib/qa/qa-context-hydrator.js +0 -217
- package/.agents/scripts/lib/signals/detectors/index.js +0 -14
- package/.agents/scripts/lib/signals/detectors/retry.js +0 -253
- package/.agents/scripts/lib/signals/detectors/rework.js +0 -167
- package/.agents/scripts/lib/signals/read.js +0 -268
- package/.agents/scripts/lib/signals/span-tree.js +0 -291
- package/.agents/scripts/lib/story-lifecycle.js +0 -194
- package/.agents/scripts/lib/story-plan.js +0 -379
- package/.agents/scripts/lib/util/phase-timer-state.js +0 -72
- package/.agents/scripts/lib/util/phase-timer.js +0 -163
- package/.agents/scripts/lib/workers/combined-mi-crap-worker.js +0 -169
- package/.agents/scripts/lint-baseline.js +0 -507
- package/.agents/scripts/providers/github/prs.js +0 -103
- package/.agents/scripts/signals-view.js +0 -309
- package/.agents/scripts/story-plan.js +0 -370
- package/.agents/scripts/sync-branch-from-base.js +0 -149
- package/.agents/scripts/validate-docs-freshness.js +0 -314
- package/.agents/skills/core/diagnose-friction/SKILL.md +0 -78
- package/.agents/workflows/helpers/signals.md +0 -112
|
@@ -61,6 +61,35 @@
|
|
|
61
61
|
|
|
62
62
|
import { AGENT_LABELS } from '../label-constants.js';
|
|
63
63
|
import { buildStoryAdjacency } from '../story-adjacency.js';
|
|
64
|
+
import { detectCollision } from './footprint.js';
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* How a footprint collision affects dispatch — the `footprintGuard` config
|
|
68
|
+
* knob (`delivery.deliverRunner.footprintGuard`, Story #5044).
|
|
69
|
+
*
|
|
70
|
+
* `enforce` is the default and encodes delivery-time-only knowledge the
|
|
71
|
+
* planner cannot have: which implementation windows are open right now, which
|
|
72
|
+
* Stories a foreign lease holds, how far the ground has moved since the plan
|
|
73
|
+
* was authored. It is never demoted automatically.
|
|
74
|
+
*
|
|
75
|
+
* `advisory` is an explicit operator trade for throughput on a run whose
|
|
76
|
+
* `depends_on` edges are known to be complete. Detection still runs; only the
|
|
77
|
+
* withholding stops.
|
|
78
|
+
*/
|
|
79
|
+
export const GUARD_MODES = Object.freeze({
|
|
80
|
+
ENFORCE: 'enforce',
|
|
81
|
+
ADVISORY: 'advisory',
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Which guard produced a withhold: a peer admitted **this beat**, or a Story
|
|
86
|
+
* still in flight from an **earlier** one. The two clear on different events,
|
|
87
|
+
* so an operator reading the report needs them apart.
|
|
88
|
+
*/
|
|
89
|
+
export const WITHHOLD_SCOPES = Object.freeze({
|
|
90
|
+
BEAT: 'beat',
|
|
91
|
+
IN_FLIGHT: 'in-flight',
|
|
92
|
+
});
|
|
64
93
|
|
|
65
94
|
/**
|
|
66
95
|
* @typedef {object} StoryRecord
|
|
@@ -135,128 +164,6 @@ export function classifyStory(story) {
|
|
|
135
164
|
return 'ready';
|
|
136
165
|
}
|
|
137
166
|
|
|
138
|
-
/**
|
|
139
|
-
* Extract a Story's declared file footprint as a normalized set of path
|
|
140
|
-
* strings. Accepts the three footprint shapes a Story record can carry:
|
|
141
|
-
*
|
|
142
|
-
* - `files: string[]` — explicit footprint.
|
|
143
|
-
* - `changes: string[]` — string-array sketch.
|
|
144
|
-
* - `changeset: Array<{ path }>` / — object-array sketch (the
|
|
145
|
-
* `changes: Array<{ path }>` `{ path, assumption }`
|
|
146
|
-
* shape from a Story body).
|
|
147
|
-
*
|
|
148
|
-
* Paths are trimmed; empty / non-string entries are dropped. A Story with
|
|
149
|
-
* no declared footprint yields an empty set, which (by `storiesOverlap`'s
|
|
150
|
-
* contract) means it overlaps with nothing and is never withheld by the
|
|
151
|
-
* co-dispatch guard.
|
|
152
|
-
*
|
|
153
|
-
* @param {StoryRecord} story
|
|
154
|
-
* @returns {Set<string>}
|
|
155
|
-
*/
|
|
156
|
-
export function storyFootprint(story) {
|
|
157
|
-
const out = new Set();
|
|
158
|
-
const push = (entry) => {
|
|
159
|
-
const path =
|
|
160
|
-
typeof entry === 'string'
|
|
161
|
-
? entry
|
|
162
|
-
: typeof entry?.path === 'string'
|
|
163
|
-
? entry.path
|
|
164
|
-
: null;
|
|
165
|
-
if (!path) return;
|
|
166
|
-
const trimmed = path.trim();
|
|
167
|
-
if (trimmed) out.add(trimmed);
|
|
168
|
-
};
|
|
169
|
-
if (Array.isArray(story?.files)) for (const e of story.files) push(e);
|
|
170
|
-
if (Array.isArray(story?.changes)) for (const e of story.changes) push(e);
|
|
171
|
-
if (Array.isArray(story?.changeset)) for (const e of story.changeset) push(e);
|
|
172
|
-
return out;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* Does a declared path contain a glob metacharacter? Mirrors the detection
|
|
177
|
-
* in `story-body.js#extractChangePaths`, whose `isGlob` flag documents an
|
|
178
|
-
* "unknown-width footprint" policy that was never implemented downstream.
|
|
179
|
-
*
|
|
180
|
-
* @param {string} path
|
|
181
|
-
* @returns {boolean}
|
|
182
|
-
*/
|
|
183
|
-
function isGlobPath(path) {
|
|
184
|
-
return path.includes('*') || path.includes('?') || path.includes('{');
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* Repo-relative file paths as they appear in Story prose: at least one `/`
|
|
189
|
-
* separator and a short file extension. Deliberately narrow — a token has to
|
|
190
|
-
* look like a real path before it can widen a footprint and withhold a Story.
|
|
191
|
-
*/
|
|
192
|
-
const PROSE_PATH_RE = /(?:[\w.@~-]+\/)+[\w.@-]+\.[A-Za-z0-9]{1,6}/g;
|
|
193
|
-
|
|
194
|
-
/**
|
|
195
|
-
* Scrape file paths a Story's **text** mentions but its `changes[]` never
|
|
196
|
-
* declared (Story #4875).
|
|
197
|
-
*
|
|
198
|
-
* The declared footprint is a planner's *prediction*, and it is systematically
|
|
199
|
-
* a lower bound: a Story's `## Spec` names the module it must also touch, its
|
|
200
|
-
* acceptance criteria name the caller that must be updated, and none of that
|
|
201
|
-
* reaches `changes[]`. The overlap guard exists to stop two Stories racing the
|
|
202
|
-
* same file, so trusting the declaration outright means the guard is blind to
|
|
203
|
-
* precisely the collisions nobody predicted.
|
|
204
|
-
*
|
|
205
|
-
* Evidence is only ever **added** — nothing here can shrink a declared
|
|
206
|
-
* footprint, so widening can withhold a Story for a beat but can never
|
|
207
|
-
* co-dispatch one the declared comparison would have caught.
|
|
208
|
-
*
|
|
209
|
-
* @param {StoryRecord} story
|
|
210
|
-
* @returns {Set<string>}
|
|
211
|
-
*/
|
|
212
|
-
function storyEvidencePaths(story) {
|
|
213
|
-
const out = new Set();
|
|
214
|
-
for (const field of [story?.title, story?.body, story?.spec]) {
|
|
215
|
-
if (typeof field !== 'string' || field === '') continue;
|
|
216
|
-
for (const match of field.matchAll(PROSE_PATH_RE)) {
|
|
217
|
-
const trimmed = match[0].trim();
|
|
218
|
-
if (trimmed) out.add(trimmed);
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
return out;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
/**
|
|
225
|
-
* A Story's footprint **widened from observable evidence** — the set the
|
|
226
|
-
* co-dispatch guard actually compares (Story #4875).
|
|
227
|
-
*
|
|
228
|
-
* `declared ∪ scraped-from-prose`. See {@link storyEvidencePaths} for why the
|
|
229
|
-
* declaration is treated as a lower bound rather than the answer.
|
|
230
|
-
*
|
|
231
|
-
* @param {StoryRecord} story
|
|
232
|
-
* @returns {Set<string>}
|
|
233
|
-
*/
|
|
234
|
-
function storyWidenedFootprint(story) {
|
|
235
|
-
const out = storyFootprint(story);
|
|
236
|
-
for (const path of storyEvidencePaths(story)) out.add(path);
|
|
237
|
-
return out;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
/**
|
|
241
|
-
* Both Stories' widened footprints, or `null` when either is empty.
|
|
242
|
-
*
|
|
243
|
-
* **An empty footprint means "no known overlap"**, so both guards below
|
|
244
|
-
* short-circuit to `false` on one. This is permissive by necessity: a Story
|
|
245
|
-
* with no declared footprint and no path evidence in its text carries no
|
|
246
|
-
* information, and withholding on absence would serialize every run.
|
|
247
|
-
*
|
|
248
|
-
* @param {StoryRecord} a
|
|
249
|
-
* @param {StoryRecord} b
|
|
250
|
-
* @returns {[Set<string>, Set<string>]|null}
|
|
251
|
-
*/
|
|
252
|
-
function widenedFootprintPair(a, b) {
|
|
253
|
-
const fa = storyWidenedFootprint(a);
|
|
254
|
-
if (fa.size === 0) return null;
|
|
255
|
-
const fb = storyWidenedFootprint(b);
|
|
256
|
-
if (fb.size === 0) return null;
|
|
257
|
-
return [fa, fb];
|
|
258
|
-
}
|
|
259
|
-
|
|
260
167
|
/**
|
|
261
168
|
* **Beat-local** file-overlap co-dispatch guard. Returns `true` when two
|
|
262
169
|
* Stories' file footprints intersect — meaning they would race the same file
|
|
@@ -282,17 +189,8 @@ function widenedFootprintPair(a, b) {
|
|
|
282
189
|
* @param {StoryRecord} b
|
|
283
190
|
* @returns {boolean}
|
|
284
191
|
*/
|
|
285
|
-
export function storiesOverlap(a, b) {
|
|
286
|
-
|
|
287
|
-
if (pair === null) return false;
|
|
288
|
-
const [fa, fb] = pair;
|
|
289
|
-
for (const path of fa) {
|
|
290
|
-
if (isGlobPath(path) || fb.has(path)) return true;
|
|
291
|
-
}
|
|
292
|
-
for (const path of fb) {
|
|
293
|
-
if (isGlobPath(path)) return true;
|
|
294
|
-
}
|
|
295
|
-
return false;
|
|
192
|
+
export function storiesOverlap(a, b, options = {}) {
|
|
193
|
+
return detectCollision(a, b, options) !== null;
|
|
296
194
|
}
|
|
297
195
|
|
|
298
196
|
/**
|
|
@@ -319,16 +217,11 @@ export function storiesOverlap(a, b) {
|
|
|
319
217
|
*
|
|
320
218
|
* @param {StoryRecord} held The in-flight Story holding the reservation.
|
|
321
219
|
* @param {StoryRecord} candidate The Story being considered for admission.
|
|
322
|
-
* @
|
|
220
|
+
* @param {object} [options] Evidence-scrape options (see {@link storyEvidencePaths}).
|
|
221
|
+
* @returns {{ paths: string[], source: string }|null}
|
|
323
222
|
*/
|
|
324
|
-
function reservesConcretePath(held, candidate) {
|
|
325
|
-
|
|
326
|
-
if (pair === null) return false;
|
|
327
|
-
const [fa, fb] = pair;
|
|
328
|
-
for (const path of fa) {
|
|
329
|
-
if (!isGlobPath(path) && fb.has(path)) return true;
|
|
330
|
-
}
|
|
331
|
-
return false;
|
|
223
|
+
function reservesConcretePath(held, candidate, options = {}) {
|
|
224
|
+
return detectCollision(held, candidate, { ...options, concreteOnly: true });
|
|
332
225
|
}
|
|
333
226
|
|
|
334
227
|
/**
|
|
@@ -396,11 +289,28 @@ function reservesConcretePath(held, candidate) {
|
|
|
396
289
|
* footprint reserves nothing (Story #4960). Callers that hold only ids (the
|
|
397
290
|
* `--dag`/`--in-flight` flag mode) pass nothing and get the pre-#4950
|
|
398
291
|
* same-beat-only behaviour.
|
|
399
|
-
* @
|
|
292
|
+
* @param {'enforce'|'advisory'} [args.footprintGuard='enforce'] Whether a
|
|
293
|
+
* footprint collision **withholds** a Story (`enforce`, the default and
|
|
294
|
+
* today's behaviour) or merely **reports** one while dispatch follows the
|
|
295
|
+
* declared `depends_on` edges alone (`advisory`). Advisory never changes what
|
|
296
|
+
* the guard *detects* — every would-be withhold is still computed and
|
|
297
|
+
* returned in `footprintWithholds` with `enforced: false` — so turning it on
|
|
298
|
+
* trades serialization for throughput without going blind (Story #5044).
|
|
299
|
+
* @param {string} [args.tempRoot] Resolved `project.paths.tempRoot`, threaded
|
|
300
|
+
* so the evidence scrape can ignore gitignored scratch paths.
|
|
301
|
+
* @returns {{
|
|
302
|
+
* selected: StoryRecord[],
|
|
303
|
+
* withheldByInFlight: Array<{id: number, blockedBy: number}>,
|
|
304
|
+
* footprintWithholds: Array<{id: number, blockedBy: number, scope: string, source: string, paths: string[], enforced: boolean}>,
|
|
305
|
+
* guardMode: 'enforce'|'advisory'
|
|
306
|
+
* }}
|
|
400
307
|
* `selected` is the dispatch set: a subset of `stories`, ascending by id,
|
|
401
308
|
* overlap-free, length ≤ `globalCap − inFlight`. `withheldByInFlight`
|
|
402
309
|
* names each eligible Story a reservation held back and the in-flight
|
|
403
|
-
* Story that holds it.
|
|
310
|
+
* Story that holds it. `footprintWithholds` is the **complete** ledger —
|
|
311
|
+
* beat-local skips as well as cross-beat reservations, each with the
|
|
312
|
+
* colliding paths and its `declared-overlap` / `scraped-overlap` source — so
|
|
313
|
+
* no withheld dispatch is unexplained (Story #5044).
|
|
404
314
|
*/
|
|
405
315
|
export function planReadySet({
|
|
406
316
|
stories,
|
|
@@ -409,14 +319,25 @@ export function planReadySet({
|
|
|
409
319
|
globalCap,
|
|
410
320
|
dropForeign = false,
|
|
411
321
|
inFlightRecords = [],
|
|
322
|
+
footprintGuard = GUARD_MODES.ENFORCE,
|
|
323
|
+
tempRoot,
|
|
412
324
|
} = {}) {
|
|
413
325
|
const records = Array.isArray(stories) ? stories : [];
|
|
326
|
+
const guardMode =
|
|
327
|
+
footprintGuard === GUARD_MODES.ADVISORY
|
|
328
|
+
? GUARD_MODES.ADVISORY
|
|
329
|
+
: GUARD_MODES.ENFORCE;
|
|
414
330
|
const cap = Number.isInteger(globalCap) ? globalCap : 0;
|
|
415
331
|
const inFlightCount =
|
|
416
332
|
Number.isInteger(inFlight) && inFlight > 0 ? inFlight : 0;
|
|
417
333
|
const slots = Math.max(0, cap - inFlightCount);
|
|
418
334
|
if (slots <= 0 || records.length === 0) {
|
|
419
|
-
return {
|
|
335
|
+
return {
|
|
336
|
+
selected: [],
|
|
337
|
+
withheldByInFlight: [],
|
|
338
|
+
footprintWithholds: [],
|
|
339
|
+
guardMode,
|
|
340
|
+
};
|
|
420
341
|
}
|
|
421
342
|
|
|
422
343
|
// Step 1 — adjacency keyed by id. The `dropForeign` policy decides whether
|
|
@@ -425,7 +346,47 @@ export function planReadySet({
|
|
|
425
346
|
// rationale.
|
|
426
347
|
const adjacency = buildStoryAdjacency(records, { dropForeign });
|
|
427
348
|
|
|
428
|
-
//
|
|
349
|
+
// Steps 2 + 3 — who is eligible at all, before any footprint reasoning.
|
|
350
|
+
const { eligibleIds, byId } = resolveEligibility({
|
|
351
|
+
records,
|
|
352
|
+
doneIds,
|
|
353
|
+
adjacency,
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
// Steps 4 + 5 — greedily admit up to `slots`, skipping file-overlap
|
|
357
|
+
// collisions against the already-admitted set AND against the footprints
|
|
358
|
+
// reserved by Stories still in flight from an earlier beat.
|
|
359
|
+
return admitStories({
|
|
360
|
+
eligibleIds,
|
|
361
|
+
byId,
|
|
362
|
+
slots,
|
|
363
|
+
reserved: Array.isArray(inFlightRecords) ? inFlightRecords : [],
|
|
364
|
+
guardMode,
|
|
365
|
+
evidence: { tempRoot },
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Resolve which Stories are eligible to dispatch on dependency grounds alone —
|
|
371
|
+
* `agent::ready` with every declared blocker done — plus the id→record index
|
|
372
|
+
* the admission loop reads.
|
|
373
|
+
*
|
|
374
|
+
* Separated from {@link planReadySet} because it answers a different question:
|
|
375
|
+
* this is the *declared graph* half of the decision (edges and lifecycle
|
|
376
|
+
* state), while everything after it reasons about footprints. Keeping the two
|
|
377
|
+
* apart is also what holds `planReadySet` under the cyclomatic ceiling ratchet.
|
|
378
|
+
*
|
|
379
|
+
* The done set is the union of two sources: ids the caller resolved from live
|
|
380
|
+
* state, and records in this batch that classify done — a Story can be both,
|
|
381
|
+
* and neither alone is complete.
|
|
382
|
+
*
|
|
383
|
+
* @param {object} args
|
|
384
|
+
* @param {StoryRecord[]} args.records
|
|
385
|
+
* @param {number[]|Set<number>} args.doneIds
|
|
386
|
+
* @param {Map<number, number[]>} args.adjacency
|
|
387
|
+
* @returns {{ eligibleIds: number[], byId: Map<number, StoryRecord> }}
|
|
388
|
+
*/
|
|
389
|
+
function resolveEligibility({ records, doneIds, adjacency }) {
|
|
429
390
|
const done = new Set();
|
|
430
391
|
for (const raw of doneIds instanceof Set ? doneIds : (doneIds ?? [])) {
|
|
431
392
|
const id = Number(raw);
|
|
@@ -439,25 +400,14 @@ export function planReadySet({
|
|
|
439
400
|
if (classifyStory(rec) === 'done') done.add(id);
|
|
440
401
|
}
|
|
441
402
|
|
|
442
|
-
//
|
|
443
|
-
// deterministic admission order.
|
|
403
|
+
// Ascending id for deterministic admission order.
|
|
444
404
|
const eligibleIds = [];
|
|
445
405
|
for (const id of [...byId.keys()].sort((a, b) => a - b)) {
|
|
446
|
-
|
|
447
|
-
if (classifyStory(rec) !== 'ready') continue;
|
|
406
|
+
if (classifyStory(byId.get(id)) !== 'ready') continue;
|
|
448
407
|
const deps = adjacency.get(id) ?? [];
|
|
449
408
|
if (deps.every((dep) => done.has(dep))) eligibleIds.push(id);
|
|
450
409
|
}
|
|
451
|
-
|
|
452
|
-
// Steps 4 + 5 — greedily admit up to `slots`, skipping file-overlap
|
|
453
|
-
// collisions against the already-admitted set AND against the footprints
|
|
454
|
-
// reserved by Stories still in flight from an earlier beat.
|
|
455
|
-
return admitStories({
|
|
456
|
-
eligibleIds,
|
|
457
|
-
byId,
|
|
458
|
-
slots,
|
|
459
|
-
reserved: Array.isArray(inFlightRecords) ? inFlightRecords : [],
|
|
460
|
-
});
|
|
410
|
+
return { eligibleIds, byId };
|
|
461
411
|
}
|
|
462
412
|
|
|
463
413
|
/**
|
|
@@ -469,37 +419,102 @@ export function planReadySet({
|
|
|
469
419
|
* ({@link reservesConcretePath}), while the same-beat guard also serializes
|
|
470
420
|
* unknown-width footprints ({@link storiesOverlap}). See both for why.
|
|
471
421
|
*
|
|
472
|
-
*
|
|
473
|
-
*
|
|
474
|
-
*
|
|
475
|
-
*
|
|
476
|
-
*
|
|
477
|
-
*
|
|
478
|
-
*
|
|
479
|
-
*
|
|
422
|
+
* Which of the two a candidate is reported against is {@link blockingCollision}'s
|
|
423
|
+
* decision, not this loop's.
|
|
424
|
+
*
|
|
425
|
+
* Under `footprintGuard: 'advisory'` neither rule withholds: dispatch follows
|
|
426
|
+
* the declared `depends_on` edges alone. Detection is unchanged — every hit is
|
|
427
|
+
* still computed and recorded with `enforced: false` — so advisory mode is a
|
|
428
|
+
* deliberate throughput trade an operator can read the cost of, not a blind
|
|
429
|
+
* spot (Story #5044).
|
|
480
430
|
*
|
|
481
431
|
* @param {object} args
|
|
482
432
|
* @param {number[]} args.eligibleIds Ascending eligible Story ids.
|
|
483
433
|
* @param {Map<number, StoryRecord>} args.byId
|
|
484
434
|
* @param {number} args.slots Remaining dispatch capacity.
|
|
485
435
|
* @param {StoryRecord[]} args.reserved In-flight Story records.
|
|
486
|
-
* @
|
|
436
|
+
* @param {'enforce'|'advisory'} args.guardMode
|
|
437
|
+
* @param {object} args.evidence Evidence-scrape options.
|
|
438
|
+
* @returns {{ selected: StoryRecord[], withheldByInFlight: Array<{id: number, blockedBy: number}>, footprintWithholds: object[], guardMode: string }}
|
|
487
439
|
*/
|
|
488
|
-
function admitStories({
|
|
440
|
+
function admitStories({
|
|
441
|
+
eligibleIds,
|
|
442
|
+
byId,
|
|
443
|
+
slots,
|
|
444
|
+
reserved,
|
|
445
|
+
guardMode,
|
|
446
|
+
evidence,
|
|
447
|
+
}) {
|
|
448
|
+
const enforced = guardMode !== GUARD_MODES.ADVISORY;
|
|
489
449
|
const selected = [];
|
|
490
|
-
const
|
|
450
|
+
const footprintWithholds = [];
|
|
491
451
|
for (const id of eligibleIds) {
|
|
492
452
|
if (selected.length >= slots) break;
|
|
493
453
|
const rec = byId.get(id);
|
|
494
|
-
const
|
|
495
|
-
if (
|
|
496
|
-
|
|
497
|
-
continue;
|
|
498
|
-
}
|
|
499
|
-
if (selected.some((picked) => storiesOverlap(picked, rec))) continue;
|
|
454
|
+
const hit = blockingCollision({ rec, id, selected, reserved, evidence });
|
|
455
|
+
if (hit) footprintWithholds.push({ id, ...hit, enforced });
|
|
456
|
+
if (hit && enforced) continue;
|
|
500
457
|
selected.push(rec);
|
|
501
458
|
}
|
|
502
|
-
return {
|
|
459
|
+
return {
|
|
460
|
+
selected,
|
|
461
|
+
// The legacy cross-beat projection, kept at its original `{ id, blockedBy }`
|
|
462
|
+
// shape: it is `stories-wave-tick.js`'s long-standing reservation input and
|
|
463
|
+
// narrowing the scrape must not reshape it.
|
|
464
|
+
withheldByInFlight: footprintWithholds
|
|
465
|
+
.filter((w) => w.enforced && w.scope === WITHHOLD_SCOPES.IN_FLIGHT)
|
|
466
|
+
.map(({ id, blockedBy }) => ({ id, blockedBy })),
|
|
467
|
+
footprintWithholds,
|
|
468
|
+
guardMode,
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* The one footprint collision withholding this candidate, or `null`.
|
|
474
|
+
*
|
|
475
|
+
* The in-flight reservation is checked **first**, so a Story racing both an
|
|
476
|
+
* in-flight Story and a same-beat peer is reported against the in-flight one:
|
|
477
|
+
* that is the longer-lived and more informative blocker (a Story that has been
|
|
478
|
+
* implementing for beats, not one merely admitted a moment ago), and checking it
|
|
479
|
+
* first is what makes the reservation report complete. Order cannot change
|
|
480
|
+
* `selected` — a candidate either rule rejects is skipped whichever runs first.
|
|
481
|
+
*
|
|
482
|
+
* @param {object} args
|
|
483
|
+
* @param {StoryRecord} args.rec
|
|
484
|
+
* @param {number} args.id
|
|
485
|
+
* @param {StoryRecord[]} args.selected Peers already admitted this beat.
|
|
486
|
+
* @param {StoryRecord[]} args.reserved In-flight Story records.
|
|
487
|
+
* @param {object} args.evidence
|
|
488
|
+
* @returns {{ blockedBy: number, scope: string, paths: string[], source: string }|null}
|
|
489
|
+
*/
|
|
490
|
+
function blockingCollision({ rec, id, selected, reserved, evidence }) {
|
|
491
|
+
const held = findInFlightBlocker(rec, id, reserved, evidence);
|
|
492
|
+
if (held) return { ...held, scope: WITHHOLD_SCOPES.IN_FLIGHT };
|
|
493
|
+
const peer = findBeatBlocker(rec, selected, evidence);
|
|
494
|
+
return peer ? { ...peer, scope: WITHHOLD_SCOPES.BEAT } : null;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* The **already-admitted peer** whose footprint this candidate would race on
|
|
499
|
+
* this beat, with the colliding paths — or `null` when none does.
|
|
500
|
+
*
|
|
501
|
+
* Until Story #5044 this was an anonymous `continue`: the candidate was
|
|
502
|
+
* silently dropped from the beat and nothing in any envelope said why. An
|
|
503
|
+
* unfilled slot with no explanation is indistinguishable from a cap that was
|
|
504
|
+
* simply not reached, which is what let a footprint-widening artifact
|
|
505
|
+
* serialize a whole audit-derived plan without leaving a trace to notice.
|
|
506
|
+
*
|
|
507
|
+
* @param {StoryRecord} candidate
|
|
508
|
+
* @param {StoryRecord[]} selected Stories already admitted this beat.
|
|
509
|
+
* @param {object} [options]
|
|
510
|
+
* @returns {{ blockedBy: number, paths: string[], source: string }|null}
|
|
511
|
+
*/
|
|
512
|
+
function findBeatBlocker(candidate, selected, options = {}) {
|
|
513
|
+
for (const picked of selected) {
|
|
514
|
+
const collision = detectCollision(picked, candidate, options);
|
|
515
|
+
if (collision) return { blockedBy: storyIdOf(picked), ...collision };
|
|
516
|
+
}
|
|
517
|
+
return null;
|
|
503
518
|
}
|
|
504
519
|
|
|
505
520
|
/**
|
|
@@ -520,13 +535,15 @@ function admitStories({ eligibleIds, byId, slots, reserved }) {
|
|
|
520
535
|
* @param {StoryRecord} candidate
|
|
521
536
|
* @param {number} candidateId
|
|
522
537
|
* @param {StoryRecord[]} reserved
|
|
523
|
-
* @
|
|
538
|
+
* @param {object} [options]
|
|
539
|
+
* @returns {{ blockedBy: number, paths: string[], source: string }|null}
|
|
524
540
|
*/
|
|
525
|
-
function findInFlightBlocker(candidate, candidateId, reserved) {
|
|
541
|
+
function findInFlightBlocker(candidate, candidateId, reserved, options = {}) {
|
|
526
542
|
for (const held of reserved) {
|
|
527
543
|
const heldId = storyIdOf(held);
|
|
528
544
|
if (heldId === null || heldId === candidateId) continue;
|
|
529
|
-
|
|
545
|
+
const collision = reservesConcretePath(held, candidate, options);
|
|
546
|
+
if (collision) return { blockedBy: heldId, ...collision };
|
|
530
547
|
}
|
|
531
548
|
return null;
|
|
532
549
|
}
|
|
@@ -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
|
|