mergestorm 0.3.18 → 0.3.19
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 +4 -3
- package/dist/api.d.ts +2 -2
- package/dist/api.js +42 -2
- package/dist/automation-catalog.d.ts +44 -0
- package/dist/automation-catalog.js +10 -0
- package/dist/client/index.d.ts +3 -1
- package/dist/client/index.js +3 -1
- package/dist/commands/browse.d.ts +1 -1
- package/dist/commands/browse.js +3 -3
- package/dist/commands/help.js +1 -1
- package/dist/commands/queue.d.ts +1 -1
- package/dist/commands/queue.js +21 -6
- package/dist/commands/registry.js +13 -6
- package/dist/commands/review-client.js +2 -10
- package/dist/commands/settings.d.ts +12 -4
- package/dist/commands/settings.js +78 -16
- package/dist/commands/skill.d.ts +4 -3
- package/dist/commands/skill.js +16 -6
- package/dist/commands/stack.d.ts +14 -2
- package/dist/commands/stack.js +89 -4
- package/dist/errors.d.ts +1 -1
- package/dist/stack-dto.d.ts +51 -1
- package/dist/stack-dto.js +37 -1
- package/dist/stack-watch.d.ts +44 -0
- package/dist/stack-watch.js +232 -0
- package/dist/ui/banner.js +3 -0
- package/package.json +1 -1
- package/skill/SKILL.md +6 -4
- package/skill/mergestorm-bounce-watch/SKILL.md +50 -0
- package/skill/mergestorm-pr-loop/SKILL.md +8 -4
package/dist/commands/skill.js
CHANGED
|
@@ -7,14 +7,16 @@ import { gitTopLevel } from "../repo-identity.js";
|
|
|
7
7
|
import { present } from "../ui/present.js";
|
|
8
8
|
export const SKILL_NAME = "mergestorm-review";
|
|
9
9
|
export const PR_LOOP_SKILL_NAME = "mergestorm-pr-loop";
|
|
10
|
-
export const
|
|
10
|
+
export const BOUNCE_WATCH_SKILL_NAME = "mergestorm-bounce-watch";
|
|
11
|
+
export const SKILL_NAMES = [SKILL_NAME, PR_LOOP_SKILL_NAME, BOUNCE_WATCH_SKILL_NAME];
|
|
11
12
|
export const SKILL_USAGE = `usage:
|
|
12
|
-
mergestorm skill install --claude|--cursor [--json]
|
|
13
|
+
mergestorm skill install --claude|--cursor|--agents [--json]
|
|
13
14
|
|
|
14
|
-
Copies
|
|
15
|
+
Copies ${SKILL_NAMES.join(", ")} into this repo (git toplevel, else cwd):
|
|
15
16
|
--claude .claude/skills/<name>/SKILL.md
|
|
16
17
|
--cursor .cursor/skills/<name>/SKILL.md
|
|
17
|
-
|
|
18
|
+
--agents .agents/skills/<name>/SKILL.md
|
|
19
|
+
Pass multiple flags to write multiple trees. Overwrites existing copies.`;
|
|
18
20
|
export function parseSkillArgs(args) {
|
|
19
21
|
const [sub, ...rest] = args;
|
|
20
22
|
if (!sub || sub === "-h" || sub === "--help" || sub === "help") {
|
|
@@ -40,13 +42,18 @@ export function parseSkillArgs(args) {
|
|
|
40
42
|
targets.push("cursor");
|
|
41
43
|
continue;
|
|
42
44
|
}
|
|
45
|
+
if (arg === "--agents") {
|
|
46
|
+
if (!targets.includes("agents"))
|
|
47
|
+
targets.push("agents");
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
43
50
|
if (arg === "-h" || arg === "--help" || arg === "help") {
|
|
44
51
|
return { help: true, json: false, targets: [] };
|
|
45
52
|
}
|
|
46
53
|
throw new CommandError(`${SKILL_USAGE}\nunknown flag: ${arg}`, 1, "usage");
|
|
47
54
|
}
|
|
48
55
|
if (targets.length === 0) {
|
|
49
|
-
throw new CommandError(`${SKILL_USAGE}\npass --claude and/or --
|
|
56
|
+
throw new CommandError(`${SKILL_USAGE}\npass --claude, --cursor, and/or --agents`, 1, "usage");
|
|
50
57
|
}
|
|
51
58
|
return { help: false, json, targets };
|
|
52
59
|
}
|
|
@@ -65,6 +72,9 @@ export function skillDestination(root, target, name = SKILL_NAME) {
|
|
|
65
72
|
if (target === "claude") {
|
|
66
73
|
return path.join(root, ".claude", "skills", name, "SKILL.md");
|
|
67
74
|
}
|
|
75
|
+
if (target === "agents") {
|
|
76
|
+
return path.join(root, ".agents", "skills", name, "SKILL.md");
|
|
77
|
+
}
|
|
68
78
|
return path.join(root, ".cursor", "skills", name, "SKILL.md");
|
|
69
79
|
}
|
|
70
80
|
export function resolveBundledSkillPath(fromFile = import.meta.url, name = SKILL_NAME) {
|
|
@@ -90,7 +100,7 @@ export function resolveBundledSkillPath(fromFile = import.meta.url, name = SKILL
|
|
|
90
100
|
}
|
|
91
101
|
async function assertNoSymlink(dest) {
|
|
92
102
|
const parts = dest.split(path.sep);
|
|
93
|
-
const marker = parts.findIndex((p) => p === ".claude" || p === ".cursor");
|
|
103
|
+
const marker = parts.findIndex((p) => p === ".claude" || p === ".cursor" || p === ".agents");
|
|
94
104
|
const first = marker === -1 ? parts.length - 1 : marker;
|
|
95
105
|
for (let i = first; i < parts.length; i++) {
|
|
96
106
|
const candidate = parts.slice(0, i + 1).join(path.sep);
|
package/dist/commands/stack.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { adoptStack, ensureUpperPark, listStacks, setStackPolicy, type StackDto, type StackPolicyPatch } from "../api.js";
|
|
1
|
+
import { adoptStack, ensureUpperPark, getEnrichedStack, listStacks, setStackPolicy, type StackDto, type StackPolicyPatch } from "../api.js";
|
|
2
2
|
import { loadConfig } from "../config.js";
|
|
3
3
|
import { createPr, findOpenPrNumber } from "../gh.js";
|
|
4
4
|
import { parseGithubOriginRepo } from "../git.js";
|
|
@@ -6,6 +6,7 @@ import { branchExists, commitsAheadOf, createBranchFromHead, currentBranch, defa
|
|
|
6
6
|
import { buildPrBodyFromCommit } from "../pr-body.js";
|
|
7
7
|
import { gitTopLevel } from "../repo-identity.js";
|
|
8
8
|
import { loadStackMeta, saveStackMeta } from "../stack-meta.js";
|
|
9
|
+
import { pollStackWatch } from "../stack-watch.js";
|
|
9
10
|
/** A registered stack layer that would become the parent of a new layer. */
|
|
10
11
|
export type RegisteredParentHit = {
|
|
11
12
|
stackId: string;
|
|
@@ -120,6 +121,15 @@ export type StackSubmitDeps = {
|
|
|
120
121
|
ensureUpperPark?: typeof ensureUpperPark;
|
|
121
122
|
};
|
|
122
123
|
export declare function cmdStackSubmit(args: string[], deps?: StackSubmitDeps): Promise<void>;
|
|
124
|
+
export declare function cmdStackWait(args: string[], deps?: {
|
|
125
|
+
loadConfig?: typeof loadConfig;
|
|
126
|
+
pollStackWatch?: typeof pollStackWatch;
|
|
127
|
+
signal?: AbortSignal;
|
|
128
|
+
}): Promise<void>;
|
|
129
|
+
export declare function cmdStackStatus(args: string[], deps?: {
|
|
130
|
+
loadConfig?: typeof loadConfig;
|
|
131
|
+
getEnrichedStack?: typeof getEnrichedStack;
|
|
132
|
+
}): Promise<void>;
|
|
123
133
|
export type StackSetDeps = {
|
|
124
134
|
loadConfig?: typeof loadConfig;
|
|
125
135
|
setStackPolicy?: typeof setStackPolicy;
|
|
@@ -127,4 +137,6 @@ export type StackSetDeps = {
|
|
|
127
137
|
/** Human summary lines for a `stack set` write, one per flag. */
|
|
128
138
|
export declare function stackSetSummary(parsed: StackSetArgs): string[];
|
|
129
139
|
export declare function cmdStackSet(args: string[], deps?: StackSetDeps): Promise<void>;
|
|
130
|
-
export declare function cmdStack(args: string[]
|
|
140
|
+
export declare function cmdStack(args: string[], options?: {
|
|
141
|
+
signal?: AbortSignal;
|
|
142
|
+
}): Promise<void>;
|
package/dist/commands/stack.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import { adoptStack, ensureUpperPark, landNextStack, listStacks, restackStack, setStackPolicy, } from "../api.js";
|
|
1
|
+
import { adoptStack, ensureUpperPark, getEnrichedStack, landNextStack, listStacks, restackStack, setStackPolicy, } from "../api.js";
|
|
2
2
|
import { loadConfig } from "../config.js";
|
|
3
|
-
import { CommandError } from "../errors.js";
|
|
3
|
+
import { rateLimitedMessage, REVIEW_EXIT, CommandError } from "../errors.js";
|
|
4
4
|
import { createPr, findOpenPrNumber, requireGh } from "../gh.js";
|
|
5
5
|
import { parseGithubOriginRepo } from "../git.js";
|
|
6
6
|
import { branchExists, commitsAheadOf, createBranchFromHead, currentBranch, defaultLayerBranchName, deleteBranch, discoverTrunk, fetchRemoteBranch, pushBranch, tipCommitMessage, tipCommitSubject, worktreeDirty, } from "../git-stack.js";
|
|
7
7
|
import { buildPrBodyFromCommit } from "../pr-body.js";
|
|
8
8
|
import { gitTopLevel } from "../repo-identity.js";
|
|
9
9
|
import { activeLayers, appendLayer, emptyStackMeta, loadStackMeta, removeActiveStack, resetStackMeta, saveStackMeta, selectStackForCreate, setActivePolicy, } from "../stack-meta.js";
|
|
10
|
+
import { pollStackWatch, StackWatchError, StackWatchTimeoutError, } from "../stack-watch.js";
|
|
10
11
|
import { isMgParkBranch, planSubmitLayerBases } from "../submit-pr-base.js";
|
|
11
12
|
import { ansi } from "../ui/ansi.js";
|
|
12
13
|
import { runLineTabsBrowser } from "../ui/line-tabs.js";
|
|
@@ -18,6 +19,8 @@ const STACK_USAGE = `usage:
|
|
|
18
19
|
mergestorm stack submit [--extend] [--auto-land on|off] [--auto-review on|off] [--auto-patch on|off] [--json]
|
|
19
20
|
mergestorm stack reset --force
|
|
20
21
|
mergestorm stack list [--json]
|
|
22
|
+
mergestorm stack status <stack-id> [--json]
|
|
23
|
+
mergestorm stack wait <stack-id> [--json] [--timeout <s>]
|
|
21
24
|
mergestorm stack set <stack-id> [--auto-land on|off] [--auto-review on|off|default] [--auto-patch on|off|default] [--json]
|
|
22
25
|
mergestorm stack adopt <owner/repo>#<pr> [--auto-land on|off] [--auto-review on|off] [--auto-patch on|off] [--json]
|
|
23
26
|
mergestorm stack restack <stack-id> [--json]
|
|
@@ -143,7 +146,7 @@ export function stackPolicyLabels(s) {
|
|
|
143
146
|
}
|
|
144
147
|
return labels;
|
|
145
148
|
}
|
|
146
|
-
function formatStackHuman(s) {
|
|
149
|
+
function formatStackHuman(s, includeLayers = true) {
|
|
147
150
|
const lines = [];
|
|
148
151
|
lines.push(` ${ansi.bold(`${s.owner}/${s.repo}`)} trunk=${s.trunkBranch}` +
|
|
149
152
|
stackPolicyLabels(s)
|
|
@@ -154,6 +157,8 @@ function formatStackHuman(s) {
|
|
|
154
157
|
lines.push(ansi.dim(" (no layers)"));
|
|
155
158
|
return lines;
|
|
156
159
|
}
|
|
160
|
+
if (!includeLayers)
|
|
161
|
+
return lines;
|
|
157
162
|
for (const layer of s.layers) {
|
|
158
163
|
const title = layer.title ? ` ${layer.title}` : "";
|
|
159
164
|
const pr = layer.prNumber > 0 ? `#${layer.prNumber}` : "—";
|
|
@@ -787,6 +792,82 @@ async function cmdStackList(args) {
|
|
|
787
792
|
listed.push(ansi.dim(" Dashboard: https://mergestorm.ai/dashboard"));
|
|
788
793
|
await present("Stacks", listed);
|
|
789
794
|
}
|
|
795
|
+
export async function cmdStackWait(args, deps = {}) {
|
|
796
|
+
const usage = "usage: mergestorm stack wait <stack-id> [--json] [--timeout <s>]";
|
|
797
|
+
const positional = [];
|
|
798
|
+
let timeoutS = 45;
|
|
799
|
+
for (let i = 0; i < args.length; i++) {
|
|
800
|
+
const arg = args[i];
|
|
801
|
+
if (arg === "--json")
|
|
802
|
+
continue;
|
|
803
|
+
if (arg === "--timeout" || arg.startsWith("--timeout=")) {
|
|
804
|
+
const value = arg === "--timeout" ? args[++i] : arg.slice("--timeout=".length);
|
|
805
|
+
timeoutS = value?.trim() ? Number(value) : NaN;
|
|
806
|
+
if (!Number.isFinite(timeoutS) || timeoutS < 0 || timeoutS > 300) {
|
|
807
|
+
throw new CommandError(`${usage}\n--timeout must be between 0 and 300 seconds`, REVIEW_EXIT.usage, "usage");
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
else if (arg.startsWith("-")) {
|
|
811
|
+
throw new CommandError(usage, REVIEW_EXIT.usage, "usage");
|
|
812
|
+
}
|
|
813
|
+
else {
|
|
814
|
+
positional.push(arg);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
if (positional.length !== 1)
|
|
818
|
+
throw new CommandError(usage, REVIEW_EXIT.usage, "usage");
|
|
819
|
+
const stackId = requireStackId(positional[0], usage);
|
|
820
|
+
const cfg = await (deps.loadConfig ?? loadConfig)();
|
|
821
|
+
const print = (envelope, retryAfterSeconds) => console.log(args.includes("--json")
|
|
822
|
+
? JSON.stringify({
|
|
823
|
+
...envelope,
|
|
824
|
+
...(retryAfterSeconds !== undefined ? { retry_after_seconds: retryAfterSeconds } : {}),
|
|
825
|
+
}, null, 2)
|
|
826
|
+
: `Stack ${envelope.stackId} · ${envelope.status}${envelope.blocker ? ` · ${envelope.blocker}` : ""}`);
|
|
827
|
+
try {
|
|
828
|
+
print(await (deps.pollStackWatch ?? pollStackWatch)(cfg, stackId, {
|
|
829
|
+
timeoutMs: timeoutS * 1000,
|
|
830
|
+
signal: deps.signal,
|
|
831
|
+
}));
|
|
832
|
+
}
|
|
833
|
+
catch (err) {
|
|
834
|
+
if (err instanceof StackWatchError && err.lastEnvelope.status === "rate_limited") {
|
|
835
|
+
print(err.lastEnvelope, err.retryAfterSeconds);
|
|
836
|
+
throw new CommandError(rateLimitedMessage(err.retryAfterSeconds), REVIEW_EXIT.rate_limited, "rate_limited", { retryAfterSeconds: err.retryAfterSeconds });
|
|
837
|
+
}
|
|
838
|
+
if (!(err instanceof StackWatchTimeoutError))
|
|
839
|
+
throw err;
|
|
840
|
+
print(err.lastEnvelope);
|
|
841
|
+
throw new CommandError(err.message, REVIEW_EXIT.timeout, "review_timeout");
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
export async function cmdStackStatus(args, deps = {}) {
|
|
845
|
+
const usage = "usage: mergestorm stack status <stack-id> [--json]";
|
|
846
|
+
const positional = args.filter((arg) => arg !== "--json");
|
|
847
|
+
if (positional.length !== 1)
|
|
848
|
+
throw new CommandError(usage, 2, "usage");
|
|
849
|
+
const stackId = requireStackId(positional[0], usage);
|
|
850
|
+
const cfg = await (deps.loadConfig ?? loadConfig)();
|
|
851
|
+
const stack = await (deps.getEnrichedStack ?? getEnrichedStack)(stackId, cfg);
|
|
852
|
+
if (!stack) {
|
|
853
|
+
throw new CommandError(`Stack not found or not owned by the current user: ${stackId}`, 1, "not_found");
|
|
854
|
+
}
|
|
855
|
+
if (args.includes("--json")) {
|
|
856
|
+
console.log(JSON.stringify(stack, null, 2));
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
const lines = formatStackHuman(stack, false);
|
|
860
|
+
for (const layer of stack.layers) {
|
|
861
|
+
const head = layer.headSha ? `@${layer.headSha.slice(0, 7)}` : "";
|
|
862
|
+
const pr = layer.prNumber > 0 ? `#${layer.prNumber}` : "—";
|
|
863
|
+
lines.push(` ${String(layer.position).padStart(2)} ${pr}${head} ${layer.state.padEnd(14)} ${layer.branch}` +
|
|
864
|
+
` CI: ${layer.ciStatus ?? "unknown"} review: ${layer.reviewStatus ?? "unknown"}` +
|
|
865
|
+
(layer.vortexStatus ? ` Vortex: ${layer.vortexStatus}` : "") +
|
|
866
|
+
(layer.cycloneStatus ? ` Cyclone: ${layer.cycloneStatus}` : "") +
|
|
867
|
+
(layer.tempestStatus ? ` Tempest: ${layer.tempestStatus}` : ""));
|
|
868
|
+
}
|
|
869
|
+
await present("Stack status", lines);
|
|
870
|
+
}
|
|
790
871
|
/** Human summary lines for a `stack set` write, one per flag. */
|
|
791
872
|
export function stackSetSummary(parsed) {
|
|
792
873
|
const tri = (value) => value === null ? "default (account setting)" : value ? "on" : "off";
|
|
@@ -885,7 +966,7 @@ async function cmdStackLand(args) {
|
|
|
885
966
|
` on ${id}`),
|
|
886
967
|
]);
|
|
887
968
|
}
|
|
888
|
-
export async function cmdStack(args) {
|
|
969
|
+
export async function cmdStack(args, options = {}) {
|
|
889
970
|
const sub = args[0]?.toLowerCase();
|
|
890
971
|
const rest = args.slice(1);
|
|
891
972
|
// Help must exit 0 — print usage, do not throw CommandError.
|
|
@@ -905,6 +986,10 @@ export async function cmdStack(args) {
|
|
|
905
986
|
return cmdStackReset(rest);
|
|
906
987
|
if (sub === "list" || sub === "ls")
|
|
907
988
|
return cmdStackList(rest);
|
|
989
|
+
if (sub === "status")
|
|
990
|
+
return cmdStackStatus(rest);
|
|
991
|
+
if (sub === "wait")
|
|
992
|
+
return cmdStackWait(rest, { signal: options.signal });
|
|
908
993
|
if (sub === "set")
|
|
909
994
|
return cmdStackSet(rest);
|
|
910
995
|
if (sub === "adopt")
|
package/dist/errors.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Stable machine codes for CommandError control flow.
|
|
3
3
|
* UX copy in `message` may change freely; match on `code`, never on message text.
|
|
4
4
|
*/
|
|
5
|
-
export type CommandErrorCode = "auth_invalid" | "api_timeout" | "not_a_repo" | "not_found" | "missing_api_key" | "registered_parent" | "usage" | "review_quota" | "review_failed" | "review_timeout" | "rate_limited";
|
|
5
|
+
export type CommandErrorCode = "auth_invalid" | "api_timeout" | "not_a_repo" | "not_found" | "missing_api_key" | "registered_parent" | "usage" | "review_quota" | "review_failed" | "review_timeout" | "rate_limited" | "cyclone_not_installed" | "pr_merged" | "pr_closed" | "not_stacked" | "draft" | "reserved_branch" | "supabase_unconfigured" | "cyclone_not_connected" | "busy" | "adopt_failed";
|
|
6
6
|
/** Stable one-shot exit codes for machine callers of `mergestorm review`. */
|
|
7
7
|
export declare const REVIEW_EXIT: {
|
|
8
8
|
readonly success: 0;
|
package/dist/stack-dto.d.ts
CHANGED
|
@@ -21,6 +21,31 @@ export type StackReviewStatus = "approved" | "changes_requested" | "reviewed" |
|
|
|
21
21
|
* (#1947): Work paints Incomplete, never Reviewed or all clear.
|
|
22
22
|
*/
|
|
23
23
|
export type StackVortexStatus = "queued" | "skipped" | "reviewing" | "seam_pending" | "all_clear" | "findings" | "throttled" | "failed" | "incomplete";
|
|
24
|
+
/**
|
|
25
|
+
* MS-14 viewer: the one lifecycle status of the Vortex review on a PR head.
|
|
26
|
+
* `pr_reviews` is the authority when a row exists for the SHA; a leftover
|
|
27
|
+
* `github_webhook_jobs` row paints `queued` / `reviewing` only when no pass row
|
|
28
|
+
* exists yet. `none` = never dispatched. `done` = a pass posted; the finer
|
|
29
|
+
* verdict detail lives on `vortexStatus` / `agentRuns`.
|
|
30
|
+
*/
|
|
31
|
+
export type StackVortexReviewViewStatus = "none" | "queued" | "reviewing" | "skipped" | "done" | "failed";
|
|
32
|
+
export type StackVortexReviewView = {
|
|
33
|
+
status: StackVortexReviewViewStatus;
|
|
34
|
+
/** `pr_reviews.skip_reason` for `skipped`; null when the writer recorded none. */
|
|
35
|
+
skip_reason: string | null;
|
|
36
|
+
/** SHA + pass identity of the attempt (#2027); null before a pass row exists. */
|
|
37
|
+
pass: number | null;
|
|
38
|
+
/** Head the status speaks for; null when neither table named one. */
|
|
39
|
+
head_sha: string | null;
|
|
40
|
+
/** In-flight sub-state while `reviewing` (fanout / synthesizing). */
|
|
41
|
+
phase: string | null;
|
|
42
|
+
/** Earliest live job start while `reviewing`; null when lease-only. */
|
|
43
|
+
started_at: string | null;
|
|
44
|
+
/** Stop control visibility: true only while a pass can actually be cancelled. */
|
|
45
|
+
stoppable: boolean;
|
|
46
|
+
/** Which table spoke: the authority row, a live job, a lease, or nothing. */
|
|
47
|
+
source: "pr_reviews" | "github_webhook_jobs" | "lease" | "none";
|
|
48
|
+
};
|
|
24
49
|
export type StackCycloneStatus = "patching" | "awaiting_fix";
|
|
25
50
|
export type StackTempestStatus = "reviewing" | "findings" | "clear" | "failed" | "stale";
|
|
26
51
|
export type StackAgentName = "vortex" | "cyclone" | "tempest";
|
|
@@ -140,6 +165,12 @@ export type StackLayerDto = {
|
|
|
140
165
|
* Undefined = producer predates runs (clients fall back to the rollup).
|
|
141
166
|
*/
|
|
142
167
|
agentRuns?: StackAgentRun[];
|
|
168
|
+
/**
|
|
169
|
+
* MS-14 viewer output for this layer's head. Every surface renders the
|
|
170
|
+
* lifecycle status from here; `vortexStatus` / `agentRuns` only add verdict
|
|
171
|
+
* detail once it says `done`. Undefined = producer predates the viewer.
|
|
172
|
+
*/
|
|
173
|
+
vortexReview?: StackVortexReviewView;
|
|
143
174
|
conflictDetail: string | null;
|
|
144
175
|
restackError?: RestackError | null;
|
|
145
176
|
lastRestackedSha: string | null;
|
|
@@ -249,6 +280,21 @@ export type StackDto = {
|
|
|
249
280
|
* Missing on older payloads is off.
|
|
250
281
|
*/
|
|
251
282
|
autoEnqueueWhenReady?: boolean;
|
|
283
|
+
/**
|
|
284
|
+
* Auto land Settling certificate. The stacks watcher publishes the identity
|
|
285
|
+
* of the settle clock it actually started (action, exact PR and head, server
|
|
286
|
+
* start time) and clears it when that clock is consumed or invalidated.
|
|
287
|
+
* Absent (older payloads) or `null` means no certified settle: paint nothing.
|
|
288
|
+
* Remaining time derives from `startedAt`; a refresh never restarts it.
|
|
289
|
+
* Partial or invalid server rows map to `null`, never to a guessed value.
|
|
290
|
+
*/
|
|
291
|
+
autoEnqueueSettle?: {
|
|
292
|
+
action: "ready" | "promote";
|
|
293
|
+
prNumber: number;
|
|
294
|
+
headSha: string;
|
|
295
|
+
/** ISO timestamp of the watcher's clock start. */
|
|
296
|
+
startedAt: string;
|
|
297
|
+
} | null;
|
|
252
298
|
/**
|
|
253
299
|
* Per-stack Vortex auto-review override. `null` (or missing) follows the
|
|
254
300
|
* account `auto_review_enabled` flag; a boolean wins in both directions.
|
|
@@ -286,7 +332,11 @@ export type StackDto = {
|
|
|
286
332
|
export type MergeQueueEntryState = "queued" | "running" | "waiting" | "landed" | "bounced" | "cancelled";
|
|
287
333
|
export type MergeQueueEnqueuedBy = "human" | "agent";
|
|
288
334
|
export type MergeQueueEnqueuedVia = "dashboard" | "cli" | "chat" | "api" | "mcp";
|
|
289
|
-
export type MergeQueueBounceKind = "ci_failure" | "ci_timeout" | "head_moved" | "tempest_findings" | "seam_findings" | "restack_conflict" | "pr_draft" | "merge_failed" | "gh_error";
|
|
335
|
+
export type MergeQueueBounceKind = "ci_failure" | "ci_timeout" | "head_moved" | "tempest_findings" | "seam_findings" | "restack_conflict" | "pr_draft" | "must_consolidate" | "merge_failed" | "gh_error";
|
|
336
|
+
/** Every bounce kind understood by the shared contract. */
|
|
337
|
+
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"];
|
|
338
|
+
export declare const BOUNCE_KIND_LABELS: Readonly<Record<MergeQueueBounceKind, string>>;
|
|
339
|
+
export declare function mergeQueueBounceLabel(entry: MergeQueueEntryDto): string;
|
|
290
340
|
/** Structured bounce handoff — enough for the bar chip and the chat quote. */
|
|
291
341
|
export type MergeQueueBounceDetail = {
|
|
292
342
|
kind: MergeQueueBounceKind;
|
package/dist/stack-dto.js
CHANGED
|
@@ -1 +1,37 @@
|
|
|
1
|
-
|
|
1
|
+
/** Every bounce kind understood by the shared contract. */
|
|
2
|
+
export const MERGE_QUEUE_BOUNCE_KINDS = [
|
|
3
|
+
"ci_failure",
|
|
4
|
+
"ci_timeout",
|
|
5
|
+
"head_moved",
|
|
6
|
+
"tempest_findings",
|
|
7
|
+
"seam_findings",
|
|
8
|
+
"restack_conflict",
|
|
9
|
+
"pr_draft",
|
|
10
|
+
"must_consolidate",
|
|
11
|
+
"merge_failed",
|
|
12
|
+
"gh_error",
|
|
13
|
+
];
|
|
14
|
+
export const BOUNCE_KIND_LABELS = {
|
|
15
|
+
ci_failure: "CI failed",
|
|
16
|
+
ci_timeout: "CI timed out",
|
|
17
|
+
head_moved: "head moved",
|
|
18
|
+
tempest_findings: "Tempest findings",
|
|
19
|
+
seam_findings: "seam review findings",
|
|
20
|
+
restack_conflict: "restack conflict",
|
|
21
|
+
pr_draft: "draft PR",
|
|
22
|
+
must_consolidate: "needs promote",
|
|
23
|
+
merge_failed: "merge failed",
|
|
24
|
+
gh_error: "GitHub error",
|
|
25
|
+
};
|
|
26
|
+
export function mergeQueueBounceLabel(entry) {
|
|
27
|
+
const detail = entry.bounceDetail;
|
|
28
|
+
const kindLabel = detail ? BOUNCE_KIND_LABELS[detail.kind] : undefined;
|
|
29
|
+
if (kindLabel) {
|
|
30
|
+
const specifics = detail?.failingCheck ??
|
|
31
|
+
detail?.conflictBranch ??
|
|
32
|
+
detail?.message ??
|
|
33
|
+
(detail?.prNumber != null ? `PR #${detail.prNumber}` : null);
|
|
34
|
+
return specifics ? `${kindLabel} — ${specifics}` : kindLabel;
|
|
35
|
+
}
|
|
36
|
+
return entry.bounceReason?.trim() || "bounced";
|
|
37
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { apiFetch } from "./api.js";
|
|
2
|
+
import type { Config } from "./config.js";
|
|
3
|
+
export declare const STACK_WATCH_SCHEMA: "mergestorm.stack_watch/v1";
|
|
4
|
+
export declare const STACK_WATCH_DEFAULT_TIMEOUT_MS = 45000;
|
|
5
|
+
/** Resume selectors are fixed for the entire wait, including transient retries. */
|
|
6
|
+
export type StackWatchCursor = {
|
|
7
|
+
readonly stackId: string;
|
|
8
|
+
readonly enrolledHeadSha: string | null;
|
|
9
|
+
readonly afterFinishedAt?: string | null;
|
|
10
|
+
readonly bounceId?: string | null;
|
|
11
|
+
};
|
|
12
|
+
export type StackWatchEnvelope = {
|
|
13
|
+
schema: typeof STACK_WATCH_SCHEMA;
|
|
14
|
+
status: "attention" | "waiting" | "in_progress" | "rate_limited" | "failed";
|
|
15
|
+
stackId: string;
|
|
16
|
+
blocker: string | null;
|
|
17
|
+
bounceKind: string | null;
|
|
18
|
+
prNumber: number | null;
|
|
19
|
+
headSha: string | null;
|
|
20
|
+
cursor: StackWatchCursor;
|
|
21
|
+
};
|
|
22
|
+
export type PollStackWatchOptions = {
|
|
23
|
+
timeoutMs?: number;
|
|
24
|
+
intervalMs?: number;
|
|
25
|
+
cursor?: StackWatchCursor;
|
|
26
|
+
signal?: AbortSignal;
|
|
27
|
+
onTick?: (envelope: StackWatchEnvelope) => void;
|
|
28
|
+
sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;
|
|
29
|
+
now?: () => number;
|
|
30
|
+
random?: () => number;
|
|
31
|
+
/** Test seam at the apiFetch boundary, including HTTP status and Retry-After. */
|
|
32
|
+
fetch?: typeof apiFetch;
|
|
33
|
+
};
|
|
34
|
+
export declare class StackWatchTimeoutError extends Error {
|
|
35
|
+
readonly lastEnvelope: StackWatchEnvelope;
|
|
36
|
+
constructor(lastEnvelope: StackWatchEnvelope);
|
|
37
|
+
}
|
|
38
|
+
export declare class StackWatchError extends Error {
|
|
39
|
+
readonly lastEnvelope: StackWatchEnvelope;
|
|
40
|
+
readonly retryAfterSeconds?: number | undefined;
|
|
41
|
+
constructor(message: string, lastEnvelope: StackWatchEnvelope, options?: ErrorOptions, retryAfterSeconds?: number | undefined);
|
|
42
|
+
}
|
|
43
|
+
/** Wait on queue invalidations, then evaluate a fresh stack snapshot. */
|
|
44
|
+
export declare function pollStackWatch(cfg: Config, stackId: string, opts?: PollStackWatchOptions): Promise<StackWatchEnvelope>;
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { setTimeout as sleep } from "node:timers/promises";
|
|
2
|
+
import { apiFetch } from "./api.js";
|
|
3
|
+
import { REVIEW_POLL_INTERVAL_MS, REVIEW_POLL_MAX_TRANSIENT_RETRIES, isTransientReviewPollError, isTransientReviewPollStatus, transientRetryWaitMs, } from "./commands/review-client.js";
|
|
4
|
+
import { mergeQueueBounceLabel } from "./stack-dto.js";
|
|
5
|
+
export const STACK_WATCH_SCHEMA = "mergestorm.stack_watch/v1";
|
|
6
|
+
export const STACK_WATCH_DEFAULT_TIMEOUT_MS = 45_000;
|
|
7
|
+
export class StackWatchTimeoutError extends Error {
|
|
8
|
+
lastEnvelope;
|
|
9
|
+
constructor(lastEnvelope) {
|
|
10
|
+
super(`Timed out waiting for stack ${lastEnvelope.stackId}`);
|
|
11
|
+
this.lastEnvelope = lastEnvelope;
|
|
12
|
+
this.name = "StackWatchTimeoutError";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export class StackWatchError extends Error {
|
|
16
|
+
lastEnvelope;
|
|
17
|
+
retryAfterSeconds;
|
|
18
|
+
constructor(message, lastEnvelope, options, retryAfterSeconds) {
|
|
19
|
+
super(message, options);
|
|
20
|
+
this.lastEnvelope = lastEnvelope;
|
|
21
|
+
this.retryAfterSeconds = retryAfterSeconds;
|
|
22
|
+
this.name = "StackWatchError";
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function sameHead(a, b) {
|
|
26
|
+
const left = a?.trim().toLowerCase();
|
|
27
|
+
const right = b?.trim().toLowerCase();
|
|
28
|
+
return !!left && !!right && (left.startsWith(right) || right.startsWith(left));
|
|
29
|
+
}
|
|
30
|
+
function currentLayer(stack) {
|
|
31
|
+
const open = stack.layers.filter((layer) => layer.prNumber > 0 && layer.state !== "merged" && layer.state !== "closed");
|
|
32
|
+
const promote = open.filter((layer) => layer.prNumber !== stack.unit?.landPrNumber &&
|
|
33
|
+
!stack.unit?.members.some((member) => member.prNumber === layer.prNumber && member.promotedHeadSha)).sort((a, b) => a.position - b.position);
|
|
34
|
+
const land = stack.unit?.landPr;
|
|
35
|
+
return promote[0] ?? open.find((layer) => layer.prNumber === stack.unit?.landPrNumber) ??
|
|
36
|
+
(land && land.prNumber > 0 && land.state !== "merged" && land.state !== "closed" ? land : undefined);
|
|
37
|
+
}
|
|
38
|
+
/** Keep label text and precedence aligned with MCP stack-summary.blockerLabel. */
|
|
39
|
+
function attention(stack, layer, queue, cursor) {
|
|
40
|
+
const none = { blocker: null, bounceKind: null };
|
|
41
|
+
if (stack.archivedAt || queue.some((entry) => ["queued", "running", "waiting"].includes(entry.state)))
|
|
42
|
+
return none;
|
|
43
|
+
const bounce = queue.filter((entry) => entry.state === "bounced")
|
|
44
|
+
.sort((a, b) => (Date.parse(b.finishedAt ?? "") || 0) - (Date.parse(a.finishedAt ?? "") || 0))[0];
|
|
45
|
+
const kind = bounce?.bounceDetail?.kind;
|
|
46
|
+
const recoverable = kind === "head_moved" || kind === "must_consolidate";
|
|
47
|
+
const recoverableSha = kind === "head_moved"
|
|
48
|
+
? bounce?.bounceDetail?.headSha ?? bounce?.verifyHeadSha
|
|
49
|
+
: bounce?.verifyHeadSha ?? bounce?.bounceDetail?.headSha;
|
|
50
|
+
const currentRecoverable = recoverable && sameHead(layer?.headSha, recoverableSha);
|
|
51
|
+
const hardBlock = (blocker) => ({ blocker, bounceKind: null });
|
|
52
|
+
if (layer) {
|
|
53
|
+
if (layer.restackError && layer.state !== "restacking") {
|
|
54
|
+
return hardBlock(layer.restackError.kind === "rebase_conflict" ? "Conflict" : "Restack failed");
|
|
55
|
+
}
|
|
56
|
+
if (layer.state === "conflict")
|
|
57
|
+
return hardBlock("Conflict");
|
|
58
|
+
if (layer.draft)
|
|
59
|
+
return hardBlock("Draft PR");
|
|
60
|
+
if ((!layer.headSha || layer.mergeableHeadSha === layer.headSha) &&
|
|
61
|
+
(layer.mergeable === false || layer.mergeableState?.toLowerCase() === "dirty"))
|
|
62
|
+
return hardBlock("Merge conflicts");
|
|
63
|
+
if (!currentRecoverable && (layer.ciStatus === "failure" || (layer.checks?.failure ?? 0) > 0)) {
|
|
64
|
+
const name = layer.checks?.failingName?.trim();
|
|
65
|
+
return hardBlock(`CI failed${name ? ` — ${name}` : ""}`);
|
|
66
|
+
}
|
|
67
|
+
const tempest = layer.agentRuns?.find((run) => run.agent === "tempest" &&
|
|
68
|
+
["findings", "failed"].includes(run.status) && sameHead(run.sha, layer.headSha));
|
|
69
|
+
if (tempest)
|
|
70
|
+
return hardBlock(`Tempest ${tempest.status}`);
|
|
71
|
+
if (layer.vortexStatus === "failed")
|
|
72
|
+
return hardBlock("Review failed");
|
|
73
|
+
if (stack.unit?.landPrNumber === layer.prNumber) {
|
|
74
|
+
if (stack.unit.landingBlockReason?.trim())
|
|
75
|
+
return hardBlock(stack.unit.landingBlockReason.trim());
|
|
76
|
+
if (stack.unit.tempestLandStatus?.toLowerCase() === "failed")
|
|
77
|
+
return hardBlock("Tempest failed");
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (!bounce || !kind || recoverable || !layer)
|
|
81
|
+
return none;
|
|
82
|
+
if (cursor.bounceId === bounce.id || (cursor.afterFinishedAt &&
|
|
83
|
+
!(Date.parse(bounce.finishedAt ?? "") > Date.parse(cursor.afterFinishedAt))))
|
|
84
|
+
return none;
|
|
85
|
+
// Compare with the observed promote/land head, never the enrollment or verify head.
|
|
86
|
+
if (!sameHead(bounce.bounceDetail?.headSha ?? bounce.verifyHeadSha, layer.headSha) ||
|
|
87
|
+
(bounce.bounceDetail?.prNumber != null && bounce.bounceDetail.prNumber !== layer.prNumber))
|
|
88
|
+
return none;
|
|
89
|
+
return { blocker: mergeQueueBounceLabel(bounce), bounceKind: kind, bounce };
|
|
90
|
+
}
|
|
91
|
+
/** Wait on queue invalidations, then evaluate a fresh stack snapshot. */
|
|
92
|
+
export async function pollStackWatch(cfg, stackId, opts = {}) {
|
|
93
|
+
const id = stackId.trim().toLowerCase();
|
|
94
|
+
if (!id || (opts.cursor && opts.cursor.stackId.trim().toLowerCase() !== id)) {
|
|
95
|
+
throw new TypeError("A matching stackId is required");
|
|
96
|
+
}
|
|
97
|
+
const timeoutMs = opts.timeoutMs ?? STACK_WATCH_DEFAULT_TIMEOUT_MS;
|
|
98
|
+
const intervalMs = opts.intervalMs ?? REVIEW_POLL_INTERVAL_MS;
|
|
99
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs < 0 || !Number.isFinite(intervalMs) || intervalMs <= 0) {
|
|
100
|
+
throw new TypeError("timeoutMs must be nonnegative and intervalMs must be positive");
|
|
101
|
+
}
|
|
102
|
+
const now = opts.now ?? Date.now;
|
|
103
|
+
const wait = opts.sleep ?? ((ms, signal) => sleep(ms, undefined, { signal }));
|
|
104
|
+
const fetch = opts.fetch ?? apiFetch;
|
|
105
|
+
const startedAt = now();
|
|
106
|
+
const deadline = startedAt + timeoutMs;
|
|
107
|
+
let enrolled = opts.cursor !== undefined;
|
|
108
|
+
let cursor = Object.freeze({
|
|
109
|
+
...(opts.cursor ?? { stackId: id, enrolledHeadSha: null }),
|
|
110
|
+
stackId: id,
|
|
111
|
+
});
|
|
112
|
+
let lastEnvelope = {
|
|
113
|
+
schema: STACK_WATCH_SCHEMA, status: "waiting", stackId: id, blocker: null,
|
|
114
|
+
bounceKind: null, prNumber: null, headSha: null, cursor,
|
|
115
|
+
};
|
|
116
|
+
let transientFailures = 0;
|
|
117
|
+
const timeout = () => new StackWatchTimeoutError({ ...lastEnvelope, status: "waiting" });
|
|
118
|
+
const pause = async (ms) => {
|
|
119
|
+
const remaining = Math.min(ms, Math.max(0, deadline - now()));
|
|
120
|
+
if (remaining > 0)
|
|
121
|
+
await wait(remaining, opts.signal);
|
|
122
|
+
};
|
|
123
|
+
const request = async (route, finalSnapshot = false) => {
|
|
124
|
+
let readAfterDeadline = finalSnapshot;
|
|
125
|
+
while (readAfterDeadline || now() < deadline) {
|
|
126
|
+
readAfterDeadline = false;
|
|
127
|
+
opts.signal?.throwIfAborted();
|
|
128
|
+
let response;
|
|
129
|
+
try {
|
|
130
|
+
response = await fetch(cfg, route, {
|
|
131
|
+
signal: opts.signal, timeoutMs: route.includes("&wait=")
|
|
132
|
+
? Number(new URL(route, "https://local").searchParams.get("wait")) * 1000 + 5_000
|
|
133
|
+
: finalSnapshot ? 5_000 : Math.max(1, Math.min(30_000, deadline - now())),
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
catch (err) {
|
|
137
|
+
if (opts.signal?.aborted || (err instanceof Error && err.name === "AbortError"))
|
|
138
|
+
throw err;
|
|
139
|
+
if (now() >= deadline)
|
|
140
|
+
throw timeout();
|
|
141
|
+
if (isTransientReviewPollError(err) && transientFailures < REVIEW_POLL_MAX_TRANSIENT_RETRIES) {
|
|
142
|
+
transientFailures += 1;
|
|
143
|
+
opts.onTick?.(lastEnvelope);
|
|
144
|
+
await pause(transientRetryWaitMs({ failureCount: transientFailures, random: opts.random }));
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
throw new StackWatchError(err instanceof Error ? err.message : String(err), { ...lastEnvelope, status: "failed" }, { cause: err });
|
|
148
|
+
}
|
|
149
|
+
if (response.status === 200)
|
|
150
|
+
return response.body;
|
|
151
|
+
if (isTransientReviewPollStatus(response.status) && transientFailures < REVIEW_POLL_MAX_TRANSIENT_RETRIES) {
|
|
152
|
+
transientFailures += 1;
|
|
153
|
+
opts.onTick?.({ ...lastEnvelope, status: response.status === 429 ? "rate_limited" : lastEnvelope.status });
|
|
154
|
+
await pause(transientRetryWaitMs({
|
|
155
|
+
failureCount: transientFailures, retryAfterSeconds: response.retryAfterSeconds, random: opts.random,
|
|
156
|
+
}));
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
throw new StackWatchError(`Stack poll failed (HTTP ${response.status})`, {
|
|
160
|
+
...lastEnvelope, status: response.status === 429 ? "rate_limited" : "failed",
|
|
161
|
+
}, undefined, response.retryAfterSeconds);
|
|
162
|
+
}
|
|
163
|
+
throw timeout();
|
|
164
|
+
};
|
|
165
|
+
while (now() < deadline) {
|
|
166
|
+
const snapshot = await request(`/api/v1/stacks/enrich?stackId=${encodeURIComponent(id)}`);
|
|
167
|
+
let stack = snapshot && Array.isArray(snapshot.stacks)
|
|
168
|
+
? snapshot.stacks.find((stack) => stack.id.trim().toLowerCase() === id)
|
|
169
|
+
: undefined;
|
|
170
|
+
if (!stack || !Array.isArray(stack.layers)) {
|
|
171
|
+
throw new StackWatchError("Stack snapshot missing or invalid", { ...lastEnvelope, status: "failed" });
|
|
172
|
+
}
|
|
173
|
+
let layer = currentLayer(stack);
|
|
174
|
+
if (!enrolled) {
|
|
175
|
+
cursor = Object.freeze({ stackId: id, enrolledHeadSha: layer?.headSha ?? null });
|
|
176
|
+
enrolled = true;
|
|
177
|
+
}
|
|
178
|
+
lastEnvelope = { ...lastEnvelope, cursor, prNumber: layer?.prNumber ?? null, headSha: layer?.headSha ?? null };
|
|
179
|
+
const initialQueue = await request(`/api/v1/stacks/queue?stackId=${encodeURIComponent(id)}`);
|
|
180
|
+
if (!initialQueue || !Array.isArray(initialQueue.entries)) {
|
|
181
|
+
throw new StackWatchError("Invalid merge queue response", { ...lastEnvelope, status: "failed" });
|
|
182
|
+
}
|
|
183
|
+
const initialEntries = initialQueue.entries.filter((entry) => entry.stackId.trim().toLowerCase() === id);
|
|
184
|
+
const initial = attention(stack, layer, initialEntries, cursor);
|
|
185
|
+
if (initial.blocker) {
|
|
186
|
+
const { bounce, ...result } = initial;
|
|
187
|
+
if (bounce) {
|
|
188
|
+
cursor = Object.freeze({
|
|
189
|
+
...cursor,
|
|
190
|
+
bounceId: bounce.id,
|
|
191
|
+
...(bounce.finishedAt !== undefined ? { afterFinishedAt: bounce.finishedAt } : {}),
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
return { ...lastEnvelope, ...result, cursor, status: "attention" };
|
|
195
|
+
}
|
|
196
|
+
const seconds = Math.min(45, Math.max(1, Math.ceil((deadline - now()) / 1000)));
|
|
197
|
+
const queue = await request(`/api/v1/stacks/queue?stackId=${encodeURIComponent(id)}&wait=${seconds}`);
|
|
198
|
+
if (!queue || !Array.isArray(queue.entries)) {
|
|
199
|
+
throw new StackWatchError("Invalid merge queue response", { ...lastEnvelope, status: "failed" });
|
|
200
|
+
}
|
|
201
|
+
if (now() < deadline) {
|
|
202
|
+
const refreshed = await request(`/api/v1/stacks/enrich?stackId=${encodeURIComponent(id)}`, true);
|
|
203
|
+
stack = Array.isArray(refreshed?.stacks)
|
|
204
|
+
? refreshed.stacks.find((candidate) => candidate.id.trim().toLowerCase() === id)
|
|
205
|
+
: undefined;
|
|
206
|
+
if (!stack || !Array.isArray(stack.layers)) {
|
|
207
|
+
throw new StackWatchError("Stack snapshot missing or invalid", { ...lastEnvelope, status: "failed" });
|
|
208
|
+
}
|
|
209
|
+
layer = currentLayer(stack);
|
|
210
|
+
lastEnvelope = { ...lastEnvelope, prNumber: layer?.prNumber ?? null, headSha: layer?.headSha ?? null };
|
|
211
|
+
}
|
|
212
|
+
transientFailures = 0;
|
|
213
|
+
const entries = queue.entries.filter((entry) => entry.stackId.trim().toLowerCase() === id);
|
|
214
|
+
const { bounce, ...result } = attention(stack, layer, entries, cursor);
|
|
215
|
+
if (bounce) {
|
|
216
|
+
cursor = Object.freeze({
|
|
217
|
+
...cursor,
|
|
218
|
+
bounceId: bounce.id,
|
|
219
|
+
...(bounce.finishedAt !== undefined ? { afterFinishedAt: bounce.finishedAt } : {}),
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
lastEnvelope = {
|
|
223
|
+
...lastEnvelope, ...result, cursor,
|
|
224
|
+
status: result.blocker ? "attention" : entries.some((entry) => ["queued", "running", "waiting"].includes(entry.state))
|
|
225
|
+
? "in_progress" : "waiting",
|
|
226
|
+
};
|
|
227
|
+
if (lastEnvelope.status === "attention")
|
|
228
|
+
return lastEnvelope;
|
|
229
|
+
opts.onTick?.(lastEnvelope);
|
|
230
|
+
}
|
|
231
|
+
throw timeout();
|
|
232
|
+
}
|
package/dist/ui/banner.js
CHANGED