mergestorm 0.3.62 → 0.3.64
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 +12 -6
- package/dist/api.d.ts +21 -3
- package/dist/api.js +24 -2
- package/dist/automation-catalog.d.ts +13 -3
- package/dist/automation-catalog.js +3 -1
- package/dist/client/index.d.ts +4 -3
- package/dist/client/index.js +4 -3
- package/dist/commands/browse.d.ts +1 -1
- package/dist/commands/dismiss.js +4 -1
- package/dist/commands/memory.d.ts +30 -0
- package/dist/commands/memory.js +152 -0
- package/dist/commands/registry.js +18 -1
- package/dist/commands/settings.d.ts +2 -1
- package/dist/commands/settings.js +49 -4
- package/dist/commands/stack.d.ts +10 -1
- package/dist/commands/stack.js +92 -27
- package/dist/config.d.ts +2 -0
- package/dist/config.js +11 -1
- package/dist/errors.d.ts +1 -1
- package/dist/git-stack.d.ts +2 -0
- package/dist/git-stack.js +15 -0
- package/dist/memory-api.d.ts +73 -0
- package/dist/memory-api.js +69 -0
- package/dist/server-publish.d.ts +21 -6
- package/dist/server-publish.js +77 -42
- package/dist/stack-blockers.d.ts +9 -2
- package/dist/stack-blockers.js +55 -16
- package/dist/stack-dto.d.ts +3 -0
- package/dist/stack-watch-loop.js +23 -2
- package/dist/stack-watch.d.ts +2 -1
- package/dist/stack-watch.js +6 -2
- package/dist/ui/banner.js +6 -0
- package/package.json +11 -2
- package/skill/SKILL.md +1 -1
- package/skill/mergestorm-bounce-watch/SKILL.md +5 -3
- package/skill/mergestorm-pr-loop/SKILL.md +7 -3
package/dist/server-publish.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
import { getStackPublication, submitStackPublication, } from "./api.js";
|
|
3
3
|
import { loadConfig } from "./config.js";
|
|
4
|
-
import { CommandError } from "./errors.js";
|
|
4
|
+
import { CommandError, isCommandErrorCode } from "./errors.js";
|
|
5
5
|
import { parseGithubOriginRepo } from "./git.js";
|
|
6
6
|
import { currentBranch, discoverTrunk, tipCommitMessage, tipCommitSubject } from "./git-stack.js";
|
|
7
7
|
import { buildPrBodyFromCommit } from "./pr-body.js";
|
|
@@ -12,26 +12,30 @@ export const SERVER_PUBLISH_MAX_TOTAL_BYTES = 8 * 1024 * 1024;
|
|
|
12
12
|
export const SERVER_PUBLISH_MAX_COMMIT_BYTES = 1024 * 1024;
|
|
13
13
|
export const SERVER_PUBLISH_FILE_MODES = ["100644", "100755", "120000"];
|
|
14
14
|
export const SERVER_PUBLISH_HISTORY_SCAN = 500;
|
|
15
|
-
const READ_ONLY_GIT = new Set(["cat-file", "diff-tree", "merge-base", "rev-list", "rev-parse", "symbolic-ref"]);
|
|
15
|
+
export const READ_ONLY_GIT = new Set(["cat-file", "diff-tree", "merge-base", "rev-list", "rev-parse", "symbolic-ref"]);
|
|
16
16
|
export function readGit(args, cwd) {
|
|
17
17
|
if (!READ_ONLY_GIT.has(args[0] ?? "")) {
|
|
18
18
|
throw new Error(`server-side publication only reads the repository; refusing git ${args[0] ?? ""}`);
|
|
19
19
|
}
|
|
20
20
|
const result = spawnSync("git", args, { cwd, maxBuffer: 256 * 1024 * 1024 });
|
|
21
|
+
if (result.error)
|
|
22
|
+
throw result.error;
|
|
21
23
|
if (result.status !== 0) {
|
|
22
|
-
throw new
|
|
24
|
+
throw new CommandError(result.stderr?.toString("utf8").trim() || `git ${args.join(" ")} failed`, 1, "git_failed");
|
|
23
25
|
}
|
|
24
26
|
return result.stdout;
|
|
25
27
|
}
|
|
26
|
-
function text(read, args, cwd) {
|
|
27
|
-
return read(args, cwd).toString("utf8").trim();
|
|
28
|
+
async function text(read, args, cwd) {
|
|
29
|
+
return (await read(args, cwd)).toString("utf8").trim();
|
|
28
30
|
}
|
|
29
|
-
function resolves(read, ref, cwd) {
|
|
31
|
+
async function resolves(read, ref, cwd) {
|
|
30
32
|
try {
|
|
31
|
-
return text(read, ["rev-parse", "--verify", "--quiet", `${ref}^{commit}`], cwd) || null;
|
|
33
|
+
return await text(read, ["rev-parse", "--verify", "--quiet", `${ref}^{commit}`], cwd) || null;
|
|
32
34
|
}
|
|
33
|
-
catch {
|
|
34
|
-
|
|
35
|
+
catch (err) {
|
|
36
|
+
if (isCommandErrorCode(err, "git_failed"))
|
|
37
|
+
return null;
|
|
38
|
+
throw err;
|
|
35
39
|
}
|
|
36
40
|
}
|
|
37
41
|
function isoWithOffset(epochSeconds, offset) {
|
|
@@ -52,12 +56,12 @@ function parseIdentity(line, sha) {
|
|
|
52
56
|
}
|
|
53
57
|
return { name: match[1].trim() ? match[1] : match[2], email: match[2], date };
|
|
54
58
|
}
|
|
55
|
-
export function readCommitHeader(read, sha, cwd) {
|
|
56
|
-
const commitBytes = Number(text(read, ["cat-file", "-s", sha], cwd));
|
|
59
|
+
export async function readCommitHeader(read, sha, cwd) {
|
|
60
|
+
const commitBytes = Number(await text(read, ["cat-file", "-s", sha], cwd));
|
|
57
61
|
if (commitBytes > SERVER_PUBLISH_MAX_COMMIT_BYTES) {
|
|
58
62
|
throw new CommandError(`Commit ${sha} is ${commitBytes} bytes; server-side publication reads commits up to ${SERVER_PUBLISH_MAX_COMMIT_BYTES} bytes.`, 1, "payload_too_large");
|
|
59
63
|
}
|
|
60
|
-
const commitObject = read(["cat-file", "commit", sha], cwd);
|
|
64
|
+
const commitObject = await read(["cat-file", "commit", sha], cwd);
|
|
61
65
|
let raw;
|
|
62
66
|
try {
|
|
63
67
|
raw = new TextDecoder("utf-8", { fatal: true }).decode(commitObject);
|
|
@@ -87,8 +91,8 @@ export function readCommitHeader(read, sha, cwd) {
|
|
|
87
91
|
}
|
|
88
92
|
return { tree, parents, author: parseIdentity(author, sha), committer: parseIdentity(committer, sha), message };
|
|
89
93
|
}
|
|
90
|
-
export function readCommitChanges(read, parent, sha, cwd, budget) {
|
|
91
|
-
const listing = read(["diff-tree", "-r", "-z", "--raw", "--no-renames", "--no-commit-id", "--abbrev=40", parent, sha], cwd);
|
|
94
|
+
export async function readCommitChanges(read, parent, sha, cwd, budget) {
|
|
95
|
+
const listing = await read(["diff-tree", "-r", "-z", "--raw", "--no-renames", "--no-commit-id", "--abbrev=40", parent, sha], cwd);
|
|
92
96
|
let raw;
|
|
93
97
|
try {
|
|
94
98
|
raw = new TextDecoder("utf-8", { fatal: true }).decode(listing);
|
|
@@ -114,7 +118,7 @@ export function readCommitChanges(read, parent, sha, cwd, budget) {
|
|
|
114
118
|
throw new CommandError(`${path} has Git mode ${newMode}, which server-side publication does not support` +
|
|
115
119
|
`${newMode === "160000" ? " (it is a submodule)" : ""}. Supported modes are ${SERVER_PUBLISH_FILE_MODES.join(", ")}.`, 1, "unsupported_publication");
|
|
116
120
|
}
|
|
117
|
-
const size = Number(text(read, ["cat-file", "-s", newSha], cwd));
|
|
121
|
+
const size = Number(await text(read, ["cat-file", "-s", newSha], cwd));
|
|
118
122
|
if (size > SERVER_PUBLISH_MAX_FILE_BYTES) {
|
|
119
123
|
throw new CommandError(`${path} is ${size} bytes; server-side publication carries files up to ${SERVER_PUBLISH_MAX_FILE_BYTES} bytes.`, 1, "payload_too_large");
|
|
120
124
|
}
|
|
@@ -122,30 +126,32 @@ export function readCommitChanges(read, parent, sha, cwd, budget) {
|
|
|
122
126
|
if (budget.bytes > SERVER_PUBLISH_MAX_TOTAL_BYTES) {
|
|
123
127
|
throw new CommandError(`More than ${SERVER_PUBLISH_MAX_TOTAL_BYTES} bytes of file content; server-side publication cannot carry this branch in one request.`, 1, "payload_too_large");
|
|
124
128
|
}
|
|
125
|
-
const content = read(["cat-file", "blob", newSha], cwd);
|
|
129
|
+
const content = await read(["cat-file", "blob", newSha], cwd);
|
|
126
130
|
changes.push({ path, mode: newMode, sha: newSha, content: content.toString("base64") });
|
|
127
131
|
}
|
|
128
132
|
return changes;
|
|
129
133
|
}
|
|
130
|
-
export function readPublicationCommits(read, from, to, cwd) {
|
|
134
|
+
export async function readPublicationCommits(read, from, to, cwd) {
|
|
131
135
|
try {
|
|
132
|
-
read(["merge-base", "--is-ancestor", from, to], cwd);
|
|
136
|
+
await read(["merge-base", "--is-ancestor", from, to], cwd);
|
|
133
137
|
}
|
|
134
|
-
catch {
|
|
138
|
+
catch (err) {
|
|
139
|
+
if (!isCommandErrorCode(err, "git_failed"))
|
|
140
|
+
throw err;
|
|
135
141
|
throw new CommandError(`${from} is not an ancestor of ${to}; the commits to publish are not a straight line.`, 1, "unsupported_publication");
|
|
136
142
|
}
|
|
137
|
-
if (text(read, ["rev-list", "--min-parents=2", "--max-count=1", `${from}..${to}`], cwd)) {
|
|
143
|
+
if (await text(read, ["rev-list", "--min-parents=2", "--max-count=1", `${from}..${to}`], cwd)) {
|
|
138
144
|
throw new CommandError(`A merge commit sits between ${from} and ${to}. Server-side publication carries single-parent commits only; rebase or squash the branch.`, 1, "unsupported_publication");
|
|
139
145
|
}
|
|
140
|
-
const shas = text(read, ["rev-list", "--reverse", `${from}..${to}`], cwd).split("\n").filter(Boolean);
|
|
146
|
+
const shas = (await text(read, ["rev-list", "--reverse", `${from}..${to}`], cwd)).split("\n").filter(Boolean);
|
|
141
147
|
if (shas.length > SERVER_PUBLISH_MAX_COMMITS) {
|
|
142
148
|
throw new CommandError(`${shas.length} commits to publish; server-side publication carries at most ${SERVER_PUBLISH_MAX_COMMITS} at once. Squash the branch.`, 1, "payload_too_large");
|
|
143
149
|
}
|
|
144
150
|
const commits = [];
|
|
145
151
|
const budget = { changes: 0, bytes: 0 };
|
|
146
152
|
for (const sha of shas) {
|
|
147
|
-
const header = readCommitHeader(read, sha, cwd);
|
|
148
|
-
const changes = readCommitChanges(read, header.parents[0], sha, cwd, budget);
|
|
153
|
+
const header = await readCommitHeader(read, sha, cwd);
|
|
154
|
+
const changes = await readCommitChanges(read, header.parents[0], sha, cwd, budget);
|
|
149
155
|
commits.push({
|
|
150
156
|
message: header.message,
|
|
151
157
|
author: header.author,
|
|
@@ -156,8 +162,8 @@ export function readPublicationCommits(read, from, to, cwd) {
|
|
|
156
162
|
}
|
|
157
163
|
return commits;
|
|
158
164
|
}
|
|
159
|
-
function localCommitWithTree(read, from, branch, head, cwd) {
|
|
160
|
-
const lines = text(read, ["rev-list", `--max-count=${SERVER_PUBLISH_HISTORY_SCAN}`, "--format=%H %T", "--no-commit-header", `${from}..${branch}`], cwd)
|
|
165
|
+
async function localCommitWithTree(read, from, branch, head, cwd) {
|
|
166
|
+
const lines = (await text(read, ["rev-list", `--max-count=${SERVER_PUBLISH_HISTORY_SCAN}`, "--format=%H %T", "--no-commit-header", `${from}..${branch}`], cwd))
|
|
161
167
|
.split("\n")
|
|
162
168
|
.filter(Boolean);
|
|
163
169
|
let match = null;
|
|
@@ -176,21 +182,23 @@ export async function publishBranchServerSide(input, deps = {}) {
|
|
|
176
182
|
const status = deps.getStackPublication ?? getStackPublication;
|
|
177
183
|
const submit = deps.submitStackPublication ?? submitStackPublication;
|
|
178
184
|
const { cfg, cwd, owner, repo, branch } = input;
|
|
179
|
-
|
|
180
|
-
|
|
185
|
+
const local = input.localRef ?? branch;
|
|
186
|
+
if (!(await resolves(read, local, cwd)))
|
|
187
|
+
throw new CommandError(`Branch missing locally: ${local}`);
|
|
181
188
|
const remote = await status(cfg, { owner, repo, branch });
|
|
182
189
|
const baseBranch = remote.pr?.base ?? input.baseBranch;
|
|
190
|
+
deps.assertBase?.(baseBranch);
|
|
183
191
|
const localBase = input.localBaseRef ??
|
|
184
|
-
(resolves(read, `refs/remotes/origin/${input.baseBranch}`, cwd) ? `refs/remotes/origin/${input.baseBranch}` : input.baseBranch);
|
|
185
|
-
if (!resolves(read, localBase, cwd))
|
|
192
|
+
((await resolves(read, `refs/remotes/origin/${input.baseBranch}`, cwd)) ? `refs/remotes/origin/${input.baseBranch}` : input.baseBranch);
|
|
193
|
+
if (!(await resolves(read, localBase, cwd)))
|
|
186
194
|
throw new CommandError(`Base branch missing locally: ${localBase}`);
|
|
187
|
-
const fork = text(read, ["merge-base", localBase,
|
|
195
|
+
const fork = await text(read, ["merge-base", localBase, local], cwd);
|
|
188
196
|
const baseSha = input.remoteBaseSha ?? fork;
|
|
189
197
|
let onto = "base";
|
|
190
198
|
let expectedHead = null;
|
|
191
199
|
let from = fork;
|
|
192
200
|
if (remote.head) {
|
|
193
|
-
const published = remote.head.tree ? localCommitWithTree(read, fork,
|
|
201
|
+
const published = remote.head.tree ? await localCommitWithTree(read, fork, local, { sha: remote.head.sha, tree: remote.head.tree }, cwd) : null;
|
|
194
202
|
if (published) {
|
|
195
203
|
onto = "head";
|
|
196
204
|
expectedHead = remote.head.sha;
|
|
@@ -208,7 +216,7 @@ export async function publishBranchServerSide(input, deps = {}) {
|
|
|
208
216
|
`To replace that head with your local branch, pass --replace-head ${remote.head.sha}.`, 1, "head_changed");
|
|
209
217
|
}
|
|
210
218
|
}
|
|
211
|
-
const commits = readPublicationCommits(read, from,
|
|
219
|
+
const commits = await readPublicationCommits(read, from, local, cwd);
|
|
212
220
|
if (commits.length === 0 && onto === "base") {
|
|
213
221
|
throw new CommandError(`No commits on \`${branch}\` ahead of \`${input.baseBranch}\`. Commit your changes, then retry.`);
|
|
214
222
|
}
|
|
@@ -223,12 +231,19 @@ export async function publishBranchServerSide(input, deps = {}) {
|
|
|
223
231
|
pr: { title: input.title, body: input.body },
|
|
224
232
|
...(typeof input.autoLand === "boolean" ? { autoLand: input.autoLand } : {}),
|
|
225
233
|
});
|
|
226
|
-
const localTree = text(read, ["rev-parse", `${
|
|
234
|
+
const localTree = await text(read, ["rev-parse", `${local}^{tree}`], cwd);
|
|
227
235
|
if (publication.head.tree !== localTree) {
|
|
228
236
|
throw new CommandError(`GitHub has tree ${publication.head.tree} at ${branch}, but your local branch has tree ${localTree}.`, 1, "tree_mismatch");
|
|
229
237
|
}
|
|
230
238
|
return { ...publication, baseBranch };
|
|
231
239
|
}
|
|
240
|
+
export function assertExplicitStacking(input) {
|
|
241
|
+
if (input.extend || input.parent === input.trunk)
|
|
242
|
+
return;
|
|
243
|
+
throw new CommandError(`\`${input.parent}\` is not trunk (\`${input.trunk}\`), and stacking onto it needs --extend.\n` +
|
|
244
|
+
`• Stack onto \`${input.parent}\`: ${input.stackCommand}\n` +
|
|
245
|
+
`• New stack from \`${input.trunk}\`: ${input.trunkCommand}`, 1, "implicit_stack");
|
|
246
|
+
}
|
|
232
247
|
export function workingBranchTarget(input) {
|
|
233
248
|
const cwd = input.cwd ?? process.cwd();
|
|
234
249
|
const origin = input.owner && input.repo ? { owner: input.owner, repo: input.repo } : parseGithubOriginRepo(cwd);
|
|
@@ -242,17 +257,36 @@ export function workingBranchTarget(input) {
|
|
|
242
257
|
}
|
|
243
258
|
export async function publishWorkingBranch(input, deps = {}) {
|
|
244
259
|
const { cwd, owner, repo, branch } = workingBranchTarget(input);
|
|
245
|
-
|
|
260
|
+
const { publishBranchServerSide: publish = publishBranchServerSide, loadConfig: load = loadConfig, discoverTrunk: findTrunk = discoverTrunk, ...publishDeps } = deps;
|
|
261
|
+
let trunk = null;
|
|
262
|
+
let trunkError = null;
|
|
263
|
+
try {
|
|
264
|
+
trunk = findTrunk(cwd);
|
|
265
|
+
}
|
|
266
|
+
catch (err) {
|
|
267
|
+
trunkError = err;
|
|
268
|
+
}
|
|
269
|
+
const trunkMessage = trunkError instanceof Error ? trunkError.message : String(trunkError);
|
|
270
|
+
const baseBranch = input.base ?? trunk;
|
|
246
271
|
if (!baseBranch) {
|
|
247
|
-
|
|
248
|
-
|
|
272
|
+
throw new CommandError(`${trunkMessage} Name the base branch.`);
|
|
273
|
+
}
|
|
274
|
+
const assertBase = (parent) => {
|
|
275
|
+
if (trunk) {
|
|
276
|
+
assertExplicitStacking({
|
|
277
|
+
parent,
|
|
278
|
+
trunk,
|
|
279
|
+
extend: input.extend === true,
|
|
280
|
+
stackCommand: `\`mg stack publish --base ${parent} --extend\` (MCP stack_publish: extend true)`,
|
|
281
|
+
trunkCommand: "`mg stack publish` with no --base (MCP stack_publish: no base)",
|
|
282
|
+
});
|
|
249
283
|
}
|
|
250
|
-
|
|
251
|
-
throw new CommandError(`${
|
|
284
|
+
else if (input.extend !== true) {
|
|
285
|
+
throw new CommandError(`${trunkMessage} \`${parent}\` cannot be verified as trunk, so stacking onto it needs --extend.`, 1, "implicit_stack");
|
|
252
286
|
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
|
|
287
|
+
};
|
|
288
|
+
assertBase(baseBranch);
|
|
289
|
+
const result = await publish({
|
|
256
290
|
cfg: input.cfg ?? (await load()),
|
|
257
291
|
cwd,
|
|
258
292
|
owner,
|
|
@@ -263,7 +297,8 @@ export async function publishWorkingBranch(input, deps = {}) {
|
|
|
263
297
|
body: input.body ?? buildPrBodyFromCommit(tipCommitMessage(branch, cwd)),
|
|
264
298
|
...(typeof input.autoLand === "boolean" ? { autoLand: input.autoLand } : {}),
|
|
265
299
|
...(input.replaceHead ? { replaceHead: input.replaceHead } : {}),
|
|
266
|
-
}, publishDeps);
|
|
300
|
+
}, { ...publishDeps, assertBase });
|
|
301
|
+
return { ...result, stacked: !trunk || result.baseBranch !== trunk };
|
|
267
302
|
}
|
|
268
303
|
export async function workingBranchPublication(input, deps = {}) {
|
|
269
304
|
const { owner, repo, branch } = workingBranchTarget(input);
|
package/dist/stack-blockers.d.ts
CHANGED
|
@@ -43,6 +43,7 @@ export type StackRepairHint = {
|
|
|
43
43
|
branch: string;
|
|
44
44
|
liveParent: string | null;
|
|
45
45
|
files: string[];
|
|
46
|
+
publishedBy?: "breeze";
|
|
46
47
|
steps: string;
|
|
47
48
|
note?: string;
|
|
48
49
|
} | {
|
|
@@ -74,6 +75,7 @@ export type StackRepairHint = {
|
|
|
74
75
|
branch: string;
|
|
75
76
|
liveParent: string | null;
|
|
76
77
|
restackKind: string;
|
|
78
|
+
publishedBy?: "breeze";
|
|
77
79
|
steps: string;
|
|
78
80
|
} | {
|
|
79
81
|
kind: "seam_review_stuck";
|
|
@@ -170,6 +172,8 @@ export declare const SEAM_REVIEW_STUCK = "Seam review stuck in reviewing, no rev
|
|
|
170
172
|
export declare const SEAM_REREVIEW_NOT_RUNNING = "Seam re-review pending, no review running";
|
|
171
173
|
export declare const SEAM_VERDICT_STALE = "Seam verdict is for an older head, no review running";
|
|
172
174
|
export declare const VORTEX_FINDINGS_NEED_PERSON = "Vortex findings need a person";
|
|
175
|
+
export declare const CYCLONE_NOT_RUN_ATTENTION_MS: number;
|
|
176
|
+
export declare const CYCLONE_NOT_RUN_REASON = "not_run";
|
|
173
177
|
export declare function cycloneStatusWords(layer: Pick<StackLayerDto, "cycloneStatus" | "cycloneHandoff" | "headSha">): string | null;
|
|
174
178
|
export declare function autoResolveActive(stack: StackDto, layer: StackLayerDto): boolean;
|
|
175
179
|
export declare function autoFixCiActive(stack: StackDto, layer: StackLayerDto): boolean;
|
|
@@ -211,12 +215,15 @@ export type MergeQueueWait = {
|
|
|
211
215
|
export declare function mergeQueueWait(queue: readonly MergeQueueEntryDto[], nowMs: number): MergeQueueWait | null;
|
|
212
216
|
export declare function conflictLiveParent(stack: StackDto, layer: StackLayerDto): string | null;
|
|
213
217
|
export declare function conflictRepairSteps(branch: string, liveParent: string | null): string;
|
|
218
|
+
export declare const BREEZE_BRANCH_PREFIX = "breeze/";
|
|
219
|
+
export declare function isBreezeBranch(branch: string | null | undefined): boolean;
|
|
220
|
+
export declare function breezeConflictRepairSteps(branch: string, liveParent: string | null, prNumber: number): string;
|
|
214
221
|
export declare function restackFailedRepairSteps(branch: string, liveParent: string | null, failure: {
|
|
215
222
|
kind: string;
|
|
216
223
|
detail?: string | null;
|
|
217
|
-
}): string;
|
|
224
|
+
}, prNumber?: number): string;
|
|
218
225
|
export declare function agentsCannotClear(reason: StackBlockerReason, stack?: StackDto, layer?: StackLayerDto): boolean;
|
|
219
|
-
export declare function stackRepair(stack: StackDto, attention: ClassifiedStackBlocker | null, layer: StackLayerDto | undefined, bounce: MergeQueueEntryDto | undefined, queueWait?: MergeQueueWait | null): StackRepairHint | null;
|
|
226
|
+
export declare function stackRepair(stack: StackDto, attention: ClassifiedStackBlocker | null, layer: StackLayerDto | undefined, bounce: MergeQueueEntryDto | undefined, queueWait?: MergeQueueWait | null, nowMs?: number): StackRepairHint | null;
|
|
220
227
|
export declare const LAND_PR_MISSING_PREFIX = "No open land PR for ";
|
|
221
228
|
/** Pair-gate attention plus live hard blocks on every other open layer. */
|
|
222
229
|
export declare function stackBlockers(stack: StackDto, entries?: MergeQueueEntryDto[], cursor?: Pick<StackWatchCursor, "bounceId" | "afterFinishedAt">, nowMs?: number): {
|
package/dist/stack-blockers.js
CHANGED
|
@@ -72,10 +72,26 @@ function vortexFindingsRestAtHead(layer) {
|
|
|
72
72
|
function vortexChangesRequestedAtHead(layer) {
|
|
73
73
|
return vortexFindingsRestAtHead(layer) && layer.reviewStatus === "changes_requested";
|
|
74
74
|
}
|
|
75
|
-
|
|
75
|
+
export const CYCLONE_NOT_RUN_ATTENTION_MS = 30 * 60_000;
|
|
76
|
+
export const CYCLONE_NOT_RUN_REASON = "not_run";
|
|
77
|
+
function cycloneNotRunAtHead(layer, nowMs) {
|
|
78
|
+
const review = layer.vortexReview;
|
|
79
|
+
if (layer.cycloneFindingsTakenAtHead !== false || layer.vortexStatus !== "findings")
|
|
80
|
+
return false;
|
|
81
|
+
if (review?.status !== "done" || !sameHead(review.head_sha, layer.headSha))
|
|
82
|
+
return false;
|
|
83
|
+
const settledMs = Date.parse(review.settled_at ?? "");
|
|
84
|
+
return Number.isFinite(settledMs) && nowMs - settledMs >= CYCLONE_NOT_RUN_ATTENTION_MS;
|
|
85
|
+
}
|
|
86
|
+
function cycloneHandoffAtHead(stack, layer, nowMs) {
|
|
76
87
|
if (sameHead(layer.cycloneHandoff?.headSha, layer.headSha))
|
|
77
88
|
return layer.cycloneHandoff.reason;
|
|
78
|
-
|
|
89
|
+
if (layer.cycloneStatus === "failed")
|
|
90
|
+
return "failed";
|
|
91
|
+
const off = stack.cyclonePatchOff ?? (stack.autoPatchOverride === false ? "auto_patch_off" : null);
|
|
92
|
+
if (off)
|
|
93
|
+
return off;
|
|
94
|
+
return cycloneNotRunAtHead(layer, nowMs) ? CYCLONE_NOT_RUN_REASON : null;
|
|
79
95
|
}
|
|
80
96
|
const CYCLONE_HANDOFF_WORDS = {
|
|
81
97
|
failed: "Cyclone's patch failed, so it pushed nothing",
|
|
@@ -93,6 +109,11 @@ const CYCLONE_HANDOFF_WORDS = {
|
|
|
93
109
|
unsupported_language: "the findings are in a language Cyclone does not patch",
|
|
94
110
|
policy_owner_changed: "the stack's owner changed while the patch was queued",
|
|
95
111
|
patch_job_lost: "Cyclone's patch job was lost before it finished",
|
|
112
|
+
pr_state_unavailable: "Cyclone could not read the PR's state, so it pushed nothing",
|
|
113
|
+
left_for_human: "Cyclone did not patch these findings and pushed nothing",
|
|
114
|
+
hold: "the review told Cyclone to hold, so it did not patch these findings",
|
|
115
|
+
vortex_patch_cap: "Cyclone already patched this PR as many times as it may",
|
|
116
|
+
[CYCLONE_NOT_RUN_REASON]: `Cyclone has not run on this head in the ${CYCLONE_NOT_RUN_ATTENTION_MS / 60_000} minutes since Vortex posted these findings, and nothing is patching it`,
|
|
96
117
|
conflict_needs_person: "Cyclone could not resolve the conflict and left it for a person",
|
|
97
118
|
conflict_unresolved: "Cyclone's conflict resolution failed",
|
|
98
119
|
auto_resolve_daily_cap: "Cyclone already resolved conflicts on this PR twice today, its daily cap",
|
|
@@ -179,6 +200,10 @@ const CYCLONE_HELD_FINDINGS_REASONS = [
|
|
|
179
200
|
"pr_state_unavailable",
|
|
180
201
|
"vortex_patch_cap",
|
|
181
202
|
"patch_job_lost",
|
|
203
|
+
"unsupported_language",
|
|
204
|
+
"credits_exhausted",
|
|
205
|
+
"no_billing",
|
|
206
|
+
"policy_owner_changed",
|
|
182
207
|
];
|
|
183
208
|
function vortexImportantFindingsHeldAtHead(layer) {
|
|
184
209
|
return vortexFindingsRestAtHead(layer) && layer.reviewStatus === "reviewed" &&
|
|
@@ -189,13 +214,13 @@ function seamApprovedAtHead(stack, layer) {
|
|
|
189
214
|
const member = (stack.unit?.members ?? []).find((entry) => entry.prNumber === layer.prNumber);
|
|
190
215
|
return member?.seamState?.trim().toLowerCase() === "approved" && sameHead(member.seamReviewedSha, layer.headSha);
|
|
191
216
|
}
|
|
192
|
-
function vortexFindingsLeftForPerson(stack, layer) {
|
|
217
|
+
function vortexFindingsLeftForPerson(stack, layer, nowMs) {
|
|
193
218
|
if (layerAgentsBusy(layer).cyclone)
|
|
194
219
|
return false;
|
|
195
220
|
if (seamApprovedAtHead(stack, layer))
|
|
196
221
|
return false;
|
|
197
222
|
return vortexImportantFindingsHeldAtHead(layer) ||
|
|
198
|
-
(vortexChangesRequestedAtHead(layer) && cycloneHandoffAtHead(stack, layer) !== null);
|
|
223
|
+
(vortexChangesRequestedAtHead(layer) && cycloneHandoffAtHead(stack, layer, nowMs) !== null);
|
|
199
224
|
}
|
|
200
225
|
function quotaSkippedReview(layer) {
|
|
201
226
|
const review = layer.vortexReview;
|
|
@@ -310,7 +335,7 @@ function headChecksRerunning(layer) {
|
|
|
310
335
|
return layer.ciStatus === "pending" && !!checks && checks.failure === 0 && checks.pending > 0 && checks.cancelledOnly !== true;
|
|
311
336
|
}
|
|
312
337
|
/** Shared live-fact precedence; bounce history is only passed for the candidate. */
|
|
313
|
-
function layerAttention(stack, layer, queue, cursor) {
|
|
338
|
+
function layerAttention(stack, layer, queue, cursor, nowMs) {
|
|
314
339
|
const none = { blocker: null, reason: null, bounceKind: null };
|
|
315
340
|
if (stack.archivedAt)
|
|
316
341
|
return none;
|
|
@@ -363,7 +388,7 @@ function layerAttention(stack, layer, queue, cursor) {
|
|
|
363
388
|
if (landing && !landGateIsPending(landing))
|
|
364
389
|
return hardBlock(landing, "land_gate");
|
|
365
390
|
}
|
|
366
|
-
if (vortexFindingsLeftForPerson(stack, layer))
|
|
391
|
+
if (vortexFindingsLeftForPerson(stack, layer, nowMs))
|
|
367
392
|
return hardBlock(VORTEX_FINDINGS_NEED_PERSON, "vortex_findings_need_person");
|
|
368
393
|
}
|
|
369
394
|
if (!bounce || !kind || recoverable || !layer || queue.some((entry) => ["queued", "running", "waiting"].includes(entry.state)))
|
|
@@ -654,11 +679,22 @@ export function conflictRepairSteps(branch, liveParent) {
|
|
|
654
679
|
}
|
|
655
680
|
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.`;
|
|
656
681
|
}
|
|
657
|
-
export
|
|
682
|
+
export const BREEZE_BRANCH_PREFIX = "breeze/";
|
|
683
|
+
export function isBreezeBranch(branch) {
|
|
684
|
+
return typeof branch === "string" && branch.startsWith(BREEZE_BRANCH_PREFIX);
|
|
685
|
+
}
|
|
686
|
+
export function breezeConflictRepairSteps(branch, liveParent, prNumber) {
|
|
687
|
+
if (!liveParent)
|
|
688
|
+
return conflictRepairSteps(branch, liveParent);
|
|
689
|
+
return `${branch} is a Breeze branch: Breeze updates it only with open_pull_request, which publishes single-parent commits, so it cannot publish a merge of ${liveParent}. Rebase it in the Breeze chat that opened #${prNumber}: fetch ${liveParent} with fetch_repository, git rebase origin/${liveParent}, resolve the conflicts, run the tests, then call open_pull_request again. Or have Cyclone resolve it: cyclone_start with task resolve_conflicts and pr_number ${prNumber}. Do not retarget the PR base.`;
|
|
690
|
+
}
|
|
691
|
+
export function restackFailedRepairSteps(branch, liveParent, failure, prNumber) {
|
|
658
692
|
const why = `Mergestorm could not restack ${branch}${liveParent ? ` onto ${liveParent}` : ""} (${failure.kind}${failure.detail?.trim() ? `: ${failure.detail.trim()}` : ""}) and will not try this head again.`;
|
|
659
693
|
if (!liveParent) {
|
|
660
694
|
return `${why} 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.`;
|
|
661
695
|
}
|
|
696
|
+
if (prNumber !== undefined && isBreezeBranch(branch))
|
|
697
|
+
return `${why} ${breezeConflictRepairSteps(branch, liveParent, prNumber)}`;
|
|
662
698
|
return `${why} Merge ${liveParent} into ${branch}: git fetch origin, check out ${branch} at its live remote head, git merge origin/${liveParent}, resolve any conflicts, run the tests, then an ordinary git push (no force). Mergestorm clears Restack failed once the pushed head contains the tip of ${liveParent}. If GitHub refuses the push (branch protection or permissions), stop and tell the human. Do not rebase onto trunk, main, or mg-park-*, and do not retarget the PR base.`;
|
|
663
699
|
}
|
|
664
700
|
function landPrLiveParent(stack, layer) {
|
|
@@ -682,7 +718,7 @@ export function agentsCannotClear(reason, stack, layer) {
|
|
|
682
718
|
return false;
|
|
683
719
|
return AGENTS_CANNOT_CLEAR.includes(reason);
|
|
684
720
|
}
|
|
685
|
-
export function stackRepair(stack, attention, layer, bounce, queueWait = null) {
|
|
721
|
+
export function stackRepair(stack, attention, layer, bounce, queueWait = null, nowMs = Date.now()) {
|
|
686
722
|
if (!attention || !layer || attention.prNumber !== layer.prNumber)
|
|
687
723
|
return null;
|
|
688
724
|
const reason = attention.reason;
|
|
@@ -700,6 +736,7 @@ export function stackRepair(stack, attention, layer, bounce, queueWait = null) {
|
|
|
700
736
|
const landPr = !!stack.unit?.branch && layer.prNumber === stack.unit.landPrNumber && layer.branch === stack.unit.branch;
|
|
701
737
|
const restackConflict = reason === "restack_conflict" || attention.bounceKind === "restack_conflict";
|
|
702
738
|
const mergeConflict = reason === "merge_conflict";
|
|
739
|
+
const breeze = !landPr && isBreezeBranch(layer.branch);
|
|
703
740
|
if (restackConflict || mergeConflict) {
|
|
704
741
|
const liveParent = landPr ? landPrLiveParent(stack, layer) : conflictLiveParent(stack, layer);
|
|
705
742
|
const note = conflictHandoffNote(stack, layer);
|
|
@@ -710,7 +747,8 @@ export function stackRepair(stack, attention, layer, bounce, queueWait = null) {
|
|
|
710
747
|
branch: layer.branch,
|
|
711
748
|
liveParent,
|
|
712
749
|
files: conflictFiles(layer, bounce),
|
|
713
|
-
|
|
750
|
+
...(breeze ? { publishedBy: "breeze" } : {}),
|
|
751
|
+
steps: breeze ? breezeConflictRepairSteps(layer.branch, liveParent, layer.prNumber) : conflictRepairSteps(layer.branch, liveParent),
|
|
714
752
|
...(note ? { note } : {}),
|
|
715
753
|
};
|
|
716
754
|
}
|
|
@@ -723,7 +761,8 @@ export function stackRepair(stack, attention, layer, bounce, queueWait = null) {
|
|
|
723
761
|
branch: layer.branch,
|
|
724
762
|
liveParent,
|
|
725
763
|
restackKind: layer.restackError.kind,
|
|
726
|
-
|
|
764
|
+
...(breeze ? { publishedBy: "breeze" } : {}),
|
|
765
|
+
steps: restackFailedRepairSteps(layer.branch, liveParent, layer.restackError, layer.prNumber),
|
|
727
766
|
};
|
|
728
767
|
}
|
|
729
768
|
if (reason === "unit_abandoned") {
|
|
@@ -866,7 +905,7 @@ export function stackRepair(stack, attention, layer, bounce, queueWait = null) {
|
|
|
866
905
|
}
|
|
867
906
|
if (reason === "vortex_findings_need_person") {
|
|
868
907
|
const findings = `gh api repos/${stack.owner}/${stack.repo}/pulls/${layer.prNumber}/reviews`;
|
|
869
|
-
const handoff = cycloneHandoffAtHead(stack, layer);
|
|
908
|
+
const handoff = cycloneHandoffAtHead(stack, layer, nowMs);
|
|
870
909
|
return {
|
|
871
910
|
kind: "vortex_findings",
|
|
872
911
|
prNumber: layer.prNumber,
|
|
@@ -874,7 +913,7 @@ export function stackRepair(stack, attention, layer, bounce, queueWait = null) {
|
|
|
874
913
|
branch: layer.branch,
|
|
875
914
|
handoff,
|
|
876
915
|
findings,
|
|
877
|
-
steps: `Vortex ${vortexImportantFindingsHeldAtHead(layer) ? "left Important findings" : "requested changes"} on ${layer.branch} at ${layer.headSha ?? "its live head"} and Cyclone left them
|
|
916
|
+
steps: `Vortex ${vortexImportantFindingsHeldAtHead(layer) ? "left Important findings" : "requested changes"} on ${layer.branch} at ${layer.headSha ?? "its live head"} and Cyclone left them undone${handoff ? ` (${cycloneHandoffWords(handoff, cycloneHandoffDetailAtHead(layer))})` : ""}, so nothing will patch them and Auto land waits. You fix them: read the newest Vortex review and its inline comments (${findings}), verify each finding against ${layer.branch} at its live remote head, fix the real ones with the smallest patch, run the tests, confirm the remote head is still ${layer.headSha ?? "the head you started from"}, then an ordinary git push (no force). Dismiss a finding you verified is wrong yourself: mg dismiss ${stack.owner}/${stack.repo}#${layer.prNumber} --head <sha> --review <id> --finding <id> --reason <why> --evidence <link>. Ask the human only for a product or design decision. Do not retarget the PR base.`,
|
|
878
917
|
};
|
|
879
918
|
}
|
|
880
919
|
if (reason === "ci_failed" || attention.bounceKind === "ci_failure") {
|
|
@@ -964,7 +1003,7 @@ export function stackBlockers(stack, entries = [], cursor = {}, nowMs = Date.now
|
|
|
964
1003
|
for (const layer of open) {
|
|
965
1004
|
const selected = layer.prNumber === candidate?.prNumber;
|
|
966
1005
|
const inScope = selected || !candidate || layer.position > candidate.position;
|
|
967
|
-
let result = layerAttention(stack, layer, selected ? queue : [], cursor);
|
|
1006
|
+
let result = layerAttention(stack, layer, selected ? queue : [], cursor, nowMs);
|
|
968
1007
|
let disarmed;
|
|
969
1008
|
if (!result.blocker && selected) {
|
|
970
1009
|
disarmed = disarmingBounce(stack, queue);
|
|
@@ -1071,7 +1110,7 @@ export function stackBlockers(stack, entries = [], cursor = {}, nowMs = Date.now
|
|
|
1071
1110
|
if (busy.length && attention && classified && agentsCannotClear(classified.reason, stack, attentionLayer)) {
|
|
1072
1111
|
held = { ...attention, actAfter: "agents_idle", waitingOn: busy.map((entry) => entry.agent) };
|
|
1073
1112
|
shownIssues = [attention, ...issues];
|
|
1074
|
-
repair = stackRepair(stack, classified, attentionLayer, undefined, queueWait);
|
|
1113
|
+
repair = stackRepair(stack, classified, attentionLayer, undefined, queueWait, nowMs);
|
|
1075
1114
|
attention = null;
|
|
1076
1115
|
bounce = undefined;
|
|
1077
1116
|
}
|
|
@@ -1081,7 +1120,7 @@ export function stackBlockers(stack, entries = [], cursor = {}, nowMs = Date.now
|
|
|
1081
1120
|
bounce = undefined;
|
|
1082
1121
|
}
|
|
1083
1122
|
else {
|
|
1084
|
-
repair = landMissing?.repair ?? stackRepair(stack, classified, attentionLayer, bounce, queueWait);
|
|
1123
|
+
repair = landMissing?.repair ?? stackRepair(stack, classified, attentionLayer, bounce, queueWait, nowMs);
|
|
1085
1124
|
}
|
|
1086
1125
|
if (!attention) {
|
|
1087
1126
|
const next = found.find((item) => !layerBusy(item.layer));
|
|
@@ -1089,7 +1128,7 @@ export function stackBlockers(stack, entries = [], cursor = {}, nowMs = Date.now
|
|
|
1089
1128
|
attention = next.issue;
|
|
1090
1129
|
repair = next.repair !== undefined
|
|
1091
1130
|
? next.repair
|
|
1092
|
-
: stackRepair(stack, { ...next.issue, reason: next.reason }, next.layer, next.bounce, queueWait);
|
|
1131
|
+
: stackRepair(stack, { ...next.issue, reason: next.reason }, next.layer, next.bounce, queueWait, nowMs);
|
|
1093
1132
|
bounce = next.bounce;
|
|
1094
1133
|
if (next.layer)
|
|
1095
1134
|
agents = layerAgents(next.layer);
|
package/dist/stack-dto.d.ts
CHANGED
|
@@ -59,6 +59,7 @@ export type StackVortexReviewView = {
|
|
|
59
59
|
phase: string | null;
|
|
60
60
|
/** Earliest live job start while `reviewing`; null when lease-only. */
|
|
61
61
|
started_at: string | null;
|
|
62
|
+
settled_at?: string | null;
|
|
62
63
|
/** Stop control visibility: true only while a pass can actually be cancelled. */
|
|
63
64
|
stoppable: boolean;
|
|
64
65
|
/** Which table spoke: the authority row, a live job, a lease, or nothing. */
|
|
@@ -225,6 +226,7 @@ export type StackLayerDto = {
|
|
|
225
226
|
cycloneConflictPendingSince?: string | null;
|
|
226
227
|
cycloneCiHandoff?: StackCycloneHandoff | null;
|
|
227
228
|
cycloneCiPendingSince?: string | null;
|
|
229
|
+
cycloneFindingsTakenAtHead?: boolean;
|
|
228
230
|
conflictDetail: string | null;
|
|
229
231
|
restackError?: RestackError | null;
|
|
230
232
|
lastRestackedSha: string | null;
|
|
@@ -495,6 +497,7 @@ export type MergeQueueEntryDto = {
|
|
|
495
497
|
verifyBaseSha: string | null;
|
|
496
498
|
cancelRequestedAt?: string | null;
|
|
497
499
|
batch?: MergeQueueEntryBatchDto | null;
|
|
500
|
+
ci?: MergeQueueBatchCiDto | null;
|
|
498
501
|
finishedAt: string | null;
|
|
499
502
|
claimedAt?: string | null;
|
|
500
503
|
aheadInRepo?: MergeQueueAheadDto | null;
|
package/dist/stack-watch-loop.js
CHANGED
|
@@ -171,9 +171,17 @@ export async function runStackWatch(cfg, stackId, opts = {}) {
|
|
|
171
171
|
await pause(STACK_WATCH_FAILED_RETRY_MS);
|
|
172
172
|
return null;
|
|
173
173
|
};
|
|
174
|
+
const unreadTooLong = (message, envelope, since) => {
|
|
175
|
+
transientSince ??= since;
|
|
176
|
+
if (now() - transientSince < STACK_WATCH_TRANSIENT_WINDOW_MS)
|
|
177
|
+
return null;
|
|
178
|
+
emit(stackWatchFailedLine(id, `No stack read for 15 minutes: ${message}`), envelope, "failed");
|
|
179
|
+
return { outcome: "failed", exitCode: STACK_WATCH_EXIT.attention, envelope };
|
|
180
|
+
};
|
|
174
181
|
try {
|
|
175
182
|
while (now() < deadline) {
|
|
176
183
|
let envelope;
|
|
184
|
+
const sliceStartedAt = now();
|
|
177
185
|
try {
|
|
178
186
|
envelope = await poll(cfg, id, {
|
|
179
187
|
timeoutMs: Math.min(STACK_WATCH_SLICE_MS, Math.max(0, deadline - now())),
|
|
@@ -189,8 +197,13 @@ export async function runStackWatch(cfg, stackId, opts = {}) {
|
|
|
189
197
|
cursor = err.lastEnvelope.cursor;
|
|
190
198
|
failures = 0;
|
|
191
199
|
permanentFailures = 0;
|
|
192
|
-
if (err.lastEnvelope.
|
|
200
|
+
if (err.lastEnvelope.status !== "failed")
|
|
193
201
|
transientSince = null;
|
|
202
|
+
else {
|
|
203
|
+
const unread = unreadTooLong("No stack snapshot was read in a whole watch slice", err.lastEnvelope, sliceStartedAt);
|
|
204
|
+
if (unread)
|
|
205
|
+
return unread;
|
|
206
|
+
}
|
|
194
207
|
continue;
|
|
195
208
|
}
|
|
196
209
|
if (err instanceof StackWatchError && err.lastEnvelope.watch.done) {
|
|
@@ -198,7 +211,15 @@ export async function runStackWatch(cfg, stackId, opts = {}) {
|
|
|
198
211
|
return await finished(err.lastEnvelope);
|
|
199
212
|
}
|
|
200
213
|
if (err instanceof StackWatchError && err.lastEnvelope.status === "rate_limited") {
|
|
201
|
-
|
|
214
|
+
if (err.readSnapshot)
|
|
215
|
+
transientSince = now();
|
|
216
|
+
else {
|
|
217
|
+
const unread = unreadTooLong(err.message, err.lastEnvelope, sliceStartedAt);
|
|
218
|
+
if (unread)
|
|
219
|
+
return unread;
|
|
220
|
+
}
|
|
221
|
+
const retryAfterMs = err.retryAfterSeconds !== undefined ? Math.max(1_000, err.retryAfterSeconds * 1000) : STACK_WATCH_RATE_LIMIT_WAIT_MS;
|
|
222
|
+
await pause(Math.min(retryAfterMs, Math.max(1_000, transientSince + STACK_WATCH_TRANSIENT_WINDOW_MS - now())));
|
|
202
223
|
continue;
|
|
203
224
|
}
|
|
204
225
|
const result = await failed(err instanceof Error ? err.message : String(err), err instanceof StackWatchError ? err.lastEnvelope : null, isTransientWatchFailure(err));
|
package/dist/stack-watch.d.ts
CHANGED
|
@@ -58,7 +58,8 @@ export declare class StackWatchTimeoutError extends Error {
|
|
|
58
58
|
export declare class StackWatchError extends Error {
|
|
59
59
|
readonly lastEnvelope: StackWatchEnvelope;
|
|
60
60
|
readonly retryAfterSeconds?: number | undefined;
|
|
61
|
-
|
|
61
|
+
readonly readSnapshot: boolean;
|
|
62
|
+
constructor(message: string, lastEnvelope: StackWatchEnvelope, options?: ErrorOptions, retryAfterSeconds?: number | undefined, readSnapshot?: boolean);
|
|
62
63
|
}
|
|
63
64
|
/** Wait on queue invalidations, then evaluate a fresh stack snapshot. */
|
|
64
65
|
export declare function pollStackWatch(cfg: Config, stackId: string, opts?: PollStackWatchOptions): Promise<StackWatchEnvelope>;
|
package/dist/stack-watch.js
CHANGED
|
@@ -20,10 +20,12 @@ export class StackWatchTimeoutError extends Error {
|
|
|
20
20
|
export class StackWatchError extends Error {
|
|
21
21
|
lastEnvelope;
|
|
22
22
|
retryAfterSeconds;
|
|
23
|
-
|
|
23
|
+
readSnapshot;
|
|
24
|
+
constructor(message, lastEnvelope, options, retryAfterSeconds, readSnapshot = false) {
|
|
24
25
|
super(message, options);
|
|
25
26
|
this.lastEnvelope = lastEnvelope;
|
|
26
27
|
this.retryAfterSeconds = retryAfterSeconds;
|
|
28
|
+
this.readSnapshot = readSnapshot;
|
|
27
29
|
this.name = "StackWatchError";
|
|
28
30
|
}
|
|
29
31
|
}
|
|
@@ -55,6 +57,7 @@ export async function pollStackWatch(cfg, stackId, opts = {}) {
|
|
|
55
57
|
landGatePending: null,
|
|
56
58
|
};
|
|
57
59
|
let terminal = null;
|
|
60
|
+
let readSnapshot = false;
|
|
58
61
|
const seal = (snapshot, retryAfterSeconds) => ({
|
|
59
62
|
...snapshot,
|
|
60
63
|
watch: stackWatchObligation({
|
|
@@ -116,7 +119,7 @@ export async function pollStackWatch(cfg, stackId, opts = {}) {
|
|
|
116
119
|
throw new StackWatchError(`Stack poll failed (HTTP ${response.status})`, seal({
|
|
117
120
|
...lastEnvelope, status: response.status === 429 ? "rate_limited" : "failed",
|
|
118
121
|
assessment: "unavailable",
|
|
119
|
-
}, response.retryAfterSeconds), undefined, response.retryAfterSeconds);
|
|
122
|
+
}, response.retryAfterSeconds), undefined, response.retryAfterSeconds, readSnapshot);
|
|
120
123
|
}
|
|
121
124
|
throw timeout();
|
|
122
125
|
};
|
|
@@ -149,6 +152,7 @@ export async function pollStackWatch(cfg, stackId, opts = {}) {
|
|
|
149
152
|
};
|
|
150
153
|
const evaluate = (stack, entries) => {
|
|
151
154
|
const { attention, held: named, issues, currentCandidate, bounce, busy, agents, repair, landGatePending, queueWait } = stackBlockers(stack, entries, cursor, now());
|
|
155
|
+
readSnapshot = true;
|
|
152
156
|
if (bounce)
|
|
153
157
|
cursor = Object.freeze({ ...cursor, bounceId: bounce.id,
|
|
154
158
|
...(bounce.finishedAt !== undefined ? { afterFinishedAt: bounce.finishedAt } : {}) });
|
package/dist/ui/banner.js
CHANGED
|
@@ -224,6 +224,12 @@ const WHATS_NEW = {
|
|
|
224
224
|
"0.3.62": [
|
|
225
225
|
"Watch wakes for a blocker on any layer",
|
|
226
226
|
],
|
|
227
|
+
"0.3.63": [
|
|
228
|
+
"Stack create starts a new stack unless --extend",
|
|
229
|
+
],
|
|
230
|
+
"0.3.64": [
|
|
231
|
+
"Memory commands and Overdrive merge batches",
|
|
232
|
+
],
|
|
227
233
|
};
|
|
228
234
|
function densityFromVariant(variant) {
|
|
229
235
|
if (variant === "mini" || variant === "nano")
|