mergestorm 0.3.24 → 0.3.26
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 +6 -6
- package/dist/client/index.d.ts +4 -3
- package/dist/client/index.js +4 -3
- package/dist/commands/queue.js +5 -1
- package/dist/commands/registry.js +1 -1
- package/dist/commands/stack.d.ts +8 -0
- package/dist/commands/stack.js +68 -7
- package/dist/shell.js +1 -1
- package/dist/stack-blockers.d.ts +54 -2
- package/dist/stack-blockers.js +139 -6
- package/dist/stack-dto.d.ts +17 -0
- package/dist/stack-dto.js +28 -0
- package/dist/stack-watch-obligation.d.ts +43 -0
- package/dist/stack-watch-obligation.js +79 -0
- package/dist/stack-watch.d.ts +8 -1
- package/dist/stack-watch.js +44 -23
- package/dist/ui/ansi.d.ts +14 -1
- package/dist/ui/ansi.js +25 -5
- package/dist/ui/banner.d.ts +2 -1
- package/dist/ui/banner.js +26 -47
- package/dist/ui/logo.d.ts +13 -8
- package/dist/ui/logo.js +25 -13
- package/dist/ui/prompt.d.ts +11 -1
- package/dist/ui/prompt.js +92 -9
- package/package.json +1 -1
- package/skill/SKILL.md +2 -2
- package/skill/mergestorm-bounce-watch/SKILL.md +8 -4
- package/skill/mergestorm-pr-loop/SKILL.md +3 -3
package/README.md
CHANGED
|
@@ -45,12 +45,12 @@ On a TTY, bare `mergestorm` (or `mergestorm shell`) opens a branded REPL: a comp
|
|
|
45
45
|
|
|
46
46
|
```
|
|
47
47
|
╭──────────────────────────────────────────────────────────────────────────────╮
|
|
48
|
-
│
|
|
49
|
-
│
|
|
50
|
-
│ [████████░░░░░░░░░░░░░░░░░░░░░░] 25% used
|
|
51
|
-
│
|
|
52
|
-
│
|
|
53
|
-
│ New
|
|
48
|
+
│ ▀▀▀▀▀▀▀▀▀▀▘ mergestorm v0.3.26 │
|
|
49
|
+
│ ▀▀▀▀▀▀▀▀▀ ● msk_live_… · maelstrom │
|
|
50
|
+
│ ▀▀▀▀▀▀ [████████░░░░░░░░░░░░░░░░░░░░░░] 25% used │
|
|
51
|
+
│ ▝▀▀▀▀▀ local reviews + stacked PRs · mergestorm.ai │
|
|
52
|
+
│ ▝▀▀▘ review a diff · usage for tabs · /help for all commands │
|
|
53
|
+
│ ▀ New: stack results say the watch ends only at land │
|
|
54
54
|
╰──────────────────────────────────────────────────────────────────────────────╯
|
|
55
55
|
mergestorm
|
|
56
56
|
╭──────────────────────────────────────────────────────────────────────────────╮
|
package/dist/client/index.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export { CommandError, DetachedError, RATE_LIMITED_HINT, REVIEW_EXIT, isCommandE
|
|
|
5
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";
|
|
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
|
-
export { BOUNCE_KIND_LABELS, mergeQueueBounceLabel } from "../stack-dto.js";
|
|
9
|
-
export { STACK_WATCH_SCHEMA, STACK_WATCH_DEFAULT_TIMEOUT_MS, StackWatchError, StackWatchTimeoutError, pollStackWatch, type PollStackWatchOptions, type StackWatchCursor, type StackWatchEnvelope, } from "../stack-watch.js";
|
|
10
|
-
export { stackBlockers, stackBlockersSummary, sameHead, type StackBlocker } from "../stack-blockers.js";
|
|
8
|
+
export { BOUNCE_KIND_LABELS, deriveStackAgentsBusy, mergeQueueBounceLabel, type StackAgentsBusy } from "../stack-dto.js";
|
|
9
|
+
export { STACK_WATCH_SCHEMA, STACK_WATCH_DEFAULT_TIMEOUT_MS, STACK_WATCH_BUSY_RECHECK_S, StackWatchError, StackWatchTimeoutError, pollStackWatch, type PollStackWatchOptions, type StackWatchCursor, type StackWatchEnvelope, } from "../stack-watch.js";
|
|
10
|
+
export { stackBlockers, stackBlockersSummary, sameHead, layerAgents, layerAgentsBusy, conflictLiveParent, stackRepair, type StackAgentBusy, type StackBlocker, type StackLandGatePending, type StackLayerAgents, type StackRepairHint, } from "../stack-blockers.js";
|
|
11
|
+
export { STACK_WATCH_NOT_DONE_SENTENCE, stackTerminalReason, stackWatchNextArgs, stackWatchObligation, type StackWatchDoneReason, type StackWatchNext, type StackWatchNextArgs, type StackWatchObligation, type StackWatchObligationInput, type StackWatchReason, } from "../stack-watch-obligation.js";
|
package/dist/client/index.js
CHANGED
|
@@ -5,6 +5,7 @@ export { CommandError, DetachedError, RATE_LIMITED_HINT, REVIEW_EXIT, isCommandE
|
|
|
5
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";
|
|
6
6
|
export { REVIEW_JOB_ENVELOPE_SCHEMA, toReviewJobEnvelope, } from "../ui/job-envelope.js";
|
|
7
7
|
export { discoverTrunk } from "../git-stack.js";
|
|
8
|
-
export { BOUNCE_KIND_LABELS, mergeQueueBounceLabel } from "../stack-dto.js";
|
|
9
|
-
export { STACK_WATCH_SCHEMA, STACK_WATCH_DEFAULT_TIMEOUT_MS, StackWatchError, StackWatchTimeoutError, pollStackWatch, } from "../stack-watch.js";
|
|
10
|
-
export { stackBlockers, stackBlockersSummary, sameHead } from "../stack-blockers.js";
|
|
8
|
+
export { BOUNCE_KIND_LABELS, deriveStackAgentsBusy, mergeQueueBounceLabel } from "../stack-dto.js";
|
|
9
|
+
export { STACK_WATCH_SCHEMA, STACK_WATCH_DEFAULT_TIMEOUT_MS, STACK_WATCH_BUSY_RECHECK_S, StackWatchError, StackWatchTimeoutError, pollStackWatch, } from "../stack-watch.js";
|
|
10
|
+
export { stackBlockers, stackBlockersSummary, sameHead, layerAgents, layerAgentsBusy, conflictLiveParent, stackRepair, } from "../stack-blockers.js";
|
|
11
|
+
export { STACK_WATCH_NOT_DONE_SENTENCE, stackTerminalReason, stackWatchNextArgs, stackWatchObligation, } from "../stack-watch-obligation.js";
|
package/dist/commands/queue.js
CHANGED
|
@@ -144,8 +144,12 @@ async function removeQueue(args, asJson) {
|
|
|
144
144
|
console.log(JSON.stringify(body, null, 2));
|
|
145
145
|
return;
|
|
146
146
|
}
|
|
147
|
+
const cancelRequested = typeof body === "object" && body !== null &&
|
|
148
|
+
body.cancelRequested === true;
|
|
147
149
|
await present("Merge queue", [
|
|
148
|
-
|
|
150
|
+
cancelRequested
|
|
151
|
+
? ansi.brightGreen(` Cancel requested for ${entryId}: it is merging, and the queue cancels it before the merge call unless the merge already started`)
|
|
152
|
+
: ansi.brightGreen(` Removed ${entryId} from the merge queue`),
|
|
149
153
|
]);
|
|
150
154
|
}
|
|
151
155
|
export async function cmdQueue(args, deps = {}) {
|
|
@@ -224,7 +224,7 @@ export const COMMAND_REGISTRY = [
|
|
|
224
224
|
"mergestorm stack reset --force Clear this repo's pre-submit authoring state",
|
|
225
225
|
"mergestorm stack list [--json] List registered stacks",
|
|
226
226
|
"mergestorm stack status <stack-id> [--json] Enriched stack status",
|
|
227
|
-
"mergestorm stack wait <stack-id> [--json] [--timeout <s>] Wait for stack attention (default 45s, max 300s; 0 = one snapshot, no wait)",
|
|
227
|
+
"mergestorm stack wait <stack-id> [--json] [--timeout <s>] Wait for stack attention (default 45s, max 300s; 0 = one snapshot, no wait). --json: one document on stdout; timeout notice on stderr, exit 5",
|
|
228
228
|
"mergestorm stack set <stack-id> [--auto-land on|off] [--auto-review on|off|default] [--auto-patch on|off|default] [--json] Per-stack policy",
|
|
229
229
|
"mergestorm stack adopt <owner/repo>#<pr> [--auto-land on|off] [--auto-review on|off] [--auto-patch on|off] Import an open PR chain",
|
|
230
230
|
"mergestorm stack restack <stack-id> Restack stack descendants",
|
package/dist/commands/stack.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { type StackRepairHint } from "../stack-blockers.js";
|
|
2
|
+
import { type StackWatchObligation } from "../stack-watch-obligation.js";
|
|
1
3
|
import { adoptStack, ensureUpperPark, findStackPull, getEnrichedStack, listStacks, openStackPull, setStackPolicy, type StackDto, type StackPolicyPatch } from "../api.js";
|
|
2
4
|
import { loadConfig } from "../config.js";
|
|
3
5
|
import { createPrs, findOpenPrNumber } from "../gh.js";
|
|
@@ -7,6 +9,11 @@ import { buildPrBodyFromCommit } from "../pr-body.js";
|
|
|
7
9
|
import { gitTopLevel } from "../repo-identity.js";
|
|
8
10
|
import { loadStackMeta, saveStackMeta } from "../stack-meta.js";
|
|
9
11
|
import { pollStackWatch } from "../stack-watch.js";
|
|
12
|
+
export declare function stackRepairLine(repair: StackRepairHint | null | undefined): string[];
|
|
13
|
+
export declare function stackWatchTextLines(watch: StackWatchObligation | null | undefined): string[];
|
|
14
|
+
export declare function openStackWatch(stackId: string, opts?: {
|
|
15
|
+
unread?: boolean;
|
|
16
|
+
}): StackWatchObligation;
|
|
10
17
|
/** A registered stack layer that would become the parent of a new layer. */
|
|
11
18
|
export type RegisteredParentHit = {
|
|
12
19
|
stackId: string;
|
|
@@ -140,6 +147,7 @@ export type StackSetDeps = {
|
|
|
140
147
|
};
|
|
141
148
|
/** Human summary lines for a `stack set` write, one per flag. */
|
|
142
149
|
export declare function stackSetSummary(parsed: StackSetArgs): string[];
|
|
150
|
+
export declare function withWatch(body: unknown, watch: StackWatchObligation | null): unknown;
|
|
143
151
|
export declare function cmdStackSet(args: string[], deps?: StackSetDeps): Promise<void>;
|
|
144
152
|
export declare function cmdStack(args: string[], options?: {
|
|
145
153
|
signal?: AbortSignal;
|
package/dist/commands/stack.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { stackBlockersSummary } from "../stack-blockers.js";
|
|
1
|
+
import { stackBlockers, stackBlockersSummary } from "../stack-blockers.js";
|
|
2
|
+
import { STACK_WATCH_NOT_DONE_SENTENCE, stackTerminalReason, stackWatchObligation, } from "../stack-watch-obligation.js";
|
|
2
3
|
import { adoptStack, ensureUpperPark, findStackPull, getEnrichedStack, landNextStack, listStacks, openStackPull, restackStack, setStackPolicy, } from "../api.js";
|
|
3
4
|
import { loadConfig } from "../config.js";
|
|
4
5
|
import { rateLimitedMessage, REVIEW_EXIT, CommandError } from "../errors.js";
|
|
@@ -15,6 +16,26 @@ import { runLineTabsBrowser } from "../ui/line-tabs.js";
|
|
|
15
16
|
import { present } from "../ui/present.js";
|
|
16
17
|
import { canBrowse } from "./browse.js";
|
|
17
18
|
import { openHelpBrowser } from "./help.js";
|
|
19
|
+
export function stackRepairLine(repair) {
|
|
20
|
+
if (!repair)
|
|
21
|
+
return [];
|
|
22
|
+
const files = "files" in repair && repair.files.length ? ` (files: ${repair.files.join(", ")})` : "";
|
|
23
|
+
return [`Repair #${repair.prNumber} ${repair.kind}${files}: ${repair.steps}`];
|
|
24
|
+
}
|
|
25
|
+
export function stackWatchTextLines(watch) {
|
|
26
|
+
if (!watch)
|
|
27
|
+
return [];
|
|
28
|
+
if (watch.done)
|
|
29
|
+
return [watch.message];
|
|
30
|
+
const next = watch.next;
|
|
31
|
+
return [
|
|
32
|
+
...(next ? [`Next: ${next.command} (MCP: stack_wait ${JSON.stringify(next.args)})`] : []),
|
|
33
|
+
STACK_WATCH_NOT_DONE_SENTENCE,
|
|
34
|
+
];
|
|
35
|
+
}
|
|
36
|
+
export function openStackWatch(stackId, opts = {}) {
|
|
37
|
+
return stackWatchObligation({ stackId, terminal: null, unread: opts.unread, freshCursor: true });
|
|
38
|
+
}
|
|
18
39
|
const STACK_USAGE = `usage:
|
|
19
40
|
mergestorm stack create [name] [--onto <branch>] [--trunk <branch>] [--extend] [--auto-land on|off] [--auto-review on|off] [--auto-patch on|off] [--json]
|
|
20
41
|
mergestorm stack submit [--extend] [--auto-land on|off] [--auto-review on|off] [--auto-patch on|off] [--rest] [--json]
|
|
@@ -22,6 +43,8 @@ const STACK_USAGE = `usage:
|
|
|
22
43
|
mergestorm stack list [--json]
|
|
23
44
|
mergestorm stack status <stack-id> [--json]
|
|
24
45
|
mergestorm stack wait <stack-id> [--json] [--timeout <s>]
|
|
46
|
+
--json writes exactly one JSON document to stdout. The timeout or error notice goes to stderr
|
|
47
|
+
(exit 5 on timeout), so do not merge the streams (2>&1) before parsing stdout.
|
|
25
48
|
mergestorm stack set <stack-id> [--auto-land on|off] [--auto-review on|off|default] [--auto-patch on|off|default] [--json]
|
|
26
49
|
mergestorm stack adopt <owner/repo>#<pr> [--auto-land on|off] [--auto-review on|off] [--auto-patch on|off] [--json]
|
|
27
50
|
mergestorm stack restack <stack-id> [--json]
|
|
@@ -780,6 +803,7 @@ export async function cmdStackSubmit(args, deps = {}) {
|
|
|
780
803
|
stackId: stackId ?? null,
|
|
781
804
|
trunk: data.stack?.trunkBranch ?? meta.trunk,
|
|
782
805
|
chain: data.chain ?? null,
|
|
806
|
+
watch: stackId ? openStackWatch(stackId) : null,
|
|
783
807
|
}, null, 2));
|
|
784
808
|
return;
|
|
785
809
|
}
|
|
@@ -799,6 +823,8 @@ export async function cmdStackSubmit(args, deps = {}) {
|
|
|
799
823
|
summary.push(ansi.dim(" Stack registration returned no stack ID"));
|
|
800
824
|
}
|
|
801
825
|
summary.push(ansi.dim(" Dashboard: https://mergestorm.ai/dashboard"));
|
|
826
|
+
if (stackId)
|
|
827
|
+
summary.push(...stackWatchTextLines(openStackWatch(stackId)).map((line) => ` ${line}`));
|
|
802
828
|
await present("Stack", summary);
|
|
803
829
|
}
|
|
804
830
|
async function cmdStackReset(args) {
|
|
@@ -868,7 +894,11 @@ export async function cmdStackWait(args, deps = {}) {
|
|
|
868
894
|
...envelope,
|
|
869
895
|
...(retryAfterSeconds !== undefined ? { retry_after_seconds: retryAfterSeconds } : {}),
|
|
870
896
|
}, null, 2)
|
|
871
|
-
:
|
|
897
|
+
: [
|
|
898
|
+
`Stack ${envelope.stackId} · ${envelope.status}${stackBlockersSummary(envelope.blocker && envelope.prNumber != null ? { prNumber: envelope.prNumber, blocker: envelope.blocker } : null, envelope.issues, envelope.busy, envelope.landGatePending ?? null)}${envelope.assessment === "unavailable" ? " · assessment unavailable" : ""}`,
|
|
899
|
+
...stackRepairLine(envelope.repair),
|
|
900
|
+
...stackWatchTextLines(envelope.watch),
|
|
901
|
+
].join("\n"));
|
|
872
902
|
try {
|
|
873
903
|
print(await (deps.pollStackWatch ?? pollStackWatch)(cfg, stackId, {
|
|
874
904
|
timeoutMs: timeoutS * 1000,
|
|
@@ -892,6 +922,7 @@ export async function cmdStackWait(args, deps = {}) {
|
|
|
892
922
|
}
|
|
893
923
|
export async function cmdStackStatus(args, deps = {}) {
|
|
894
924
|
const usage = "usage: mergestorm stack status <stack-id> [--json]";
|
|
925
|
+
const asJson = args.includes("--json");
|
|
895
926
|
const positional = args.filter((arg) => arg !== "--json");
|
|
896
927
|
if (positional.length !== 1)
|
|
897
928
|
throw new CommandError(usage, 2, "usage");
|
|
@@ -899,10 +930,25 @@ export async function cmdStackStatus(args, deps = {}) {
|
|
|
899
930
|
const cfg = await (deps.loadConfig ?? loadConfig)();
|
|
900
931
|
const stack = await (deps.getEnrichedStack ?? getEnrichedStack)(stackId, cfg);
|
|
901
932
|
if (!stack) {
|
|
933
|
+
const watch = stackWatchObligation({ stackId, terminal: "not_found" });
|
|
934
|
+
if (asJson) {
|
|
935
|
+
console.log(JSON.stringify({ error: { code: "not_found", stack_id: stackId }, watch }, null, 2));
|
|
936
|
+
process.exitCode = 1;
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
939
|
+
console.log(watch.message);
|
|
902
940
|
throw new CommandError(`Stack not found or not owned by the current user: ${stackId}`, 1, "not_found");
|
|
903
941
|
}
|
|
904
|
-
|
|
905
|
-
|
|
942
|
+
const { attention, currentCandidate, repair } = stackBlockers(stack);
|
|
943
|
+
const watch = stackWatchObligation({
|
|
944
|
+
stackId: stack.id,
|
|
945
|
+
terminal: stackTerminalReason(stack),
|
|
946
|
+
cursor: { enrolledHeadSha: currentCandidate?.headSha ?? null },
|
|
947
|
+
attention: attention ? { prNumber: attention.prNumber, blocker: attention.blocker } : null,
|
|
948
|
+
freshCursor: true,
|
|
949
|
+
});
|
|
950
|
+
if (asJson) {
|
|
951
|
+
console.log(JSON.stringify({ ...stack, repair, watch }, null, 2));
|
|
906
952
|
return;
|
|
907
953
|
}
|
|
908
954
|
const lines = formatStackHuman(stack, false);
|
|
@@ -915,6 +961,7 @@ export async function cmdStackStatus(args, deps = {}) {
|
|
|
915
961
|
(layer.cycloneStatus ? ` Cyclone: ${layer.cycloneStatus}` : "") +
|
|
916
962
|
(layer.tempestStatus ? ` Tempest: ${layer.tempestStatus}` : ""));
|
|
917
963
|
}
|
|
964
|
+
lines.push(...[...stackRepairLine(repair), ...stackWatchTextLines(watch)].map((line) => ` ${line}`));
|
|
918
965
|
await present("Stack status", lines);
|
|
919
966
|
}
|
|
920
967
|
/** Human summary lines for a `stack set` write, one per flag. */
|
|
@@ -932,6 +979,11 @@ export function stackSetSummary(parsed) {
|
|
|
932
979
|
}
|
|
933
980
|
return lines;
|
|
934
981
|
}
|
|
982
|
+
export function withWatch(body, watch) {
|
|
983
|
+
if (!watch || typeof body !== "object" || body === null || Array.isArray(body))
|
|
984
|
+
return body;
|
|
985
|
+
return { ...body, watch };
|
|
986
|
+
}
|
|
935
987
|
export async function cmdStackSet(args, deps = {}) {
|
|
936
988
|
const parsed = parseStackSetArgs(args);
|
|
937
989
|
const cfg = await (deps.loadConfig ?? loadConfig)();
|
|
@@ -940,18 +992,26 @@ export async function cmdStackSet(args, deps = {}) {
|
|
|
940
992
|
...(parsed.autoReview !== undefined ? { autoReviewOverride: parsed.autoReview } : {}),
|
|
941
993
|
...(parsed.autoPatch !== undefined ? { autoPatchOverride: parsed.autoPatch } : {}),
|
|
942
994
|
}, cfg);
|
|
995
|
+
const watch = openStackWatch(parsed.stackId, { unread: true });
|
|
943
996
|
if (parsed.asJson) {
|
|
944
|
-
console.log(JSON.stringify(body, null, 2));
|
|
997
|
+
console.log(JSON.stringify(withWatch(body, watch), null, 2));
|
|
945
998
|
return;
|
|
946
999
|
}
|
|
947
|
-
await present("Stack",
|
|
1000
|
+
await present("Stack", [
|
|
1001
|
+
...stackSetSummary(parsed).map((line) => ansi.brightGreen(` ${line} for ${parsed.stackId}`)),
|
|
1002
|
+
...stackWatchTextLines(watch).map((line) => ` ${line}`),
|
|
1003
|
+
]);
|
|
948
1004
|
}
|
|
949
1005
|
async function cmdStackAdopt(args) {
|
|
950
1006
|
const { owner, repo, prNumber, autoLand, autoReview, autoPatch, asJson } = parseStackAdoptArgs(args);
|
|
951
1007
|
const cfg = await loadConfig();
|
|
952
1008
|
const body = await adoptStack(owner, repo, prNumber, cfg, openPolicyPatch({ autoLand, autoReview, autoPatch }));
|
|
1009
|
+
const adoptedId = typeof body === "object" && body !== null
|
|
1010
|
+
? body.stack?.id
|
|
1011
|
+
: undefined;
|
|
1012
|
+
const watch = typeof adoptedId === "string" && adoptedId ? openStackWatch(adoptedId, { unread: true }) : null;
|
|
953
1013
|
if (asJson) {
|
|
954
|
-
console.log(JSON.stringify(body, null, 2));
|
|
1014
|
+
console.log(JSON.stringify(withWatch(body, watch), null, 2));
|
|
955
1015
|
return;
|
|
956
1016
|
}
|
|
957
1017
|
if (typeof body !== "object" || body === null) {
|
|
@@ -969,6 +1029,7 @@ async function cmdStackAdopt(args) {
|
|
|
969
1029
|
imported.push(ansi.dim(` trunk: ${stack.trunkBranch}`));
|
|
970
1030
|
}
|
|
971
1031
|
imported.push(ansi.dim(" Dashboard: https://mergestorm.ai/dashboard"));
|
|
1032
|
+
imported.push(...stackWatchTextLines(watch).map((line) => ` ${line}`));
|
|
972
1033
|
await present("Stack", imported);
|
|
973
1034
|
}
|
|
974
1035
|
async function cmdStackRestack(args) {
|
package/dist/shell.js
CHANGED
package/dist/stack-blockers.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type MergeQueueEntryDto, type StackDto, type StackLayerDto } from "./stack-dto.js";
|
|
1
|
+
import { type MergeQueueEntryDto, type StackAgentsBusy, type StackDto, type StackLayerDto } from "./stack-dto.js";
|
|
2
2
|
import type { StackWatchCursor } from "./stack-watch.js";
|
|
3
3
|
export type StackBlocker = {
|
|
4
4
|
prNumber: number;
|
|
@@ -6,11 +6,59 @@ export type StackBlocker = {
|
|
|
6
6
|
blocker: string;
|
|
7
7
|
bounceKind: string | null;
|
|
8
8
|
};
|
|
9
|
+
export type StackAgentBusy = {
|
|
10
|
+
prNumber: number;
|
|
11
|
+
headSha: string | null;
|
|
12
|
+
agent: "vortex" | "cyclone";
|
|
13
|
+
blocker: string;
|
|
14
|
+
};
|
|
15
|
+
export type StackLayerAgents = {
|
|
16
|
+
prNumber: number;
|
|
17
|
+
headSha: string | null;
|
|
18
|
+
vortexStatus: StackLayerDto["vortexStatus"];
|
|
19
|
+
cycloneStatus: StackLayerDto["cycloneStatus"];
|
|
20
|
+
vortexReview: {
|
|
21
|
+
status: string;
|
|
22
|
+
headSha: string | null;
|
|
23
|
+
pass: number | null;
|
|
24
|
+
} | null;
|
|
25
|
+
busy: {
|
|
26
|
+
vortex: boolean;
|
|
27
|
+
cyclone: boolean;
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
export type StackRepairHint = {
|
|
31
|
+
kind: "restack_conflict" | "merge_conflict";
|
|
32
|
+
prNumber: number;
|
|
33
|
+
headSha: string | null;
|
|
34
|
+
branch: string;
|
|
35
|
+
liveParent: string | null;
|
|
36
|
+
files: string[];
|
|
37
|
+
steps: string;
|
|
38
|
+
} | {
|
|
39
|
+
kind: "ci_failure";
|
|
40
|
+
prNumber: number;
|
|
41
|
+
headSha: string | null;
|
|
42
|
+
branch: string;
|
|
43
|
+
failingCheck: string | null;
|
|
44
|
+
steps: string;
|
|
45
|
+
};
|
|
46
|
+
export type StackLandGatePending = {
|
|
47
|
+
prNumber: number;
|
|
48
|
+
headSha: string | null;
|
|
49
|
+
reason: string;
|
|
50
|
+
};
|
|
51
|
+
export declare function landGateIsPending(reason: string | null | undefined): boolean;
|
|
52
|
+
export declare function layerAgentsBusy(layer: StackLayerDto): StackAgentsBusy;
|
|
53
|
+
export declare function layerAgents(layer: StackLayerDto): StackLayerAgents;
|
|
9
54
|
export declare function isMgParkBase(branch: string | null | undefined): boolean;
|
|
10
55
|
/** GitHub DIRTY vs an mg-park-* freeze is not a merge conflict, verified or not. */
|
|
11
56
|
export declare function boundDirty(layer: StackLayerDto): boolean;
|
|
12
57
|
export declare function sameHead(a: string | null | undefined, b: string | null | undefined): boolean;
|
|
13
58
|
export declare function currentLayer(stack: StackDto): StackLayerDto | undefined;
|
|
59
|
+
export declare function conflictLiveParent(stack: StackDto, layer: StackLayerDto): string | null;
|
|
60
|
+
export declare function conflictRepairSteps(branch: string, liveParent: string | null): string;
|
|
61
|
+
export declare function stackRepair(stack: StackDto, attention: StackBlocker | null, layer: StackLayerDto | undefined, bounce: MergeQueueEntryDto | undefined): StackRepairHint | null;
|
|
14
62
|
/** Pair-gate attention plus live hard blocks on every other open layer. */
|
|
15
63
|
export declare function stackBlockers(stack: StackDto, entries?: MergeQueueEntryDto[], cursor?: Pick<StackWatchCursor, "bounceId" | "afterFinishedAt">): {
|
|
16
64
|
attention: StackBlocker | null;
|
|
@@ -20,6 +68,10 @@ export declare function stackBlockers(stack: StackDto, entries?: MergeQueueEntry
|
|
|
20
68
|
headSha: string | null;
|
|
21
69
|
} | null;
|
|
22
70
|
bounce: MergeQueueEntryDto | undefined;
|
|
71
|
+
busy: StackAgentBusy[];
|
|
72
|
+
agents: StackLayerAgents | null;
|
|
73
|
+
repair: StackRepairHint | null;
|
|
74
|
+
landGatePending: StackLandGatePending | null;
|
|
23
75
|
};
|
|
24
76
|
/** Compact contract text shared by status and wait summaries. */
|
|
25
|
-
export declare function stackBlockersSummary(attention: Pick<StackBlocker, "prNumber" | "blocker"> | null, issues: StackBlocker[]): string;
|
|
77
|
+
export declare function stackBlockersSummary(attention: Pick<StackBlocker, "prNumber" | "blocker"> | null, issues: StackBlocker[], busy?: readonly StackAgentBusy[], landGatePending?: Pick<StackLandGatePending, "prNumber" | "reason"> | null): string;
|
package/dist/stack-blockers.js
CHANGED
|
@@ -1,4 +1,23 @@
|
|
|
1
|
-
import { mergeQueueBounceLabel, restackRetryPending } from "./stack-dto.js";
|
|
1
|
+
import { deriveStackAgentsBusy, mergeQueueBounceLabel, restackRetryPending, } from "./stack-dto.js";
|
|
2
|
+
export function landGateIsPending(reason) {
|
|
3
|
+
const token = reason?.trim().replace(/^landing blocked:\s*/i, "").match(/^[a-z_]+/i)?.[0] ?? "";
|
|
4
|
+
return /_(?:pending|running)$/i.test(token);
|
|
5
|
+
}
|
|
6
|
+
export function layerAgentsBusy(layer) {
|
|
7
|
+
return layer.agentsBusy ?? deriveStackAgentsBusy(layer);
|
|
8
|
+
}
|
|
9
|
+
export function layerAgents(layer) {
|
|
10
|
+
const busy = layerAgentsBusy(layer);
|
|
11
|
+
const review = layer.vortexReview;
|
|
12
|
+
return {
|
|
13
|
+
prNumber: layer.prNumber,
|
|
14
|
+
headSha: layer.headSha ?? null,
|
|
15
|
+
vortexStatus: layer.vortexStatus ?? null,
|
|
16
|
+
cycloneStatus: layer.cycloneStatus ?? null,
|
|
17
|
+
vortexReview: review ? { status: review.status, headSha: review.head_sha, pass: review.pass } : null,
|
|
18
|
+
busy: { vortex: busy.vortex, cyclone: busy.cyclone },
|
|
19
|
+
};
|
|
20
|
+
}
|
|
2
21
|
export function isMgParkBase(branch) {
|
|
3
22
|
return (branch ?? "").trim().toLowerCase().startsWith("mg-park-");
|
|
4
23
|
}
|
|
@@ -26,6 +45,22 @@ export function currentLayer(stack) {
|
|
|
26
45
|
return promote[0] ?? open.find((layer) => layer.prNumber === stack.unit?.landPrNumber) ??
|
|
27
46
|
(land && land.prNumber > 0 && land.state !== "merged" && land.state !== "closed" ? land : undefined);
|
|
28
47
|
}
|
|
48
|
+
function seamGateBlocker(stack, layer) {
|
|
49
|
+
const members = stack.unit?.members ?? [];
|
|
50
|
+
if (layer.prNumber === stack.unit?.landPrNumber)
|
|
51
|
+
return null;
|
|
52
|
+
if (!members.some((member) => member.promotedHeadSha?.trim()))
|
|
53
|
+
return null;
|
|
54
|
+
const member = members.find((entry) => entry.prNumber === layer.prNumber);
|
|
55
|
+
if (!member || member.promotedHeadSha?.trim())
|
|
56
|
+
return null;
|
|
57
|
+
const seamState = member.seamState?.trim().toLowerCase();
|
|
58
|
+
if (seamState === "failed")
|
|
59
|
+
return "Seam review failed";
|
|
60
|
+
if (seamState === "findings" && sameHead(member.seamReviewedSha, layer.headSha))
|
|
61
|
+
return "Seam findings";
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
29
64
|
/** Shared live-fact precedence; bounce history is only passed for the candidate. */
|
|
30
65
|
function layerAttention(stack, layer, queue, cursor) {
|
|
31
66
|
const none = { blocker: null, bounceKind: null };
|
|
@@ -63,9 +98,13 @@ function layerAttention(stack, layer, queue, cursor) {
|
|
|
63
98
|
return hardBlock(`Tempest ${tempest.status}`);
|
|
64
99
|
if (layer.vortexStatus === "failed")
|
|
65
100
|
return hardBlock("Review failed");
|
|
101
|
+
const seam = seamGateBlocker(stack, layer);
|
|
102
|
+
if (seam)
|
|
103
|
+
return hardBlock(seam);
|
|
66
104
|
if (stack.unit?.landPrNumber === layer.prNumber) {
|
|
67
|
-
|
|
68
|
-
|
|
105
|
+
const landing = stack.unit.landingBlockReason?.trim();
|
|
106
|
+
if (landing && !landGateIsPending(landing))
|
|
107
|
+
return hardBlock(landing);
|
|
69
108
|
if (stack.unit.tempestLandStatus?.toLowerCase() === "failed")
|
|
70
109
|
return hardBlock("Tempest failed");
|
|
71
110
|
}
|
|
@@ -81,6 +120,74 @@ function layerAttention(stack, layer, queue, cursor) {
|
|
|
81
120
|
return none;
|
|
82
121
|
return { blocker: mergeQueueBounceLabel(bounce), bounceKind: kind, bounce };
|
|
83
122
|
}
|
|
123
|
+
function isTerminalLayer(layer) {
|
|
124
|
+
return layer.state === "merged" || layer.state === "closed";
|
|
125
|
+
}
|
|
126
|
+
export function conflictLiveParent(stack, layer) {
|
|
127
|
+
const promoted = (candidate) => !!stack.unit?.members?.some((member) => member.prNumber === candidate.prNumber && member.promotedHeadSha?.trim());
|
|
128
|
+
const sibling = stack.layers
|
|
129
|
+
.filter((candidate) => candidate.prNumber > 0 && candidate.position < layer.position &&
|
|
130
|
+
!isTerminalLayer(candidate) && !promoted(candidate))
|
|
131
|
+
.sort((a, b) => b.position - a.position)[0];
|
|
132
|
+
if (sibling)
|
|
133
|
+
return sibling.branch;
|
|
134
|
+
const retired = new Set([
|
|
135
|
+
...stack.layers.filter((candidate) => isTerminalLayer(candidate) || promoted(candidate)).map((candidate) => candidate.branch),
|
|
136
|
+
...(stack.unit?.members ?? []).filter((member) => member.promotedHeadSha?.trim()).map((member) => member.branch),
|
|
137
|
+
]);
|
|
138
|
+
const usable = (branch) => !!branch?.trim() && !isMgParkBase(branch) && !retired.has(branch) && branch !== layer.branch;
|
|
139
|
+
for (const branch of [layer.restackError?.from?.branch, layer.parentBranch, stack.unit?.branch, stack.trunkBranch]) {
|
|
140
|
+
if (usable(branch))
|
|
141
|
+
return branch;
|
|
142
|
+
}
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
function conflictFiles(layer, bounce) {
|
|
146
|
+
const paths = bounce?.bounceDetail?.conflictPaths?.filter((path) => path.trim());
|
|
147
|
+
if (paths?.length)
|
|
148
|
+
return [...paths];
|
|
149
|
+
const detail = layer.restackError?.kind === "rebase_conflict" ? layer.restackError.detail : layer.conflictDetail;
|
|
150
|
+
const listed = detail?.match(/^conflict in (.+)$/)?.[1];
|
|
151
|
+
return listed ? listed.split(", ").map((path) => path.trim()).filter(Boolean) : [];
|
|
152
|
+
}
|
|
153
|
+
export function conflictRepairSteps(branch, liveParent) {
|
|
154
|
+
if (!liveParent) {
|
|
155
|
+
return `The live parent of ${branch} is unknown (it is never an mg-park-* freeze). Do not merge or rebase anything; stop and tell the human.`;
|
|
156
|
+
}
|
|
157
|
+
return `Merge ${liveParent} into ${branch}: git fetch origin, check out ${branch} at its live remote head, git merge origin/${liveParent}, resolve the conflicts, run the tests, then an ordinary git push (no force). Do not rebase onto trunk, main, or mg-park-*, and do not retarget the PR base.`;
|
|
158
|
+
}
|
|
159
|
+
export function stackRepair(stack, attention, layer, bounce) {
|
|
160
|
+
if (!attention || !layer || attention.prNumber !== layer.prNumber)
|
|
161
|
+
return null;
|
|
162
|
+
if (stack.unit && layer.prNumber === stack.unit.landPrNumber && layer.branch === stack.unit.branch)
|
|
163
|
+
return null;
|
|
164
|
+
const restackConflict = attention.blocker === "Conflict" || attention.bounceKind === "restack_conflict";
|
|
165
|
+
const mergeConflict = attention.blocker.startsWith("Merge conflicts");
|
|
166
|
+
if (restackConflict || mergeConflict) {
|
|
167
|
+
const liveParent = conflictLiveParent(stack, layer);
|
|
168
|
+
return {
|
|
169
|
+
kind: restackConflict ? "restack_conflict" : "merge_conflict",
|
|
170
|
+
prNumber: layer.prNumber,
|
|
171
|
+
headSha: layer.headSha ?? null,
|
|
172
|
+
branch: layer.branch,
|
|
173
|
+
liveParent,
|
|
174
|
+
files: conflictFiles(layer, bounce),
|
|
175
|
+
steps: conflictRepairSteps(layer.branch, liveParent),
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
if (attention.blocker.startsWith("CI failed") || attention.bounceKind === "ci_failure") {
|
|
179
|
+
const failingCheck = bounce?.bounceDetail?.failingCheck?.trim() || layer.checks?.failingName?.trim() || null;
|
|
180
|
+
return {
|
|
181
|
+
kind: "ci_failure",
|
|
182
|
+
prNumber: layer.prNumber,
|
|
183
|
+
headSha: layer.headSha ?? null,
|
|
184
|
+
branch: layer.branch,
|
|
185
|
+
failingCheck,
|
|
186
|
+
steps: `Read the failing ${failingCheck ? `check ${failingCheck}` : "checks"} on ${layer.branch} (gh run list --branch ${layer.branch}, then gh run view <run-id> --log-failed), fix it on ${layer.branch} with the smallest patch, run that check locally, confirm the remote head is still ${layer.headSha ?? "the head you started from"}, then an ordinary git push. Do not retarget the PR base.`,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
84
191
|
/** Pair-gate attention plus live hard blocks on every other open layer. */
|
|
85
192
|
export function stackBlockers(stack, entries = [], cursor = {}) {
|
|
86
193
|
const candidate = currentLayer(stack);
|
|
@@ -94,6 +201,7 @@ export function stackBlockers(stack, entries = [], cursor = {}) {
|
|
|
94
201
|
(boundDirty(layer) || layer.state === "conflict" ||
|
|
95
202
|
(layer.state !== "restacking" && layer.restackError?.kind === "rebase_conflict"))).sort((a, b) => a.position - b.position)[0];
|
|
96
203
|
let attention = null;
|
|
204
|
+
let attentionLayer;
|
|
97
205
|
let bounce;
|
|
98
206
|
const issues = [];
|
|
99
207
|
for (const layer of open) {
|
|
@@ -105,16 +213,41 @@ export function stackBlockers(stack, entries = [], cursor = {}) {
|
|
|
105
213
|
blocker: result.blocker, bounceKind: result.bounceKind };
|
|
106
214
|
if (selected || (!attention && layer === child)) {
|
|
107
215
|
attention = issue;
|
|
216
|
+
attentionLayer = layer;
|
|
108
217
|
bounce = result.bounce;
|
|
109
218
|
}
|
|
110
219
|
else if (!candidate || layer.position > candidate.position)
|
|
111
220
|
issues.push(issue);
|
|
112
221
|
}
|
|
113
|
-
|
|
222
|
+
const focus = attentionLayer ?? candidate;
|
|
223
|
+
const agents = focus ? layerAgents(focus) : null;
|
|
224
|
+
const busy = [];
|
|
225
|
+
if (attention && agents) {
|
|
226
|
+
for (const agent of ["cyclone", "vortex"]) {
|
|
227
|
+
if (agents.busy[agent])
|
|
228
|
+
busy.push({ prNumber: attention.prNumber, headSha: attention.headSha, agent, blocker: attention.blocker });
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
const landing = stack.unit?.landingBlockReason?.trim();
|
|
232
|
+
const landLayer = stack.unit?.landPrNumber != null && !stack.archivedAt
|
|
233
|
+
? open.find((layer) => layer.prNumber === stack.unit?.landPrNumber)
|
|
234
|
+
: undefined;
|
|
235
|
+
const landGatePending = landLayer && landing && landGateIsPending(landing)
|
|
236
|
+
? { prNumber: landLayer.prNumber, headSha: landLayer.headSha ?? null, reason: landing }
|
|
237
|
+
: null;
|
|
238
|
+
if (busy.length)
|
|
239
|
+
return { attention: null, issues, currentCandidate, bounce: undefined, busy, agents, repair: null, landGatePending };
|
|
240
|
+
return { attention, issues, currentCandidate, bounce, busy, agents, repair: stackRepair(stack, attention, attentionLayer, bounce), landGatePending };
|
|
114
241
|
}
|
|
115
242
|
/** Compact contract text shared by status and wait summaries. */
|
|
116
|
-
export function stackBlockersSummary(attention, issues) {
|
|
243
|
+
export function stackBlockersSummary(attention, issues, busy = [], landGatePending = null) {
|
|
117
244
|
const blocked = attention ? ` · blocked: #${attention.prNumber} ${attention.blocker}` : "";
|
|
245
|
+
const working = busy.length
|
|
246
|
+
? ` · held: #${busy[0].prNumber} ${busy[0].blocker} while ${busy.map((entry) => entry.agent === "cyclone" ? "Cyclone patches" : "Vortex reviews").join(" and ")}`
|
|
247
|
+
: "";
|
|
118
248
|
const labels = issues.slice(0, 3).map(issue => `#${issue.prNumber} ${issue.blocker}`).join("; ");
|
|
119
|
-
|
|
249
|
+
const gate = landGatePending && !attention
|
|
250
|
+
? ` · land gate: #${landGatePending.prNumber} ${landGatePending.reason} (nothing to fix; wait)`
|
|
251
|
+
: "";
|
|
252
|
+
return blocked + working + gate + (issues.length ? ` · issues: ${labels}${issues.length > 3 ? `; +${issues.length - 3} more` : ""}` : "");
|
|
120
253
|
}
|
package/dist/stack-dto.d.ts
CHANGED
|
@@ -5,6 +5,10 @@ export type RestackError = {
|
|
|
5
5
|
attemptedAt: string;
|
|
6
6
|
attempts: number;
|
|
7
7
|
backupRef: string | null;
|
|
8
|
+
from?: {
|
|
9
|
+
branch: string;
|
|
10
|
+
oldSha: string;
|
|
11
|
+
};
|
|
8
12
|
};
|
|
9
13
|
export declare function restackAttemptsExhausted(error: Pick<RestackError, "attempts">): boolean;
|
|
10
14
|
export declare function pushRejectionIsRetryable(message: string): boolean;
|
|
@@ -98,6 +102,16 @@ export type StackAgentRun = {
|
|
|
98
102
|
total: number;
|
|
99
103
|
} | null;
|
|
100
104
|
};
|
|
105
|
+
export type StackAgentsBusy = {
|
|
106
|
+
vortex: boolean;
|
|
107
|
+
cyclone: boolean;
|
|
108
|
+
headSha: string | null;
|
|
109
|
+
};
|
|
110
|
+
export declare const STACK_AGENT_QUEUED_STALE_MS: number;
|
|
111
|
+
export declare function deriveStackAgentsBusy(layer: Pick<StackLayerDto, "headSha" | "vortexStatus" | "cycloneStatus" | "agentRuns" | "vortexReview">, opts?: {
|
|
112
|
+
cycloneClaimed?: boolean;
|
|
113
|
+
nowMs?: number;
|
|
114
|
+
}): StackAgentsBusy;
|
|
101
115
|
export type StackLayerChecks = {
|
|
102
116
|
total: number;
|
|
103
117
|
success: number;
|
|
@@ -182,6 +196,7 @@ export type StackLayerDto = {
|
|
|
182
196
|
* detail once it says `done`. Undefined = producer predates the viewer.
|
|
183
197
|
*/
|
|
184
198
|
vortexReview?: StackVortexReviewView;
|
|
199
|
+
agentsBusy?: StackAgentsBusy | null;
|
|
185
200
|
conflictDetail: string | null;
|
|
186
201
|
restackError?: RestackError | null;
|
|
187
202
|
lastRestackedSha: string | null;
|
|
@@ -357,6 +372,7 @@ export type MergeQueueBounceDetail = {
|
|
|
357
372
|
failingCheck?: string;
|
|
358
373
|
conflictBranch?: string;
|
|
359
374
|
conflictDetail?: string;
|
|
375
|
+
conflictPaths?: string[];
|
|
360
376
|
message?: string;
|
|
361
377
|
};
|
|
362
378
|
export type MergeQueueEntryDto = {
|
|
@@ -379,5 +395,6 @@ export type MergeQueueEntryDto = {
|
|
|
379
395
|
verifyHeadSha: string | null;
|
|
380
396
|
/** Target tip included in the verified PR head. Null until recorded. */
|
|
381
397
|
verifyBaseSha: string | null;
|
|
398
|
+
cancelRequestedAt?: string | null;
|
|
382
399
|
finishedAt: string | null;
|
|
383
400
|
};
|
package/dist/stack-dto.js
CHANGED
|
@@ -16,6 +16,33 @@ export function restackRetryPending(error, layer) {
|
|
|
16
16
|
return false;
|
|
17
17
|
return !/\(origin\/\S+ moved to /.test(error.detail) && pushRejectionIsRetryable(error.detail);
|
|
18
18
|
}
|
|
19
|
+
export const STACK_AGENT_QUEUED_STALE_MS = 15 * 60 * 1000;
|
|
20
|
+
function agentShaMayBeHead(sha, head) {
|
|
21
|
+
const left = sha?.trim().toLowerCase();
|
|
22
|
+
const right = head?.trim().toLowerCase();
|
|
23
|
+
if (!left || !right)
|
|
24
|
+
return false;
|
|
25
|
+
return left.startsWith(right) || right.startsWith(left);
|
|
26
|
+
}
|
|
27
|
+
export function deriveStackAgentsBusy(layer, opts = {}) {
|
|
28
|
+
const headSha = layer.headSha?.trim() || null;
|
|
29
|
+
const nowMs = opts.nowMs ?? Date.now();
|
|
30
|
+
const runs = (layer.agentRuns ?? []).filter((run) => !run.finishedAt);
|
|
31
|
+
const vortexRuns = runs.filter((run) => run.agent === "vortex" && agentShaMayBeHead(run.sha, headSha));
|
|
32
|
+
const freshQueued = vortexRuns.some((run) => {
|
|
33
|
+
const queuedAt = Date.parse(run.startedAt ?? "");
|
|
34
|
+
return run.status === "queued" && Number.isFinite(queuedAt) && nowMs - queuedAt <= STACK_AGENT_QUEUED_STALE_MS;
|
|
35
|
+
});
|
|
36
|
+
const review = layer.vortexReview;
|
|
37
|
+
const reviewOnHead = !!review && agentShaMayBeHead(review.head_sha, headSha);
|
|
38
|
+
const vortex = (reviewOnHead && review.status === "reviewing") ||
|
|
39
|
+
vortexRuns.some((run) => run.status === "reviewing") ||
|
|
40
|
+
freshQueued ||
|
|
41
|
+
(layer.agentRuns === undefined && review === undefined && layer.vortexStatus === "reviewing");
|
|
42
|
+
const cyclone = opts.cycloneClaimed === true || layer.cycloneStatus === "patching" ||
|
|
43
|
+
runs.some((run) => run.agent === "cyclone" && run.status === "patching");
|
|
44
|
+
return { vortex, cyclone, headSha };
|
|
45
|
+
}
|
|
19
46
|
/** Every bounce kind understood by the shared contract. */
|
|
20
47
|
export const MERGE_QUEUE_BOUNCE_KINDS = [
|
|
21
48
|
"ci_failure",
|
|
@@ -47,6 +74,7 @@ export function mergeQueueBounceLabel(entry) {
|
|
|
47
74
|
if (kindLabel) {
|
|
48
75
|
const specifics = detail?.failingCheck ??
|
|
49
76
|
detail?.conflictBranch ??
|
|
77
|
+
(detail?.conflictPaths?.length ? detail.conflictPaths.join(", ") : null) ??
|
|
50
78
|
detail?.message ??
|
|
51
79
|
(detail?.prNumber != null ? `PR #${detail.prNumber}` : null);
|
|
52
80
|
return specifics ? `${kindLabel} — ${specifics}` : kindLabel;
|