@cardor/agent-harness-kit 2.1.1 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -20,7 +20,7 @@ import pc21 from "picocolors";
20
20
 
21
21
  // src/commands/build.ts
22
22
  import { watch } from "fs";
23
- import * as p2 from "@clack/prompts";
23
+ import * as p3 from "@clack/prompts";
24
24
  import pc2 from "picocolors";
25
25
 
26
26
  // src/core/materializer/claude-code.ts
@@ -53,7 +53,7 @@ import { fileURLToPath } from "url";
53
53
  var require2 = createRequire(import.meta.url);
54
54
  var here = dirname(fileURLToPath(import.meta.url));
55
55
  var candidates = [join2(here, "..", "..", "package.json"), join2(here, "..", "package.json")];
56
- var pkgPath = candidates.find((p9) => existsSync(p9)) ?? candidates[0];
56
+ var pkgPath = candidates.find((p10) => existsSync(p10)) ?? candidates[0];
57
57
  var pkg = require2(pkgPath);
58
58
 
59
59
  // src/core/local-install-guard.ts
@@ -373,6 +373,17 @@ function mergeTomlSection(content, sectionName, sectionBody) {
373
373
  ];
374
374
  return newLines.join("\n");
375
375
  }
376
+ function ensureTomlTopLevelKey(content, key, value) {
377
+ const lines = content.length > 0 ? content.split("\n") : [];
378
+ const firstSectionIdx = lines.findIndex((l) => /^\s*\[/.test(l));
379
+ const preambleEnd = firstSectionIdx === -1 ? lines.length : firstSectionIdx;
380
+ const keyRe = new RegExp(`^\\s*${key}\\s*=`);
381
+ const alreadyPresent = lines.slice(0, preambleEnd).some((l) => keyRe.test(l));
382
+ if (alreadyPresent) return content;
383
+ const newLines = [...lines];
384
+ newLines.splice(preambleEnd, 0, `${key} = ${JSON.stringify(value)}`);
385
+ return newLines.join("\n");
386
+ }
376
387
  function mergeCodexConfigToml(filePath, port, cwd2, pm = "npm") {
377
388
  mkdirSync2(dirname2(filePath), { recursive: true });
378
389
  let content = "";
@@ -383,8 +394,10 @@ function mergeCodexConfigToml(filePath, port, cwd2, pm = "npm") {
383
394
  const sectionBody = [
384
395
  `command = ${JSON.stringify(command)}`,
385
396
  `args = ${JSON.stringify(args)}`,
386
- 'default_tools_approval_mode = "auto"'
397
+ 'default_tools_approval_mode = "approve"'
387
398
  ].join("\n");
399
+ content = ensureTomlTopLevelKey(content, "model", "gpt-5.6-terra");
400
+ content = ensureTomlTopLevelKey(content, "model_reasoning_effort", "medium");
388
401
  content = mergeTomlSection(content, "mcp_servers.agent-harness-kit", sectionBody);
389
402
  writeFileSync2(filePath, content, "utf8");
390
403
  }
@@ -428,14 +441,16 @@ function claudeDisallowedTools(agentName) {
428
441
  function opencodePermissions(agentName) {
429
442
  return restrictionFor(agentName) === "no-write" ? { edit: "deny" } : {};
430
443
  }
431
- function codexSandboxMode(agentName) {
432
- return restrictionFor(agentName) === "no-write" ? "read-only" : "workspace-write";
444
+ function codexSandboxMode(_agentName) {
445
+ return "danger-full-access";
433
446
  }
434
- var CODEX_READ_ONLY_NOTICE = `## Tool restrictions (enforced by the sandbox)
447
+ var CODEX_READ_ONLY_NOTICE = `## Tool restrictions (enforced by instruction only \u2014 NOT by the sandbox)
435
448
 
436
- This agent runs with \`sandbox_mode = "read-only"\`. You MUST NOT create, modify, or delete any file: no \`Write\`, no \`Edit\`, no \`apply_patch\`, and no shell command that writes to disk (\`>\`, \`tee\`, \`sed -i\`, \`mv\`, \`rm\`, ...).
449
+ This agent runs UNSANDBOXED: \`sandbox_mode = "danger-full-access"\`. There is no OS-level write protection. This is a deliberate project configuration choice, not an oversight.
437
450
 
438
- These tools may still appear available to you. The sandbox will reject the call. Do not retry a rejected write \u2014 report it as a blocker instead.`;
451
+ You MUST NOT create, modify, or delete any file: no \`Write\`, no \`Edit\`, no \`apply_patch\`, and no shell command that writes to disk (\`>\`, \`tee\`, \`sed -i\`, \`mv\`, \`rm\`, ...). This restriction is enforced ONLY by you following this instruction \u2014 nothing will technically block or reject the call.
452
+
453
+ If you find yourself about to perform a write, STOP. Do not perform it. Report it as a blocker instead. Treat this as a hard rule: breaking it will not fail loudly, it will silently break the harness's audit trail and workflow guarantees.`;
439
454
  function codexRestrictionNotice(agentName) {
440
455
  return restrictionFor(agentName) === "no-write" ? CODEX_READ_ONLY_NOTICE : "";
441
456
  }
@@ -783,7 +798,7 @@ function stripFrontmatter(md) {
783
798
  }
784
799
  return { description, body };
785
800
  }
786
- function toCodexToml(tomlName, agentName, description, body) {
801
+ function toCodexToml(tomlName, agentName, description, body, opts) {
787
802
  const safe = (s) => s.replace(/"""/g, '""\\u0022');
788
803
  const sandboxMode = codexSandboxMode(agentName);
789
804
  const notice = codexRestrictionNotice(agentName);
@@ -792,9 +807,14 @@ function toCodexToml(tomlName, agentName, description, body) {
792
807
  ---
793
808
 
794
809
  ${notice}` : body.trimEnd();
810
+ const modelLines = [];
811
+ if (opts?.model) modelLines.push(`model = "${opts.model}"`);
812
+ if (opts?.effort) modelLines.push(`model_reasoning_effort = "${opts.effort}"`);
813
+ const modelBlock = modelLines.length > 0 ? `${modelLines.join("\n")}
814
+ ` : "";
795
815
  return `name = "${tomlName}"
796
816
  sandbox_mode = "${sandboxMode}"
797
-
817
+ ${modelBlock}
798
818
  description = """
799
819
  ${safe(description)}
800
820
  """
@@ -804,29 +824,29 @@ ${safe(instructions)}
804
824
  """
805
825
  `;
806
826
  }
807
- function agentLeadToml(vars) {
827
+ function agentLeadToml(vars, opts) {
808
828
  const { description, body } = stripFrontmatter(loadAgentTemplate("lead", vars));
809
- return toCodexToml("lead", "lead", description, body);
829
+ return toCodexToml("lead", "lead", description, body, opts);
810
830
  }
811
- function agentLeadAsDefaultToml(vars) {
831
+ function agentLeadAsDefaultToml(vars, opts) {
812
832
  const { description, body } = stripFrontmatter(loadAgentTemplate("lead", vars));
813
- return toCodexToml("default", "lead", description, body);
833
+ return toCodexToml("default", "lead", description, body, opts);
814
834
  }
815
- function agentExplorerToml(vars) {
835
+ function agentExplorerToml(vars, opts) {
816
836
  const { description, body } = stripFrontmatter(loadAgentTemplate("explorer", vars));
817
- return toCodexToml("explorer", "explorer", description, body);
837
+ return toCodexToml("explorer", "explorer", description, body, opts);
818
838
  }
819
- function agentBuilderToml(vars) {
839
+ function agentBuilderToml(vars, opts) {
820
840
  const { description, body } = stripFrontmatter(loadAgentTemplate("builder", vars));
821
- return toCodexToml("builder", "builder", description, body);
841
+ return toCodexToml("builder", "builder", description, body, opts);
822
842
  }
823
- function agentReviewerToml(vars) {
843
+ function agentReviewerToml(vars, opts) {
824
844
  const { description, body } = stripFrontmatter(loadAgentTemplate("reviewer", vars));
825
- return toCodexToml("reviewer", "reviewer", description, body);
845
+ return toCodexToml("reviewer", "reviewer", description, body, opts);
826
846
  }
827
- function agentConsultantToml(vars) {
847
+ function agentConsultantToml(vars, opts) {
828
848
  const { description, body } = stripFrontmatter(loadAgentTemplate("consultant", vars));
829
- return toCodexToml("consultant", "consultant", description, body);
849
+ return toCodexToml("consultant", "consultant", description, body, opts);
830
850
  }
831
851
  function stripFrontmatterBlockSequence(md, key) {
832
852
  const re = new RegExp(`^${key}:\\n(?: - [^\\n]+\\n)+`, "m");
@@ -1128,20 +1148,20 @@ No tasks in progress.
1128
1148
  // src/core/materializer/codex-cli.ts
1129
1149
  import { existsSync as existsSync7, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
1130
1150
  import { join as join8, resolve as resolve3 } from "path";
1131
- function codexAgentFiles(config) {
1151
+ function codexAgentFiles(config, modelsByRole) {
1132
1152
  const projectName = config.project.name;
1133
1153
  return [
1134
- { relPath: ".codex/agents/lead.toml", content: agentLeadToml({ projectName }) },
1135
- { relPath: ".codex/agents/explorer.toml", content: agentExplorerToml({ projectName }) },
1136
- { relPath: ".codex/agents/consultant.toml", content: agentConsultantToml({ projectName }) },
1137
- { relPath: ".codex/agents/builder.toml", content: agentBuilderToml({ projectName }) },
1138
- { relPath: ".codex/agents/reviewer.toml", content: agentReviewerToml({ projectName }) },
1139
- { relPath: ".codex/agents/default.toml", content: agentLeadAsDefaultToml({ projectName }) }
1154
+ { relPath: ".codex/agents/lead.toml", content: agentLeadToml({ projectName }, modelsByRole?.lead) },
1155
+ { relPath: ".codex/agents/explorer.toml", content: agentExplorerToml({ projectName }, modelsByRole?.explorer) },
1156
+ { relPath: ".codex/agents/consultant.toml", content: agentConsultantToml({ projectName }, modelsByRole?.consultant) },
1157
+ { relPath: ".codex/agents/builder.toml", content: agentBuilderToml({ projectName }, modelsByRole?.builder) },
1158
+ { relPath: ".codex/agents/reviewer.toml", content: agentReviewerToml({ projectName }, modelsByRole?.reviewer) },
1159
+ { relPath: ".codex/agents/default.toml", content: agentLeadAsDefaultToml({ projectName }, modelsByRole?.lead) }
1140
1160
  ];
1141
1161
  }
1142
1162
  var CodexCliMaterializer = class {
1143
1163
  async scaffold(config, opts) {
1144
- const { cwd: cwd2 } = opts;
1164
+ const { cwd: cwd2, codexAgentModels } = opts;
1145
1165
  const write2 = (relPath, content, mode) => {
1146
1166
  const abs = join8(cwd2, relPath);
1147
1167
  mkdirSync4(resolve3(abs, ".."), { recursive: true });
@@ -1163,7 +1183,7 @@ No tasks in progress.
1163
1183
  `
1164
1184
  );
1165
1185
  }
1166
- writeAgentFiles(cwd2, codexAgentFiles(config));
1186
+ writeAgentFiles(cwd2, codexAgentFiles(config, codexAgentModels));
1167
1187
  mergeCodexConfigToml(join8(cwd2, ".codex/config.toml"), config.tools.mcp.port, cwd2, detectPackageManager(cwd2));
1168
1188
  appendGitignore(cwd2);
1169
1189
  writeSkills(cwd2, ".agents/skills");
@@ -1174,7 +1194,7 @@ No tasks in progress.
1174
1194
  [{ relPath: "AGENTS.md", content: agentsMd(config) }],
1175
1195
  { force: opts.force, backupRoot: join8(cwd2, config.storage.dir, "backups") }
1176
1196
  );
1177
- const agents = writeAgentFiles(cwd2, codexAgentFiles(config), {
1197
+ const agents = writeAgentFiles(cwd2, codexAgentFiles(config, opts.codexAgentModels), {
1178
1198
  force: opts.force,
1179
1199
  backupRoot: join8(cwd2, config.storage.dir, "backups")
1180
1200
  });
@@ -1367,20 +1387,69 @@ async function promptClaudeAgentModels(provider) {
1367
1387
  return claudeAgentModels;
1368
1388
  }
1369
1389
 
1390
+ // src/commands/codex-model-prompt.ts
1391
+ import * as p2 from "@clack/prompts";
1392
+ var AGENT_LABELS2 = [
1393
+ { key: "lead", label: "Lead" },
1394
+ { key: "explorer", label: "Explorer" },
1395
+ { key: "consultant", label: "Consultant" },
1396
+ { key: "builder", label: "Builder" },
1397
+ { key: "reviewer", label: "Reviewer" }
1398
+ ];
1399
+ var CODEX_MODEL_CHOICES = [
1400
+ "gpt-5.6-sol",
1401
+ "gpt-5.6-terra",
1402
+ "gpt-5.6-luna",
1403
+ "gpt-5.5",
1404
+ "gpt-5.4",
1405
+ "gpt-5.4-mini",
1406
+ "gpt-5.3-codex-spark"
1407
+ ];
1408
+ var CODEX_EFFORT_CHOICES = ["minimal", "low", "medium", "high", "xhigh"];
1409
+ async function promptCodexAgentModels(provider) {
1410
+ const codexAgentModels = {};
1411
+ if (provider !== "codex-cli") return codexAgentModels;
1412
+ for (const agent of AGENT_LABELS2) {
1413
+ const modelVal = await p2.select({
1414
+ message: `Model for ${agent.label}`,
1415
+ options: CODEX_MODEL_CHOICES.map((value) => ({ value, label: value })),
1416
+ initialValue: "gpt-5.6-terra"
1417
+ });
1418
+ if (p2.isCancel(modelVal)) {
1419
+ p2.cancel("Cancelled.");
1420
+ process.exit(0);
1421
+ }
1422
+ const effortVal = await p2.select({
1423
+ message: `Reasoning effort for ${agent.label}`,
1424
+ options: CODEX_EFFORT_CHOICES.map((value) => ({ value, label: value })),
1425
+ initialValue: "medium"
1426
+ });
1427
+ if (p2.isCancel(effortVal)) {
1428
+ p2.cancel("Cancelled.");
1429
+ process.exit(0);
1430
+ }
1431
+ codexAgentModels[agent.key] = {
1432
+ model: modelVal,
1433
+ effort: effortVal
1434
+ };
1435
+ }
1436
+ return codexAgentModels;
1437
+ }
1438
+
1370
1439
  // src/commands/build.ts
1371
1440
  async function runBuild(cwd2, opts) {
1372
1441
  await buildOnce(cwd2, opts.force);
1373
1442
  if (opts.sync) {
1374
- p2.log.step("Syncing agent permissions...");
1443
+ p3.log.step("Syncing agent permissions...");
1375
1444
  const config = await loadConfig(cwd2);
1376
1445
  const materializer = getMaterializer(config.provider);
1377
1446
  await materializer.syncPermissions(cwd2);
1378
1447
  }
1379
1448
  if (opts.watch) {
1380
- p2.log.info(`Watching agent-harness-kit.config.ts for changes...`);
1449
+ p3.log.info(`Watching agent-harness-kit.config.ts for changes...`);
1381
1450
  watch(cwd2, { recursive: false }, async (_, filename) => {
1382
1451
  if (filename?.startsWith("agent-harness-kit.config")) {
1383
- p2.log.step("Config changed \u2014 rebuilding...");
1452
+ p3.log.step("Config changed \u2014 rebuilding...");
1384
1453
  await buildOnce(cwd2, false);
1385
1454
  }
1386
1455
  });
@@ -1393,41 +1462,45 @@ async function buildOnce(cwd2, force) {
1393
1462
  try {
1394
1463
  config = await loadConfig(cwd2);
1395
1464
  } catch (err) {
1396
- p2.log.error(err instanceof Error ? err.message : String(err));
1465
+ p3.log.error(err instanceof Error ? err.message : String(err));
1397
1466
  process.exit(1);
1398
1467
  }
1399
1468
  let claudeAgentModels;
1400
1469
  if (force && config.provider === "claude-code") {
1401
1470
  claudeAgentModels = await promptClaudeAgentModels(config.provider);
1402
1471
  }
1403
- const spinner6 = p2.spinner();
1472
+ let codexAgentModels;
1473
+ if (force && config.provider === "codex-cli") {
1474
+ codexAgentModels = await promptCodexAgentModels(config.provider);
1475
+ }
1476
+ const spinner6 = p3.spinner();
1404
1477
  spinner6.start("Rebuilding files...");
1405
1478
  try {
1406
1479
  const materializer = getMaterializer(config.provider);
1407
- const report = await materializer.build(config, cwd2, { force, claudeAgentModels });
1480
+ const report = await materializer.build(config, cwd2, { force, claudeAgentModels, codexAgentModels });
1408
1481
  spinner6.stop(pc2.green("Build complete"));
1409
1482
  const d = report.derived;
1410
1483
  const upToDate = [...d.created, ...d.current, ...d.propagated];
1411
1484
  if (upToDate.length > 0) {
1412
- p2.log.success(upToDate.join(", "));
1485
+ p3.log.success(upToDate.join(", "));
1413
1486
  }
1414
1487
  if (d.propagated.length > 0) {
1415
- p2.log.info(`Propagated config changes to ${d.propagated.length} generated file(s):
1488
+ p3.log.info(`Propagated config changes to ${d.propagated.length} generated file(s):
1416
1489
  ${d.propagated.join("\n ")}`);
1417
1490
  }
1418
1491
  if (d.overwritten.length > 0) {
1419
- p2.log.warn(
1492
+ p3.log.warn(
1420
1493
  pc2.yellow(
1421
1494
  `--force REGENERATED ${d.overwritten.length} hand-edited generated file(s), discarding your edits:
1422
1495
  ` + d.overwritten.join("\n ")
1423
1496
  )
1424
1497
  );
1425
1498
  if (d.backupDir) {
1426
- p2.log.info(pc2.yellow(` Previous content backed up \u2192 ${d.backupDir}`));
1499
+ p3.log.info(pc2.yellow(` Previous content backed up \u2192 ${d.backupDir}`));
1427
1500
  }
1428
1501
  }
1429
1502
  if (d.preserved.length > 0) {
1430
- p2.log.warn(
1503
+ p3.log.warn(
1431
1504
  pc2.yellow(
1432
1505
  `Left ${d.preserved.length} hand-edited generated file(s) UNTOUCHED \u2014 your edits are safe:
1433
1506
  ` + d.preserved.join("\n ") + `
@@ -1436,26 +1509,26 @@ async function buildOnce(cwd2, force) {
1436
1509
  )
1437
1510
  );
1438
1511
  }
1439
- p2.log.success(`Agent definitions (${config.provider})`);
1440
- p2.log.success("MCP config");
1512
+ p3.log.success(`Agent definitions (${config.provider})`);
1513
+ p3.log.success("MCP config");
1441
1514
  const { created, overwritten, preserved, backupDir } = report.agents;
1442
1515
  if (created.length > 0) {
1443
- p2.log.info(`Created ${created.length} missing agent file(s):
1516
+ p3.log.info(`Created ${created.length} missing agent file(s):
1444
1517
  ${created.join("\n ")}`);
1445
1518
  }
1446
1519
  if (overwritten.length > 0) {
1447
- p2.log.warn(
1520
+ p3.log.warn(
1448
1521
  pc2.yellow(
1449
1522
  `--force REGENERATED ${overwritten.length} existing agent file(s), discarding any customizations:
1450
1523
  ` + overwritten.join("\n ")
1451
1524
  )
1452
1525
  );
1453
1526
  if (backupDir) {
1454
- p2.log.info(pc2.yellow(` Previous content backed up \u2192 ${backupDir}`));
1527
+ p3.log.info(pc2.yellow(` Previous content backed up \u2192 ${backupDir}`));
1455
1528
  }
1456
1529
  }
1457
1530
  if (preserved.length > 0) {
1458
- p2.log.info(
1531
+ p3.log.info(
1459
1532
  `Left ${preserved.length} existing agent file(s) untouched \u2014 agent files are yours to edit.
1460
1533
  Re-run with --force to regenerate them from the packaged templates (this DESTROYS your edits;
1461
1534
  a backup is written first).`
@@ -1463,7 +1536,7 @@ async function buildOnce(cwd2, force) {
1463
1536
  }
1464
1537
  } catch (err) {
1465
1538
  spinner6.stop(pc2.red("Build failed"));
1466
- p2.log.error(err instanceof Error ? err.message : String(err));
1539
+ p3.log.error(err instanceof Error ? err.message : String(err));
1467
1540
  process.exit(1);
1468
1541
  }
1469
1542
  }
@@ -2077,7 +2150,7 @@ function getProviderHealthFiles(provider) {
2077
2150
  // src/commands/init.ts
2078
2151
  import { existsSync as existsSync14, mkdirSync as mkdirSync7, readFileSync as readFileSync9, writeFileSync as writeFileSync8 } from "fs";
2079
2152
  import { join as join16 } from "path";
2080
- import * as p4 from "@clack/prompts";
2153
+ import * as p5 from "@clack/prompts";
2081
2154
  import pc8 from "picocolors";
2082
2155
 
2083
2156
  // src/schema/init.ts
@@ -2116,7 +2189,7 @@ var taskDescriptionSchema = v2.pipe(
2116
2189
  );
2117
2190
 
2118
2191
  // src/utils/form.ts
2119
- import * as p3 from "@clack/prompts";
2192
+ import * as p4 from "@clack/prompts";
2120
2193
  import * as v3 from "valibot";
2121
2194
  var cliFormWithRetry = async (formFn, schema) => {
2122
2195
  while (true) {
@@ -2124,8 +2197,8 @@ var cliFormWithRetry = async (formFn, schema) => {
2124
2197
  const result = v3.safeParse(schema, res);
2125
2198
  if (result.success) return result.output;
2126
2199
  const messages = result.issues.map((i) => i.message).join(", ");
2127
- p3.log.error(messages);
2128
- p3.log.info("Please try again.\n");
2200
+ p4.log.error(messages);
2201
+ p4.log.info("Please try again.\n");
2129
2202
  }
2130
2203
  };
2131
2204
 
@@ -2286,25 +2359,25 @@ async function runInit(cwd2, flags) {
2286
2359
  name = flags.name;
2287
2360
  } else {
2288
2361
  name = await cliFormWithRetry(async () => {
2289
- const val = await p4.text({
2362
+ const val = await p5.text({
2290
2363
  message: "Project name",
2291
2364
  placeholder: "my-app",
2292
2365
  ...detectedName && { initialValue: detectedName }
2293
2366
  });
2294
- if (p4.isCancel(val)) {
2295
- p4.cancel("Cancelled.");
2367
+ if (p5.isCancel(val)) {
2368
+ p5.cancel("Cancelled.");
2296
2369
  process.exit(0);
2297
2370
  }
2298
2371
  return val;
2299
2372
  }, initNameSchema);
2300
2373
  }
2301
2374
  const description = await cliFormWithRetry(async () => {
2302
- const val = await p4.text({
2375
+ const val = await p5.text({
2303
2376
  message: "Short description (shown to agents as context)",
2304
2377
  placeholder: "A REST API for managing notes"
2305
2378
  });
2306
- if (p4.isCancel(val)) {
2307
- p4.cancel("Cancelled.");
2379
+ if (p5.isCancel(val)) {
2380
+ p5.cancel("Cancelled.");
2308
2381
  process.exit(0);
2309
2382
  }
2310
2383
  return val;
@@ -2313,7 +2386,7 @@ async function runInit(cwd2, flags) {
2313
2386
  if (flags.provider && ["claude-code", "opencode", "codex-cli", "grok-cli"].includes(flags.provider)) {
2314
2387
  provider = flags.provider;
2315
2388
  } else {
2316
- const val = await p4.select({
2389
+ const val = await p5.select({
2317
2390
  message: "AI provider",
2318
2391
  options: [
2319
2392
  { value: "opencode", label: "OpenCode" },
@@ -2322,24 +2395,25 @@ async function runInit(cwd2, flags) {
2322
2395
  { value: "grok-cli", label: "Grok CLI" }
2323
2396
  ]
2324
2397
  });
2325
- if (p4.isCancel(val)) {
2326
- p4.cancel("Cancelled.");
2398
+ if (p5.isCancel(val)) {
2399
+ p5.cancel("Cancelled.");
2327
2400
  process.exit(0);
2328
2401
  }
2329
2402
  provider = val;
2330
2403
  }
2331
2404
  const claudeAgentModels = await promptClaudeAgentModels(provider);
2405
+ const codexAgentModels = await promptCodexAgentModels(provider);
2332
2406
  let docsPath;
2333
2407
  if (flags.docs) {
2334
2408
  docsPath = flags.docs;
2335
2409
  } else {
2336
2410
  docsPath = await cliFormWithRetry(async () => {
2337
- const val = await p4.text({
2411
+ const val = await p5.text({
2338
2412
  message: "Docs folder path (agents will search here)",
2339
2413
  initialValue: "./docs"
2340
2414
  });
2341
- if (p4.isCancel(val)) {
2342
- p4.cancel("Cancelled.");
2415
+ if (p5.isCancel(val)) {
2416
+ p5.cancel("Cancelled.");
2343
2417
  process.exit(0);
2344
2418
  }
2345
2419
  return val;
@@ -2349,7 +2423,7 @@ async function runInit(cwd2, flags) {
2349
2423
  if (flags.storageScope && ["local", "global"].includes(flags.storageScope)) {
2350
2424
  storageScope = flags.storageScope;
2351
2425
  } else {
2352
- const val = await p4.select({
2426
+ const val = await p5.select({
2353
2427
  message: "Storage scope",
2354
2428
  options: [
2355
2429
  { value: "local", label: "Local \u2014 .harness/harness.db lives in this project" },
@@ -2360,8 +2434,8 @@ async function runInit(cwd2, flags) {
2360
2434
  ],
2361
2435
  initialValue: "local"
2362
2436
  });
2363
- if (p4.isCancel(val)) {
2364
- p4.cancel("Cancelled.");
2437
+ if (p5.isCancel(val)) {
2438
+ p5.cancel("Cancelled.");
2365
2439
  process.exit(0);
2366
2440
  }
2367
2441
  storageScope = val;
@@ -2370,7 +2444,7 @@ async function runInit(cwd2, flags) {
2370
2444
  if (flags.tasks && ["local", "jira", "linear"].includes(flags.tasks)) {
2371
2445
  tasksAdapter = flags.tasks;
2372
2446
  } else {
2373
- const val = await p4.select({
2447
+ const val = await p5.select({
2374
2448
  message: "Task adapter",
2375
2449
  options: [
2376
2450
  { value: "local", label: "Local (feature_list.json)" },
@@ -2378,50 +2452,50 @@ async function runInit(cwd2, flags) {
2378
2452
  { value: "linear", label: "Linear (coming soon)" }
2379
2453
  ]
2380
2454
  });
2381
- if (p4.isCancel(val)) {
2382
- p4.cancel("Cancelled");
2455
+ if (p5.isCancel(val)) {
2456
+ p5.cancel("Cancelled");
2383
2457
  process.exit(0);
2384
2458
  }
2385
2459
  tasksAdapter = val;
2386
2460
  }
2387
- const addFirstTask = await p4.confirm({ message: "Add your first task now?", initialValue: false });
2388
- if (p4.isCancel(addFirstTask)) {
2389
- p4.cancel("Cancelled");
2461
+ const addFirstTask = await p5.confirm({ message: "Add your first task now?", initialValue: false });
2462
+ if (p5.isCancel(addFirstTask)) {
2463
+ p5.cancel("Cancelled");
2390
2464
  process.exit(0);
2391
2465
  }
2392
2466
  let firstTask;
2393
2467
  if (addFirstTask) {
2394
2468
  const taskTitle = await cliFormWithRetry(async () => {
2395
- const val = await p4.text({ message: "Task title" });
2396
- if (p4.isCancel(val)) {
2397
- p4.cancel("Cancelled");
2469
+ const val = await p5.text({ message: "Task title" });
2470
+ if (p5.isCancel(val)) {
2471
+ p5.cancel("Cancelled");
2398
2472
  process.exit(0);
2399
2473
  }
2400
2474
  return val.trim();
2401
2475
  }, taskTitleSchema);
2402
2476
  const taskDesc = await cliFormWithRetry(async () => {
2403
- const val = await p4.text({ message: "Task description", placeholder: "What and why" });
2404
- if (p4.isCancel(val)) {
2405
- p4.cancel("Cancelled");
2477
+ const val = await p5.text({ message: "Task description", placeholder: "What and why" });
2478
+ if (p5.isCancel(val)) {
2479
+ p5.cancel("Cancelled");
2406
2480
  process.exit(0);
2407
2481
  }
2408
2482
  return val.trim();
2409
2483
  }, taskDescriptionSchema);
2410
2484
  const acceptance = [];
2411
- p4.log.info("Acceptance criteria \u2014 one per line, empty line to finish");
2485
+ p5.log.info("Acceptance criteria \u2014 one per line, empty line to finish");
2412
2486
  while (true) {
2413
- const criterionVal = await p4.text({
2487
+ const criterionVal = await p5.text({
2414
2488
  message: ">",
2415
2489
  placeholder: "Criterion (or press Enter to finish)"
2416
2490
  });
2417
- if (p4.isCancel(criterionVal) || !criterionVal || !criterionVal.trim()) break;
2491
+ if (p5.isCancel(criterionVal) || !criterionVal || !criterionVal.trim()) break;
2418
2492
  acceptance.push(criterionVal.trim());
2419
2493
  }
2420
2494
  firstTask = { title: taskTitle, description: taskDesc, acceptance };
2421
2495
  }
2422
2496
  let configExt = "ts";
2423
2497
  let featureListParseFailedPath = null;
2424
- const spinner6 = p4.spinner();
2498
+ const spinner6 = p5.spinner();
2425
2499
  spinner6.start("Scaffolding...");
2426
2500
  try {
2427
2501
  const config = applyConfigDefaults({
@@ -2451,7 +2525,7 @@ async function runInit(cwd2, flags) {
2451
2525
  mkdirSync7(join16(installDir, config.storage.dir), { recursive: true });
2452
2526
  const db = await openDB(config, installDir);
2453
2527
  await db.writeStorageState(installDir);
2454
- await materializer.scaffold(config, { cwd: installDir, firstTask, claudeAgentModels });
2528
+ await materializer.scaffold(config, { cwd: installDir, firstTask, claudeAgentModels, codexAgentModels });
2455
2529
  const { parseFailed } = await reconcileFeatureList(db, installDir, config.storage.dir, firstTask);
2456
2530
  if (parseFailed) {
2457
2531
  featureListParseFailedPath = join16(config.storage.dir, "feature_list.json");
@@ -2460,7 +2534,7 @@ async function runInit(cwd2, flags) {
2460
2534
  spinner6.stop("");
2461
2535
  } catch (err) {
2462
2536
  spinner6.stop("Failed");
2463
- p4.log.error(err instanceof Error ? err.message : String(err));
2537
+ p5.log.error(err instanceof Error ? err.message : String(err));
2464
2538
  throw err;
2465
2539
  }
2466
2540
  if (featureListParseFailedPath) {
@@ -2512,7 +2586,7 @@ async function runInit(cwd2, flags) {
2512
2586
  }
2513
2587
 
2514
2588
  // src/commands/migrate.ts
2515
- import * as p5 from "@clack/prompts";
2589
+ import * as p6 from "@clack/prompts";
2516
2590
  import pc9 from "picocolors";
2517
2591
  async function runMigrate(cwd2, opts) {
2518
2592
  const config = await loadConfig(cwd2);
@@ -2520,7 +2594,7 @@ async function runMigrate(cwd2, opts) {
2520
2594
  if (opts.to && ["claude-code", "opencode", "codex-cli", "grok-cli"].includes(opts.to)) {
2521
2595
  target = opts.to;
2522
2596
  } else {
2523
- const val = await p5.select({
2597
+ const val = await p6.select({
2524
2598
  message: "Migrate to provider",
2525
2599
  options: [
2526
2600
  { value: "claude-code", label: "Claude Code" },
@@ -2529,8 +2603,8 @@ async function runMigrate(cwd2, opts) {
2529
2603
  { value: "grok-cli", label: "Grok CLI" }
2530
2604
  ]
2531
2605
  });
2532
- if (p5.isCancel(val)) {
2533
- p5.cancel("Cancelled.");
2606
+ if (p6.isCancel(val)) {
2607
+ p6.cancel("Cancelled.");
2534
2608
  process.exit(0);
2535
2609
  }
2536
2610
  target = val;
@@ -2539,17 +2613,19 @@ async function runMigrate(cwd2, opts) {
2539
2613
  console.log(pc9.dim(`Already on ${target} \u2014 nothing to migrate.`));
2540
2614
  return;
2541
2615
  }
2542
- const spinner6 = p5.spinner();
2616
+ const claudeAgentModels = await promptClaudeAgentModels(target);
2617
+ const codexAgentModels = await promptCodexAgentModels(target);
2618
+ const spinner6 = p6.spinner();
2543
2619
  spinner6.start(`Migrating from ${config.provider} to ${target}...`);
2544
2620
  try {
2545
2621
  const targetMaterializer = getMaterializer(target);
2546
- await targetMaterializer.build(config, cwd2);
2622
+ await targetMaterializer.build(config, cwd2, { claudeAgentModels, codexAgentModels });
2547
2623
  spinner6.stop(pc9.green(`Migrated to ${target}`));
2548
- p5.log.warn(`Update agent-harness-kit.config.ts: set provider: '${target}'`);
2549
- p5.log.warn(`Then run: ahk build`);
2624
+ p6.log.warn(`Update agent-harness-kit.config.ts: set provider: '${target}'`);
2625
+ p6.log.warn(`Then run: ahk build`);
2550
2626
  } catch (err) {
2551
2627
  spinner6.stop(pc9.red("Migration failed"));
2552
- p5.log.error(err instanceof Error ? err.message : String(err));
2628
+ p6.log.error(err instanceof Error ? err.message : String(err));
2553
2629
  process.exit(1);
2554
2630
  }
2555
2631
  }
@@ -2851,7 +2927,7 @@ async function runModels(cwd2) {
2851
2927
  import { existsSync as existsSync16, readdirSync, rmSync as rmSync2 } from "fs";
2852
2928
  import { homedir as homedir4 } from "os";
2853
2929
  import { join as join19, resolve as resolve8 } from "path";
2854
- import * as p6 from "@clack/prompts";
2930
+ import * as p7 from "@clack/prompts";
2855
2931
  import pc12 from "picocolors";
2856
2932
  var AGENT_MD_FILES = ["lead", "explorer", "consultant", "builder", "reviewer"];
2857
2933
  var PROVIDER_AGENT_DIRS = {
@@ -2891,11 +2967,11 @@ async function resetAgentMds(cwd2, provider) {
2891
2967
  return;
2892
2968
  }
2893
2969
  for (const file of existingFiles) {
2894
- const confirm3 = await p6.confirm({
2970
+ const confirm3 = await p7.confirm({
2895
2971
  message: `Remove ${file}?`,
2896
2972
  initialValue: true
2897
2973
  });
2898
- if (p6.isCancel(confirm3)) {
2974
+ if (p7.isCancel(confirm3)) {
2899
2975
  console.log(pc12.red(" Cancelled by user."));
2900
2976
  return;
2901
2977
  }
@@ -2934,11 +3010,11 @@ async function runReset(cwd2, opts) {
2934
3010
  console.log(pc12.yellow(` Skipping DB reset \u2014 database type "${config.database.type}" is not managed by this command.`));
2935
3011
  resetDb = false;
2936
3012
  } else {
2937
- const confirm3 = await p6.confirm({
3013
+ const confirm3 = await p7.confirm({
2938
3014
  message: `Delete database (${dbPath})?`,
2939
3015
  initialValue: true
2940
3016
  });
2941
- if (p6.isCancel(confirm3)) {
3017
+ if (p7.isCancel(confirm3)) {
2942
3018
  console.log(pc12.red(" Cancelled by user."));
2943
3019
  return;
2944
3020
  }
@@ -2952,11 +3028,11 @@ async function runReset(cwd2, opts) {
2952
3028
  if (opts.force) {
2953
3029
  resetFeatureList = true;
2954
3030
  } else {
2955
- const confirm3 = await p6.confirm({
3031
+ const confirm3 = await p7.confirm({
2956
3032
  message: `Delete feature list (${storageDir}/feature_list.json)?`,
2957
3033
  initialValue: true
2958
3034
  });
2959
- if (p6.isCancel(confirm3)) {
3035
+ if (p7.isCancel(confirm3)) {
2960
3036
  console.log(pc12.red(" Cancelled by user."));
2961
3037
  return;
2962
3038
  }
@@ -3791,15 +3867,15 @@ async function syncOut(db, cwd2, dryRun) {
3791
3867
  }
3792
3868
 
3793
3869
  // src/commands/task/add.ts
3794
- import * as p7 from "@clack/prompts";
3870
+ import * as p8 from "@clack/prompts";
3795
3871
  import pc15 from "picocolors";
3796
3872
  async function runTaskAdd(cwd2) {
3797
- p7.intro(pc15.bold("agent-harness-kit \u2014 add task"));
3873
+ p8.intro(pc15.bold("agent-harness-kit \u2014 add task"));
3798
3874
  const title = await cliFormWithRetry(
3799
3875
  async () => {
3800
- const val = await p7.text({ message: "Task title" });
3801
- if (p7.isCancel(val)) {
3802
- p7.cancel("Cancelled.");
3876
+ const val = await p8.text({ message: "Task title" });
3877
+ if (p8.isCancel(val)) {
3878
+ p8.cancel("Cancelled.");
3803
3879
  process.exit(0);
3804
3880
  }
3805
3881
  return val.trim();
@@ -3808,12 +3884,12 @@ async function runTaskAdd(cwd2) {
3808
3884
  );
3809
3885
  const description = await cliFormWithRetry(
3810
3886
  async () => {
3811
- const val = await p7.text({
3887
+ const val = await p8.text({
3812
3888
  message: "Description (what and why)",
3813
3889
  placeholder: "Describe the task in more detail, including any relevant context or instructions for the agents."
3814
3890
  });
3815
- if (p7.isCancel(val)) {
3816
- p7.cancel("Cancelled.");
3891
+ if (p8.isCancel(val)) {
3892
+ p8.cancel("Cancelled.");
3817
3893
  process.exit(0);
3818
3894
  }
3819
3895
  return val.trim();
@@ -3821,13 +3897,13 @@ async function runTaskAdd(cwd2) {
3821
3897
  taskDescriptionSchema
3822
3898
  );
3823
3899
  const acceptance = [];
3824
- p7.log.info("Acceptance criteria \u2014 one per line, empty line to finish");
3900
+ p8.log.info("Acceptance criteria \u2014 one per line, empty line to finish");
3825
3901
  while (true) {
3826
- const val = await p7.text({ message: ">", placeholder: "Criterion (or press Enter to finish)" });
3827
- if (p7.isCancel(val) || !val || !val.trim()) break;
3902
+ const val = await p8.text({ message: ">", placeholder: "Criterion (or press Enter to finish)" });
3903
+ if (p8.isCancel(val) || !val || !val.trim()) break;
3828
3904
  acceptance.push(val.trim());
3829
3905
  }
3830
- const spinner6 = p7.spinner();
3906
+ const spinner6 = p8.spinner();
3831
3907
  spinner6.start("Saving...");
3832
3908
  try {
3833
3909
  const config = await loadConfig(cwd2);
@@ -3841,7 +3917,7 @@ async function runTaskAdd(cwd2) {
3841
3917
  console.log(pc15.cyan("\u2192") + " " + pc15.cyan("ahk status") + " to see all tasks");
3842
3918
  } catch (err) {
3843
3919
  spinner6.stop(pc15.red("Failed"));
3844
- p7.log.error(err instanceof Error ? err.message : String(err));
3920
+ p8.log.error(err instanceof Error ? err.message : String(err));
3845
3921
  process.exit(1);
3846
3922
  }
3847
3923
  }
@@ -3887,63 +3963,63 @@ async function runTaskDone(cwd2, idOrSlug) {
3887
3963
  }
3888
3964
 
3889
3965
  // src/commands/task/edit.ts
3890
- import * as p8 from "@clack/prompts";
3966
+ import * as p9 from "@clack/prompts";
3891
3967
  import pc17 from "picocolors";
3892
3968
  async function runTaskEdit(cwd2) {
3893
- p8.intro(pc17.bold("agent-harness-kit \u2014 edit task"));
3969
+ p9.intro(pc17.bold("agent-harness-kit \u2014 edit task"));
3894
3970
  const config = await loadConfig(cwd2);
3895
3971
  const db = await openDB(config, cwd2);
3896
3972
  try {
3897
3973
  const allTasks = await db.getTasks();
3898
3974
  const activeTasks = allTasks.filter((t) => t.status !== "done");
3899
3975
  if (activeTasks.length === 0) {
3900
- p8.log.error("No active tasks to edit.");
3976
+ p9.log.error("No active tasks to edit.");
3901
3977
  return;
3902
3978
  }
3903
- const taskId = await p8.select({
3979
+ const taskId = await p9.select({
3904
3980
  message: "Select a task to edit",
3905
3981
  options: activeTasks.map((t) => ({
3906
3982
  label: `#${t.id} \u2014 ${t.title} (${t.slug})`,
3907
3983
  value: t.id
3908
3984
  }))
3909
3985
  });
3910
- if (p8.isCancel(taskId)) {
3911
- p8.cancel("Cancelled.");
3986
+ if (p9.isCancel(taskId)) {
3987
+ p9.cancel("Cancelled.");
3912
3988
  process.exit(0);
3913
3989
  }
3914
3990
  const task2 = await db.getTaskById(taskId);
3915
3991
  if (!task2) {
3916
- p8.log.error("Task not found");
3992
+ p9.log.error("Task not found");
3917
3993
  process.exit(1);
3918
3994
  }
3919
- const title = await p8.text({
3995
+ const title = await p9.text({
3920
3996
  message: "Title",
3921
3997
  initialValue: task2.title
3922
3998
  });
3923
- if (p8.isCancel(title)) {
3924
- p8.cancel("Cancelled.");
3999
+ if (p9.isCancel(title)) {
4000
+ p9.cancel("Cancelled.");
3925
4001
  process.exit(0);
3926
4002
  }
3927
- const description = await p8.text({
4003
+ const description = await p9.text({
3928
4004
  message: "Description (what and why)",
3929
4005
  initialValue: task2.description ?? ""
3930
4006
  });
3931
- if (p8.isCancel(description)) {
3932
- p8.cancel("Cancelled.");
4007
+ if (p9.isCancel(description)) {
4008
+ p9.cancel("Cancelled.");
3933
4009
  process.exit(0);
3934
4010
  }
3935
4011
  const currentAcceptance = await db.getTaskAcceptance(task2.id);
3936
4012
  const newAcceptance = [];
3937
- p8.log.info("Acceptance criteria \u2014 edit each, empty to delete. Add new ones at the end.");
4013
+ p9.log.info("Acceptance criteria \u2014 edit each, empty to delete. Add new ones at the end.");
3938
4014
  for (let i = 0; i < currentAcceptance.length; i++) {
3939
4015
  const ac = currentAcceptance[i];
3940
- const val = await p8.text({
4016
+ const val = await p9.text({
3941
4017
  message: `#${i + 1}/${currentAcceptance.length}`,
3942
4018
  initialValue: ac.criterion,
3943
4019
  defaultValue: ""
3944
4020
  });
3945
- if (p8.isCancel(val)) {
3946
- p8.cancel("Cancelled.");
4021
+ if (p9.isCancel(val)) {
4022
+ p9.cancel("Cancelled.");
3947
4023
  process.exit(0);
3948
4024
  }
3949
4025
  const trimmed = val.trim();
@@ -3952,11 +4028,11 @@ async function runTaskEdit(cwd2) {
3952
4028
  }
3953
4029
  }
3954
4030
  while (true) {
3955
- const val = await p8.text({ message: "New acceptance criterion", placeholder: "(press Enter to finish)" });
3956
- if (p8.isCancel(val) || !val || !val.trim()) break;
4031
+ const val = await p9.text({ message: "New acceptance criterion", placeholder: "(press Enter to finish)" });
4032
+ if (p9.isCancel(val) || !val || !val.trim()) break;
3957
4033
  newAcceptance.push(val.trim());
3958
4034
  }
3959
- const spinner6 = p8.spinner();
4035
+ const spinner6 = p9.spinner();
3960
4036
  spinner6.start("Saving...");
3961
4037
  try {
3962
4038
  const newSlug = slugify(title);
@@ -3971,7 +4047,7 @@ async function runTaskEdit(cwd2) {
3971
4047
  console.log(pc17.green(`\u2713 Task #${task2.id} updated \u2014 ${newSlug}`));
3972
4048
  } catch (err) {
3973
4049
  spinner6.stop(pc17.red("Failed"));
3974
- p8.log.error(err instanceof Error ? err.message : String(err));
4050
+ p9.log.error(err instanceof Error ? err.message : String(err));
3975
4051
  process.exit(1);
3976
4052
  }
3977
4053
  } finally {