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
|
@@ -9,23 +9,27 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import path from 'node:path';
|
|
12
|
-
import { parseSprintArgs } from '../../../cli-args.js';
|
|
12
|
+
import { parseMergeWatchMode, parseSprintArgs } from '../../../cli-args.js';
|
|
13
13
|
import { getDeliveryRouting } from '../../../config/delivery-routing.js';
|
|
14
14
|
import { PROJECT_ROOT } from '../../../project-root.js';
|
|
15
15
|
import { isOperatorMergeReason } from './auto-merge.js';
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
|
-
* Resolve a flag value from an explicit override
|
|
19
|
-
*
|
|
18
|
+
* Resolve a flag value from an explicit override or a parsed CLI arg.
|
|
19
|
+
*
|
|
20
|
+
* Returns `undefined` when neither is supplied — that absence is itself the
|
|
21
|
+
* answer here, letting each caller below apply its own default (a `!!` coerce,
|
|
22
|
+
* a config lookup, or a deliberate `undefined` passed further down). The
|
|
23
|
+
* former third `defaultValue` parameter was dropped in Story #4961: no call
|
|
24
|
+
* site passed it, so it documented a mode nobody used.
|
|
20
25
|
*
|
|
21
26
|
* @template T
|
|
22
27
|
* @param {T|undefined} paramValue
|
|
23
28
|
* @param {T|undefined} parsedValue
|
|
24
|
-
* @
|
|
25
|
-
* @returns {T}
|
|
29
|
+
* @returns {T|undefined}
|
|
26
30
|
*/
|
|
27
|
-
function resolveFlag(paramValue, parsedValue
|
|
28
|
-
return paramValue ?? parsedValue
|
|
31
|
+
function resolveFlag(paramValue, parsedValue) {
|
|
32
|
+
return paramValue ?? parsedValue;
|
|
29
33
|
}
|
|
30
34
|
|
|
31
35
|
/**
|
|
@@ -87,8 +91,8 @@ export function resolveWaitForMerge({
|
|
|
87
91
|
* (`waitForMergeExplicit` / `noWaitForMerge`) for the runner to resolve once
|
|
88
92
|
* the config and the arm outcome exist.
|
|
89
93
|
*
|
|
90
|
-
* @param {{ storyIdParam, cwdParam, skipValidationParam, skipSyncParam, noAutoMergeParam, waitForMergeParam, noWaitForMergeParam, maxWaitSecondsParam }} raw
|
|
91
|
-
* @returns {{ storyId, cwd, skipValidation, skipSync, noAutoMerge, waitForMergeExplicit, noWaitForMerge, maxWaitSeconds }}
|
|
94
|
+
* @param {{ storyIdParam, cwdParam, skipValidationParam, skipSyncParam, noAutoMergeParam, waitForMergeParam, noWaitForMergeParam, maxWaitSecondsParam, mergeWatchModeParam }} raw
|
|
95
|
+
* @returns {{ storyId, cwd, skipValidation, skipSync, noAutoMerge, waitForMergeExplicit, noWaitForMerge, maxWaitSeconds, mergeWatchMode }}
|
|
92
96
|
*/
|
|
93
97
|
export function parseCloseOptions({
|
|
94
98
|
storyIdParam,
|
|
@@ -99,26 +103,27 @@ export function parseCloseOptions({
|
|
|
99
103
|
waitForMergeParam,
|
|
100
104
|
noWaitForMergeParam,
|
|
101
105
|
maxWaitSecondsParam,
|
|
106
|
+
mergeWatchModeParam,
|
|
102
107
|
}) {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
108
|
+
// An injecting caller (`storyIdParam` supplied) is not reading argv at all,
|
|
109
|
+
// so there is nothing to parse and `parsed` stays empty. This used to build a
|
|
110
|
+
// stand-in object that copied every `*Param` into the slot of the same name —
|
|
111
|
+
// which is precisely what `resolveFlag` already does below, preferring the
|
|
112
|
+
// param over the parsed slot. One expression per flag now serves both
|
|
113
|
+
// callers, so a new flag is added in one place instead of two that can drift.
|
|
114
|
+
const parsed = storyIdParam === undefined ? parseSprintArgs() : {};
|
|
115
|
+
// Preserve undefined so resolveWaitForMerge can apply the closeAndLand
|
|
116
|
+
// config default when neither flag was supplied.
|
|
117
|
+
const waitForMergeExplicit = resolveFlag(
|
|
118
|
+
waitForMergeParam,
|
|
119
|
+
parsed.waitForMerge,
|
|
120
|
+
);
|
|
121
|
+
const maxWaitSeconds = resolveFlag(
|
|
122
|
+
maxWaitSecondsParam,
|
|
123
|
+
parsed.maxWaitSeconds,
|
|
124
|
+
);
|
|
120
125
|
return {
|
|
121
|
-
storyId: parsed.storyId,
|
|
126
|
+
storyId: resolveFlag(storyIdParam, parsed.storyId),
|
|
122
127
|
cwd: path.resolve(cwdParam ?? parsed.cwd ?? PROJECT_ROOT),
|
|
123
128
|
// `undefined` when unsupplied — the merge wait then reads
|
|
124
129
|
// `delivery.mergeWatch.maxWaitSeconds`. A per-run override exists so a
|
|
@@ -128,21 +133,20 @@ export function parseCloseOptions({
|
|
|
128
133
|
Number.isInteger(maxWaitSeconds) && maxWaitSeconds > 0
|
|
129
134
|
? maxWaitSeconds
|
|
130
135
|
: undefined,
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
136
|
+
// `undefined` when unsupplied — the merge wait then reads
|
|
137
|
+
// `delivery.mergeWatch.mode`. The two merge-watch flags stay composable and
|
|
138
|
+
// mode-agnostic: `--merge-watch-mode async` picks the posture, and an
|
|
139
|
+
// explicit `--max-wait-seconds` still wins over that posture's probe cap.
|
|
140
|
+
mergeWatchMode: parseMergeWatchMode(
|
|
141
|
+
resolveFlag(mergeWatchModeParam, parsed.mergeWatchMode),
|
|
135
142
|
),
|
|
136
|
-
|
|
137
|
-
|
|
143
|
+
skipValidation: !!resolveFlag(skipValidationParam, parsed.skipValidation),
|
|
144
|
+
skipSync: !!resolveFlag(skipSyncParam, parsed.skipSync),
|
|
145
|
+
noAutoMerge: !!resolveFlag(noAutoMergeParam, parsed.noAutoMerge),
|
|
138
146
|
waitForMergeExplicit:
|
|
139
147
|
typeof waitForMergeExplicit === 'boolean'
|
|
140
148
|
? waitForMergeExplicit
|
|
141
149
|
: undefined,
|
|
142
|
-
noWaitForMerge: resolveFlag(
|
|
143
|
-
noWaitForMergeParam,
|
|
144
|
-
parsed.noWaitForMerge,
|
|
145
|
-
false,
|
|
146
|
-
),
|
|
150
|
+
noWaitForMerge: !!resolveFlag(noWaitForMergeParam, parsed.noWaitForMerge),
|
|
147
151
|
};
|
|
148
152
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* phases/pull-request.js — open or
|
|
2
|
+
* phases/pull-request.js — open, reuse, or decline to open the PR for a
|
|
3
|
+
* standalone Story.
|
|
3
4
|
*
|
|
4
|
-
* Probes for an existing
|
|
5
|
-
*
|
|
5
|
+
* Probes for an existing PR with `head = storyBranch`; creates one if none
|
|
6
|
+
* exists. Returns `{ url, alreadyMerged, created }`.
|
|
6
7
|
*
|
|
7
8
|
* `gh pr view --head` is not available on all `gh` versions, so we probe
|
|
8
9
|
* with `gh pr list --head <branch>` and fall back to `gh pr create`.
|
|
@@ -13,17 +14,99 @@
|
|
|
13
14
|
* error classification, timeout handling, and JSON parsing surface
|
|
14
15
|
* instead of carrying its own `execFileSync('gh', …)` plumbing.
|
|
15
16
|
*
|
|
17
|
+
* ## An already-merged PR is not an absent PR (Story #4873)
|
|
18
|
+
*
|
|
19
|
+
* The probe used to ask for `--state open` only. On the recovery path that is
|
|
20
|
+
* a false negative with teeth: a resumed close pushes the branch, the push
|
|
21
|
+
* turns the checks green, armed auto-merge lands the ORIGINAL PR server-side,
|
|
22
|
+
* and the resumed close then sees no OPEN PR on the head — so it opened a
|
|
23
|
+
* SECOND PR against a branch that is now byte-identical to base and
|
|
24
|
+
* squash-merged a zero-file commit onto `main`. Both halves of that are now
|
|
25
|
+
* closed:
|
|
26
|
+
*
|
|
27
|
+
* 1. The probe reads `--state all` and reports a MERGED PR on this head as
|
|
28
|
+
* the outcome (`alreadyMerged: true`), so the caller lands on the merge
|
|
29
|
+
* that already happened instead of manufacturing a new one. An OPEN PR
|
|
30
|
+
* still wins over a merged one — a re-opened head is a live PR.
|
|
31
|
+
* 2. Creation is REFUSED outright when the head-versus-base diff contains no
|
|
32
|
+
* files. An empty diff means there is nothing to merge, so a PR opened on
|
|
33
|
+
* it can only ever produce an empty commit.
|
|
34
|
+
*
|
|
35
|
+
* The empty-diff guard fails **open**, not closed: `computeChangeSet` returns
|
|
36
|
+
* `files: null` when it cannot enumerate the diff at all, and absence of
|
|
37
|
+
* evidence must never block a legitimate PR — only a positively-observed empty
|
|
38
|
+
* file list refuses.
|
|
39
|
+
*
|
|
16
40
|
* The function still accepts an injected `gh` facade so tests can wire
|
|
17
41
|
* a fake without spawning real children.
|
|
18
42
|
*/
|
|
19
43
|
|
|
20
44
|
import { gh as defaultGh } from '../../../gh-exec.js';
|
|
21
45
|
import { Logger } from '../../../Logger.js';
|
|
46
|
+
import { computeChangeSet as defaultComputeChangeSet } from '../../change-set.js';
|
|
22
47
|
import { normalizePrTitle } from './normalize-pr-title.js';
|
|
23
48
|
|
|
24
49
|
/**
|
|
25
|
-
*
|
|
26
|
-
*
|
|
50
|
+
* Pick the PR this head branch should resolve to from a `gh pr list
|
|
51
|
+
* --state all` projection. A live PR always wins; otherwise the first MERGED
|
|
52
|
+
* PR is the outcome to report. A head whose only PRs were CLOSED without
|
|
53
|
+
* merging resolves to nothing — there is a new PR to open.
|
|
54
|
+
*
|
|
55
|
+
* A row carrying a url but no recognizable `state` reads as live, not as
|
|
56
|
+
* nothing: the projection this phase asks for always includes `state`, so an
|
|
57
|
+
* absent one means an older/other `gh`, and the old `--state open` probe
|
|
58
|
+
* treated every returned row as a reusable open PR. Guessing "no PR" there is
|
|
59
|
+
* the failure mode with teeth — it opens a duplicate.
|
|
60
|
+
*
|
|
61
|
+
* Pure, and module-private on purpose: `ensurePullRequestWith` is the only
|
|
62
|
+
* caller and the only surface worth pinning, so the precedence is asserted
|
|
63
|
+
* through it rather than through a test-only export the production
|
|
64
|
+
* dead-export ratchet would then flag.
|
|
65
|
+
*
|
|
66
|
+
* @param {Array<{url?: string, state?: string, mergedAt?: string}>} rows
|
|
67
|
+
* @returns {{ url: string, state: 'OPEN'|'MERGED' }|null}
|
|
68
|
+
*/
|
|
69
|
+
function pickHeadPullRequest(rows) {
|
|
70
|
+
if (!Array.isArray(rows)) return null;
|
|
71
|
+
let merged = null;
|
|
72
|
+
for (const row of rows) {
|
|
73
|
+
const url = String(row?.url ?? '').trim();
|
|
74
|
+
if (!url) continue;
|
|
75
|
+
const state = String(row?.state ?? '').toUpperCase();
|
|
76
|
+
if (state === 'MERGED' || (state !== 'OPEN' && row?.mergedAt)) {
|
|
77
|
+
merged ??= { url, state: 'MERGED' };
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (state === 'CLOSED') continue;
|
|
81
|
+
return { url, state: 'OPEN' };
|
|
82
|
+
}
|
|
83
|
+
return merged;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Enumerate the head-versus-base diff and report whether it is positively
|
|
88
|
+
* empty. `null` (diff unenumerable) is NOT empty — see the module header.
|
|
89
|
+
*
|
|
90
|
+
* The diff is taken against `origin/<baseBranch>` when that ref resolves,
|
|
91
|
+
* because the local base ref can trail the remote by exactly the merge that
|
|
92
|
+
* makes this diff empty — the very state the guard exists to catch. It falls
|
|
93
|
+
* back to the local ref, and finally to "unknown", when the remote ref cannot
|
|
94
|
+
* be enumerated.
|
|
95
|
+
*
|
|
96
|
+
* @returns {{ empty: boolean, baseRef: string|null }}
|
|
97
|
+
*/
|
|
98
|
+
function probeEmptyDiff({ cwd, baseBranch, storyBranch, computeChangeSet }) {
|
|
99
|
+
for (const baseRef of [`origin/${baseBranch}`, baseBranch]) {
|
|
100
|
+
const set = computeChangeSet({ baseRef, headRef: storyBranch, cwd });
|
|
101
|
+
if (!set.enumerated) continue;
|
|
102
|
+
return { empty: (set.files ?? []).length === 0, baseRef };
|
|
103
|
+
}
|
|
104
|
+
return { empty: false, baseRef: null };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Probe for an existing PR with `head = storyBranch`; create one if none
|
|
109
|
+
* exists. Exported for testing.
|
|
27
110
|
*
|
|
28
111
|
* @param {{
|
|
29
112
|
* cwd: string,
|
|
@@ -32,9 +115,10 @@ import { normalizePrTitle } from './normalize-pr-title.js';
|
|
|
32
115
|
* storyBranch: string,
|
|
33
116
|
* baseBranch: string,
|
|
34
117
|
* gh?: ReturnType<typeof import('../../../gh-exec.js').createGh>,
|
|
118
|
+
* computeChangeSetFn?: typeof defaultComputeChangeSet,
|
|
35
119
|
* progress?: (tag: string, msg: string) => void,
|
|
36
120
|
* }} args
|
|
37
|
-
* @returns {Promise<string>}
|
|
121
|
+
* @returns {Promise<{ url: string, alreadyMerged: boolean, created: boolean }>}
|
|
38
122
|
*/
|
|
39
123
|
export async function ensurePullRequestWith({
|
|
40
124
|
cwd: _cwd,
|
|
@@ -43,6 +127,7 @@ export async function ensurePullRequestWith({
|
|
|
43
127
|
storyBranch,
|
|
44
128
|
baseBranch,
|
|
45
129
|
gh = defaultGh,
|
|
130
|
+
computeChangeSetFn = defaultComputeChangeSet,
|
|
46
131
|
progress = () => {},
|
|
47
132
|
}) {
|
|
48
133
|
// `cwd` is preserved on the call signature for backwards compatibility
|
|
@@ -51,22 +136,26 @@ export async function ensurePullRequestWith({
|
|
|
51
136
|
// before invoking the phase, so the effective cwd matches the legacy
|
|
52
137
|
// `execFileSync('gh', …, { cwd })` shape.
|
|
53
138
|
try {
|
|
54
|
-
// `gh pr list --head <branch> --state
|
|
55
|
-
// JSON array of
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
// `gh.pr.list` facade can stay generic.
|
|
139
|
+
// `gh pr list --head <branch> --state all --json url,state,mergedAt`
|
|
140
|
+
// returns a JSON array of rows; an empty array means this head has never
|
|
141
|
+
// had a PR. `--state all` (not `open`) is load-bearing — see the module
|
|
142
|
+
// header's duplicate-empty-PR note.
|
|
59
143
|
const rows = await gh.pr.list(
|
|
60
|
-
['--head', storyBranch, '--state', '
|
|
61
|
-
['url'],
|
|
144
|
+
['--head', storyBranch, '--state', 'all'],
|
|
145
|
+
['url', 'state', 'mergedAt'],
|
|
62
146
|
);
|
|
63
|
-
const existing =
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
147
|
+
const existing = pickHeadPullRequest(rows);
|
|
148
|
+
if (existing?.state === 'OPEN') {
|
|
149
|
+
progress('PR', `Reusing existing PR: ${existing.url}`);
|
|
150
|
+
return { url: existing.url, alreadyMerged: false, created: false };
|
|
151
|
+
}
|
|
152
|
+
if (existing?.state === 'MERGED') {
|
|
153
|
+
progress(
|
|
154
|
+
'PR',
|
|
155
|
+
`✅ PR for ${storyBranch} is already MERGED: ${existing.url} — ` +
|
|
156
|
+
'reporting that outcome instead of opening a second PR.',
|
|
157
|
+
);
|
|
158
|
+
return { url: existing.url, alreadyMerged: true, created: false };
|
|
70
159
|
}
|
|
71
160
|
} catch (err) {
|
|
72
161
|
// `gh pr list` failure is recoverable — fall through to create. Log
|
|
@@ -76,6 +165,25 @@ export async function ensurePullRequestWith({
|
|
|
76
165
|
);
|
|
77
166
|
}
|
|
78
167
|
|
|
168
|
+
// Nothing to merge → nothing to open. Refused before `gh pr create` so the
|
|
169
|
+
// failure names the empty diff rather than surfacing later as a zero-file
|
|
170
|
+
// squash commit on the base branch.
|
|
171
|
+
const emptyDiff = probeEmptyDiff({
|
|
172
|
+
cwd: _cwd ?? process.cwd(),
|
|
173
|
+
baseBranch,
|
|
174
|
+
storyBranch,
|
|
175
|
+
computeChangeSet: computeChangeSetFn,
|
|
176
|
+
});
|
|
177
|
+
if (emptyDiff.empty) {
|
|
178
|
+
throw new Error(
|
|
179
|
+
`[single-story-close] refusing to open a pull request for ${storyBranch}: ` +
|
|
180
|
+
`the head-versus-base diff (${emptyDiff.baseRef}...${storyBranch}) contains no files. ` +
|
|
181
|
+
'An empty diff has nothing to merge, and a PR opened on it can only squash an empty ' +
|
|
182
|
+
'commit onto the base branch. If the work already landed, confirm the merge instead: ' +
|
|
183
|
+
`node .agents/scripts/single-story-confirm-merge.js --story ${storyId}`,
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
79
187
|
progress('PR', `Opening PR for ${storyBranch} → ${baseBranch}...`);
|
|
80
188
|
// The repo squash-merges and GitHub uses the PR title as the squash
|
|
81
189
|
// subject on `main`. A raw human issue title is not a Conventional
|
|
@@ -110,7 +218,7 @@ export async function ensurePullRequestWith({
|
|
|
110
218
|
]);
|
|
111
219
|
const url = (createResult?.stdout ?? '').trim();
|
|
112
220
|
progress('PR', `✅ Opened: ${url}`);
|
|
113
|
-
return url;
|
|
221
|
+
return { url, alreadyMerged: false, created: true };
|
|
114
222
|
} catch (err) {
|
|
115
223
|
throw new Error(
|
|
116
224
|
`[single-story-close] \`gh pr create\` failed: ${err?.message ?? err}`,
|
|
@@ -42,7 +42,12 @@ export function pushStoryBranch({
|
|
|
42
42
|
}) {
|
|
43
43
|
progress('GIT', `Pushing ${storyBranch} to origin...`);
|
|
44
44
|
try {
|
|
45
|
-
|
|
45
|
+
// No hook-bypass flag here, deliberately. Close runs its own gate chain
|
|
46
|
+
// before this point, but `--skip-validation` skips that chain, and the
|
|
47
|
+
// bypass then left nothing running at all. `pre-push` is the backstop,
|
|
48
|
+
// and it only became reachable once hooks were materialized into
|
|
49
|
+
// worktrees — which is where every Story branch is built.
|
|
50
|
+
gitSync(cwd, 'push', '-u', 'origin', storyBranch);
|
|
46
51
|
progress('GIT', `✅ Pushed ${storyBranch}.`);
|
|
47
52
|
} catch (err) {
|
|
48
53
|
throw new Error(
|