mergestorm 0.3.26 → 0.3.27
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 +15 -2
- package/dist/api.d.ts +59 -0
- package/dist/api.js +40 -0
- package/dist/client/index.d.ts +2 -2
- package/dist/client/index.js +2 -2
- package/dist/commands/dismiss.d.ts +15 -0
- package/dist/commands/dismiss.js +145 -0
- package/dist/commands/registry.js +16 -0
- package/dist/commands/stack.d.ts +2 -1
- package/dist/commands/stack.js +10 -4
- package/dist/stack-blockers.d.ts +22 -1
- package/dist/stack-blockers.js +42 -12
- package/dist/stack-dto.d.ts +10 -0
- package/dist/stack-watch.d.ts +3 -1
- package/dist/stack-watch.js +8 -6
- package/dist/ui/banner.js +3 -0
- package/package.json +1 -1
- package/skill/mergestorm-bounce-watch/SKILL.md +3 -3
- package/skill/mergestorm-pr-loop/SKILL.md +7 -1
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.27 │
|
|
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:
|
|
53
|
+
│ ▀ New: mg dismiss records an audited finding dismissal │
|
|
54
54
|
╰──────────────────────────────────────────────────────────────────────────────╯
|
|
55
55
|
mergestorm
|
|
56
56
|
╭──────────────────────────────────────────────────────────────────────────────╮
|
|
@@ -96,8 +96,21 @@ mergestorm stack restack <stack-id> Restack descendants
|
|
|
96
96
|
mergestorm stack land <stack-id> Land / promote (into review unit when present)
|
|
97
97
|
mergestorm stack wait <stack-id> [--timeout <s>] Wait for stack attention (default 45s, max 300s; 0 = one snapshot, no wait)
|
|
98
98
|
mergestorm stack reset --force Clear local authoring state (not branches/PRs)
|
|
99
|
+
mergestorm dismiss <owner/repo>#<n> --head <sha> --review <id> --preview List a Vortex review's finding ids and the seam gate
|
|
100
|
+
mergestorm dismiss <owner/repo>#<n> --head <sha> --review <id> --finding <id> --reason <text> Dismiss verified-wrong findings (`--all` for the whole review, `--evidence <url>`, `--json`)
|
|
99
101
|
```
|
|
100
102
|
|
|
103
|
+
### Dismissing a finding
|
|
104
|
+
|
|
105
|
+
Use `mergestorm dismiss` when a Vortex finding is wrong and you have checked it against the code. It is recorded server side with your GitHub login, the reason, the evidence URL, the scope and the time. Rules:
|
|
106
|
+
|
|
107
|
+
- You need write access to the repository on GitHub, and the repository must be monitored by your Mergestorm account or the PR must be in one of your stacks.
|
|
108
|
+
- `--head` must be the PR's live head and `--review` a Vortex review made at that head. A moved head, another review, or a finding id that is not in that review is refused, and nothing is written.
|
|
109
|
+
- Retrying the same dismissal is safe; it reports the earlier record.
|
|
110
|
+
- The seam gate clears only when every finding of that integration review is dismissed and the review is still the current seam verdict. CI, other reviews and Auto land policy still apply.
|
|
111
|
+
- Vortex does not raise a dismissed finding again on the same diff (the same head, or a restack with an unchanged diff).
|
|
112
|
+
- With `--json`, the result carries `gate` and, for a stack PR, `watch.next` (the next `stack_wait` call).
|
|
113
|
+
|
|
101
114
|
`mg` is a short alias for `mergestorm` (same binary), e.g. `mg stack create`. Happy path: **create → commit → submit → restack → land**. On review-unit stacks, `stack land` **promotes** the tip into the unit (same gates as the dashboard); otherwise it lands the bottom open PR. `stack adopt` is for importing a chain that already exists on GitHub (legacy / Graphite). Restack/land use the same login key (`/api/v1/stacks`). Unattended landing is `mg queue add`.
|
|
102
115
|
|
|
103
116
|
`stack create` checks out a new branch from the current tip (or `--onto <branch>`), discovers trunk (`main` / `master` / `origin/HEAD`, overridable with `--trunk`), and records `{ branch, parentBranch }` in CLI-managed state under **`~/.mergestorm/stacks/<repo-id>/stack.json`**. The CLI updates this automatically; never edit it. Each worktree has its own authoring state; independent clones stay isolated too. Creating onto trunk with a non-empty active stack starts a new one. `--trunk` only sets trunk metadata — it does **not** change the parent; use `--onto main` (etc.) for a fresh stack from trunk.
|
package/dist/api.d.ts
CHANGED
|
@@ -267,3 +267,62 @@ export declare function enqueueStack(stackId: string, cfg?: Config): Promise<unk
|
|
|
267
267
|
export declare function cancelMergeQueueEntry(entryId: string, cfg?: Config): Promise<unknown>;
|
|
268
268
|
/** Recently reviewed branches/threads (Bearer GET /api/v1/threads). */
|
|
269
269
|
export declare function listThreads(limit?: number, cfg?: Config): Promise<ThreadListItem[]>;
|
|
270
|
+
export type PrFindingDismissInput = {
|
|
271
|
+
owner: string;
|
|
272
|
+
repo: string;
|
|
273
|
+
prNumber: number;
|
|
274
|
+
headSha: string;
|
|
275
|
+
reviewId: number;
|
|
276
|
+
findingIds?: string[];
|
|
277
|
+
scope?: "findings" | "review";
|
|
278
|
+
reason?: string;
|
|
279
|
+
evidenceUrl?: string;
|
|
280
|
+
preview?: boolean;
|
|
281
|
+
};
|
|
282
|
+
export type PrDismissedFinding = {
|
|
283
|
+
finding_id: string;
|
|
284
|
+
finding_key: string;
|
|
285
|
+
path: string | null;
|
|
286
|
+
line: number | null;
|
|
287
|
+
title: string;
|
|
288
|
+
};
|
|
289
|
+
export type PrFindingDismissResult = {
|
|
290
|
+
status: "dismissed" | "preview";
|
|
291
|
+
owner: string;
|
|
292
|
+
repo: string;
|
|
293
|
+
pr_number: number;
|
|
294
|
+
head_sha: string;
|
|
295
|
+
review_id: number;
|
|
296
|
+
review_kind: "seam" | "core";
|
|
297
|
+
scope: "findings" | "review";
|
|
298
|
+
dismissed: PrDismissedFinding[];
|
|
299
|
+
already_dismissed: Array<PrDismissedFinding & {
|
|
300
|
+
actor_login: string;
|
|
301
|
+
dismissed_at: string;
|
|
302
|
+
}>;
|
|
303
|
+
remaining: PrDismissedFinding[];
|
|
304
|
+
all_dismissed: boolean;
|
|
305
|
+
gate: {
|
|
306
|
+
seam: {
|
|
307
|
+
state: string;
|
|
308
|
+
reviewed_sha: string | null;
|
|
309
|
+
review_id: number | null;
|
|
310
|
+
cleared: boolean;
|
|
311
|
+
blocking: boolean;
|
|
312
|
+
} | null;
|
|
313
|
+
other_gates: "unchanged";
|
|
314
|
+
};
|
|
315
|
+
stack_id: string | null;
|
|
316
|
+
};
|
|
317
|
+
export type PrFindingDismissOutcome = {
|
|
318
|
+
ok: true;
|
|
319
|
+
result: PrFindingDismissResult;
|
|
320
|
+
} | {
|
|
321
|
+
ok: false;
|
|
322
|
+
status: number;
|
|
323
|
+
error: string;
|
|
324
|
+
message: string;
|
|
325
|
+
};
|
|
326
|
+
export declare function dismissPrFindings(input: PrFindingDismissInput, cfg?: Config, opts?: {
|
|
327
|
+
signal?: AbortSignal;
|
|
328
|
+
}): Promise<PrFindingDismissOutcome>;
|
package/dist/api.js
CHANGED
|
@@ -597,3 +597,43 @@ export async function listThreads(limit = 20, cfg) {
|
|
|
597
597
|
const items = body.items;
|
|
598
598
|
return Array.isArray(items) ? items : [];
|
|
599
599
|
}
|
|
600
|
+
export async function dismissPrFindings(input, cfg, opts) {
|
|
601
|
+
const resolved = cfg ?? (await loadConfig());
|
|
602
|
+
const json = {
|
|
603
|
+
owner: input.owner.trim(),
|
|
604
|
+
repo: input.repo.trim(),
|
|
605
|
+
pr_number: input.prNumber,
|
|
606
|
+
head_sha: input.headSha.trim(),
|
|
607
|
+
review_id: input.reviewId,
|
|
608
|
+
...(input.scope ? { scope: input.scope } : {}),
|
|
609
|
+
...(input.findingIds && input.findingIds.length > 0 ? { finding_ids: input.findingIds } : {}),
|
|
610
|
+
...(input.reason !== undefined ? { reason: input.reason } : {}),
|
|
611
|
+
...(input.evidenceUrl ? { evidence_url: input.evidenceUrl } : {}),
|
|
612
|
+
...(input.preview ? { preview: true } : {}),
|
|
613
|
+
};
|
|
614
|
+
const { status, body, retryAfterSeconds } = await apiFetch(resolved, "/api/v1/stacks/pr-review/dismiss", {
|
|
615
|
+
method: "POST",
|
|
616
|
+
json,
|
|
617
|
+
signal: opts?.signal,
|
|
618
|
+
});
|
|
619
|
+
if (status === 429) {
|
|
620
|
+
throw new CommandError(rateLimitedMessage(retryAfterSeconds), REVIEW_EXIT.rate_limited, "rate_limited", {
|
|
621
|
+
retryAfterSeconds,
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
if (status === 200)
|
|
625
|
+
return { ok: true, result: body };
|
|
626
|
+
const rejection = (body ?? {});
|
|
627
|
+
if (status === 404 && typeof rejection.error !== "string") {
|
|
628
|
+
throw new CommandError("Finding dismissal is not available on this server yet. Deploy the API update.", REVIEW_EXIT.failed, "not_found");
|
|
629
|
+
}
|
|
630
|
+
if (typeof rejection.error === "string") {
|
|
631
|
+
return {
|
|
632
|
+
ok: false,
|
|
633
|
+
status,
|
|
634
|
+
error: rejection.error,
|
|
635
|
+
message: typeof rejection.message === "string" ? rejection.message : rejection.error,
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
throw new CommandError(`Failed to dismiss findings (HTTP ${status}): ${JSON.stringify(body)}`, REVIEW_EXIT.failed, "review_failed");
|
|
639
|
+
}
|
package/dist/client/index.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
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, enqueueStack, ensureUpperPark, getEnrichedStack, getMe, getPrVortexReview, getReview, getSettings, landNextStack, listJobs, listMergeQueueEntries, listStacks, listThreads, parseRetryAfterSeconds, patchSettings, restackStack, setStackAutoLand, setStackPolicy, stackPolicyBody, type ApiFetchInit, type ApiFetchResult, type JobListItem, type MeResponse, type MergeQueueBounceDetail, type MergeQueueBounceKind, type MergeQueueEnqueuedBy, type MergeQueueEnqueuedVia, type MergeQueueEntryDto, type MergeQueueEntryState, type PrReviewFinding, type PrReviewFindings, type PrVortexReview, type SettingsPatch, type SettingsResponse, type SettingsWritableKey, type StackDto, type StackPolicyPatch, type ThreadDetail, type ThreadListItem, } from "../api.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, landNextStack, listJobs, listMergeQueueEntries, listStacks, listThreads, parseRetryAfterSeconds, patchSettings, restackStack, setStackAutoLand, setStackPolicy, stackPolicyBody, type ApiFetchInit, type ApiFetchResult, type JobListItem, type MeResponse, type MergeQueueBounceDetail, type MergeQueueBounceKind, type MergeQueueEnqueuedBy, type MergeQueueEnqueuedVia, type MergeQueueEntryDto, type MergeQueueEntryState, type PrDismissedFinding, type PrFindingDismissInput, type PrFindingDismissOutcome, type PrFindingDismissResult, type PrReviewFinding, type PrReviewFindings, type PrVortexReview, type SettingsPatch, type SettingsResponse, type SettingsWritableKey, type StackDto, type StackPolicyPatch, type ThreadDetail, type ThreadListItem, } from "../api.js";
|
|
4
4
|
export { CommandError, DetachedError, RATE_LIMITED_HINT, REVIEW_EXIT, isCommandErrorCode, rateLimitedMessage, type CommandErrorCode, type CommandErrorExtras, } from "../errors.js";
|
|
5
5
|
export { REVIEW_POLL_BACKOFF_MS, REVIEW_POLL_DEFAULT_TIMEOUT_MS, REVIEW_POLL_INTERVAL_MS, REVIEW_POLL_MAX_INTERVAL_MS, REVIEW_POLL_MAX_TRANSIENT_RETRIES, REVIEW_POLL_STRETCH_AFTER_MS, REVIEW_POLL_STRETCH_SPAN_MS, REVIEW_THREAD_SLUG_RE, PrReviewPollTimeoutError, ReviewPollTimeoutError, collectReviewInput, formatReviewSubmitError, isTransientReviewPollError, isTransientReviewPollStatus, loadReviewContext, pollPrVortexReview, pollReview, stretchedPollIntervalMs, submitReview, transientRetryWaitMs, type PollReviewOptions, type PollPrVortexReviewOptions, type ReviewInput, type SubmitReviewOptions, } from "../commands/review-client.js";
|
|
6
6
|
export { REVIEW_JOB_ENVELOPE_SCHEMA, toReviewJobEnvelope, type ReviewEnvelopeFallbacks, type ReviewJobEnvelope, type ReviewJobRow, } from "../ui/job-envelope.js";
|
|
7
7
|
export { discoverTrunk } from "../git-stack.js";
|
|
8
8
|
export { BOUNCE_KIND_LABELS, deriveStackAgentsBusy, mergeQueueBounceLabel, type StackAgentsBusy } from "../stack-dto.js";
|
|
9
9
|
export { STACK_WATCH_SCHEMA, STACK_WATCH_DEFAULT_TIMEOUT_MS, STACK_WATCH_BUSY_RECHECK_S, StackWatchError, StackWatchTimeoutError, pollStackWatch, type PollStackWatchOptions, type StackWatchCursor, type StackWatchEnvelope, } from "../stack-watch.js";
|
|
10
|
-
export { stackBlockers, stackBlockersSummary, sameHead, layerAgents, layerAgentsBusy, conflictLiveParent, stackRepair, type StackAgentBusy, type StackBlocker, type StackLandGatePending, type StackLayerAgents, type StackRepairHint, } from "../stack-blockers.js";
|
|
10
|
+
export { stackBlockers, stackBlockersSummary, sameHead, layerAgents, layerAgentsBusy, conflictLiveParent, stackRepair, agentsCannotClear, type StackAgentBusy, type StackBlocker, type StackBusyAgent, type StackHeldBlocker, type StackLandGatePending, type StackLayerAgents, type StackRepairHint, } from "../stack-blockers.js";
|
|
11
11
|
export { STACK_WATCH_NOT_DONE_SENTENCE, stackTerminalReason, stackWatchNextArgs, stackWatchObligation, type StackWatchDoneReason, type StackWatchNext, type StackWatchNextArgs, type StackWatchObligation, type StackWatchObligationInput, type StackWatchReason, } from "../stack-watch-obligation.js";
|
package/dist/client/index.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
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, enqueueStack, ensureUpperPark, getEnrichedStack, getMe, getPrVortexReview, getReview, getSettings, landNextStack, listJobs, listMergeQueueEntries, listStacks, listThreads, parseRetryAfterSeconds, patchSettings, restackStack, setStackAutoLand, setStackPolicy, stackPolicyBody, } from "../api.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, landNextStack, listJobs, listMergeQueueEntries, listStacks, listThreads, parseRetryAfterSeconds, patchSettings, restackStack, setStackAutoLand, setStackPolicy, stackPolicyBody, } from "../api.js";
|
|
4
4
|
export { CommandError, DetachedError, RATE_LIMITED_HINT, REVIEW_EXIT, isCommandErrorCode, rateLimitedMessage, } from "../errors.js";
|
|
5
5
|
export { REVIEW_POLL_BACKOFF_MS, REVIEW_POLL_DEFAULT_TIMEOUT_MS, REVIEW_POLL_INTERVAL_MS, REVIEW_POLL_MAX_INTERVAL_MS, REVIEW_POLL_MAX_TRANSIENT_RETRIES, REVIEW_POLL_STRETCH_AFTER_MS, REVIEW_POLL_STRETCH_SPAN_MS, REVIEW_THREAD_SLUG_RE, PrReviewPollTimeoutError, ReviewPollTimeoutError, collectReviewInput, formatReviewSubmitError, isTransientReviewPollError, isTransientReviewPollStatus, loadReviewContext, pollPrVortexReview, pollReview, stretchedPollIntervalMs, submitReview, transientRetryWaitMs, } from "../commands/review-client.js";
|
|
6
6
|
export { REVIEW_JOB_ENVELOPE_SCHEMA, toReviewJobEnvelope, } from "../ui/job-envelope.js";
|
|
7
7
|
export { discoverTrunk } from "../git-stack.js";
|
|
8
8
|
export { BOUNCE_KIND_LABELS, deriveStackAgentsBusy, mergeQueueBounceLabel } from "../stack-dto.js";
|
|
9
9
|
export { STACK_WATCH_SCHEMA, STACK_WATCH_DEFAULT_TIMEOUT_MS, STACK_WATCH_BUSY_RECHECK_S, StackWatchError, StackWatchTimeoutError, pollStackWatch, } from "../stack-watch.js";
|
|
10
|
-
export { stackBlockers, stackBlockersSummary, sameHead, layerAgents, layerAgentsBusy, conflictLiveParent, stackRepair, } from "../stack-blockers.js";
|
|
10
|
+
export { stackBlockers, stackBlockersSummary, sameHead, layerAgents, layerAgentsBusy, conflictLiveParent, stackRepair, agentsCannotClear, } from "../stack-blockers.js";
|
|
11
11
|
export { STACK_WATCH_NOT_DONE_SENTENCE, stackTerminalReason, stackWatchNextArgs, stackWatchObligation, } from "../stack-watch-obligation.js";
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type PrFindingDismissInput, type PrFindingDismissOutcome, type PrFindingDismissResult } from "../api.js";
|
|
2
|
+
import { type Config } from "../config.js";
|
|
3
|
+
import { type StackWatchObligation } from "../stack-watch-obligation.js";
|
|
4
|
+
export declare const DISMISS_USAGE: string;
|
|
5
|
+
export type ParsedDismissArgs = PrFindingDismissInput & {
|
|
6
|
+
json: boolean;
|
|
7
|
+
};
|
|
8
|
+
export declare function parseDismissArgs(args: string[]): ParsedDismissArgs;
|
|
9
|
+
export declare function dismissWatch(result: PrFindingDismissResult): StackWatchObligation | null;
|
|
10
|
+
export declare function formatDismissResult(result: PrFindingDismissResult, watch: StackWatchObligation | null): string;
|
|
11
|
+
export type DismissDeps = {
|
|
12
|
+
loadConfig?: () => Promise<Config>;
|
|
13
|
+
dismiss?: (input: PrFindingDismissInput, cfg: Config) => Promise<PrFindingDismissOutcome>;
|
|
14
|
+
};
|
|
15
|
+
export declare function cmdDismiss(args: string[], deps?: DismissDeps): Promise<void>;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { dismissPrFindings, } from "../api.js";
|
|
2
|
+
import { loadConfig } from "../config.js";
|
|
3
|
+
import { CommandError, REVIEW_EXIT } from "../errors.js";
|
|
4
|
+
import { stackWatchObligation } from "../stack-watch-obligation.js";
|
|
5
|
+
import { parseAdoptTarget, withWatch } from "./stack.js";
|
|
6
|
+
export const DISMISS_USAGE = [
|
|
7
|
+
"usage: mergestorm dismiss <owner/repo>#<n> --head <sha> --review <id>",
|
|
8
|
+
" (--finding <id> [--finding <id>…] | --all) --reason <text> [--evidence <url>] [--json]",
|
|
9
|
+
" mergestorm dismiss <owner/repo>#<n> --head <sha> --review <id> --preview [--json]",
|
|
10
|
+
].join("\n");
|
|
11
|
+
function usage(message) {
|
|
12
|
+
return new CommandError(message ? `${message}\n${DISMISS_USAGE}` : DISMISS_USAGE, REVIEW_EXIT.usage, "usage");
|
|
13
|
+
}
|
|
14
|
+
export function parseDismissArgs(args) {
|
|
15
|
+
const target = [];
|
|
16
|
+
const findingIds = [];
|
|
17
|
+
let headSha = "";
|
|
18
|
+
let reviewId;
|
|
19
|
+
let reason;
|
|
20
|
+
let evidenceUrl;
|
|
21
|
+
let all = false;
|
|
22
|
+
let preview = false;
|
|
23
|
+
let json = false;
|
|
24
|
+
const value = (flag, raw) => {
|
|
25
|
+
if (raw === undefined || raw.startsWith("--"))
|
|
26
|
+
throw usage(`${flag} needs a value`);
|
|
27
|
+
return raw;
|
|
28
|
+
};
|
|
29
|
+
for (let i = 0; i < args.length; i++) {
|
|
30
|
+
const arg = args[i];
|
|
31
|
+
if (arg === "--head")
|
|
32
|
+
headSha = value(arg, args[++i]).trim().toLowerCase();
|
|
33
|
+
else if (arg === "--review") {
|
|
34
|
+
const raw = value(arg, args[++i]).trim();
|
|
35
|
+
if (!/^[1-9]\d*$/.test(raw) || !Number.isSafeInteger(Number(raw)))
|
|
36
|
+
throw usage("--review needs the numeric GitHub review id");
|
|
37
|
+
reviewId = Number(raw);
|
|
38
|
+
}
|
|
39
|
+
else if (arg === "--finding")
|
|
40
|
+
findingIds.push(...value(arg, args[++i]).split(",").map((id) => id.trim()).filter(Boolean));
|
|
41
|
+
else if (arg === "--all")
|
|
42
|
+
all = true;
|
|
43
|
+
else if (arg === "--reason")
|
|
44
|
+
reason = value(arg, args[++i]);
|
|
45
|
+
else if (arg === "--evidence")
|
|
46
|
+
evidenceUrl = value(arg, args[++i]).trim();
|
|
47
|
+
else if (arg === "--preview")
|
|
48
|
+
preview = true;
|
|
49
|
+
else if (arg === "--json")
|
|
50
|
+
json = true;
|
|
51
|
+
else if (arg.startsWith("--"))
|
|
52
|
+
throw usage(`unknown flag ${arg}`);
|
|
53
|
+
else
|
|
54
|
+
target.push(arg);
|
|
55
|
+
}
|
|
56
|
+
let parsedTarget;
|
|
57
|
+
try {
|
|
58
|
+
parsedTarget = parseAdoptTarget(target);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
throw usage("name the PR as <owner/repo>#<n>");
|
|
62
|
+
}
|
|
63
|
+
if (!/^[0-9a-f]{40}$/.test(headSha))
|
|
64
|
+
throw usage("--head needs the full 40-character PR head SHA");
|
|
65
|
+
if (reviewId === undefined)
|
|
66
|
+
throw usage("--review is required");
|
|
67
|
+
if (!preview) {
|
|
68
|
+
if (all && findingIds.length > 0)
|
|
69
|
+
throw usage("pass --finding or --all, not both");
|
|
70
|
+
if (!all && findingIds.length === 0)
|
|
71
|
+
throw usage("pass --finding <id> for each finding, or --all for the whole review");
|
|
72
|
+
if (!reason?.trim())
|
|
73
|
+
throw usage("--reason is required: say why the finding is wrong or not actionable");
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
...parsedTarget,
|
|
77
|
+
headSha,
|
|
78
|
+
reviewId,
|
|
79
|
+
findingIds: all ? [] : findingIds,
|
|
80
|
+
scope: all ? "review" : "findings",
|
|
81
|
+
...(reason !== undefined ? { reason } : {}),
|
|
82
|
+
...(evidenceUrl ? { evidenceUrl } : {}),
|
|
83
|
+
preview,
|
|
84
|
+
json,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
export function dismissWatch(result) {
|
|
88
|
+
if (!result.stack_id)
|
|
89
|
+
return null;
|
|
90
|
+
return stackWatchObligation({ stackId: result.stack_id, terminal: null, unread: true, freshCursor: true });
|
|
91
|
+
}
|
|
92
|
+
function findingLine(finding) {
|
|
93
|
+
const where = finding.path ? `${finding.path}${finding.line ? `:${finding.line}` : ""}` : "review body";
|
|
94
|
+
return ` ${finding.finding_id} ${where} ${finding.title}`;
|
|
95
|
+
}
|
|
96
|
+
export function formatDismissResult(result, watch) {
|
|
97
|
+
const lines = [];
|
|
98
|
+
const pr = `${result.owner}/${result.repo}#${result.pr_number}`;
|
|
99
|
+
const kind = result.review_kind === "seam" ? "seam review" : "Vortex review";
|
|
100
|
+
if (result.status === "preview") {
|
|
101
|
+
lines.push(`${pr} ${kind} ${result.review_id} at ${result.head_sha.slice(0, 7)}: ${result.remaining.length} open finding(s)`);
|
|
102
|
+
lines.push(...result.remaining.map(findingLine));
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
lines.push(`${pr} ${kind} ${result.review_id} at ${result.head_sha.slice(0, 7)}`);
|
|
106
|
+
if (result.dismissed.length)
|
|
107
|
+
lines.push("Dismissed:", ...result.dismissed.map(findingLine));
|
|
108
|
+
if (result.already_dismissed.length) {
|
|
109
|
+
lines.push("Already dismissed:", ...result.already_dismissed.map((f) => `${findingLine(f)} (by @${f.actor_login})`));
|
|
110
|
+
}
|
|
111
|
+
if (result.remaining.length)
|
|
112
|
+
lines.push("Still open:", ...result.remaining.map(findingLine));
|
|
113
|
+
}
|
|
114
|
+
const seam = result.gate.seam;
|
|
115
|
+
if (seam) {
|
|
116
|
+
lines.push(seam.cleared
|
|
117
|
+
? "Seam gate: cleared (approved at this head). CI, other reviews and Auto land policy still apply."
|
|
118
|
+
: seam.blocking
|
|
119
|
+
? `Seam gate: still blocking (seam_state=${seam.state}).`
|
|
120
|
+
: `Seam gate: not blocking (seam_state=${seam.state}).`);
|
|
121
|
+
}
|
|
122
|
+
if (watch?.next)
|
|
123
|
+
lines.push(`Next: ${watch.next.command}`);
|
|
124
|
+
return lines.join("\n");
|
|
125
|
+
}
|
|
126
|
+
export async function cmdDismiss(args, deps = {}) {
|
|
127
|
+
const parsed = parseDismissArgs(args);
|
|
128
|
+
const cfg = await (deps.loadConfig ?? loadConfig)();
|
|
129
|
+
const { json, ...input } = parsed;
|
|
130
|
+
const outcome = await (deps.dismiss ?? ((i, c) => dismissPrFindings(i, c)))(input, cfg);
|
|
131
|
+
if (!outcome.ok) {
|
|
132
|
+
if (json) {
|
|
133
|
+
console.log(JSON.stringify({ error: { code: outcome.error, message: outcome.message } }, null, 2));
|
|
134
|
+
process.exitCode = REVIEW_EXIT.failed;
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
throw new CommandError(`Dismissal refused (${outcome.error}): ${outcome.message}`, REVIEW_EXIT.failed, "review_failed");
|
|
138
|
+
}
|
|
139
|
+
const watch = dismissWatch(outcome.result);
|
|
140
|
+
if (json) {
|
|
141
|
+
console.log(JSON.stringify(withWatch(outcome.result, watch), null, 2));
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
console.log(formatDismissResult(outcome.result, watch));
|
|
145
|
+
}
|
|
@@ -3,6 +3,7 @@ import { CommandError } from "../errors.js";
|
|
|
3
3
|
import { cmdBranches, cmdChain } from "./branches.js";
|
|
4
4
|
import { canBrowse, openTabsBrowser } from "./browse.js";
|
|
5
5
|
import { cmdCredits } from "./credits.js";
|
|
6
|
+
import { cmdDismiss } from "./dismiss.js";
|
|
6
7
|
import { cmdJobs, cmdThread } from "./jobs.js";
|
|
7
8
|
import { cmdLogin } from "./login.js";
|
|
8
9
|
import { cmdLogout } from "./logout.js";
|
|
@@ -79,6 +80,21 @@ export const COMMAND_REGISTRY = [
|
|
|
79
80
|
});
|
|
80
81
|
},
|
|
81
82
|
},
|
|
83
|
+
{
|
|
84
|
+
name: "dismiss",
|
|
85
|
+
summary: "Dismiss Vortex findings you verified are wrong (audited)",
|
|
86
|
+
usage: [
|
|
87
|
+
"mergestorm dismiss <owner/repo>#<n> --head <sha> --review <id> --preview",
|
|
88
|
+
" Lists the review's findings (ids) and the seam gate; writes nothing",
|
|
89
|
+
"mergestorm dismiss <owner/repo>#<n> --head <sha> --review <id> --finding <id> --reason <text>",
|
|
90
|
+
" [--finding <id>]… | --all (every finding of that review) [--evidence <url>] [--json]",
|
|
91
|
+
" Needs write access on GitHub; refuses a moved head, another review or an unknown id",
|
|
92
|
+
" Clears the seam gate only when every finding of that seam review is dismissed",
|
|
93
|
+
],
|
|
94
|
+
async run(args) {
|
|
95
|
+
await cmdDismiss(args);
|
|
96
|
+
},
|
|
97
|
+
},
|
|
82
98
|
{
|
|
83
99
|
name: "status",
|
|
84
100
|
summary: "Job panel (id) or Status / Usage / Jobs tabs",
|
package/dist/commands/stack.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type StackRepairHint } from "../stack-blockers.js";
|
|
1
|
+
import { type StackHeldBlocker, type StackRepairHint } from "../stack-blockers.js";
|
|
2
2
|
import { type StackWatchObligation } from "../stack-watch-obligation.js";
|
|
3
3
|
import { adoptStack, ensureUpperPark, findStackPull, getEnrichedStack, listStacks, openStackPull, setStackPolicy, type StackDto, type StackPolicyPatch } from "../api.js";
|
|
4
4
|
import { loadConfig } from "../config.js";
|
|
@@ -9,6 +9,7 @@ import { buildPrBodyFromCommit } from "../pr-body.js";
|
|
|
9
9
|
import { gitTopLevel } from "../repo-identity.js";
|
|
10
10
|
import { loadStackMeta, saveStackMeta } from "../stack-meta.js";
|
|
11
11
|
import { pollStackWatch } from "../stack-watch.js";
|
|
12
|
+
export declare function stackHeldLine(held: StackHeldBlocker | null | undefined): string[];
|
|
12
13
|
export declare function stackRepairLine(repair: StackRepairHint | null | undefined): string[];
|
|
13
14
|
export declare function stackWatchTextLines(watch: StackWatchObligation | null | undefined): string[];
|
|
14
15
|
export declare function openStackWatch(stackId: string, opts?: {
|
package/dist/commands/stack.js
CHANGED
|
@@ -16,6 +16,12 @@ import { runLineTabsBrowser } from "../ui/line-tabs.js";
|
|
|
16
16
|
import { present } from "../ui/present.js";
|
|
17
17
|
import { canBrowse } from "./browse.js";
|
|
18
18
|
import { openHelpBrowser } from "./help.js";
|
|
19
|
+
export function stackHeldLine(held) {
|
|
20
|
+
if (!held)
|
|
21
|
+
return [];
|
|
22
|
+
const agents = held.waitingOn.map((agent) => agent === "cyclone" ? "Cyclone" : "Vortex").join(" and ");
|
|
23
|
+
return [`Held: #${held.prNumber} ${held.blocker}. Plan the fix; act once ${agents} ${held.waitingOn.length > 1 ? "are" : "is"} idle and the watch returns attention.`];
|
|
24
|
+
}
|
|
19
25
|
export function stackRepairLine(repair) {
|
|
20
26
|
if (!repair)
|
|
21
27
|
return [];
|
|
@@ -895,7 +901,7 @@ export async function cmdStackWait(args, deps = {}) {
|
|
|
895
901
|
...(retryAfterSeconds !== undefined ? { retry_after_seconds: retryAfterSeconds } : {}),
|
|
896
902
|
}, null, 2)
|
|
897
903
|
: [
|
|
898
|
-
`Stack ${envelope.stackId} · ${envelope.status}${stackBlockersSummary(envelope.blocker && envelope.prNumber != null ? { prNumber: envelope.prNumber, blocker: envelope.blocker } : null, envelope.issues, envelope.busy, envelope.landGatePending ?? null)}${envelope.assessment === "unavailable" ? " · assessment unavailable" : ""}`,
|
|
904
|
+
`Stack ${envelope.stackId} · ${envelope.status}${stackBlockersSummary(envelope.blocker && envelope.prNumber != null ? { prNumber: envelope.prNumber, blocker: envelope.blocker } : null, envelope.issues, envelope.busy, envelope.landGatePending ?? null, envelope.actAfter ?? null)}${envelope.assessment === "unavailable" ? " · assessment unavailable" : ""}`,
|
|
899
905
|
...stackRepairLine(envelope.repair),
|
|
900
906
|
...stackWatchTextLines(envelope.watch),
|
|
901
907
|
].join("\n"));
|
|
@@ -939,7 +945,7 @@ export async function cmdStackStatus(args, deps = {}) {
|
|
|
939
945
|
console.log(watch.message);
|
|
940
946
|
throw new CommandError(`Stack not found or not owned by the current user: ${stackId}`, 1, "not_found");
|
|
941
947
|
}
|
|
942
|
-
const { attention, currentCandidate, repair } = stackBlockers(stack);
|
|
948
|
+
const { attention, held, currentCandidate, repair } = stackBlockers(stack);
|
|
943
949
|
const watch = stackWatchObligation({
|
|
944
950
|
stackId: stack.id,
|
|
945
951
|
terminal: stackTerminalReason(stack),
|
|
@@ -948,7 +954,7 @@ export async function cmdStackStatus(args, deps = {}) {
|
|
|
948
954
|
freshCursor: true,
|
|
949
955
|
});
|
|
950
956
|
if (asJson) {
|
|
951
|
-
console.log(JSON.stringify({ ...stack, repair, watch }, null, 2));
|
|
957
|
+
console.log(JSON.stringify({ ...stack, held, repair, watch }, null, 2));
|
|
952
958
|
return;
|
|
953
959
|
}
|
|
954
960
|
const lines = formatStackHuman(stack, false);
|
|
@@ -961,7 +967,7 @@ export async function cmdStackStatus(args, deps = {}) {
|
|
|
961
967
|
(layer.cycloneStatus ? ` Cyclone: ${layer.cycloneStatus}` : "") +
|
|
962
968
|
(layer.tempestStatus ? ` Tempest: ${layer.tempestStatus}` : ""));
|
|
963
969
|
}
|
|
964
|
-
lines.push(...[...stackRepairLine(repair), ...stackWatchTextLines(watch)].map((line) => ` ${line}`));
|
|
970
|
+
lines.push(...[...stackHeldLine(held), ...stackRepairLine(repair), ...stackWatchTextLines(watch)].map((line) => ` ${line}`));
|
|
965
971
|
await present("Stack status", lines);
|
|
966
972
|
}
|
|
967
973
|
/** Human summary lines for a `stack set` write, one per flag. */
|
package/dist/stack-blockers.d.ts
CHANGED
|
@@ -12,6 +12,11 @@ export type StackAgentBusy = {
|
|
|
12
12
|
agent: "vortex" | "cyclone";
|
|
13
13
|
blocker: string;
|
|
14
14
|
};
|
|
15
|
+
export type StackBusyAgent = StackAgentBusy["agent"];
|
|
16
|
+
export type StackHeldBlocker = StackBlocker & {
|
|
17
|
+
actAfter: "agents_idle";
|
|
18
|
+
waitingOn: StackBusyAgent[];
|
|
19
|
+
};
|
|
15
20
|
export type StackLayerAgents = {
|
|
16
21
|
prNumber: number;
|
|
17
22
|
headSha: string | null;
|
|
@@ -54,14 +59,30 @@ export declare function layerAgents(layer: StackLayerDto): StackLayerAgents;
|
|
|
54
59
|
export declare function isMgParkBase(branch: string | null | undefined): boolean;
|
|
55
60
|
/** GitHub DIRTY vs an mg-park-* freeze is not a merge conflict, verified or not. */
|
|
56
61
|
export declare function boundDirty(layer: StackLayerDto): boolean;
|
|
62
|
+
export declare function mergeabilityPending(layer: StackLayerDto): boolean;
|
|
57
63
|
export declare function sameHead(a: string | null | undefined, b: string | null | undefined): boolean;
|
|
58
64
|
export declare function currentLayer(stack: StackDto): StackLayerDto | undefined;
|
|
59
65
|
export declare function conflictLiveParent(stack: StackDto, layer: StackLayerDto): string | null;
|
|
60
66
|
export declare function conflictRepairSteps(branch: string, liveParent: string | null): string;
|
|
67
|
+
export declare function agentsCannotClear(blocker: string): boolean;
|
|
61
68
|
export declare function stackRepair(stack: StackDto, attention: StackBlocker | null, layer: StackLayerDto | undefined, bounce: MergeQueueEntryDto | undefined): StackRepairHint | null;
|
|
62
69
|
/** Pair-gate attention plus live hard blocks on every other open layer. */
|
|
63
70
|
export declare function stackBlockers(stack: StackDto, entries?: MergeQueueEntryDto[], cursor?: Pick<StackWatchCursor, "bounceId" | "afterFinishedAt">): {
|
|
71
|
+
attention: null;
|
|
72
|
+
held: StackHeldBlocker;
|
|
73
|
+
issues: StackBlocker[];
|
|
74
|
+
currentCandidate: {
|
|
75
|
+
prNumber: number;
|
|
76
|
+
headSha: string | null;
|
|
77
|
+
} | null;
|
|
78
|
+
bounce: undefined;
|
|
79
|
+
busy: StackAgentBusy[];
|
|
80
|
+
agents: StackLayerAgents | null;
|
|
81
|
+
repair: StackRepairHint | null;
|
|
82
|
+
landGatePending: StackLandGatePending | null;
|
|
83
|
+
} | {
|
|
64
84
|
attention: StackBlocker | null;
|
|
85
|
+
held: null;
|
|
65
86
|
issues: StackBlocker[];
|
|
66
87
|
currentCandidate: {
|
|
67
88
|
prNumber: number;
|
|
@@ -74,4 +95,4 @@ export declare function stackBlockers(stack: StackDto, entries?: MergeQueueEntry
|
|
|
74
95
|
landGatePending: StackLandGatePending | null;
|
|
75
96
|
};
|
|
76
97
|
/** Compact contract text shared by status and wait summaries. */
|
|
77
|
-
export declare function stackBlockersSummary(attention: Pick<StackBlocker, "prNumber" | "blocker"> | null, issues: StackBlocker[], busy?: readonly StackAgentBusy[], landGatePending?: Pick<StackLandGatePending, "prNumber" | "reason"> | null): string;
|
|
98
|
+
export declare function stackBlockersSummary(attention: Pick<StackBlocker, "prNumber" | "blocker"> | null, issues: StackBlocker[], busy?: readonly StackAgentBusy[], landGatePending?: Pick<StackLandGatePending, "prNumber" | "reason"> | null, actAfter?: StackHeldBlocker["actAfter"] | null): string;
|
package/dist/stack-blockers.js
CHANGED
|
@@ -30,6 +30,12 @@ export function boundDirty(layer) {
|
|
|
30
30
|
return (!layer.headSha || layer.mergeableHeadSha === layer.headSha) &&
|
|
31
31
|
(layer.mergeable === false || layer.mergeableState?.toLowerCase() === "dirty");
|
|
32
32
|
}
|
|
33
|
+
export function mergeabilityPending(layer) {
|
|
34
|
+
if (isMgParkBase(layer.parentBranch))
|
|
35
|
+
return false;
|
|
36
|
+
return layer.mergeable == null && !!layer.headSha && layer.mergeableHeadSha === layer.headSha &&
|
|
37
|
+
layer.mergeableState?.toLowerCase() === "pending";
|
|
38
|
+
}
|
|
33
39
|
export function sameHead(a, b) {
|
|
34
40
|
const left = a?.trim().toLowerCase();
|
|
35
41
|
const right = b?.trim().toLowerCase();
|
|
@@ -92,6 +98,9 @@ function layerAttention(stack, layer, queue, cursor) {
|
|
|
92
98
|
const name = layer.checks?.failingName?.trim();
|
|
93
99
|
return hardBlock(`CI failed${name ? ` — ${name}` : ""}`);
|
|
94
100
|
}
|
|
101
|
+
if (layer.agentRuns?.some((run) => run.agent === "cyclone" && run.status === "failed" &&
|
|
102
|
+
sameHead(run.sha, layer.headSha)))
|
|
103
|
+
return hardBlock("Cyclone failed");
|
|
95
104
|
const tempest = layer.agentRuns?.find((run) => run.agent === "tempest" &&
|
|
96
105
|
["findings", "failed"].includes(run.status) && sameHead(run.sha, layer.headSha));
|
|
97
106
|
if (tempest)
|
|
@@ -156,15 +165,22 @@ export function conflictRepairSteps(branch, liveParent) {
|
|
|
156
165
|
}
|
|
157
166
|
return `Merge ${liveParent} into ${branch}: git fetch origin, check out ${branch} at its live remote head, git merge origin/${liveParent}, resolve the conflicts, run the tests, then an ordinary git push (no force). Do not rebase onto trunk, main, or mg-park-*, and do not retarget the PR base.`;
|
|
158
167
|
}
|
|
168
|
+
function landPrLiveParent(stack, layer) {
|
|
169
|
+
const base = stack.unit?.landTarget?.trim() || layer.parentBranch?.trim();
|
|
170
|
+
return base && !isMgParkBase(base) && base !== layer.branch ? base : null;
|
|
171
|
+
}
|
|
172
|
+
export function agentsCannotClear(blocker) {
|
|
173
|
+
return blocker === "Conflict" || blocker === "Restack failed" || blocker === "Draft PR" ||
|
|
174
|
+
blocker.startsWith("Merge conflicts");
|
|
175
|
+
}
|
|
159
176
|
export function stackRepair(stack, attention, layer, bounce) {
|
|
160
177
|
if (!attention || !layer || attention.prNumber !== layer.prNumber)
|
|
161
178
|
return null;
|
|
162
|
-
|
|
163
|
-
return null;
|
|
179
|
+
const landPr = !!stack.unit && layer.prNumber === stack.unit.landPrNumber && layer.branch === stack.unit.branch;
|
|
164
180
|
const restackConflict = attention.blocker === "Conflict" || attention.bounceKind === "restack_conflict";
|
|
165
181
|
const mergeConflict = attention.blocker.startsWith("Merge conflicts");
|
|
166
182
|
if (restackConflict || mergeConflict) {
|
|
167
|
-
const liveParent = conflictLiveParent(stack, layer);
|
|
183
|
+
const liveParent = landPr ? landPrLiveParent(stack, layer) : conflictLiveParent(stack, layer);
|
|
168
184
|
return {
|
|
169
185
|
kind: restackConflict ? "restack_conflict" : "merge_conflict",
|
|
170
186
|
prNumber: layer.prNumber,
|
|
@@ -207,8 +223,13 @@ export function stackBlockers(stack, entries = [], cursor = {}) {
|
|
|
207
223
|
for (const layer of open) {
|
|
208
224
|
const selected = layer.prNumber === candidate?.prNumber;
|
|
209
225
|
const result = layerAttention(stack, layer, selected ? queue : [], cursor);
|
|
210
|
-
if (!result.blocker)
|
|
226
|
+
if (!result.blocker) {
|
|
227
|
+
if (mergeabilityPending(layer) && (selected || !candidate || layer.position > candidate.position)) {
|
|
228
|
+
issues.push({ prNumber: layer.prNumber, headSha: layer.headSha ?? null,
|
|
229
|
+
blocker: `Merge state unknown${layer.parentBranch ? ` vs ${layer.parentBranch}` : ""}`, bounceKind: null });
|
|
230
|
+
}
|
|
211
231
|
continue;
|
|
232
|
+
}
|
|
212
233
|
const issue = { prNumber: layer.prNumber, headSha: layer.headSha ?? null,
|
|
213
234
|
blocker: result.blocker, bounceKind: result.bounceKind };
|
|
214
235
|
if (selected || (!attention && layer === child)) {
|
|
@@ -235,19 +256,28 @@ export function stackBlockers(stack, entries = [], cursor = {}) {
|
|
|
235
256
|
const landGatePending = landLayer && landing && landGateIsPending(landing)
|
|
236
257
|
? { prNumber: landLayer.prNumber, headSha: landLayer.headSha ?? null, reason: landing }
|
|
237
258
|
: null;
|
|
259
|
+
if (busy.length && attention && agentsCannotClear(attention.blocker)) {
|
|
260
|
+
const held = { ...attention, actAfter: "agents_idle", waitingOn: busy.map((entry) => entry.agent) };
|
|
261
|
+
return { attention: null, held, issues: [attention, ...issues], currentCandidate, bounce: undefined, busy, agents,
|
|
262
|
+
repair: stackRepair(stack, attention, attentionLayer, undefined), landGatePending };
|
|
263
|
+
}
|
|
238
264
|
if (busy.length)
|
|
239
|
-
return { attention: null, issues, currentCandidate, bounce: undefined, busy, agents, repair: null, landGatePending };
|
|
240
|
-
return { attention, issues, currentCandidate, bounce, busy, agents, repair: stackRepair(stack, attention, attentionLayer, bounce), landGatePending };
|
|
265
|
+
return { attention: null, held: null, issues, currentCandidate, bounce: undefined, busy, agents, repair: null, landGatePending };
|
|
266
|
+
return { attention, held: null, issues, currentCandidate, bounce, busy, agents, repair: stackRepair(stack, attention, attentionLayer, bounce), landGatePending };
|
|
241
267
|
}
|
|
242
268
|
/** Compact contract text shared by status and wait summaries. */
|
|
243
|
-
export function stackBlockersSummary(attention, issues, busy = [], landGatePending = null) {
|
|
244
|
-
const
|
|
245
|
-
const
|
|
246
|
-
? ` ·
|
|
269
|
+
export function stackBlockersSummary(attention, issues, busy = [], landGatePending = null, actAfter = null) {
|
|
270
|
+
const agentWork = (entry) => entry.agent === "cyclone" ? "Cyclone patches" : "Vortex reviews";
|
|
271
|
+
const blocked = attention && actAfter && busy.length
|
|
272
|
+
? ` · blocked after the agents: #${attention.prNumber} ${attention.blocker} (plan the fix; act once ${busy.map(agentWork).join(" and ")} finish and the watch returns attention)`
|
|
273
|
+
: attention ? ` · blocked: #${attention.prNumber} ${attention.blocker}` : "";
|
|
274
|
+
const working = busy.length && !actAfter
|
|
275
|
+
? ` · held: #${busy[0].prNumber} ${busy[0].blocker} while ${busy.map(agentWork).join(" and ")}`
|
|
247
276
|
: "";
|
|
248
|
-
const
|
|
277
|
+
const listed = actAfter && attention ? issues.filter((issue) => issue.prNumber !== attention.prNumber || issue.blocker !== attention.blocker) : issues;
|
|
278
|
+
const labels = listed.slice(0, 3).map(issue => `#${issue.prNumber} ${issue.blocker}`).join("; ");
|
|
249
279
|
const gate = landGatePending && !attention
|
|
250
280
|
? ` · land gate: #${landGatePending.prNumber} ${landGatePending.reason} (nothing to fix; wait)`
|
|
251
281
|
: "";
|
|
252
|
-
return blocked + working + gate + (
|
|
282
|
+
return blocked + working + gate + (listed.length ? ` · issues: ${labels}${listed.length > 3 ? `; +${listed.length - 3} more` : ""}` : "");
|
|
253
283
|
}
|
package/dist/stack-dto.d.ts
CHANGED
|
@@ -158,6 +158,14 @@ export type StackLayerConflictResolution = {
|
|
|
158
158
|
*/
|
|
159
159
|
dispatchId: string | null;
|
|
160
160
|
};
|
|
161
|
+
export type StackLayerCiFixStatus = "verified" | "unverified" | "no_changes" | "failed";
|
|
162
|
+
export type StackLayerCiFix = {
|
|
163
|
+
status: StackLayerCiFixStatus;
|
|
164
|
+
failingCheck: string | null;
|
|
165
|
+
resultSha: string | null;
|
|
166
|
+
reason: string | null;
|
|
167
|
+
at: string;
|
|
168
|
+
};
|
|
161
169
|
export type StackLayerDto = {
|
|
162
170
|
branch: string;
|
|
163
171
|
parentBranch: string | null;
|
|
@@ -232,6 +240,7 @@ export type StackLayerDto = {
|
|
|
232
240
|
* layers and when the specialist never ran.
|
|
233
241
|
*/
|
|
234
242
|
conflictResolution?: StackLayerConflictResolution | null;
|
|
243
|
+
ciFix?: StackLayerCiFix | null;
|
|
235
244
|
};
|
|
236
245
|
export type StackUnitMemberDto = {
|
|
237
246
|
prNumber: number;
|
|
@@ -374,6 +383,7 @@ export type MergeQueueBounceDetail = {
|
|
|
374
383
|
conflictDetail?: string;
|
|
375
384
|
conflictPaths?: string[];
|
|
376
385
|
message?: string;
|
|
386
|
+
warning?: string;
|
|
377
387
|
};
|
|
378
388
|
export type MergeQueueEntryDto = {
|
|
379
389
|
id: string;
|
package/dist/stack-watch.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type StackAgentBusy, type StackBlocker, type StackLayerAgents, type StackLandGatePending, type StackRepairHint } from "./stack-blockers.js";
|
|
1
|
+
import { type StackAgentBusy, type StackBlocker, type StackBusyAgent, type StackHeldBlocker, type StackLayerAgents, type StackLandGatePending, type StackRepairHint } from "./stack-blockers.js";
|
|
2
2
|
import { type StackWatchObligation } from "./stack-watch-obligation.js";
|
|
3
3
|
import { apiFetch } from "./api.js";
|
|
4
4
|
import type { Config } from "./config.js";
|
|
@@ -28,6 +28,8 @@ export type StackWatchEnvelope = {
|
|
|
28
28
|
} | null;
|
|
29
29
|
assessment: "available" | "unavailable";
|
|
30
30
|
busy: StackAgentBusy[];
|
|
31
|
+
actAfter: StackHeldBlocker["actAfter"] | null;
|
|
32
|
+
waitingOn: StackBusyAgent[];
|
|
31
33
|
agents: StackLayerAgents | null;
|
|
32
34
|
repair: StackRepairHint | null;
|
|
33
35
|
landGatePending: StackLandGatePending | null;
|
package/dist/stack-watch.js
CHANGED
|
@@ -48,7 +48,7 @@ export async function pollStackWatch(cfg, stackId, opts = {}) {
|
|
|
48
48
|
let lastEnvelope = {
|
|
49
49
|
schema: STACK_WATCH_SCHEMA, status: "failed", stackId: id, blocker: null,
|
|
50
50
|
bounceKind: null, prNumber: null, headSha: null, cursor,
|
|
51
|
-
issues: [], currentCandidate: null, assessment: "unavailable", busy: [], agents: null, repair: null,
|
|
51
|
+
issues: [], currentCandidate: null, assessment: "unavailable", busy: [], actAfter: null, waitingOn: [], agents: null, repair: null,
|
|
52
52
|
landGatePending: null,
|
|
53
53
|
};
|
|
54
54
|
let terminal = null;
|
|
@@ -119,7 +119,7 @@ export async function pollStackWatch(cfg, stackId, opts = {}) {
|
|
|
119
119
|
: undefined;
|
|
120
120
|
if (snapshot && Array.isArray(snapshot.stacks) && !stack) {
|
|
121
121
|
terminal = "not_found";
|
|
122
|
-
throw new StackWatchError("Stack not found or not owned by the current user", seal({ ...lastEnvelope, status: "failed", assessment: "unavailable", blocker: null, issues: [], busy: [],
|
|
122
|
+
throw new StackWatchError("Stack not found or not owned by the current user", seal({ ...lastEnvelope, status: "failed", assessment: "unavailable", blocker: null, issues: [], busy: [], actAfter: null, waitingOn: [],
|
|
123
123
|
prNumber: null, headSha: null, repair: null, agents: null, currentCandidate: null, landGatePending: null }));
|
|
124
124
|
}
|
|
125
125
|
if (!stack || !Array.isArray(stack.layers)) {
|
|
@@ -140,15 +140,17 @@ export async function pollStackWatch(cfg, stackId, opts = {}) {
|
|
|
140
140
|
};
|
|
141
141
|
};
|
|
142
142
|
const evaluate = (stack, entries) => {
|
|
143
|
-
const { attention, issues, currentCandidate, bounce, busy, agents, repair, landGatePending } = stackBlockers(stack, entries, cursor);
|
|
143
|
+
const { attention, held: named, issues, currentCandidate, bounce, busy, agents, repair, landGatePending } = stackBlockers(stack, entries, cursor);
|
|
144
144
|
if (bounce)
|
|
145
145
|
cursor = Object.freeze({ ...cursor, bounceId: bounce.id,
|
|
146
146
|
...(bounce.finishedAt !== undefined ? { afterFinishedAt: bounce.finishedAt } : {}) });
|
|
147
147
|
const held = busy[0];
|
|
148
|
+
const shown = attention ?? named;
|
|
148
149
|
lastEnvelope = { ...lastEnvelope, cursor, issues, currentCandidate, assessment: "available", busy, agents, repair, landGatePending,
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
150
|
+
actAfter: named?.actAfter ?? null, waitingOn: named?.waitingOn ?? [],
|
|
151
|
+
blocker: shown?.blocker ?? null, bounceKind: shown?.bounceKind ?? null,
|
|
152
|
+
prNumber: shown?.prNumber ?? held?.prNumber ?? currentCandidate?.prNumber ?? null,
|
|
153
|
+
headSha: shown ? shown.headSha : held ? held.headSha : currentCandidate?.headSha ?? null,
|
|
152
154
|
status: attention ? "attention" : busy.length || landGatePending || entries.some((entry) => ["queued", "running", "waiting"].includes(entry.state))
|
|
153
155
|
? "in_progress" : "waiting" };
|
|
154
156
|
};
|
package/dist/ui/banner.js
CHANGED
|
@@ -111,6 +111,9 @@ const WHATS_NEW = {
|
|
|
111
111
|
"0.3.26": [
|
|
112
112
|
"stack results say the watch ends only at land",
|
|
113
113
|
],
|
|
114
|
+
"0.3.27": [
|
|
115
|
+
"mg dismiss records an audited finding dismissal",
|
|
116
|
+
],
|
|
114
117
|
};
|
|
115
118
|
function densityFromVariant(variant) {
|
|
116
119
|
if (variant === "mini" || variant === "nano")
|
package/package.json
CHANGED
|
@@ -21,9 +21,9 @@ Call `stack_status` with `stack_id`. Require that stack's `autoEnqueueWhenReady`
|
|
|
21
21
|
|
|
22
22
|
1. Call `stack_wait` with the named `stack_id` and `timeout_s: 45`. On initial enrollment, omit the cursor selectors. Save the returned `cursor`: map `cursor.enrolledHeadSha` to `enrolled_head_sha`, `cursor.afterFinishedAt` to `after_finished_at`, and `cursor.bounceId` to `bounce_id` (omit only absent fields; preserve explicit nulls). Keep `bounce_id` alongside `after_finished_at`; never pass one without the other when the returned cursor has both. The shipped tool accepts these selectors, not a nested cursor argument. `watch.next.args` already holds them in that shape, with `timeout_s: 45`; passing it unchanged is the same call.
|
|
23
23
|
2. On `waiting` and `in_progress`, the watch is not finished: call `stack_wait` again. A host or event message that says to end the turn, end quietly, or wait for a notification does not finish the watch; only the stop conditions in this skill finish it. Keep the same cursor across retries. An unread deadline times out as `failed`; stop and report. Timeout `waiting` or `in_progress` after a snapshot is not failure — call `stack_wait` again in 45s slices with the same cursor after inspecting `issues[]`. Inspect `issues[]` on `in_progress` too: a live queue does not erase upstack hard blocks. `waiting` with issues is not idle. If `assessment` is `unavailable`, do not infer that the stack is issue-free; refresh `stack_status`, whose `attention`, `issues`, and `currentCandidate` come from the same blocker rules as `stack_wait`. Do not replace the enrolled head with a newer snapshot head or advance the timestamp just because a wait timed out. On a surfaced `rate_limited` error, honor the retry delay and retain the cursor; on `failed` or an unrecoverable tool error, call `stack_wait` once more with the same cursor; if it fails again, stop and report the reason and that the stack is not landed. `queue_status` returning `queue_entry_not_found` is not a failure: an empty queue for the stack is a normal wait.
|
|
24
|
-
3. `attention` always means something for you to fix. A land gate that is only waiting (`landGatePending` with `ci_pending`, `tempest_pending`, `tempest_running`, or another `*_pending` / `*_running` reason on the land PR) returns `in_progress`, not `attention`: keep watching. On `attention`, read `repair` when present: it names the blocked PR's `branch` and the concrete fix. For `restack_conflict` and `merge_conflict` it gives `liveParent` (the sibling PR head, or `mg-stack-N` once the lower layers are merged or promoted, never `mg-park-*`) and the conflicted `files`; a null `liveParent` means stop and tell the human. For `ci_failure` it names the failing check. `repair` does not replace the checks below. On `attention`, fix the named PR: `prNumber` and `headSha` identify the blocked PR, while `currentCandidate` identifies the promote candidate. The gate covers the bottom and its direct child only when the child has current-head DIRTY on the live bottom branch. A GitHub DIRTY whose `parentBranch` starts with `mg-park-` is not that gate. Refresh `stack_status`; use `queue_status` with the same `stack_id` when bounce details are needed. Verify the blocker still applies to the current PR head. Never treat `verifyHeadSha` as the current head: it is a queue verification SHA and can be stale or synthetic. Compare the live PR branch head, the current stack layer head, and the checkout before editing. Read the live PR head with `git ls-remote origin refs/heads/<branch>` or `gh api repos/OWNER/REPO/pulls/N --jq .head.sha`. If you need CI logs, use `gh run list --branch <branch>`, `gh run view <run-id> --log-failed`, or `gh api repos/OWNER/REPO/commits/SHA/check-runs`. Read GitHub only through git and these REST calls; do not use other `gh pr` subcommands. Restack `clean` does not mean mergeable or CI-green. Attention is a diagnosis, not permission to mutate stack or queue policy. A bounce is the reason to look, not the moment to commit. `stack_wait` and `stack_status` hold the blocked PR's attention themselves while Vortex or Cyclone is still working on that PR, whatever the blocker (a red CI failure, a merge conflict, a bounce, Draft, a failed restack or review): they return `in_progress` instead of `attention`, with `busy[]` naming the PR (`prNumber`, `headSha`), the `blocker`, and the `agent` (`vortex` or `cyclone`), and `agents` carrying that PR's `vortexStatus`, `cycloneStatus`, `vortexReview`, and `busy` flags. `in_progress` with a nonempty `busy[]` means do not patch yet: keep calling `stack_wait` with the same cursor until that work has finished and the watch returns `attention`, then make the follow-up commit. You do not need an extra `stack_status` refresh each slice for this check. A nonempty `busy[]` reports still-working agents on that PR's current head. Vortex holds when `vortexReview` is `reviewing` at the head, an unfinished vortex `agentRuns` row at the head is `reviewing`, or an unfinished vortex run at the head is `queued` and started within the last 15 minutes (with no `agentRuns` or `vortexReview` on the layer, `vortexStatus` `reviewing` holds). Cyclone holds when `cycloneStatus` is `patching`, an unfinished cyclone `agentRuns` row is `patching`, or the server sees a live Cyclone patch lease or an open Cyclone check run on the head. `seam_pending` alone, and a `queued` run older than 15 minutes, are not treated as work in progress. `all_clear`, `findings`, `skipped`, `failed`, `throttled`, `awaiting_fix`, and a null status are finished. A queued Vortex run holds for at most 15 minutes; reviewing and patching hold until the run finishes or its lease or check run is cleared.
|
|
25
|
-
4. Use a clean worktree on that named PR's head branch and record the remote starting SHA. Fix the current blocker with the smallest patch, and run the relevant checks. For conflicts, the live parent is the sibling PR head or the unit branch (`mg-stack-N`, Mergestorm's owned trunk, not an alias for main) — not the DTO `parentBranch` when that field starts with `mg-park-*`. If `parentBranch` is `mg-park-*`, never merge or rebase onto it; if the real live parent is unknown, stop. Otherwise prefer merging that live parent into the PR branch, resolving locally, then an ordinary push. Never use Dashboard Continue. When the live parent is a sibling, never rebase onto trunk or `mg-stack-N` instead. Parked-freeze DIRTY alone needs no conflict patch. Leave GitHub bases as Mergestorm set them: the bottom of a 2+ PR stack is based on `mg-stack-N`, layer 2 on layer 1, PR3+ on `mg-park-*`.
|
|
26
|
-
5. CI-only fixes may run while Cyclone auto-patch is still on for the stack. Auto-patch may stay on, and you still wait until the current Vortex run and the current Cyclone run on that PR have finished before the follow-up commit. When the gate is clean, inspect `issues[]` and fix a live upstack CI failure one PR at a time, refreshing the gate before the next PR. Do not use higher-layer bounce history as a patch target. Do not fight Cyclone: if the remote head moved, stop and tell the human. Do not take over Cyclone's review-finding work while it is active; if the blocker requires that or a policy/queue action rather than a concrete patch, stop and explain what needs human attention. On a `Seam findings` blocker, read that PR's seam finding, the Vortex review "Integration review — U<n> seam" (`gh api repos/OWNER/REPO/pulls/N/reviews` and `gh api repos/OWNER/REPO/pulls/N/comments`), and patch its concrete findings on that PR's head; a "Decision required" seam finding needs a human, so tell them and do not guess. On `Seam review failed`, do not patch; tell the human, unless a new push already has a seam re-review held in `busy[]`.
|
|
24
|
+
3. `attention` always means something for you to fix. A land gate that is only waiting (`landGatePending` with `ci_pending`, `tempest_pending`, `tempest_running`, or another `*_pending` / `*_running` reason on the land PR) returns `in_progress`, not `attention`: keep watching. On `attention`, read `repair` when present: it names the blocked PR's `branch` and the concrete fix. For `restack_conflict` and `merge_conflict` it gives `liveParent` (the sibling PR head, or `mg-stack-N` once the lower layers are merged or promoted, or the land PR's base such as `main` when the blocked PR is the land PR, never `mg-park-*`) and the conflicted `files`; a null `liveParent` means stop and tell the human. For `ci_failure` it names the failing check. `repair` does not replace the checks below. On `attention`, fix the named PR: `prNumber` and `headSha` identify the blocked PR, while `currentCandidate` identifies the promote candidate. The gate covers the bottom and its direct child only when the child has current-head DIRTY on the live bottom branch. A GitHub DIRTY whose `parentBranch` starts with `mg-park-` is not that gate. Refresh `stack_status`; use `queue_status` with the same `stack_id` when bounce details are needed. Verify the blocker still applies to the current PR head. Never treat `verifyHeadSha` as the current head: it is a queue verification SHA and can be stale or synthetic. Compare the live PR branch head, the current stack layer head, and the checkout before editing. Read the live PR head with `git ls-remote origin refs/heads/<branch>` or `gh api repos/OWNER/REPO/pulls/N --jq .head.sha`. If you need CI logs, use `gh run list --branch <branch>`, `gh run view <run-id> --log-failed`, or `gh api repos/OWNER/REPO/commits/SHA/check-runs`. Read GitHub only through git and these REST calls; do not use other `gh pr` subcommands. Restack `clean` does not mean mergeable or CI-green. Attention is a diagnosis, not permission to mutate stack or queue policy. A bounce is the reason to look, not the moment to commit. `stack_wait` and `stack_status` hold the blocked PR's attention themselves while Vortex or Cyclone is still working on that PR, whatever the blocker (a red CI failure, a merge conflict, a bounce, Draft, a failed restack or review): they return `in_progress` instead of `attention`, with `busy[]` naming the PR (`prNumber`, `headSha`), the `blocker`, and the `agent` (`vortex` or `cyclone`), and `agents` carrying that PR's `vortexStatus`, `cycloneStatus`, `vortexReview`, and `busy` flags. `in_progress` with a nonempty `busy[]` means do not patch yet: keep calling `stack_wait` with the same cursor until that work has finished and the watch returns `attention`, then make the follow-up commit. You do not need an extra `stack_status` refresh each slice for this check. A nonempty `busy[]` reports still-working agents on that PR's current head. Vortex holds when `vortexReview` is `reviewing` at the head, an unfinished vortex `agentRuns` row at the head is `reviewing`, or an unfinished vortex run at the head is `queued` and started within the last 15 minutes (with no `agentRuns` or `vortexReview` on the layer, `vortexStatus` `reviewing` holds). Cyclone holds when `cycloneStatus` is `patching`, an unfinished cyclone `agentRuns` row is `patching`, or the server sees a live Cyclone patch lease or an open Cyclone check run on the head. `seam_pending` alone, and a `queued` run older than 15 minutes, are not treated as work in progress. `all_clear`, `findings`, `skipped`, `failed`, `throttled`, `awaiting_fix`, and a null status are finished. A queued Vortex run holds for at most 15 minutes; reviewing and patching hold until the run finishes or its lease or check run is cleared. A blocker no Vortex or Cyclone run can clear is not hidden by the hold: a merge conflict vs the live parent, a restack `Conflict` or `Restack failed` (a parked freeze included), or a Draft PR. For those the envelope stays `in_progress` but names the blocker in the top-level `blocker`, `prNumber`, `headSha`, and `issues[]`, with `actAfter: "agents_idle"`, `waitingOn` listing the busy agents, and `repair` when there is one. Plan the fix from it, but do not patch, merge, or push while `actAfter` is set: keep calling `stack_wait` with the same cursor, and act when a snapshot after the agents finish returns `attention`; a Cyclone push can change or clear the blocker, so re-read it then. Blockers the agents can change (a red CI failure, a failed review or Tempest run, seam findings, a bounce) keep the plain hold: `blocker` is null, `actAfter` is null, and only `busy[]` names them. If a hold looks stuck, refresh `stack_status` and tell the human. Do not turn Cyclone off, and do not turn Auto land off or on, to end the wait. If the remote head moves while you wait, stop and tell the human.
|
|
25
|
+
4. Use a clean worktree on that named PR's head branch and record the remote starting SHA. Fix the current blocker with the smallest patch, and run the relevant checks. For conflicts, the live parent is the sibling PR head or the unit branch (`mg-stack-N`, Mergestorm's owned trunk, not an alias for main) — not the DTO `parentBranch` when that field starts with `mg-park-*`. If `parentBranch` is `mg-park-*`, never merge or rebase onto it; if the real live parent is unknown, stop. The land PR is the one PR whose head is `mg-stack-N`: its live parent is its base (`main`), and merging that base into `mg-stack-N` with an ordinary push is its conflict repair, the same as for any other layer. Otherwise prefer merging that live parent into the PR branch, resolving locally, then an ordinary push. Never use Dashboard Continue. When the live parent is a sibling, never rebase onto trunk or `mg-stack-N` instead. Parked-freeze DIRTY alone needs no conflict patch. Leave GitHub bases as Mergestorm set them: the bottom of a 2+ PR stack is based on `mg-stack-N`, layer 2 on layer 1, PR3+ on `mg-park-*`.
|
|
26
|
+
5. CI-only fixes may run while Cyclone auto-patch is still on for the stack. Auto-patch may stay on, and you still wait until the current Vortex run and the current Cyclone run on that PR have finished before the follow-up commit. When the gate is clean, inspect `issues[]` and fix a live upstack CI failure one PR at a time, refreshing the gate before the next PR. Do not use higher-layer bounce history as a patch target. Do not fight Cyclone: if the remote head moved, stop and tell the human. Do not take over Cyclone's review-finding work while it is active; if the blocker requires that or a policy/queue action rather than a concrete patch, stop and explain what needs human attention. On a `Seam findings` blocker, read that PR's seam finding, the Vortex review "Integration review — U<n> seam" (`gh api repos/OWNER/REPO/pulls/N/reviews` and `gh api repos/OWNER/REPO/pulls/N/comments`), and patch its concrete findings on that PR's head; a "Decision required" seam finding needs a human, so tell them and do not guess. A seam finding you verified is wrong is recorded with `mg dismiss` (the audited dismissal in `mergestorm-pr-loop`) at that PR's live head and that seam review's id; a `mergestorm-loop: dismiss` comment alone does not clear the seam gate. On `Seam review failed`, do not patch; tell the human, unless a new push already has a seam re-review held in `busy[]`.
|
|
27
27
|
6. Recheck Auto land and the remote PR head (`git ls-remote origin refs/heads/<branch>`) immediately before pushing. Commit the patch and push the PR head with an ordinary push. A rebase onto that same live parent followed by force-with-lease is allowed only if the remote is still exactly the SHA you started from and Cyclone did not move it; use an explicit lease bound to that starting SHA. This exception never permits rebasing onto a parked freeze or substituting trunk / the unit for a sibling parent. If the remote head moved or the push is rejected because it moved, stop; do not rebase and retry.
|
|
28
28
|
7. After your successful push, leave a clean worktree and keep watching the named stack. Start a new watch cycle with `enrolled_head_sha` set to the SHA you pushed. If you handled a bounce, retain the returned `cursor.afterFinishedAt` as `after_finished_at` and `cursor.bounceId` as `bounce_id` so that attempt is not handled again. Keep these selectors unchanged across subsequent retries. Continue watching; do not enqueue or re-arm anything. If further progress requires re-arming, stop and tell the human.
|
|
29
29
|
|
|
@@ -48,6 +48,11 @@ mg skill install --claude --cursor --agents
|
|
|
48
48
|
- Prefer the smallest correct patch. Do not refactor around a finding.
|
|
49
49
|
- Patch concrete bugs. A chat-only explanation is not a dismiss.
|
|
50
50
|
- If you skip a finding (not reproducible, policy fork, or needs a human), post a public GitHub PR comment before you stop. First line: `mergestorm-loop: dismiss`. Then one line per skipped finding (path, severity, why). That first line is the only dismiss marker Vortex reads.
|
|
51
|
+
- If you verified a finding is wrong (not merely skipped) and it blocks the PR, for example a seam finding holding `Seam findings`, record the dismissal with `review_dismiss` (CLI: `mg dismiss`):
|
|
52
|
+
- First call it with `preview: true`, the PR's live `head_sha` and the review id (review_id) of the Vortex review that raised the finding. It lists that review's finding ids and the seam gate and writes nothing.
|
|
53
|
+
- Then call it with the `finding_ids` you checked (or `scope: "review"` when you checked every finding of that review), a `reason` that says why each is wrong, and an `evidence_url` when you have one.
|
|
54
|
+
- A refusal (stale_head, review_mismatch, unknown_finding, forbidden) means nothing was written. Re-read the pass at the live head; do not retry with another review's ids.
|
|
55
|
+
- The seam gate clears only when every finding of that seam review is dismissed. The result's `gate` says whether it did; CI, other reviews and Auto land are unchanged. For a stack PR, follow `watch.next` (the next `stack_wait` call).
|
|
51
56
|
6. Patch on the PR head branch only.
|
|
52
57
|
7. Push that head branch with an ordinary push. If the push is rejected because the remote branch moved, another patcher already pushed for this pass. Do not rebase, do not force, do not retry the push: stop and tell the human that a second patcher is active on this PR.
|
|
53
58
|
8. Call `review_wait_pr` again with `after_sha` set to the SHA you just pushed (7+ hex) and `timeout_s` 45. If the last envelope you acted on has that same `head_sha` (no new commit, for example a re-review requested on the same head), also pass `after_pass` set to that envelope's saved `pass`, so the wait cannot hand you that attempt again. For a freshly pushed SHA omit `after_pass`; its first pass is 1. Keep `after_sha` and `after_pass` unchanged across 45s timeout retries; never replace `after_pass` with the `pass` of an in-progress envelope. `pass` reads one exact attempt; `timeout_s` 0 reads once without waiting. An envelope whose `head_sha` is an older commit is a previous pass, not yours; keep waiting.
|
|
@@ -79,7 +84,8 @@ Map these to `mergestorm.pr_review/v1` envelope fields; do not invent statuses.
|
|
|
79
84
|
- patch a pass whose `head_sha` is not the commit you have checked out
|
|
80
85
|
- wrap the CLI in a shell escape hatch
|
|
81
86
|
- claim a finding is fixed without a diff that touches its path
|
|
82
|
-
- claim a finding is dismissed without posting a `mergestorm-loop: dismiss` comment on the PR
|
|
87
|
+
- claim a finding is dismissed without posting a `mergestorm-loop: dismiss` comment on the PR or a `review_dismiss` result that lists it
|
|
88
|
+
- dismiss a finding you did not verify, one a human must decide (Decision required, policy fork), or a finding of a newer review than the one you checked
|
|
83
89
|
|
|
84
90
|
## Fallback (no MCP)
|
|
85
91
|
|