helloloop 0.1.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/.codex-plugin/plugin.json +33 -0
- package/README.md +334 -0
- package/bin/helloloop.mjs +8 -0
- package/package.json +37 -0
- package/scripts/helloloop.mjs +8 -0
- package/scripts/install-home-plugin.ps1 +30 -0
- package/skills/helloloop/SKILL.md +53 -0
- package/src/backlog.mjs +170 -0
- package/src/cli.mjs +262 -0
- package/src/common.mjs +66 -0
- package/src/config.mjs +141 -0
- package/src/context.mjs +56 -0
- package/src/doc_loader.mjs +106 -0
- package/src/install.mjs +130 -0
- package/src/lifecycle.mjs +58 -0
- package/src/process.mjs +264 -0
- package/src/prompt.mjs +116 -0
- package/src/runner.mjs +341 -0
- package/templates/STATE.template.md +12 -0
- package/templates/backlog.template.json +26 -0
- package/templates/policy.template.json +16 -0
- package/templates/project.template.json +14 -0
- package/templates/status.template.json +18 -0
package/src/runner.mjs
ADDED
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
ensureDir,
|
|
5
|
+
nowIso,
|
|
6
|
+
sanitizeId,
|
|
7
|
+
tailText,
|
|
8
|
+
timestampForFile,
|
|
9
|
+
writeText,
|
|
10
|
+
} from "./common.mjs";
|
|
11
|
+
import {
|
|
12
|
+
loadBacklog,
|
|
13
|
+
loadPolicy,
|
|
14
|
+
loadProjectConfig,
|
|
15
|
+
loadRepoStateText,
|
|
16
|
+
loadVerifyCommands,
|
|
17
|
+
saveBacklog,
|
|
18
|
+
writeStateMarkdown,
|
|
19
|
+
writeStatus,
|
|
20
|
+
} from "./config.mjs";
|
|
21
|
+
import {
|
|
22
|
+
getTask,
|
|
23
|
+
renderTaskSummary,
|
|
24
|
+
selectNextTask,
|
|
25
|
+
summarizeBacklog,
|
|
26
|
+
unresolvedDependencies,
|
|
27
|
+
updateTask,
|
|
28
|
+
} from "./backlog.mjs";
|
|
29
|
+
import { buildTaskPrompt } from "./prompt.mjs";
|
|
30
|
+
import { runCodexExec, runVerifyCommands } from "./process.mjs";
|
|
31
|
+
|
|
32
|
+
function makeRunDir(context, taskId) {
|
|
33
|
+
return path.join(context.runsDir, `${timestampForFile()}-${sanitizeId(taskId)}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function makeAttemptDir(runDir, strategyIndex, attemptIndex) {
|
|
37
|
+
return path.join(
|
|
38
|
+
runDir,
|
|
39
|
+
`strategy-${String(strategyIndex).padStart(2, "0")}-attempt-${String(attemptIndex).padStart(2, "0")}`,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isHardStopFailure(kind, summary) {
|
|
44
|
+
const normalized = String(summary || "").toLowerCase();
|
|
45
|
+
if (!normalized) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (kind === "codex" && normalized.includes("enoent")) {
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return [
|
|
54
|
+
"command not found",
|
|
55
|
+
"is not recognized",
|
|
56
|
+
"无法将",
|
|
57
|
+
"找不到路径",
|
|
58
|
+
"no such file or directory",
|
|
59
|
+
"permission denied",
|
|
60
|
+
"access is denied",
|
|
61
|
+
].some((signal) => normalized.includes(signal));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function buildExhaustedSummary({
|
|
65
|
+
failureHistory,
|
|
66
|
+
maxStrategies,
|
|
67
|
+
maxAttemptsPerStrategy,
|
|
68
|
+
}) {
|
|
69
|
+
const lastFailure = failureHistory.at(-1)?.summary || "未知失败。";
|
|
70
|
+
return [
|
|
71
|
+
`已按 Ralph Loop 执行 ${maxStrategies} 轮策略、每轮最多 ${maxAttemptsPerStrategy} 次重试,当前任务仍未收敛。`,
|
|
72
|
+
"",
|
|
73
|
+
"最后一次失败信息:",
|
|
74
|
+
lastFailure,
|
|
75
|
+
].join("\n").trim();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function renderStatusMarkdown(context, { summary, currentTask, lastResult, nextTask }) {
|
|
79
|
+
return [
|
|
80
|
+
"## 当前状态",
|
|
81
|
+
`- backlog 文件:${context.backlogFile.replaceAll("\\", "/")}`,
|
|
82
|
+
`- 总任务数:${summary.total}`,
|
|
83
|
+
`- 已完成:${summary.done}`,
|
|
84
|
+
`- 待处理:${summary.pending}`,
|
|
85
|
+
`- 进行中:${summary.inProgress}`,
|
|
86
|
+
`- 失败:${summary.failed}`,
|
|
87
|
+
`- 阻塞:${summary.blocked}`,
|
|
88
|
+
`- 当前任务:${currentTask ? currentTask.title : "无"}`,
|
|
89
|
+
`- 最近结果:${lastResult || "暂无"}`,
|
|
90
|
+
`- 下一建议:${nextTask ? nextTask.title : "暂无可执行任务"}`,
|
|
91
|
+
].join("\n");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function resolveTask(backlog, options) {
|
|
95
|
+
if (options.taskId) {
|
|
96
|
+
const task = getTask(backlog, options.taskId);
|
|
97
|
+
if (!task) throw new Error(`未找到任务:${options.taskId}`);
|
|
98
|
+
return task;
|
|
99
|
+
}
|
|
100
|
+
return selectNextTask(backlog, options);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function buildFailureSummary(kind, payload) {
|
|
104
|
+
if (kind === "codex") {
|
|
105
|
+
return [
|
|
106
|
+
`Codex 执行失败,退出码:${payload.code}`,
|
|
107
|
+
"",
|
|
108
|
+
"stdout 尾部:",
|
|
109
|
+
tailText(payload.stdout, 60),
|
|
110
|
+
"",
|
|
111
|
+
"stderr 尾部:",
|
|
112
|
+
tailText(payload.stderr, 60),
|
|
113
|
+
].join("\n").trim();
|
|
114
|
+
}
|
|
115
|
+
return payload.summary;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function executeSingleTask(context, options = {}) {
|
|
119
|
+
const policy = loadPolicy(context);
|
|
120
|
+
const projectConfig = loadProjectConfig(context);
|
|
121
|
+
const backlog = loadBacklog(context);
|
|
122
|
+
const repoStateText = loadRepoStateText(context);
|
|
123
|
+
const task = resolveTask(backlog, options);
|
|
124
|
+
|
|
125
|
+
if (!task) {
|
|
126
|
+
const summary = summarizeBacklog(backlog);
|
|
127
|
+
writeStatus(context, { ok: true, stage: "idle", summary });
|
|
128
|
+
writeStateMarkdown(context, renderStatusMarkdown(context, {
|
|
129
|
+
summary,
|
|
130
|
+
currentTask: null,
|
|
131
|
+
lastResult: "没有可执行任务",
|
|
132
|
+
nextTask: null,
|
|
133
|
+
}));
|
|
134
|
+
return { ok: true, kind: "idle", task: null };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const unresolved = unresolvedDependencies(backlog, task);
|
|
138
|
+
if (unresolved.length) {
|
|
139
|
+
throw new Error(`任务 ${task.id} 仍有未完成依赖:${unresolved.join(", ")}`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const verifyCommands = Array.isArray(task.verify) && task.verify.length
|
|
143
|
+
? task.verify
|
|
144
|
+
: loadVerifyCommands(context);
|
|
145
|
+
const runDir = makeRunDir(context, task.id);
|
|
146
|
+
const requiredDocs = [
|
|
147
|
+
...(projectConfig.requiredDocs || []),
|
|
148
|
+
...(options.requiredDocs || []),
|
|
149
|
+
];
|
|
150
|
+
const constraints = [
|
|
151
|
+
...(projectConfig.constraints || []),
|
|
152
|
+
...(options.constraints || []),
|
|
153
|
+
];
|
|
154
|
+
const maxAttemptsPerStrategy = Math.max(1, Number(options.maxAttempts || policy.maxTaskAttempts || 1));
|
|
155
|
+
const configuredStrategies = Math.max(1, Number(options.maxStrategies || policy.maxTaskStrategies || 1));
|
|
156
|
+
const maxStrategies = policy.stopOnFailure ? 1 : configuredStrategies;
|
|
157
|
+
|
|
158
|
+
if (options.dryRun) {
|
|
159
|
+
const prompt = buildTaskPrompt({
|
|
160
|
+
task,
|
|
161
|
+
repoStateText,
|
|
162
|
+
verifyCommands,
|
|
163
|
+
requiredDocs,
|
|
164
|
+
constraints,
|
|
165
|
+
strategyIndex: 1,
|
|
166
|
+
maxStrategies,
|
|
167
|
+
attemptIndex: 1,
|
|
168
|
+
maxAttemptsPerStrategy,
|
|
169
|
+
});
|
|
170
|
+
ensureDir(runDir);
|
|
171
|
+
writeText(path.join(runDir, "codex-prompt.md"), prompt);
|
|
172
|
+
return { ok: true, kind: "dry-run", task, runDir, prompt, verifyCommands };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
updateTask(backlog, task.id, { status: "in_progress", startedAt: nowIso() });
|
|
176
|
+
saveBacklog(context, backlog);
|
|
177
|
+
|
|
178
|
+
let previousFailure = "";
|
|
179
|
+
const failureHistory = [];
|
|
180
|
+
|
|
181
|
+
for (let strategyIndex = 1; strategyIndex <= maxStrategies; strategyIndex += 1) {
|
|
182
|
+
for (let attemptIndex = 1; attemptIndex <= maxAttemptsPerStrategy; attemptIndex += 1) {
|
|
183
|
+
const prompt = buildTaskPrompt({
|
|
184
|
+
task,
|
|
185
|
+
repoStateText,
|
|
186
|
+
verifyCommands,
|
|
187
|
+
requiredDocs,
|
|
188
|
+
constraints,
|
|
189
|
+
previousFailure,
|
|
190
|
+
failureHistory,
|
|
191
|
+
strategyIndex,
|
|
192
|
+
maxStrategies,
|
|
193
|
+
attemptIndex,
|
|
194
|
+
maxAttemptsPerStrategy,
|
|
195
|
+
});
|
|
196
|
+
const attemptDir = makeAttemptDir(runDir, strategyIndex, attemptIndex);
|
|
197
|
+
const codexResult = await runCodexExec({ context, prompt, runDir: attemptDir, policy });
|
|
198
|
+
|
|
199
|
+
if (!codexResult.ok) {
|
|
200
|
+
previousFailure = buildFailureSummary("codex", codexResult);
|
|
201
|
+
failureHistory.push({
|
|
202
|
+
strategyIndex,
|
|
203
|
+
attemptIndex,
|
|
204
|
+
kind: "codex",
|
|
205
|
+
summary: previousFailure,
|
|
206
|
+
});
|
|
207
|
+
if (isHardStopFailure("codex", previousFailure)) {
|
|
208
|
+
updateTask(backlog, task.id, {
|
|
209
|
+
status: "failed",
|
|
210
|
+
finishedAt: nowIso(),
|
|
211
|
+
lastFailure: previousFailure,
|
|
212
|
+
attempts: failureHistory.length,
|
|
213
|
+
});
|
|
214
|
+
saveBacklog(context, backlog);
|
|
215
|
+
return { ok: false, kind: "codex-failed", task, runDir, summary: previousFailure };
|
|
216
|
+
}
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const verifyResult = await runVerifyCommands(context, verifyCommands, attemptDir);
|
|
221
|
+
if (verifyResult.ok) {
|
|
222
|
+
updateTask(backlog, task.id, {
|
|
223
|
+
status: "done",
|
|
224
|
+
finishedAt: nowIso(),
|
|
225
|
+
lastFailure: "",
|
|
226
|
+
attempts: failureHistory.length + 1,
|
|
227
|
+
});
|
|
228
|
+
saveBacklog(context, backlog);
|
|
229
|
+
return {
|
|
230
|
+
ok: true,
|
|
231
|
+
kind: "done",
|
|
232
|
+
task,
|
|
233
|
+
runDir,
|
|
234
|
+
finalMessage: codexResult.finalMessage,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
previousFailure = buildFailureSummary("verify", verifyResult);
|
|
239
|
+
failureHistory.push({
|
|
240
|
+
strategyIndex,
|
|
241
|
+
attemptIndex,
|
|
242
|
+
kind: "verify",
|
|
243
|
+
summary: previousFailure,
|
|
244
|
+
});
|
|
245
|
+
if (isHardStopFailure("verify", previousFailure)) {
|
|
246
|
+
updateTask(backlog, task.id, {
|
|
247
|
+
status: "failed",
|
|
248
|
+
finishedAt: nowIso(),
|
|
249
|
+
lastFailure: previousFailure,
|
|
250
|
+
attempts: failureHistory.length,
|
|
251
|
+
});
|
|
252
|
+
saveBacklog(context, backlog);
|
|
253
|
+
return { ok: false, kind: "verify-failed", task, runDir, summary: previousFailure };
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
previousFailure = [
|
|
258
|
+
previousFailure,
|
|
259
|
+
"",
|
|
260
|
+
`上一种策略已连续失败 ${maxAttemptsPerStrategy} 次。下一轮必须明确更换实现或排查思路,不能重复原路径。`,
|
|
261
|
+
].join("\n").trim();
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const exhaustedSummary = buildExhaustedSummary({
|
|
265
|
+
failureHistory,
|
|
266
|
+
maxStrategies,
|
|
267
|
+
maxAttemptsPerStrategy,
|
|
268
|
+
});
|
|
269
|
+
updateTask(backlog, task.id, {
|
|
270
|
+
status: "failed",
|
|
271
|
+
finishedAt: nowIso(),
|
|
272
|
+
lastFailure: exhaustedSummary,
|
|
273
|
+
attempts: failureHistory.length,
|
|
274
|
+
});
|
|
275
|
+
saveBacklog(context, backlog);
|
|
276
|
+
return { ok: false, kind: "strategy-exhausted", task, runDir, summary: exhaustedSummary };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export async function runOnce(context, options = {}) {
|
|
280
|
+
const result = await executeSingleTask(context, options);
|
|
281
|
+
const backlog = loadBacklog(context);
|
|
282
|
+
const summary = summarizeBacklog(backlog);
|
|
283
|
+
const nextTask = selectNextTask(backlog, options);
|
|
284
|
+
|
|
285
|
+
writeStatus(context, {
|
|
286
|
+
ok: result.ok,
|
|
287
|
+
stage: result.kind,
|
|
288
|
+
taskId: result.task?.id || null,
|
|
289
|
+
taskTitle: result.task?.title || "",
|
|
290
|
+
runDir: result.runDir || "",
|
|
291
|
+
summary,
|
|
292
|
+
message: result.summary || result.finalMessage || "",
|
|
293
|
+
});
|
|
294
|
+
writeStateMarkdown(context, renderStatusMarkdown(context, {
|
|
295
|
+
summary,
|
|
296
|
+
currentTask: result.task,
|
|
297
|
+
lastResult: result.ok ? "本轮成功" : (result.summary || result.kind),
|
|
298
|
+
nextTask,
|
|
299
|
+
}));
|
|
300
|
+
|
|
301
|
+
return result;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export async function runLoop(context, options = {}) {
|
|
305
|
+
const policy = loadPolicy(context);
|
|
306
|
+
const maxTasks = Math.max(1, Number(options.maxTasks || policy.maxLoopTasks || 1));
|
|
307
|
+
const results = [];
|
|
308
|
+
|
|
309
|
+
for (let index = 0; index < maxTasks; index += 1) {
|
|
310
|
+
const result = await runOnce(context, options);
|
|
311
|
+
results.push(result);
|
|
312
|
+
if (options.dryRun) break;
|
|
313
|
+
if (!result.ok || !result.task) break;
|
|
314
|
+
const backlog = loadBacklog(context);
|
|
315
|
+
if (!selectNextTask(backlog, options)) break;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return results;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export function renderStatusText(context, options = {}) {
|
|
322
|
+
const backlog = loadBacklog(context);
|
|
323
|
+
const summary = summarizeBacklog(backlog);
|
|
324
|
+
const nextTask = selectNextTask(backlog, options);
|
|
325
|
+
|
|
326
|
+
return [
|
|
327
|
+
"HelloLoop 状态",
|
|
328
|
+
"============",
|
|
329
|
+
`仓库:${context.repoRoot}`,
|
|
330
|
+
`总任务:${summary.total}`,
|
|
331
|
+
`已完成:${summary.done}`,
|
|
332
|
+
`待处理:${summary.pending}`,
|
|
333
|
+
`进行中:${summary.inProgress}`,
|
|
334
|
+
`失败:${summary.failed}`,
|
|
335
|
+
`阻塞:${summary.blocked}`,
|
|
336
|
+
"",
|
|
337
|
+
nextTask ? "下一任务:" : "下一任务:无",
|
|
338
|
+
nextTask ? renderTaskSummary(nextTask) : "",
|
|
339
|
+
].filter(Boolean).join("\n");
|
|
340
|
+
}
|
|
341
|
+
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"project": "replace-with-your-project",
|
|
4
|
+
"updatedAt": "2026-03-27T00:00:00.000Z",
|
|
5
|
+
"tasks": [
|
|
6
|
+
{
|
|
7
|
+
"id": "example-task-1",
|
|
8
|
+
"title": "示例任务:完成某个低风险功能",
|
|
9
|
+
"status": "pending",
|
|
10
|
+
"priority": "P1",
|
|
11
|
+
"risk": "low",
|
|
12
|
+
"goal": "按开发文档完成一个可验证的小工作包。",
|
|
13
|
+
"docs": [
|
|
14
|
+
"docs/architecture/00-架构索引.md"
|
|
15
|
+
],
|
|
16
|
+
"paths": [
|
|
17
|
+
"src/"
|
|
18
|
+
],
|
|
19
|
+
"acceptance": [
|
|
20
|
+
"功能完成",
|
|
21
|
+
"验证通过",
|
|
22
|
+
"文档同步"
|
|
23
|
+
]
|
|
24
|
+
}
|
|
25
|
+
]
|
|
26
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"updatedAt": "2026-03-27T00:00:00.000Z",
|
|
4
|
+
"maxLoopTasks": 4,
|
|
5
|
+
"maxTaskAttempts": 2,
|
|
6
|
+
"maxTaskStrategies": 4,
|
|
7
|
+
"stopOnFailure": false,
|
|
8
|
+
"stopOnHighRisk": true,
|
|
9
|
+
"codex": {
|
|
10
|
+
"model": "",
|
|
11
|
+
"executable": "",
|
|
12
|
+
"sandbox": "workspace-write",
|
|
13
|
+
"dangerouslyBypassSandbox": false,
|
|
14
|
+
"jsonOutput": true
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"ok": true,
|
|
3
|
+
"stage": "idle",
|
|
4
|
+
"taskId": null,
|
|
5
|
+
"taskTitle": "",
|
|
6
|
+
"runDir": "",
|
|
7
|
+
"summary": {
|
|
8
|
+
"total": 1,
|
|
9
|
+
"pending": 1,
|
|
10
|
+
"inProgress": 0,
|
|
11
|
+
"done": 0,
|
|
12
|
+
"failed": 0,
|
|
13
|
+
"blocked": 0
|
|
14
|
+
},
|
|
15
|
+
"message": "HelloLoop 已初始化。",
|
|
16
|
+
"updatedAt": "2026-03-27T00:00:00.000Z"
|
|
17
|
+
}
|
|
18
|
+
|