helloloop 0.3.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 +157 -81
- package/hosts/claude/marketplace/plugins/helloloop/.claude-plugin/plugin.json +1 -1
- package/hosts/claude/marketplace/plugins/helloloop/commands/helloloop.md +13 -10
- package/hosts/claude/marketplace/plugins/helloloop/skills/helloloop/SKILL.md +9 -4
- package/hosts/gemini/extension/GEMINI.md +12 -7
- package/hosts/gemini/extension/commands/helloloop.toml +14 -10
- package/hosts/gemini/extension/gemini-extension.json +1 -1
- package/package.json +2 -2
- package/skills/helloloop/SKILL.md +16 -6
- 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 +50 -0
- package/src/discovery_prompt.mjs +2 -27
- 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 +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 +90 -0
- package/src/process.mjs +482 -39
- 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 +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 +0 -1
- package/templates/policy.template.json +27 -0
- package/templates/project.template.json +2 -0
- package/templates/task-review-output.schema.json +70 -0
|
@@ -9,7 +9,8 @@ description: 当用户希望 Codex 先分析仓库当前进度、生成确认单
|
|
|
9
9
|
|
|
10
10
|
## 强制入口规则
|
|
11
11
|
|
|
12
|
-
- 用户显式调用 `$helloloop` / `#helloloop` / `helloloop:helloloop` 时,默认必须优先执行 `npx helloloop` 或 `npx helloloop <PATH
|
|
12
|
+
- 用户显式调用 `$helloloop` / `#helloloop` / `helloloop:helloloop` 时,默认必须优先执行 `npx helloloop` 或 `npx helloloop <PATH>`;如果用户又明确指定了执行引擎,也允许使用 `npx helloloop codex|claude|gemini ...`。
|
|
13
|
+
- 用户没有明确指定执行引擎时,不允许由 skill 自行补成 `codex` / `claude` / `gemini`;必须让 `HelloLoop` 先完成引擎确认。
|
|
13
14
|
- 不允许在对话里手工模拟 `HelloLoop` 的分析、确认单、backlog 编排和自动续跑流程来代替 CLI。
|
|
14
15
|
- 只有在以下情况,才允许先停下来问用户而不是直接执行 CLI:
|
|
15
16
|
1. 用户既没有给路径,当前目录也无法判断项目仓库或开发文档
|
|
@@ -19,10 +20,12 @@ description: 当用户希望 Codex 先分析仓库当前进度、生成确认单
|
|
|
19
20
|
|
|
20
21
|
## `$helloloop` 的默认执行映射
|
|
21
22
|
|
|
22
|
-
- 当前目录已经是目标项目仓库或开发文档目录 → 先执行 `npx helloloop`
|
|
23
|
-
- 用户给了单一路径 → 先执行 `npx helloloop <PATH>`
|
|
23
|
+
- 当前目录已经是目标项目仓库或开发文档目录 → 先执行 `npx helloloop --host-context codex`
|
|
24
|
+
- 用户给了单一路径 → 先执行 `npx helloloop --host-context codex <PATH>`
|
|
24
25
|
- 用户明确只想先看分析和确认单 → 执行 `npx helloloop --dry-run`
|
|
25
26
|
- 用户明确要求跳过确认直接开始 → 执行 `npx helloloop -y`
|
|
27
|
+
- 用户未明确指定执行引擎 → 保持命令里不带引擎首参数,让 `HelloLoop` 先做引擎确认;当前宿主只作为推荐依据
|
|
28
|
+
- 用户明确指定执行引擎 → 保留该引擎首参数;如果在 `Codex` 宿主内要求改用 `Claude` / `Gemini`,先确认,不允许静默切换
|
|
26
29
|
- 用户在命令后附带了额外路径或自然语言要求 → 必须把这些附加内容一并传给主 CLI,不允许丢弃或手工改写
|
|
27
30
|
|
|
28
31
|
## 附加输入处理规则
|
|
@@ -57,7 +60,7 @@ description: 当用户希望 Codex 先分析仓库当前进度、生成确认单
|
|
|
57
60
|
2. 打开目标项目仓库目录,或者打开开发文档所在目录。
|
|
58
61
|
3. 运行 `npx helloloop` 或 `npx helloloop <PATH>`。
|
|
59
62
|
4. 命中 `$helloloop` 后,优先按上面的默认执行映射直接调用 CLI。
|
|
60
|
-
5. `HelloLoop`
|
|
63
|
+
5. `HelloLoop` 会先明确执行引擎,再自动分析并输出执行确认单。
|
|
61
64
|
6. 用户确认后,`HelloLoop` 才开始正式自动执行。
|
|
62
65
|
|
|
63
66
|
如果无法自动判断仓库路径或开发文档路径,就停下来提示用户补充;`--repo` 和 `--docs` 只作为显式覆盖选项使用。
|
|
@@ -67,14 +70,21 @@ description: 当用户希望 Codex 先分析仓库当前进度、生成确认单
|
|
|
67
70
|
- 代码是事实源,开发文档是目标源。
|
|
68
71
|
- `HelloLoop` 会先分析当前真实进度,再生成或刷新 `.helloloop/backlog.json`。
|
|
69
72
|
- 分析后会展示中文执行确认单,明确告知路径判断、语义理解、项目匹配、当前进度、待办任务、验证命令和执行边界。
|
|
70
|
-
- 用户确认后,默认会持续执行到 backlog
|
|
71
|
-
-
|
|
73
|
+
- 用户确认后,默认会持续执行到 backlog 清空且主线终态复核通过,或开发文档的最终目标完成且测试、验收通过,或遇到硬阻塞。
|
|
74
|
+
- 每个任务在执行引擎声称“完成”后,还必须再过一层任务完成复核;如果只是部分完成,继续当前主线任务,不直接结束。
|
|
75
|
+
- 用户需求明确且当前任务可直接完成时,必须一次性完成本轮应交付的全部工作;禁止做一半后用“如果你要”“是否继续”之类的话术停下,也禁止用客套套话收尾。
|
|
76
|
+
- 真正的代码分析与实现由本轮选中的 `codex` / `claude` / `gemini` CLI 原生完成。
|
|
77
|
+
- 如果当前引擎在运行中遇到 429、5xx、网络抖动、流中断或长时间卡死,必须优先按无人值守策略做同引擎自动恢复,不要中途停下来询问用户。
|
|
78
|
+
- 只有识别为 400 请求错误、登录/鉴权/订阅问题、本地 CLI 缺失或权限错误等硬阻塞时,才允许结束本轮自动执行。
|
|
72
79
|
- `$helloloop` 的职责是把用户请求路由到主 CLI 流程,而不是在对话里手工复刻一套平行流程。
|
|
73
80
|
|
|
74
81
|
## 核心命令
|
|
75
82
|
|
|
76
83
|
- `npx helloloop`
|
|
77
84
|
- `npx helloloop <PATH>`
|
|
85
|
+
- `npx helloloop codex`
|
|
86
|
+
- `npx helloloop claude <PATH>`
|
|
87
|
+
- `npx helloloop gemini <PATH> <补充说明>`
|
|
78
88
|
- `npx helloloop <PATH> <补充说明>`
|
|
79
89
|
- `npx helloloop --dry-run`
|
|
80
90
|
- `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
|
+
}
|