mergestorm 0.3.66 → 0.3.67

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 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.66 │
48
+ │ ▀▀▀▀▀▀▀▀▀▀▘ mergestorm v0.3.67 │
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.66: `issue wait`; submit needs an approved issue │
53
+ │ ▀ New in v0.3.67: Issue review: reply once, then one re-check │
54
54
  ╰──────────────────────────────────────────────────────────────────────────────╯
55
55
  mergestorm
56
56
  ╭──────────────────────────────────────────────────────────────────────────────╮
@@ -171,7 +171,7 @@ mg stack publish --status # absent | pushed | opened | registered
171
171
  ```
172
172
 
173
173
  - **Issues too.** `mg issue create` opens the issue a PR needs through the same Surge App, with existing labels only. Running it again with the same title returns the issue it already opened. Put `Fixes #<n>` in the commit message; `stack submit` carries it into the PR body.
174
- - **Issue reviews first.** On a repository with Vortex issue reviews on, Vortex reviews each new issue before any PR. `mg issue wait <n>` (or `mg issue create --wait`) waits for that review: it exits 0 once the issue is approved, or exits 4 with the numbered points to answer. Answer them in a comment on the issue and wait again; Vortex re-reviews on each reply, up to four rounds, after which a person approves with an issue comment whose first line is `mergestorm-issue: approve`. `stack submit` refuses to open a PR whose tip commit links an issue that is not approved yet, or links no issue at all while issue reviews are on.
174
+ - **Issue reviews first.** On a repository with Vortex issue reviews on, Vortex reviews each new issue before any PR. `mg issue wait <n>` (or `mg issue create --wait`) waits for that review: it exits 0 once the issue is approved, or exits 4 with the numbered points to answer. Reply once in a comment on the issue, accepting each blocking point's suggested fix or saying why not, and wait again; Vortex re-checks only the blocking points, once, and approves the issue. Notes never block; the PR review checks them against the code. A person can approve early with an issue comment whose first line is `mergestorm-issue: approve`. `stack submit` refuses to open a PR whose tip commit links an issue that is not approved yet, or links no issue at all while issue reviews are on.
175
175
  - **Exact contents.** Every commit travels with its Git tree SHA and the whole content of each changed file, so binaries, deletions, renames, executable bits and symlinks arrive unchanged. Mergestorm moves the branch only when GitHub builds the same tree, and `mg` checks the returned tree against your local branch.
176
176
  - **Never overwrites surprises.** `mg` looks at the branch on GitHub first. If its files match one of your local commits, only the commits after that one are appended. If they match none (someone else pushed, or you rebased), `mg` stops with `head_changed`; `--replace-head <sha>` replaces exactly that head and nothing else. Mergestorm refuses `main`, `master`, the default branch, protected branches and its own `mg-stack-*`, `mg-park-*` and `mq-batch-*` branches.
177
177
  - **Safe to run again.** After a timeout or an error, run the same command again. It finds the branch, pull request and stack it already made and finishes the missing step; it does not create duplicates.
package/dist/api.d.ts CHANGED
@@ -188,8 +188,8 @@ export type SettingsResponse = import("./automation-catalog.js").BearerSettingsV
188
188
  github_connected: boolean;
189
189
  stacks_ready?: boolean;
190
190
  };
191
- export declare const REPO_SETTINGS_SWITCH_KEYS: readonly ["auto_patch_enabled", "auto_review_enabled", "auto_land_default", "merge_queue_batch_enabled", "merge_queue_overdrive", "issue_review_enabled"];
192
- export declare const REPO_SETTINGS_KEYS: readonly ["auto_patch_enabled", "auto_review_enabled", "auto_land_default", "merge_queue_batch_enabled", "merge_queue_overdrive", "issue_review_enabled", "merge_queue_batch_size", "merge_queue_lands_alone_paths"];
191
+ export declare const REPO_SETTINGS_SWITCH_KEYS: readonly ["auto_patch_enabled", "auto_review_enabled", "auto_land_default", "merge_queue_batch_enabled", "merge_queue_overdrive", "issue_review_enabled", "issue_auto_review_enabled"];
192
+ export declare const REPO_SETTINGS_KEYS: readonly ["auto_patch_enabled", "auto_review_enabled", "auto_land_default", "merge_queue_batch_enabled", "merge_queue_overdrive", "issue_review_enabled", "issue_auto_review_enabled", "merge_queue_batch_size", "merge_queue_lands_alone_paths"];
193
193
  export type RepoSettingsKey = (typeof REPO_SETTINGS_KEYS)[number];
