helloloop 0.8.6 → 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.
Files changed (52) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/README.md +230 -498
  4. package/hosts/claude/marketplace/plugins/helloloop/.claude-plugin/plugin.json +1 -1
  5. package/hosts/gemini/extension/gemini-extension.json +1 -1
  6. package/native/windows-hidden-shell-proxy/HelloLoopHiddenShellProxy.csproj +11 -0
  7. package/native/windows-hidden-shell-proxy/Program.cs +498 -0
  8. package/package.json +4 -2
  9. package/src/activity_projection.mjs +294 -0
  10. package/src/analyze_confirmation.mjs +3 -1
  11. package/src/analyzer.mjs +2 -1
  12. package/src/auto_execution_options.mjs +13 -0
  13. package/src/background_launch.mjs +73 -0
  14. package/src/cli.mjs +51 -1
  15. package/src/cli_analyze_command.mjs +12 -14
  16. package/src/cli_args.mjs +106 -32
  17. package/src/cli_command_handlers.mjs +73 -25
  18. package/src/cli_support.mjs +2 -0
  19. package/src/common.mjs +11 -0
  20. package/src/dashboard_command.mjs +371 -0
  21. package/src/dashboard_tui.mjs +289 -0
  22. package/src/dashboard_web.mjs +351 -0
  23. package/src/dashboard_web_client.mjs +167 -0
  24. package/src/dashboard_web_page.mjs +49 -0
  25. package/src/engine_event_parser_codex.mjs +167 -0
  26. package/src/engine_process_support.mjs +7 -2
  27. package/src/engine_selection.mjs +24 -0
  28. package/src/engine_selection_probe.mjs +10 -6
  29. package/src/engine_selection_settings.mjs +53 -44
  30. package/src/execution_interactivity.mjs +12 -0
  31. package/src/host_continuation.mjs +305 -0
  32. package/src/install_codex.mjs +20 -30
  33. package/src/install_shared.mjs +9 -0
  34. package/src/node_process_launch.mjs +28 -0
  35. package/src/process.mjs +2 -0
  36. package/src/runner_execute_task.mjs +15 -1
  37. package/src/runner_execution_support.mjs +69 -3
  38. package/src/runner_once.mjs +5 -0
  39. package/src/runner_status.mjs +72 -4
  40. package/src/runtime_engine_support.mjs +52 -5
  41. package/src/runtime_engine_task.mjs +7 -0
  42. package/src/runtime_settings.mjs +105 -0
  43. package/src/runtime_settings_loader.mjs +19 -0
  44. package/src/shell_invocation.mjs +227 -9
  45. package/src/supervisor_cli_support.mjs +49 -0
  46. package/src/supervisor_guardian.mjs +307 -0
  47. package/src/supervisor_runtime.mjs +142 -83
  48. package/src/supervisor_state.mjs +64 -0
  49. package/src/supervisor_watch.mjs +364 -0
  50. package/src/terminal_session_limits.mjs +1 -21
  51. package/src/windows_hidden_shell_proxy.mjs +405 -0
  52. package/src/workspace_registry.mjs +155 -0
@@ -1,5 +1,10 @@
1
1
  import path from "node:path";
2
2
 
3
+ import {
4
+ readJsonIfExists,
5
+ selectLatestActivityFile,
6
+ selectLatestRuntimeFile,
7
+ } from "./activity_projection.mjs";
3
8
  import { fileExists, readJson, sanitizeId, tailText, timestampForFile } from "./common.mjs";
4
9
  import { renderTaskSummary, selectNextTask, summarizeBacklog } from "./backlog.mjs";
5
10
  import { loadBacklog } from "./config.mjs";
@@ -83,13 +88,42 @@ export function renderStatusMarkdown(context, { summary, currentTask, lastResult
83
88
  ].join("\n");
84
89
  }
85
90
 
