helloloop 0.1.0 → 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.
@@ -0,0 +1,220 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ import { readJson } from "./common.mjs";
5
+ import { expandDocumentEntries } from "./doc_loader.mjs";
6
+ import {
7
+ findRepoRootFromPath,
8
+ isDocFile,
9
+ isDocsDirectory,
10
+ looksLikeProjectRoot,
11
+ normalizeForRepo,
12
+ pathExists,
13
+ resolveAbsolute,
14
+ uniquePaths,
15
+ } from "./discovery_paths.mjs";
16
+
17
+ function loadExistingRequiredDocs(repoRoot, configDirName) {
18
+ const projectFile = path.join(repoRoot, configDirName, "project.json");
19
+ if (!pathExists(projectFile)) {
20
+ return [];
21
+ }
22
+
23
+ const projectConfig = readJson(projectFile);
24
+ const entries = Array.isArray(projectConfig.requiredDocs) ? projectConfig.requiredDocs : [];
25
+ const resolved = expandDocumentEntries(repoRoot, entries);
26
+ return resolved.length ? entries : [];
27
+ }
28
+
29
+ function readDocPreview(docEntries, cwd) {
30
+ let remaining = 24000;
31
+ const snippets = [];
32
+
33
+ for (const entry of docEntries) {
34
+ const expanded = expandDocumentEntries(cwd, [entry]);
35
+ for (const file of expanded) {
36
+ if (remaining <= 0) {
37
+ return snippets.join("\n");
38
+ }
39
+
40
+ const content = fs.readFileSync(file.absolutePath, "utf8");
41
+ const slice = content.slice(0, Math.min(content.length, remaining));
42
+ snippets.push(slice);
43
+ remaining -= slice.length;
44
+ }
45
+ }
46
+
47
+ return snippets.join("\n");
48
+ }
49
+
50
+ function normalizePathHint(rawValue) {
51
+ const trimmed = String(rawValue || "").trim().replace(/^["'`(<\[]+|[>"'`)\].,;:]+$/g, "");
52
+ if (/^\/[A-Za-z]:[\\/]/.test(trimmed)) {
53
+ return trimmed.slice(1);
54
+ }
55
+ return trimmed;
56
+ }
57
+
58
+ function extractPathHintsFromText(text) {
59
+ const hints = new Set();
60
+ const normalizedText = String(text || "");
61
+ const windowsMatches = normalizedText.match(/\/?[A-Za-z]:[\\/][^\s"'`<>()\]]+/g) || [];
62
+ const posixMatches = normalizedText.match(/(?:^|[\s("'`])\/[^\s"'`<>()\]]+/g) || [];
63
+
64
+ for (const rawMatch of [...windowsMatches, ...posixMatches]) {
65
+ const normalized = normalizePathHint(rawMatch);
66
+ if (normalized) {
67
+ hints.add(normalized);
68
+ }
69
+ }
70
+
71
+ return [...hints];
72
+ }
73
+
74
+ function extractRepoNameHintsFromText(text) {
75
+ const hints = new Set();
76
+ const matches = String(text || "").match(/`([A-Za-z0-9._-]{3,})`/g) || [];
77
+
78
+ for (const match of matches) {
79
+ const value = match.slice(1, -1);
80
+ if (/^(docs?|src|tests?|readme|sqlite|wal|sqlcipher)$/i.test(value)) {
81
+ continue;
82
+ }
83
+ hints.add(value.toLowerCase());
84
+ }
85
+
86
+ return [...hints];
87
+ }
88
+
89
+ function listNearbyProjectCandidates(searchRoot) {
90
+ if (!pathExists(searchRoot) || !fs.statSync(searchRoot).isDirectory()) {
91
+ return [];
92
+ }
93
+
94
+ return fs.readdirSync(searchRoot, { withFileTypes: true })
95
+ .filter((entry) => entry.isDirectory())
96
+ .map((entry) => path.join(searchRoot, entry.name))
97
+ .filter((directoryPath) => looksLikeProjectRoot(directoryPath));
98
+ }
99
+
100
+ export function inferRepoFromDocs(docEntries, cwd) {
101
+ const expandedFiles = expandDocumentEntries(cwd, docEntries);
102
+ const absoluteEntries = expandedFiles.map((item) => item.absolutePath);
103
+
104
+ for (const absolutePath of absoluteEntries) {
105
+ const repoRoot = findRepoRootFromPath(absolutePath);
106
+ if (repoRoot) {
107
+ return {
108
+ repoRoot,
109
+ candidates: [repoRoot],
110
+ source: "ancestor",
111
+ };
112
+ }
113
+ }
114
+
115
+ const previewText = readDocPreview(docEntries, cwd);
116
+ const pathHintCandidates = extractPathHintsFromText(previewText)
117
+ .map((item) => resolveAbsolute(item, cwd))
118
+ .filter((candidate) => pathExists(candidate))
119
+ .map((candidate) => (fs.statSync(candidate).isDirectory() ? candidate : path.dirname(candidate)))
120
+ .map((candidate) => findRepoRootFromPath(candidate) || (looksLikeProjectRoot(candidate) ? candidate : ""))
121
+ .filter(Boolean);
122
+
123
+ const uniquePathCandidates = uniquePaths(pathHintCandidates);
124
+ if (uniquePathCandidates.length === 1) {
125
+ return {
126
+ repoRoot: uniquePathCandidates[0],
127
+ candidates: uniquePathCandidates,
128
+ source: "doc_path_hint",
129
+ };
130
+ }
131
+
132
+ const repoNameHints = extractRepoNameHintsFromText(previewText);
133
+ const searchRoots = uniquePaths(
134
+ absoluteEntries.flatMap((absolutePath) => {
135
+ const directory = fs.statSync(absolutePath).isDirectory()
136
+ ? absolutePath
137
+ : path.dirname(absolutePath);
138
+ const parent = path.dirname(directory);
139
+ return [directory, parent, path.dirname(parent)];
140
+ }),
141
+ );
142
+ const nearbyCandidates = uniquePaths(
143
+ searchRoots.flatMap((searchRoot) => listNearbyProjectCandidates(searchRoot)),
144
+ );
145
+ const namedCandidates = nearbyCandidates.filter((directoryPath) => (
146
+ repoNameHints.includes(path.basename(directoryPath).toLowerCase())
147
+ ));
148
+
149
+ if (namedCandidates.length === 1) {
150
+ return {
151
+ repoRoot: namedCandidates[0],
152
+ candidates: namedCandidates,
153
+ source: "doc_repo_name_hint",
154
+ };
155
+ }
156
+
157
+ return {
158
+ repoRoot: "",
159
+ candidates: uniquePaths([...uniquePathCandidates, ...namedCandidates]),
160
+ source: "",
161
+ };
162
+ }
163
+
164
+ export function inferDocsForRepo(repoRoot, cwd, configDirName) {
165
+ const existingRequiredDocs = loadExistingRequiredDocs(repoRoot, configDirName);
166
+ if (existingRequiredDocs.length) {
167
+ return {
168
+ docsEntries: existingRequiredDocs,
169
+ source: "existing_state",
170
+ candidates: existingRequiredDocs,
171
+ };
172
+ }
173
+
174
+ const cwdIsDocSource = pathExists(cwd) && (isDocFile(cwd) || isDocsDirectory(cwd));
175
+ if (cwdIsDocSource) {
176
+ return {
177
+ docsEntries: [normalizeForRepo(repoRoot, cwd)],
178
+ source: "cwd",
179
+ candidates: [cwd],
180
+ };
181
+ }
182
+
183
+ const repoDocsCandidates = uniquePaths([
184
+ path.join(repoRoot, "docs"),
185
+ path.join(repoRoot, "Docs"),
186
+ ].filter((candidate) => isDocsDirectory(candidate)));
187
+
188
+ if (repoDocsCandidates.length === 1) {
189
+ return {
190
+ docsEntries: [normalizeForRepo(repoRoot, repoDocsCandidates[0])],
191
+ source: "repo_docs",
192
+ candidates: repoDocsCandidates,
193
+ };
194
+ }
195
+
196
+ return {
197
+ docsEntries: [],
198
+ source: "",
199
+ candidates: repoDocsCandidates,
200
+ };
201
+ }
202
+
203
+ export function renderMissingRepoMessage(docEntries, candidates) {
204
+ return [
205
+ "无法自动确定要开发的项目仓库路径。",
206
+ docEntries.length ? `已找到开发文档:${docEntries.join(", ")}` : "",
207
+ candidates.length > 1 ? `本地发现多个候选项目:${candidates.map((item) => path.basename(item)).join(", ")}` : "",
208
+ "请补充项目仓库路径后重试,例如:",
209
+ "npx helloloop --repo <PROJECT_ROOT>",
210
+ ].filter(Boolean).join("\n");
211
+ }
212
+
213
+ export function renderMissingDocsMessage(repoRoot) {
214
+ return [
215
+ "无法自动确定开发文档位置。",
216
+ `已找到项目仓库:${repoRoot}`,
217
+ "请补充开发文档路径后重试,例如:",
218
+ "npx helloloop --docs <DOCS_PATH>",
219
+ ].join("\n");
220
+ }
@@ -0,0 +1,166 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ import { fileExists } from "./common.mjs";
5
+
6
+ const DOC_FILE_SUFFIX = new Set([
7
+ ".md",
8
+ ".markdown",
9
+ ".mdx",
10
+ ".txt",
11
+ ".rst",
12
+ ".adoc",
13
+ ]);
14
+
15
+ const DOC_DIR_NAMES = new Set([
16
+ "docs",
17
+ "doc",
18
+ "documentation",
19
+ ]);
20
+
21
+ const PROJECT_MARKERS = [
22
+ ".git",
23
+ "package.json",
24
+ "pyproject.toml",
25
+ "Cargo.toml",
26
+ "go.mod",
27
+ "pom.xml",
28
+ "composer.json",
29
+ ];
30
+
31
+ const PROJECT_DIR_MARKERS = [
32
+ "src",
33
+ "app",
34
+ "apps",
35
+ "packages",
36
+ "tests",
37
+ ];
38
+
39
+ function directoryContainsDocs(directoryPath) {
40
+ if (!pathExists(directoryPath) || !fs.statSync(directoryPath).isDirectory()) {
41
+ return false;
42
+ }
43
+
44
+ const entries = fs.readdirSync(directoryPath, { withFileTypes: true });
45
+ return entries.some((entry) => (
46
+ entry.isFile() && DOC_FILE_SUFFIX.has(path.extname(entry.name).toLowerCase())
47
+ ));
48
+ }
49
+
50
+ function walkUpDirectories(startPath) {
51
+ const directories = [];
52
+ let current = fs.statSync(startPath).isDirectory()
53
+ ? startPath
54
+ : path.dirname(startPath);
55
+
56
+ while (true) {
57
+ directories.push(current);
58
+ const parent = path.dirname(current);
59
+ if (parent === current) {
60
+ break;
61
+ }
62
+ current = parent;
63
+ }
64
+
65
+ return directories;
66
+ }
67
+
68
+ export function resolveAbsolute(rawPath, cwd) {
69
+ return path.isAbsolute(rawPath)
70
+ ? path.resolve(rawPath)
71
+ : path.resolve(cwd, rawPath);
72
+ }
73
+
74
+ export function pathExists(targetPath) {
75
+ return fileExists(targetPath);
76
+ }
77
+
78
+ export function isDocFile(targetPath) {
79
+ return pathExists(targetPath)
80
+ && fs.statSync(targetPath).isFile()
81
+ && DOC_FILE_SUFFIX.has(path.extname(targetPath).toLowerCase());
82
+ }
83
+
84
+ export function looksLikeProjectRoot(directoryPath) {
85
+ if (!pathExists(directoryPath) || !fs.statSync(directoryPath).isDirectory()) {
86
+ return false;
87
+ }
88
+
89
+ if (PROJECT_MARKERS.some((name) => pathExists(path.join(directoryPath, name)))) {
90
+ return true;
91
+ }
92
+
93
+ return PROJECT_DIR_MARKERS.some((name) => pathExists(path.join(directoryPath, name)));
94
+ }
95
+
96
+ export function isDocsDirectory(directoryPath) {
97
+ if (!pathExists(directoryPath) || !fs.statSync(directoryPath).isDirectory()) {
98
+ return false;
99
+ }
100
+
101
+ const baseName = path.basename(directoryPath).toLowerCase();
102
+ if (DOC_DIR_NAMES.has(baseName)) {
103
+ return true;
104
+ }
105
+
106
+ return directoryContainsDocs(directoryPath) && !looksLikeProjectRoot(directoryPath);
107
+ }
108
+
109
+ export function findRepoRootFromPath(startPath) {
110
+ if (!pathExists(startPath)) {
111
+ return "";
112
+ }
113
+
114
+ for (const directory of walkUpDirectories(startPath)) {
115
+ if (pathExists(path.join(directory, ".git"))) {
116
+ return directory;
117
+ }
118
+ }
119
+
120
+ if (fs.statSync(startPath).isDirectory() && looksLikeProjectRoot(startPath)) {
121
+ return startPath;
122
+ }
123
+
124
+ return "";
125
+ }
126
+
127
+ export function normalizeForRepo(repoRoot, targetPath) {
128
+ const relative = path.relative(repoRoot, targetPath);
129
+ if (relative && !relative.startsWith("..") && !path.isAbsolute(relative)) {
130
+ return relative.replaceAll("\\", "/");
131
+ }
132
+
133
+ return targetPath.replaceAll("\\", "/");
134
+ }
135
+
136
+ export function uniquePaths(items) {
137
+ const seen = new Set();
138
+ return items.filter((item) => {
139
+ const key = path.resolve(item).toLowerCase();
140
+ if (seen.has(key)) {
141
+ return false;
142
+ }
143
+ seen.add(key);
144
+ return true;
145
+ });
146
+ }
147
+
148
+ export function classifyExplicitPath(inputPath) {
149
+ if (!inputPath) {
150
+ return { kind: "", absolutePath: "" };
151
+ }
152
+
153
+ if (!pathExists(inputPath)) {
154
+ return { kind: "missing", absolutePath: inputPath };
155
+ }
156
+
157
+ if (isDocFile(inputPath) || isDocsDirectory(inputPath)) {
158
+ return { kind: "docs", absolutePath: inputPath };
159
+ }
160
+
161
+ if (fs.statSync(inputPath).isDirectory()) {
162
+ return { kind: "repo", absolutePath: inputPath };
163
+ }
164
+
165
+ return { kind: "unsupported", absolutePath: inputPath };
166
+ }
@@ -0,0 +1,59 @@
1
+ function normalizeRule(rule) {
2
+ return String(rule || "").trim();
3
+ }
4
+
5
+ function uniqueRules(items) {
6
+ const result = [];
7
+ const seen = new Set();
8
+
9
+ for (const item of items || []) {
10
+ const normalized = normalizeRule(item);
11
+ if (!normalized) {
12
+ continue;
13
+ }
14
+
15
+ const key = normalized.toLowerCase();
16
+ if (seen.has(key)) {
17
+ continue;
18
+ }
19
+
20
+ seen.add(key);
21
+ result.push(normalized);
22
+ }
23
+
24
+ return result;
25
+ }
26
+
27
+ const mandatoryGuardrails = [
28
+ "所有 shell 操作优先使用结构化命令与参数,避免字符串拼接命令。",
29
+ "涉及路径的 shell 操作必须正确引用路径,避免空格、中文、特殊字符造成路径逃逸。",
30
+ "涉及多路径或多子命令时必须拆成多次独立执行,禁止把多个路径操作拼接进单条命令。",
31
+ "Windows 环境禁止使用 cmd /c、cmd.exe、Start-Process cmd 或任何 cmd 嵌套命令;只允许 pwsh、bash(如 Git Bash)或 powershell 这类安全 shell。",
32
+ "涉及删除、移动、覆盖等危险文件操作前,必须先确认目标路径位于当前仓库或用户明确允许的目录内。",
33
+ "禁止执行 EHRB 高风险命令或其等价危险操作,例如 rm -rf /、git reset --hard、DROP DATABASE、FLUSHALL 等。",
34
+ "不得把密钥、令牌、.env、PII、真实绝对隐私路径写入代码、日志、文档和最终输出。",
35
+ "目标实现必须兼容 Windows、macOS、Linux,禁止硬编码平台专属路径分隔符或 shell 语法。",
36
+ ];
37
+
38
+ const defaultProjectConstraints = [
39
+ "代码是事实源;文档与代码冲突时,以当前代码、测试和真实目录结构为准。",
40
+ "产出必须达到专业级水准:架构清晰、实现完整、表达专业,不接受“能用就行”的交付。",
41
+ "所有文件修改必须使用当前环境提供的安全编辑方式,例如 apply_patch,而不是危险的原地批量命令。",
42
+ "不得通过压缩代码、删除必要空行、缩短命名来规避体积控制;单文件超过 400 行后按职责拆分。",
43
+ "不添加不必要的抽象层;仅为复杂逻辑添加必要注释,新增公共函数补简洁说明。",
44
+ "完成前必须主动运行验证;失败后先分析根因,再修复并重跑。",
45
+ "不允许静默失败、静默降级、静默回退;遇到阻塞必须明确说明原因。",
46
+ ];
47
+
48
+ export function listMandatoryGuardrails() {
49
+ return [...mandatoryGuardrails];
50
+ }
51
+
52
+ export function hasCustomProjectConstraints(items = []) {
53
+ return uniqueRules(items).length > 0;
54
+ }
55
+
56
+ export function resolveProjectConstraints(items = []) {
57
+ const customRules = uniqueRules(items);
58
+ return customRules.length ? customRules : [...defaultProjectConstraints];
59
+ }
package/src/install.mjs CHANGED
@@ -10,6 +10,7 @@ const __dirname = path.dirname(__filename);
10
10
 
11
11
  export const runtimeBundleEntries = [
12
12
  ".codex-plugin",
13
+ "LICENSE",
13
14
  "README.md",
14
15
  "bin",
15
16
  "package.json",
@@ -73,8 +74,6 @@ function updateMarketplace(marketplaceFile) {
73
74
  category: "Coding",
74
75
  };
75
76
 
76
- marketplace.plugins = marketplace.plugins.filter((plugin) => plugin?.name !== "autoloop");
77
-
78
77
  const existingIndex = marketplace.plugins.findIndex((plugin) => plugin?.name === "helloloop");
79
78
  if (existingIndex >= 0) {
80
79
  marketplace.plugins.splice(existingIndex, 1, nextEntry);
@@ -90,7 +89,6 @@ export function installPluginBundle(options = {}) {
90
89
  const resolvedCodexHome = resolveCodexHome(options.codexHome);
91
90
  const targetPluginsRoot = path.join(resolvedCodexHome, "plugins");
92
91
  const targetPluginRoot = path.join(targetPluginsRoot, "helloloop");
93
- const legacyPluginRoot = path.join(targetPluginsRoot, "autoloop");
94
92
  const marketplaceFile = path.join(resolvedCodexHome, ".agents", "plugins", "marketplace.json");
95
93
  const manifestFile = path.join(bundleRoot, ".codex-plugin", "plugin.json");
96
94
 
@@ -107,10 +105,6 @@ export function installPluginBundle(options = {}) {
107
105
  fs.rmSync(targetPluginRoot, { recursive: true, force: true });
108
106
  }
109
107
 
110
- if (fileExists(legacyPluginRoot)) {
111
- fs.rmSync(legacyPluginRoot, { recursive: true, force: true });
112
- }
113
-
114
108
  ensureDir(targetPluginsRoot);
115
109
  ensureDir(targetPluginRoot);
116
110
  copyBundleEntries(bundleRoot, targetPluginRoot);
package/src/process.mjs CHANGED
@@ -1,8 +1,9 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- import { spawn, spawnSync } from "node:child_process";
3
+ import { spawn } from "node:child_process";
4
4
 
5
5
  import { ensureDir, nowIso, tailText, writeText } from "./common.mjs";
6
+ import { resolveCodexInvocation, resolveVerifyShellInvocation } from "./shell_invocation.mjs";
6
7
 
7
8
  function runChild(command, args, options = {}) {
8
9
  return new Promise((resolve) => {
@@ -51,79 +52,6 @@ function runChild(command, args, options = {}) {
51
52
  });
52
53
  }
53
54
 
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
55
  function writeCodexRunArtifacts(runDir, prefix, result, finalMessage) {
128
56
  writeText(path.join(runDir, `${prefix}-stdout.log`), result.stdout);
129
57
  writeText(path.join(runDir, `${prefix}-stderr.log`), result.stderr);
@@ -154,7 +82,7 @@ export async function runCodexTask({
154
82
  ensureDir(runDir);
155
83
 
156
84
  const lastMessageFile = path.join(runDir, `${outputPrefix}-last-message.txt`);
157
- const invocation = resolveCodexInvocation(executable);
85
+ const invocation = resolveCodexInvocation({ explicitExecutable: executable });
158
86
  const codexArgs = ["exec", "-C", context.repoRoot];
159
87
 
160
88
  if (model) {
@@ -179,9 +107,19 @@ export async function runCodexTask({
179
107
  }
180
108
  codexArgs.push("-o", lastMessageFile, "-");
181
109
 
182
- const args = invocation.executable
183
- ? [...invocation.argsPrefix, buildCmdCommandLine(invocation.executable, codexArgs)]
184
- : [...invocation.argsPrefix, ...codexArgs];
110
+ if (invocation.error) {
111
+ const result = {
112
+ ok: false,
113
+ code: 1,
114
+ stdout: "",
115
+ stderr: invocation.error,
116
+ };
117
+ writeText(path.join(runDir, `${outputPrefix}-prompt.md`), prompt);
118
+ writeCodexRunArtifacts(runDir, outputPrefix, result, "");
119
+ return { ...result, finalMessage: "" };
120
+ }
121
+
122
+ const args = [...invocation.argsPrefix, ...codexArgs];
185
123
 
186
124
  const result = await runChild(invocation.command, args, {
187
125
  cwd: context.repoRoot,
@@ -214,13 +152,28 @@ export async function runCodexExec({ context, prompt, runDir, policy }) {
214
152
  }
215
153
 
216
154
  export async function runShellCommand(context, commandLine, runDir, index) {
217
- const result = await runChild("pwsh", [
218
- "-NoLogo",
219
- "-NoProfile",
220
- "-Command",
155
+ const shellInvocation = resolveVerifyShellInvocation();
156
+ if (shellInvocation.error) {
157
+ const result = {
158
+ command: commandLine,
159
+ ok: false,
160
+ code: 1,
161
+ stdout: "",
162
+ stderr: shellInvocation.error,
163
+ };
164
+ const prefix = String(index + 1).padStart(2, "0");
165
+ writeText(path.join(runDir, `${prefix}-verify-command.txt`), commandLine);
166
+ writeText(path.join(runDir, `${prefix}-verify-stdout.log`), result.stdout);
167
+ writeText(path.join(runDir, `${prefix}-verify-stderr.log`), result.stderr);
168
+ return result;
169
+ }
170
+
171
+ const result = await runChild(shellInvocation.command, [
172
+ ...shellInvocation.argsPrefix,
221
173
  commandLine,
222
174
  ], {
223
175
  cwd: context.repoRoot,
176
+ shell: shellInvocation.shell,
224
177
  });
225
178
 
226
179
  const prefix = String(index + 1).padStart(2, "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.length
68
- ? constraints
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("实现约束", effectiveConstraints),
95
+ listSection("内建安全底线", mandatoryGuardrails),
96
+ listSection(usingFallbackConstraints ? "默认工程约束(文档未明确时生效)" : "项目/用户约束", effectiveConstraints),
99
97
  repoStateText ? section("仓库当前状态", repoStateText) : "",
100
98
  failureHistory.length
101
99
  ? section("已失败尝试摘要", failureHistory.slice(-4).map((item) => (
package/src/runner.mjs CHANGED
@@ -338,4 +338,4 @@ export function renderStatusText(context, options = {}) {
338
338
  nextTask ? renderTaskSummary(nextTask) : "",
339
339
  ].filter(Boolean).join("\n");
340
340
  }
341
-
341
+