194
194
  type RepoSettingsValues = Record<(typeof REPO_SETTINGS_SWITCH_KEYS)[number], boolean | null> & {
195
195
  merge_queue_batch_size: number | null;
@@ -409,12 +409,17 @@ export type IssueReviewState = (typeof ISSUE_REVIEW_STATES)[number];
409
409
  export type IssueReviewPoint = {
410
410
  kind: string;
411
411
  text: string;
412
+ blocking?: boolean;
412
413
  };
414
+ export declare const ISSUE_REVIEW_MODES: readonly ["off", "mention", "auto"];
415
+ export type IssueReviewMode = (typeof ISSUE_REVIEW_MODES)[number];
416
+ export declare const ISSUE_REVIEW_TAG = "@mergestorm-vortex";
413
417
  export type IssueReviewStatus = {
414
418
  owner: string;
415
419
  repo: string;
416
420
  number: number;
417
421
  enabled: boolean;
422
+ mode: IssueReviewMode;
418
423
  state: IssueReviewState;
419
424
  approvedBy: "vortex" | "human" | null;
420
425
  round: number | null;
@@ -437,6 +442,14 @@ export declare function getIssueReview(cfg: Config, target: IssueReviewTarget, o
437
442
  timeoutMs?: number;
438
443
  signal?: AbortSignal;
439
444
  }): Promise<IssueReviewStatus | null>;
445
+ export declare function getIssueReviewMode(cfg: Config, target: {
446
+ owner: string;
447
+ repo: string;
448
+ }, opts?: {
449
+ timeoutMs?: number;
450
+ }): Promise<IssueReviewMode | null>;
451
+ export declare function issueReviewSettled(status: IssueReviewStatus): boolean;
452
+ export declare function issueReviewBlocksPr(status: IssueReviewStatus): boolean;
440
453
  export declare class IssueReviewTimeoutError extends CommandError {
441
454
  readonly lastStatus: IssueReviewStatus;
442
455
  constructor(lastStatus: IssueReviewStatus);
@@ -452,6 +465,7 @@ export type PollIssueReviewOptions = {
452
465
  export declare function pollIssueReview(cfg: Config, target: IssueReviewTarget, opts?: PollIssueReviewOptions): Promise<IssueReviewStatus | null>;
453
466
  export declare function issueReviewLines(status: IssueReviewStatus): string[];
454
467
  export declare function issueNotApprovedError(issues: IssueReviewStatus[], message?: string | null): CommandError;
468
+ export declare const NO_LINKED_ISSUE = "no_linked_issue";
455
469
  export declare function issueNotApprovedRefusal(status: number, body: unknown, owner: string, repo: string): CommandError | null;
456
470
  /**
457
471
  * Patch any subset of one owned stack's automation policy
package/dist/api.js CHANGED
@@ -188,6 +188,7 @@ export const REPO_SETTINGS_SWITCH_KEYS = [
188
188
  "merge_queue_batch_enabled",
189
189
  "merge_queue_overdrive",
190
190
  "issue_review_enabled",
191
+ "issue_auto_review_enabled",
191
192
  ];
192
193
  export const REPO_SETTINGS_KEYS = [...REPO_SETTINGS_SWITCH_KEYS, "merge_queue_batch_size", "merge_queue_lands_alone_paths"];
193
194
  function repoSettingsFailure(status, body, retryAfterSeconds, action) {
@@ -691,10 +692,12 @@ export async function openStackIssue(input, cfg) {
691
692
  return { number: row.number, url: typeof row.url === "string" ? row.url : "", created: row.created === true };
692
693
  }
693
694
  export const ISSUE_REVIEW_STATES = ["approved", "needs_answers", "pending", "unreviewed", "capped", "off"];
695
+ export const ISSUE_REVIEW_MODES = ["off", "mention", "auto"];
696
+ export const ISSUE_REVIEW_TAG = "@mergestorm-vortex";
694
697
  export const ISSUE_REVIEW_POLL_INTERVAL_MS = 10_000;
695
698
  export const ISSUE_REVIEW_DEFAULT_TIMEOUT_MS = 900_000;
696
699
  const ISSUE_REVIEW_MAX_POINTS = 8;
697
- const ISSUE_REVIEW_DEFAULT_MAX_ROUNDS = 4;
700
+ const ISSUE_REVIEW_DEFAULT_MAX_ROUNDS = 2;
698
701
  function issueReviewText(value) {
699
702
  if (typeof value !== "string")
700
703
  return null;
@@ -714,7 +717,7 @@ export function parseIssueReviewStatus(body, target) {
714
717
  const text = issueReviewText(point?.text);
715
718
  if (!text)
716
719
  return [];
717
- return [{ kind: issueReviewText(point.kind) ?? "note", text }];
720
+ return [{ kind: issueReviewText(point.kind) ?? "note", text, blocking: point.blocking !== false }];
718
721
  })
719
722
  .slice(0, ISSUE_REVIEW_MAX_POINTS)
720
723
  : [];
@@ -724,6 +727,7 @@ export function parseIssueReviewStatus(body, target) {
724
727
  repo: typeof row.repo === "string" && row.repo ? row.repo : target.repo,
725
728
  number: isPositiveInteger(row.number) ? row.number : target.number,
726
729
  enabled: row.enabled === true,
730
+ mode: ISSUE_REVIEW_MODES.find((known) => known === row.mode) ?? (row.enabled === true ? "auto" : "off"),
727
731
  state,
728
732
  approvedBy: row.approvedBy === "vortex" || row.approvedBy === "human" ? row.approvedBy : null,
729
733
  round: isPositiveInteger(row.round) ? row.round : null,
@@ -738,8 +742,12 @@ export function parseIssueReviewStatus(body, target) {
738
742
  export async function getIssueReview(cfg, target, opts = {}) {
739
743
  const query = new URLSearchParams({ owner: target.owner, repo: target.repo }).toString();
740
744
  const { status, body, retryAfterSeconds } = await apiFetch(cfg, `/api/v1/stacks/issues/${encodeURIComponent(String(target.number))}/review?${query}`, opts);
741
- if (status === 404)
742
- return null;
745
+ if (status === 404) {
746
+ const message = serverMessage(body?.message);
747
+ if (!message)
748
+ return null;
749
+ throw new CommandError(`${message} Connect it in Mergestorm, then try again.`, 1, "not_found");
750
+ }
743
751
  if (status === 429) {
744
752
  throw new CommandError(rateLimitedMessage(retryAfterSeconds), REVIEW_EXIT.rate_limited, "rate_limited", retryAfterSeconds !== undefined ? { retryAfterSeconds } : undefined);
745
753
  }
@@ -750,6 +758,29 @@ export async function getIssueReview(cfg, target, opts = {}) {
750
758
  }
751
759
  return parsed;
752
760
  }
761
+ export async function getIssueReviewMode(cfg, target, opts = {}) {
762
+ const query = new URLSearchParams({ owner: target.owner, repo: target.repo }).toString();
763
+ const { status, body, retryAfterSeconds } = await apiFetch(cfg, `/api/v1/stacks/issues/review-mode?${query}`, opts);
764
+ if (status === 404)
765
+ return null;
766
+ if (status === 429) {
767
+ throw new CommandError(rateLimitedMessage(retryAfterSeconds), REVIEW_EXIT.rate_limited, "rate_limited", retryAfterSeconds !== undefined ? { retryAfterSeconds } : undefined);
768
+ }
769
+ const mode = status === 200 ? ISSUE_REVIEW_MODES.find((known) => known === body?.mode) : undefined;
770
+ if (!mode) {
771
+ const message = serverMessage(body?.message);
772
+ throw new CommandError(message ?? `Failed to read the issue review mode of ${target.owner}/${target.repo} (HTTP ${status})`);
773
+ }
774
+ return mode;
775
+ }
776
+ export function issueReviewSettled(status) {
777
+ if (status.state === "pending")
778
+ return false;
779
+ return !(status.state === "unreviewed" && status.mode === "auto");
780
+ }
781
+ export function issueReviewBlocksPr(status) {
782
+ return status.mode === "auto" && status.state !== "approved" && status.state !== "off";
783
+ }
753
784
  export class IssueReviewTimeoutError extends CommandError {
754
785
  lastStatus;
755
786
  constructor(lastStatus) {
@@ -795,7 +826,7 @@ export async function pollIssueReview(cfg, target, opts = {}) {
795
826
  for (;;) {
796
827
  try {
797
828
  const status = await getIssueReview(cfg, target, { signal: opts.signal });
798
- if (!status || status.state !== "pending")
829
+ if (!status || issueReviewSettled(status))
799
830
  return status;
800
831
  last = status;
801
832
  opts.onTick?.(status);
@@ -820,7 +851,7 @@ export function issueReviewLines(status) {
820
851
  const lines = [`${status.owner}/${status.repo}#${status.number}: ${status.state}${round}`];
821
852
  if (status.summary)
822
853
  lines.push(` ${status.summary}`);
823
- status.points.forEach((point, i) => lines.push(` ${i + 1}. [${point.kind}] ${point.text}`));
854
+ status.points.forEach((point, i) => lines.push(` ${i + 1}. [${point.blocking === false ? "note: " : ""}${point.kind}] ${point.text}`));
824
855
  if (status.commentUrl)
825
856
  lines.push(` Review: ${status.commentUrl}`);
826
857
  if (status.next)
@@ -831,10 +862,12 @@ export function issueNotApprovedError(issues, message) {
831
862
  const lines = [message?.trim() || "Vortex has not approved the linked issue yet; refusing to open the pull request."];
832
863
  for (const issue of issues) {
833
864
  lines.push(...issueReviewLines(issue));
834
- lines.push(` Reply in a comment on #${issue.number}, then run \`mergestorm issue wait ${issue.number}\`.`);
865
+ const reply = issue.state === "needs_answers" ? `Reply once in a comment on #${issue.number}, accepting each blocking point's suggested fix or saying why not; then run` : "Run";
866
+ lines.push(` ${reply} \`mergestorm issue wait ${issue.number}\`.`);
835
867
  }
836
- return new CommandError(lines.join("\n"), REVIEW_EXIT.failed, "issue_not_approved");
868
+ return new CommandError(lines.join("\n"), REVIEW_EXIT.failed, "issue_not_approved", issues.length === 0 ? { reason: NO_LINKED_ISSUE } : undefined);
837
869
  }
870
+ export const NO_LINKED_ISSUE = "no_linked_issue";
838
871
  export function issueNotApprovedRefusal(status, body, owner, repo) {
839
872
  if (status !== 409 || !body || typeof body !== "object")
840
873
  return null;
@@ -46,6 +46,10 @@ export declare const BEARER_SETTINGS: readonly [{
46
46
  readonly key: "issue_review_enabled";
47
47
  readonly flag: "--issue-review";
48
48
  readonly label: "Vortex issue reviews";
49
+ }, {
50
+ readonly key: "issue_auto_review_enabled";
51
+ readonly flag: "--issue-auto-review";
52
+ readonly label: "Vortex issue auto review";
49
53
  }, {
50
54
  readonly key: "auto_land_default";
51
55
  readonly flag: "--auto-land";
@@ -116,4 +120,4 @@ export type BearerSettingsValues = {
116
120
  values: readonly (infer Value)[];
117
121
  } ? Value : boolean;
118
122
  };
119
- export declare const BEARER_BOOLEAN_SETTINGS_KEYS: ("auto_review_enabled" | "auto_patch_enabled" | "auto_resolve_conflicts_enabled" | "auto_fix_ci_enabled" | "vortex_show_thinking_traces" | "repo_overview_enabled" | "cyclone_skip_ci_enabled" | "cyclone_patch_unverified_languages" | "vortex_auto_overflow_enabled" | "vortex_skip_all_clear_comments" | "vortex_seam_specialist_enabled" | "issue_review_enabled" | "auto_land_default" | "auto_land_settle_seconds" | "merge_queue_batch_enabled" | "merge_queue_batch_size" | "merge_queue_lands_alone_paths" | "merge_queue_overdrive" | "ignored_bot_logins" | "vortex_bot_skip_check" | "vortex_findings_check" | "cyclone_patch_failure_check")[];
123
+ export declare const BEARER_BOOLEAN_SETTINGS_KEYS: ("auto_review_enabled" | "auto_patch_enabled" | "auto_resolve_conflicts_enabled" | "auto_fix_ci_enabled" | "vortex_show_thinking_traces" | "repo_overview_enabled" | "cyclone_skip_ci_enabled" | "cyclone_patch_unverified_languages" | "vortex_auto_overflow_enabled" | "vortex_skip_all_clear_comments" | "vortex_seam_specialist_enabled" | "issue_review_enabled" | "issue_auto_review_enabled" | "auto_land_default" | "auto_land_settle_seconds" | "merge_queue_batch_enabled" | "merge_queue_batch_size" | "merge_queue_lands_alone_paths" | "merge_queue_overdrive" | "ignored_bot_logins" | "vortex_bot_skip_check" | "vortex_findings_check" | "cyclone_patch_failure_check")[];
@@ -11,6 +11,7 @@ export const BEARER_SETTINGS = [
11
11
  { key: "vortex_skip_all_clear_comments", flag: "--vortex-skip-all-clear", label: "Vortex skip All-clear comments" },
12
12
  { key: "vortex_seam_specialist_enabled", flag: "--vortex-seam", label: "Vortex seam specialist" },
13
13
  { key: "issue_review_enabled", flag: "--issue-review", label: "Vortex issue reviews" },
14
+ { key: "issue_auto_review_enabled", flag: "--issue-auto-review", label: "Vortex issue auto review" },
14
15
  { key: "auto_land_default", flag: "--auto-land", label: "Auto land (new stacks)" },
15
16
  { key: "auto_land_settle_seconds", flag: "--auto-land-settle", label: "Auto land settle (seconds)", kind: "seconds", min: 15, max: 300 },
16
17
  { key: "merge_queue_batch_enabled", flag: "--merge-queue-batch", label: "Merge queue batching" },
@@ -1,6 +1,6 @@
1
1
  export { ACCOUNT_LIMIT, DEFAULT_API, accountNameFromMe, apiBase, configPath, keyDisplay, listAccounts, loadConfig, mergestormHome, removeAccount, resolveApiKey, saveAccount, settleAccountName, splitAccountFlag, useAccount, withAccount, type AccountSummary, type Config, type StoredAccount, } 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, accountLabel, apiFetch, cancelMergeQueueEntry, devicePost, dismissPrFindings, dismissThreadsLine, enqueueStack, ensureUpperPark, getEnrichedStack, getIssueReview, ISSUE_REVIEW_DEFAULT_TIMEOUT_MS, ISSUE_REVIEW_POLL_INTERVAL_MS, ISSUE_REVIEW_STATES, IssueReviewTimeoutError, issueNotApprovedError, issueNotApprovedRefusal, issueReviewLines, parseIssueReviewStatus, pollIssueReview, type IssueReviewPoint, type IssueReviewState, type IssueReviewStatus, type IssueReviewTarget, type PollIssueReviewOptions, getMe, getPrVortexReview, revokeOwnKey, 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 AccountIdentity, 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";
3
+ export { API_TIMEOUT_PREFIX, DEFAULT_API_TIMEOUT_MS, SETTINGS_WRITABLE_KEYS, adoptStack, accountLabel, apiFetch, cancelMergeQueueEntry, devicePost, dismissPrFindings, dismissThreadsLine, enqueueStack, ensureUpperPark, getEnrichedStack, getIssueReview, ISSUE_REVIEW_DEFAULT_TIMEOUT_MS, ISSUE_REVIEW_POLL_INTERVAL_MS, ISSUE_REVIEW_STATES, IssueReviewTimeoutError, NO_LINKED_ISSUE, issueNotApprovedError, issueNotApprovedRefusal, issueReviewLines, parseIssueReviewStatus, pollIssueReview, type IssueReviewPoint, type IssueReviewState, type IssueReviewStatus, type IssueReviewTarget, type PollIssueReviewOptions, getMe, getPrVortexReview, revokeOwnKey, 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 AccountIdentity, 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";
@@ -1,6 +1,6 @@
1
1
  export { ACCOUNT_LIMIT, DEFAULT_API, accountNameFromMe, apiBase, configPath, keyDisplay, listAccounts, loadConfig, mergestormHome, removeAccount, resolveApiKey, saveAccount, settleAccountName, splitAccountFlag, useAccount, withAccount, } 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, accountLabel, apiFetch, cancelMergeQueueEntry, devicePost, dismissPrFindings, dismissThreadsLine, enqueueStack, ensureUpperPark, getEnrichedStack, getIssueReview, ISSUE_REVIEW_DEFAULT_TIMEOUT_MS, ISSUE_REVIEW_POLL_INTERVAL_MS, ISSUE_REVIEW_STATES, IssueReviewTimeoutError, issueNotApprovedError, issueNotApprovedRefusal, issueReviewLines, parseIssueReviewStatus, pollIssueReview, getMe, getPrVortexReview, revokeOwnKey, getReview, getSettings, listRepoSettings, patchRepoSettings, REPO_SETTINGS_KEYS, getStackPublication, landNextStack, listJobs, listMergeQueueEntries, listStacks, listThreads, openStackIssue, parseRetryAfterSeconds, patchSettings, restackStack, setStackPolicy, stackPolicyBody, submitStackPublication, } from "../api.js";
3
+ export { API_TIMEOUT_PREFIX, DEFAULT_API_TIMEOUT_MS, SETTINGS_WRITABLE_KEYS, adoptStack, accountLabel, apiFetch, cancelMergeQueueEntry, devicePost, dismissPrFindings, dismissThreadsLine, enqueueStack, ensureUpperPark, getEnrichedStack, getIssueReview, ISSUE_REVIEW_DEFAULT_TIMEOUT_MS, ISSUE_REVIEW_POLL_INTERVAL_MS, ISSUE_REVIEW_STATES, IssueReviewTimeoutError, NO_LINKED_ISSUE, issueNotApprovedError, issueNotApprovedRefusal, issueReviewLines, parseIssueReviewStatus, pollIssueReview, getMe, getPrVortexReview, revokeOwnKey, 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";
@@ -135,6 +135,10 @@ function asAuthExit(err) {
135
135
  function approvedLine(status) {
136
136
  if (status.state === "off")
137
137
  return `Issue reviews are off for ${status.owner}/${status.repo}; #${status.number} needs no approval.`;
138
+ if (status.state === "unreviewed")
139
+ return `Issue reviews for ${status.owner}/${status.repo} run on mention; #${status.number} needs no approval.`;
140
+ if (status.approvedBy === null)
141
+ return `The Vortex review of ${status.owner}/${status.repo}#${status.number} failed, so it does not hold the pull request back.`;
138
142
  const by = status.approvedBy === "human" ? "a person" : "Vortex";
139
143
  return `${status.owner}/${status.repo}#${status.number} is approved by ${by}.`;
140
144
  }
@@ -161,7 +165,7 @@ export async function waitForIssueReview(target, opts, deps = {}) {
161
165
  }
162
166
  if (opts.json)
163
167
  printJson(status);
164
- if (status.state === "approved" || status.state === "off") {
168
+ if (status.state === "approved" || status.state === "off" || (status.mode === "mention" && status.state === "unreviewed")) {
165
169
  if (!opts.json) {
166
170
  console.log(approvedLine(status));
167
171
  if (status.next)
@@ -1,11 +1,10 @@
1
1
  import { type Config } from "../config.js";
2
- import { type AccountIdentity, type MeResponse } from "../api.js";
2
+ import { type MeResponse } from "../api.js";
3
3
  /**
4
4
  * Probe /api/v1/me with an in-memory config. Does not write disk.
5
5
  * Throws CommandError when the key must not be saved.
6
6
  */
7
7
  export declare function validateApiKey(cfg: Config): Promise<MeResponse>;
8
- export declare function loggedInLine(account: AccountIdentity | null | undefined): string;
9
8
  export declare function pendingLoginPath(): string;
10
9
  export interface LoginDeps {
11
10
  openBrowser: (url: string) => void;
@@ -44,10 +44,6 @@ async function loginConfig() {
44
44
  const store = await readStore();
45
45
  return configFor(store, store.activeAccount);
46
46
  }
47
- export function loggedInLine(account) {
48
- const label = accountLabel(account);
49
- return label ? `Logged in as ${label}.` : "Logged in.";
50
- }
51
47
  async function storeLogin(cfg, me, opts) {
52
48
  const apiKey = cfg.apiKey ?? "";
53
49
  const name = accountNameFromMe(me) ?? provisionalAccountName(apiKey);
@@ -3,7 +3,7 @@ import { type RepoSettingsPatch, type SettingsPatch, type SettingsResponse } fro
3
3
  * CLI flag → Bearer settings key. Boolean flags take on|off. The connected flags are
4
4
  * read-only on the API and deliberately have no flag here.
5
5
  */
6
- export declare const SETTINGS_FLAGS: Record<string, "auto_review_enabled" | "auto_patch_enabled" | "auto_resolve_conflicts_enabled" | "auto_fix_ci_enabled" | "vortex_show_thinking_traces" | "repo_overview_enabled" | "cyclone_skip_ci_enabled" | "cyclone_patch_unverified_languages" | "vortex_auto_overflow_enabled" | "vortex_skip_all_clear_comments" | "vortex_seam_specialist_enabled" | "issue_review_enabled" | "auto_land_default" | "auto_land_settle_seconds" | "merge_queue_batch_enabled" | "merge_queue_batch_size" | "merge_queue_lands_alone_paths" | "merge_queue_overdrive" | "ignored_bot_logins" | "vortex_bot_skip_check" | "vortex_findings_check" | "cyclone_patch_failure_check">;
6
+ export declare const SETTINGS_FLAGS: Record<string, "auto_review_enabled" | "auto_patch_enabled" | "auto_resolve_conflicts_enabled" | "auto_fix_ci_enabled" | "vortex_show_thinking_traces" | "repo_overview_enabled" | "cyclone_skip_ci_enabled" | "cyclone_patch_unverified_languages" | "vortex_auto_overflow_enabled" | "vortex_skip_all_clear_comments" | "vortex_seam_specialist_enabled" | "issue_review_enabled" | "issue_auto_review_enabled" | "auto_land_default" | "auto_land_settle_seconds" | "merge_queue_batch_enabled" | "merge_queue_batch_size" | "merge_queue_lands_alone_paths" | "merge_queue_overdrive" | "ignored_bot_logins" | "vortex_bot_skip_check" | "vortex_findings_check" | "cyclone_patch_failure_check">;
7
7
  type IgnoreBotOperation = {
8
8
  action: "add" | "remove";
9
9
  login: string;
@@ -19,6 +19,7 @@ const REPO_FLAG_LABELS = {
19
19
  merge_queue_lands_alone_paths: "Merge queue lands-alone paths",
20
20
  merge_queue_overdrive: "Merge queue overdrive",
21
21
  issue_review_enabled: "Vortex issue reviews",
22
+ issue_auto_review_enabled: "Vortex issue auto review",
22
23
  };
23
24
  function parsePathsFlagValue(flag, value, defaults) {
24
25
  if (value === undefined || value === "" || value.startsWith("--")) {
@@ -80,7 +81,7 @@ function parseRepoSettingsArgs(slug, args) {
80
81
  throw new CommandError(`Unknown settings flag: ${flag}. See \`mergestorm --help\`.`, 2, "usage");
81
82
  }
82
83
  if (!REPO_SETTINGS_KEYS.includes(key)) {
83
- throw new CommandError(`${flag} is an account setting. With --repo only --auto-patch, --auto-review, --auto-land, --merge-queue-batch, --merge-queue-batch-size, --merge-queue-lands-alone, --merge-queue-overdrive and --issue-review can be set.`, 2, "usage");
84
+ throw new CommandError(`${flag} is an account setting. With --repo only --auto-patch, --auto-review, --auto-land, --merge-queue-batch, --merge-queue-batch-size, --merge-queue-lands-alone, --merge-queue-overdrive, --issue-review and --issue-auto-review can be set.`, 2, "usage");
84
85
  }
85
86
  const value = eq >= 0 ? arg.slice(eq + 1) : args[++i];
86
87
  const row = BEARER_SETTINGS.find((row) => row.key === key);
@@ -1,6 +1,6 @@
1
1
  import { type StackRepairHint } from "../stack-blockers.js";
2
2
  import { type StackWatchObligation } from "../stack-watch-obligation.js";
3
- import { adoptStack, ensureUpperPark, findStackPull, getEnrichedStack, getIssueReview, getStackPublication, listMergeQueueEntries, listStacks, openStackPull, claimStackPulls, setStackPolicy, type StackDto, type StackPolicyPatch } from "../api.js";
3
+ import { adoptStack, ensureUpperPark, findStackPull, getEnrichedStack, getIssueReview, type IssueReviewMode, getStackPublication, listMergeQueueEntries, listStacks, openStackPull, claimStackPulls, setStackPolicy, type StackDto, type StackPolicyPatch } from "../api.js";
4
4
  import { loadConfig } from "../config.js";
5
5
  import { createPrs, findOpenPrNumber } from "../gh.js";
6
6
  import { parseGithubOriginRepo } from "../git.js";
@@ -126,14 +126,14 @@ export type StackCreateDeps = {
126
126
  };
127
127
  export declare function cmdStackCreate(args: string[], deps?: StackCreateDeps): Promise<void>;
128
128
  /** Injectable seams for unit tests (STRUCT-04). Production callers omit deps. */
129
- export declare function issueReviewsEnabledFor(cfg: Awaited<ReturnType<typeof loadConfig>>, owner: string, repo: string): Promise<boolean>;
129
+ export declare function issueReviewModeFor(cfg: Awaited<ReturnType<typeof loadConfig>>, owner: string, repo: string): Promise<IssueReviewMode>;
130
130
  export type IssueGateLayer = {
131
131
  branch: string;
132
132
  texts: string[];
133
133
  };
134
134
  export type IssueGateDeps = {
135
135
  getIssueReview?: typeof getIssueReview;
136
- issueReviewsEnabled?: typeof issueReviewsEnabledFor;
136
+ issueReviewMode?: typeof issueReviewModeFor;
137
137
  };
138
138
  export declare function assertLinkedIssuesApproved(input: {
139
139
  cfg: Awaited<ReturnType<typeof loadConfig>>;
@@ -1,6 +1,6 @@
1
1
  import { cycloneStatusWords, stackBlockers, stackBlockersSummary } from "../stack-blockers.js";
2
2
  import { STACK_WATCH_NOT_DONE_SENTENCE, STACK_WATCH_NOTIFY_PATTERN, stackTerminalReason, stackWatchObligation, } from "../stack-watch-obligation.js";
3
- import { adoptStack, ensureUpperPark, findStackPull, getEnrichedStack, getIssueReview, getSettings, getStackPublication, issueNotApprovedError, landNextStack, listRepoSettings, listMergeQueueEntries, listStacks, openStackPull, claimStackPulls, restackStack, setStackPolicy, } from "../api.js";
3
+ import { adoptStack, ensureUpperPark, findStackPull, getEnrichedStack, getIssueReview, getIssueReviewMode, issueReviewBlocksPr, getSettings, getStackPublication, issueNotApprovedError, NO_LINKED_ISSUE, landNextStack, listRepoSettings, listMergeQueueEntries, listStacks, openStackPull, claimStackPulls, restackStack, setStackPolicy, } from "../api.js";
4
4
  import { loadConfig } from "../config.js";
5
5
  import { rateLimitedMessage, REVIEW_EXIT, CommandError } from "../errors.js";
6
6
  import { createPrs, findOpenPrNumber, ghReady, githubApiFromEnv, } from "../gh.js";
@@ -681,15 +681,21 @@ export async function cmdStackCreate(args, deps = {}) {
681
681
  await present("Stack", created);
682
682
  }
683
683
  /** Injectable seams for unit tests (STRUCT-04). Production callers omit deps. */
684
- export async function issueReviewsEnabledFor(cfg, owner, repo) {
684
+ export async function issueReviewModeFor(cfg, owner, repo) {
685
+ const served = await getIssueReviewMode(cfg, { owner, repo }, { timeoutMs: STACK_GUARD_TIMEOUT_MS });
686
+ if (served)
687
+ return served;
685
688
  const repos = await listRepoSettings(cfg, { timeoutMs: STACK_GUARD_TIMEOUT_MS });
686
689
  const row = repos.find((r) => r.owner.toLowerCase() === owner.toLowerCase() && r.repo.toLowerCase() === repo.toLowerCase());
687
- if (typeof row?.issue_review_enabled === "boolean")
688
- return row.issue_review_enabled;
689
690
  const settings = await getSettings(cfg, { timeoutMs: STACK_GUARD_TIMEOUT_MS });
690
691
  if (!settings)
691
692
  throw new CommandError("Could not read the account settings");
692
- return settings.issue_review_enabled === true;
693
+ const account = settings;
694
+ const on = typeof row?.issue_review_enabled === "boolean" ? row.issue_review_enabled : account.issue_review_enabled === true;
695
+ if (!on)
696
+ return "off";
697
+ const auto = typeof row?.issue_auto_review_enabled === "boolean" ? row.issue_auto_review_enabled : account.issue_auto_review_enabled !== false;
698
+ return auto ? "auto" : "mention";
693
699
  }
694
700
  export async function assertLinkedIssuesApproved(input, deps = {}) {
695
701
  const { cfg, owner, repo, layers, warn } = input;
@@ -720,22 +726,22 @@ export async function assertLinkedIssuesApproved(input, deps = {}) {
720
726
  }
721
727
  const unlinked = linked.filter((layer) => layer.numbers.length === 0);
722
728
  if (unlinked.length > 0) {
723
- let enabled;
729
+ let mode;
724
730
  try {
725
- enabled = await (deps.issueReviewsEnabled ?? issueReviewsEnabledFor)(cfg, owner, repo);
731
+ mode = await (deps.issueReviewMode ?? issueReviewModeFor)(cfg, owner, repo);
726
732
  }
727
733
  catch (err) {
728
734
  throw unverified(err);
729
735
  }
730
- if (enabled) {
736
+ if (mode === "auto") {
731
737
  throw new CommandError([
732
738
  `Issue reviews are on for ${owner}/${repo}, and ${unlinked.map((layer) => layer.branch).join(", ")} links no issue.`,
733
- "Open one with `mergestorm issue create --title <problem> --wait`, answer the Vortex review until it is approved,",
739
+ "Open one with `mergestorm issue create --title <problem> --wait`, reply once to any blocking points in the Vortex review,",
734
740
  "then add `Fixes #N` to the tip commit message and submit again.",
735
- ].join("\n"), REVIEW_EXIT.failed, "issue_not_approved");
741
+ ].join("\n"), REVIEW_EXIT.failed, "issue_not_approved", { reason: NO_LINKED_ISSUE });
736
742
  }
737
743
  }
738
- const blocking = statuses.filter((status) => status.state !== "approved" && status.state !== "off");
744
+ const blocking = statuses.filter(issueReviewBlocksPr);
739
745
  if (blocking.length > 0)
740
746
  throw issueNotApprovedError(blocking);
741
747
  }
@@ -1561,11 +1567,15 @@ async function cmdStackLand(args) {
1561
1567
  ]);
1562
1568
  return;
1563
1569
  }
1570
+ const cascadeFailed = body.cascadeFailed;
1564
1571
  await present("Stack", [
1565
1572
  ansi.brightGreen(` Landed` +
1566
1573
  (pr != null ? ` #${pr}` : "") +
1567
1574
  (branch ? ` (${branch})` : "") +
1568
1575
  ` on ${id}`),
1576
+ ...(cascadeFailed
1577
+ ? [ansi.yellow(` The layers above it were not updated: ${cascadeFailed}`)]
1578
+ : []),
1569
1579
  ]);
1570
1580
  }
1571
1581
  export async function cmdStack(args, options = {}) {
@@ -487,6 +487,8 @@ const AUTO_LAND_WAIT_WORDS = {
487
487
  missing_head: "GitHub returned no head commit for this PR",
488
488
  pr_draft: "this PR is a draft",
489
489
  trunk_in_land_target_without_land_pr: "the stack's commits are already in the land target, but no merged land PR was found",
490
+ land_pr_push_unreviewed: "someone other than Stacks pushed to the stack trunk, and the land PR has no clean Vortex review at that push or at its head",
491
+ land_pr_head_unexplained: "GitHub's activity list for the stack trunk does not show the push that made the land PR head",
490
492
  conflict_unchanged: "the merge queue bounced this PR for a conflict, and neither the PR nor its base has changed since",
491
493
  land_pr_unavailable: "the land PR cannot be opened or found",
492
494
  github_unreadable: "Mergestorm cannot read this PR from GitHub",
package/dist/ui/banner.js CHANGED
@@ -236,6 +236,9 @@ const WHATS_NEW = {
236
236
  "0.3.66": [
237
237
  "`issue wait`; submit needs an approved issue",
238
238
  ],
239
+ "0.3.67": [
240
+ "Issue review: reply once, then one re-check",
241
+ ],
239
242
  };
240
243
  function densityFromVariant(variant) {
241
244
  if (variant === "mini" || variant === "nano")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mergestorm",
3
- "version": "0.3.66",
3
+ "version": "0.3.67",
4
4
  "description": "Mergestorm CLI (mg) — local reviews & stacked PRs. MIT; build from source: https://github.com/marginsystems/mergestorm-cli",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/skill/SKILL.md CHANGED
@@ -15,7 +15,7 @@ Not for merging, landing, or pushing. For a PR that is already open on GitHub, u
15
15
 
16
16
  ## Before the PR: the issue review
17
17
 
18
- Vortex reviews the issue against the repository before any code. Wait for its verdict with `mg issue wait <n>`, which reads the verdict from Vortex itself; never judge it from comment text. The issue is approved when the latest verdict is Ready, or Ready with notes after you have answered once, or a person has approved it. If the verdict lists points, answer them in one comment on the issue, or update the issue; Vortex reviews it again with your answer in a new comment, and you wait again. Answer points; do not dismiss them. Start the PR only once the issue is approved: `mg stack submit` refuses each PR whose linked issue (`Fixes #N`) is not, so every layer of a stack needs its own approved issue. On a repository with issue reviews off the wait returns at once. If the wait times out, tell the person. After the last round only a person approves, with a comment whose first line is `mergestorm-issue: approve`; never post that command yourself.
18
+ Vortex reviews the issue against the repository before any code. Wait for its verdict with `mg issue wait N`, which reads the verdict from Vortex itself; never judge it from comment text. Ready or Ready with notes approves the issue; notes do not block, and the PR review checks them against the code. If the verdict is Needs answers, reply once in a comment on the issue: for each blocking point, accept the suggested fix or say why not, and update the issue to match. Vortex then re-checks only those points, once, and approves the issue; a point still open goes to the PR review. Wait again after your reply. Start the PR only once the issue is approved: `mg stack submit` refuses each PR whose linked issue (`Fixes #N`) is not, so every layer of a stack needs its own approved issue. On a repository with issue reviews off the wait returns at once. If the wait times out, tell the person. A person can approve early with a comment whose first line is `mergestorm-issue: approve`; never post that command yourself.
19
19
 
20
20
  ## Install (Claude Code / Cursor / Agents)
21
21