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.
@@ -0,0 +1,130 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ import { ensureDir, fileExists, readJson, writeJson } from "./common.mjs";
7
+
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = path.dirname(__filename);
10
+
11
+ export const runtimeBundleEntries = [
12
+ ".codex-plugin",
13
+ "README.md",
14
+ "bin",
15
+ "package.json",
16
+ "scripts",
17
+ "skills",
18
+ "src",
19
+ "templates",
20
+ ];
21
+
22
+ function resolveCodexHome(codexHome) {
23
+ return path.resolve(codexHome || path.join(os.homedir(), ".codex"));
24
+ }
25
+
26
+ function assertPathInside(parentDir, targetDir, label) {
27
+ const relative = path.relative(parentDir, targetDir);
28
+ if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {
29
+ throw new Error(`${label} 超出允许范围:${targetDir}`);
30
+ }
31
+ }
32
+
33
+ function copyBundleEntries(bundleRoot, targetPluginRoot) {
34
+ for (const entry of runtimeBundleEntries) {
35
+ const sourcePath = path.join(bundleRoot, entry);
36
+ if (!fileExists(sourcePath)) {
37
+ continue;
38
+ }
39
+
40
+ fs.cpSync(sourcePath, path.join(targetPluginRoot, entry), {
41
+ force: true,
42
+ recursive: true,
43
+ });
44
+ }
45
+ }
46
+
47
+ function updateMarketplace(marketplaceFile) {
48
+ const marketplace = fileExists(marketplaceFile)
49
+ ? readJson(marketplaceFile)
50
+ : {
51
+ name: "local-plugins",
52
+ interface: {
53
+ displayName: "Local Plugins",
54
+ },
55
+ plugins: [],
56
+ };
57
+
58
+ marketplace.interface = marketplace.interface || {
59
+ displayName: "Local Plugins",
60
+ };
61
+ marketplace.plugins = Array.isArray(marketplace.plugins) ? marketplace.plugins : [];
62
+
63
+ const nextEntry = {
64
+ name: "helloloop",
65
+ source: {
66
+ source: "local",
67
+ path: "./plugins/helloloop",
68
+ },
69
+ policy: {
70
+ installation: "AVAILABLE",
71
+ authentication: "ON_INSTALL",
72
+ },
73
+ category: "Coding",
74
+ };
75
+
76
+ marketplace.plugins = marketplace.plugins.filter((plugin) => plugin?.name !== "autoloop");
77
+
78
+ const existingIndex = marketplace.plugins.findIndex((plugin) => plugin?.name === "helloloop");
79
+ if (existingIndex >= 0) {
80
+ marketplace.plugins.splice(existingIndex, 1, nextEntry);
81
+ } else {
82
+ marketplace.plugins.push(nextEntry);
83
+ }
84
+
85
+ writeJson(marketplaceFile, marketplace);
86
+ }
87
+
88
+ export function installPluginBundle(options = {}) {
89
+ const bundleRoot = path.resolve(options.bundleRoot || path.join(__dirname, ".."));
90
+ const resolvedCodexHome = resolveCodexHome(options.codexHome);
91
+ const targetPluginsRoot = path.join(resolvedCodexHome, "plugins");
92
+ const targetPluginRoot = path.join(targetPluginsRoot, "helloloop");
93
+ const legacyPluginRoot = path.join(targetPluginsRoot, "autoloop");
94
+ const marketplaceFile = path.join(resolvedCodexHome, ".agents", "plugins", "marketplace.json");
95
+ const manifestFile = path.join(bundleRoot, ".codex-plugin", "plugin.json");
96
+
97
+ if (!fileExists(manifestFile)) {
98
+ throw new Error(`未找到插件 manifest:${manifestFile}`);
99
+ }
100
+
101
+ assertPathInside(resolvedCodexHome, targetPluginRoot, "目标插件目录");
102
+
103
+ if (fileExists(targetPluginRoot)) {
104
+ if (!options.force) {
105
+ throw new Error(`目标插件目录已存在:${targetPluginRoot}。若要覆盖,请追加 --force。`);
106
+ }
107
+ fs.rmSync(targetPluginRoot, { recursive: true, force: true });
108
+ }
109
+
110
+ if (fileExists(legacyPluginRoot)) {
111
+ fs.rmSync(legacyPluginRoot, { recursive: true, force: true });
112
+ }
113
+
114
+ ensureDir(targetPluginsRoot);
115
+ ensureDir(targetPluginRoot);
116
+ copyBundleEntries(bundleRoot, targetPluginRoot);
117
+
118
+ const gitMetadataPath = path.join(targetPluginRoot, ".git");
119
+ if (fileExists(gitMetadataPath)) {
120
+ fs.rmSync(gitMetadataPath, { recursive: true, force: true });
121
+ }
122
+
123
+ ensureDir(path.dirname(marketplaceFile));
124
+ updateMarketplace(marketplaceFile);
125
+
126
+ return {
127
+ targetPluginRoot,
128
+ marketplaceFile,
129
+ };
130
+ }
@@ -0,0 +1,58 @@
1
+ export const sessionStatus = Object.freeze({
2
+ running: "running",
3
+ needsInput: "needs_input",
4
+ done: "done",
5
+ stopped: "stopped",
6
+ });
7
+
8
+ export const turnState = Object.freeze({
9
+ ready: "ready",
10
+ done: "done",
11
+ blockedMissingBacklog: "blocked_missing_backlog",
12
+ blockedInProgress: "blocked_in_progress",
13
+ blockedFailed: "blocked_failed",
14
+ blockedRisk: "blocked_risk",
15
+ blockedDependencies: "blocked_dependencies",
16
+ });
17
+
18
+ const turnStateLabels = Object.freeze({
19
+ [turnState.ready]: "可执行",
20
+ [turnState.done]: "已完成",
21
+ [turnState.blockedMissingBacklog]: "缺少 backlog",
22
+ [turnState.blockedInProgress]: "存在未收束任务",
23
+ [turnState.blockedFailed]: "存在失败或阻塞任务",
24
+ [turnState.blockedRisk]: "风险门限阻塞",
25
+ [turnState.blockedDependencies]: "依赖阻塞",
26
+ });
27
+
28
+ export function isBlockedTurnState(state) {
29
+ return String(state || "").startsWith("blocked_");
30
+ }
31
+
32
+ export function isReadyTurnState(state) {
33
+ return String(state || "") === turnState.ready;
34
+ }
35
+
36
+ export function isDoneTurnState(state) {
37
+ return String(state || "") === turnState.done;
38
+ }
39
+
40
+ export function renderTurnStateLabel(state) {
41
+ return turnStateLabels[String(state || "")] || "未知状态";
42
+ }
43
+
44
+ export function deriveSessionStatusFromTurnState(state) {
45
+ if (isReadyTurnState(state)) {
46
+ return sessionStatus.running;
47
+ }
48
+
49
+ if (isDoneTurnState(state)) {
50
+ return sessionStatus.done;
51
+ }
52
+
53
+ if (isBlockedTurnState(state)) {
54
+ return sessionStatus.needsInput;
55
+ }
56
+
57
+ return sessionStatus.needsInput;
58
+ }
@@ -0,0 +1,264 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { spawn, spawnSync } from "node:child_process";
4
+
5
+ import { ensureDir, nowIso, tailText, writeText } from "./common.mjs";
6
+
7
+ function runChild(command, args, options = {}) {
8
+ return new Promise((resolve) => {
9
+ const child = spawn(command, args, {
10
+ cwd: options.cwd,
11
+ env: {
12
+ ...process.env,
13
+ ...(options.env || {}),
14
+ },
15
+ stdio: ["pipe", "pipe", "pipe"],
16
+ shell: Boolean(options.shell),
17
+ });
18
+
19
+ let stdout = "";
20
+ let stderr = "";
21
+
22
+ child.stdout.on("data", (chunk) => {
23
+ stdout += chunk.toString();
24
+ });
25
+ child.stderr.on("data", (chunk) => {
26
+ stderr += chunk.toString();
27
+ });
28
+
29
+ if (options.stdin) {
30
+ child.stdin.write(options.stdin);
31
+ }
32
+ child.stdin.end();
33
+
34
+ child.on("error", (error) => {
35
+ resolve({
36
+ ok: false,
37
+ code: 1,
38
+ stdout,
39
+ stderr: String(error?.stack || error || ""),
40
+ });
41
+ });
42
+
43
+ child.on("close", (code) => {
44
+ resolve({
45
+ ok: code === 0,
46
+ code: code ?? 1,
47
+ stdout,
48
+ stderr,
49
+ });
50
+ });
51
+ });
52
+ }
53
+
54
+ function quoteForCmd(value) {
55
+ const normalized = String(value ?? "");
56
+ if (!normalized.length) {
57
+ return "\"\"";
58
+ }
59
+ if (!/[\s"&<>|^]/.test(normalized)) {
60
+ return normalized;
61
+ }
62
+ return `"${normalized.replace(/"/g, "\"\"")}"`;
63
+ }
64
+
65
+ function buildCmdCommandLine(executable, args) {
66
+ return [quoteForCmd(executable), ...args.map((item) => quoteForCmd(item))].join(" ");
67
+ }
68
+
69
+ function resolveCodexExecutable(explicitExecutable = "") {
70
+ if (explicitExecutable) {
71
+ return explicitExecutable;
72
+ }
73
+
74
+ if (process.platform !== "win32") {
75
+ return "codex";
76
+ }
77
+
78
+ const candidates = ["codex.cmd", "codex", "codex.ps1"];
79
+ for (const candidate of candidates) {
80
+ const result = spawnSync("where.exe", [candidate], {
81
+ encoding: "utf8",
82
+ shell: false,
83
+ });
84
+ if (result.status !== 0) {
85
+ continue;
86
+ }
87
+
88
+ const firstLine = String(result.stdout || "")
89
+ .split(/\r?\n/)
90
+ .map((line) => line.trim())
91
+ .find(Boolean);
92
+ if (firstLine) {
93
+ return firstLine;
94
+ }
95
+ }
96
+
97
+ return "codex";
98
+ }
99
+
100
+ function resolveCodexInvocation(explicitExecutable = "") {
101
+ const executable = resolveCodexExecutable(explicitExecutable);
102
+
103
+ if (process.platform === "win32" && /\.ps1$/i.test(executable)) {
104
+ return {
105
+ command: "pwsh",
106
+ argsPrefix: ["-NoLogo", "-NoProfile", "-File", executable],
107
+ shell: false,
108
+ };
109
+ }
110
+
111
+ if (process.platform === "win32" && /\.(cmd|bat)$/i.test(executable)) {
112
+ return {
113
+ command: "cmd.exe",
114
+ argsPrefix: ["/d", "/s", "/c"],
115
+ executable,
116
+ shell: false,
117
+ };
118
+ }
119
+
120
+ return {
121
+ command: executable,
122
+ argsPrefix: [],
123
+ shell: false,
124
+ };
125
+ }
126
+
127
+ function writeCodexRunArtifacts(runDir, prefix, result, finalMessage) {
128
+ writeText(path.join(runDir, `${prefix}-stdout.log`), result.stdout);
129
+ writeText(path.join(runDir, `${prefix}-stderr.log`), result.stderr);
130
+ writeText(path.join(runDir, `${prefix}-summary.txt`), [
131
+ `ok=${result.ok}`,
132
+ `code=${result.code}`,
133
+ `finished_at=${nowIso()}`,
134
+ "",
135
+ finalMessage,
136
+ ].join("\n"));
137
+ }
138
+
139
+ export async function runCodexTask({
140
+ context,
141
+ prompt,
142
+ runDir,
143
+ model = "",
144
+ executable = "",
145
+ sandbox = "workspace-write",
146
+ dangerouslyBypassSandbox = false,
147
+ jsonOutput = true,
148
+ outputSchemaFile = "",
149
+ outputPrefix = "codex",
150
+ ephemeral = false,
151
+ skipGitRepoCheck = false,
152
+ env = {},
153
+ }) {
154
+ ensureDir(runDir);
155
+
156
+ const lastMessageFile = path.join(runDir, `${outputPrefix}-last-message.txt`);
157
+ const invocation = resolveCodexInvocation(executable);
158
+ const codexArgs = ["exec", "-C", context.repoRoot];
159
+
160
+ if (model) {
161
+ codexArgs.push("--model", model);
162
+ }
163
+ if (dangerouslyBypassSandbox) {
164
+ codexArgs.push("--dangerously-bypass-approvals-and-sandbox");
165
+ } else {
166
+ codexArgs.push("--sandbox", sandbox);
167
+ }
168
+ if (skipGitRepoCheck) {
169
+ codexArgs.push("--skip-git-repo-check");
170
+ }
171
+ if (ephemeral) {
172
+ codexArgs.push("--ephemeral");
173
+ }
174
+ if (outputSchemaFile) {
175
+ codexArgs.push("--output-schema", outputSchemaFile);
176
+ }
177
+ if (jsonOutput) {
178
+ codexArgs.push("--json");
179
+ }
180
+ codexArgs.push("-o", lastMessageFile, "-");
181
+
182
+ const args = invocation.executable
183
+ ? [...invocation.argsPrefix, buildCmdCommandLine(invocation.executable, codexArgs)]
184
+ : [...invocation.argsPrefix, ...codexArgs];
185
+
186
+ const result = await runChild(invocation.command, args, {
187
+ cwd: context.repoRoot,
188
+ stdin: prompt,
189
+ env,
190
+ shell: invocation.shell,
191
+ });
192
+ const finalMessage = fs.existsSync(lastMessageFile)
193
+ ? fs.readFileSync(lastMessageFile, "utf8").trim()
194
+ : "";
195
+
196
+ writeText(path.join(runDir, `${outputPrefix}-prompt.md`), prompt);
197
+ writeCodexRunArtifacts(runDir, outputPrefix, result, finalMessage);
198
+
199
+ return { ...result, finalMessage };
200
+ }
201
+
202
+ export async function runCodexExec({ context, prompt, runDir, policy }) {
203
+ return runCodexTask({
204
+ context,
205
+ prompt,
206
+ runDir,
207
+ model: policy.codex.model,
208
+ executable: policy.codex.executable,
209
+ sandbox: policy.codex.sandbox,
210
+ dangerouslyBypassSandbox: policy.codex.dangerouslyBypassSandbox,
211
+ jsonOutput: policy.codex.jsonOutput,
212
+ outputPrefix: "codex",
213
+ });
214
+ }
215
+
216
+ export async function runShellCommand(context, commandLine, runDir, index) {
217
+ const result = await runChild("pwsh", [
218
+ "-NoLogo",
219
+ "-NoProfile",
220
+ "-Command",
221
+ commandLine,
222
+ ], {
223
+ cwd: context.repoRoot,
224
+ });
225
+
226
+ const prefix = String(index + 1).padStart(2, "0");
227
+ writeText(path.join(runDir, `${prefix}-verify-command.txt`), commandLine);
228
+ writeText(path.join(runDir, `${prefix}-verify-stdout.log`), result.stdout);
229
+ writeText(path.join(runDir, `${prefix}-verify-stderr.log`), result.stderr);
230
+
231
+ return { command: commandLine, ...result };
232
+ }
233
+
234
+ export async function runVerifyCommands(context, commands, runDir) {
235
+ const results = [];
236
+
237
+ for (const [index, command] of commands.entries()) {
238
+ const result = await runShellCommand(context, command, runDir, index);
239
+ results.push(result);
240
+ if (!result.ok) {
241
+ return {
242
+ ok: false,
243
+ results,
244
+ failed: result,
245
+ summary: [
246
+ `验证失败:${result.command}`,
247
+ "",
248
+ "stdout 尾部:",
249
+ tailText(result.stdout, 40),
250
+ "",
251
+ "stderr 尾部:",
252
+ tailText(result.stderr, 40),
253
+ ].join("\n").trim(),
254
+ };
255
+ }
256
+ }
257
+
258
+ return {
259
+ ok: true,
260
+ results,
261
+ failed: null,
262
+ summary: "全部验证命令通过。",
263
+ };
264
+ }
package/src/prompt.mjs ADDED
@@ -0,0 +1,116 @@
1
+ import { formatList } from "./common.mjs";
2
+
3
+ function normalizeDocEntry(doc) {
4
+ return String(doc || "").trim().replaceAll("\\", "/");
5
+ }
6
+
7
+ function isAgentsDoc(doc) {
8
+ const normalized = normalizeDocEntry(doc).toLowerCase();
9
+ if (!normalized) {
10
+ return false;
11
+ }
12
+
13
+ const segments = normalized.split("/");
14
+ return segments[segments.length - 1] === "agents.md";
15
+ }
16
+
17
+ function normalizeDocList(items) {
18
+ const result = [];
19
+ const seen = new Set();
20
+
21
+ for (const item of items || []) {
22
+ const normalized = normalizeDocEntry(item);
23
+ if (!normalized || isAgentsDoc(normalized)) {
24
+ continue;
25
+ }
26
+
27
+ const key = normalized.toLowerCase();
28
+ if (seen.has(key)) {
29
+ continue;
30
+ }
31
+
32
+ seen.add(key);
33
+ result.push(normalized);
34
+ }
35
+
36
+ return result;
37
+ }
38
+
39
+ function section(title, content) {
40
+ if (!content || !String(content).trim()) return "";
41
+ return `## ${title}\n${String(content).trim()}\n`;
42
+ }
43
+
44
+ function listSection(title, items) {
45
+ if (!Array.isArray(items) || !items.length) return "";
46
+ return section(title, formatList(items));
47
+ }
48
+
49
+ export function buildTaskPrompt({
50
+ task,
51
+ repoStateText,
52
+ verifyCommands,
53
+ requiredDocs = [],
54
+ constraints = [],
55
+ previousFailure = "",
56
+ failureHistory = [],
57
+ strategyIndex = 1,
58
+ maxStrategies = 1,
59
+ attemptIndex = 1,
60
+ maxAttemptsPerStrategy = 1,
61
+ }) {
62
+ const allDocs = normalizeDocList([
63
+ ...requiredDocs,
64
+ ...(task.docs || []),
65
+ ]);
66
+
67
+ const effectiveConstraints = constraints.length
68
+ ? constraints
69
+ : [
70
+ "所有文件修改必须用 apply_patch。",
71
+ "不得通过压缩代码、删空行、缩短命名来压行数。",
72
+ "Rust / TS / TSX / Vue / Python / Go 单文件强制拆分上限 400 行。",
73
+ "新增可见文案必须同时补齐 zh-CN 与 en-US。",
74
+ "完成后必须同步相关中文文档与联调文档。",
75
+ "完成前必须主动运行验证,不要停在分析层。",
76
+ ];
77
+
78
+ return [
79
+ "你在本地仓库中执行一个连续开发任务。",
80
+ "必须严格按仓库开发文档推进,不要偏离产品路线。",
81
+ "当前执行模式是 Ralph Loop:失败后先修复重试,连续失败后必须换一种思路继续。",
82
+ "",
83
+ listSection("开发前必读", allDocs),
84
+ section("当前循环阶段", [
85
+ `- 当前策略轮次:${strategyIndex}/${maxStrategies}`,
86
+ `- 当前策略内重试:${attemptIndex}/${maxAttemptsPerStrategy}`,
87
+ strategyIndex > 1 ? "- 这是换路后的新一轮尝试,禁止简单重复上一轮做法。" : "",
88
+ ].filter(Boolean).join("\n")),
89
+ section("当前任务", [
90
+ `- 标题:${task.title}`,
91
+ `- 编号:${task.id}`,
92
+ `- 优先级:${task.priority || "P2"}`,
93
+ `- 风险等级:${task.risk || "low"}`,
94
+ `- 目标:${task.goal || "按文档完成当前工作包。"}`,
95
+ ].join("\n")),
96
+ listSection("涉及路径", task.paths || []),
97
+ listSection("验收条件", task.acceptance || []),
98
+ listSection("实现约束", effectiveConstraints),
99
+ repoStateText ? section("仓库当前状态", repoStateText) : "",
100
+ failureHistory.length
101
+ ? section("已失败尝试摘要", failureHistory.slice(-4).map((item) => (
102
+ `- 策略 ${item.strategyIndex} / 尝试 ${item.attemptIndex} / ${item.kind}:${item.summary}`
103
+ )).join("\n"))
104
+ : "",
105
+ previousFailure ? section("上一轮失败信息", previousFailure) : "",
106
+ listSection("完成前必须运行的验证", verifyCommands),
107
+ section("交付要求", [
108
+ "1. 直接在仓库中完成实现。",
109
+ "2. 运行验证;若失败,先分析根因,再修复并重跑。",
110
+ "3. 同一路径连续失败后,必须明确换一种实现或排查思路。",
111
+ "4. 除非遇到外部权限、环境损坏、文档缺口等硬阻塞,否则不要停止。",
112
+ "5. 用简洁中文总结变更、验证结果和剩余风险。",
113
+ "6. 不要提问,不要等待确认,直接完成。",
114
+ ].join("\n")),
115
+ ].filter(Boolean).join("\n");
116
+ }