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.
Files changed (58) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +3 -3
  3. package/README.md +297 -272
  4. package/hosts/claude/marketplace/plugins/helloloop/.claude-plugin/plugin.json +1 -1
  5. package/hosts/claude/marketplace/plugins/helloloop/commands/helloloop.md +19 -9
  6. package/hosts/claude/marketplace/plugins/helloloop/skills/helloloop/SKILL.md +12 -4
  7. package/hosts/gemini/extension/GEMINI.md +13 -4
  8. package/hosts/gemini/extension/commands/helloloop.toml +19 -8
  9. package/hosts/gemini/extension/gemini-extension.json +1 -1
  10. package/package.json +2 -2
  11. package/scripts/uninstall-home-plugin.ps1 +25 -0
  12. package/skills/helloloop/SKILL.md +42 -7
  13. package/src/analyze_confirmation.mjs +108 -8
  14. package/src/analyze_prompt.mjs +17 -1
  15. package/src/analyze_user_input.mjs +321 -0
  16. package/src/analyzer.mjs +167 -42
  17. package/src/cli.mjs +34 -308
  18. package/src/cli_analyze_command.mjs +248 -0
  19. package/src/cli_args.mjs +106 -0
  20. package/src/cli_command_handlers.mjs +120 -0
  21. package/src/cli_context.mjs +31 -0
  22. package/src/cli_render.mjs +70 -0
  23. package/src/cli_support.mjs +95 -31
  24. package/src/completion_review.mjs +243 -0
  25. package/src/config.mjs +50 -0
  26. package/src/discovery.mjs +243 -9
  27. package/src/discovery_inference.mjs +62 -18
  28. package/src/discovery_paths.mjs +143 -8
  29. package/src/discovery_prompt.mjs +273 -0
  30. package/src/engine_metadata.mjs +79 -0
  31. package/src/engine_selection.mjs +335 -0
  32. package/src/engine_selection_failure.mjs +51 -0
  33. package/src/engine_selection_messages.mjs +119 -0
  34. package/src/engine_selection_probe.mjs +78 -0
  35. package/src/engine_selection_prompt.mjs +48 -0
  36. package/src/engine_selection_settings.mjs +38 -0
  37. package/src/guardrails.mjs +15 -4
  38. package/src/install.mjs +20 -266
  39. package/src/install_claude.mjs +189 -0
  40. package/src/install_codex.mjs +114 -0
  41. package/src/install_gemini.mjs +43 -0
  42. package/src/install_shared.mjs +90 -0
  43. package/src/process.mjs +482 -39
  44. package/src/prompt.mjs +9 -5
  45. package/src/prompt_session.mjs +40 -0
  46. package/src/rebuild.mjs +116 -0
  47. package/src/runner.mjs +3 -341
  48. package/src/runner_execute_task.mjs +301 -0
  49. package/src/runner_execution_support.mjs +155 -0
  50. package/src/runner_loop.mjs +106 -0
  51. package/src/runner_once.mjs +29 -0
  52. package/src/runner_status.mjs +104 -0
  53. package/src/runtime_recovery.mjs +301 -0
  54. package/src/shell_invocation.mjs +16 -0
  55. package/templates/analysis-output.schema.json +58 -1
  56. package/templates/policy.template.json +27 -0
  57. package/templates/project.template.json +2 -0
  58. package/templates/task-review-output.schema.json +70 -0
