@namewta/speculo 0.7.1 → 0.7.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.
Files changed (37) hide show
  1. package/README.md +2 -1
  2. package/package.json +1 -1
  3. package/template/canonical/canonical-specdev-engineering-cognitive-mentor.md +178 -0
  4. package/template/canonical/canonical-specdev-goal-plan.md +386 -74
  5. package/template/canonical/canonical-specdev-grill-with-docs.md +178 -0
  6. package/template/canonical/canonical-specdev-spec.md +178 -0
  7. package/template/canonical/canonical-specdev-tickets.md +247 -21
  8. package/template/canonical/canonical-specdev-wayfinder.md +178 -0
  9. package/template/skills/optimize-codex-config/SKILL.md +81 -0
  10. package/template/skills/optimize-codex-config/references/configuration-contract.md +103 -0
  11. package/template/skills/optimize-codex-config/references/troubleshooting.md +79 -0
  12. package/template/skills/optimize-codex-config/scripts/audit-codex-config.mjs +747 -0
  13. package/template/workflows/specdev/I-implement/I-implement.md +11 -10
  14. package/template/workflows/specdev/I-implement/delegated-evidence-template.md +2 -1
  15. package/template/workflows/specdev/I-implement/execution-preflight.md +5 -3
  16. package/template/workflows/specdev/I-implement/merge-conflict-protocol.md +6 -5
  17. package/template/workflows/specdev/INDEX.md +5 -4
  18. package/template/workflows/specdev/P-goal-plan/P-goal-plan.md +27 -18
  19. package/template/workflows/specdev/P-goal-plan/completion-control.md +3 -3
  20. package/template/workflows/specdev/P-goal-plan/delegated-execution-template.md +6 -4
  21. package/template/workflows/specdev/P-goal-plan/delegated-execution.md +13 -7
  22. package/template/workflows/specdev/P-goal-plan/goal-plan-template.md +11 -2
  23. package/template/workflows/specdev/P-goal-plan/orchestration-protocol.md +7 -3
  24. package/template/workflows/specdev/P-goal-plan/planning-modes.md +23 -8
  25. package/template/workflows/specdev/P-goal-plan/workspace-execution-template.md +24 -0
  26. package/template/workflows/specdev/common/README.md +2 -2
  27. package/template/workflows/specdev/common/rules/change-completion.md +3 -2
  28. package/template/workflows/specdev/common/rules/path-ownership.md +2 -2
  29. package/template/workflows/specdev/common/schemas/change-status.schema.json +178 -0
  30. package/template/workflows/specdev/common/schemas/goal-plan.schema.json +10 -0
  31. package/template/workflows/specdev/common/skills/dev-worktree/SKILL.md +11 -11
  32. package/template/workflows/specdev/common/skills/dev-worktree/references/create.md +19 -4
  33. package/template/workflows/specdev/common/skills/dev-worktree/references/finalize.md +12 -5
  34. package/template/workflows/specdev/common/skills/subagent-delivery/SKILL.md +4 -3
  35. package/template/workflows/specdev/common/skills/subagent-delivery/references/external-web-subagent.md +1 -1
  36. package/template/workflows/specdev/common/skills/subagent-delivery/references/native-subagent.md +2 -3
  37. package/template/workflows/specdev/common/tools/validate-specdev.mjs +218 -5
