@coresource/hz 0.20.2 → 0.20.4
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 +1347 -668
- package/package.json +2 -2
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";
|
|
@@ -3670,11 +3986,36 @@ function asNonEmptyString8(value) {
|
|
|
3670
3986
|
function asStringArray2(value) {
|
|
3671
3987
|
return Array.isArray(value) ? value.map((item) => asNonEmptyString8(item)).filter((item) => item !== null) : [];
|
|
3672
3988
|
}
|
|
3989
|
+
function asCodeSnippets(value) {
|
|
3990
|
+
if (!Array.isArray(value)) {
|
|
3991
|
+
return [];
|
|
3992
|
+
}
|
|
3993
|
+
const snippets = [];
|
|
3994
|
+
const dedupe = /* @__PURE__ */ new Set();
|
|
3995
|
+
for (const entry of value) {
|
|
3996
|
+
const snippet = asNonEmptyString8(
|
|
3997
|
+
typeof entry === "string" ? entry : isRecord7(entry) ? entry.snippet ?? entry.code ?? entry.code_evidence ?? entry.content : void 0
|
|
3998
|
+
);
|
|
3999
|
+
if (!snippet) {
|
|
4000
|
+
continue;
|
|
4001
|
+
}
|
|
4002
|
+
const pathValue = isRecord7(entry) ? asNonEmptyString8(entry.path ?? entry.file_path ?? entry.filePath) : null;
|
|
4003
|
+
const key = `${pathValue ?? ""}\0${snippet}`;
|
|
4004
|
+
if (dedupe.has(key)) {
|
|
4005
|
+
continue;
|
|
4006
|
+
}
|
|
4007
|
+
dedupe.add(key);
|
|
4008
|
+
snippets.push(pathValue ? { path: pathValue, snippet } : { snippet });
|
|
4009
|
+
}
|
|
4010
|
+
return snippets;
|
|
4011
|
+
}
|
|
3673
4012
|
function extractQuestions(payload) {
|
|
3674
4013
|
if (!payload || !Array.isArray(payload.questions)) {
|
|
3675
4014
|
return [];
|
|
3676
4015
|
}
|
|
3677
4016
|
return payload.questions.filter((question) => isRecord7(question)).map((question) => ({
|
|
4017
|
+
codeSnippets: asCodeSnippets(question.codeSnippets ?? question.code_snippets ?? question.snippets),
|
|
4018
|
+
context: asNonEmptyString8(question.context) ?? void 0,
|
|
3678
4019
|
detailPrompt: asNonEmptyString8(question.detailPrompt) ?? void 0,
|
|
3679
4020
|
freeTextOptionId: asNonEmptyString8(question.freeTextOptionId) ?? void 0,
|
|
3680
4021
|
id: asNonEmptyString8(question.id) ?? "question",
|
|
@@ -3743,6 +4084,54 @@ function extractErrorCode(error) {
|
|
|
3743
4084
|
function extractRound(payload) {
|
|
3744
4085
|
return typeof payload?.round === "number" && Number.isFinite(payload.round) ? payload.round : 1;
|
|
3745
4086
|
}
|
|
4087
|
+
function isAbortError3(error) {
|
|
4088
|
+
return error instanceof DOMException && error.name === "AbortError";
|
|
4089
|
+
}
|
|
4090
|
+
function isAbortPromptError2(error) {
|
|
4091
|
+
return error instanceof Error && error.name === "AbortPromptError";
|
|
4092
|
+
}
|
|
4093
|
+
function isInterruptedError2(error) {
|
|
4094
|
+
return error instanceof CliError && error.exitCode === 130 || isAbortError3(error) || isAbortPromptError2(error);
|
|
4095
|
+
}
|
|
4096
|
+
function throwIfInterrupted3(interruption) {
|
|
4097
|
+
if (interruption.interrupted) {
|
|
4098
|
+
throw new CliError("Planning interrupted.", 130);
|
|
4099
|
+
}
|
|
4100
|
+
}
|
|
4101
|
+
function interruptPlanning(interruption) {
|
|
4102
|
+
if (interruption.interrupted) {
|
|
4103
|
+
return;
|
|
4104
|
+
}
|
|
4105
|
+
interruption.interrupted = true;
|
|
4106
|
+
interruption.activeSpinner?.stop();
|
|
4107
|
+
interruption.activeSpinner = void 0;
|
|
4108
|
+
interruption.releaseActiveWait?.();
|
|
4109
|
+
interruption.releaseActiveWait = void 0;
|
|
4110
|
+
if (!interruption.abortController.signal.aborted) {
|
|
4111
|
+
interruption.abortController.abort();
|
|
4112
|
+
}
|
|
4113
|
+
writeLine(
|
|
4114
|
+
interruption.stdout,
|
|
4115
|
+
pc9.yellow("Interrupted. Re-run this command to resume the planning session.")
|
|
4116
|
+
);
|
|
4117
|
+
}
|
|
4118
|
+
async function waitForInterruptibleDelay(sleep, ms, interruption) {
|
|
4119
|
+
throwIfInterrupted3(interruption);
|
|
4120
|
+
let resolveWait;
|
|
4121
|
+
await Promise.race([
|
|
4122
|
+
sleep(ms).then(() => {
|
|
4123
|
+
resolveWait?.();
|
|
4124
|
+
}),
|
|
4125
|
+
new Promise((resolve) => {
|
|
4126
|
+
resolveWait = () => resolve();
|
|
4127
|
+
interruption.releaseActiveWait = resolveWait;
|
|
4128
|
+
})
|
|
4129
|
+
]);
|
|
4130
|
+
if (interruption.releaseActiveWait === resolveWait) {
|
|
4131
|
+
interruption.releaseActiveWait = void 0;
|
|
4132
|
+
}
|
|
4133
|
+
throwIfInterrupted3(interruption);
|
|
4134
|
+
}
|
|
3746
4135
|
function defaultCreateSpinner2(text) {
|
|
3747
4136
|
const spinner = ora2({ text });
|
|
3748
4137
|
return {
|
|
@@ -3776,66 +4165,96 @@ function defaultCreateSpinner2(text) {
|
|
|
3776
4165
|
};
|
|
3777
4166
|
}
|
|
3778
4167
|
async function defaultPromptSelect2(options) {
|
|
3779
|
-
return defaultSelectPrompt2(
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
4168
|
+
return defaultSelectPrompt2(
|
|
4169
|
+
{
|
|
4170
|
+
choices: options.choices.map((choice) => ({
|
|
4171
|
+
description: choice.description,
|
|
4172
|
+
name: choice.name,
|
|
4173
|
+
value: choice.value
|
|
4174
|
+
})),
|
|
4175
|
+
message: options.message
|
|
4176
|
+
},
|
|
4177
|
+
{ signal: options.signal }
|
|
4178
|
+
);
|
|
3787
4179
|
}
|
|
3788
4180
|
async function defaultPromptInput2(options) {
|
|
3789
|
-
return defaultInputPrompt2(
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
4181
|
+
return defaultInputPrompt2(
|
|
4182
|
+
{
|
|
4183
|
+
default: options.default,
|
|
4184
|
+
message: options.message
|
|
4185
|
+
},
|
|
4186
|
+
{ signal: options.signal }
|
|
4187
|
+
);
|
|
3793
4188
|
}
|
|
3794
4189
|
async function defaultPromptConfirm2(options) {
|
|
3795
|
-
return defaultConfirmPrompt2(
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
|
|
4190
|
+
return defaultConfirmPrompt2(
|
|
4191
|
+
{
|
|
4192
|
+
default: options.default,
|
|
4193
|
+
message: options.message
|
|
4194
|
+
},
|
|
4195
|
+
{ signal: options.signal }
|
|
4196
|
+
);
|
|
3799
4197
|
}
|
|
3800
|
-
async function getPlanningSession(client, sessionId) {
|
|
4198
|
+
async function getPlanningSession(client, sessionId, signal) {
|
|
3801
4199
|
return client.request({
|
|
3802
|
-
path: `/plan/${sessionId}
|
|
4200
|
+
path: `/plan/${sessionId}`,
|
|
4201
|
+
signal
|
|
3803
4202
|
});
|
|
3804
4203
|
}
|
|
3805
|
-
async function postClarification(client, sessionId, body) {
|
|
4204
|
+
async function postClarification(client, sessionId, signal, body) {
|
|
3806
4205
|
return client.request({
|
|
3807
4206
|
body,
|
|
3808
4207
|
method: "POST",
|
|
3809
|
-
path: `/plan/${sessionId}/clarify
|
|
4208
|
+
path: `/plan/${sessionId}/clarify`,
|
|
4209
|
+
signal
|
|
3810
4210
|
});
|
|
3811
4211
|
}
|
|
3812
|
-
async function postMilestoneConfirmation(client, sessionId, body) {
|
|
4212
|
+
async function postMilestoneConfirmation(client, sessionId, signal, body) {
|
|
3813
4213
|
return client.request({
|
|
3814
4214
|
body,
|
|
3815
4215
|
method: "POST",
|
|
3816
|
-
path: `/plan/${sessionId}/confirm-milestones
|
|
4216
|
+
path: `/plan/${sessionId}/confirm-milestones`,
|
|
4217
|
+
signal
|
|
3817
4218
|
});
|
|
3818
4219
|
}
|
|
3819
|
-
async function postDraftApproval(client, sessionId) {
|
|
4220
|
+
async function postDraftApproval(client, sessionId, signal) {
|
|
3820
4221
|
return client.request({
|
|
3821
4222
|
method: "POST",
|
|
3822
|
-
path: `/plan/${sessionId}/approve
|
|
4223
|
+
path: `/plan/${sessionId}/approve`,
|
|
4224
|
+
signal
|
|
3823
4225
|
});
|
|
3824
4226
|
}
|
|
3825
4227
|
function writeSection3(stdout, title) {
|
|
3826
4228
|
writeLine(stdout);
|
|
3827
|
-
writeLine(stdout,
|
|
4229
|
+
writeLine(stdout, pc9.bold(title));
|
|
4230
|
+
}
|
|
4231
|
+
function formatInlineSnippet(snippet) {
|
|
4232
|
+
return snippet.replace(/\s+/g, " ").trim();
|
|
4233
|
+
}
|
|
4234
|
+
function isFreeTextQuestion(question) {
|
|
4235
|
+
return Boolean(question.freeTextOptionId) || !question.options || question.options.length === 0;
|
|
3828
4236
|
}
|
|
3829
4237
|
function renderQuestion(stdout, question, index) {
|
|
3830
4238
|
writeLine(stdout, `${index + 1}. ${question.text}`);
|
|
3831
4239
|
if (question.references && question.references.length > 0) {
|
|
3832
|
-
writeLine(stdout, ` ${
|
|
4240
|
+
writeLine(stdout, ` ${pc9.dim(`References: ${question.references.join(", ")}`)}`);
|
|
4241
|
+
}
|
|
4242
|
+
if (question.context) {
|
|
4243
|
+
writeLine(stdout, ` ${pc9.dim(`Context: ${question.context}`)}`);
|
|
4244
|
+
}
|
|
4245
|
+
if (question.codeSnippets && question.codeSnippets.length > 0) {
|
|
4246
|
+
writeLine(stdout, ` ${pc9.dim("Code:")}`);
|
|
4247
|
+
for (const codeSnippet of question.codeSnippets) {
|
|
4248
|
+
const renderedSnippet = formatInlineSnippet(codeSnippet.snippet);
|
|
4249
|
+
const label = codeSnippet.path ? `${codeSnippet.path}: ` : "";
|
|
4250
|
+
writeLine(stdout, ` ${label}${renderedSnippet}`);
|
|
4251
|
+
}
|
|
3833
4252
|
}
|
|
3834
4253
|
}
|
|
3835
4254
|
function renderMilestones(stdout, milestones, reviewRound) {
|
|
3836
4255
|
writeSection3(stdout, `Milestone review round ${reviewRound}`);
|
|
3837
4256
|
milestones.forEach((milestone, index) => {
|
|
3838
|
-
writeLine(stdout, `${index + 1}. ${
|
|
4257
|
+
writeLine(stdout, `${index + 1}. ${pc9.cyan(milestone.name)}`);
|
|
3839
4258
|
if (milestone.description) {
|
|
3840
4259
|
writeLine(stdout, ` ${milestone.description}`);
|
|
3841
4260
|
}
|
|
@@ -3923,24 +4342,28 @@ function buildAutoApprovedDetail(question, taskDescription, repoPaths) {
|
|
|
3923
4342
|
}
|
|
3924
4343
|
return `${task}. Use ${repoPhrase} as the source of truth.${references}`;
|
|
3925
4344
|
}
|
|
3926
|
-
async function promptForAnswers(options, questions, round) {
|
|
4345
|
+
async function promptForAnswers(options, interruption, questions, round) {
|
|
3927
4346
|
const promptSelect = options.promptSelect ?? defaultPromptSelect2;
|
|
3928
4347
|
const promptInput = options.promptInput ?? defaultPromptInput2;
|
|
3929
4348
|
const answers = [];
|
|
3930
4349
|
const transcript = [];
|
|
3931
4350
|
for (const question of questions) {
|
|
3932
|
-
|
|
4351
|
+
throwIfInterrupted3(interruption);
|
|
4352
|
+
if (!isFreeTextQuestion(question)) {
|
|
4353
|
+
const selectableOptions = question.options ?? [];
|
|
3933
4354
|
const autoSelectedOption = options.autoApprove ? pickAutoApprovedOption(question, options.taskDescription, options.repoPaths) : null;
|
|
3934
|
-
const choices =
|
|
4355
|
+
const choices = selectableOptions.map((option) => ({
|
|
3935
4356
|
description: option.description,
|
|
3936
4357
|
name: option.label,
|
|
3937
4358
|
value: option.id
|
|
3938
4359
|
}));
|
|
3939
4360
|
const optionId2 = autoSelectedOption?.id ?? await promptSelect({
|
|
3940
4361
|
choices,
|
|
3941
|
-
message: question.text
|
|
4362
|
+
message: question.text,
|
|
4363
|
+
signal: interruption.abortController.signal
|
|
3942
4364
|
});
|
|
3943
|
-
|
|
4365
|
+
throwIfInterrupted3(interruption);
|
|
4366
|
+
const selectedOption = selectableOptions.find((option) => option.id === optionId2);
|
|
3944
4367
|
answers.push({
|
|
3945
4368
|
optionId: optionId2,
|
|
3946
4369
|
questionId: question.id
|
|
@@ -3957,9 +4380,11 @@ async function promptForAnswers(options, questions, round) {
|
|
|
3957
4380
|
}
|
|
3958
4381
|
const detail = options.autoApprove ? buildAutoApprovedDetail(question, options.taskDescription, options.repoPaths) : await promptInput({
|
|
3959
4382
|
default: question.inputDefault,
|
|
3960
|
-
message: question.detailPrompt ?? question.text
|
|
4383
|
+
message: question.detailPrompt ?? question.text,
|
|
4384
|
+
signal: interruption.abortController.signal
|
|
3961
4385
|
});
|
|
3962
|
-
|
|
4386
|
+
throwIfInterrupted3(interruption);
|
|
4387
|
+
const optionId = FREE_TEXT_OPTION_ID;
|
|
3963
4388
|
answers.push({
|
|
3964
4389
|
detail,
|
|
3965
4390
|
optionId,
|
|
@@ -3979,7 +4404,7 @@ async function promptForAnswers(options, questions, round) {
|
|
|
3979
4404
|
transcript
|
|
3980
4405
|
};
|
|
3981
4406
|
}
|
|
3982
|
-
async function waitForAnalysis(options, sessionId) {
|
|
4407
|
+
async function waitForAnalysis(options, interruption, sessionId) {
|
|
3983
4408
|
const createSpinner = options.createSpinner ?? defaultCreateSpinner2;
|
|
3984
4409
|
const sleep = options.sleep ?? (async (ms) => {
|
|
3985
4410
|
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -3989,49 +4414,81 @@ async function waitForAnalysis(options, sessionId) {
|
|
|
3989
4414
|
const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
3990
4415
|
const deadline = now() + timeoutMs;
|
|
3991
4416
|
const spinner = createSpinner("Analyzing repository for planning...");
|
|
4417
|
+
interruption.activeSpinner = spinner;
|
|
3992
4418
|
spinner.start();
|
|
3993
4419
|
try {
|
|
3994
|
-
|
|
4420
|
+
throwIfInterrupted3(interruption);
|
|
4421
|
+
let session = await getPlanningSession(
|
|
4422
|
+
options.client,
|
|
4423
|
+
sessionId,
|
|
4424
|
+
interruption.abortController.signal
|
|
4425
|
+
);
|
|
3995
4426
|
while (session.state === "created" || session.state === "analyzing") {
|
|
4427
|
+
throwIfInterrupted3(interruption);
|
|
3996
4428
|
if (now() >= deadline) {
|
|
3997
4429
|
spinner.fail("Planning analysis timed out.");
|
|
3998
4430
|
throw new CliError("Planning analysis timed out after 5 minutes.");
|
|
3999
4431
|
}
|
|
4000
|
-
await sleep
|
|
4001
|
-
session = await getPlanningSession(
|
|
4432
|
+
await waitForInterruptibleDelay(sleep, pollIntervalMs, interruption);
|
|
4433
|
+
session = await getPlanningSession(
|
|
4434
|
+
options.client,
|
|
4435
|
+
sessionId,
|
|
4436
|
+
interruption.abortController.signal
|
|
4437
|
+
);
|
|
4002
4438
|
}
|
|
4439
|
+
throwIfInterrupted3(interruption);
|
|
4003
4440
|
spinner.succeed("Analysis complete.");
|
|
4004
4441
|
return session;
|
|
4005
4442
|
} catch (error) {
|
|
4443
|
+
if (isInterruptedError2(error)) {
|
|
4444
|
+
throw error;
|
|
4445
|
+
}
|
|
4006
4446
|
if (error instanceof CliError) {
|
|
4007
4447
|
throw error;
|
|
4008
4448
|
}
|
|
4009
4449
|
spinner.fail("Planning analysis failed.");
|
|
4010
4450
|
throw error;
|
|
4451
|
+
} finally {
|
|
4452
|
+
if (interruption.activeSpinner === spinner) {
|
|
4453
|
+
interruption.activeSpinner = void 0;
|
|
4454
|
+
}
|
|
4011
4455
|
}
|
|
4012
4456
|
}
|
|
4013
|
-
async function resolveClarification(options, sessionId, initialPayload) {
|
|
4457
|
+
async function resolveClarification(options, interruption, sessionId, initialPayload) {
|
|
4014
4458
|
let payload = initialPayload;
|
|
4015
4459
|
const localTranscript = [];
|
|
4016
4460
|
while (payload.state === "clarifying") {
|
|
4461
|
+
throwIfInterrupted3(interruption);
|
|
4017
4462
|
let questions = extractQuestions(payload);
|
|
4018
4463
|
let round = extractRound(payload);
|
|
4019
4464
|
if (questions.length === 0) {
|
|
4020
|
-
payload = await postClarification(
|
|
4465
|
+
payload = await postClarification(
|
|
4466
|
+
options.client,
|
|
4467
|
+
sessionId,
|
|
4468
|
+
interruption.abortController.signal,
|
|
4469
|
+
{}
|
|
4470
|
+
);
|
|
4021
4471
|
questions = extractQuestions(payload);
|
|
4022
4472
|
round = extractRound(payload);
|
|
4023
4473
|
}
|
|
4474
|
+
throwIfInterrupted3(interruption);
|
|
4024
4475
|
if (payload.state !== "clarifying") {
|
|
4025
4476
|
break;
|
|
4026
4477
|
}
|
|
4027
4478
|
writeSection3(options.stdout, `Clarification round ${round}`);
|
|
4028
4479
|
questions.forEach((question, index) => renderQuestion(options.stdout, question, index));
|
|
4029
|
-
const prompted = await promptForAnswers(options, questions, round);
|
|
4480
|
+
const prompted = await promptForAnswers(options, interruption, questions, round);
|
|
4030
4481
|
localTranscript.push(...prompted.transcript);
|
|
4031
4482
|
try {
|
|
4032
|
-
|
|
4033
|
-
|
|
4034
|
-
|
|
4483
|
+
throwIfInterrupted3(interruption);
|
|
4484
|
+
payload = await postClarification(
|
|
4485
|
+
options.client,
|
|
4486
|
+
sessionId,
|
|
4487
|
+
interruption.abortController.signal,
|
|
4488
|
+
{
|
|
4489
|
+
answers: prompted.answers
|
|
4490
|
+
}
|
|
4491
|
+
);
|
|
4035
4492
|
} catch (error) {
|
|
4036
4493
|
if (extractErrorCode(error) !== "resolvedness_gate_failed") {
|
|
4037
4494
|
throw error;
|
|
@@ -4045,9 +4502,15 @@ async function resolveClarification(options, sessionId, initialPayload) {
|
|
|
4045
4502
|
}
|
|
4046
4503
|
writeLine(
|
|
4047
4504
|
options.stdout,
|
|
4048
|
-
|
|
4505
|
+
pc9.yellow("Proceeding because --force is enabled.")
|
|
4506
|
+
);
|
|
4507
|
+
throwIfInterrupted3(interruption);
|
|
4508
|
+
payload = await postMilestoneConfirmation(
|
|
4509
|
+
options.client,
|
|
4510
|
+
sessionId,
|
|
4511
|
+
interruption.abortController.signal,
|
|
4512
|
+
{}
|
|
4049
4513
|
);
|
|
4050
|
-
payload = await postMilestoneConfirmation(options.client, sessionId, {});
|
|
4051
4514
|
return {
|
|
4052
4515
|
payload,
|
|
4053
4516
|
persistedClarification: {
|
|
@@ -4077,35 +4540,55 @@ async function resolveClarification(options, sessionId, initialPayload) {
|
|
|
4077
4540
|
}
|
|
4078
4541
|
};
|
|
4079
4542
|
}
|
|
4080
|
-
async function resolveMilestones(options, sessionId, initialPayload) {
|
|
4543
|
+
async function resolveMilestones(options, interruption, sessionId, initialPayload) {
|
|
4081
4544
|
let reviewRound = 1;
|
|
4082
4545
|
let payload = initialPayload;
|
|
4083
4546
|
if (extractMilestones(payload).length === 0) {
|
|
4084
|
-
payload = await postMilestoneConfirmation(
|
|
4547
|
+
payload = await postMilestoneConfirmation(
|
|
4548
|
+
options.client,
|
|
4549
|
+
sessionId,
|
|
4550
|
+
interruption.abortController.signal,
|
|
4551
|
+
{}
|
|
4552
|
+
);
|
|
4085
4553
|
}
|
|
4086
4554
|
while (true) {
|
|
4555
|
+
throwIfInterrupted3(interruption);
|
|
4087
4556
|
const milestones = extractMilestones(payload);
|
|
4088
4557
|
renderMilestones(options.stdout, milestones, reviewRound);
|
|
4089
4558
|
const confirmed = options.autoApprove ? true : await (options.promptConfirm ?? defaultPromptConfirm2)({
|
|
4090
4559
|
default: true,
|
|
4091
|
-
message: "Do these milestones look correct?"
|
|
4560
|
+
message: "Do these milestones look correct?",
|
|
4561
|
+
signal: interruption.abortController.signal
|
|
4092
4562
|
});
|
|
4563
|
+
throwIfInterrupted3(interruption);
|
|
4093
4564
|
if (confirmed) {
|
|
4094
|
-
return postMilestoneConfirmation(
|
|
4095
|
-
|
|
4096
|
-
|
|
4565
|
+
return postMilestoneConfirmation(
|
|
4566
|
+
options.client,
|
|
4567
|
+
sessionId,
|
|
4568
|
+
interruption.abortController.signal,
|
|
4569
|
+
{
|
|
4570
|
+
confirmed: true
|
|
4571
|
+
}
|
|
4572
|
+
);
|
|
4097
4573
|
}
|
|
4098
4574
|
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
|
|
4575
|
+
message: "What should change about these milestones?",
|
|
4576
|
+
signal: interruption.abortController.signal
|
|
4104
4577
|
});
|
|
4578
|
+
throwIfInterrupted3(interruption);
|
|
4579
|
+
payload = await postMilestoneConfirmation(
|
|
4580
|
+
options.client,
|
|
4581
|
+
sessionId,
|
|
4582
|
+
interruption.abortController.signal,
|
|
4583
|
+
{
|
|
4584
|
+
confirmed: false,
|
|
4585
|
+
feedback
|
|
4586
|
+
}
|
|
4587
|
+
);
|
|
4105
4588
|
reviewRound += 1;
|
|
4106
4589
|
}
|
|
4107
4590
|
}
|
|
4108
|
-
async function resolveDraft(options, sessionId, payload) {
|
|
4591
|
+
async function resolveDraft(options, interruption, sessionId, payload) {
|
|
4109
4592
|
const directDraft = extractDraft(payload);
|
|
4110
4593
|
if (directDraft) {
|
|
4111
4594
|
return {
|
|
@@ -4113,7 +4596,11 @@ async function resolveDraft(options, sessionId, payload) {
|
|
|
4113
4596
|
payload
|
|
4114
4597
|
};
|
|
4115
4598
|
}
|
|
4116
|
-
const approvedPayload = await postDraftApproval(
|
|
4599
|
+
const approvedPayload = await postDraftApproval(
|
|
4600
|
+
options.client,
|
|
4601
|
+
sessionId,
|
|
4602
|
+
interruption.abortController.signal
|
|
4603
|
+
);
|
|
4117
4604
|
const approvedDraft = extractDraft(approvedPayload);
|
|
4118
4605
|
if (!approvedDraft) {
|
|
4119
4606
|
throw new CliError("Planner did not return a mission draft.");
|
|
@@ -4127,7 +4614,7 @@ async function runPlanningFlow(options) {
|
|
|
4127
4614
|
if (options.existingMissionDraft) {
|
|
4128
4615
|
writeLine(
|
|
4129
4616
|
options.stdout,
|
|
4130
|
-
|
|
4617
|
+
pc9.cyan("Skipping planning because mission-draft.json already exists.")
|
|
4131
4618
|
);
|
|
4132
4619
|
return {
|
|
4133
4620
|
cancelled: false,
|
|
@@ -4136,69 +4623,113 @@ async function runPlanningFlow(options) {
|
|
|
4136
4623
|
skippedPlanning: true
|
|
4137
4624
|
};
|
|
4138
4625
|
}
|
|
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);
|
|
4626
|
+
const interruption = {
|
|
4627
|
+
abortController: new AbortController(),
|
|
4628
|
+
interrupted: false,
|
|
4629
|
+
stdout: options.stdout
|
|
4630
|
+
};
|
|
4631
|
+
let sessionId = asNonEmptyString8(options.existingSessionId) ?? "";
|
|
4632
|
+
const unregisterSignalHandler = options.registerSignalHandler?.("SIGINT", () => {
|
|
4633
|
+
interruptPlanning(interruption);
|
|
4634
|
+
});
|
|
4635
|
+
try {
|
|
4636
|
+
sessionId = sessionId || asNonEmptyString8(
|
|
4637
|
+
(await options.client.request({
|
|
4638
|
+
body: {
|
|
4639
|
+
repos: options.repoPaths,
|
|
4640
|
+
task_description: options.taskDescription
|
|
4641
|
+
},
|
|
4642
|
+
method: "POST",
|
|
4643
|
+
path: "/plan/create",
|
|
4644
|
+
signal: interruption.abortController.signal,
|
|
4645
|
+
timeoutMs: 3e4
|
|
4646
|
+
})).session_id
|
|
4647
|
+
) || "";
|
|
4648
|
+
if (!sessionId) {
|
|
4649
|
+
throw new CliError("Planner did not return a session id.");
|
|
4165
4650
|
}
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4651
|
+
if (options.existingSessionId) {
|
|
4652
|
+
writeLine(options.stdout, `Resuming planning session ${sessionId}.`);
|
|
4653
|
+
} else {
|
|
4654
|
+
await options.persistSessionId(sessionId);
|
|
4655
|
+
throwIfInterrupted3(interruption);
|
|
4656
|
+
writeLine(options.stdout, `Created planning session ${sessionId}.`);
|
|
4657
|
+
}
|
|
4658
|
+
let payload = await waitForAnalysis(options, interruption, sessionId);
|
|
4659
|
+
throwIfInterrupted3(interruption);
|
|
4660
|
+
if (payload.state === "clarifying") {
|
|
4661
|
+
const clarification = await resolveClarification(
|
|
4662
|
+
options,
|
|
4663
|
+
interruption,
|
|
4664
|
+
sessionId,
|
|
4665
|
+
payload
|
|
4666
|
+
);
|
|
4667
|
+
payload = clarification.payload;
|
|
4668
|
+
throwIfInterrupted3(interruption);
|
|
4669
|
+
if (clarification.persistedClarification) {
|
|
4670
|
+
await options.persistClarification(clarification.persistedClarification);
|
|
4671
|
+
throwIfInterrupted3(interruption);
|
|
4672
|
+
}
|
|
4673
|
+
}
|
|
4674
|
+
throwIfInterrupted3(interruption);
|
|
4675
|
+
if (payload.state === "confirming") {
|
|
4676
|
+
payload = await resolveMilestones(options, interruption, sessionId, payload);
|
|
4677
|
+
}
|
|
4678
|
+
throwIfInterrupted3(interruption);
|
|
4679
|
+
if (payload.state !== "complete") {
|
|
4680
|
+
throw new CliError(
|
|
4681
|
+
`Planner returned an unexpected state after milestone confirmation: ${payload.state ?? "unknown"}.`
|
|
4682
|
+
);
|
|
4683
|
+
}
|
|
4684
|
+
const { draft, payload: summaryPayload } = await resolveDraft(
|
|
4685
|
+
options,
|
|
4686
|
+
interruption,
|
|
4687
|
+
sessionId,
|
|
4688
|
+
payload
|
|
4173
4689
|
);
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
options
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
|
|
4690
|
+
throwIfInterrupted3(interruption);
|
|
4691
|
+
renderDraftSummary(options.stdout, summaryPayload, draft);
|
|
4692
|
+
const approved = options.autoApprove ? true : await (options.promptConfirm ?? defaultPromptConfirm2)({
|
|
4693
|
+
default: true,
|
|
4694
|
+
message: "Approve this draft and continue to upload?",
|
|
4695
|
+
signal: interruption.abortController.signal
|
|
4696
|
+
});
|
|
4697
|
+
throwIfInterrupted3(interruption);
|
|
4698
|
+
if (!approved) {
|
|
4699
|
+
writeLine(options.stdout, pc9.yellow("Planning cancelled before upload."));
|
|
4700
|
+
return {
|
|
4701
|
+
cancelled: true,
|
|
4702
|
+
draft,
|
|
4703
|
+
sessionId,
|
|
4704
|
+
skippedPlanning: false
|
|
4705
|
+
};
|
|
4706
|
+
}
|
|
4707
|
+
await options.persistMissionDraft(draft);
|
|
4708
|
+
throwIfInterrupted3(interruption);
|
|
4709
|
+
writeLine(options.stdout, pc9.green("Saved approved mission draft."));
|
|
4187
4710
|
return {
|
|
4188
|
-
cancelled:
|
|
4711
|
+
cancelled: false,
|
|
4189
4712
|
draft,
|
|
4190
4713
|
sessionId,
|
|
4191
4714
|
skippedPlanning: false
|
|
4192
4715
|
};
|
|
4716
|
+
} catch (error) {
|
|
4717
|
+
if (interruption.interrupted && isInterruptedError2(error)) {
|
|
4718
|
+
return {
|
|
4719
|
+
cancelled: false,
|
|
4720
|
+
draft: null,
|
|
4721
|
+
exitCode: 130,
|
|
4722
|
+
interrupted: true,
|
|
4723
|
+
sessionId,
|
|
4724
|
+
skippedPlanning: false
|
|
4725
|
+
};
|
|
4726
|
+
}
|
|
4727
|
+
throw error;
|
|
4728
|
+
} finally {
|
|
4729
|
+
if (typeof unregisterSignalHandler === "function") {
|
|
4730
|
+
unregisterSignalHandler();
|
|
4731
|
+
}
|
|
4193
4732
|
}
|
|
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
4733
|
}
|
|
4203
4734
|
|
|
4204
4735
|
// src/upload.ts
|
|
@@ -4207,7 +4738,7 @@ import { createReadStream as createReadStream2 } from "fs";
|
|
|
4207
4738
|
import path6 from "path";
|
|
4208
4739
|
import { Transform as Transform2 } from "stream";
|
|
4209
4740
|
import ora3 from "ora";
|
|
4210
|
-
import
|
|
4741
|
+
import pc10 from "picocolors";
|
|
4211
4742
|
|
|
4212
4743
|
// src/snapshot.ts
|
|
4213
4744
|
import { execFile } from "child_process";
|
|
@@ -4645,10 +5176,16 @@ async function writeDeterministicArchive(analysis, outputDir) {
|
|
|
4645
5176
|
const tempArchivePath = `${finalArchivePath}.${process.pid}.${Date.now()}.tmp`;
|
|
4646
5177
|
const archiveHash = createHash2("sha256");
|
|
4647
5178
|
let archiveBytes = 0;
|
|
4648
|
-
const
|
|
5179
|
+
const archiveHashMeter = new Transform({
|
|
4649
5180
|
transform(chunk, _encoding, callback) {
|
|
4650
5181
|
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
4651
5182
|
archiveHash.update(buffer);
|
|
5183
|
+
callback(null, buffer);
|
|
5184
|
+
}
|
|
5185
|
+
});
|
|
5186
|
+
const outputMeter = new Transform({
|
|
5187
|
+
transform(chunk, _encoding, callback) {
|
|
5188
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
4652
5189
|
archiveBytes += buffer.length;
|
|
4653
5190
|
callback(null, buffer);
|
|
4654
5191
|
}
|
|
@@ -4680,8 +5217,9 @@ async function writeDeterministicArchive(analysis, outputDir) {
|
|
|
4680
5217
|
try {
|
|
4681
5218
|
await pipeline(
|
|
4682
5219
|
tarStream,
|
|
5220
|
+
archiveHashMeter,
|
|
4683
5221
|
createZstdCompress(),
|
|
4684
|
-
|
|
5222
|
+
outputMeter,
|
|
4685
5223
|
createWriteStream(tempArchivePath, { mode: 384 })
|
|
4686
5224
|
);
|
|
4687
5225
|
await rename3(tempArchivePath, finalArchivePath);
|
|
@@ -4759,8 +5297,41 @@ async function defaultPromptConfirm3(options) {
|
|
|
4759
5297
|
return confirm({
|
|
4760
5298
|
default: options.default,
|
|
4761
5299
|
message: options.message
|
|
5300
|
+
}, {
|
|
5301
|
+
signal: options.signal
|
|
4762
5302
|
});
|
|
4763
5303
|
}
|
|
5304
|
+
function isAbortError4(error) {
|
|
5305
|
+
return error instanceof DOMException && error.name === "AbortError";
|
|
5306
|
+
}
|
|
5307
|
+
function isAbortPromptError3(error) {
|
|
5308
|
+
return error instanceof Error && error.name === "AbortPromptError";
|
|
5309
|
+
}
|
|
5310
|
+
function isInterruptedError3(error) {
|
|
5311
|
+
return error instanceof CliError && error.exitCode === 130 || isAbortError4(error) || isAbortPromptError3(error);
|
|
5312
|
+
}
|
|
5313
|
+
function throwIfInterrupted4(interruption) {
|
|
5314
|
+
if (interruption.interrupted) {
|
|
5315
|
+
throw new CliError("Upload interrupted.", 130);
|
|
5316
|
+
}
|
|
5317
|
+
}
|
|
5318
|
+
function interruptUpload(interruption) {
|
|
5319
|
+
if (interruption.interrupted) {
|
|
5320
|
+
return;
|
|
5321
|
+
}
|
|
5322
|
+
interruption.interrupted = true;
|
|
5323
|
+
interruption.activeSpinner?.stop();
|
|
5324
|
+
interruption.activeSpinner = void 0;
|
|
5325
|
+
if (!interruption.abortController.signal.aborted) {
|
|
5326
|
+
interruption.abortController.abort();
|
|
5327
|
+
}
|
|
5328
|
+
writeLine(
|
|
5329
|
+
interruption.stdout,
|
|
5330
|
+
pc10.yellow(
|
|
5331
|
+
"Interrupted. Local upload state is saved and any remote launch is preserved. Re-run this command to resume the upload."
|
|
5332
|
+
)
|
|
5333
|
+
);
|
|
5334
|
+
}
|
|
4764
5335
|
function formatBytes2(bytes) {
|
|
4765
5336
|
if (!Number.isFinite(bytes) || bytes < 1024) {
|
|
4766
5337
|
return `${bytes} B`;
|
|
@@ -4804,15 +5375,15 @@ function isTerminalUploadStatus2(status) {
|
|
|
4804
5375
|
return status === "uploaded" || status === "blob_exists";
|
|
4805
5376
|
}
|
|
4806
5377
|
function renderSafetyGate(stdout, repos, existingBlobs) {
|
|
4807
|
-
writeLine(stdout,
|
|
5378
|
+
writeLine(stdout, pc10.bold("Upload safety check"));
|
|
4808
5379
|
writeLine(stdout, "Repos queued for upload:");
|
|
4809
5380
|
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) ?
|
|
5381
|
+
const cleanliness = repo.manifest.dirty ? pc10.yellow("dirty") : pc10.green("clean");
|
|
5382
|
+
const secretSummary = repo.secretFindings.length > 0 ? pc10.red(`${repo.secretFindings.length} finding(s)`) : pc10.green("no findings");
|
|
5383
|
+
const dedupSummary = existingBlobs.has(repo.manifest.archiveSha256) ? pc10.cyan("blob already exists remotely") : "new upload";
|
|
4813
5384
|
writeLine(
|
|
4814
5385
|
stdout,
|
|
4815
|
-
`- ${
|
|
5386
|
+
`- ${pc10.cyan(repo.manifest.repoId)} \u2014 ${repo.manifest.localPath}`
|
|
4816
5387
|
);
|
|
4817
5388
|
writeLine(
|
|
4818
5389
|
stdout,
|
|
@@ -4842,7 +5413,7 @@ function assertArtifactsMatchPersistedState(repos, persistedManifests) {
|
|
|
4842
5413
|
);
|
|
4843
5414
|
}
|
|
4844
5415
|
}
|
|
4845
|
-
async function createRemoteLaunch(options, repos, taskDescription) {
|
|
5416
|
+
async function createRemoteLaunch(options, repos, taskDescription, signal) {
|
|
4846
5417
|
const response = await options.apiClient.request({
|
|
4847
5418
|
body: {
|
|
4848
5419
|
...taskDescription ? {
|
|
@@ -4858,7 +5429,8 @@ async function createRemoteLaunch(options, repos, taskDescription) {
|
|
|
4858
5429
|
}))
|
|
4859
5430
|
},
|
|
4860
5431
|
method: "POST",
|
|
4861
|
-
path: "/launches"
|
|
5432
|
+
path: "/launches",
|
|
5433
|
+
signal
|
|
4862
5434
|
});
|
|
4863
5435
|
const remoteLaunchId = normalizeLaunchId2(response.launchId);
|
|
4864
5436
|
if (remoteLaunchId.length === 0) {
|
|
@@ -4869,12 +5441,13 @@ async function createRemoteLaunch(options, repos, taskDescription) {
|
|
|
4869
5441
|
remoteLaunchId
|
|
4870
5442
|
};
|
|
4871
5443
|
}
|
|
4872
|
-
async function uploadArchive(options, repo, uploadUrl) {
|
|
5444
|
+
async function uploadArchive(options, repo, uploadUrl, signal, interruption) {
|
|
4873
5445
|
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
4874
5446
|
const createSpinner = options.createSpinner ?? defaultCreateSpinner3;
|
|
4875
5447
|
const spinner = createSpinner(
|
|
4876
5448
|
`Uploading ${repo.manifest.repoId} 0 B / ${formatBytes2(repo.manifest.archiveBytes)} (0 B/s)`
|
|
4877
5449
|
);
|
|
5450
|
+
interruption.activeSpinner = spinner;
|
|
4878
5451
|
spinner.start(spinner.text);
|
|
4879
5452
|
const startedAt = Date.now();
|
|
4880
5453
|
let bytesTransferred = 0;
|
|
@@ -4886,27 +5459,49 @@ async function uploadArchive(options, repo, uploadUrl) {
|
|
|
4886
5459
|
callback(null, chunk);
|
|
4887
5460
|
}
|
|
4888
5461
|
});
|
|
5462
|
+
const archiveStream = createReadStream2(repo.archivePath);
|
|
5463
|
+
const abortStream = () => {
|
|
5464
|
+
archiveStream.destroy();
|
|
5465
|
+
meter.destroy();
|
|
5466
|
+
};
|
|
5467
|
+
if (signal.aborted) {
|
|
5468
|
+
abortStream();
|
|
5469
|
+
} else {
|
|
5470
|
+
signal.addEventListener("abort", abortStream, { once: true });
|
|
5471
|
+
}
|
|
4889
5472
|
let response;
|
|
4890
5473
|
try {
|
|
4891
|
-
const body =
|
|
5474
|
+
const body = archiveStream.pipe(meter);
|
|
4892
5475
|
const requestInit = {
|
|
4893
5476
|
body,
|
|
4894
5477
|
duplex: "half",
|
|
4895
5478
|
headers: {
|
|
4896
5479
|
"content-length": String(repo.manifest.archiveBytes)
|
|
4897
5480
|
},
|
|
4898
|
-
method: "PUT"
|
|
5481
|
+
method: "PUT",
|
|
5482
|
+
signal
|
|
4899
5483
|
};
|
|
4900
5484
|
response = await fetchImpl(uploadUrl, requestInit);
|
|
4901
5485
|
} catch (error) {
|
|
5486
|
+
if (isInterruptedError3(error)) {
|
|
5487
|
+
throw error;
|
|
5488
|
+
}
|
|
4902
5489
|
spinner.fail(`Upload failed for ${repo.manifest.repoId}`);
|
|
5490
|
+
if (interruption.activeSpinner === spinner) {
|
|
5491
|
+
interruption.activeSpinner = void 0;
|
|
5492
|
+
}
|
|
4903
5493
|
throw new CliError(
|
|
4904
5494
|
`Upload failed for ${repo.manifest.repoId}: ${error instanceof Error ? error.message : String(error)}`
|
|
4905
5495
|
);
|
|
5496
|
+
} finally {
|
|
5497
|
+
signal.removeEventListener("abort", abortStream);
|
|
4906
5498
|
}
|
|
4907
5499
|
if (!response.ok) {
|
|
4908
5500
|
const detail = await response.text().catch(() => "");
|
|
4909
5501
|
spinner.fail(`Upload failed for ${repo.manifest.repoId}`);
|
|
5502
|
+
if (interruption.activeSpinner === spinner) {
|
|
5503
|
+
interruption.activeSpinner = void 0;
|
|
5504
|
+
}
|
|
4910
5505
|
throw new CliError(
|
|
4911
5506
|
`Upload failed for ${repo.manifest.repoId}: HTTP ${response.status}${detail ? ` ${detail}` : ""}`
|
|
4912
5507
|
);
|
|
@@ -4915,8 +5510,38 @@ async function uploadArchive(options, repo, uploadUrl) {
|
|
|
4915
5510
|
spinner.succeed(
|
|
4916
5511
|
`Uploaded ${repo.manifest.repoId} ${formatBytes2(bytesTransferred)} in ${formatDuration(elapsedMs)} (${formatRate(bytesTransferred, elapsedMs)})`
|
|
4917
5512
|
);
|
|
5513
|
+
if (interruption.activeSpinner === spinner) {
|
|
5514
|
+
interruption.activeSpinner = void 0;
|
|
5515
|
+
}
|
|
5516
|
+
}
|
|
5517
|
+
async function persistCreatedLaunchState(options, remoteLaunchId, repos) {
|
|
5518
|
+
for (const repo of repos) {
|
|
5519
|
+
await recordRepoManifest(
|
|
5520
|
+
options.launchId,
|
|
5521
|
+
repo.manifest.repoId,
|
|
5522
|
+
repo.manifest,
|
|
5523
|
+
{
|
|
5524
|
+
cwd: options.cwd,
|
|
5525
|
+
homeDir: options.homeDir
|
|
5526
|
+
}
|
|
5527
|
+
);
|
|
5528
|
+
}
|
|
5529
|
+
for (const repo of repos) {
|
|
5530
|
+
await updateRepoUploadState(
|
|
5531
|
+
options.launchId,
|
|
5532
|
+
{
|
|
5533
|
+
remoteLaunchId,
|
|
5534
|
+
repoId: repo.manifest.repoId,
|
|
5535
|
+
status: "pending"
|
|
5536
|
+
},
|
|
5537
|
+
{
|
|
5538
|
+
cwd: options.cwd,
|
|
5539
|
+
homeDir: options.homeDir
|
|
5540
|
+
}
|
|
5541
|
+
);
|
|
5542
|
+
}
|
|
4918
5543
|
}
|
|
4919
|
-
async function processRepoUpload(options, repo, remoteLaunchId) {
|
|
5544
|
+
async function processRepoUpload(options, repo, remoteLaunchId, signal, interruption) {
|
|
4920
5545
|
const snapshot = await loadLaunchSnapshot(options.launchId, {
|
|
4921
5546
|
cwd: options.cwd,
|
|
4922
5547
|
homeDir: options.homeDir
|
|
@@ -4939,7 +5564,8 @@ async function processRepoUpload(options, repo, remoteLaunchId) {
|
|
|
4939
5564
|
const response = await options.apiClient.request({
|
|
4940
5565
|
body: repo.manifest,
|
|
4941
5566
|
method: "POST",
|
|
4942
|
-
path: `/launches/${remoteLaunchId}/repos/${repo.manifest.repoId}
|
|
5567
|
+
path: `/launches/${remoteLaunchId}/repos/${repo.manifest.repoId}`,
|
|
5568
|
+
signal
|
|
4943
5569
|
});
|
|
4944
5570
|
if (response.status === "blob_exists") {
|
|
4945
5571
|
await updateRepoUploadState(
|
|
@@ -4956,7 +5582,7 @@ async function processRepoUpload(options, repo, remoteLaunchId) {
|
|
|
4956
5582
|
);
|
|
4957
5583
|
writeLine(
|
|
4958
5584
|
options.stdout,
|
|
4959
|
-
|
|
5585
|
+
pc10.cyan(`Skipping upload for ${repo.manifest.repoId}; blob already exists remotely.`)
|
|
4960
5586
|
);
|
|
4961
5587
|
return;
|
|
4962
5588
|
}
|
|
@@ -4978,7 +5604,7 @@ async function processRepoUpload(options, repo, remoteLaunchId) {
|
|
|
4978
5604
|
homeDir: options.homeDir
|
|
4979
5605
|
}
|
|
4980
5606
|
);
|
|
4981
|
-
await uploadArchive(options, repo, uploadUrl);
|
|
5607
|
+
await uploadArchive(options, repo, uploadUrl, signal, interruption);
|
|
4982
5608
|
await updateRepoUploadState(
|
|
4983
5609
|
options.launchId,
|
|
4984
5610
|
{
|
|
@@ -4994,152 +5620,193 @@ async function processRepoUpload(options, repo, remoteLaunchId) {
|
|
|
4994
5620
|
}
|
|
4995
5621
|
async function runUploadPipeline(options) {
|
|
4996
5622
|
const promptConfirm = options.promptConfirm ?? defaultPromptConfirm3;
|
|
5623
|
+
const interruption = {
|
|
5624
|
+
abortController: new AbortController(),
|
|
5625
|
+
interrupted: false,
|
|
5626
|
+
stdout: options.stdout
|
|
5627
|
+
};
|
|
5628
|
+
let remoteLaunchId = null;
|
|
5629
|
+
const unregisterSignalHandler = options.registerSignalHandler?.("SIGINT", () => {
|
|
5630
|
+
interruptUpload(interruption);
|
|
5631
|
+
});
|
|
4997
5632
|
const launchSnapshot = await loadLaunchSnapshot(options.launchId, {
|
|
4998
5633
|
cwd: options.cwd,
|
|
4999
5634
|
homeDir: options.homeDir
|
|
5000
5635
|
});
|
|
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
5636
|
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) {
|
|
5637
|
+
if (!launchSnapshot.missionDraft) {
|
|
5043
5638
|
throw new CliError(
|
|
5044
|
-
"
|
|
5639
|
+
"Cannot start uploads before a mission draft has been approved."
|
|
5045
5640
|
);
|
|
5046
5641
|
}
|
|
5047
|
-
|
|
5048
|
-
|
|
5049
|
-
|
|
5050
|
-
|
|
5051
|
-
|
|
5052
|
-
|
|
5053
|
-
|
|
5642
|
+
let repos;
|
|
5643
|
+
try {
|
|
5644
|
+
repos = await snapshotRepositories({
|
|
5645
|
+
allowSecrets: options.allowSecrets === true,
|
|
5646
|
+
cwd: options.cwd,
|
|
5647
|
+
outputDir: path6.join(launchSnapshot.paths.launchDir, "archives"),
|
|
5648
|
+
repoPaths: options.repoPaths
|
|
5649
|
+
});
|
|
5650
|
+
} catch (error) {
|
|
5651
|
+
if (!(error instanceof SecretDetectionError) || options.allowSecrets === true) {
|
|
5652
|
+
throw error;
|
|
5653
|
+
}
|
|
5654
|
+
repos = await snapshotRepositories({
|
|
5655
|
+
allowSecrets: true,
|
|
5656
|
+
cwd: options.cwd,
|
|
5657
|
+
outputDir: path6.join(launchSnapshot.paths.launchDir, "archives"),
|
|
5658
|
+
repoPaths: options.repoPaths
|
|
5659
|
+
});
|
|
5660
|
+
repos = repos.map((repo) => ({
|
|
5661
|
+
...repo,
|
|
5662
|
+
warnings: repo.warnings.filter(
|
|
5663
|
+
(warning) => !warning.includes("--allow-secrets is enabled")
|
|
5664
|
+
)
|
|
5665
|
+
}));
|
|
5666
|
+
}
|
|
5667
|
+
if (Object.keys(launchSnapshot.repoManifests).length > 0) {
|
|
5668
|
+
assertArtifactsMatchPersistedState(
|
|
5669
|
+
repos,
|
|
5670
|
+
launchSnapshot.repoManifests
|
|
5054
5671
|
);
|
|
5055
|
-
return {
|
|
5056
|
-
finalized: false,
|
|
5057
|
-
remoteLaunchId: null
|
|
5058
|
-
};
|
|
5059
5672
|
}
|
|
5060
|
-
|
|
5061
|
-
|
|
5062
|
-
|
|
5063
|
-
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
|
|
5071
|
-
|
|
5072
|
-
|
|
5673
|
+
throwIfInterrupted4(interruption);
|
|
5674
|
+
let existingBlobs = /* @__PURE__ */ new Set();
|
|
5675
|
+
remoteLaunchId = launchSnapshot.uploadState?.remoteLaunchId ?? null;
|
|
5676
|
+
if (!remoteLaunchId) {
|
|
5677
|
+
renderSafetyGate(options.stdout, repos, existingBlobs);
|
|
5678
|
+
const hasSecretFindings = repos.some((repo) => repo.secretFindings.length > 0);
|
|
5679
|
+
if (hasSecretFindings && options.allowSecrets !== true) {
|
|
5680
|
+
throw new CliError(
|
|
5681
|
+
"Secret scan findings blocked the upload. Re-run with `--allow-secrets` to proceed."
|
|
5682
|
+
);
|
|
5683
|
+
}
|
|
5684
|
+
throwIfInterrupted4(interruption);
|
|
5685
|
+
if (options.autoApprove !== true && !await promptConfirm({
|
|
5686
|
+
default: false,
|
|
5687
|
+
message: "Proceed with launch upload?",
|
|
5688
|
+
signal: interruption.abortController.signal
|
|
5689
|
+
})) {
|
|
5690
|
+
writeLine(
|
|
5691
|
+
options.stdout,
|
|
5692
|
+
pc10.yellow("Upload cancelled before creating a remote launch.")
|
|
5693
|
+
);
|
|
5694
|
+
return {
|
|
5695
|
+
finalized: false,
|
|
5696
|
+
remoteLaunchId: null
|
|
5697
|
+
};
|
|
5698
|
+
}
|
|
5699
|
+
throwIfInterrupted4(interruption);
|
|
5700
|
+
const createdLaunch = await createRemoteLaunch(
|
|
5701
|
+
options,
|
|
5702
|
+
repos,
|
|
5703
|
+
launchSnapshot.request?.task ?? null,
|
|
5704
|
+
interruption.abortController.signal
|
|
5073
5705
|
);
|
|
5706
|
+
existingBlobs = createdLaunch.existingBlobs;
|
|
5707
|
+
remoteLaunchId = createdLaunch.remoteLaunchId;
|
|
5708
|
+
await persistCreatedLaunchState(options, remoteLaunchId, repos);
|
|
5709
|
+
throwIfInterrupted4(interruption);
|
|
5710
|
+
if (existingBlobs.size > 0) {
|
|
5711
|
+
writeLine(
|
|
5712
|
+
options.stdout,
|
|
5713
|
+
pc10.cyan(
|
|
5714
|
+
`Remote dedup will reuse ${existingBlobs.size} existing blob${existingBlobs.size === 1 ? "" : "s"}.`
|
|
5715
|
+
)
|
|
5716
|
+
);
|
|
5717
|
+
}
|
|
5074
5718
|
}
|
|
5719
|
+
const currentSnapshot = await loadLaunchSnapshot(options.launchId, {
|
|
5720
|
+
cwd: options.cwd,
|
|
5721
|
+
homeDir: options.homeDir
|
|
5722
|
+
});
|
|
5723
|
+
const uploadStates = new Map(
|
|
5724
|
+
Object.entries(currentSnapshot.uploadState?.repos ?? {}).map(
|
|
5725
|
+
([repoId, state]) => [repoId, state?.status]
|
|
5726
|
+
)
|
|
5727
|
+
);
|
|
5728
|
+
const failures = [];
|
|
5075
5729
|
for (const repo of repos) {
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
|
|
5079
|
-
|
|
5080
|
-
|
|
5730
|
+
throwIfInterrupted4(interruption);
|
|
5731
|
+
if (isTerminalUploadStatus2(uploadStates.get(repo.manifest.repoId))) {
|
|
5732
|
+
writeLine(
|
|
5733
|
+
options.stdout,
|
|
5734
|
+
pc10.cyan(`Skipping ${repo.manifest.repoId}; already completed in upload-state.json.`)
|
|
5735
|
+
);
|
|
5736
|
+
continue;
|
|
5737
|
+
}
|
|
5738
|
+
try {
|
|
5739
|
+
await processRepoUpload(
|
|
5740
|
+
options,
|
|
5741
|
+
repo,
|
|
5742
|
+
remoteLaunchId,
|
|
5743
|
+
interruption.abortController.signal,
|
|
5744
|
+
interruption
|
|
5745
|
+
);
|
|
5746
|
+
const refreshedSnapshot = await loadLaunchSnapshot(options.launchId, {
|
|
5081
5747
|
cwd: options.cwd,
|
|
5082
5748
|
homeDir: options.homeDir
|
|
5749
|
+
});
|
|
5750
|
+
uploadStates.set(
|
|
5751
|
+
repo.manifest.repoId,
|
|
5752
|
+
refreshedSnapshot.uploadState?.repos[repo.manifest.repoId]?.status
|
|
5753
|
+
);
|
|
5754
|
+
} catch (error) {
|
|
5755
|
+
if (interruption.interrupted || isInterruptedError3(error)) {
|
|
5756
|
+
throw error;
|
|
5083
5757
|
}
|
|
5084
|
-
|
|
5758
|
+
failures.push(repo.manifest.repoId);
|
|
5759
|
+
writeLine(
|
|
5760
|
+
options.stdout,
|
|
5761
|
+
pc10.red(
|
|
5762
|
+
error instanceof Error ? error.message : `Upload failed for ${repo.manifest.repoId}.`
|
|
5763
|
+
)
|
|
5764
|
+
);
|
|
5765
|
+
}
|
|
5085
5766
|
}
|
|
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.`)
|
|
5767
|
+
if (failures.length > 0) {
|
|
5768
|
+
throw new CliError(
|
|
5769
|
+
`Upload failed for ${failures.join(", ")}. Re-run the command to resume the remaining repos.`
|
|
5098
5770
|
);
|
|
5099
|
-
continue;
|
|
5100
5771
|
}
|
|
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
|
-
)
|
|
5772
|
+
throwIfInterrupted4(interruption);
|
|
5773
|
+
const finalizeResponse = await options.apiClient.request({
|
|
5774
|
+
method: "POST",
|
|
5775
|
+
path: `/launches/${remoteLaunchId}/finalize`,
|
|
5776
|
+
signal: interruption.abortController.signal
|
|
5777
|
+
});
|
|
5778
|
+
if (finalizeResponse.status !== "complete") {
|
|
5779
|
+
throw new CliError(
|
|
5780
|
+
`Remote launch ${remoteLaunchId} did not finalize successfully.`
|
|
5118
5781
|
);
|
|
5119
5782
|
}
|
|
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.`
|
|
5783
|
+
writeLine(
|
|
5784
|
+
options.stdout,
|
|
5785
|
+
pc10.green(`Upload complete. Remote launch ${remoteLaunchId} is finalized.`)
|
|
5133
5786
|
);
|
|
5787
|
+
return {
|
|
5788
|
+
finalized: true,
|
|
5789
|
+
remoteLaunchId
|
|
5790
|
+
};
|
|
5791
|
+
} catch (error) {
|
|
5792
|
+
if (interruption.interrupted && isInterruptedError3(error)) {
|
|
5793
|
+
return {
|
|
5794
|
+
exitCode: 130,
|
|
5795
|
+
finalized: false,
|
|
5796
|
+
interrupted: true,
|
|
5797
|
+
remoteLaunchId
|
|
5798
|
+
};
|
|
5799
|
+
}
|
|
5800
|
+
throw error;
|
|
5801
|
+
} finally {
|
|
5802
|
+
if (typeof unregisterSignalHandler === "function") {
|
|
5803
|
+
unregisterSignalHandler();
|
|
5804
|
+
}
|
|
5805
|
+
if (interruption.activeSpinner) {
|
|
5806
|
+
interruption.activeSpinner.stop();
|
|
5807
|
+
interruption.activeSpinner = void 0;
|
|
5808
|
+
}
|
|
5134
5809
|
}
|
|
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
5810
|
}
|
|
5144
5811
|
|
|
5145
5812
|
// src/commands/mission-run.ts
|
|
@@ -5212,6 +5879,9 @@ function registerMissionRunCommand(mission, context, dependencies = {}) {
|
|
|
5212
5879
|
sleep: dependencies.sleep,
|
|
5213
5880
|
verbose: globalOptions.verbose === true
|
|
5214
5881
|
});
|
|
5882
|
+
const registerSignalHandler = createSignalHandlerCoordinator(
|
|
5883
|
+
dependencies.registerSignalHandler
|
|
5884
|
+
);
|
|
5215
5885
|
const result = await runPlanningFlow({
|
|
5216
5886
|
autoApprove: options.yes === true,
|
|
5217
5887
|
client,
|
|
@@ -5248,11 +5918,15 @@ function registerMissionRunCommand(mission, context, dependencies = {}) {
|
|
|
5248
5918
|
promptConfirm: dependencies.promptConfirm,
|
|
5249
5919
|
promptInput: dependencies.promptInput,
|
|
5250
5920
|
promptSelect: dependencies.promptSelect,
|
|
5921
|
+
registerSignalHandler,
|
|
5251
5922
|
repoPaths,
|
|
5252
5923
|
sleep: dependencies.sleep,
|
|
5253
5924
|
stdout: context.stdout,
|
|
5254
5925
|
taskDescription: task
|
|
5255
5926
|
});
|
|
5927
|
+
if (result.exitCode && result.exitCode !== 0) {
|
|
5928
|
+
throw new CommanderError3(result.exitCode, "mission-planning", "");
|
|
5929
|
+
}
|
|
5256
5930
|
if (!result.cancelled && !result.skippedPlanning) {
|
|
5257
5931
|
writeLine(
|
|
5258
5932
|
context.stdout,
|
|
@@ -5271,9 +5945,13 @@ function registerMissionRunCommand(mission, context, dependencies = {}) {
|
|
|
5271
5945
|
homeDir,
|
|
5272
5946
|
launchId: launchSnapshot.launchId,
|
|
5273
5947
|
promptConfirm: dependencies.promptConfirm,
|
|
5948
|
+
registerSignalHandler,
|
|
5274
5949
|
repoPaths,
|
|
5275
5950
|
stdout: context.stdout
|
|
5276
5951
|
});
|
|
5952
|
+
if (uploadResult.exitCode && uploadResult.exitCode !== 0) {
|
|
5953
|
+
throw new CommanderError3(uploadResult.exitCode, "mission-upload", "");
|
|
5954
|
+
}
|
|
5277
5955
|
if (!uploadResult.finalized) {
|
|
5278
5956
|
return;
|
|
5279
5957
|
}
|
|
@@ -5297,22 +5975,23 @@ function registerMissionRunCommand(mission, context, dependencies = {}) {
|
|
|
5297
5975
|
missionId: mission2.missionId,
|
|
5298
5976
|
promptInput: dependencies.promptInput,
|
|
5299
5977
|
promptSelect: dependencies.promptSelect,
|
|
5300
|
-
registerSignalHandler
|
|
5978
|
+
registerSignalHandler,
|
|
5301
5979
|
sleep: dependencies.sleep,
|
|
5302
|
-
stdout: context.stdout
|
|
5980
|
+
stdout: context.stdout,
|
|
5981
|
+
verbose: globalOptions.verbose === true
|
|
5303
5982
|
});
|
|
5304
5983
|
if (monitorResult.exitCode !== 0) {
|
|
5305
|
-
throw new
|
|
5984
|
+
throw new CommanderError3(monitorResult.exitCode, "mission-monitor", "");
|
|
5306
5985
|
}
|
|
5307
5986
|
}
|
|
5308
5987
|
);
|
|
5309
5988
|
}
|
|
5310
5989
|
|
|
5311
5990
|
// src/commands/mission-status.ts
|
|
5312
|
-
import
|
|
5991
|
+
import pc11 from "picocolors";
|
|
5313
5992
|
function writeSection4(stdout, title) {
|
|
5314
5993
|
writeLine(stdout);
|
|
5315
|
-
writeLine(stdout,
|
|
5994
|
+
writeLine(stdout, pc11.bold(title));
|
|
5316
5995
|
}
|
|
5317
5996
|
function normalizeMilestoneOrder(milestones, features, assertions) {
|
|
5318
5997
|
const known = new Map(milestones.map((milestone) => [milestone.name, milestone]));
|
|
@@ -5412,7 +6091,7 @@ function renderMilestones2(stdout, milestones, features) {
|
|
|
5412
6091
|
for (const milestone of milestones) {
|
|
5413
6092
|
writeLine(
|
|
5414
6093
|
stdout,
|
|
5415
|
-
`- ${
|
|
6094
|
+
`- ${pc11.cyan(milestone.name)} (${milestone.state}) \xB7 ${milestone.completedFeatureCount}/${milestone.featureCount} features \xB7 ${milestone.passedAssertionCount}/${milestone.assertionCount} assertions`
|
|
5416
6095
|
);
|
|
5417
6096
|
const milestoneFeatures = features.filter(
|
|
5418
6097
|
(feature) => feature.milestone === milestone.name
|
|
@@ -5510,7 +6189,7 @@ function writeError(stream, message) {
|
|
|
5510
6189
|
writeLine(stream, `Error: ${message}`);
|
|
5511
6190
|
}
|
|
5512
6191
|
function handleRunError(error, stderr) {
|
|
5513
|
-
if (error instanceof
|
|
6192
|
+
if (error instanceof CommanderError4) {
|
|
5514
6193
|
return error.exitCode;
|
|
5515
6194
|
}
|
|
5516
6195
|
if (error instanceof CliError) {
|