86
- export function renderStatusText(context, options = {}) {
91
+ export function collectRepoStatusSnapshot(context, options = {}) {
87
92
  const backlog = loadBacklog(context);
88
93
  const summary = summarizeBacklog(backlog);
89
94
  const nextTask = selectNextTask(backlog, options);
90
- const supervisor = fileExists(context.supervisorStateFile)
91
- ? readJson(context.supervisorStateFile)
92
- : null;
95
+ const supervisor = fileExists(context.supervisorStateFile) ? readJson(context.supervisorStateFile) : null;
96
+ const latestStatus = fileExists(context.statusFile) ? readJson(context.statusFile) : null;
97
+ const runtimeFile = latestStatus?.runDir ? selectLatestRuntimeFile(latestStatus.runDir) : "";
98
+ const runtime = readJsonIfExists(runtimeFile);
99
+ const activityFile = runtime?.activityFile && fileExists(runtime.activityFile)
100
+ ? runtime.activityFile
101
+ : (latestStatus?.runDir ? selectLatestActivityFile(latestStatus.runDir, runtime?.attemptPrefix || "") : "");
102
+ const activity = readJsonIfExists(activityFile);
103
+
104
+ return {
105
+ summary,
106
+ nextTask,
107
+ supervisor,
108
+ latestStatus,
109
+ runtimeFile,
110
+ runtime,
111
+ activityFile,
112
+ activity,
113
+ };
114
+ }
115
+
116
+ export function renderStatusText(context, options = {}) {
117
+ const snapshot = collectRepoStatusSnapshot(context, options);
118
+ const {
119
+ summary,
120
+ nextTask,
121
+ supervisor,
122
+ latestStatus,
123
+ runtime,
124
+ activity,
125
+ } = snapshot;
126
+ const hostResume = options.hostResume || null;
93
127
 
94
128
  return [
95
129
  "HelloLoop 状态",
@@ -106,10 +140,44 @@ export function renderStatusText(context, options = {}) {
106
140
  `后台会话:${supervisor.status}`,
107
141
  `后台会话 ID:${supervisor.sessionId || "unknown"}`,
108
142
  `后台租约:${renderHostLeaseLabel(supervisor.lease)}`,
143
+ ...(Number.isFinite(Number(supervisor.guardianRestartCount)) && Number(supervisor.guardianRestartCount) > 0
144
+ ? [`守护重拉起次数:${supervisor.guardianRestartCount}`]
145
+ : []),
146
+ ]
147
+ : []),
148
+ ...(latestStatus?.taskTitle
149
+ ? [
150
+ `当前运行任务:${latestStatus.taskTitle}`,
151
+ `当前运行目录:${latestStatus.runDir || "unknown"}`,
152
+ `当前运行阶段:${latestStatus.stage || "unknown"}`,
109
153
  ]
110
154
  : []),
155
+ ...(runtime?.status
156
+ ? [
157
+ `当前引擎状态:${runtime.status}`,
158
+ ...(Number.isFinite(Number(runtime.recoveryCount))
159
+ ? [`自动恢复次数:${runtime.recoveryCount}`]
160
+ : []),
161
+ ]
162
+ : []),
163
+ ...(activity?.current?.label
164
+ ? [`当前动作:${activity.current.label}`]
165
+ : []),
166
+ ...(activity?.todo?.total
167
+ ? [`当前待办:${activity.todo.completed}/${activity.todo.total}`]
168
+ : []),
169
+ ...(Array.isArray(activity?.activeCommands) && activity.activeCommands[0]?.label
170
+ ? [`活动命令:${activity.activeCommands[0].label}`]
171
+ : []),
172
+ ...(hostResume?.issue?.label
173
+ ? [`宿主续跑:${hostResume.issue.label}`]
174
+ : (hostResume?.supervisorActive ? ["宿主续跑:后台仍在运行,可直接接续观察"] : [])),
111
175
  "",
112
176
  nextTask ? "下一任务:" : "下一任务:无",
113
177
  nextTask ? renderTaskSummary(nextTask) : "",
178
+ "",
179
+ "聚合看板:helloloop dashboard",
180
+ "续跑提示:helloloop resume-host",
181
+ "实时观察:helloloop watch",
114
182
  ].filter(Boolean).join("\n");
115
183
  }
@@ -1,7 +1,8 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
 
4
- import { nowIso, writeText } from "./common.mjs";
4
+ import { createActivityProjector } from "./activity_projection.mjs";
5
+ import { appendText, nowIso, writeText } from "./common.mjs";
5
6
  import { getEngineDisplayName } from "./engine_metadata.mjs";
6
7
  import {
7
8
  buildClaudeArgs,
@@ -53,14 +54,16 @@ export function buildHostLeaseStoppedResult(reason) {
53
54
  };
54
55
  }
