helloloop 0.9.1 → 0.10.0
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/.claude-plugin/plugin.json +1 -1
- package/README.md +230 -506
- package/hosts/claude/marketplace/plugins/helloloop/.claude-plugin/plugin.json +1 -1
- package/hosts/gemini/extension/gemini-extension.json +1 -1
- package/native/windows-hidden-shell-proxy/HelloLoopHiddenShellProxy.csproj +11 -0
- package/native/windows-hidden-shell-proxy/Program.cs +498 -0
- package/package.json +4 -2
- package/src/activity_projection.mjs +294 -0
- package/src/analyze_confirmation.mjs +3 -1
- package/src/analyzer.mjs +2 -1
- package/src/auto_execution_options.mjs +13 -0
- package/src/background_launch.mjs +73 -0
- package/src/cli.mjs +49 -1
- package/src/cli_analyze_command.mjs +9 -5
- package/src/cli_args.mjs +102 -37
- package/src/cli_command_handlers.mjs +44 -4
- package/src/cli_support.mjs +2 -0
- package/src/dashboard_command.mjs +371 -0
- package/src/dashboard_tui.mjs +289 -0
- package/src/dashboard_web.mjs +351 -0
- package/src/dashboard_web_client.mjs +167 -0
- package/src/dashboard_web_page.mjs +49 -0
- package/src/engine_event_parser_codex.mjs +167 -0
- package/src/engine_process_support.mjs +1 -0
- package/src/engine_selection.mjs +24 -0
- package/src/engine_selection_probe.mjs +10 -6
- package/src/engine_selection_settings.mjs +12 -19
- package/src/execution_interactivity.mjs +12 -0
- package/src/host_continuation.mjs +305 -0
- package/src/install_codex.mjs +20 -8
- package/src/install_shared.mjs +9 -0
- package/src/node_process_launch.mjs +28 -0
- package/src/process.mjs +2 -0
- package/src/runner_execute_task.mjs +4 -0
- package/src/runner_execution_support.mjs +69 -3
- package/src/runner_once.mjs +4 -0
- package/src/runner_status.mjs +63 -7
- package/src/runtime_engine_support.mjs +41 -4
- package/src/runtime_engine_task.mjs +7 -0
- package/src/runtime_settings.mjs +105 -0
- package/src/runtime_settings_loader.mjs +19 -0
- package/src/shell_invocation.mjs +227 -9
- package/src/supervisor_cli_support.mjs +3 -2
- package/src/supervisor_guardian.mjs +307 -0
- package/src/supervisor_runtime.mjs +138 -82
- package/src/supervisor_state.mjs +64 -0
- package/src/supervisor_watch.mjs +92 -48
- package/src/terminal_session_limits.mjs +1 -21
- package/src/windows_hidden_shell_proxy.mjs +405 -0
- package/src/workspace_registry.mjs +155 -0
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { appendText, fileExists, nowIso, readJson, writeJson } from "./common.mjs";
|
|
5
|
+
import { parseCodexJsonlEventLine } from "./engine_event_parser_codex.mjs";
|
|
6
|
+
|
|
7
|
+
const RECENT_EVENTS_LIMIT = 24;
|
|
8
|
+
const RECENT_REASONING_LIMIT = 8;
|
|
9
|
+
const RECENT_COMMANDS_LIMIT = 10;
|
|
10
|
+
const RECENT_FILE_CHANGES_LIMIT = 10;
|
|
11
|
+
|
|
12
|
+
function limitList(list, maxLength) {
|
|
13
|
+
if (list.length > maxLength) {
|
|
14
|
+
list.splice(0, list.length - maxLength);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function readDirEntriesSafe(dirPath) {
|
|
19
|
+
try {
|
|
20
|
+
return fs.readdirSync(dirPath, { withFileTypes: true });
|
|
21
|
+
} catch {
|
|
22
|
+
return [];
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function collectArtifactCandidates(runDir, suffix) {
|
|
27
|
+
if (!runDir || !fileExists(runDir)) {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const candidates = [];
|
|
32
|
+
for (const entry of readDirEntriesSafe(runDir)) {
|
|
33
|
+
const fullPath = path.join(runDir, entry.name);
|
|
34
|
+
if (entry.isFile() && entry.name.endsWith(suffix)) {
|
|
35
|
+
candidates.push(fullPath);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (!entry.isDirectory()) {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
for (const nestedEntry of readDirEntriesSafe(fullPath)) {
|
|
42
|
+
if (nestedEntry.isFile() && nestedEntry.name.endsWith(suffix)) {
|
|
43
|
+
candidates.push(path.join(fullPath, nestedEntry.name));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return candidates.sort((left, right) => fs.statSync(right).mtimeMs - fs.statSync(left).mtimeMs);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function relativeToRepo(repoRoot, targetPath) {
|
|
52
|
+
const normalized = String(targetPath || "").trim();
|
|
53
|
+
if (!normalized) {
|
|
54
|
+
return "";
|
|
55
|
+
}
|
|
56
|
+
return path.isAbsolute(normalized)
|
|
57
|
+
? path.relative(repoRoot, normalized).replaceAll("\\", "/")
|
|
58
|
+
: normalized.replaceAll("\\", "/");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function compactEvent(event, repoRoot) {
|
|
62
|
+
return {
|
|
63
|
+
kind: event.kind,
|
|
64
|
+
itemId: event.itemId || "",
|
|
65
|
+
status: event.status || "",
|
|
66
|
+
label: event.label || "",
|
|
67
|
+
exitCode: event.exitCode ?? null,
|
|
68
|
+
summary: event.summary || event.outputSummary || "",
|
|
69
|
+
changes: Array.isArray(event.changes)
|
|
70
|
+
? event.changes.map((change) => ({
|
|
71
|
+
path: relativeToRepo(repoRoot, change.path),
|
|
72
|
+
kind: change.kind,
|
|
73
|
+
}))
|
|
74
|
+
: [],
|
|
75
|
+
updatedAt: nowIso(),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function createBaseSnapshot(options) {
|
|
80
|
+
return {
|
|
81
|
+
schemaVersion: 1,
|
|
82
|
+
engine: options.engine || "",
|
|
83
|
+
phase: options.phase || "",
|
|
84
|
+
repoRoot: options.repoRoot || "",
|
|
85
|
+
runDir: options.runDir || "",
|
|
86
|
+
outputPrefix: options.outputPrefix || "",
|
|
87
|
+
attemptPrefix: options.attemptPrefix || "",
|
|
88
|
+
activityFile: options.activityFile || "",
|
|
89
|
+
activityEventsFile: options.activityEventsFile || "",
|
|
90
|
+
status: "running",
|
|
91
|
+
threadId: "",
|
|
92
|
+
current: {
|
|
93
|
+
kind: "",
|
|
94
|
+
status: "",
|
|
95
|
+
label: "",
|
|
96
|
+
itemId: "",
|
|
97
|
+
},
|
|
98
|
+
todo: {
|
|
99
|
+
total: 0,
|
|
100
|
+
completed: 0,
|
|
101
|
+
pending: 0,
|
|
102
|
+
items: [],
|
|
103
|
+
},
|
|
104
|
+
activeCommands: [],
|
|
105
|
+
recentCommands: [],
|
|
106
|
+
recentReasoning: [],
|
|
107
|
+
recentFileChanges: [],
|
|
108
|
+
recentEvents: [],
|
|
109
|
+
runtime: {},
|
|
110
|
+
finalMessage: "",
|
|
111
|
+
code: null,
|
|
112
|
+
startedAt: nowIso(),
|
|
113
|
+
updatedAt: nowIso(),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function resolveParser(engine) {
|
|
118
|
+
return engine === "codex" ? parseCodexJsonlEventLine : null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function readJsonIfExists(filePath) {
|
|
122
|
+
if (!filePath || !fileExists(filePath)) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
try {
|
|
126
|
+
return readJson(filePath);
|
|
127
|
+
} catch {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function selectLatestRuntimeFile(runDir) {
|
|
133
|
+
return collectArtifactCandidates(runDir, "-runtime.json")[0] || "";
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function selectLatestActivityFile(runDir, attemptPrefix = "") {
|
|
137
|
+
if (attemptPrefix) {
|
|
138
|
+
const namedCandidates = collectArtifactCandidates(runDir, `${attemptPrefix}-activity.json`);
|
|
139
|
+
if (namedCandidates[0]) {
|
|
140
|
+
return namedCandidates[0];
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return collectArtifactCandidates(runDir, "-activity.json")[0] || "";
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function createActivityProjector(options = {}) {
|
|
147
|
+
const parser = resolveParser(options.engine);
|
|
148
|
+
const activityFile = options.activityFile || path.join(options.runDir, `${options.attemptPrefix}-activity.json`);
|
|
149
|
+
const activityEventsFile = options.activityEventsFile || path.join(options.runDir, `${options.attemptPrefix}-activity.jsonl`);
|
|
150
|
+
const snapshot = createBaseSnapshot({
|
|
151
|
+
...options,
|
|
152
|
+
activityFile,
|
|
153
|
+
activityEventsFile,
|
|
154
|
+
});
|
|
155
|
+
const activeCommands = new Map();
|
|
156
|
+
let stdoutBuffer = "";
|
|
157
|
+
|
|
158
|
+
const persistSnapshot = () => {
|
|
159
|
+
snapshot.updatedAt = nowIso();
|
|
160
|
+
snapshot.activeCommands = [...activeCommands.values()];
|
|
161
|
+
writeJson(activityFile, snapshot);
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const appendActivityEvent = (event) => {
|
|
165
|
+
const compact = compactEvent(event, snapshot.repoRoot);
|
|
166
|
+
snapshot.current = {
|
|
167
|
+
kind: compact.kind,
|
|
168
|
+
status: compact.status,
|
|
169
|
+
label: compact.label,
|
|
170
|
+
itemId: compact.itemId,
|
|
171
|
+
};
|
|
172
|
+
snapshot.recentEvents.push(compact);
|
|
173
|
+
limitList(snapshot.recentEvents, RECENT_EVENTS_LIMIT);
|
|
174
|
+
|
|
175
|
+
if (compact.kind === "thread" && event.threadId) {
|
|
176
|
+
snapshot.threadId = event.threadId;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (compact.kind === "todo" && event.todo) {
|
|
180
|
+
snapshot.todo = {
|
|
181
|
+
...event.todo,
|
|
182
|
+
items: Array.isArray(event.todo.items) ? event.todo.items : [],
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (compact.kind === "reasoning" && compact.label) {
|
|
187
|
+
snapshot.recentReasoning.push({
|
|
188
|
+
label: compact.label,
|
|
189
|
+
status: compact.status,
|
|
190
|
+
updatedAt: compact.updatedAt,
|
|
191
|
+
});
|
|
192
|
+
limitList(snapshot.recentReasoning, RECENT_REASONING_LIMIT);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (compact.kind === "command") {
|
|
196
|
+
const previous = activeCommands.get(compact.itemId) || {};
|
|
197
|
+
const next = {
|
|
198
|
+
id: compact.itemId,
|
|
199
|
+
label: compact.label,
|
|
200
|
+
status: compact.status,
|
|
201
|
+
exitCode: compact.exitCode,
|
|
202
|
+
summary: compact.summary,
|
|
203
|
+
startedAt: previous.startedAt || compact.updatedAt,
|
|
204
|
+
updatedAt: compact.updatedAt,
|
|
205
|
+
};
|
|
206
|
+
if (compact.status === "in_progress") {
|
|
207
|
+
activeCommands.set(compact.itemId, next);
|
|
208
|
+
} else {
|
|
209
|
+
activeCommands.delete(compact.itemId);
|
|
210
|
+
snapshot.recentCommands.push({
|
|
211
|
+
...previous,
|
|
212
|
+
...next,
|
|
213
|
+
completedAt: compact.updatedAt,
|
|
214
|
+
});
|
|
215
|
+
limitList(snapshot.recentCommands, RECENT_COMMANDS_LIMIT);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (compact.kind === "file_change") {
|
|
220
|
+
snapshot.recentFileChanges.push({
|
|
221
|
+
label: compact.label,
|
|
222
|
+
status: compact.status,
|
|
223
|
+
changes: compact.changes,
|
|
224
|
+
updatedAt: compact.updatedAt,
|
|
225
|
+
});
|
|
226
|
+
limitList(snapshot.recentFileChanges, RECENT_FILE_CHANGES_LIMIT);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
appendText(activityEventsFile, `${JSON.stringify(compact)}\n`);
|
|
230
|
+
persistSnapshot();
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
const consumeStdoutLine = (line) => {
|
|
234
|
+
if (!parser || !line) {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
try {
|
|
238
|
+
const event = parser(line);
|
|
239
|
+
if (event) {
|
|
240
|
+
appendActivityEvent(event);
|
|
241
|
+
}
|
|
242
|
+
} catch {
|
|
243
|
+
// Ignore non-JSON diagnostic lines from the engine output.
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
return {
|
|
248
|
+
activityFile,
|
|
249
|
+
activityEventsFile,
|
|
250
|
+
onStdoutChunk(text) {
|
|
251
|
+
if (!parser || !text) {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
stdoutBuffer += text;
|
|
255
|
+
while (stdoutBuffer.includes("\n")) {
|
|
256
|
+
const newlineIndex = stdoutBuffer.indexOf("\n");
|
|
257
|
+
const line = stdoutBuffer.slice(0, newlineIndex).trim();
|
|
258
|
+
stdoutBuffer = stdoutBuffer.slice(newlineIndex + 1);
|
|
259
|
+
consumeStdoutLine(line);
|
|
260
|
+
}
|
|
261
|
+
},
|
|
262
|
+
onRuntimeStatus(payload = {}) {
|
|
263
|
+
snapshot.status = String(payload.status || snapshot.status || "running").trim() || "running";
|
|
264
|
+
snapshot.runtime = {
|
|
265
|
+
...snapshot.runtime,
|
|
266
|
+
...payload,
|
|
267
|
+
};
|
|
268
|
+
persistSnapshot();
|
|
269
|
+
},
|
|
270
|
+
finalize(payload = {}) {
|
|
271
|
+
if (stdoutBuffer.trim()) {
|
|
272
|
+
consumeStdoutLine(stdoutBuffer.trim());
|
|
273
|
+
}
|
|
274
|
+
stdoutBuffer = "";
|
|
275
|
+
const finalStatus = String(
|
|
276
|
+
payload.status
|
|
277
|
+
|| (payload.result ? (payload.result.ok ? "completed" : "failed") : snapshot.status)
|
|
278
|
+
|| "completed",
|
|
279
|
+
).trim();
|
|
280
|
+
snapshot.status = finalStatus || snapshot.status;
|
|
281
|
+
snapshot.finalMessage = String(payload.finalMessage || snapshot.finalMessage || "").trim();
|
|
282
|
+
if (payload.result) {
|
|
283
|
+
const code = Number(payload.result.code);
|
|
284
|
+
snapshot.code = Number.isFinite(code) ? code : snapshot.code;
|
|
285
|
+
snapshot.runtime = {
|
|
286
|
+
...snapshot.runtime,
|
|
287
|
+
watchdogTriggered: payload.result.watchdogTriggered === true,
|
|
288
|
+
leaseExpired: payload.result.leaseExpired === true,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
persistSnapshot();
|
|
292
|
+
},
|
|
293
|
+
};
|
|
294
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
|
|
3
3
|
import { renderInputIssueLines, renderUserIntentLines } from "./analyze_user_input.mjs";
|
|
4
|
+
import { resolveFullAutoMainlineOptions } from "./auto_execution_options.mjs";
|
|
4
5
|
import { analyzeExecution, summarizeBacklog } from "./backlog.mjs";
|
|
5
6
|
import { loadPolicy, loadVerifyCommands } from "./config.mjs";
|
|
6
7
|
import { getEngineDisplayName } from "./engine_metadata.mjs";
|
|
@@ -167,7 +168,7 @@ export function resolveAutoRunMaxTasks(backlog, options = {}) {
|
|
|
167
168
|
|
|
168
169
|
export function renderAnalyzeConfirmation(context, analysis, backlog, options = {}, discovery = {}) {
|
|
169
170
|
const summary = summarizeBacklog(backlog);
|
|
170
|
-
const execution = analyzeExecution(backlog, options);
|
|
171
|
+
const execution = analyzeExecution(backlog, resolveFullAutoMainlineOptions(options));
|
|
171
172
|
const policy = loadPolicy(context);
|
|
172
173
|
const verifyCommands = resolvePreviewVerifyCommands(context, execution);
|
|
173
174
|
const autoRunMaxTasks = resolveAutoRunMaxTasks(backlog, options);
|
|
@@ -213,6 +214,7 @@ export function renderAnalyzeConfirmation(context, analysis, backlog, options =
|
|
|
213
214
|
execution.blockedReason
|
|
214
215
|
? `- 当前阻塞:${execution.blockedReason}`
|
|
215
216
|
: "- 当前阻塞:无",
|
|
217
|
+
"- 风险策略:确认后进入主线自动执行时,medium/high/critical 任务不会单独卡住续跑;手动 run-once/run-loop 仍需显式允许高风险",
|
|
216
218
|
"- 偏差修正:按 backlog 优先级执行;如果分析识别出偏差修正任务,会先收口再继续后续开发",
|
|
217
219
|
autoRunMaxTasks > 0
|
|
218
220
|
? `- 自动推进:最多 ${autoRunMaxTasks} 个任务;若主线终态复核仍发现缺口,则到达上限后暂停`
|
package/src/analyzer.mjs
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
rememberEngineSelection,
|
|
17
17
|
resolveEngineSelection,
|
|
18
18
|
} from "./engine_selection.mjs";
|
|
19
|
+
import { shouldPromptForEngineSelection } from "./execution_interactivity.mjs";
|
|
19
20
|
import {
|
|
20
21
|
buildAnalysisSummaryText,
|
|
21
22
|
buildCurrentWorkspaceDiscovery,
|
|
@@ -36,7 +37,7 @@ async function analyzeResolvedWorkspace(context, discovery, options = {}) {
|
|
|
36
37
|
context,
|
|
37
38
|
policy,
|
|
38
39
|
options,
|
|
39
|
-
interactive:
|
|
40
|
+
interactive: shouldPromptForEngineSelection(options),
|
|
40
41
|
});
|
|
41
42
|
if (!engineResolution.ok) {
|
|
42
43
|
return {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Full-auto mainline begins only after the user has explicitly approved the
|
|
3
|
+
* analyze confirmation. Once that approval exists, HelloLoop should continue
|
|
4
|
+
* the backlog instead of stopping at medium/high/critical risk gates that are
|
|
5
|
+
* meant for manual run-once / run-loop invocations.
|
|
6
|
+
*/
|
|
7
|
+
export function resolveFullAutoMainlineOptions(options = {}) {
|
|
8
|
+
return {
|
|
9
|
+
...options,
|
|
10
|
+
allowHighRisk: true,
|
|
11
|
+
fullAutoMainline: true,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { ensureDir, timestampForFile } from "./common.mjs";
|
|
5
|
+
import { createContext } from "./context.mjs";
|
|
6
|
+
import { spawnNodeProcess } from "./node_process_launch.mjs";
|
|
7
|
+
|
|
8
|
+
export const HELLOLOOP_BACKGROUND_LAUNCH_ENV = "HELLOLOOP_BACKGROUND_LAUNCH_ACTIVE";
|
|
9
|
+
|
|
10
|
+
function isDetachedAnalyzeCommand(command, options = {}) {
|
|
11
|
+
return command === "analyze"
|
|
12
|
+
&& options.detach === true
|
|
13
|
+
&& options.yes === true
|
|
14
|
+
&& options.watch !== true;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function shouldUseDetachedBackgroundLaunch(command, options = {}) {
|
|
18
|
+
return process.platform === "win32"
|
|
19
|
+
&& process.env.HELLOLOOP_SUPERVISOR_ACTIVE !== "1"
|
|
20
|
+
&& process.env[HELLOLOOP_BACKGROUND_LAUNCH_ENV] !== "1"
|
|
21
|
+
&& isDetachedAnalyzeCommand(command, options);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function resolveLaunchFiles(options = {}) {
|
|
25
|
+
const context = createContext({
|
|
26
|
+
repoRoot: options.repoRoot || process.cwd(),
|
|
27
|
+
configDirName: options.configDirName,
|
|
28
|
+
});
|
|
29
|
+
const launcherRoot = path.join(context.configRoot, "launcher");
|
|
30
|
+
ensureDir(launcherRoot);
|
|
31
|
+
|
|
32
|
+
const stamp = timestampForFile();
|
|
33
|
+
return {
|
|
34
|
+
launcherRoot,
|
|
35
|
+
stdoutFile: path.join(launcherRoot, `${stamp}-analyze-stdout.log`),
|
|
36
|
+
stderrFile: path.join(launcherRoot, `${stamp}-analyze-stderr.log`),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function launchDetachedBackgroundCli(argv, options = {}) {
|
|
41
|
+
const context = createContext({
|
|
42
|
+
repoRoot: options.repoRoot || process.cwd(),
|
|
43
|
+
configDirName: options.configDirName,
|
|
44
|
+
});
|
|
45
|
+
const files = resolveLaunchFiles(options);
|
|
46
|
+
const stdoutFd = fs.openSync(files.stdoutFile, "w");
|
|
47
|
+
const stderrFd = fs.openSync(files.stderrFile, "w");
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
const child = spawnNodeProcess({
|
|
51
|
+
args: [
|
|
52
|
+
path.join(context.bundleRoot, "bin", "helloloop.js"),
|
|
53
|
+
...argv,
|
|
54
|
+
],
|
|
55
|
+
cwd: process.cwd(),
|
|
56
|
+
detached: true,
|
|
57
|
+
stdio: ["ignore", stdoutFd, stderrFd],
|
|
58
|
+
env: {
|
|
59
|
+
[HELLOLOOP_BACKGROUND_LAUNCH_ENV]: "1",
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
child.unref();
|
|
63
|
+
return {
|
|
64
|
+
pid: child.pid ?? 0,
|
|
65
|
+
stdoutFile: files.stdoutFile,
|
|
66
|
+
stderrFile: files.stderrFile,
|
|
67
|
+
launcherRoot: files.launcherRoot,
|
|
68
|
+
};
|
|
69
|
+
} finally {
|
|
70
|
+
fs.closeSync(stdoutFd);
|
|
71
|
+
fs.closeSync(stderrFd);
|
|
72
|
+
}
|
|
73
|
+
}
|
package/src/cli.mjs
CHANGED
|
@@ -3,18 +3,29 @@ import { printHelp, parseArgs } from "./cli_args.mjs";
|
|
|
3
3
|
import { handleAnalyzeCommand } from "./cli_analyze_command.mjs";
|
|
4
4
|
import {
|
|
5
5
|
handleDoctorCommand,
|
|
6
|
+
handleDashboardCommand,
|
|
6
7
|
handleInitCommand,
|
|
7
8
|
handleInstallCommand,
|
|
8
9
|
handleNextCommand,
|
|
10
|
+
handleResumeHostCommand,
|
|
9
11
|
handleRunLoopCommand,
|
|
10
12
|
handleRunOnceCommand,
|
|
11
13
|
handleStatusCommand,
|
|
14
|
+
handleTuiCommand,
|
|
12
15
|
handleUninstallCommand,
|
|
16
|
+
handleWebCommand,
|
|
13
17
|
handleWatchCommand,
|
|
14
18
|
} from "./cli_command_handlers.mjs";
|
|
15
19
|
import { resolveContextFromOptions, resolveStandardCommandOptions } from "./cli_context.mjs";
|
|
16
20
|
import { runDoctor } from "./cli_support.mjs";
|
|
21
|
+
import { runSupervisorGuardianFromSessionFile } from "./supervisor_guardian.mjs";
|
|
22
|
+
import { runDashboardWebCommand } from "./dashboard_web.mjs";
|
|
17
23
|
import { runSupervisedCommandFromSessionFile } from "./supervisor_runtime.mjs";
|
|
24
|
+
import {
|
|
25
|
+
launchDetachedBackgroundCli,
|
|
26
|
+
shouldUseDetachedBackgroundLaunch,
|
|
27
|
+
HELLOLOOP_BACKGROUND_LAUNCH_ENV,
|
|
28
|
+
} from "./background_launch.mjs";
|
|
18
29
|
import {
|
|
19
30
|
acquireVisibleTerminalSession,
|
|
20
31
|
releaseCurrentTerminalSession,
|
|
@@ -33,11 +44,34 @@ export async function runCli(argv) {
|
|
|
33
44
|
if (!parsed.options.sessionFile) {
|
|
34
45
|
throw new Error("缺少 --session-file,无法启动 HelloLoop supervisor。");
|
|
35
46
|
}
|
|
47
|
+
await runSupervisorGuardianFromSessionFile(parsed.options.sessionFile);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (command === "__supervise-worker") {
|
|
51
|
+
if (!parsed.options.sessionFile) {
|
|
52
|
+
throw new Error("缺少 --session-file,无法启动 HelloLoop supervisor worker。");
|
|
53
|
+
}
|
|
36
54
|
await runSupervisedCommandFromSessionFile(parsed.options.sessionFile);
|
|
37
55
|
return;
|
|
38
56
|
}
|
|
57
|
+
if (command === "__web-server") {
|
|
58
|
+
process.exitCode = await runDashboardWebCommand({
|
|
59
|
+
...parsed.options,
|
|
60
|
+
foreground: true,
|
|
61
|
+
});
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (shouldUseDetachedBackgroundLaunch(command, parsed.options)) {
|
|
66
|
+
launchDetachedBackgroundCli(argv, parsed.options);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
39
69
|
|
|
40
|
-
if (
|
|
70
|
+
if (
|
|
71
|
+
process.env.HELLOLOOP_SUPERVISOR_ACTIVE !== "1"
|
|
72
|
+
&& process.env[HELLOLOOP_BACKGROUND_LAUNCH_ENV] !== "1"
|
|
73
|
+
&& shouldTrackVisibleTerminalCommand(command)
|
|
74
|
+
) {
|
|
41
75
|
acquireVisibleTerminalSession({
|
|
42
76
|
command,
|
|
43
77
|
repoRoot: process.cwd(),
|
|
@@ -59,11 +93,25 @@ export async function runCli(argv) {
|
|
|
59
93
|
return;
|
|
60
94
|
}
|
|
61
95
|
|
|
96
|
+
if (command === "dashboard") {
|
|
97
|
+
process.exitCode = await handleDashboardCommand(options);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (command === "tui") {
|
|
101
|
+
process.exitCode = await handleTuiCommand(options);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (command === "web") {
|
|
105
|
+
process.exitCode = await handleWebCommand(options);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
62
109
|
const context = resolveContextFromOptions(options);
|
|
63
110
|
const handlers = {
|
|
64
111
|
doctor: () => handleDoctorCommand(context, options, runDoctor),
|
|
65
112
|
init: () => handleInitCommand(context),
|
|
66
113
|
next: () => handleNextCommand(context, options),
|
|
114
|
+
"resume-host": () => handleResumeHostCommand(context, options),
|
|
67
115
|
"run-loop": () => handleRunLoopCommand(context, options),
|
|
68
116
|
"run-once": () => handleRunOnceCommand(context, options),
|
|
69
117
|
status: () => handleStatusCommand(context, options),
|
|
@@ -15,10 +15,12 @@ import { loadPolicy } from "./config.mjs";
|
|
|
15
15
|
import { createContext } from "./context.mjs";
|
|
16
16
|
import { createDiscoveryPromptSession, resolveDiscoveryFailureInteractively } from "./discovery_prompt.mjs";
|
|
17
17
|
import { resolveEngineSelection } from "./engine_selection.mjs";
|
|
18
|
+
import { shouldPromptForEngineSelection } from "./execution_interactivity.mjs";
|
|
18
19
|
import { resetRepoForRebuild } from "./rebuild.mjs";
|
|
19
20
|
import { renderRebuildSummary } from "./cli_render.mjs";
|
|
20
21
|
import { shouldConfirmRepoRebuild } from "./cli_support.mjs";
|
|
21
22
|
import { launchAndMaybeWatchSupervisedCommand } from "./supervisor_cli_support.mjs";
|
|
23
|
+
import { resolveFullAutoMainlineOptions } from "./auto_execution_options.mjs";
|
|
22
24
|
|
|
23
25
|
async function resolveAnalyzeEngineSelection(options) {
|
|
24
26
|
if (options.engineResolution?.ok) {
|
|
@@ -33,7 +35,7 @@ async function resolveAnalyzeEngineSelection(options) {
|
|
|
33
35
|
context: provisionalContext,
|
|
34
36
|
policy: loadPolicy(provisionalContext),
|
|
35
37
|
options,
|
|
36
|
-
interactive:
|
|
38
|
+
interactive: shouldPromptForEngineSelection(options),
|
|
37
39
|
});
|
|
38
40
|
}
|
|
39
41
|
|
|
@@ -202,7 +204,11 @@ async function prepareAnalyzeExecution(initialOptions) {
|
|
|
202
204
|
}
|
|
203
205
|
|
|
204
206
|
async function maybeRunAutoExecution(result, activeOptions) {
|
|
205
|
-
const
|
|
207
|
+
const autoOptions = resolveFullAutoMainlineOptions({
|
|
208
|
+
...activeOptions,
|
|
209
|
+
engineResolution: result.engineResolution?.ok ? result.engineResolution : activeOptions.engineResolution,
|
|
210
|
+
});
|
|
211
|
+
const execution = analyzeExecution(result.backlog, autoOptions);
|
|
206
212
|
|
|
207
213
|
if (activeOptions.dryRun) {
|
|
208
214
|
console.log("已按 --dry-run 跳过自动执行。");
|
|
@@ -222,10 +228,8 @@ async function maybeRunAutoExecution(result, activeOptions) {
|
|
|
222
228
|
console.log("");
|
|
223
229
|
console.log("开始自动接续执行...");
|
|
224
230
|
const payload = await launchAndMaybeWatchSupervisedCommand(result.context, "run-loop", {
|
|
225
|
-
...
|
|
226
|
-
engineResolution: result.engineResolution?.ok ? result.engineResolution : activeOptions.engineResolution,
|
|
231
|
+
...autoOptions,
|
|
227
232
|
maxTasks: resolveAutoRunMaxTasks(result.backlog, activeOptions) || undefined,
|
|
228
|
-
fullAutoMainline: true,
|
|
229
233
|
});
|
|
230
234
|
return payload.exitCode || 0;
|
|
231
235
|
}
|