mandrel 2.64.0 → 2.65.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 +3 -2
- package/.agents/agents/auditor.md +3 -2
- package/.agents/agents/plan-critic.md +3 -2
- package/.agents/agents/story-worker.md +2 -2
- package/.agents/audit-checklists/quality.md +3 -0
- package/.agents/docs/agentrc-reference.json +1 -9
- package/.agents/docs/configuration.md +8 -7
- package/.agents/schemas/agentrc.schema.json +6 -13
- package/.agents/schemas/audit-rules.schema.json +1 -1
- package/.agents/schemas/story-deliver-terminal.schema.json +5 -0
- package/.agents/scripts/bootstrap.js +8 -2
- package/.agents/scripts/check-context-budget.js +1 -1
- package/.agents/scripts/lib/ITicketingProvider.js +1 -3
- package/.agents/scripts/lib/audit-suite/findings.js +1 -17
- package/.agents/scripts/lib/audit-suite/frontmatter.js +0 -28
- package/.agents/scripts/lib/audit-suite/index.js +0 -6
- package/.agents/scripts/lib/audit-suite/selector.js +0 -31
- package/.agents/scripts/lib/bootstrap/agents-md-fold.js +156 -0
- package/.agents/scripts/lib/bootstrap/commit-push.js +1 -1
- package/.agents/scripts/lib/bootstrap/manifest.js +2 -2
- package/.agents/scripts/lib/bootstrap/project-bootstrap.js +13 -29
- package/.agents/scripts/lib/config/review-chain-default.js +13 -0
- package/.agents/scripts/lib/config-settings-schema-delivery.js +2 -2
- package/.agents/scripts/lib/config-settings-schema-quality.js +11 -13
- package/.agents/scripts/lib/doc-tiers.js +25 -6
- package/.agents/scripts/lib/generated/agentrc-validator.js +1 -1
- package/.agents/scripts/lib/observability/metrics-ledger.js +0 -72
- package/.agents/scripts/lib/orchestration/code-review.js +11 -6
- package/.agents/scripts/lib/orchestration/epic-rollup.js +29 -12
- package/.agents/scripts/lib/orchestration/merge-block-class.js +20 -4
- package/.agents/scripts/lib/orchestration/merge-poll.js +41 -22
- package/.agents/scripts/lib/orchestration/required-checks.js +147 -0
- package/.agents/scripts/lib/orchestration/review-providers/code-review.js +203 -0
- package/.agents/scripts/lib/orchestration/review-providers/review-provider-factory.js +6 -4
- package/.agents/scripts/lib/orchestration/review-providers/security-review.js +3 -2
- package/.agents/scripts/lib/orchestration/single-story-close/failed-terminal.js +1 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +0 -12
- package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +27 -11
- package/.agents/scripts/lib/orchestration/single-story-close/phases/post-land.js +112 -82
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +72 -5
- package/.agents/scripts/lib/orchestration/story-close/phases/review-core.js +12 -87
- package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +3 -0
- package/.agents/scripts/lib/templates/decomposer-prompts.js +5 -24
- package/.agents/scripts/providers/github/issues.js +14 -23
- package/.agents/scripts/sync-claude-agents.js +1 -1
- package/.agents/workflows/audit-quality.md +42 -7
- package/.agents/workflows/helpers/acceptance-self-eval.md +1 -1
- package/.agents/workflows/helpers/code-review.md +15 -38
- package/.agents/workflows/helpers/deliver-reference.md +4 -2
- package/.agents/workflows/helpers/deliver-story.md +3 -0
- package/.agents/workflows/helpers/plan-reference.md +9 -8
- package/.agents/workflows/mandrel-deliver.md +2 -1
- package/.agents/workflows/mandrel-plan.md +10 -7
- package/.agents/workflows/mandrel-update.md +5 -3
- package/docs/CHANGELOG.md +31 -0
- package/lib/cli/claude-code-version.js +73 -0
- package/lib/cli/doctor.js +2 -2
- package/lib/cli/registry.js +9 -0
- package/lib/cli/uninstall.js +37 -9
- package/lib/migrations/index.js +2 -0
- package/lib/migrations/steps/2.65.0-fold-claude-md-into-agents-md.js +38 -0
- package/package.json +2 -1
- package/.agents/scripts/lib/audit-suite/lens-diff-floor.js +0 -99
- package/.agents/scripts/lib/audit-suite/runner.js +0 -205
- package/.agents/scripts/lib/audit-suite/substitutions.js +0 -96
- package/.agents/scripts/lib/audit-suite/workflow-loader.js +0 -37
- package/.agents/scripts/lib/orchestration/story-close/phases/local-lens-review.js +0 -234
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* review-providers/code-review.js — a low-effort model bug review of the
|
|
3
|
+
* Story diff through `claude --print --effort low`. It asks only for
|
|
4
|
+
* merge-blocking problems, so every parsed finding is `critical` and halts
|
|
5
|
+
* close before auto-merge. The reviewer is handed the diff range, the Story
|
|
6
|
+
* id and the diff text — never `acceptance[]` or the self-eval verdict: this
|
|
7
|
+
* is a bug review, not a second acceptance scoring.
|
|
8
|
+
*
|
|
9
|
+
* A missing CLI throws at construction (the default chain entry is
|
|
10
|
+
* `optional: true`, so hosts without it skip). A review that cannot run or
|
|
11
|
+
* whose output does not parse degrades to one non-halting `suggestion`.
|
|
12
|
+
*
|
|
13
|
+
* @typedef {import('./types.js').Finding} Finding
|
|
14
|
+
* @typedef {import('./types.js').ReviewInput} ReviewInput
|
|
15
|
+
* @typedef {import('./types.js').ReviewProvider} ReviewProvider
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { spawnCapture } from '../../child-exec.js';
|
|
19
|
+
import { gitSpawn } from '../../git-utils.js';
|
|
20
|
+
import { PROJECT_ROOT } from '../../project-root.js';
|
|
21
|
+
import { parseProviderFindings } from './parse-findings.js';
|
|
22
|
+
import { renderDepthDirective } from './review-depth.js';
|
|
23
|
+
import { probeClaudeCli } from './security-review.js';
|
|
24
|
+
|
|
25
|
+
const CLAUDE_ARGS = Object.freeze(['--print', '--effort', 'low']);
|
|
26
|
+
const INVOKE_TIMEOUT_MS = 10 * 60 * 1000;
|
|
27
|
+
/** Larger diffs are truncated; the reviewer is told so. */
|
|
28
|
+
const MAX_DIFF_CHARS = 200_000;
|
|
29
|
+
|
|
30
|
+
const PROMPT_HEAD =
|
|
31
|
+
'You are reviewing the diff `{baseRef}...{headRef}` for Story #{ticketId} ' +
|
|
32
|
+
'before it merges. {depthDirective}\n\n' +
|
|
33
|
+
'Report ONLY problems you would block this merge for: a bug that makes ' +
|
|
34
|
+
'the change behave incorrectly, crash, lose data, or break an existing ' +
|
|
35
|
+
'caller. Do not report style, naming, refactoring ideas, missing tests or ' +
|
|
36
|
+
'anything you would merge anyway. For each problem give the file, the ' +
|
|
37
|
+
'line in the new version, why it is wrong, and how to show it fails (an ' +
|
|
38
|
+
'input, command or test that exposes it).\n\n' +
|
|
39
|
+
'The diff below is authoritative — files on disk may not reflect it. ' +
|
|
40
|
+
'Emit ONLY a JSON array on stdout with this exact shape, no prose around ' +
|
|
41
|
+
'it:\n\n' +
|
|
42
|
+
'[{"title":"...","body":"Why it is wrong: ... How to show it fails: ...",' +
|
|
43
|
+
'"file":"...","line":1,"category":"bug"}]\n\n' +
|
|
44
|
+
'Emit [] if there is nothing you would block the merge for.\n\n';
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @param {ReviewInput} input
|
|
48
|
+
* @param {string} diff
|
|
49
|
+
* @returns {string}
|
|
50
|
+
*/
|
|
51
|
+
function buildPrompt(input, diff) {
|
|
52
|
+
const truncated = diff.length > MAX_DIFF_CHARS;
|
|
53
|
+
const body = truncated ? diff.slice(0, MAX_DIFF_CHARS) : diff;
|
|
54
|
+
const note = truncated
|
|
55
|
+
? `\n[diff truncated at ${MAX_DIFF_CHARS} characters]\n`
|
|
56
|
+
: '';
|
|
57
|
+
const head = PROMPT_HEAD.replace('{baseRef}', input.baseRef)
|
|
58
|
+
.replace('{headRef}', input.headRef)
|
|
59
|
+
.replace('{ticketId}', String(input.ticketId))
|
|
60
|
+
.replace('{depthDirective}', renderDepthDirective(input.depth));
|
|
61
|
+
return `${head}<diff>\n${body}${note}\n</diff>\n`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The prompt rides stdin so no shell ever quotes it.
|
|
66
|
+
*
|
|
67
|
+
* @param {string} prompt
|
|
68
|
+
* @param {Function} [run] - `spawnSync`-shaped seam for tests.
|
|
69
|
+
* @returns {{ status: number, stdout: string, stderr: string }}
|
|
70
|
+
*/
|
|
71
|
+
function invokeClaude(prompt, run) {
|
|
72
|
+
return spawnCapture('claude', [...CLAUDE_ARGS], {
|
|
73
|
+
cwd: PROJECT_ROOT,
|
|
74
|
+
input: prompt,
|
|
75
|
+
shell: process.platform === 'win32',
|
|
76
|
+
timeout: INVOKE_TIMEOUT_MS,
|
|
77
|
+
...(run ? { run } : {}),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** A model often fences its JSON; strip one surrounding fence. */
|
|
82
|
+
function stripFence(text) {
|
|
83
|
+
const match = /^\s*```[a-z]*\s*\n([\s\S]*?)\n\s*```\s*$/i.exec(text ?? '');
|
|
84
|
+
return match ? match[1] : (text ?? '');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* @param {string} title
|
|
89
|
+
* @param {string} detail
|
|
90
|
+
* @returns {Finding}
|
|
91
|
+
*/
|
|
92
|
+
function advisory(title, detail) {
|
|
93
|
+
return {
|
|
94
|
+
severity: 'suggestion',
|
|
95
|
+
title,
|
|
96
|
+
body:
|
|
97
|
+
`${detail} The model bug review did not produce a verdict — inspect ` +
|
|
98
|
+
'the diff manually before merging. Advisory only; the chain did not halt.',
|
|
99
|
+
category: 'bug',
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* @param {string} stdout
|
|
105
|
+
* @returns {Finding[]}
|
|
106
|
+
*/
|
|
107
|
+
function parseFindings(stdout) {
|
|
108
|
+
try {
|
|
109
|
+
return parseProviderFindings(stripFence(stdout), {
|
|
110
|
+
errorPrefix: '[code-review] Failed to parse reviewer stdout as JSON',
|
|
111
|
+
mapSeverity: () => 'critical',
|
|
112
|
+
defaultCategory: 'bug',
|
|
113
|
+
});
|
|
114
|
+
} catch (err) {
|
|
115
|
+
return [
|
|
116
|
+
advisory(
|
|
117
|
+
'Code review output not parseable as JSON',
|
|
118
|
+
`The reviewer returned text that did not parse as a JSON findings array (${err.message}).`,
|
|
119
|
+
),
|
|
120
|
+
];
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* @param {ReviewInput} input
|
|
126
|
+
*/
|
|
127
|
+
function assertInput(input) {
|
|
128
|
+
const { baseRef, headRef, ticketId } = input ?? {};
|
|
129
|
+
if (!baseRef || !headRef) {
|
|
130
|
+
throw new TypeError(
|
|
131
|
+
'[code-review] runReview requires baseRef and headRef.',
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
if (!Number.isInteger(ticketId) || ticketId <= 0) {
|
|
135
|
+
throw new TypeError(
|
|
136
|
+
'[code-review] runReview requires a positive integer ticketId.',
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* @param {{
|
|
143
|
+
* probeFn?: () => boolean,
|
|
144
|
+
* gitSpawnFn?: typeof gitSpawn,
|
|
145
|
+
* spawnFn?: Function,
|
|
146
|
+
* logger?: { info?: Function, warn?: Function },
|
|
147
|
+
* }} [deps] - `spawnFn` replaces `spawnSync` for the `claude` call.
|
|
148
|
+
* @returns {ReviewProvider}
|
|
149
|
+
*/
|
|
150
|
+
export function createCodeReviewProviderForRegistry(deps = {}) {
|
|
151
|
+
const probeFn = deps.probeFn ?? probeClaudeCli;
|
|
152
|
+
if (!probeFn()) {
|
|
153
|
+
throw new Error(
|
|
154
|
+
'[ReviewProviderFactory] codeReview provider "code-review" requires ' +
|
|
155
|
+
'the `claude` CLI on PATH but it was not detected. Install the ' +
|
|
156
|
+
'Claude Code CLI, or keep the entry `optional: true` so hosts ' +
|
|
157
|
+
'without it skip the model bug review.',
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
const gitSpawnFn = deps.gitSpawnFn ?? gitSpawn;
|
|
161
|
+
const { spawnFn, logger } = deps;
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
async runReview(input) {
|
|
165
|
+
assertInput(input);
|
|
166
|
+
const { baseRef, headRef, ticketId } = input;
|
|
167
|
+
const diff = gitSpawnFn(
|
|
168
|
+
PROJECT_ROOT,
|
|
169
|
+
'diff',
|
|
170
|
+
'--no-color',
|
|
171
|
+
`${baseRef}...${headRef}`,
|
|
172
|
+
);
|
|
173
|
+
if (diff.status !== 0) {
|
|
174
|
+
return [
|
|
175
|
+
advisory(
|
|
176
|
+
'Code review could not read the diff',
|
|
177
|
+
`\`git diff ${baseRef}...${headRef}\` failed: ${diff.stderr || '<no output>'}.`,
|
|
178
|
+
),
|
|
179
|
+
];
|
|
180
|
+
}
|
|
181
|
+
if (diff.stdout.trim().length === 0) return [];
|
|
182
|
+
|
|
183
|
+
logger?.info?.(
|
|
184
|
+
`[code-review] Invoking claude --print --effort low for Story #${ticketId} (${baseRef}...${headRef})...`,
|
|
185
|
+
);
|
|
186
|
+
const result = invokeClaude(buildPrompt(input, diff.stdout), spawnFn);
|
|
187
|
+
if (result.status !== 0) {
|
|
188
|
+
logger?.warn?.(
|
|
189
|
+
`[code-review] claude exited ${result.status}; emitting advisory.`,
|
|
190
|
+
);
|
|
191
|
+
return [
|
|
192
|
+
advisory(
|
|
193
|
+
'Code review did not complete',
|
|
194
|
+
`\`claude --print\` exited with status ${result.status}: ${
|
|
195
|
+
result.stderr || result.stdout || '<no output>'
|
|
196
|
+
}.`,
|
|
197
|
+
),
|
|
198
|
+
];
|
|
199
|
+
}
|
|
200
|
+
return parseFindings(result.stdout);
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
}
|
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
* @typedef {import('./types.js').ProviderChain} ProviderChain
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
+
import { DEFAULT_REVIEW_PROVIDERS } from '../../config/review-chain-default.js';
|
|
18
|
+
import { createCodeReviewProviderForRegistry } from './code-review.js';
|
|
17
19
|
import { createCodexProviderForRegistry } from './codex.js';
|
|
18
20
|
import { mergeChainDegradations } from './degraded-gates.js';
|
|
19
21
|
import { createNativeProviderForRegistry } from './native.js';
|
|
@@ -22,6 +24,7 @@ import { createUltrareviewProviderForRegistry } from './ultrareview.js';
|
|
|
22
24
|
|
|
23
25
|
/** @type {Readonly<Record<string, () => ReviewProvider>>} */
|
|
24
26
|
const INLINE_PROVIDERS = Object.freeze({
|
|
27
|
+
'code-review': createCodeReviewProviderForRegistry,
|
|
25
28
|
codex: createCodexProviderForRegistry,
|
|
26
29
|
native: createNativeProviderForRegistry,
|
|
27
30
|
'security-review': createSecurityReviewProviderForRegistry,
|
|
@@ -36,8 +39,6 @@ const PROMPT_PROVIDERS = Object.freeze({
|
|
|
36
39
|
ultrareview: createUltrareviewProviderForRegistry,
|
|
37
40
|
});
|
|
38
41
|
|
|
39
|
-
export const DEFAULT_PROVIDER_NAME = 'native';
|
|
40
|
-
|
|
41
42
|
/**
|
|
42
43
|
* Gate predicate from a `when` clause (`label` / `labelAny`); absent → always true.
|
|
43
44
|
*
|
|
@@ -79,7 +80,8 @@ export function isScopeApplicable(declaredScopes, currentScope) {
|
|
|
79
80
|
}
|
|
80
81
|
|
|
81
82
|
/**
|
|
82
|
-
* Takes the `codeReview` sub-object; unset/empty `providers`
|
|
83
|
+
* Takes the `codeReview` sub-object; unset/empty `providers` falls back to
|
|
84
|
+
* `DEFAULT_REVIEW_PROVIDERS`.
|
|
83
85
|
*
|
|
84
86
|
* @param {{
|
|
85
87
|
* providers?: Array<object>,
|
|
@@ -105,7 +107,7 @@ export function createReviewProvider(codeReviewConfig, opts = {}) {
|
|
|
105
107
|
Array.isArray(codeReviewConfig.providers) &&
|
|
106
108
|
codeReviewConfig.providers.length > 0
|
|
107
109
|
? codeReviewConfig.providers
|
|
108
|
-
:
|
|
110
|
+
: DEFAULT_REVIEW_PROVIDERS;
|
|
109
111
|
|
|
110
112
|
const chain = buildProviderChain(entries, {
|
|
111
113
|
inlineRegistry,
|
|
@@ -24,11 +24,12 @@ export const SECURITY_REVIEW_REMEDIATIONS = Object.freeze({
|
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
26
|
* Synchronous so a missing CLI surfaces at construction, not mid-review.
|
|
27
|
+
* Shared by every `claude --print` provider.
|
|
27
28
|
*
|
|
28
29
|
* @param {{ spawnFn?: typeof spawnSync }} [opts]
|
|
29
30
|
* @returns {boolean}
|
|
30
31
|
*/
|
|
31
|
-
function
|
|
32
|
+
export function probeClaudeCli(opts = {}) {
|
|
32
33
|
const spawnFn = opts.spawnFn ?? spawnSync;
|
|
33
34
|
try {
|
|
34
35
|
const result = spawnFn('claude', ['--version'], {
|
|
@@ -173,7 +174,7 @@ export function buildUnparseableFallbackFinding() {
|
|
|
173
174
|
* @returns {ReviewProvider}
|
|
174
175
|
*/
|
|
175
176
|
export function createSecurityReviewProvider(deps = {}) {
|
|
176
|
-
const probeFn = deps.probeFn ??
|
|
177
|
+
const probeFn = deps.probeFn ?? probeClaudeCli;
|
|
177
178
|
if (!probeFn()) {
|
|
178
179
|
throw buildSecurityReviewUnavailableError();
|
|
179
180
|
}
|
|
@@ -107,6 +107,7 @@ export function failedTerminalFor(err, args = {}) {
|
|
|
107
107
|
failure: { reason: String(err?.message ?? err) },
|
|
108
108
|
nextCommand: NEXT_COMMANDS.recover(storyId),
|
|
109
109
|
elapsedSeconds: 0,
|
|
110
|
+
phaseDurations: err?.closePhaseDurations ?? undefined,
|
|
110
111
|
});
|
|
111
112
|
} catch (buildErr) {
|
|
112
113
|
Logger.error(
|
|
@@ -56,8 +56,6 @@ async function invokeStoryReviewCore({
|
|
|
56
56
|
prNumber,
|
|
57
57
|
provider,
|
|
58
58
|
runCodeReviewFn,
|
|
59
|
-
runLocalLensReviewFn,
|
|
60
|
-
appendFindingsYieldFn,
|
|
61
59
|
gitSpawnFn,
|
|
62
60
|
progress,
|
|
63
61
|
}) {
|
|
@@ -71,8 +69,6 @@ async function invokeStoryReviewCore({
|
|
|
71
69
|
progressTag: 'REVIEW',
|
|
72
70
|
runCodeReviewFn,
|
|
73
71
|
gitSpawnFn,
|
|
74
|
-
...(runLocalLensReviewFn ? { runLocalLensReviewFn } : {}),
|
|
75
|
-
...(appendFindingsYieldFn ? { appendFindingsYieldFn } : {}),
|
|
76
72
|
});
|
|
77
73
|
}
|
|
78
74
|
|
|
@@ -131,8 +127,6 @@ async function postStoryReviewCrossRef({
|
|
|
131
127
|
* prNumber: number|null,
|
|
132
128
|
* provider: object,
|
|
133
129
|
* runCodeReviewFn: Function,
|
|
134
|
-
* runLocalLensReviewFn?: Function,
|
|
135
|
-
* appendFindingsYieldFn?: Function,
|
|
136
130
|
* gitSpawnFn?: Function,
|
|
137
131
|
* progress: (tag: string, msg: string) => void,
|
|
138
132
|
* }} args
|
|
@@ -145,7 +139,6 @@ async function postStoryReviewCrossRef({
|
|
|
145
139
|
* degraded?: boolean,
|
|
146
140
|
* degradations?: Array<object>,
|
|
147
141
|
* crossRefPosted?: boolean,
|
|
148
|
-
* localLensReview?: object,
|
|
149
142
|
* }>}
|
|
150
143
|
*/
|
|
151
144
|
export async function runStoryScopeReview({
|
|
@@ -157,8 +150,6 @@ export async function runStoryScopeReview({
|
|
|
157
150
|
prNumber,
|
|
158
151
|
provider,
|
|
159
152
|
runCodeReviewFn,
|
|
160
|
-
runLocalLensReviewFn,
|
|
161
|
-
appendFindingsYieldFn,
|
|
162
153
|
gitSpawnFn,
|
|
163
154
|
progress,
|
|
164
155
|
}) {
|
|
@@ -192,8 +183,6 @@ export async function runStoryScopeReview({
|
|
|
192
183
|
prNumber,
|
|
193
184
|
provider,
|
|
194
185
|
runCodeReviewFn,
|
|
195
|
-
runLocalLensReviewFn,
|
|
196
|
-
appendFindingsYieldFn,
|
|
197
186
|
gitSpawnFn,
|
|
198
187
|
progress,
|
|
199
188
|
});
|
|
@@ -229,6 +218,5 @@ export async function runStoryScopeReview({
|
|
|
229
218
|
postedCommentId: result.postedCommentId ?? null,
|
|
230
219
|
...degradationEnvelope(result.degradations),
|
|
231
220
|
crossRefPosted,
|
|
232
|
-
localLensReview: result.localLensReview,
|
|
233
221
|
};
|
|
234
222
|
}
|
|
@@ -44,14 +44,15 @@ import {
|
|
|
44
44
|
decideMergeWaitFailFast,
|
|
45
45
|
deriveChecksStatus,
|
|
46
46
|
deriveRedHeadRuns,
|
|
47
|
-
deriveRequiredRunEvidence,
|
|
48
47
|
isPrMerged,
|
|
49
48
|
MERGE_WAIT_GH_TIMEOUT_MS,
|
|
50
49
|
parseWorkflowRunId,
|
|
50
|
+
pollIntervalMs,
|
|
51
51
|
readRunSummary,
|
|
52
52
|
resolveAdvisoryGateVerdict,
|
|
53
53
|
} from '../../merge-poll.js';
|
|
54
54
|
import { readMergeQueueState } from '../../merge-queue.js';
|
|
55
|
+
import { readProbeRunEvidence } from '../../required-checks.js';
|
|
55
56
|
import { NEXT_COMMANDS } from '../../story-deliver-terminal.js';
|
|
56
57
|
import {
|
|
57
58
|
postStructuredComment,
|
|
@@ -155,6 +156,7 @@ export async function readPrWaitProbe({
|
|
|
155
156
|
gh = defaultGh,
|
|
156
157
|
ghTimeoutMs = MERGE_WAIT_GH_TIMEOUT_MS,
|
|
157
158
|
readMergeQueueStateFn = readMergeQueueState,
|
|
159
|
+
readRequiredCheckNamesFn,
|
|
158
160
|
}) {
|
|
159
161
|
try {
|
|
160
162
|
const view = await withGhTimeout(
|
|
@@ -188,7 +190,14 @@ export async function readPrWaitProbe({
|
|
|
188
190
|
}),
|
|
189
191
|
// Head-anchored, so superseded/pending runs don't read as red; `null`
|
|
190
192
|
// when the rollup is empty (the consecutive-probe fallback owns that).
|
|
191
|
-
requiredRunEvidence:
|
|
193
|
+
requiredRunEvidence: await readProbeRunEvidence({
|
|
194
|
+
view,
|
|
195
|
+
checksStatus,
|
|
196
|
+
prNumber,
|
|
197
|
+
gh,
|
|
198
|
+
ghTimeoutMs,
|
|
199
|
+
readFn: readRequiredCheckNamesFn,
|
|
200
|
+
}),
|
|
192
201
|
redHeadRuns: deriveRedHeadRuns(view?.statusCheckRollup),
|
|
193
202
|
headSha: readString(view?.headRefOid),
|
|
194
203
|
};
|
|
@@ -1105,7 +1114,7 @@ export async function runConfirmMergePhase({
|
|
|
1105
1114
|
remaining: rerunAllowance,
|
|
1106
1115
|
issued: new Set(),
|
|
1107
1116
|
};
|
|
1108
|
-
|
|
1117
|
+
let intervalMs = intervalSeconds * 1000;
|
|
1109
1118
|
const startedAtMs = nowMsFn();
|
|
1110
1119
|
let anchorMs = startedAtMs;
|
|
1111
1120
|
let updatesUsed = 0;
|
|
@@ -1129,6 +1138,7 @@ export async function runConfirmMergePhase({
|
|
|
1129
1138
|
ghTimeoutMs,
|
|
1130
1139
|
});
|
|
1131
1140
|
polls += 1;
|
|
1141
|
+
intervalMs = pollIntervalMs(probe.checksStatus, intervalSeconds);
|
|
1132
1142
|
|
|
1133
1143
|
anchorMs = resolveBudgetAnchorMs({
|
|
1134
1144
|
createdAt: probe.createdAt,
|
|
@@ -1147,12 +1157,7 @@ export async function runConfirmMergePhase({
|
|
|
1147
1157
|
// Heartbeat: a backgrounded close's output-file growth is its liveness signal.
|
|
1148
1158
|
progress?.(
|
|
1149
1159
|
'CONFIRM',
|
|
1150
|
-
|
|
1151
|
-
`checks=${probe.checksStatus ?? 'unknown'} ` +
|
|
1152
|
-
`mergeState=${probe.mergeStateStatus ?? 'unknown'} ` +
|
|
1153
|
-
`(${waitBudget.waitedSeconds}s of ${maxWaitSeconds}s this invocation; ` +
|
|
1154
|
-
`${waitBudget.cumulativeSeconds}s of ${maxBudgetSeconds}s cumulative)` +
|
|
1155
|
-
(probe.error ? ` — probe error: ${probe.error}` : ''),
|
|
1160
|
+
pollHeartbeat({ polls, prNumber, probe, waitBudget }),
|
|
1156
1161
|
);
|
|
1157
1162
|
|
|
1158
1163
|
if (isPrMerged(probe)) {
|
|
@@ -1324,13 +1329,24 @@ export async function runConfirmMergePhase({
|
|
|
1324
1329
|
},
|
|
1325
1330
|
predicate: (result) => result?.done === true,
|
|
1326
1331
|
intervalMs,
|
|
1327
|
-
// No pollUntil timeout: the tick owns both bounds and
|
|
1328
|
-
sleepFn: (
|
|
1332
|
+
// No pollUntil timeout: the tick owns both bounds and the cadence.
|
|
1333
|
+
sleepFn: () => sleepFn(intervalMs),
|
|
1329
1334
|
});
|
|
1330
1335
|
if (tick.thrown) throw tick.thrown;
|
|
1331
1336
|
return tick.outcome;
|
|
1332
1337
|
}
|
|
1333
1338
|
|
|
1339
|
+
function pollHeartbeat({ polls, prNumber, probe, waitBudget }) {
|
|
1340
|
+
return (
|
|
1341
|
+
`⏱ poll ${polls}: PR #${prNumber} state=${probe.state ?? 'unknown'} ` +
|
|
1342
|
+
`checks=${probe.checksStatus ?? 'unknown'} ` +
|
|
1343
|
+
`mergeState=${probe.mergeStateStatus ?? 'unknown'} ` +
|
|
1344
|
+
`(${waitBudget.waitedSeconds}s of ${waitBudget.maxWaitSeconds}s this invocation; ` +
|
|
1345
|
+
`${waitBudget.cumulativeSeconds}s of ${waitBudget.maxBudgetSeconds}s cumulative)` +
|
|
1346
|
+
(probe.error ? ` — probe error: ${probe.error}` : '')
|
|
1347
|
+
);
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1334
1350
|
function doneWith(outcome) {
|
|
1335
1351
|
return { done: true, outcome };
|
|
1336
1352
|
}
|
|
@@ -282,11 +282,61 @@ async function stepPlanRunLabelReap({
|
|
|
282
282
|
}
|
|
283
283
|
|
|
284
284
|
/**
|
|
285
|
-
*
|
|
286
|
-
*
|
|
287
|
-
*
|
|
288
|
-
|
|
289
|
-
|
|
285
|
+
* The local-checkout mutations (ref reap first, then fast-forward) under a
|
|
286
|
+
* cross-process lock on the main checkout, since concurrent closes race on
|
|
287
|
+
* the base ref and worktree registry. Best-effort: on timeout they run anyway.
|
|
288
|
+
*/
|
|
289
|
+
async function runLockedLocalSteps({
|
|
290
|
+
storyId,
|
|
291
|
+
storyBranch,
|
|
292
|
+
baseBranch,
|
|
293
|
+
cwd,
|
|
294
|
+
config,
|
|
295
|
+
progress,
|
|
296
|
+
gitSpawnFn,
|
|
297
|
+
planFastForwardFn,
|
|
298
|
+
executeFastForwardFn,
|
|
299
|
+
acquireLockWithWaitFn,
|
|
300
|
+
}) {
|
|
301
|
+
const lockCfg = config?.delivery?.postLandLock ?? {};
|
|
302
|
+
const lock = await acquireLockWithWaitFn({
|
|
303
|
+
lockPath: postLandLockPath(cwd),
|
|
304
|
+
waitMs: lockCfg.waitMs,
|
|
305
|
+
pollMs: lockCfg.pollMs,
|
|
306
|
+
timeoutMs: lockCfg.timeoutMs,
|
|
307
|
+
ownerId: `post-land-${storyId}`,
|
|
308
|
+
});
|
|
309
|
+
if (!lock.acquired) {
|
|
310
|
+
progress?.(
|
|
311
|
+
'POST-LAND',
|
|
312
|
+
`⚠️ post-land lock not acquired (${lock.reason}); proceeding unserialized.`,
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
try {
|
|
316
|
+
const refCleanup = await step(
|
|
317
|
+
() => stepRefCleanup({ cwd, storyBranch, progress, gitSpawnFn }),
|
|
318
|
+
{ name: 'local ref cleanup', progress },
|
|
319
|
+
);
|
|
320
|
+
const baseFastForward = await step(
|
|
321
|
+
() =>
|
|
322
|
+
stepBaseFastForward({
|
|
323
|
+
cwd,
|
|
324
|
+
baseBranch,
|
|
325
|
+
progress,
|
|
326
|
+
planFastForwardFn,
|
|
327
|
+
executeFastForwardFn,
|
|
328
|
+
}),
|
|
329
|
+
{ name: 'base fast-forward', progress },
|
|
330
|
+
);
|
|
331
|
+
return { refCleanup, baseFastForward };
|
|
332
|
+
} finally {
|
|
333
|
+
if (lock.acquired) lock.release();
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Run the post-land tail: friction markers, then the GitHub steps and the
|
|
339
|
+
* locked local steps concurrently, then temp purge, then lease release.
|
|
290
340
|
*
|
|
291
341
|
* @param {object} args
|
|
292
342
|
* @param {number} args.storyId
|
|
@@ -348,87 +398,67 @@ export async function runPostLandTail({
|
|
|
348
398
|
config,
|
|
349
399
|
});
|
|
350
400
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
provider,
|
|
356
|
-
config,
|
|
357
|
-
cwd,
|
|
358
|
-
progress,
|
|
359
|
-
captureStoryFollowUpsFn,
|
|
360
|
-
}),
|
|
361
|
-
{ name: 'follow-up capture', progress },
|
|
362
|
-
);
|
|
363
|
-
const statusResync = await step(
|
|
364
|
-
() =>
|
|
365
|
-
stepStatusResync({
|
|
366
|
-
storyId,
|
|
367
|
-
provider,
|
|
368
|
-
config,
|
|
369
|
-
progress,
|
|
370
|
-
reassertStatusColumnFn,
|
|
371
|
-
}),
|
|
372
|
-
{ name: 'status-column resync', progress },
|
|
373
|
-
);
|
|
374
|
-
await step(
|
|
375
|
-
() =>
|
|
376
|
-
stepPlanRunLabelReap({
|
|
377
|
-
storyId,
|
|
378
|
-
provider,
|
|
379
|
-
progress,
|
|
380
|
-
reapPlanRunLabelsForStoryFn,
|
|
381
|
-
}),
|
|
382
|
-
{ name: 'plan-run label reap', progress },
|
|
383
|
-
);
|
|
384
|
-
|
|
385
|
-
const epicRollup = await step(
|
|
386
|
-
() =>
|
|
387
|
-
stepEpicRollup({
|
|
388
|
-
storyId,
|
|
389
|
-
provider,
|
|
390
|
-
config,
|
|
391
|
-
progress,
|
|
392
|
-
rollUpEpicForStoryFn,
|
|
393
|
-
}),
|
|
394
|
-
{ name: 'epic rollup', progress },
|
|
395
|
-
);
|
|
396
|
-
|
|
397
|
-
const lockCfg = config?.delivery?.postLandLock ?? {};
|
|
398
|
-
const lock = await acquireLockWithWaitFn({
|
|
399
|
-
lockPath: postLandLockPath(cwd),
|
|
400
|
-
waitMs: lockCfg.waitMs,
|
|
401
|
-
pollMs: lockCfg.pollMs,
|
|
402
|
-
timeoutMs: lockCfg.timeoutMs,
|
|
403
|
-
ownerId: `post-land-${storyId}`,
|
|
404
|
-
});
|
|
405
|
-
if (!lock.acquired) {
|
|
406
|
-
progress?.(
|
|
407
|
-
'POST-LAND',
|
|
408
|
-
`⚠️ post-land lock not acquired (${lock.reason}); proceeding unserialized.`,
|
|
409
|
-
);
|
|
410
|
-
}
|
|
411
|
-
let refCleanup;
|
|
412
|
-
let baseFastForward;
|
|
413
|
-
try {
|
|
414
|
-
refCleanup = await step(
|
|
415
|
-
() => stepRefCleanup({ cwd, storyBranch, progress, gitSpawnFn }),
|
|
416
|
-
{ name: 'local ref cleanup', progress },
|
|
417
|
-
);
|
|
418
|
-
baseFastForward = await step(
|
|
401
|
+
// The GitHub steps are independent of each other and of the local
|
|
402
|
+
// lock-held mutations, so all of them run concurrently.
|
|
403
|
+
const [followUps, statusResync, , epicRollup, local] = await Promise.all([
|
|
404
|
+
step(
|
|
419
405
|
() =>
|
|
420
|
-
|
|
406
|
+
stepFollowUps({
|
|
407
|
+
storyId,
|
|
408
|
+
provider,
|
|
409
|
+
config,
|
|
421
410
|
cwd,
|
|
422
|
-
baseBranch,
|
|
423
411
|
progress,
|
|
424
|
-
|
|
425
|
-
executeFastForwardFn,
|
|
412
|
+
captureStoryFollowUpsFn,
|
|
426
413
|
}),
|
|
427
|
-
{ name: '
|
|
428
|
-
)
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
414
|
+
{ name: 'follow-up capture', progress },
|
|
415
|
+
),
|
|
416
|
+
step(
|
|
417
|
+
() =>
|
|
418
|
+
stepStatusResync({
|
|
419
|
+
storyId,
|
|
420
|
+
provider,
|
|
421
|
+
config,
|
|
422
|
+
progress,
|
|
423
|
+
reassertStatusColumnFn,
|
|
424
|
+
}),
|
|
425
|
+
{ name: 'status-column resync', progress },
|
|
426
|
+
),
|
|
427
|
+
step(
|
|
428
|
+
() =>
|
|
429
|
+
stepPlanRunLabelReap({
|
|
430
|
+
storyId,
|
|
431
|
+
provider,
|
|
432
|
+
progress,
|
|
433
|
+
reapPlanRunLabelsForStoryFn,
|
|
434
|
+
}),
|
|
435
|
+
{ name: 'plan-run label reap', progress },
|
|
436
|
+
),
|
|
437
|
+
step(
|
|
438
|
+
() =>
|
|
439
|
+
stepEpicRollup({
|
|
440
|
+
storyId,
|
|
441
|
+
provider,
|
|
442
|
+
config,
|
|
443
|
+
progress,
|
|
444
|
+
rollUpEpicForStoryFn,
|
|
445
|
+
}),
|
|
446
|
+
{ name: 'epic rollup', progress },
|
|
447
|
+
),
|
|
448
|
+
runLockedLocalSteps({
|
|
449
|
+
storyId,
|
|
450
|
+
storyBranch,
|
|
451
|
+
baseBranch,
|
|
452
|
+
cwd,
|
|
453
|
+
config,
|
|
454
|
+
progress,
|
|
455
|
+
gitSpawnFn,
|
|
456
|
+
planFastForwardFn,
|
|
457
|
+
executeFastForwardFn,
|
|
458
|
+
acquireLockWithWaitFn,
|
|
459
|
+
}),
|
|
460
|
+
]);
|
|
461
|
+
const { refCleanup, baseFastForward } = local;
|
|
432
462
|
|
|
433
463
|
// After every step that reads the temp artifacts; `signals.ndjson` survives.
|
|
434
464
|
const tempPurge = await step(
|