@cardor/agent-harness-kit 1.10.4 → 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";
@@ -1736,8 +1759,7 @@ var taskTitleSchema = v2.pipe(
1736
1759
  );
1737
1760
  var taskDescriptionSchema = v2.pipe(
1738
1761
  v2.string(),
1739
- v2.nonEmpty("Description is required"),
1740
- v2.maxLength(1e3, "Description must be at most 1000 characters")
1762
+ v2.nonEmpty("Description is required")
1741
1763
  );
1742
1764
 
1743
1765
  // src/utils/form.ts
@@ -1756,13 +1778,52 @@ var cliFormWithRetry = async (formFn, schema) => {
1756
1778
 
1757
1779
  // src/commands/init-helpers.ts
1758
1780
  import { randomUUID } from "crypto";
1759
- 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";
1760
1787
  import { join as join13 } from "path";
1761
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
1762
1823
  function readProjectNameFromPackageJson(cwd2) {
1763
1824
  try {
1764
- const pkgPath2 = join13(cwd2, "package.json");
1765
- if (!existsSync11(pkgPath2)) return null;
1825
+ const pkgPath2 = join14(cwd2, "package.json");
1826
+ if (!existsSync12(pkgPath2)) return null;
1766
1827
  const content = readFileSync8(pkgPath2, "utf8");
1767
1828
  const pkg2 = JSON.parse(content);
1768
1829
  const name = pkg2?.name;
@@ -1773,10 +1834,11 @@ function readProjectNameFromPackageJson(cwd2) {
1773
1834
  }
1774
1835
  }
1775
1836
  function detectConfigExtension(cwd2) {
1837
+ if (!isLocalInstallSatisfied(cwd2)) return "json";
1776
1838
  try {
1777
- if (existsSync11(join13(cwd2, "tsconfig.json"))) return "ts";
1778
- const pkgPath2 = join13(cwd2, "package.json");
1779
- 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";
1780
1842
  const pkg2 = JSON.parse(readFileSync8(pkgPath2, "utf8"));
1781
1843
  if (pkg2?.type === "module") return "mjs";
1782
1844
  } catch {
@@ -1784,7 +1846,6 @@ function detectConfigExtension(cwd2) {
1784
1846
  return "mjs";
1785
1847
  }
1786
1848
  function applyConfigDefaults(params) {
1787
- const models = params.models ?? {};
1788
1849
  const scope = params.scope ?? "local";
1789
1850
  const projectId = params.projectId ?? randomUUID();
1790
1851
  const baseStorage = {
@@ -1807,22 +1868,10 @@ function applyConfigDefaults(params) {
1807
1868
  docsPath: params.docsPath,
1808
1869
  agentsMd: "./AGENTS.md"
1809
1870
  },
1810
- agents: {
1811
- lead: { instructionsPath: null, ...models.lead && { model: models.lead } },
1812
- explorer: {
1813
- instructionsPath: null,
1814
- allowedPaths: [params.docsPath, "./src"],
1815
- ...models.explorer && { model: models.explorer }
1816
- },
1817
- builder: {
1818
- instructionsPath: null,
1819
- writablePaths: ["./src", "./tests"],
1820
- ...models.builder && { model: models.builder }
1821
- },
1822
- reviewer: { instructionsPath: null, ...models.reviewer && { model: models.reviewer } },
1823
- ...models.consultant && { consultant: { instructionsPath: null, model: models.consultant } },
1824
- custom: []
1825
- },
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.
1826
1875
  database: { type: "sqlite" },
1827
1876
  storage,
1828
1877
  health: {
@@ -1841,27 +1890,27 @@ function stripAnsi(str2) {
1841
1890
  function drawBox(lines) {
1842
1891
  const width = Math.max(...lines.map((l) => stripAnsi(l).length));
1843
1892
  const border = "\u2500".repeat(width);
1844
- console.log(pc6.yellow(`\u250C${border}\u2510`));
1893
+ console.log(pc7.yellow(`\u250C${border}\u2510`));
1845
1894
  for (const line of lines) {
1846
1895
  const pad = width - stripAnsi(line).length;
1847
1896
  const padStr = pad > 0 ? " ".repeat(pad) : "";
1848
- console.log(pc6.yellow("\u2502") + line + padStr + pc6.yellow("\u2502"));
1897
+ console.log(pc7.yellow("\u2502") + line + padStr + pc7.yellow("\u2502"));
1849
1898
  }
1850
- console.log(pc6.yellow(`\u2514${border}\u2518`));
1899
+ console.log(pc7.yellow(`\u2514${border}\u2518`));
1851
1900
  }
1852
1901
  function printWelcomeMessage(projectName) {
1853
1902
  const sep = "\u2500".repeat(38);
1854
1903
  const lines = [
1855
- ` ${pc6.bold(pc6.white("agent-harness-kit"))} `,
1856
- ` ${pc6.gray("\u2014")} harness scaffolding ${pc6.gray("\u2014")} `,
1857
- ` ${pc6.gray(sep)} `,
1858
- ` ${pc6.bold("Project:")} ${projectName || "\u2014"} `,
1859
- ` ${pc6.bold("Status:")} ${pc6.green("ready to configure")} `,
1860
- ` ${pc6.gray(sep)} `,
1861
- ` ${pc6.gray("Next steps:")} `,
1862
- ` ${pc6.gray("\u2192")} ${pc6.gray("Set up your AI provider config")} `,
1863
- ` ${pc6.gray("\u2192")} ${pc6.gray("Run your health check to verify")} `,
1864
- ` ${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")} `
1865
1914
  ];
1866
1915
  console.log();
1867
1916
  drawBox(lines);
@@ -1873,18 +1922,18 @@ async function runInit(cwd2, flags) {
1873
1922
  const existingConfig = findConfigFile(cwd2);
1874
1923
  if (existingConfig) {
1875
1924
  console.log(
1876
- pc7.yellow("\u26A0") + " " + pc7.bold("Project already initialized.") + pc7.dim(` (${existingConfig})`)
1925
+ pc8.yellow("\u26A0") + " " + pc8.bold("Project already initialized.") + pc8.dim(` (${existingConfig})`)
1877
1926
  );
1878
1927
  console.log();
1879
- console.log(pc7.dim("Suggested next steps:"));
1928
+ console.log(pc8.dim("Suggested next steps:"));
1880
1929
  console.log(
1881
- " " + 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")
1882
1931
  );
1883
- 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"));
1884
1933
  console.log(
1885
- " " + 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")
1886
1935
  );
1887
- 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"));
1888
1937
  process.exit(0);
1889
1938
  }
1890
1939
  const detectedName = flags.name ?? readProjectNameFromPackageJson(cwd2);
@@ -1936,61 +1985,6 @@ async function runInit(cwd2, flags) {
1936
1985
  }
1937
1986
  provider = val;
1938
1987
  }
1939
- const AGENT_LABELS = [
1940
- { key: "lead", label: "Lead" },
1941
- { key: "explorer", label: "Explorer" },
1942
- { key: "consultant", label: "Consultant" },
1943
- { key: "builder", label: "Builder" },
1944
- { key: "reviewer", label: "Reviewer" }
1945
- ];
1946
- const modelOverrides = {};
1947
- if (provider === "claude-code" || provider === "codex-cli") {
1948
- const wantsModelCustomization = await p3.confirm({
1949
- message: "Customize the model per agent?",
1950
- initialValue: false
1951
- });
1952
- if (p3.isCancel(wantsModelCustomization)) {
1953
- p3.cancel("Cancelled.");
1954
- process.exit(0);
1955
- }
1956
- if (wantsModelCustomization) {
1957
- if (provider === "claude-code") {
1958
- for (const agent of AGENT_LABELS) {
1959
- const val = await p3.select({
1960
- message: `Model for ${agent.label}`,
1961
- options: [
1962
- { value: "inherit", label: "inherit (default)" },
1963
- { value: "haiku", label: "haiku" },
1964
- { value: "sonnet", label: "sonnet" },
1965
- { value: "opus", label: "opus" },
1966
- { value: "fable", label: "fable" }
1967
- ],
1968
- initialValue: "inherit"
1969
- });
1970
- if (p3.isCancel(val)) {
1971
- p3.cancel("Cancelled.");
1972
- process.exit(0);
1973
- }
1974
- modelOverrides[agent.key] = val;
1975
- }
1976
- } else {
1977
- for (const agent of AGENT_LABELS) {
1978
- const val = await p3.text({
1979
- message: `Model for ${agent.label} (Codex does not validate this value)`,
1980
- placeholder: "e.g. gpt-5 (empty or <3 chars = no override)"
1981
- });
1982
- if (p3.isCancel(val)) {
1983
- p3.cancel("Cancelled.");
1984
- process.exit(0);
1985
- }
1986
- const trimmed = val.trim();
1987
- if (trimmed.length >= 3) {
1988
- modelOverrides[agent.key] = trimmed;
1989
- }
1990
- }
1991
- }
1992
- }
1993
- }
1994
1988
  let docsPath;
1995
1989
  if (flags.docs) {
1996
1990
  docsPath = flags.docs;
@@ -2091,14 +2085,13 @@ async function runInit(cwd2, flags) {
2091
2085
  provider,
2092
2086
  docsPath,
2093
2087
  tasksAdapter,
2094
- models: modelOverrides,
2095
2088
  scope: storageScope
2096
2089
  });
2097
2090
  const materializer = getMaterializer(provider);
2098
2091
  const installDir = cwd2;
2099
2092
  configExt = detectConfigExtension(cwd2);
2100
2093
  const configFileName = `agent-harness-kit.config.${configExt}`;
2101
- const templateFn = configExt === "ts" ? configTs : configExt === "mjs" ? configMjs : configCjs;
2094
+ const templateFn = configExt === "json" ? configJson : configExt === "ts" ? configTs : configExt === "mjs" ? configMjs : configCjs;
2102
2095
  const configContent = templateFn({
2103
2096
  name,
2104
2097
  description,
@@ -2106,12 +2099,11 @@ async function runInit(cwd2, flags) {
2106
2099
  docsPath,
2107
2100
  tasksAdapter,
2108
2101
  port: config.tools.mcp.port,
2109
- models: modelOverrides,
2110
2102
  scope: config.storage.scope,
2111
2103
  projectId: config.storage.projectId
2112
2104
  });
2113
- writeFileSync8(join14(installDir, configFileName), configContent, "utf8");
2114
- mkdirSync7(join14(installDir, config.storage.dir), { recursive: true });
2105
+ writeFileSync8(join15(installDir, configFileName), configContent, "utf8");
2106
+ mkdirSync7(join15(installDir, config.storage.dir), { recursive: true });
2115
2107
  const db = await openDB(config, installDir);
2116
2108
  await db.writeStorageState(installDir);
2117
2109
  await materializer.scaffold(config, { cwd: installDir, firstTask });
@@ -2131,39 +2123,39 @@ async function runInit(cwd2, flags) {
2131
2123
  p3.log.error(err instanceof Error ? err.message : String(err));
2132
2124
  throw err;
2133
2125
  }
2134
- console.log(pc7.green("\u2713 Scaffolded harness in current directory"));
2126
+ console.log(pc8.green("\u2713 Scaffolded harness in current directory"));
2135
2127
  const agentsDir = provider === "claude-code" ? ".claude/agents/" : ".opencode/agents/";
2136
2128
  const mcpFile = provider === "claude-code" ? ".claude/mcp.json" : "./opencode.json";
2137
2129
  console.log("");
2138
- console.log(pc7.green(`\u2713 agent-harness-kit.config.${configExt}`));
2139
- console.log(pc7.green("\u2713 AGENTS.md"));
2140
- 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"));
2141
2133
  console.log(
2142
- pc7.green(
2134
+ pc8.green(
2143
2135
  storageScope === "global" ? "\u2713 ~/.harness/dbs/<projectId>/harness.db" : "\u2713 .harness/harness.db"
2144
2136
  )
2145
2137
  );
2146
2138
  console.log(
2147
- pc7.green(
2139
+ pc8.green(
2148
2140
  storageScope === "global" ? "\u2713 ~/.harness/dbs/<projectId>/current.md" : "\u2713 .harness/current.md"
2149
2141
  )
2150
2142
  );
2151
- console.log(pc7.green("\u2713 .harness/storage-state.json"));
2152
- console.log(pc7.green(`\u2713 ${agentsDir}lead.md`));
2153
- console.log(pc7.green(`\u2713 ${agentsDir}explorer.md`));
2154
- console.log(pc7.green(`\u2713 ${agentsDir}builder.md`));
2155
- console.log(pc7.green(`\u2713 ${agentsDir}reviewer.md`));
2156
- console.log(pc7.green(`\u2713 ${mcpFile}`));
2157
- 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"));
2158
2150
  console.log("");
2159
- console.log(pc7.cyan("\u2192") + ` Edit ${pc7.cyan("health.sh")} with your project checks`);
2160
- 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`);
2161
2153
  console.log(
2162
- 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")}`
2163
2155
  );
2164
2156
  const recommendations = [
2165
2157
  ` Give a try to Heimdall MCP: Transparent proxy that traces every MCP tool call with OpenTelemetry. `,
2166
- ` Learn more: ${pc7.cyan("https://github.com/enmanuelmag/heimdall-mcp")} `
2158
+ ` Learn more: ${pc8.cyan("https://github.com/enmanuelmag/heimdall-mcp")} `
2167
2159
  ];
2168
2160
  console.log("");
2169
2161
  drawBox(recommendations);
@@ -2171,7 +2163,7 @@ async function runInit(cwd2, flags) {
2171
2163
 
2172
2164
  // src/commands/migrate.ts
2173
2165
  import * as p4 from "@clack/prompts";
2174
- import pc8 from "picocolors";
2166
+ import pc9 from "picocolors";
2175
2167
  async function runMigrate(cwd2, opts) {
2176
2168
  const config = await loadConfig(cwd2);
2177
2169
  let target;
@@ -2193,7 +2185,7 @@ async function runMigrate(cwd2, opts) {
2193
2185
  target = val;
2194
2186
  }
2195
2187
  if (target === config.provider) {
2196
- console.log(pc8.dim(`Already on ${target} \u2014 nothing to migrate.`));
2188
+ console.log(pc9.dim(`Already on ${target} \u2014 nothing to migrate.`));
2197
2189
  return;
2198
2190
  }
2199
2191
  const spinner6 = p4.spinner();
@@ -2201,21 +2193,21 @@ async function runMigrate(cwd2, opts) {
2201
2193
  try {
2202
2194
  const targetMaterializer = getMaterializer(target);
2203
2195
  await targetMaterializer.build(config, cwd2);
2204
- spinner6.stop(pc8.green(`Migrated to ${target}`));
2196
+ spinner6.stop(pc9.green(`Migrated to ${target}`));
2205
2197
  p4.log.warn(`Update agent-harness-kit.config.ts: set provider: '${target}'`);
2206
2198
  p4.log.warn(`Then run: ahk build`);
2207
2199
  } catch (err) {
2208
- spinner6.stop(pc8.red("Migration failed"));
2200
+ spinner6.stop(pc9.red("Migration failed"));
2209
2201
  p4.log.error(err instanceof Error ? err.message : String(err));
2210
2202
  process.exit(1);
2211
2203
  }
2212
2204
  }
2213
2205
 
2214
2206
  // src/commands/migrate-storage.ts
2215
- 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";
2216
2208
  import { homedir as homedir3 } from "os";
2217
- import { dirname as dirname7, join as join15, resolve as resolve7 } from "path";
2218
- import pc9 from "picocolors";
2209
+ import { dirname as dirname7, join as join16, resolve as resolve7 } from "path";
2210
+ import pc10 from "picocolors";
2219
2211
  function log5(msg) {
2220
2212
  console.log(msg);
2221
2213
  }
@@ -2226,14 +2218,14 @@ function defaultMarkdownPathForConfig(config) {
2226
2218
  return config.storage.scope === "local" ? config.storage.markdownFallback.path : DEFAULT_MARKDOWN_PATH;
2227
2219
  }
2228
2220
  function currentMdPathForScope(scope, config, cwd2, homeDir) {
2229
- 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));
2230
2222
  }
2231
2223
  function defaultSqlitePathForConfig(config) {
2232
2224
  return config.storage.scope === "local" && config.database.type === "sqlite" ? config.storage.sqlitePath ?? DEFAULT_SQLITE_PATH : DEFAULT_SQLITE_PATH;
2233
2225
  }
2234
2226
  async function backupDestination(cwd2, storageDir, data) {
2235
2227
  const backupsDir = resolve7(cwd2, storageDir, "backups");
2236
- 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`);
2237
2229
  try {
2238
2230
  mkdirSync8(backupsDir, { recursive: true });
2239
2231
  writeFileSync9(path, JSON.stringify(data, null, 2) + "\n", "utf8");
@@ -2248,11 +2240,11 @@ function copySqliteFile(srcPath, destPath) {
2248
2240
  mkdirSync8(dirname7(destPath), { recursive: true });
2249
2241
  copyFileSync(srcPath, destPath);
2250
2242
  for (const suffix of ["-wal", "-shm"]) {
2251
- if (existsSync12(`${srcPath}${suffix}`)) {
2243
+ if (existsSync13(`${srcPath}${suffix}`)) {
2252
2244
  copyFileSync(`${srcPath}${suffix}`, `${destPath}${suffix}`);
2253
2245
  }
2254
2246
  }
2255
- if (!existsSync12(destPath)) {
2247
+ if (!existsSync13(destPath)) {
2256
2248
  throw new Error(`Copy verification failed: ${destPath} does not exist after copy.`);
2257
2249
  }
2258
2250
  }
@@ -2280,13 +2272,13 @@ async function runMigrateStorage(cwd2, opts, homeDir = homedir3()) {
2280
2272
  } finally {
2281
2273
  await db.close();
2282
2274
  }
2283
- 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."));
2284
2276
  return;
2285
2277
  }
2286
2278
  realScope = localCount > 0 ? "local" : "global";
2287
2279
  realDbType = "sqlite";
2288
2280
  log5(
2289
- pc9.yellow(
2281
+ pc10.yellow(
2290
2282
  `storage-state.json was missing. Detected real data at ${realScope} sqlite location (${realScope === "local" ? localPath : globalPath}) \u2014 using it as the migration source.`
2291
2283
  )
2292
2284
  );
@@ -2297,7 +2289,7 @@ async function runMigrateStorage(cwd2, opts, homeDir = homedir3()) {
2297
2289
  const desiredScope = config.storage.scope;
2298
2290
  const desiredDbType = config.database.type;
2299
2291
  if (realScope === desiredScope && realDbType === desiredDbType) {
2300
- 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.`));
2301
2293
  return;
2302
2294
  }
2303
2295
  if (realDbType !== "sqlite") {
@@ -2311,7 +2303,7 @@ async function runMigrateStorage(cwd2, opts, homeDir = homedir3()) {
2311
2303
  return migrateAcrossDbType(cwd2, config, homeDir, realScope, opts);
2312
2304
  }
2313
2305
  async function probeTaskCount(dbPath) {
2314
- if (!existsSync12(dbPath)) return 0;
2306
+ if (!existsSync13(dbPath)) return 0;
2315
2307
  const { SQLiteDriver } = await import("./sqlite-TR4D324R.js");
2316
2308
  const driver = new SQLiteDriver(dbPath);
2317
2309
  try {
@@ -2328,10 +2320,10 @@ async function migrateScopeOnly(cwd2, config, homeDir, fromScope, toScope, opts)
2328
2320
  const destDb = resolveSqlitePathForScope(toScope, sqlitePath, cwd2, config, homeDir);
2329
2321
  const srcMd = currentMdPathForScope(fromScope, config, cwd2, homeDir);
2330
2322
  const destMd = currentMdPathForScope(toScope, config, cwd2, homeDir);
2331
- if (!existsSync12(srcDb)) {
2323
+ if (!existsSync13(srcDb)) {
2332
2324
  fail(`Source database not found at ${srcDb} (expected ${fromScope} scope) \u2014 nothing to move.`);
2333
2325
  }
2334
- const destExists = existsSync12(destDb);
2326
+ const destExists = existsSync13(destDb);
2335
2327
  if (destExists) {
2336
2328
  const { SQLiteDriver } = await import("./sqlite-TR4D324R.js");
2337
2329
  const destDriver = new SQLiteDriver(destDb);
@@ -2360,36 +2352,36 @@ async function migrateScopeOnly(cwd2, config, homeDir, fromScope, toScope, opts)
2360
2352
  await backupDriver.close();
2361
2353
  }
2362
2354
  const backupPath = await backupDestination(cwd2, config.storage.dir, data);
2363
- log5(pc9.yellow(` Backed up existing destination data \u2192 ${backupPath}`));
2355
+ log5(pc10.yellow(` Backed up existing destination data \u2192 ${backupPath}`));
2364
2356
  }
2365
2357
  }
2366
2358
  if (opts.dryRun) {
2367
- 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.`));
2368
2360
  return;
2369
2361
  }
2370
2362
  copySqliteFile(srcDb, destDb);
2371
- log5(pc9.green(`\u2713 Copied database ${srcDb} \u2192 ${destDb}`));
2372
- if (existsSync12(srcMd)) {
2363
+ log5(pc10.green(`\u2713 Copied database ${srcDb} \u2192 ${destDb}`));
2364
+ if (existsSync13(srcMd)) {
2373
2365
  mkdirSync8(dirname7(destMd), { recursive: true });
2374
2366
  copyFileSync(srcMd, destMd);
2375
- log5(pc9.green(`\u2713 Copied current.md ${srcMd} \u2192 ${destMd}`));
2367
+ log5(pc10.green(`\u2713 Copied current.md ${srcMd} \u2192 ${destMd}`));
2376
2368
  }
2377
2369
  rmSync(srcDb, { force: true });
2378
2370
  rmSync(`${srcDb}-wal`, { force: true });
2379
2371
  rmSync(`${srcDb}-shm`, { force: true });
2380
- if (existsSync12(srcMd) && srcMd !== destMd) rmSync(srcMd, { force: true });
2372
+ if (existsSync13(srcMd) && srcMd !== destMd) rmSync(srcMd, { force: true });
2381
2373
  const db = await openDB(config, cwd2, homeDir);
2382
2374
  try {
2383
2375
  await db.writeStorageState(cwd2);
2384
2376
  } finally {
2385
2377
  await db.close();
2386
2378
  }
2387
- log5(pc9.green(`\u2713 Storage migrated: scope ${fromScope} \u2192 ${toScope}`));
2379
+ log5(pc10.green(`\u2713 Storage migrated: scope ${fromScope} \u2192 ${toScope}`));
2388
2380
  }
2389
2381
  async function migrateAcrossDbType(cwd2, config, homeDir, sourceScope, opts) {
2390
2382
  const sqlitePath = defaultSqlitePathForConfig(config);
2391
2383
  const srcPath = resolveSqlitePathForScope(sourceScope, sqlitePath, cwd2, config, homeDir);
2392
- if (!existsSync12(srcPath)) {
2384
+ if (!existsSync13(srcPath)) {
2393
2385
  fail(`Source sqlite database not found at ${srcPath} (expected ${sourceScope} scope) \u2014 nothing to migrate.`);
2394
2386
  }
2395
2387
  const { SQLiteDriver } = await import("./sqlite-TR4D324R.js");
@@ -2425,7 +2417,7 @@ async function migrateAcrossDbType(cwd2, config, homeDir, sourceScope, opts) {
2425
2417
  }
2426
2418
  if (opts.dryRun) {
2427
2419
  log5(
2428
- pc9.dim(
2420
+ pc10.dim(
2429
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)"}.`
2430
2422
  )
2431
2423
  );
@@ -2435,34 +2427,34 @@ async function migrateAcrossDbType(cwd2, config, homeDir, sourceScope, opts) {
2435
2427
  if (!destEmpty && opts.force) {
2436
2428
  const currentDestData = await destDb.exportJson();
2437
2429
  backupPath = await backupDestination(cwd2, config.storage.dir, currentDestData);
2438
- log5(pc9.yellow(` Backed up existing destination data \u2192 ${backupPath}`));
2430
+ log5(pc10.yellow(` Backed up existing destination data \u2192 ${backupPath}`));
2439
2431
  }
2440
2432
  await destDb.importFullExport(sourceData, config.database.type, { truncateFirst: !destEmpty });
2441
2433
  await destDb.writeStorageState(cwd2);
2442
2434
  log5(
2443
- pc9.green(
2435
+ pc10.green(
2444
2436
  `\u2713 Migrated ${sourceData.tasks.length} task(s), ${sourceData.actions.length} action(s) from sqlite (${sourceScope}) \u2192 ${config.database.type}.`
2445
2437
  )
2446
2438
  );
2447
- if (backupPath) log5(pc9.dim(` Destination backup: ${backupPath}`));
2448
- 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.`));
2449
2441
  } finally {
2450
2442
  await destDb.close();
2451
2443
  }
2452
2444
  }
2453
2445
 
2454
2446
  // src/commands/reset.ts
2455
- import { existsSync as existsSync13, readdirSync, rmSync as rmSync2 } from "fs";
2447
+ import { existsSync as existsSync14, readdirSync, rmSync as rmSync2 } from "fs";
2456
2448
  import { homedir as homedir4 } from "os";
2457
- import { join as join16, resolve as resolve8 } from "path";
2449
+ import { join as join17, resolve as resolve8 } from "path";
2458
2450
  import * as p5 from "@clack/prompts";
2459
- import pc10 from "picocolors";
2451
+ import pc11 from "picocolors";
2460
2452
  var AGENT_MD_FILES = ["lead", "explorer", "consultant", "builder", "reviewer"];
2461
2453
  async function resetAgentMds(cwd2, provider) {
2462
2454
  const agentDir = provider === "claude-code" ? ".claude/agents" : ".opencode/agents";
2463
2455
  const agentDirPath = resolve8(cwd2, agentDir);
2464
- if (!existsSync13(agentDirPath)) {
2465
- 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}`));
2466
2458
  return;
2467
2459
  }
2468
2460
  const existingFiles = [];
@@ -2474,11 +2466,11 @@ async function resetAgentMds(cwd2, provider) {
2474
2466
  }
2475
2467
  }
2476
2468
  } catch {
2477
- 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`));
2478
2470
  return;
2479
2471
  }
2480
2472
  if (existingFiles.length === 0) {
2481
- console.log(pc10.yellow(` No agent MD files found in ${agentDir}/`));
2473
+ console.log(pc11.yellow(` No agent MD files found in ${agentDir}/`));
2482
2474
  return;
2483
2475
  }
2484
2476
  for (const file of existingFiles) {
@@ -2487,19 +2479,19 @@ async function resetAgentMds(cwd2, provider) {
2487
2479
  initialValue: true
2488
2480
  });
2489
2481
  if (p5.isCancel(confirm3)) {
2490
- console.log(pc10.red(" Cancelled by user."));
2482
+ console.log(pc11.red(" Cancelled by user."));
2491
2483
  return;
2492
2484
  }
2493
2485
  if (confirm3) {
2494
2486
  try {
2495
- const filePath = join16(agentDirPath, file);
2487
+ const filePath = join17(agentDirPath, file);
2496
2488
  rmSync2(filePath, { force: true });
2497
- console.log(pc10.green(` Removed ${file}`));
2489
+ console.log(pc11.green(` Removed ${file}`));
2498
2490
  } catch {
2499
- console.error(pc10.red(` Failed to remove ${file}`));
2491
+ console.error(pc11.red(` Failed to remove ${file}`));
2500
2492
  }
2501
2493
  } else {
2502
- console.log(pc10.cyan(` Skipped ${file}`));
2494
+ console.log(pc11.cyan(` Skipped ${file}`));
2503
2495
  }
2504
2496
  }
2505
2497
  }
@@ -2508,7 +2500,7 @@ async function runReset(cwd2, opts) {
2508
2500
  try {
2509
2501
  config = await loadConfig(cwd2);
2510
2502
  } catch {
2511
- 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"));
2512
2504
  process.exit(1);
2513
2505
  }
2514
2506
  const storageDir = config.storage.dir || ".harness";
@@ -2517,12 +2509,12 @@ async function runReset(cwd2, opts) {
2517
2509
  let resetDb = false;
2518
2510
  let resetFeatureList = false;
2519
2511
  let resetAgentMdsFlag = false;
2520
- if (dbPath && existsSync13(dbPath)) {
2512
+ if (dbPath && existsSync14(dbPath)) {
2521
2513
  if (opts.force) {
2522
2514
  resetDb = true;
2523
2515
  } else {
2524
2516
  if (config.database.type !== "sqlite") {
2525
- 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.`));
2526
2518
  resetDb = false;
2527
2519
  } else {
2528
2520
  const confirm3 = await p5.confirm({
@@ -2530,16 +2522,16 @@ async function runReset(cwd2, opts) {
2530
2522
  initialValue: true
2531
2523
  });
2532
2524
  if (p5.isCancel(confirm3)) {
2533
- console.log(pc10.red(" Cancelled by user."));
2525
+ console.log(pc11.red(" Cancelled by user."));
2534
2526
  return;
2535
2527
  }
2536
2528
  resetDb = confirm3;
2537
2529
  }
2538
2530
  }
2539
2531
  } else if (!dbPath) {
2540
- 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.`));
2541
2533
  }
2542
- if (existsSync13(featureListPath)) {
2534
+ if (existsSync14(featureListPath)) {
2543
2535
  if (opts.force) {
2544
2536
  resetFeatureList = true;
2545
2537
  } else {
@@ -2548,7 +2540,7 @@ async function runReset(cwd2, opts) {
2548
2540
  initialValue: true
2549
2541
  });
2550
2542
  if (p5.isCancel(confirm3)) {
2551
- console.log(pc10.red(" Cancelled by user."));
2543
+ console.log(pc11.red(" Cancelled by user."));
2552
2544
  return;
2553
2545
  }
2554
2546
  resetFeatureList = confirm3;
@@ -2562,17 +2554,17 @@ async function runReset(cwd2, opts) {
2562
2554
  rmSync2(dbPath, { force: true });
2563
2555
  rmSync2(`${dbPath}-wal`, { force: true });
2564
2556
  rmSync2(`${dbPath}-shm`, { force: true });
2565
- console.log(pc10.green(` \u2713 Removed ${dbPath}`));
2557
+ console.log(pc11.green(` \u2713 Removed ${dbPath}`));
2566
2558
  } catch {
2567
- console.error(pc10.red(` \u2717 Failed to remove ${dbPath}`));
2559
+ console.error(pc11.red(` \u2717 Failed to remove ${dbPath}`));
2568
2560
  }
2569
2561
  }
2570
2562
  if (resetFeatureList) {
2571
2563
  try {
2572
2564
  rmSync2(featureListPath, { force: true });
2573
- console.log(pc10.green(` \u2713 Removed ${storageDir}/feature_list.json`));
2565
+ console.log(pc11.green(` \u2713 Removed ${storageDir}/feature_list.json`));
2574
2566
  } catch {
2575
- console.error(pc10.red(` \u2717 Failed to remove ${featureListPath}`));
2567
+ console.error(pc11.red(` \u2717 Failed to remove ${featureListPath}`));
2576
2568
  }
2577
2569
  }
2578
2570
  if (resetAgentMdsFlag) {
@@ -2580,16 +2572,16 @@ async function runReset(cwd2, opts) {
2580
2572
  await resetAgentMds(cwd2, opts.provider || "claude-code");
2581
2573
  }
2582
2574
  if (!resetDb && !resetFeatureList && !resetAgentMdsFlag) {
2583
- 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)."));
2584
2576
  return;
2585
2577
  }
2586
2578
  console.log("");
2587
- 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.'));
2588
2580
  }
2589
2581
 
2590
2582
  // src/core/mcp-server.ts
2591
- import { existsSync as existsSync15, mkdirSync as mkdirSync9, readdirSync as readdirSync2, readFileSync as readFileSync10, statSync, writeFileSync as writeFileSync10 } from "fs";
2592
- 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";
2593
2585
  import { Server } from "@modelcontextprotocol/sdk/server";
2594
2586
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
2595
2587
  import {
@@ -2598,45 +2590,20 @@ import {
2598
2590
  } from "@modelcontextprotocol/sdk/types.js";
2599
2591
 
2600
2592
  // src/core/permissions-check.ts
2601
- import { existsSync as existsSync14, readFileSync as readFileSync9 } from "fs";
2602
- import { join as join17 } from "path";
2603
- var CANONICAL = {
2604
- lead: [...MCP_CLAUDE_PERMISSIONS_LEAD],
2605
- explorer: [...MCP_CLAUDE_PERMISSIONS_EXPLORER],
2606
- consultant: [...MCP_CLAUDE_PERMISSIONS_CONSULTANT],
2607
- builder: [...MCP_CLAUDE_PERMISSIONS_BUILDER],
2608
- reviewer: [...MCP_CLAUDE_PERMISSIONS_REVIEWER]
2609
- };
2610
- function parseToolsFromFrontmatter(content) {
2611
- const match = content.match(/^---\n([\s\S]*?)\n---/m);
2612
- if (!match) return [];
2613
- const fm = match[1];
2614
- const toolsMatch = fm.match(/^tools:\n((?: - [^\n]+\n?)*)/m);
2615
- if (!toolsMatch) return [];
2616
- return toolsMatch[1].split("\n").map((l) => l.trim().replace(/^- /, "")).filter((l) => l.startsWith("mcp__"));
2617
- }
2593
+ import { existsSync as existsSync15 } from "fs";
2594
+ import { join as join18 } from "path";
2595
+ var AGENTS = ["lead", "explorer", "consultant", "builder", "reviewer"];
2618
2596
  function checkPermissionsSync(cwd2, config) {
2619
2597
  if (config.provider !== "claude-code") {
2620
2598
  return { in_sync: true };
2621
2599
  }
2622
2600
  const agents = {};
2623
2601
  let in_sync = true;
2624
- for (const agent of ["lead", "explorer", "consultant", "builder", "reviewer"]) {
2625
- const filePath = join17(cwd2, ".claude", "agents", `${agent}.md`);
2626
- if (!existsSync14(filePath)) {
2627
- const missing2 = CANONICAL[agent];
2628
- agents[agent] = { ok: false, missing: missing2, extra: [] };
2629
- in_sync = false;
2630
- continue;
2631
- }
2632
- const content = readFileSync9(filePath, "utf-8");
2633
- const installed = parseToolsFromFrontmatter(content);
2634
- const canonical = CANONICAL[agent];
2635
- const missing = canonical.filter((t) => !installed.includes(t));
2636
- const extra = installed.filter((t) => !canonical.includes(t));
2637
- const ok3 = missing.length === 0 && extra.length === 0;
2638
- if (!ok3) in_sync = false;
2639
- 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" };
2640
2607
  }
2641
2608
  return { in_sync, agents };
2642
2609
  }
@@ -2670,7 +2637,10 @@ var TOOLS = [
2670
2637
  type: "string",
2671
2638
  description: "Section name: result | tools_used | blockers | next_steps | <custom>. Do NOT use files_modified to track files \u2014 it is stored as plain text only. Use actions.record_file instead."
2672
2639
  },
2673
- content: { type: "string", description: "Content for this section" }
2640
+ content: {
2641
+ type: "string",
2642
+ description: "Content for this section. No length limit \u2014 include all information that's relevant and necessary, but avoid unnecessary padding to prevent context bottlenecks between agents."
2643
+ }
2674
2644
  },
2675
2645
  required: ["actionId", "sectionType", "content"]
2676
2646
  }
@@ -2808,11 +2778,14 @@ var TOOLS = [
2808
2778
  type: "string",
2809
2779
  description: "URL-safe identifier (lowercase, hyphens). Auto-derived from title if omitted."
2810
2780
  },
2811
- description: { type: "string", description: "Longer description of the task goal" },
2781
+ description: {
2782
+ type: "string",
2783
+ description: "Longer description of the task goal. No length limit \u2014 include all information that's relevant and necessary, but avoid unnecessary padding to prevent context bottlenecks between agents."
2784
+ },
2812
2785
  acceptance: {
2813
2786
  type: "array",
2814
2787
  items: { type: "string" },
2815
- description: "List of acceptance criteria (plain sentences)"
2788
+ description: "List of acceptance criteria (plain sentences). No length limit \u2014 include all information that's relevant and necessary, but avoid unnecessary padding to prevent context bottlenecks between agents."
2816
2789
  }
2817
2790
  },
2818
2791
  required: ["title"]
@@ -2880,7 +2853,7 @@ var TOOLS = [
2880
2853
  },
2881
2854
  {
2882
2855
  name: "permissions.check",
2883
- 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.',
2884
2857
  inputSchema: { type: "object", properties: {}, required: [] }
2885
2858
  },
2886
2859
  {
@@ -3049,19 +3022,19 @@ async function dispatch(name, args, db, docsPath, cwd2, config) {
3049
3022
  return ok2(JSON.stringify(result, null, 2));
3050
3023
  }
3051
3024
  case "deps.snapshot": {
3052
- const pkgPath2 = join18(cwd2, "package.json");
3053
- if (!existsSync15(pkgPath2)) {
3025
+ const pkgPath2 = join19(cwd2, "package.json");
3026
+ if (!existsSync16(pkgPath2)) {
3054
3027
  return ok2("package.json not found in project root", true);
3055
3028
  }
3056
- const pkg2 = JSON.parse(readFileSync10(pkgPath2, "utf8"));
3029
+ const pkg2 = JSON.parse(readFileSync9(pkgPath2, "utf8"));
3057
3030
  const snapshot = {
3058
3031
  capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
3059
3032
  dependencies: pkg2.dependencies ?? {},
3060
3033
  devDependencies: pkg2.devDependencies ?? {}
3061
3034
  };
3062
- const harnessDir = join18(cwd2, ".harness");
3035
+ const harnessDir = join19(cwd2, ".harness");
3063
3036
  mkdirSync9(harnessDir, { recursive: true });
3064
- 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");
3065
3038
  return ok2(
3066
3039
  JSON.stringify({
3067
3040
  message: "Snapshot saved to .harness/deps-lock.json",
@@ -3070,12 +3043,12 @@ async function dispatch(name, args, db, docsPath, cwd2, config) {
3070
3043
  );
3071
3044
  }
3072
3045
  case "deps.check": {
3073
- const pkgPath2 = join18(cwd2, "package.json");
3074
- const lockPath = join18(cwd2, ".harness", "deps-lock.json");
3075
- if (!existsSync15(pkgPath2)) {
3046
+ const pkgPath2 = join19(cwd2, "package.json");
3047
+ const lockPath = join19(cwd2, ".harness", "deps-lock.json");
3048
+ if (!existsSync16(pkgPath2)) {
3076
3049
  return ok2("package.json not found in project root", true);
3077
3050
  }
3078
- if (!existsSync15(lockPath)) {
3051
+ if (!existsSync16(lockPath)) {
3079
3052
  return ok2(
3080
3053
  JSON.stringify({
3081
3054
  status: "no-snapshot",
@@ -3083,8 +3056,8 @@ async function dispatch(name, args, db, docsPath, cwd2, config) {
3083
3056
  })
3084
3057
  );
3085
3058
  }
3086
- const pkg2 = JSON.parse(readFileSync10(pkgPath2, "utf8"));
3087
- const lock = JSON.parse(readFileSync10(lockPath, "utf8"));
3059
+ const pkg2 = JSON.parse(readFileSync9(pkgPath2, "utf8"));
3060
+ const lock = JSON.parse(readFileSync9(lockPath, "utf8"));
3088
3061
  const current = { ...pkg2.dependencies ?? {}, ...pkg2.devDependencies ?? {} };
3089
3062
  const previous = { ...lock.dependencies ?? {}, ...lock.devDependencies ?? {} };
3090
3063
  const added = [];
@@ -3126,9 +3099,10 @@ async function dispatch(name, args, db, docsPath, cwd2, config) {
3126
3099
  const status = await getDoctorStatus(cwd2);
3127
3100
  const result = {
3128
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.
3129
3104
  agents: {
3130
3105
  missing: status.agents.filter((a) => a.status === "missing").map((a) => a.name),
3131
- outdated: status.agents.filter((a) => a.status === "outdated").map((a) => a.name),
3132
3106
  ok: status.agents.filter((a) => a.status === "ok").map((a) => a.name)
3133
3107
  },
3134
3108
  skills: {
@@ -3151,7 +3125,7 @@ function searchDocs(docsPath, query, maxResults = 10) {
3151
3125
  for (const file of files) {
3152
3126
  if (results.length >= maxResults) break;
3153
3127
  try {
3154
- const content = readFileSync10(file, "utf8");
3128
+ const content = readFileSync9(file, "utf8");
3155
3129
  const lines = content.split("\n");
3156
3130
  for (let i = 0; i < lines.length; i++) {
3157
3131
  const lower = lines[i].toLowerCase();
@@ -3176,7 +3150,7 @@ function collectMarkdownFiles(dir) {
3176
3150
  const files = [];
3177
3151
  try {
3178
3152
  for (const entry of readdirSync2(dir)) {
3179
- const full = join18(dir, entry);
3153
+ const full = join19(dir, entry);
3180
3154
  const stat = statSync(full);
3181
3155
  if (stat.isDirectory()) {
3182
3156
  files.push(...collectMarkdownFiles(full));
@@ -3212,14 +3186,8 @@ async function runServe(cwd2, opts) {
3212
3186
  `);
3213
3187
  const syncResult = checkPermissionsSync(cwd2, config);
3214
3188
  if (!syncResult.in_sync && syncResult.agents) {
3215
- const affected = Object.entries(syncResult.agents).filter(([, r]) => !r.ok).map(([name, r]) => {
3216
- const parts = [];
3217
- if (r.missing.length) parts.push(`missing: ${r.missing.map((t) => t.replace("mcp__agent-harness-kit__", "")).join(", ")}`);
3218
- if (r.extra.length) parts.push(`extra: ${r.extra.map((t) => t.replace("mcp__agent-harness-kit__", "")).join(", ")}`);
3219
- return `${name} (${parts.join("; ")})`;
3220
- }).join("\n ");
3221
- process.stderr.write(`[agent-harness-kit] Agent permissions out of sync. Run: ahk build --sync
3222
- ${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
3223
3191
  `);
3224
3192
  }
3225
3193
  await startMcpServer(config, cwd2);
@@ -3227,12 +3195,12 @@ async function runServe(cwd2, opts) {
3227
3195
 
3228
3196
  // src/commands/status.ts
3229
3197
  import Table from "cli-table3";
3230
- import pc11 from "picocolors";
3198
+ import pc12 from "picocolors";
3231
3199
  var STATUS_COLOR = {
3232
- pending: (s) => pc11.dim(s),
3233
- in_progress: (s) => pc11.cyan(s),
3234
- done: (s) => pc11.green(s),
3235
- 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)
3236
3204
  };
3237
3205
  async function runStatus(cwd2, opts) {
3238
3206
  const config = await loadConfig(cwd2);
@@ -3253,11 +3221,11 @@ async function runStatus(cwd2, opts) {
3253
3221
  return;
3254
3222
  }
3255
3223
  if (tasks.length === 0) {
3256
- console.log(pc11.dim("No tasks yet. Run: ahk task add"));
3224
+ console.log(pc12.dim("No tasks yet. Run: ahk task add"));
3257
3225
  return;
3258
3226
  }
3259
3227
  const table = new Table({
3260
- 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)),
3261
3229
  style: { head: [], border: [] }
3262
3230
  });
3263
3231
  for (const t of tasks) {
@@ -3275,12 +3243,12 @@ async function runStatus(cwd2, opts) {
3275
3243
  const inProgress = tasks.filter((t) => t.status === "in_progress");
3276
3244
  if (inProgress.length > 0) {
3277
3245
  console.log("");
3278
- console.log(pc11.bold("Active actions:"));
3246
+ console.log(pc12.bold("Active actions:"));
3279
3247
  for (const t of inProgress) {
3280
3248
  const actions = await db.getActionsForTask(t.id);
3281
3249
  const active = actions.filter((a) => a.status === "in_progress");
3282
3250
  for (const a of active) {
3283
- 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}`);
3284
3252
  }
3285
3253
  }
3286
3254
  }
@@ -3289,10 +3257,10 @@ async function runStatus(cwd2, opts) {
3289
3257
  const fn = STATUS_COLOR[s.status] ?? ((x) => x);
3290
3258
  return `${fn(s.status)}: ${s.total}`;
3291
3259
  });
3292
- console.log(pc11.dim("Tasks \u2014 ") + parts.join(pc11.dim(" | ")));
3260
+ console.log(pc12.dim("Tasks \u2014 ") + parts.join(pc12.dim(" | ")));
3293
3261
  const archivedTasks = await db.getArchivedTasks();
3294
3262
  if (archivedTasks.length > 0) {
3295
- 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)`));
3296
3264
  }
3297
3265
  } finally {
3298
3266
  await db.close();
@@ -3300,13 +3268,13 @@ async function runStatus(cwd2, opts) {
3300
3268
  }
3301
3269
 
3302
3270
  // src/commands/sync.ts
3303
- import { existsSync as existsSync16, readFileSync as readFileSync11 } from "fs";
3304
- import { join as join19, resolve as resolve10 } from "path";
3305
- 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";
3306
3274
  async function runSync(cwd2, opts) {
3307
3275
  const config = await loadConfig(cwd2);
3308
3276
  const direction = opts.direction ?? "both";
3309
- const featureListPath = resolve10(join19(cwd2, config.storage.dir, "feature_list.json"));
3277
+ const featureListPath = resolve10(join20(cwd2, config.storage.dir, "feature_list.json"));
3310
3278
  const db = await openDB(config, cwd2);
3311
3279
  try {
3312
3280
  if (direction === "in" || direction === "both") {
@@ -3320,44 +3288,44 @@ async function runSync(cwd2, opts) {
3320
3288
  }
3321
3289
  }
3322
3290
  async function syncIn(featureListPath, db, dryRun) {
3323
- if (!existsSync16(featureListPath)) {
3324
- 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`));
3325
3293
  return;
3326
3294
  }
3327
3295
  let seeds;
3328
3296
  try {
3329
- seeds = JSON.parse(readFileSync11(featureListPath, "utf8"));
3297
+ seeds = JSON.parse(readFileSync10(featureListPath, "utf8"));
3330
3298
  } catch (err) {
3331
- console.error(pc12.red(`Failed to parse feature_list.json: ${err}`));
3299
+ console.error(pc13.red(`Failed to parse feature_list.json: ${err}`));
3332
3300
  process.exit(1);
3333
3301
  }
3334
3302
  if (dryRun) {
3335
- 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):"));
3336
3304
  for (const t of seeds) {
3337
3305
  const existing = await db.getTaskBySlug(t.slug);
3338
- console.log(` ${existing ? pc12.dim("skip") : pc12.green("add ")} ${t.slug}`);
3306
+ console.log(` ${existing ? pc13.dim("skip") : pc13.green("add ")} ${t.slug}`);
3339
3307
  }
3340
3308
  return;
3341
3309
  }
3342
3310
  const result = await db.syncFromFeatureList(seeds);
3343
- 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`));
3344
3312
  }
3345
3313
  async function syncOut(db, cwd2, dryRun) {
3346
3314
  if (dryRun) {
3347
3315
  const tasks = await db.getTasks();
3348
- 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):"));
3349
3317
  console.log(` ${tasks.length} tasks would be written`);
3350
3318
  return;
3351
3319
  }
3352
3320
  await db.writeFeatureList(cwd2);
3353
- console.log(pc12.green("\u2713 Out-sync: feature_list.json updated"));
3321
+ console.log(pc13.green("\u2713 Out-sync: feature_list.json updated"));
3354
3322
  }
3355
3323
 
3356
3324
  // src/commands/task/add.ts
3357
3325
  import * as p6 from "@clack/prompts";
3358
- import pc13 from "picocolors";
3326
+ import pc14 from "picocolors";
3359
3327
  async function runTaskAdd(cwd2) {
3360
- p6.intro(pc13.bold("agent-harness-kit \u2014 add task"));
3328
+ p6.intro(pc14.bold("agent-harness-kit \u2014 add task"));
3361
3329
  const title = await cliFormWithRetry(
3362
3330
  async () => {
3363
3331
  const val = await p6.text({ message: "Task title" });
@@ -3400,10 +3368,10 @@ async function runTaskAdd(cwd2) {
3400
3368
  await db.writeFeatureList(cwd2);
3401
3369
  await db.close();
3402
3370
  spinner6.stop("");
3403
- console.log(pc13.green(`\u2713 Task #${task2.id} added \u2014 ${task2.slug} (pending)`));
3404
- 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");
3405
3373
  } catch (err) {
3406
- spinner6.stop(pc13.red("Failed"));
3374
+ spinner6.stop(pc14.red("Failed"));
3407
3375
  p6.log.error(err instanceof Error ? err.message : String(err));
3408
3376
  process.exit(1);
3409
3377
  }
@@ -3411,17 +3379,17 @@ async function runTaskAdd(cwd2) {
3411
3379
 
3412
3380
  // src/commands/task/done.ts
3413
3381
  import { spawnSync as spawnSync2 } from "child_process";
3414
- import { existsSync as existsSync17 } from "fs";
3382
+ import { existsSync as existsSync18 } from "fs";
3415
3383
  import { resolve as resolve11 } from "path";
3416
- import pc14 from "picocolors";
3384
+ import pc15 from "picocolors";
3417
3385
  async function runTaskDone(cwd2, idOrSlug) {
3418
3386
  const config = await loadConfig(cwd2);
3419
3387
  if (config.health.required) {
3420
3388
  const scriptPath = resolve11(cwd2, config.health.scriptPath);
3421
- if (existsSync17(scriptPath)) {
3389
+ if (existsSync18(scriptPath)) {
3422
3390
  const result = spawnSync2("bash", [scriptPath], { cwd: cwd2, stdio: "pipe", encoding: "utf8" });
3423
3391
  if (result.status !== 0) {
3424
- 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."));
3425
3393
  if (result.stdout) console.error(result.stdout);
3426
3394
  if (result.stderr) console.error(result.stderr);
3427
3395
  process.exit(1);
@@ -3434,16 +3402,16 @@ async function runTaskDone(cwd2, idOrSlug) {
3434
3402
  const isId = !isNaN(parsed);
3435
3403
  const task2 = isId ? await db.getTaskById(parsed) : await db.getTaskBySlug(idOrSlug);
3436
3404
  if (!task2) {
3437
- console.error(pc14.red(`Task not found: ${idOrSlug}`));
3405
+ console.error(pc15.red(`Task not found: ${idOrSlug}`));
3438
3406
  process.exit(1);
3439
3407
  }
3440
3408
  if (task2.status === "done") {
3441
- console.log(pc14.dim(`Task #${task2.id} is already done.`));
3409
+ console.log(pc15.dim(`Task #${task2.id} is already done.`));
3442
3410
  return;
3443
3411
  }
3444
3412
  await db.updateTaskStatus(task2.id, "done");
3445
3413
  await db.writeFeatureList(cwd2);
3446
- 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`));
3447
3415
  } finally {
3448
3416
  await db.close();
3449
3417
  }
@@ -3451,9 +3419,9 @@ async function runTaskDone(cwd2, idOrSlug) {
3451
3419
 
3452
3420
  // src/commands/task/edit.ts
3453
3421
  import * as p7 from "@clack/prompts";
3454
- import pc15 from "picocolors";
3422
+ import pc16 from "picocolors";
3455
3423
  async function runTaskEdit(cwd2) {
3456
- p7.intro(pc15.bold("agent-harness-kit \u2014 edit task"));
3424
+ p7.intro(pc16.bold("agent-harness-kit \u2014 edit task"));
3457
3425
  const config = await loadConfig(cwd2);
3458
3426
  const db = await openDB(config, cwd2);
3459
3427
  try {
@@ -3531,9 +3499,9 @@ async function runTaskEdit(cwd2) {
3531
3499
  await db.updateTaskAcceptance(task2.id, newAcceptance);
3532
3500
  await db.writeFeatureList(cwd2);
3533
3501
  spinner6.stop("");
3534
- console.log(pc15.green(`\u2713 Task #${task2.id} updated \u2014 ${newSlug}`));
3502
+ console.log(pc16.green(`\u2713 Task #${task2.id} updated \u2014 ${newSlug}`));
3535
3503
  } catch (err) {
3536
- spinner6.stop(pc15.red("Failed"));
3504
+ spinner6.stop(pc16.red("Failed"));
3537
3505
  p7.log.error(err instanceof Error ? err.message : String(err));
3538
3506
  process.exit(1);
3539
3507
  }
@@ -3544,12 +3512,12 @@ async function runTaskEdit(cwd2) {
3544
3512
 
3545
3513
  // src/commands/task/list.ts
3546
3514
  import Table2 from "cli-table3";
3547
- import pc16 from "picocolors";
3515
+ import pc17 from "picocolors";
3548
3516
  var STATUS_COLOR2 = {
3549
- pending: (s) => pc16.dim(s),
3550
- in_progress: (s) => pc16.cyan(s),
3551
- done: (s) => pc16.green(s),
3552
- 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)
3553
3521
  };
3554
3522
  async function runTaskList(cwd2, opts) {
3555
3523
  const config = await loadConfig(cwd2);
@@ -3566,11 +3534,11 @@ async function runTaskList(cwd2, opts) {
3566
3534
  let msg = "No tasks";
3567
3535
  if (filterStatus) msg += ` with status: ${filterStatus}`;
3568
3536
  if (opts.archived) msg += " (archived)";
3569
- console.log(pc16.dim(msg + "."));
3537
+ console.log(pc17.dim(msg + "."));
3570
3538
  return;
3571
3539
  }
3572
3540
  const table = new Table2({
3573
- head: ["ID", "Slug", "Title", "Status"].map((h) => pc16.bold(h)),
3541
+ head: ["ID", "Slug", "Title", "Status"].map((h) => pc17.bold(h)),
3574
3542
  style: { head: [], border: [] }
3575
3543
  });
3576
3544
  for (const t of tasks) {
@@ -3581,7 +3549,7 @@ async function runTaskList(cwd2, opts) {
3581
3549
  if (!opts.archived && !opts.includeArchived) {
3582
3550
  const archivedTasks = await db.getArchivedTasks();
3583
3551
  if (archivedTasks.length > 0) {
3584
- 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)`));
3585
3553
  }
3586
3554
  }
3587
3555
  } finally {
@@ -3589,43 +3557,6 @@ async function runTaskList(cwd2, opts) {
3589
3557
  }
3590
3558
  }
3591
3559
 
3592
- // src/core/local-install-guard.ts
3593
- import { existsSync as existsSync18, readFileSync as readFileSync12 } from "fs";
3594
- import { join as join20 } from "path";
3595
- import pc17 from "picocolors";
3596
- function isLocalInstallSatisfied(cwd2) {
3597
- const selfPkgPath = join20(cwd2, "package.json");
3598
- let projectPkg = null;
3599
- if (existsSync18(selfPkgPath)) {
3600
- try {
3601
- const selfPkg = JSON.parse(readFileSync12(selfPkgPath, "utf8"));
3602
- if (selfPkg?.name === pkg.name) return true;
3603
- projectPkg = selfPkg;
3604
- } catch {
3605
- }
3606
- }
3607
- const [scope, name] = pkg.name.split("/");
3608
- const localPath = pkg.name.startsWith("@") ? join20(cwd2, "node_modules", scope, name) : join20(cwd2, "node_modules", pkg.name);
3609
- if (existsSync18(localPath)) return true;
3610
- const isPnp = existsSync18(join20(cwd2, ".pnp.cjs")) || existsSync18(join20(cwd2, ".pnp.loader.mjs"));
3611
- if (isPnp && projectPkg) {
3612
- const deps = {
3613
- ...projectPkg.dependencies ?? {},
3614
- ...projectPkg.devDependencies ?? {}
3615
- };
3616
- if (Object.prototype.hasOwnProperty.call(deps, pkg.name)) return true;
3617
- }
3618
- return false;
3619
- }
3620
- function printLocalInstallWarning() {
3621
- console.error(pc17.yellow(`\u26A0 ${pkg.name} is not installed locally in this project.`));
3622
- console.error(pc17.dim(" This is only a recommendation for reproducibility: pinning a local"));
3623
- console.error(pc17.dim(" version keeps behavior consistent across your team and CI, instead of"));
3624
- console.error(pc17.dim(" drifting with whatever version is installed globally on each machine."));
3625
- console.error(pc17.dim(` Run: npm install --save-dev ${pkg.name}`));
3626
- console.error(pc17.dim(" (or the equivalent for your package manager: pnpm add -D, yarn add --dev, bun add -d)"));
3627
- }
3628
-
3629
3560
  // src/core/update-check.ts
3630
3561
  import pc18 from "picocolors";
3631
3562
  var REGISTRY_URL2 = `https://registry.npmjs.org/${pkg.name}/latest`;
@@ -3667,7 +3598,10 @@ program.name("ahk").description("agent-harness-kit \u2014 CLI scaffolding for mu
3667
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) => {
3668
3599
  await runInit(cwd, opts);
3669
3600
  });
3670
- 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) => {
3671
3605
  await runBuild(cwd, opts);
3672
3606
  });
3673
3607
  program.command("health").description("Run health.sh and report result").action(async () => {