@cardor/agent-harness-kit 1.10.5 → 1.11.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
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  findConfigFile,
3
3
  loadConfig
4
- } from "./chunk-D64KK6UU.js";
4
+ } from "./chunk-U3O77CGE.js";
5
5
  import {
6
6
  DEFAULT_MARKDOWN_PATH,
7
7
  DEFAULT_SQLITE_PATH,
@@ -24,7 +24,7 @@ import * as p from "@clack/prompts";
24
24
  import pc from "picocolors";
25
25
 
26
26
  // src/core/materializer/claude-code.ts
27
- import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
27
+ import { existsSync as existsSync4, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
28
28
  import { join as join5, resolve as resolve3 } from "path";
29
29
 
30
30
  // src/utils/file.ts
@@ -335,6 +335,37 @@ import { fileURLToPath as fileURLToPath2 } from "url";
335
335
  import { readFileSync as readFileSync3 } from "fs";
336
336
  import { dirname as dirname2, join as join3 } from "path";
337
337
  import { fileURLToPath } from "url";
338
+
339
+ // src/core/materializer/agent-restrictions.ts
340
+ var AGENT_RESTRICTIONS = {
341
+ lead: "no-write",
342
+ explorer: "no-write",
343
+ consultant: "no-write",
344
+ builder: "none",
345
+ reviewer: "no-write"
346
+ };
347
+ function restrictionFor(agentName) {
348
+ return AGENT_RESTRICTIONS[agentName] ?? "no-write";
349
+ }
350
+ function claudeDisallowedTools(agentName) {
351
+ return restrictionFor(agentName) === "no-write" ? ["Write", "Edit"] : [];
352
+ }
353
+ function opencodePermissions(agentName) {
354
+ return restrictionFor(agentName) === "no-write" ? { edit: "deny" } : {};
355
+ }
356
+ function codexSandboxMode(agentName) {
357
+ return restrictionFor(agentName) === "no-write" ? "read-only" : "workspace-write";
358
+ }
359
+ var CODEX_READ_ONLY_NOTICE = `## Tool restrictions (enforced by the sandbox)
360
+
361
+ 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\`, ...).
362
+
363
+ 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.`;
364
+ function codexRestrictionNotice(agentName) {
365
+ return restrictionFor(agentName) === "no-write" ? CODEX_READ_ONLY_NOTICE : "";
366
+ }
367
+
368
+ // src/core/materializer/templates.ts
338
369
  var __dirname = dirname2(fileURLToPath(import.meta.url));
339
370
  var TEMPLATES_DIR = join3(__dirname, "agent-templates");
340
371
  function loadAgentTemplate(name, vars = {}) {
@@ -530,11 +561,7 @@ If orchestrating: Agent definition files in .claude/agents/
530
561
  \`\`\`
531
562
  `;
532
563
  }
533
- function modelField(model) {
534
- return model ? `, model: ${JSON.stringify(model)}` : "";
535
- }
536
564
  function configObjectBody(params) {
537
- const models = params.models ?? {};
538
565
  const isGlobal = params.scope === "global";
539
566
  const markdownFallbackLine = isGlobal ? `markdownFallback: { enabled: true },` : `markdownFallback: { enabled: true, path: '.harness/current.md' },`;
540
567
  return ` project: {
@@ -545,14 +572,13 @@ function configObjectBody(params) {
545
572
 
546
573
  provider: '${params.provider}',
547
574
 
548
- agents: {
549
- lead: { instructionsPath: null${modelField(models.lead)} },
550
- explorer: { instructionsPath: null, allowedPaths: ['${params.docsPath}', './src']${modelField(models.explorer)} },
551
- builder: { instructionsPath: null, writablePaths: ['./src', './tests']${modelField(models.builder)} },
552
- reviewer: { instructionsPath: null${modelField(models.reviewer)} },
553
- ${models.consultant ? `consultant: { instructionsPath: null${modelField(models.consultant)} },
554
- ` : ""}custom: [],
555
- },
575
+ // There is no 'agents' key. Agent files are yours: edit the role prompt and
576
+ // the 'model:' frontmatter line directly in the generated file. 'ahk build'
577
+ // creates them when missing and never overwrites them \u2014 use
578
+ // 'ahk build --force' to regenerate them from the packaged templates.
579
+ // What each role may NOT do is enforced per-tool inside those files
580
+ // (disallowedTools / permission.edit / sandbox_mode); see
581
+ // src/core/materializer/agent-restrictions.ts.
556
582
 
557
583
  // SQLite (default). Switch to postgres/mysql by changing database.type.
558
584
  // database: { type: 'postgres', connectionString: process.env.DATABASE_URL },
@@ -587,6 +613,47 @@ function configObjectBody(params) {
587
613
  },
588
614
  `;
589
615
  }
616
+ function configObject(params) {
617
+ const isGlobal = params.scope === "global";
618
+ return {
619
+ project: {
620
+ name: params.name,
621
+ description: params.description,
622
+ docsPath: params.docsPath
623
+ },
624
+ provider: params.provider,
625
+ // There is no 'agents' key here either — it was removed from the config
626
+ // entirely, so the JSON variant must not reintroduce it.
627
+ database: { type: "sqlite" },
628
+ storage: {
629
+ dir: ".harness",
630
+ tasks: { adapter: params.tasksAdapter },
631
+ sections: {
632
+ toolsUsed: true,
633
+ filesModified: true,
634
+ result: true,
635
+ blockers: true,
636
+ nextSteps: false
637
+ },
638
+ // Same scope rule as configObjectBody(): 'global' has no local path to
639
+ // declare, so markdownFallback.path is omitted for it.
640
+ markdownFallback: isGlobal ? { enabled: true } : { enabled: true, path: ".harness/current.md" },
641
+ scope: params.scope,
642
+ projectId: params.projectId
643
+ },
644
+ health: {
645
+ scriptPath: "./health.sh",
646
+ required: true
647
+ },
648
+ tools: {
649
+ mcp: { enabled: true, port: params.port },
650
+ scripts: { enabled: true, outputDir: "./.harness/scripts" }
651
+ }
652
+ };
653
+ }
654
+ function configJson(params) {
655
+ return JSON.stringify(configObject(params), null, 2) + "\n";
656
+ }
590
657
  function configTs(params) {
591
658
  return `import type { HarnessConfig } from '@cardor/agent-harness-kit'
592
659
 
@@ -643,81 +710,83 @@ function stripFrontmatter(md) {
643
710
  }
644
711
  return { description, body };
645
712
  }
646
- function toCodexToml(name, description, body, sandboxMode, model) {
713
+ function toCodexToml(tomlName, agentName, description, body) {
647
714
  const safe = (s) => s.replace(/"""/g, '""\\u0022');
648
- const trimmedModel = model?.trim() ?? "";
649
- const modelLine = trimmedModel.length >= 3 ? `model = "${trimmedModel}"
650
- ` : "";
651
- return `name = "${name}"
715
+ const sandboxMode = codexSandboxMode(agentName);
716
+ const notice = codexRestrictionNotice(agentName);
717
+ const instructions = notice ? `${body.trimEnd()}
718
+
719
+ ---
720
+
721
+ ${notice}` : body.trimEnd();
722
+ return `name = "${tomlName}"
652
723
  sandbox_mode = "${sandboxMode}"
653
- ${modelLine}
724
+
654
725
  description = """
655
726
  ${safe(description)}
656
727
  """
657
728
 
658
729
  developer_instructions = """
659
- ${safe(body.trimEnd())}
730
+ ${safe(instructions)}
660
731
  """
661
732
  `;
662
733
  }
663
734
  function agentLeadToml(vars) {
664
735
  const { description, body } = stripFrontmatter(loadAgentTemplate("lead", vars));
665
- return toCodexToml("lead", description, body, "read-only", vars.model);
736
+ return toCodexToml("lead", "lead", description, body);
666
737
  }
667
738
  function agentLeadAsDefaultToml(vars) {
668
739
  const { description, body } = stripFrontmatter(loadAgentTemplate("lead", vars));
669
- return toCodexToml("default", description, body, "read-only", vars.model);
740
+ return toCodexToml("default", "lead", description, body);
670
741
  }
671
742
  function agentExplorerToml(vars) {
672
743
  const { description, body } = stripFrontmatter(loadAgentTemplate("explorer", vars));
673
- return toCodexToml("explorer", description, body, "read-only", vars.model);
744
+ return toCodexToml("explorer", "explorer", description, body);
674
745
  }
675
746
  function agentBuilderToml(vars) {
676
747
  const { description, body } = stripFrontmatter(loadAgentTemplate("builder", vars));
677
- return toCodexToml("builder", description, body, "workspace-write", vars.model);
748
+ return toCodexToml("builder", "builder", description, body);
678
749
  }
679
750
  function agentReviewerToml(vars) {
680
751
  const { description, body } = stripFrontmatter(loadAgentTemplate("reviewer", vars));
681
- return toCodexToml("reviewer", description, body, "read-only", vars.model);
752
+ return toCodexToml("reviewer", "reviewer", description, body);
682
753
  }
683
754
  function agentConsultantToml(vars) {
684
755
  const { description, body } = stripFrontmatter(loadAgentTemplate("consultant", vars));
685
- return toCodexToml("consultant", description, body, "read-only", vars.model);
686
- }
687
- function translateFrontmatterForClaudeCode(md, agentName, model) {
688
- const permissionsMap = {
689
- lead: [...MCP_CLAUDE_PERMISSIONS_LEAD],
690
- explorer: [...MCP_CLAUDE_PERMISSIONS_EXPLORER],
691
- consultant: [...MCP_CLAUDE_PERMISSIONS_CONSULTANT],
692
- builder: [...MCP_CLAUDE_PERMISSIONS_BUILDER],
693
- reviewer: [...MCP_CLAUDE_PERMISSIONS_REVIEWER]
694
- };
695
- const permissions = permissionsMap[agentName] ?? MCP_CLAUDE_PERMISSIONS;
696
- const mcpLines = permissions.map((t) => ` - ${t}`).join("\n");
697
- let result = md.replace(/(tools:\n(?: - (?!mcp__)[^\n]+\n)+)/, (match) => {
698
- const trimmed = match.trimEnd();
699
- return `${trimmed}
700
- - Task
701
- ${mcpLines}
756
+ return toCodexToml("consultant", "consultant", description, body);
757
+ }
758
+ function stripFrontmatterBlockSequence(md, key) {
759
+ const re = new RegExp(`^${key}:\\n(?: - [^\\n]+\\n)+`, "m");
760
+ return md.replace(re, "");
761
+ }
762
+ function appendFrontmatterBlockSequence(md, key, values) {
763
+ if (values.length === 0) return md;
764
+ const block = `${key}:
765
+ ${values.map((v4) => ` - ${v4}`).join("\n")}
702
766
  `;
703
- });
704
- if (model) {
705
- result = injectModelFrontmatterLine(result, model);
706
- }
707
- return result;
767
+ return md.replace(/^---\n([\s\S]*?)^---\n/m, (_m, body) => `---
768
+ ${body}${block}---
769
+ `);
708
770
  }
709
- function injectModelFrontmatterLine(md, model) {
710
- if (/^model:\s*.*$/m.test(md)) {
711
- return md.replace(/^model:\s*.*$/m, `model: ${model}`);
712
- }
713
- return md.replace(/^(name:.*)$/m, `$1
714
- model: ${model}`);
771
+ function appendFrontmatterMapping(md, key, entries) {
772
+ const keys = Object.keys(entries);
773
+ if (keys.length === 0) return md;
774
+ const block = `${key}:
775
+ ${keys.map((k) => ` ${k}: ${entries[k]}`).join("\n")}
776
+ `;
777
+ return md.replace(/^---\n([\s\S]*?)^---\n/m, (_m, body) => `---
778
+ ${body}${block}---
779
+ `);
715
780
  }
716
- function translateFrontmatterForOpenCode(md) {
717
- return md.replace(/(tools:\n(?: - [^\n]+\n)+)/, (match) => {
718
- const tools = [...match.matchAll(/ - ([^\n]+)/g)].map((m) => m[1].trim());
719
- return "tools:\n" + tools.map((t) => ` ${t.toLocaleLowerCase()}: true`).join("\n") + "\n";
720
- });
781
+ function translateFrontmatterForClaudeCode(md, agentName) {
782
+ let result = stripFrontmatterBlockSequence(md, "tools");
783
+ result = stripFrontmatterBlockSequence(result, "disallowedTools");
784
+ return appendFrontmatterBlockSequence(result, "disallowedTools", claudeDisallowedTools(agentName));
785
+ }
786
+ function translateFrontmatterForOpenCode(md, agentName) {
787
+ let result = stripFrontmatterBlockSequence(md, "tools");
788
+ result = stripFrontmatterBlockSequence(result, "disallowedTools");
789
+ return appendFrontmatterMapping(result, "permission", opencodePermissions(agentName));
721
790
  }
722
791
  var GITIGNORE_ENTRIES = `
723
792
  # agent-harness-kit
@@ -729,11 +798,43 @@ var GITIGNORE_ENTRIES = `
729
798
 
730
799
  // src/core/materializer/scaffold-utils.ts
731
800
  var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
732
- function writeAgentFile(cwd2, relPath, content) {
733
- const abs = join4(cwd2, relPath);
734
- if (existsSync3(abs)) return;
735
- mkdirSync3(resolve2(abs, ".."), { recursive: true });
736
- writeFileSync3(abs, content, "utf8");
801
+ function writeAgentFiles(cwd2, entries, opts = {}) {
802
+ const result = { created: [], overwritten: [], preserved: [] };
803
+ const existing = entries.filter((e) => existsSync3(join4(cwd2, e.relPath)));
804
+ if (opts.force && existing.length > 0) {
805
+ if (!opts.backupRoot) {
806
+ throw new Error(
807
+ "writeAgentFiles: force is set and existing agent files would be overwritten, but no backupRoot was provided. Refusing to overwrite without a backup."
808
+ );
809
+ }
810
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
811
+ const backupDir = join4(opts.backupRoot, `agents-${stamp}`);
812
+ try {
813
+ for (const entry of existing) {
814
+ const dest = join4(backupDir, entry.relPath);
815
+ mkdirSync3(resolve2(dest, ".."), { recursive: true });
816
+ writeFileSync3(dest, readFileSync4(join4(cwd2, entry.relPath), "utf8"), "utf8");
817
+ }
818
+ } catch (err) {
819
+ throw new Error(
820
+ `Could not back up existing agent files to ${backupDir} (${err instanceof Error ? err.message : String(err)}). Aborting WITHOUT overwriting anything \u2014 no agent file was modified.`
821
+ );
822
+ }
823
+ result.backupDir = backupDir;
824
+ }
825
+ for (const entry of entries) {
826
+ const abs = join4(cwd2, entry.relPath);
827
+ const exists = existsSync3(abs);
828
+ if (exists && !opts.force) {
829
+ result.preserved.push(entry.relPath);
830
+ continue;
831
+ }
832
+ mkdirSync3(resolve2(abs, ".."), { recursive: true });
833
+ writeFileSync3(abs, entry.content, "utf8");
834
+ if (exists) result.overwritten.push(entry.relPath);
835
+ else result.created.push(entry.relPath);
836
+ }
837
+ return result;
737
838
  }
738
839
  function appendGitignore(cwd2) {
739
840
  const giPath = join4(cwd2, ".gitignore");
@@ -758,6 +859,16 @@ function writeSkills(cwd2, skillsDir) {
758
859
  }
759
860
 
760
861
  // src/core/materializer/claude-code.ts
862
+ function claudeAgentFiles(config) {
863
+ const projectName = config.project.name;
864
+ return [
865
+ { relPath: ".claude/agents/lead.md", content: translateFrontmatterForClaudeCode(agentLead({ projectName }), "lead") },
866
+ { relPath: ".claude/agents/explorer.md", content: translateFrontmatterForClaudeCode(agentExplorer({ projectName }), "explorer") },
867
+ { relPath: ".claude/agents/consultant.md", content: translateFrontmatterForClaudeCode(agentConsultant({ projectName }), "consultant") },
868
+ { relPath: ".claude/agents/builder.md", content: translateFrontmatterForClaudeCode(agentBuilder({ projectName }), "builder") },
869
+ { relPath: ".claude/agents/reviewer.md", content: translateFrontmatterForClaudeCode(agentReviewer({ projectName }), "reviewer") }
870
+ ];
871
+ }
761
872
  var ClaudeCodeMaterializer = class {
762
873
  async scaffold(config, opts) {
763
874
  const { cwd: cwd2 } = opts;
@@ -781,26 +892,14 @@ No tasks in progress.
781
892
  `
782
893
  );
783
894
  }
784
- const projectName = config.project.name;
785
- const allowedPaths = (config.agents.explorer.allowedPaths ?? []).join(", ");
786
- const writablePaths = (config.agents.builder.writablePaths ?? []).join(", ");
787
- const leadModel = config.agents.lead.model;
788
- const explorerModel = config.agents.explorer.model;
789
- const consultantModel = config.agents.consultant?.model;
790
- const builderModel = config.agents.builder.model;
791
- const reviewerModel = config.agents.reviewer.model;
792
- writeAgentFile(cwd2, ".claude/agents/lead.md", translateFrontmatterForClaudeCode(agentLead({ projectName }), "lead", leadModel));
793
- writeAgentFile(cwd2, ".claude/agents/explorer.md", translateFrontmatterForClaudeCode(agentExplorer({ projectName, allowedPaths }), "explorer", explorerModel));
794
- writeAgentFile(cwd2, ".claude/agents/consultant.md", translateFrontmatterForClaudeCode(agentConsultant({ projectName }), "consultant", consultantModel));
795
- writeAgentFile(cwd2, ".claude/agents/builder.md", translateFrontmatterForClaudeCode(agentBuilder({ projectName, writablePaths }), "builder", builderModel));
796
- writeAgentFile(cwd2, ".claude/agents/reviewer.md", translateFrontmatterForClaudeCode(agentReviewer({ projectName }), "reviewer", reviewerModel));
895
+ writeAgentFiles(cwd2, claudeAgentFiles(config));
797
896
  mergeClaudeMcpJson(join5(cwd2, ".mcp.json"), config.tools.mcp.port, detectPackageManager(cwd2));
798
897
  mergeClaudeSettingsJson(join5(cwd2, ".claude/settings.json"));
799
898
  mergeClaudeSettingsLocalJson(join5(cwd2, ".claude/settings.local.json"));
800
899
  appendGitignore(cwd2);
801
900
  writeSkills(cwd2, ".claude/skills");
802
901
  }
803
- async build(config, cwd2) {
902
+ async build(config, cwd2, opts = {}) {
804
903
  const write2 = (relPath, content) => {
805
904
  const abs = join5(cwd2, relPath);
806
905
  mkdirSync4(resolve3(abs, ".."), { recursive: true });
@@ -808,64 +907,54 @@ No tasks in progress.
808
907
  };
809
908
  write2("AGENTS.md", agentsMd(config));
810
909
  write2("CLAUDE.md", claudeMd(config));
811
- const projectName = config.project.name;
812
- const allowedPaths = (config.agents.explorer.allowedPaths ?? []).join(", ");
813
- const writablePaths = (config.agents.builder.writablePaths ?? []).join(", ");
814
- const leadModel = config.agents.lead.model;
815
- const explorerModel = config.agents.explorer.model;
816
- const consultantModel = config.agents.consultant?.model;
817
- const builderModel = config.agents.builder.model;
818
- const reviewerModel = config.agents.reviewer.model;
819
- write2(".claude/agents/lead.md", translateFrontmatterForClaudeCode(agentLead({ projectName }), "lead", leadModel));
820
- write2(".claude/agents/explorer.md", translateFrontmatterForClaudeCode(agentExplorer({ projectName, allowedPaths }), "explorer", explorerModel));
821
- write2(".claude/agents/consultant.md", translateFrontmatterForClaudeCode(agentConsultant({ projectName }), "consultant", consultantModel));
822
- write2(".claude/agents/builder.md", translateFrontmatterForClaudeCode(agentBuilder({ projectName, writablePaths }), "builder", builderModel));
823
- write2(".claude/agents/reviewer.md", translateFrontmatterForClaudeCode(agentReviewer({ projectName }), "reviewer", reviewerModel));
910
+ const agents = writeAgentFiles(cwd2, claudeAgentFiles(config), {
911
+ force: opts.force,
912
+ backupRoot: join5(cwd2, config.storage.dir, "backups")
913
+ });
824
914
  mergeClaudeMcpJson(join5(cwd2, ".mcp.json"), config.tools.mcp.port, detectPackageManager(cwd2));
825
915
  mergeClaudeSettingsJson(join5(cwd2, ".claude/settings.json"));
826
916
  mergeClaudeSettingsLocalJson(join5(cwd2, ".claude/settings.local.json"));
827
917
  writeSkills(cwd2, ".claude/skills");
918
+ return { agents };
828
919
  }
829
920
  async migrate(config, _to, _cwd) {
830
921
  void config;
831
922
  }
832
- async syncPermissions(cwd2) {
833
- const AGENT_TOOLS = {
834
- lead: [...MCP_CLAUDE_PERMISSIONS_LEAD],
835
- explorer: [...MCP_CLAUDE_PERMISSIONS_EXPLORER],
836
- consultant: [...MCP_CLAUDE_PERMISSIONS_CONSULTANT],
837
- builder: [...MCP_CLAUDE_PERMISSIONS_BUILDER],
838
- reviewer: [...MCP_CLAUDE_PERMISSIONS_REVIEWER]
839
- };
840
- for (const [agent, tools] of Object.entries(AGENT_TOOLS)) {
841
- const filePath = join5(cwd2, ".claude", "agents", `${agent}.md`);
842
- if (!existsSync4(filePath)) {
843
- console.log(` ${agent}.md not found \u2014 skipping`);
844
- continue;
845
- }
846
- const content = readFileSync5(filePath, "utf-8");
847
- const updated = content.replace(
848
- /(tools:\n)((?: - [^\n]+\n)*)/m,
849
- (_match, header, toolsSection) => {
850
- const nativeLines = toolsSection.split("\n").filter((line) => line.trim() && !line.includes("mcp__"));
851
- const nativeSection = nativeLines.length ? nativeLines.join("\n") + "\n" : "";
852
- const mcpSection = tools.map((t) => ` - ${t}`).join("\n") + "\n";
853
- return header + nativeSection + mcpSection;
854
- }
855
- );
856
- if (updated === content) {
857
- console.log(` ${agent}.md already in sync`);
858
- } else {
859
- writeFileSync4(filePath, updated, "utf-8");
860
- console.log(` ${agent}.md updated`);
861
- }
862
- }
923
+ /**
924
+ * No-op by design.
925
+ *
926
+ * This used to rewrite the `tools:` frontmatter block of every agent file,
927
+ * re-injecting the canonical `mcp__agent-harness-kit__*` allowlist. Agent
928
+ * files no longer declare `tools` at all: they inherit the full tool set
929
+ * (Task and every MCP tool included) and express restrictions as a
930
+ * `disallowedTools` denylist instead.
931
+ *
932
+ * Keeping the old rewrite alive would be actively harmful — on a project
933
+ * upgraded from an older version, whose agent files still carry a legacy
934
+ * `tools:` block, it would re-inject the allowlist and silently undo the
935
+ * migration on exactly the files that most need it. Run `ahk build` to
936
+ * regenerate agent files in the current shape.
937
+ */
938
+ async syncPermissions(_cwd) {
939
+ console.log(" Agent files inherit tools and declare a disallowedTools denylist \u2014");
940
+ console.log(" there is no permission allowlist to sync. Run `ahk build` to regenerate them.");
863
941
  }
864
942
  };
865
943
 
866
944
  // src/core/materializer/codex-cli.ts
867
945
  import { existsSync as existsSync5, mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "fs";
868
946
  import { join as join6, resolve as resolve4 } from "path";
947
+ function codexAgentFiles(config) {
948
+ const projectName = config.project.name;
949
+ return [
950
+ { relPath: ".codex/agents/lead.toml", content: agentLeadToml({ projectName }) },
951
+ { relPath: ".codex/agents/explorer.toml", content: agentExplorerToml({ projectName }) },
952
+ { relPath: ".codex/agents/consultant.toml", content: agentConsultantToml({ projectName }) },
953
+ { relPath: ".codex/agents/builder.toml", content: agentBuilderToml({ projectName }) },
954
+ { relPath: ".codex/agents/reviewer.toml", content: agentReviewerToml({ projectName }) },
955
+ { relPath: ".codex/agents/default.toml", content: agentLeadAsDefaultToml({ projectName }) }
956
+ ];
957
+ }
869
958
  var CodexCliMaterializer = class {
870
959
  async scaffold(config, opts) {
871
960
  const { cwd: cwd2 } = opts;
@@ -892,47 +981,25 @@ No tasks in progress.
892
981
  `
893
982
  );
894
983
  }
895
- const projectName = config.project.name;
896
- const allowedPaths = (config.agents.explorer.allowedPaths ?? []).join(", ");
897
- const writablePaths = (config.agents.builder.writablePaths ?? []).join(", ");
898
- const leadModel = config.agents.lead.model;
899
- const explorerModel = config.agents.explorer.model;
900
- const consultantModel = config.agents.consultant?.model;
901
- const builderModel = config.agents.builder.model;
902
- const reviewerModel = config.agents.reviewer.model;
903
- writeAgentFile(cwd2, ".codex/agents/lead.toml", agentLeadToml({ projectName, model: leadModel }));
904
- writeAgentFile(cwd2, ".codex/agents/explorer.toml", agentExplorerToml({ projectName, allowedPaths, model: explorerModel }));
905
- writeAgentFile(cwd2, ".codex/agents/consultant.toml", agentConsultantToml({ projectName, model: consultantModel }));
906
- writeAgentFile(cwd2, ".codex/agents/builder.toml", agentBuilderToml({ projectName, writablePaths, model: builderModel }));
907
- writeAgentFile(cwd2, ".codex/agents/reviewer.toml", agentReviewerToml({ projectName, model: reviewerModel }));
908
- writeAgentFile(cwd2, ".codex/agents/default.toml", agentLeadAsDefaultToml({ projectName, model: leadModel }));
984
+ writeAgentFiles(cwd2, codexAgentFiles(config));
909
985
  mergeCodexConfigToml(join6(cwd2, ".codex/config.toml"), config.tools.mcp.port, detectPackageManager(cwd2));
910
986
  appendGitignore(cwd2);
911
987
  writeSkills(cwd2, ".agents/skills");
912
988
  }
913
- async build(config, cwd2) {
989
+ async build(config, cwd2, opts = {}) {
914
990
  const write2 = (relPath, content) => {
915
991
  const abs = join6(cwd2, relPath);
916
992
  mkdirSync5(resolve4(abs, ".."), { recursive: true });
917
993
  writeFileSync5(abs, content, "utf8");
918
994
  };
919
995
  write2("AGENTS.md", agentsMd(config));
920
- const projectName = config.project.name;
921
- const allowedPaths = (config.agents.explorer.allowedPaths ?? []).join(", ");
922
- const writablePaths = (config.agents.builder.writablePaths ?? []).join(", ");
923
- const leadModel = config.agents.lead.model;
924
- const explorerModel = config.agents.explorer.model;
925
- const consultantModel = config.agents.consultant?.model;
926
- const builderModel = config.agents.builder.model;
927
- const reviewerModel = config.agents.reviewer.model;
928
- writeAgentFile(cwd2, ".codex/agents/lead.toml", agentLeadToml({ projectName, model: leadModel }));
929
- writeAgentFile(cwd2, ".codex/agents/explorer.toml", agentExplorerToml({ projectName, allowedPaths, model: explorerModel }));
930
- writeAgentFile(cwd2, ".codex/agents/consultant.toml", agentConsultantToml({ projectName, model: consultantModel }));
931
- writeAgentFile(cwd2, ".codex/agents/builder.toml", agentBuilderToml({ projectName, writablePaths, model: builderModel }));
932
- writeAgentFile(cwd2, ".codex/agents/reviewer.toml", agentReviewerToml({ projectName, model: reviewerModel }));
933
- writeAgentFile(cwd2, ".codex/agents/default.toml", agentLeadAsDefaultToml({ projectName, model: leadModel }));
996
+ const agents = writeAgentFiles(cwd2, codexAgentFiles(config), {
997
+ force: opts.force,
998
+ backupRoot: join6(cwd2, config.storage.dir, "backups")
999
+ });
934
1000
  mergeCodexConfigToml(join6(cwd2, ".codex/config.toml"), config.tools.mcp.port, detectPackageManager(cwd2));
935
1001
  writeSkills(cwd2, ".agents/skills");
1002
+ return { agents };
936
1003
  }
937
1004
  async migrate(config, _to, _cwd) {
938
1005
  void config;
@@ -945,6 +1012,16 @@ No tasks in progress.
945
1012
  // src/core/materializer/opencode.ts
946
1013
  import { existsSync as existsSync6, mkdirSync as mkdirSync6, writeFileSync as writeFileSync6 } from "fs";
947
1014
  import { join as join7, resolve as resolve5 } from "path";
1015
+ function opencodeAgentFiles(config) {
1016
+ const projectName = config.project.name;
1017
+ return [
1018
+ { relPath: ".opencode/agents/lead.md", content: translateFrontmatterForOpenCode(agentLead({ projectName }), "lead") },
1019
+ { relPath: ".opencode/agents/explorer.md", content: translateFrontmatterForOpenCode(agentExplorer({ projectName }), "explorer") },
1020
+ { relPath: ".opencode/agents/consultant.md", content: translateFrontmatterForOpenCode(agentConsultant({ projectName }), "consultant") },
1021
+ { relPath: ".opencode/agents/builder.md", content: translateFrontmatterForOpenCode(agentBuilder({ projectName }), "builder") },
1022
+ { relPath: ".opencode/agents/reviewer.md", content: translateFrontmatterForOpenCode(agentReviewer({ projectName }), "reviewer") }
1023
+ ];
1024
+ }
948
1025
  var OpenCodeMaterializer = class {
949
1026
  async scaffold(config, opts) {
950
1027
  const { cwd: cwd2 } = opts;
@@ -971,35 +1048,25 @@ No tasks in progress.
971
1048
  `
972
1049
  );
973
1050
  }
974
- const projectName = config.project.name;
975
- const allowedPaths = (config.agents.explorer.allowedPaths ?? []).join(", ");
976
- const writablePaths = (config.agents.builder.writablePaths ?? []).join(", ");
977
- writeAgentFile(cwd2, ".opencode/agents/lead.md", translateFrontmatterForOpenCode(agentLead({ projectName })));
978
- writeAgentFile(cwd2, ".opencode/agents/explorer.md", translateFrontmatterForOpenCode(agentExplorer({ projectName, allowedPaths })));
979
- writeAgentFile(cwd2, ".opencode/agents/consultant.md", translateFrontmatterForOpenCode(agentConsultant({ projectName })));
980
- writeAgentFile(cwd2, ".opencode/agents/builder.md", translateFrontmatterForOpenCode(agentBuilder({ projectName, writablePaths })));
981
- writeAgentFile(cwd2, ".opencode/agents/reviewer.md", translateFrontmatterForOpenCode(agentReviewer({ projectName })));
1051
+ writeAgentFiles(cwd2, opencodeAgentFiles(config));
982
1052
  mergeOpencodeJson(join7(cwd2, "opencode.json"), config.tools.mcp.port, detectPackageManager(cwd2));
983
1053
  appendGitignore(cwd2);
984
1054
  writeSkills(cwd2, ".opencode/skills");
985
1055
  }
986
- async build(config, cwd2) {
1056
+ async build(config, cwd2, opts = {}) {
987
1057
  const write2 = (relPath, content) => {
988
1058
  const abs = join7(cwd2, relPath);
989
1059
  mkdirSync6(resolve5(abs, ".."), { recursive: true });
990
1060
  writeFileSync6(abs, content, "utf8");
991
1061
  };
992
1062
  write2("AGENTS.md", agentsMd(config));
993
- const projectName = config.project.name;
994
- const allowedPaths = (config.agents.explorer.allowedPaths ?? []).join(", ");
995
- const writablePaths = (config.agents.builder.writablePaths ?? []).join(", ");
996
- writeAgentFile(cwd2, ".opencode/agents/lead.md", translateFrontmatterForOpenCode(agentLead({ projectName })));
997
- writeAgentFile(cwd2, ".opencode/agents/explorer.md", translateFrontmatterForOpenCode(agentExplorer({ projectName, allowedPaths })));
998
- writeAgentFile(cwd2, ".opencode/agents/consultant.md", translateFrontmatterForOpenCode(agentConsultant({ projectName })));
999
- writeAgentFile(cwd2, ".opencode/agents/builder.md", translateFrontmatterForOpenCode(agentBuilder({ projectName, writablePaths })));
1000
- writeAgentFile(cwd2, ".opencode/agents/reviewer.md", translateFrontmatterForOpenCode(agentReviewer({ projectName })));
1063
+ const agents = writeAgentFiles(cwd2, opencodeAgentFiles(config), {
1064
+ force: opts.force,
1065
+ backupRoot: join7(cwd2, config.storage.dir, "backups")
1066
+ });
1001
1067
  mergeOpencodeJson(join7(cwd2, "opencode.json"), config.tools.mcp.port, detectPackageManager(cwd2));
1002
1068
  writeSkills(cwd2, ".opencode/skills");
1069
+ return { agents };
1003
1070
  }
1004
1071
  async migrate(config, _to, _cwd) {
1005
1072
  void config;
@@ -1025,7 +1092,7 @@ function getMaterializer(provider) {
1025
1092
 
1026
1093
  // src/commands/build.ts
1027
1094
  async function runBuild(cwd2, opts) {
1028
- await buildOnce(cwd2);
1095
+ await buildOnce(cwd2, opts.force);
1029
1096
  if (opts.sync) {
1030
1097
  p.log.step("Syncing agent permissions...");
1031
1098
  const config = await loadConfig(cwd2);
@@ -1037,25 +1104,48 @@ async function runBuild(cwd2, opts) {
1037
1104
  watch(cwd2, { recursive: false }, async (_, filename) => {
1038
1105
  if (filename?.startsWith("agent-harness-kit.config")) {
1039
1106
  p.log.step("Config changed \u2014 rebuilding...");
1040
- await buildOnce(cwd2);
1107
+ await buildOnce(cwd2, false);
1041
1108
  }
1042
1109
  });
1043
1110
  await new Promise(() => {
1044
1111
  });
1045
1112
  }
1046
1113
  }
1047
- async function buildOnce(cwd2) {
1114
+ async function buildOnce(cwd2, force) {
1048
1115
  const spinner6 = p.spinner();
1049
1116
  spinner6.start("Loading config...");
1050
1117
  try {
1051
1118
  const config = await loadConfig(cwd2);
1052
1119
  spinner6.message("Rebuilding files...");
1053
1120
  const materializer = getMaterializer(config.provider);
1054
- await materializer.build(config, cwd2);
1121
+ const report = await materializer.build(config, cwd2, { force });
1055
1122
  spinner6.stop(pc.green("Build complete"));
1056
1123
  p.log.success("AGENTS.md");
1057
1124
  p.log.success(`Agent definitions (${config.provider})`);
1058
1125
  p.log.success("MCP config");
1126
+ const { created, overwritten, preserved, backupDir } = report.agents;
1127
+ if (created.length > 0) {
1128
+ p.log.info(`Created ${created.length} missing agent file(s):
1129
+ ${created.join("\n ")}`);
1130
+ }
1131
+ if (overwritten.length > 0) {
1132
+ p.log.warn(
1133
+ pc.yellow(
1134
+ `--force REGENERATED ${overwritten.length} existing agent file(s), discarding any customizations:
1135
+ ` + overwritten.join("\n ")
1136
+ )
1137
+ );
1138
+ if (backupDir) {
1139
+ p.log.info(pc.yellow(` Previous content backed up \u2192 ${backupDir}`));
1140
+ }
1141
+ }
1142
+ if (preserved.length > 0) {
1143
+ p.log.info(
1144
+ `Left ${preserved.length} existing agent file(s) untouched \u2014 agent files are yours to edit.
1145
+ Re-run with --force to regenerate them from the packaged templates (this DESTROYS your edits;
1146
+ a backup is written first).`
1147
+ );
1148
+ }
1059
1149
  } catch (err) {
1060
1150
  spinner6.stop(pc.red("Build failed"));
1061
1151
  p.log.error(err instanceof Error ? err.message : String(err));
@@ -1071,7 +1161,7 @@ import pc2 from "picocolors";
1071
1161
 
1072
1162
  // src/core/dashboard-server.ts
1073
1163
  import { watch as watch2 } from "fs";
1074
- import { existsSync as existsSync7, readFileSync as readFileSync6 } from "fs";
1164
+ import { existsSync as existsSync7, readFileSync as readFileSync5 } from "fs";
1075
1165
  import { extname, join as join8 } from "path";
1076
1166
  import { serve } from "@hono/node-server";
1077
1167
  import { Hono } from "hono";
@@ -1114,7 +1204,7 @@ var MIME = {
1114
1204
  ".ttf": "font/ttf"
1115
1205
  };
1116
1206
  function fileResponse(filePath) {
1117
- const content = readFileSync6(filePath);
1207
+ const content = readFileSync5(filePath);
1118
1208
  const mime = MIME[extname(filePath)] ?? "application/octet-stream";
1119
1209
  return new Response(content, {
1120
1210
  headers: { "Content-Type": mime, "Cache-Control": "no-cache" }
@@ -1299,7 +1389,7 @@ async function runDashboard(cwd2, opts) {
1299
1389
  import pc3 from "picocolors";
1300
1390
 
1301
1391
  // src/core/doctor.ts
1302
- import { existsSync as existsSync9, readFileSync as readFileSync7 } from "fs";
1392
+ import { existsSync as existsSync9, readFileSync as readFileSync6 } from "fs";
1303
1393
  import { dirname as dirname6, join as join11 } from "path";
1304
1394
  import { fileURLToPath as fileURLToPath5 } from "url";
1305
1395
 
@@ -1366,68 +1456,15 @@ function getProviderAgentInfo(provider) {
1366
1456
  return { agentsDir: ".claude/agents", ext: ".md" };
1367
1457
  }
1368
1458
  }
1369
- function generateExpectedAgentContent(agentName, provider, vars) {
1370
- const { projectName, allowedPaths, writablePaths, model } = vars;
1371
- if (provider === "claude-code") {
1372
- const templateFns = {
1373
- lead: () => agentLead({ projectName }),
1374
- explorer: () => agentExplorer({ projectName, allowedPaths }),
1375
- consultant: () => agentConsultant({ projectName }),
1376
- builder: () => agentBuilder({ projectName, writablePaths }),
1377
- reviewer: () => agentReviewer({ projectName })
1378
- };
1379
- return translateFrontmatterForClaudeCode(templateFns[agentName](), agentName, model);
1380
- }
1381
- if (provider === "opencode") {
1382
- const templateFns = {
1383
- lead: () => agentLead({ projectName }),
1384
- explorer: () => agentExplorer({ projectName, allowedPaths }),
1385
- consultant: () => agentConsultant({ projectName }),
1386
- builder: () => agentBuilder({ projectName, writablePaths }),
1387
- reviewer: () => agentReviewer({ projectName })
1388
- };
1389
- return translateFrontmatterForOpenCode(templateFns[agentName]());
1390
- }
1391
- const tomlFns = {
1392
- lead: () => agentLeadToml({ projectName, model }),
1393
- explorer: () => agentExplorerToml({ projectName, allowedPaths, model }),
1394
- consultant: () => agentConsultantToml({ projectName, model }),
1395
- builder: () => agentBuilderToml({ projectName, writablePaths, model }),
1396
- reviewer: () => agentReviewerToml({ projectName, model })
1397
- };
1398
- return tomlFns[agentName]();
1399
- }
1400
- function checkAgentFilesAtRoot(agentsRoot, ext, provider, projectName, allowedPaths, writablePaths, models) {
1459
+ function checkAgentFilesAtRoot(agentsRoot, ext) {
1401
1460
  return AGENT_NAMES.map((name) => {
1402
1461
  const filePath = join11(agentsRoot, `${name}${ext}`);
1403
- if (!existsSync9(filePath)) {
1404
- return { name, status: "missing" };
1405
- }
1406
- try {
1407
- const live = readFileSync7(filePath, "utf8");
1408
- const expected = generateExpectedAgentContent(name, provider, {
1409
- projectName,
1410
- allowedPaths,
1411
- writablePaths,
1412
- model: models[name]
1413
- });
1414
- return { name, status: live === expected ? "ok" : "outdated" };
1415
- } catch {
1416
- return { name, status: "outdated" };
1417
- }
1462
+ return { name, status: existsSync9(filePath) ? "ok" : "missing" };
1418
1463
  });
1419
1464
  }
1420
- function checkAgentFiles(cwd2, provider, projectName, allowedPaths, writablePaths, models) {
1465
+ function checkAgentFiles(cwd2, provider) {
1421
1466
  const { agentsDir, ext } = getProviderAgentInfo(provider);
1422
- return checkAgentFilesAtRoot(
1423
- join11(cwd2, agentsDir),
1424
- ext,
1425
- provider,
1426
- projectName,
1427
- allowedPaths,
1428
- writablePaths,
1429
- models
1430
- );
1467
+ return checkAgentFilesAtRoot(join11(cwd2, agentsDir), ext);
1431
1468
  }
1432
1469
  function getProviderSkillsDir(provider) {
1433
1470
  switch (provider) {
@@ -1450,8 +1487,8 @@ function checkSkillsAtRoot(skillsRoot) {
1450
1487
  return { name, status: "missing" };
1451
1488
  }
1452
1489
  try {
1453
- const live = readFileSync7(livePath, "utf8");
1454
- const source = readFileSync7(sourcePath, "utf8");
1490
+ const live = readFileSync6(livePath, "utf8");
1491
+ const source = readFileSync6(sourcePath, "utf8");
1455
1492
  return { name, status: live === source ? "ok" : "outdated" };
1456
1493
  } catch {
1457
1494
  return { name, status: "outdated" };
@@ -1475,17 +1512,7 @@ async function getDoctorStatus(cwd2) {
1475
1512
  };
1476
1513
  }
1477
1514
  const provider = config.provider;
1478
- const projectName = config.project.name;
1479
- const allowedPaths = (config.agents.explorer.allowedPaths ?? []).join(", ");
1480
- const writablePaths = (config.agents.builder.writablePaths ?? []).join(", ");
1481
- const models = {
1482
- lead: config.agents.lead.model,
1483
- explorer: config.agents.explorer.model,
1484
- consultant: config.agents.consultant?.model,
1485
- builder: config.agents.builder.model,
1486
- reviewer: config.agents.reviewer.model
1487
- };
1488
- const agents = checkAgentFiles(cwd2, provider, projectName, allowedPaths, writablePaths, models);
1515
+ const agents = checkAgentFiles(cwd2, provider);
1489
1516
  const skills = checkSkills(cwd2, provider);
1490
1517
  return { lib, agents, skills };
1491
1518
  }
@@ -1522,17 +1549,13 @@ function printAgentsSection(agents) {
1522
1549
  return;
1523
1550
  }
1524
1551
  const missing = agents.filter((a) => a.status === "missing");
1525
- const outdated = agents.filter((a) => a.status === "outdated");
1526
- if (missing.length === 0 && outdated.length === 0) {
1527
- ok("agent files", "all up to date");
1552
+ if (missing.length === 0) {
1553
+ ok("agent files", "all present");
1528
1554
  return;
1529
1555
  }
1530
1556
  for (const agent of missing) {
1531
1557
  warn("agent files", `${agent.name} missing`, "run: ahk build");
1532
1558
  }
1533
- for (const agent of outdated) {
1534
- warn("agent files", `${agent.name} outdated`, "run: ahk build");
1535
- }
1536
1559
  }
1537
1560
  function printSkillsSection(skills) {
1538
1561
  if (skills.length === 0) {
@@ -1700,9 +1723,9 @@ function getProviderHealthFiles(provider) {
1700
1723
 
1701
1724
  // src/commands/init.ts
1702
1725
  import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync8 } from "fs";
1703
- import { join as join14 } from "path";
1726
+ import { join as join15 } from "path";
1704
1727
  import * as p3 from "@clack/prompts";
1705
- import pc7 from "picocolors";
1728
+ import pc8 from "picocolors";
1706
1729
 
1707
1730
  // src/schema/init.ts
1708
1731
  import * as v from "valibot";
@@ -1755,13 +1778,52 @@ var cliFormWithRetry = async (formFn, schema) => {
1755
1778
 
1756
1779
  // src/commands/init-helpers.ts
1757
1780
  import { randomUUID } from "crypto";
1758
- import { existsSync as existsSync11, readFileSync as readFileSync8 } from "fs";
1781
+ import { existsSync as existsSync12, readFileSync as readFileSync8 } from "fs";
1782
+ import { join as join14 } from "path";
1783
+ import pc7 from "picocolors";
1784
+
1785
+ // src/core/local-install-guard.ts
1786
+ import { existsSync as existsSync11, readFileSync as readFileSync7 } from "fs";
1759
1787
  import { join as join13 } from "path";
1760
1788
  import pc6 from "picocolors";
1789
+ function isLocalInstallSatisfied(cwd2) {
1790
+ const selfPkgPath = join13(cwd2, "package.json");
1791
+ let projectPkg = null;
1792
+ if (existsSync11(selfPkgPath)) {
1793
+ try {
1794
+ const selfPkg = JSON.parse(readFileSync7(selfPkgPath, "utf8"));
1795
+ if (selfPkg?.name === pkg.name) return true;
1796
+ projectPkg = selfPkg;
1797
+ } catch {
1798
+ }
1799
+ }
1800
+ const [scope, name] = pkg.name.split("/");
1801
+ const localPath = pkg.name.startsWith("@") ? join13(cwd2, "node_modules", scope, name) : join13(cwd2, "node_modules", pkg.name);
1802
+ if (existsSync11(localPath)) return true;
1803
+ const isPnp = existsSync11(join13(cwd2, ".pnp.cjs")) || existsSync11(join13(cwd2, ".pnp.loader.mjs"));
1804
+ if (isPnp && projectPkg) {
1805
+ const deps = {
1806
+ ...projectPkg.dependencies ?? {},
1807
+ ...projectPkg.devDependencies ?? {}
1808
+ };
1809
+ if (Object.prototype.hasOwnProperty.call(deps, pkg.name)) return true;
1810
+ }
1811
+ return false;
1812
+ }
1813
+ function printLocalInstallWarning() {
1814
+ console.error(pc6.yellow(`\u26A0 ${pkg.name} is not installed locally in this project.`));
1815
+ console.error(pc6.dim(" This is only a recommendation for reproducibility: pinning a local"));
1816
+ console.error(pc6.dim(" version keeps behavior consistent across your team and CI, instead of"));
1817
+ console.error(pc6.dim(" drifting with whatever version is installed globally on each machine."));
1818
+ console.error(pc6.dim(` Run: npm install --save-dev ${pkg.name}`));
1819
+ console.error(pc6.dim(" (or the equivalent for your package manager: pnpm add -D, yarn add --dev, bun add -d)"));
1820
+ }
1821
+
1822
+ // src/commands/init-helpers.ts
1761
1823
  function readProjectNameFromPackageJson(cwd2) {
1762
1824
  try {
1763
- const pkgPath2 = join13(cwd2, "package.json");
1764
- if (!existsSync11(pkgPath2)) return null;
1825
+ const pkgPath2 = join14(cwd2, "package.json");
1826
+ if (!existsSync12(pkgPath2)) return null;
1765
1827
  const content = readFileSync8(pkgPath2, "utf8");
1766
1828
  const pkg2 = JSON.parse(content);
1767
1829
  const name = pkg2?.name;
@@ -1772,10 +1834,11 @@ function readProjectNameFromPackageJson(cwd2) {
1772
1834
  }
1773
1835
  }
1774
1836
  function detectConfigExtension(cwd2) {
1837
+ if (!isLocalInstallSatisfied(cwd2)) return "json";
1775
1838
  try {
1776
- if (existsSync11(join13(cwd2, "tsconfig.json"))) return "ts";
1777
- const pkgPath2 = join13(cwd2, "package.json");
1778
- if (!existsSync11(pkgPath2)) return "mjs";
1839
+ if (existsSync12(join14(cwd2, "tsconfig.json"))) return "ts";
1840
+ const pkgPath2 = join14(cwd2, "package.json");
1841
+ if (!existsSync12(pkgPath2)) return "mjs";
1779
1842
  const pkg2 = JSON.parse(readFileSync8(pkgPath2, "utf8"));
1780
1843
  if (pkg2?.type === "module") return "mjs";
1781
1844
  } catch {
@@ -1783,7 +1846,6 @@ function detectConfigExtension(cwd2) {
1783
1846
  return "mjs";
1784
1847
  }
1785
1848
  function applyConfigDefaults(params) {
1786
- const models = params.models ?? {};
1787
1849
  const scope = params.scope ?? "local";
1788
1850
  const projectId = params.projectId ?? randomUUID();
1789
1851
  const baseStorage = {
@@ -1806,22 +1868,10 @@ function applyConfigDefaults(params) {
1806
1868
  docsPath: params.docsPath,
1807
1869
  agentsMd: "./AGENTS.md"
1808
1870
  },
1809
- agents: {
1810
- lead: { instructionsPath: null, ...models.lead && { model: models.lead } },
1811
- explorer: {
1812
- instructionsPath: null,
1813
- allowedPaths: [params.docsPath, "./src"],
1814
- ...models.explorer && { model: models.explorer }
1815
- },
1816
- builder: {
1817
- instructionsPath: null,
1818
- writablePaths: ["./src", "./tests"],
1819
- ...models.builder && { model: models.builder }
1820
- },
1821
- reviewer: { instructionsPath: null, ...models.reviewer && { model: models.reviewer } },
1822
- ...models.consultant && { consultant: { instructionsPath: null, model: models.consultant } },
1823
- custom: []
1824
- },
1871
+ // No `agents` key: per-agent settings (model, role instructions) live in
1872
+ // the generated agent file, which is user-owned. This object is the runtime
1873
+ // twin of the config body emitted by configObjectBody() in templates.ts —
1874
+ // drift between the two is the bug this pairing has to keep out.
1825
1875
  database: { type: "sqlite" },
1826
1876
  storage,
1827
1877
  health: {
@@ -1840,27 +1890,27 @@ function stripAnsi(str2) {
1840
1890
  function drawBox(lines) {
1841
1891
  const width = Math.max(...lines.map((l) => stripAnsi(l).length));
1842
1892
  const border = "\u2500".repeat(width);
1843
- console.log(pc6.yellow(`\u250C${border}\u2510`));
1893
+ console.log(pc7.yellow(`\u250C${border}\u2510`));
1844
1894
  for (const line of lines) {
1845
1895
  const pad = width - stripAnsi(line).length;
1846
1896
  const padStr = pad > 0 ? " ".repeat(pad) : "";
1847
- console.log(pc6.yellow("\u2502") + line + padStr + pc6.yellow("\u2502"));
1897
+ console.log(pc7.yellow("\u2502") + line + padStr + pc7.yellow("\u2502"));
1848
1898
  }
1849
- console.log(pc6.yellow(`\u2514${border}\u2518`));
1899
+ console.log(pc7.yellow(`\u2514${border}\u2518`));
1850
1900
  }
1851
1901
  function printWelcomeMessage(projectName) {
1852
1902
  const sep = "\u2500".repeat(38);
1853
1903
  const lines = [
1854
- ` ${pc6.bold(pc6.white("agent-harness-kit"))} `,
1855
- ` ${pc6.gray("\u2014")} harness scaffolding ${pc6.gray("\u2014")} `,
1856
- ` ${pc6.gray(sep)} `,
1857
- ` ${pc6.bold("Project:")} ${projectName || "\u2014"} `,
1858
- ` ${pc6.bold("Status:")} ${pc6.green("ready to configure")} `,
1859
- ` ${pc6.gray(sep)} `,
1860
- ` ${pc6.gray("Next steps:")} `,
1861
- ` ${pc6.gray("\u2192")} ${pc6.gray("Set up your AI provider config")} `,
1862
- ` ${pc6.gray("\u2192")} ${pc6.gray("Run your health check to verify")} `,
1863
- ` ${pc6.gray("\u2192")} ${pc6.gray("Start adding tasks for your agents")} `
1904
+ ` ${pc7.bold(pc7.white("agent-harness-kit"))} `,
1905
+ ` ${pc7.gray("\u2014")} harness scaffolding ${pc7.gray("\u2014")} `,
1906
+ ` ${pc7.gray(sep)} `,
1907
+ ` ${pc7.bold("Project:")} ${projectName || "\u2014"} `,
1908
+ ` ${pc7.bold("Status:")} ${pc7.green("ready to configure")} `,
1909
+ ` ${pc7.gray(sep)} `,
1910
+ ` ${pc7.gray("Next steps:")} `,
1911
+ ` ${pc7.gray("\u2192")} ${pc7.gray("Set up your AI provider config")} `,
1912
+ ` ${pc7.gray("\u2192")} ${pc7.gray("Run your health check to verify")} `,
1913
+ ` ${pc7.gray("\u2192")} ${pc7.gray("Start adding tasks for your agents")} `
1864
1914
  ];
1865
1915
  console.log();
1866
1916
  drawBox(lines);
@@ -1872,18 +1922,18 @@ async function runInit(cwd2, flags) {
1872
1922
  const existingConfig = findConfigFile(cwd2);
1873
1923
  if (existingConfig) {
1874
1924
  console.log(
1875
- pc7.yellow("\u26A0") + " " + pc7.bold("Project already initialized.") + pc7.dim(` (${existingConfig})`)
1925
+ pc8.yellow("\u26A0") + " " + pc8.bold("Project already initialized.") + pc8.dim(` (${existingConfig})`)
1876
1926
  );
1877
1927
  console.log();
1878
- console.log(pc7.dim("Suggested next steps:"));
1928
+ console.log(pc8.dim("Suggested next steps:"));
1879
1929
  console.log(
1880
- " " + pc7.cyan("ahk build") + pc7.dim(" \u2014 re-sync agent files after updating the library")
1930
+ " " + pc8.cyan("ahk build") + pc8.dim(" \u2014 re-sync agent files after updating the library")
1881
1931
  );
1882
- console.log(" " + pc7.cyan("ahk build --sync") + pc7.dim(" \u2014 also sync agent permissions"));
1932
+ console.log(" " + pc8.cyan("ahk build --sync") + pc8.dim(" \u2014 also sync agent permissions"));
1883
1933
  console.log(
1884
- " " + pc7.cyan("ahk reset") + pc7.dim(" \u2014 wipe and re-initialize from scratch")
1934
+ " " + pc8.cyan("ahk reset") + pc8.dim(" \u2014 wipe and re-initialize from scratch")
1885
1935
  );
1886
- console.log(" " + pc7.cyan("ahk dashboard") + pc7.dim(" \u2014 open the harness dashboard"));
1936
+ console.log(" " + pc8.cyan("ahk dashboard") + pc8.dim(" \u2014 open the harness dashboard"));
1887
1937
  process.exit(0);
1888
1938
  }
1889
1939
  const detectedName = flags.name ?? readProjectNameFromPackageJson(cwd2);
@@ -1935,61 +1985,6 @@ async function runInit(cwd2, flags) {
1935
1985
  }
1936
1986
  provider = val;
1937
1987
  }
1938
- const AGENT_LABELS = [
1939
- { key: "lead", label: "Lead" },
1940
- { key: "explorer", label: "Explorer" },
1941
- { key: "consultant", label: "Consultant" },
1942
- { key: "builder", label: "Builder" },
1943
- { key: "reviewer", label: "Reviewer" }
1944
- ];
1945
- const modelOverrides = {};
1946
- if (provider === "claude-code" || provider === "codex-cli") {
1947
- const wantsModelCustomization = await p3.confirm({
1948
- message: "Customize the model per agent?",
1949
- initialValue: false
1950
- });
1951
- if (p3.isCancel(wantsModelCustomization)) {
1952
- p3.cancel("Cancelled.");
1953
- process.exit(0);
1954
- }
1955
- if (wantsModelCustomization) {
1956
- if (provider === "claude-code") {
1957
- for (const agent of AGENT_LABELS) {
1958
- const val = await p3.select({
1959
- message: `Model for ${agent.label}`,
1960
- options: [
1961
- { value: "inherit", label: "inherit (default)" },
1962
- { value: "haiku", label: "haiku" },
1963
- { value: "sonnet", label: "sonnet" },
1964
- { value: "opus", label: "opus" },
1965
- { value: "fable", label: "fable" }
1966
- ],
1967
- initialValue: "inherit"
1968
- });
1969
- if (p3.isCancel(val)) {
1970
- p3.cancel("Cancelled.");
1971
- process.exit(0);
1972
- }
1973
- modelOverrides[agent.key] = val;
1974
- }
1975
- } else {
1976
- for (const agent of AGENT_LABELS) {
1977
- const val = await p3.text({
1978
- message: `Model for ${agent.label} (Codex does not validate this value)`,
1979
- placeholder: "e.g. gpt-5 (empty or <3 chars = no override)"
1980
- });
1981
- if (p3.isCancel(val)) {
1982
- p3.cancel("Cancelled.");
1983
- process.exit(0);
1984
- }
1985
- const trimmed = val.trim();
1986
- if (trimmed.length >= 3) {
1987
- modelOverrides[agent.key] = trimmed;
1988
- }
1989
- }
1990
- }
1991
- }
1992
- }
1993
1988
  let docsPath;
1994
1989
  if (flags.docs) {
1995
1990
  docsPath = flags.docs;
@@ -2090,14 +2085,13 @@ async function runInit(cwd2, flags) {
2090
2085
  provider,
2091
2086
  docsPath,
2092
2087
  tasksAdapter,
2093
- models: modelOverrides,
2094
2088
  scope: storageScope
2095
2089
  });
2096
2090
  const materializer = getMaterializer(provider);
2097
2091
  const installDir = cwd2;
2098
2092
  configExt = detectConfigExtension(cwd2);
2099
2093
  const configFileName = `agent-harness-kit.config.${configExt}`;
2100
- const templateFn = configExt === "ts" ? configTs : configExt === "mjs" ? configMjs : configCjs;
2094
+ const templateFn = configExt === "json" ? configJson : configExt === "ts" ? configTs : configExt === "mjs" ? configMjs : configCjs;
2101
2095
  const configContent = templateFn({
2102
2096
  name,
2103
2097
  description,
@@ -2105,12 +2099,11 @@ async function runInit(cwd2, flags) {
2105
2099
  docsPath,
2106
2100
  tasksAdapter,
2107
2101
  port: config.tools.mcp.port,
2108
- models: modelOverrides,
2109
2102
  scope: config.storage.scope,
2110
2103
  projectId: config.storage.projectId
2111
2104
  });
2112
- writeFileSync8(join14(installDir, configFileName), configContent, "utf8");
2113
- mkdirSync7(join14(installDir, config.storage.dir), { recursive: true });
2105
+ writeFileSync8(join15(installDir, configFileName), configContent, "utf8");
2106
+ mkdirSync7(join15(installDir, config.storage.dir), { recursive: true });
2114
2107
  const db = await openDB(config, installDir);
2115
2108
  await db.writeStorageState(installDir);
2116
2109
  await materializer.scaffold(config, { cwd: installDir, firstTask });
@@ -2130,39 +2123,39 @@ async function runInit(cwd2, flags) {
2130
2123
  p3.log.error(err instanceof Error ? err.message : String(err));
2131
2124
  throw err;
2132
2125
  }
2133
- console.log(pc7.green("\u2713 Scaffolded harness in current directory"));
2126
+ console.log(pc8.green("\u2713 Scaffolded harness in current directory"));
2134
2127
  const agentsDir = provider === "claude-code" ? ".claude/agents/" : ".opencode/agents/";
2135
2128
  const mcpFile = provider === "claude-code" ? ".claude/mcp.json" : "./opencode.json";
2136
2129
  console.log("");
2137
- console.log(pc7.green(`\u2713 agent-harness-kit.config.${configExt}`));
2138
- console.log(pc7.green("\u2713 AGENTS.md"));
2139
- console.log(pc7.green("\u2713 health.sh"));
2130
+ console.log(pc8.green(`\u2713 agent-harness-kit.config.${configExt}`));
2131
+ console.log(pc8.green("\u2713 AGENTS.md"));
2132
+ console.log(pc8.green("\u2713 health.sh"));
2140
2133
  console.log(
2141
- pc7.green(
2134
+ pc8.green(
2142
2135
  storageScope === "global" ? "\u2713 ~/.harness/dbs/<projectId>/harness.db" : "\u2713 .harness/harness.db"
2143
2136
  )
2144
2137
  );
2145
2138
  console.log(
2146
- pc7.green(
2139
+ pc8.green(
2147
2140
  storageScope === "global" ? "\u2713 ~/.harness/dbs/<projectId>/current.md" : "\u2713 .harness/current.md"
2148
2141
  )
2149
2142
  );
2150
- console.log(pc7.green("\u2713 .harness/storage-state.json"));
2151
- console.log(pc7.green(`\u2713 ${agentsDir}lead.md`));
2152
- console.log(pc7.green(`\u2713 ${agentsDir}explorer.md`));
2153
- console.log(pc7.green(`\u2713 ${agentsDir}builder.md`));
2154
- console.log(pc7.green(`\u2713 ${agentsDir}reviewer.md`));
2155
- console.log(pc7.green(`\u2713 ${mcpFile}`));
2156
- console.log(pc7.green("\u2713 .gitignore entries added"));
2143
+ console.log(pc8.green("\u2713 .harness/storage-state.json"));
2144
+ console.log(pc8.green(`\u2713 ${agentsDir}lead.md`));
2145
+ console.log(pc8.green(`\u2713 ${agentsDir}explorer.md`));
2146
+ console.log(pc8.green(`\u2713 ${agentsDir}builder.md`));
2147
+ console.log(pc8.green(`\u2713 ${agentsDir}reviewer.md`));
2148
+ console.log(pc8.green(`\u2713 ${mcpFile}`));
2149
+ console.log(pc8.green("\u2713 .gitignore entries added"));
2157
2150
  console.log("");
2158
- console.log(pc7.cyan("\u2192") + ` Edit ${pc7.cyan("health.sh")} with your project checks`);
2159
- console.log(pc7.cyan("\u2192") + ` ${pc7.cyan("ahk task add")} to queue work for agents`);
2151
+ console.log(pc8.cyan("\u2192") + ` Edit ${pc8.cyan("health.sh")} with your project checks`);
2152
+ console.log(pc8.cyan("\u2192") + ` ${pc8.cyan("ahk task add")} to queue work for agents`);
2160
2153
  console.log(
2161
- pc7.cyan("\u2192") + ` Enrich your docs with knowledge graphs: ${pc7.cyan("https://github.com/safishamsi/graphify")}`
2154
+ pc8.cyan("\u2192") + ` Enrich your docs with knowledge graphs: ${pc8.cyan("https://github.com/safishamsi/graphify")}`
2162
2155
  );
2163
2156
  const recommendations = [
2164
2157
  ` Give a try to Heimdall MCP: Transparent proxy that traces every MCP tool call with OpenTelemetry. `,
2165
- ` Learn more: ${pc7.cyan("https://github.com/enmanuelmag/heimdall-mcp")} `
2158
+ ` Learn more: ${pc8.cyan("https://github.com/enmanuelmag/heimdall-mcp")} `
2166
2159
  ];
2167
2160
  console.log("");
2168
2161
  drawBox(recommendations);
@@ -2170,7 +2163,7 @@ async function runInit(cwd2, flags) {
2170
2163
 
2171
2164
  // src/commands/migrate.ts
2172
2165
  import * as p4 from "@clack/prompts";
2173
- import pc8 from "picocolors";
2166
+ import pc9 from "picocolors";
2174
2167
  async function runMigrate(cwd2, opts) {
2175
2168
  const config = await loadConfig(cwd2);
2176
2169
  let target;
@@ -2192,7 +2185,7 @@ async function runMigrate(cwd2, opts) {
2192
2185
  target = val;
2193
2186
  }
2194
2187
  if (target === config.provider) {
2195
- console.log(pc8.dim(`Already on ${target} \u2014 nothing to migrate.`));
2188
+ console.log(pc9.dim(`Already on ${target} \u2014 nothing to migrate.`));
2196
2189
  return;
2197
2190
  }
2198
2191
  const spinner6 = p4.spinner();
@@ -2200,21 +2193,21 @@ async function runMigrate(cwd2, opts) {
2200
2193
  try {
2201
2194
  const targetMaterializer = getMaterializer(target);
2202
2195
  await targetMaterializer.build(config, cwd2);
2203
- spinner6.stop(pc8.green(`Migrated to ${target}`));
2196
+ spinner6.stop(pc9.green(`Migrated to ${target}`));
2204
2197
  p4.log.warn(`Update agent-harness-kit.config.ts: set provider: '${target}'`);
2205
2198
  p4.log.warn(`Then run: ahk build`);
2206
2199
  } catch (err) {
2207
- spinner6.stop(pc8.red("Migration failed"));
2200
+ spinner6.stop(pc9.red("Migration failed"));
2208
2201
  p4.log.error(err instanceof Error ? err.message : String(err));
2209
2202
  process.exit(1);
2210
2203
  }
2211
2204
  }
2212
2205
 
2213
2206
  // src/commands/migrate-storage.ts
2214
- import { copyFileSync, existsSync as existsSync12, mkdirSync as mkdirSync8, rmSync, writeFileSync as writeFileSync9 } from "fs";
2207
+ import { copyFileSync, existsSync as existsSync13, mkdirSync as mkdirSync8, rmSync, writeFileSync as writeFileSync9 } from "fs";
2215
2208
  import { homedir as homedir3 } from "os";
2216
- import { dirname as dirname7, join as join15, resolve as resolve7 } from "path";
2217
- import pc9 from "picocolors";
2209
+ import { dirname as dirname7, join as join16, resolve as resolve7 } from "path";
2210
+ import pc10 from "picocolors";
2218
2211
  function log5(msg) {
2219
2212
  console.log(msg);
2220
2213
  }
@@ -2225,14 +2218,14 @@ function defaultMarkdownPathForConfig(config) {
2225
2218
  return config.storage.scope === "local" ? config.storage.markdownFallback.path : DEFAULT_MARKDOWN_PATH;
2226
2219
  }
2227
2220
  function currentMdPathForScope(scope, config, cwd2, homeDir) {
2228
- return scope === "global" ? join15(resolveGlobalStorageDir(config, homeDir), "current.md") : resolve7(cwd2, defaultMarkdownPathForConfig(config));
2221
+ return scope === "global" ? join16(resolveGlobalStorageDir(config, homeDir), "current.md") : resolve7(cwd2, defaultMarkdownPathForConfig(config));
2229
2222
  }
2230
2223
  function defaultSqlitePathForConfig(config) {
2231
2224
  return config.storage.scope === "local" && config.database.type === "sqlite" ? config.storage.sqlitePath ?? DEFAULT_SQLITE_PATH : DEFAULT_SQLITE_PATH;
2232
2225
  }
2233
2226
  async function backupDestination(cwd2, storageDir, data) {
2234
2227
  const backupsDir = resolve7(cwd2, storageDir, "backups");
2235
- const path = join15(backupsDir, `pre-migrate-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.json`);
2228
+ const path = join16(backupsDir, `pre-migrate-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.json`);
2236
2229
  try {
2237
2230
  mkdirSync8(backupsDir, { recursive: true });
2238
2231
  writeFileSync9(path, JSON.stringify(data, null, 2) + "\n", "utf8");
@@ -2247,11 +2240,11 @@ function copySqliteFile(srcPath, destPath) {
2247
2240
  mkdirSync8(dirname7(destPath), { recursive: true });
2248
2241
  copyFileSync(srcPath, destPath);
2249
2242
  for (const suffix of ["-wal", "-shm"]) {
2250
- if (existsSync12(`${srcPath}${suffix}`)) {
2243
+ if (existsSync13(`${srcPath}${suffix}`)) {
2251
2244
  copyFileSync(`${srcPath}${suffix}`, `${destPath}${suffix}`);
2252
2245
  }
2253
2246
  }
2254
- if (!existsSync12(destPath)) {
2247
+ if (!existsSync13(destPath)) {
2255
2248
  throw new Error(`Copy verification failed: ${destPath} does not exist after copy.`);
2256
2249
  }
2257
2250
  }
@@ -2279,13 +2272,13 @@ async function runMigrateStorage(cwd2, opts, homeDir = homedir3()) {
2279
2272
  } finally {
2280
2273
  await db.close();
2281
2274
  }
2282
- log5(pc9.dim("storage-state.json was missing; no data found at either candidate location. Nothing to migrate \u2014 state recorded."));
2275
+ log5(pc10.dim("storage-state.json was missing; no data found at either candidate location. Nothing to migrate \u2014 state recorded."));
2283
2276
  return;
2284
2277
  }
2285
2278
  realScope = localCount > 0 ? "local" : "global";
2286
2279
  realDbType = "sqlite";
2287
2280
  log5(
2288
- pc9.yellow(
2281
+ pc10.yellow(
2289
2282
  `storage-state.json was missing. Detected real data at ${realScope} sqlite location (${realScope === "local" ? localPath : globalPath}) \u2014 using it as the migration source.`
2290
2283
  )
2291
2284
  );
@@ -2296,7 +2289,7 @@ async function runMigrateStorage(cwd2, opts, homeDir = homedir3()) {
2296
2289
  const desiredScope = config.storage.scope;
2297
2290
  const desiredDbType = config.database.type;
2298
2291
  if (realScope === desiredScope && realDbType === desiredDbType) {
2299
- log5(pc9.green(`\u2713 Storage already matches config (scope=${desiredScope}, database=${desiredDbType}) \u2014 nothing to migrate.`));
2292
+ log5(pc10.green(`\u2713 Storage already matches config (scope=${desiredScope}, database=${desiredDbType}) \u2014 nothing to migrate.`));
2300
2293
  return;
2301
2294
  }
2302
2295
  if (realDbType !== "sqlite") {
@@ -2310,7 +2303,7 @@ async function runMigrateStorage(cwd2, opts, homeDir = homedir3()) {
2310
2303
  return migrateAcrossDbType(cwd2, config, homeDir, realScope, opts);
2311
2304
  }
2312
2305
  async function probeTaskCount(dbPath) {
2313
- if (!existsSync12(dbPath)) return 0;
2306
+ if (!existsSync13(dbPath)) return 0;
2314
2307
  const { SQLiteDriver } = await import("./sqlite-TR4D324R.js");
2315
2308
  const driver = new SQLiteDriver(dbPath);
2316
2309
  try {
@@ -2327,10 +2320,10 @@ async function migrateScopeOnly(cwd2, config, homeDir, fromScope, toScope, opts)
2327
2320
  const destDb = resolveSqlitePathForScope(toScope, sqlitePath, cwd2, config, homeDir);
2328
2321
  const srcMd = currentMdPathForScope(fromScope, config, cwd2, homeDir);
2329
2322
  const destMd = currentMdPathForScope(toScope, config, cwd2, homeDir);
2330
- if (!existsSync12(srcDb)) {
2323
+ if (!existsSync13(srcDb)) {
2331
2324
  fail(`Source database not found at ${srcDb} (expected ${fromScope} scope) \u2014 nothing to move.`);
2332
2325
  }
2333
- const destExists = existsSync12(destDb);
2326
+ const destExists = existsSync13(destDb);
2334
2327
  if (destExists) {
2335
2328
  const { SQLiteDriver } = await import("./sqlite-TR4D324R.js");
2336
2329
  const destDriver = new SQLiteDriver(destDb);
@@ -2359,36 +2352,36 @@ async function migrateScopeOnly(cwd2, config, homeDir, fromScope, toScope, opts)
2359
2352
  await backupDriver.close();
2360
2353
  }
2361
2354
  const backupPath = await backupDestination(cwd2, config.storage.dir, data);
2362
- log5(pc9.yellow(` Backed up existing destination data \u2192 ${backupPath}`));
2355
+ log5(pc10.yellow(` Backed up existing destination data \u2192 ${backupPath}`));
2363
2356
  }
2364
2357
  }
2365
2358
  if (opts.dryRun) {
2366
- log5(pc9.dim(`[dry-run] Would copy ${srcDb} \u2192 ${destDb} (scope ${fromScope} \u2192 ${toScope}), and move current.md.`));
2359
+ log5(pc10.dim(`[dry-run] Would copy ${srcDb} \u2192 ${destDb} (scope ${fromScope} \u2192 ${toScope}), and move current.md.`));
2367
2360
  return;
2368
2361
  }
2369
2362
  copySqliteFile(srcDb, destDb);
2370
- log5(pc9.green(`\u2713 Copied database ${srcDb} \u2192 ${destDb}`));
2371
- if (existsSync12(srcMd)) {
2363
+ log5(pc10.green(`\u2713 Copied database ${srcDb} \u2192 ${destDb}`));
2364
+ if (existsSync13(srcMd)) {
2372
2365
  mkdirSync8(dirname7(destMd), { recursive: true });
2373
2366
  copyFileSync(srcMd, destMd);
2374
- log5(pc9.green(`\u2713 Copied current.md ${srcMd} \u2192 ${destMd}`));
2367
+ log5(pc10.green(`\u2713 Copied current.md ${srcMd} \u2192 ${destMd}`));
2375
2368
  }
2376
2369
  rmSync(srcDb, { force: true });
2377
2370
  rmSync(`${srcDb}-wal`, { force: true });
2378
2371
  rmSync(`${srcDb}-shm`, { force: true });
2379
- if (existsSync12(srcMd) && srcMd !== destMd) rmSync(srcMd, { force: true });
2372
+ if (existsSync13(srcMd) && srcMd !== destMd) rmSync(srcMd, { force: true });
2380
2373
  const db = await openDB(config, cwd2, homeDir);
2381
2374
  try {
2382
2375
  await db.writeStorageState(cwd2);
2383
2376
  } finally {
2384
2377
  await db.close();
2385
2378
  }
2386
- log5(pc9.green(`\u2713 Storage migrated: scope ${fromScope} \u2192 ${toScope}`));
2379
+ log5(pc10.green(`\u2713 Storage migrated: scope ${fromScope} \u2192 ${toScope}`));
2387
2380
  }
2388
2381
  async function migrateAcrossDbType(cwd2, config, homeDir, sourceScope, opts) {
2389
2382
  const sqlitePath = defaultSqlitePathForConfig(config);
2390
2383
  const srcPath = resolveSqlitePathForScope(sourceScope, sqlitePath, cwd2, config, homeDir);
2391
- if (!existsSync12(srcPath)) {
2384
+ if (!existsSync13(srcPath)) {
2392
2385
  fail(`Source sqlite database not found at ${srcPath} (expected ${sourceScope} scope) \u2014 nothing to migrate.`);
2393
2386
  }
2394
2387
  const { SQLiteDriver } = await import("./sqlite-TR4D324R.js");
@@ -2424,7 +2417,7 @@ async function migrateAcrossDbType(cwd2, config, homeDir, sourceScope, opts) {
2424
2417
  }
2425
2418
  if (opts.dryRun) {
2426
2419
  log5(
2427
- pc9.dim(
2420
+ pc10.dim(
2428
2421
  `[dry-run] Would migrate ${sourceCounts.tasks} task(s) from sqlite (${sourceScope}, ${srcPath}) to ${config.database.type}${destEmpty ? "" : " (destination has data \u2014 would back up first, then overwrite)"}.`
2429
2422
  )
2430
2423
  );
@@ -2434,34 +2427,34 @@ async function migrateAcrossDbType(cwd2, config, homeDir, sourceScope, opts) {
2434
2427
  if (!destEmpty && opts.force) {
2435
2428
  const currentDestData = await destDb.exportJson();
2436
2429
  backupPath = await backupDestination(cwd2, config.storage.dir, currentDestData);
2437
- log5(pc9.yellow(` Backed up existing destination data \u2192 ${backupPath}`));
2430
+ log5(pc10.yellow(` Backed up existing destination data \u2192 ${backupPath}`));
2438
2431
  }
2439
2432
  await destDb.importFullExport(sourceData, config.database.type, { truncateFirst: !destEmpty });
2440
2433
  await destDb.writeStorageState(cwd2);
2441
2434
  log5(
2442
- pc9.green(
2435
+ pc10.green(
2443
2436
  `\u2713 Migrated ${sourceData.tasks.length} task(s), ${sourceData.actions.length} action(s) from sqlite (${sourceScope}) \u2192 ${config.database.type}.`
2444
2437
  )
2445
2438
  );
2446
- if (backupPath) log5(pc9.dim(` Destination backup: ${backupPath}`));
2447
- log5(pc9.yellow(` Note: the original sqlite file at ${srcPath} was NOT deleted \u2014 remove it manually once you've verified the migration.`));
2439
+ if (backupPath) log5(pc10.dim(` Destination backup: ${backupPath}`));
2440
+ log5(pc10.yellow(` Note: the original sqlite file at ${srcPath} was NOT deleted \u2014 remove it manually once you've verified the migration.`));
2448
2441
  } finally {
2449
2442
  await destDb.close();
2450
2443
  }
2451
2444
  }
2452
2445
 
2453
2446
  // src/commands/reset.ts
2454
- import { existsSync as existsSync13, readdirSync, rmSync as rmSync2 } from "fs";
2447
+ import { existsSync as existsSync14, readdirSync, rmSync as rmSync2 } from "fs";
2455
2448
  import { homedir as homedir4 } from "os";
2456
- import { join as join16, resolve as resolve8 } from "path";
2449
+ import { join as join17, resolve as resolve8 } from "path";
2457
2450
  import * as p5 from "@clack/prompts";
2458
- import pc10 from "picocolors";
2451
+ import pc11 from "picocolors";
2459
2452
  var AGENT_MD_FILES = ["lead", "explorer", "consultant", "builder", "reviewer"];
2460
2453
  async function resetAgentMds(cwd2, provider) {
2461
2454
  const agentDir = provider === "claude-code" ? ".claude/agents" : ".opencode/agents";
2462
2455
  const agentDirPath = resolve8(cwd2, agentDir);
2463
- if (!existsSync13(agentDirPath)) {
2464
- console.log(pc10.yellow(` Skipping agent files \u2014 directory not found: ${agentDirPath}`));
2456
+ if (!existsSync14(agentDirPath)) {
2457
+ console.log(pc11.yellow(` Skipping agent files \u2014 directory not found: ${agentDirPath}`));
2465
2458
  return;
2466
2459
  }
2467
2460
  const existingFiles = [];
@@ -2473,11 +2466,11 @@ async function resetAgentMds(cwd2, provider) {
2473
2466
  }
2474
2467
  }
2475
2468
  } catch {
2476
- console.log(pc10.yellow(` Skipping agent files \u2014 ${agentDirPath} is not readable`));
2469
+ console.log(pc11.yellow(` Skipping agent files \u2014 ${agentDirPath} is not readable`));
2477
2470
  return;
2478
2471
  }
2479
2472
  if (existingFiles.length === 0) {
2480
- console.log(pc10.yellow(` No agent MD files found in ${agentDir}/`));
2473
+ console.log(pc11.yellow(` No agent MD files found in ${agentDir}/`));
2481
2474
  return;
2482
2475
  }
2483
2476
  for (const file of existingFiles) {
@@ -2486,19 +2479,19 @@ async function resetAgentMds(cwd2, provider) {
2486
2479
  initialValue: true
2487
2480
  });
2488
2481
  if (p5.isCancel(confirm3)) {
2489
- console.log(pc10.red(" Cancelled by user."));
2482
+ console.log(pc11.red(" Cancelled by user."));
2490
2483
  return;
2491
2484
  }
2492
2485
  if (confirm3) {
2493
2486
  try {
2494
- const filePath = join16(agentDirPath, file);
2487
+ const filePath = join17(agentDirPath, file);
2495
2488
  rmSync2(filePath, { force: true });
2496
- console.log(pc10.green(` Removed ${file}`));
2489
+ console.log(pc11.green(` Removed ${file}`));
2497
2490
  } catch {
2498
- console.error(pc10.red(` Failed to remove ${file}`));
2491
+ console.error(pc11.red(` Failed to remove ${file}`));
2499
2492
  }
2500
2493
  } else {
2501
- console.log(pc10.cyan(` Skipped ${file}`));
2494
+ console.log(pc11.cyan(` Skipped ${file}`));
2502
2495
  }
2503
2496
  }
2504
2497
  }
@@ -2507,7 +2500,7 @@ async function runReset(cwd2, opts) {
2507
2500
  try {
2508
2501
  config = await loadConfig(cwd2);
2509
2502
  } catch {
2510
- console.error(pc10.red("\u2717 No agent-harness-kit.config found. Run: ahk init"));
2503
+ console.error(pc11.red("\u2717 No agent-harness-kit.config found. Run: ahk init"));
2511
2504
  process.exit(1);
2512
2505
  }
2513
2506
  const storageDir = config.storage.dir || ".harness";
@@ -2516,12 +2509,12 @@ async function runReset(cwd2, opts) {
2516
2509
  let resetDb = false;
2517
2510
  let resetFeatureList = false;
2518
2511
  let resetAgentMdsFlag = false;
2519
- if (dbPath && existsSync13(dbPath)) {
2512
+ if (dbPath && existsSync14(dbPath)) {
2520
2513
  if (opts.force) {
2521
2514
  resetDb = true;
2522
2515
  } else {
2523
2516
  if (config.database.type !== "sqlite") {
2524
- console.log(pc10.yellow(` Skipping DB reset \u2014 database type "${config.database.type}" is not managed by this command.`));
2517
+ console.log(pc11.yellow(` Skipping DB reset \u2014 database type "${config.database.type}" is not managed by this command.`));
2525
2518
  resetDb = false;
2526
2519
  } else {
2527
2520
  const confirm3 = await p5.confirm({
@@ -2529,16 +2522,16 @@ async function runReset(cwd2, opts) {
2529
2522
  initialValue: true
2530
2523
  });
2531
2524
  if (p5.isCancel(confirm3)) {
2532
- console.log(pc10.red(" Cancelled by user."));
2525
+ console.log(pc11.red(" Cancelled by user."));
2533
2526
  return;
2534
2527
  }
2535
2528
  resetDb = confirm3;
2536
2529
  }
2537
2530
  }
2538
2531
  } else if (!dbPath) {
2539
- console.log(pc10.dim(` Skipping DB reset \u2014 remote ${config.database.type} database is not managed by this command.`));
2532
+ console.log(pc11.dim(` Skipping DB reset \u2014 remote ${config.database.type} database is not managed by this command.`));
2540
2533
  }
2541
- if (existsSync13(featureListPath)) {
2534
+ if (existsSync14(featureListPath)) {
2542
2535
  if (opts.force) {
2543
2536
  resetFeatureList = true;
2544
2537
  } else {
@@ -2547,7 +2540,7 @@ async function runReset(cwd2, opts) {
2547
2540
  initialValue: true
2548
2541
  });
2549
2542
  if (p5.isCancel(confirm3)) {
2550
- console.log(pc10.red(" Cancelled by user."));
2543
+ console.log(pc11.red(" Cancelled by user."));
2551
2544
  return;
2552
2545
  }
2553
2546
  resetFeatureList = confirm3;
@@ -2561,17 +2554,17 @@ async function runReset(cwd2, opts) {
2561
2554
  rmSync2(dbPath, { force: true });
2562
2555
  rmSync2(`${dbPath}-wal`, { force: true });
2563
2556
  rmSync2(`${dbPath}-shm`, { force: true });
2564
- console.log(pc10.green(` \u2713 Removed ${dbPath}`));
2557
+ console.log(pc11.green(` \u2713 Removed ${dbPath}`));
2565
2558
  } catch {
2566
- console.error(pc10.red(` \u2717 Failed to remove ${dbPath}`));
2559
+ console.error(pc11.red(` \u2717 Failed to remove ${dbPath}`));
2567
2560
  }
2568
2561
  }
2569
2562
  if (resetFeatureList) {
2570
2563
  try {
2571
2564
  rmSync2(featureListPath, { force: true });
2572
- console.log(pc10.green(` \u2713 Removed ${storageDir}/feature_list.json`));
2565
+ console.log(pc11.green(` \u2713 Removed ${storageDir}/feature_list.json`));
2573
2566
  } catch {
2574
- console.error(pc10.red(` \u2717 Failed to remove ${featureListPath}`));
2567
+ console.error(pc11.red(` \u2717 Failed to remove ${featureListPath}`));
2575
2568
  }
2576
2569
  }
2577
2570
  if (resetAgentMdsFlag) {
@@ -2579,16 +2572,16 @@ async function runReset(cwd2, opts) {
2579
2572
  await resetAgentMds(cwd2, opts.provider || "claude-code");
2580
2573
  }
2581
2574
  if (!resetDb && !resetFeatureList && !resetAgentMdsFlag) {
2582
- console.log(pc10.yellow(" Nothing to reset (all items missing or skipped)."));
2575
+ console.log(pc11.yellow(" Nothing to reset (all items missing or skipped)."));
2583
2576
  return;
2584
2577
  }
2585
2578
  console.log("");
2586
- console.log(pc10.green('\u2713 Reset complete. Run "ahk init" to scaffold a fresh harness.'));
2579
+ console.log(pc11.green('\u2713 Reset complete. Run "ahk init" to scaffold a fresh harness.'));
2587
2580
  }
2588
2581
 
2589
2582
  // src/core/mcp-server.ts
2590
- import { existsSync as existsSync15, mkdirSync as mkdirSync9, readdirSync as readdirSync2, readFileSync as readFileSync10, statSync, writeFileSync as writeFileSync10 } from "fs";
2591
- import { join as join18, resolve as resolve9 } from "path";
2583
+ import { existsSync as existsSync16, mkdirSync as mkdirSync9, readdirSync as readdirSync2, readFileSync as readFileSync9, statSync, writeFileSync as writeFileSync10 } from "fs";
2584
+ import { join as join19, resolve as resolve9 } from "path";
2592
2585
  import { Server } from "@modelcontextprotocol/sdk/server";
2593
2586
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
2594
2587
  import {
@@ -2597,45 +2590,20 @@ import {
2597
2590
  } from "@modelcontextprotocol/sdk/types.js";
2598
2591
 
2599
2592
  // src/core/permissions-check.ts
2600
- import { existsSync as existsSync14, readFileSync as readFileSync9 } from "fs";
2601
- import { join as join17 } from "path";
2602
- var CANONICAL = {
2603
- lead: [...MCP_CLAUDE_PERMISSIONS_LEAD],
2604
- explorer: [...MCP_CLAUDE_PERMISSIONS_EXPLORER],
2605
- consultant: [...MCP_CLAUDE_PERMISSIONS_CONSULTANT],
2606
- builder: [...MCP_CLAUDE_PERMISSIONS_BUILDER],
2607
- reviewer: [...MCP_CLAUDE_PERMISSIONS_REVIEWER]
2608
- };
2609
- function parseToolsFromFrontmatter(content) {
2610
- const match = content.match(/^---\n([\s\S]*?)\n---/m);
2611
- if (!match) return [];
2612
- const fm = match[1];
2613
- const toolsMatch = fm.match(/^tools:\n((?: - [^\n]+\n?)*)/m);
2614
- if (!toolsMatch) return [];
2615
- return toolsMatch[1].split("\n").map((l) => l.trim().replace(/^- /, "")).filter((l) => l.startsWith("mcp__"));
2616
- }
2593
+ import { existsSync as existsSync15 } from "fs";
2594
+ import { join as join18 } from "path";
2595
+ var AGENTS = ["lead", "explorer", "consultant", "builder", "reviewer"];
2617
2596
  function checkPermissionsSync(cwd2, config) {
2618
2597
  if (config.provider !== "claude-code") {
2619
2598
  return { in_sync: true };
2620
2599
  }
2621
2600
  const agents = {};
2622
2601
  let in_sync = true;
2623
- for (const agent of ["lead", "explorer", "consultant", "builder", "reviewer"]) {
2624
- const filePath = join17(cwd2, ".claude", "agents", `${agent}.md`);
2625
- if (!existsSync14(filePath)) {
2626
- const missing2 = CANONICAL[agent];
2627
- agents[agent] = { ok: false, missing: missing2, extra: [] };
2628
- in_sync = false;
2629
- continue;
2630
- }
2631
- const content = readFileSync9(filePath, "utf-8");
2632
- const installed = parseToolsFromFrontmatter(content);
2633
- const canonical = CANONICAL[agent];
2634
- const missing = canonical.filter((t) => !installed.includes(t));
2635
- const extra = installed.filter((t) => !canonical.includes(t));
2636
- const ok3 = missing.length === 0 && extra.length === 0;
2637
- if (!ok3) in_sync = false;
2638
- agents[agent] = { ok: ok3, missing, extra };
2602
+ for (const agent of AGENTS) {
2603
+ const filePath = join18(cwd2, ".claude", "agents", `${agent}.md`);
2604
+ const exists = existsSync15(filePath);
2605
+ if (!exists) in_sync = false;
2606
+ agents[agent] = exists ? { ok: true } : { ok: false, reason: "missing_file" };
2639
2607
  }
2640
2608
  return { in_sync, agents };
2641
2609
  }
@@ -2885,7 +2853,7 @@ var TOOLS = [
2885
2853
  },
2886
2854
  {
2887
2855
  name: "permissions.check",
2888
- description: "Check whether the .claude/agents/*.md tool permission lists are in sync with the current canonical permission constants. Returns per-agent diff with missing and extra tools. Call this at session start to detect outdated agent files after an ahk upgrade.",
2856
+ description: 'Check that a .claude/agents/*.md definition file exists for every role. Returns { in_sync, agents } where each agent is { ok } or { ok: false, reason: "missing_file" }. Agent file CONTENTS are never inspected \u2014 they are meant to be customised freely \u2014 so this never reports drift, only absence. Run `ahk build` to restore a missing file.',
2889
2857
  inputSchema: { type: "object", properties: {}, required: [] }
2890
2858
  },
2891
2859
  {
@@ -3054,19 +3022,19 @@ async function dispatch(name, args, db, docsPath, cwd2, config) {
3054
3022
  return ok2(JSON.stringify(result, null, 2));
3055
3023
  }
3056
3024
  case "deps.snapshot": {
3057
- const pkgPath2 = join18(cwd2, "package.json");
3058
- if (!existsSync15(pkgPath2)) {
3025
+ const pkgPath2 = join19(cwd2, "package.json");
3026
+ if (!existsSync16(pkgPath2)) {
3059
3027
  return ok2("package.json not found in project root", true);
3060
3028
  }
3061
- const pkg2 = JSON.parse(readFileSync10(pkgPath2, "utf8"));
3029
+ const pkg2 = JSON.parse(readFileSync9(pkgPath2, "utf8"));
3062
3030
  const snapshot = {
3063
3031
  capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
3064
3032
  dependencies: pkg2.dependencies ?? {},
3065
3033
  devDependencies: pkg2.devDependencies ?? {}
3066
3034
  };
3067
- const harnessDir = join18(cwd2, ".harness");
3035
+ const harnessDir = join19(cwd2, ".harness");
3068
3036
  mkdirSync9(harnessDir, { recursive: true });
3069
- writeFileSync10(join18(harnessDir, "deps-lock.json"), JSON.stringify(snapshot, null, 2), "utf8");
3037
+ writeFileSync10(join19(harnessDir, "deps-lock.json"), JSON.stringify(snapshot, null, 2), "utf8");
3070
3038
  return ok2(
3071
3039
  JSON.stringify({
3072
3040
  message: "Snapshot saved to .harness/deps-lock.json",
@@ -3075,12 +3043,12 @@ async function dispatch(name, args, db, docsPath, cwd2, config) {
3075
3043
  );
3076
3044
  }
3077
3045
  case "deps.check": {
3078
- const pkgPath2 = join18(cwd2, "package.json");
3079
- const lockPath = join18(cwd2, ".harness", "deps-lock.json");
3080
- if (!existsSync15(pkgPath2)) {
3046
+ const pkgPath2 = join19(cwd2, "package.json");
3047
+ const lockPath = join19(cwd2, ".harness", "deps-lock.json");
3048
+ if (!existsSync16(pkgPath2)) {
3081
3049
  return ok2("package.json not found in project root", true);
3082
3050
  }
3083
- if (!existsSync15(lockPath)) {
3051
+ if (!existsSync16(lockPath)) {
3084
3052
  return ok2(
3085
3053
  JSON.stringify({
3086
3054
  status: "no-snapshot",
@@ -3088,8 +3056,8 @@ async function dispatch(name, args, db, docsPath, cwd2, config) {
3088
3056
  })
3089
3057
  );
3090
3058
  }
3091
- const pkg2 = JSON.parse(readFileSync10(pkgPath2, "utf8"));
3092
- const lock = JSON.parse(readFileSync10(lockPath, "utf8"));
3059
+ const pkg2 = JSON.parse(readFileSync9(pkgPath2, "utf8"));
3060
+ const lock = JSON.parse(readFileSync9(lockPath, "utf8"));
3093
3061
  const current = { ...pkg2.dependencies ?? {}, ...pkg2.devDependencies ?? {} };
3094
3062
  const previous = { ...lock.dependencies ?? {}, ...lock.devDependencies ?? {} };
3095
3063
  const added = [];
@@ -3131,9 +3099,10 @@ async function dispatch(name, args, db, docsPath, cwd2, config) {
3131
3099
  const status = await getDoctorStatus(cwd2);
3132
3100
  const result = {
3133
3101
  lib: { current: status.lib.current, latest: status.lib.latest, outdated: status.lib.outdated },
3102
+ // Agents are existence-checked only — there is no `outdated` bucket.
3103
+ // Hand-edited agent definitions are supported and must not be flagged.
3134
3104
  agents: {
3135
3105
  missing: status.agents.filter((a) => a.status === "missing").map((a) => a.name),
3136
- outdated: status.agents.filter((a) => a.status === "outdated").map((a) => a.name),
3137
3106
  ok: status.agents.filter((a) => a.status === "ok").map((a) => a.name)
3138
3107
  },
3139
3108
  skills: {
@@ -3156,7 +3125,7 @@ function searchDocs(docsPath, query, maxResults = 10) {
3156
3125
  for (const file of files) {
3157
3126
  if (results.length >= maxResults) break;
3158
3127
  try {
3159
- const content = readFileSync10(file, "utf8");
3128
+ const content = readFileSync9(file, "utf8");
3160
3129
  const lines = content.split("\n");
3161
3130
  for (let i = 0; i < lines.length; i++) {
3162
3131
  const lower = lines[i].toLowerCase();
@@ -3181,7 +3150,7 @@ function collectMarkdownFiles(dir) {
3181
3150
  const files = [];
3182
3151
  try {
3183
3152
  for (const entry of readdirSync2(dir)) {
3184
- const full = join18(dir, entry);
3153
+ const full = join19(dir, entry);
3185
3154
  const stat = statSync(full);
3186
3155
  if (stat.isDirectory()) {
3187
3156
  files.push(...collectMarkdownFiles(full));
@@ -3217,14 +3186,8 @@ async function runServe(cwd2, opts) {
3217
3186
  `);
3218
3187
  const syncResult = checkPermissionsSync(cwd2, config);
3219
3188
  if (!syncResult.in_sync && syncResult.agents) {
3220
- const affected = Object.entries(syncResult.agents).filter(([, r]) => !r.ok).map(([name, r]) => {
3221
- const parts = [];
3222
- if (r.missing.length) parts.push(`missing: ${r.missing.map((t) => t.replace("mcp__agent-harness-kit__", "")).join(", ")}`);
3223
- if (r.extra.length) parts.push(`extra: ${r.extra.map((t) => t.replace("mcp__agent-harness-kit__", "")).join(", ")}`);
3224
- return `${name} (${parts.join("; ")})`;
3225
- }).join("\n ");
3226
- process.stderr.write(`[agent-harness-kit] Agent permissions out of sync. Run: ahk build --sync
3227
- ${affected}
3189
+ const affected = Object.entries(syncResult.agents).filter(([, r]) => !r.ok).map(([name]) => name).join(", ");
3190
+ process.stderr.write(`[agent-harness-kit] Agent definition files missing: ${affected}. Run: ahk build
3228
3191
  `);
3229
3192
  }
3230
3193
  await startMcpServer(config, cwd2);
@@ -3232,12 +3195,12 @@ async function runServe(cwd2, opts) {
3232
3195
 
3233
3196
  // src/commands/status.ts
3234
3197
  import Table from "cli-table3";
3235
- import pc11 from "picocolors";
3198
+ import pc12 from "picocolors";
3236
3199
  var STATUS_COLOR = {
3237
- pending: (s) => pc11.dim(s),
3238
- in_progress: (s) => pc11.cyan(s),
3239
- done: (s) => pc11.green(s),
3240
- blocked: (s) => pc11.red(s)
3200
+ pending: (s) => pc12.dim(s),
3201
+ in_progress: (s) => pc12.cyan(s),
3202
+ done: (s) => pc12.green(s),
3203
+ blocked: (s) => pc12.red(s)
3241
3204
  };
3242
3205
  async function runStatus(cwd2, opts) {
3243
3206
  const config = await loadConfig(cwd2);
@@ -3258,11 +3221,11 @@ async function runStatus(cwd2, opts) {
3258
3221
  return;
3259
3222
  }
3260
3223
  if (tasks.length === 0) {
3261
- console.log(pc11.dim("No tasks yet. Run: ahk task add"));
3224
+ console.log(pc12.dim("No tasks yet. Run: ahk task add"));
3262
3225
  return;
3263
3226
  }
3264
3227
  const table = new Table({
3265
- head: ["ID", "Slug", "Title", "Status", "Assigned", "Started"].map((h) => pc11.bold(h)),
3228
+ head: ["ID", "Slug", "Title", "Status", "Assigned", "Started"].map((h) => pc12.bold(h)),
3266
3229
  style: { head: [], border: [] }
3267
3230
  });
3268
3231
  for (const t of tasks) {
@@ -3280,12 +3243,12 @@ async function runStatus(cwd2, opts) {
3280
3243
  const inProgress = tasks.filter((t) => t.status === "in_progress");
3281
3244
  if (inProgress.length > 0) {
3282
3245
  console.log("");
3283
- console.log(pc11.bold("Active actions:"));
3246
+ console.log(pc12.bold("Active actions:"));
3284
3247
  for (const t of inProgress) {
3285
3248
  const actions = await db.getActionsForTask(t.id);
3286
3249
  const active = actions.filter((a) => a.status === "in_progress");
3287
3250
  for (const a of active) {
3288
- console.log(` ${pc11.cyan(a.agent.padEnd(10))} \u2192 task #${t.id} ${t.slug}`);
3251
+ console.log(` ${pc12.cyan(a.agent.padEnd(10))} \u2192 task #${t.id} ${t.slug}`);
3289
3252
  }
3290
3253
  }
3291
3254
  }
@@ -3294,10 +3257,10 @@ async function runStatus(cwd2, opts) {
3294
3257
  const fn = STATUS_COLOR[s.status] ?? ((x) => x);
3295
3258
  return `${fn(s.status)}: ${s.total}`;
3296
3259
  });
3297
- console.log(pc11.dim("Tasks \u2014 ") + parts.join(pc11.dim(" | ")));
3260
+ console.log(pc12.dim("Tasks \u2014 ") + parts.join(pc12.dim(" | ")));
3298
3261
  const archivedTasks = await db.getArchivedTasks();
3299
3262
  if (archivedTasks.length > 0) {
3300
- console.log(pc11.dim(`${archivedTasks.length} archived (use \`ahk task list --archived\` to view)`));
3263
+ console.log(pc12.dim(`${archivedTasks.length} archived (use \`ahk task list --archived\` to view)`));
3301
3264
  }
3302
3265
  } finally {
3303
3266
  await db.close();
@@ -3305,13 +3268,13 @@ async function runStatus(cwd2, opts) {
3305
3268
  }
3306
3269
 
3307
3270
  // src/commands/sync.ts
3308
- import { existsSync as existsSync16, readFileSync as readFileSync11 } from "fs";
3309
- import { join as join19, resolve as resolve10 } from "path";
3310
- import pc12 from "picocolors";
3271
+ import { existsSync as existsSync17, readFileSync as readFileSync10 } from "fs";
3272
+ import { join as join20, resolve as resolve10 } from "path";
3273
+ import pc13 from "picocolors";
3311
3274
  async function runSync(cwd2, opts) {
3312
3275
  const config = await loadConfig(cwd2);
3313
3276
  const direction = opts.direction ?? "both";
3314
- const featureListPath = resolve10(join19(cwd2, config.storage.dir, "feature_list.json"));
3277
+ const featureListPath = resolve10(join20(cwd2, config.storage.dir, "feature_list.json"));
3315
3278
  const db = await openDB(config, cwd2);
3316
3279
  try {
3317
3280
  if (direction === "in" || direction === "both") {
@@ -3325,44 +3288,44 @@ async function runSync(cwd2, opts) {
3325
3288
  }
3326
3289
  }
3327
3290
  async function syncIn(featureListPath, db, dryRun) {
3328
- if (!existsSync16(featureListPath)) {
3329
- console.log(pc12.dim(`feature_list.json not found at ${featureListPath} \u2014 skipping in-sync`));
3291
+ if (!existsSync17(featureListPath)) {
3292
+ console.log(pc13.dim(`feature_list.json not found at ${featureListPath} \u2014 skipping in-sync`));
3330
3293
  return;
3331
3294
  }
3332
3295
  let seeds;
3333
3296
  try {
3334
- seeds = JSON.parse(readFileSync11(featureListPath, "utf8"));
3297
+ seeds = JSON.parse(readFileSync10(featureListPath, "utf8"));
3335
3298
  } catch (err) {
3336
- console.error(pc12.red(`Failed to parse feature_list.json: ${err}`));
3299
+ console.error(pc13.red(`Failed to parse feature_list.json: ${err}`));
3337
3300
  process.exit(1);
3338
3301
  }
3339
3302
  if (dryRun) {
3340
- console.log(pc12.bold("Dry run \u2014 in-sync (feature_list.json \u2192 SQLite):"));
3303
+ console.log(pc13.bold("Dry run \u2014 in-sync (feature_list.json \u2192 SQLite):"));
3341
3304
  for (const t of seeds) {
3342
3305
  const existing = await db.getTaskBySlug(t.slug);
3343
- console.log(` ${existing ? pc12.dim("skip") : pc12.green("add ")} ${t.slug}`);
3306
+ console.log(` ${existing ? pc13.dim("skip") : pc13.green("add ")} ${t.slug}`);
3344
3307
  }
3345
3308
  return;
3346
3309
  }
3347
3310
  const result = await db.syncFromFeatureList(seeds);
3348
- console.log(pc12.green(`\u2713 In-sync: ${result.added} added, ${result.skipped} already existed`));
3311
+ console.log(pc13.green(`\u2713 In-sync: ${result.added} added, ${result.skipped} already existed`));
3349
3312
  }
3350
3313
  async function syncOut(db, cwd2, dryRun) {
3351
3314
  if (dryRun) {
3352
3315
  const tasks = await db.getTasks();
3353
- console.log(pc12.bold("Dry run \u2014 out-sync (SQLite \u2192 feature_list.json):"));
3316
+ console.log(pc13.bold("Dry run \u2014 out-sync (SQLite \u2192 feature_list.json):"));
3354
3317
  console.log(` ${tasks.length} tasks would be written`);
3355
3318
  return;
3356
3319
  }
3357
3320
  await db.writeFeatureList(cwd2);
3358
- console.log(pc12.green("\u2713 Out-sync: feature_list.json updated"));
3321
+ console.log(pc13.green("\u2713 Out-sync: feature_list.json updated"));
3359
3322
  }
3360
3323
 
3361
3324
  // src/commands/task/add.ts
3362
3325
  import * as p6 from "@clack/prompts";
3363
- import pc13 from "picocolors";
3326
+ import pc14 from "picocolors";
3364
3327
  async function runTaskAdd(cwd2) {
3365
- p6.intro(pc13.bold("agent-harness-kit \u2014 add task"));
3328
+ p6.intro(pc14.bold("agent-harness-kit \u2014 add task"));
3366
3329
  const title = await cliFormWithRetry(
3367
3330
  async () => {
3368
3331
  const val = await p6.text({ message: "Task title" });
@@ -3405,10 +3368,10 @@ async function runTaskAdd(cwd2) {
3405
3368
  await db.writeFeatureList(cwd2);
3406
3369
  await db.close();
3407
3370
  spinner6.stop("");
3408
- console.log(pc13.green(`\u2713 Task #${task2.id} added \u2014 ${task2.slug} (pending)`));
3409
- console.log(pc13.cyan("\u2192") + " " + pc13.cyan("ahk status") + " to see all tasks");
3371
+ console.log(pc14.green(`\u2713 Task #${task2.id} added \u2014 ${task2.slug} (pending)`));
3372
+ console.log(pc14.cyan("\u2192") + " " + pc14.cyan("ahk status") + " to see all tasks");
3410
3373
  } catch (err) {
3411
- spinner6.stop(pc13.red("Failed"));
3374
+ spinner6.stop(pc14.red("Failed"));
3412
3375
  p6.log.error(err instanceof Error ? err.message : String(err));
3413
3376
  process.exit(1);
3414
3377
  }
@@ -3416,17 +3379,17 @@ async function runTaskAdd(cwd2) {
3416
3379
 
3417
3380
  // src/commands/task/done.ts
3418
3381
  import { spawnSync as spawnSync2 } from "child_process";
3419
- import { existsSync as existsSync17 } from "fs";
3382
+ import { existsSync as existsSync18 } from "fs";
3420
3383
  import { resolve as resolve11 } from "path";
3421
- import pc14 from "picocolors";
3384
+ import pc15 from "picocolors";
3422
3385
  async function runTaskDone(cwd2, idOrSlug) {
3423
3386
  const config = await loadConfig(cwd2);
3424
3387
  if (config.health.required) {
3425
3388
  const scriptPath = resolve11(cwd2, config.health.scriptPath);
3426
- if (existsSync17(scriptPath)) {
3389
+ if (existsSync18(scriptPath)) {
3427
3390
  const result = spawnSync2("bash", [scriptPath], { cwd: cwd2, stdio: "pipe", encoding: "utf8" });
3428
3391
  if (result.status !== 0) {
3429
- console.error(pc14.red("\u2717 Health check failed \u2014 cannot mark task as done."));
3392
+ console.error(pc15.red("\u2717 Health check failed \u2014 cannot mark task as done."));
3430
3393
  if (result.stdout) console.error(result.stdout);
3431
3394
  if (result.stderr) console.error(result.stderr);
3432
3395
  process.exit(1);
@@ -3439,16 +3402,16 @@ async function runTaskDone(cwd2, idOrSlug) {
3439
3402
  const isId = !isNaN(parsed);
3440
3403
  const task2 = isId ? await db.getTaskById(parsed) : await db.getTaskBySlug(idOrSlug);
3441
3404
  if (!task2) {
3442
- console.error(pc14.red(`Task not found: ${idOrSlug}`));
3405
+ console.error(pc15.red(`Task not found: ${idOrSlug}`));
3443
3406
  process.exit(1);
3444
3407
  }
3445
3408
  if (task2.status === "done") {
3446
- console.log(pc14.dim(`Task #${task2.id} is already done.`));
3409
+ console.log(pc15.dim(`Task #${task2.id} is already done.`));
3447
3410
  return;
3448
3411
  }
3449
3412
  await db.updateTaskStatus(task2.id, "done");
3450
3413
  await db.writeFeatureList(cwd2);
3451
- console.log(pc14.green(`\u2713 Task #${task2.id} \u2014 ${task2.slug} marked as done`));
3414
+ console.log(pc15.green(`\u2713 Task #${task2.id} \u2014 ${task2.slug} marked as done`));
3452
3415
  } finally {
3453
3416
  await db.close();
3454
3417
  }
@@ -3456,9 +3419,9 @@ async function runTaskDone(cwd2, idOrSlug) {
3456
3419
 
3457
3420
  // src/commands/task/edit.ts
3458
3421
  import * as p7 from "@clack/prompts";
3459
- import pc15 from "picocolors";
3422
+ import pc16 from "picocolors";
3460
3423
  async function runTaskEdit(cwd2) {
3461
- p7.intro(pc15.bold("agent-harness-kit \u2014 edit task"));
3424
+ p7.intro(pc16.bold("agent-harness-kit \u2014 edit task"));
3462
3425
  const config = await loadConfig(cwd2);
3463
3426
  const db = await openDB(config, cwd2);
3464
3427
  try {
@@ -3536,9 +3499,9 @@ async function runTaskEdit(cwd2) {
3536
3499
  await db.updateTaskAcceptance(task2.id, newAcceptance);
3537
3500
  await db.writeFeatureList(cwd2);
3538
3501
  spinner6.stop("");
3539
- console.log(pc15.green(`\u2713 Task #${task2.id} updated \u2014 ${newSlug}`));
3502
+ console.log(pc16.green(`\u2713 Task #${task2.id} updated \u2014 ${newSlug}`));
3540
3503
  } catch (err) {
3541
- spinner6.stop(pc15.red("Failed"));
3504
+ spinner6.stop(pc16.red("Failed"));
3542
3505
  p7.log.error(err instanceof Error ? err.message : String(err));
3543
3506
  process.exit(1);
3544
3507
  }
@@ -3549,12 +3512,12 @@ async function runTaskEdit(cwd2) {
3549
3512
 
3550
3513
  // src/commands/task/list.ts
3551
3514
  import Table2 from "cli-table3";
3552
- import pc16 from "picocolors";
3515
+ import pc17 from "picocolors";
3553
3516
  var STATUS_COLOR2 = {
3554
- pending: (s) => pc16.dim(s),
3555
- in_progress: (s) => pc16.cyan(s),
3556
- done: (s) => pc16.green(s),
3557
- blocked: (s) => pc16.red(s)
3517
+ pending: (s) => pc17.dim(s),
3518
+ in_progress: (s) => pc17.cyan(s),
3519
+ done: (s) => pc17.green(s),
3520
+ blocked: (s) => pc17.red(s)
3558
3521
  };
3559
3522
  async function runTaskList(cwd2, opts) {
3560
3523
  const config = await loadConfig(cwd2);
@@ -3571,11 +3534,11 @@ async function runTaskList(cwd2, opts) {
3571
3534
  let msg = "No tasks";
3572
3535
  if (filterStatus) msg += ` with status: ${filterStatus}`;
3573
3536
  if (opts.archived) msg += " (archived)";
3574
- console.log(pc16.dim(msg + "."));
3537
+ console.log(pc17.dim(msg + "."));
3575
3538
  return;
3576
3539
  }
3577
3540
  const table = new Table2({
3578
- head: ["ID", "Slug", "Title", "Status"].map((h) => pc16.bold(h)),
3541
+ head: ["ID", "Slug", "Title", "Status"].map((h) => pc17.bold(h)),
3579
3542
  style: { head: [], border: [] }
3580
3543
  });
3581
3544
  for (const t of tasks) {
@@ -3586,7 +3549,7 @@ async function runTaskList(cwd2, opts) {
3586
3549
  if (!opts.archived && !opts.includeArchived) {
3587
3550
  const archivedTasks = await db.getArchivedTasks();
3588
3551
  if (archivedTasks.length > 0) {
3589
- console.log(pc16.dim(`${archivedTasks.length} archived task${archivedTasks.length !== 1 ? "s" : ""} (use --archived to view)`));
3552
+ console.log(pc17.dim(`${archivedTasks.length} archived task${archivedTasks.length !== 1 ? "s" : ""} (use --archived to view)`));
3590
3553
  }
3591
3554
  }
3592
3555
  } finally {
@@ -3594,43 +3557,6 @@ async function runTaskList(cwd2, opts) {
3594
3557
  }
3595
3558
  }
3596
3559
 
3597
- // src/core/local-install-guard.ts
3598
- import { existsSync as existsSync18, readFileSync as readFileSync12 } from "fs";
3599
- import { join as join20 } from "path";
3600
- import pc17 from "picocolors";
3601
- function isLocalInstallSatisfied(cwd2) {
3602
- const selfPkgPath = join20(cwd2, "package.json");
3603
- let projectPkg = null;
3604
- if (existsSync18(selfPkgPath)) {
3605
- try {
3606
- const selfPkg = JSON.parse(readFileSync12(selfPkgPath, "utf8"));
3607
- if (selfPkg?.name === pkg.name) return true;
3608
- projectPkg = selfPkg;
3609
- } catch {
3610
- }
3611
- }
3612
- const [scope, name] = pkg.name.split("/");
3613
- const localPath = pkg.name.startsWith("@") ? join20(cwd2, "node_modules", scope, name) : join20(cwd2, "node_modules", pkg.name);
3614
- if (existsSync18(localPath)) return true;
3615
- const isPnp = existsSync18(join20(cwd2, ".pnp.cjs")) || existsSync18(join20(cwd2, ".pnp.loader.mjs"));
3616
- if (isPnp && projectPkg) {
3617
- const deps = {
3618
- ...projectPkg.dependencies ?? {},
3619
- ...projectPkg.devDependencies ?? {}
3620
- };
3621
- if (Object.prototype.hasOwnProperty.call(deps, pkg.name)) return true;
3622
- }
3623
- return false;
3624
- }
3625
- function printLocalInstallWarning() {
3626
- console.error(pc17.yellow(`\u26A0 ${pkg.name} is not installed locally in this project.`));
3627
- console.error(pc17.dim(" This is only a recommendation for reproducibility: pinning a local"));
3628
- console.error(pc17.dim(" version keeps behavior consistent across your team and CI, instead of"));
3629
- console.error(pc17.dim(" drifting with whatever version is installed globally on each machine."));
3630
- console.error(pc17.dim(` Run: npm install --save-dev ${pkg.name}`));
3631
- console.error(pc17.dim(" (or the equivalent for your package manager: pnpm add -D, yarn add --dev, bun add -d)"));
3632
- }
3633
-
3634
3560
  // src/core/update-check.ts
3635
3561
  import pc18 from "picocolors";
3636
3562
  var REGISTRY_URL2 = `https://registry.npmjs.org/${pkg.name}/latest`;
@@ -3672,7 +3598,10 @@ program.name("ahk").description("agent-harness-kit \u2014 CLI scaffolding for mu
3672
3598
  program.command("init").description("Scaffold a harness interactively in the current directory").option("--name <name>", "Project name (skip prompt)").option("--provider <provider>", "AI provider: claude-code | opencode (skip prompt)").option("--docs <path>", "Docs folder path (skip prompt)").option("--tasks <adapter>", "Task adapter: local | jira | linear (skip prompt)").option("--storage-scope <scope>", "Storage scope: local | global (skip prompt)").action(async (opts) => {
3673
3599
  await runInit(cwd, opts);
3674
3600
  });
3675
- program.command("build").description("Regenerate AGENTS.md and provider files from agent-harness-kit.config.ts").option("--watch", "Rebuild on config changes").option("--sync", "Sync tools: frontmatter in existing .claude/agents/*.md to match current permission constants").action(async (opts) => {
3601
+ program.command("build").description("Regenerate AGENTS.md and provider files from agent-harness-kit.config.ts").option("--watch", "Rebuild on config changes").option("--sync", "Sync tools: frontmatter in existing .claude/agents/*.md to match current permission constants").option(
3602
+ "--force",
3603
+ "Regenerate agent files that already exist, DESTROYING your customizations (a backup is written first). Without this flag, build only creates agent files that are missing and never modifies existing ones."
3604
+ ).action(async (opts) => {
3676
3605
  await runBuild(cwd, opts);
3677
3606
  });
3678
3607
  program.command("health").description("Run health.sh and report result").action(async () => {