@@ -0,0 +1,747 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { createHash } from "node:crypto";
4
+ import { spawnSync } from "node:child_process";
5
+ import { accessSync, constants, createReadStream } from "node:fs";
6
+ import { lstat, readFile, readdir } from "node:fs/promises";
7
+ import { homedir } from "node:os";
8
+ import { createInterface } from "node:readline";
9
+ import { delimiter, isAbsolute, join, relative, sep } from "node:path";
10
+
11
+ const MAX_SESSION_FILES = 200;
12
+ const MAX_SESSION_BYTES = 64 * 1024 * 1024;
13
+ const MAX_INCIDENTS = 100;
14
+ const COMMAND_TIMEOUT_MS = 20_000;
15
+
16
+ const SAFE_SETTINGS = new Set([
17
+ "model",
18
+ "model_provider",
19
+ "model_reasoning_effort",
20
+ "model_reasoning_summary",
21
+ "model_verbosity",
22
+ "plan_mode_reasoning_effort",
23
+ "model_auto_compact_token_limit",
24
+ "model_auto_compact_token_limit_scope",
25
+ "model_context_window",
26
+ "approval_policy",
27
+ "approvals_reviewer",
28
+ "sandbox_mode",
29
+ "default_permissions",
30
+ "cli_auth_credentials_store",
31
+ "agents.enabled",
32
+ "agents.max_concurrent_threads_per_session",
33
+ "agents.max_threads",
34
+ "agents.default_subagent_model",
35
+ "agents.default_subagent_reasoning_effort",
36
+ "sandbox_workspace_write.network_access",
37
+ "history.persistence",
38
+ "history.max_bytes",
39
+ "features.remote_compaction_v2",
40
+ ]);
41
+
42
+ class UsageError extends Error {}
43
+
44
+ function usage() {
45
+ return [
46
+ "Usage:",
47
+ " node audit-codex-config.mjs --codex-home <absolute-directory> [options]",
48
+ "",
49
+ "Options:",
50
+ " --codex-bin <absolute-path> Codex executable (default: resolve codex from PATH)",
51
+ " --since-days <number> Scan recent rollout files (default: 7, range: 1-365)",
52
+ " --no-command-probes Skip Codex CLI and config-writer probes",
53
+ " --json Emit the schema-v1 JSON report",
54
+ " -h, --help Show this help",
55
+ "",
56
+ "The audit is read-only. It never reads auth.json contents or emits rollout prompt/tool content.",
57
+ "",
58
+ ].join("\n");
59
+ }
60
+
61
+ function parseArgs(argv) {
62
+ const options = {
63
+ codex_home: null,
64
+ codex_bin: "codex",
65
+ since_days: 7,
66
+ command_probes: true,
67
+ json: false,
68
+ help: false,
69
+ };
70
+
71
+ for (let index = 0; index < argv.length; index += 1) {
72
+ const item = argv[index];
73
+ if (item === "--codex-home" || item === "--codex-bin" || item === "--since-days") {
74
+ const value = argv[index + 1];
75
+ if (!value || value.startsWith("--")) throw new UsageError(item + " requires a value");
76
+ const key = item.slice(2).replaceAll("-", "_");
77
+ options[key] = value;
78
+ index += 1;
79
+ } else if (item === "--no-command-probes") {
80
+ options.command_probes = false;
81
+ } else if (item === "--json") {
82
+ options.json = true;
83
+ } else if (item === "--help" || item === "-h") {
84
+ options.help = true;
85
+ } else {
86
+ throw new UsageError("unknown argument: " + item);
87
+ }
88
+ }
89
+
90
+ if (options.help) return options;
91
+ if (!options.codex_home) throw new UsageError("--codex-home is required");
92
+ if (!isAbsolute(options.codex_home)) throw new UsageError("--codex-home must be an absolute path");
93
+ if (options.codex_bin !== "codex" && !isAbsolute(options.codex_bin)) {
94
+ throw new UsageError("--codex-bin must be an absolute path");
95
+ }
96
+ const sinceDays = Number(options.since_days);
97
+ if (!Number.isInteger(sinceDays) || sinceDays < 1 || sinceDays > 365) {
98
+ throw new UsageError("--since-days must be an integer from 1 to 365");
99
+ }
100
+ options.since_days = sinceDays;
101
+ return options;
102
+ }
103
+
104
+ async function fileMetadata(path) {
105
+ try {
106
+ const stat = await lstat(path);
107
+ let type = "other";
108
+ if (stat.isSymbolicLink()) type = "symlink";
109
+ else if (stat.isFile()) type = "file";
110
+ else if (stat.isDirectory()) type = "directory";
111
+ return {
112
+ exists: true,
113
+ type,
114
+ ...(stat.isFile() ? { bytes: stat.size } : {}),
115
+ mode: "0" + (stat.mode & 0o777).toString(8).padStart(3, "0"),
116
+ mtime: stat.mtime.toISOString(),
117
+ };
118
+ } catch (error) {
119
+ if (error?.code === "ENOENT") return { exists: false };
120
+ throw error;
121
+ }
122
+ }
123
+
124
+ async function hashFile(path) {
125
+ return createHash("sha256").update(await readFile(path)).digest("hex");
126
+ }
127
+
128
+ function parsePrimitive(raw) {
129
+ const value = raw.trim();
130
+ const doubleQuoted = value.match(/^("(?:[^"\\]|\\.)*")\s*(?:#.*)?$/);
131
+ if (doubleQuoted) {
132
+ try {
133
+ return JSON.parse(doubleQuoted[1]);
134
+ } catch {
135
+ return null;
136
+ }
137
+ }
138
+ const singleQuoted = value.match(/^'([^']*)'\s*(?:#.*)?$/);
139
+ if (singleQuoted) return singleQuoted[1];
140
+ const boolean = value.match(/^(true|false)\s*(?:#.*)?$/);
141
+ if (boolean) return boolean[1] === "true";
142
+ const number = value.match(/^(-?\d+)\s*(?:#.*)?$/);
143
+ if (number) return Number(number[1]);
144
+ return null;
145
+ }
146
+
147
+ function redactConfigPath(path) {
148
+ const parts = path.split(".");
149
+ const dynamicParents = new Set(["model_providers", "mcp_servers", "profiles", "plugins", "skills"]);
150
+ if (dynamicParents.has(parts[0]) && parts.length > 1) parts[1] = "<id>";
151
+ if (parts[0] === "agents" && parts.length > 2) parts[1] = "<role>";
152
+ return parts.join(".");
153
+ }
154
+
155
+ function endpointScheme(value) {
156
+ if (typeof value !== "string") return "unknown";
157
+ const match = value.match(/^([a-z][a-z0-9+.-]*):\/\//i);
158
+ return match ? match[1].toLowerCase() : "unknown";
159
+ }
160
+
161
+ function scanConfig(text) {
162
+ const safeSettings = {};
163
+ const declaredSections = new Set();
164
+ const declaredKeys = new Set();
165
+ const providers = new Map();
166
+ let table = "";
167
+ let containsInlineSecretMaterial = false;
168
+
169
+ for (const line of text.split(/\r?\n/)) {
170
+ const tableMatch = line.match(/^\s*\[\[?\s*([A-Za-z0-9_.-]+)\s*\]\]?\s*(?:#.*)?$/);
171
+ if (tableMatch) {
172
+ table = tableMatch[1];
173
+ declaredSections.add(redactConfigPath(table));
174
+ continue;
175
+ }
176
+ const keyMatch = line.match(/^\s*([A-Za-z0-9_.-]+)\s*=\s*(.*)$/);
177
+ if (!keyMatch) continue;
178
+ const localKey = keyMatch[1];
179
+ const fullKey = table ? table + "." + localKey : localKey;
180
+ const redactedKey = redactConfigPath(fullKey);
181
+ const value = parsePrimitive(keyMatch[2]);
182
+ declaredKeys.add(redactedKey);
183
+
184
+ const lowerKey = fullKey.toLowerCase();
185
+ if (
186
+ lowerKey.includes("experimental_bearer_token") ||
187
+ lowerKey.includes("authorization") ||
188
+ lowerKey.includes("api_key") ||
189
+ lowerKey.includes("password") ||
190
+ lowerKey.includes("secret") ||
191
+ (lowerKey.includes("http_headers") && /bearer|basic|token/i.test(keyMatch[2]))
192
+ ) {
193
+ containsInlineSecretMaterial = true;
194
+ }
195
+
196
+ if (SAFE_SETTINGS.has(fullKey) && value !== null) safeSettings[fullKey] = value;
197
+
198
+ const providerMatch = fullKey.match(/^model_providers\.([A-Za-z0-9_-]+)\.(.+)$/);
199
+ if (!providerMatch) continue;
200
+ const providerId = providerMatch[1];
201
+ const providerKey = providerMatch[2];
202
+ if (!providers.has(providerId)) providers.set(providerId, {});
203
+ const provider = providers.get(providerId);
204
+ if (providerKey === "base_url") provider.base_url_scheme = endpointScheme(value);
205
+ else if (providerKey === "wire_api" && typeof value === "string") provider.wire_api = value;
206
+ else if (providerKey === "requires_openai_auth" && typeof value === "boolean") provider.requires_openai_auth = value;
207
+ else if (providerKey === "env_key") provider.has_env_key = true;
208
+ else if (providerKey === "experimental_bearer_token") provider.has_inline_bearer_token = true;
209
+ else if (providerKey === "auth.command") provider.has_auth_command = true;
210
+ else if (providerKey === "request_max_retries" && typeof value === "number") provider.request_max_retries = value;
211
+ else if (providerKey === "stream_max_retries" && typeof value === "number") provider.stream_max_retries = value;
212
+ else if (providerKey === "stream_idle_timeout_ms" && typeof value === "number") provider.stream_idle_timeout_ms = value;
213
+ }
214
+
215
+ if (typeof safeSettings.model_provider === "string" && providers.has(safeSettings.model_provider)) {
216
+ safeSettings.model_provider = "<custom-provider>";
217
+ }
218
+
219
+ return {
220
+ line_count: text.split(/\r?\n/).length,
221
+ declared_sections: [...declaredSections].sort(),
222
+ declared_keys: [...declaredKeys].sort(),
223
+ safe_settings: safeSettings,
224
+ custom_providers: [...providers.values()].map((provider, index) => ({
225
+ id: "provider-" + (index + 1),
226
+ ...provider,
227
+ })),
228
+ contains_inline_secret_material: containsInlineSecretMaterial,
229
+ };
230
+ }
231
+
232
+ function redactString(value) {
233
+ let output = value;
234
+ output = output.replace(/https?:\/\/[^\s"'<>),]+/gi, (url) => {
235
+ const scheme = url.slice(0, url.indexOf(":"));
236
+ return "<redacted-url:" + scheme.toLowerCase() + ">";
237
+ });
238
+ output = output.replace(/\bsk-[A-Za-z0-9_-]{6,}\b/g, "<redacted-secret>");
239
+ output = output.replace(
240
+ /(authorization|api[_-]?key|bearer|password|secret)\s*[:=]\s*[^\s,;]+/gi,
241
+ "$1=<redacted>",
242
+ );
243
+ if (/^(?:\/|[A-Za-z]:[\\/])/.test(output)) return "<absolute-path>";
244
+ return output.length > 1_000 ? output.slice(0, 1_000) + "<truncated>" : output;
245
+ }
246
+
247
+ function sanitizeValue(value, key = "", depth = 0) {
248
+ if (depth > 12) return "<depth-limit>";
249
+ if (value === null || typeof value === "number" || typeof value === "boolean") return value;
250
+ if (typeof value === "string") {
251
+ if (/(password|secret|bearer|api[_-]?key|authorization|credential|headers?)/i.test(key)) {
252
+ return "<redacted>";
253
+ }
254
+ return redactString(value);
255
+ }
256
+ if (Array.isArray(value)) return value.slice(0, 200).map((item) => sanitizeValue(item, key, depth + 1));
257
+ if (typeof value === "object") {
258
+ const result = {};
259
+ for (const [childKey, childValue] of Object.entries(value).slice(0, 300)) {
260
+ result[childKey] = sanitizeValue(childValue, childKey, depth + 1);
261
+ }
262
+ return result;
263
+ }
264
+ return String(value);
265
+ }
266
+
267
+ function runCommand(command, args, codexHome) {
268
+ const result = spawnSync(command, args, {
269
+ encoding: "utf8",
270
+ timeout: COMMAND_TIMEOUT_MS,
271
+ maxBuffer: 8 * 1024 * 1024,
272
+ env: { ...process.env, CODEX_HOME: codexHome, NO_COLOR: "1" },
273
+ });
274
+ return {
275
+ status: result.status,
276
+ stdout: result.stdout ?? "",
277
+ error_code: result.error?.code ?? null,
278
+ timed_out: result.error?.code === "ETIMEDOUT",
279
+ };
280
+ }
281
+
282
+ function resolveExecutable(command) {
283
+ if (isAbsolute(command)) return command;
284
+ const extensions = process.platform === "win32"
285
+ ? (process.env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";")
286
+ : [""];
287
+ const directories = [
288
+ ...(process.env.PATH ?? "").split(delimiter).filter(Boolean),
289
+ join(homedir(), ".volta", "bin"),
290
+ join(homedir(), ".local", "bin"),
291
+ "/opt/homebrew/bin",
292
+ "/usr/local/bin",
293
+ ];
294
+ for (const directory of [...new Set(directories)]) {
295
+ for (const extension of extensions) {
296
+ const candidate = join(directory, command + extension.toLowerCase());
297
+ try {
298
+ accessSync(candidate, process.platform === "win32" ? constants.F_OK : constants.X_OK);
299
+ return candidate;
300
+ } catch {
301
+ // Try the next deterministic candidate.
302
+ }
303
+ }
304
+ }
305
+ return command;
306
+ }
307
+
308
+ function parseFeatures(text) {
309
+ const features = [];
310
+ for (const line of text.split(/\r?\n/)) {
311
+ const match = line.trim().match(/^(\S+)\s+(.+?)\s+(true|false)$/);
312
+ if (!match) continue;
313
+ features.push({ name: match[1], maturity: match[2].trim(), enabled: match[3] === "true" });
314
+ }
315
+ return features;
316
+ }
317
+
318
+ function collectModels(value, output = [], seen = new Set()) {
319
+ if (output.length >= 100 || value === null || typeof value !== "object") return output;
320
+ if (Array.isArray(value)) {
321
+ for (const item of value) collectModels(item, output, seen);
322
+ return output;
323
+ }
324
+ const slug = typeof value.slug === "string" ? value.slug : typeof value.model === "string" ? value.model : null;
325
+ if (slug && !seen.has(slug)) {
326
+ seen.add(slug);
327
+ const levels = Array.isArray(value.supported_reasoning_levels)
328
+ ? value.supported_reasoning_levels
329
+ .map((item) => typeof item === "string" ? item : item?.effort ?? item?.reasoning_effort)
330
+ .filter((item) => typeof item === "string")
331
+ : [];
332
+ output.push({
333
+ slug,
334
+ ...(Number.isFinite(value.context_window) ? { context_window: value.context_window } : {}),
335
+ ...(Number.isFinite(value.max_context_window) ? { max_context_window: value.max_context_window } : {}),
336
+ ...(Number.isFinite(value.effective_context_window_percent)
337
+ ? { effective_context_window_percent: value.effective_context_window_percent }
338
+ : {}),
339
+ ...(levels.length ? { reasoning_efforts: levels } : {}),
340
+ });
341
+ }
342
+ for (const child of Object.values(value)) collectModels(child, output, seen);
343
+ return output;
344
+ }
345
+
346
+ function commandProbes(codexBin, codexHome) {
347
+ const executable = resolveExecutable(codexBin);
348
+ const versionRun = runCommand(executable, ["--version"], codexHome);
349
+ const doctorRun = runCommand(executable, ["doctor", "--json"], codexHome);
350
+ const featuresRun = runCommand(executable, ["features", "list"], codexHome);
351
+ const modelsRun = runCommand(executable, ["debug", "models", "--bundled"], codexHome);
352
+
353
+ let doctor = null;
354
+ let doctorParsed = false;
355
+ if (doctorRun.stdout.trim()) {
356
+ try {
357
+ doctor = sanitizeValue(JSON.parse(doctorRun.stdout));
358
+ doctorParsed = true;
359
+ } catch {
360
+ doctor = null;
361
+ }
362
+ }
363
+
364
+ let models = [];
365
+ let modelsParsed = false;
366
+ if (modelsRun.stdout.trim()) {
367
+ try {
368
+ models = collectModels(JSON.parse(modelsRun.stdout));
369
+ modelsParsed = true;
370
+ } catch {
371
+ models = [];
372
+ }
373
+ }
374
+
375
+ return {
376
+ version: {
377
+ status: versionRun.status,
378
+ available: versionRun.error_code !== "ENOENT",
379
+ value: versionRun.status === 0 ? redactString(versionRun.stdout.trim()) : null,
380
+ },
381
+ doctor: {
382
+ status: doctorRun.status,
383
+ parsed: doctorParsed,
384
+ report: doctor,
385
+ timed_out: doctorRun.timed_out,
386
+ },
387
+ features: {
388
+ status: featuresRun.status,
389
+ entries: featuresRun.status === 0 ? parseFeatures(featuresRun.stdout) : [],
390
+ timed_out: featuresRun.timed_out,
391
+ },
392
+ models: {
393
+ status: modelsRun.status,
394
+ parsed: modelsParsed,
395
+ entries: models,
396
+ timed_out: modelsRun.timed_out,
397
+ },
398
+ };
399
+ }
400
+
401
+ function parseWritableLsofHandles(text) {
402
+ const writers = new Set();
403
+ let pid = null;
404
+ for (const line of text.split(/\r?\n/)) {
405
+ const field = line[0];
406
+ const value = line.slice(1);
407
+ if (field === "p") {
408
+ pid = /^\d+$/.test(value) ? Number(value) : null;
409
+ } else if (field === "a" && pid !== null && (value === "w" || value === "u")) {
410
+ writers.add(pid);
411
+ }
412
+ }
413
+ return [...writers].sort((left, right) => left - right);
414
+ }
415
+
416
+ function activeConfigWriters(configPath) {
417
+ if (process.platform === "win32") {
418
+ return { available: false, detected: false, count: 0, probe: "unsupported" };
419
+ }
420
+ const run = spawnSync("lsof", ["-F", "pca", "--", configPath], {
421
+ encoding: "utf8",
422
+ timeout: 5_000,
423
+ maxBuffer: 2 * 1024 * 1024,
424
+ });
425
+ if (run.error?.code === "ENOENT") {
426
+ return { available: false, detected: false, count: 0, probe: "lsof-unavailable" };
427
+ }
428
+ if (run.status !== 0 && !(run.status === 1 && !(run.stderr ?? "").trim())) {
429
+ return { available: false, detected: false, count: 0, probe: "lsof-failed" };
430
+ }
431
+ const pids = parseWritableLsofHandles(run.stdout ?? "");
432
+ return {
433
+ available: true,
434
+ detected: pids.length > 0,
435
+ count: pids.length,
436
+ probe: "config-writable-handle",
437
+ pids,
438
+ };
439
+ }
440
+
441
+ async function collectSessionFiles(root, cutoff, output = []) {
442
+ let entries;
443
+ try {
444
+ entries = await readdir(root, { withFileTypes: true });
445
+ } catch (error) {
446
+ if (error?.code === "ENOENT") return output;
447
+ throw error;
448
+ }
449
+ for (const entry of entries) {
450
+ if (entry.isSymbolicLink()) continue;
451
+ const path = join(root, entry.name);
452
+ if (entry.isDirectory()) {
453
+ await collectSessionFiles(path, cutoff, output);
454
+ } else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
455
+ const stat = await lstat(path);
456
+ if (stat.mtimeMs >= cutoff) output.push({ path, bytes: stat.size, mtime_ms: stat.mtimeMs });
457
+ }
458
+ }
459
+ return output;
460
+ }
461
+
462
+ function finiteNumber(value) {
463
+ return Number.isFinite(value) ? value : null;
464
+ }
465
+
466
+ function classifyError(message) {
467
+ if (typeof message !== "string") return null;
468
+ const statusMatch = message.match(/(?:status|HTTP(?: status)?)\s*[:=]?\s*(401|403|404|413|429)\b/i)
469
+ ?? message.match(/\b(401|403|404|413|429)\s+(?:Unauthorized|Forbidden|Not Found|Payload Too Large|Request Entity Too Large|Too Many Requests)\b/i);
470
+ const status = statusMatch ? Number(statusMatch[1]) : null;
471
+ const remoteCompaction = /remote compact|compaction/i.test(message);
472
+ const proxySignatures = [
473
+ ["nginx", /\bnginx(?:\/|\b)/i],
474
+ ["envoy", /\benvoy(?:\/|\b)/i],
475
+ ["haproxy", /\bhaproxy(?:\/|\b)/i],
476
+ ["cloudflare", /\bcloudflare(?:\/|\b)/i],
477
+ ["varnish", /\bvarnish(?:\/|\b)/i],
478
+ ];
479
+ const proxy_signature = proxySignatures.find(([, pattern]) => pattern.test(message))?.[0] ?? null;
480
+ const schemeMatch = message.match(/\b(https?):\/\//i);
481
+ const endpoint_scheme = schemeMatch ? schemeMatch[1].toLowerCase() : "unknown";
482
+
483
+ let category = null;
484
+ let scope = null;
485
+ if (status === 413) {
486
+ category = proxy_signature ? "external_proxy_body_limit" : "request_body_limit_unattributed";
487
+ scope = proxy_signature ? "external_proxy" : "provider_or_proxy";
488
+ } else if (status === 401) {
489
+ category = "authentication_rejected";
490
+ scope = "authentication";
491
+ } else if (status === 403) {
492
+ category = "authorization_rejected";
493
+ scope = "authentication_or_policy";
494
+ } else if (status === 404) {
495
+ category = "endpoint_or_wire_mismatch";
496
+ scope = "provider_contract";
497
+ } else if (status === 429) {
498
+ category = "provider_rate_limited";
499
+ scope = "upstream_service";
500
+ } else if (/SSE|event-stream|stream (?:closed|disconnect|interruption)/i.test(message)) {
501
+ category = "stream_interruption";
502
+ scope = "provider_or_proxy";
503
+ } else if (/timed? out|timeout/i.test(message)) {
504
+ category = "provider_timeout";
505
+ scope = "provider_or_proxy";
506
+ } else if (remoteCompaction) {
507
+ category = "remote_compaction_failed";
508
+ scope = "provider_or_proxy";
509
+ }
510
+ if (!category) return null;
511
+
512
+ return {
513
+ category,
514
+ scope,
515
+ status,
516
+ operation: remoteCompaction ? "remote_compaction" : "responses_request",
517
+ proxy_signature,
518
+ endpoint_scheme,
519
+ evidence: [
520
+ ...(status ? ["http_status_" + status] : []),
521
+ ...(remoteCompaction ? ["remote_compaction"] : []),
522
+ ...(proxy_signature ? ["proxy_generated_response"] : []),
523
+ ],
524
+ };
525
+ }
526
+
527
+ async function scanSessionFile(file, codexHome, incidents, counters) {
528
+ let lastTokenUsage = null;
529
+ const input = createReadStream(file.path, { encoding: "utf8" });
530
+ const lines = createInterface({ input, crlfDelay: Infinity });
531
+ for await (const line of lines) {
532
+ counters.lines_scanned += 1;
533
+ let event;
534
+ try {
535
+ event = JSON.parse(line);
536
+ } catch {
537
+ counters.invalid_json_lines += 1;
538
+ continue;
539
+ }
540
+ if (event?.type !== "event_msg") continue;
541
+ if (event?.payload?.type === "token_count") {
542
+ lastTokenUsage = {
543
+ input_tokens: finiteNumber(event.payload?.info?.last_token_usage?.input_tokens),
544
+ cached_input_tokens: finiteNumber(event.payload?.info?.last_token_usage?.cached_input_tokens),
545
+ output_tokens: finiteNumber(event.payload?.info?.last_token_usage?.output_tokens),
546
+ total_tokens: finiteNumber(event.payload?.info?.last_token_usage?.total_tokens),
547
+ model_context_window: finiteNumber(event.payload?.info?.model_context_window),
548
+ };
549
+ continue;
550
+ }
551
+ const classified = classifyError(event?.payload?.error?.message);
552
+ if (!classified) continue;
553
+ if (incidents.length >= MAX_INCIDENTS) {
554
+ counters.incidents_truncated = true;
555
+ continue;
556
+ }
557
+ incidents.push({
558
+ timestamp: typeof event.timestamp === "string" ? event.timestamp : null,
559
+ source: relative(codexHome, file.path).split(sep).join("/"),
560
+ ...classified,
561
+ token_state: lastTokenUsage,
562
+ });
563
+ }
564
+ }
565
+
566
+ async function scanSessions(codexHome, sinceDays) {
567
+ const cutoff = Date.now() - sinceDays * 24 * 60 * 60 * 1_000;
568
+ const allFiles = await collectSessionFiles(join(codexHome, "sessions"), cutoff);
569
+ allFiles.sort((left, right) => right.mtime_ms - left.mtime_ms);
570
+ const selected = allFiles.slice(0, MAX_SESSION_FILES);
571
+ const incidents = [];
572
+ const counters = {
573
+ files_found: allFiles.length,
574
+ files_scanned: 0,
575
+ files_skipped_oversize: 0,
576
+ files_truncated: allFiles.length > MAX_SESSION_FILES,
577
+ lines_scanned: 0,
578
+ invalid_json_lines: 0,
579
+ incidents_truncated: false,
580
+ };
581
+ for (const file of selected) {
582
+ if (file.bytes > MAX_SESSION_BYTES) {
583
+ counters.files_skipped_oversize += 1;
584
+ continue;
585
+ }
586
+ try {
587
+ await scanSessionFile(file, codexHome, incidents, counters);
588
+ counters.files_scanned += 1;
589
+ } catch {
590
+ counters.invalid_json_lines += 1;
591
+ }
592
+ }
593
+ incidents.sort((left, right) => String(right.timestamp).localeCompare(String(left.timestamp)));
594
+ return { since_days: sinceDays, ...counters, incidents };
595
+ }
596
+
597
+ function sameFingerprint(left, right, leftHash, rightHash) {
598
+ return left.exists === right.exists
599
+ && left.type === right.type
600
+ && left.bytes === right.bytes
601
+ && left.mtime === right.mtime
602
+ && leftHash === rightHash;
603
+ }
604
+
605
+ function buildFindings(report) {
606
+ const findings = [];
607
+ const add = (code, severity, scope) => {
608
+ if (!findings.some((item) => item.code === code)) findings.push({ code, severity, scope });
609
+ };
610
+
611
+ if (!report.files.config.exists) add("config_missing", "warning", "local_config");
612
+ if (report.files.config.type === "symlink") add("config_is_symlink", "error", "local_config");
613
+ if (report.files.auth.type === "symlink") add("auth_is_symlink", "error", "authentication");
614
+ if (process.platform !== "win32" && report.files.auth.exists && report.files.auth.mode !== "0600") {
615
+ add("auth_permissions_not_0600", "error", "authentication");
616
+ }
617
+ if (report.config?.contains_inline_secret_material) add("config_contains_inline_secret_material", "error", "local_config");
618
+ if (report.config?.custom_providers.some((provider) => provider.base_url_scheme === "http")) {
619
+ add("provider_transport_is_plain_http", "warning", "provider_contract");
620
+ }
621
+ if (report.runtime.config_drift_detected) add("config_changed_during_audit", "error", "local_config");
622
+ if (report.runtime.active_writers?.detected) add("active_codex_writer_detected", "warning", "local_config");
623
+ for (const incident of report.sessions.incidents) {
624
+ const severity = incident.status && incident.status >= 400 ? "error" : "warning";
625
+ add(incident.category, severity, incident.scope);
626
+ }
627
+ if (report.commands.enabled) {
628
+ for (const key of ["version", "doctor", "features", "models"]) {
629
+ if (report.commands[key].status !== 0) add("codex_" + key + "_probe_failed", "warning", "local_runtime");
630
+ }
631
+ }
632
+ return findings;
633
+ }
634
+
635
+ function humanReport(report) {
636
+ const counts = report.findings.reduce((result, finding) => {
637
+ result[finding.severity] = (result[finding.severity] ?? 0) + 1;
638
+ return result;
639
+ }, {});
640
+ const lines = [
641
+ "Codex configuration audit",
642
+ "config.toml: " + (report.files.config.exists ? report.files.config.type : "missing"),
643
+ "auth.json: " + (report.files.auth.exists ? report.files.auth.type + " mode=" + report.files.auth.mode : "missing"),
644
+ "rollout incidents: " + report.sessions.incidents.length,
645
+ "findings: " + (counts.error ?? 0) + " error, " + (counts.warning ?? 0) + " warning",
646
+ ];
647
+ for (const finding of report.findings) lines.push("[" + finding.severity + "] " + finding.code + " (" + finding.scope + ")");
648
+ return lines.join("\n") + "\n";
649
+ }
650
+
651
+ async function audit(options) {
652
+ const homeMetadata = await fileMetadata(options.codex_home);
653
+ if (!homeMetadata.exists) throw new UsageError("--codex-home does not exist");
654
+ if (homeMetadata.type !== "directory") throw new UsageError("--codex-home must name a directory, not " + homeMetadata.type);
655
+
656
+ const configPath = join(options.codex_home, "config.toml");
657
+ const authPath = join(options.codex_home, "auth.json");
658
+ const configBefore = await fileMetadata(configPath);
659
+ const auth = await fileMetadata(authPath);
660
+ let config = null;
661
+ let configHashBefore = null;
662
+ let configReadError = false;
663
+ if (configBefore.type === "file") {
664
+ try {
665
+ const text = await readFile(configPath, "utf8");
666
+ config = scanConfig(text);
667
+ configHashBefore = createHash("sha256").update(text).digest("hex");
668
+ } catch {
669
+ configReadError = true;
670
+ }
671
+ }
672
+
673
+ const sessions = await scanSessions(options.codex_home, options.since_days);
674
+ const commands = options.command_probes
675
+ ? { enabled: true, ...commandProbes(options.codex_bin, options.codex_home) }
676
+ : { enabled: false };
677
+ const activeWriters = options.command_probes
678
+ ? activeConfigWriters(configPath)
679
+ : { available: false, detected: false, count: 0, probe: "disabled" };
680
+
681
+ const configAfter = await fileMetadata(configPath);
682
+ let configHashAfter = null;
683
+ if (configAfter.type === "file") {
684
+ try {
685
+ configHashAfter = await hashFile(configPath);
686
+ } catch {
687
+ configHashAfter = null;
688
+ }
689
+ }
690
+
691
+ const report = {
692
+ schema_version: 1,
693
+ generated_at: new Date().toISOString(),
694
+ scope: "local_codex_only",
695
+ codex_home: "<codex-home>",
696
+ files: {
697
+ config: { ...configBefore, sha256: configHashBefore },
698
+ auth,
699
+ },
700
+ config,
701
+ sessions,
702
+ runtime: {
703
+ active_writers: activeWriters,
704
+ config_drift_detected: !sameFingerprint(configBefore, configAfter, configHashBefore, configHashAfter),
705
+ config_read_error: configReadError,
706
+ },
707
+ commands,
708
+ findings: [],
709
+ };
710
+ report.findings = buildFindings(report);
711
+ if (configReadError) report.findings.push({ code: "config_read_failed", severity: "error", scope: "local_config" });
712
+ return report;
713
+ }
714
+
715
+ async function main() {
716
+ let options;
717
+ try {
718
+ options = parseArgs(process.argv.slice(2));
719
+ } catch (error) {
720
+ if (error instanceof UsageError) {
721
+ process.stderr.write("audit-codex-config: " + error.message + "\n");
722
+ process.exitCode = 2;
723
+ return;
724
+ }
725
+ throw error;
726
+ }
727
+
728
+ if (options.help) {
729
+ process.stdout.write(usage());
730
+ return;
731
+ }
732
+
733
+ try {
734
+ const report = await audit(options);
735
+ process.stdout.write(options.json ? JSON.stringify(report, null, 2) + "\n" : humanReport(report));
736
+ } catch (error) {
737
+ if (error instanceof UsageError) {
738
+ process.stderr.write("audit-codex-config: " + error.message + "\n");
739
+ process.exitCode = 2;
740
+ return;
741
+ }
742
+ process.stderr.write("audit-codex-config: audit failed\n");
743
+ process.exitCode = 1;
744
+ }
745
+ }
746
+
747
+ await main();