helloloop 0.1.1 → 0.2.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 +14 -0
- package/.codex-plugin/plugin.json +6 -6
- package/LICENSE +176 -0
- package/README.md +323 -146
- package/hosts/claude/marketplace/.claude-plugin/marketplace.json +14 -0
- package/hosts/claude/marketplace/plugins/helloloop/.claude-plugin/plugin.json +9 -0
- package/hosts/claude/marketplace/plugins/helloloop/commands/helloloop.md +28 -0
- package/hosts/claude/marketplace/plugins/helloloop/skills/helloloop/SKILL.md +38 -0
- package/hosts/gemini/extension/GEMINI.md +29 -0
- package/hosts/gemini/extension/commands/helloloop.toml +24 -0
- package/hosts/gemini/extension/gemini-extension.json +13 -0
- package/package.json +9 -3
- package/skills/helloloop/SKILL.md +59 -31
- package/src/analyze_confirmation.mjs +128 -0
- package/src/analyze_prompt.mjs +75 -0
- package/src/analyzer.mjs +208 -0
- package/src/cli.mjs +148 -91
- package/src/cli_support.mjs +307 -0
- package/src/discovery.mjs +155 -0
- package/src/discovery_inference.mjs +220 -0
- package/src/discovery_paths.mjs +166 -0
- package/src/guardrails.mjs +59 -0
- package/src/install.mjs +142 -28
- package/src/process.mjs +31 -129
- package/src/prompt.mjs +10 -12
- package/src/shell_invocation.mjs +225 -0
- package/templates/analysis-output.schema.json +144 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# HelloLoop for Gemini CLI
|
|
2
|
+
|
|
3
|
+
你正在使用 `HelloLoop` 的 Gemini CLI 原生扩展。
|
|
4
|
+
|
|
5
|
+
## 工作目标
|
|
6
|
+
|
|
7
|
+
基于开发文档持续推进仓库开发,先分析真实进度,再在确认后自动接续开发、测试和验收。
|
|
8
|
+
|
|
9
|
+
## 默认流程
|
|
10
|
+
|
|
11
|
+
1. 自动识别项目仓库与开发文档
|
|
12
|
+
2. 分析当前代码与开发文档的差距
|
|
13
|
+
3. 在目标仓库根目录创建或刷新 `.helloloop/`
|
|
14
|
+
4. 输出中文执行确认单
|
|
15
|
+
5. 用户确认后,再继续执行后续开发任务
|
|
16
|
+
|
|
17
|
+
## 强制规则
|
|
18
|
+
|
|
19
|
+
- 代码是事实源,开发文档是目标源。
|
|
20
|
+
- 开发前必须先输出执行确认单。
|
|
21
|
+
- 如果无法识别目标仓库或开发文档,必须停下来询问用户。
|
|
22
|
+
- 如果识别到偏差修正任务,优先收口偏差,再推进后续 backlog。
|
|
23
|
+
- 真正执行前确认,真正结束前验证。
|
|
24
|
+
|
|
25
|
+
## Windows 安全
|
|
26
|
+
|
|
27
|
+
- 谨慎使用 shell;优先使用原生文件工具完成读写与目录操作。
|
|
28
|
+
- 禁止危险删除、格式化、强推、硬重置等命令。
|
|
29
|
+
- 不允许通过路径拼接生成破坏性命令。
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
description = "根据开发文档分析当前进度、生成确认单,并在确认后继续接续开发"
|
|
2
|
+
prompt = """
|
|
3
|
+
你现在正在执行 HelloLoop 的 Gemini CLI 原生工作流。
|
|
4
|
+
|
|
5
|
+
执行要求:
|
|
6
|
+
|
|
7
|
+
1. 自动识别目标仓库与开发文档;如果用户在命令后提供了参数,只把它解释为一个路径。
|
|
8
|
+
2. 分析当前代码与开发文档之间的真实进度和偏差。
|
|
9
|
+
3. 在目标仓库根目录创建或刷新 `.helloloop/`,至少维护 `backlog.json`、`project.json`、`status.json`、`STATE.md` 与 `runs/`。
|
|
10
|
+
4. 在真正开发前,必须输出中文执行确认单,至少包含:
|
|
11
|
+
- 目标仓库
|
|
12
|
+
- 开发文档
|
|
13
|
+
- 当前进度
|
|
14
|
+
- 已实现事项
|
|
15
|
+
- 待完成事项
|
|
16
|
+
- 任务统计
|
|
17
|
+
- 首个待执行任务
|
|
18
|
+
- 验证命令预览
|
|
19
|
+
- 自动执行停止条件
|
|
20
|
+
5. 用户未确认前,不要开始正式修改代码。
|
|
21
|
+
6. 用户确认后,继续用 Gemini CLI 原生工具推进 backlog,直到完成、遇到硬阻塞,或需要用户补充关键信息。
|
|
22
|
+
7. 如果用户给了开发文档但找不到项目仓库,或给了项目路径但找不到开发文档,必须停下来询问。
|
|
23
|
+
8. Windows 上优先用安全的文件与 shell 操作;禁止危险删除、硬重置、强推和其他破坏性命令。
|
|
24
|
+
"""
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "helloloop",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "HelloLoop 的 Gemini CLI 原生扩展,用于按开发文档接续推进项目开发。",
|
|
5
|
+
"contextFileName": "GEMINI.md",
|
|
6
|
+
"excludeTools": [
|
|
7
|
+
"run_shell_command(rm -rf)",
|
|
8
|
+
"run_shell_command(git reset --hard)",
|
|
9
|
+
"run_shell_command(git push --force)",
|
|
10
|
+
"run_shell_command(FLUSHALL)",
|
|
11
|
+
"run_shell_command(FLUSHDB)"
|
|
12
|
+
]
|
|
13
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "helloloop",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "面向 Codex CLI、Claude Code、Gemini CLI 的多宿主开发工作流插件",
|
|
5
|
+
"author": "HelloLoop",
|
|
6
|
+
"license": "Apache-2.0",
|
|
7
|
+
"homepage": "https://github.com/hellowind777/helloloop",
|
|
5
8
|
"type": "module",
|
|
6
9
|
"repository": {
|
|
7
10
|
"type": "git",
|
|
@@ -14,9 +17,12 @@
|
|
|
14
17
|
"helloloop": "bin/helloloop.js"
|
|
15
18
|
},
|
|
16
19
|
"files": [
|
|
20
|
+
".claude-plugin",
|
|
17
21
|
".codex-plugin",
|
|
22
|
+
"LICENSE",
|
|
18
23
|
"README.md",
|
|
19
24
|
"bin",
|
|
25
|
+
"hosts",
|
|
20
26
|
"package.json",
|
|
21
27
|
"scripts",
|
|
22
28
|
"skills",
|
|
@@ -24,7 +30,7 @@
|
|
|
24
30
|
"templates"
|
|
25
31
|
],
|
|
26
32
|
"scripts": {
|
|
27
|
-
"test": "node --test tests/cli_surface.test.mjs tests/install_script.test.mjs tests/process_shell.test.mjs tests/ralph_loop.test.mjs tests/plugin_bundle.test.mjs"
|
|
33
|
+
"test": "node --test tests/analyze_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"
|
|
28
34
|
},
|
|
29
35
|
"engines": {
|
|
30
36
|
"node": ">=20"
|
|
@@ -1,51 +1,79 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: helloloop
|
|
3
|
-
description:
|
|
3
|
+
description: 当用户希望 Codex 先分析仓库当前进度、生成确认单,再自动按 backlog 持续接续开发时使用。
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# HelloLoop
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
当任务目标不是单轮对话里改一点代码,而是要基于开发文档持续推进整个仓库时,使用这个插件。
|
|
9
9
|
|
|
10
|
-
##
|
|
10
|
+
## 强制入口规则
|
|
11
11
|
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
12
|
+
- 用户显式调用 `$helloloop` / `helloloop:helloloop` 时,默认必须优先执行 `npx helloloop` 或 `npx helloloop <PATH>`。
|
|
13
|
+
- 不允许在对话里手工模拟 `HelloLoop` 的分析、确认单、backlog 编排和自动续跑流程来代替 CLI。
|
|
14
|
+
- 只有在以下情况,才允许先停下来问用户而不是直接执行 CLI:
|
|
15
|
+
1. 用户既没有给路径,当前目录也无法判断项目仓库或开发文档
|
|
16
|
+
2. 用户给了开发文档,但无法定位目标项目仓库
|
|
17
|
+
3. 用户给了项目路径,但无法找到开发文档
|
|
18
|
+
4. 用户明确要求先只讲解、不执行
|
|
15
19
|
|
|
16
|
-
##
|
|
20
|
+
## `$helloloop` 的默认执行映射
|
|
17
21
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
22
|
+
- 当前目录已经是目标项目仓库或开发文档目录 → 先执行 `npx helloloop`
|
|
23
|
+
- 用户给了单一路径 → 先执行 `npx helloloop <PATH>`
|
|
24
|
+
- 用户明确只想先看分析和确认单 → 执行 `npx helloloop --dry-run`
|
|
25
|
+
- 用户明确要求跳过确认直接开始 → 执行 `npx helloloop -y`
|
|
21
26
|
|
|
22
|
-
##
|
|
27
|
+
## 插件边界
|
|
23
28
|
|
|
24
|
-
-
|
|
25
|
-
-
|
|
26
|
-
-
|
|
29
|
+
- 当前 bundle 根目录就是 `HelloLoop` 的官方插件目录。
|
|
30
|
+
- 插件元数据位于 `.codex-plugin/plugin.json`,执行逻辑位于 `skills/`、`bin/`、`scripts/`、`src/`、`templates/`。
|
|
31
|
+
- 运行状态统一写入目标仓库根目录下的 `.helloloop/`。
|
|
27
32
|
|
|
28
|
-
##
|
|
33
|
+
## 使用前准备
|
|
29
34
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
35
|
+
1. 先通过 `npx helloloop install --codex-home <CODEX_HOME>` 或 `scripts/install-home-plugin.ps1` 安装插件。
|
|
36
|
+
2. 打开目标项目仓库目录,或者打开开发文档所在目录。
|
|
37
|
+
3. 运行 `npx helloloop` 或 `npx helloloop <PATH>`。
|
|
38
|
+
4. 命中 `$helloloop` 后,优先按上面的默认执行映射直接调用 CLI。
|
|
39
|
+
5. `HelloLoop` 会先自动分析,再输出执行确认单。
|
|
40
|
+
6. 用户确认后,`HelloLoop` 才开始正式自动执行。
|
|
34
41
|
|
|
35
|
-
|
|
42
|
+
如果无法自动判断仓库路径或开发文档路径,就停下来提示用户补充;`--repo` 和 `--docs` 只作为显式覆盖选项使用。
|
|
36
43
|
|
|
37
|
-
|
|
38
|
-
- `npx helloloop next --repo <repo-root>`
|
|
39
|
-
- `npx helloloop run-once --repo <repo-root>`
|
|
40
|
-
- `npx helloloop run-loop --repo <repo-root>`
|
|
44
|
+
## 工作模式
|
|
41
45
|
|
|
42
|
-
|
|
46
|
+
- 代码是事实源,开发文档是目标源。
|
|
47
|
+
- `HelloLoop` 会先分析当前真实进度,再生成或刷新 `.helloloop/backlog.json`。
|
|
48
|
+
- 分析后会展示执行确认单,明确告知当前进度、待办任务、验证命令和执行边界。
|
|
49
|
+
- 用户确认后,默认会持续执行到 backlog 清空或遇到硬阻塞。
|
|
50
|
+
- 真正的代码分析与实现仍由本机 `codex` CLI 完成。
|
|
51
|
+
- `$helloloop` 的职责是把用户请求路由到主 CLI 流程,而不是在对话里手工复刻一套平行流程。
|
|
43
52
|
|
|
44
|
-
|
|
45
|
-
- Keep Ralph Loop guardrails and repo verification intact.
|
|
53
|
+
## 核心命令
|
|
46
54
|
|
|
47
|
-
|
|
55
|
+
- `npx helloloop`
|
|
56
|
+
- `npx helloloop <PATH>`
|
|
57
|
+
- `npx helloloop --dry-run`
|
|
58
|
+
- `npx helloloop -y`
|
|
48
59
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
-
|
|
60
|
+
## 手动控制命令
|
|
61
|
+
|
|
62
|
+
- `npx helloloop status`
|
|
63
|
+
- `npx helloloop next`
|
|
64
|
+
- `npx helloloop run-once`
|
|
65
|
+
- `npx helloloop run-loop --max-tasks <n>`
|
|
66
|
+
- `npx helloloop doctor`
|
|
67
|
+
- `npx helloloop init`
|
|
68
|
+
|
|
69
|
+
## 调用方式
|
|
70
|
+
|
|
71
|
+
- 在官方 Codex 插件模式下,明确的 skill 名称是 `helloloop:helloloop`。
|
|
72
|
+
- 在对话里显式提到 `helloloop` 插件,也会帮助 Codex 更准确地命中这个 skill。
|
|
73
|
+
|
|
74
|
+
## 参考文档
|
|
75
|
+
|
|
76
|
+
- 主说明:`README.md`
|
|
77
|
+
- 安装说明:`docs/install.md`
|
|
78
|
+
- Bundle 说明:`docs/README.md`
|
|
79
|
+
- 插件标准映射:`docs/plugin-standard.md`
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
import { analyzeExecution, summarizeBacklog } from "./backlog.mjs";
|
|
4
|
+
import { loadPolicy, loadVerifyCommands } from "./config.mjs";
|
|
5
|
+
|
|
6
|
+
function toDisplayPath(repoRoot, targetPath) {
|
|
7
|
+
const absolutePath = path.resolve(targetPath);
|
|
8
|
+
const relativePath = path.relative(repoRoot, absolutePath);
|
|
9
|
+
if (relativePath && !relativePath.startsWith("..") && !path.isAbsolute(relativePath)) {
|
|
10
|
+
return relativePath.replaceAll("\\", "/");
|
|
11
|
+
}
|
|
12
|
+
return absolutePath.replaceAll("\\", "/");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function formatList(items, fallback = "无") {
|
|
16
|
+
if (!Array.isArray(items) || !items.length) {
|
|
17
|
+
return [`- ${fallback}`];
|
|
18
|
+
}
|
|
19
|
+
return items.map((item) => `- ${item}`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function formatTaskPreview(tasks) {
|
|
23
|
+
const preview = tasks
|
|
24
|
+
.filter((task) => ["pending", "in_progress"].includes(String(task.status || "pending")))
|
|
25
|
+
.slice(0, 5);
|
|
26
|
+
|
|
27
|
+
if (!preview.length) {
|
|
28
|
+
return ["- 当前没有待执行任务"];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return preview.map((task) => {
|
|
32
|
+
const parts = [
|
|
33
|
+
task.title,
|
|
34
|
+
`#${task.id}`,
|
|
35
|
+
task.priority || "P2",
|
|
36
|
+
`risk:${task.risk || "low"}`,
|
|
37
|
+
];
|
|
38
|
+
return `- ${parts.join(" | ")}`;
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function formatExecutionState(execution) {
|
|
43
|
+
const stateMap = {
|
|
44
|
+
ready: "可自动执行",
|
|
45
|
+
blocked_in_progress: "存在未收束任务",
|
|
46
|
+
blocked_failed: "存在失败或阻塞任务",
|
|
47
|
+
blocked_risk: "风险超过自动阈值",
|
|
48
|
+
blocked_dependencies: "依赖未满足",
|
|
49
|
+
done: "backlog 已完成",
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
return stateMap[execution.state] || execution.state;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function resolvePreviewVerifyCommands(context, execution) {
|
|
56
|
+
const taskCommands = Array.isArray(execution.task?.verify) && execution.task.verify.length
|
|
57
|
+
? execution.task.verify
|
|
58
|
+
: [];
|
|
59
|
+
if (taskCommands.length) {
|
|
60
|
+
return taskCommands;
|
|
61
|
+
}
|
|
62
|
+
return loadVerifyCommands(context);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function resolveAutoRunMaxTasks(backlog, options = {}) {
|
|
66
|
+
const explicitMaxTasks = Number(options.maxTasks);
|
|
67
|
+
if (Number.isFinite(explicitMaxTasks) && explicitMaxTasks > 0) {
|
|
68
|
+
return explicitMaxTasks;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const summary = summarizeBacklog(backlog);
|
|
72
|
+
const pendingTotal = summary.pending + summary.inProgress;
|
|
73
|
+
return Math.max(1, pendingTotal);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function renderAnalyzeConfirmation(context, analysis, backlog, options = {}) {
|
|
77
|
+
const summary = summarizeBacklog(backlog);
|
|
78
|
+
const execution = analyzeExecution(backlog, options);
|
|
79
|
+
const policy = loadPolicy(context);
|
|
80
|
+
const verifyCommands = resolvePreviewVerifyCommands(context, execution);
|
|
81
|
+
const autoRunMaxTasks = resolveAutoRunMaxTasks(backlog, options);
|
|
82
|
+
const docsDisplay = analysis.requiredDocs.map((entry) => (
|
|
83
|
+
toDisplayPath(context.repoRoot, path.resolve(context.repoRoot, entry))
|
|
84
|
+
));
|
|
85
|
+
|
|
86
|
+
return [
|
|
87
|
+
"执行确认单",
|
|
88
|
+
"============",
|
|
89
|
+
`目标仓库:${context.repoRoot.replaceAll("\\", "/")}`,
|
|
90
|
+
`开发文档:${docsDisplay.length ? docsDisplay.join(",") : "未识别"}`,
|
|
91
|
+
"",
|
|
92
|
+
"当前进度:",
|
|
93
|
+
`- ${analysis.summary.currentState}`,
|
|
94
|
+
"",
|
|
95
|
+
"已实现:",
|
|
96
|
+
...formatList(analysis.summary.implemented, "暂无已实现摘要"),
|
|
97
|
+
"",
|
|
98
|
+
"待完成:",
|
|
99
|
+
...formatList(analysis.summary.remaining, "暂无待完成摘要"),
|
|
100
|
+
"",
|
|
101
|
+
"任务统计:",
|
|
102
|
+
`- 总任务:${summary.total}`,
|
|
103
|
+
`- 已完成:${summary.done}`,
|
|
104
|
+
`- 待处理:${summary.pending}`,
|
|
105
|
+
`- 进行中:${summary.inProgress}`,
|
|
106
|
+
`- 阻塞:${summary.blocked}`,
|
|
107
|
+
`- 失败:${summary.failed}`,
|
|
108
|
+
"",
|
|
109
|
+
"执行判断:",
|
|
110
|
+
`- 当前状态:${formatExecutionState(execution)}`,
|
|
111
|
+
`- 优先动作:${analysis.summary.nextAction}`,
|
|
112
|
+
execution.task
|
|
113
|
+
? `- 首个任务:${execution.task.title}`
|
|
114
|
+
: `- 首个任务:${execution.blockedTask?.title || "暂无"}`,
|
|
115
|
+
execution.blockedReason
|
|
116
|
+
? `- 当前阻塞:${execution.blockedReason}`
|
|
117
|
+
: "- 当前阻塞:无",
|
|
118
|
+
"- 偏差修正:按 backlog 优先级执行;如果分析识别出偏差修正任务,会先收口再继续后续开发",
|
|
119
|
+
`- 自动推进:最多 ${autoRunMaxTasks} 个任务,直到 backlog 清空或遇到硬阻塞`,
|
|
120
|
+
`- 单任务重试:每种策略最多 ${policy.maxTaskAttempts} 次,共 ${policy.maxTaskStrategies} 轮策略`,
|
|
121
|
+
"",
|
|
122
|
+
"待执行任务预览:",
|
|
123
|
+
...formatTaskPreview(backlog.tasks),
|
|
124
|
+
"",
|
|
125
|
+
"验证命令预览:",
|
|
126
|
+
...formatList(verifyCommands, "未配置 verify.yaml,执行阶段将仅依赖任务自带验证"),
|
|
127
|
+
].join("\n");
|
|
128
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { formatList } from "./common.mjs";
|
|
2
|
+
import {
|
|
3
|
+
hasCustomProjectConstraints,
|
|
4
|
+
listMandatoryGuardrails,
|
|
5
|
+
resolveProjectConstraints,
|
|
6
|
+
} from "./guardrails.mjs";
|
|
7
|
+
|
|
8
|
+
function section(title, content) {
|
|
9
|
+
if (!content || !String(content).trim()) {
|
|
10
|
+
return "";
|
|
11
|
+
}
|
|
12
|
+
return `## ${title}\n${String(content).trim()}\n`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function listSection(title, items) {
|
|
16
|
+
if (!Array.isArray(items) || !items.length) {
|
|
17
|
+
return "";
|
|
18
|
+
}
|
|
19
|
+
return section(title, formatList(items));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function renderDocPackets(docPackets) {
|
|
23
|
+
return docPackets.map((packet) => [
|
|
24
|
+
`### 文档:${packet.path}`,
|
|
25
|
+
packet.truncated ? "(已截断)" : "",
|
|
26
|
+
"",
|
|
27
|
+
packet.content,
|
|
28
|
+
].filter(Boolean).join("\n")).join("\n\n");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function buildAnalysisPrompt({
|
|
32
|
+
repoRoot,
|
|
33
|
+
docsEntries,
|
|
34
|
+
docPackets,
|
|
35
|
+
existingStateText = "",
|
|
36
|
+
existingBacklogText = "",
|
|
37
|
+
existingProjectConstraints = [],
|
|
38
|
+
}) {
|
|
39
|
+
const mandatoryGuardrails = listMandatoryGuardrails();
|
|
40
|
+
const effectiveConstraints = resolveProjectConstraints(existingProjectConstraints);
|
|
41
|
+
const usingFallbackConstraints = !hasCustomProjectConstraints(existingProjectConstraints);
|
|
42
|
+
|
|
43
|
+
return [
|
|
44
|
+
"你要为一个本地代码仓库做“接续开发分析”。",
|
|
45
|
+
"目标不是直接改代码,而是判断当前已经做到哪里,并生成后续可执行 backlog。",
|
|
46
|
+
"",
|
|
47
|
+
"必须遵守以下原则:",
|
|
48
|
+
"1. 代码是事实源,开发文档是目标源。",
|
|
49
|
+
"2. 判断“已完成 / 部分完成 / 未完成”时,以仓库当前真实代码、测试、目录结构为准,不能盲信文档。",
|
|
50
|
+
"3. 生成的任务必须颗粒度足够,能直接进入开发,不允许输出“继续开发”这类空泛任务。",
|
|
51
|
+
"4. 只关注当前目标仓库,不要把其他仓库的任务混进来;如果文档覆盖多仓库,只提取当前仓库相关任务。",
|
|
52
|
+
"5. 如果某项工作依赖其他仓库或外部输入,允许输出 `blocked` 任务,但必须明确阻塞原因。",
|
|
53
|
+
"",
|
|
54
|
+
section("目标仓库", `- 路径:${repoRoot.replaceAll("\\", "/")}`),
|
|
55
|
+
section("开发文档入口", docsEntries.map((item) => `- ${item}`).join("\n")),
|
|
56
|
+
existingStateText ? section("已有状态摘要", existingStateText) : "",
|
|
57
|
+
listSection("内建安全底线", mandatoryGuardrails),
|
|
58
|
+
listSection(usingFallbackConstraints ? "默认工程约束(文档未明确时也必须遵守)" : "已有项目约束", effectiveConstraints),
|
|
59
|
+
existingBacklogText ? section("已有 backlog(供参考,可重组)", existingBacklogText) : "",
|
|
60
|
+
section("文档内容摘录", renderDocPackets(docPackets)),
|
|
61
|
+
section("输出要求", [
|
|
62
|
+
"1. 严格输出 JSON,不要带 Markdown 代码块。",
|
|
63
|
+
"2. `summary.currentState` 要清晰描述当前仓库真实进度。",
|
|
64
|
+
"3. `summary.implemented` 写已确认完成的关键能力。",
|
|
65
|
+
"4. `summary.remaining` 写尚未完成的关键缺口。",
|
|
66
|
+
"5. `summary.nextAction` 写最合理的下一步。",
|
|
67
|
+
"6. `tasks` 总数控制在 4 到 12 个之间,优先覆盖真正剩余工作。",
|
|
68
|
+
"7. 每个任务必须包含:id、title、status、priority、risk、goal、docs、paths、acceptance。",
|
|
69
|
+
"8. `status` 只能是 `done`、`pending`、`blocked`,不要输出 `in_progress` 或 `failed`。",
|
|
70
|
+
"9. `docs` 必须引用本次文档入口中的相关路径;`paths` 必须写当前仓库内的实际目录或文件模式。",
|
|
71
|
+
"10. `acceptance` 必须可验证,不要写空话。",
|
|
72
|
+
"11. `constraints` 只写从项目文档或现有项目配置中提炼出的项目特有约束;不要重复内建安全底线。",
|
|
73
|
+
].join("\n")),
|
|
74
|
+
].filter(Boolean).join("\n");
|
|
75
|
+
}
|
package/src/analyzer.mjs
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
import { summarizeBacklog, selectNextTask } from "./backlog.mjs";
|
|
4
|
+
import { nowIso, writeJson, writeText, readTextIfExists } from "./common.mjs";
|
|
5
|
+
import { loadPolicy, loadProjectConfig, scaffoldIfMissing, writeStateMarkdown, writeStatus } from "./config.mjs";
|
|
6
|
+
import { createContext } from "./context.mjs";
|
|
7
|
+
import { discoverWorkspace } from "./discovery.mjs";
|
|
8
|
+
import { readDocumentPackets } from "./doc_loader.mjs";
|
|
9
|
+
import { runCodexTask } from "./process.mjs";
|
|
10
|
+
import { buildAnalysisPrompt } from "./analyze_prompt.mjs";
|
|
11
|
+
|
|
12
|
+
function renderAnalysisState(context, backlog, analysis) {
|
|
13
|
+
const summary = summarizeBacklog(backlog);
|
|
14
|
+
const nextTask = selectNextTask(backlog);
|
|
15
|
+
|
|
16
|
+
return [
|
|
17
|
+
"## 当前状态",
|
|
18
|
+
`- backlog 文件:${path.relative(context.repoRoot, context.backlogFile).replaceAll("\\", "/")}`,
|
|
19
|
+
`- 总任务数:${summary.total}`,
|
|
20
|
+
`- 已完成:${summary.done}`,
|
|
21
|
+
`- 待处理:${summary.pending}`,
|
|
22
|
+
`- 进行中:${summary.inProgress}`,
|
|
23
|
+
`- 失败:${summary.failed}`,
|
|
24
|
+
`- 阻塞:${summary.blocked}`,
|
|
25
|
+
`- 当前任务:${nextTask ? nextTask.title : "无"}`,
|
|
26
|
+
`- 最近结果:${analysis.summary.currentState}`,
|
|
27
|
+
`- 下一建议:${analysis.summary.nextAction}`,
|
|
28
|
+
].join("\n");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function sanitizeTask(task) {
|
|
32
|
+
return {
|
|
33
|
+
id: String(task.id || "").trim(),
|
|
34
|
+
title: String(task.title || "").trim(),
|
|
35
|
+
status: ["done", "blocked"].includes(String(task.status || "")) ? String(task.status) : "pending",
|
|
36
|
+
priority: ["P0", "P1", "P2", "P3"].includes(String(task.priority || "")) ? String(task.priority) : "P2",
|
|
37
|
+
risk: ["medium", "high", "critical"].includes(String(task.risk || "")) ? String(task.risk) : "low",
|
|
38
|
+
goal: String(task.goal || "").trim(),
|
|
39
|
+
docs: Array.isArray(task.docs) ? task.docs.map((item) => String(item || "").trim()).filter(Boolean) : [],
|
|
40
|
+
paths: Array.isArray(task.paths) ? task.paths.map((item) => String(item || "").trim()).filter(Boolean) : [],
|
|
41
|
+
acceptance: Array.isArray(task.acceptance) ? task.acceptance.map((item) => String(item || "").trim()).filter(Boolean) : [],
|
|
42
|
+
dependsOn: Array.isArray(task.dependsOn) ? task.dependsOn.map((item) => String(item || "").trim()).filter(Boolean) : [],
|
|
43
|
+
verify: Array.isArray(task.verify) ? task.verify.map((item) => String(item || "").trim()).filter(Boolean) : [],
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function normalizeAnalysisPayload(payload, docsEntries) {
|
|
48
|
+
const tasks = Array.isArray(payload.tasks)
|
|
49
|
+
? payload.tasks.map((task) => sanitizeTask(task)).filter((task) => (
|
|
50
|
+
task.id && task.title && task.goal && task.acceptance.length
|
|
51
|
+
))
|
|
52
|
+
: [];
|
|
53
|
+
|
|
54
|
+
if (!tasks.length) {
|
|
55
|
+
throw new Error("Codex 分析结果无效:未生成可用任务。");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
project: String(payload.project || "").trim() || "helloloop-project",
|
|
60
|
+
summary: {
|
|
61
|
+
currentState: String(payload?.summary?.currentState || "").trim() || "已完成接续分析。",
|
|
62
|
+
implemented: Array.isArray(payload?.summary?.implemented) ? payload.summary.implemented.map((item) => String(item || "").trim()).filter(Boolean) : [],
|
|
63
|
+
remaining: Array.isArray(payload?.summary?.remaining) ? payload.summary.remaining.map((item) => String(item || "").trim()).filter(Boolean) : [],
|
|
64
|
+
nextAction: String(payload?.summary?.nextAction || "").trim() || "查看下一任务。",
|
|
65
|
+
},
|
|
66
|
+
constraints: Array.isArray(payload.constraints)
|
|
67
|
+
? payload.constraints.map((item) => String(item || "").trim()).filter(Boolean)
|
|
68
|
+
: [],
|
|
69
|
+
tasks,
|
|
70
|
+
requiredDocs: docsEntries,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function buildAnalysisSummaryText(context, analysis, backlog) {
|
|
75
|
+
const summary = summarizeBacklog(backlog);
|
|
76
|
+
const nextTask = selectNextTask(backlog);
|
|
77
|
+
|
|
78
|
+
return [
|
|
79
|
+
"HelloLoop 已完成接续分析。",
|
|
80
|
+
`项目仓库:${context.repoRoot}`,
|
|
81
|
+
`开发文档:${analysis.requiredDocs.join(", ")}`,
|
|
82
|
+
"",
|
|
83
|
+
"当前进度:",
|
|
84
|
+
analysis.summary.currentState,
|
|
85
|
+
"",
|
|
86
|
+
`任务统计:done ${summary.done} / pending ${summary.pending} / blocked ${summary.blocked}`,
|
|
87
|
+
nextTask ? `下一任务:${nextTask.title}` : "下一任务:暂无可执行任务",
|
|
88
|
+
"",
|
|
89
|
+
"下一步建议:",
|
|
90
|
+
`- npx helloloop`,
|
|
91
|
+
`- npx helloloop --dry-run`,
|
|
92
|
+
].join("\n");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export async function analyzeWorkspace(options = {}) {
|
|
96
|
+
const discovery = discoverWorkspace({
|
|
97
|
+
cwd: options.cwd,
|
|
98
|
+
inputPath: options.inputPath,
|
|
99
|
+
repoRoot: options.repoRoot,
|
|
100
|
+
docsPath: options.docsPath,
|
|
101
|
+
configDirName: options.configDirName,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
if (!discovery.ok) {
|
|
105
|
+
return {
|
|
106
|
+
ok: false,
|
|
107
|
+
code: discovery.code,
|
|
108
|
+
summary: discovery.message,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const context = createContext({
|
|
113
|
+
repoRoot: discovery.repoRoot,
|
|
114
|
+
configDirName: options.configDirName,
|
|
115
|
+
});
|
|
116
|
+
scaffoldIfMissing(context);
|
|
117
|
+
|
|
118
|
+
const existingProjectConfig = loadProjectConfig(context);
|
|
119
|
+
const existingStateText = readTextIfExists(context.stateFile, "");
|
|
120
|
+
const existingBacklogText = readTextIfExists(context.backlogFile, "");
|
|
121
|
+
const docPackets = readDocumentPackets(context.repoRoot, discovery.docsEntries, {
|
|
122
|
+
maxCharsPerFile: 18000,
|
|
123
|
+
maxTotalChars: 90000,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const prompt = buildAnalysisPrompt({
|
|
127
|
+
repoRoot: context.repoRoot,
|
|
128
|
+
docsEntries: discovery.docsEntries,
|
|
129
|
+
docPackets,
|
|
130
|
+
existingStateText,
|
|
131
|
+
existingBacklogText,
|
|
132
|
+
existingProjectConstraints: existingProjectConfig.constraints,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const runDir = path.join(context.runsDir, `${nowIso().replaceAll(":", "-").replaceAll(".", "-")}-analysis`);
|
|
136
|
+
const policy = loadPolicy(context);
|
|
137
|
+
const schemaFile = path.join(context.templatesDir, "analysis-output.schema.json");
|
|
138
|
+
const codexResult = await runCodexTask({
|
|
139
|
+
context,
|
|
140
|
+
prompt,
|
|
141
|
+
runDir,
|
|
142
|
+
model: policy.codex.model,
|
|
143
|
+
executable: policy.codex.executable,
|
|
144
|
+
sandbox: policy.codex.sandbox,
|
|
145
|
+
dangerouslyBypassSandbox: policy.codex.dangerouslyBypassSandbox,
|
|
146
|
+
outputSchemaFile: schemaFile,
|
|
147
|
+
outputPrefix: "analysis",
|
|
148
|
+
jsonOutput: false,
|
|
149
|
+
skipGitRepoCheck: true,
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
if (!codexResult.ok) {
|
|
153
|
+
return {
|
|
154
|
+
ok: false,
|
|
155
|
+
code: "analysis_failed",
|
|
156
|
+
summary: codexResult.stderr || codexResult.stdout || "Codex 接续分析失败。",
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
let payload;
|
|
161
|
+
try {
|
|
162
|
+
payload = JSON.parse(codexResult.finalMessage);
|
|
163
|
+
} catch (error) {
|
|
164
|
+
return {
|
|
165
|
+
ok: false,
|
|
166
|
+
code: "invalid_analysis_json",
|
|
167
|
+
summary: `Codex 分析结果无法解析为 JSON:${String(error?.message || error || "")}`,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const analysis = normalizeAnalysisPayload(payload, discovery.docsEntries);
|
|
172
|
+
const backlog = {
|
|
173
|
+
version: 1,
|
|
174
|
+
project: analysis.project,
|
|
175
|
+
updatedAt: nowIso(),
|
|
176
|
+
tasks: analysis.tasks,
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const projectConfig = {
|
|
180
|
+
requiredDocs: analysis.requiredDocs,
|
|
181
|
+
constraints: analysis.constraints.length ? analysis.constraints : existingProjectConfig.constraints,
|
|
182
|
+
planner: existingProjectConfig.planner,
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
writeJson(context.backlogFile, backlog);
|
|
186
|
+
writeJson(context.projectFile, projectConfig);
|
|
187
|
+
writeStateMarkdown(context, renderAnalysisState(context, backlog, analysis));
|
|
188
|
+
writeStatus(context, {
|
|
189
|
+
ok: true,
|
|
190
|
+
stage: "analyzed",
|
|
191
|
+
taskId: null,
|
|
192
|
+
taskTitle: "",
|
|
193
|
+
runDir,
|
|
194
|
+
summary: summarizeBacklog(backlog),
|
|
195
|
+
message: analysis.summary.currentState,
|
|
196
|
+
});
|
|
197
|
+
writeText(path.join(runDir, "analysis-summary.txt"), buildAnalysisSummaryText(context, analysis, backlog));
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
ok: true,
|
|
201
|
+
code: "analyzed",
|
|
202
|
+
context,
|
|
203
|
+
runDir,
|
|
204
|
+
analysis,
|
|
205
|
+
backlog,
|
|
206
|
+
summary: buildAnalysisSummaryText(context, analysis, backlog),
|
|
207
|
+
};
|
|
208
|
+
}
|