@coresource/hz 0.20.1 → 0.20.3

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.
Files changed (2) hide show
  1. package/dist/hz.mjs +1300 -664
  2. package/package.json +1 -1
package/dist/hz.mjs CHANGED
@@ -3,7 +3,7 @@
3
3
  // src/cli.ts
4
4
  import { readFileSync, realpathSync } from "fs";
5
5
  import { fileURLToPath } from "url";
6
- import { Command, CommanderError as CommanderError3 } from "commander";
6
+ import { Command, CommanderError as CommanderError4 } from "commander";
7
7
 
8
8
  // src/cli-support.ts
9
9
  import { password } from "@inquirer/prompts";
@@ -28,6 +28,50 @@ function writeLine(stream, message = "") {
28
28
  stream.write(`${message}
29
29
  `);
30
30
  }
31
+ function hasExitCode(error, exitCode) {
32
+ return error instanceof Error && "exitCode" in error && error.exitCode === exitCode;
33
+ }
34
+ function isAbortError(error) {
35
+ return error instanceof DOMException && error.name === "AbortError";
36
+ }
37
+ function isAbortPromptError(error) {
38
+ return error instanceof Error && error.name === "AbortPromptError";
39
+ }
40
+ function isInterruptedError(error) {
41
+ return hasExitCode(error, 130) || isAbortError(error) || isAbortPromptError(error);
42
+ }
43
+ function createSignalHandlerCoordinator(registerSignalHandler, exitProcess = (code) => process.exit(code)) {
44
+ if (!registerSignalHandler) {
45
+ return void 0;
46
+ }
47
+ let activeUnregister;
48
+ let cleanupInProgress = false;
49
+ return (signal, handler) => {
50
+ activeUnregister?.();
51
+ cleanupInProgress = false;
52
+ const wrappedHandler = () => {
53
+ if (cleanupInProgress) {
54
+ exitProcess(130);
55
+ return;
56
+ }
57
+ cleanupInProgress = true;
58
+ handler();
59
+ };
60
+ const underlyingUnregister = registerSignalHandler(signal, wrappedHandler);
61
+ const unregister = () => {
62
+ if (activeUnregister !== unregister) {
63
+ return;
64
+ }
65
+ activeUnregister = void 0;
66
+ cleanupInProgress = false;
67
+ if (typeof underlyingUnregister === "function") {
68
+ underlyingUnregister();
69
+ }
70
+ };
71
+ activeUnregister = unregister;
72
+ return unregister;
73
+ };
74
+ }
31
75
 
32
76
  // src/config.ts
33
77
  import { open } from "fs/promises";
@@ -300,6 +344,9 @@ function formatNetworkError(error) {
300
344
  }
301
345
  return new ApiError("Unexpected error while contacting the Horizon API.");
302
346
  }
347
+ function isAbortError2(error) {
348
+ return error instanceof DOMException && error.name === "AbortError";
349
+ }
303
350
  function createApiClient(options) {
304
351
  const fetchImpl = options.fetch ?? globalThis.fetch;
305
352
  const sleep = options.sleep ?? ((ms) => delay(ms).then(() => void 0));
@@ -323,7 +370,8 @@ function createApiClient(options) {
323
370
  );
324
371
  }
