@peterxiaoyang/superspec 0.1.5 → 0.1.7

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.
@@ -4,6 +4,7 @@ import { CLAIM_ADJUDICATION_DECISIONS, EVIDENCE_KINDS, EVIDENCE_STATUSES, FINAL_
4
4
  import { normalize_gate } from "./openspec.js";
5
5
  import { superspec_dir } from "./paths.js";
6
6
  import { findings_schema_reasons, review_digest_schema_reasons, standing_authorization_schema_reasons, user_decision_schema_reasons, } from "./disclosure.js";
7
+ import { pinned_artifact_ref_reasons as shared_pinned_artifact_ref_reasons, worker_test_run_reasons, } from "./apply_worker_chain.js";
7
8
  function file_ref_reasons(baseRoot, ev, field, code) {
8
9
  const problems = [];
9
10
  const raw = ev[field];
@@ -29,6 +30,46 @@ function file_ref_reasons(baseRoot, ev, field, code) {
29
30
  }
30
31
  return problems;
31
32
  }
33
+ function contractField(line) {
34
+ const match = line.match(/^\s*-\s+`?([A-Za-z0-9_-]+)`?\s*:\s*(.*)$/);
35
+ if (!match)
36
+ return null;
37
+ return { key: match[1], value: match[2].trim() };
38
+ }
39
+ function test_contract_section_lines(changeRoot, testId) {
40
+ const pathValue = join(changeRoot, ".superspec", "artifacts", "test-contract.md");
41
+ if (!existsSync(pathValue) || !statSync(pathValue).isFile())
42
+ return [];
43
+ const lines = readFileSync(pathValue, "utf8").split(/\r?\n/);
44
+ const out = [];
45
+ let inSection = false;
46
+ for (const line of lines) {
47
+ const heading = line.match(/^###\s+(.+?)\s*$/);
48
+ if (heading) {
49
+ if (inSection)
50
+ break;
51
+ inSection = heading[1].trim() === testId;
52
+ continue;
53
+ }
54
+ if (inSection)
55
+ out.push(line);
56
+ }
57
+ return out;
58
+ }
59
+ function expected_failure_contract(changeRoot, testId) {
60
+ const fields = new Map();
61
+ for (const line of test_contract_section_lines(changeRoot, testId)) {
62
+ const field = contractField(line);
63
+ if (!field)
64
+ continue;
65
+ const values = fields.get(field.key) ?? [];
66
+ values.push(field.value);
67
+ fields.set(field.key, values);
68
+ }
69
+ const signature = (fields.get("expected_failure_signature") ?? [])[0];
70
+ const classifier = (fields.get("expected_failure_classifier") ?? [])[0];
71
+ return { signature, classifier };
72
+ }
32
73
  function pinned_ref_list_reasons(ev, field, baseRoot, staleCode, opts = {}) {
33
74
  const problems = [];
34
75
  const raw = ev[field];
@@ -94,6 +135,30 @@ function pinned_ref_reasons(ev, field, baseRoot, staleCode) {
94
135
  }
95
136
  return problems;
96
137
  }
138
+ function string_list_field_reasons(ev, field, code, opts = {}) {
139
+ const raw = ev[field];
140
+ if (!Array.isArray(raw) || (!opts.allowEmpty && raw.length === 0) || !raw.every((item) => typeof item === "string" && item.length > 0)) {
141
+ return [reason(code, `${ev._path}: ${field} must be a non-empty string list`)];
142
+ }
143
+ return [];
144
+ }
145
+ function fingerprint_field_reasons(ev, field, code) {
146
+ const raw = ev[field];
147
+ if (typeof raw === "string") {
148
+ return raw.startsWith("sha256:") ? [] : [reason(code, `${ev._path}: ${field} must be a sha256 fingerprint`)];
149
+ }
150
+ if (isObject(raw) && typeof raw.fingerprint_digest === "string" && raw.fingerprint_digest.startsWith("sha256:"))
151
+ return [];
152
+ return [reason(code, `${ev._path}: ${field} must be a sha256 fingerprint or fingerprint object`)];
153
+ }
154
+ function pinned_artifact_ref_item_reasons(refItem, field, changeRoot, expected = {}) {
155
+ return shared_pinned_artifact_ref_reasons(changeRoot, refItem, field, {
156
+ kind: expected.kind,
157
+ role: expected.role,
158
+ taskId: expected.task_id,
159
+ chainId: expected.chain_id,
160
+ });
161
+ }
97
162
  export function role_target_ref_reasons(ev, targetRoot) {
98
163
  return pinned_ref_reasons(ev, "target_refs", targetRoot, "stale_review");
99
164
  }
@@ -328,6 +393,26 @@ function test_run_reasons(ev, changeRoot) {
328
393
  if (typeof ev.result_summary !== "string" || !ev.result_summary.trim()) {
329
394
  problems.push(reason("test_run_summary_missing", `${ev._path}: test_run requires non-empty result_summary`));
330
395
  }
396
+ const runnerOrigin = ev.runner_origin;
397
+ const executorWorkerRun = ev.apply_execution_chain === "executor_worker";
398
+ if (runnerOrigin !== undefined && runnerOrigin !== "main-thread" && runnerOrigin !== "test-runner") {
399
+ problems.push(reason("test_run_runner_origin_invalid", `${ev._path}: runner_origin must be main-thread or test-runner`));
400
+ }
401
+ if (runnerOrigin === "test-runner" && (typeof ev.phase !== "string" || !ev.phase)) {
402
+ problems.push(reason("test_run_runner_origin_invalid", `${ev._path}: runner_origin=test-runner requires phase`));
403
+ }
404
+ if (runnerOrigin === "test-runner" && ev.gate === "task_complete" && ev.semantic_status === "expected_success" && !executorWorkerRun) {
405
+ problems.push(reason("test_run_runner_origin_invalid", `${ev._path}: test-runner GREEN evidence must belong to apply_execution_chain=executor_worker`));
406
+ }
407
+ if (executorWorkerRun && runnerOrigin !== "test-runner") {
408
+ problems.push(reason("test_run_runner_origin_invalid", `${ev._path}: apply_execution_chain=executor_worker requires runner_origin=test-runner`));
409
+ }
410
+ if (executorWorkerRun && (typeof ev.apply_worker_chain_id !== "string" || !ev.apply_worker_chain_id)) {
411
+ problems.push(reason("test_run_worker_ref_missing", `${ev._path}: apply_execution_chain=executor_worker requires apply_worker_chain_id`));
412
+ }
413
+ if (runnerOrigin === "test-runner" || executorWorkerRun) {
414
+ problems.push(...worker_test_run_reasons(changeRoot, ev, typeof ev.task_id === "string" ? ev.task_id : "", executorWorkerRun && typeof ev.apply_worker_chain_id === "string" ? ev.apply_worker_chain_id : null));
415
+ }
331
416
  if (logTexts.length > 0) {
332
417
  const claimed = [
333
418
  ...(typeof ev.test_id === "string" && ev.test_id.trim() ? [ev.test_id.trim()] : []),
@@ -339,6 +424,71 @@ function test_run_reasons(ev, changeRoot) {
339
424
  }
340
425
  }
341
426
  }
427
+ if (ev.semantic_status === "expected_failure" && typeof ev.test_id === "string" && ev.test_id.trim()) {
428
+ const expected = expected_failure_contract(changeRoot, ev.test_id.trim());
429
+ if (expected.signature) {
430
+ if (ev.expected_failure_signature !== expected.signature) {
431
+ problems.push(reason("test_run_wrong_failure_reason", `${ev._path}: RED evidence expected_failure_signature must match test contract for ${ev.test_id}`, [ev.test_id]));
432
+ }
433
+ if (!logTexts.some((text) => text.includes(String(expected.signature)))) {
434
+ problems.push(reason("test_run_wrong_failure_reason", `${ev._path}: RED raw_log_refs do not contain expected_failure_signature for ${ev.test_id}`, [ev.test_id]));
435
+ }
436
+ }
437
+ if (expected.classifier && ev.expected_failure_classifier !== expected.classifier) {
438
+ problems.push(reason("test_run_wrong_failure_reason", `${ev._path}: RED evidence expected_failure_classifier must match test contract for ${ev.test_id}`, [ev.test_id]));
439
+ }
440
+ }
441
+ return problems;
442
+ }
443
+ function apply_worker_chain_reasons(ev, changeRoot) {
444
+ const problems = [];
445
+ const state = String(ev.chain_state ?? "");
446
+ if (!["active", "closed", "abandoned"].includes(state)) {
447
+ problems.push(reason("apply_worker_chain_invalid", `${ev._path}: apply_worker_chain chain_state must be active, closed, or abandoned`));
448
+ }
449
+ for (const field of ["task_id", "apply_worker_chain_id"]) {
450
+ if (typeof ev[field] !== "string" || !ev[field])
451
+ problems.push(reason("apply_worker_chain_invalid", `${ev._path}: apply_worker_chain requires ${field}`));
452
+ }
453
+ if (state === "active") {
454
+ problems.push(...fingerprint_field_reasons(ev, "executor_packet_fingerprint", "apply_worker_chain_invalid"));
455
+ problems.push(...fingerprint_field_reasons(ev, "source_implementation_fingerprint", "apply_worker_chain_invalid"));
456
+ problems.push(...string_list_field_reasons(ev, "declared_task_write_scope", "apply_worker_chain_invalid"));
457
+ problems.push(...string_list_field_reasons(ev, "pre_edit_evidence_refs", "apply_worker_chain_invalid"));
458
+ }
459
+ if (state === "closed") {
460
+ const taskId = typeof ev.task_id === "string" ? ev.task_id : undefined;
461
+ const chainId = typeof ev.apply_worker_chain_id === "string" ? ev.apply_worker_chain_id : undefined;
462
+ const refProblems = [
463
+ ...pinned_artifact_ref_item_reasons(ev.executor_report_ref, "executor_report_ref", changeRoot, { kind: "worker_report", role: "executor", task_id: taskId, chain_id: chainId }),
464
+ ...pinned_artifact_ref_item_reasons(ev.task_code_review_report_ref, "task_code_review_report_ref", changeRoot, { kind: "worker_report", role: "code-reviewer", task_id: taskId, chain_id: chainId }),
465
+ ...pinned_artifact_ref_item_reasons(ev.verifier_report_ref, "verifier_report_ref", changeRoot, { kind: "worker_report", role: "verifier", task_id: taskId, chain_id: chainId }),
466
+ ];
467
+ problems.push(...refProblems);
468
+ if (refProblems.length > 0)
469
+ problems.push(reason("apply_worker_chain_invalid", `${ev._path}: closed apply_worker_chain report refs must be pinned same-chain worker reports`));
470
+ if (!isObject(ev.green_test_run_evidence_ref) && typeof ev.green_test_run_evidence_ref !== "string") {
471
+ problems.push(reason("apply_worker_chain_invalid", `${ev._path}: closed apply_worker_chain requires green_test_run_evidence_ref`));
472
+ }
473
+ problems.push(...fingerprint_field_reasons(ev, "observed_freshness_fingerprint", "apply_worker_chain_invalid"));
474
+ }
475
+ if (state === "abandoned") {
476
+ const taskId = typeof ev.task_id === "string" ? ev.task_id : undefined;
477
+ const chainId = typeof ev.apply_worker_chain_id === "string" ? ev.apply_worker_chain_id : undefined;
478
+ if ("restored_implementation_fingerprint" in ev) {
479
+ problems.push(...fingerprint_field_reasons(ev, "restored_implementation_fingerprint", "apply_worker_chain_invalid"));
480
+ }
481
+ if (!("restored_implementation_fingerprint" in ev) && !("serial_takeover_baseline_ref" in ev)) {
482
+ problems.push(reason("apply_worker_chain_invalid", `${ev._path}: abandoned apply_worker_chain requires restored_implementation_fingerprint or serial_takeover_baseline_ref`));
483
+ }
484
+ if ("serial_takeover_baseline_ref" in ev) {
485
+ const baselineProblems = pinned_artifact_ref_item_reasons(ev.serial_takeover_baseline_ref, "serial_takeover_baseline_ref", changeRoot, { kind: "status_report", role: "verifier", task_id: taskId, chain_id: chainId });
486
+ problems.push(...baselineProblems);
487
+ if (baselineProblems.length > 0)
488
+ problems.push(reason("apply_worker_chain_invalid", `${ev._path}: abandoned apply_worker_chain serial_takeover_baseline_ref must be a pinned same-chain status_report`));
489
+ problems.push(...string_list_field_reasons(ev, "successor_green_evidence_refs", "apply_worker_chain_invalid"));
490
+ }
491
+ }
342
492
  return problems;
343
493
  }
344
494
  export function validate_evidence_schema(ev, change, changeRoot, repoRoot) {
@@ -359,6 +509,8 @@ export function validate_evidence_schema(ev, change, changeRoot, repoRoot) {
359
509
  problems.push(...human_confirmation_reasons(ev));
360
510
  if (ev.kind === "test_run")
361
511
  problems.push(...test_run_reasons(ev, changeRoot));
512
+ if (ev.kind === "apply_worker_chain")
513
+ problems.push(...apply_worker_chain_reasons(ev, changeRoot));
362
514
  // DISC Phase 1: disclosure evidence kinds and reviewer findings[] are schema-checked fail-closed.
363
515
  if (ev.kind === "main_review_digest")
364
516
  problems.push(...review_digest_schema_reasons(ev));
@@ -10,6 +10,7 @@ export declare function check_artifact(change: string, status: JsonMap, changeRo
10
10
  export declare function check_task_reopen(change: string, status: JsonMap, changeRoot: string, evidences: JsonMap[], taskId: string): Decision;
11
11
  export declare function check_apply_ready(change: string, status: JsonMap, changeRoot: string, evidences: JsonMap[]): Decision;
12
12
  export declare function check_task_edit(change: string, status: JsonMap, changeRoot: string, evidences: JsonMap[], taskId: string): Decision;
13
+ export declare function apply_worker_chain_lifecycle_reasons(repoRoot: string, changeRoot: string, evidences: JsonMap[], taskId: string): Reason[];
13
14
  export declare function check_task_complete(change: string, status: JsonMap, changeRoot: string, evidences: JsonMap[], taskId: string): Decision;
14
15
  export declare function check_review_ready(change: string, status: JsonMap, changeRoot: string, evidences: JsonMap[]): Decision;
15
16
  export declare function check_review_complete(change: string, status: JsonMap, changeRoot: string, evidences: JsonMap[]): Decision;
package/dist/src/gates.js CHANGED
@@ -8,6 +8,8 @@ import { evidence_test_id_reasons, declared_test_evidence_reasons, parse_spec_sc
8
8
  import { duplicate_evidence_id_reasons, dangling_evidence_ref_reasons, final_verification_evidences, live_task_reopens, live_task_reopen_resolutions, live_pass, live_user_confirmations, pass_task_reopens, supersede_reasons, unresolved_live_task_reopens, validate_evidence_schema, verify_reference_reasons, } from "./evidence.js";
9
9
  import { review_disclosure_reasons } from "./disclosure.js";
10
10
  import { archive_manifest_path } from "./archive.js";
11
+ import { fingerprint_matches, } from "./apply_worker_chain.js";
12
+ import { apply_worker_chain_lifecycle_reasons as shared_apply_worker_chain_lifecycle_reasons, apply_worker_chain_lifecycle_state, serial_completion_green, } from "./apply_worker_chain_lifecycle.js";
11
13
  function action_list(...items) {
12
14
  return [...new Set(items.filter((item) => typeof item === "string" && item.length > 0))];
13
15
  }
@@ -1144,6 +1146,9 @@ export function check_task_edit(change, status, changeRoot, evidences, taskId) {
1144
1146
  }
1145
1147
  return allow(change, gate, { task_id: taskId });
1146
1148
  }
1149
+ export function apply_worker_chain_lifecycle_reasons(repoRoot, changeRoot, evidences, taskId) {
1150
+ return shared_apply_worker_chain_lifecycle_reasons(repoRoot, changeRoot, evidences, taskId);
1151
+ }
1147
1152
  export function check_task_complete(change, status, changeRoot, evidences, taskId) {
1148
1153
  const gate = "task_complete";
1149
1154
  const reasons = [];
@@ -1158,6 +1163,8 @@ export function check_task_complete(change, status, changeRoot, evidences, taskI
1158
1163
  const task = parse_tasks(changeRoot)[taskId];
1159
1164
  if (!task)
1160
1165
  return block(change, gate, [reason("unknown_task", `task ${taskId} not found`)], { task_id: taskId });
1166
+ const workerChainState = apply_worker_chain_lifecycle_state(get_repo_root(status), changeRoot, evidences, taskId);
1167
+ reasons.push(...workerChainState.completionReasons);
1161
1168
  const reopenHistory = task_reopen_history(evidences, taskId);
1162
1169
  if (task.checked && reopenHistory.pass.length > 1) {
1163
1170
  reasons.push(reason("reopen_lifecycle_exhausted", `task ${taskId} has multiple task_reopen histories in this change; v1 allows at most one`, [taskId]));
@@ -1177,9 +1184,22 @@ export function check_task_complete(change, status, changeRoot, evidences, taskI
1177
1184
  if (!TDD_MODES.has(tddMode))
1178
1185
  reasons.push(reason("invalid_tdd_mode", `task ${taskId}: tdd_mode=${repr(tddMode)}`));
1179
1186
  if (tddRequired) {
1180
- const green = task_test_evidence(evidences, taskId, "expected_success", "task_complete");
1187
+ const allGreen = task_test_evidence(evidences, taskId, "expected_success", "task_complete");
1188
+ const green = workerChainState.validClosedGreenEvidenceIds.size > 0
1189
+ ? allGreen.filter((ev) => (ev.apply_execution_chain === "executor_worker"
1190
+ && workerChainState.validClosedChainIds.has(String(ev.apply_worker_chain_id ?? ""))
1191
+ && workerChainState.validClosedGreenEvidenceIds.has(String(ev.evidence_id ?? ""))))
1192
+ : workerChainState.takeoverBaselines.length > 0
1193
+ ? allGreen.filter((ev) => serial_completion_green(ev) && workerChainState.takeoverBaselines.some((baseline) => (baseline.successorGreenRefs.includes(String(ev.evidence_id ?? ""))
1194
+ && fingerprint_matches(ev.implementation_fingerprint, baseline.fingerprint))))
1195
+ : allGreen.filter(serial_completion_green);
1181
1196
  if (green.length === 0)
1182
1197
  reasons.push(reason("missing_green_evidence", `task ${taskId} requires GREEN evidence (expected_success) before completion`));
1198
+ if (workerChainState.validClosedGreenEvidenceIds.size === 0 && workerChainState.takeoverBaselines.length > 0) {
1199
+ if (green.length === 0) {
1200
+ reasons.push(reason("apply_worker_chain_takeover_green_mismatch", `task ${taskId} requires declared successor serial GREEN evidence after takeover baseline with matching implementation fingerprint`));
1201
+ }
1202
+ }
1183
1203
  const declared = new Set(splitList(attrs.test_refs ?? ""));
1184
1204
  const declaredInvariants = new Set(splitList(attrs.invariant_refs ?? ""));
1185
1205
  if (declared.size === 0)
@@ -16,6 +16,11 @@ export type ManifestFileEntry = {
16
16
  managed: boolean;
17
17
  preexisting: boolean;
18
18
  };
19
+ export type ManifestConfigPatchEntry = {
20
+ path: string;
21
+ retainedOnUninstall: boolean;
22
+ managed: boolean;
23
+ };
19
24
  export type EngineAction = {
20
25
  action: string;
21
26
  status: "ok" | "created" | "updated" | "skipped" | "removed" | "would_remove" | "failed";
@@ -27,6 +32,8 @@ export type EngineResult = {
27
32
  problems: string[];
28
33
  manifest: JsonMap | null;
29
34
  };
35
+ export declare const CODEX_NATIVE_AGENT_MAX_THREADS = 12;
36
+ export declare const CODEX_NATIVE_AGENT_MAX_DEPTH = 1;
30
37
  export declare function load_install_map(packageRoot?: string): {
31
38
  mappings: InstallMapping[];
32
39
  problems: string[];
@@ -39,6 +46,16 @@ export declare function read_install_manifest(repoRoot: string, opts?: {
39
46
  problems: string[];
40
47
  };
41
48
  export declare function install_manifest_rel(scope?: InstallScope): string;
49
+ export declare function codex_config_rel(scope?: InstallScope): string;
50
+ export declare function merge_codex_config(text: string, opts?: {
51
+ force?: boolean;
52
+ }): string;
53
+ export declare function ensure_codex_config(repoRoot: string, scope?: InstallScope, opts?: {
54
+ force?: boolean;
55
+ }): {
56
+ action: EngineAction;
57
+ problems: string[];
58
+ };
42
59
  export declare function install_workflow(repoRoot: string, opts?: {
43
60
  force?: boolean;
44
61
  packageRoot?: string;
@@ -21,6 +21,14 @@ export const PACKAGE_ROOT = find_package_root(import.meta.url);
21
21
  export const PROJECT_INSTALL_MANIFEST_REL = join(".codex", "superspec", "install-manifest.json");
22
22
  export const USER_INSTALL_MANIFEST_REL = join("superspec", "install-manifest.json");
23
23
  export const INSTALL_MANIFEST_REL = PROJECT_INSTALL_MANIFEST_REL;
24
+ export const CODEX_NATIVE_AGENT_MAX_THREADS = 12;
25
+ export const CODEX_NATIVE_AGENT_MAX_DEPTH = 1;
26
+ const CODEX_CONFIG_ENTRIES = [
27
+ { table: "features", key: "multi_agent", value: "true" },
28
+ { table: "features", key: "child_agents_md", value: "true" },
29
+ { table: "agents", key: "max_threads", value: String(CODEX_NATIVE_AGENT_MAX_THREADS) },
30
+ { table: "agents", key: "max_depth", value: String(CODEX_NATIVE_AGENT_MAX_DEPTH) },
31
+ ];
24
32
  function package_version(packageRoot) {
25
33
  try {
26
34
  const pkg = JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf8"));
@@ -78,6 +86,15 @@ export function manifest_shape_problems(manifest) {
78
86
  problems.push("createdDirs malformed");
79
87
  if (!Array.isArray(manifest.dataGlobs) || manifest.dataGlobs.some((item) => typeof item !== "string" || !item))
80
88
  problems.push("dataGlobs malformed");
89
+ if (manifest.configPatch !== undefined) {
90
+ if (!isObject(manifest.configPatch)
91
+ || typeof manifest.configPatch.path !== "string"
92
+ || !manifest.configPatch.path
93
+ || manifest.configPatch.retainedOnUninstall !== true
94
+ || manifest.configPatch.managed !== false) {
95
+ problems.push("configPatch malformed");
96
+ }
97
+ }
81
98
  if (!Array.isArray(manifest.files)) {
82
99
  problems.push("files missing");
83
100
  }
@@ -128,6 +145,101 @@ function scoped_mappings(mappings, scope) {
128
145
  return scoped === null ? [] : [scoped];
129
146
  });
130
147
  }
148
+ export function codex_config_rel(scope = "project") {
149
+ return scope === "user" ? "config.toml" : join(".codex", "config.toml");
150
+ }
151
+ function table_header_name(line) {
152
+ const match = /^\s*\[([A-Za-z0-9_.-]+)\]\s*(?:#.*)?$/u.exec(line);
153
+ return match?.[1] ?? null;
154
+ }
155
+ function is_any_table_header(line) {
156
+ return /^\s*\[[^\]]+\]\s*(?:#.*)?$/u.test(line);
157
+ }
158
+ function escape_regexp(text) {
159
+ return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
160
+ }
161
+ function split_toml_lines(text) {
162
+ if (!text)
163
+ return [];
164
+ const lines = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
165
+ if (lines[lines.length - 1] === "")
166
+ lines.pop();
167
+ return lines;
168
+ }
169
+ function ensure_toml_key(lines, table, key, value, overwriteExisting) {
170
+ let start = -1;
171
+ let end = lines.length;
172
+ for (let idx = 0; idx < lines.length; idx += 1) {
173
+ if (table_header_name(lines[idx]) !== table)
174
+ continue;
175
+ start = idx;
176
+ end = lines.length;
177
+ for (let next = idx + 1; next < lines.length; next += 1) {
178
+ if (!is_any_table_header(lines[next]))
179
+ continue;
180
+ end = next;
181
+ break;
182
+ }
183
+ break;
184
+ }
185
+ if (start === -1) {
186
+ if (lines.length > 0 && lines[lines.length - 1].trim() !== "")
187
+ lines.push("");
188
+ lines.push(`[${table}]`, `${key} = ${value}`);
189
+ return true;
190
+ }
191
+ const keyRe = new RegExp(`^\\s*${escape_regexp(key)}\\s*=`, "u");
192
+ const desired = `${key} = ${value}`;
193
+ for (let idx = start + 1; idx < end; idx += 1) {
194
+ if (!keyRe.test(lines[idx]))
195
+ continue;
196
+ if (lines[idx] === desired)
197
+ return false;
198
+ if (!overwriteExisting)
199
+ return false;
200
+ lines[idx] = desired;
201
+ return true;
202
+ }
203
+ let insertAt = end;
204
+ while (insertAt > start + 1 && lines[insertAt - 1].trim() === "")
205
+ insertAt -= 1;
206
+ lines.splice(insertAt, 0, desired);
207
+ return true;
208
+ }
209
+ export function merge_codex_config(text, opts = {}) {
210
+ const lines = split_toml_lines(text);
211
+ for (const entry of CODEX_CONFIG_ENTRIES) {
212
+ ensure_toml_key(lines, entry.table, entry.key, entry.value, opts.force === true);
213
+ }
214
+ return `${lines.join("\n")}\n`;
215
+ }
216
+ export function ensure_codex_config(repoRoot, scope = "project", opts = {}) {
217
+ const rel = codex_config_rel(scope);
218
+ const configPath = join(repoRoot, rel);
219
+ const existed = existsSync(configPath);
220
+ try {
221
+ const before = existed ? readFileSync(configPath, "utf8") : "";
222
+ const after = merge_codex_config(before, opts);
223
+ if (after === before)
224
+ return { action: { action: `configure ${rel}`, status: "ok" }, problems: [] };
225
+ mkdirSync(dirname(configPath), { recursive: true });
226
+ writeFileSync(configPath, after, "utf8");
227
+ return {
228
+ action: {
229
+ action: `configure ${rel}`,
230
+ status: existed ? "updated" : "created",
231
+ detail: `Codex native subagent concurrency set to ${CODEX_NATIVE_AGENT_MAX_THREADS}`,
232
+ },
233
+ problems: [],
234
+ };
235
+ }
236
+ catch (err) {
237
+ return {
238
+ action: { action: `configure ${rel}`, status: "failed" },
239
+ problems: [`Codex config update failed: ${err.message}`],
240
+ };
241
+ }
242
+ }
131
243
  function write_install_manifest(repoRoot, packageRoot, files, createdDirs, scope) {
132
244
  const manifest = {
133
245
  superspecVersion: package_version(packageRoot),
@@ -138,6 +250,11 @@ function write_install_manifest(repoRoot, packageRoot, files, createdDirs, scope
138
250
  files,
139
251
  createdDirs: [...new Set(createdDirs)].sort(),
140
252
  dataGlobs: ["**/.superspec"],
253
+ configPatch: {
254
+ path: codex_config_rel(scope),
255
+ retainedOnUninstall: true,
256
+ managed: false,
257
+ },
141
258
  };
142
259
  const manifestPath = join(repoRoot, install_manifest_rel(scope));
143
260
  mkdirSync(dirname(manifestPath), { recursive: true });
@@ -212,8 +329,11 @@ export function install_workflow(repoRoot, opts = {}) {
212
329
  actions.push({ action: `install ${mapping.target}`, status: "skipped", detail: "pre-existing file with different content kept; rerun with --force to overwrite (backs up *.bak)" });
213
330
  }
214
331
  }
332
+ const configResult = ensure_codex_config(repoRoot, scope, { force: opts.force === true });
333
+ actions.push(configResult.action);
334
+ problems.push(...configResult.problems);
215
335
  const manifest = write_install_manifest(repoRoot, packageRoot, files, createdDirs, scope);
216
- return { actions, problems: [], manifest };
336
+ return { actions, problems, manifest };
217
337
  }
218
338
  export function update_workflow(repoRoot, opts = {}) {
219
339
  const packageRoot = opts.packageRoot ?? PACKAGE_ROOT;
@@ -287,8 +407,11 @@ export function update_workflow(repoRoot, opts = {}) {
287
407
  actions.push({ action: `update ${prev.path}`, status: "skipped", detail: "no longer shipped but user-modified; kept" });
288
408
  }
289
409
  }
410
+ const configResult = ensure_codex_config(repoRoot, scope);
411
+ actions.push(configResult.action);
412
+ problems.push(...configResult.problems);
290
413
  const manifest = write_install_manifest(repoRoot, packageRoot, files, createdDirs, scope);
291
- return { actions, problems: [], manifest };
414
+ return { actions, problems, manifest };
292
415
  }
293
416
  function remove_empty_created_dirs(repoRoot, createdDirs, actions) {
294
417
  const byDepth = [...new Set(createdDirs)].sort((a, b) => b.split("/").length - a.split("/").length);
@@ -49,9 +49,11 @@ const REPRESENTATIVE_SCENARIOS = [
49
49
  },
50
50
  {
51
51
  name: "apply_ready",
52
- description: "Post-bridge runtime surface for apply orchestration before task execution.",
52
+ description: "Post-bridge runtime surface for apply orchestration and bounded executor handoff.",
53
53
  files: [
54
54
  ".codex/skills/superspec-apply/SKILL.md",
55
+ ".codex/prompts/executor.md",
56
+ ".codex/agents/executor.toml",
55
57
  ],
56
58
  },
57
59
  {
@@ -99,9 +101,11 @@ const REPRESENTATIVE_SCENARIOS = [
99
101
  },
100
102
  {
101
103
  name: "task_reopen_to_resolved",
102
- description: "Post-bridge runtime surface for reopened apply work from revert through successor completion.",
104
+ description: "Post-bridge runtime surface for reopened apply work from revert through successor executor completion.",
103
105
  files: [
104
106
  ".codex/skills/superspec-apply/SKILL.md",
107
+ ".codex/prompts/executor.md",
108
+ ".codex/agents/executor.toml",
105
109
  ],
106
110
  },
107
111
  {
@@ -159,10 +163,28 @@ const LEDGER_BLOCK_SAMPLES = [
159
163
  ];
160
164
  function ensureReadableTextFile(repoRoot, relPath) {
161
165
  const absPath = join(repoRoot, relPath);
162
- if (!existsSync(absPath) || !statSync(absPath).isFile()) {
163
- throw new GuardError(`packet_measure_missing_file: ${relPath}`);
166
+ if (existsSync(absPath) && statSync(absPath).isFile()) {
167
+ return readFileSync(absPath, "utf8");
164
168
  }
165
- return readFileSync(absPath, "utf8");
169
+ const skillMatch = /^\.codex\/skills\/([^/]+)\/SKILL\.md$/u.exec(relPath);
170
+ if (skillMatch) {
171
+ const fallback = join(repoRoot, "templates", "workflow", "skills", skillMatch[1], "SKILL.md");
172
+ if (existsSync(fallback) && statSync(fallback).isFile())
173
+ return readFileSync(fallback, "utf8");
174
+ }
175
+ const promptMatch = /^\.codex\/prompts\/([^/]+)\.md$/u.exec(relPath);
176
+ if (promptMatch) {
177
+ const fallback = join(repoRoot, "templates", "workflow", "prompts", `${promptMatch[1]}.md`);
178
+ if (existsSync(fallback) && statSync(fallback).isFile())
179
+ return readFileSync(fallback, "utf8");
180
+ }
181
+ const agentMatch = /^\.codex\/agents\/([^/]+)\.toml$/u.exec(relPath);
182
+ if (agentMatch) {
183
+ const fallback = join(repoRoot, "adapters", "codex", "agents", `${agentMatch[1]}.toml`);
184
+ if (existsSync(fallback) && statSync(fallback).isFile())
185
+ return readFileSync(fallback, "utf8");
186
+ }
187
+ throw new GuardError(`packet_measure_missing_file: ${relPath}`);
166
188
  }
167
189
  function dedupePaths(paths) {
168
190
  return [...new Set(paths)].sort();