helloloop 0.2.1 → 0.6.1
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/.codex-plugin/plugin.json +3 -3
- package/README.md +297 -272
- package/hosts/claude/marketplace/plugins/helloloop/.claude-plugin/plugin.json +1 -1
- package/hosts/claude/marketplace/plugins/helloloop/commands/helloloop.md +19 -9
- package/hosts/claude/marketplace/plugins/helloloop/skills/helloloop/SKILL.md +12 -4
- package/hosts/gemini/extension/GEMINI.md +13 -4
- package/hosts/gemini/extension/commands/helloloop.toml +19 -8
- package/hosts/gemini/extension/gemini-extension.json +1 -1
- package/package.json +2 -2
- package/scripts/uninstall-home-plugin.ps1 +25 -0
- package/skills/helloloop/SKILL.md +42 -7
- package/src/analyze_confirmation.mjs +108 -8
- package/src/analyze_prompt.mjs +17 -1
- package/src/analyze_user_input.mjs +321 -0
- package/src/analyzer.mjs +167 -42
- package/src/cli.mjs +34 -308
- package/src/cli_analyze_command.mjs +248 -0
- package/src/cli_args.mjs +106 -0
- package/src/cli_command_handlers.mjs +120 -0
- package/src/cli_context.mjs +31 -0
- package/src/cli_render.mjs +70 -0
- package/src/cli_support.mjs +95 -31
- package/src/completion_review.mjs +243 -0
- package/src/config.mjs +50 -0
- package/src/discovery.mjs +243 -9
- package/src/discovery_inference.mjs +62 -18
- package/src/discovery_paths.mjs +143 -8
- package/src/discovery_prompt.mjs +273 -0
- package/src/engine_metadata.mjs +79 -0
- package/src/engine_selection.mjs +335 -0
- package/src/engine_selection_failure.mjs +51 -0
- package/src/engine_selection_messages.mjs +119 -0
- package/src/engine_selection_probe.mjs +78 -0
- package/src/engine_selection_prompt.mjs +48 -0
- package/src/engine_selection_settings.mjs +38 -0
- package/src/guardrails.mjs +15 -4
- package/src/install.mjs +20 -266
- package/src/install_claude.mjs +189 -0
- package/src/install_codex.mjs +114 -0
- package/src/install_gemini.mjs +43 -0
- package/src/install_shared.mjs +90 -0
- package/src/process.mjs +482 -39
- package/src/prompt.mjs +9 -5
- package/src/prompt_session.mjs +40 -0
- package/src/rebuild.mjs +116 -0
- package/src/runner.mjs +3 -341
- package/src/runner_execute_task.mjs +301 -0
- package/src/runner_execution_support.mjs +155 -0
- package/src/runner_loop.mjs +106 -0
- package/src/runner_once.mjs +29 -0
- package/src/runner_status.mjs +104 -0
- package/src/runtime_recovery.mjs +301 -0
- package/src/shell_invocation.mjs +16 -0
- package/templates/analysis-output.schema.json +58 -1
- package/templates/policy.template.json +27 -0
- package/templates/project.template.json +2 -0
- package/templates/task-review-output.schema.json +70 -0
package/src/cli.mjs
CHANGED
|
@@ -1,328 +1,54 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
import { renderAnalyzeConfirmation, resolveAutoRunMaxTasks } from "./analyze_confirmation.mjs";
|
|
1
|
+
import { normalizeAnalyzeOptions } from "./analyze_user_input.mjs";
|
|
2
|
+
import { printHelp, parseArgs } from "./cli_args.mjs";
|
|
3
|
+
import { handleAnalyzeCommand } from "./cli_analyze_command.mjs";
|
|
5
4
|
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
|
|
18
|
-
const REPO_ROOT_PLACEHOLDER = "<REPO_ROOT>";
|
|
19
|
-
const DOCS_PATH_PLACEHOLDER = "<DOCS_PATH>";
|
|
20
|
-
const KNOWN_COMMANDS = new Set([
|
|
21
|
-
"analyze",
|
|
22
|
-
"install",
|
|
23
|
-
"init",
|
|
24
|
-
"status",
|
|
25
|
-
"next",
|
|
26
|
-
"run-once",
|
|
27
|
-
"run-loop",
|
|
28
|
-
"doctor",
|
|
29
|
-
"help",
|
|
30
|
-
"--help",
|
|
31
|
-
"-h",
|
|
32
|
-
]);
|
|
33
|
-
|
|
34
|
-
function parseArgs(argv) {
|
|
35
|
-
const [first = "", ...restArgs] = argv;
|
|
36
|
-
const command = !first
|
|
37
|
-
? "analyze"
|
|
38
|
-
: (KNOWN_COMMANDS.has(first) ? first : "analyze");
|
|
39
|
-
const rest = !first
|
|
40
|
-
? []
|
|
41
|
-
: (KNOWN_COMMANDS.has(first) ? restArgs : argv);
|
|
42
|
-
const options = {
|
|
43
|
-
requiredDocs: [],
|
|
44
|
-
constraints: [],
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
for (let index = 0; index < rest.length; index += 1) {
|
|
48
|
-
const arg = rest[index];
|
|
49
|
-
if (arg === "--dry-run") options.dryRun = true;
|
|
50
|
-
else if (arg === "--yes" || arg === "-y") options.yes = true;
|
|
51
|
-
else if (arg === "--allow-high-risk") options.allowHighRisk = true;
|
|
52
|
-
else if (arg === "--force") options.force = true;
|
|
53
|
-
else if (arg === "--task-id") { options.taskId = rest[index + 1]; index += 1; }
|
|
54
|
-
else if (arg === "--max-tasks") { options.maxTasks = Number(rest[index + 1]); index += 1; }
|
|
55
|
-
else if (arg === "--max-attempts") { options.maxAttempts = Number(rest[index + 1]); index += 1; }
|
|
56
|
-
else if (arg === "--max-strategies") { options.maxStrategies = Number(rest[index + 1]); index += 1; }
|
|
57
|
-
else if (arg === "--repo") { options.repoRoot = rest[index + 1]; index += 1; }
|
|
58
|
-
else if (arg === "--docs") { options.docsPath = rest[index + 1]; index += 1; }
|
|
59
|
-
else if (arg === "--host") { options.host = rest[index + 1]; index += 1; }
|
|
60
|
-
else if (arg === "--codex-home") { options.codexHome = rest[index + 1]; index += 1; }
|
|
61
|
-
else if (arg === "--claude-home") { options.claudeHome = rest[index + 1]; index += 1; }
|
|
62
|
-
else if (arg === "--gemini-home") { options.geminiHome = rest[index + 1]; index += 1; }
|
|
63
|
-
else if (arg === "--config-dir") { options.configDirName = rest[index + 1]; index += 1; }
|
|
64
|
-
else if (arg === "--required-doc") { options.requiredDocs.push(rest[index + 1]); index += 1; }
|
|
65
|
-
else if (arg === "--constraint") { options.constraints.push(rest[index + 1]); index += 1; }
|
|
66
|
-
else if (!options.inputPath) { options.inputPath = arg; }
|
|
67
|
-
else {
|
|
68
|
-
throw new Error(`未知参数:${arg}`);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
return { command, options };
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function helpText() {
|
|
76
|
-
return [
|
|
77
|
-
"用法:helloloop [command] [path] [options]",
|
|
78
|
-
"",
|
|
79
|
-
"命令:",
|
|
80
|
-
" analyze 自动分析并生成执行确认单;确认后继续自动接续开发(默认)",
|
|
81
|
-
" install 安装插件到 Codex Home(适合 npx / npm bin 分发)",
|
|
82
|
-
" init 初始化 .helloloop 配置",
|
|
83
|
-
" status 查看 backlog 与下一任务",
|
|
84
|
-
" next 生成下一任务干跑预览",
|
|
85
|
-
" run-once 执行一个任务",
|
|
86
|
-
" run-loop 连续执行多个任务",
|
|
87
|
-
" doctor 检查 Codex、当前插件 bundle 与目标仓库 .helloloop 配置是否可用",
|
|
88
|
-
"",
|
|
89
|
-
"选项:",
|
|
90
|
-
" --host <name> 安装宿主:codex | claude | gemini | all(默认 codex)",
|
|
91
|
-
" --codex-home <dir> Codex Home,install 默认使用 ~/.codex",
|
|
92
|
-
" --claude-home <dir> Claude Home,install 默认使用 ~/.claude",
|
|
93
|
-
" --gemini-home <dir> Gemini Home,install 默认使用 ~/.gemini",
|
|
94
|
-
" --repo <dir> 高级选项:显式指定项目仓库根目录",
|
|
95
|
-
" --docs <dir|file> 高级选项:显式指定开发文档目录或文件",
|
|
96
|
-
" --config-dir <dir> 配置目录,默认 .helloloop",
|
|
97
|
-
" -y, --yes 跳过交互确认,分析后直接开始自动执行",
|
|
98
|
-
" --dry-run 只分析并输出确认单,不真正开始自动执行",
|
|
99
|
-
" --task-id <id> 指定任务 id",
|
|
100
|
-
" --max-tasks <n> run-loop 最多执行 n 个任务",
|
|
101
|
-
" --max-attempts <n> 每种策略内最多重试 n 次",
|
|
102
|
-
" --max-strategies <n> 单任务最多切换 n 种策略继续重试",
|
|
103
|
-
" --allow-high-risk 允许执行 medium/high/critical 风险任务",
|
|
104
|
-
" --required-doc <p> 增加一个全局必读文档(AGENTS.md 会被自动忽略)",
|
|
105
|
-
" --constraint <text> 增加一个全局实现约束",
|
|
106
|
-
].join("\n");
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function printHelp() {
|
|
110
|
-
console.log(helpText());
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function renderFollowupExamples() {
|
|
114
|
-
return [
|
|
115
|
-
"下一步示例:",
|
|
116
|
-
`npx helloloop`,
|
|
117
|
-
`npx helloloop <PATH>`,
|
|
118
|
-
`npx helloloop --dry-run`,
|
|
119
|
-
`npx helloloop install --host all`,
|
|
120
|
-
`npx helloloop next`,
|
|
121
|
-
`如需显式补充路径:npx helloloop --repo ${REPO_ROOT_PLACEHOLDER} --docs ${DOCS_PATH_PLACEHOLDER}`,
|
|
122
|
-
].join("\n");
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function renderInstallSummary(result) {
|
|
126
|
-
const lines = [
|
|
127
|
-
"HelloLoop 已安装到以下宿主:",
|
|
128
|
-
];
|
|
129
|
-
|
|
130
|
-
for (const item of result.installedHosts) {
|
|
131
|
-
lines.push(`- ${item.displayName}:${item.targetRoot}`);
|
|
132
|
-
if (item.marketplaceFile) {
|
|
133
|
-
lines.push(` marketplace:${item.marketplaceFile}`);
|
|
134
|
-
}
|
|
135
|
-
if (item.settingsFile) {
|
|
136
|
-
lines.push(` settings:${item.settingsFile}`);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
lines.push("");
|
|
141
|
-
lines.push("使用入口:");
|
|
142
|
-
lines.push("- Codex:`$helloloop` / `npx helloloop`");
|
|
143
|
-
lines.push("- Claude:`/helloloop`");
|
|
144
|
-
lines.push("- Gemini:`/helloloop`");
|
|
145
|
-
lines.push("");
|
|
146
|
-
lines.push(renderFollowupExamples());
|
|
147
|
-
return lines.join("\n");
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function resolveContextFromOptions(options) {
|
|
151
|
-
const resolvedRepo = resolveRepoRoot({
|
|
152
|
-
cwd: process.cwd(),
|
|
153
|
-
repoRoot: options.repoRoot,
|
|
154
|
-
inputPath: options.inputPath,
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
if (!resolvedRepo.ok) {
|
|
158
|
-
throw new Error(resolvedRepo.message);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
return createContext({
|
|
162
|
-
repoRoot: resolvedRepo.repoRoot,
|
|
163
|
-
configDirName: options.configDirName,
|
|
164
|
-
});
|
|
165
|
-
}
|
|
5
|
+
handleDoctorCommand,
|
|
6
|
+
handleInitCommand,
|
|
7
|
+
handleInstallCommand,
|
|
8
|
+
handleNextCommand,
|
|
9
|
+
handleRunLoopCommand,
|
|
10
|
+
handleRunOnceCommand,
|
|
11
|
+
handleStatusCommand,
|
|
12
|
+
handleUninstallCommand,
|
|
13
|
+
} from "./cli_command_handlers.mjs";
|
|
14
|
+
import { resolveContextFromOptions, resolveStandardCommandOptions } from "./cli_context.mjs";
|
|
15
|
+
import { runDoctor } from "./cli_support.mjs";
|
|
166
16
|
|
|
167
17
|
export async function runCli(argv) {
|
|
168
|
-
const
|
|
169
|
-
|
|
18
|
+
const parsed = parseArgs(argv);
|
|
19
|
+
const command = parsed.command;
|
|
170
20
|
if (command === "help" || command === "--help" || command === "-h") {
|
|
171
21
|
printHelp();
|
|
172
22
|
return;
|
|
173
23
|
}
|
|
174
24
|
|
|
175
|
-
if (command === "install") {
|
|
176
|
-
const context = createContext({
|
|
177
|
-
repoRoot: options.repoRoot,
|
|
178
|
-
configDirName: options.configDirName,
|
|
179
|
-
});
|
|
180
|
-
const result = installPluginBundle({
|
|
181
|
-
bundleRoot: context.bundleRoot,
|
|
182
|
-
host: options.host,
|
|
183
|
-
codexHome: options.codexHome,
|
|
184
|
-
claudeHome: options.claudeHome,
|
|
185
|
-
geminiHome: options.geminiHome,
|
|
186
|
-
force: options.force,
|
|
187
|
-
});
|
|
188
|
-
console.log(renderInstallSummary(result));
|
|
189
|
-
return;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
25
|
if (command === "analyze") {
|
|
193
|
-
|
|
194
|
-
cwd: process.cwd(),
|
|
195
|
-
inputPath: options.inputPath,
|
|
196
|
-
repoRoot: options.repoRoot,
|
|
197
|
-
docsPath: options.docsPath,
|
|
198
|
-
configDirName: options.configDirName,
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
if (!result.ok) {
|
|
202
|
-
console.error(result.summary);
|
|
203
|
-
process.exitCode = 1;
|
|
204
|
-
return;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
const confirmationText = renderAnalyzeConfirmation(result.context, result.analysis, result.backlog, options);
|
|
208
|
-
const execution = analyzeExecution(result.backlog, options);
|
|
209
|
-
|
|
210
|
-
console.log(confirmationText);
|
|
211
|
-
console.log("");
|
|
212
|
-
|
|
213
|
-
if (options.dryRun) {
|
|
214
|
-
console.log("已按 --dry-run 跳过自动执行。");
|
|
215
|
-
return;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
if (execution.state !== "ready") {
|
|
219
|
-
console.log(renderAnalyzeStopMessage(execution.blockedReason || "当前 backlog 已无可自动执行任务。"));
|
|
220
|
-
return;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
const approved = options.yes ? true : await confirmAutoExecution();
|
|
224
|
-
if (!approved) {
|
|
225
|
-
console.log("已取消自动执行;分析结果与 backlog 已保留在 .helloloop/。");
|
|
226
|
-
return;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
console.log("");
|
|
230
|
-
console.log("开始自动接续执行...");
|
|
231
|
-
const results = await runLoop(result.context, {
|
|
232
|
-
...options,
|
|
233
|
-
maxTasks: resolveAutoRunMaxTasks(result.backlog, options),
|
|
234
|
-
});
|
|
235
|
-
const refreshedBacklog = loadBacklog(result.context);
|
|
236
|
-
console.log(renderAutoRunSummary(result.context, refreshedBacklog, results, options));
|
|
237
|
-
if (results.some((item) => !item.ok)) {
|
|
238
|
-
process.exitCode = 1;
|
|
239
|
-
}
|
|
240
|
-
return;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
const context = resolveContextFromOptions(options);
|
|
244
|
-
|
|
245
|
-
if (command === "init") {
|
|
246
|
-
const created = scaffoldIfMissing(context);
|
|
247
|
-
if (!created.length) {
|
|
248
|
-
console.log("HelloLoop 配置已存在,无需初始化。");
|
|
249
|
-
return;
|
|
250
|
-
}
|
|
251
|
-
console.log([
|
|
252
|
-
"已初始化以下文件:",
|
|
253
|
-
...created.map((item) => `- ${path.relative(context.repoRoot, item).replaceAll("\\", "/")}`),
|
|
254
|
-
].join("\n"));
|
|
255
|
-
return;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
if (command === "doctor") {
|
|
259
|
-
await runDoctor(context, options);
|
|
260
|
-
return;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
if (command === "status") {
|
|
264
|
-
console.log(renderStatusText(context, options));
|
|
26
|
+
process.exitCode = await handleAnalyzeCommand(normalizeAnalyzeOptions(parsed.options, process.cwd()));
|
|
265
27
|
return;
|
|
266
28
|
}
|
|
267
29
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
console.log("当前没有可执行任务。");
|
|
272
|
-
return;
|
|
273
|
-
}
|
|
274
|
-
console.log([
|
|
275
|
-
"下一任务预览",
|
|
276
|
-
"============",
|
|
277
|
-
`任务:${result.task.title}`,
|
|
278
|
-
`编号:${result.task.id}`,
|
|
279
|
-
`运行目录:${result.runDir}`,
|
|
280
|
-
"",
|
|
281
|
-
"验证命令:",
|
|
282
|
-
...result.verifyCommands.map((item) => `- ${item}`),
|
|
283
|
-
"",
|
|
284
|
-
"提示词:",
|
|
285
|
-
result.prompt,
|
|
286
|
-
].join("\n"));
|
|
30
|
+
const options = resolveStandardCommandOptions(parsed.options);
|
|
31
|
+
if (command === "install") {
|
|
32
|
+
process.exitCode = handleInstallCommand(options);
|
|
287
33
|
return;
|
|
288
34
|
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
const result = await runOnce(context, options);
|
|
292
|
-
if (!result.ok) {
|
|
293
|
-
console.error(result.summary || "执行失败。");
|
|
294
|
-
process.exitCode = 1;
|
|
295
|
-
return;
|
|
296
|
-
}
|
|
297
|
-
if (options.dryRun) {
|
|
298
|
-
console.log(result.task
|
|
299
|
-
? `已生成干跑预览:${result.task.title}\n运行目录:${result.runDir}`
|
|
300
|
-
: "当前没有可执行任务。");
|
|
301
|
-
return;
|
|
302
|
-
}
|
|
303
|
-
console.log(result.task
|
|
304
|
-
? `完成任务:${result.task.title}\n运行目录:${result.runDir}`
|
|
305
|
-
: "没有可执行任务。");
|
|
35
|
+
if (command === "uninstall") {
|
|
36
|
+
process.exitCode = handleUninstallCommand(options);
|
|
306
37
|
return;
|
|
307
38
|
}
|
|
308
39
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
console.error(failed.summary || "连续执行中断。");
|
|
321
|
-
process.exitCode = 1;
|
|
322
|
-
}
|
|
323
|
-
return;
|
|
40
|
+
const context = resolveContextFromOptions(options);
|
|
41
|
+
const handlers = {
|
|
42
|
+
doctor: () => handleDoctorCommand(context, options, runDoctor),
|
|
43
|
+
init: () => handleInitCommand(context),
|
|
44
|
+
next: () => handleNextCommand(context, options),
|
|
45
|
+
"run-loop": () => handleRunLoopCommand(context, options),
|
|
46
|
+
"run-once": () => handleRunOnceCommand(context, options),
|
|
47
|
+
status: () => handleStatusCommand(context, options),
|
|
48
|
+
};
|
|
49
|
+
if (!handlers[command]) {
|
|
50
|
+
throw new Error(`未知命令:${command}`);
|
|
324
51
|
}
|
|
325
52
|
|
|
326
|
-
|
|
53
|
+
process.exitCode = await handlers[command]();
|
|
327
54
|
}
|
|
328
|
-
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { analyzeExecution } from "./backlog.mjs";
|
|
2
|
+
import { renderAnalyzeConfirmation, resolveAutoRunMaxTasks } from "./analyze_confirmation.mjs";
|
|
3
|
+
import {
|
|
4
|
+
confirmAutoExecution,
|
|
5
|
+
confirmRepoConflictResolution,
|
|
6
|
+
renderAnalyzeStopMessage,
|
|
7
|
+
renderAutoRunSummary,
|
|
8
|
+
renderRepoConflictStopMessage,
|
|
9
|
+
} from "./cli_support.mjs";
|
|
10
|
+
import { analyzeWorkspace } from "./analyzer.mjs";
|
|
11
|
+
import {
|
|
12
|
+
hasBlockingInputIssues,
|
|
13
|
+
renderBlockingInputIssueMessage,
|
|
14
|
+
} from "./analyze_user_input.mjs";
|
|
15
|
+
import { loadBacklog, loadPolicy } from "./config.mjs";
|
|
16
|
+
import { createContext } from "./context.mjs";
|
|
17
|
+
import { createDiscoveryPromptSession, resolveDiscoveryFailureInteractively } from "./discovery_prompt.mjs";
|
|
18
|
+
import { resolveEngineSelection } from "./engine_selection.mjs";
|
|
19
|
+
import { resetRepoForRebuild } from "./rebuild.mjs";
|
|
20
|
+
import { runLoop } from "./runner.mjs";
|
|
21
|
+
import { renderRebuildSummary } from "./cli_render.mjs";
|
|
22
|
+
import { shouldConfirmRepoRebuild } from "./cli_support.mjs";
|
|
23
|
+
|
|
24
|
+
async function resolveAnalyzeEngineSelection(options) {
|
|
25
|
+
if (options.engineResolution?.ok) {
|
|
26
|
+
return options.engineResolution;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const provisionalContext = createContext({
|
|
30
|
+
repoRoot: options.repoRoot || process.cwd(),
|
|
31
|
+
configDirName: options.configDirName,
|
|
32
|
+
});
|
|
33
|
+
return resolveEngineSelection({
|
|
34
|
+
context: provisionalContext,
|
|
35
|
+
policy: loadPolicy(provisionalContext),
|
|
36
|
+
options,
|
|
37
|
+
interactive: !options.yes,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function analyzeWithResolvedDiscovery(options) {
|
|
42
|
+
let currentOptions = { ...options };
|
|
43
|
+
let lastResult = null;
|
|
44
|
+
let promptSession = null;
|
|
45
|
+
|
|
46
|
+
function getPromptSession() {
|
|
47
|
+
if (currentOptions.yes) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
if (!promptSession) {
|
|
51
|
+
promptSession = createDiscoveryPromptSession();
|
|
52
|
+
}
|
|
53
|
+
return promptSession;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
const engineResolution = await resolveAnalyzeEngineSelection(currentOptions);
|
|
58
|
+
if (!engineResolution.ok) {
|
|
59
|
+
return {
|
|
60
|
+
options: currentOptions,
|
|
61
|
+
result: {
|
|
62
|
+
ok: false,
|
|
63
|
+
code: engineResolution.code,
|
|
64
|
+
summary: engineResolution.message,
|
|
65
|
+
discovery: null,
|
|
66
|
+
engineResolution,
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
currentOptions = {
|
|
71
|
+
...currentOptions,
|
|
72
|
+
engineResolution,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
76
|
+
lastResult = await analyzeWorkspace({
|
|
77
|
+
cwd: process.cwd(),
|
|
78
|
+
inputPath: currentOptions.inputPath,
|
|
79
|
+
repoRoot: currentOptions.repoRoot,
|
|
80
|
+
docsPath: currentOptions.docsPath,
|
|
81
|
+
configDirName: currentOptions.configDirName,
|
|
82
|
+
allowNewRepoRoot: currentOptions.allowNewRepoRoot,
|
|
83
|
+
engine: currentOptions.engine,
|
|
84
|
+
engineSource: currentOptions.engineSource,
|
|
85
|
+
engineResolution: currentOptions.engineResolution,
|
|
86
|
+
hostContext: currentOptions.hostContext,
|
|
87
|
+
userRequestText: currentOptions.userRequestText,
|
|
88
|
+
yes: currentOptions.yes,
|
|
89
|
+
selectionSources: currentOptions.selectionSources,
|
|
90
|
+
userIntent: currentOptions.userIntent,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
if (lastResult.ok) {
|
|
94
|
+
return { options: currentOptions, result: lastResult };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const nextOptions = await resolveDiscoveryFailureInteractively(
|
|
98
|
+
lastResult,
|
|
99
|
+
currentOptions,
|
|
100
|
+
process.cwd(),
|
|
101
|
+
!currentOptions.yes,
|
|
102
|
+
getPromptSession(),
|
|
103
|
+
);
|
|
104
|
+
if (!nextOptions) {
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
currentOptions = nextOptions;
|
|
108
|
+
}
|
|
109
|
+
} finally {
|
|
110
|
+
promptSession?.close();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return { options: currentOptions, result: lastResult };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function printAnalyzeConfirmation(result, activeOptions) {
|
|
117
|
+
console.log(renderAnalyzeConfirmation(
|
|
118
|
+
result.context,
|
|
119
|
+
result.analysis,
|
|
120
|
+
result.backlog,
|
|
121
|
+
activeOptions,
|
|
122
|
+
result.discovery,
|
|
123
|
+
));
|
|
124
|
+
console.log("");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function resolveRepoConflict(analyzed, result, activeOptions) {
|
|
128
|
+
if (!shouldConfirmRepoRebuild(result.analysis, result.discovery)) {
|
|
129
|
+
return { state: "ready", analyzed, result, activeOptions };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (activeOptions.rebuildExisting) {
|
|
133
|
+
const resetSummary = resetRepoForRebuild(result.context, result.discovery);
|
|
134
|
+
console.log(renderRebuildSummary(resetSummary));
|
|
135
|
+
console.log("");
|
|
136
|
+
return {
|
|
137
|
+
state: "reanalyze",
|
|
138
|
+
options: {
|
|
139
|
+
...activeOptions,
|
|
140
|
+
repoRoot: result.context.repoRoot,
|
|
141
|
+
rebuildExisting: false,
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (activeOptions.yes) {
|
|
147
|
+
console.log(renderRepoConflictStopMessage(result.analysis));
|
|
148
|
+
return { state: "exit", exitCode: 1 };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const repoConflictDecision = await confirmRepoConflictResolution(result.analysis);
|
|
152
|
+
if (repoConflictDecision === "cancel") {
|
|
153
|
+
console.log("已取消自动执行;分析结果与 backlog 已保留在 .helloloop/。");
|
|
154
|
+
return { state: "done" };
|
|
155
|
+
}
|
|
156
|
+
if (repoConflictDecision === "continue") {
|
|
157
|
+
return { state: "ready", analyzed, result, activeOptions };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const resetSummary = resetRepoForRebuild(result.context, result.discovery);
|
|
161
|
+
console.log(renderRebuildSummary(resetSummary));
|
|
162
|
+
console.log("");
|
|
163
|
+
return {
|
|
164
|
+
state: "reanalyze",
|
|
165
|
+
options: {
|
|
166
|
+
...activeOptions,
|
|
167
|
+
repoRoot: result.context.repoRoot,
|
|
168
|
+
rebuildExisting: false,
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function prepareAnalyzeExecution(initialOptions) {
|
|
174
|
+
let analyzed = await analyzeWithResolvedDiscovery(initialOptions);
|
|
175
|
+
let result = analyzed.result;
|
|
176
|
+
let activeOptions = analyzed.options;
|
|
177
|
+
|
|
178
|
+
while (true) {
|
|
179
|
+
if (!result.ok) {
|
|
180
|
+
console.error(result.summary);
|
|
181
|
+
return { exitCode: 1 };
|
|
182
|
+
}
|
|
183
|
+
if (result.engineResolution?.ok) {
|
|
184
|
+
activeOptions = {
|
|
185
|
+
...activeOptions,
|
|
186
|
+
engineResolution: result.engineResolution,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
printAnalyzeConfirmation(result, activeOptions);
|
|
191
|
+
const conflictResolution = await resolveRepoConflict(analyzed, result, activeOptions);
|
|
192
|
+
if (conflictResolution.state === "ready") {
|
|
193
|
+
return { result, activeOptions };
|
|
194
|
+
}
|
|
195
|
+
if (conflictResolution.state === "done" || conflictResolution.state === "exit") {
|
|
196
|
+
return { exitCode: conflictResolution.exitCode || 0 };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
analyzed = await analyzeWithResolvedDiscovery(conflictResolution.options);
|
|
200
|
+
result = analyzed.result;
|
|
201
|
+
activeOptions = analyzed.options;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async function maybeRunAutoExecution(result, activeOptions) {
|
|
206
|
+
const execution = analyzeExecution(result.backlog, activeOptions);
|
|
207
|
+
|
|
208
|
+
if (activeOptions.dryRun) {
|
|
209
|
+
console.log("已按 --dry-run 跳过自动执行。");
|
|
210
|
+
return 0;
|
|
211
|
+
}
|
|
212
|
+
if (execution.state !== "ready") {
|
|
213
|
+
console.log(renderAnalyzeStopMessage(execution.blockedReason || "当前 backlog 已无可自动执行任务。"));
|
|
214
|
+
return 0;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const approved = activeOptions.yes ? true : await confirmAutoExecution();
|
|
218
|
+
if (!approved) {
|
|
219
|
+
console.log("已取消自动执行;分析结果与 backlog 已保留在 .helloloop/。");
|
|
220
|
+
return 0;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
console.log("");
|
|
224
|
+
console.log("开始自动接续执行...");
|
|
225
|
+
const results = await runLoop(result.context, {
|
|
226
|
+
...activeOptions,
|
|
227
|
+
engineResolution: result.engineResolution?.ok ? result.engineResolution : activeOptions.engineResolution,
|
|
228
|
+
maxTasks: resolveAutoRunMaxTasks(result.backlog, activeOptions) || undefined,
|
|
229
|
+
fullAutoMainline: true,
|
|
230
|
+
});
|
|
231
|
+
const refreshedBacklog = loadBacklog(result.context);
|
|
232
|
+
console.log(renderAutoRunSummary(result.context, refreshedBacklog, results, activeOptions));
|
|
233
|
+
return results.some((item) => !item.ok) ? 1 : 0;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export async function handleAnalyzeCommand(options) {
|
|
237
|
+
if (hasBlockingInputIssues(options.inputIssues)) {
|
|
238
|
+
console.error(renderBlockingInputIssueMessage(options.inputIssues));
|
|
239
|
+
return 1;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const prepared = await prepareAnalyzeExecution(options);
|
|
243
|
+
if (!prepared.result) {
|
|
244
|
+
return prepared.exitCode || 0;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return maybeRunAutoExecution(prepared.result, prepared.activeOptions);
|
|
248
|
+
}
|