mandrel 2.24.0 → 2.26.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/agents/acceptance-critic.md +19 -21
- package/.agents/agents/auditor.md +16 -19
- package/.agents/agents/story-worker.md +41 -54
- package/.agents/audit-checklists/architecture.md +5 -1
- package/.agents/audit-checklists/baselines.md +21 -0
- package/.agents/audit-checklists/quality.md +3 -0
- package/.agents/docs/agentrc-reference.json +2 -1
- package/.agents/docs/configuration.md +2 -1
- package/.agents/docs/quality-gates.md +80 -18
- package/.agents/docs/workflows.md +4 -2
- package/.agents/instructions.md +1 -1
- package/.agents/rules/ci-remediation.md +68 -3
- package/.agents/schemas/agentrc.schema.json +6 -1
- package/.agents/schemas/audit-rules.json +15 -0
- package/.agents/schemas/baselines/audit-baselines-envelope.schema.json +242 -0
- package/.agents/schemas/baselines/baseline-envelope.schema.json +8 -0
- package/.agents/schemas/baselines/crap.schema.json +13 -1
- package/.agents/schemas/crap-report.schema.json +37 -0
- package/.agents/schemas/model-attribution.schema.json +4 -0
- package/.agents/scripts/acceptance-eval.js +124 -15
- package/.agents/scripts/audit-baselines.js +136 -0
- package/.agents/scripts/audit-labels-bootstrap.js +16 -95
- package/.agents/scripts/audit-to-stories.js +33 -3
- package/.agents/scripts/check-arch-cycles.js +12 -93
- package/.agents/scripts/check-baseline-drift.js +16 -3
- package/.agents/scripts/check-baselines.js +19 -3
- package/.agents/scripts/check-context-budget.js +95 -14
- package/.agents/scripts/check-cyclomatic.js +214 -0
- package/.agents/scripts/check-schema-references.js +392 -0
- package/.agents/scripts/check-test-temp-hygiene.js +121 -1
- package/.agents/scripts/check-workflow-timeouts.js +291 -0
- package/.agents/scripts/cleanup-repo-test-temp.js +7 -5
- package/.agents/scripts/deliver-light.js +3 -4
- package/.agents/scripts/deliver-recover.js +13 -0
- package/.agents/scripts/diagnose-friction.js +85 -19
- package/.agents/scripts/lib/audit-baselines/engine.js +177 -0
- package/.agents/scripts/lib/audit-baselines/gate-surface.js +63 -0
- package/.agents/scripts/lib/audit-baselines/headroom.js +72 -0
- package/.agents/scripts/lib/audit-baselines/hotspots.js +69 -0
- package/.agents/scripts/lib/audit-baselines/kinds.js +313 -0
- package/.agents/scripts/lib/audit-baselines/outliers.js +100 -0
- package/.agents/scripts/lib/audit-baselines/read.js +87 -0
- package/.agents/scripts/lib/audit-baselines/staleness.js +123 -0
- package/.agents/scripts/lib/audit-baselines/surface-entry.js +106 -0
- package/.agents/scripts/lib/audit-baselines/trend.js +125 -0
- package/.agents/scripts/lib/audit-baselines/weights.js +193 -0
- package/.agents/scripts/lib/audit-suite/index.js +0 -5
- package/.agents/scripts/lib/audit-suite/selector.js +9 -62
- package/.agents/scripts/lib/audit-to-stories/audit-label-taxonomy.js +177 -0
- package/.agents/scripts/lib/audit-to-stories/audit-lenses.js +1 -0
- package/.agents/scripts/lib/audit-to-stories/build-story-body.js +35 -3
- package/.agents/scripts/lib/audit-to-stories/group-findings.js +20 -11
- package/.agents/scripts/lib/audit-to-stories/parse-audit-md.js +23 -9
- package/.agents/scripts/lib/audit-to-stories/seed-from-findings.js +9 -2
- package/.agents/scripts/lib/baseline-loader.js +0 -0
- package/.agents/scripts/lib/baseline-schema-registry.js +13 -1
- package/.agents/scripts/lib/baselines/diff-scope-cli.js +22 -160
- package/.agents/scripts/lib/baselines/duplication-scanner.js +27 -0
- package/.agents/scripts/lib/baselines/git-base.js +0 -0
- package/.agents/scripts/lib/baselines/kinds/crap.js +499 -57
- package/.agents/scripts/lib/baselines/preview-gates.js +58 -15
- package/.agents/scripts/lib/baselines/reader.js +10 -0
- package/.agents/scripts/lib/baselines/refresh-service.js +69 -11
- package/.agents/scripts/lib/baselines/scope.js +39 -90
- package/.agents/scripts/lib/baselines/writer.js +16 -7
- package/.agents/scripts/lib/changed-files.js +8 -1
- package/.agents/scripts/lib/cli-args.js +115 -1
- package/.agents/scripts/lib/close-validation/runner.js +70 -25
- package/.agents/scripts/lib/config-settings-schema-delivery.js +5 -0
- package/.agents/scripts/lib/crap-engine.js +124 -27
- package/.agents/scripts/lib/crap-method-identity.js +153 -0
- package/.agents/scripts/lib/crap-utils.js +86 -13
- package/.agents/scripts/lib/cyclomatic-ceiling.js +265 -0
- package/.agents/scripts/lib/env-loader.js +46 -16
- package/.agents/scripts/lib/feedback-loop/audit-results-graduator.js +0 -2
- package/.agents/scripts/lib/feedback-loop/prior-feedback-fetcher.js +0 -2
- package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +0 -2
- package/.agents/scripts/lib/findings/route-finding.js +123 -17
- package/.agents/scripts/lib/findings/severity.js +80 -2
- package/.agents/scripts/lib/git/cached-fetch.js +0 -0
- package/.agents/scripts/lib/git-utils.js +136 -80
- package/.agents/scripts/lib/import-graph.js +156 -0
- package/.agents/scripts/lib/label-constants.js +17 -0
- package/.agents/scripts/lib/observability/runtime-friction.js +17 -2
- package/.agents/scripts/lib/observability/source-classifier.js +175 -2
- package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +110 -17
- package/.agents/scripts/lib/orchestration/ceremony-routing.js +17 -12
- package/.agents/scripts/lib/orchestration/check-baselines/phases/compare.js +36 -6
- package/.agents/scripts/lib/orchestration/check-baselines/phases/evaluate.js +5 -0
- package/.agents/scripts/lib/orchestration/check-baselines/phases/floors.js +12 -1
- package/.agents/scripts/lib/orchestration/check-baselines/phases/report.js +8 -1
- package/.agents/scripts/lib/orchestration/ci-rerun-guard.js +548 -0
- package/.agents/scripts/lib/orchestration/deliver-recover.js +2 -2
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/phase-drivers.js +10 -5
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/render.js +39 -3
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/watcher.js +71 -20
- package/.agents/scripts/lib/orchestration/light-backstop.js +20 -3
- package/.agents/scripts/lib/orchestration/light-escalation.js +82 -3
- package/.agents/scripts/lib/orchestration/light-suitability.js +103 -5
- package/.agents/scripts/lib/orchestration/plan-context.js +119 -66
- package/.agents/scripts/lib/orchestration/plan-persist/fan-out-gate.js +31 -5
- package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +216 -109
- package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +61 -13
- package/.agents/scripts/lib/orchestration/plan-persist/supersede-ops.js +79 -22
- package/.agents/scripts/lib/orchestration/plan-text-hygiene.js +51 -20
- package/.agents/scripts/lib/orchestration/planning/authoring-context.js +70 -74
- package/.agents/scripts/lib/orchestration/planning/memory-pool-advisory.js +231 -0
- package/.agents/scripts/lib/orchestration/resolve-stories.js +18 -17
- package/.agents/scripts/lib/orchestration/retro-proposals.js +0 -0
- package/.agents/scripts/lib/orchestration/run-epilogue.js +12 -0
- package/.agents/scripts/lib/orchestration/run-scoped-config.js +276 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/base-sync.js +41 -10
- package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +125 -37
- package/.agents/scripts/lib/orchestration/single-story-close/phases/normalize-pr-title.js +6 -6
- package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +42 -38
- package/.agents/scripts/lib/orchestration/single-story-close/phases/pull-request.js +129 -21
- package/.agents/scripts/lib/orchestration/single-story-close/phases/push.js +6 -1
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +267 -125
- package/.agents/scripts/lib/orchestration/spec-budget.js +16 -5
- package/.agents/scripts/lib/orchestration/story-follow-ups.js +182 -95
- package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +22 -0
- package/.agents/scripts/lib/orchestration/ticket-validator.js +5 -11
- package/.agents/scripts/lib/orchestration/ticketing/reads.js +4 -4
- package/.agents/scripts/lib/reserved-test-ids.js +77 -0
- package/.agents/scripts/lib/story-adjacency.js +3 -3
- package/.agents/scripts/lib/story-plan.js +137 -42
- package/.agents/scripts/lib/templates/decomposer-prompts.js +4 -4
- package/.agents/scripts/lib/test-runner-contract.js +134 -0
- package/.agents/scripts/lib/test-tiers.js +11 -2
- package/.agents/scripts/lib/ticket-body-sections.js +1 -1
- package/.agents/scripts/lib/util/concurrent-map.js +17 -0
- package/.agents/scripts/lib/util/parse-id-list.js +103 -0
- package/.agents/scripts/lib/util/poll-loop.js +18 -2
- package/.agents/scripts/lib/wave-runner/live-probe.js +29 -13
- package/.agents/scripts/lib/wave-runner/ready-set.js +254 -43
- package/.agents/scripts/lib/workers/combined-mi-crap-worker.js +10 -13
- package/.agents/scripts/lib/workers/crap-worker.js +8 -13
- package/.agents/scripts/lib/workers/maintainability-report-worker.js +4 -10
- package/.agents/scripts/lib/workers/maintainability-worker.js +4 -10
- package/.agents/scripts/lib/workers/serve-worker-messages.js +35 -0
- package/.agents/scripts/lib/worktree/git-hooks.js +206 -0
- package/.agents/scripts/lib/worktree/lifecycle/creation.js +6 -0
- package/.agents/scripts/lib/worktree-manager.js +14 -0
- package/.agents/scripts/plan-run-epilogue.js +17 -5
- package/.agents/scripts/pr-watch-with-update.js +568 -213
- package/.agents/scripts/providers/github/tickets.js +33 -10
- package/.agents/scripts/provision-git-hooks.js +85 -0
- package/.agents/scripts/quality-preview.js +137 -28
- package/.agents/scripts/resolve-stories.js +4 -1
- package/.agents/scripts/run-coverage.js +86 -35
- package/.agents/scripts/run-lint.js +20 -0
- package/.agents/scripts/run-tests.js +36 -36
- package/.agents/scripts/single-story-close.js +34 -2
- package/.agents/scripts/single-story-confirm-merge.js +22 -6
- package/.agents/scripts/single-story-init.js +7 -0
- package/.agents/scripts/stories-wave-tick.js +308 -47
- package/.agents/scripts/story-plan.js +65 -9
- package/.agents/scripts/update-coverage-baseline.js +34 -4
- package/.agents/scripts/update-crap-baseline.js +42 -4
- package/.agents/scripts/update-duplication-baseline.js +209 -83
- package/.agents/scripts/update-maintainability-baseline.js +31 -4
- package/.agents/scripts/validate-docs-freshness.js +1 -0
- package/.agents/skills/core/diagnose-friction/SKILL.md +4 -1
- package/.agents/skills/core/gates-and-baselines/SKILL.md +17 -11
- package/.agents/skills/skills.index.json +2 -2
- package/.agents/templates/single-story-body.md +16 -8
- package/.agents/workflows/audit-architecture.md +69 -13
- package/.agents/workflows/audit-baselines.md +289 -0
- package/.agents/workflows/audit-navigability.md +5 -4
- package/.agents/workflows/audit-quality.md +26 -0
- package/.agents/workflows/audit-to-stories.md +30 -4
- package/.agents/workflows/deliver.md +92 -97
- package/.agents/workflows/helpers/acceptance-self-eval.md +47 -10
- package/.agents/workflows/helpers/audit-lens-core.md +25 -4
- package/.agents/workflows/helpers/code-quality-guardrails.md +9 -2
- package/.agents/workflows/helpers/deliver-digest.md +41 -21
- package/.agents/workflows/helpers/deliver-reference.md +95 -4
- package/.agents/workflows/helpers/deliver-story-reference.md +174 -52
- package/.agents/workflows/helpers/deliver-story.md +104 -119
- package/.agents/workflows/helpers/plan-reference.md +49 -5
- package/.agents/workflows/memory-consolidate.md +116 -0
- package/.agents/workflows/plan.md +62 -75
- package/README.md +13 -6
- package/docs/CHANGELOG.md +93 -0
- package/lib/cli/update.js +14 -11
- package/lib/cli/version-check.js +9 -1
- package/package.json +9 -4
- package/.agents/schemas/friction-event.schema.json +0 -56
- package/.agents/scripts/lib/feedback-loop/memory-freshness.js +0 -707
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* run-scoped-config.js — pin the config values a delivery run was seeded
|
|
3
|
+
* from, and fail closed when the file they came from changed underneath it.
|
|
4
|
+
*
|
|
5
|
+
* The problem this exists for: `single-story-init.js` resolves config and
|
|
6
|
+
* seeds `story-<id>` from `project.baseBranch`; `single-story-close` used to
|
|
7
|
+
* re-resolve the SAME key and run close-validation, format-autofix, the gate
|
|
8
|
+
* baseline and base-sync against its own second answer. Nothing pinned the
|
|
9
|
+
* first value and nothing checked the two agreed — and the window between
|
|
10
|
+
* them is however long implementation takes. A concurrent session editing
|
|
11
|
+
* `.agentrc.json` / `.agentrc.local.json` mid-run therefore base-synced a
|
|
12
|
+
* Story against a base it was never seeded from, which surfaced only as an
|
|
13
|
+
* ordinary content conflict with nothing pointing at config.
|
|
14
|
+
*
|
|
15
|
+
* The base a Story was seeded from is a property of **that run**, not of
|
|
16
|
+
* whatever the config file says later. So init pins it and close reads the
|
|
17
|
+
* pin back.
|
|
18
|
+
*
|
|
19
|
+
* ## Where the pin lives
|
|
20
|
+
*
|
|
21
|
+
* The `story-init` structured comment, not a temp log: the post-land tail
|
|
22
|
+
* purges the temp tree, while the ticket comment survives every close re-run
|
|
23
|
+
* and every recovery path. `single-story-init.js` already upserts that
|
|
24
|
+
* comment; `pinRunScopedConfig` supplies the block it records, and
|
|
25
|
+
* `resolveRunScopedConfig` reads it back through the existing
|
|
26
|
+
* `findStructuredComment` seam.
|
|
27
|
+
*
|
|
28
|
+
* ## Adding another run-scoped key
|
|
29
|
+
*
|
|
30
|
+
* Add one row to `RUN_SCOPED_CONFIG_KEYS`. Both halves — the pin init writes
|
|
31
|
+
* and the comparison close makes — enumerate that registry, so a new key
|
|
32
|
+
* needs no second mechanism, no reader change and no writer change. Only
|
|
33
|
+
* `baseBranch` is enforced today because only its corruption is destructive
|
|
34
|
+
* (a wrong base merged into the Story branch permanently contaminates the
|
|
35
|
+
* branch and its PR diff); ceremony profile, quality floors and the
|
|
36
|
+
* concurrency cap are deliberately NOT pinned here.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
import { parseFencedJsonComment } from './structured-comment-parser.js';
|
|
40
|
+
import { findStructuredComment } from './ticketing.js';
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The run-scoped config registry. One row per key whose value belongs to the
|
|
44
|
+
* run that seeded the Story rather than to the current contents of the config
|
|
45
|
+
* file. `read` resolves the key from a resolved config (including its
|
|
46
|
+
* default); `label` is the dotted config path the operator has to go fix.
|
|
47
|
+
*
|
|
48
|
+
* @type {Record<string, { read: (config: object) => unknown, label: string }>}
|
|
49
|
+
*/
|
|
50
|
+
const RUN_SCOPED_CONFIG_KEYS = {
|
|
51
|
+
baseBranch: {
|
|
52
|
+
read: (config) => config?.project?.baseBranch ?? 'main',
|
|
53
|
+
label: 'project.baseBranch',
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Snapshot the run-scoped config values from a resolved config. This is the
|
|
59
|
+
* write half of the pin: `single-story-init.js` records the returned object
|
|
60
|
+
* in the `story-init` receipt so close can compare against it later.
|
|
61
|
+
*
|
|
62
|
+
* @param {object} config Resolved config (`resolveConfig` output).
|
|
63
|
+
* @param {typeof RUN_SCOPED_CONFIG_KEYS} [keys] Registry override — the
|
|
64
|
+
* default is the module registry; supplying one is how a caller (or a test)
|
|
65
|
+
* exercises the registry-driven property without a second mechanism.
|
|
66
|
+
* @returns {Record<string, unknown>} the pinned values, keyed by config key.
|
|
67
|
+
*/
|
|
68
|
+
export function pinRunScopedConfig(config, keys = RUN_SCOPED_CONFIG_KEYS) {
|
|
69
|
+
const pinned = {};
|
|
70
|
+
for (const [key, spec] of Object.entries(keys)) {
|
|
71
|
+
pinned[key] = spec.read(config);
|
|
72
|
+
}
|
|
73
|
+
return pinned;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Read the pinned block out of the run's `story-init` receipt.
|
|
78
|
+
*
|
|
79
|
+
* Three distinct non-success states, all reported rather than collapsed —
|
|
80
|
+
* the whole point of this module is that a fallback is never silent:
|
|
81
|
+
* - `absent`: no `story-init` comment on the ticket. Real and expected —
|
|
82
|
+
* the upsert is best-effort (init logs and continues on failure), and a
|
|
83
|
+
* recovery path may close a Story whose init predates this receipt.
|
|
84
|
+
* - `unreadable`: the comment exists but carries no parseable JSON payload.
|
|
85
|
+
* - `provider-error`: the comment read itself failed.
|
|
86
|
+
*
|
|
87
|
+
* @param {{
|
|
88
|
+
* provider: object,
|
|
89
|
+
* storyId: number,
|
|
90
|
+
* findCommentFn?: typeof findStructuredComment,
|
|
91
|
+
* }} args
|
|
92
|
+
* @returns {Promise<{ status: string, values: Record<string, unknown>|null, detail: string|null }>}
|
|
93
|
+
*/
|
|
94
|
+
async function readRunScopedConfigReceipt({
|
|
95
|
+
provider,
|
|
96
|
+
storyId,
|
|
97
|
+
findCommentFn = findStructuredComment,
|
|
98
|
+
}) {
|
|
99
|
+
let comment;
|
|
100
|
+
try {
|
|
101
|
+
comment = await findCommentFn(provider, Number(storyId), 'story-init');
|
|
102
|
+
} catch (err) {
|
|
103
|
+
return {
|
|
104
|
+
status: 'provider-error',
|
|
105
|
+
values: null,
|
|
106
|
+
detail: `story-init comment could not be read (${err?.message ?? err})`,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
if (!comment) {
|
|
110
|
+
return {
|
|
111
|
+
status: 'absent',
|
|
112
|
+
values: null,
|
|
113
|
+
detail: 'no story-init comment on the ticket',
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
const payload = parseFencedJsonComment(comment);
|
|
117
|
+
if (!payload || typeof payload !== 'object') {
|
|
118
|
+
return {
|
|
119
|
+
status: 'unreadable',
|
|
120
|
+
values: null,
|
|
121
|
+
detail: 'story-init comment carries no parseable JSON payload',
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
// Receipts written before the `runScopedConfig` block existed carry the
|
|
125
|
+
// pinned values as top-level payload fields (`baseBranch` has been recorded
|
|
126
|
+
// there since Story #831). Reading the payload itself as the fallback block
|
|
127
|
+
// is what lets a Story initialized by an older init still close against its
|
|
128
|
+
// own pinned base instead of degrading to the fallback warning.
|
|
129
|
+
const block =
|
|
130
|
+
payload.runScopedConfig && typeof payload.runScopedConfig === 'object'
|
|
131
|
+
? payload.runScopedConfig
|
|
132
|
+
: payload;
|
|
133
|
+
return { status: 'found', values: block, detail: null };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Build the refusal message for a mid-run config change. Names BOTH values
|
|
138
|
+
* for every conflicting key — the pinned one and the one config resolves to
|
|
139
|
+
* now — because "which base was this branch actually seeded from" is the
|
|
140
|
+
* question the operator could not previously answer.
|
|
141
|
+
*
|
|
142
|
+
* @param {{ storyId: number, conflicts: Array<{ label: string, pinned: unknown, current: unknown }> }} args
|
|
143
|
+
* @returns {string}
|
|
144
|
+
*/
|
|
145
|
+
function formatRunScopedConflict({ storyId, conflicts }) {
|
|
146
|
+
const rows = conflicts.map(
|
|
147
|
+
({ label, pinned, current }) =>
|
|
148
|
+
`${label}: pinned at init = \`${String(pinned)}\`, currently resolves to \`${String(current)}\``,
|
|
149
|
+
);
|
|
150
|
+
return (
|
|
151
|
+
`[single-story-close] Refusing to close Story #${storyId}: run-scoped config changed mid-run. ` +
|
|
152
|
+
`${rows.join('; ')}. ` +
|
|
153
|
+
'The Story branch was seeded from the pinned value, so closing against the current one would ' +
|
|
154
|
+
'base-sync it onto a base it was never seeded from. No base-sync, format-autofix or gate run was ' +
|
|
155
|
+
'performed. A concurrent session most likely edited `.agentrc.json` / `.agentrc.local.json` during ' +
|
|
156
|
+
'the implementation window: restore the pinned value and re-run close, or re-init the Story against ' +
|
|
157
|
+
'the new value deliberately.'
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Resolve the run-scoped config for a close, preferring the run's pin over a
|
|
163
|
+
* fresh resolution of the config file.
|
|
164
|
+
*
|
|
165
|
+
* Throws on disagreement — fail closed, before any gate, format-autofix or
|
|
166
|
+
* base-sync has run, with both values named. Never silently prefers either
|
|
167
|
+
* side: preferring the pin would base-sync correctly but hide a config change
|
|
168
|
+
* the operator needs to know about, and preferring current config is the bug
|
|
169
|
+
* this module exists to close.
|
|
170
|
+
*
|
|
171
|
+
* @param {{
|
|
172
|
+
* provider: object,
|
|
173
|
+
* storyId: number,
|
|
174
|
+
* config: object,
|
|
175
|
+
* keys?: typeof RUN_SCOPED_CONFIG_KEYS,
|
|
176
|
+
* findCommentFn?: typeof findStructuredComment,
|
|
177
|
+
* progress?: (tag: string, msg: string) => void,
|
|
178
|
+
* }} args
|
|
179
|
+
* @returns {Promise<{
|
|
180
|
+
* values: Record<string, unknown>,
|
|
181
|
+
* confirmed: boolean,
|
|
182
|
+
* receiptStatus: string,
|
|
183
|
+
* warning: string|null,
|
|
184
|
+
* }>} `confirmed` is true only when every key came from the receipt and
|
|
185
|
+
* agreed with current config — i.e. when downstream remediation may safely
|
|
186
|
+
* assume the pinned value is the one in play.
|
|
187
|
+
*/
|
|
188
|
+
export async function resolveRunScopedConfig({
|
|
189
|
+
provider,
|
|
190
|
+
storyId,
|
|
191
|
+
config,
|
|
192
|
+
keys = RUN_SCOPED_CONFIG_KEYS,
|
|
193
|
+
findCommentFn,
|
|
194
|
+
progress,
|
|
195
|
+
}) {
|
|
196
|
+
const current = pinRunScopedConfig(config, keys);
|
|
197
|
+
const receipt = await readRunScopedConfigReceipt({
|
|
198
|
+
provider,
|
|
199
|
+
storyId,
|
|
200
|
+
findCommentFn,
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
if (receipt.status !== 'found') {
|
|
204
|
+
// A missing receipt is a real state, not an error — but the fallback is
|
|
205
|
+
// announced, because a silent one reintroduces exactly the bug above.
|
|
206
|
+
const warning =
|
|
207
|
+
`Run-scoped config could not be read from the run's init receipt ` +
|
|
208
|
+
`(${receipt.detail}); falling back to the currently-resolved config ` +
|
|
209
|
+
`(${describeValues(current, keys)}). This close cannot confirm the Story ` +
|
|
210
|
+
'was seeded from these values.';
|
|
211
|
+
progress?.('PIN', `⚠️ ${warning}`);
|
|
212
|
+
return {
|
|
213
|
+
values: current,
|
|
214
|
+
confirmed: false,
|
|
215
|
+
receiptStatus: receipt.status,
|
|
216
|
+
warning,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const values = {};
|
|
221
|
+
const conflicts = [];
|
|
222
|
+
const unpinned = [];
|
|
223
|
+
for (const [key, spec] of Object.entries(keys)) {
|
|
224
|
+
const pinned = receipt.values?.[key];
|
|
225
|
+
if (pinned === undefined || pinned === null) {
|
|
226
|
+
unpinned.push(spec.label);
|
|
227
|
+
values[key] = current[key];
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
values[key] = pinned;
|
|
231
|
+
if (pinned !== current[key]) {
|
|
232
|
+
conflicts.push({ label: spec.label, pinned, current: current[key] });
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (conflicts.length > 0) {
|
|
237
|
+
throw new Error(formatRunScopedConflict({ storyId, conflicts }));
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (unpinned.length > 0) {
|
|
241
|
+
const warning =
|
|
242
|
+
`The run's init receipt pins no value for ${unpinned.join(', ')}; ` +
|
|
243
|
+
`falling back to the currently-resolved config (${describeValues(values, keys)}).`;
|
|
244
|
+
progress?.('PIN', `⚠️ ${warning}`);
|
|
245
|
+
return {
|
|
246
|
+
values,
|
|
247
|
+
confirmed: false,
|
|
248
|
+
receiptStatus: receipt.status,
|
|
249
|
+
warning,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
progress?.(
|
|
254
|
+
'PIN',
|
|
255
|
+
`📌 Run-scoped config pinned by the story-init receipt (${describeValues(values, keys)}).`,
|
|
256
|
+
);
|
|
257
|
+
return {
|
|
258
|
+
values,
|
|
259
|
+
confirmed: true,
|
|
260
|
+
receiptStatus: receipt.status,
|
|
261
|
+
warning: null,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Render `label=value` pairs for the operator-facing lines above.
|
|
267
|
+
*
|
|
268
|
+
* @param {Record<string, unknown>} values
|
|
269
|
+
* @param {typeof RUN_SCOPED_CONFIG_KEYS} keys
|
|
270
|
+
* @returns {string}
|
|
271
|
+
*/
|
|
272
|
+
function describeValues(values, keys) {
|
|
273
|
+
return Object.entries(keys)
|
|
274
|
+
.map(([key, spec]) => `${spec.label}=\`${String(values[key])}\``)
|
|
275
|
+
.join(', ');
|
|
276
|
+
}
|
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
* cwd: string,
|
|
36
36
|
* worktreePath: string|null,
|
|
37
37
|
* baseBranch: string,
|
|
38
|
+
* baseConfirmed?: boolean,
|
|
38
39
|
* storyBranch: string,
|
|
39
40
|
* storyId: number,
|
|
40
41
|
* provider: object,
|
|
@@ -46,6 +47,7 @@ export async function runBaseSyncPhase({
|
|
|
46
47
|
cwd,
|
|
47
48
|
worktreePath,
|
|
48
49
|
baseBranch,
|
|
50
|
+
baseConfirmed = false,
|
|
49
51
|
storyBranch,
|
|
50
52
|
storyId,
|
|
51
53
|
provider,
|
|
@@ -69,6 +71,7 @@ export async function runBaseSyncPhase({
|
|
|
69
71
|
storyId,
|
|
70
72
|
syncCwd,
|
|
71
73
|
baseBranch,
|
|
74
|
+
baseConfirmed,
|
|
72
75
|
storyBranch,
|
|
73
76
|
result: syncResult,
|
|
74
77
|
progress,
|
|
@@ -95,6 +98,7 @@ export async function runBaseSyncPhase({
|
|
|
95
98
|
* storyId: number,
|
|
96
99
|
* syncCwd: string,
|
|
97
100
|
* baseBranch: string,
|
|
101
|
+
* baseConfirmed?: boolean,
|
|
98
102
|
* storyBranch: string,
|
|
99
103
|
* result: { kind: string, conflictFiles?: string[], stderr?: string },
|
|
100
104
|
* progress: (tag: string, msg: string) => void,
|
|
@@ -105,6 +109,7 @@ export async function handleSyncFailure({
|
|
|
105
109
|
storyId,
|
|
106
110
|
syncCwd,
|
|
107
111
|
baseBranch,
|
|
112
|
+
baseConfirmed = false,
|
|
108
113
|
storyBranch,
|
|
109
114
|
result,
|
|
110
115
|
progress,
|
|
@@ -113,6 +118,7 @@ export async function handleSyncFailure({
|
|
|
113
118
|
storyId,
|
|
114
119
|
storyBranch,
|
|
115
120
|
baseBranch,
|
|
121
|
+
baseConfirmed,
|
|
116
122
|
syncCwd,
|
|
117
123
|
result,
|
|
118
124
|
});
|
|
@@ -147,13 +153,23 @@ export async function handleSyncFailure({
|
|
|
147
153
|
* Build the markdown body posted on a base-sync failure. Pure; exported
|
|
148
154
|
* for tests so the operator-recoverable surface stays reviewable.
|
|
149
155
|
*
|
|
150
|
-
*
|
|
156
|
+
* Story #4891 — `baseConfirmed` gates the merge-the-base recovery block, and
|
|
157
|
+
* defaults to `false` so it fails closed. Telling the operator to run
|
|
158
|
+
* `git merge origin/<baseBranch>` is actively harmful when `<baseBranch>` is
|
|
159
|
+
* not the base the Story was seeded from: it permanently contaminates the
|
|
160
|
+
* branch and its PR diff with an unrelated base. Close confirms the base
|
|
161
|
+
* against the run's `story-init` receipt before that advice is emitted; when
|
|
162
|
+
* it could not (receipt absent, unreadable, or unpinned), the operator is
|
|
163
|
+
* told to establish the real base first instead.
|
|
164
|
+
*
|
|
165
|
+
* @param {{ storyId: number, storyBranch: string, baseBranch: string, baseConfirmed?: boolean, syncCwd: string, result: { kind: string, conflictFiles?: string[], stderr?: string } }} args
|
|
151
166
|
* @returns {string}
|
|
152
167
|
*/
|
|
153
168
|
export function buildSyncFailureCommentBody({
|
|
154
169
|
storyId,
|
|
155
170
|
storyBranch,
|
|
156
171
|
baseBranch,
|
|
172
|
+
baseConfirmed = false,
|
|
157
173
|
syncCwd,
|
|
158
174
|
result,
|
|
159
175
|
}) {
|
|
@@ -170,15 +186,30 @@ export function buildSyncFailureCommentBody({
|
|
|
170
186
|
`sync against \`origin/${baseBranch}\` could not complete. The Story has`,
|
|
171
187
|
`been transitioned to \`agent::blocked\`. To resume:`,
|
|
172
188
|
'',
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
189
|
+
...(baseConfirmed
|
|
190
|
+
? [
|
|
191
|
+
'```bash',
|
|
192
|
+
`cd ${syncCwd}`,
|
|
193
|
+
`git fetch origin ${baseBranch}`,
|
|
194
|
+
`git merge --no-edit origin/${baseBranch}`,
|
|
195
|
+
'# resolve any conflicts, then:',
|
|
196
|
+
`git add -A ; git commit --no-edit`,
|
|
197
|
+
'# re-run close:',
|
|
198
|
+
`node .agents/scripts/single-story-close.js --story ${storyId}`,
|
|
199
|
+
'```',
|
|
200
|
+
]
|
|
201
|
+
: [
|
|
202
|
+
`⚠️ **No merge advice: \`${baseBranch}\` is unconfirmed.** This close could not`,
|
|
203
|
+
`read the base branch \`${storyBranch}\` was seeded from off the run's`,
|
|
204
|
+
'`story-init` receipt, so merging that base in could contaminate the branch',
|
|
205
|
+
'and its PR diff with an unrelated base. Establish the real base first —',
|
|
206
|
+
`check the \`story-init\` comment on this issue and \`project.baseBranch\` in`,
|
|
207
|
+
'`.agentrc.json` / `.agentrc.local.json` — then merge that base and re-run:',
|
|
208
|
+
'',
|
|
209
|
+
'```bash',
|
|
210
|
+
`node .agents/scripts/single-story-close.js --story ${storyId}`,
|
|
211
|
+
'```',
|
|
212
|
+
]),
|
|
182
213
|
];
|
|
183
214
|
if (kind === 'conflict' && fileList.length > 0) {
|
|
184
215
|
lines.push('', '**Conflicting files:**', '', ...fileList);
|
|
@@ -95,6 +95,7 @@ import {
|
|
|
95
95
|
confirmStoryMerged as defaultConfirmStoryMerged,
|
|
96
96
|
readPrMergeState as defaultReadPrMergeState,
|
|
97
97
|
} from '../../../single-story/confirm-merge.js';
|
|
98
|
+
import { pollUntil } from '../../../util/poll-loop.js';
|
|
98
99
|
import {
|
|
99
100
|
emitMergeFlipFailed as defaultEmitMergeFlipFailed,
|
|
100
101
|
MERGED_FLIP_FAILED_BLOCK_CLASS,
|
|
@@ -281,15 +282,33 @@ export async function readPrWaitProbe({
|
|
|
281
282
|
* explicit `maxWaitSecondsOverride` still wins over the async cap — a headless
|
|
282
283
|
* caller with no host ceiling opts back into single-block waiting.
|
|
283
284
|
*
|
|
285
|
+
* `modeOverride` is the per-invocation `--merge-watch-mode` flag (Story #4949)
|
|
286
|
+
* and wins over `delivery.mergeWatch.mode` on exactly the precedence
|
|
287
|
+
* `maxWaitSecondsOverride` already uses. It exists because run topology is
|
|
288
|
+
* knowable only to the orchestrator: close sees one Story and cannot tell a
|
|
289
|
+
* solo delivery (where a foreground wait is the cheapest ending) from the Nth
|
|
290
|
+
* close of a wave (where each foreground wait is serialized dead time). The
|
|
291
|
+
* config default therefore stays `sync`, and the caller that knows better says
|
|
292
|
+
* so per invocation. The two flags remain composable — `--merge-watch-mode
|
|
293
|
+
* async --max-wait-seconds 900` selects the async posture and then overrides
|
|
294
|
+
* its probe cap, because the cap check below keys on the override's presence,
|
|
295
|
+
* not on where the mode came from.
|
|
296
|
+
*
|
|
284
297
|
* @param {object} [config]
|
|
285
298
|
* @param {number} [maxWaitSecondsOverride]
|
|
299
|
+
* @param {'sync'|'async'} [modeOverride]
|
|
286
300
|
* @returns {{ mode: 'sync'|'async', intervalSeconds: number, maxWaitSeconds: number, maxBudgetSeconds: number, updateAttempts: number }}
|
|
287
301
|
*/
|
|
288
|
-
export function resolveMergeWaitConfig(
|
|
302
|
+
export function resolveMergeWaitConfig(
|
|
303
|
+
config,
|
|
304
|
+
maxWaitSecondsOverride,
|
|
305
|
+
modeOverride,
|
|
306
|
+
) {
|
|
289
307
|
const mergeWatch = config?.delivery?.mergeWatch ?? {};
|
|
290
308
|
const int = (value, fallback, min = 1) =>
|
|
291
309
|
Number.isInteger(value) && value >= min ? value : fallback;
|
|
292
|
-
const
|
|
310
|
+
const requestedMode = modeOverride ?? mergeWatch.mode;
|
|
311
|
+
const mode = requestedMode === 'async' ? 'async' : 'sync';
|
|
293
312
|
const configuredMaxWait = int(
|
|
294
313
|
maxWaitSecondsOverride,
|
|
295
314
|
int(mergeWatch.maxWaitSeconds, DEFAULT_MAX_WAIT_SECONDS),
|
|
@@ -363,7 +382,8 @@ function formatUnlandedFriction({
|
|
|
363
382
|
const remedy =
|
|
364
383
|
blockClass === 'checks-failed'
|
|
365
384
|
? `A required check is **red**. Fix the failure and push a new commit on \`story-${storyId}\`; ` +
|
|
366
|
-
`auto-merge
|
|
385
|
+
`the red disarms auto-merge, and only a green on a new head SHA re-arms it — ` +
|
|
386
|
+
`re-running the failed job is forbidden. Watch the checks with:\n\n` +
|
|
367
387
|
`\`\`\`bash\n${NEXT_COMMANDS.watchCi(storyId, prNumber)}\n\`\`\``
|
|
368
388
|
: `Resolve the underlying condition (branch protection, required checks, ` +
|
|
369
389
|
`or a manual merge), then resume the land:\n\n` +
|
|
@@ -757,6 +777,9 @@ async function onMergeObserved({
|
|
|
757
777
|
* @param {string|null} args.autoMergeReason
|
|
758
778
|
* @param {object} args.provider
|
|
759
779
|
* @param {object} [args.config]
|
|
780
|
+
* @param {'sync'|'async'} [args.mergeWatchMode] Per-invocation
|
|
781
|
+
* `--merge-watch-mode` override (Story #4949); wins over
|
|
782
|
+
* `delivery.mergeWatch.mode`.
|
|
760
783
|
* @param {(tag: string, msg: string) => void} [args.progress]
|
|
761
784
|
* @param {object} [args.injectedGh]
|
|
762
785
|
* @param {Function} [args.injectedNotify]
|
|
@@ -789,6 +812,7 @@ export async function runConfirmMergePhase({
|
|
|
789
812
|
provider,
|
|
790
813
|
config,
|
|
791
814
|
maxWaitSeconds: maxWaitSecondsOverride,
|
|
815
|
+
mergeWatchMode: mergeWatchModeOverride,
|
|
792
816
|
progress,
|
|
793
817
|
injectedGh,
|
|
794
818
|
injectedNotify,
|
|
@@ -831,7 +855,11 @@ export async function runConfirmMergePhase({
|
|
|
831
855
|
maxWaitSeconds,
|
|
832
856
|
maxBudgetSeconds,
|
|
833
857
|
updateAttempts,
|
|
834
|
-
} = resolveMergeWaitConfig(
|
|
858
|
+
} = resolveMergeWaitConfig(
|
|
859
|
+
config,
|
|
860
|
+
maxWaitSecondsOverride,
|
|
861
|
+
mergeWatchModeOverride,
|
|
862
|
+
);
|
|
835
863
|
const intervalMs = intervalSeconds * 1000;
|
|
836
864
|
const startedAtMs = nowMsFn();
|
|
837
865
|
let anchorMs = startedAtMs;
|
|
@@ -851,7 +879,20 @@ export async function runConfirmMergePhase({
|
|
|
851
879
|
`cumulative budget=${maxBudgetSeconds}s)...`,
|
|
852
880
|
);
|
|
853
881
|
|
|
854
|
-
|
|
882
|
+
/**
|
|
883
|
+
* One poll iteration. Returns `{ done: false }` to keep polling, or
|
|
884
|
+
* `{ done: true, outcome }` with the phase's terminal. Story #4873 lifted
|
|
885
|
+
* this body out of a bespoke unbounded loop so the cadence is owned by the
|
|
886
|
+
* shared {@link pollUntil} primitive — the loop below sleeps, aborts, and
|
|
887
|
+
* counts ticks in exactly one place for every wait in the codebase. Every
|
|
888
|
+
* budget, floor, and classification decision is unchanged; only who owns the
|
|
889
|
+
* `await sleep(...)` moved.
|
|
890
|
+
*
|
|
891
|
+
* A throw from any of the terminal handlers is captured rather than allowed
|
|
892
|
+
* to escape into `pollUntil` (which treats a throwing `fn` as a non-match
|
|
893
|
+
* and would spin forever on it); the caller re-throws it after the loop.
|
|
894
|
+
*/
|
|
895
|
+
async function runMergePoll() {
|
|
855
896
|
const probe = await readPrWaitProbeFn({
|
|
856
897
|
prNumber,
|
|
857
898
|
gh: injectedGh,
|
|
@@ -875,26 +916,44 @@ export async function runConfirmMergePhase({
|
|
|
875
916
|
maxBudgetSeconds,
|
|
876
917
|
};
|
|
877
918
|
|
|
919
|
+
// Heartbeat (Story #4873). A backgrounded close writes this phase's
|
|
920
|
+
// progress to its own output file, and between the opening banner and the
|
|
921
|
+
// terminal there used to be NOTHING for minutes at a time — so an
|
|
922
|
+
// orchestrator watching that file could not tell a healthy in-flight wait
|
|
923
|
+
// from a wedged process without going back to GitHub itself. One line per
|
|
924
|
+
// poll makes the file's own growth the liveness signal.
|
|
925
|
+
progress?.(
|
|
926
|
+
'CONFIRM',
|
|
927
|
+
`⏱ poll ${polls}: PR #${prNumber} state=${probe.state ?? 'unknown'} ` +
|
|
928
|
+
`checks=${probe.checksStatus ?? 'unknown'} ` +
|
|
929
|
+
`mergeState=${probe.mergeStateStatus ?? 'unknown'} ` +
|
|
930
|
+
`(${waitBudget.waitedSeconds}s of ${maxWaitSeconds}s this invocation; ` +
|
|
931
|
+
`${waitBudget.cumulativeSeconds}s of ${maxBudgetSeconds}s cumulative)` +
|
|
932
|
+
(probe.error ? ` — probe error: ${probe.error}` : ''),
|
|
933
|
+
);
|
|
934
|
+
|
|
878
935
|
if (probe.state === 'MERGED' || probe.mergedAt) {
|
|
879
|
-
return
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
936
|
+
return doneWith(
|
|
937
|
+
await onMergeObserved({
|
|
938
|
+
storyId,
|
|
939
|
+
storyBranch,
|
|
940
|
+
baseBranch,
|
|
941
|
+
prNumber,
|
|
942
|
+
prUrl,
|
|
943
|
+
cwd,
|
|
944
|
+
config,
|
|
945
|
+
provider,
|
|
946
|
+
progress,
|
|
947
|
+
injectedGh,
|
|
948
|
+
injectedNotify,
|
|
949
|
+
readPrMergeStateFn,
|
|
950
|
+
confirmStoryMergedFn,
|
|
951
|
+
runPostLandTailFn,
|
|
952
|
+
emitMergeFlipFailedFn,
|
|
953
|
+
prProbe: probe,
|
|
954
|
+
elapsedSeconds: Math.round(waitedMs / 1000),
|
|
955
|
+
}),
|
|
956
|
+
);
|
|
898
957
|
}
|
|
899
958
|
|
|
900
959
|
// Everything below funnels into ONE terminal exit (Story #4710): each
|
|
@@ -987,16 +1046,18 @@ export async function runConfirmMergePhase({
|
|
|
987
1046
|
}
|
|
988
1047
|
|
|
989
1048
|
if (unlanded) {
|
|
990
|
-
return
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1049
|
+
return doneWith(
|
|
1050
|
+
await blockOnUnlanded({
|
|
1051
|
+
storyId,
|
|
1052
|
+
prNumber,
|
|
1053
|
+
prUrl,
|
|
1054
|
+
...unlanded,
|
|
1055
|
+
provider,
|
|
1056
|
+
progress,
|
|
1057
|
+
classifyMergeBlockFn,
|
|
1058
|
+
emitMergeUnlandedFn,
|
|
1059
|
+
}),
|
|
1060
|
+
);
|
|
1000
1061
|
}
|
|
1001
1062
|
|
|
1002
1063
|
// This invocation's bound expired → PENDING. Deliberately NOT a block:
|
|
@@ -1011,16 +1072,43 @@ export async function runConfirmMergePhase({
|
|
|
1011
1072
|
`${waitBudget.cumulativeSeconds}s of ${maxBudgetSeconds}s cumulative). PR #${prNumber} still in flight ` +
|
|
1012
1073
|
`(checks=${probe.checksStatus ?? 'unknown'}). Story stays at agent::closing — resumable.`,
|
|
1013
1074
|
);
|
|
1014
|
-
return {
|
|
1075
|
+
return doneWith({
|
|
1015
1076
|
confirmed: false,
|
|
1016
1077
|
terminal: 'pending',
|
|
1017
1078
|
reason: `merge wait bound reached with the PR still in flight (checks=${probe.checksStatus ?? 'unknown'})`,
|
|
1018
1079
|
prProbe: probe,
|
|
1019
1080
|
waitBudget,
|
|
1020
1081
|
elapsedSeconds: waitBudget.waitedSeconds,
|
|
1021
|
-
};
|
|
1082
|
+
});
|
|
1022
1083
|
}
|
|
1023
1084
|
|
|
1024
|
-
|
|
1085
|
+
return { done: false };
|
|
1025
1086
|
}
|
|
1087
|
+
|
|
1088
|
+
const tick = await pollUntil({
|
|
1089
|
+
fn: async () => {
|
|
1090
|
+
try {
|
|
1091
|
+
return await runMergePoll();
|
|
1092
|
+
} catch (err) {
|
|
1093
|
+
// A terminal handler threw. `pollUntil` treats a throwing `fn` as a
|
|
1094
|
+
// non-match and would poll forever on it, so the throw is carried out
|
|
1095
|
+
// as a match and re-raised below.
|
|
1096
|
+
return { done: true, thrown: err };
|
|
1097
|
+
}
|
|
1098
|
+
},
|
|
1099
|
+
predicate: (result) => result?.done === true,
|
|
1100
|
+
intervalMs,
|
|
1101
|
+
// The wait owns its own bounds (`maxWaitSeconds` → `pending`,
|
|
1102
|
+
// `maxBudgetSeconds` → blocked), and both are decided from the probe
|
|
1103
|
+
// inside the tick. A second, cruder wall-clock timeout here would throw
|
|
1104
|
+
// past those classifications.
|
|
1105
|
+
sleepFn: (ms) => sleepFn(ms),
|
|
1106
|
+
});
|
|
1107
|
+
if (tick.thrown) throw tick.thrown;
|
|
1108
|
+
return tick.outcome;
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
/** Wrap a phase terminal as the poll loop's match. */
|
|
1112
|
+
function doneWith(outcome) {
|
|
1113
|
+
return { done: true, outcome };
|
|
1026
1114
|
}
|
|
@@ -38,7 +38,7 @@ import { gitSpawn as defaultGitSpawn } from '../../../git-utils.js';
|
|
|
38
38
|
import { Logger as DefaultLogger } from '../../../Logger.js';
|
|
39
39
|
|
|
40
40
|
/** Safe default Conventional-Commit type when none can be derived. */
|
|
41
|
-
|
|
41
|
+
const DEFAULT_CONVENTIONAL_TYPE = 'chore';
|
|
42
42
|
|
|
43
43
|
/**
|
|
44
44
|
* The Conventional-Commit types Mandrel accepts. Mirrors
|
|
@@ -46,7 +46,7 @@ export const DEFAULT_CONVENTIONAL_TYPE = 'chore';
|
|
|
46
46
|
* `changelog-sections`. Kept in sync by hand (single hard-cutover, no
|
|
47
47
|
* shim) — adding a type means touching all three.
|
|
48
48
|
*/
|
|
49
|
-
|
|
49
|
+
const CONVENTIONAL_TYPES = Object.freeze([
|
|
50
50
|
'feat',
|
|
51
51
|
'fix',
|
|
52
52
|
'perf',
|
|
@@ -99,7 +99,7 @@ const LEADING_TYPE_RE = new RegExp(
|
|
|
99
99
|
* @param {string} subject
|
|
100
100
|
* @returns {boolean}
|
|
101
101
|
*/
|
|
102
|
-
|
|
102
|
+
function isConventionalSubject(subject) {
|
|
103
103
|
if (typeof subject !== 'string') return false;
|
|
104
104
|
return CONVENTIONAL_HEADER_RE.test(subject.trim());
|
|
105
105
|
}
|
|
@@ -111,7 +111,7 @@ export function isConventionalSubject(subject) {
|
|
|
111
111
|
* @param {string} subject
|
|
112
112
|
* @returns {string|null}
|
|
113
113
|
*/
|
|
114
|
-
|
|
114
|
+
function parseConventionalType(subject) {
|
|
115
115
|
if (typeof subject !== 'string') return null;
|
|
116
116
|
const match = subject.trim().match(LEADING_TYPE_RE);
|
|
117
117
|
return match ? match[1] : null;
|
|
@@ -125,7 +125,7 @@ export function parseConventionalType(subject) {
|
|
|
125
125
|
* @param {string[]} types
|
|
126
126
|
* @returns {string|null}
|
|
127
127
|
*/
|
|
128
|
-
|
|
128
|
+
function pickDominantType(types) {
|
|
129
129
|
const present = new Set(types.filter(Boolean));
|
|
130
130
|
for (const candidate of TYPE_PRECEDENCE) {
|
|
131
131
|
if (present.has(candidate)) return candidate;
|
|
@@ -148,7 +148,7 @@ export function pickDominantType(types) {
|
|
|
148
148
|
* }} args
|
|
149
149
|
* @returns {string}
|
|
150
150
|
*/
|
|
151
|
-
|
|
151
|
+
function deriveTypeFromBranchCommits({
|
|
152
152
|
storyBranch,
|
|
153
153
|
baseBranch,
|
|
154
154
|
cwd = process.cwd(),
|