mergestorm 0.3.37 → 0.3.39
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/README.md +1 -1
- package/dist/automation-catalog.d.ts +13 -2
- package/dist/automation-catalog.js +2 -0
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.js +1 -1
- package/dist/commands/browse.d.ts +1 -1
- package/dist/commands/registry.js +2 -0
- package/dist/commands/review-client.d.ts +8 -1
- package/dist/commands/review-client.js +18 -3
- package/dist/commands/settings.d.ts +1 -1
- package/dist/commands/settings.js +7 -6
- package/dist/stack-blockers.d.ts +32 -0
- package/dist/stack-blockers.js +237 -17
- package/dist/stack-dto.d.ts +2 -2
- package/dist/stack-dto.js +2 -0
- package/dist/ui/banner.js +6 -0
- package/package.json +1 -1
- package/skill/mergestorm-bounce-watch/SKILL.md +4 -1
- package/skill/mergestorm-pr-loop/SKILL.md +1 -1
package/README.md
CHANGED
|
@@ -45,7 +45,7 @@ On a TTY, bare `mergestorm` (or `mergestorm shell`) opens a branded REPL: a comp
|
|
|
45
45
|
|
|
46
46
|
```
|
|
47
47
|
╭──────────────────────────────────────────────────────────────────────────────╮
|
|
48
|
-
│ ▀▀▀▀▀▀▀▀▀▀▘ mergestorm v0.3.
|
|
48
|
+
│ ▀▀▀▀▀▀▀▀▀▀▘ mergestorm v0.3.39 │
|
|
49
49
|
│ ▀▀▀▀▀▀▀▀▀ ● msk_live_… · maelstrom │
|
|
50
50
|
│ ▀▀▀▀▀▀ [████████░░░░░░░░░░░░░░░░░░░░░░] 25% used │
|
|
51
51
|
│ ▝▀▀▀▀▀ local reviews + stacked PRs · mergestorm.ai │
|
|
@@ -49,6 +49,17 @@ export declare const BEARER_SETTINGS: readonly [{
|
|
|
49
49
|
readonly kind: "seconds";
|
|
50
50
|
readonly min: 15;
|
|
51
51
|
readonly max: 300;
|
|
52
|
+
}, {
|
|
53
|
+
readonly key: "merge_queue_batch_enabled";
|
|
54
|
+
readonly flag: "--merge-queue-batch";
|
|
55
|
+
readonly label: "Merge queue batching";
|
|
56
|
+
}, {
|
|
57
|
+
readonly key: "merge_queue_batch_size";
|
|
58
|
+
readonly flag: "--merge-queue-batch-size";
|
|
59
|
+
readonly label: "Merge queue batch size (pull requests)";
|
|
60
|
+
readonly kind: "count";
|
|
61
|
+
readonly min: 2;
|
|
62
|
+
readonly max: 8;
|
|
52
63
|
}, {
|
|
53
64
|
readonly key: "ignored_bot_logins";
|
|
54
65
|
readonly flag: "--ignore-bot";
|
|
@@ -82,9 +93,9 @@ export type BearerSettingsValues = {
|
|
|
82
93
|
[Row in (typeof BEARER_SETTINGS)[number] as Row["key"]]: Row extends {
|
|
83
94
|
kind: "logins";
|
|
84
95
|
} ? string[] : Row extends {
|
|
85
|
-
kind: "seconds";
|
|
96
|
+
kind: "seconds" | "count";
|
|
86
97
|
} ? number : Row extends {
|
|
87
98
|
values: readonly (infer Value)[];
|
|
88
99
|
} ? Value : boolean;
|
|
89
100
|
};
|
|
90
|
-
export declare const BEARER_BOOLEAN_SETTINGS_KEYS: ("auto_review_enabled" | "auto_patch_enabled" | "vortex_show_thinking_traces" | "repo_overview_enabled" | "review_unit_land_prs_enabled" | "cyclone_skip_ci_enabled" | "cyclone_patch_unverified_languages" | "vortex_auto_overflow_enabled" | "vortex_skip_all_clear_comments" | "vortex_seam_specialist_enabled" | "auto_land_default" | "auto_land_settle_seconds" | "ignored_bot_logins" | "vortex_bot_skip_check" | "vortex_findings_check" | "cyclone_patch_failure_check")[];
|
|
101
|
+
export declare const BEARER_BOOLEAN_SETTINGS_KEYS: ("auto_review_enabled" | "auto_patch_enabled" | "vortex_show_thinking_traces" | "repo_overview_enabled" | "review_unit_land_prs_enabled" | "cyclone_skip_ci_enabled" | "cyclone_patch_unverified_languages" | "vortex_auto_overflow_enabled" | "vortex_skip_all_clear_comments" | "vortex_seam_specialist_enabled" | "auto_land_default" | "auto_land_settle_seconds" | "merge_queue_batch_enabled" | "merge_queue_batch_size" | "ignored_bot_logins" | "vortex_bot_skip_check" | "vortex_findings_check" | "cyclone_patch_failure_check")[];
|
|
@@ -11,6 +11,8 @@ export const BEARER_SETTINGS = [
|
|
|
11
11
|
{ key: "vortex_seam_specialist_enabled", flag: "--vortex-seam", label: "Vortex seam specialist" },
|
|
12
12
|
{ key: "auto_land_default", flag: "--auto-land", label: "Auto land (new stacks)" },
|
|
13
13
|
{ key: "auto_land_settle_seconds", flag: "--auto-land-settle", label: "Auto land settle (seconds)", kind: "seconds", min: 15, max: 300 },
|
|
14
|
+
{ key: "merge_queue_batch_enabled", flag: "--merge-queue-batch", label: "Merge queue batching" },
|
|
15
|
+
{ key: "merge_queue_batch_size", flag: "--merge-queue-batch-size", label: "Merge queue batch size (pull requests)", kind: "count", min: 2, max: 8 },
|
|
14
16
|
{ key: "ignored_bot_logins", flag: "--ignore-bot", label: "Ignored bot logins", kind: "logins" },
|
|
15
17
|
{ key: "vortex_bot_skip_check", flag: "--vortex-skip-check", label: "Vortex bot skip check", kind: "enum", values: ["none", "neutral"] },
|
|
16
18
|
{ key: "vortex_findings_check", flag: "--vortex-findings", label: "Vortex findings check", kind: "enum", values: ["failure", "neutral", "success"] },
|
package/dist/client/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ export { DEFAULT_API, apiBase, clearConfig, configPath, keyDisplay, loadConfig,
|
|
|
2
2
|
export { BEARER_SETTINGS, BEARER_BOOLEAN_SETTINGS_KEYS, BEARER_SETTINGS_FLAGS, BEARER_SETTINGS_KEYS, BEARER_SETTINGS_LABELS, type BearerSettingsKey, } from "../automation-catalog.js";
|
|
3
3
|
export { API_TIMEOUT_PREFIX, DEFAULT_API_TIMEOUT_MS, SETTINGS_WRITABLE_KEYS, adoptStack, apiFetch, cancelMergeQueueEntry, devicePost, dismissPrFindings, enqueueStack, ensureUpperPark, getEnrichedStack, getMe, getPrVortexReview, getReview, getSettings, landNextStack, listJobs, listMergeQueueEntries, listStacks, listThreads, parseRetryAfterSeconds, patchSettings, restackStack, setStackAutoLand, setStackPolicy, stackPolicyBody, type ApiFetchInit, type ApiFetchResult, type JobListItem, type MeResponse, type MergeQueueBounceDetail, type MergeQueueBounceKind, type MergeQueueEnqueuedBy, type MergeQueueEnqueuedVia, type MergeQueueEntryDto, type MergeQueueEntryState, type PrDismissedFinding, type PrFindingDismissInput, type PrFindingDismissOutcome, type PrFindingDismissResult, type PrReviewFinding, type PrReviewFindings, type PrVortexReview, type SettingsPatch, type SettingsResponse, type SettingsWritableKey, type StackDto, type StackPolicyPatch, type ThreadDetail, type ThreadListItem, } from "../api.js";
|
|
4
4
|
export { CommandError, DetachedError, RATE_LIMITED_HINT, REVIEW_EXIT, isCommandErrorCode, rateLimitedMessage, type CommandErrorCode, type CommandErrorExtras, } from "../errors.js";
|
|
5
|
-
export { REVIEW_POLL_BACKOFF_MS, REVIEW_POLL_DEFAULT_TIMEOUT_MS, REVIEW_POLL_INTERVAL_MS, REVIEW_POLL_MAX_INTERVAL_MS, REVIEW_POLL_MAX_TRANSIENT_RETRIES, REVIEW_POLL_STRETCH_AFTER_MS, REVIEW_POLL_STRETCH_SPAN_MS, REVIEW_THREAD_SLUG_RE, PrReviewPollTimeoutError, ReviewPollTimeoutError, collectReviewInput, formatReviewSubmitError, isTransientReviewPollError, isTransientReviewPollStatus, loadReviewContext, pollPrVortexReview, pollReview, stretchedPollIntervalMs, submitReview, transientRetryWaitMs, type PollReviewOptions, type PollPrVortexReviewOptions, type ReviewInput, type SubmitReviewOptions, } from "../commands/review-client.js";
|
|
5
|
+
export { REVIEW_POLL_BACKOFF_MS, REVIEW_POLL_DEFAULT_TIMEOUT_MS, REVIEW_POLL_INTERVAL_MS, REVIEW_POLL_MAX_INTERVAL_MS, REVIEW_POLL_MAX_TRANSIENT_RETRIES, REVIEW_POLL_STRETCH_AFTER_MS, REVIEW_POLL_STRETCH_SPAN_MS, REVIEW_THREAD_SLUG_RE, PrReviewPollTimeoutError, ReviewPollTimeoutError, noPrReviewYetMessage, collectReviewInput, formatReviewSubmitError, isTransientReviewPollError, isTransientReviewPollStatus, loadReviewContext, pollPrVortexReview, pollReview, stretchedPollIntervalMs, submitReview, transientRetryWaitMs, type PollReviewOptions, type PollPrVortexReviewOptions, type ReviewInput, type SubmitReviewOptions, } from "../commands/review-client.js";
|
|
6
6
|
export { REVIEW_JOB_ENVELOPE_SCHEMA, toReviewJobEnvelope, type ReviewEnvelopeFallbacks, type ReviewJobEnvelope, type ReviewJobRow, } from "../ui/job-envelope.js";
|
|
7
7
|
export { discoverTrunk } from "../git-stack.js";
|
|
8
8
|
export { BOUNCE_KIND_LABELS, deriveStackAgentsBusy, mergeQueueBounceLabel, type StackAgentsBusy } from "../stack-dto.js";
|
package/dist/client/index.js
CHANGED
|
@@ -2,7 +2,7 @@ export { DEFAULT_API, apiBase, clearConfig, configPath, keyDisplay, loadConfig,
|
|
|
2
2
|
export { BEARER_SETTINGS, BEARER_BOOLEAN_SETTINGS_KEYS, BEARER_SETTINGS_FLAGS, BEARER_SETTINGS_KEYS, BEARER_SETTINGS_LABELS, } from "../automation-catalog.js";
|
|
3
3
|
export { API_TIMEOUT_PREFIX, DEFAULT_API_TIMEOUT_MS, SETTINGS_WRITABLE_KEYS, adoptStack, apiFetch, cancelMergeQueueEntry, devicePost, dismissPrFindings, enqueueStack, ensureUpperPark, getEnrichedStack, getMe, getPrVortexReview, getReview, getSettings, landNextStack, listJobs, listMergeQueueEntries, listStacks, listThreads, parseRetryAfterSeconds, patchSettings, restackStack, setStackAutoLand, setStackPolicy, stackPolicyBody, } from "../api.js";
|
|
4
4
|
export { CommandError, DetachedError, RATE_LIMITED_HINT, REVIEW_EXIT, isCommandErrorCode, rateLimitedMessage, } from "../errors.js";
|
|
5
|
-
export { REVIEW_POLL_BACKOFF_MS, REVIEW_POLL_DEFAULT_TIMEOUT_MS, REVIEW_POLL_INTERVAL_MS, REVIEW_POLL_MAX_INTERVAL_MS, REVIEW_POLL_MAX_TRANSIENT_RETRIES, REVIEW_POLL_STRETCH_AFTER_MS, REVIEW_POLL_STRETCH_SPAN_MS, REVIEW_THREAD_SLUG_RE, PrReviewPollTimeoutError, ReviewPollTimeoutError, collectReviewInput, formatReviewSubmitError, isTransientReviewPollError, isTransientReviewPollStatus, loadReviewContext, pollPrVortexReview, pollReview, stretchedPollIntervalMs, submitReview, transientRetryWaitMs, } from "../commands/review-client.js";
|
|
5
|
+
export { REVIEW_POLL_BACKOFF_MS, REVIEW_POLL_DEFAULT_TIMEOUT_MS, REVIEW_POLL_INTERVAL_MS, REVIEW_POLL_MAX_INTERVAL_MS, REVIEW_POLL_MAX_TRANSIENT_RETRIES, REVIEW_POLL_STRETCH_AFTER_MS, REVIEW_POLL_STRETCH_SPAN_MS, REVIEW_THREAD_SLUG_RE, PrReviewPollTimeoutError, ReviewPollTimeoutError, noPrReviewYetMessage, collectReviewInput, formatReviewSubmitError, isTransientReviewPollError, isTransientReviewPollStatus, loadReviewContext, pollPrVortexReview, pollReview, stretchedPollIntervalMs, submitReview, transientRetryWaitMs, } from "../commands/review-client.js";
|
|
6
6
|
export { REVIEW_JOB_ENVELOPE_SCHEMA, toReviewJobEnvelope, } from "../ui/job-envelope.js";
|
|
7
7
|
export { discoverTrunk } from "../git-stack.js";
|
|
8
8
|
export { BOUNCE_KIND_LABELS, deriveStackAgentsBusy, mergeQueueBounceLabel } from "../stack-dto.js";
|
|
@@ -27,7 +27,7 @@ export declare function buildStatusTabLines(input: StatusTabInput): string[];
|
|
|
27
27
|
/** In-place job detail for the Jobs tab (same envelope as `status <id>`). */
|
|
28
28
|
export declare function formatJobDetailLines(row: ReviewJobRow): string[];
|
|
29
29
|
/** Human labels for the Config tab and the static `settings` printout. */
|
|
30
|
-
export declare const SETTINGS_LABELS: Record<"auto_review_enabled" | "auto_patch_enabled" | "vortex_show_thinking_traces" | "repo_overview_enabled" | "review_unit_land_prs_enabled" | "cyclone_skip_ci_enabled" | "cyclone_patch_unverified_languages" | "vortex_auto_overflow_enabled" | "vortex_skip_all_clear_comments" | "vortex_seam_specialist_enabled" | "auto_land_default" | "auto_land_settle_seconds" | "ignored_bot_logins" | "vortex_bot_skip_check" | "vortex_findings_check" | "cyclone_patch_failure_check", string>;
|
|
30
|
+
export declare const SETTINGS_LABELS: Record<"auto_review_enabled" | "auto_patch_enabled" | "vortex_show_thinking_traces" | "repo_overview_enabled" | "review_unit_land_prs_enabled" | "cyclone_skip_ci_enabled" | "cyclone_patch_unverified_languages" | "vortex_auto_overflow_enabled" | "vortex_skip_all_clear_comments" | "vortex_seam_specialist_enabled" | "auto_land_default" | "auto_land_settle_seconds" | "merge_queue_batch_enabled" | "merge_queue_batch_size" | "ignored_bot_logins" | "vortex_bot_skip_check" | "vortex_findings_check" | "cyclone_patch_failure_check", string>;
|
|
31
31
|
/** Config rows in a stable order: writable toggles, then connected flags. */
|
|
32
32
|
export declare function buildConfigRows(settings: SettingsResponse): TabsConfigRow[];
|
|
33
33
|
/**
|
|
@@ -186,6 +186,8 @@ export const COMMAND_REGISTRY = [
|
|
|
186
186
|
" [--vortex-seam on|off] [--vortex-skip-all-clear on|off]",
|
|
187
187
|
" [--auto-land on|off] Auto land default for new stacks",
|
|
188
188
|
" [--auto-land-settle <seconds>] How long Auto land waits before queueing (15-300, default 60)",
|
|
189
|
+
" [--merge-queue-batch on|off] Test several queued pull requests on one CI run",
|
|
190
|
+
" [--merge-queue-batch-size <n>] Most pull requests per batch (2-8, default 4)",
|
|
189
191
|
" [--ignore-bot add <login>|remove <login>|clear]",
|
|
190
192
|
" [--vortex-skip-check none|neutral] [--vortex-findings failure|neutral|success]",
|
|
191
193
|
" [--cyclone-fail-check failure|neutral]",
|
|
@@ -84,12 +84,19 @@ export declare class ReviewPollTimeoutError extends Error {
|
|
|
84
84
|
readonly lastRow: ReviewJobRow;
|
|
85
85
|
constructor(jobId: string, lastRow: ReviewJobRow);
|
|
86
86
|
}
|
|
87
|
+
export type PrReviewWaitTarget = {
|
|
88
|
+
afterSha?: string;
|
|
89
|
+
pass?: number;
|
|
90
|
+
afterPass?: number;
|
|
91
|
+
};
|
|
92
|
+
export declare function noPrReviewYetMessage(owner: string, repo: string, prNumber: number, target?: PrReviewWaitTarget): string;
|
|
87
93
|
export declare class PrReviewPollTimeoutError extends Error {
|
|
88
94
|
readonly owner: string;
|
|
89
95
|
readonly repo: string;
|
|
90
96
|
readonly prNumber: number;
|
|
91
97
|
readonly lastEnvelope: PrVortexReview | null;
|
|
92
|
-
|
|
98
|
+
readonly reviewSeen: boolean;
|
|
99
|
+
constructor(owner: string, repo: string, prNumber: number, lastEnvelope: PrVortexReview | null, target?: PrReviewWaitTarget, reviewSeen?: boolean);
|
|
93
100
|
}
|
|
94
101
|
export type PollReviewOptions = {
|
|
95
102
|
timeoutMs?: number;
|
|
@@ -249,18 +249,31 @@ export class ReviewPollTimeoutError extends Error {
|
|
|
249
249
|
this.lastRow = lastRow;
|
|
250
250
|
}
|
|
251
251
|
}
|
|
252
|
+
export function noPrReviewYetMessage(owner, repo, prNumber, target = {}) {
|
|
253
|
+
const sha = target.afterSha?.trim();
|
|
254
|
+
const pass = target.pass !== undefined
|
|
255
|
+
? `Vortex pass ${target.pass}`
|
|
256
|
+
: target.afterPass !== undefined ? `Vortex pass after pass ${target.afterPass}` : "Vortex review";
|
|
257
|
+
return `No ${pass} exists for ${owner}/${repo}#${prNumber}${sha ? ` at ${sha.slice(0, 12)}` : ""} yet. ` +
|
|
258
|
+
"If the push was in the last few minutes, wait once more. If it is still missing after that, Vortex is not reviewing this head " +
|
|
259
|
+
"(auto-review may be off for the repo or the stack, or the push did not reach Mergestorm): " +
|
|
260
|
+
`comment "@mergestorm-vortex review" on the PR to run it, or review the branch locally with mg review; a new push also queues a review.`;
|
|
261
|
+
}
|
|
252
262
|
export class PrReviewPollTimeoutError extends Error {
|
|
253
263
|
owner;
|
|
254
264
|
repo;
|
|
255
265
|
prNumber;
|
|
256
266
|
lastEnvelope;
|
|
257
|
-
|
|
258
|
-
|
|
267
|
+
reviewSeen;
|
|
268
|
+
constructor(owner, repo, prNumber, lastEnvelope, target = {}, reviewSeen = lastEnvelope !== null) {
|
|
269
|
+
super(`Timed out waiting for PR review ${owner}/${repo}#${prNumber}` +
|
|
270
|
+
(reviewSeen ? "" : `. ${noPrReviewYetMessage(owner, repo, prNumber, target)}`));
|
|
259
271
|
this.name = "PrReviewPollTimeoutError";
|
|
260
272
|
this.owner = owner;
|
|
261
273
|
this.repo = repo;
|
|
262
274
|
this.prNumber = prNumber;
|
|
263
275
|
this.lastEnvelope = lastEnvelope;
|
|
276
|
+
this.reviewSeen = reviewSeen;
|
|
264
277
|
}
|
|
265
278
|
}
|
|
266
279
|
function shaMatches(actual, expected) {
|
|
@@ -286,6 +299,7 @@ export async function pollPrVortexReview(cfg, owner, repo, prNumber, opts = {})
|
|
|
286
299
|
applyPrReviewQuery(query, opts);
|
|
287
300
|
let transientFailures = 0;
|
|
288
301
|
let lastEnvelope = null;
|
|
302
|
+
let apiTimeoutSeen = false;
|
|
289
303
|
const sleepBeforeRetry = async (retryAfterSeconds) => {
|
|
290
304
|
const waitMs = Math.min(transientRetryWaitMs({
|
|
291
305
|
retryAfterSeconds,
|
|
@@ -315,6 +329,7 @@ export async function pollPrVortexReview(cfg, owner, repo, prNumber, opts = {})
|
|
|
315
329
|
if (isCommandErrorCode(err, "api_timeout")) {
|
|
316
330
|
if (opts.propagateTimeout)
|
|
317
331
|
throw err;
|
|
332
|
+
apiTimeoutSeen = true;
|
|
318
333
|
break;
|
|
319
334
|
}
|
|
320
335
|
if (isTransientReviewPollError(err)) {
|
|
@@ -364,7 +379,7 @@ export async function pollPrVortexReview(cfg, owner, repo, prNumber, opts = {})
|
|
|
364
379
|
return lastEnvelope;
|
|
365
380
|
opts.onTick?.("progress");
|
|
366
381
|
}
|
|
367
|
-
throw new PrReviewPollTimeoutError(owner, repo, prNumber, lastEnvelope);
|
|
382
|
+
throw new PrReviewPollTimeoutError(owner, repo, prNumber, lastEnvelope, opts, lastEnvelope !== null || apiTimeoutSeen);
|
|
368
383
|
}
|
|
369
384
|
export async function pollReview(cfg, jobId, opts = {}) {
|
|
370
385
|
const timeoutMs = opts.timeoutMs ?? REVIEW_POLL_DEFAULT_TIMEOUT_MS;
|
|
@@ -3,7 +3,7 @@ import { type SettingsPatch, type SettingsResponse } from "../api.js";
|
|
|
3
3
|
* CLI flag → Bearer settings key. Boolean flags take on|off. The connected flags are
|
|
4
4
|
* read-only on the API and deliberately have no flag here.
|
|
5
5
|
*/
|
|
6
|
-
export declare const SETTINGS_FLAGS: Record<string, "auto_review_enabled" | "auto_patch_enabled" | "vortex_show_thinking_traces" | "repo_overview_enabled" | "review_unit_land_prs_enabled" | "cyclone_skip_ci_enabled" | "cyclone_patch_unverified_languages" | "vortex_auto_overflow_enabled" | "vortex_skip_all_clear_comments" | "vortex_seam_specialist_enabled" | "auto_land_default" | "auto_land_settle_seconds" | "ignored_bot_logins" | "vortex_bot_skip_check" | "vortex_findings_check" | "cyclone_patch_failure_check">;
|
|
6
|
+
export declare const SETTINGS_FLAGS: Record<string, "auto_review_enabled" | "auto_patch_enabled" | "vortex_show_thinking_traces" | "repo_overview_enabled" | "review_unit_land_prs_enabled" | "cyclone_skip_ci_enabled" | "cyclone_patch_unverified_languages" | "vortex_auto_overflow_enabled" | "vortex_skip_all_clear_comments" | "vortex_seam_specialist_enabled" | "auto_land_default" | "auto_land_settle_seconds" | "merge_queue_batch_enabled" | "merge_queue_batch_size" | "ignored_bot_logins" | "vortex_bot_skip_check" | "vortex_findings_check" | "cyclone_patch_failure_check">;
|
|
7
7
|
type IgnoreBotOperation = {
|
|
8
8
|
action: "add" | "remove";
|
|
9
9
|
login: string;
|
|
@@ -46,12 +46,13 @@ export function parseSettingsArgs(args) {
|
|
|
46
46
|
}
|
|
47
47
|
continue;
|
|
48
48
|
}
|
|
49
|
-
if ("kind" in row && row.kind === "seconds") {
|
|
50
|
-
const
|
|
51
|
-
if (!Number.isInteger(
|
|
52
|
-
|
|
49
|
+
if ("kind" in row && (row.kind === "seconds" || row.kind === "count")) {
|
|
50
|
+
const amount = value !== undefined && /^\d+$/.test(value) ? Number(value) : Number.NaN;
|
|
51
|
+
if (!Number.isInteger(amount) || amount < row.min || amount > row.max) {
|
|
52
|
+
const unit = row.kind === "seconds" ? " of seconds" : "";
|
|
53
|
+
throw new CommandError(`${flag} takes a whole number${unit} from ${row.min} through ${row.max}.`, 2, "usage");
|
|
53
54
|
}
|
|
54
|
-
Object.assign(patch, { [key]:
|
|
55
|
+
Object.assign(patch, { [key]: amount });
|
|
55
56
|
continue;
|
|
56
57
|
}
|
|
57
58
|
if ("kind" in row && row.kind === "enum") {
|
|
@@ -85,7 +86,7 @@ export function formatSettingsLines(settings) {
|
|
|
85
86
|
: typeof row.value === "string"
|
|
86
87
|
? row.value
|
|
87
88
|
: typeof row.value === "number"
|
|
88
|
-
? `${row.value}s`
|
|
89
|
+
? "kind" in row && row.kind === "seconds" ? `${row.value}s` : String(row.value)
|
|
89
90
|
: row.writable
|
|
90
91
|
? row.value
|
|
91
92
|
? "on"
|
package/dist/stack-blockers.d.ts
CHANGED
|
@@ -104,6 +104,14 @@ export type StackRepairHint = {
|
|
|
104
104
|
branch: string;
|
|
105
105
|
since: string;
|
|
106
106
|
steps: string;
|
|
107
|
+
} | {
|
|
108
|
+
kind: "ci_unreadable" | "auto_land_waiting";
|
|
109
|
+
prNumber: number;
|
|
110
|
+
headSha: string | null;
|
|
111
|
+
branch: string;
|
|
112
|
+
since: string;
|
|
113
|
+
reason: string;
|
|
114
|
+
steps: string;
|
|
107
115
|
} | {
|
|
108
116
|
kind: "queue_stalled";
|
|
109
117
|
prNumber: number;
|
|
@@ -112,6 +120,20 @@ export type StackRepairHint = {
|
|
|
112
120
|
since: string | null;
|
|
113
121
|
waitReason: string | null;
|
|
114
122
|
steps: string;
|
|
123
|
+
} | {
|
|
124
|
+
kind: "members_remaining" | "vortex_failed" | "tempest_trigger_failed";
|
|
125
|
+
prNumber: number;
|
|
126
|
+
headSha: string | null;
|
|
127
|
+
branch: string;
|
|
128
|
+
detail: string | null;
|
|
129
|
+
steps: string;
|
|
130
|
+
} | {
|
|
131
|
+
kind: "land_pr_unavailable";
|
|
132
|
+
prNumber: number;
|
|
133
|
+
headSha: string | null;
|
|
134
|
+
branch: string;
|
|
135
|
+
detail: string | null;
|
|
136
|
+
steps: string;
|
|
115
137
|
} | {
|
|
116
138
|
kind: "vortex_skipped";
|
|
117
139
|
prNumber: number;
|
|
@@ -152,10 +174,18 @@ export declare const VORTEX_FINDINGS_NEED_PERSON = "Vortex findings need a perso
|
|
|
152
174
|
export declare const VORTEX_AUTO_REVIEW_OFF = "Vortex auto-review is off, so this head was not reviewed";
|
|
153
175
|
export declare const VORTEX_BILLING_SKIPPED = "Vortex skipped this head: it could not confirm billing";
|
|
154
176
|
export declare const VORTEX_REVIEW_INCOMPLETE = "Vortex reviewed only part of this PR";
|
|
177
|
+
export declare const VORTEX_IGNORED_AUTHOR = "Vortex skipped this head: the PR author is on the ignored authors list";
|
|
178
|
+
export declare const VORTEX_REPO_DISABLED = "Vortex skipped this head: reviews are turned off for this repository";
|
|
179
|
+
export declare const VORTEX_RETRY_SCHEDULED = "Vortex hit a temporary error on this head and will retry by itself";
|
|
180
|
+
export declare const VORTEX_REVIEW_FAILED = "Review failed";
|
|
181
|
+
export declare function vortexRetryScheduled(layer: StackLayerDto): boolean;
|
|
155
182
|
export declare const AUTO_LAND_CI_PENDING_ATTENTION_MS: number;
|
|
156
183
|
export declare const AUTO_LAND_VORTEX_WAIT_ATTENTION_MS: number;
|
|
157
184
|
export declare const AUTO_LAND_PROMOTE_FAILED_ATTEMPTS = 3;
|
|
158
185
|
export declare const AUTO_LAND_CI_FAILED_BLOCKER = "CI failed \u2014 Auto land reads a failing check on GitHub at this head";
|
|
186
|
+
export declare const AUTO_LAND_WAIT_ATTENTION_MS: number;
|
|
187
|
+
export declare const AUTO_LAND_CI_UNREADABLE_ATTENTION_MS: number;
|
|
188
|
+
export declare function autoLandWaitLayer(stack: StackDto): StackLayerDto | undefined;
|
|
159
189
|
export declare function autoLandWaitBlocker(stack: StackDto, layer: StackLayerDto, nowMs: number): string | null;
|
|
160
190
|
export declare const MERGE_QUEUE_STALL_ATTENTION_MS: number;
|
|
161
191
|
export type StackQueueWait = {
|
|
@@ -168,6 +198,7 @@ export type StackQueueWait = {
|
|
|
168
198
|
export type MergeQueueWait = {
|
|
169
199
|
text: string;
|
|
170
200
|
stalled: boolean;
|
|
201
|
+
notPickedUp?: boolean;
|
|
171
202
|
since: string | null;
|
|
172
203
|
waitReason: string | null;
|
|
173
204
|
aheadStackId: string | null;
|
|
@@ -182,6 +213,7 @@ export declare function restackFailedRepairSteps(branch: string, liveParent: str
|
|
|
182
213
|
}): string;
|
|
183
214
|
export declare function agentsCannotClear(blocker: string): boolean;
|
|
184
215
|
export declare function stackRepair(stack: StackDto, attention: StackBlocker | null, layer: StackLayerDto | undefined, bounce: MergeQueueEntryDto | undefined, queueWait?: MergeQueueWait | null): StackRepairHint | null;
|
|
216
|
+
export declare const LAND_PR_MISSING_PREFIX = "No open land PR for ";
|
|
185
217
|
/** Pair-gate attention plus live hard blocks on every other open layer. */
|
|
186
218
|
export declare function stackBlockers(stack: StackDto, entries?: MergeQueueEntryDto[], cursor?: Pick<StackWatchCursor, "bounceId" | "afterFinishedAt">, nowMs?: number): {
|
|
187
219
|
attention: null;
|
package/dist/stack-blockers.js
CHANGED
|
@@ -79,6 +79,13 @@ const CYCLONE_HANDOFF_WORDS = {
|
|
|
79
79
|
stack_auto_patch_off: "auto-patch is off for this stack",
|
|
80
80
|
account_auto_patch_off: "auto-patch is off for the account and this stack does not turn it on",
|
|
81
81
|
cyclone_not_connected: "Cyclone is not connected to this repo",
|
|
82
|
+
infra_failure: "Cyclone could not start the patch: GitHub sign-in, comment load or checkout kept failing",
|
|
83
|
+
no_changes: "Cyclone found nothing to change for these findings",
|
|
84
|
+
credits_exhausted: "the account is out of standard credits this month",
|
|
85
|
+
no_billing: "the account has no billing set up",
|
|
86
|
+
unsupported_language: "the findings are in a language Cyclone does not patch",
|
|
87
|
+
policy_owner_changed: "the stack's owner changed while the patch was queued",
|
|
88
|
+
patch_job_lost: "Cyclone's patch job was lost before it finished",
|
|
82
89
|
};
|
|
83
90
|
function cycloneHandoffWords(reason) {
|
|
84
91
|
return CYCLONE_HANDOFF_WORDS[reason] ?? reason.replaceAll("_", " ");
|
|
@@ -95,11 +102,21 @@ function quotaSkippedReview(layer) {
|
|
|
95
102
|
export const VORTEX_AUTO_REVIEW_OFF = "Vortex auto-review is off, so this head was not reviewed";
|
|
96
103
|
export const VORTEX_BILLING_SKIPPED = "Vortex skipped this head: it could not confirm billing";
|
|
97
104
|
export const VORTEX_REVIEW_INCOMPLETE = "Vortex reviewed only part of this PR";
|
|
105
|
+
export const VORTEX_IGNORED_AUTHOR = "Vortex skipped this head: the PR author is on the ignored authors list";
|
|
106
|
+
export const VORTEX_REPO_DISABLED = "Vortex skipped this head: reviews are turned off for this repository";
|
|
107
|
+
export const VORTEX_RETRY_SCHEDULED = "Vortex hit a temporary error on this head and will retry by itself";
|
|
108
|
+
export const VORTEX_REVIEW_FAILED = "Review failed";
|
|
98
109
|
const VORTEX_SKIP_BLOCKERS = {
|
|
99
110
|
auto_review_off: VORTEX_AUTO_REVIEW_OFF,
|
|
100
111
|
billing_blocked: VORTEX_BILLING_SKIPPED,
|
|
101
112
|
billing_unavailable: VORTEX_BILLING_SKIPPED,
|
|
113
|
+
ignored_author: VORTEX_IGNORED_AUTHOR,
|
|
114
|
+
repo_disabled: VORTEX_REPO_DISABLED,
|
|
102
115
|
};
|
|
116
|
+
export function vortexRetryScheduled(layer) {
|
|
117
|
+
const review = layer.vortexReview;
|
|
118
|
+
return review?.skip_reason?.trim() === "retry_scheduled" && sameHead(review.head_sha, layer.headSha);
|
|
119
|
+
}
|
|
103
120
|
function vortexIdleSkipBlocker(layer) {
|
|
104
121
|
if (layerAgentsBusy(layer).vortex)
|
|
105
122
|
return null;
|
|
@@ -121,6 +138,12 @@ function vortexSkippedSteps(stack, layer, skipReason) {
|
|
|
121
138
|
if (skipReason === "auto_review_off") {
|
|
122
139
|
return `Vortex auto-review is off for ${stack.owner}/${stack.repo} or for this stack, so it skipped #${layer.prNumber} at ${head} and Auto land waits for a review that will not come. Do not patch or push for this. Tell the human: turning auto-review on (mg stack set ${stack.id} --auto-review on, or the account setting in the dashboard) queues a review of this head by itself. To review only this head and leave auto-review off, ${mention}.`;
|
|
123
140
|
}
|
|
141
|
+
if (skipReason === "ignored_author") {
|
|
142
|
+
return `Vortex skipped #${layer.prNumber} at ${head} because the PR's author is on the account's ignored authors list, so Auto land waits for a review that will not come. Do not patch or push for this. To review this head once, ${mention}. To have this author's PRs reviewed by themselves, tell the human to remove the author from the ignored authors list in the Mergestorm dashboard settings.`;
|
|
143
|
+
}
|
|
144
|
+
if (skipReason === "repo_disabled") {
|
|
145
|
+
return `Vortex skipped #${layer.prNumber} at ${head} because reviews are turned off for ${stack.owner}/${stack.repo}, so Auto land waits for a review that will not come. Do not patch or push for this, and a mention will not run a review while the repository is off. Tell the human to turn the repository on in the Mergestorm dashboard; after that a push or a comment @mergestorm-vortex review on #${layer.prNumber} runs the review.`;
|
|
146
|
+
}
|
|
124
147
|
if (skipReason === "incomplete") {
|
|
125
148
|
return `Vortex reviewed only some of the files in #${layer.prNumber} at ${head} and left the rest unreviewed, so Auto land waits for full coverage. Do not patch or push for this. To review the remaining files, ${mention} (the Continue button on the stack is unrelated), or tell the human to do it; turning on automatic overflow reviews in the dashboard settings covers this for later heads.`;
|
|
126
149
|
}
|
|
@@ -199,8 +222,10 @@ function layerAttention(stack, layer, queue, cursor) {
|
|
|
199
222
|
run.status === "findings" && sameHead(run.sha, layer.headSha));
|
|
200
223
|
if (tempest)
|
|
201
224
|
return hardBlock(`Tempest ${tempest.status}`);
|
|
202
|
-
if (layer.vortexStatus === "failed")
|
|
203
|
-
|
|
225
|
+
if (layer.vortexStatus === "failed" && !vortexRetryScheduled(layer) &&
|
|
226
|
+
(!layer.vortexReview?.head_sha || sameHead(layer.vortexReview.head_sha, layer.headSha))) {
|
|
227
|
+
return hardBlock(VORTEX_REVIEW_FAILED);
|
|
228
|
+
}
|
|
204
229
|
if (quotaSkippedReview(layer))
|
|
205
230
|
return hardBlock(VORTEX_REVIEW_OUT_OF_QUOTA);
|
|
206
231
|
const vortexSkip = vortexIdleSkipBlocker(layer);
|
|
@@ -271,17 +296,84 @@ export const AUTO_LAND_CI_FAILED_BLOCKER = "CI failed — Auto land reads a fail
|
|
|
271
296
|
const AUTO_LAND_CI_PENDING_PREFIX = "CI pending for ";
|
|
272
297
|
const AUTO_LAND_VORTEX_WAIT_PREFIX = "No green Vortex review for ";
|
|
273
298
|
const AUTO_LAND_PROMOTE_FAILED_PREFIX = "Promote failed ";
|
|
299
|
+
export const AUTO_LAND_WAIT_ATTENTION_MS = 45 * 60_000;
|
|
300
|
+
export const AUTO_LAND_CI_UNREADABLE_ATTENTION_MS = 10 * 60_000;
|
|
301
|
+
const AUTO_LAND_CI_UNREADABLE_PREFIX = "Mergestorm cannot read ";
|
|
302
|
+
const AUTO_LAND_WAITED_PREFIX = "Auto land has waited ";
|
|
303
|
+
const AUTO_LAND_MEMBERS_REMAINING_PREFIX = "The land PR cannot be queued: ";
|
|
304
|
+
const CI_UNREADABLE_WHAT = {
|
|
305
|
+
check_runs_unreadable: "the check runs",
|
|
306
|
+
commit_statuses_unreadable: "the commit statuses",
|
|
307
|
+
ci_unreadable: "the check runs and commit statuses",
|
|
308
|
+
};
|
|
309
|
+
const CI_UNREADABLE_PERMISSION = {
|
|
310
|
+
check_runs_unreadable: "Checks: read",
|
|
311
|
+
commit_statuses_unreadable: "Commit statuses: read",
|
|
312
|
+
ci_unreadable: "Checks: read and Commit statuses: read",
|
|
313
|
+
};
|
|
314
|
+
const AUTO_LAND_WAIT_WORDS = {
|
|
315
|
+
mergeability_unknown: "GitHub has not said whether this PR can merge into its base",
|
|
316
|
+
tempest_pending: "Tempest's review of this head has not started",
|
|
317
|
+
tempest_running: "Tempest's review of this head has not finished",
|
|
318
|
+
tempest_trigger_failed: "Mergestorm could not reach Tempest to review the land PR; it keeps trying every 5 minutes",
|
|
319
|
+
tempest_skipped: "Tempest skipped its review of this head",
|
|
320
|
+
members_remaining: "a PR that is not a layer of this stack is still open on the stack branch",
|
|
321
|
+
tempest_stopped: "Tempest's review of this head was stopped",
|
|
322
|
+
tempest_failed: "Tempest's review of this head broke without a result",
|
|
323
|
+
tempest_findings: "Tempest reported findings on this head",
|
|
324
|
+
cyclone_failed: "Cyclone's patch run failed on this head",
|
|
325
|
+
stack_dirty: "this layer is not clean (it has a conflict or needs a restack)",
|
|
326
|
+
must_consolidate: "this PR's base is not the stack's land target, and the stack has no review unit to promote it into",
|
|
327
|
+
missing_head: "GitHub returned no head commit for this PR",
|
|
328
|
+
pr_draft: "this PR is a draft",
|
|
329
|
+
trunk_in_land_target_without_land_pr: "the stack's commits are already in the land target, but no merged land PR was found",
|
|
330
|
+
conflict_unchanged: "the merge queue bounced this PR for a conflict, and neither the PR nor its base has changed since",
|
|
331
|
+
land_pr_unavailable: "the land PR cannot be opened or found",
|
|
332
|
+
github_unreadable: "Mergestorm cannot read this PR from GitHub",
|
|
333
|
+
land_gate_failed: "the check before landing keeps failing with an error",
|
|
334
|
+
readiness_check_failed: "the readiness check for this PR keeps failing with an error",
|
|
335
|
+
layer_without_pr: "a layer of this stack has no pull request",
|
|
336
|
+
parked_base: "this PR's base is a parked branch (mg-park-*) and has not been moved back",
|
|
337
|
+
pr_not_open: "GitHub shows this PR merged or closed, but the stack still lists it as open",
|
|
338
|
+
stack_without_unit: "the stack has more than one open layer but no review unit to promote them into",
|
|
339
|
+
ci_error: "CI reported an error on this head",
|
|
340
|
+
ci_unknown: "CI on this head is in a state Mergestorm cannot read",
|
|
341
|
+
ci_missing: "no CI run exists for this head",
|
|
342
|
+
};
|
|
343
|
+
function autoLandWaitWords(reason) {
|
|
344
|
+
return AUTO_LAND_WAIT_WORDS[reason] ?? reason.replaceAll("_", " ");
|
|
345
|
+
}
|
|
346
|
+
function ciUnreadableForGood(wait) {
|
|
347
|
+
return /\bHTTP 40[34]\b/.test(wait.detail ?? "");
|
|
348
|
+
}
|
|
274
349
|
function waitedMs(since, nowMs) {
|
|
275
350
|
const sinceMs = Date.parse(since);
|
|
276
351
|
return Number.isFinite(sinceMs) ? Math.max(0, nowMs - sinceMs) : null;
|
|
277
352
|
}
|
|
278
|
-
function
|
|
353
|
+
function armedAutoLandWait(stack) {
|
|
279
354
|
const wait = stack.autoLandWait;
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
355
|
+
return wait && !stack.archivedAt && stack.autoEnqueueWhenReady === true ? wait : null;
|
|
356
|
+
}
|
|
357
|
+
function lastPrLayer(stack) {
|
|
358
|
+
const land = stack.unit?.landPr;
|
|
359
|
+
if (land && land.prNumber > 0)
|
|
360
|
+
return land;
|
|
361
|
+
return stack.layers.filter((layer) => layer.prNumber > 0).sort((a, b) => b.position - a.position)[0];
|
|
362
|
+
}
|
|
363
|
+
export function autoLandWaitLayer(stack) {
|
|
364
|
+
const wait = armedAutoLandWait(stack);
|
|
365
|
+
if (!wait)
|
|
366
|
+
return undefined;
|
|
367
|
+
const candidate = currentLayer(stack);
|
|
368
|
+
if (wait.prNumber == null)
|
|
369
|
+
return candidate ?? lastPrLayer(stack);
|
|
370
|
+
const named = candidate?.prNumber === wait.prNumber
|
|
371
|
+
? candidate
|
|
372
|
+
: stack.layers.find((layer) => layer.prNumber === wait.prNumber && !isTerminalLayer(layer));
|
|
373
|
+
return named && (!wait.headSha || sameHead(wait.headSha, named.headSha)) ? named : undefined;
|
|
374
|
+
}
|
|
375
|
+
function autoLandWaitOn(stack, layer) {
|
|
376
|
+
return autoLandWaitLayer(stack)?.prNumber === layer.prNumber ? armedAutoLandWait(stack) : null;
|
|
285
377
|
}
|
|
286
378
|
export function autoLandWaitBlocker(stack, layer, nowMs) {
|
|
287
379
|
const wait = autoLandWaitOn(stack, layer);
|
|
@@ -298,8 +390,10 @@ export function autoLandWaitBlocker(stack, layer, nowMs) {
|
|
|
298
390
|
return waited >= AUTO_LAND_CI_PENDING_ATTENTION_MS
|
|
299
391
|
? `${AUTO_LAND_CI_PENDING_PREFIX}${minutes}m — a check on this head has not finished, so Auto land will not queue it`
|
|
300
392
|
: null;
|
|
393
|
+
case "members_remaining":
|
|
394
|
+
return `${AUTO_LAND_MEMBERS_REMAINING_PREFIX}${wait.detail ?? AUTO_LAND_WAIT_WORDS.members_remaining}`;
|
|
301
395
|
case "vortex_not_green":
|
|
302
|
-
return waited >= AUTO_LAND_VORTEX_WAIT_ATTENTION_MS
|
|
396
|
+
return waited >= AUTO_LAND_VORTEX_WAIT_ATTENTION_MS && !vortexRetryScheduled(layer)
|
|
303
397
|
? `${AUTO_LAND_VORTEX_WAIT_PREFIX}${minutes}m — Auto land is waiting for an approving Vortex review at this head`
|
|
304
398
|
: null;
|
|
305
399
|
case "promote_merge_failed":
|
|
@@ -307,8 +401,17 @@ export function autoLandWaitBlocker(stack, layer, nowMs) {
|
|
|
307
401
|
? `${AUTO_LAND_PROMOTE_FAILED_PREFIX}${wait.attempts} times (merge_failed) — Auto land stopped retrying this head${wait.detail ? `: ${wait.detail}` : ""}`
|
|
308
402
|
: null;
|
|
309
403
|
default:
|
|
310
|
-
|
|
404
|
+
break;
|
|
405
|
+
}
|
|
406
|
+
const unreadable = CI_UNREADABLE_WHAT[wait.reason];
|
|
407
|
+
if (unreadable) {
|
|
408
|
+
return ciUnreadableForGood(wait) || waited >= AUTO_LAND_CI_UNREADABLE_ATTENTION_MS
|
|
409
|
+
? `${AUTO_LAND_CI_UNREADABLE_PREFIX}${unreadable} on this head${wait.detail ? ` (${wait.detail})` : ""} — Auto land cannot tell whether CI passed`
|
|
410
|
+
: null;
|
|
311
411
|
}
|
|
412
|
+
return waited >= AUTO_LAND_WAIT_ATTENTION_MS
|
|
413
|
+
? `${AUTO_LAND_WAITED_PREFIX}${minutes}m: ${autoLandWaitWords(wait.reason)}`
|
|
414
|
+
: null;
|
|
312
415
|
}
|
|
313
416
|
export const MERGE_QUEUE_STALL_ATTENTION_MS = 45 * 60_000;
|
|
314
417
|
const MERGE_QUEUE_STALLED_PREFIX = "Merge queue stuck: ";
|
|
@@ -322,12 +425,14 @@ export function mergeQueueWait(queue, nowMs) {
|
|
|
322
425
|
return null;
|
|
323
426
|
const reason = entry.waitReason?.trim() || null;
|
|
324
427
|
if (entry.state === "queued") {
|
|
325
|
-
const
|
|
428
|
+
const queuedMs = waitedMs(entry.enqueuedAt, nowMs);
|
|
429
|
+
const queuedFor = minutesOf(queuedMs);
|
|
326
430
|
const ahead = entry.aheadInRepo;
|
|
327
431
|
if (!ahead) {
|
|
328
432
|
return {
|
|
329
433
|
text: `Queued ${queuedFor}m in the merge queue, not picked up yet${reason ? ` (last note: ${reason})` : ""}`,
|
|
330
|
-
stalled:
|
|
434
|
+
stalled: queuedMs != null && queuedMs >= MERGE_QUEUE_STALL_ATTENTION_MS,
|
|
435
|
+
notPickedUp: true,
|
|
331
436
|
since: entry.enqueuedAt, waitReason: reason, aheadStackId: null, aheadOther: false,
|
|
332
437
|
};
|
|
333
438
|
}
|
|
@@ -357,6 +462,9 @@ function queueStalledSteps(stack, layer, wait) {
|
|
|
357
462
|
: "That entry belongs to another Mergestorm user, so you cannot fix it from here.";
|
|
358
463
|
return `#${layer.prNumber} is waiting in the merge queue behind another entry in ${stack.owner}/${stack.repo} that has made no progress since ${wait.since ?? "it was claimed"}. Nothing on ${layer.branch} needs a change. ${ahead} If it stays stuck, tell the human: the queue for this repo is held by that entry.`;
|
|
359
464
|
}
|
|
465
|
+
if (wait.notPickedUp) {
|
|
466
|
+
return `#${layer.prNumber} has been queued in the merge queue for ${stack.owner}/${stack.repo} since ${wait.since ?? "it was enqueued"} and nothing is working on it or on an entry ahead of it${wait.waitReason ? `; its last note is: ${wait.waitReason}` : ""}. Nothing on ${layer.branch} needs a change: do not patch or push for this. If the note says the base branch keeps moving, the queue is waiting for a quiet moment on it and continues by itself once pushes to that branch pause. Otherwise tell the human: the merge queue is not picking this entry up; they can take the stack out of the queue and enqueue it again (mg queue rm ${stack.id}, then mg queue add ${stack.id}). Do not retarget the PR base.`;
|
|
467
|
+
}
|
|
360
468
|
return `The merge queue has held #${layer.prNumber} at ${layer.headSha ?? "its head"} since ${wait.since ?? "it was claimed"} without landing or bouncing it${wait.waitReason ? `; its last note is: ${wait.waitReason}` : ", and it recorded no reason"}. If the note names a failing check or a conflict on ${layer.branch}, fix that with an ordinary push. If it waits on CI, list the head's checks (gh run list --branch ${layer.branch}) and re-run the one that never finished (gh run rerun <run-id>). If nothing moves after that, tell the human: they can take the stack out of the queue and enqueue it again (mg queue rm ${stack.id}, then mg queue add ${stack.id}). Do not retarget the PR base.`;
|
|
361
469
|
}
|
|
362
470
|
function isTerminalLayer(layer) {
|
|
@@ -410,11 +518,12 @@ export function agentsCannotClear(blocker) {
|
|
|
410
518
|
return blocker === "Conflict" || blocker === RESTACK_FAILED || blocker === "Draft PR" ||
|
|
411
519
|
blocker === SEAM_REVIEW_NOT_RUNNING || blocker === VORTEX_REVIEW_OUT_OF_QUOTA ||
|
|
412
520
|
blocker === VORTEX_AUTO_REVIEW_OFF || blocker === VORTEX_BILLING_SKIPPED ||
|
|
413
|
-
blocker === VORTEX_REVIEW_INCOMPLETE ||
|
|
521
|
+
blocker === VORTEX_REVIEW_INCOMPLETE || blocker === VORTEX_IGNORED_AUTHOR || blocker === VORTEX_REPO_DISABLED ||
|
|
414
522
|
STUCK_SEAM_BLOCKERS.includes(blocker) ||
|
|
415
523
|
blocker.startsWith("Merge conflicts") || blocker.startsWith(UNIT_ABANDONED_BLOCKER_PREFIX);
|
|
416
524
|
}
|
|
417
|
-
const TEMPEST_RERUN_BLOCKER = /^(?:landing blocked:\s*)?tempest_(failed|stopped)\b/i;
|
|
525
|
+
const TEMPEST_RERUN_BLOCKER = /^(?:landing blocked:\s*)?tempest_(failed|stopped|skipped)\b/i;
|
|
526
|
+
const TEMPEST_TRIGGER_FAILED_BLOCKER = /^(?:landing blocked:\s*)?tempest_trigger_failed\b/i;
|
|
418
527
|
function isTempestFindingsBlocker(attention, bounce) {
|
|
419
528
|
const blocker = attention.blocker.trim();
|
|
420
529
|
const reason = bounce?.bounceDetail?.message?.trim();
|
|
@@ -515,7 +624,8 @@ export function stackRepair(stack, attention, layer, bounce, queueWait = null) {
|
|
|
515
624
|
bounce?.bounceDetail?.message?.trim().match(TEMPEST_RERUN_BLOCKER);
|
|
516
625
|
const rerun = landPr ? rerunBlocker : null;
|
|
517
626
|
if (rerun) {
|
|
518
|
-
const
|
|
627
|
+
const outcome = rerun[1].toLowerCase();
|
|
628
|
+
const stoppedByPerson = outcome === "stopped";
|
|
519
629
|
const comment = `gh pr comment ${layer.prNumber} --repo ${stack.owner}/${stack.repo} --body`;
|
|
520
630
|
const head = layer.headSha ?? "its live head";
|
|
521
631
|
return {
|
|
@@ -526,7 +636,29 @@ export function stackRepair(stack, attention, layer, bounce, queueWait = null) {
|
|
|
526
636
|
stoppedByPerson,
|
|
527
637
|
steps: stoppedByPerson
|
|
528
638
|
? `Tempest's review of land PR #${layer.prNumber} at ${head} was stopped, so Auto land will not land it. There are no findings to fix: do not patch or push. Ask the human whether to run Tempest again. If they say yes, run ${comment} "@mergestorm-tempest review --force" and keep watching.`
|
|
529
|
-
:
|
|
639
|
+
: outcome === "skipped"
|
|
640
|
+
? `Tempest skipped its review of land PR #${layer.prNumber} at ${head}, so Auto land will not land it. There are no findings to fix: do not patch or push. Ask the human to run ${comment} "@mergestorm-tempest review" and keep watching.`
|
|
641
|
+
: `Tempest's review of land PR #${layer.prNumber} at ${head} broke before it produced a result, and Mergestorm already re-ran it the most times it will on its own. There are no findings to fix: do not patch or push. Run it once more with ${comment} "@mergestorm-tempest review" and keep watching. If it breaks again, tell the human.`,
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
if (landPr && TEMPEST_TRIGGER_FAILED_BLOCKER.test(attention.blocker.trim())) {
|
|
645
|
+
return {
|
|
646
|
+
kind: "tempest_trigger_failed",
|
|
647
|
+
prNumber: layer.prNumber,
|
|
648
|
+
headSha: layer.headSha ?? null,
|
|
649
|
+
branch: layer.branch,
|
|
650
|
+
detail: attention.blocker.trim(),
|
|
651
|
+
steps: `Mergestorm could not reach Tempest to review land PR #${layer.prNumber} at ${layer.headSha ?? "its head"} for over 5 minutes, so Auto land will not land it yet. Mergestorm keeps trying every 5 minutes. Nothing in the PR needs a change: do not patch or push. Keep watching; if this stays, tell the human that Tempest cannot be reached.`,
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
if (attention.blocker === VORTEX_REVIEW_FAILED) {
|
|
655
|
+
return {
|
|
656
|
+
kind: "vortex_failed",
|
|
657
|
+
prNumber: layer.prNumber,
|
|
658
|
+
headSha: layer.headSha ?? null,
|
|
659
|
+
branch: layer.branch,
|
|
660
|
+
detail: null,
|
|
661
|
+
steps: `Vortex's review of #${layer.prNumber} at ${layer.headSha ?? "its head"} failed and nothing will run it again by itself, so Auto land waits. There are no findings to fix. Run the review again: comment @mergestorm-vortex review on #${layer.prNumber} (gh pr comment ${layer.prNumber} --repo ${stack.owner}/${stack.repo} --body "@mergestorm-vortex review"); a new commit pushed to ${layer.branch} also queues one. If it fails again, tell the human.`,
|
|
530
662
|
};
|
|
531
663
|
}
|
|
532
664
|
const vortexSkip = vortexIdleSkipBlocker(layer);
|
|
@@ -541,6 +673,16 @@ export function stackRepair(stack, attention, layer, bounce, queueWait = null) {
|
|
|
541
673
|
};
|
|
542
674
|
}
|
|
543
675
|
const wait = autoLandWaitOn(stack, layer);
|
|
676
|
+
if (wait?.reason === "members_remaining" && attention.blocker.startsWith(AUTO_LAND_MEMBERS_REMAINING_PREFIX)) {
|
|
677
|
+
return {
|
|
678
|
+
kind: "members_remaining",
|
|
679
|
+
prNumber: layer.prNumber,
|
|
680
|
+
headSha: layer.headSha ?? null,
|
|
681
|
+
branch: layer.branch,
|
|
682
|
+
detail: wait.detail,
|
|
683
|
+
steps: `Auto land will not send land PR #${layer.prNumber} to the merge queue while another PR is open on ${layer.branch} that is not a layer of this stack${wait.detail ? ` (${wait.detail})` : ""}. Nothing on ${layer.branch} needs a change: do not patch or push for this. Close that PR, retarget it to another base, or adopt it into the stack (mg stack adopt ${stack.owner}/${stack.repo}#<that PR>); a draft PR or a PR from a fork cannot be adopted, so close or retarget it. If the PR is not yours, tell the human. Auto land sends the land PR to the queue by itself once that PR is gone from ${layer.branch}.`,
|
|
684
|
+
};
|
|
685
|
+
}
|
|
544
686
|
if (wait && attention.blocker.startsWith(AUTO_LAND_CI_PENDING_PREFIX)) {
|
|
545
687
|
return {
|
|
546
688
|
kind: "ci_pending",
|
|
@@ -561,6 +703,30 @@ export function stackRepair(stack, attention, layer, bounce, queueWait = null) {
|
|
|
561
703
|
steps: `Auto land has waited since ${wait.since} for an approving Vortex review of ${layer.branch} at ${layer.headSha ?? "its head"}, and no Vortex run is working on it. Read the PR's latest Vortex review (gh api repos/${stack.owner}/${stack.repo}/pulls/${layer.prNumber}/reviews). If it has findings, verify and fix the real ones on ${layer.branch} with an ordinary push, or record a wrong one with mg dismiss as mergestorm-pr-loop describes. If no review exists at this head, or it failed or was skipped, tell the human: nothing re-reviews this head until they re-run the review or push a new head.`,
|
|
562
704
|
};
|
|
563
705
|
}
|
|
706
|
+
if (wait && attention.blocker.startsWith(AUTO_LAND_CI_UNREADABLE_PREFIX)) {
|
|
707
|
+
const what = CI_UNREADABLE_WHAT[wait.reason] ?? "the checks";
|
|
708
|
+
const permission = CI_UNREADABLE_PERMISSION[wait.reason] ?? "Checks: read and Commit statuses: read";
|
|
709
|
+
return {
|
|
710
|
+
kind: "ci_unreadable",
|
|
711
|
+
prNumber: layer.prNumber,
|
|
712
|
+
headSha: layer.headSha ?? null,
|
|
713
|
+
branch: layer.branch,
|
|
714
|
+
since: wait.since,
|
|
715
|
+
reason: wait.reason,
|
|
716
|
+
steps: `GitHub has not let Mergestorm read ${what} for #${layer.prNumber} at ${layer.headSha ?? "its head"} since ${wait.since}${wait.detail ? ` (${wait.detail})` : ""}, so Auto land cannot tell whether CI passed and will not move this PR. Nothing on ${layer.branch} needs a change: do not patch or push for this. Tell the human: the Mergestorm GitHub App needs the permission ${permission} on ${stack.owner}/${stack.repo}. They open the app's installation on GitHub (Settings, then GitHub Apps or Installed GitHub Apps), accept the pending permission request, and check that ${stack.repo} is in the app's repository access. Auto land continues by itself once the read works. If GitHub itself is having an outage, the wait clears when it ends.`,
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
if (wait && attention.blocker.startsWith(AUTO_LAND_WAITED_PREFIX)) {
|
|
720
|
+
return {
|
|
721
|
+
kind: "auto_land_waiting",
|
|
722
|
+
prNumber: layer.prNumber,
|
|
723
|
+
headSha: layer.headSha ?? null,
|
|
724
|
+
branch: layer.branch,
|
|
725
|
+
since: wait.since,
|
|
726
|
+
reason: wait.reason,
|
|
727
|
+
steps: `Auto land has been waiting on this stack since ${wait.since} because ${autoLandWaitWords(wait.reason)} (reason ${wait.reason}${wait.detail ? `: ${wait.detail}` : ""}), and nothing has changed that in the meantime. Run mg stack status ${stack.id} and read what it shows for #${layer.prNumber}. If it names something on ${layer.branch} that a commit fixes, fix it there with an ordinary push. Otherwise do not patch or push: tell the human what Auto land is waiting for and since when. Do not retarget the PR base.`,
|
|
728
|
+
};
|
|
729
|
+
}
|
|
564
730
|
if (wait && attention.blocker.startsWith(AUTO_LAND_PROMOTE_FAILED_PREFIX)) {
|
|
565
731
|
const attempts = wait.attempts ?? AUTO_LAND_PROMOTE_FAILED_ATTEMPTS;
|
|
566
732
|
return {
|
|
@@ -599,6 +765,43 @@ export function stackRepair(stack, attention, layer, bounce, queueWait = null) {
|
|
|
599
765
|
}
|
|
600
766
|
return null;
|
|
601
767
|
}
|
|
768
|
+
export const LAND_PR_MISSING_PREFIX = "No open land PR for ";
|
|
769
|
+
function landPrMissing(stack, candidate) {
|
|
770
|
+
const unit = stack.unit;
|
|
771
|
+
if (!unit || candidate || stack.archivedAt || unit.state === "landed" || unit.state === "abandoned")
|
|
772
|
+
return null;
|
|
773
|
+
if (stack.layers.some((layer) => !isTerminalLayer(layer)))
|
|
774
|
+
return null;
|
|
775
|
+
const landing = unit.landingBlockReason?.trim();
|
|
776
|
+
if (!landing || landGateIsPending(landing) || unit.landPr?.state === "merged")
|
|
777
|
+
return null;
|
|
778
|
+
const closed = unit.landPr && unit.landPr.prNumber > 0 ? unit.landPr : null;
|
|
779
|
+
const prNumber = unit.landPrNumber ?? closed?.prNumber ??
|
|
780
|
+
stack.layers.filter((layer) => layer.prNumber > 0).sort((a, b) => b.position - a.position)[0]?.prNumber;
|
|
781
|
+
if (prNumber == null || prNumber <= 0)
|
|
782
|
+
return null;
|
|
783
|
+
const headSha = closed?.headSha ?? null;
|
|
784
|
+
const target = unit.landTarget?.trim() || stack.landTarget;
|
|
785
|
+
const detail = landing.replace(/^landing blocked:\s*/i, "").replace(/^land_pr_unavailable:\s*/i, "").trim() || null;
|
|
786
|
+
const landPrRef = unit.landPrNumber ?? closed?.prNumber ?? null;
|
|
787
|
+
const restore = landPrRef != null
|
|
788
|
+
? `restore it from the closed land PR's head (gh pr view ${landPrRef} --repo ${stack.owner}/${stack.repo} --json headRefOid,state, then git push origin <that sha>:refs/heads/${unit.branch}) and reopen #${landPrRef}`
|
|
789
|
+
: `restore it from the commit it pointed at (git push origin <sha>:refs/heads/${unit.branch})`;
|
|
790
|
+
return {
|
|
791
|
+
attention: {
|
|
792
|
+
prNumber, headSha, bounceKind: null,
|
|
793
|
+
blocker: `${LAND_PR_MISSING_PREFIX}${unit.branch} — every layer is merged into it, but it cannot land in ${target} without one`,
|
|
794
|
+
},
|
|
795
|
+
repair: {
|
|
796
|
+
kind: "land_pr_unavailable",
|
|
797
|
+
prNumber,
|
|
798
|
+
headSha,
|
|
799
|
+
branch: unit.branch,
|
|
800
|
+
detail,
|
|
801
|
+
steps: `Every layer of this stack is merged into ${unit.branch}, but there is no open land PR from ${unit.branch} into ${target} and Mergestorm cannot open one${detail ? ` (${detail})` : ""}, so the stack cannot land. Nothing in the layers needs a change: do not patch or push for this. Check whether the stack branch still exists: git ls-remote origin refs/heads/${unit.branch}. If it is gone (deleting it makes GitHub close the land PR), tell the human: ${restore}, or close the stack if the work is no longer wanted. If the branch exists, tell the human that Mergestorm cannot open the land PR and give them the error above. Do not open a PR into ${target} yourself.`,
|
|
802
|
+
},
|
|
803
|
+
};
|
|
804
|
+
}
|
|
602
805
|
/** Pair-gate attention plus live hard blocks on every other open layer. */
|
|
603
806
|
export function stackBlockers(stack, entries = [], cursor = {}, nowMs = Date.now()) {
|
|
604
807
|
const candidate = currentLayer(stack);
|
|
@@ -639,6 +842,9 @@ export function stackBlockers(stack, entries = [], cursor = {}, nowMs = Date.now
|
|
|
639
842
|
result = { blocker: `${MERGE_QUEUE_STALLED_PREFIX}${queueWait.text}`, bounceKind: null };
|
|
640
843
|
}
|
|
641
844
|
if (!result.blocker) {
|
|
845
|
+
if (vortexRetryScheduled(layer) && (selected || !candidate || layer.position > candidate.position)) {
|
|
846
|
+
issues.push({ prNumber: layer.prNumber, headSha: layer.headSha ?? null, blocker: VORTEX_RETRY_SCHEDULED, bounceKind: null });
|
|
847
|
+
}
|
|
642
848
|
if (mergeabilityPending(layer) && (selected || !candidate || layer.position > candidate.position)) {
|
|
643
849
|
issues.push({ prNumber: layer.prNumber, headSha: layer.headSha ?? null,
|
|
644
850
|
blocker: `Merge state unknown${layer.parentBranch ? ` vs ${layer.parentBranch}` : ""}`, bounceKind: null });
|
|
@@ -660,6 +866,14 @@ export function stackBlockers(stack, entries = [], cursor = {}, nowMs = Date.now
|
|
|
660
866
|
else if (!candidate || layer.position > candidate.position)
|
|
661
867
|
issues.push(issue);
|
|
662
868
|
}
|
|
869
|
+
if (!attention) {
|
|
870
|
+
const unlisted = autoLandWaitLayer(stack);
|
|
871
|
+
const waiting = unlisted && !open.includes(unlisted) ? autoLandWaitBlocker(stack, unlisted, nowMs) : null;
|
|
872
|
+
if (unlisted && waiting) {
|
|
873
|
+
attention = { prNumber: unlisted.prNumber, headSha: unlisted.headSha ?? null, blocker: waiting, bounceKind: null };
|
|
874
|
+
attentionLayer = unlisted;
|
|
875
|
+
}
|
|
876
|
+
}
|
|
663
877
|
const queued = queueWait && candidate && !queueWait.stalled
|
|
664
878
|
? { prNumber: candidate.prNumber, headSha: candidate.headSha ?? null, text: queueWait.text,
|
|
665
879
|
since: queueWait.since, waitReason: queueWait.waitReason }
|
|
@@ -667,6 +881,11 @@ export function stackBlockers(stack, entries = [], cursor = {}, nowMs = Date.now
|
|
|
667
881
|
const settleIssue = mergeabilitySettleIssue(stack, open, issues, attention);
|
|
668
882
|
if (settleIssue)
|
|
669
883
|
issues.push(settleIssue);
|
|
884
|
+
const landMissing = landPrMissing(stack, candidate);
|
|
885
|
+
if (landMissing) {
|
|
886
|
+
attention = landMissing.attention;
|
|
887
|
+
attentionLayer = undefined;
|
|
888
|
+
}
|
|
670
889
|
const focus = attentionLayer ?? candidate;
|
|
671
890
|
const agents = focus ? layerAgents(focus) : null;
|
|
672
891
|
const busy = [];
|
|
@@ -693,7 +912,8 @@ export function stackBlockers(stack, entries = [], cursor = {}, nowMs = Date.now
|
|
|
693
912
|
queueWait: queued };
|
|
694
913
|
}
|
|
695
914
|
return { attention, held: null, issues, currentCandidate, bounce, busy, agents,
|
|
696
|
-
repair: stackRepair(stack, attention, attentionLayer, bounce, queueWait), landGatePending,
|
|
915
|
+
repair: landMissing?.repair ?? stackRepair(stack, attention, attentionLayer, bounce, queueWait), landGatePending,
|
|
916
|
+
queueWait: queued };
|
|
697
917
|
}
|
|
698
918
|
function mergeabilitySettleIssue(stack, open, issues, attention) {
|
|
699
919
|
const settle = stack.autoEnqueueSettle;
|
package/dist/stack-dto.d.ts
CHANGED
|
@@ -392,9 +392,9 @@ export type StackDto = {
|
|
|
392
392
|
export type MergeQueueEntryState = "queued" | "running" | "waiting" | "landed" | "bounced" | "cancelled";
|
|
393
393
|
export type MergeQueueEnqueuedBy = "human" | "agent";
|
|
394
394
|
export type MergeQueueEnqueuedVia = "dashboard" | "cli" | "chat" | "api" | "mcp";
|
|
395
|
-
export type MergeQueueBounceKind = "ci_failure" | "ci_timeout" | "head_moved" | "tempest_findings" | "seam_findings" | "restack_conflict" | "pr_draft" | "must_consolidate" | "merge_failed" | "gh_error";
|
|
395
|
+
export type MergeQueueBounceKind = "ci_failure" | "ci_timeout" | "head_moved" | "tempest_findings" | "tempest_rerun" | "seam_findings" | "restack_conflict" | "pr_draft" | "must_consolidate" | "merge_failed" | "gh_error";
|
|
396
396
|
/** Every bounce kind understood by the shared contract. */
|
|
397
|
-
export declare const MERGE_QUEUE_BOUNCE_KINDS: readonly ["ci_failure", "ci_timeout", "head_moved", "tempest_findings", "seam_findings", "restack_conflict", "pr_draft", "must_consolidate", "merge_failed", "gh_error"];
|
|
397
|
+
export declare const MERGE_QUEUE_BOUNCE_KINDS: readonly ["ci_failure", "ci_timeout", "head_moved", "tempest_findings", "tempest_rerun", "seam_findings", "restack_conflict", "pr_draft", "must_consolidate", "merge_failed", "gh_error"];
|
|
398
398
|
export declare const BOUNCE_KIND_LABELS: Readonly<Record<MergeQueueBounceKind, string>>;
|
|
399
399
|
export declare function mergeQueueBounceLabel(entry: MergeQueueEntryDto): string;
|
|
400
400
|
/** Structured bounce handoff — enough for the bar chip and the chat quote. */
|
package/dist/stack-dto.js
CHANGED
|
@@ -56,6 +56,7 @@ export const MERGE_QUEUE_BOUNCE_KINDS = [
|
|
|
56
56
|
"ci_timeout",
|
|
57
57
|
"head_moved",
|
|
58
58
|
"tempest_findings",
|
|
59
|
+
"tempest_rerun",
|
|
59
60
|
"seam_findings",
|
|
60
61
|
"restack_conflict",
|
|
61
62
|
"pr_draft",
|
|
@@ -68,6 +69,7 @@ export const BOUNCE_KIND_LABELS = {
|
|
|
68
69
|
ci_timeout: "CI timed out",
|
|
69
70
|
head_moved: "head moved",
|
|
70
71
|
tempest_findings: "Tempest findings",
|
|
72
|
+
tempest_rerun: "Tempest needs rerun",
|
|
71
73
|
seam_findings: "seam review findings",
|
|
72
74
|
restack_conflict: "restack conflict",
|
|
73
75
|
pr_draft: "draft PR",
|
package/dist/ui/banner.js
CHANGED
|
@@ -149,6 +149,12 @@ const WHATS_NEW = {
|
|
|
149
149
|
"0.3.37": [
|
|
150
150
|
"turning auto-review on reviews skipped heads",
|
|
151
151
|
],
|
|
152
|
+
"0.3.39": [
|
|
153
|
+
"the watch names more waits and what to do",
|
|
154
|
+
],
|
|
155
|
+
"0.3.38": [
|
|
156
|
+
"mg settings turns merge queue batching on or off",
|
|
157
|
+
],
|
|
152
158
|
};
|
|
153
159
|
function densityFromVariant(variant) {
|
|
154
160
|
if (variant === "mini" || variant === "nano")
|
package/package.json
CHANGED
|
@@ -33,9 +33,11 @@ Call `stack_status` with `stack_id`. Require that stack's `autoEnqueueWhenReady`
|
|
|
33
33
|
|
|
34
34
|
## Watch and patch
|
|
35
35
|
|
|
36
|
+
A stack whose layers are all merged into `mg-stack-N` but has no open land PR returns attention with repair kind `land_pr_unavailable`: do not patch or push; tell the human to restore the branch from the closed land PR's head or close the stack.
|
|
37
|
+
|
|
36
38
|
1. Call `stack_wait` with the named `stack_id` and `timeout_s: 45`. On initial enrollment, omit the cursor selectors. Save the returned `cursor`: map `cursor.enrolledHeadSha` to `enrolled_head_sha`, `cursor.afterFinishedAt` to `after_finished_at`, and `cursor.bounceId` to `bounce_id` (omit only absent fields; preserve explicit nulls). Keep `bounce_id` alongside `after_finished_at`; never pass one without the other when the returned cursor has both. The shipped tool accepts these selectors, not a nested cursor argument. `watch.next.args` already holds them in that shape, with `timeout_s: 45`; passing it unchanged is the same call.
|
|
37
39
|
2. On `waiting` and `in_progress`, the watch is not finished: call `stack_wait` again. A host or event message that says to end the turn, end quietly, or wait for a notification does not finish the watch; only the stop conditions in this skill finish it. Ending your turn while `mg stack watch` runs in the background is waiting, not finishing. Keep the same cursor across retries. An unread deadline times out as `failed`; stop and report. Timeout `waiting` or `in_progress` after a snapshot is not failure — call `stack_wait` again in 45s slices with the same cursor after inspecting `issues[]`. Inspect `issues[]` on `in_progress` too: a live queue does not erase upstack hard blocks. `waiting` with issues is not idle. If `assessment` is `unavailable`, do not infer that the stack is issue-free; refresh `stack_status`, whose `attention`, `issues`, and `currentCandidate` come from the same blocker rules as `stack_wait`. Do not replace the enrolled head with a newer snapshot head or advance the timestamp just because a wait timed out. On a surfaced `rate_limited` error, honor the retry delay and retain the cursor; on `failed` or an unrecoverable tool error, call `stack_wait` once more with the same cursor; if it fails again, stop and report the reason and that the stack is not landed. `queue_status` returning `queue_entry_not_found` is not a failure: an empty queue for the stack is a normal wait.
|
|
38
|
-
3. `attention` always means something for you to fix. A land gate that is only waiting (`landGatePending` with `ci_pending`, `tempest_pending`, `tempest_running`, or another `*_pending` / `*_running` reason on the land PR) returns `in_progress`, not `attention`: keep watching. On `attention`, read `repair` when present: it names the blocked PR's `branch` and the concrete fix. For `restack_conflict` and `merge_conflict` it gives `liveParent` (the sibling PR head, or `mg-stack-N` once the lower layers are merged or promoted, or the land PR's base such as `main` when the blocked PR is the land PR, never `mg-park-*`) and the conflicted `files`; a null `liveParent` means stop and tell the human. For `ci_failure` it names the failing check. For `tempest_findings` on the land PR it names the land PR, its `branch` (`mg-stack-N`), and where the Tempest report is (`findings`). A stack whose `autoLandOff.reason` is `bounced` (a `merge_failed`, `gh_error`, or `ci_timeout` bounce turned Auto land off and nobody took the stack back since) returns `attention` with the blocker `Auto land off after <kind> bounce`, the bounce in `issues[]`, and a `repair` of kind `auto_land_off`; handle it as "Re-arm after a bounce" describes. A stack whose review unit was abandoned because a human moved its bottom PR off `mg-stack-N` returns `attention` with the blocker `Review unit abandoned: based on <base>` and a `repair` of kind `unit_abandoned`: do not patch or retarget anything. Ask the human whether to re-root it: if at least two PRs remain open, tell them that `mg stack adopt owner/repo#<bottom PR>` re-roots it on a new review unit; with only one open PR, re-adopting will not create a review unit, so tell them to close and resubmit it to create a fresh stack, then stop. Auto land records why it is waiting in the stack's `autoLandWait` (`reason`, `prNumber`, `headSha`, `since`), and the watch returns `attention` on that PR when the wait cannot clear on its own: a failing check Auto land reads live on GitHub while the ledger may still show CI green (blocker `CI failed — Auto land reads a failing check on GitHub at this head`, `repair` kind `ci_failure`); a check pending for 45 minutes (`repair` kind `ci_pending`: find the check that never finished and re-run it, or tell the human when no workflow reports it); no approving Vortex review for 30 minutes with no Vortex run working (`repair` kind `vortex_not_green`: fix or dismiss the findings, or tell the human when no review exists at the head); and a promote GitHub refused 3 times with `merge_failed`, after which Auto land stops retrying that head (`repair` kind `promote_failed`: fix the cause on the PR and push a new head, or tell the human when the cause is outside the PR; never toggle Auto land yourself for it). A layer whose live head has a Vortex verdict that requests changes after Cyclone left those findings for a person (a hold directive, the Vortex patch cap, auto-patch off, no billing, a batch it gave up on, or a PR state it could not read) returns `attention` with the blocker `Vortex findings need a person` and a `repair` of kind `vortex_findings` naming the Cyclone reason (`handoff`) and where the review is (`findings`): verify each finding against the checkout, fix the real ones with the smallest patch and an ordinary push, and tell the human about the ones you judge wrong. `repair` does not replace the checks below. On `attention`, fix the named PR: `prNumber` and `headSha` identify the blocked PR, while `currentCandidate` identifies the promote candidate. The gate covers the bottom and its direct child only when the child has current-head DIRTY on the live bottom branch. A GitHub DIRTY whose `parentBranch` starts with `mg-park-` is not that gate. Refresh `stack_status`; use `queue_status` with the same `stack_id` when bounce details are needed. Verify the blocker still applies to the current PR head. Never treat `verifyHeadSha` as the current head: it is a queue verification SHA and can be stale or synthetic. Compare the live PR branch head, the current stack layer head, and the checkout before editing. Read the live PR head with `git ls-remote origin refs/heads/<branch>` or `gh api repos/OWNER/REPO/pulls/N --jq .head.sha`. If you need CI logs, use `gh run list --branch <branch>`, `gh run view <run-id> --log-failed`, or `gh api repos/OWNER/REPO/commits/SHA/check-runs`. Read GitHub only through git and these REST calls; do not use other `gh pr` subcommands. Restack `clean` does not mean mergeable or CI-green. Attention is a diagnosis, not permission to mutate stack or queue policy; the only policy change you make is the re-arm in "Re-arm after a bounce". A bounce is the reason to look, not the moment to commit. `stack_wait` and `stack_status` hold the blocked PR's attention themselves while Vortex or Cyclone is still working on that PR, whatever the blocker (a red CI failure, a merge conflict, a bounce, Draft, a failed restack or review): they return `in_progress` instead of `attention`, with `busy[]` naming the PR (`prNumber`, `headSha`), the `blocker`, and the `agent` (`vortex` or `cyclone`), and `agents` carrying that PR's `vortexStatus`, `cycloneStatus`, `vortexReview`, and `busy` flags. `in_progress` with a nonempty `busy[]` means do not patch yet: keep calling `stack_wait` with the same cursor until that work has finished and the watch returns `attention`, then make the follow-up commit. You do not need an extra `stack_status` refresh each slice for this check. A nonempty `busy[]` reports still-working agents on that PR's current head. Vortex holds when `vortexReview` is `reviewing` at the head, an unfinished vortex `agentRuns` row at the head is `reviewing`, or an unfinished vortex run at the head is `queued` and started within the last 15 minutes (with no `agentRuns` or `vortexReview` on the layer, `vortexStatus` `reviewing` holds). Cyclone holds when `cycloneStatus` is `patching`, an unfinished cyclone `agentRuns` row is `patching`, or the server sees a live Cyclone patch lease or an open Cyclone check run on the head. `seam_pending` alone, and a `queued` run older than 15 minutes, are not treated as work in progress. `all_clear`, `findings`, `skipped`, `failed`, `throttled`, `awaiting_fix`, and a null status are finished. A queued Vortex run holds for at most 15 minutes; reviewing and patching hold until the run finishes or its lease or check run is cleared. A blocker no Vortex or Cyclone run can clear is not hidden by the hold: a merge conflict vs the live parent, a restack `Conflict` or `Restack failed` (a parked freeze included), a Draft PR, `Seam review pending, no review running`, or `Review skipped, out of review quota`. For those the envelope stays `in_progress` but names the blocker in the top-level `blocker`, `prNumber`, `headSha`, and `issues[]`, with `actAfter: "agents_idle"`, `waitingOn` listing the busy agents, and `repair` when there is one. Plan the fix from it, but do not patch, merge, or push while `actAfter` is set: keep calling `stack_wait` with the same cursor, and act when a snapshot after the agents finish returns `attention`; a Cyclone push can change or clear the blocker, so re-read it then. Blockers the agents can change (a red CI failure, a failed review or Tempest run, seam findings, a bounce) keep the plain hold: `blocker` is null, `actAfter` is null, and only `busy[]` names them. If a hold looks stuck, refresh `stack_status` and tell the human. Do not turn Cyclone off or Auto land off, and do not turn Auto land on to end the wait; turn it on only as "Re-arm after a bounce" allows. If the remote head moves while you wait, stop and tell the human.
|
|
40
|
+
3. `attention` always means something for you to fix. A land gate that is only waiting (`landGatePending` with `ci_pending`, `tempest_pending`, `tempest_running`, or another `*_pending` / `*_running` reason on the land PR) returns `in_progress`, not `attention`: keep watching. On `attention`, read `repair` when present: it names the blocked PR's `branch` and the concrete fix. For `restack_conflict` and `merge_conflict` it gives `liveParent` (the sibling PR head, or `mg-stack-N` once the lower layers are merged or promoted, or the land PR's base such as `main` when the blocked PR is the land PR, never `mg-park-*`) and the conflicted `files`; a null `liveParent` means stop and tell the human. For `ci_failure` it names the failing check. For `tempest_findings` on the land PR it names the land PR, its `branch` (`mg-stack-N`), and where the Tempest report is (`findings`). A stack whose `autoLandOff.reason` is `bounced` (a `merge_failed`, `gh_error`, or `ci_timeout` bounce turned Auto land off and nobody took the stack back since) returns `attention` with the blocker `Auto land off after <kind> bounce`, the bounce in `issues[]`, and a `repair` of kind `auto_land_off`; handle it as "Re-arm after a bounce" describes. A stack whose review unit was abandoned because a human moved its bottom PR off `mg-stack-N` returns `attention` with the blocker `Review unit abandoned: based on <base>` and a `repair` of kind `unit_abandoned`: do not patch or retarget anything. Ask the human whether to re-root it: if at least two PRs remain open, tell them that `mg stack adopt owner/repo#<bottom PR>` re-roots it on a new review unit; with only one open PR, re-adopting will not create a review unit, so tell them to close and resubmit it to create a fresh stack, then stop. Auto land records why it is waiting in the stack's `autoLandWait` (`reason`, `prNumber`, `headSha`, `since`), and the watch returns `attention` on that PR when the wait cannot clear on its own: a failing check Auto land reads live on GitHub while the ledger may still show CI green (blocker `CI failed — Auto land reads a failing check on GitHub at this head`, `repair` kind `ci_failure`); a check pending for 45 minutes (`repair` kind `ci_pending`: find the check that never finished and re-run it, or tell the human when no workflow reports it); no approving Vortex review for 30 minutes with no Vortex run working (`repair` kind `vortex_not_green`: fix or dismiss the findings, or tell the human when no review exists at the head); and a promote GitHub refused 3 times with `merge_failed`, after which Auto land stops retrying that head (`repair` kind `promote_failed`: fix the cause on the PR and push a new head, or tell the human when the cause is outside the PR; never toggle Auto land yourself for it). The watch also returns `attention` when GitHub refuses to let Mergestorm read the head's check runs or commit statuses (blocker `Mergestorm cannot read ... on this head`, `repair` kind `ci_unreadable`: do not patch or push; tell the human to grant the Mergestorm GitHub App the permission the repair names), and when any other wait reason has held for 45 minutes, a land gate left pending or running on the land PR included (blocker `Auto land has waited <N>m: <reason in plain words>`, `repair` kind `auto_land_waiting`: read `stack_status`, fix what a commit on the named branch can fix, otherwise tell the human what Auto land waits for and since when). A merge queue entry that has sat queued for 45 minutes with nothing working on it or ahead of it returns `attention` with the blocker `Merge queue stuck: ...` and a `repair` whose kind is queue_stalled: do not patch or push; tell the human. A Vortex review skipped because the PR author is on the ignored authors list, or because reviews are turned off for the repository, returns `attention` at once with a `repair` of kind `vortex_skipped`; a Vortex retry that is scheduled (`Vortex hit a temporary error on this head and will retry by itself`) is a wait listed in `issues[]`, never `attention`. `Review failed` carries a `repair` of kind `vortex_failed`: comment `@mergestorm-vortex review` on the PR or push a new commit, and tell the human if it fails again. A PR open on `mg-stack-N` that is not a layer of the stack returns `attention` at once with the blocker `The land PR cannot be queued: ...` and a `repair` of kind `members_remaining`: do not patch or push; close, retarget or adopt that PR (a draft or fork PR cannot be adopted). On the land PR, `tempest_skipped` carries a `repair` of kind `tempest_rerun` (comment `@mergestorm-tempest review`), and `tempest_trigger_failed` carries a `repair` of kind `tempest_trigger_failed`: Mergestorm keeps trying every 5 minutes, nothing in the PR needs a fix, and if it stays you tell the human. A layer whose live head has a Vortex verdict that requests changes after Cyclone left those findings for a person (a hold directive, the Vortex patch cap, auto-patch off, no billing, a batch it gave up on, or a PR state it could not read) returns `attention` with the blocker `Vortex findings need a person` and a `repair` of kind `vortex_findings` naming the Cyclone reason (`handoff`) and where the review is (`findings`): verify each finding against the checkout, fix the real ones with the smallest patch and an ordinary push, and tell the human about the ones you judge wrong. `repair` does not replace the checks below. On `attention`, fix the named PR: `prNumber` and `headSha` identify the blocked PR, while `currentCandidate` identifies the promote candidate. The gate covers the bottom and its direct child only when the child has current-head DIRTY on the live bottom branch. A GitHub DIRTY whose `parentBranch` starts with `mg-park-` is not that gate. Refresh `stack_status`; use `queue_status` with the same `stack_id` when bounce details are needed. Verify the blocker still applies to the current PR head. Never treat `verifyHeadSha` as the current head: it is a queue verification SHA and can be stale or synthetic. Compare the live PR branch head, the current stack layer head, and the checkout before editing. Read the live PR head with `git ls-remote origin refs/heads/<branch>` or `gh api repos/OWNER/REPO/pulls/N --jq .head.sha`. If you need CI logs, use `gh run list --branch <branch>`, `gh run view <run-id> --log-failed`, or `gh api repos/OWNER/REPO/commits/SHA/check-runs`. Read GitHub only through git and these REST calls; do not use other `gh pr` subcommands. Restack `clean` does not mean mergeable or CI-green. Attention is a diagnosis, not permission to mutate stack or queue policy; the only policy change you make is the re-arm in "Re-arm after a bounce". A bounce is the reason to look, not the moment to commit. `stack_wait` and `stack_status` hold the blocked PR's attention themselves while Vortex or Cyclone is still working on that PR, whatever the blocker (a red CI failure, a merge conflict, a bounce, Draft, a failed restack or review): they return `in_progress` instead of `attention`, with `busy[]` naming the PR (`prNumber`, `headSha`), the `blocker`, and the `agent` (`vortex` or `cyclone`), and `agents` carrying that PR's `vortexStatus`, `cycloneStatus`, `vortexReview`, and `busy` flags. `in_progress` with a nonempty `busy[]` means do not patch yet: keep calling `stack_wait` with the same cursor until that work has finished and the watch returns `attention`, then make the follow-up commit. You do not need an extra `stack_status` refresh each slice for this check. A nonempty `busy[]` reports still-working agents on that PR's current head. Vortex holds when `vortexReview` is `reviewing` at the head, an unfinished vortex `agentRuns` row at the head is `reviewing`, or an unfinished vortex run at the head is `queued` and started within the last 15 minutes (with no `agentRuns` or `vortexReview` on the layer, `vortexStatus` `reviewing` holds). Cyclone holds when `cycloneStatus` is `patching`, an unfinished cyclone `agentRuns` row is `patching`, or the server sees a live Cyclone patch lease or an open Cyclone check run on the head. `seam_pending` alone, and a `queued` run older than 15 minutes, are not treated as work in progress. `all_clear`, `findings`, `skipped`, `failed`, `throttled`, `awaiting_fix`, and a null status are finished. A queued Vortex run holds for at most 15 minutes; reviewing and patching hold until the run finishes or its lease or check run is cleared. A blocker no Vortex or Cyclone run can clear is not hidden by the hold: a merge conflict vs the live parent, a restack `Conflict` or `Restack failed` (a parked freeze included), a Draft PR, `Seam review pending, no review running`, or `Review skipped, out of review quota`. For those the envelope stays `in_progress` but names the blocker in the top-level `blocker`, `prNumber`, `headSha`, and `issues[]`, with `actAfter: "agents_idle"`, `waitingOn` listing the busy agents, and `repair` when there is one. Plan the fix from it, but do not patch, merge, or push while `actAfter` is set: keep calling `stack_wait` with the same cursor, and act when a snapshot after the agents finish returns `attention`; a Cyclone push can change or clear the blocker, so re-read it then. Blockers the agents can change (a red CI failure, a failed review or Tempest run, seam findings, a bounce) keep the plain hold: `blocker` is null, `actAfter` is null, and only `busy[]` names them. If a hold looks stuck, refresh `stack_status` and tell the human. Do not turn Cyclone off or Auto land off, and do not turn Auto land on to end the wait; turn it on only as "Re-arm after a bounce" allows. If the remote head moves while you wait, stop and tell the human.
|
|
39
41
|
4. Use a clean worktree on that named PR's head branch and record the remote starting SHA. Fix the current blocker with the smallest patch, and run the relevant checks. For conflicts, the live parent is the sibling PR head or the unit branch (`mg-stack-N`, Mergestorm's owned trunk, not an alias for main) — not the DTO `parentBranch` when that field starts with `mg-park-*`. If `parentBranch` is `mg-park-*`, never merge or rebase onto it; if the real live parent is unknown, stop. The land PR is the one PR whose head is `mg-stack-N`: its live parent is its base (`main`), and merging that base into `mg-stack-N` with an ordinary push is its conflict repair, the same as for any other layer. Tempest never patches: Cyclone patches a land PR when auto-patch is on for that stack, otherwise you do. On `tempest_findings` on the land PR, when `busy[]` shows no Cyclone run on it, read the Tempest report, verify each finding against the checkout as for Vortex findings, fix the real ones with the smallest patch on `mg-stack-N`, and push with an ordinary push, never a force push; while Cyclone is patching it the envelope stays `in_progress` with `busy[]`, so wait. Otherwise prefer merging that live parent into the PR branch, resolving locally, then an ordinary push. Never use Dashboard Continue. When the live parent is a sibling, never rebase onto trunk or `mg-stack-N` instead. Parked-freeze DIRTY alone needs no conflict patch. Leave GitHub bases as Mergestorm set them: the bottom of a 2+ PR stack is based on `mg-stack-N`, layer 2 on layer 1, PR3+ on `mg-park-*`.
|
|
40
42
|
5. CI-only fixes may run while Cyclone auto-patch is still on for the stack. Auto-patch may stay on, and you still wait until the current Vortex run and the current Cyclone run on that PR have finished before the follow-up commit. When the gate is clean, inspect `issues[]` and fix a live upstack CI failure one PR at a time, refreshing the gate before the next PR. Do not use higher-layer bounce history as a patch target. Do not fight Cyclone: if the remote head moved, stop and tell the human. Do not take over Cyclone's review-finding work while it is active; if the blocker requires that or a policy/queue action rather than a concrete patch, stop and explain what needs human attention. On a `Seam findings` blocker, read that PR's seam finding, the Vortex review "Integration review — U<n> seam" (`gh api repos/OWNER/REPO/pulls/N/reviews` and `gh api repos/OWNER/REPO/pulls/N/comments`), and patch its concrete findings on that PR's head; a "Decision required" seam finding needs a human, so tell them and do not guess. A seam finding you verified is wrong is recorded with `mg dismiss` (the audited dismissal in `mergestorm-pr-loop`) at that PR's live head and that seam review's id; a `mergestorm-loop: dismiss` comment alone does not clear the seam gate. On `Seam review failed`, do not patch; tell the human, unless a new push already has a seam re-review held in `busy[]`. On `Seam review pending, no review running`, do not patch; tell the human: the seam review was skipped (usually because the monthly review limit is reached) and nothing re-runs it until they raise the limit and click Continue on the stack, or push a new head. On `Seam review stuck in reviewing, no review running`, `Seam re-review pending, no review running`, or `Seam verdict is for an older head, no review running` (repair kind `seam_review_stuck`), do not patch or push; tell the human: no seam review is queued or running for that PR's live head, and clicking Continue on the stack re-queues it (a new pushed head also does). On `Review skipped, out of review quota`, do not patch; tell the human: the review credit limit for the period is reached and nothing re-reviews that head until they raise the limit and push a new head or re-run the review.
|
|
41
43
|
6. Recheck Auto land (off is expected only after a disarming bounce you are handling) and the remote PR head (`git ls-remote origin refs/heads/<branch>`) immediately before pushing. Commit the patch and push the PR head with an ordinary push. A rebase onto that same live parent followed by force-with-lease is allowed only if the remote is still exactly the SHA you started from and Cyclone did not move it; use an explicit lease bound to that starting SHA. This exception never permits rebasing onto a parked freeze or substituting trunk / the unit for a sibling parent. If the remote head moved or the push is rejected because it moved, stop; do not rebase and retry.
|
|
@@ -67,6 +69,7 @@ The kind list is exactly `MERGE_QUEUE_BOUNCE_KINDS` from stack-dto:
|
|
|
67
69
|
- `ci_timeout`
|
|
68
70
|
- `head_moved`
|
|
69
71
|
- `tempest_findings`
|
|
72
|
+
- `tempest_rerun`
|
|
70
73
|
- `seam_findings`
|
|
71
74
|
- `restack_conflict`
|
|
72
75
|
- `pr_draft`
|
|
@@ -67,7 +67,7 @@ Map these to `mergestorm.pr_review/v1` envelope fields; do not invent statuses.
|
|
|
67
67
|
- status is `rate_limited`, or `patch_policy.mode` is `"hold"`: surface the findings to the human and stop. Do not patch.
|
|
68
68
|
- status is `failed`: report to the human. Do not keep pushing to retrigger.
|
|
69
69
|
- status is `in_progress` and the envelope has a `head_sha`: the pass is still running; call `review_wait_pr` again with the same `after_sha` and the same `after_pass`.
|
|
70
|
-
- status is `in_progress` and the envelope has no `head_sha
|
|
70
|
+
- status is `none` (from an older MCP server: status is `in_progress` and the envelope has no `head_sha`): the wait timed out and no Vortex pass exists for your SHA yet. Call `review_wait_pr` once more with the same `after_sha` and the same `after_pass`. If it comes back the same way a second time in a row, stop and tell the human that Vortex did not pick up the push, and that a comment `@mergestorm-vortex review` on the PR runs it. Do not push again to retrigger.
|
|
71
71
|
- status is `quota_exceeded` (or its legacy spelling `trial_expired`): the account's review credits are used up for this period; tell the human calmly and stop. It is not an error and not a lapsed trial. Do not patch or push.
|
|
72
72
|
- status is `skipped`, `stopped`, or `synced`: surface the status and reason to the human and stop. Do not patch or push.
|
|
73
73
|
|