@@ -0,0 +1,106 @@
1
+ const REPO_ROOT_PLACEHOLDER = "<REPO_ROOT>";
2
+ const DOCS_PATH_PLACEHOLDER = "<DOCS_PATH>";
3
+ const KNOWN_COMMANDS = new Set([
4
+ "analyze",
5
+ "install",
6
+ "uninstall",
7
+ "init",
8
+ "status",
9
+ "next",
10
+ "run-once",
11
+ "run-loop",
12
+ "doctor",
13
+ "help",
14
+ "--help",
15
+ "-h",
16
+ ]);
17
+
18
+ export function parseArgs(argv) {
19
+ const [first = "", ...restArgs] = argv;
20
+ const command = !first
21
+ ? "analyze"
22
+ : (KNOWN_COMMANDS.has(first) ? first : "analyze");
23
+ const rest = !first
24
+ ? []
25
+ : (KNOWN_COMMANDS.has(first) ? restArgs : argv);
26
+ const options = {
27
+ requiredDocs: [],
28
+ constraints: [],
29
+ positionalArgs: [],
30
+ };
31
+
32
+ for (let index = 0; index < rest.length; index += 1) {
33
+ const arg = rest[index];
34
+ if (arg === "--dry-run") options.dryRun = true;
35
+ else if (arg === "--yes" || arg === "-y") options.yes = true;
36
+ else if (arg === "--allow-high-risk") options.allowHighRisk = true;
37
+ else if (arg === "--rebuild-existing") options.rebuildExisting = true;
38
+ else if (arg === "--force") options.force = true;
39
+ else if (arg === "--task-id") { options.taskId = rest[index + 1]; index += 1; }
40
+ else if (arg === "--max-tasks") { options.maxTasks = Number(rest[index + 1]); index += 1; }
41
+ else if (arg === "--max-attempts") { options.maxAttempts = Number(rest[index + 1]); index += 1; }
42
+ else if (arg === "--max-strategies") { options.maxStrategies = Number(rest[index + 1]); index += 1; }
43
+ else if (arg === "--repo") { options.repoRoot = rest[index + 1]; index += 1; }
44
+ else if (arg === "--docs") { options.docsPath = rest[index + 1]; index += 1; }
45
+ else if (arg === "--host") { options.host = rest[index + 1]; index += 1; }
46
+ else if (arg === "--engine") { options.engine = rest[index + 1]; options.engineSource = "flag"; index += 1; }
47
+ else if (arg === "--host-context") { options.hostContext = rest[index + 1]; index += 1; }
48
+ else if (arg === "--codex-home") { options.codexHome = rest[index + 1]; index += 1; }
49
+ else if (arg === "--claude-home") { options.claudeHome = rest[index + 1]; index += 1; }
50
+ else if (arg === "--gemini-home") { options.geminiHome = rest[index + 1]; index += 1; }
51
+ else if (arg === "--config-dir") { options.configDirName = rest[index + 1]; index += 1; }
52
+ else if (arg === "--required-doc") { options.requiredDocs.push(rest[index + 1]); index += 1; }
53
+ else if (arg === "--constraint") { options.constraints.push(rest[index + 1]); index += 1; }
54
+ else { options.positionalArgs.push(arg); }
55
+ }
56
+
57
+ return { command, options };
58
+ }
59
+
60
+ function helpText() {
61
+ return [
62
+ "用法:helloloop [command] [engine] [path|需求说明...] [options]",
63
+ "",
64
+ "命令:",
65
+ " analyze 自动分析并生成执行确认单;确认后继续自动接续开发(默认)",
66
+ " install 安装插件到 Codex Home(适合 npx / npm bin 分发)",
67
+ " uninstall 从所选宿主卸载插件并清理注册信息",
68
+ " init 初始化 .helloloop 配置",
69
+ " status 查看 backlog 与下一任务",
70
+ " next 生成下一任务干跑预览",
71
+ " run-once 执行一个任务",
72
+ " run-loop 连续执行多个任务",
73
+ " doctor 检查 Codex、当前插件 bundle 与目标仓库 .helloloop 配置是否可用",
74
+ "",
75
+ "选项:",
76
+ " --host <name> 安装宿主:codex | claude | gemini | all(默认 codex)",
77
+ " --codex-home <dir> Codex Home,install 默认使用 ~/.codex",
78
+ " --claude-home <dir> Claude Home,install 默认使用 ~/.claude",
79
+ " --gemini-home <dir> Gemini Home,install 默认使用 ~/.gemini",
80
+ " [engine] analyze 默认支持直接写:codex | claude | gemini",
81
+ " --repo <dir> 高级选项:显式指定项目仓库根目录",
82
+ " --docs <dir|file> 高级选项:显式指定开发文档目录或文件",
83
+ " --config-dir <dir> 配置目录,默认 .helloloop",
84
+ " -y, --yes 跳过交互确认,分析后直接开始自动执行",
85
+ " --dry-run 只分析并输出确认单,不真正开始自动执行",
86
+ " --task-id <id> 指定任务 id",
87
+ " --max-tasks <n> run-loop 最多执行 n 个任务",
88
+ " --max-attempts <n> 每种策略内最多重试 n 次",
89
+ " --max-strategies <n> 单任务最多切换 n 种策略继续重试",
90
+ " --allow-high-risk 允许执行 medium/high/critical 风险任务",
91
+ " --rebuild-existing 分析判断当前项目与文档冲突时,自动清理当前项目后按文档重建",
92
+ " --required-doc <p> 增加一个全局必读文档(AGENTS.md 会被自动忽略)",
93
+ " --constraint <text> 增加一个全局实现约束",
94
+ "",
95
+ "补充说明:",
96
+ " analyze 默认支持在命令后混合传入引擎、路径和自然语言要求。",
97
+ " 如果同时检测到多个可用引擎且没有明确指定,会先询问你选择。",
98
+ " 示例:npx helloloop claude <DOCS_PATH> <PROJECT_ROOT> 先分析偏差,不要执行",
99
+ ].join("\n");
100
+ }
101
+
102
+ export function printHelp() {
103
+ console.log(helpText());
104
+ }
105
+
106
+ export { DOCS_PATH_PLACEHOLDER, REPO_ROOT_PLACEHOLDER };
@@ -0,0 +1,120 @@
1
+ import path from "node:path";
2
+
3
+ import { createContext } from "./context.mjs";
4
+ import { loadBacklog, scaffoldIfMissing } from "./config.mjs";
5
+ import { installPluginBundle, uninstallPluginBundle } from "./install.mjs";
6
+ import { runLoop, runOnce, renderStatusText } from "./runner.mjs";
7
+ import { renderInstallSummary, renderUninstallSummary } from "./cli_render.mjs";
8
+
9
+ export function handleInstallCommand(options) {
10
+ const context = createContext({
11
+ repoRoot: options.repoRoot,
12
+ configDirName: options.configDirName,
13
+ });
14
+ const result = installPluginBundle({
15
+ bundleRoot: context.bundleRoot,
16
+ host: options.host,
17
+ codexHome: options.codexHome,
18
+ claudeHome: options.claudeHome,
19
+ geminiHome: options.geminiHome,
20
+ force: options.force,
21
+ });
22
+ console.log(renderInstallSummary(result));
23
+ return 0;
24
+ }
25
+
26
+ export function handleUninstallCommand(options) {
27
+ const result = uninstallPluginBundle({
28
+ host: options.host,
29
+ codexHome: options.codexHome,
30
+ claudeHome: options.claudeHome,
31
+ geminiHome: options.geminiHome,
32
+ });
33
+ console.log(renderUninstallSummary(result));
34
+ return 0;
35
+ }
36
+
37
+ export function handleInitCommand(context) {
38
+ const created = scaffoldIfMissing(context);
39
+ if (!created.length) {
40
+ console.log("HelloLoop 配置已存在,无需初始化。");
41
+ return 0;
42
+ }
43
+
44
+ console.log([
45
+ "已初始化以下文件:",
46
+ ...created.map((item) => `- ${path.relative(context.repoRoot, item).replaceAll("\\", "/")}`),
47
+ ].join("\n"));
48
+ return 0;
49
+ }
50
+
51
+ export async function handleDoctorCommand(context, options, runDoctor) {
52
+ await runDoctor(context, options);
53
+ return 0;
54
+ }
55
+
56
+ export function handleStatusCommand(context, options) {
57
+ console.log(renderStatusText(context, options));
58
+ return 0;
59
+ }
60
+
61
+ export async function handleNextCommand(context, options) {
62
+ const result = await runOnce(context, { ...options, dryRun: true });
63
+ if (!result.task) {
64
+ console.log("当前没有可执行任务。");
65
+ return 0;
66
+ }
67
+
68
+ console.log([
69
+ "下一任务预览",
70
+ "============",
71
+ `任务:${result.task.title}`,
72
+ `编号:${result.task.id}`,
73
+ `运行目录:${result.runDir}`,
74
+ "",
75
+ "验证命令:",
76
+ ...result.verifyCommands.map((item) => `- ${item}`),
77
+ "",
78
+ "提示词:",
79
+ result.prompt,
80
+ ].join("\n"));
81
+ return 0;
82
+ }
83
+
84
+ export async function handleRunOnceCommand(context, options) {
85
+ const result = await runOnce(context, options);
86
+ if (!result.ok) {
87
+ console.error(result.summary || "执行失败。");
88
+ return 1;
89
+ }
90
+ if (options.dryRun) {
91
+ console.log(result.task
92
+ ? `已生成干跑预览:${result.task.title}\n运行目录:${result.runDir}`
93
+ : "当前没有可执行任务。");
94
+ return 0;
95
+ }
96
+
97
+ console.log(result.task
98
+ ? `完成任务:${result.task.title}\n运行目录:${result.runDir}`
99
+ : "没有可执行任务。");
100
+ return 0;
101
+ }
102
+
103
+ export async function handleRunLoopCommand(context, options) {
104
+ const results = await runLoop(context, options);
105
+ const failed = results.find((item) => !item.ok);
106
+
107
+ for (const item of results) {
108
+ if (!item.task) {
109
+ console.log("没有更多可执行任务。");
110
+ break;
111
+ }
112
+ console.log(`${item.ok ? "成功" : "失败"}:${item.task.title}`);
113
+ }
114
+
115
+ if (failed) {
116
+ console.error(failed.summary || "连续执行中断。");
117
+ return 1;
118
+ }
119
+ return 0;
120
+ }
@@ -0,0 +1,31 @@
1
+ import { createContext } from "./context.mjs";
2
+ import { resolveRepoRoot } from "./discovery.mjs";
3
+
4
+ export function resolveContextFromOptions(options) {
5
+ const resolvedRepo = resolveRepoRoot({
6
+ cwd: process.cwd(),
7
+ repoRoot: options.repoRoot,
8
+ inputPath: options.inputPath,
9
+ });
10
+
11
+ if (!resolvedRepo.ok) {
12
+ throw new Error(resolvedRepo.message);
13
+ }
14
+
15
+ return createContext({
16
+ repoRoot: resolvedRepo.repoRoot,
17
+ configDirName: options.configDirName,
18
+ });
19
+ }
20
+
21
+ export function resolveStandardCommandOptions(options) {
22
+ const nextOptions = { ...options };
23
+ const positionals = Array.isArray(nextOptions.positionalArgs) ? nextOptions.positionalArgs : [];
24
+ if (positionals.length > 1) {
25
+ throw new Error(`未知参数:${positionals.slice(1).join(" ")}`);
26
+ }
27
+ if (positionals.length === 1 && !nextOptions.inputPath) {
28
+ nextOptions.inputPath = positionals[0];
29
+ }
30
+ return nextOptions;
31
+ }
@@ -0,0 +1,70 @@
1
+ import { DOCS_PATH_PLACEHOLDER, REPO_ROOT_PLACEHOLDER } from "./cli_args.mjs";
2
+
3
+ function renderFollowupExamples() {
4
+ return [
5
+ "下一步示例:",
6
+ "npx helloloop",
7
+ "npx helloloop <PATH>",
8
+ "npx helloloop codex",
9
+ "npx helloloop claude <PATH>",
10
+ "npx helloloop gemini <PATH> 继续完成后续开发",
11
+ "npx helloloop --dry-run",
12
+ "npx helloloop install --host all",
13
+ "npx helloloop uninstall --host all",
14
+ "npx helloloop next",
15
+ `如需显式补充路径:npx helloloop --repo ${REPO_ROOT_PLACEHOLDER} --docs ${DOCS_PATH_PLACEHOLDER}`,
16
+ ].join("\n");
17
+ }
18
+
19
+ export function renderInstallSummary(result) {
20
+ const lines = ["HelloLoop 已安装到以下宿主:"];
21
+
22
+ for (const item of result.installedHosts) {
23
+ lines.push(`- ${item.displayName}:${item.targetRoot}`);
24
+ if (item.marketplaceFile) {
25
+ lines.push(` marketplace:${item.marketplaceFile}`);
26
+ }
27
+ if (item.settingsFile) {
28
+ lines.push(` settings:${item.settingsFile}`);
29
+ }
30
+ }
31
+
32
+ lines.push("");
33
+ lines.push("使用入口:");
34
+ lines.push("- Codex:`$helloloop` / `npx helloloop`");
35
+ lines.push("- Claude:`/helloloop`");
36
+ lines.push("- Gemini:`/helloloop`");
37
+ lines.push("");
38
+ lines.push(renderFollowupExamples());
39
+ return lines.join("\n");
40
+ }
41
+
42
+ export function renderUninstallSummary(result) {
43
+ const lines = ["HelloLoop 已从以下宿主卸载:"];
44
+
45
+ for (const item of result.uninstalledHosts) {
46
+ lines.push(`- ${item.displayName}:${item.removed ? "已清理" : "未发现现有安装"}`);
47
+ lines.push(` 目标目录:${item.targetRoot}`);
48
+ if (item.marketplaceFile) {
49
+ lines.push(` marketplace:${item.marketplaceFile}`);
50
+ }
51
+ if (item.settingsFile) {
52
+ lines.push(` settings:${item.settingsFile}`);
53
+ }
54
+ }
55
+
56
+ lines.push("");
57
+ lines.push("如需重新安装:");
58
+ lines.push("- `npx helloloop install --host codex`");
59
+ lines.push("- `npx helloloop install --host all`");
60
+ return lines.join("\n");
61
+ }
62
+
63
+ export function renderRebuildSummary(resetSummary) {
64
+ return [
65
+ "已按确认结果清理当前项目,并准备按开发文档重新开始。",
66
+ `- 已清理顶层条目:${resetSummary.removedEntries.length ? resetSummary.removedEntries.join(",") : "无"}`,
67
+ `- 已保留开发文档:${resetSummary.preservedDocs.length ? resetSummary.preservedDocs.join(",") : "无"}`,
68
+ `- 重建记录:${resetSummary.manifestFile.replaceAll("\\", "/")}`,
69
+ ].join("\n");
70
+ }
@@ -1,9 +1,9 @@
1
1
  import { spawnSync } from "node:child_process";
