mergestorm 0.3.61 → 0.3.62
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 +5 -5
- package/dist/api.d.ts +3 -8
- package/dist/api.js +3 -23
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.js +1 -1
- package/dist/commands/login.d.ts +4 -1
- package/dist/commands/login.js +5 -3
- package/dist/commands/registry.js +1 -1
- package/dist/commands/review-client.d.ts +1 -2
- package/dist/commands/review-client.js +24 -3
- package/dist/commands/stack.js +35 -18
- package/dist/repo-identity.d.ts +0 -3
- package/dist/repo-identity.js +0 -23
- package/dist/stack-blockers.d.ts +1 -16
- package/dist/stack-blockers.js +58 -16
- package/dist/stack-meta.d.ts +0 -2
- package/dist/stack-meta.js +3 -81
- package/dist/stack-watch-loop.js +4 -1
- package/dist/stack-watch-obligation.d.ts +4 -1
- package/dist/stack-watch-obligation.js +7 -4
- package/dist/stack-watch.js +2 -1
- package/dist/ui/banner.js +3 -0
- package/package.json +1 -1
- package/skill/mergestorm-bounce-watch/SKILL.md +6 -2
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
|
-
│ ▀▀▀▀▀▀▀▀▀▀▘ mergestorm v0.3.
|
|
48
|
+
│ ▀▀▀▀▀▀▀▀▀▀▘ mergestorm v0.3.62 │
|
|
49
49
|
│ ▀▀▀▀▀▀▀▀▀ ● msk_live_… · maelstrom │
|
|
50
50
|
│ ▀▀▀▀▀▀ [████████░░░░░░░░░░░░░░░░░░░░░░] 25% used │
|
|
51
51
|
│ ▝▀▀▀▀▀ local reviews + stacked PRs · mergestorm.ai │
|
|
52
52
|
│ ▝▀▀▘ review a diff · usage for tabs · /help for all commands │
|
|
53
|
-
│ ▀ New in v0.3.
|
|
53
|
+
│ ▀ New in v0.3.62: Watch wakes for a blocker on any layer │
|
|
54
54
|
╰──────────────────────────────────────────────────────────────────────────────╯
|
|
55
55
|
mergestorm
|
|
56
56
|
╭──────────────────────────────────────────────────────────────────────────────╮
|
|
@@ -111,7 +111,7 @@ mergestorm dismiss <owner/repo>#<n> --head <sha> --review <id> --finding <id> --
|
|
|
111
111
|
|
|
112
112
|
- It calls `stack wait` in slices of at most 300s and carries the cursor (`enrolled_head_sha`, `after_finished_at`, `bounce_id`) from one slice to the next.
|
|
113
113
|
- It prints nothing while the stack is `waiting` or `in_progress`, including while Cyclone or Vortex holds the blocked PR (`busy[]`, `actAfter: "agents_idle"`).
|
|
114
|
-
- On attention it prints `MS-WATCH ATTENTION pr=<n> head=<sha> blocker="<blocker>" repair=<kind>` and exits 3.
|
|
114
|
+
- On attention it prints `MS-WATCH ATTENTION pr=<n> head=<sha> blocker="<blocker>" repair=<kind>` and exits 3. Attention names the lowest layer with a blocker an agent must fix, on any layer of the stack, not only the current candidate.
|
|
115
115
|
- A confirmed landing prints `MS-WATCH LANDED stack=<id> reason=landed` and exits 0. `closed`, `archived` and `not_found` end the watch with `MS-WATCH ATTENTION stack=<id> reason=<reason>` and exit 3. A missing stack does not prove a landing; verify each PR before reporting it merged.
|
|
116
116
|
- A failed read is retried once. If the retry fails too, it prints `MS-WATCH ATTENTION failed stack=<id> error="<message>"` and exits 3. A rate limit is waited out.
|
|
117
117
|
- `--head <sha>` enrolls the SHA you just pushed. After fixing a blocker, restart the watcher with it.
|
|
@@ -148,7 +148,7 @@ If the parent branch is already a layer of a **registered** (submitted) stack, `
|
|
|
148
148
|
|
|
149
149
|
`mg stack submit --server-side` and `mg stack publish` are for a machine that can reach Mergestorm but cannot authenticate to GitHub, such as a cloud coding sandbox. They never run `gh`, `git push` or `git fetch`. `mg` reads your commits from the local repository and sends them to Mergestorm with your API key; Mergestorm writes those exact commits and the branch on GitHub as the Surge App, opens a ready pull request, and registers the stack. Surge must be installed on the repository and your linked GitHub account needs write access to it.
|
|
150
150
|
|
|
151
|
-
|
|
151
|
+
The stack is registered with Auto land on for a new stack (`--auto-land off` to keep it off). Auto patch and Auto review follow the stack, repository and account settings like any other stack; `--auto-patch` and `--auto-review` pin them on the stack after it is registered. Send later commits with `mg stack publish`.
|
|
152
152
|
|
|
153
153
|
```bash
|
|
154
154
|
mg issue create --title "The thing is broken" --body-file issue.md --label bug --json # no gh
|
|
@@ -167,7 +167,7 @@ mg stack publish --status # absent | pushed | opened | registered
|
|
|
167
167
|
- **Read-only home directory.** `mg` keeps the saved login and the local stack state of `stack create` / `stack submit` under `~/.mergestorm`. Where the home directory cannot be written, set `MERGESTORM_HOME` to a writable directory (outside the repository) and every command uses that instead. `MERGESTORM_API_KEY` supplies the key without `mg login`.
|
|
168
168
|
- **Limits.** 20 commits, 500 changed paths, 2 MiB a file and 8 MiB in total per publication. Submodules and merge commits are refused with the path or the reason. `stack submit --server-side` publishes the first two layers of a stack; a third layer needs a park freeze, which this mode does not create, so add it once the first two are registered.
|
|
169
169
|
|
|
170
|
-
|
|
170
|
+
If local authoring state is stale or malformed, use `mg stack reset --force`; this clears only pre-submit CLI state and never deletes branches, PRs, or registered stacks.
|
|
171
171
|
|
|
172
172
|
### `login`
|
|
173
173
|
|
package/dist/api.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { type Config } from "./config.js";
|
|
2
|
-
import { BEARER_SETTINGS_KEYS as SETTINGS_WRITABLE_KEYS
|
|
2
|
+
import { BEARER_SETTINGS_KEYS as SETTINGS_WRITABLE_KEYS } from "./automation-catalog.js";
|
|
3
3
|
import { CommandError } from "./errors.js";
|
|
4
4
|
import type { MergeQueueEntryDto, StackDto } from "./stack-dto.js";
|
|
5
|
-
export type { MergeQueueBounceDetail, MergeQueueBounceKind, MergeQueueEnqueuedBy, MergeQueueEnqueuedVia, MergeQueueEntryDto, MergeQueueEntryState,
|
|
5
|
+
export type { MergeQueueBounceDetail, MergeQueueBounceKind, MergeQueueEnqueuedBy, MergeQueueEnqueuedVia, MergeQueueEntryDto, MergeQueueEntryState, StackDto, StackLayerDto, } from "./stack-dto.js";
|
|
6
6
|
/** Default per-request timeout so a hung API cannot stall the shell/CI forever. */
|
|
7
7
|
export declare const DEFAULT_API_TIMEOUT_MS = 30000;
|
|
8
8
|
/** Message prefix of the CommandError thrown when a request exceeds its timeout. */
|
|
@@ -167,7 +167,6 @@ export declare function applyPrReviewQuery(query: URLSearchParams, opts: PrRevie
|
|
|
167
167
|
* The connected flags are read-only there and deliberately absent here.
|
|
168
168
|
*/
|
|
169
169
|
export { SETTINGS_WRITABLE_KEYS };
|
|
170
|
-
export type { SettingsWritableKey };
|
|
171
170
|
export type SettingsPatch = Partial<import("./automation-catalog.js").BearerSettingsValues>;
|
|
172
171
|
/** GET and PATCH `/api/v1/settings` both answer with this shape. */
|
|
173
172
|
export type SettingsResponse = import("./automation-catalog.js").BearerSettingsValues & {
|
|
@@ -389,8 +388,6 @@ export declare function openStackIssue(input: {
|
|
|
389
388
|
url: string;
|
|
390
389
|
created: boolean;
|
|
391
390
|
}>;
|
|
392
|
-
/** Arm or disarm Auto land on one owned stack. */
|
|
393
|
-
export declare function setStackAutoLand(stackId: string, enabled: boolean, cfg?: Config): Promise<unknown>;
|
|
394
391
|
/**
|
|
395
392
|
* Patch any subset of one owned stack's automation policy
|
|
396
393
|
* (Bearer PATCH /api/v1/stacks/:id). `null` clears an override.
|
|
@@ -399,9 +396,7 @@ export declare function setStackPolicy(stackId: string, policy: StackPolicyPatch
|
|
|
399
396
|
/** Restack descendants (Bearer POST /api/v1/stacks/:id/restack). */
|
|
400
397
|
export declare function restackStack(stackId: string, cfg?: Config): Promise<unknown>;
|
|
401
398
|
/** Land the bottom open layer (Bearer POST /api/v1/stacks/:id/land-next). */
|
|
402
|
-
export declare function landNextStack(stackId: string, cfg?: Config
|
|
403
|
-
prNumber?: number;
|
|
404
|
-
}): Promise<unknown>;
|
|
399
|
+
export declare function landNextStack(stackId: string, cfg?: Config): Promise<unknown>;
|
|
405
400
|
/** List the authenticated user's live merge-queue entries. */
|
|
406
401
|
export declare function listMergeQueueEntries(cfg?: Config, opts?: {
|
|
407
402
|
stackId?: string;
|
package/dist/api.js
CHANGED
|
@@ -269,8 +269,6 @@ export async function getMe(cfg, opts) {
|
|
|
269
269
|
const { status, body } = await apiFetch(resolved, "/api/v1/me", {
|
|
270
270
|
timeoutMs: opts?.timeoutMs,
|
|
271
271
|
});
|
|
272
|
-
if (status === 404)
|
|
273
|
-
return null;
|
|
274
272
|
if (status !== 200)
|
|
275
273
|
return null;
|
|
276
274
|
return body;
|
|
@@ -432,18 +430,7 @@ export async function adoptStack(owner, repo, prNumber, cfg, policy) {
|
|
|
432
430
|
const message = body && typeof body === "object" && "message" in body && typeof body.message === "string"
|
|
433
431
|
? body.message
|
|
434
432
|
: undefined;
|
|
435
|
-
const code = error === "
|
|
436
|
-
error === "api_timeout" ||
|
|
437
|
-
error === "not_a_repo" ||
|
|
438
|
-
error === "not_found" ||
|
|
439
|
-
error === "missing_api_key" ||
|
|
440
|
-
error === "registered_parent" ||
|
|
441
|
-
error === "usage" ||
|
|
442
|
-
error === "review_quota" ||
|
|
443
|
-
error === "review_failed" ||
|
|
444
|
-
error === "review_timeout" ||
|
|
445
|
-
error === "rate_limited" ||
|
|
446
|
-
error === "cyclone_not_installed" ||
|
|
433
|
+
const code = error === "cyclone_not_installed" ||
|
|
447
434
|
error === "pr_merged" ||
|
|
448
435
|
error === "pr_closed" ||
|
|
449
436
|
error === "not_stacked" ||
|
|
@@ -689,10 +676,6 @@ async function stackMutation(stackId, pathSuffix, method, json, failLabel, cfg)
|
|
|
689
676
|
}
|
|
690
677
|
return body;
|
|
691
678
|
}
|
|
692
|
-
/** Arm or disarm Auto land on one owned stack. */
|
|
693
|
-
export async function setStackAutoLand(stackId, enabled, cfg) {
|
|
694
|
-
return setStackPolicy(stackId, { autoEnqueueWhenReady: enabled }, cfg);
|
|
695
|
-
}
|
|
696
679
|
/**
|
|
697
680
|
* Patch any subset of one owned stack's automation policy
|
|
698
681
|
* (Bearer PATCH /api/v1/stacks/:id). `null` clears an override.
|
|
@@ -709,11 +692,8 @@ export async function restackStack(stackId, cfg) {
|
|
|
709
692
|
return stackMutation(stackId, "/restack", "POST", {}, "Failed to restack", cfg);
|
|
710
693
|
}
|
|
711
694
|
/** Land the bottom open layer (Bearer POST /api/v1/stacks/:id/land-next). */
|
|
712
|
-
export async function landNextStack(stackId, cfg
|
|
713
|
-
|
|
714
|
-
? { prNumber: opts.prNumber }
|
|
715
|
-
: {};
|
|
716
|
-
return stackMutation(stackId, "/land-next", "POST", json, "Failed to land next", cfg);
|
|
695
|
+
export async function landNextStack(stackId, cfg) {
|
|
696
|
+
return stackMutation(stackId, "/land-next", "POST", {}, "Failed to land next", cfg);
|
|
717
697
|
}
|
|
718
698
|
/** List the authenticated user's live merge-queue entries. */
|
|
719
699
|
export async function listMergeQueueEntries(cfg, opts) {
|
package/dist/client/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { DEFAULT_API, apiBase, clearConfig, configPath, keyDisplay, loadConfig, mergestormHome, resolveApiKey, saveConfig, type Config, } from "../config.js";
|
|
2
2
|
export { BEARER_SETTINGS, BEARER_BOOLEAN_SETTINGS_KEYS, BEARER_SETTINGS_FLAGS, BEARER_SETTINGS_KEYS, BEARER_SETTINGS_LABELS, type BearerSettingsKey, } from "../automation-catalog.js";
|
|
3
|
-
export { API_TIMEOUT_PREFIX, DEFAULT_API_TIMEOUT_MS, SETTINGS_WRITABLE_KEYS, adoptStack, apiFetch, cancelMergeQueueEntry, devicePost, dismissPrFindings, enqueueStack, ensureUpperPark, getEnrichedStack, getMe, getPrVortexReview, getReview, getSettings, listRepoSettings, patchRepoSettings, REPO_SETTINGS_KEYS, getStackPublication, landNextStack, listJobs, listMergeQueueEntries, listStacks, listThreads, openStackIssue, parseRetryAfterSeconds, patchSettings, restackStack,
|
|
3
|
+
export { API_TIMEOUT_PREFIX, DEFAULT_API_TIMEOUT_MS, SETTINGS_WRITABLE_KEYS, adoptStack, apiFetch, cancelMergeQueueEntry, devicePost, dismissPrFindings, enqueueStack, ensureUpperPark, getEnrichedStack, getMe, getPrVortexReview, getReview, getSettings, listRepoSettings, patchRepoSettings, REPO_SETTINGS_KEYS, getStackPublication, landNextStack, listJobs, listMergeQueueEntries, listStacks, listThreads, openStackIssue, parseRetryAfterSeconds, patchSettings, restackStack, setStackPolicy, stackPolicyBody, submitStackPublication, type ApiFetchInit, type ApiFetchResult, type JobListItem, type MeResponse, type MergeQueueBounceDetail, type MergeQueueBounceKind, type MergeQueueEnqueuedBy, type MergeQueueEnqueuedVia, type MergeQueueEntryDto, type MergeQueueEntryState, type PrDismissedFinding, type PrFindingDismissInput, type PrFindingDismissOutcome, type PrFindingDismissResult, type PrReviewFinding, type PrReviewFindings, type PrVortexReview, type SettingsPatch, type RepoSettings, type RepoSettingsKey, type RepoSettingsPatch, type SettingsResponse, type StackDto, type StackPolicyPatch, type StackPublication, type StackPublicationRequest, type StackPublicationStatus, type ThreadDetail, type ThreadListItem, } from "../api.js";
|
|
4
4
|
export { CommandError, DetachedError, RATE_LIMITED_HINT, REVIEW_EXIT, isCommandErrorCode, rateLimitedMessage, type CommandErrorCode, type CommandErrorExtras, } from "../errors.js";
|
|
5
5
|
export { REVIEW_POLL_BACKOFF_MS, REVIEW_POLL_DEFAULT_TIMEOUT_MS, REVIEW_POLL_INTERVAL_MS, REVIEW_POLL_MAX_INTERVAL_MS, REVIEW_POLL_MAX_TRANSIENT_RETRIES, REVIEW_POLL_STRETCH_AFTER_MS, REVIEW_POLL_STRETCH_SPAN_MS, REVIEW_THREAD_SLUG_RE, PrReviewPollTimeoutError, ReviewPollTimeoutError, noPrReviewYetMessage, collectReviewInput, formatReviewSubmitError, isTransientReviewPollError, isTransientReviewPollStatus, loadReviewContext, pollPrVortexReview, pollReview, stretchedPollIntervalMs, submitReview, transientRetryWaitMs, type PollReviewOptions, type PollPrVortexReviewOptions, type ReviewInput, type SubmitReviewOptions, } from "../commands/review-client.js";
|
|
6
6
|
export { REVIEW_JOB_ENVELOPE_SCHEMA, toReviewJobEnvelope, type ReviewEnvelopeFallbacks, type ReviewJobEnvelope, type ReviewJobRow, } from "../ui/job-envelope.js";
|
package/dist/client/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { DEFAULT_API, apiBase, clearConfig, configPath, keyDisplay, loadConfig, mergestormHome, resolveApiKey, saveConfig, } from "../config.js";
|
|
2
2
|
export { BEARER_SETTINGS, BEARER_BOOLEAN_SETTINGS_KEYS, BEARER_SETTINGS_FLAGS, BEARER_SETTINGS_KEYS, BEARER_SETTINGS_LABELS, } from "../automation-catalog.js";
|
|
3
|
-
export { API_TIMEOUT_PREFIX, DEFAULT_API_TIMEOUT_MS, SETTINGS_WRITABLE_KEYS, adoptStack, apiFetch, cancelMergeQueueEntry, devicePost, dismissPrFindings, enqueueStack, ensureUpperPark, getEnrichedStack, getMe, getPrVortexReview, getReview, getSettings, listRepoSettings, patchRepoSettings, REPO_SETTINGS_KEYS, getStackPublication, landNextStack, listJobs, listMergeQueueEntries, listStacks, listThreads, openStackIssue, parseRetryAfterSeconds, patchSettings, restackStack,
|
|
3
|
+
export { API_TIMEOUT_PREFIX, DEFAULT_API_TIMEOUT_MS, SETTINGS_WRITABLE_KEYS, adoptStack, apiFetch, cancelMergeQueueEntry, devicePost, dismissPrFindings, enqueueStack, ensureUpperPark, getEnrichedStack, getMe, getPrVortexReview, getReview, getSettings, listRepoSettings, patchRepoSettings, REPO_SETTINGS_KEYS, getStackPublication, landNextStack, listJobs, listMergeQueueEntries, listStacks, listThreads, openStackIssue, parseRetryAfterSeconds, patchSettings, restackStack, setStackPolicy, stackPolicyBody, submitStackPublication, } from "../api.js";
|
|
4
4
|
export { CommandError, DetachedError, RATE_LIMITED_HINT, REVIEW_EXIT, isCommandErrorCode, rateLimitedMessage, } from "../errors.js";
|
|
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, noPrReviewYetMessage, collectReviewInput, formatReviewSubmitError, isTransientReviewPollError, isTransientReviewPollStatus, loadReviewContext, pollPrVortexReview, pollReview, stretchedPollIntervalMs, submitReview, transientRetryWaitMs, } from "../commands/review-client.js";
|
|
6
6
|
export { REVIEW_JOB_ENVELOPE_SCHEMA, toReviewJobEnvelope, } from "../ui/job-envelope.js";
|
package/dist/commands/login.d.ts
CHANGED
|
@@ -6,4 +6,7 @@ import { type MeResponse } from "../api.js";
|
|
|
6
6
|
*/
|
|
7
7
|
export declare function validateApiKey(cfg: Config): Promise<MeResponse>;
|
|
8
8
|
export declare function pendingLoginPath(): string;
|
|
9
|
-
export
|
|
9
|
+
export interface LoginDeps {
|
|
10
|
+
openBrowser: (url: string) => void;
|
|
11
|
+
}
|
|
12
|
+
export declare function cmdLogin(args: string[], deps?: LoginDeps): Promise<void>;
|
package/dist/commands/login.js
CHANGED
|
@@ -200,7 +200,7 @@ async function loginFinish() {
|
|
|
200
200
|
console.log(`Waiting for approval of code ${pending.userCode} at ${pending.openUrl}`);
|
|
201
201
|
await pollDeviceLogin(pending, "mergestorm login --finish", "Run `mergestorm login --start`, then `mergestorm login --finish`.");
|
|
202
202
|
}
|
|
203
|
-
export async function cmdLogin(args) {
|
|
203
|
+
export async function cmdLogin(args, deps = { openBrowser }) {
|
|
204
204
|
if (args.includes("--key")) {
|
|
205
205
|
await loginWithKey();
|
|
206
206
|
return;
|
|
@@ -219,8 +219,10 @@ export async function cmdLogin(args) {
|
|
|
219
219
|
console.log("To sign in, open this URL and confirm the code:\n");
|
|
220
220
|
console.log(` ${ansi.brightGreen(pending.openUrl)}`);
|
|
221
221
|
console.log(`\n Code: ${ansi.bold(pending.userCode)}\n`);
|
|
222
|
-
|
|
223
|
-
|
|
222
|
+
if (process.stdout.isTTY) {
|
|
223
|
+
deps.openBrowser(pending.openUrl);
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
224
226
|
console.log("No terminal here. If this command may be cut off before the approval, use `mergestorm login --start`, then `mergestorm login --finish`.");
|
|
225
227
|
}
|
|
226
228
|
console.log("Waiting for approval…");
|
|
@@ -264,7 +264,7 @@ export const COMMAND_REGISTRY = [
|
|
|
264
264
|
"mergestorm stack list [--json] List registered stacks",
|
|
265
265
|
"mergestorm stack status <stack-id> [--json] Enriched stack status",
|
|
266
266
|
"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",
|
|
267
|
-
"mergestorm stack watch <stack-id> [--until attention|landed] [--ignore <blocker-substring>] [--max <minutes>] [--head <sha>] [--json] Background watcher: silent while waiting; MS-WATCH ATTENTION exits 3, MS-WATCH LANDED exits 0",
|
|
267
|
+
"mergestorm stack watch <stack-id> [--until attention|landed] [--ignore <blocker-substring>] [--max <minutes>] [--head <sha>] [--bounce-id <id>] [--after-finished-at <iso>] [--json] Background watcher: silent while waiting; MS-WATCH ATTENTION exits 3, MS-WATCH LANDED exits 0",
|
|
268
268
|
"mergestorm stack set <stack-id> [--auto-land on|off] [--auto-review on|off|default] [--auto-patch on|off|default] [--auto-resolve-conflicts on|off|default] [--auto-fix-ci on|off|default] [--json] Per-stack policy",
|
|
269
269
|
"mergestorm stack adopt <owner/repo>#<pr> [--auto-land on|off] [--auto-review on|off] [--auto-patch on|off] [--auto-resolve-conflicts on|off] [--auto-fix-ci on|off] Import an open PR chain",
|
|
270
270
|
"mergestorm stack restack <stack-id> Restack stack descendants",
|
|
@@ -10,8 +10,6 @@ export declare const REVIEW_POLL_STRETCH_AFTER_MS = 60000;
|
|
|
10
10
|
export declare const REVIEW_POLL_STRETCH_SPAN_MS = 60000;
|
|
11
11
|
/** Same thread slug contract as POST /api/v1/reviews. */
|
|
12
12
|
export declare const REVIEW_THREAD_SLUG_RE: RegExp;
|
|
13
|
-
/** Same product limit as `api/src/v1/reviews.ts` `MAX_DIFF_BYTES`. */
|
|
14
|
-
export declare const MAX_REVIEW_DIFF_BYTES = 1000000;
|
|
15
13
|
export declare const MAX_CONTEXT_TEXT_CHARS = 60000;
|
|
16
14
|
export declare const MAX_CONTEXT_FILES = 10;
|
|
17
15
|
export declare const MAX_CONTEXT_FILE_CHARS = 16000;
|
|
@@ -59,6 +57,7 @@ export type LoadReviewContextOptions = {
|
|
|
59
57
|
};
|
|
60
58
|
/** Load --context / --context-file and enforce the API caps client-side. */
|
|
61
59
|
export declare function loadReviewContext(opts: LoadReviewContextOptions): Promise<ReviewContextPayload>;
|
|
60
|
+
export declare function reviewIdempotencyKey(input: ReviewInput, opts?: SubmitReviewOptions, afterJobId?: string | null): string;
|
|
62
61
|
export declare function submitReview(cfg: Config, input: ReviewInput, opts?: SubmitReviewOptions): Promise<{
|
|
63
62
|
status: number;
|
|
64
63
|
row: ReviewJobRow;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { readFile, realpath } from "node:fs/promises";
|
|
2
3
|
import { join, relative, resolve, sep } from "node:path";
|
|
3
4
|
import { apiFetch, applyPrReviewQuery, isPositiveInteger, } from "../api.js";
|
|
@@ -12,8 +13,6 @@ export const REVIEW_POLL_STRETCH_AFTER_MS = 60_000;
|
|
|
12
13
|
export const REVIEW_POLL_STRETCH_SPAN_MS = 60_000;
|
|
13
14
|
/** Same thread slug contract as POST /api/v1/reviews. */
|
|
14
15
|
export const REVIEW_THREAD_SLUG_RE = /^[a-zA-Z0-9][a-zA-Z0-9._/-]{0,119}$/;
|
|
15
|
-
/** Same product limit as `api/src/v1/reviews.ts` `MAX_DIFF_BYTES`. */
|
|
16
|
-
export const MAX_REVIEW_DIFF_BYTES = 1_000_000;
|
|
17
16
|
export const MAX_CONTEXT_TEXT_CHARS = 60_000;
|
|
18
17
|
export const MAX_CONTEXT_FILES = 10;
|
|
19
18
|
export const MAX_CONTEXT_FILE_CHARS = 16_000;
|
|
@@ -128,7 +127,29 @@ export async function loadReviewContext(opts) {
|
|
|
128
127
|
...(contextFiles.length > 0 ? { contextFiles } : {}),
|
|
129
128
|
};
|
|
130
129
|
}
|
|
130
|
+
const REPLAYED_JOB_RESUBMIT_STATUSES = ["failed", "quota_exceeded"];
|
|
131
|
+
const MAX_REPLAYED_JOB_RESUBMITS = 5;
|
|
132
|
+
export function reviewIdempotencyKey(input, opts = {}, afterJobId = null) {
|
|
133
|
+
const request = [
|
|
134
|
+
input.thread, input.branch, input.repo ?? null, input.baseLabel, input.headLabel, input.diff, input.files,
|
|
135
|
+
opts.routerMode ?? null, [...new Set(opts.specialists ?? [])].sort(), opts.context ?? null, opts.contextFiles ?? [], opts.webhookUrl ?? null,
|
|
136
|
+
afterJobId,
|
|
137
|
+
];
|
|
138
|
+
return `mg-auto-${createHash("sha256").update(JSON.stringify(request)).digest("hex")}`;
|
|
139
|
+
}
|
|
131
140
|
export async function submitReview(cfg, input, opts = {}) {
|
|
141
|
+
const explicitKey = opts.idempotencyKey?.trim() || undefined;
|
|
142
|
+
let idempotencyKey = explicitKey ?? reviewIdempotencyKey(input, opts);
|
|
143
|
+
for (let resubmits = 0;; resubmits++) {
|
|
144
|
+
const submitted = await postReview(cfg, input, opts, idempotencyKey);
|
|
145
|
+
const replayedJobId = submitted.row.job_id ?? submitted.row.id ?? null;
|
|
146
|
+
if (explicitKey || submitted.status !== 200 || !replayedJobId || resubmits >= MAX_REPLAYED_JOB_RESUBMITS ||
|
|
147
|
+
!REPLAYED_JOB_RESUBMIT_STATUSES.includes(submitted.row.status ?? ""))
|
|
148
|
+
return submitted;
|
|
149
|
+
idempotencyKey = reviewIdempotencyKey(input, opts, replayedJobId);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
async function postReview(cfg, input, opts, idempotencyKey) {
|
|
132
153
|
const { status, body, retryAfterSeconds } = await apiFetch(cfg, "/api/v1/reviews", {
|
|
133
154
|
method: "POST",
|
|
134
155
|
json: {
|
|
@@ -143,7 +164,7 @@ export async function submitReview(cfg, input, opts = {}) {
|
|
|
143
164
|
...(opts.specialists?.length ? { specialists: opts.specialists } : {}),
|
|
144
165
|
...(opts.context ? { context: opts.context } : {}),
|
|
145
166
|
...(opts.contextFiles?.length ? { context_files: opts.contextFiles } : {}),
|
|
146
|
-
|
|
167
|
+
idempotency_key: idempotencyKey,
|
|
147
168
|
...(opts.webhookUrl ? { webhook_url: opts.webhookUrl } : {}),
|
|
148
169
|
},
|
|
149
170
|
signal: opts.signal,
|
package/dist/commands/stack.js
CHANGED
|
@@ -59,11 +59,13 @@ const STACK_USAGE = `usage:
|
|
|
59
59
|
mergestorm stack wait <stack-id> [--json] [--timeout <s>]
|
|
60
60
|
--json writes exactly one JSON document to stdout. The timeout or error notice goes to stderr
|
|
61
61
|
(exit 5 on timeout), so do not merge the streams (2>&1) before parsing stdout.
|
|
62
|
-
mergestorm stack watch <stack-id> [--until attention|landed] [--ignore <blocker-substring>] [--max <minutes>] [--head <sha>] [--json]
|
|
62
|
+
mergestorm stack watch <stack-id> [--until attention|landed] [--ignore <blocker-substring>] [--max <minutes>] [--head <sha>] [--bounce-id <id>] [--after-finished-at <iso>] [--json]
|
|
63
63
|
Loops stack wait with the cursor and stays silent while the stack is waiting or in progress.
|
|
64
64
|
Prints one MS-WATCH ATTENTION line and exits 3 on attention or on reads that keep failing (a permanent
|
|
65
65
|
error twice, or 15 minutes of timeouts and 5xx, which it retries with backoff); prints MS-WATCH LANDED
|
|
66
66
|
and exits 0 only for a confirmed landing, including a stack Mergestorm deleted after its queue entry landed. Other terminal reasons exit 3 with ATTENTION. Run it as a background command.
|
|
67
|
+
An ATTENTION line for a merge queue bounce ends with restart="mg stack watch <id> --bounce-id <id> --after-finished-at <iso>":
|
|
68
|
+
restart with that command (plus --head <sha> after a push) so a bounce you already handled is not reported again.
|
|
67
69
|
--json writes exactly one JSON document to stdout and sends the MS-WATCH lines to stderr.
|
|
68
70
|
mergestorm stack set <stack-id> [--auto-land on|off] [--auto-review on|off|default] [--auto-patch on|off|default] [--auto-resolve-conflicts on|off|default] [--auto-fix-ci on|off|default] [--json]
|
|
69
71
|
mergestorm stack adopt <owner/repo>#<pr> [--auto-land on|off] [--auto-review on|off] [--auto-patch on|off] [--auto-resolve-conflicts on|off] [--auto-fix-ci on|off] [--json]
|
|
@@ -85,7 +87,8 @@ const STACK_USAGE = `usage:
|
|
|
85
87
|
stack submit --server-side and stack publish need no GitHub credentials: mg
|
|
86
88
|
reads your commits from the local repository and sends them to Mergestorm,
|
|
87
89
|
which writes the commits and the branch on GitHub as the Surge App, opens the
|
|
88
|
-
pull request, and registers the stack
|
|
90
|
+
pull request, and registers the stack. Auto patch follows the stack, the
|
|
91
|
+
repository and your account, like any other stack. They never
|
|
89
92
|
run gh, git push or git fetch. stack publish does this for one branch and is
|
|
90
93
|
how you send later commits to a branch that is already published. Mergestorm
|
|
91
94
|
refuses to move a branch whose head on GitHub matches no local commit unless
|
|
@@ -782,9 +785,6 @@ export async function cmdStackSubmit(args, deps = {}) {
|
|
|
782
785
|
}
|
|
783
786
|
}
|
|
784
787
|
if (serverSide) {
|
|
785
|
-
if (autoPatch === true) {
|
|
786
|
-
throw new CommandError("--server-side publishes your own commits and keeps Auto patch off for the stack. Drop --auto-patch on, or submit without --server-side.", REVIEW_EXIT.usage, "usage");
|
|
787
|
-
}
|
|
788
788
|
const parked = plans.find((plan) => plan.githubBase == null);
|
|
789
789
|
if (parked) {
|
|
790
790
|
throw new CommandError(`Layer ${parked.branch} opens onto a park freeze, which --server-side does not mint. ` +
|
|
@@ -796,6 +796,7 @@ export async function cmdStackSubmit(args, deps = {}) {
|
|
|
796
796
|
const remoteHeads = new Map();
|
|
797
797
|
const published = [];
|
|
798
798
|
let publishedStackId = null;
|
|
799
|
+
let publishedAutoPatch = null;
|
|
799
800
|
for (const plan of plans) {
|
|
800
801
|
if (!branchExistsFn(plan.branch, cwd)) {
|
|
801
802
|
throw new CommandError(`Stack layer branch missing locally: ${plan.branch}`);
|
|
@@ -825,6 +826,7 @@ export async function cmdStackSubmit(args, deps = {}) {
|
|
|
825
826
|
});
|
|
826
827
|
remoteHeads.set(plan.branch, result.head.sha);
|
|
827
828
|
publishedStackId = result.stack.id;
|
|
829
|
+
publishedAutoPatch = result.stack.autoPatchOverride;
|
|
828
830
|
published.push({
|
|
829
831
|
branch: plan.branch,
|
|
830
832
|
base: result.baseBranch,
|
|
@@ -834,19 +836,19 @@ export async function cmdStackSubmit(args, deps = {}) {
|
|
|
834
836
|
tree: result.head.tree,
|
|
835
837
|
});
|
|
836
838
|
}
|
|
837
|
-
const { autoEnqueueWhenReady: _autoLand,
|
|
839
|
+
const { autoEnqueueWhenReady: _autoLand, ...otherOverrides } = policy ?? {};
|
|
838
840
|
if (publishedStackId && Object.keys(otherOverrides).length > 0) {
|
|
839
841
|
await (deps.setStackPolicy ?? setStackPolicy)(publishedStackId, otherOverrides, config);
|
|
840
842
|
}
|
|
841
843
|
await saveStackMetaFn(removeActiveStack(meta), cwd);
|
|
842
844
|
const watch = publishedStackId ? openStackWatch(publishedStackId) : null;
|
|
843
845
|
if (asJson) {
|
|
844
|
-
console.log(JSON.stringify({ owner, repo, layers: published, stackId: publishedStackId, trunk: meta.trunk, chain: null, serverSide: true, autoPatchOverride:
|
|
846
|
+
console.log(JSON.stringify({ owner, repo, layers: published, stackId: publishedStackId, trunk: meta.trunk, chain: null, serverSide: true, autoPatchOverride: otherOverrides.autoPatchOverride ?? publishedAutoPatch, watch }, null, 2));
|
|
845
847
|
return;
|
|
846
848
|
}
|
|
847
849
|
const lines = published.map((row) => ansi.brightGreen(` #${row.prNumber} ${row.branch} → ${row.base} (${row.created ? "opened" : "exists"}, head ${row.head.slice(0, 12)})`));
|
|
848
850
|
if (publishedStackId)
|
|
849
|
-
lines.push(ansi.brightGreen(` Registered stack ${publishedStackId}
|
|
851
|
+
lines.push(ansi.brightGreen(` Registered stack ${publishedStackId}`));
|
|
850
852
|
lines.push(ansi.dim(" Dashboard: https://mergestorm.ai/dashboard"));
|
|
851
853
|
if (watch)
|
|
852
854
|
lines.push(...stackWatchTextLines(watch).map((line) => ` ${line}`));
|
|
@@ -963,9 +965,6 @@ export async function cmdStackSubmit(args, deps = {}) {
|
|
|
963
965
|
await registerPr(opened[opened.length - 1].prNumber);
|
|
964
966
|
}
|
|
965
967
|
else {
|
|
966
|
-
if (opened.length === 0) {
|
|
967
|
-
throw new CommandError("No pull requests to register after submit.");
|
|
968
|
-
}
|
|
969
968
|
await registerPr(opened[0].prNumber);
|
|
970
969
|
}
|
|
971
970
|
const data = adoptData;
|
|
@@ -1057,7 +1056,7 @@ export async function cmdStackPublish(args, deps = {}) {
|
|
|
1057
1056
|
...(status.head ? [ansi.dim(` head ${status.head.sha} (tree ${status.head.tree ?? "unknown"})`)] : []),
|
|
1058
1057
|
...(status.pr ? [ansi.dim(` #${status.pr.number} → ${status.pr.base} ${status.pr.url}`)] : []),
|
|
1059
1058
|
...(status.stack
|
|
1060
|
-
? [ansi.dim(` stack ${status.stack.id} auto-patch ${status.stack.autoPatchOverride ===
|
|
1059
|
+
? [ansi.dim(` stack ${status.stack.id} auto-patch ${status.stack.autoPatchOverride === null ? "default" : status.stack.autoPatchOverride ? "on" : "off"} auto-land ${status.stack.autoLand ? "on" : "off"}`)]
|
|
1061
1060
|
: []),
|
|
1062
1061
|
]);
|
|
1063
1062
|
return;
|
|
@@ -1077,7 +1076,7 @@ export async function cmdStackPublish(args, deps = {}) {
|
|
|
1077
1076
|
await present("Stack", [
|
|
1078
1077
|
ansi.brightGreen(` ${result.branch} ${moved} at ${result.head.sha.slice(0, 12)} (tree ${result.head.tree.slice(0, 12)})`),
|
|
1079
1078
|
ansi.brightGreen(` #${result.pr.number} ${result.branch} → ${result.baseBranch} (${result.pr.created ? "opened" : "exists"}) ${result.pr.url}`),
|
|
1080
|
-
ansi.brightGreen(` Registered stack ${result.stack.id}
|
|
1079
|
+
ansi.brightGreen(` Registered stack ${result.stack.id}`),
|
|
1081
1080
|
...stackWatchTextLines(watch).map((line) => ` ${line}`),
|
|
1082
1081
|
]);
|
|
1083
1082
|
}
|
|
@@ -1175,13 +1174,15 @@ export async function cmdStackWait(args, deps = {}) {
|
|
|
1175
1174
|
}
|
|
1176
1175
|
}
|
|
1177
1176
|
export async function cmdStackWatch(args, deps = {}) {
|
|
1178
|
-
const usage = "usage: mergestorm stack watch <stack-id> [--until attention|landed] [--ignore <blocker-substring>] [--max <minutes>] [--head <sha>] [--json]";
|
|
1177
|
+
const usage = "usage: mergestorm stack watch <stack-id> [--until attention|landed] [--ignore <blocker-substring>] [--max <minutes>] [--head <sha>] [--bounce-id <id>] [--after-finished-at <iso>] [--json]";
|
|
1179
1178
|
const fail = (detail) => new CommandError(detail ? `${usage}\n${detail}` : usage, REVIEW_EXIT.usage, "usage");
|
|
1180
1179
|
const positional = [];
|
|
1181
1180
|
const ignore = [];
|
|
1182
1181
|
let until = "attention";
|
|
1183
1182
|
let maxMinutes;
|
|
1184
1183
|
let head;
|
|
1184
|
+
let bounceId;
|
|
1185
|
+
let afterFinishedAt;
|
|
1185
1186
|
let json = false;
|
|
1186
1187
|
for (let i = 0; i < args.length; i++) {
|
|
1187
1188
|
const arg = args[i];
|
|
@@ -1212,6 +1213,17 @@ export async function cmdStackWatch(args, deps = {}) {
|
|
|
1212
1213
|
if (!/^[0-9a-f]{7,40}$/i.test(head))
|
|
1213
1214
|
throw fail("--head must be a commit SHA");
|
|
1214
1215
|
}
|
|
1216
|
+
else if (flag === "--bounce-id") {
|
|
1217
|
+
bounceId = value();
|
|
1218
|
+
if (!/^[0-9a-z-]{1,64}$/i.test(bounceId))
|
|
1219
|
+
throw fail("--bounce-id must be a merge queue entry id");
|
|
1220
|
+
}
|
|
1221
|
+
else if (flag === "--after-finished-at") {
|
|
1222
|
+
const raw = value();
|
|
1223
|
+
if (Number.isNaN(Date.parse(raw)))
|
|
1224
|
+
throw fail("--after-finished-at must be an ISO timestamp");
|
|
1225
|
+
afterFinishedAt = new Date(raw).toISOString();
|
|
1226
|
+
}
|
|
1215
1227
|
else if (arg.startsWith("-"))
|
|
1216
1228
|
throw fail();
|
|
1217
1229
|
else
|
|
@@ -1227,7 +1239,12 @@ export async function cmdStackWatch(args, deps = {}) {
|
|
|
1227
1239
|
json,
|
|
1228
1240
|
signal: deps.signal,
|
|
1229
1241
|
...(maxMinutes !== undefined ? { maxMs: maxMinutes * 60_000 } : {}),
|
|
1230
|
-
...(head
|
|
1242
|
+
...(head || bounceId || afterFinishedAt ? { cursor: {
|
|
1243
|
+
stackId: stackId.trim().toLowerCase(),
|
|
1244
|
+
enrolledHeadSha: head ? head.toLowerCase() : null,
|
|
1245
|
+
...(bounceId ? { bounceId } : {}),
|
|
1246
|
+
...(afterFinishedAt ? { afterFinishedAt } : {}),
|
|
1247
|
+
} } : {}),
|
|
1231
1248
|
});
|
|
1232
1249
|
if (result.exitCode === STACK_WATCH_EXIT.landed)
|
|
1233
1250
|
return;
|
|
@@ -1273,7 +1290,7 @@ export async function cmdStackStatus(args, deps = {}) {
|
|
|
1273
1290
|
freshCursor: true,
|
|
1274
1291
|
});
|
|
1275
1292
|
if (asJson) {
|
|
1276
|
-
console.log(JSON.stringify({ ...stack, held, repair, queueWait, watch }, null, 2));
|
|
1293
|
+
console.log(JSON.stringify({ ...stack, attention, held, repair, queueWait, watch }, null, 2));
|
|
1277
1294
|
return;
|
|
1278
1295
|
}
|
|
1279
1296
|
const lines = formatStackHuman(stack, false);
|
|
@@ -1287,7 +1304,7 @@ export async function cmdStackStatus(args, deps = {}) {
|
|
|
1287
1304
|
}
|
|
1288
1305
|
if (queueWait && !attention)
|
|
1289
1306
|
lines.push(` Queue: #${queueWait.prNumber} ${queueWait.text}`);
|
|
1290
|
-
lines.push(...[...stackHeldLine(held), ...stackRepairLine(repair), ...stackWatchTextLines(watch)].map((line) => ` ${line}`));
|
|
1307
|
+
lines.push(...[...stackHeldLine(attention ? null : held), ...stackRepairLine(repair), ...stackWatchTextLines(watch)].map((line) => ` ${line}`));
|
|
1291
1308
|
await present("Stack status", lines);
|
|
1292
1309
|
}
|
|
1293
1310
|
/** Human summary lines for a `stack set` write, one per flag. */
|
|
@@ -1446,7 +1463,7 @@ export async function cmdStack(args, options = {}) {
|
|
|
1446
1463
|
return cmdStackAdopt(rest);
|
|
1447
1464
|
if (sub === "restack")
|
|
1448
1465
|
return cmdStackRestack(rest);
|
|
1449
|
-
if (sub === "land"
|
|
1466
|
+
if (sub === "land")
|
|
1450
1467
|
return cmdStackLand(rest);
|
|
1451
1468
|
throw new CommandError(`${STACK_USAGE}\nunknown stack subcommand: ${sub}`);
|
|
1452
1469
|
}
|
package/dist/repo-identity.d.ts
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
export declare const LEGACY_STACK_META_REL: string;
|
|
2
1
|
export declare function gitTopLevel(cwd?: string): string;
|
|
3
|
-
export declare function gitCommonDir(cwd?: string): string;
|
|
4
2
|
/**
|
|
5
3
|
* Each worktree has its own authoring state.
|
|
6
4
|
* Independent clones remain isolated even when they point at the same remote.
|
|
@@ -9,4 +7,3 @@ export declare function stableRepoId(cwd?: string): string;
|
|
|
9
7
|
export declare function cliStacksRoot(): string;
|
|
10
8
|
export declare function repoStateDir(cwd?: string, stacksRoot?: string): string;
|
|
11
9
|
export declare function stackMetaPath(cwd?: string, stacksRoot?: string): string;
|
|
12
|
-
export declare function legacyStackMetaPath(cwd?: string): string;
|
package/dist/repo-identity.js
CHANGED
|
@@ -4,7 +4,6 @@ import path from "node:path";
|
|
|
4
4
|
import { mergestormHome } from "./config.js";
|
|
5
5
|
import { CommandError } from "./errors.js";
|
|
6
6
|
import { git } from "./git.js";
|
|
7
|
-
export const LEGACY_STACK_META_REL = path.join(".mergestorm", "stack.json");
|
|
8
7
|
/** Classify git "not a repo" failures once at the boundary; callers match on code. */
|
|
9
8
|
function rethrowIfNotARepo(err) {
|
|
10
9
|
const detail = err instanceof Error ? err.message : String(err);
|
|
@@ -21,25 +20,6 @@ export function gitTopLevel(cwd = process.cwd()) {
|
|
|
21
20
|
rethrowIfNotARepo(err);
|
|
22
21
|
}
|
|
23
22
|
}
|
|
24
|
-
export function gitCommonDir(cwd = process.cwd()) {
|
|
25
|
-
let raw;
|
|
26
|
-
try {
|
|
27
|
-
raw = git(["rev-parse", "--git-common-dir"], cwd).trim();
|
|
28
|
-
}
|
|
29
|
-
catch (err) {
|
|
30
|
-
rethrowIfNotARepo(err);
|
|
31
|
-
}
|
|
32
|
-
const absolute = path.isAbsolute(raw) ? raw : path.resolve(cwd, raw);
|
|
33
|
-
try {
|
|
34
|
-
return realpathSync(absolute);
|
|
35
|
-
}
|
|
36
|
-
catch (err) {
|
|
37
|
-
if (err?.code === "ENOENT") {
|
|
38
|
-
throw new CommandError(`git common directory is missing (${absolute}); this is often a linked worktree whose main repository was deleted. Run \`mg stack reset --force\` to clear local pre-submit state.`, 1, "not_a_repo");
|
|
39
|
-
}
|
|
40
|
-
throw err;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
23
|
/**
|
|
44
24
|
* Each worktree has its own authoring state.
|
|
45
25
|
* Independent clones remain isolated even when they point at the same remote.
|
|
@@ -67,6 +47,3 @@ export function repoStateDir(cwd = process.cwd(), stacksRoot = cliStacksRoot())
|
|
|
67
47
|
export function stackMetaPath(cwd = process.cwd(), stacksRoot = cliStacksRoot()) {
|
|
68
48
|
return path.join(repoStateDir(cwd, stacksRoot), "stack.json");
|
|
69
49
|
}
|
|
70
|
-
export function legacyStackMetaPath(cwd = process.cwd()) {
|
|
71
|
-
return path.join(gitTopLevel(cwd), LEGACY_STACK_META_REL);
|
|
72
|
-
}
|
package/dist/stack-blockers.d.ts
CHANGED
|
@@ -190,7 +190,6 @@ export declare const AUTO_LAND_CI_FAILED_BLOCKER = "CI failed \u2014 Auto land r
|
|
|
190
190
|
export declare const AUTO_LAND_WAIT_ATTENTION_MS: number;
|
|
191
191
|
export declare const AUTO_LAND_CI_UNREADABLE_ATTENTION_MS: number;
|
|
192
192
|
export declare function autoLandWaitLayer(stack: StackDto): StackLayerDto | undefined;
|
|
193
|
-
export declare function autoLandWaitBlocker(stack: StackDto, layer: StackLayerDto, nowMs: number): string | null;
|
|
194
193
|
export declare const MERGE_QUEUE_STALL_ATTENTION_MS: number;
|
|
195
194
|
export type StackQueueWait = {
|
|
196
195
|
prNumber: number;
|
|
@@ -221,22 +220,8 @@ export declare function stackRepair(stack: StackDto, attention: ClassifiedStackB
|
|
|
221
220
|
export declare const LAND_PR_MISSING_PREFIX = "No open land PR for ";
|
|
222
221
|
/** Pair-gate attention plus live hard blocks on every other open layer. */
|
|
223
222
|
export declare function stackBlockers(stack: StackDto, entries?: MergeQueueEntryDto[], cursor?: Pick<StackWatchCursor, "bounceId" | "afterFinishedAt">, nowMs?: number): {
|
|
224
|
-
attention: null;
|
|
225
|
-
held: StackHeldBlocker;
|
|
226
|
-
issues: StackBlocker[];
|
|
227
|
-
currentCandidate: {
|
|
228
|
-
prNumber: number;
|
|
229
|
-
headSha: string | null;
|
|
230
|
-
} | null;
|
|
231
|
-
bounce: undefined;
|
|
232
|
-
busy: StackAgentBusy[];
|
|
233
|
-
agents: StackLayerAgents | null;
|
|
234
|
-
repair: StackRepairHint | null;
|
|
235
|
-
landGatePending: StackLandGatePending | null;
|
|
236
|
-
queueWait: StackQueueWait | null;
|
|
237
|
-
} | {
|
|
238
223
|
attention: StackBlocker | null;
|
|
239
|
-
held: null;
|
|
224
|
+
held: StackHeldBlocker | null;
|
|
240
225
|
issues: StackBlocker[];
|
|
241
226
|
currentCandidate: {
|
|
242
227
|
prNumber: number;
|
package/dist/stack-blockers.js
CHANGED
|
@@ -185,9 +185,15 @@ function vortexImportantFindingsHeldAtHead(layer) {
|
|
|
185
185
|
sameHead(layer.cycloneHandoff?.headSha, layer.headSha) &&
|
|
186
186
|
CYCLONE_HELD_FINDINGS_REASONS.includes(layer.cycloneHandoff.reason);
|
|
187
187
|
}
|
|
188
|
+
function seamApprovedAtHead(stack, layer) {
|
|
189
|
+
const member = (stack.unit?.members ?? []).find((entry) => entry.prNumber === layer.prNumber);
|
|
190
|
+
return member?.seamState?.trim().toLowerCase() === "approved" && sameHead(member.seamReviewedSha, layer.headSha);
|
|
191
|
+
}
|
|
188
192
|
function vortexFindingsLeftForPerson(stack, layer) {
|
|
189
193
|
if (layerAgentsBusy(layer).cyclone)
|
|
190
194
|
return false;
|
|
195
|
+
if (seamApprovedAtHead(stack, layer))
|
|
196
|
+
return false;
|
|
191
197
|
return vortexImportantFindingsHeldAtHead(layer) ||
|
|
192
198
|
(vortexChangesRequestedAtHead(layer) && cycloneHandoffAtHead(stack, layer) !== null);
|
|
193
199
|
}
|
|
@@ -494,9 +500,6 @@ export function autoLandWaitLayer(stack) {
|
|
|
494
500
|
function autoLandWaitOn(stack, layer) {
|
|
495
501
|
return autoLandWaitLayer(stack)?.prNumber === layer.prNumber ? armedAutoLandWait(stack) : null;
|
|
496
502
|
}
|
|
497
|
-
export function autoLandWaitBlocker(stack, layer, nowMs) {
|
|
498
|
-
return autoLandWaitNamed(stack, layer, nowMs)?.blocker ?? null;
|
|
499
|
-
}
|
|
500
503
|
function autoLandWaitNamed(stack, layer, nowMs) {
|
|
501
504
|
const wait = autoLandWaitOn(stack, layer);
|
|
502
505
|
if (!wait)
|
|
@@ -929,6 +932,15 @@ function landPrMissing(stack, candidate) {
|
|
|
929
932
|
},
|
|
930
933
|
};
|
|
931
934
|
}
|
|
935
|
+
function layerBusy(layer) {
|
|
936
|
+
if (!layer)
|
|
937
|
+
return false;
|
|
938
|
+
const busy = layerAgentsBusy(layer);
|
|
939
|
+
return busy.cyclone || busy.vortex;
|
|
940
|
+
}
|
|
941
|
+
function sameBlocker(a, b) {
|
|
942
|
+
return a.prNumber === b.prNumber && a.blocker === b.blocker;
|
|
943
|
+
}
|
|
932
944
|
/** Pair-gate attention plus live hard blocks on every other open layer. */
|
|
933
945
|
export function stackBlockers(stack, entries = [], cursor = {}, nowMs = Date.now()) {
|
|
934
946
|
const candidate = currentLayer(stack);
|
|
@@ -946,10 +958,12 @@ export function stackBlockers(stack, entries = [], cursor = {}, nowMs = Date.now
|
|
|
946
958
|
let attentionLayer;
|
|
947
959
|
let bounce;
|
|
948
960
|
const issues = [];
|
|
961
|
+
const found = [];
|
|
949
962
|
const mergeStateUnknownPrs = new Set();
|
|
950
963
|
const queueWait = stack.archivedAt ? null : mergeQueueWait(queue, nowMs);
|
|
951
964
|
for (const layer of open) {
|
|
952
965
|
const selected = layer.prNumber === candidate?.prNumber;
|
|
966
|
+
const inScope = selected || !candidate || layer.position > candidate.position;
|
|
953
967
|
let result = layerAttention(stack, layer, selected ? queue : [], cursor);
|
|
954
968
|
let disarmed;
|
|
955
969
|
if (!result.blocker && selected) {
|
|
@@ -978,10 +992,10 @@ export function stackBlockers(stack, entries = [], cursor = {}, nowMs = Date.now
|
|
|
978
992
|
result = { blocker: `${MERGE_QUEUE_STALLED_PREFIX}${queueWait.text}`, reason: "queue_stalled", bounceKind: null };
|
|
979
993
|
}
|
|
980
994
|
if (!result.blocker) {
|
|
981
|
-
if (vortexRetryScheduled(layer) &&
|
|
995
|
+
if (vortexRetryScheduled(layer) && inScope) {
|
|
982
996
|
issues.push({ prNumber: layer.prNumber, headSha: layer.headSha ?? null, blocker: VORTEX_RETRY_SCHEDULED, bounceKind: null });
|
|
983
997
|
}
|
|
984
|
-
if (mergeabilityPending(layer) &&
|
|
998
|
+
if (mergeabilityPending(layer) && inScope) {
|
|
985
999
|
mergeStateUnknownPrs.add(layer.prNumber);
|
|
986
1000
|
issues.push({ prNumber: layer.prNumber, headSha: layer.headSha ?? null,
|
|
987
1001
|
blocker: `Merge state unknown${layer.parentBranch ? ` vs ${layer.parentBranch}` : ""}`, bounceKind: null });
|
|
@@ -990,6 +1004,9 @@ export function stackBlockers(stack, entries = [], cursor = {}, nowMs = Date.now
|
|
|
990
1004
|
}
|
|
991
1005
|
const issue = { prNumber: layer.prNumber, headSha: layer.headSha ?? null,
|
|
992
1006
|
blocker: result.blocker, bounceKind: result.bounceKind };
|
|
1007
|
+
if (inScope || result.reason === "unit_abandoned" || layer === child || waitNamed) {
|
|
1008
|
+
found.push({ issue, reason: result.reason, layer, bounce: result.bounce });
|
|
1009
|
+
}
|
|
993
1010
|
if (selected ||
|
|
994
1011
|
result.reason === "unit_abandoned" ||
|
|
995
1012
|
(!attention && (layer === child || waitNamed))) {
|
|
@@ -1001,7 +1018,7 @@ export function stackBlockers(stack, entries = [], cursor = {}, nowMs = Date.now
|
|
|
1001
1018
|
issues.push({ prNumber: disarmed.bounceDetail?.prNumber ?? layer.prNumber,
|
|
1002
1019
|
headSha: disarmed.bounceDetail?.headSha ?? null, blocker: mergeQueueBounceLabel(disarmed), bounceKind: result.bounceKind });
|
|
1003
1020
|
}
|
|
1004
|
-
else if (
|
|
1021
|
+
else if (inScope)
|
|
1005
1022
|
issues.push(issue);
|
|
1006
1023
|
}
|
|
1007
1024
|
if (!attention) {
|
|
@@ -1011,6 +1028,7 @@ export function stackBlockers(stack, entries = [], cursor = {}, nowMs = Date.now
|
|
|
1011
1028
|
attention = { prNumber: unlisted.prNumber, headSha: unlisted.headSha ?? null, blocker: waiting.blocker, bounceKind: null };
|
|
1012
1029
|
attentionReason = waiting.reason;
|
|
1013
1030
|
attentionLayer = unlisted;
|
|
1031
|
+
found.push({ issue: attention, reason: waiting.reason, layer: unlisted, bounce: undefined });
|
|
1014
1032
|
}
|
|
1015
1033
|
}
|
|
1016
1034
|
const queued = queueWait && candidate && !queueWait.stalled
|
|
@@ -1025,13 +1043,15 @@ export function stackBlockers(stack, entries = [], cursor = {}, nowMs = Date.now
|
|
|
1025
1043
|
attention = landMissing.attention;
|
|
1026
1044
|
attentionReason = landMissing.reason;
|
|
1027
1045
|
attentionLayer = undefined;
|
|
1046
|
+
found.unshift({ issue: landMissing.attention, reason: landMissing.reason, layer: undefined, bounce: undefined,
|
|
1047
|
+
repair: landMissing.repair });
|
|
1028
1048
|
}
|
|
1029
1049
|
const classified = attention && attentionReason
|
|
1030
1050
|
? { ...attention, reason: attentionReason }
|
|
1031
1051
|
: null;
|
|
1032
1052
|
const focus = attentionLayer ?? candidate;
|
|
1033
|
-
|
|
1034
|
-
|
|
1053
|
+
let agents = focus ? layerAgents(focus) : null;
|
|
1054
|
+
let busy = [];
|
|
1035
1055
|
if (attention && agents) {
|
|
1036
1056
|
for (const agent of ["cyclone", "vortex"]) {
|
|
1037
1057
|
if (agents.busy[agent])
|
|
@@ -1045,17 +1065,39 @@ export function stackBlockers(stack, entries = [], cursor = {}, nowMs = Date.now
|
|
|
1045
1065
|
const landGatePending = landLayer && landing && landGateIsPending(landing)
|
|
1046
1066
|
? { prNumber: landLayer.prNumber, headSha: landLayer.headSha ?? null, reason: landing }
|
|
1047
1067
|
: null;
|
|
1068
|
+
let held = null;
|
|
1069
|
+
let shownIssues = issues;
|
|
1070
|
+
let repair;
|
|
1048
1071
|
if (busy.length && attention && classified && agentsCannotClear(classified.reason, stack, attentionLayer)) {
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1072
|
+
held = { ...attention, actAfter: "agents_idle", waitingOn: busy.map((entry) => entry.agent) };
|
|
1073
|
+
shownIssues = [attention, ...issues];
|
|
1074
|
+
repair = stackRepair(stack, classified, attentionLayer, undefined, queueWait);
|
|
1075
|
+
attention = null;
|
|
1076
|
+
bounce = undefined;
|
|
1077
|
+
}
|
|
1078
|
+
else if (busy.length) {
|
|
1079
|
+
repair = null;
|
|
1080
|
+
attention = null;
|
|
1081
|
+
bounce = undefined;
|
|
1052
1082
|
}
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1083
|
+
else {
|
|
1084
|
+
repair = landMissing?.repair ?? stackRepair(stack, classified, attentionLayer, bounce, queueWait);
|
|
1085
|
+
}
|
|
1086
|
+
if (!attention) {
|
|
1087
|
+
const next = found.find((item) => !layerBusy(item.layer));
|
|
1088
|
+
if (next) {
|
|
1089
|
+
attention = next.issue;
|
|
1090
|
+
repair = next.repair !== undefined
|
|
1091
|
+
? next.repair
|
|
1092
|
+
: stackRepair(stack, { ...next.issue, reason: next.reason }, next.layer, next.bounce, queueWait);
|
|
1093
|
+
bounce = next.bounce;
|
|
1094
|
+
if (next.layer)
|
|
1095
|
+
agents = layerAgents(next.layer);
|
|
1096
|
+
busy = [];
|
|
1097
|
+
shownIssues = shownIssues.filter((issue) => !sameBlocker(issue, next.issue));
|
|
1098
|
+
}
|
|
1056
1099
|
}
|
|
1057
|
-
return { attention, held
|
|
1058
|
-
repair: landMissing?.repair ?? stackRepair(stack, classified, attentionLayer, bounce, queueWait), landGatePending,
|
|
1100
|
+
return { attention, held, issues: shownIssues, currentCandidate, bounce, busy, agents, repair, landGatePending,
|
|
1059
1101
|
queueWait: queued };
|
|
1060
1102
|
}
|
|
1061
1103
|
function mergeabilitySettleIssue(stack, open, mergeStateUnknownPrs, attention) {
|
package/dist/stack-meta.d.ts
CHANGED
|
@@ -19,8 +19,6 @@ export type StackMeta = {
|
|
|
19
19
|
/** Index into `stacks` for create/submit. */
|
|
20
20
|
active: number;
|
|
21
21
|
};
|
|
22
|
-
/** Exported only for legacy migration/tests. New state never uses this path. */
|
|
23
|
-
export declare const STACK_META_REL: string;
|
|
24
22
|
export declare function stackMetaPath(cwd?: string, stacksRoot?: string): string;
|
|
25
23
|
export declare function loadStackMeta(cwd?: string, stacksRoot?: string): Promise<StackMeta | null>;
|
|
26
24
|
export declare function emptyStackMeta(trunk: string): StackMeta;
|
package/dist/stack-meta.js
CHANGED
|
@@ -3,11 +3,10 @@
|
|
|
3
3
|
* Stored outside repositories under ~/.mergestorm/stacks/<repo-id>/.
|
|
4
4
|
*/
|
|
5
5
|
import { randomUUID } from "node:crypto";
|
|
6
|
-
import { chmod,
|
|
6
|
+
import { chmod, mkdir, readFile, readdir, rename, rm, writeFile, } from "node:fs/promises";
|
|
7
7
|
import path from "node:path";
|
|
8
8
|
import { CommandError, isCommandErrorCode } from "./errors.js";
|
|
9
|
-
import {
|
|
10
|
-
import { cliStacksRoot, gitTopLevel, LEGACY_STACK_META_REL, legacyStackMetaPath, repoStateDir, stackMetaPath as resolveStackMetaPath, } from "./repo-identity.js";
|
|
9
|
+
import { cliStacksRoot, stackMetaPath as resolveStackMetaPath, } from "./repo-identity.js";
|
|
11
10
|
const LOCAL_STACK_FLAGS = [
|
|
12
11
|
"autoEnqueueWhenReady",
|
|
13
12
|
"autoReviewOverride",
|
|
@@ -15,8 +14,6 @@ const LOCAL_STACK_FLAGS = [
|
|
|
15
14
|
"autoResolveConflictsOverride",
|
|
16
15
|
"autoFixCiOverride",
|
|
17
16
|
];
|
|
18
|
-
/** Exported only for legacy migration/tests. New state never uses this path. */
|
|
19
|
-
export const STACK_META_REL = LEGACY_STACK_META_REL;
|
|
20
17
|
export function stackMetaPath(cwd = process.cwd(), stacksRoot = cliStacksRoot()) {
|
|
21
18
|
return resolveStackMetaPath(cwd, stacksRoot);
|
|
22
19
|
}
|
|
@@ -64,17 +61,6 @@ function normalizeMeta(raw) {
|
|
|
64
61
|
}
|
|
65
62
|
return { version: 2, trunk: obj.trunk, stacks, active };
|
|
66
63
|
}
|
|
67
|
-
if (obj.version === 1) {
|
|
68
|
-
if (!Array.isArray(obj.layers) || !obj.layers.every(isLayer)) {
|
|
69
|
-
throw new Error("Stack authoring state has malformed legacy layers; run `mg stack reset --force` to clear it.");
|
|
70
|
-
}
|
|
71
|
-
return {
|
|
72
|
-
version: 2,
|
|
73
|
-
trunk: obj.trunk,
|
|
74
|
-
stacks: [{ layers: obj.layers }],
|
|
75
|
-
active: 0,
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
64
|
throw new Error("Stack authoring state has an unknown version; run `mg stack reset --force` to clear it.");
|
|
79
65
|
}
|
|
80
66
|
function parseStateJson(raw) {
|
|
@@ -96,71 +82,17 @@ async function readIfExists(file) {
|
|
|
96
82
|
throw err;
|
|
97
83
|
}
|
|
98
84
|
}
|
|
99
|
-
async function removeLegacyFile(file, cwd = process.cwd(), force = false) {
|
|
100
|
-
const top = gitTopLevel(cwd);
|
|
101
|
-
const rel = path.relative(top, file);
|
|
102
|
-
try {
|
|
103
|
-
git(["rm", "-f", "--", rel], top);
|
|
104
|
-
}
|
|
105
|
-
catch (err) {
|
|
106
|
-
// Fall back to a plain unlink only for untracked files; otherwise the
|
|
107
|
-
// index entry would remain after the worktree copy is gone.
|
|
108
|
-
if (git(["ls-files", "--", rel], top).trim() !== "") {
|
|
109
|
-
if (!force) {
|
|
110
|
-
throw new Error(`git rm failed for tracked ${rel}: ${err instanceof Error ? err.message : String(err)}`);
|
|
111
|
-
}
|
|
112
|
-
git(["update-index", "--force-remove", "--", rel], top);
|
|
113
|
-
}
|
|
114
|
-
await unlink(file);
|
|
115
|
-
}
|
|
116
|
-
try {
|
|
117
|
-
await rmdir(path.dirname(file));
|
|
118
|
-
}
|
|
119
|
-
catch {
|
|
120
|
-
// Leave a non-empty .mergestorm directory untouched.
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
85
|
export async function loadStackMeta(cwd = process.cwd(), stacksRoot = cliStacksRoot()) {
|
|
124
86
|
const file = stackMetaPath(cwd, stacksRoot);
|
|
125
|
-
const legacyFile = legacyStackMetaPath(cwd);
|
|
126
87
|
const raw = await readIfExists(file);
|
|
127
|
-
const legacyRaw = await readIfExists(legacyFile);
|
|
128
88
|
if (raw !== null) {
|
|
129
89
|
const meta = normalizeMeta(parseStateJson(raw));
|
|
130
90
|
if (meta === null) {
|
|
131
91
|
throw new CommandError("Stack authoring state is not an object; run `mg stack reset --force` to clear it.");
|
|
132
92
|
}
|
|
133
|
-
if (legacyRaw !== null) {
|
|
134
|
-
const stateDir = repoStateDir(cwd, stacksRoot);
|
|
135
|
-
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
136
|
-
const backup = path.join(stateDir, `legacy-conflict-${stamp}.json`);
|
|
137
|
-
try {
|
|
138
|
-
await mkdir(stateDir, { recursive: true, mode: 0o700 });
|
|
139
|
-
await chmod(stateDir, 0o700);
|
|
140
|
-
await copyFile(legacyFile, backup);
|
|
141
|
-
await chmod(backup, 0o600);
|
|
142
|
-
await removeLegacyFile(legacyFile, cwd);
|
|
143
|
-
}
|
|
144
|
-
catch (err) {
|
|
145
|
-
throw new CommandError(`Failed to preserve legacy stack state${err instanceof Error ? `: ${err.message}` : ""}; run \`mg stack reset --force\` to clear it.`);
|
|
146
|
-
}
|
|
147
|
-
console.warn(`Warning: global stack state already existed; preserved legacy state at ${backup}.`);
|
|
148
|
-
}
|
|
149
93
|
return meta;
|
|
150
94
|
}
|
|
151
|
-
|
|
152
|
-
return null;
|
|
153
|
-
const migrated = normalizeMeta(parseStateJson(legacyRaw));
|
|
154
|
-
if (!migrated)
|
|
155
|
-
return null;
|
|
156
|
-
try {
|
|
157
|
-
await saveStackMeta(migrated, cwd, stacksRoot);
|
|
158
|
-
await removeLegacyFile(legacyFile, cwd);
|
|
159
|
-
}
|
|
160
|
-
catch (err) {
|
|
161
|
-
throw new CommandError(`Failed to migrate legacy stack state${err instanceof Error ? `: ${err.message}` : ""}; run \`mg stack reset --force\` to clear it.`);
|
|
162
|
-
}
|
|
163
|
-
return migrated;
|
|
95
|
+
return null;
|
|
164
96
|
}
|
|
165
97
|
export function emptyStackMeta(trunk) {
|
|
166
98
|
return {
|
|
@@ -225,10 +157,8 @@ export async function saveStackMeta(meta, cwd = process.cwd(), stacksRoot = cliS
|
|
|
225
157
|
/** Explicit recovery. Does not touch branches, PRs, or registered server stacks. */
|
|
226
158
|
export async function resetStackMeta(cwd = process.cwd(), stacksRoot = cliStacksRoot()) {
|
|
227
159
|
let stateFile;
|
|
228
|
-
let legacyFile;
|
|
229
160
|
try {
|
|
230
161
|
stateFile = stackMetaPath(cwd, stacksRoot);
|
|
231
|
-
legacyFile = legacyStackMetaPath(cwd);
|
|
232
162
|
}
|
|
233
163
|
catch (err) {
|
|
234
164
|
// Outside a worktree / deleted main repo: reset is a no-op (nothing to clear).
|
|
@@ -238,14 +168,6 @@ export async function resetStackMeta(cwd = process.cwd(), stacksRoot = cliStacks
|
|
|
238
168
|
throw err;
|
|
239
169
|
}
|
|
240
170
|
await rm(stateFile, { force: true });
|
|
241
|
-
try {
|
|
242
|
-
await removeLegacyFile(legacyFile, cwd, true);
|
|
243
|
-
}
|
|
244
|
-
catch (err) {
|
|
245
|
-
const code = err?.code;
|
|
246
|
-
if (code !== "ENOENT")
|
|
247
|
-
throw err;
|
|
248
|
-
}
|
|
249
171
|
}
|
|
250
172
|
export function appendLayer(meta, layer) {
|
|
251
173
|
const current = activeLayers(meta);
|
package/dist/stack-watch-loop.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { setTimeout as sleep } from "node:timers/promises";
|
|
2
2
|
import { API_TIMEOUT_PREFIX, DEFAULT_API_TIMEOUT_MS, LandedQueueReadError, readLandedQueueEntry } from "./api.js";
|
|
3
3
|
import { isTransientReviewPollError } from "./commands/review-client.js";
|
|
4
|
+
import { stackWatchBackgroundCommand } from "./stack-watch-obligation.js";
|
|
4
5
|
import { pollStackWatch, StackWatchError, StackWatchTimeoutError, } from "./stack-watch.js";
|
|
5
6
|
export const STACK_WATCH_MARKER = "MS-WATCH";
|
|
6
7
|
export const STACK_WATCH_EXIT = { landed: 0, attention: 3, timeout: 5 };
|
|
@@ -30,7 +31,9 @@ export function stackWatchAttentionKey(envelope) {
|
|
|
30
31
|
return `${envelope.prNumber ?? ""}|${envelope.headSha ?? ""}|${blockerWithoutElapsedMinutes(envelope.blocker)}`;
|
|
31
32
|
}
|
|
32
33
|
export function stackWatchAttentionLine(envelope) {
|
|
33
|
-
|
|
34
|
+
const restart = envelope.cursor.bounceId
|
|
35
|
+
? ` restart=${JSON.stringify(stackWatchBackgroundCommand(envelope.stackId, envelope.cursor))}` : "";
|
|
36
|
+
return `${STACK_WATCH_MARKER} ATTENTION pr=${envelope.prNumber ?? "none"} head=${envelope.headSha ?? "none"} blocker=${JSON.stringify(envelope.blocker ?? "")} repair=${envelope.repair?.kind ?? "none"}${restart}`;
|
|
34
37
|
}
|
|
35
38
|
export function stackWatchLandedLine(envelope) {
|
|
36
39
|
return `${STACK_WATCH_MARKER} LANDED stack=${envelope.stackId} reason=${envelope.watch.reason}`;
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import type { StackDto } from "./stack-dto.js";
|
|
2
2
|
export declare const STACK_WATCH_NOT_DONE_SENTENCE = "This stack is not landed. Your task is not done. Call stack_wait again with this cursor.";
|
|
3
3
|
export declare const STACK_WATCH_NOTIFY_PATTERN = "MS-WATCH (ATTENTION|LANDED)";
|
|
4
|
-
export declare function stackWatchBackgroundCommand(stackId: string
|
|
4
|
+
export declare function stackWatchBackgroundCommand(stackId: string, cursor?: {
|
|
5
|
+
afterFinishedAt?: string | null;
|
|
6
|
+
bounceId?: string | null;
|
|
7
|
+
} | null): string;
|
|
5
8
|
export type StackWatchDoneReason = "landed" | "closed" | "archived" | "not_found";
|
|
6
9
|
export type StackWatchReason = StackWatchDoneReason | "open" | "attention" | "unread" | "rate_limited" | "failed" | "landing_unconfirmed";
|
|
7
10
|
export type StackWatchNextArgs = {
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
export const STACK_WATCH_NOT_DONE_SENTENCE = "This stack is not landed. Your task is not done. Call stack_wait again with this cursor.";
|
|
2
2
|
export const STACK_WATCH_NOTIFY_PATTERN = "MS-WATCH (ATTENTION|LANDED)";
|
|
3
|
-
|
|
4
|
-
return
|
|
3
|
+
function shellArg(value) {
|
|
4
|
+
return /^[A-Za-z0-9_./:@%+=,-]+$/.test(value) ? value : `'${value.replace(/'/g, `'\\''`)}'`;
|
|
5
|
+
}
|
|
6
|
+
export function stackWatchBackgroundCommand(stackId, cursor) {
|
|
7
|
+
return `mg stack watch ${shellArg(stackId)}${cursor?.bounceId ? ` --bounce-id ${shellArg(cursor.bounceId)}` : ""}${cursor?.afterFinishedAt ? ` --after-finished-at ${shellArg(cursor.afterFinishedAt)}` : ""}`;
|
|
5
8
|
}
|
|
6
9
|
const DONE_MESSAGES = {
|
|
7
10
|
landed: "This stack is landed. The watch is done; stop calling stack_wait for it.",
|
|
@@ -53,7 +56,7 @@ export function stackWatchObligation(input) {
|
|
|
53
56
|
: input.status === "failed"
|
|
54
57
|
? "failed"
|
|
55
58
|
: input.attention ? "attention" : input.unread ? "unread" : "open";
|
|
56
|
-
const background = stackWatchBackgroundCommand(input.stackId);
|
|
59
|
+
const background = stackWatchBackgroundCommand(input.stackId, input.cursor);
|
|
57
60
|
const parts = [
|
|
58
61
|
STACK_WATCH_NOT_DONE_SENTENCE,
|
|
59
62
|
`If your host has a confirmed notification that resumes this task, run \`${background}\` as a background command, notify on output matching ${STACK_WATCH_NOTIFY_PATTERN}, and end your turn. The command monitors the stack; it cannot wake an agent by itself, and a process left running is not a notification. Without a confirmed notification, keep using stack_wait with the returned cursor, or report that automatic follow-up is unavailable before ending the turn.`,
|
|
@@ -66,7 +69,7 @@ export function stackWatchObligation(input) {
|
|
|
66
69
|
parts.push(`This read failed, which does not end the task. Call stack_wait once more with the same cursor; if that fails too, tell the human that stack ${input.stackId} is not landed and is no longer being watched.`);
|
|
67
70
|
}
|
|
68
71
|
else if (reason === "attention" && input.attention) {
|
|
69
|
-
parts.push(`First fix #${input.attention.prNumber} (${input.attention.blocker}); repair, when present, names the fix. Calling stack_wait again before fixing it returns the same attention. After your push, call stack_wait with this cursor and enrolled_head_sha set to the SHA you pushed, or start \`${background} --head <pushed-sha>\` in the background.`);
|
|
72
|
+
parts.push(`First fix #${input.attention.prNumber} (${input.attention.blocker}); repair, when present, names the fix. Calling stack_wait again before fixing it returns the same attention. After your push, call stack_wait with this cursor and enrolled_head_sha set to the SHA you pushed, or start \`${background} --head <pushed-sha>\` in the background.${input.cursor?.bounceId ? ` If you handle it without a push (for example by turning Auto land back on), start \`${background}\` so this bounce is not reported again.` : ""}`);
|
|
70
73
|
}
|
|
71
74
|
else if (reason === "unread") {
|
|
72
75
|
parts.push("This tool did not read the stack's state; stack_wait will.");
|
package/dist/stack-watch.js
CHANGED
|
@@ -154,8 +154,9 @@ export async function pollStackWatch(cfg, stackId, opts = {}) {
|
|
|
154
154
|
...(bounce.finishedAt !== undefined ? { afterFinishedAt: bounce.finishedAt } : {}) });
|
|
155
155
|
const held = busy[0];
|
|
156
156
|
const shown = attention ?? named;
|
|
157
|
+
const waitingNamed = attention ? null : named;
|
|
157
158
|
lastEnvelope = { ...lastEnvelope, cursor, issues, currentCandidate, assessment: "available", busy, agents, repair, landGatePending, queueWait,
|
|
158
|
-
actAfter:
|
|
159
|
+
actAfter: waitingNamed?.actAfter ?? null, waitingOn: waitingNamed?.waitingOn ?? [],
|
|
159
160
|
blocker: shown?.blocker ?? null, bounceKind: shown?.bounceKind ?? null,
|
|
160
161
|
prNumber: shown?.prNumber ?? held?.prNumber ?? currentCandidate?.prNumber ?? null,
|
|
161
162
|
headSha: shown ? shown.headSha : held ? held.headSha : currentCandidate?.headSha ?? null,
|
package/dist/ui/banner.js
CHANGED
|
@@ -221,6 +221,9 @@ const WHATS_NEW = {
|
|
|
221
221
|
"0.3.61": [
|
|
222
222
|
"Watch names a job Cyclone's push skips",
|
|
223
223
|
],
|
|
224
|
+
"0.3.62": [
|
|
225
|
+
"Watch wakes for a blocker on any layer",
|
|
226
|
+
],
|
|
224
227
|
};
|
|
225
228
|
function densityFromVariant(variant) {
|
|
226
229
|
if (variant === "mini" || variant === "nano")
|
package/package.json
CHANGED
|
@@ -25,10 +25,14 @@ The CLI watcher monitors the stack but cannot wake an agent by itself. Use the b
|
|
|
25
25
|
- `MS-WATCH ATTENTION failed stack=<id> error="<message>"` (exit 3): a read failed and its one retry failed too.
|
|
26
26
|
- `MS-WATCH LANDED stack=<id> reason=landed` (exit 0): the stack is confirmed landed. `closed`, `archived`, and `not_found` instead emit `MS-WATCH ATTENTION stack=<id> reason=<reason>` (exit 3); follow "Not done until landed".
|
|
27
27
|
With `--max <minutes>` it can also stop with `MS-WATCH TIMEOUT` (exit 5). The stack is not landed then; start the watcher again. `--json` writes exactly one JSON document to stdout: the final `mergestorm.stack_watch/v1` envelope, or `{ stackId, outcome }` when there is no envelope (a repeated failed read, timeout, or abort). MS-WATCH marker lines go to stderr; do not merge the streams before parsing stdout.
|
|
28
|
-
3. When the host resumes you on `MS-WATCH ATTENTION pr=`, call `stack_status` once for that stack, then fix the blocker as steps 3 to 7 of "Watch and patch" say, push, and start the watcher again with `--head` set to the SHA you pushed. On `MS-WATCH ATTENTION failed`, the watcher already retried once: tell the human the stack is not landed and is no longer being watched.
|
|
29
|
-
4. When the human has taken one blocker over and asked you to keep watching the
|
|
28
|
+
3. When the host resumes you on `MS-WATCH ATTENTION pr=`, call `stack_status` once for that stack, then fix the blocker as steps 3 to 7 of "Watch and patch" say, push, and start the watcher again with `--head` set to the SHA you pushed. When the ATTENTION line ends with `restart="mg stack watch <id> --bounce-id <id> --after-finished-at <time>"` (a merge queue bounce), start the watcher again with that command, adding `--head <sha>` after a push; without those flags a bounce you handled without a push (for example by turning Auto land back on) is reported again at once. On `MS-WATCH ATTENTION failed`, the watcher already retried once: tell the human the stack is not landed and is no longer being watched.
|
|
29
|
+
4. When the human has taken one blocker over and asked you to keep watching the stack, pass `--ignore <blocker-substring>`. The watcher stays silent on the first attention whose blocker contains that text until the blocker, its PR, or its head changes. Attention names one blocker at a time, so while the ignored blocker stands, blockers on higher layers stay hidden too; the next blocked layer wakes you only after the ignored blocker, its PR, or its head changes.
|
|
30
30
|
5. `--until landed` keeps the watcher running after an attention: it prints each new attention line once and exits only when the watch is done or a read fails twice. Use it only when your host streams output lines to you while the command runs.
|
|
31
31
|
|
|
32
|
+
## Attention on any layer
|
|
33
|
+
|
|
34
|
+
Attention covers the whole stack, not only `currentCandidate`. It names the lowest open layer, a parked one included, that has a blocker an agent must fix while no Vortex or Cyclone run is working on that layer. On a layer with auto-patch off, request_changes from Vortex at the live head is `Vortex findings need a person`, whichever layer it is on. There is one attention at a time: fix it, push, and restart the watch; the next read names the next blocked layer at once. Vortex findings that do not stop Auto land (Important findings Cyclone never took, Info notes) are not attention and the watch does not report them.
|
|
35
|
+
|
|
32
36
|
## Scope and enrollment
|
|
33
37
|
|
|
34
38
|
Watch only stacks the human named. If no unambiguous stack ID is available, ask the human to identify it; do not discover and enroll other stacks. Keep a separate cursor for each named stack.
|