325
372
  for (let attempt = 0; attempt <= retryCount; attempt += 1) {
326
- const signal = AbortSignal.timeout(request.timeoutMs ?? timeoutMs);
373
+ const timeoutSignal = AbortSignal.timeout(request.timeoutMs ?? timeoutMs);
374
+ const signal = request.signal ? AbortSignal.any([request.signal, timeoutSignal]) : timeoutSignal;
327
375
  try {
328
376
  const response = await fetchImpl(url, {
329
377
  method,
@@ -346,6 +394,9 @@ function createApiClient(options) {
346
394
  }
347
395
  throw formatHttpError(response, payload);
348
396
  } catch (error) {
397
+ if (isAbortError2(error)) {
398
+ throw error;
399
+ }
349
400
  if (error instanceof ApiError) {
350
401
  throw error;
351
402
  }
@@ -552,11 +603,11 @@ function normalizeMissionSummary(value) {
552
603
  return {
553
604
  completedFeatures: asFiniteNumber(value.completedFeatures),
554
605
  createdAt: asNonEmptyString(value.createdAt),
555
- description: asNonEmptyString(value.description) ?? asNonEmptyString(value.taskDescription),
606
+ description: asNonEmptyString(value.description),
556
607
  missionId,
557
608
  passedAssertions: asFiniteNumber(value.passedAssertions),
558
609
  state: asNonEmptyString(value.state) ?? "unknown",
559
- taskDescription: asNonEmptyString(value.taskDescription) ?? asNonEmptyString(value.description),
610
+ taskDescription: asNonEmptyString(value.taskDescription),
560
611
  totalAssertions: asFiniteNumber(value.totalAssertions),
561
612
  totalFeatures: asFiniteNumber(value.totalFeatures)
562
613
  };
@@ -811,70 +862,87 @@ function registerMissionInfoCommand(mission, context, dependencies = {}) {
811
862
  }
812
863
 
813
864
  // src/commands/mission-list.ts
814
- async function resolveMissionDescription(apiClient, missionSummary) {
815
- if (missionSummary.taskDescription || missionSummary.description) {
816
- return missionSummary.taskDescription ?? missionSummary.description;
817
- }
818
- try {
819
- const missionState = normalizeMissionState(
820
- await apiClient.request({
821
- path: `/missions/${missionSummary.missionId}/mission/state`
822
- })
823
- );
824
- return missionState.taskDescription;
825
- } catch {
826
- return null;
827
- }
828
- }
865
+ import { CommanderError } from "commander";
866
+ import pc3 from "picocolors";
829
867
  function registerMissionListCommand(mission, context, dependencies = {}) {
830
868
  mission.command("list").description("List recent missions").action(async (_options, command) => {
831
- const apiClient = await createMissionApiClient(context, command, dependencies);
832
- const missionSummaries = normalizeMissionSummaries(
833
- await apiClient.request({
834
- path: "/missions"
835
- })
869
+ const registerSignalHandler = createSignalHandlerCoordinator(
870
+ dependencies.registerSignalHandler
836
871
  );
837
- if (missionSummaries.length === 0) {
838
- writeLine(context.stdout, "No missions found");
839
- return;
872
+ const abortController = new AbortController();
873
+ let interrupted = false;
874
+ const unregisterSignalHandler = registerSignalHandler?.("SIGINT", () => {
875
+ interrupted = true;
876
+ if (!abortController.signal.aborted) {
877
+ abortController.abort();
878
+ }
879
+ writeLine(
880
+ context.stdout,
881
+ pc3.yellow(
882
+ "Interrupted. Remote mission state is preserved. Re-run `hz mission list` when ready."
883
+ )
884
+ );
885
+ });
886
+ try {
887
+ const apiClient = await createMissionApiClient(context, command, dependencies);
888
+ const missionSummaries = normalizeMissionSummaries(
889
+ await apiClient.request({
890
+ path: "/missions",
891
+ signal: abortController.signal
892
+ })
893
+ );
894
+ if (interrupted) {
895
+ throw new CommanderError(130, "mission-list", "");
896
+ }
897
+ if (missionSummaries.length === 0) {
898
+ writeLine(context.stdout, "No missions found");
899
+ return;
900
+ }
901
+ renderTable(
902
+ context.stdout,
903
+ ["Mission ID", "State", "Created", "Features", "Assertions", "Description"],
904
+ missionSummaries.map((missionSummary) => [
905
+ plainCell(missionSummary.missionId),
906
+ coloredCell(
907
+ formatMissionState(missionSummary.state),
908
+ missionSummary.state
909
+ ),
910
+ plainCell(formatDateTime(missionSummary.createdAt)),
911
+ plainCell(
912
+ `${missionSummary.completedFeatures}/${missionSummary.totalFeatures}`
913
+ ),
914
+ plainCell(
915
+ `${missionSummary.passedAssertions}/${missionSummary.totalAssertions}`
916
+ ),
917
+ plainCell(
918
+ summarizeMissionDescription(
919
+ missionSummary.description ?? missionSummary.taskDescription
920
+ )
921
+ )
922
+ ])
923
+ );
924
+ } catch (error) {
925
+ if (interrupted && isAbortError(error)) {
926
+ throw new CommanderError(130, "mission-list", "");
927
+ }
928
+ throw error;
929
+ } finally {
930
+ if (typeof unregisterSignalHandler === "function") {
931
+ unregisterSignalHandler();
932
+ }
840
933
  }
841
- const missions = await Promise.all(
842
- missionSummaries.map(async (missionSummary) => ({
843
- ...missionSummary,
844
- description: await resolveMissionDescription(apiClient, missionSummary)
845
- }))
846
- );
847
- renderTable(
848
- context.stdout,
849
- ["Mission ID", "State", "Created", "Features", "Assertions", "Description"],
850
- missions.map((missionSummary) => [
851
- plainCell(missionSummary.missionId),
852
- coloredCell(
853
- formatMissionState(missionSummary.state),
854
- missionSummary.state
855
- ),
856
- plainCell(formatDateTime(missionSummary.createdAt)),
857
- plainCell(
858
- `${missionSummary.completedFeatures}/${missionSummary.totalFeatures}`
859
- ),
860
- plainCell(
861
- `${missionSummary.passedAssertions}/${missionSummary.totalAssertions}`
862
- ),
863
- plainCell(summarizeMissionDescription(missionSummary.description))
864
- ])
865
- );
866
934
  });
867
935
  }
868
936
 
869
937
  // src/commands/mission-lifecycle.ts
870
938
  import { confirm as defaultConfirmPrompt } from "@inquirer/prompts";
871
- import { CommanderError } from "commander";
872
- import pc6 from "picocolors";
939
+ import { CommanderError as CommanderError2 } from "commander";
940
+ import pc7 from "picocolors";
873
941
 
874
942
  // src/monitor.ts
875
943
  import { setTimeout as delay2 } from "timers/promises";
876
944
  import ora from "ora";
877
- import pc4 from "picocolors";
945
+ import pc5 from "picocolors";
878
946
  import WebSocket from "ws";
879
947
 
880
948
  // src/state.ts
@@ -1641,7 +1709,7 @@ import {
1641
1709
  input as defaultInputPrompt,
1642
1710
  select as defaultSelectPrompt
1643
1711
  } from "@inquirer/prompts";
1644
- import pc3 from "picocolors";
1712
+ import pc4 from "picocolors";
1645
1713
  var TRIAGE_PROGRESS_PAGE_SIZE = 200;
1646
1714
  var AUTO_DISMISS_RATIONALE = "Auto-dismissed by `hz mission run --yes`.";
1647
1715
  function isRecord3(value) {
@@ -1725,48 +1793,63 @@ function normalizeProgressEvent(value) {
1725
1793
  };
1726
1794
  }
1727
1795
  async function defaultPromptSelect(options) {
1728
- return defaultSelectPrompt({
1729
- choices: options.choices.map((choice) => ({
1730
- description: choice.description,
1731
- name: choice.name,
1732
- value: choice.value
1733
- })),
1734
- message: options.message
1735
- });
1796
+ return defaultSelectPrompt(
1797
+ {
1798
+ choices: options.choices.map((choice) => ({
1799
+ description: choice.description,
1800
+ name: choice.name,
1801
+ value: choice.value
1802
+ })),
1803
+ message: options.message
1804
+ },
1805
+ { signal: options.signal }
1806
+ );
1736
1807
  }
1737
1808
  async function defaultPromptInput(options) {
1738
- return defaultInputPrompt({
1739
- default: options.default,
1740
- message: options.message
1741
- });
1809
+ return defaultInputPrompt(
1810
+ {
1811
+ default: options.default,
1812
+ message: options.message
1813
+ },
1814
+ { signal: options.signal }
1815
+ );
1742
1816
  }
1743
1817
  function buildIssueId(featureId) {
1744
1818
  return `triage:${featureId}`;
1745
1819
  }
1746
- async function fetchTriageFeatures(apiClient, missionId) {
1820
+ function throwIfInterrupted(signal) {
1821
+ if (signal?.aborted) {
1822
+ throw new CliError("Triage interrupted.", 130);
1823
+ }
1824
+ }
1825
+ async function fetchTriageFeatures(apiClient, missionId, signal) {
1747
1826
  const response = await apiClient.request({
1748
- path: `/missions/${missionId}/features`
1827
+ path: `/missions/${missionId}/features`,
1828
+ signal
1749
1829
  });
1750
1830
  if (!Array.isArray(response)) {
1751
1831
  throw new CliError("Mission features response was not an array.");
1752
1832
  }
1753
1833
  return response.map((feature) => normalizeFeature(feature));
1754
1834
  }
1755
- async function fetchAssertions(apiClient, missionId) {
1835
+ async function fetchAssertions(apiClient, missionId, signal) {
1756
1836
  const response = await apiClient.request({
1757
- path: `/missions/${missionId}/assertions`
1837
+ path: `/missions/${missionId}/assertions`,
1838
+ signal
1758
1839
  });
1759
1840
  if (!Array.isArray(response)) {
1760
1841
  throw new CliError("Mission assertions response was not an array.");
1761
1842
  }
1762
1843
  return response.map((assertion) => normalizeAssertion(assertion));
1763
1844
  }
1764
- async function fetchLatestTriageEvents(apiClient, missionId) {
1845
+ async function fetchLatestTriageEvents(apiClient, missionId, signal) {
1765
1846
  const eventsByFeature = /* @__PURE__ */ new Map();
1766
1847
  let since = 0;
1767
1848
  for (; ; ) {
1849
+ throwIfInterrupted(signal);
1768
1850
  const response = await apiClient.request({
1769
- path: `/missions/${missionId}/progress?since=${since}&limit=${TRIAGE_PROGRESS_PAGE_SIZE}`
1851
+ path: `/missions/${missionId}/progress?since=${since}&limit=${TRIAGE_PROGRESS_PAGE_SIZE}`,
1852
+ signal
1770
1853
  });
1771
1854
  if (!Array.isArray(response) || response.length === 0) {
1772
1855
  break;
@@ -1856,7 +1939,7 @@ function buildTriageItem(feature, allAssertions, suggestedActionsByFeature) {
1856
1939
  }
1857
1940
  function writeItemBlock(stdout, item) {
1858
1941
  writeLine(stdout);
1859
- writeLine(stdout, pc3.bold(`Triage required for ${pc3.cyan(item.feature.id)}`));
1942
+ writeLine(stdout, pc4.bold(`Triage required for ${pc4.cyan(item.feature.id)}`));
1860
1943
  writeLine(stdout, `Failure: ${item.failureDescription}`);
1861
1944
  writeLine(
1862
1945
  stdout,
@@ -1930,11 +2013,12 @@ function toFixFeaturePayload(item, features) {
1930
2013
  verificationSteps: item.feature.verificationSteps.length > 0 ? [...item.feature.verificationSteps] : [`Verify that ${item.feature.id} no longer requires triage.`]
1931
2014
  };
1932
2015
  }
1933
- async function dismissItem(apiClient, missionId, item, rationale) {
2016
+ async function dismissItem(apiClient, missionId, item, rationale, signal) {
1934
2017
  const trimmedRationale = rationale.trim();
1935
2018
  if (trimmedRationale.length === 0) {
1936
2019
  throw new CliError("Dismissal rationale is required.");
1937
2020
  }
2021
+ throwIfInterrupted(signal);
1938
2022
  await apiClient.request({
1939
2023
  body: {
1940
2024
  items: [
@@ -1945,19 +2029,22 @@ async function dismissItem(apiClient, missionId, item, rationale) {
1945
2029
  ]
1946
2030
  },
1947
2031
  method: "POST",
1948
- path: `/missions/${missionId}/triage/dismiss`
2032
+ path: `/missions/${missionId}/triage/dismiss`,
2033
+ signal
1949
2034
  });
1950
2035
  }
1951
- async function createFixFeature(apiClient, missionId, item, features) {
2036
+ async function createFixFeature(apiClient, missionId, item, features, signal) {
1952
2037
  const feature = toFixFeaturePayload(item, features);
2038
+ throwIfInterrupted(signal);
1953
2039
  await apiClient.request({
1954
2040
  body: { feature },
1955
2041
  method: "POST",
1956
- path: `/missions/${missionId}/triage/create-fix`
2042
+ path: `/missions/${missionId}/triage/create-fix`,
2043
+ signal
1957
2044
  });
1958
2045
  return feature;
1959
2046
  }
1960
- async function reassignAssertions(apiClient, missionId, item, features, promptSelect) {
2047
+ async function reassignAssertions(apiClient, missionId, item, features, promptSelect, signal) {
1961
2048
  if (item.assertions.length === 0) {
1962
2049
  return 0;
1963
2050
  }
@@ -1971,9 +2058,11 @@ async function reassignAssertions(apiClient, missionId, item, features, promptSe
1971
2058
  }
1972
2059
  const moves = [];
1973
2060
  for (const assertion of item.assertions) {
2061
+ throwIfInterrupted(signal);
1974
2062
  const targetFeatureId = await promptSelect({
1975
2063
  choices: destinationChoices,
1976
- message: `Move ${assertion.id} to which feature?`
2064
+ message: `Move ${assertion.id} to which feature?`,
2065
+ signal
1977
2066
  });
1978
2067
  const targetFeature = features.find((feature) => feature.id === targetFeatureId);
1979
2068
  const targetMilestone = targetFeature?.milestone ?? assertion.milestone ?? item.feature.milestone;
@@ -1991,149 +2080,174 @@ async function reassignAssertions(apiClient, missionId, item, features, promptSe
1991
2080
  if (moves.length === 0) {
1992
2081
  return 0;
1993
2082
  }
2083
+ throwIfInterrupted(signal);
1994
2084
  await apiClient.request({
1995
2085
  body: { moves },
1996
2086
  method: "POST",
1997
- path: `/missions/${missionId}/triage/reassign-assertions`
2087
+ path: `/missions/${missionId}/triage/reassign-assertions`,
2088
+ signal
1998
2089
  });
1999
2090
  return moves.length;
2000
2091
  }
2001
2092
  async function runTriageInteraction(options) {
2002
2093
  const promptSelect = options.promptSelect ?? defaultPromptSelect;
2003
2094
  const promptInput = options.promptInput ?? defaultPromptInput;
2004
- const [features, assertions, suggestedActionsByFeature] = await Promise.all([
2005
- fetchTriageFeatures(options.apiClient, options.missionId),
2006
- fetchAssertions(options.apiClient, options.missionId),
2007
- fetchLatestTriageEvents(options.apiClient, options.missionId)
2008
- ]);
2009
- const triageItems = features.filter((feature) => feature.status === "needs_triage").map((feature) => buildTriageItem(feature, assertions, suggestedActionsByFeature));
2010
- if (triageItems.length === 0) {
2011
- writeLine(options.stdout, pc3.dim("No outstanding triage items were found."));
2012
- return {
2095
+ try {
2096
+ throwIfInterrupted(options.signal);
2097
+ const [features, assertions, suggestedActionsByFeature] = await Promise.all([
2098
+ fetchTriageFeatures(options.apiClient, options.missionId, options.signal),
2099
+ fetchAssertions(options.apiClient, options.missionId, options.signal),
2100
+ fetchLatestTriageEvents(options.apiClient, options.missionId, options.signal)
2101
+ ]);
2102
+ throwIfInterrupted(options.signal);
2103
+ const triageItems = features.filter((feature) => feature.status === "needs_triage").map((feature) => buildTriageItem(feature, assertions, suggestedActionsByFeature));
2104
+ if (triageItems.length === 0) {
2105
+ writeLine(options.stdout, pc4.dim("No outstanding triage items were found."));
2106
+ return {
2107
+ createdFixes: 0,
2108
+ dismissed: 0,
2109
+ handledFeatureIds: [],
2110
+ movedAssertions: 0,
2111
+ skipped: 0,
2112
+ totalItems: 0
2113
+ };
2114
+ }
2115
+ if (options.autoApprove) {
2116
+ writeLine(
2117
+ options.stdout,
2118
+ pc4.yellow(
2119
+ `Auto-dismissing triage items: ${triageItems.map((item) => item.feature.id).join(", ")}`
2120
+ )
2121
+ );
2122
+ throwIfInterrupted(options.signal);
2123
+ await options.apiClient.request({
2124
+ body: {
2125
+ items: triageItems.map((item) => ({
2126
+ issueId: item.issueId,
2127
+ rationale: AUTO_DISMISS_RATIONALE
2128
+ }))
2129
+ },
2130
+ method: "POST",
2131
+ path: `/missions/${options.missionId}/triage/dismiss`,
2132
+ signal: options.signal
2133
+ });
2134
+ return {
2135
+ createdFixes: 0,
2136
+ dismissed: triageItems.length,
2137
+ handledFeatureIds: triageItems.map((item) => item.feature.id),
2138
+ movedAssertions: 0,
2139
+ skipped: 0,
2140
+ totalItems: triageItems.length
2141
+ };
2142
+ }
2143
+ const result = {
2013
2144
  createdFixes: 0,
2014
2145
  dismissed: 0,
2015
2146
  handledFeatureIds: [],
2016
2147
  movedAssertions: 0,
2017
2148
  skipped: 0,
2018
- totalItems: 0
2019
- };
2020
- }
2021
- if (options.autoApprove) {
2022
- writeLine(
2023
- options.stdout,
2024
- pc3.yellow(
2025
- `Auto-dismissing triage items: ${triageItems.map((item) => item.feature.id).join(", ")}`
2026
- )
2027
- );
2028
- await options.apiClient.request({
2029
- body: {
2030
- items: triageItems.map((item) => ({
2031
- issueId: item.issueId,
2032
- rationale: AUTO_DISMISS_RATIONALE
2033
- }))
2034
- },
2035
- method: "POST",
2036
- path: `/missions/${options.missionId}/triage/dismiss`
2037
- });
2038
- return {
2039
- createdFixes: 0,
2040
- dismissed: triageItems.length,
2041
- handledFeatureIds: triageItems.map((item) => item.feature.id),
2042
- movedAssertions: 0,
2043
- skipped: 0,
2044
2149
  totalItems: triageItems.length
2045
2150
  };
2046
- }
2047
- const result = {
2048
- createdFixes: 0,
2049
- dismissed: 0,
2050
- handledFeatureIds: [],
2051
- movedAssertions: 0,
2052
- skipped: 0,
2053
- totalItems: triageItems.length
2054
- };
2055
- const mutableFeatures = [...features];
2056
- for (const item of triageItems) {
2057
- writeItemBlock(options.stdout, item);
2058
- const action = await promptSelect({
2059
- choices: buildActionChoices(item),
2060
- message: `Choose triage action for ${item.feature.id}`
2061
- });
2062
- switch (action) {
2063
- case "dismiss": {
2064
- const rationale = await promptInput({
2065
- message: `Why are you dismissing triage for ${item.feature.id}?`
2066
- });
2067
- await dismissItem(options.apiClient, options.missionId, item, rationale);
2068
- result.dismissed += 1;
2069
- writeLine(options.stdout, `Dismissed triage for ${pc3.cyan(item.feature.id)}.`);
2070
- break;
2071
- }
2072
- case "create-fix": {
2073
- const createdFeature = await createFixFeature(
2074
- options.apiClient,
2075
- options.missionId,
2076
- item,
2077
- mutableFeatures
2078
- );
2079
- mutableFeatures.push({
2080
- attemptCount: 0,
2081
- description: createdFeature.description,
2082
- expectedBehavior: [...createdFeature.expectedBehavior],
2083
- fulfills: [],
2084
- id: createdFeature.id,
2085
- isFixFeature: true,
2086
- milestone: createdFeature.milestone,
2087
- preconditions: [...createdFeature.preconditions],
2088
- skillName: createdFeature.skillName,
2089
- status: "pending",
2090
- verificationSteps: [...createdFeature.verificationSteps],
2091
- workerSessionIds: []
2092
- });
2093
- result.createdFixes += 1;
2094
- writeLine(
2095
- options.stdout,
2096
- `Created fix feature ${pc3.cyan(createdFeature.id)} for ${pc3.cyan(item.feature.id)}.`
2097
- );
2098
- break;
2099
- }
2100
- case "reassign-assertions": {
2101
- const moved = await reassignAssertions(
2102
- options.apiClient,
2103
- options.missionId,
2104
- item,
2105
- mutableFeatures,
2106
- promptSelect
2107
- );
2108
- if (moved === 0) {
2109
- result.skipped += 1;
2151
+ const mutableFeatures = [...features];
2152
+ for (const item of triageItems) {
2153
+ throwIfInterrupted(options.signal);
2154
+ writeItemBlock(options.stdout, item);
2155
+ const action = await promptSelect({
2156
+ choices: buildActionChoices(item),
2157
+ message: `Choose triage action for ${item.feature.id}`,
2158
+ signal: options.signal
2159
+ });
2160
+ throwIfInterrupted(options.signal);
2161
+ switch (action) {
2162
+ case "dismiss": {
2163
+ const rationale = await promptInput({
2164
+ message: `Why are you dismissing triage for ${item.feature.id}?`,
2165
+ signal: options.signal
2166
+ });
2167
+ await dismissItem(
2168
+ options.apiClient,
2169
+ options.missionId,
2170
+ item,
2171
+ rationale,
2172
+ options.signal
2173
+ );
2174
+ result.dismissed += 1;
2175
+ writeLine(options.stdout, `Dismissed triage for ${pc4.cyan(item.feature.id)}.`);
2176
+ break;
2177
+ }
2178
+ case "create-fix": {
2179
+ const createdFeature = await createFixFeature(
2180
+ options.apiClient,
2181
+ options.missionId,
2182
+ item,
2183
+ mutableFeatures,
2184
+ options.signal
2185
+ );
2186
+ mutableFeatures.push({
2187
+ attemptCount: 0,
2188
+ description: createdFeature.description,
2189
+ expectedBehavior: [...createdFeature.expectedBehavior],
2190
+ fulfills: [],
2191
+ id: createdFeature.id,
2192
+ isFixFeature: true,
2193
+ milestone: createdFeature.milestone,
2194
+ preconditions: [...createdFeature.preconditions],
2195
+ skillName: createdFeature.skillName,
2196
+ status: "pending",
2197
+ verificationSteps: [...createdFeature.verificationSteps],
2198
+ workerSessionIds: []
2199
+ });
2200
+ result.createdFixes += 1;
2110
2201
  writeLine(
2111
2202
  options.stdout,
2112
- pc3.yellow(
2113
- `No assertions were reassigned for ${pc3.cyan(item.feature.id)}.`
2114
- )
2203
+ `Created fix feature ${pc4.cyan(createdFeature.id)} for ${pc4.cyan(item.feature.id)}.`
2115
2204
  );
2116
2205
  break;
2117
2206
  }
2118
- result.movedAssertions += moved;
2119
- writeLine(
2120
- options.stdout,
2121
- `Reassigned ${moved} assertion${moved === 1 ? "" : "s"} for ${pc3.cyan(item.feature.id)}.`
2122
- );
2123
- break;
2207
+ case "reassign-assertions": {
2208
+ const moved = await reassignAssertions(
2209
+ options.apiClient,
2210
+ options.missionId,
2211
+ item,
2212
+ mutableFeatures,
2213
+ promptSelect,
2214
+ options.signal
2215
+ );
2216
+ if (moved === 0) {
2217
+ result.skipped += 1;
2218
+ writeLine(
2219
+ options.stdout,
2220
+ pc4.yellow(
2221
+ `No assertions were reassigned for ${pc4.cyan(item.feature.id)}.`
2222
+ )
2223
+ );
2224
+ break;
2225
+ }
2226
+ result.movedAssertions += moved;
2227
+ writeLine(
2228
+ options.stdout,
2229
+ `Reassigned ${moved} assertion${moved === 1 ? "" : "s"} for ${pc4.cyan(item.feature.id)}.`
2230
+ );
2231
+ break;
2232
+ }
2233
+ case "skip":
2234
+ default:
2235
+ result.skipped += 1;
2236
+ writeLine(
2237
+ options.stdout,
2238
+ pc4.dim(`Skipped triage for ${item.feature.id}; it will remain unresolved.`)
2239
+ );
2240
+ break;
2124
2241
  }
2125
- case "skip":
2126
- default:
2127
- result.skipped += 1;
2128
- writeLine(
2129
- options.stdout,
2130
- pc3.dim(`Skipped triage for ${item.feature.id}; it will remain unresolved.`)
2131
- );
2132
- break;
2242
+ result.handledFeatureIds.push(item.feature.id);
2243
+ }
2244
+ return result;
2245
+ } catch (error) {
2246
+ if (options.signal?.aborted || isInterruptedError(error)) {
2247
+ throw new CliError("Triage interrupted.", 130);
2133
2248
  }
2134
- result.handledFeatureIds.push(item.feature.id);
2249
+ throw error;
2135
2250
  }
2136
- return result;
2137
2251
  }
2138
2252
 
2139
2253
  // src/monitor.ts
@@ -2226,7 +2340,19 @@ function formatSummaryLine(summary) {
2226
2340
  return `${summary.completedFeatures}/${summary.totalFeatures} features \xB7 ${summary.passedAssertions}/${summary.totalAssertions} assertions`;
2227
2341
  }
2228
2342
  function formatSpinnerText(missionId, summary) {
2229
- return `${pc4.cyan(missionId)} ${pc4.bold(summary.state)} \xB7 ${formatSummaryLine(summary)}`;
2343
+ return `${pc5.cyan(missionId)} ${pc5.bold(summary.state)} \xB7 ${formatSummaryLine(summary)}`;
2344
+ }
2345
+ function shouldShowMilestoneUpdate(status, verbose) {
2346
+ if (verbose) {
2347
+ return true;
2348
+ }
2349
+ return status === "sealed" || status === "completed";
2350
+ }
2351
+ function shouldShowFeatureUpdate(status, verbose) {
2352
+ if (verbose) {
2353
+ return true;
2354
+ }
2355
+ return status === "completed" || status === "failed" || status === "needs_triage";
2230
2356
  }
2231
2357
  function normalizeSummary(missionId, value) {
2232
2358
  if (!isRecord4(value)) {
@@ -2289,13 +2415,13 @@ function buildBackfillDescription(event) {
2289
2415
  const milestone = asNonEmptyString4(data.milestone);
2290
2416
  const assertionId = asNonEmptyString4(data.assertionId);
2291
2417
  if (featureId) {
2292
- return `Backfilled ${type} for ${pc4.cyan(featureId)}.`;
2418
+ return `Backfilled ${type} for ${pc5.cyan(featureId)}.`;
2293
2419
  }
2294
2420
  if (assertionId) {
2295
- return `Backfilled ${type} for ${pc4.cyan(assertionId)}.`;
2421
+ return `Backfilled ${type} for ${pc5.cyan(assertionId)}.`;
2296
2422
  }
2297
2423
  if (milestone) {
2298
- return `Backfilled ${type} for milestone ${pc4.cyan(milestone)}.`;
2424
+ return `Backfilled ${type} for milestone ${pc5.cyan(milestone)}.`;
2299
2425
  }
2300
2426
  return `Backfilled ${type}.`;
2301
2427
  }
@@ -2334,13 +2460,36 @@ async function fetchSummary(apiClient, missionId) {
2334
2460
  const response = await apiClient.request({
2335
2461
  path: `/missions/${missionId}/progress/summary`
2336
2462
  });
2337
- return normalizeSummary(missionId, response);
2463
+ const summary = normalizeSummary(missionId, response);
2464
+ try {
2465
+ const authoritativeState = normalizeMissionState(
2466
+ await apiClient.request({
2467
+ path: `/missions/${missionId}/mission/state`
2468
+ })
2469
+ );
2470
+ return {
2471
+ ...summary,
2472
+ completedFeatures: authoritativeState.completedFeatures,
2473
+ currentFeatureId: authoritativeState.currentFeatureId,
2474
+ currentWorkerSessionId: authoritativeState.currentWorkerSessionId,
2475
+ lastEventAt: authoritativeState.updatedAt,
2476
+ state: authoritativeState.state,
2477
+ passedAssertions: authoritativeState.passedAssertions,
2478
+ sealedMilestones: authoritativeState.sealedMilestones,
2479
+ totalAssertions: authoritativeState.totalAssertions,
2480
+ totalFeatures: authoritativeState.totalFeatures,
2481
+ totalMilestones: authoritativeState.totalMilestones
2482
+ };
2483
+ } catch {
2484
+ return summary;
2485
+ }
2338
2486
  }
2339
2487
  async function monitorMission(options) {
2340
2488
  const createSpinner = options.createSpinner ?? defaultCreateSpinner;
2341
2489
  const createWebSocket = options.createWebSocket ?? defaultCreateWebSocket;
2342
2490
  const registerSignalHandler = options.registerSignalHandler ?? defaultRegisterSignalHandler;
2343
2491
  const sleep = options.sleep ?? ((ms) => delay2(ms).then(() => void 0));
2492
+ const verbose = options.verbose === true;
2344
2493
  let summary = await fetchSummary(options.apiClient, options.missionId);
2345
2494
  let lastSeen = summary.head;
2346
2495
  let pausedForBudget = false;
@@ -2348,7 +2497,9 @@ async function monitorMission(options) {
2348
2497
  let reconnectAttempt = 0;
2349
2498
  let connectedOnce = false;
2350
2499
  let interrupted = false;
2500
+ let interruptionReported = false;
2351
2501
  let triageInFlight = false;
2502
+ const interruptionController = new AbortController();
2352
2503
  let activeSocket = null;
2353
2504
  let settleActiveConnection = null;
2354
2505
  let startInFlight = false;
@@ -2356,7 +2507,7 @@ async function monitorMission(options) {
2356
2507
  spinner.start();
2357
2508
  writeLine(
2358
2509
  options.stdout,
2359
- `Monitoring mission ${pc4.cyan(options.missionId)} \xB7 ${formatSummaryLine(summary)}`
2510
+ `Monitoring mission ${pc5.cyan(options.missionId)} \xB7 ${formatSummaryLine(summary)}`
2360
2511
  );
2361
2512
  const updateSpinner = () => {
2362
2513
  spinner.text = formatSpinnerText(options.missionId, summary);
@@ -2371,7 +2522,7 @@ async function monitorMission(options) {
2371
2522
  await refreshSummary();
2372
2523
  const message = `Mission completed \xB7 ${formatSummaryLine(summary)}`;
2373
2524
  spinner.succeed(message);
2374
- writeLine(options.stdout, pc4.green(message));
2525
+ writeLine(options.stdout, pc5.green(message));
2375
2526
  return {
2376
2527
  exitCode: 0,
2377
2528
  reason: "completed",
@@ -2383,7 +2534,7 @@ async function monitorMission(options) {
2383
2534
  await refreshSummary();
2384
2535
  const message = `Budget exceeded \xB7 ${formatBudgetUsage(details)}`;
2385
2536
  spinner.fail(message);
2386
- writeLine(options.stdout, pc4.red(message));
2537
+ writeLine(options.stdout, pc5.red(message));
2387
2538
  return {
2388
2539
  exitCode: 1,
2389
2540
  reason: "budget_exceeded",
@@ -2402,21 +2553,27 @@ async function monitorMission(options) {
2402
2553
  missionId: options.missionId,
2403
2554
  promptInput: options.promptInput,
2404
2555
  promptSelect: options.promptSelect,
2556
+ signal: interruptionController.signal,
2405
2557
  stdout: options.stdout
2406
2558
  });
2407
2559
  if (triageResult.totalItems === 0) {
2408
2560
  writeLine(
2409
2561
  options.stdout,
2410
- pc4.dim("No triage items required action. Resuming the orchestration loop.")
2562
+ pc5.dim("No triage items required action. Resuming the orchestration loop.")
2411
2563
  );
2412
2564
  } else {
2413
2565
  writeLine(
2414
2566
  options.stdout,
2415
- pc4.green(
2567
+ pc5.green(
2416
2568
  `Triage complete \xB7 ${triageResult.dismissed} dismissed \xB7 ${triageResult.createdFixes} fix features \xB7 ${triageResult.movedAssertions} assertions moved \xB7 ${triageResult.skipped} skipped`
2417
2569
  )
2418
2570
  );
2419
2571
  }
2572
+ } catch (error) {
2573
+ if (interrupted && error instanceof CliError && error.exitCode === 130) {
2574
+ return interruptedResult();
2575
+ }
2576
+ throw error;
2420
2577
  } finally {
2421
2578
  triageInFlight = false;
2422
2579
  pausedForTriage = false;
@@ -2425,11 +2582,14 @@ async function monitorMission(options) {
2425
2582
  return maybeAutoStart();
2426
2583
  };
2427
2584
  const interruptedResult = () => {
2428
- spinner.fail("Monitoring stopped.");
2429
- writeLine(
2430
- options.stdout,
2431
- pc4.yellow("Interrupted. Local state is saved. Remote mission continues.")
2432
- );
2585
+ if (!interruptionReported) {
2586
+ interruptionReported = true;
2587
+ spinner.fail("Monitoring stopped.");
2588
+ writeLine(
2589
+ options.stdout,
2590
+ pc5.yellow("Interrupted. Local state is saved. Remote mission continues.")
2591
+ );
2592
+ }
2433
2593
  return {
2434
2594
  exitCode: 130,
2435
2595
  reason: "interrupted",
@@ -2445,16 +2605,20 @@ async function monitorMission(options) {
2445
2605
  const result = await startMission(options.apiClient, options.missionId);
2446
2606
  switch (result.type) {
2447
2607
  case "dispatched":
2448
- writeLine(
2449
- options.stdout,
2450
- `Dispatching ${pc4.cyan(result.featureId)} (${pc4.dim(result.workerSessionId)}).`
2451
- );
2608
+ if (verbose) {
2609
+ writeLine(
2610
+ options.stdout,
2611
+ `Dispatching ${pc5.cyan(result.featureId)} (${pc5.dim(result.workerSessionId)}).`
2612
+ );
2613
+ }
2452
2614
  return null;
2453
2615
  case "no_work":
2454
- writeLine(
2455
- options.stdout,
2456
- pc4.dim("No eligible work is available yet. Waiting for updates.")
2457
- );
2616
+ if (verbose) {
2617
+ writeLine(
2618
+ options.stdout,
2619
+ pc5.dim("No eligible work is available yet. Waiting for updates.")
2620
+ );
2621
+ }
2458
2622
  return null;
2459
2623
  case "completed":
2460
2624
  return completedResult();
@@ -2467,6 +2631,9 @@ async function monitorMission(options) {
2467
2631
  };
2468
2632
  const unregisterSignalHandler = registerSignalHandler("SIGINT", () => {
2469
2633
  interrupted = true;
2634
+ if (!interruptionController.signal.aborted) {
2635
+ interruptionController.abort();
2636
+ }
2470
2637
  safeCloseSocket(activeSocket, 1e3, "SIGINT");
2471
2638
  settleActiveConnection?.(interruptedResult());
2472
2639
  });
@@ -2492,12 +2659,14 @@ async function monitorMission(options) {
2492
2659
  }
2493
2660
  const delayMs2 = toReconnectDelay(reconnectAttempt);
2494
2661
  reconnectAttempt += 1;
2495
- writeLine(
2496
- options.stdout,
2497
- pc4.yellow(
2498
- `WebSocket connection failed: ${error instanceof Error ? error.message : String(error)}. Reconnecting in ${delayMs2} ms...`
2499
- )
2500
- );
2662
+ if (verbose) {
2663
+ writeLine(
2664
+ options.stdout,
2665
+ pc5.yellow(
2666
+ `WebSocket connection failed: ${error instanceof Error ? error.message : String(error)}. Reconnecting in ${delayMs2} ms...`
2667
+ )
2668
+ );
2669
+ }
2501
2670
  await sleep(delayMs2);
2502
2671
  continue;
2503
2672
  }
@@ -2529,7 +2698,9 @@ async function monitorMission(options) {
2529
2698
  switch (type) {
2530
2699
  case "state_changed": {
2531
2700
  const state = asNonEmptyString4(data.state) ?? "unknown";
2532
- writeLine(options.stdout, `Mission state \u2192 ${pc4.cyan(state)}`);
2701
+ if (verbose) {
2702
+ writeLine(options.stdout, `Mission state \u2192 ${pc5.cyan(state)}`);
2703
+ }
2533
2704
  await refreshSummary();
2534
2705
  if (state === "completed") {
2535
2706
  return completedResult();
@@ -2545,40 +2716,48 @@ async function monitorMission(options) {
2545
2716
  if (status === "needs_triage") {
2546
2717
  pausedForTriage = true;
2547
2718
  }
2548
- writeLine(
2549
- options.stdout,
2550
- `Feature ${pc4.cyan(featureId)} \u2192 ${pc4.bold(status)}`
2551
- );
2719
+ if (shouldShowFeatureUpdate(status, verbose)) {
2720
+ writeLine(
2721
+ options.stdout,
2722
+ `Feature ${pc5.cyan(featureId)} \u2192 ${pc5.bold(status)}`
2723
+ );
2724
+ }
2552
2725
  await refreshSummary();
2553
2726
  return null;
2554
2727
  }
2555
2728
  case "milestone_updated": {
2556
2729
  const milestone = asNonEmptyString4(data.milestone) ?? "milestone";
2557
2730
  const status = asNonEmptyString4(data.status) ?? "updated";
2558
- writeLine(
2559
- options.stdout,
2560
- `Milestone ${pc4.cyan(milestone)} \u2192 ${pc4.bold(status)}`
2561
- );
2731
+ if (shouldShowMilestoneUpdate(status, verbose)) {
2732
+ writeLine(
2733
+ options.stdout,
2734
+ `Milestone ${pc5.cyan(milestone)} \u2192 ${pc5.bold(status)}`
2735
+ );
2736
+ }
2562
2737
  await refreshSummary();
2563
2738
  return null;
2564
2739
  }
2565
2740
  case "assertion_updated": {
2566
2741
  const assertionId = asNonEmptyString4(data.assertionId) ?? "assertion";
2567
2742
  const status = asNonEmptyString4(data.status) ?? "updated";
2568
- writeLine(
2569
- options.stdout,
2570
- `Assertion ${pc4.cyan(assertionId)} \u2192 ${pc4.bold(status)}`
2571
- );
2743
+ if (verbose) {
2744
+ writeLine(
2745
+ options.stdout,
2746
+ `Assertion ${pc5.cyan(assertionId)} \u2192 ${pc5.bold(status)}`
2747
+ );
2748
+ }
2572
2749
  await refreshSummary();
2573
2750
  return null;
2574
2751
  }
2575
2752
  case "handoff_received": {
2576
2753
  const featureId = asNonEmptyString4(data.featureId) ?? "feature";
2577
2754
  const status = asNonEmptyString4(data.status) ?? "received";
2578
- writeLine(
2579
- options.stdout,
2580
- `Handoff received for ${pc4.cyan(featureId)} (${status}).`
2581
- );
2755
+ if (verbose) {
2756
+ writeLine(
2757
+ options.stdout,
2758
+ `Handoff received for ${pc5.cyan(featureId)} (${status}).`
2759
+ );
2760
+ }
2582
2761
  return null;
2583
2762
  }
2584
2763
  case "triage_needed": {
@@ -2586,8 +2765,8 @@ async function monitorMission(options) {
2586
2765
  const featureId = asNonEmptyString4(data.featureId) ?? "feature";
2587
2766
  writeLine(
2588
2767
  options.stdout,
2589
- pc4.yellow(
2590
- `Triage is required for ${pc4.cyan(featureId)}. Auto-start is paused.`
2768
+ pc5.yellow(
2769
+ `Triage is required for ${pc5.cyan(featureId)}. Auto-start is paused.`
2591
2770
  )
2592
2771
  );
2593
2772
  return completeTriage();
@@ -2596,7 +2775,7 @@ async function monitorMission(options) {
2596
2775
  const featureId = asNonEmptyString4(data.featureId) ?? "feature";
2597
2776
  writeLine(
2598
2777
  options.stdout,
2599
- pc4.yellow(`Worker timed out for ${pc4.cyan(featureId)}.`)
2778
+ pc5.yellow(`Worker timed out for ${pc5.cyan(featureId)}.`)
2600
2779
  );
2601
2780
  await refreshSummary();
2602
2781
  return null;
@@ -2605,7 +2784,7 @@ async function monitorMission(options) {
2605
2784
  const details = normalizeBudgetDetails(data);
2606
2785
  writeLine(
2607
2786
  options.stdout,
2608
- pc4.yellow(`Budget warning \xB7 ${formatBudgetUsage(details)}`)
2787
+ pc5.yellow(`Budget warning \xB7 ${formatBudgetUsage(details)}`)
2609
2788
  );
2610
2789
  await refreshSummary();
2611
2790
  return null;
@@ -2629,23 +2808,27 @@ async function monitorMission(options) {
2629
2808
  safeCloseSocket(socket, 1e3, result.reason);
2630
2809
  resolveOnce(result);
2631
2810
  }).catch((error) => {
2632
- writeLine(
2633
- options.stdout,
2634
- pc4.yellow(
2635
- `Monitoring update failed: ${error instanceof Error ? error.message : String(error)}. Reconnecting...`
2636
- )
2637
- );
2811
+ if (verbose) {
2812
+ writeLine(
2813
+ options.stdout,
2814
+ pc5.yellow(
2815
+ `Monitoring update failed: ${error instanceof Error ? error.message : String(error)}. Reconnecting...`
2816
+ )
2817
+ );
2818
+ }
2638
2819
  safeCloseSocket(socket, 1011, "processing_error");
2639
2820
  resolveOnce("reconnect");
2640
2821
  });
2641
2822
  });
2642
2823
  socket.on("error", (error) => {
2643
- writeLine(
2644
- options.stdout,
2645
- pc4.yellow(
2646
- `WebSocket error: ${error instanceof Error ? error.message : String(error)}`
2647
- )
2648
- );
2824
+ if (verbose) {
2825
+ writeLine(
2826
+ options.stdout,
2827
+ pc5.yellow(
2828
+ `WebSocket error: ${error instanceof Error ? error.message : String(error)}`
2829
+ )
2830
+ );
2831
+ }
2649
2832
  safeCloseSocket(socket, 1011, "socket_error");
2650
2833
  });
2651
2834
  socket.on("close", () => {
@@ -2670,7 +2853,9 @@ async function monitorMission(options) {
2670
2853
  continue;
2671
2854
  }
2672
2855
  lastSeen = Math.max(lastSeen, asFiniteNumber3(event.seq));
2673
- writeLine(options.stdout, pc4.dim(buildBackfillDescription(event)));
2856
+ if (verbose) {
2857
+ writeLine(options.stdout, pc5.dim(buildBackfillDescription(event)));
2858
+ }
2674
2859
  }
2675
2860
  }
2676
2861
  connectedOnce = true;
@@ -2692,13 +2877,15 @@ async function monitorMission(options) {
2692
2877
  }
2693
2878
  const delayMs = toReconnectDelay(reconnectAttempt);
2694
2879
  reconnectAttempt += 1;
2695
- writeLine(
2696
- options.stdout,
2697
- pc4.yellow(`Reconnecting to mission updates in ${delayMs} ms...`)
2698
- );
2699
- await sleep(delayMs);
2700
- }
2701
- } finally {
2880
+ if (verbose) {
2881
+ writeLine(
2882
+ options.stdout,
2883
+ pc5.yellow(`Reconnecting to mission updates in ${delayMs} ms...`)
2884
+ );
2885
+ }
2886
+ await sleep(delayMs);
2887
+ }
2888
+ } finally {
2702
2889
  const unregister = typeof unregisterSignalHandler === "function" ? unregisterSignalHandler : void 0;
2703
2890
  unregister?.();
2704
2891
  }
@@ -2707,7 +2894,7 @@ async function monitorMission(options) {
2707
2894
  // src/commands/mission-id.ts
2708
2895
  import { readdir as readdir2, readFile as readFile3, stat } from "fs/promises";
2709
2896
  import path3 from "path";
2710
- import pc5 from "picocolors";
2897
+ import pc6 from "picocolors";
2711
2898
  function asNonEmptyString5(value) {
2712
2899
  return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
2713
2900
  }
@@ -2767,7 +2954,7 @@ async function resolveMissionId(missionId, context, command, homeDir) {
2767
2954
  if (detectedMissionId) {
2768
2955
  writeLine(
2769
2956
  context.stdout,
2770
- `Using the most recent local mission: ${pc5.cyan(detectedMissionId)}`
2957
+ `Using the most recent local mission: ${pc6.cyan(detectedMissionId)}`
2771
2958
  );
2772
2959
  return detectedMissionId;
2773
2960
  }
@@ -2789,14 +2976,46 @@ function asFiniteNumber4(value) {
2789
2976
  return typeof value === "number" && Number.isFinite(value) ? value : null;
2790
2977
  }
2791
2978
  async function defaultPromptConfirm(options) {
2792
- return defaultConfirmPrompt({
2793
- default: options.default,
2794
- message: options.message
2979
+ return defaultConfirmPrompt(
2980
+ {
2981
+ default: options.default,
2982
+ message: options.message
2983
+ },
2984
+ { signal: options.signal }
2985
+ );
2986
+ }
2987
+ function createLifecycleInterruptContext(registerSignalHandler, stdout, message) {
2988
+ const coordinatedRegisterSignalHandler = createSignalHandlerCoordinator(
2989
+ registerSignalHandler
2990
+ );
2991
+ const interruptState = {
2992
+ abortController: new AbortController(),
2993
+ interrupted: false
2994
+ };
2995
+ const unregisterSignalHandler = coordinatedRegisterSignalHandler?.("SIGINT", () => {
2996
+ if (interruptState.interrupted) {
2997
+ return;
2998
+ }
2999
+ interruptState.interrupted = true;
3000
+ if (!interruptState.abortController.signal.aborted) {
3001
+ interruptState.abortController.abort();
3002
+ }
3003
+ writeLine(stdout, pc7.yellow(message));
2795
3004
  });
3005
+ return {
3006
+ interruptState,
3007
+ registerSignalHandler: coordinatedRegisterSignalHandler,
3008
+ unregisterSignalHandler
3009
+ };
3010
+ }
3011
+ function throwIfInterrupted2(interruptState, commandName) {
3012
+ if (interruptState.interrupted) {
3013
+ throw new CommanderError2(130, commandName, "");
3014
+ }
2796
3015
  }
2797
3016
  function writeSection(stdout, title) {
2798
3017
  writeLine(stdout);
2799
- writeLine(stdout, pc6.bold(title));
3018
+ writeLine(stdout, pc7.bold(title));
2800
3019
  }
2801
3020
  function summarizeAssertions(assertions) {
2802
3021
  return assertions.reduce(
@@ -2847,19 +3066,20 @@ function renderMissionSnapshot(stdout, snapshot) {
2847
3066
  );
2848
3067
  }
2849
3068
  }
2850
- async function fetchMissionStateRecord(apiClient, missionId) {
3069
+ async function fetchMissionStateRecord(apiClient, missionId, signal) {
2851
3070
  return normalizeMissionState(
2852
3071
  await apiClient.request({
2853
- path: `/missions/${missionId}/mission/state`
3072
+ path: `/missions/${missionId}/mission/state`,
3073
+ signal
2854
3074
  })
2855
3075
  );
2856
3076
  }
2857
- async function fetchMissionSnapshot(apiClient, missionId) {
3077
+ async function fetchMissionSnapshot(apiClient, missionId, signal) {
2858
3078
  const [state, features, milestones, assertions] = await Promise.all([
2859
- fetchMissionStateRecord(apiClient, missionId),
2860
- apiClient.request({ path: `/missions/${missionId}/features` }).then((value) => normalizeMissionFeatures(value)),
2861
- apiClient.request({ path: `/missions/${missionId}/milestones` }).then((value) => normalizeMissionMilestones(value)),
2862
- apiClient.request({ path: `/missions/${missionId}/assertions` }).then((value) => normalizeMissionAssertions(value))
3079
+ fetchMissionStateRecord(apiClient, missionId, signal),
3080
+ apiClient.request({ path: `/missions/${missionId}/features`, signal }).then((value) => normalizeMissionFeatures(value)),
3081
+ apiClient.request({ path: `/missions/${missionId}/milestones`, signal }).then((value) => normalizeMissionMilestones(value)),
3082
+ apiClient.request({ path: `/missions/${missionId}/assertions`, signal }).then((value) => normalizeMissionAssertions(value))
2863
3083
  ]);
2864
3084
  return {
2865
3085
  assertions,
@@ -2885,16 +3105,16 @@ async function createMissionClientContext(context, command, dependencies, homeDi
2885
3105
  homeDir
2886
3106
  };
2887
3107
  }
2888
- async function fetchMissionStateAfterConflict(apiClient, missionId, error) {
3108
+ async function fetchMissionStateAfterConflict(apiClient, missionId, error, signal) {
2889
3109
  try {
2890
- return await fetchMissionStateRecord(apiClient, missionId);
3110
+ return await fetchMissionStateRecord(apiClient, missionId, signal);
2891
3111
  } catch {
2892
3112
  throw error;
2893
3113
  }
2894
3114
  }
2895
- async function fetchMissionSnapshotAfterConflict(apiClient, missionId, error) {
3115
+ async function fetchMissionSnapshotAfterConflict(apiClient, missionId, error, signal) {
2896
3116
  try {
2897
- return await fetchMissionSnapshot(apiClient, missionId);
3117
+ return await fetchMissionSnapshot(apiClient, missionId, signal);
2898
3118
  } catch {
2899
3119
  throw error;
2900
3120
  }
@@ -2949,11 +3169,11 @@ function normalizeMissionDeleteResponse(value) {
2949
3169
  function formatMissionDeleteStatus(status) {
2950
3170
  switch (status) {
2951
3171
  case "success":
2952
- return pc6.green(status);
3172
+ return pc7.green(status);
2953
3173
  case "failed":
2954
- return pc6.red(status);
3174
+ return pc7.red(status);
2955
3175
  case "skipped":
2956
- return pc6.yellow(status);
3176
+ return pc7.yellow(status);
2957
3177
  default:
2958
3178
  return status;
2959
3179
  }
@@ -3022,7 +3242,7 @@ function renderMissionDeleteResponse(stdout, response) {
3022
3242
  if (failedSteps.length > 0) {
3023
3243
  writeLine(
3024
3244
  stdout,
3025
- pc6.yellow(
3245
+ pc7.yellow(
3026
3246
  `Warning: Mission cleanup completed with cleanup warnings (${failedSteps.length} failed step${failedSteps.length === 1 ? "" : "s"}).`
3027
3247
  )
3028
3248
  );
@@ -3047,39 +3267,58 @@ async function runLifecycleMutation(action, missionIdValue, command, context, de
3047
3267
  dependencies,
3048
3268
  homeDir
3049
3269
  );
3270
+ const {
3271
+ interruptState,
3272
+ unregisterSignalHandler
3273
+ } = createLifecycleInterruptContext(
3274
+ dependencies.registerSignalHandler,
3275
+ context.stdout,
3276
+ "Interrupted. Remote mission state is preserved. Re-run this command to check or retry the lifecycle change."
3277
+ );
3050
3278
  try {
3051
3279
  const response = await apiClient.request({
3052
3280
  body: {},
3053
3281
  method: "POST",
3054
- path: `/missions/${missionId}/mission/${action}`
3282
+ path: `/missions/${missionId}/mission/${action}`,
3283
+ signal: interruptState.abortController.signal
3055
3284
  });
3285
+ throwIfInterrupted2(interruptState, `mission-${action}`);
3056
3286
  const nextState = asNonEmptyString6(response.state) ?? (action === "pause" ? "paused" : "cancelled");
3057
3287
  if (action === "pause") {
3058
3288
  writeLine(
3059
3289
  context.stdout,
3060
- `Mission ${pc6.cyan(missionId)} paused. Current state: ${pc6.bold(nextState)}.`
3290
+ `Mission ${pc7.cyan(missionId)} paused. Current state: ${pc7.bold(nextState)}.`
3061
3291
  );
3062
3292
  return;
3063
3293
  }
3064
3294
  writeLine(
3065
3295
  context.stdout,
3066
- `Mission ${pc6.cyan(missionId)} cancelled. This action is irreversible. Current state: ${pc6.bold(nextState)}.`
3296
+ `Mission ${pc7.cyan(missionId)} cancelled. This action is irreversible. Current state: ${pc7.bold(nextState)}.`
3067
3297
  );
3068
3298
  } catch (error) {
3299
+ if (interruptState.interrupted && isInterruptedError(error)) {
3300
+ throw new CommanderError2(130, `mission-${action}`, "");
3301
+ }
3069
3302
  if (!(error instanceof ApiError) || error.status !== 409) {
3070
3303
  throw error;
3071
3304
  }
3072
3305
  const missionState = await fetchMissionStateAfterConflict(
3073
3306
  apiClient,
3074
3307
  missionId,
3075
- error
3308
+ error,
3309
+ interruptState.abortController.signal
3076
3310
  );
3311
+ throwIfInterrupted2(interruptState, `mission-${action}`);
3077
3312
  const verb = action === "pause" ? "paused" : "cancelled";
3078
3313
  writeLine(
3079
3314
  context.stdout,
3080
- `Mission ${pc6.cyan(missionId)} cannot be ${verb} because it is currently ${pc6.bold(missionState.state)}.`
3315
+ `Mission ${pc7.cyan(missionId)} cannot be ${verb} because it is currently ${pc7.bold(missionState.state)}.`
3081
3316
  );
3082
- throw new CommanderError(1, `mission-${action}`, "");
3317
+ throw new CommanderError2(1, `mission-${action}`, "");
3318
+ } finally {
3319
+ if (typeof unregisterSignalHandler === "function") {
3320
+ unregisterSignalHandler();
3321
+ }
3083
3322
  }
3084
3323
  }
3085
3324
  function buildDeleteConfirmationMessage(missionId, forceDelete) {
@@ -3094,33 +3333,56 @@ function buildDeleteConfirmationMessage(missionId, forceDelete) {
3094
3333
  async function runMissionDelete(missionIdValue, options, command, context, dependencies) {
3095
3334
  const homeDir = resolveMissionCommandHomeDir(context);
3096
3335
  const missionId = await resolveMissionId(missionIdValue, context, command, homeDir);
3097
- if (options.yes !== true) {
3098
- const confirmed = await (dependencies.promptConfirm ?? defaultPromptConfirm)({
3099
- default: false,
3100
- message: buildDeleteConfirmationMessage(missionId, options.force === true)
3101
- });
3102
- if (!confirmed) {
3103
- writeLine(context.stdout, pc6.yellow("Mission deletion cancelled."));
3104
- return;
3105
- }
3106
- }
3107
- const { apiClient } = await createMissionClientContext(
3108
- context,
3109
- command,
3110
- dependencies,
3111
- homeDir
3112
- );
3113
- const query = options.force === true ? "?force=true" : "";
3114
- const response = normalizeMissionDeleteResponse(
3115
- await apiClient.request({
3116
- method: "DELETE",
3117
- path: `/missions/${missionId}${query}`
3118
- })
3336
+ const {
3337
+ interruptState,
3338
+ unregisterSignalHandler
3339
+ } = createLifecycleInterruptContext(
3340
+ dependencies.registerSignalHandler,
3341
+ context.stdout,
3342
+ "Interrupted. Remote mission state is preserved. Mission deletion was not attempted."
3119
3343
  );
3120
- if (!response.deleted) {
3121
- throw new CliError(`Mission ${missionId} was not deleted.`, 1);
3344
+ try {
3345
+ if (options.yes !== true) {
3346
+ const confirmed = await (dependencies.promptConfirm ?? defaultPromptConfirm)({
3347
+ default: false,
3348
+ message: buildDeleteConfirmationMessage(missionId, options.force === true),
3349
+ signal: interruptState.abortController.signal
3350
+ });
3351
+ throwIfInterrupted2(interruptState, "mission-delete");
3352
+ if (!confirmed) {
3353
+ writeLine(context.stdout, pc7.yellow("Mission deletion cancelled."));
3354
+ return;
3355
+ }
3356
+ }
3357
+ const { apiClient } = await createMissionClientContext(
3358
+ context,
3359
+ command,
3360
+ dependencies,
3361
+ homeDir
3362
+ );
3363
+ const query = options.force === true ? "?force=true" : "";
3364
+ const response = normalizeMissionDeleteResponse(
3365
+ await apiClient.request({
3366
+ method: "DELETE",
3367
+ path: `/missions/${missionId}${query}`,
3368
+ signal: interruptState.abortController.signal
3369
+ })
3370
+ );
3371
+ throwIfInterrupted2(interruptState, "mission-delete");
3372
+ if (!response.deleted) {
3373
+ throw new CliError(`Mission ${missionId} was not deleted.`, 1);
3374
+ }
3375
+ renderMissionDeleteResponse(context.stdout, response);
3376
+ } catch (error) {
3377
+ if (interruptState.interrupted && isInterruptedError(error)) {
3378
+ throw new CommanderError2(130, "mission-delete", "");
3379
+ }
3380
+ throw error;
3381
+ } finally {
3382
+ if (typeof unregisterSignalHandler === "function") {
3383
+ unregisterSignalHandler();
3384
+ }
3122
3385
  }
3123
- renderMissionDeleteResponse(context.stdout, response);
3124
3386
  }
3125
3387
  function resolveResumeExitCode(state) {
3126
3388
  switch (state) {
@@ -3154,66 +3416,99 @@ async function runMissionResume(missionIdValue, command, context, dependencies)
3154
3416
  dependencies,
3155
3417
  homeDir
3156
3418
  );
3157
- let snapshot = await fetchMissionSnapshot(apiClient, missionId);
3158
- if (snapshot.state.state === "paused") {
3159
- try {
3160
- const response = await apiClient.request({
3161
- body: {},
3162
- method: "POST",
3163
- path: `/missions/${missionId}/mission/resume`
3164
- });
3165
- const nextState = asNonEmptyString6(response.state) ?? "orchestrator_turn";
3166
- writeLine(
3167
- context.stdout,
3168
- `Mission ${pc6.cyan(missionId)} resumed. Current state: ${pc6.bold(nextState)}.`
3169
- );
3170
- } catch (error) {
3171
- if (!(error instanceof ApiError) || error.status !== 409) {
3172
- throw error;
3419
+ const lifecycleSignals = createLifecycleInterruptContext(
3420
+ dependencies.registerSignalHandler,
3421
+ context.stdout,
3422
+ "Interrupted. Remote mission continues. Re-run this command to reconnect when ready."
3423
+ );
3424
+ try {
3425
+ let snapshot = await fetchMissionSnapshot(
3426
+ apiClient,
3427
+ missionId,
3428
+ lifecycleSignals.interruptState.abortController.signal
3429
+ );
3430
+ throwIfInterrupted2(lifecycleSignals.interruptState, "mission-resume");
3431
+ if (snapshot.state.state === "paused") {
3432
+ try {
3433
+ const response = await apiClient.request({
3434
+ body: {},
3435
+ method: "POST",
3436
+ path: `/missions/${missionId}/mission/resume`,
3437
+ signal: lifecycleSignals.interruptState.abortController.signal
3438
+ });
3439
+ throwIfInterrupted2(lifecycleSignals.interruptState, "mission-resume");
3440
+ const nextState = asNonEmptyString6(response.state) ?? "orchestrator_turn";
3441
+ writeLine(
3442
+ context.stdout,
3443
+ `Mission ${pc7.cyan(missionId)} resumed. Current state: ${pc7.bold(nextState)}.`
3444
+ );
3445
+ } catch (error) {
3446
+ if (lifecycleSignals.interruptState.interrupted && isInterruptedError(error)) {
3447
+ throw new CommanderError2(130, "mission-resume", "");
3448
+ }
3449
+ if (!(error instanceof ApiError) || error.status !== 409) {
3450
+ throw error;
3451
+ }
3452
+ snapshot = await fetchMissionSnapshotAfterConflict(
3453
+ apiClient,
3454
+ missionId,
3455
+ error,
3456
+ lifecycleSignals.interruptState.abortController.signal
3457
+ );
3458
+ throwIfInterrupted2(lifecycleSignals.interruptState, "mission-resume");
3459
+ writeLine(
3460
+ context.stdout,
3461
+ `Mission ${pc7.cyan(missionId)} is currently ${pc7.bold(snapshot.state.state)}. Reconnecting to live updates if available.`
3462
+ );
3173
3463
  }
3174
- snapshot = await fetchMissionSnapshotAfterConflict(
3464
+ snapshot = await fetchMissionSnapshot(
3175
3465
  apiClient,
3176
3466
  missionId,
3177
- error
3467
+ lifecycleSignals.interruptState.abortController.signal
3178
3468
  );
3469
+ throwIfInterrupted2(lifecycleSignals.interruptState, "mission-resume");
3470
+ } else if (snapshot.state.state !== "cancelled" && snapshot.state.state !== "completed") {
3179
3471
  writeLine(
3180
3472
  context.stdout,
3181
- `Mission ${pc6.cyan(missionId)} is currently ${pc6.bold(snapshot.state.state)}. Reconnecting to live updates if available.`
3473
+ `Reconnecting to mission ${pc7.cyan(missionId)} from state ${pc7.bold(snapshot.state.state)}.`
3182
3474
  );
3183
3475
  }
3184
- snapshot = await fetchMissionSnapshot(apiClient, missionId);
3185
- } else if (snapshot.state.state !== "cancelled" && snapshot.state.state !== "completed") {
3186
- writeLine(
3187
- context.stdout,
3188
- `Reconnecting to mission ${pc6.cyan(missionId)} from state ${pc6.bold(snapshot.state.state)}.`
3189
- );
3190
- }
3191
- renderMissionSnapshot(context.stdout, snapshot);
3192
- const resumeExitCode = resolveResumeExitCode(snapshot.state.state);
3193
- if (resumeExitCode === 0) {
3194
- return;
3195
- }
3196
- if (resumeExitCode === 1) {
3197
- throw new CliError(
3198
- `Mission ${missionId} is currently ${snapshot.state.state}.`,
3199
- 1
3200
- );
3201
- }
3202
- const monitorResult = await monitorMission({
3203
- apiClient,
3204
- apiKey: authConfig.apiKey,
3205
- createSpinner: dependencies.createSpinner,
3206
- createWebSocket: dependencies.createWebSocket,
3207
- endpoint: authConfig.endpoint,
3208
- missionId,
3209
- promptInput: dependencies.promptInput,
3210
- promptSelect: dependencies.promptSelect,
3211
- registerSignalHandler: dependencies.registerSignalHandler,
3212
- sleep: dependencies.sleep,
3213
- stdout: context.stdout
3214
- });
3215
- if (monitorResult.exitCode !== 0) {
3216
- throw new CommanderError(monitorResult.exitCode, "mission-monitor", "");
3476
+ renderMissionSnapshot(context.stdout, snapshot);
3477
+ const resumeExitCode = resolveResumeExitCode(snapshot.state.state);
3478
+ if (resumeExitCode === 0) {
3479
+ return;
3480
+ }
3481
+ if (resumeExitCode === 1) {
3482
+ throw new CliError(
3483
+ `Mission ${missionId} is currently ${snapshot.state.state}.`,
3484
+ 1
3485
+ );
3486
+ }
3487
+ const monitorResult = await monitorMission({
3488
+ apiClient,
3489
+ apiKey: authConfig.apiKey,
3490
+ createSpinner: dependencies.createSpinner,
3491
+ createWebSocket: dependencies.createWebSocket,
3492
+ endpoint: authConfig.endpoint,
3493
+ missionId,
3494
+ promptInput: dependencies.promptInput,
3495
+ promptSelect: dependencies.promptSelect,
3496
+ registerSignalHandler: lifecycleSignals.registerSignalHandler,
3497
+ sleep: dependencies.sleep,
3498
+ stdout: context.stdout
3499
+ });
3500
+ if (monitorResult.exitCode !== 0) {
3501
+ throw new CommanderError2(monitorResult.exitCode, "mission-monitor", "");
3502
+ }
3503
+ } catch (error) {
3504
+ if (lifecycleSignals.interruptState.interrupted && isInterruptedError(error)) {
3505
+ throw new CommanderError2(130, "mission-resume", "");
3506
+ }
3507
+ throw error;
3508
+ } finally {
3509
+ if (typeof lifecycleSignals.unregisterSignalHandler === "function") {
3510
+ lifecycleSignals.unregisterSignalHandler();
3511
+ }
3217
3512
  }
3218
3513
  }
3219
3514
  async function runMissionWatch(missionIdValue, command, context, dependencies) {
@@ -3225,37 +3520,58 @@ async function runMissionWatch(missionIdValue, command, context, dependencies) {
3225
3520
  dependencies,
3226
3521
  homeDir
3227
3522
  );
3228
- const snapshot = await fetchMissionSnapshot(apiClient, missionId);
3229
- writeLine(
3523
+ const lifecycleSignals = createLifecycleInterruptContext(
3524
+ dependencies.registerSignalHandler,
3230
3525
  context.stdout,
3231
- `Watching mission ${pc6.cyan(missionId)} from state ${pc6.bold(snapshot.state.state)}.`
3526
+ "Interrupted. Remote mission continues. Re-run this command to reconnect when ready."
3232
3527
  );
3233
- renderMissionSnapshot(context.stdout, snapshot);
3234
- const watchExitCode = resolveWatchExitCode(snapshot.state.state);
3235
- if (watchExitCode === 0) {
3236
- return;
3237
- }
3238
- if (watchExitCode === 1) {
3239
- throw new CliError(
3240
- `Mission ${missionId} is currently ${snapshot.state.state}.`,
3241
- 1
3528
+ try {
3529
+ const snapshot = await fetchMissionSnapshot(
3530
+ apiClient,
3531
+ missionId,
3532
+ lifecycleSignals.interruptState.abortController.signal
3242
3533
  );
3243
- }
3244
- const monitorResult = await monitorMission({
3245
- apiClient,
3246
- apiKey: authConfig.apiKey,
3247
- createSpinner: dependencies.createSpinner,
3248
- createWebSocket: dependencies.createWebSocket,
3249
- endpoint: authConfig.endpoint,
3250
- missionId,
3251
- promptInput: dependencies.promptInput,
3252
- promptSelect: dependencies.promptSelect,
3253
- registerSignalHandler: dependencies.registerSignalHandler,
3254
- sleep: dependencies.sleep,
3255
- stdout: context.stdout
3256
- });
3257
- if (monitorResult.exitCode !== 0) {
3258
- throw new CommanderError(monitorResult.exitCode, "mission-monitor", "");
3534
+ throwIfInterrupted2(lifecycleSignals.interruptState, "mission-watch");
3535
+ writeLine(
3536
+ context.stdout,
3537
+ `Watching mission ${pc7.cyan(missionId)} from state ${pc7.bold(snapshot.state.state)}.`
3538
+ );
3539
+ renderMissionSnapshot(context.stdout, snapshot);
3540
+ const watchExitCode = resolveWatchExitCode(snapshot.state.state);
3541
+ if (watchExitCode === 0) {
3542
+ return;
3543
+ }
3544
+ if (watchExitCode === 1) {
3545
+ throw new CliError(
3546
+ `Mission ${missionId} is currently ${snapshot.state.state}.`,
3547
+ 1
3548
+ );
3549
+ }
3550
+ const monitorResult = await monitorMission({
3551
+ apiClient,
3552
+ apiKey: authConfig.apiKey,
3553
+ createSpinner: dependencies.createSpinner,
3554
+ createWebSocket: dependencies.createWebSocket,
3555
+ endpoint: authConfig.endpoint,
3556
+ missionId,
3557
+ promptInput: dependencies.promptInput,
3558
+ promptSelect: dependencies.promptSelect,
3559
+ registerSignalHandler: lifecycleSignals.registerSignalHandler,
3560
+ sleep: dependencies.sleep,
3561
+ stdout: context.stdout
3562
+ });
3563
+ if (monitorResult.exitCode !== 0) {
3564
+ throw new CommanderError2(monitorResult.exitCode, "mission-monitor", "");
3565
+ }
3566
+ } catch (error) {
3567
+ if (lifecycleSignals.interruptState.interrupted && isInterruptedError(error)) {
3568
+ throw new CommanderError2(130, "mission-watch", "");
3569
+ }
3570
+ throw error;
3571
+ } finally {
3572
+ if (typeof lifecycleSignals.unregisterSignalHandler === "function") {
3573
+ lifecycleSignals.unregisterSignalHandler();
3574
+ }
3259
3575
  }
3260
3576
  }
3261
3577
  function registerMissionLifecycleCommands(mission, context, dependencies = {}) {
@@ -3295,7 +3611,7 @@ function registerMissionLifecycleCommands(mission, context, dependencies = {}) {
3295
3611
  }
3296
3612
 
3297
3613
  // src/commands/mission-resources.ts
3298
- import pc7 from "picocolors";
3614
+ import pc8 from "picocolors";
3299
3615
  function isRecord6(value) {
3300
3616
  return value !== null && !Array.isArray(value) && typeof value === "object";
3301
3617
  }
@@ -3344,7 +3660,7 @@ function formatBytes(value) {
3344
3660
  }
3345
3661
  function writeSection2(stdout, title) {
3346
3662
  writeLine(stdout);
3347
- writeLine(stdout, pc7.bold(title));
3663
+ writeLine(stdout, pc8.bold(title));
3348
3664
  }
3349
3665
  function renderMetricTable(stdout, rows) {
3350
3666
  renderTable(
@@ -3647,7 +3963,7 @@ function registerMissionResourcesCommand(mission, context, dependencies = {}) {
3647
3963
  // src/commands/mission-run.ts
3648
3964
  import { stat as stat3 } from "fs/promises";
3649
3965
  import path7 from "path";
3650
- import { CommanderError as CommanderError2 } from "commander";
3966
+ import { CommanderError as CommanderError3 } from "commander";
3651
3967
 
3652
3968
  // src/planning.ts
3653
3969
  import path4 from "path";
@@ -3657,7 +3973,7 @@ import {
3657
3973
  select as defaultSelectPrompt2
3658
3974
  } from "@inquirer/prompts";
3659
3975
  import ora2 from "ora";
3660
- import pc8 from "picocolors";
3976
+ import pc9 from "picocolors";
3661
3977
  var DEFAULT_ANALYSIS_TIMEOUT_MS = 5 * 60 * 1e3;
3662
3978
  var DEFAULT_POLL_INTERVAL_MS = 5e3;
3663
3979
  var FREE_TEXT_OPTION_ID = "free_text";
@@ -3743,6 +4059,54 @@ function extractErrorCode(error) {
3743
4059
  function extractRound(payload) {
3744
4060
  return typeof payload?.round === "number" && Number.isFinite(payload.round) ? payload.round : 1;
3745
4061
  }
4062
+ function isAbortError3(error) {
4063
+ return error instanceof DOMException && error.name === "AbortError";
4064
+ }
4065
+ function isAbortPromptError2(error) {
4066
+ return error instanceof Error && error.name === "AbortPromptError";
4067
+ }
4068
+ function isInterruptedError2(error) {
4069
+ return error instanceof CliError && error.exitCode === 130 || isAbortError3(error) || isAbortPromptError2(error);
4070
+ }
4071
+ function throwIfInterrupted3(interruption) {
4072
+ if (interruption.interrupted) {
4073
+ throw new CliError("Planning interrupted.", 130);
4074
+ }
4075
+ }
4076
+ function interruptPlanning(interruption) {
4077
+ if (interruption.interrupted) {
4078
+ return;
4079
+ }
4080
+ interruption.interrupted = true;
4081
+ interruption.activeSpinner?.stop();
4082
+ interruption.activeSpinner = void 0;
4083
+ interruption.releaseActiveWait?.();
4084
+ interruption.releaseActiveWait = void 0;
4085
+ if (!interruption.abortController.signal.aborted) {
4086
+ interruption.abortController.abort();
4087
+ }
4088
+ writeLine(
4089
+ interruption.stdout,
4090
+ pc9.yellow("Interrupted. Re-run this command to resume the planning session.")
4091
+ );
4092
+ }
4093
+ async function waitForInterruptibleDelay(sleep, ms, interruption) {
4094
+ throwIfInterrupted3(interruption);
4095
+ let resolveWait;
4096
+ await Promise.race([
4097
+ sleep(ms).then(() => {
4098
+ resolveWait?.();
4099
+ }),
4100
+ new Promise((resolve) => {
4101
+ resolveWait = () => resolve();
4102
+ interruption.releaseActiveWait = resolveWait;
4103
+ })
4104
+ ]);
4105
+ if (interruption.releaseActiveWait === resolveWait) {
4106
+ interruption.releaseActiveWait = void 0;
4107
+ }
4108
+ throwIfInterrupted3(interruption);
4109
+ }
3746
4110
  function defaultCreateSpinner2(text) {
3747
4111
  const spinner = ora2({ text });
3748
4112
  return {
@@ -3776,66 +4140,79 @@ function defaultCreateSpinner2(text) {
3776
4140
  };
3777
4141
  }
3778
4142
  async function defaultPromptSelect2(options) {
3779
- return defaultSelectPrompt2({
3780
- choices: options.choices.map((choice) => ({
3781
- description: choice.description,
3782
- name: choice.name,
3783
- value: choice.value
3784
- })),
3785
- message: options.message
3786
- });
4143
+ return defaultSelectPrompt2(
4144
+ {
4145
+ choices: options.choices.map((choice) => ({
4146
+ description: choice.description,
4147
+ name: choice.name,
4148
+ value: choice.value
4149
+ })),
4150
+ message: options.message
4151
+ },
4152
+ { signal: options.signal }
4153
+ );
3787
4154
  }
3788
4155
  async function defaultPromptInput2(options) {
3789
- return defaultInputPrompt2({
3790
- default: options.default,
3791
- message: options.message
3792
- });
4156
+ return defaultInputPrompt2(
4157
+ {
4158
+ default: options.default,
4159
+ message: options.message
4160
+ },
4161
+ { signal: options.signal }
4162
+ );
3793
4163
  }
3794
4164
  async function defaultPromptConfirm2(options) {
3795
- return defaultConfirmPrompt2({
3796
- default: options.default,
3797
- message: options.message
3798
- });
4165
+ return defaultConfirmPrompt2(
4166
+ {
4167
+ default: options.default,
4168
+ message: options.message
4169
+ },
4170
+ { signal: options.signal }
4171
+ );
3799
4172
  }
3800
- async function getPlanningSession(client, sessionId) {
4173
+ async function getPlanningSession(client, sessionId, signal) {
3801
4174
  return client.request({
3802
- path: `/plan/${sessionId}`
4175
+ path: `/plan/${sessionId}`,
4176
+ signal
3803
4177
  });
3804
4178
  }
3805
- async function postClarification(client, sessionId, body) {
4179
+ async function postClarification(client, sessionId, signal, body) {
3806
4180
  return client.request({
3807
4181
  body,
3808
4182
  method: "POST",
3809
- path: `/plan/${sessionId}/clarify`
4183
+ path: `/plan/${sessionId}/clarify`,
4184
+ signal
3810
4185
  });
3811
4186
  }
3812
- async function postMilestoneConfirmation(client, sessionId, body) {
4187
+ async function postMilestoneConfirmation(client, sessionId, signal, body) {
3813
4188
  return client.request({
3814
4189
  body,
3815
4190
  method: "POST",
3816
- path: `/plan/${sessionId}/confirm-milestones`
4191
+ path: `/plan/${sessionId}/confirm-milestones`,
4192
+ signal
3817
4193
  });
3818
4194
  }
3819
- async function postDraftApproval(client, sessionId) {
4195
+ async function postDraftApproval(client, sessionId, signal) {
3820
4196
  return client.request({
3821
4197
  method: "POST",
3822
- path: `/plan/${sessionId}/approve`
4198
+ path: `/plan/${sessionId}/approve`,
4199
+ signal
3823
4200
  });
3824
4201
  }
3825
4202
  function writeSection3(stdout, title) {
3826
4203
  writeLine(stdout);
3827
- writeLine(stdout, pc8.bold(title));
4204
+ writeLine(stdout, pc9.bold(title));
3828
4205
  }
3829
4206
  function renderQuestion(stdout, question, index) {
3830
4207
  writeLine(stdout, `${index + 1}. ${question.text}`);
3831
4208
  if (question.references && question.references.length > 0) {
3832
- writeLine(stdout, ` ${pc8.dim(`References: ${question.references.join(", ")}`)}`);
4209
+ writeLine(stdout, ` ${pc9.dim(`References: ${question.references.join(", ")}`)}`);
3833
4210
  }
3834
4211
  }
3835
4212
  function renderMilestones(stdout, milestones, reviewRound) {
3836
4213
  writeSection3(stdout, `Milestone review round ${reviewRound}`);
3837
4214
  milestones.forEach((milestone, index) => {
3838
- writeLine(stdout, `${index + 1}. ${pc8.cyan(milestone.name)}`);
4215
+ writeLine(stdout, `${index + 1}. ${pc9.cyan(milestone.name)}`);
3839
4216
  if (milestone.description) {
3840
4217
  writeLine(stdout, ` ${milestone.description}`);
3841
4218
  }
@@ -3923,12 +4300,13 @@ function buildAutoApprovedDetail(question, taskDescription, repoPaths) {
3923
4300
  }
3924
4301
  return `${task}. Use ${repoPhrase} as the source of truth.${references}`;
3925
4302
  }
3926
- async function promptForAnswers(options, questions, round) {
4303
+ async function promptForAnswers(options, interruption, questions, round) {
3927
4304
  const promptSelect = options.promptSelect ?? defaultPromptSelect2;
3928
4305
  const promptInput = options.promptInput ?? defaultPromptInput2;
3929
4306
  const answers = [];
3930
4307
  const transcript = [];
3931
4308
  for (const question of questions) {
4309
+ throwIfInterrupted3(interruption);
3932
4310
  if (question.options && question.options.length > 0) {
3933
4311
  const autoSelectedOption = options.autoApprove ? pickAutoApprovedOption(question, options.taskDescription, options.repoPaths) : null;
3934
4312
  const choices = question.options.map((option) => ({
@@ -3938,8 +4316,10 @@ async function promptForAnswers(options, questions, round) {
3938
4316
  }));
3939
4317
  const optionId2 = autoSelectedOption?.id ?? await promptSelect({
3940
4318
  choices,
3941
- message: question.text
4319
+ message: question.text,
4320
+ signal: interruption.abortController.signal
3942
4321
  });
4322
+ throwIfInterrupted3(interruption);
3943
4323
  const selectedOption = question.options.find((option) => option.id === optionId2);
3944
4324
  answers.push({
3945
4325
  optionId: optionId2,
@@ -3957,8 +4337,10 @@ async function promptForAnswers(options, questions, round) {
3957
4337
  }
3958
4338
  const detail = options.autoApprove ? buildAutoApprovedDetail(question, options.taskDescription, options.repoPaths) : await promptInput({
3959
4339
  default: question.inputDefault,
3960
- message: question.detailPrompt ?? question.text
4340
+ message: question.detailPrompt ?? question.text,
4341
+ signal: interruption.abortController.signal
3961
4342
  });
4343
+ throwIfInterrupted3(interruption);
3962
4344
  const optionId = question.freeTextOptionId ?? FREE_TEXT_OPTION_ID;
3963
4345
  answers.push({
3964
4346
  detail,
@@ -3979,7 +4361,7 @@ async function promptForAnswers(options, questions, round) {
3979
4361
  transcript
3980
4362
  };
3981
4363
  }
3982
- async function waitForAnalysis(options, sessionId) {
4364
+ async function waitForAnalysis(options, interruption, sessionId) {
3983
4365
  const createSpinner = options.createSpinner ?? defaultCreateSpinner2;
3984
4366
  const sleep = options.sleep ?? (async (ms) => {
3985
4367
  await new Promise((resolve) => setTimeout(resolve, ms));
@@ -3989,49 +4371,81 @@ async function waitForAnalysis(options, sessionId) {
3989
4371
  const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
3990
4372
  const deadline = now() + timeoutMs;
3991
4373
  const spinner = createSpinner("Analyzing repository for planning...");
4374
+ interruption.activeSpinner = spinner;
3992
4375
  spinner.start();
3993
4376
  try {
3994
- let session = await getPlanningSession(options.client, sessionId);
4377
+ throwIfInterrupted3(interruption);
4378
+ let session = await getPlanningSession(
4379
+ options.client,
4380
+ sessionId,
4381
+ interruption.abortController.signal
4382
+ );
3995
4383
  while (session.state === "created" || session.state === "analyzing") {
4384
+ throwIfInterrupted3(interruption);
3996
4385
  if (now() >= deadline) {
3997
4386
  spinner.fail("Planning analysis timed out.");
3998
4387
  throw new CliError("Planning analysis timed out after 5 minutes.");
3999
4388
  }
4000
- await sleep(pollIntervalMs);
4001
- session = await getPlanningSession(options.client, sessionId);
4389
+ await waitForInterruptibleDelay(sleep, pollIntervalMs, interruption);
4390
+ session = await getPlanningSession(
4391
+ options.client,
4392
+ sessionId,
4393
+ interruption.abortController.signal
4394
+ );
4002
4395
  }
4396
+ throwIfInterrupted3(interruption);
4003
4397
  spinner.succeed("Analysis complete.");
4004
4398
  return session;
4005
4399
  } catch (error) {
4400
+ if (isInterruptedError2(error)) {
4401
+ throw error;
4402
+ }
4006
4403
  if (error instanceof CliError) {
4007
4404
  throw error;
4008
4405
  }
4009
4406
  spinner.fail("Planning analysis failed.");
4010
4407
  throw error;
4408
+ } finally {
4409
+ if (interruption.activeSpinner === spinner) {
4410
+ interruption.activeSpinner = void 0;
4411
+ }
4011
4412
  }
4012
4413
  }
4013
- async function resolveClarification(options, sessionId, initialPayload) {
4414
+ async function resolveClarification(options, interruption, sessionId, initialPayload) {
4014
4415
  let payload = initialPayload;
4015
4416
  const localTranscript = [];
4016
4417
  while (payload.state === "clarifying") {
4418
+ throwIfInterrupted3(interruption);
4017
4419
  let questions = extractQuestions(payload);
4018
4420
  let round = extractRound(payload);
4019
4421
  if (questions.length === 0) {
4020
- payload = await postClarification(options.client, sessionId, {});
4422
+ payload = await postClarification(
4423
+ options.client,
4424
+ sessionId,
4425
+ interruption.abortController.signal,
4426
+ {}
4427
+ );
4021
4428
  questions = extractQuestions(payload);
4022
4429
  round = extractRound(payload);
4023
4430
  }
4431
+ throwIfInterrupted3(interruption);
4024
4432
  if (payload.state !== "clarifying") {
4025
4433
  break;
4026
4434
  }
4027
4435
  writeSection3(options.stdout, `Clarification round ${round}`);
4028
4436
  questions.forEach((question, index) => renderQuestion(options.stdout, question, index));
4029
- const prompted = await promptForAnswers(options, questions, round);
4437
+ const prompted = await promptForAnswers(options, interruption, questions, round);
4030
4438
  localTranscript.push(...prompted.transcript);
4031
4439
  try {
4032
- payload = await postClarification(options.client, sessionId, {
4033
- answers: prompted.answers
4034
- });
4440
+ throwIfInterrupted3(interruption);
4441
+ payload = await postClarification(
4442
+ options.client,
4443
+ sessionId,
4444
+ interruption.abortController.signal,
4445
+ {
4446
+ answers: prompted.answers
4447
+ }
4448
+ );
4035
4449
  } catch (error) {
4036
4450
  if (extractErrorCode(error) !== "resolvedness_gate_failed") {
4037
4451
  throw error;
@@ -4045,9 +4459,15 @@ async function resolveClarification(options, sessionId, initialPayload) {
4045
4459
  }
4046
4460
  writeLine(
4047
4461
  options.stdout,
4048
- pc8.yellow("Proceeding because --force is enabled.")
4462
+ pc9.yellow("Proceeding because --force is enabled.")
4463
+ );
4464
+ throwIfInterrupted3(interruption);
4465
+ payload = await postMilestoneConfirmation(
4466
+ options.client,
4467
+ sessionId,
4468
+ interruption.abortController.signal,
4469
+ {}
4049
4470
  );
4050
- payload = await postMilestoneConfirmation(options.client, sessionId, {});
4051
4471
  return {
4052
4472
  payload,
4053
4473
  persistedClarification: {
@@ -4077,35 +4497,55 @@ async function resolveClarification(options, sessionId, initialPayload) {
4077
4497
  }
4078
4498
  };
4079
4499
  }
4080
- async function resolveMilestones(options, sessionId, initialPayload) {
4500
+ async function resolveMilestones(options, interruption, sessionId, initialPayload) {
4081
4501
  let reviewRound = 1;
4082
4502
  let payload = initialPayload;
4083
4503
  if (extractMilestones(payload).length === 0) {
4084
- payload = await postMilestoneConfirmation(options.client, sessionId, {});
4504
+ payload = await postMilestoneConfirmation(
4505
+ options.client,
4506
+ sessionId,
4507
+ interruption.abortController.signal,
4508
+ {}
4509
+ );
4085
4510
  }
4086
4511
  while (true) {
4512
+ throwIfInterrupted3(interruption);
4087
4513
  const milestones = extractMilestones(payload);
4088
4514
  renderMilestones(options.stdout, milestones, reviewRound);
4089
4515
  const confirmed = options.autoApprove ? true : await (options.promptConfirm ?? defaultPromptConfirm2)({
4090
4516
  default: true,
4091
- message: "Do these milestones look correct?"
4517
+ message: "Do these milestones look correct?",
4518
+ signal: interruption.abortController.signal
4092
4519
  });
4520
+ throwIfInterrupted3(interruption);
4093
4521
  if (confirmed) {
4094
- return postMilestoneConfirmation(options.client, sessionId, {
4095
- confirmed: true
4096
- });
4522
+ return postMilestoneConfirmation(
4523
+ options.client,
4524
+ sessionId,
4525
+ interruption.abortController.signal,
4526
+ {
4527
+ confirmed: true
4528
+ }
4529
+ );
4097
4530
  }
4098
4531
  const feedback = await (options.promptInput ?? defaultPromptInput2)({
4099
- message: "What should change about these milestones?"
4100
- });
4101
- payload = await postMilestoneConfirmation(options.client, sessionId, {
4102
- confirmed: false,
4103
- feedback
4532
+ message: "What should change about these milestones?",
4533
+ signal: interruption.abortController.signal
4104
4534
  });
4535
+ throwIfInterrupted3(interruption);
4536
+ payload = await postMilestoneConfirmation(
4537
+ options.client,
4538
+ sessionId,
4539
+ interruption.abortController.signal,
4540
+ {
4541
+ confirmed: false,
4542
+ feedback
4543
+ }
4544
+ );
4105
4545
  reviewRound += 1;
4106
4546
  }
4107
4547
  }
4108
- async function resolveDraft(options, sessionId, payload) {
4548
+ async function resolveDraft(options, interruption, sessionId, payload) {
4109
4549
  const directDraft = extractDraft(payload);
4110
4550
  if (directDraft) {
4111
4551
  return {
@@ -4113,7 +4553,11 @@ async function resolveDraft(options, sessionId, payload) {
4113
4553
  payload
4114
4554
  };
4115
4555
  }
4116
- const approvedPayload = await postDraftApproval(options.client, sessionId);
4556
+ const approvedPayload = await postDraftApproval(
4557
+ options.client,
4558
+ sessionId,
4559
+ interruption.abortController.signal
4560
+ );
4117
4561
  const approvedDraft = extractDraft(approvedPayload);
4118
4562
  if (!approvedDraft) {
4119
4563
  throw new CliError("Planner did not return a mission draft.");
@@ -4127,7 +4571,7 @@ async function runPlanningFlow(options) {
4127
4571
  if (options.existingMissionDraft) {
4128
4572
  writeLine(
4129
4573
  options.stdout,
4130
- pc8.cyan("Skipping planning because mission-draft.json already exists.")
4574
+ pc9.cyan("Skipping planning because mission-draft.json already exists.")
4131
4575
  );
4132
4576
  return {
4133
4577
  cancelled: false,
@@ -4136,69 +4580,113 @@ async function runPlanningFlow(options) {
4136
4580
  skippedPlanning: true
4137
4581
  };
4138
4582
  }
4139
- const sessionId = asNonEmptyString8(options.existingSessionId) ?? asNonEmptyString8(
4140
- (await options.client.request({
4141
- body: {
4142
- repos: options.repoPaths,
4143
- task_description: options.taskDescription
4144
- },
4145
- method: "POST",
4146
- path: "/plan/create",
4147
- timeoutMs: 3e4
4148
- })).session_id
4149
- );
4150
- if (!sessionId) {
4151
- throw new CliError("Planner did not return a session id.");
4152
- }
4153
- if (options.existingSessionId) {
4154
- writeLine(options.stdout, `Resuming planning session ${sessionId}.`);
4155
- } else {
4156
- await options.persistSessionId(sessionId);
4157
- writeLine(options.stdout, `Created planning session ${sessionId}.`);
4158
- }
4159
- let payload = await waitForAnalysis(options, sessionId);
4160
- if (payload.state === "clarifying") {
4161
- const clarification = await resolveClarification(options, sessionId, payload);
4162
- payload = clarification.payload;
4163
- if (clarification.persistedClarification) {
4164
- await options.persistClarification(clarification.persistedClarification);
4583
+ const interruption = {
4584
+ abortController: new AbortController(),
4585
+ interrupted: false,
4586
+ stdout: options.stdout
4587
+ };
4588
+ let sessionId = asNonEmptyString8(options.existingSessionId) ?? "";
4589
+ const unregisterSignalHandler = options.registerSignalHandler?.("SIGINT", () => {
4590
+ interruptPlanning(interruption);
4591
+ });
4592
+ try {
4593
+ sessionId = sessionId || asNonEmptyString8(
4594
+ (await options.client.request({
4595
+ body: {
4596
+ repos: options.repoPaths,
4597
+ task_description: options.taskDescription
4598
+ },
4599
+ method: "POST",
4600
+ path: "/plan/create",
4601
+ signal: interruption.abortController.signal,
4602
+ timeoutMs: 3e4
4603
+ })).session_id
4604
+ ) || "";
4605
+ if (!sessionId) {
4606
+ throw new CliError("Planner did not return a session id.");
4165
4607
  }
4166
- }
4167
- if (payload.state === "confirming") {
4168
- payload = await resolveMilestones(options, sessionId, payload);
4169
- }
4170
- if (payload.state !== "complete") {
4171
- throw new CliError(
4172
- `Planner returned an unexpected state after milestone confirmation: ${payload.state ?? "unknown"}.`
4608
+ if (options.existingSessionId) {
4609
+ writeLine(options.stdout, `Resuming planning session ${sessionId}.`);
4610
+ } else {
4611
+ await options.persistSessionId(sessionId);
4612
+ throwIfInterrupted3(interruption);
4613
+ writeLine(options.stdout, `Created planning session ${sessionId}.`);
4614
+ }
4615
+ let payload = await waitForAnalysis(options, interruption, sessionId);
4616
+ throwIfInterrupted3(interruption);
4617
+ if (payload.state === "clarifying") {
4618
+ const clarification = await resolveClarification(
4619
+ options,
4620
+ interruption,
4621
+ sessionId,
4622
+ payload
4623
+ );
4624
+ payload = clarification.payload;
4625
+ throwIfInterrupted3(interruption);
4626
+ if (clarification.persistedClarification) {
4627
+ await options.persistClarification(clarification.persistedClarification);
4628
+ throwIfInterrupted3(interruption);
4629
+ }
4630
+ }
4631
+ throwIfInterrupted3(interruption);
4632
+ if (payload.state === "confirming") {
4633
+ payload = await resolveMilestones(options, interruption, sessionId, payload);
4634
+ }
4635
+ throwIfInterrupted3(interruption);
4636
+ if (payload.state !== "complete") {
4637
+ throw new CliError(
4638
+ `Planner returned an unexpected state after milestone confirmation: ${payload.state ?? "unknown"}.`
4639
+ );
4640
+ }
4641
+ const { draft, payload: summaryPayload } = await resolveDraft(
4642
+ options,
4643
+ interruption,
4644
+ sessionId,
4645
+ payload
4173
4646
  );
4174
- }
4175
- const { draft, payload: summaryPayload } = await resolveDraft(
4176
- options,
4177
- sessionId,
4178
- payload
4179
- );
4180
- renderDraftSummary(options.stdout, summaryPayload, draft);
4181
- const approved = options.autoApprove ? true : await (options.promptConfirm ?? defaultPromptConfirm2)({
4182
- default: true,
4183
- message: "Approve this draft and continue to upload?"
4184
- });
4185
- if (!approved) {
4186
- writeLine(options.stdout, pc8.yellow("Planning cancelled before upload."));
4647
+ throwIfInterrupted3(interruption);
4648
+ renderDraftSummary(options.stdout, summaryPayload, draft);
4649
+ const approved = options.autoApprove ? true : await (options.promptConfirm ?? defaultPromptConfirm2)({
4650
+ default: true,
4651
+ message: "Approve this draft and continue to upload?",
4652
+ signal: interruption.abortController.signal
4653
+ });
4654
+ throwIfInterrupted3(interruption);
4655
+ if (!approved) {
4656
+ writeLine(options.stdout, pc9.yellow("Planning cancelled before upload."));
4657
+ return {
4658
+ cancelled: true,
4659
+ draft,
4660
+ sessionId,
4661
+ skippedPlanning: false
4662
+ };
4663
+ }
4664
+ await options.persistMissionDraft(draft);
4665
+ throwIfInterrupted3(interruption);
4666
+ writeLine(options.stdout, pc9.green("Saved approved mission draft."));
4187
4667
  return {
4188
- cancelled: true,
4668
+ cancelled: false,
4189
4669
  draft,
4190
4670
  sessionId,
4191
4671
  skippedPlanning: false
4192
4672
  };
4673
+ } catch (error) {
4674
+ if (interruption.interrupted && isInterruptedError2(error)) {
4675
+ return {
4676
+ cancelled: false,
4677
+ draft: null,
4678
+ exitCode: 130,
4679
+ interrupted: true,
4680
+ sessionId,
4681
+ skippedPlanning: false
4682
+ };
4683
+ }
4684
+ throw error;
4685
+ } finally {
4686
+ if (typeof unregisterSignalHandler === "function") {
4687
+ unregisterSignalHandler();
4688
+ }
4193
4689
  }
4194
- await options.persistMissionDraft(draft);
4195
- writeLine(options.stdout, pc8.green("Saved approved mission draft."));
4196
- return {
4197
- cancelled: false,
4198
- draft,
4199
- sessionId,
4200
- skippedPlanning: false
4201
- };
4202
4690
  }
4203
4691
 
4204
4692
  // src/upload.ts
@@ -4207,7 +4695,7 @@ import { createReadStream as createReadStream2 } from "fs";
4207
4695
  import path6 from "path";
4208
4696
  import { Transform as Transform2 } from "stream";
4209
4697
  import ora3 from "ora";
4210
- import pc9 from "picocolors";
4698
+ import pc10 from "picocolors";
4211
4699
 
4212
4700
  // src/snapshot.ts
4213
4701
  import { execFile } from "child_process";
@@ -4645,10 +5133,16 @@ async function writeDeterministicArchive(analysis, outputDir) {
4645
5133
  const tempArchivePath = `${finalArchivePath}.${process.pid}.${Date.now()}.tmp`;
4646
5134
  const archiveHash = createHash2("sha256");
4647
5135
  let archiveBytes = 0;
4648
- const meter = new Transform({
5136
+ const archiveHashMeter = new Transform({
4649
5137
  transform(chunk, _encoding, callback) {
4650
5138
  const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
4651
5139
  archiveHash.update(buffer);
5140
+ callback(null, buffer);
5141
+ }
5142
+ });
5143
+ const outputMeter = new Transform({
5144
+ transform(chunk, _encoding, callback) {
5145
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
4652
5146
  archiveBytes += buffer.length;
4653
5147
  callback(null, buffer);
4654
5148
  }
@@ -4680,8 +5174,9 @@ async function writeDeterministicArchive(analysis, outputDir) {
4680
5174
  try {
4681
5175
  await pipeline(
4682
5176
  tarStream,
5177
+ archiveHashMeter,
4683
5178
  createZstdCompress(),
4684
- meter,
5179
+ outputMeter,
4685
5180
  createWriteStream(tempArchivePath, { mode: 384 })
4686
5181
  );
4687
5182
  await rename3(tempArchivePath, finalArchivePath);
@@ -4759,8 +5254,41 @@ async function defaultPromptConfirm3(options) {
4759
5254
  return confirm({
4760
5255
  default: options.default,
4761
5256
  message: options.message
5257
+ }, {
5258
+ signal: options.signal
4762
5259
  });
4763
5260
  }
5261
+ function isAbortError4(error) {
5262
+ return error instanceof DOMException && error.name === "AbortError";
5263
+ }
5264
+ function isAbortPromptError3(error) {
5265
+ return error instanceof Error && error.name === "AbortPromptError";
5266
+ }
5267
+ function isInterruptedError3(error) {
5268
+ return error instanceof CliError && error.exitCode === 130 || isAbortError4(error) || isAbortPromptError3(error);
5269
+ }
5270
+ function throwIfInterrupted4(interruption) {
5271
+ if (interruption.interrupted) {
5272
+ throw new CliError("Upload interrupted.", 130);
5273
+ }
5274
+ }
5275
+ function interruptUpload(interruption) {
5276
+ if (interruption.interrupted) {
5277
+ return;
5278
+ }
5279
+ interruption.interrupted = true;
5280
+ interruption.activeSpinner?.stop();
5281
+ interruption.activeSpinner = void 0;
5282
+ if (!interruption.abortController.signal.aborted) {
5283
+ interruption.abortController.abort();
5284
+ }
5285
+ writeLine(
5286
+ interruption.stdout,
5287
+ pc10.yellow(
5288
+ "Interrupted. Local upload state is saved and any remote launch is preserved. Re-run this command to resume the upload."
5289
+ )
5290
+ );
5291
+ }
4764
5292
  function formatBytes2(bytes) {
4765
5293
  if (!Number.isFinite(bytes) || bytes < 1024) {
4766
5294
  return `${bytes} B`;
@@ -4804,15 +5332,15 @@ function isTerminalUploadStatus2(status) {
4804
5332
  return status === "uploaded" || status === "blob_exists";
4805
5333
  }
4806
5334
  function renderSafetyGate(stdout, repos, existingBlobs) {
4807
- writeLine(stdout, pc9.bold("Upload safety check"));
5335
+ writeLine(stdout, pc10.bold("Upload safety check"));
4808
5336
  writeLine(stdout, "Repos queued for upload:");
4809
5337
  for (const repo of repos) {
4810
- const cleanliness = repo.manifest.dirty ? pc9.yellow("dirty") : pc9.green("clean");
4811
- const secretSummary = repo.secretFindings.length > 0 ? pc9.red(`${repo.secretFindings.length} finding(s)`) : pc9.green("no findings");
4812
- const dedupSummary = existingBlobs.has(repo.manifest.archiveSha256) ? pc9.cyan("blob already exists remotely") : "new upload";
5338
+ const cleanliness = repo.manifest.dirty ? pc10.yellow("dirty") : pc10.green("clean");
5339
+ const secretSummary = repo.secretFindings.length > 0 ? pc10.red(`${repo.secretFindings.length} finding(s)`) : pc10.green("no findings");
5340
+ const dedupSummary = existingBlobs.has(repo.manifest.archiveSha256) ? pc10.cyan("blob already exists remotely") : "new upload";
4813
5341
  writeLine(
4814
5342
  stdout,
4815
- `- ${pc9.cyan(repo.manifest.repoId)} \u2014 ${repo.manifest.localPath}`
5343
+ `- ${pc10.cyan(repo.manifest.repoId)} \u2014 ${repo.manifest.localPath}`
4816
5344
  );
4817
5345
  writeLine(
4818
5346
  stdout,
@@ -4842,7 +5370,7 @@ function assertArtifactsMatchPersistedState(repos, persistedManifests) {
4842
5370
  );
4843
5371
  }
4844
5372
  }
4845
- async function createRemoteLaunch(options, repos, taskDescription) {
5373
+ async function createRemoteLaunch(options, repos, taskDescription, signal) {
4846
5374
  const response = await options.apiClient.request({
4847
5375
  body: {
4848
5376
  ...taskDescription ? {
@@ -4858,7 +5386,8 @@ async function createRemoteLaunch(options, repos, taskDescription) {
4858
5386
  }))
4859
5387
  },
4860
5388
  method: "POST",
4861
- path: "/launches"
5389
+ path: "/launches",
5390
+ signal
4862
5391
  });
4863
5392
  const remoteLaunchId = normalizeLaunchId2(response.launchId);
4864
5393
  if (remoteLaunchId.length === 0) {
@@ -4869,12 +5398,13 @@ async function createRemoteLaunch(options, repos, taskDescription) {
4869
5398
  remoteLaunchId
4870
5399
  };
4871
5400
  }
4872
- async function uploadArchive(options, repo, uploadUrl) {
5401
+ async function uploadArchive(options, repo, uploadUrl, signal, interruption) {
4873
5402
  const fetchImpl = options.fetch ?? globalThis.fetch;
4874
5403
  const createSpinner = options.createSpinner ?? defaultCreateSpinner3;
4875
5404
  const spinner = createSpinner(
4876
5405
  `Uploading ${repo.manifest.repoId} 0 B / ${formatBytes2(repo.manifest.archiveBytes)} (0 B/s)`
4877
5406
  );
5407
+ interruption.activeSpinner = spinner;
4878
5408
  spinner.start(spinner.text);
4879
5409
  const startedAt = Date.now();
4880
5410
  let bytesTransferred = 0;
@@ -4886,27 +5416,49 @@ async function uploadArchive(options, repo, uploadUrl) {
4886
5416
  callback(null, chunk);
4887
5417
  }
4888
5418
  });
5419
+ const archiveStream = createReadStream2(repo.archivePath);
5420
+ const abortStream = () => {
5421
+ archiveStream.destroy();
5422
+ meter.destroy();
5423
+ };
5424
+ if (signal.aborted) {
5425
+ abortStream();
5426
+ } else {
5427
+ signal.addEventListener("abort", abortStream, { once: true });
5428
+ }
4889
5429
  let response;
4890
5430
  try {
4891
- const body = createReadStream2(repo.archivePath).pipe(meter);
5431
+ const body = archiveStream.pipe(meter);
4892
5432
  const requestInit = {
4893
5433
  body,
4894
5434
  duplex: "half",
4895
5435
  headers: {
4896
5436
  "content-length": String(repo.manifest.archiveBytes)
4897
5437
  },
4898
- method: "PUT"
5438
+ method: "PUT",
5439
+ signal
4899
5440
  };
4900
5441
  response = await fetchImpl(uploadUrl, requestInit);
4901
5442
  } catch (error) {
5443
+ if (isInterruptedError3(error)) {
5444
+ throw error;
5445
+ }
4902
5446
  spinner.fail(`Upload failed for ${repo.manifest.repoId}`);
5447
+ if (interruption.activeSpinner === spinner) {
5448
+ interruption.activeSpinner = void 0;
5449
+ }
4903
5450
  throw new CliError(
4904
5451
  `Upload failed for ${repo.manifest.repoId}: ${error instanceof Error ? error.message : String(error)}`
4905
5452
  );
5453
+ } finally {
5454
+ signal.removeEventListener("abort", abortStream);
4906
5455
  }
4907
5456
  if (!response.ok) {
4908
5457
  const detail = await response.text().catch(() => "");
4909
5458
  spinner.fail(`Upload failed for ${repo.manifest.repoId}`);
5459
+ if (interruption.activeSpinner === spinner) {
5460
+ interruption.activeSpinner = void 0;
5461
+ }
4910
5462
  throw new CliError(
4911
5463
  `Upload failed for ${repo.manifest.repoId}: HTTP ${response.status}${detail ? ` ${detail}` : ""}`
4912
5464
  );
@@ -4915,8 +5467,38 @@ async function uploadArchive(options, repo, uploadUrl) {
4915
5467
  spinner.succeed(
4916
5468
  `Uploaded ${repo.manifest.repoId} ${formatBytes2(bytesTransferred)} in ${formatDuration(elapsedMs)} (${formatRate(bytesTransferred, elapsedMs)})`
4917
5469
  );
5470
+ if (interruption.activeSpinner === spinner) {
5471
+ interruption.activeSpinner = void 0;
5472
+ }
5473
+ }
5474
+ async function persistCreatedLaunchState(options, remoteLaunchId, repos) {
5475
+ for (const repo of repos) {
5476
+ await recordRepoManifest(
5477
+ options.launchId,
5478
+ repo.manifest.repoId,
5479
+ repo.manifest,
5480
+ {
5481
+ cwd: options.cwd,
5482
+ homeDir: options.homeDir
5483
+ }
5484
+ );
5485
+ }
5486
+ for (const repo of repos) {
5487
+ await updateRepoUploadState(
5488
+ options.launchId,
5489
+ {
5490
+ remoteLaunchId,
5491
+ repoId: repo.manifest.repoId,
5492
+ status: "pending"
5493
+ },
5494
+ {
5495
+ cwd: options.cwd,
5496
+ homeDir: options.homeDir
5497
+ }
5498
+ );
5499
+ }
4918
5500
  }
4919
- async function processRepoUpload(options, repo, remoteLaunchId) {
5501
+ async function processRepoUpload(options, repo, remoteLaunchId, signal, interruption) {
4920
5502
  const snapshot = await loadLaunchSnapshot(options.launchId, {
4921
5503
  cwd: options.cwd,
4922
5504
  homeDir: options.homeDir
@@ -4939,7 +5521,8 @@ async function processRepoUpload(options, repo, remoteLaunchId) {
4939
5521
  const response = await options.apiClient.request({
4940
5522
  body: repo.manifest,
4941
5523
  method: "POST",
4942
- path: `/launches/${remoteLaunchId}/repos/${repo.manifest.repoId}`
5524
+ path: `/launches/${remoteLaunchId}/repos/${repo.manifest.repoId}`,
5525
+ signal
4943
5526
  });
4944
5527
  if (response.status === "blob_exists") {
4945
5528
  await updateRepoUploadState(
@@ -4956,7 +5539,7 @@ async function processRepoUpload(options, repo, remoteLaunchId) {
4956
5539
  );
4957
5540
  writeLine(
4958
5541
  options.stdout,
4959
- pc9.cyan(`Skipping upload for ${repo.manifest.repoId}; blob already exists remotely.`)
5542
+ pc10.cyan(`Skipping upload for ${repo.manifest.repoId}; blob already exists remotely.`)
4960
5543
  );
4961
5544
  return;
4962
5545
  }
@@ -4978,7 +5561,7 @@ async function processRepoUpload(options, repo, remoteLaunchId) {
4978
5561
  homeDir: options.homeDir
4979
5562
  }
4980
5563
  );
4981
- await uploadArchive(options, repo, uploadUrl);
5564
+ await uploadArchive(options, repo, uploadUrl, signal, interruption);
4982
5565
  await updateRepoUploadState(
4983
5566
  options.launchId,
4984
5567
  {
@@ -4994,152 +5577,193 @@ async function processRepoUpload(options, repo, remoteLaunchId) {
4994
5577
  }
4995
5578
  async function runUploadPipeline(options) {
4996
5579
  const promptConfirm = options.promptConfirm ?? defaultPromptConfirm3;
5580
+ const interruption = {
5581
+ abortController: new AbortController(),
5582
+ interrupted: false,
5583
+ stdout: options.stdout
5584
+ };
5585
+ let remoteLaunchId = null;
5586
+ const unregisterSignalHandler = options.registerSignalHandler?.("SIGINT", () => {
5587
+ interruptUpload(interruption);
5588
+ });
4997
5589
  const launchSnapshot = await loadLaunchSnapshot(options.launchId, {
4998
5590
  cwd: options.cwd,
4999
5591
  homeDir: options.homeDir
5000
5592
  });
5001
- if (!launchSnapshot.missionDraft) {
5002
- throw new CliError(
5003
- "Cannot start uploads before a mission draft has been approved."
5004
- );
5005
- }
5006
- let repos;
5007
5593
  try {
5008
- repos = await snapshotRepositories({
5009
- allowSecrets: options.allowSecrets === true,
5010
- cwd: options.cwd,
5011
- outputDir: path6.join(launchSnapshot.paths.launchDir, "archives"),
5012
- repoPaths: options.repoPaths
5013
- });
5014
- } catch (error) {
5015
- if (!(error instanceof SecretDetectionError) || options.allowSecrets === true) {
5016
- throw error;
5017
- }
5018
- repos = await snapshotRepositories({
5019
- allowSecrets: true,
5020
- cwd: options.cwd,
5021
- outputDir: path6.join(launchSnapshot.paths.launchDir, "archives"),
5022
- repoPaths: options.repoPaths
5023
- });
5024
- repos = repos.map((repo) => ({
5025
- ...repo,
5026
- warnings: repo.warnings.filter(
5027
- (warning) => !warning.includes("--allow-secrets is enabled")
5028
- )
5029
- }));
5030
- }
5031
- if (Object.keys(launchSnapshot.repoManifests).length > 0) {
5032
- assertArtifactsMatchPersistedState(
5033
- repos,
5034
- launchSnapshot.repoManifests
5035
- );
5036
- }
5037
- let existingBlobs = /* @__PURE__ */ new Set();
5038
- let remoteLaunchId = launchSnapshot.uploadState?.remoteLaunchId ?? null;
5039
- if (!remoteLaunchId) {
5040
- renderSafetyGate(options.stdout, repos, existingBlobs);
5041
- const hasSecretFindings = repos.some((repo) => repo.secretFindings.length > 0);
5042
- if (hasSecretFindings && options.allowSecrets !== true) {
5594
+ if (!launchSnapshot.missionDraft) {
5043
5595
  throw new CliError(
5044
- "Secret scan findings blocked the upload. Re-run with `--allow-secrets` to proceed."
5596
+ "Cannot start uploads before a mission draft has been approved."
5045
5597
  );
5046
5598
  }
5047
- if (options.autoApprove !== true && !await promptConfirm({
5048
- default: false,
5049
- message: "Proceed with launch upload?"
5050
- })) {
5051
- writeLine(
5052
- options.stdout,
5053
- pc9.yellow("Upload cancelled before creating a remote launch.")
5599
+ let repos;
5600
+ try {
5601
+ repos = await snapshotRepositories({
5602
+ allowSecrets: options.allowSecrets === true,
5603
+ cwd: options.cwd,
5604
+ outputDir: path6.join(launchSnapshot.paths.launchDir, "archives"),
5605
+ repoPaths: options.repoPaths
5606
+ });
5607
+ } catch (error) {
5608
+ if (!(error instanceof SecretDetectionError) || options.allowSecrets === true) {
5609
+ throw error;
5610
+ }
5611
+ repos = await snapshotRepositories({
5612
+ allowSecrets: true,
5613
+ cwd: options.cwd,
5614
+ outputDir: path6.join(launchSnapshot.paths.launchDir, "archives"),
5615
+ repoPaths: options.repoPaths
5616
+ });
5617
+ repos = repos.map((repo) => ({
5618
+ ...repo,
5619
+ warnings: repo.warnings.filter(
5620
+ (warning) => !warning.includes("--allow-secrets is enabled")
5621
+ )
5622
+ }));
5623
+ }
5624
+ if (Object.keys(launchSnapshot.repoManifests).length > 0) {
5625
+ assertArtifactsMatchPersistedState(
5626
+ repos,
5627
+ launchSnapshot.repoManifests
5054
5628
  );
5055
- return {
5056
- finalized: false,
5057
- remoteLaunchId: null
5058
- };
5059
5629
  }
5060
- const createdLaunch = await createRemoteLaunch(
5061
- options,
5062
- repos,
5063
- launchSnapshot.request?.task ?? null
5064
- );
5065
- existingBlobs = createdLaunch.existingBlobs;
5066
- remoteLaunchId = createdLaunch.remoteLaunchId;
5067
- if (existingBlobs.size > 0) {
5068
- writeLine(
5069
- options.stdout,
5070
- pc9.cyan(
5071
- `Remote dedup will reuse ${existingBlobs.size} existing blob${existingBlobs.size === 1 ? "" : "s"}.`
5072
- )
5630
+ throwIfInterrupted4(interruption);
5631
+ let existingBlobs = /* @__PURE__ */ new Set();
5632
+ remoteLaunchId = launchSnapshot.uploadState?.remoteLaunchId ?? null;
5633
+ if (!remoteLaunchId) {
5634
+ renderSafetyGate(options.stdout, repos, existingBlobs);
5635
+ const hasSecretFindings = repos.some((repo) => repo.secretFindings.length > 0);
5636
+ if (hasSecretFindings && options.allowSecrets !== true) {
5637
+ throw new CliError(
5638
+ "Secret scan findings blocked the upload. Re-run with `--allow-secrets` to proceed."
5639
+ );
5640
+ }
5641
+ throwIfInterrupted4(interruption);
5642
+ if (options.autoApprove !== true && !await promptConfirm({
5643
+ default: false,
5644
+ message: "Proceed with launch upload?",
5645
+ signal: interruption.abortController.signal
5646
+ })) {
5647
+ writeLine(
5648
+ options.stdout,
5649
+ pc10.yellow("Upload cancelled before creating a remote launch.")
5650
+ );
5651
+ return {
5652
+ finalized: false,
5653
+ remoteLaunchId: null
5654
+ };
5655
+ }
5656
+ throwIfInterrupted4(interruption);
5657
+ const createdLaunch = await createRemoteLaunch(
5658
+ options,
5659
+ repos,
5660
+ launchSnapshot.request?.task ?? null,
5661
+ interruption.abortController.signal
5073
5662
  );
5663
+ existingBlobs = createdLaunch.existingBlobs;
5664
+ remoteLaunchId = createdLaunch.remoteLaunchId;
5665
+ await persistCreatedLaunchState(options, remoteLaunchId, repos);
5666
+ throwIfInterrupted4(interruption);
5667
+ if (existingBlobs.size > 0) {
5668
+ writeLine(
5669
+ options.stdout,
5670
+ pc10.cyan(
5671
+ `Remote dedup will reuse ${existingBlobs.size} existing blob${existingBlobs.size === 1 ? "" : "s"}.`
5672
+ )
5673
+ );
5674
+ }
5074
5675
  }
5676
+ const currentSnapshot = await loadLaunchSnapshot(options.launchId, {
5677
+ cwd: options.cwd,
5678
+ homeDir: options.homeDir
5679
+ });
5680
+ const uploadStates = new Map(
5681
+ Object.entries(currentSnapshot.uploadState?.repos ?? {}).map(
5682
+ ([repoId, state]) => [repoId, state?.status]
5683
+ )
5684
+ );
5685
+ const failures = [];
5075
5686
  for (const repo of repos) {
5076
- await recordRepoManifest(
5077
- options.launchId,
5078
- repo.manifest.repoId,
5079
- repo.manifest,
5080
- {
5687
+ throwIfInterrupted4(interruption);
5688
+ if (isTerminalUploadStatus2(uploadStates.get(repo.manifest.repoId))) {
5689
+ writeLine(
5690
+ options.stdout,
5691
+ pc10.cyan(`Skipping ${repo.manifest.repoId}; already completed in upload-state.json.`)
5692
+ );
5693
+ continue;
5694
+ }
5695
+ try {
5696
+ await processRepoUpload(
5697
+ options,
5698
+ repo,
5699
+ remoteLaunchId,
5700
+ interruption.abortController.signal,
5701
+ interruption
5702
+ );
5703
+ const refreshedSnapshot = await loadLaunchSnapshot(options.launchId, {
5081
5704
  cwd: options.cwd,
5082
5705
  homeDir: options.homeDir
5706
+ });
5707
+ uploadStates.set(
5708
+ repo.manifest.repoId,
5709
+ refreshedSnapshot.uploadState?.repos[repo.manifest.repoId]?.status
5710
+ );
5711
+ } catch (error) {
5712
+ if (interruption.interrupted || isInterruptedError3(error)) {
5713
+ throw error;
5083
5714
  }
5084
- );
5715
+ failures.push(repo.manifest.repoId);
5716
+ writeLine(
5717
+ options.stdout,
5718
+ pc10.red(
5719
+ error instanceof Error ? error.message : `Upload failed for ${repo.manifest.repoId}.`
5720
+ )
5721
+ );
5722
+ }
5085
5723
  }
5086
- }
5087
- const uploadStates = new Map(
5088
- Object.entries(launchSnapshot.uploadState?.repos ?? {}).map(
5089
- ([repoId, state]) => [repoId, state?.status]
5090
- )
5091
- );
5092
- const failures = [];
5093
- for (const repo of repos) {
5094
- if (isTerminalUploadStatus2(uploadStates.get(repo.manifest.repoId))) {
5095
- writeLine(
5096
- options.stdout,
5097
- pc9.cyan(`Skipping ${repo.manifest.repoId}; already completed in upload-state.json.`)
5724
+ if (failures.length > 0) {
5725
+ throw new CliError(
5726
+ `Upload failed for ${failures.join(", ")}. Re-run the command to resume the remaining repos.`
5098
5727
  );
5099
- continue;
5100
5728
  }
5101
- try {
5102
- await processRepoUpload(options, repo, remoteLaunchId);
5103
- const refreshedSnapshot = await loadLaunchSnapshot(options.launchId, {
5104
- cwd: options.cwd,
5105
- homeDir: options.homeDir
5106
- });
5107
- uploadStates.set(
5108
- repo.manifest.repoId,
5109
- refreshedSnapshot.uploadState?.repos[repo.manifest.repoId]?.status
5110
- );
5111
- } catch (error) {
5112
- failures.push(repo.manifest.repoId);
5113
- writeLine(
5114
- options.stdout,
5115
- pc9.red(
5116
- error instanceof Error ? error.message : `Upload failed for ${repo.manifest.repoId}.`
5117
- )
5729
+ throwIfInterrupted4(interruption);
5730
+ const finalizeResponse = await options.apiClient.request({
5731
+ method: "POST",
5732
+ path: `/launches/${remoteLaunchId}/finalize`,
5733
+ signal: interruption.abortController.signal
5734
+ });
5735
+ if (finalizeResponse.status !== "complete") {
5736
+ throw new CliError(
5737
+ `Remote launch ${remoteLaunchId} did not finalize successfully.`
5118
5738
  );
5119
5739
  }
5120
- }
5121
- if (failures.length > 0) {
5122
- throw new CliError(
5123
- `Upload failed for ${failures.join(", ")}. Re-run the command to resume the remaining repos.`
5124
- );
5125
- }
5126
- const finalizeResponse = await options.apiClient.request({
5127
- method: "POST",
5128
- path: `/launches/${remoteLaunchId}/finalize`
5129
- });
5130
- if (finalizeResponse.status !== "complete") {
5131
- throw new CliError(
5132
- `Remote launch ${remoteLaunchId} did not finalize successfully.`
5740
+ writeLine(
5741
+ options.stdout,
5742
+ pc10.green(`Upload complete. Remote launch ${remoteLaunchId} is finalized.`)
5133
5743
  );
5744
+ return {
5745
+ finalized: true,
5746
+ remoteLaunchId
5747
+ };
5748
+ } catch (error) {
5749
+ if (interruption.interrupted && isInterruptedError3(error)) {
5750
+ return {
5751
+ exitCode: 130,
5752
+ finalized: false,
5753
+ interrupted: true,
5754
+ remoteLaunchId
5755
+ };
5756
+ }
5757
+ throw error;
5758
+ } finally {
5759
+ if (typeof unregisterSignalHandler === "function") {
5760
+ unregisterSignalHandler();
5761
+ }
5762
+ if (interruption.activeSpinner) {
5763
+ interruption.activeSpinner.stop();
5764
+ interruption.activeSpinner = void 0;
5765
+ }
5134
5766
  }
5135
- writeLine(
5136
- options.stdout,
5137
- pc9.green(`Upload complete. Remote launch ${remoteLaunchId} is finalized.`)
5138
- );
5139
- return {
5140
- finalized: true,
5141
- remoteLaunchId
5142
- };
5143
5767
  }
5144
5768
 
5145
5769
  // src/commands/mission-run.ts
@@ -5212,6 +5836,9 @@ function registerMissionRunCommand(mission, context, dependencies = {}) {
5212
5836
  sleep: dependencies.sleep,
5213
5837
  verbose: globalOptions.verbose === true
5214
5838
  });
5839
+ const registerSignalHandler = createSignalHandlerCoordinator(
5840
+ dependencies.registerSignalHandler
5841
+ );
5215
5842
  const result = await runPlanningFlow({
5216
5843
  autoApprove: options.yes === true,
5217
5844
  client,
@@ -5248,11 +5875,15 @@ function registerMissionRunCommand(mission, context, dependencies = {}) {
5248
5875
  promptConfirm: dependencies.promptConfirm,
5249
5876
  promptInput: dependencies.promptInput,
5250
5877
  promptSelect: dependencies.promptSelect,
5878
+ registerSignalHandler,
5251
5879
  repoPaths,
5252
5880
  sleep: dependencies.sleep,
5253
5881
  stdout: context.stdout,
5254
5882
  taskDescription: task
5255
5883
  });
5884
+ if (result.exitCode && result.exitCode !== 0) {
5885
+ throw new CommanderError3(result.exitCode, "mission-planning", "");
5886
+ }
5256
5887
  if (!result.cancelled && !result.skippedPlanning) {
5257
5888
  writeLine(
5258
5889
  context.stdout,
@@ -5271,9 +5902,13 @@ function registerMissionRunCommand(mission, context, dependencies = {}) {
5271
5902
  homeDir,
5272
5903
  launchId: launchSnapshot.launchId,
5273
5904
  promptConfirm: dependencies.promptConfirm,
5905
+ registerSignalHandler,
5274
5906
  repoPaths,
5275
5907
  stdout: context.stdout
5276
5908
  });
5909
+ if (uploadResult.exitCode && uploadResult.exitCode !== 0) {
5910
+ throw new CommanderError3(uploadResult.exitCode, "mission-upload", "");
5911
+ }
5277
5912
  if (!uploadResult.finalized) {
5278
5913
  return;
5279
5914
  }
@@ -5297,22 +5932,23 @@ function registerMissionRunCommand(mission, context, dependencies = {}) {
5297
5932
  missionId: mission2.missionId,
5298
5933
  promptInput: dependencies.promptInput,
5299
5934
  promptSelect: dependencies.promptSelect,
5300
- registerSignalHandler: dependencies.registerSignalHandler,
5935
+ registerSignalHandler,
5301
5936
  sleep: dependencies.sleep,
5302
- stdout: context.stdout
5937
+ stdout: context.stdout,
5938
+ verbose: globalOptions.verbose === true
5303
5939
  });
5304
5940
  if (monitorResult.exitCode !== 0) {
5305
- throw new CommanderError2(monitorResult.exitCode, "mission-monitor", "");
5941
+ throw new CommanderError3(monitorResult.exitCode, "mission-monitor", "");
5306
5942
  }
5307
5943
  }
5308
5944
  );
5309
5945
  }
5310
5946
 
5311
5947
  // src/commands/mission-status.ts
5312
- import pc10 from "picocolors";
5948
+ import pc11 from "picocolors";
5313
5949
  function writeSection4(stdout, title) {
5314
5950
  writeLine(stdout);
5315
- writeLine(stdout, pc10.bold(title));
5951
+ writeLine(stdout, pc11.bold(title));
5316
5952
  }
5317
5953
  function normalizeMilestoneOrder(milestones, features, assertions) {
5318
5954
  const known = new Map(milestones.map((milestone) => [milestone.name, milestone]));
@@ -5412,7 +6048,7 @@ function renderMilestones2(stdout, milestones, features) {
5412
6048
  for (const milestone of milestones) {
5413
6049
  writeLine(
5414
6050
  stdout,
5415
- `- ${pc10.cyan(milestone.name)} (${milestone.state}) \xB7 ${milestone.completedFeatureCount}/${milestone.featureCount} features \xB7 ${milestone.passedAssertionCount}/${milestone.assertionCount} assertions`
6051
+ `- ${pc11.cyan(milestone.name)} (${milestone.state}) \xB7 ${milestone.completedFeatureCount}/${milestone.featureCount} features \xB7 ${milestone.passedAssertionCount}/${milestone.assertionCount} assertions`
5416
6052
  );
5417
6053
  const milestoneFeatures = features.filter(
5418
6054
  (feature) => feature.milestone === milestone.name
@@ -5510,7 +6146,7 @@ function writeError(stream, message) {
5510
6146
  writeLine(stream, `Error: ${message}`);
5511
6147
  }
5512
6148
  function handleRunError(error, stderr) {
5513
- if (error instanceof CommanderError3) {
6149
+ if (error instanceof CommanderError4) {
5514
6150
  return error.exitCode;
5515
6151
  }
5516
6152
  if (error instanceof CliError) {