helloloop 0.1.1 → 0.1.2
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 +6 -6
- package/LICENSE +176 -0
- package/README.md +106 -115
- package/package.json +7 -3
- package/skills/helloloop/SKILL.md +35 -31
- package/src/analyze_prompt.mjs +75 -0
- package/src/analyzer.mjs +208 -0
- package/src/cli.mjs +74 -10
- 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 +1 -7
- package/src/process.mjs +31 -129
- package/src/prompt.mjs +10 -12
- package/src/shell_invocation.mjs +211 -0
- package/templates/analysis-output.schema.json +144 -0
package/src/prompt.mjs
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import { formatList } from "./common.mjs";
|
|
2
|
+
import {
|
|
3
|
+
hasCustomProjectConstraints,
|
|
4
|
+
listMandatoryGuardrails,
|
|
5
|
+
resolveProjectConstraints,
|
|
6
|
+
} from "./guardrails.mjs";
|
|
2
7
|
|
|
3
8
|
function normalizeDocEntry(doc) {
|
|
4
9
|
return String(doc || "").trim().replaceAll("\\", "/");
|
|
@@ -63,17 +68,9 @@ export function buildTaskPrompt({
|
|
|
63
68
|
...requiredDocs,
|
|
64
69
|
...(task.docs || []),
|
|
65
70
|
]);
|
|
66
|
-
|
|
67
|
-
const effectiveConstraints = constraints
|
|
68
|
-
|
|
69
|
-
: [
|
|
70
|
-
"所有文件修改必须用 apply_patch。",
|
|
71
|
-
"不得通过压缩代码、删空行、缩短命名来压行数。",
|
|
72
|
-
"Rust / TS / TSX / Vue / Python / Go 单文件强制拆分上限 400 行。",
|
|
73
|
-
"新增可见文案必须同时补齐 zh-CN 与 en-US。",
|
|
74
|
-
"完成后必须同步相关中文文档与联调文档。",
|
|
75
|
-
"完成前必须主动运行验证,不要停在分析层。",
|
|
76
|
-
];
|
|
71
|
+
const mandatoryGuardrails = listMandatoryGuardrails();
|
|
72
|
+
const effectiveConstraints = resolveProjectConstraints(constraints);
|
|
73
|
+
const usingFallbackConstraints = !hasCustomProjectConstraints(constraints);
|
|
77
74
|
|
|
78
75
|
return [
|
|
79
76
|
"你在本地仓库中执行一个连续开发任务。",
|
|
@@ -95,7 +92,8 @@ export function buildTaskPrompt({
|
|
|
95
92
|
].join("\n")),
|
|
96
93
|
listSection("涉及路径", task.paths || []),
|
|
97
94
|
listSection("验收条件", task.acceptance || []),
|
|
98
|
-
listSection("
|
|
95
|
+
listSection("内建安全底线", mandatoryGuardrails),
|
|
96
|
+
listSection(usingFallbackConstraints ? "默认工程约束(文档未明确时生效)" : "项目/用户约束", effectiveConstraints),
|
|
99
97
|
repoStateText ? section("仓库当前状态", repoStateText) : "",
|
|
100
98
|
failureHistory.length
|
|
101
99
|
? section("已失败尝试摘要", failureHistory.slice(-4).map((item) => (
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
function createUnavailableInvocation(message) {
|
|
4
|
+
return {
|
|
5
|
+
command: "",
|
|
6
|
+
argsPrefix: [],
|
|
7
|
+
shell: false,
|
|
8
|
+
error: message,
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function parseWindowsCommandMatches(output) {
|
|
13
|
+
return String(output || "")
|
|
14
|
+
.split(/\r?\n/)
|
|
15
|
+
.map((line) => line.trim())
|
|
16
|
+
.filter(Boolean);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function hasCommand(command, platform = process.platform) {
|
|
20
|
+
if (platform === "win32") {
|
|
21
|
+
const result = spawnSync("where.exe", [command], {
|
|
22
|
+
encoding: "utf8",
|
|
23
|
+
shell: false,
|
|
24
|
+
});
|
|
25
|
+
return result.status === 0;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const result = spawnSync("sh", ["-lc", `command -v ${command}`], {
|
|
29
|
+
encoding: "utf8",
|
|
30
|
+
shell: false,
|
|
31
|
+
});
|
|
32
|
+
return result.status === 0;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function findWindowsCommandPaths(command, resolver) {
|
|
36
|
+
const lookup = resolver || ((name) => {
|
|
37
|
+
const result = spawnSync("where.exe", [name], {
|
|
38
|
+
encoding: "utf8",
|
|
39
|
+
shell: false,
|
|
40
|
+
});
|
|
41
|
+
return result.status === 0 ? parseWindowsCommandMatches(result.stdout) : [];
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return lookup(command);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function resolveWindowsPowerShellHost(options = {}) {
|
|
48
|
+
const commandExists = options.commandExists || ((command) => hasCommand(command, "win32"));
|
|
49
|
+
|
|
50
|
+
if (commandExists("pwsh")) {
|
|
51
|
+
return {
|
|
52
|
+
command: "pwsh",
|
|
53
|
+
shell: false,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (commandExists("powershell")) {
|
|
58
|
+
return {
|
|
59
|
+
command: "powershell",
|
|
60
|
+
shell: false,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function resolveWindowsCommandShell(options = {}) {
|
|
68
|
+
const commandExists = options.commandExists || ((command) => hasCommand(command, "win32"));
|
|
69
|
+
const preferredHosts = [
|
|
70
|
+
{
|
|
71
|
+
check: "pwsh",
|
|
72
|
+
invocation: {
|
|
73
|
+
command: "pwsh",
|
|
74
|
+
argsPrefix: ["-NoLogo", "-NoProfile", "-Command"],
|
|
75
|
+
shell: false,
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
check: "bash",
|
|
80
|
+
invocation: {
|
|
81
|
+
command: "bash",
|
|
82
|
+
argsPrefix: ["-lc"],
|
|
83
|
+
shell: false,
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
check: "powershell",
|
|
88
|
+
invocation: {
|
|
89
|
+
command: "powershell",
|
|
90
|
+
argsPrefix: ["-NoLogo", "-NoProfile", "-Command"],
|
|
91
|
+
shell: false,
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
for (const host of preferredHosts) {
|
|
97
|
+
if (commandExists(host.check)) {
|
|
98
|
+
return host.invocation;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function resolvePosixCommandShell(options = {}) {
|
|
106
|
+
const platform = options.platform || process.platform;
|
|
107
|
+
const commandExists = options.commandExists || ((command) => hasCommand(command, platform));
|
|
108
|
+
|
|
109
|
+
if (commandExists("bash")) {
|
|
110
|
+
return {
|
|
111
|
+
command: "bash",
|
|
112
|
+
argsPrefix: ["-lc"],
|
|
113
|
+
shell: false,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
command: "sh",
|
|
119
|
+
argsPrefix: ["-lc"],
|
|
120
|
+
shell: false,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function isCmdLikeExecutable(executable) {
|
|
125
|
+
return /\.(cmd|bat)$/i.test(String(executable || ""));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function resolveWindowsCodexExecutable(options = {}) {
|
|
129
|
+
const explicitExecutable = String(options.explicitExecutable || "").trim();
|
|
130
|
+
const findCommandPaths = options.findCommandPaths || ((command) => findWindowsCommandPaths(command));
|
|
131
|
+
|
|
132
|
+
if (explicitExecutable) {
|
|
133
|
+
return explicitExecutable;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const searchOrder = ["codex.ps1", "codex.exe", "codex"];
|
|
137
|
+
for (const query of searchOrder) {
|
|
138
|
+
const safeMatch = findCommandPaths(query).find((candidate) => !isCmdLikeExecutable(candidate));
|
|
139
|
+
if (safeMatch) {
|
|
140
|
+
return safeMatch;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return "";
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function resolveVerifyShellInvocation(options = {}) {
|
|
148
|
+
const platform = options.platform || process.platform;
|
|
149
|
+
|
|
150
|
+
if (platform === "win32") {
|
|
151
|
+
const host = resolveWindowsCommandShell(options);
|
|
152
|
+
if (!host) {
|
|
153
|
+
return createUnavailableInvocation(
|
|
154
|
+
"Windows 环境需要 pwsh、bash(如 Git Bash)或 powershell 才能安全执行验证命令;HelloLoop 已禁止回退到 cmd.exe。",
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
return host;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return resolvePosixCommandShell(options);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function resolveCodexInvocation(options = {}) {
|
|
164
|
+
const platform = options.platform || process.platform;
|
|
165
|
+
const explicitExecutable = String(options.explicitExecutable || "").trim();
|
|
166
|
+
|
|
167
|
+
if (platform !== "win32") {
|
|
168
|
+
return {
|
|
169
|
+
command: explicitExecutable || "codex",
|
|
170
|
+
argsPrefix: [],
|
|
171
|
+
shell: false,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const executable = resolveWindowsCodexExecutable({
|
|
176
|
+
explicitExecutable,
|
|
177
|
+
findCommandPaths: options.findCommandPaths,
|
|
178
|
+
});
|
|
179
|
+
if (!executable) {
|
|
180
|
+
return createUnavailableInvocation(
|
|
181
|
+
"未找到可安全执行的 codex 入口。Windows 环境需要 codex.ps1、codex.exe 或其他非 cmd 的可执行入口。",
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (isCmdLikeExecutable(executable)) {
|
|
186
|
+
return createUnavailableInvocation(
|
|
187
|
+
`HelloLoop 在 Windows 已禁止通过 cmd/bat 启动 Codex:${executable}`,
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (/\.ps1$/i.test(executable)) {
|
|
192
|
+
const host = resolveWindowsPowerShellHost(options);
|
|
193
|
+
if (!host) {
|
|
194
|
+
return createUnavailableInvocation(
|
|
195
|
+
`需要 pwsh 或 powershell 才能安全执行 ${executable};HelloLoop 不会回退到 cmd.exe。`,
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
command: host.command,
|
|
201
|
+
argsPrefix: ["-NoLogo", "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", executable],
|
|
202
|
+
shell: host.shell,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
command: executable,
|
|
208
|
+
argsPrefix: [],
|
|
209
|
+
shell: false,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"type": "object",
|
|
4
|
+
"additionalProperties": false,
|
|
5
|
+
"required": [
|
|
6
|
+
"project",
|
|
7
|
+
"summary",
|
|
8
|
+
"tasks"
|
|
9
|
+
],
|
|
10
|
+
"properties": {
|
|
11
|
+
"project": {
|
|
12
|
+
"type": "string",
|
|
13
|
+
"minLength": 1
|
|
14
|
+
},
|
|
15
|
+
"summary": {
|
|
16
|
+
"type": "object",
|
|
17
|
+
"additionalProperties": false,
|
|
18
|
+
"required": [
|
|
19
|
+
"currentState",
|
|
20
|
+
"implemented",
|
|
21
|
+
"remaining",
|
|
22
|
+
"nextAction"
|
|
23
|
+
],
|
|
24
|
+
"properties": {
|
|
25
|
+
"currentState": {
|
|
26
|
+
"type": "string",
|
|
27
|
+
"minLength": 1
|
|
28
|
+
},
|
|
29
|
+
"implemented": {
|
|
30
|
+
"type": "array",
|
|
31
|
+
"items": {
|
|
32
|
+
"type": "string"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"remaining": {
|
|
36
|
+
"type": "array",
|
|
37
|
+
"items": {
|
|
38
|
+
"type": "string"
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"nextAction": {
|
|
42
|
+
"type": "string",
|
|
43
|
+
"minLength": 1
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
"constraints": {
|
|
48
|
+
"type": "array",
|
|
49
|
+
"items": {
|
|
50
|
+
"type": "string"
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
"tasks": {
|
|
54
|
+
"type": "array",
|
|
55
|
+
"minItems": 1,
|
|
56
|
+
"items": {
|
|
57
|
+
"type": "object",
|
|
58
|
+
"additionalProperties": false,
|
|
59
|
+
"required": [
|
|
60
|
+
"id",
|
|
61
|
+
"title",
|
|
62
|
+
"status",
|
|
63
|
+
"priority",
|
|
64
|
+
"risk",
|
|
65
|
+
"goal",
|
|
66
|
+
"docs",
|
|
67
|
+
"paths",
|
|
68
|
+
"acceptance"
|
|
69
|
+
],
|
|
70
|
+
"properties": {
|
|
71
|
+
"id": {
|
|
72
|
+
"type": "string",
|
|
73
|
+
"minLength": 1
|
|
74
|
+
},
|
|
75
|
+
"title": {
|
|
76
|
+
"type": "string",
|
|
77
|
+
"minLength": 1
|
|
78
|
+
},
|
|
79
|
+
"status": {
|
|
80
|
+
"type": "string",
|
|
81
|
+
"enum": [
|
|
82
|
+
"pending",
|
|
83
|
+
"done",
|
|
84
|
+
"blocked"
|
|
85
|
+
]
|
|
86
|
+
},
|
|
87
|
+
"priority": {
|
|
88
|
+
"type": "string",
|
|
89
|
+
"enum": [
|
|
90
|
+
"P0",
|
|
91
|
+
"P1",
|
|
92
|
+
"P2",
|
|
93
|
+
"P3"
|
|
94
|
+
]
|
|
95
|
+
},
|
|
96
|
+
"risk": {
|
|
97
|
+
"type": "string",
|
|
98
|
+
"enum": [
|
|
99
|
+
"low",
|
|
100
|
+
"medium",
|
|
101
|
+
"high",
|
|
102
|
+
"critical"
|
|
103
|
+
]
|
|
104
|
+
},
|
|
105
|
+
"goal": {
|
|
106
|
+
"type": "string",
|
|
107
|
+
"minLength": 1
|
|
108
|
+
},
|
|
109
|
+
"docs": {
|
|
110
|
+
"type": "array",
|
|
111
|
+
"items": {
|
|
112
|
+
"type": "string"
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
"paths": {
|
|
116
|
+
"type": "array",
|
|
117
|
+
"items": {
|
|
118
|
+
"type": "string"
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
"acceptance": {
|
|
122
|
+
"type": "array",
|
|
123
|
+
"minItems": 1,
|
|
124
|
+
"items": {
|
|
125
|
+
"type": "string"
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
"dependsOn": {
|
|
129
|
+
"type": "array",
|
|
130
|
+
"items": {
|
|
131
|
+
"type": "string"
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
"verify": {
|
|
135
|
+
"type": "array",
|
|
136
|
+
"items": {
|
|
137
|
+
"type": "string"
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|