helloloop 0.3.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +4 -4
- package/README.md +194 -81
- package/hosts/claude/marketplace/plugins/helloloop/.claude-plugin/plugin.json +1 -1
- package/hosts/claude/marketplace/plugins/helloloop/commands/helloloop.md +17 -13
- package/hosts/claude/marketplace/plugins/helloloop/skills/helloloop/SKILL.md +12 -5
- package/hosts/gemini/extension/GEMINI.md +14 -7
- package/hosts/gemini/extension/commands/helloloop.toml +17 -12
- package/hosts/gemini/extension/gemini-extension.json +1 -1
- package/package.json +2 -2
- package/skills/helloloop/SKILL.md +18 -7
- package/src/analyze_confirmation.mjs +29 -5
- package/src/analyze_prompt.mjs +5 -1
- package/src/analyze_user_input.mjs +20 -2
- package/src/analyzer.mjs +130 -43
- package/src/cli.mjs +32 -492
- 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 +11 -14
- package/src/completion_review.mjs +243 -0
- package/src/config.mjs +51 -0
- package/src/discovery.mjs +21 -2
- package/src/discovery_prompt.mjs +2 -27
- package/src/email_notification.mjs +343 -0
- package/src/engine_metadata.mjs +79 -0
- package/src/engine_process_support.mjs +294 -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 +104 -0
- package/src/global_config.mjs +21 -0
- package/src/guardrails.mjs +15 -4
- package/src/install.mjs +6 -405
- 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 +138 -0
- package/src/process.mjs +567 -100
- package/src/prompt.mjs +9 -5
- package/src/prompt_session.mjs +40 -0
- package/src/runner.mjs +3 -341
- package/src/runner_execute_task.mjs +255 -0
- package/src/runner_execution_support.mjs +146 -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 +302 -0
- package/src/shell_invocation.mjs +16 -0
- package/templates/analysis-output.schema.json +0 -1
- package/templates/policy.template.json +25 -0
- package/templates/project.template.json +2 -0
- package/templates/task-review-output.schema.json +70 -0
|
@@ -1,17 +1,20 @@
|
|
|
1
|
-
description = "
|
|
1
|
+
description = "仅在用户显式调用 `/helloloop` 后,根据开发文档分析当前进度、生成确认单,并在确认后继续接续开发"
|
|
2
2
|
prompt = """
|
|
3
3
|
你现在正在执行 HelloLoop 的 Gemini CLI 原生工作流。
|
|
4
4
|
|
|
5
5
|
执行要求:
|
|
6
6
|
|
|
7
|
-
1.
|
|
8
|
-
2.
|
|
9
|
-
3.
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
7
|
+
1. 只有当用户显式调用 `/helloloop`,或明确要求“使用 HelloLoop / 走 HelloLoop 流程”时,才进入当前工作流;不要接管普通 Gemini 会话。
|
|
8
|
+
2. 自动识别目标仓库与开发文档;如果用户在命令后提供了参数,必须同时保留其中的显式路径和自然语言补充要求,不要靠固定关键词硬编码判断。
|
|
9
|
+
3. 当前宿主是 `Gemini`;如果用户未明确指定执行引擎,必须先确认本轮引擎,`Gemini` 只可作为推荐项,不允许自动选中;如果用户明确要求改用 `Codex` 或 `Claude`,必须先确认,不允许静默切换。
|
|
10
|
+
3. 如果当前目录没有明确开发文档,应先展示顶层文档文件、顶层目录和疑似项目目录,再询问用户开发文档路径。
|
|
11
|
+
4. 项目路径对外只有一个概念;如果用户给出的项目路径不存在,直接按新项目路径处理,不要再单独追问“新项目路径”。
|
|
12
|
+
5. 分析当前代码与开发文档之间的真实进度和偏差,并判断当前项目目录与开发文档目标是否匹配。
|
|
13
|
+
6. 在目标仓库根目录创建或刷新 `.helloloop/`,至少维护 `backlog.json`、`project.json`、`status.json`、`STATE.md` 与 `runs/`。
|
|
14
|
+
7. 在真正开发前,必须输出中文执行确认单,至少包含:
|
|
13
15
|
- 路径判断与判断依据
|
|
14
16
|
- 本次命令补充输入
|
|
17
|
+
- 执行引擎
|
|
15
18
|
- 需求语义理解
|
|
16
19
|
- 项目匹配判断
|
|
17
20
|
- 目标仓库
|
|
@@ -23,9 +26,11 @@ prompt = """
|
|
|
23
26
|
- 首个待执行任务
|
|
24
27
|
- 验证命令预览
|
|
25
28
|
- 自动执行停止条件
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
8. 如果当前项目目录已存在,但分析认为它与开发文档目标明显冲突,必须先明确提示用户,并确认是继续现有项目、还是清理后按文档重建。
|
|
30
|
+
9. 用户未确认前,不要开始正式修改代码。
|
|
31
|
+
10. 用户确认后,继续用当前确认的执行引擎原生工具推进 backlog;每个任务完成后还要再做任务完成复核,backlog 清空后还要再做主线终态复核,直到开发文档的最终目标完成,且相关测试、验证、验收都已通过;遇到硬阻塞或需要用户补充关键信息时再停下。
|
|
32
|
+
11. 如果当前引擎在运行中遇到 400、鉴权、余额、429、5xx、网络抖动、流中断或长时间卡死,必须优先按无人值守策略做同引擎“健康探测 + 条件恢复”,不要中途停下来询问用户,也不要自动切换引擎;只有自动恢复额度真正用尽时才停止。
|
|
33
|
+
12. 如果用户给了开发文档但找不到项目仓库,或给了项目路径但找不到开发文档,必须停下来询问。
|
|
34
|
+
13. 用户需求明确且当前任务可直接完成时,必须一次性完成本轮应交付的全部工作;禁止做一半后用“如果你要”“是否继续”之类的话术停下,也禁止用“希望这对你有帮助”等套话收尾。
|
|
35
|
+
14. Windows 上优先用安全的文件与 shell 操作;禁止危险删除、硬重置、强推和其他破坏性命令。
|
|
31
36
|
"""
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "helloloop",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "面向 Codex CLI、Claude Code、Gemini CLI 的多宿主开发工作流插件",
|
|
5
5
|
"author": "HelloLoop",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"templates"
|
|
31
31
|
],
|
|
32
32
|
"scripts": {
|
|
33
|
-
"test": "node --test tests/analyze_cli.test.mjs tests/analyze_intent_cli.test.mjs tests/cli_surface.test.mjs tests/install_script.test.mjs tests/process_shell.test.mjs tests/prompt_guardrails.test.mjs tests/ralph_loop.test.mjs tests/plugin_bundle.test.mjs"
|
|
33
|
+
"test": "node --test tests/analyze_cli.test.mjs tests/analyze_intent_cli.test.mjs tests/engine_selection_cli.test.mjs tests/cli_surface.test.mjs tests/cli_doctor_surface.test.mjs tests/host_lifecycle_integrity.test.mjs tests/host_single_host_integrity.test.mjs tests/install_script.test.mjs tests/mainline_continuation.test.mjs tests/multi_host_runtime.test.mjs tests/process_shell.test.mjs tests/prompt_guardrails.test.mjs tests/ralph_loop.test.mjs tests/runtime_recovery.test.mjs tests/plugin_bundle.test.mjs"
|
|
34
34
|
},
|
|
35
35
|
"engines": {
|
|
36
36
|
"node": ">=20"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: helloloop
|
|
3
|
-
description:
|
|
3
|
+
description: 仅当用户显式调用 `$helloloop` / `#helloloop` / `helloloop:helloloop`,或明确要求使用 HelloLoop 按开发文档持续接续开发时使用。
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# HelloLoop
|
|
@@ -9,7 +9,9 @@ description: 当用户希望 Codex 先分析仓库当前进度、生成确认单
|
|
|
9
9
|
|
|
10
10
|
## 强制入口规则
|
|
11
11
|
|
|
12
|
-
-
|
|
12
|
+
- 未显式调用 `helloloop`,且用户也没有明确要求“使用 HelloLoop / 使用 helloloop 插件 / 走 HelloLoop 流程”时,不允许接管普通 Codex 会话。
|
|
13
|
+
- 用户显式调用 `$helloloop` / `#helloloop` / `helloloop:helloloop` 时,默认必须优先执行 `npx helloloop` 或 `npx helloloop <PATH>`;如果用户又明确指定了执行引擎,也允许使用 `npx helloloop codex|claude|gemini ...`。
|
|
14
|
+
- 用户没有明确指定执行引擎时,不允许由 skill 自行补成 `codex` / `claude` / `gemini`;必须让 `HelloLoop` 先完成引擎确认。
|
|
13
15
|
- 不允许在对话里手工模拟 `HelloLoop` 的分析、确认单、backlog 编排和自动续跑流程来代替 CLI。
|
|
14
16
|
- 只有在以下情况,才允许先停下来问用户而不是直接执行 CLI:
|
|
15
17
|
1. 用户既没有给路径,当前目录也无法判断项目仓库或开发文档
|
|
@@ -19,10 +21,12 @@ description: 当用户希望 Codex 先分析仓库当前进度、生成确认单
|
|
|
19
21
|
|
|
20
22
|
## `$helloloop` 的默认执行映射
|
|
21
23
|
|
|
22
|
-
- 当前目录已经是目标项目仓库或开发文档目录 → 先执行 `npx helloloop`
|
|
23
|
-
- 用户给了单一路径 → 先执行 `npx helloloop <PATH>`
|
|
24
|
+
- 当前目录已经是目标项目仓库或开发文档目录 → 先执行 `npx helloloop --host-context codex`
|
|
25
|
+
- 用户给了单一路径 → 先执行 `npx helloloop --host-context codex <PATH>`
|
|
24
26
|
- 用户明确只想先看分析和确认单 → 执行 `npx helloloop --dry-run`
|
|
25
27
|
- 用户明确要求跳过确认直接开始 → 执行 `npx helloloop -y`
|
|
28
|
+
- 用户未明确指定执行引擎 → 保持命令里不带引擎首参数,让 `HelloLoop` 先做引擎确认;当前宿主只作为推荐依据
|
|
29
|
+
- 用户明确指定执行引擎 → 保留该引擎首参数;如果在 `Codex` 宿主内要求改用 `Claude` / `Gemini`,先确认,不允许静默切换
|
|
26
30
|
- 用户在命令后附带了额外路径或自然语言要求 → 必须把这些附加内容一并传给主 CLI,不允许丢弃或手工改写
|
|
27
31
|
|
|
28
32
|
## 附加输入处理规则
|
|
@@ -57,7 +61,7 @@ description: 当用户希望 Codex 先分析仓库当前进度、生成确认单
|
|
|
57
61
|
2. 打开目标项目仓库目录,或者打开开发文档所在目录。
|
|
58
62
|
3. 运行 `npx helloloop` 或 `npx helloloop <PATH>`。
|
|
59
63
|
4. 命中 `$helloloop` 后,优先按上面的默认执行映射直接调用 CLI。
|
|
60
|
-
5. `HelloLoop`
|
|
64
|
+
5. `HelloLoop` 会先明确执行引擎,再自动分析并输出执行确认单。
|
|
61
65
|
6. 用户确认后,`HelloLoop` 才开始正式自动执行。
|
|
62
66
|
|
|
63
67
|
如果无法自动判断仓库路径或开发文档路径,就停下来提示用户补充;`--repo` 和 `--docs` 只作为显式覆盖选项使用。
|
|
@@ -67,14 +71,21 @@ description: 当用户希望 Codex 先分析仓库当前进度、生成确认单
|
|
|
67
71
|
- 代码是事实源,开发文档是目标源。
|
|
68
72
|
- `HelloLoop` 会先分析当前真实进度,再生成或刷新 `.helloloop/backlog.json`。
|
|
69
73
|
- 分析后会展示中文执行确认单,明确告知路径判断、语义理解、项目匹配、当前进度、待办任务、验证命令和执行边界。
|
|
70
|
-
- 用户确认后,默认会持续执行到 backlog
|
|
71
|
-
-
|
|
74
|
+
- 用户确认后,默认会持续执行到 backlog 清空且主线终态复核通过,或开发文档的最终目标完成且测试、验收通过,或遇到硬阻塞。
|
|
75
|
+
- 每个任务在执行引擎声称“完成”后,还必须再过一层任务完成复核;如果只是部分完成,继续当前主线任务,不直接结束。
|
|
76
|
+
- 用户需求明确且当前任务可直接完成时,必须一次性完成本轮应交付的全部工作;禁止做一半后用“如果你要”“是否继续”之类的话术停下,也禁止用客套套话收尾。
|
|
77
|
+
- 真正的代码分析与实现由本轮选中的 `codex` / `claude` / `gemini` CLI 原生完成。
|
|
78
|
+
- 如果当前引擎在运行中遇到 400、鉴权、余额、429、5xx、网络抖动、流中断或长时间卡死,必须优先按无人值守策略做同引擎“健康探测 + 条件恢复”,不要中途停下来询问用户,也不要自动切换引擎。
|
|
79
|
+
- 只有当硬阻塞或软阻塞的自动恢复额度真正用尽时,才允许结束本轮自动执行;若用户已在 `~/.helloloop/settings.json` 配置邮箱通知,则结束前发送告警邮件。
|
|
72
80
|
- `$helloloop` 的职责是把用户请求路由到主 CLI 流程,而不是在对话里手工复刻一套平行流程。
|
|
73
81
|
|
|
74
82
|
## 核心命令
|
|
75
83
|
|
|
76
84
|
- `npx helloloop`
|
|
77
85
|
- `npx helloloop <PATH>`
|
|
86
|
+
- `npx helloloop codex`
|
|
87
|
+
- `npx helloloop claude <PATH>`
|
|
88
|
+
- `npx helloloop gemini <PATH> <补充说明>`
|
|
78
89
|
- `npx helloloop <PATH> <补充说明>`
|
|
79
90
|
- `npx helloloop --dry-run`
|
|
80
91
|
- `npx helloloop -y`
|
|
@@ -3,6 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import { renderInputIssueLines, renderUserIntentLines } from "./analyze_user_input.mjs";
|
|
4
4
|
import { analyzeExecution, summarizeBacklog } from "./backlog.mjs";
|
|
5
5
|
import { loadPolicy, loadVerifyCommands } from "./config.mjs";
|
|
6
|
+
import { getEngineDisplayName } from "./engine_metadata.mjs";
|
|
6
7
|
|
|
7
8
|
function toDisplayPath(repoRoot, targetPath) {
|
|
8
9
|
const absolutePath = path.resolve(targetPath);
|
|
@@ -133,15 +134,35 @@ function renderRepoDecisionLines(analysis) {
|
|
|
133
134
|
];
|
|
134
135
|
}
|
|
135
136
|
|
|
137
|
+
function renderEngineResolutionLines(engineResolution) {
|
|
138
|
+
if (!engineResolution?.engine) {
|
|
139
|
+
return [];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const availableDisplay = Array.isArray(engineResolution.availableEngines) && engineResolution.availableEngines.length
|
|
143
|
+
? engineResolution.availableEngines.map((engine) => getEngineDisplayName(engine)).join("、")
|
|
144
|
+
: "未检测到其他可用引擎";
|
|
145
|
+
const basisText = Array.isArray(engineResolution.basis) && engineResolution.basis.length
|
|
146
|
+
? engineResolution.basis.join(";")
|
|
147
|
+
: "无";
|
|
148
|
+
|
|
149
|
+
return [
|
|
150
|
+
"执行引擎:",
|
|
151
|
+
`- 当前宿主:${engineResolution.hostDisplayName || "终端"}`,
|
|
152
|
+
`- 本次引擎:${engineResolution.displayName || getEngineDisplayName(engineResolution.engine)}`,
|
|
153
|
+
`- 选择来源:${engineResolution.sourceLabel || "自动判断"}`,
|
|
154
|
+
`- 选择依据:${basisText}`,
|
|
155
|
+
`- 当前可用:${availableDisplay}`,
|
|
156
|
+
"- 故障处理:运行中若遇到 429、5xx、网络抖动、流中断或长时间卡死,HelloLoop 会优先按无人值守策略做同引擎自动恢复;只有明确硬阻塞才停止",
|
|
157
|
+
];
|
|
158
|
+
}
|
|
159
|
+
|
|
136
160
|
export function resolveAutoRunMaxTasks(backlog, options = {}) {
|
|
137
161
|
const explicitMaxTasks = Number(options.maxTasks);
|
|
138
162
|
if (Number.isFinite(explicitMaxTasks) && explicitMaxTasks > 0) {
|
|
139
163
|
return explicitMaxTasks;
|
|
140
164
|
}
|
|
141
|
-
|
|
142
|
-
const summary = summarizeBacklog(backlog);
|
|
143
|
-
const pendingTotal = summary.pending + summary.inProgress;
|
|
144
|
-
return Math.max(1, pendingTotal);
|
|
165
|
+
return 0;
|
|
145
166
|
}
|
|
146
167
|
|
|
147
168
|
export function renderAnalyzeConfirmation(context, analysis, backlog, options = {}, discovery = {}) {
|
|
@@ -162,6 +183,7 @@ export function renderAnalyzeConfirmation(context, analysis, backlog, options =
|
|
|
162
183
|
...renderResolutionLines(discovery, context, docsDisplay),
|
|
163
184
|
...(userIntentLines.length ? ["", "本次命令补充输入:", ...formatList(userIntentLines)] : []),
|
|
164
185
|
...(inputIssueLines.length ? ["", "输入提示:", ...inputIssueLines] : []),
|
|
186
|
+
...(renderEngineResolutionLines(options.engineResolution).length ? ["", ...renderEngineResolutionLines(options.engineResolution)] : []),
|
|
165
187
|
...(renderRequestInterpretationLines(analysis).length ? ["", ...renderRequestInterpretationLines(analysis)] : []),
|
|
166
188
|
...(renderRepoDecisionLines(analysis).length ? ["", ...renderRepoDecisionLines(analysis)] : []),
|
|
167
189
|
"",
|
|
@@ -192,7 +214,9 @@ export function renderAnalyzeConfirmation(context, analysis, backlog, options =
|
|
|
192
214
|
? `- 当前阻塞:${execution.blockedReason}`
|
|
193
215
|
: "- 当前阻塞:无",
|
|
194
216
|
"- 偏差修正:按 backlog 优先级执行;如果分析识别出偏差修正任务,会先收口再继续后续开发",
|
|
195
|
-
|
|
217
|
+
autoRunMaxTasks > 0
|
|
218
|
+
? `- 自动推进:最多 ${autoRunMaxTasks} 个任务;若主线终态复核仍发现缺口,则到达上限后暂停`
|
|
219
|
+
: "- 自动推进:持续执行,直到 backlog 清空且主线终态复核通过,或遇到硬阻塞",
|
|
196
220
|
`- 单任务重试:每种策略最多 ${policy.maxTaskAttempts} 次,共 ${policy.maxTaskStrategies} 轮策略`,
|
|
197
221
|
"",
|
|
198
222
|
"待执行任务预览:",
|
package/src/analyze_prompt.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import { renderUserIntentLines } from "./analyze_user_input.mjs";
|
|
|
3
3
|
import {
|
|
4
4
|
hasCustomProjectConstraints,
|
|
5
5
|
listMandatoryGuardrails,
|
|
6
|
+
listMandatoryEngineeringPrinciples,
|
|
6
7
|
resolveProjectConstraints,
|
|
7
8
|
} from "./guardrails.mjs";
|
|
8
9
|
|
|
@@ -40,6 +41,7 @@ export function buildAnalysisPrompt({
|
|
|
40
41
|
userIntent = {},
|
|
41
42
|
}) {
|
|
42
43
|
const mandatoryGuardrails = listMandatoryGuardrails();
|
|
44
|
+
const mandatoryEngineeringPrinciples = listMandatoryEngineeringPrinciples();
|
|
43
45
|
const effectiveConstraints = resolveProjectConstraints(existingProjectConstraints);
|
|
44
46
|
const usingFallbackConstraints = !hasCustomProjectConstraints(existingProjectConstraints);
|
|
45
47
|
const userIntentLines = renderUserIntentLines(userIntent);
|
|
@@ -62,6 +64,7 @@ export function buildAnalysisPrompt({
|
|
|
62
64
|
listSection("本次命令补充输入", userIntentLines),
|
|
63
65
|
existingStateText ? section("已有状态摘要", existingStateText) : "",
|
|
64
66
|
listSection("内建安全底线", mandatoryGuardrails),
|
|
67
|
+
listSection("强制编码与产出基线", mandatoryEngineeringPrinciples),
|
|
65
68
|
listSection(usingFallbackConstraints ? "默认工程约束(文档未明确时也必须遵守)" : "已有项目约束", effectiveConstraints),
|
|
66
69
|
existingBacklogText ? section("已有 backlog(供参考,可重组)", existingBacklogText) : "",
|
|
67
70
|
section("文档内容摘录", renderDocPackets(docPackets)),
|
|
@@ -71,7 +74,7 @@ export function buildAnalysisPrompt({
|
|
|
71
74
|
"3. `summary.implemented` 写已确认完成的关键能力。",
|
|
72
75
|
"4. `summary.remaining` 写尚未完成的关键缺口。",
|
|
73
76
|
"5. `summary.nextAction` 写最合理的下一步。",
|
|
74
|
-
"6. `tasks` 总数控制在
|
|
77
|
+
"6. `tasks` 总数控制在 0 到 12 个之间;如果最终目标已经完成且没有剩余缺口,允许输出空数组。",
|
|
75
78
|
"7. 每个任务必须包含:id、title、status、priority、risk、goal、docs、paths、acceptance。",
|
|
76
79
|
"8. `status` 只能是 `done`、`pending`、`blocked`,不要输出 `in_progress` 或 `failed`。",
|
|
77
80
|
"9. `docs` 必须引用本次文档入口中的相关路径;`paths` 必须写当前仓库内的实际目录或文件模式。",
|
|
@@ -82,6 +85,7 @@ export function buildAnalysisPrompt({
|
|
|
82
85
|
"14. 当当前项目目录已存在,但代码结构/产品方向与开发文档目标明显冲突,且更合理的路径是清理后重建时,`repoDecision.compatibility` 设为 `conflict`,`repoDecision.action` 设为 `confirm_rebuild`。",
|
|
83
86
|
"15. 当当前项目目录与开发文档目标一致或可以直接接续时,`repoDecision.compatibility` 设为 `compatible`,`repoDecision.action` 设为 `continue_existing`。",
|
|
84
87
|
"16. 当本次项目目录原本不存在,或文档目标明显是从零开始新建项目时,`repoDecision.action` 可设为 `start_new`。",
|
|
88
|
+
"17. 只有在文档最终目标、关键能力与验收范围都已闭合时,才能输出空 `tasks`;此时 `summary.remaining` 也必须为空。",
|
|
85
89
|
].join("\n")),
|
|
86
90
|
].filter(Boolean).join("\n");
|
|
87
91
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
|
|
3
3
|
import { classifyExplicitPath, pathExists, resolveAbsolute } from "./discovery_paths.mjs";
|
|
4
|
+
import { normalizeEngineName } from "./engine_metadata.mjs";
|
|
4
5
|
|
|
5
6
|
const DOC_SUFFIX = new Set([
|
|
6
7
|
".md",
|
|
@@ -145,7 +146,8 @@ export function normalizeAnalyzeOptions(rawOptions = {}, cwd = process.cwd()) {
|
|
|
145
146
|
requiredDocs: Array.isArray(rawOptions.requiredDocs) ? [...rawOptions.requiredDocs] : [],
|
|
146
147
|
constraints: Array.isArray(rawOptions.constraints) ? [...rawOptions.constraints] : [],
|
|
147
148
|
};
|
|
148
|
-
const
|
|
149
|
+
const rawPositionals = Array.isArray(rawOptions.positionalArgs) ? rawOptions.positionalArgs : [];
|
|
150
|
+
const positionals = [...rawPositionals];
|
|
149
151
|
const explicitRefs = [];
|
|
150
152
|
const requestTokens = [];
|
|
151
153
|
const issues = {
|
|
@@ -160,6 +162,18 @@ export function normalizeAnalyzeOptions(rawOptions = {}, cwd = process.cwd()) {
|
|
|
160
162
|
let repoRoot = options.repoRoot ? resolveAbsolute(options.repoRoot, cwd) : "";
|
|
161
163
|
let inputPath = options.inputPath ? resolveAbsolute(options.inputPath, cwd) : "";
|
|
162
164
|
let allowNewRepoRoot = Boolean(options.allowNewRepoRoot);
|
|
165
|
+
let engine = normalizeEngineName(options.engine);
|
|
166
|
+
let engineSource = options.engineSource || (engine ? "flag" : "");
|
|
167
|
+
|
|
168
|
+
if (!engine && positionals.length) {
|
|
169
|
+
const firstToken = String(positionals[0] || "").trim();
|
|
170
|
+
const leadingEngine = normalizeEngineName(firstToken);
|
|
171
|
+
if (leadingEngine && firstToken.toLowerCase() === leadingEngine) {
|
|
172
|
+
engine = leadingEngine;
|
|
173
|
+
engineSource = "leading_positional";
|
|
174
|
+
positionals.shift();
|
|
175
|
+
}
|
|
176
|
+
}
|
|
163
177
|
|
|
164
178
|
if (docsPath) {
|
|
165
179
|
ensureSelectionSource(selectionSources, "docs", rawOptions.selectionSources?.docs || "flag");
|
|
@@ -246,15 +260,19 @@ export function normalizeAnalyzeOptions(rawOptions = {}, cwd = process.cwd()) {
|
|
|
246
260
|
options.repoRoot = repoRoot;
|
|
247
261
|
options.inputPath = inputPath;
|
|
248
262
|
options.allowNewRepoRoot = allowNewRepoRoot;
|
|
263
|
+
options.engine = engine;
|
|
264
|
+
options.engineSource = engineSource;
|
|
249
265
|
options.selectionSources = selectionSources;
|
|
250
266
|
options.userRequestText = userRequestText;
|
|
251
267
|
options.inputIssues = issues;
|
|
252
268
|
options.userIntent = {
|
|
253
|
-
rawPositionals
|
|
269
|
+
rawPositionals,
|
|
254
270
|
explicitRefs,
|
|
255
271
|
requestText: userRequestText,
|
|
256
272
|
issues,
|
|
257
273
|
selectionSources,
|
|
274
|
+
explicitEngine: engine,
|
|
275
|
+
explicitEngineSource: engineSource,
|
|
258
276
|
};
|
|
259
277
|
|
|
260
278
|
return options;
|
package/src/analyzer.mjs
CHANGED
|
@@ -6,7 +6,11 @@ import { loadPolicy, loadProjectConfig, scaffoldIfMissing, writeStateMarkdown, w
|
|
|
6
6
|
import { createContext } from "./context.mjs";
|
|
7
7
|
import { discoverWorkspace } from "./discovery.mjs";
|
|
8
8
|
import { readDocumentPackets } from "./doc_loader.mjs";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
rememberEngineSelection,
|
|
11
|
+
resolveEngineSelection,
|
|
12
|
+
} from "./engine_selection.mjs";
|
|
13
|
+
import { runEngineTask } from "./process.mjs";
|
|
10
14
|
import { buildAnalysisPrompt } from "./analyze_prompt.mjs";
|
|
11
15
|
|
|
12
16
|
function renderAnalysisState(context, backlog, analysis) {
|
|
@@ -45,14 +49,20 @@ function sanitizeTask(task) {
|
|
|
45
49
|
}
|
|
46
50
|
|
|
47
51
|
function normalizeAnalysisPayload(payload, docsEntries) {
|
|
52
|
+
const summary = {
|
|
53
|
+
currentState: String(payload?.summary?.currentState || "").trim() || "已完成接续分析。",
|
|
54
|
+
implemented: Array.isArray(payload?.summary?.implemented) ? payload.summary.implemented.map((item) => String(item || "").trim()).filter(Boolean) : [],
|
|
55
|
+
remaining: Array.isArray(payload?.summary?.remaining) ? payload.summary.remaining.map((item) => String(item || "").trim()).filter(Boolean) : [],
|
|
56
|
+
nextAction: String(payload?.summary?.nextAction || "").trim() || "查看下一任务。",
|
|
57
|
+
};
|
|
48
58
|
const tasks = Array.isArray(payload.tasks)
|
|
49
59
|
? payload.tasks.map((task) => sanitizeTask(task)).filter((task) => (
|
|
50
60
|
task.id && task.title && task.goal && task.acceptance.length
|
|
51
61
|
))
|
|
52
62
|
: [];
|
|
53
63
|
|
|
54
|
-
if (!tasks.length) {
|
|
55
|
-
throw new Error("
|
|
64
|
+
if (!tasks.length && summary.remaining.length) {
|
|
65
|
+
throw new Error("分析结果无效:仍存在剩余工作,但未生成可用任务。");
|
|
56
66
|
}
|
|
57
67
|
|
|
58
68
|
const requestInterpretation = payload?.requestInterpretation && typeof payload.requestInterpretation === "object"
|
|
@@ -80,12 +90,7 @@ function normalizeAnalysisPayload(payload, docsEntries) {
|
|
|
80
90
|
|
|
81
91
|
return {
|
|
82
92
|
project: String(payload.project || "").trim() || "helloloop-project",
|
|
83
|
-
summary
|
|
84
|
-
currentState: String(payload?.summary?.currentState || "").trim() || "已完成接续分析。",
|
|
85
|
-
implemented: Array.isArray(payload?.summary?.implemented) ? payload.summary.implemented.map((item) => String(item || "").trim()).filter(Boolean) : [],
|
|
86
|
-
remaining: Array.isArray(payload?.summary?.remaining) ? payload.summary.remaining.map((item) => String(item || "").trim()).filter(Boolean) : [],
|
|
87
|
-
nextAction: String(payload?.summary?.nextAction || "").trim() || "查看下一任务。",
|
|
88
|
-
},
|
|
93
|
+
summary,
|
|
89
94
|
constraints: Array.isArray(payload.constraints)
|
|
90
95
|
? payload.constraints.map((item) => String(item || "").trim()).filter(Boolean)
|
|
91
96
|
: [],
|
|
@@ -104,7 +109,7 @@ function normalizeAnalysisPayload(payload, docsEntries) {
|
|
|
104
109
|
};
|
|
105
110
|
}
|
|
106
111
|
|
|
107
|
-
function buildAnalysisSummaryText(context, analysis, backlog) {
|
|
112
|
+
function buildAnalysisSummaryText(context, analysis, backlog, engineResolution) {
|
|
108
113
|
const summary = summarizeBacklog(backlog);
|
|
109
114
|
const nextTask = selectNextTask(backlog);
|
|
110
115
|
|
|
@@ -112,6 +117,7 @@ function buildAnalysisSummaryText(context, analysis, backlog) {
|
|
|
112
117
|
"HelloLoop 已完成接续分析。",
|
|
113
118
|
`项目仓库:${context.repoRoot}`,
|
|
114
119
|
`开发文档:${analysis.requiredDocs.join(", ")}`,
|
|
120
|
+
`执行引擎:${engineResolution?.displayName || "未记录"}`,
|
|
115
121
|
"",
|
|
116
122
|
"当前进度:",
|
|
117
123
|
analysis.summary.currentState,
|
|
@@ -125,31 +131,27 @@ function buildAnalysisSummaryText(context, analysis, backlog) {
|
|
|
125
131
|
].join("\n");
|
|
126
132
|
}
|
|
127
133
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
134
|
+
async function analyzeResolvedWorkspace(context, discovery, options = {}) {
|
|
135
|
+
scaffoldIfMissing(context);
|
|
136
|
+
const policy = loadPolicy(context);
|
|
137
|
+
let engineResolution = options.engineResolution?.ok
|
|
138
|
+
? options.engineResolution
|
|
139
|
+
: await resolveEngineSelection({
|
|
140
|
+
context,
|
|
141
|
+
policy,
|
|
142
|
+
options,
|
|
143
|
+
interactive: !options.yes,
|
|
144
|
+
});
|
|
145
|
+
if (!engineResolution.ok) {
|
|
139
146
|
return {
|
|
140
147
|
ok: false,
|
|
141
|
-
code:
|
|
142
|
-
summary:
|
|
148
|
+
code: engineResolution.code,
|
|
149
|
+
summary: engineResolution.message,
|
|
143
150
|
discovery,
|
|
151
|
+
engineResolution,
|
|
144
152
|
};
|
|
145
153
|
}
|
|
146
154
|
|
|
147
|
-
const context = createContext({
|
|
148
|
-
repoRoot: discovery.repoRoot,
|
|
149
|
-
configDirName: options.configDirName,
|
|
150
|
-
});
|
|
151
|
-
scaffoldIfMissing(context);
|
|
152
|
-
|
|
153
155
|
const existingProjectConfig = loadProjectConfig(context);
|
|
154
156
|
const existingStateText = readTextIfExists(context.stateFile, "");
|
|
155
157
|
const existingBacklogText = readTextIfExists(context.backlogFile, "");
|
|
@@ -170,38 +172,39 @@ export async function analyzeWorkspace(options = {}) {
|
|
|
170
172
|
});
|
|
171
173
|
|
|
172
174
|
const runDir = path.join(context.runsDir, `${nowIso().replaceAll(":", "-").replaceAll(".", "-")}-analysis`);
|
|
173
|
-
const policy = loadPolicy(context);
|
|
174
175
|
const schemaFile = path.join(context.templatesDir, "analysis-output.schema.json");
|
|
175
|
-
|
|
176
|
+
let analysisResult = await runEngineTask({
|
|
177
|
+
engine: engineResolution.engine,
|
|
176
178
|
context,
|
|
177
179
|
prompt,
|
|
178
180
|
runDir,
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
sandbox: policy.codex.sandbox,
|
|
182
|
-
dangerouslyBypassSandbox: policy.codex.dangerouslyBypassSandbox,
|
|
181
|
+
policy,
|
|
182
|
+
executionMode: "analyze",
|
|
183
183
|
outputSchemaFile: schemaFile,
|
|
184
|
-
outputPrefix:
|
|
185
|
-
jsonOutput: false,
|
|
184
|
+
outputPrefix: `${engineResolution.engine}-analysis`,
|
|
186
185
|
skipGitRepoCheck: true,
|
|
187
186
|
});
|
|
188
187
|
|
|
189
|
-
if (!
|
|
188
|
+
if (!analysisResult.ok) {
|
|
190
189
|
return {
|
|
191
190
|
ok: false,
|
|
192
191
|
code: "analysis_failed",
|
|
193
|
-
summary:
|
|
192
|
+
summary: analysisResult.stderr || analysisResult.stdout || `${engineResolution.displayName} 接续分析失败。`,
|
|
193
|
+
engineResolution,
|
|
194
|
+
discovery,
|
|
194
195
|
};
|
|
195
196
|
}
|
|
196
197
|
|
|
197
198
|
let payload;
|
|
198
199
|
try {
|
|
199
|
-
payload = JSON.parse(
|
|
200
|
+
payload = JSON.parse(analysisResult.finalMessage);
|
|
200
201
|
} catch (error) {
|
|
201
202
|
return {
|
|
202
203
|
ok: false,
|
|
203
204
|
code: "invalid_analysis_json",
|
|
204
|
-
summary:
|
|
205
|
+
summary: `${engineResolution.displayName} 分析结果无法解析为 JSON:${String(error?.message || error || "")}`,
|
|
206
|
+
engineResolution,
|
|
207
|
+
discovery,
|
|
205
208
|
};
|
|
206
209
|
}
|
|
207
210
|
|
|
@@ -216,11 +219,14 @@ export async function analyzeWorkspace(options = {}) {
|
|
|
216
219
|
const projectConfig = {
|
|
217
220
|
requiredDocs: analysis.requiredDocs,
|
|
218
221
|
constraints: analysis.constraints.length ? analysis.constraints : existingProjectConfig.constraints,
|
|
222
|
+
defaultEngine: existingProjectConfig.defaultEngine,
|
|
223
|
+
lastSelectedEngine: engineResolution.engine,
|
|
219
224
|
planner: existingProjectConfig.planner,
|
|
220
225
|
};
|
|
221
226
|
|
|
222
227
|
writeJson(context.backlogFile, backlog);
|
|
223
228
|
writeJson(context.projectFile, projectConfig);
|
|
229
|
+
rememberEngineSelection(context, engineResolution, options);
|
|
224
230
|
writeStateMarkdown(context, renderAnalysisState(context, backlog, analysis));
|
|
225
231
|
writeStatus(context, {
|
|
226
232
|
ok: true,
|
|
@@ -231,16 +237,97 @@ export async function analyzeWorkspace(options = {}) {
|
|
|
231
237
|
summary: summarizeBacklog(backlog),
|
|
232
238
|
message: analysis.summary.currentState,
|
|
233
239
|
});
|
|
234
|
-
writeText(path.join(runDir, "analysis-summary.txt"), buildAnalysisSummaryText(context, analysis, backlog));
|
|
240
|
+
writeText(path.join(runDir, "analysis-summary.txt"), buildAnalysisSummaryText(context, analysis, backlog, engineResolution));
|
|
235
241
|
|
|
236
242
|
return {
|
|
237
243
|
ok: true,
|
|
238
244
|
code: "analyzed",
|
|
239
245
|
context,
|
|
240
246
|
runDir,
|
|
247
|
+
engineResolution,
|
|
241
248
|
analysis,
|
|
242
249
|
backlog,
|
|
243
|
-
summary: buildAnalysisSummaryText(context, analysis, backlog),
|
|
250
|
+
summary: buildAnalysisSummaryText(context, analysis, backlog, engineResolution),
|
|
244
251
|
discovery,
|
|
245
252
|
};
|
|
246
253
|
}
|
|
254
|
+
|
|
255
|
+
function buildCurrentWorkspaceDiscovery(context, docsEntries) {
|
|
256
|
+
return {
|
|
257
|
+
ok: true,
|
|
258
|
+
repoRoot: context.repoRoot,
|
|
259
|
+
docsEntries,
|
|
260
|
+
resolvedDocs: docsEntries,
|
|
261
|
+
resolution: {
|
|
262
|
+
repo: {
|
|
263
|
+
source: "current_repo",
|
|
264
|
+
sourceLabel: "当前项目",
|
|
265
|
+
confidence: "high",
|
|
266
|
+
confidenceLabel: "高",
|
|
267
|
+
path: context.repoRoot,
|
|
268
|
+
exists: true,
|
|
269
|
+
basis: [
|
|
270
|
+
"已在当前项目基础上执行主线终态复核。",
|
|
271
|
+
],
|
|
272
|
+
},
|
|
273
|
+
docs: {
|
|
274
|
+
source: "existing_state",
|
|
275
|
+
sourceLabel: "已有 .helloloop 配置",
|
|
276
|
+
confidence: "high",
|
|
277
|
+
confidenceLabel: "高",
|
|
278
|
+
entries: docsEntries,
|
|
279
|
+
basis: [
|
|
280
|
+
"已复用 `.helloloop/project.json` 中记录的 requiredDocs。",
|
|
281
|
+
],
|
|
282
|
+
},
|
|
283
|
+
},
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export async function reanalyzeCurrentWorkspace(context, options = {}) {
|
|
288
|
+
const existingProjectConfig = loadProjectConfig(context);
|
|
289
|
+
const docsEntries = Array.isArray(options.requiredDocs) && options.requiredDocs.length
|
|
290
|
+
? options.requiredDocs
|
|
291
|
+
: existingProjectConfig.requiredDocs;
|
|
292
|
+
|
|
293
|
+
if (!docsEntries.length) {
|
|
294
|
+
return {
|
|
295
|
+
ok: false,
|
|
296
|
+
code: "missing_docs",
|
|
297
|
+
summary: "当前 `.helloloop/project.json` 未记录 requiredDocs,无法执行主线终态复核。",
|
|
298
|
+
discovery: null,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return analyzeResolvedWorkspace(
|
|
303
|
+
context,
|
|
304
|
+
buildCurrentWorkspaceDiscovery(context, docsEntries),
|
|
305
|
+
options,
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export async function analyzeWorkspace(options = {}) {
|
|
310
|
+
const discovery = discoverWorkspace({
|
|
311
|
+
cwd: options.cwd,
|
|
312
|
+
inputPath: options.inputPath,
|
|
313
|
+
repoRoot: options.repoRoot,
|
|
314
|
+
docsPath: options.docsPath,
|
|
315
|
+
configDirName: options.configDirName,
|
|
316
|
+
allowNewRepoRoot: options.allowNewRepoRoot,
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
if (!discovery.ok) {
|
|
320
|
+
return {
|
|
321
|
+
ok: false,
|
|
322
|
+
code: discovery.code,
|
|
323
|
+
summary: discovery.message,
|
|
324
|
+
discovery,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const context = createContext({
|
|
329
|
+
repoRoot: discovery.repoRoot,
|
|
330
|
+
configDirName: options.configDirName,
|
|
331
|
+
});
|
|
332
|
+
return analyzeResolvedWorkspace(context, discovery, options);
|
|
333
|
+
}
|