55
56
 
56
- export function createRuntimeStatusWriter(runtimeStatusFile, baseState) {
57
+ export function createRuntimeStatusWriter(runtimeStatusFile, baseState, onUpdate) {
57
58
  return function writeRuntimeStatus(status, extra = {}) {
58
- writeJson(runtimeStatusFile, {
59
+ const payload = {
59
60
  ...baseState,
60
61
  ...extra,
61
62
  status,
62
63
  updatedAt: nowIso(),
63
- });
64
+ };
65
+ writeJson(runtimeStatusFile, payload);
66
+ onUpdate?.(payload);
64
67
  };
65
68
  }
66
69
 
@@ -165,6 +168,16 @@ export async function runEngineAttempt({
165
168
  }) {
166
169
  const attemptPromptFile = path.join(runDir, `${attemptPrefix}-prompt.md`);
167
170
  const attemptLastMessageFile = path.join(runDir, `${attemptPrefix}-last-message.txt`);
171
+ const attemptStdoutFile = path.join(runDir, `${attemptPrefix}-stdout.log`);
172
+ const attemptStderrFile = path.join(runDir, `${attemptPrefix}-stderr.log`);
173
+ const activityProjector = createActivityProjector({
174
+ engine,
175
+ phase: executionMode,
176
+ repoRoot: context.repoRoot,
177
+ runDir,
178
+ outputPrefix: attemptPrefix,
179
+ attemptPrefix,
180
+ });
168
181
 
169
182
  if (invocation.error) {
170
183
  const result = {
@@ -181,6 +194,17 @@ export async function runEngineAttempt({
181
194
  };
182
195
  writeText(attemptPromptFile, prompt);
183
196
  writeEngineRunArtifacts(runDir, attemptPrefix, result, "");
197
+ activityProjector.onRuntimeStatus({
198
+ status: "failed",
199
+ attemptPrefix,
200
+ activityFile: activityProjector.activityFile,
201
+ activityEventsFile: activityProjector.activityEventsFile,
202
+ });
203
+ activityProjector.finalize({
204
+ status: "failed",
205
+ result,
206
+ finalMessage: "",
207
+ });
184
208
  return {
185
209
  result,
186
210
  finalMessage: "",
@@ -208,11 +232,16 @@ export async function runEngineAttempt({
208
232
  recoveryCount,
209
233
  recoveryHistory,
210
234
  });
235
+ writeText(attemptStdoutFile, "");
236
+ writeText(attemptStderrFile, "");
211
237
 
212
238
  const result = await runChild(invocation.command, finalArgs, {
213
239
  cwd: context.repoRoot,
214
240
  stdin: prompt,
215
- env,
241
+ env: {
242
+ ...(invocation.env || {}),
243
+ ...(env || {}),
244
+ },
216
245
  shell: invocation.shell,
217
246
  heartbeatIntervalMs: recoveryPolicy.heartbeatIntervalSeconds * 1000,
218
247
  stallWarningMs: recoveryPolicy.stallWarningSeconds * 1000,
@@ -224,8 +253,22 @@ export async function runEngineAttempt({
224
253
  recoveryCount,
225
254
  recoveryHistory,
226
255
  heartbeat: payload,
256
+ activityFile: activityProjector.activityFile,
257
+ activityEventsFile: activityProjector.activityEventsFile,
258
+ });
259
+ activityProjector.onRuntimeStatus({
260
+ ...payload,
261
+ attemptPrefix,
262
+ recoveryCount,
227
263
  });
228
264
  },
265
+ onStdout(text) {
266
+ appendText(attemptStdoutFile, text);
267
+ activityProjector.onStdoutChunk(text);
268
+ },
269
+ onStderr(text) {
270
+ appendText(attemptStderrFile, text);
271
+ },
229
272
  shouldKeepRunning() {
230
273
  return isHostLeaseAlive(hostLease);
231
274
  },
@@ -235,6 +278,10 @@ export async function runEngineAttempt({
235
278
 
236
279
  writeText(attemptPromptFile, prompt);
237
280
  writeEngineRunArtifacts(runDir, attemptPrefix, result, finalMessage);
281
+ activityProjector.finalize({
282
+ result,
283
+ finalMessage,
284
+ });
238
285
 
239
286
  return {
240
287
  result,
@@ -3,6 +3,7 @@ import path from "node:path";
3
3
  import { ensureDir, nowIso, tailText, writeJson, writeText } from "./common.mjs";
4
4
  import { getEngineDisplayName, normalizeEngineName } from "./engine_metadata.mjs";
5
5
  import { resolveEngineInvocation } from "./engine_process_support.mjs";
6
+ import { refreshHostContinuationArtifacts } from "./host_continuation.mjs";
6
7
  import { isHostLeaseAlive } from "./host_lease.mjs";
7
8
  import {
8
9
  buildRuntimeRecoveryPrompt,
@@ -52,6 +53,12 @@ export async function runEngineTask({
52
53
  outputPrefix: prefix,
53
54
  hardRetryBudget: recoveryPolicy.hardRetryDelaysSeconds.length,
54
55
  softRetryBudget: recoveryPolicy.softRetryDelaysSeconds.length,
56
+ }, () => {
57
+ try {
58
+ refreshHostContinuationArtifacts(context);
59
+ } catch {
60
+ // ignore continuation snapshot refresh failures during heartbeat writes
61
+ }
55
62
  });
56
63
 
57
64
  const recoveryHistory = [];
@@ -0,0 +1,105 @@
1
+ function normalizeBoolean(value, fallback = false) {
2
+ return typeof value === "boolean" ? value : fallback;
3
+ }
4
+
5
+ function normalizeNonNegativeInteger(value, fallbackValue) {
6
+ if (value === null || value === undefined || value === "") {
7
+ return fallbackValue;
8
+ }
9
+ const parsed = Number(value);
10
+ if (!Number.isFinite(parsed) || parsed < 0) {
11
+ return fallbackValue;
12
+ }
13
+ return Math.floor(parsed);
14
+ }
15
+
16
+ function normalizePositiveInteger(value, fallbackValue, minimum = 1) {
17
+ const parsed = normalizeNonNegativeInteger(value, fallbackValue);
18
+ if (!Number.isFinite(parsed) || parsed < minimum) {
19
+ return fallbackValue;
20
+ }
21
+ return parsed;
22
+ }
23
+
24
+ function normalizeSecondsList(values, fallbackValues) {
25
+ if (!Array.isArray(values) || !values.length) {
26
+ return [...fallbackValues];
27
+ }
28
+ const normalized = values
29
+ .map((item) => normalizePositiveInteger(item, 0))
30
+ .filter((item) => item > 0);
31
+ return normalized.length ? normalized : [...fallbackValues];
32
+ }
33
+
34
+ export function defaultTerminalConcurrencySettings() {
35
+ return {
36
+ enabled: true,
37
+ visibleMax: 8,
38
+ backgroundMax: 8,
39
+ totalMax: 8,
40
+ };
41
+ }
42
+
43
+ export function defaultObserverRetrySettings() {
44
+ return {
45
+ enabled: true,
46
+ missingPollsBeforeRetry: 3,
47
+ retryDelaysSeconds: [2, 5, 10, 15, 30, 60],
48
+ maxRetryCount: 0,
49
+ };
50
+ }
51
+
52
+ export function defaultSupervisorKeepAliveSettings() {
53
+ return {
54
+ enabled: true,
55
+ restartDelaysSeconds: [2, 5, 10, 15, 30, 60],
56
+ maxRestartCount: 0,
57
+ };
58
+ }
59
+
60
+ export function normalizeTerminalConcurrencySettings(settings = {}) {
61
+ const defaults = defaultTerminalConcurrencySettings();
62
+ return {
63
+ enabled: normalizeBoolean(settings?.enabled, defaults.enabled),
64
+ visibleMax: normalizeNonNegativeInteger(settings?.visibleMax, defaults.visibleMax),
65
+ backgroundMax: normalizeNonNegativeInteger(settings?.backgroundMax, defaults.backgroundMax),
66
+ totalMax: normalizeNonNegativeInteger(settings?.totalMax, defaults.totalMax),
67
+ };
68
+ }
69
+
70
+ export function normalizeObserverRetrySettings(settings = {}) {
71
+ const defaults = defaultObserverRetrySettings();
72
+ return {
73
+ enabled: normalizeBoolean(settings?.enabled, defaults.enabled),
74
+ missingPollsBeforeRetry: normalizePositiveInteger(
75
+ settings?.missingPollsBeforeRetry,
76
+ defaults.missingPollsBeforeRetry,
77
+ ),
78
+ retryDelaysSeconds: normalizeSecondsList(settings?.retryDelaysSeconds, defaults.retryDelaysSeconds),
79
+ maxRetryCount: normalizeNonNegativeInteger(settings?.maxRetryCount, defaults.maxRetryCount),
80
+ };
81
+ }
82
+
83
+ export function normalizeSupervisorKeepAliveSettings(settings = {}) {
84
+ const defaults = defaultSupervisorKeepAliveSettings();
85
+ return {
86
+ enabled: normalizeBoolean(settings?.enabled, defaults.enabled),
87
+ restartDelaysSeconds: normalizeSecondsList(settings?.restartDelaysSeconds, defaults.restartDelaysSeconds),
88
+ maxRestartCount: normalizeNonNegativeInteger(settings?.maxRestartCount, defaults.maxRestartCount),
89
+ };
90
+ }
91
+
92
+ export function pickRetryDelaySeconds(delays, attemptNumber) {
93
+ const values = Array.isArray(delays) && delays.length
94
+ ? delays.map((item) => normalizePositiveInteger(item, 0)).filter((item) => item > 0)
95
+ : [];
96
+ if (!values.length) {
97
+ return 0;
98
+ }
99
+ const index = Math.max(0, Math.min(Number(attemptNumber || 1) - 1, values.length - 1));
100
+ return values[index];
101
+ }
102
+
103
+ export function hasRetryBudget(maxRetryCount, nextAttemptNumber) {
104
+ return Number(maxRetryCount || 0) <= 0 || Number(nextAttemptNumber || 0) <= Number(maxRetryCount || 0);
105
+ }
@@ -0,0 +1,19 @@
1
+ import { loadGlobalConfig } from "./global_config.mjs";
2
+ import {
3
+ normalizeObserverRetrySettings,
4
+ normalizeSupervisorKeepAliveSettings,
5
+ normalizeTerminalConcurrencySettings,
6
+ } from "./runtime_settings.mjs";
7
+
8
+ export function loadRuntimeSettings(options = {}) {
9
+ const globalConfig = loadGlobalConfig({
10
+ globalConfigFile: options.globalConfigFile,
11
+ });
12
+
13
+ return {
14
+ terminalConcurrency: normalizeTerminalConcurrencySettings(globalConfig?.runtime?.terminalConcurrency || {}),
15
+ observerRetry: normalizeObserverRetrySettings(globalConfig?.runtime?.observerRetry || {}),
16
+ supervisorKeepAlive: normalizeSupervisorKeepAliveSettings(globalConfig?.runtime?.supervisorKeepAlive || {}),
17
+ _meta: globalConfig?._meta || {},
18
+ };
19
+ }
@@ -1,5 +1,16 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
1
3
  import { spawnSync } from "node:child_process";
2
4
 
5
+ import { resolveWindowsHiddenShellEnvPatch } from "./windows_hidden_shell_proxy.mjs";
6
+
7
+ function buildSyncSpawnOptions(platform = process.platform, extra = {}) {
8
+ return {
9
+ ...extra,
10
+ ...(platform === "win32" ? { windowsHide: true } : {}),
11
+ };
12
+ }
13
+
3
14
  function createUnavailableInvocation(message) {
4
15
  return {
5
16
  command: "",
@@ -19,15 +30,19 @@ function parseWindowsCommandMatches(output) {
19
30
  function hasCommand(command, platform = process.platform) {
20
31
  if (platform === "win32") {
21
32
  const result = spawnSync("where.exe", [command], {
22
- encoding: "utf8",
23
- shell: false,
33
+ ...buildSyncSpawnOptions(platform, {
34
+ encoding: "utf8",
35
+ shell: false,
36
+ }),
24
37
  });
25
38
  return result.status === 0;
26
39
  }
27
40
 
28
41
  const result = spawnSync("sh", ["-lc", `command -v ${command}`], {
29
- encoding: "utf8",
30
- shell: false,
42
+ ...buildSyncSpawnOptions(platform, {
43
+ encoding: "utf8",
44
+ shell: false,
45
+ }),
31
46
  });
32
47
  return result.status === 0;
33
48
  }
@@ -35,8 +50,10 @@ function hasCommand(command, platform = process.platform) {
35
50
  function findWindowsCommandPaths(command, resolver) {
36
51
  const lookup = resolver || ((name) => {
37
52
  const result = spawnSync("where.exe", [name], {
38
- encoding: "utf8",
39
- shell: false,
53
+ ...buildSyncSpawnOptions("win32", {
54
+ encoding: "utf8",
55
+ shell: false,
56
+ }),
40
57
  });
41
58
  return result.status === 0 ? parseWindowsCommandMatches(result.stdout) : [];
42
59
  });
@@ -125,6 +142,14 @@ function isCmdLikeExecutable(executable) {
125
142
  return /\.(cmd|bat)$/i.test(String(executable || ""));
126
143
  }
127
144
 
145
+ function trimOuterQuotes(value) {
146
+ return String(value || "").trim().replace(/^"(.*)"$/u, "$1");
147
+ }
148
+
149
+ function uniqueNonEmpty(values) {
150
+ return [...new Set(values.map((item) => trimOuterQuotes(item)).filter(Boolean))];
151
+ }
152
+
128
153
  function resolveWindowsNamedExecutable(toolName, options = {}) {
129
154
  const explicitExecutable = String(options.explicitExecutable || "").trim();
130
155
  const findCommandPaths = options.findCommandPaths || ((command) => findWindowsCommandPaths(command));
@@ -133,7 +158,7 @@ function resolveWindowsNamedExecutable(toolName, options = {}) {
133
158
  return explicitExecutable;
134
159
  }
135
160
 
136
- const searchOrder = [`${toolName}.ps1`, `${toolName}.exe`, toolName];
161
+ const searchOrder = [`${toolName}.exe`, `${toolName}.ps1`, toolName];
137
162
  for (const query of searchOrder) {
138
163
  const safeMatch = findCommandPaths(query).find((candidate) => !isCmdLikeExecutable(candidate));
139
164
  if (safeMatch) {
@@ -144,6 +169,189 @@ function resolveWindowsNamedExecutable(toolName, options = {}) {
144
169
  return "";
145
170
  }
146
171
 
172
+ function resolveNodeHostForWrapper(wrapperPath) {
173
+ const wrapperDir = path.dirname(wrapperPath);
174
+ const bundledNode = path.join(wrapperDir, "node.exe");
175
+ if (fs.existsSync(bundledNode)) {
176
+ return bundledNode;
177
+ }
178
+ return process.execPath || "node";
179
+ }
180
+
181
+ function getUpdatedWindowsPath(newDirs, basePath = process.env.PATH || "") {
182
+ const existing = String(basePath || "")
183
+ .split(";")
184
+ .map((item) => trimOuterQuotes(item))
185
+ .filter(Boolean);
186
+ return uniqueNonEmpty([...newDirs, ...existing]).join(";");
187
+ }
188
+
189
+ function resolveCodexWindowsTargetTriple() {
190
+ if (process.arch === "arm64") {
191
+ return {
192
+ packageName: "@openai/codex-win32-arm64",
193
+ targetTriple: "aarch64-pc-windows-msvc",
194
+ managedEnvKey: "CODEX_MANAGED_BY_NPM",
195
+ };
196
+ }
197
+
198
+ if (process.arch === "x64") {
199
+ return {
200
+ packageName: "@openai/codex-win32-x64",
201
+ targetTriple: "x86_64-pc-windows-msvc",
202
+ managedEnvKey: "CODEX_MANAGED_BY_NPM",
203
+ };
204
+ }
205
+
206
+ return null;
207
+ }
208
+
209
+ function resolveWindowsNativeCodexInvocation(options = {}) {
210
+ const platform = options.platform || process.platform;
211
+ if (platform !== "win32") {
212
+ return null;
213
+ }
214
+
215
+ const target = resolveCodexWindowsTargetTriple();
216
+ if (!target) {
217
+ return null;
218
+ }
219
+
220
+ const explicitExecutable = trimOuterQuotes(options.explicitExecutable || "");
221
+ if (explicitExecutable && /\.exe$/iu.test(explicitExecutable) && fs.existsSync(explicitExecutable)) {
222
+ return {
223
+ command: explicitExecutable,
224
+ argsPrefix: [],
225
+ shell: false,
226
+ };
227
+ }
228
+
229
+ const findCommandPaths = options.findCommandPaths || ((command) => findWindowsCommandPaths(command));
230
+ const wrapperCandidates = [];
231
+
232
+ if (explicitExecutable) {
233
+ if (fs.existsSync(explicitExecutable)) {
234
+ wrapperCandidates.push(explicitExecutable);
235
+ } else {
236
+ wrapperCandidates.push(...findCommandPaths(explicitExecutable));
237
+ }
238
+ } else {
239
+ wrapperCandidates.push(...findCommandPaths("codex.ps1"));
240
+ wrapperCandidates.push(...findCommandPaths("codex"));
241
+ }
242
+
243
+ for (const wrapperPath of uniqueNonEmpty(wrapperCandidates)) {
244
+ const wrapperDir = path.dirname(wrapperPath);
245
+ const codexPackageRoot = path.join(wrapperDir, "node_modules", "@openai", "codex");
246
+ const vendorRoot = path.join(
247
+ codexPackageRoot,
248
+ "node_modules",
249
+ target.packageName,
250
+ "vendor",
251
+ target.targetTriple,
252
+ );
253
+ const binaryPath = path.join(vendorRoot, "codex", "codex.exe");
254
+ if (!fs.existsSync(binaryPath)) {
255
+ continue;
256
+ }
257
+ const rgPathDir = path.join(vendorRoot, "path");
258
+ const envPatch = {
259
+ [target.managedEnvKey]: "1",
260
+ };
261
+ if (fs.existsSync(rgPathDir)) {
262
+ envPatch.PATH = getUpdatedWindowsPath([rgPathDir], process.env.PATH || "");
263
+ }
264
+ return {
265
+ command: binaryPath,
266
+ argsPrefix: [],
267
+ shell: false,
268
+ env: envPatch,
269
+ };
270
+ }
271
+
272
+ return null;
273
+ }
274
+
275
+ function attachWindowsHiddenShellProxy(invocation, options = {}) {
276
+ if (!invocation || (options.platform || process.platform) !== "win32") {
277
+ return invocation;
278
+ }
279
+
280
+ try {
281
+ const envPatch = resolveWindowsHiddenShellEnvPatch({
282
+ basePath: invocation.env?.PATH || process.env.PATH || "",
283
+ });
284
+ if (!envPatch || !Object.keys(envPatch).length) {
285
+ return invocation;
286
+ }
287
+ return {
288
+ ...invocation,
289
+ env: {
290
+ ...(invocation.env || {}),
291
+ ...envPatch,
292
+ },
293
+ };
294
+ } catch (error) {
295
+ return {
296
+ ...invocation,
297
+ error: [
298
+ String(invocation.error || "").trim(),
299
+ `HelloLoop 无法准备 Windows 隐藏 shell 代理:${String(error?.message || error || "未知错误")}`,
300
+ ].filter(Boolean).join("\n"),
301
+ };
302
+ }
303
+ }
304
+
305
+ function resolveWindowsNodePackageInvocation(toolName, packageScriptSegments, options = {}) {
306
+ const platform = options.platform || process.platform;
307
+ if (platform !== "win32") {
308
+ return null;
309
+ }
310
+
311
+ const explicitExecutable = trimOuterQuotes(options.explicitExecutable || "");
312
+ if (explicitExecutable && /\.m?js$/iu.test(explicitExecutable) && fs.existsSync(explicitExecutable)) {
313
+ return {
314
+ command: process.execPath || "node",
315
+ argsPrefix: [explicitExecutable],
316
+ shell: false,
317
+ };
318
+ }
319
+
320
+ const findCommandPaths = options.findCommandPaths || ((command) => findWindowsCommandPaths(command));
321
+ const wrapperCandidates = [];
322
+
323
+ if (explicitExecutable) {
324
+ if (fs.existsSync(explicitExecutable)) {
325
+ wrapperCandidates.push(explicitExecutable);
326
+ } else {
327
+ wrapperCandidates.push(...findCommandPaths(explicitExecutable));
328
+ }
329
+ }
330
+
331
+ if (!wrapperCandidates.length) {
332
+ wrapperCandidates.push(...findCommandPaths(`${toolName}.ps1`));
333
+ wrapperCandidates.push(...findCommandPaths(`${toolName}.exe`));
334
+ wrapperCandidates.push(...findCommandPaths(toolName));
335
+ }
336
+
337
+ for (const wrapperPath of uniqueNonEmpty(wrapperCandidates)) {
338
+ if (isCmdLikeExecutable(wrapperPath) || !fs.existsSync(wrapperPath)) {
339
+ continue;
340
+ }
341
+ const packageScript = path.join(path.dirname(wrapperPath), "node_modules", ...packageScriptSegments);
342
+ if (!fs.existsSync(packageScript)) {
343
+ continue;
344
+ }
345
+ return {
346
+ command: resolveNodeHostForWrapper(wrapperPath),
347
+ argsPrefix: [packageScript],
348
+ shell: false,
349
+ };
350
+ }
351
+
352
+ return null;
353
+ }
354
+
147
355
  export function resolveVerifyShellInvocation(options = {}) {
148
356
  const platform = options.platform || process.platform;
149
357
 
@@ -217,11 +425,21 @@ export function resolveCliInvocation(options = {}) {
217
425
  }
218
426
 
219
427
  export function resolveCodexInvocation(options = {}) {
220
- return resolveCliInvocation({
428
+ const nativeCodexInvocation = resolveWindowsNativeCodexInvocation(options);
429
+ if (nativeCodexInvocation) {
430
+ return attachWindowsHiddenShellProxy(nativeCodexInvocation, options);
431
+ }
432
+
433
+ const nodePackageInvocation = resolveWindowsNodePackageInvocation("codex", ["@openai", "codex", "bin", "codex.js"], options);
434
+ if (nodePackageInvocation) {
435
+ return attachWindowsHiddenShellProxy(nodePackageInvocation, options);
436
+ }
437
+
438
+ return attachWindowsHiddenShellProxy(resolveCliInvocation({
221
439
  ...options,
222
440
  commandName: "codex",
223
441
  toolDisplayName: "Codex",
224
- });
442
+ }), options);
225
443
  }
226
444
 
227
445
  export function resolveClaudeInvocation(options = {}) {
@@ -0,0 +1,49 @@
1
+ import { launchSupervisedCommand, renderSupervisorLaunchSummary } from "./supervisor_runtime.mjs";
2
+ import { watchSupervisorSessionWithRecovery } from "./supervisor_watch.mjs";
3
+
4
+ export function shouldUseSupervisor(options = {}) {
5
+ return !options.dryRun
6
+ && process.env.HELLOLOOP_SUPERVISOR_ACTIVE !== "1";
7
+ }
8
+
9
+ export function shouldAutoWatchSupervisor(options = {}) {
10
+ if (options.detach) {
11
+ return false;
12
+ }
13
+ if (options.watch === true) {
14
+ return true;
15
+ }
16
+ if (options.watch === false) {
17
+ return false;
18
+ }
19
+ return Boolean(process.stdout.isTTY);
20
+ }
21
+
22
+ export async function launchAndMaybeWatchSupervisedCommand(context, command, options = {}) {
23
+ const session = launchSupervisedCommand(context, command, options);
24
+ console.log(renderSupervisorLaunchSummary(session));
25
+
26
+ if (!shouldAutoWatchSupervisor(options)) {
27
+ console.log("- 已切换为后台执行;可稍后运行 `helloloop watch` 或 `helloloop status` 查看进度。");
28
+ return {
29
+ detached: true,
30
+ exitCode: 0,
31
+ ok: true,
32
+ session,
33
+ };
34
+ }
35
+
36
+ console.log("- 已进入附着观察模式;按 Ctrl+C 仅退出观察,不会停止后台任务。");
37
+ const watchResult = await watchSupervisorSessionWithRecovery(context, {
38
+ sessionId: session.sessionId,
39
+ pollMs: options.watchPollMs,
40
+ globalConfigFile: options.globalConfigFile,
41
+ });
42
+ return {
43
+ detached: false,
44
+ exitCode: watchResult.exitCode,
45
+ ok: watchResult.ok,
46
+ session,
47
+ watchResult,
48
+ };
49
+ }