2
2
  import path from "node:path";
3
- import { createInterface } from "node:readline/promises";
4
3
 
5
4
  import { analyzeExecution, summarizeBacklog } from "./backlog.mjs";
6
5
  import { fileExists, readJson } from "./common.mjs";
6
+ import { createPromptSession } from "./prompt_session.mjs";
7
7
  import { resolveCliInvocation, resolveCodexInvocation } from "./shell_invocation.mjs";
8
8
 
9
9
  function probeCodexVersion() {
@@ -28,34 +28,30 @@ function probeCodexVersion() {
28
28
  };
29
29
  }
30
30
 
31
- export function collectDoctorChecks(context) {
32
- const codexVersion = probeCodexVersion();
31
+ function shouldCheckProjectRuntime(context, options = {}) {
32
+ if (options.repoRoot || options.inputPath) {
33
+ return true;
34
+ }
35
+
36
+ if (context.repoRoot !== context.toolRoot) {
37
+ return true;
38
+ }
39
+
33
40
  return [
41
+ context.backlogFile,
42
+ context.policyFile,
43
+ context.projectFile,
44
+ ].some((filePath) => fileExists(filePath));
45
+ }
46
+
47
+ export function collectDoctorChecks(context, options = {}) {
48
+ const codexVersion = probeCodexVersion();
49
+ const checks = [
34
50
  {
35
51
  name: "codex CLI",
36
52
  ok: codexVersion.ok,
37
53
  detail: codexVersion.detail,
38
54
  },
39
- {
40
- name: "backlog.json",
41
- ok: fileExists(context.backlogFile),
42
- detail: context.backlogFile,
43
- },
44
- {
45
- name: "policy.json",
46
- ok: fileExists(context.policyFile),
47
- detail: context.policyFile,
48
- },
49
- {
50
- name: "verify.yaml",
51
- ok: fileExists(context.repoVerifyFile),
52
- detail: context.repoVerifyFile,
53
- },
54
- {
55
- name: "project.json",
56
- ok: fileExists(context.projectFile),
57
- detail: context.projectFile,
58
- },
59
55
  {
60
56
  name: "plugin manifest",
61
57
  ok: fileExists(context.pluginManifestFile),
@@ -72,6 +68,33 @@ export function collectDoctorChecks(context) {
72
68
  detail: context.installScriptFile,
73
69
  },
74
70
  ];
71
+
72
+ if (shouldCheckProjectRuntime(context, options)) {
73
+ checks.splice(1, 0,
74
+ {
75
+ name: "backlog.json",
76
+ ok: fileExists(context.backlogFile),
77
+ detail: context.backlogFile,
78
+ },
79
+ {
80
+ name: "policy.json",
81
+ ok: fileExists(context.policyFile),
82
+ detail: context.policyFile,
83
+ },
84
+ {
85
+ name: "verify.yaml",
86
+ ok: fileExists(context.repoVerifyFile),
87
+ detail: context.repoVerifyFile,
88
+ },
89
+ {
90
+ name: "project.json",
91
+ ok: fileExists(context.projectFile),
92
+ detail: context.projectFile,
93
+ },
94
+ );
95
+ }
96
+
97
+ return checks;
75
98
  }
76
99
 
77
100
  function probeNamedCliVersion(commandName, toolDisplayName) {
@@ -108,7 +131,7 @@ function normalizeDoctorHosts(hostOption) {
108
131
  }
109
132
 
110
133
  function collectCodexDoctorChecks(context, options = {}) {
111
- const checks = collectDoctorChecks(context);
134
+ const checks = collectDoctorChecks(context, options);
112
135
  if (options.codexHome) {
113
136
  checks.push({
114
137
  name: "codex installed plugin",
@@ -265,19 +288,57 @@ function isAffirmativeAnswer(answer) {
265
288
  }
266
289
 
267
290
  export async function confirmAutoExecution() {
268
- const readline = createInterface({
269
- input: process.stdin,
270
- output: process.stdout,
271
- });
291
+ const promptSession = createPromptSession();
272
292
 
273
293
  try {
274
- const answer = await readline.question("是否开始自动接续执行?输入 y / yes / 确认 继续,其它任意输入取消:");
294
+ const answer = await promptSession.question("是否开始自动接续执行?输入 y / yes / 确认 继续,其它任意输入取消:");
275
295
  return isAffirmativeAnswer(answer);
276
296
  } finally {
277
- readline.close();
297
+ promptSession.close();
298
+ }
299
+ }
300
+
301
+ export function shouldConfirmRepoRebuild(analysis, discovery) {
302
+ return analysis?.repoDecision?.action === "confirm_rebuild"
303
+ && discovery?.resolution?.repo?.exists !== false;
304
+ }
305
+
306
+ export async function confirmRepoConflictResolution(analysis) {
307
+ const decision = analysis?.repoDecision || {};
308
+ const promptSession = createPromptSession();
309
+
310
+ const promptText = [
311
+ "检测到当前项目与开发文档目标存在明显冲突:",
312
+ `- ${decision.reason || "分析结果认为当前项目更适合先确认处理方式。"}`,
313
+ "请选择后续动作:",
314
+ "1. 继续在当前项目上尝试接续",
315
+ "2. 清理当前项目内容后按文档目标重新开始(推荐)",
316
+ "3. 取消本次执行",
317
+ "请输入 1 / 2 / 3:",
318
+ ].join("\n");
319
+
320
+ try {
321
+ const answer = String(await promptSession.question(promptText) || "").trim();
322
+ if (["2", "重建", "rebuild"].includes(answer.toLowerCase ? answer.toLowerCase() : answer)) {
323
+ return "rebuild";
324
+ }
325
+ if (["1", "继续", "continue"].includes(answer.toLowerCase ? answer.toLowerCase() : answer)) {
326
+ return "continue";
327
+ }
328
+ return "cancel";
329
+ } finally {
330
+ promptSession.close();
278
331
  }
279
332
  }
280
333
 
334
+ export function renderRepoConflictStopMessage(analysis) {
335
+ return [
336
+ "当前项目与开发文档目标存在明显冲突,已暂停自动执行。",
337
+ analysis?.repoDecision?.reason ? `原因:${analysis.repoDecision.reason}` : "",
338
+ "请重新运行交互式 `npx helloloop` 进行选择,或显式追加 `--rebuild-existing` 后再执行。",
339
+ ].filter(Boolean).join("\n");
340
+ }
341
+
281
342
  export function renderAnalyzeStopMessage(reason) {
282
343
  return reason || "当前没有可自动执行的任务。";
283
344
  }
@@ -285,6 +346,7 @@ export function renderAnalyzeStopMessage(reason) {
285
346
  export function renderAutoRunSummary(context, backlog, results, options = {}) {
286
347
  const summary = summarizeBacklog(backlog);
287
348
  const execution = analyzeExecution(backlog, options);
349
+ const mainlineClosed = results.some((item) => item.kind === "mainline-complete");
288
350
  const lines = [
289
351
  "自动执行结果",
290
352
  "============",
@@ -316,7 +378,9 @@ export function renderAutoRunSummary(context, backlog, results, options = {}) {
316
378
  lines.push("");
317
379
  lines.push("结论:");
318
380
  if (execution.state === "done") {
319
- lines.push("- backlog 已全部完成");
381
+ lines.push(mainlineClosed
382
+ ? "- backlog 已全部完成,且主线终态复核通过"
383
+ : "- backlog 已全部完成");
320
384
  } else if (execution.blockedReason) {
321
385
  lines.push(`- 当前停止原因:${execution.blockedReason}`);
322
386
  } else {