@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.
- package/dist/hz.mjs +1300 -664
- 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
|
|
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
|
|
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)
|
|
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)
|
|
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
|
-
|
|
815
|
-
|
|
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
|
|
832
|
-
|
|
833
|
-
await apiClient.request({
|
|
834
|
-
path: "/missions"
|
|
835
|
-
})
|
|
869
|
+
const registerSignalHandler = createSignalHandlerCoordinator(
|
|
870
|
+
dependencies.registerSignalHandler
|
|
836
871
|
);
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
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
|
-
|
|
1740
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
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
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
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
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
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
|
-
|
|
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 `${
|
|
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 ${
|
|
2418
|
+
return `Backfilled ${type} for ${pc5.cyan(featureId)}.`;
|
|
2293
2419
|
}
|
|
2294
2420
|
if (assertionId) {
|
|
2295
|
-
return `Backfilled ${type} for ${
|
|
2421
|
+
return `Backfilled ${type} for ${pc5.cyan(assertionId)}.`;
|
|
2296
2422
|
}
|
|
2297
2423
|
if (milestone) {
|
|
2298
|
-
return `Backfilled ${type} for 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
|
-
|
|
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 ${
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
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
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
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
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
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
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
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
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
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
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
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
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
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
|
-
|
|
2590
|
-
`Triage is required for ${
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
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
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
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
|
|
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: ${
|
|
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
|
-
|
|
2794
|
-
|
|
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,
|
|
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
|
|
2861
|
-
apiClient.request({ path: `/missions/${missionId}/milestones
|
|
2862
|
-
apiClient.request({ path: `/missions/${missionId}/assertions
|
|
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
|
|
3172
|
+
return pc7.green(status);
|
|
2953
3173
|
case "failed":
|
|
2954
|
-
return
|
|
3174
|
+
return pc7.red(status);
|
|
2955
3175
|
case "skipped":
|
|
2956
|
-
return
|
|
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
|
-
|
|
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 ${
|
|
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 ${
|
|
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 ${
|
|
3315
|
+
`Mission ${pc7.cyan(missionId)} cannot be ${verb} because it is currently ${pc7.bold(missionState.state)}.`
|
|
3081
3316
|
);
|
|
3082
|
-
throw new
|
|
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
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
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
|
-
|
|
3121
|
-
|
|
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
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
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
|
|
3464
|
+
snapshot = await fetchMissionSnapshot(
|
|
3175
3465
|
apiClient,
|
|
3176
3466
|
missionId,
|
|
3177
|
-
|
|
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
|
-
`
|
|
3473
|
+
`Reconnecting to mission ${pc7.cyan(missionId)} from state ${pc7.bold(snapshot.state.state)}.`
|
|
3182
3474
|
);
|
|
3183
3475
|
}
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
)
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
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
|
|
3229
|
-
|
|
3523
|
+
const lifecycleSignals = createLifecycleInterruptContext(
|
|
3524
|
+
dependencies.registerSignalHandler,
|
|
3230
3525
|
context.stdout,
|
|
3231
|
-
|
|
3526
|
+
"Interrupted. Remote mission continues. Re-run this command to reconnect when ready."
|
|
3232
3527
|
);
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
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
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
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
|
-
|
|
3791
|
-
|
|
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
|
-
|
|
3797
|
-
|
|
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,
|
|
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, ` ${
|
|
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}. ${
|
|
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
|
-
|
|
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
|
|
4001
|
-
session = await getPlanningSession(
|
|
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(
|
|
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
|
-
|
|
4033
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
4095
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
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
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
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
|
-
|
|
4176
|
-
options
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
|
|
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:
|
|
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
|
|
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
|
|
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
|
-
|
|
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,
|
|
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 ?
|
|
4811
|
-
const secretSummary = repo.secretFindings.length > 0 ?
|
|
4812
|
-
const dedupSummary = existingBlobs.has(repo.manifest.archiveSha256) ?
|
|
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
|
-
`- ${
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
"
|
|
5596
|
+
"Cannot start uploads before a mission draft has been approved."
|
|
5045
5597
|
);
|
|
5046
5598
|
}
|
|
5047
|
-
|
|
5048
|
-
|
|
5049
|
-
|
|
5050
|
-
|
|
5051
|
-
|
|
5052
|
-
|
|
5053
|
-
|
|
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
|
-
|
|
5061
|
-
|
|
5062
|
-
|
|
5063
|
-
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
|
|
5071
|
-
|
|
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
|
-
|
|
5077
|
-
|
|
5078
|
-
|
|
5079
|
-
|
|
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
|
-
|
|
5088
|
-
|
|
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
|
-
|
|
5102
|
-
|
|
5103
|
-
|
|
5104
|
-
|
|
5105
|
-
|
|
5106
|
-
|
|
5107
|
-
|
|
5108
|
-
|
|
5109
|
-
|
|
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
|
-
|
|
5122
|
-
|
|
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
|
|
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
|
|
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
|
|
5948
|
+
import pc11 from "picocolors";
|
|
5313
5949
|
function writeSection4(stdout, title) {
|
|
5314
5950
|
writeLine(stdout);
|
|
5315
|
-
writeLine(stdout,
|
|
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
|
-
`- ${
|
|
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
|
|
6149
|
+
if (error instanceof CommanderError4) {
|
|
5514
6150
|
return error.exitCode;
|
|
5515
6151
|
}
|
|
5516
6152
|
if (error instanceof CliError) {
|