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
package/src/cli_args.mjs CHANGED
@@ -1,29 +1,60 @@
1
1
  const REPO_ROOT_PLACEHOLDER = "<REPO_ROOT>";
2
2
  const DOCS_PATH_PLACEHOLDER = "<DOCS_PATH>";
3
- const KNOWN_COMMANDS = new Set([
4
- "__supervise",
5
- "analyze",
6
- "install",
7
- "uninstall",
8
- "init",
9
- "status",
10
- "next",
11
- "run-once",
12
- "run-loop",
13
- "doctor",
14
- "help",
15
- "--help",
16
- "-h",
3
+ const KNOWN_ENGINES = new Set([
4
+ "codex",
5
+ "claude",
6
+ "gemini",
17
7
  ]);
8
+ const COMMAND_ALIASES = Object.freeze({
9
+ "__supervise": "__supervise",
10
+ "__supervise-worker": "__supervise-worker",
11
+ "__web-server": "__web-server",
12
+ analyze: "analyze",
13
+ a: "analyze",
14
+ dashboard: "dashboard",
15
+ dash: "dashboard",
16
+ db: "dashboard",
17
+ tui: "tui",
18
+ web: "web",
19
+ install: "install",
20
+ i: "install",
21
+ uninstall: "uninstall",
22
+ un: "uninstall",
23
+ init: "init",
24
+ resume: "resume-host",
25
+ rh: "resume-host",
26
+ "resume-host": "resume-host",
27
+ status: "status",
28
+ st: "status",
29
+ watch: "watch",
30
+ w: "watch",
31
+ next: "next",
32
+ n: "next",
33
+ "run-once": "run-once",
34
+ once: "run-once",
35
+ "run-loop": "run-loop",
36
+ loop: "run-loop",
37
+ doctor: "doctor",
38
+ dr: "doctor",
39
+ help: "help",
40
+ "--help": "help",
41
+ "-h": "help",
42
+ });
43
+ const KNOWN_COMMANDS = new Set(Object.keys(COMMAND_ALIASES));
44
+
45
+ function normalizeCommand(value = "") {
46
+ return COMMAND_ALIASES[String(value || "").trim()] || "";
47
+ }
18
48
 
19
49
  export function parseArgs(argv) {
20
50
  const [first = "", ...restArgs] = argv;
51
+ const normalizedFirstCommand = normalizeCommand(first);
21
52
  const command = !first
22
53
  ? "analyze"
23
- : (KNOWN_COMMANDS.has(first) ? first : "analyze");
54
+ : (normalizedFirstCommand || "analyze");
24
55
  const rest = !first
25
56
  ? []
26
- : (KNOWN_COMMANDS.has(first) ? restArgs : argv);
57
+ : (normalizedFirstCommand ? restArgs : argv);
27
58
  const options = {
28
59
  requiredDocs: [],
29
60
  constraints: [],
@@ -34,11 +65,11 @@ export function parseArgs(argv) {
34
65
  const arg = rest[index];
35
66
  if (arg === "--dry-run") options.dryRun = true;
36
67
  else if (arg === "--yes" || arg === "-y") options.yes = true;
37
- else if (arg === "--allow-high-risk") options.allowHighRisk = true;
68
+ else if (arg === "--allow-high-risk" || arg === "-r") options.allowHighRisk = true;
38
69
  else if (arg === "--rebuild-existing") options.rebuildExisting = true;
39
70
  else if (arg === "--force") options.force = true;
40
- else if (arg === "--task-id") { options.taskId = rest[index + 1]; index += 1; }
41
- else if (arg === "--max-tasks") { options.maxTasks = Number(rest[index + 1]); index += 1; }
71
+ else if (arg === "--task-id" || arg === "-t") { options.taskId = rest[index + 1]; index += 1; }
72
+ else if (arg === "--max-tasks" || arg === "-m") { options.maxTasks = Number(rest[index + 1]); index += 1; }
42
73
  else if (arg === "--max-attempts") { options.maxAttempts = Number(rest[index + 1]); index += 1; }
43
74
  else if (arg === "--max-strategies") { options.maxStrategies = Number(rest[index + 1]); index += 1; }
44
75
  else if (arg === "--repo") { options.repoRoot = rest[index + 1]; index += 1; }
@@ -51,12 +82,32 @@ export function parseArgs(argv) {
51
82
  else if (arg === "--gemini-home") { options.geminiHome = rest[index + 1]; index += 1; }
52
83
  else if (arg === "--config-dir") { options.configDirName = rest[index + 1]; index += 1; }
53
84
  else if (arg === "--session-file") { options.sessionFile = rest[index + 1]; index += 1; }
54
- else if (arg === "--supervised") options.supervised = true;
85
+ else if (arg === "--launch-id") { options.launchId = rest[index + 1]; index += 1; }
86
+ else if (arg === "--watch" || arg === "-w") options.watch = true;
87
+ else if (arg === "--detach" || arg === "-d") options.detach = true;
88
+ else if (arg === "--foreground") options.foreground = true;
89
+ else if (arg === "--stop") options.stop = true;
90
+ else if (arg === "--json" || arg === "-j") options.json = true;
91
+ else if (arg === "--compact" || arg === "-c") options.compact = true;
92
+ else if (arg === "--events" || arg === "-e") options.events = true;
93
+ else if (arg === "--session-id") { options.sessionId = rest[index + 1]; index += 1; }
94
+ else if (arg === "--watch-poll-ms") { options.watchPollMs = Number(rest[index + 1]); index += 1; }
95
+ else if (arg === "--poll-ms" || arg === "-p") { options.pollMs = Number(rest[index + 1]); index += 1; }
96
+ else if (arg === "--port") { options.port = Number(rest[index + 1]); index += 1; }
97
+ else if (arg === "--bind") { options.bind = rest[index + 1]; index += 1; }
55
98
  else if (arg === "--required-doc") { options.requiredDocs.push(rest[index + 1]); index += 1; }
56
99
  else if (arg === "--constraint") { options.constraints.push(rest[index + 1]); index += 1; }
57
100
  else { options.positionalArgs.push(arg); }
58
101
  }
59
102
 
103
+ const [leadingPositional = "", ...remainingPositionals] = options.positionalArgs;
104
+ const normalizedLeadingEngine = String(leadingPositional || "").trim().toLowerCase();
105
+ if (!options.engine && KNOWN_ENGINES.has(normalizedLeadingEngine)) {
106
+ options.engine = normalizedLeadingEngine;
107
+ options.engineSource = "leading_positional";
108
+ options.positionalArgs = remainingPositionals;
109
+ }
110
+
60
111
  return { command, options };
61
112
  }
62
113
 
@@ -65,15 +116,21 @@ function helpText() {
65
116
  "用法:helloloop [command] [engine] [path|需求说明...] [options]",
66
117
  "",
67
118
  "命令:",
68
- " analyze 自动分析并生成执行确认单;确认后继续自动接续开发(默认)",
69
- " install 安装插件到 Codex Home(适合 npx / npm bin 分发)",
70
- " uninstall 从所选宿主卸载插件并清理注册信息",
119
+ " analyze | a 自动分析并生成执行确认单;确认后继续自动接续开发(默认)",
120
+ " dashboard | dash | db 在当前终端启动动态 TUI 看板(默认总控台)",
121
+ " tui dashboard 的显式别名,适合从主终端直接打开实时任务看板",
122
+ " web 启动本地 Web 看板;默认后台启动并输出访问地址",
123
+ " install | i 安装插件到 Codex Home(适合 npx / npm bin 分发)",
124
+ " uninstall | un 从所选宿主卸载插件并清理注册信息",
71
125
  " init 初始化 .helloloop 配置",
72
- " status 查看 backlog 与下一任务",
73
- " next 生成下一任务干跑预览",
74
- " run-once 执行一个任务",
75
- " run-loop 连续执行多个任务",
76
- " doctor 检查 Codex、当前插件 bundle 与目标仓库 .helloloop 配置是否可用",
126
+ " resume-host | resume | rh",
127
+ " 输出当前仓库的宿主续跑自然语言提示,供主 CLI / 主代理中断后继续输入",
128
+ " status | st 查看 backlog 与下一任务",
129
+ " watch | w 附着到后台 supervisor,持续查看实时进度",
130
+ " next | n 生成下一任务干跑预览",
131
+ " run-once | once 执行一个任务",
132
+ " run-loop | loop 连续执行多个任务",
133
+ " doctor | dr 检查 Codex、当前插件 bundle 与目标仓库 .helloloop 配置是否可用",
77
134
  "",
78
135
  "选项:",
79
136
  " --host <name> 安装宿主:codex | claude | gemini | all(默认 codex)",
@@ -86,12 +143,20 @@ function helpText() {
86
143
  " --config-dir <dir> 配置目录,默认 .helloloop",
87
144
  " -y, --yes 跳过交互确认,分析后直接开始自动执行",
88
145
  " --dry-run 只分析并输出确认单,不真正开始自动执行",
89
- " --task-id <id> 指定任务 id",
90
- " --max-tasks <n> run-loop 最多执行 n 个任务",
146
+ " -t, --task-id <id> 指定任务 id",
147
+ " -m, --max-tasks <n> run-loop 最多执行 n 个任务",
91
148
  " --max-attempts <n> 每种策略内最多重试 n 次",
92
149
  " --max-strategies <n> 单任务最多切换 n 种策略继续重试",
93
- " --supervised 兼容保留;当前版本默认就会通过独立 supervisor 后台执行",
94
- " --allow-high-risk 允许执行 medium/high/critical 风险任务",
150
+ " -w, --watch 启动后台 supervisor 后,当前终端继续附着观察实时输出",
151
+ " -d, --detach 仅启动后台 supervisor 后立即返回,不进入观察模式",
152
+ " --foreground web 命令以前台模式运行本地服务",
153
+ " --stop web 命令停止当前本地看板服务",
154
+ " -j, --json 以 JSON / NDJSON 形式输出结构化状态",
155
+ " --session-id <id> status/watch 指定附着的后台会话 ID",
156
+ " -r, --allow-high-risk 手动 run-once/run-loop 时允许执行 medium/high/critical 风险任务",
157
+ " -p, --poll-ms <n> dashboard 轮询间隔(毫秒)",
158
+ " --port <n> web 命令指定监听端口,默认优先尝试 3210",
159
+ " --bind <addr> web 命令指定监听地址,默认 127.0.0.1",
95
160
  " --rebuild-existing 分析判断当前项目与文档冲突时,自动清理当前项目后按文档重建",
96
161
  " --required-doc <p> 增加一个全局必读文档(AGENTS.md 会被自动忽略)",
97
162
  " --constraint <text> 增加一个全局实现约束",
@@ -100,6 +165,15 @@ function helpText() {
100
165
  " analyze 默认支持在命令后混合传入引擎、路径和自然语言要求。",
101
166
  " 如果同时检测到多个可用引擎且没有明确指定,会先询问你选择。",
102
167
  " 当前版本默认会把自动执行 / run-once / run-loop 切到后台 supervisor。",
168
+ " `helloloop dashboard` / `helloloop tui` 会打开终端实时总控台,不再把文本 dashboard 作为默认交互形态。",
169
+ " `helloloop dashboard --json --watch` 仍可为宿主主代理提供结构化状态流。",
170
+ " `helloloop web` 会启动本地 Web 看板,适合长时间像 Jira 一样查看多仓任务板。",
171
+ " `helloloop resume-host --json` 会输出结构化续跑提示;不带 `--json` 时直接输出自然语言续跑内容。",
172
+ " Windows 下 `analyze -y --detach` 会直接切到隐藏后台引导,不再保留前台空白控制台等待分析完成。",
173
+ " Windows 下运行 Codex 时会优先绕过 codex.ps1,直接以隐藏原生 codex.exe 启动,并把内部 pwsh/powershell 调度重定向到隐藏代理,尽量避免额外 PowerShell 窗口闪烁。",
174
+ " `watch` / `status --watch` / `dashboard --json --watch` 会按全局设置自动重试附着或重连;后台守护进程也会按设置保活重拉起。",
175
+ " 交互终端默认会自动附着观察;如只想立即返回,请显式加 --detach。",
176
+ " 任何时候都可运行 `helloloop watch` / `helloloop w` 或 `helloloop status --watch` / `helloloop st -w` 重新附着观察。",
103
177
  " 示例:npx helloloop claude <DOCS_PATH> <PROJECT_ROOT> 先分析偏差,不要执行",
104
178
  ].join("\n");
105
179
  }
@@ -1,32 +1,21 @@
1
1
  import path from "node:path";
2
2
 
3
3
  import { createContext } from "./context.mjs";
4
+ import { runDashboardCommand } from "./dashboard_command.mjs";
5
+ import { runDashboardTuiCommand } from "./dashboard_tui.mjs";
6
+ import { runDashboardWebCommand } from "./dashboard_web.mjs";
4
7
  import { loadBacklog, scaffoldIfMissing } from "./config.mjs";
5
8
  import { syncUserSettingsFile } from "./engine_selection_settings.mjs";
9
+ import { readHostContinuationSnapshot, runHostContinuationCommand } from "./host_continuation.mjs";
6
10
  import { installPluginBundle, uninstallPluginBundle } from "./install.mjs";
7
11
  import { runLoop, runOnce, renderStatusText } from "./runner.mjs";
12
+ import { collectRepoStatusSnapshot } from "./runner_status.mjs";
8
13
  import { renderInstallSummary, renderUninstallSummary } from "./cli_render.mjs";
9
14
  import {
10
- launchSupervisedCommand,
11
- renderSupervisorLaunchSummary,
12
- } from "./supervisor_runtime.mjs";
13
-
14
- function shouldUseSupervisor(options = {}) {
15
- return !options.dryRun
16
- && process.env.HELLOLOOP_SUPERVISOR_ACTIVE !== "1";
17
- }
18
-
19
- async function runSupervised(context, command, options = {}) {
20
- const session = launchSupervisedCommand(context, command, options);
21
- console.log(renderSupervisorLaunchSummary(session));
22
- console.log("- 已切换为后台执行;可稍后运行 `helloloop status` 查看进度。");
23
- return {
24
- detached: true,
25
- exitCode: 0,
26
- ok: true,
27
- session,
28
- };
29
- }
15
+ launchAndMaybeWatchSupervisedCommand,
16
+ shouldUseSupervisor,
17
+ } from "./supervisor_cli_support.mjs";
18
+ import { watchSupervisorSessionWithRecovery } from "./supervisor_watch.mjs";
30
19
 
31
20
  export function handleInstallCommand(options) {
32
21
  const userSettings = syncUserSettingsFile({
@@ -79,9 +68,68 @@ export async function handleDoctorCommand(context, options, runDoctor) {
79
68
  return 0;
80
69
  }
81
70
 
82
- export function handleStatusCommand(context, options) {
83
- console.log(renderStatusText(context, options));
84
- return 0;
71
+ export async function handleStatusCommand(context, options) {
72
+ const hostResume = readHostContinuationSnapshot(context, {
73
+ refresh: true,
74
+ sessionId: options.sessionId || "",
75
+ });
76
+ if (options.json) {
77
+ console.log(JSON.stringify({
78
+ ...collectRepoStatusSnapshot(context, options),
79
+ hostResume,
80
+ }, null, 2));
81
+ } else {
82
+ console.log(renderStatusText(context, {
83
+ ...options,
84
+ hostResume,
85
+ }));
86
+ }
87
+ if (!options.watch) {
88
+ return 0;
89
+ }
90
+
91
+ const result = await watchSupervisorSessionWithRecovery(context, {
92
+ sessionId: options.sessionId,
93
+ pollMs: options.watchPollMs,
94
+ globalConfigFile: options.globalConfigFile,
95
+ });
96
+ if (result.empty) {
97
+ console.log("当前没有正在运行的后台 supervisor。");
98
+ return 1;
99
+ }
100
+ return result.exitCode || 0;
101
+ }
102
+
103
+ export async function handleDashboardCommand(options) {
104
+ if (options.json === true) {
105
+ return runDashboardCommand(options);
106
+ }
107
+ return runDashboardTuiCommand(options);
108
+ }
109
+
110
+ export async function handleTuiCommand(options) {
111
+ return runDashboardTuiCommand(options);
112
+ }
113
+
114
+ export async function handleWebCommand(options) {
115
+ return runDashboardWebCommand(options);
116
+ }
117
+
118
+ export async function handleResumeHostCommand(context, options) {
119
+ return runHostContinuationCommand(context, options);
120
+ }
121
+
122
+ export async function handleWatchCommand(context, options) {
123
+ const result = await watchSupervisorSessionWithRecovery(context, {
124
+ sessionId: options.sessionId,
125
+ pollMs: options.watchPollMs,
126
+ globalConfigFile: options.globalConfigFile,
127
+ });
128
+ if (result.empty) {
129
+ console.log("当前没有正在运行的后台 supervisor。");
130
+ return 1;
131
+ }
132
+ return result.exitCode || 0;
85
133
  }
86
134
 
87
135
  export async function handleNextCommand(context, options) {
@@ -109,7 +157,7 @@ export async function handleNextCommand(context, options) {
109
157
 
110
158
  export async function handleRunOnceCommand(context, options) {
111
159
  if (shouldUseSupervisor(options)) {
112
- const payload = await runSupervised(context, "run-once", options);
160
+ const payload = await launchAndMaybeWatchSupervisedCommand(context, "run-once", options);
113
161
  return payload.exitCode || 0;
114
162
  }
115
163
 
@@ -133,7 +181,7 @@ export async function handleRunOnceCommand(context, options) {
133
181
 
134
182
  export async function handleRunLoopCommand(context, options) {
135
183
  if (shouldUseSupervisor(options)) {
136
- const payload = await runSupervised(context, "run-loop", options);
184
+ const payload = await launchAndMaybeWatchSupervisedCommand(context, "run-loop", options);
137
185
  return payload.exitCode || 0;
138
186
  }
139
187
 
@@ -19,6 +19,7 @@ function probeCodexVersion() {
19
19
  const codexVersion = spawnSync(invocation.command, [...invocation.argsPrefix, "--version"], {
20
20
  encoding: "utf8",
21
21
  shell: invocation.shell,
22
+ windowsHide: process.platform === "win32",
22
23
  });
23
24
  const ok = codexVersion.status === 0;
24
25
  return {
@@ -113,6 +114,7 @@ function probeNamedCliVersion(commandName, toolDisplayName) {
113
114
  const result = spawnSync(invocation.command, [...invocation.argsPrefix, "--version"], {
114
115
  encoding: "utf8",
115
116
  shell: invocation.shell,
117
+ windowsHide: process.platform === "win32",
116
118
  });
117
119
  const ok = result.status === 0;
118
120
  return {
package/src/common.mjs CHANGED
@@ -26,6 +26,11 @@ export function writeText(filePath, content) {
26
26
  fs.writeFileSync(filePath, content, "utf8");
27
27
  }
28
28
 
29
+ export function appendText(filePath, content) {
30
+ ensureDir(path.dirname(filePath));
31
+ fs.appendFileSync(filePath, content, "utf8");
32
+ }
33
+
29
34
  export function writeJson(filePath, value) {
30
35
  writeText(filePath, `${JSON.stringify(value, null, 2)}\n`);
31
36
  }
@@ -64,3 +69,9 @@ export function resolveFrom(rootDir, ...segments) {
64
69
  export function normalizeRelative(rootDir, targetPath) {
65
70
  return path.relative(rootDir, targetPath).replaceAll("\\", "/");
66
71
  }
72
+
73
+ export function sleep(ms) {
74
+ return new Promise((resolve) => {
75
+ setTimeout(resolve, Math.max(0, Number(ms || 0)));
76
+ });
77
+ }