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.
@@ -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,130 +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 hasCommand(command, platform = process.platform) {
70
- if (platform === "win32") {
71
- const result = spawnSync("where.exe", [command], {
72
- encoding: "utf8",
73
- shell: false,
74
- });
75
- return result.status === 0;
76
- }
77
-
78
- const result = spawnSync("sh", ["-lc", `command -v ${command}`], {
79
- encoding: "utf8",
80
- shell: false,
81
- });
82
- return result.status === 0;
83
- }
84
-
85
- export function resolveVerifyShellInvocation(options = {}) {
86
- const platform = options.platform || process.platform;
87
- const commandExists = options.commandExists || ((command) => hasCommand(command, platform));
88
-
89
- if (platform === "win32") {
90
- if (commandExists("pwsh")) {
91
- return {
92
- command: "pwsh",
93
- argsPrefix: ["-NoLogo", "-NoProfile", "-Command"],
94
- shell: false,
95
- };
96
- }
97
-
98
- if (commandExists("powershell")) {
99
- return {
100
- command: "powershell",
101
- argsPrefix: ["-NoLogo", "-NoProfile", "-Command"],
102
- shell: false,
103
- };
104
- }
105
-
106
- return {
107
- command: "cmd.exe",
108
- argsPrefix: ["/d", "/s", "/c"],
109
- shell: false,
110
- };
111
- }
112
-
113
- return {
114
- command: "sh",
115
- argsPrefix: ["-lc"],
116
- shell: false,
117
- };
118
- }
119
-
120
- function resolveCodexExecutable(explicitExecutable = "") {
121
- if (explicitExecutable) {
122
- return explicitExecutable;
123
- }
124
-
125
- if (process.platform !== "win32") {
126
- return "codex";
127
- }
128
-
129
- const candidates = ["codex.cmd", "codex", "codex.ps1"];
130
- for (const candidate of candidates) {
131
- const result = spawnSync("where.exe", [candidate], {
132
- encoding: "utf8",
133
- shell: false,
134
- });
135
- if (result.status !== 0) {
136
- continue;
137
- }
138
-
139
- const firstLine = String(result.stdout || "")
140
- .split(/\r?\n/)
141
- .map((line) => line.trim())
142
- .find(Boolean);
143
- if (firstLine) {
144
- return firstLine;
145
- }
146
- }
147
-
148
- return "codex";
149
- }
150
-
151
- function resolveCodexInvocation(explicitExecutable = "") {
152
- const executable = resolveCodexExecutable(explicitExecutable);
153
-
154
- if (process.platform === "win32" && /\.ps1$/i.test(executable)) {
155
- return {
156
- command: "pwsh",
157
- argsPrefix: ["-NoLogo", "-NoProfile", "-File", executable],
158
- shell: false,
159
- };
160
- }
161
-
162
- if (process.platform === "win32" && /\.(cmd|bat)$/i.test(executable)) {
163
- return {
164
- command: "cmd.exe",
165
- argsPrefix: ["/d", "/s", "/c"],
166
- executable,
167
- shell: false,
168
- };
169
- }
170
-
171
- return {
172
- command: executable,
173
- argsPrefix: [],
174
- shell: false,
175
- };
176
- }
177
-
178
55
  function writeCodexRunArtifacts(runDir, prefix, result, finalMessage) {
179
56
  writeText(path.join(runDir, `${prefix}-stdout.log`), result.stdout);
180
57
  writeText(path.join(runDir, `${prefix}-stderr.log`), result.stderr);
@@ -205,7 +82,7 @@ export async function runCodexTask({
205
82
  ensureDir(runDir);
206
83
 
207
84
  const lastMessageFile = path.join(runDir, `${outputPrefix}-last-message.txt`);
208
- const invocation = resolveCodexInvocation(executable);
85
+ const invocation = resolveCodexInvocation({ explicitExecutable: executable });
209
86
  const codexArgs = ["exec", "-C", context.repoRoot];
210
87
 
211
88
  if (model) {
@@ -230,9 +107,19 @@ export async function runCodexTask({
230
107
  }
231
108
  codexArgs.push("-o", lastMessageFile, "-");
232
109
 
233
- const args = invocation.executable
234
- ? [...invocation.argsPrefix, buildCmdCommandLine(invocation.executable, codexArgs)]
235
- : [...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];
236
123
 
237
124
  const result = await runChild(invocation.command, args, {
238
125
  cwd: context.repoRoot,
@@ -266,6 +153,21 @@ export async function runCodexExec({ context, prompt, runDir, policy }) {
266
153
 
267
154
  export async function runShellCommand(context, commandLine, runDir, index) {
268
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
+
269
171
  const result = await runChild(shellInvocation.command, [
270
172
  ...shellInvocation.argsPrefix,
271
173
  commandLine,