@cardor/agent-harness-kit 1.9.0 → 1.10.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,10 +1,19 @@
1
1
  import {
2
2
  findConfigFile,
3
3
  loadConfig
4
- } from "./chunk-OEPZRC7J.js";
4
+ } from "./chunk-ADV7OPU2.js";
5
+ import {
6
+ getRowCounts,
7
+ isEmptyDatabase,
8
+ openDB,
9
+ readStorageStateFile,
10
+ resolveGlobalStorageDir,
11
+ resolveSqlitePathForScope
12
+ } from "./chunk-DNFFWQWR.js";
5
13
 
6
14
  // src/cli.ts
7
15
  import { Command } from "commander";
16
+ import pc19 from "picocolors";
8
17
 
9
18
  // src/commands/build.ts
10
19
  import { watch } from "fs";
@@ -12,8 +21,8 @@ import * as p from "@clack/prompts";
12
21
  import pc from "picocolors";
13
22
 
14
23
  // src/core/materializer/claude-code.ts
15
- import { existsSync as existsSync3, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
16
- import { join as join4, resolve as resolve3 } from "path";
24
+ import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
25
+ import { join as join5, resolve as resolve3 } from "path";
17
26
 
18
27
  // src/utils/file.ts
19
28
  import { mkdirSync, writeFileSync } from "fs";
@@ -24,29 +33,88 @@ var write = (cwd2, relPath, content, mode) => {
24
33
  writeFileSync(abs, content, { encoding: "utf8", mode });
25
34
  };
26
35
 
36
+ // src/core/materializer/detect-package-manager.ts
37
+ import { existsSync, readFileSync } from "fs";
38
+ import { join as join2 } from "path";
39
+ function detectPackageManager(cwd2) {
40
+ const fromField = detectFromPackageManagerField(cwd2);
41
+ if (fromField) return fromField;
42
+ if (existsSync(join2(cwd2, "pnpm-lock.yaml"))) return "pnpm";
43
+ if (existsSync(join2(cwd2, "bun.lockb")) || existsSync(join2(cwd2, "bun.lock"))) return "bun";
44
+ if (existsSync(join2(cwd2, "yarn.lock"))) {
45
+ return existsSync(join2(cwd2, ".yarnrc.yml")) ? "yarn-berry" : "yarn-classic";
46
+ }
47
+ if (existsSync(join2(cwd2, "package-lock.json"))) return "npm";
48
+ return "npm";
49
+ }
50
+ function detectFromPackageManagerField(cwd2) {
51
+ const pkgPath2 = join2(cwd2, "package.json");
52
+ if (!existsSync(pkgPath2)) return null;
53
+ try {
54
+ const pkg2 = JSON.parse(readFileSync(pkgPath2, "utf8"));
55
+ const field = pkg2?.packageManager;
56
+ if (typeof field !== "string" || !field.trim()) return null;
57
+ const match = field.match(/^([a-z]+)@(\d+)/i);
58
+ if (!match) return null;
59
+ const [, rawName, majorStr] = match;
60
+ const name = rawName.toLowerCase();
61
+ const major = Number(majorStr);
62
+ switch (name) {
63
+ case "npm":
64
+ return "npm";
65
+ case "pnpm":
66
+ return "pnpm";
67
+ case "bun":
68
+ return "bun";
69
+ case "yarn":
70
+ return major >= 2 ? "yarn-berry" : "yarn-classic";
71
+ default:
72
+ return null;
73
+ }
74
+ } catch {
75
+ return null;
76
+ }
77
+ }
78
+ function getMcpCommandParts(pm, port) {
79
+ const portStr = String(port);
80
+ switch (pm) {
81
+ case "pnpm":
82
+ return ["pnpm", "exec", "ahk", "serve", "--port", portStr];
83
+ case "yarn-classic":
84
+ case "yarn-berry":
85
+ return ["yarn", "run", "ahk", "serve", "--port", portStr];
86
+ case "bun":
87
+ return ["bunx", "--no-install", "ahk", "serve", "--port", portStr];
88
+ case "npm":
89
+ default:
90
+ return ["npx", "--no", "ahk", "serve", "--port", portStr];
91
+ }
92
+ }
93
+
27
94
  // src/core/materializer/mcp-merge.ts
28
- import { existsSync, mkdirSync as mkdirSync2, readFileSync, writeFileSync as writeFileSync2 } from "fs";
95
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
29
96
  import { dirname } from "path";
30
- function mergeClaudeMcpJson(filePath, port) {
97
+ function mergeClaudeMcpJson(filePath, port, pm = "npm") {
31
98
  const folderPath = dirname(filePath);
32
- if (!existsSync(folderPath)) {
99
+ if (!existsSync2(folderPath)) {
33
100
  mkdirSync2(folderPath, { recursive: true });
34
101
  }
35
102
  let existing = {};
36
- if (existsSync(filePath)) {
103
+ if (existsSync2(filePath)) {
37
104
  try {
38
- existing = JSON.parse(readFileSync(filePath, "utf8"));
105
+ existing = JSON.parse(readFileSync2(filePath, "utf8"));
39
106
  } catch {
40
107
  }
41
108
  }
109
+ const [command, ...args] = getMcpCommandParts(pm, port);
42
110
  const merged = {
43
111
  ...existing,
44
112
  mcpServers: {
45
113
  ...existing.mcpServers ?? {},
46
114
  "agent-harness-kit": {
47
115
  type: "stdio",
48
- command: "npx",
49
- args: ["ahk", "serve", "--port", String(port)]
116
+ command,
117
+ args
50
118
  }
51
119
  }
52
120
  };
@@ -56,9 +124,9 @@ function mergeClaudeMcpJson(filePath, port) {
56
124
  function mergeClaudeSettingsJson(filePath) {
57
125
  mkdirSync2(dirname(filePath), { recursive: true });
58
126
  let existing = {};
59
- if (existsSync(filePath)) {
127
+ if (existsSync2(filePath)) {
60
128
  try {
61
- existing = JSON.parse(readFileSync(filePath, "utf8"));
129
+ existing = JSON.parse(readFileSync2(filePath, "utf8"));
62
130
  } catch {
63
131
  }
64
132
  }
@@ -163,9 +231,9 @@ var MCP_CLAUDE_PERMISSIONS = [
163
231
  function mergeClaudeSettingsLocalJson(filePath) {
164
232
  mkdirSync2(dirname(filePath), { recursive: true });
165
233
  let existing = {};
166
- if (existsSync(filePath)) {
234
+ if (existsSync2(filePath)) {
167
235
  try {
168
- existing = JSON.parse(readFileSync(filePath, "utf8"));
236
+ existing = JSON.parse(readFileSync2(filePath, "utf8"));
169
237
  } catch {
170
238
  }
171
239
  }
@@ -184,15 +252,15 @@ function mergeClaudeSettingsLocalJson(filePath) {
184
252
  };
185
253
  writeFileSync2(filePath, JSON.stringify(merged, null, 2) + "\n", "utf8");
186
254
  }
187
- function mergeOpencodeJson(filePath, port) {
255
+ function mergeOpencodeJson(filePath, port, pm = "npm") {
188
256
  const folderPath = dirname(filePath);
189
- if (!existsSync(folderPath)) {
257
+ if (!existsSync2(folderPath)) {
190
258
  mkdirSync2(folderPath, { recursive: true });
191
259
  }
192
260
  let existing = {};
193
- if (existsSync(filePath)) {
261
+ if (existsSync2(filePath)) {
194
262
  try {
195
- existing = JSON.parse(readFileSync(filePath, "utf8"));
263
+ existing = JSON.parse(readFileSync2(filePath, "utf8"));
196
264
  } catch {
197
265
  }
198
266
  }
@@ -207,7 +275,9 @@ function mergeOpencodeJson(filePath, port) {
207
275
  "agent-harness-kit": {
208
276
  enabled: true,
209
277
  type: "local",
210
- command: ["npx", "ahk", "serve", "--port", String(port)]
278
+ // OpenCode's mcp.<name>.command field is a single array (unlike
279
+ // Claude/Codex, which split command/args) — pass the full token list.
280
+ command: getMcpCommandParts(pm, port)
211
281
  }
212
282
  }
213
283
  };
@@ -237,15 +307,16 @@ function mergeTomlSection(content, sectionName, sectionBody) {
237
307
  ];
238
308
  return newLines.join("\n");
239
309
  }
240
- function mergeCodexConfigToml(filePath, port) {
310
+ function mergeCodexConfigToml(filePath, port, pm = "npm") {
241
311
  mkdirSync2(dirname(filePath), { recursive: true });
242
312
  let content = "";
243
- if (existsSync(filePath)) {
244
- content = readFileSync(filePath, "utf8");
313
+ if (existsSync2(filePath)) {
314
+ content = readFileSync2(filePath, "utf8");
245
315
  }
316
+ const [command, ...args] = getMcpCommandParts(pm, port);
246
317
  const sectionBody = [
247
- 'command = "npx"',
248
- `args = ["ahk", "serve", "--port", "${port}"]`,
318
+ `command = ${JSON.stringify(command)}`,
319
+ `args = ${JSON.stringify(args)}`,
249
320
  'default_tools_approval_mode = "auto"'
250
321
  ].join("\n");
251
322
  content = mergeTomlSection(content, "mcp_servers.agent-harness-kit", sectionBody);
@@ -253,18 +324,18 @@ function mergeCodexConfigToml(filePath, port) {
253
324
  }
254
325
 
255
326
  // src/core/materializer/scaffold-utils.ts
256
- import { existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
257
- import { dirname as dirname3, join as join3, resolve as resolve2 } from "path";
327
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
328
+ import { dirname as dirname3, join as join4, resolve as resolve2 } from "path";
258
329
  import { fileURLToPath as fileURLToPath2 } from "url";
259
330
 
260
331
  // src/core/materializer/templates.ts
261
- import { readFileSync as readFileSync2 } from "fs";
262
- import { dirname as dirname2, join as join2 } from "path";
332
+ import { readFileSync as readFileSync3 } from "fs";
333
+ import { dirname as dirname2, join as join3 } from "path";
263
334
  import { fileURLToPath } from "url";
264
335
  var __dirname = dirname2(fileURLToPath(import.meta.url));
265
- var TEMPLATES_DIR = join2(__dirname, "agent-templates");
336
+ var TEMPLATES_DIR = join3(__dirname, "agent-templates");
266
337
  function loadAgentTemplate(name, vars = {}) {
267
- const raw = readFileSync2(join2(TEMPLATES_DIR, `${name}.md`), "utf8");
338
+ const raw = readFileSync3(join3(TEMPLATES_DIR, `${name}.md`), "utf8");
268
339
  return raw.replace(/\{\{(\w+)\}\}/g, (_, key) => vars[key] ?? `{{${key}}}`);
269
340
  }
270
341
  var HEALTH_SH = `#!/usr/bin/env bash
@@ -497,6 +568,10 @@ export default defineHarness({
497
568
  nextSteps: false,
498
569
  },
499
570
  markdownFallback: { enabled: true, path: '.harness/current.md' },
571
+ // 'local' \u2014 DB lives in .harness/ (project-relative). 'global' \u2014 DB lives
572
+ // under ~/.harness/dbs/<projectId>/, outside the project tree.
573
+ scope: '${params.scope}',
574
+ projectId: '${params.projectId}',
500
575
  },
501
576
 
502
577
  health: {
@@ -550,6 +625,10 @@ module.exports = defineHarness({
550
625
  nextSteps: false,
551
626
  },
552
627
  markdownFallback: { enabled: true, path: '.harness/current.md' },
628
+ // 'local' \u2014 DB lives in .harness/ (project-relative). 'global' \u2014 DB lives
629
+ // under ~/.harness/dbs/<projectId>/, outside the project tree.
630
+ scope: '${params.scope}',
631
+ projectId: '${params.projectId}',
553
632
  },
554
633
 
555
634
  health: {
@@ -684,14 +763,14 @@ var GITIGNORE_ENTRIES = `
684
763
  // src/core/materializer/scaffold-utils.ts
685
764
  var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
686
765
  function writeAgentFile(cwd2, relPath, content) {
687
- const abs = join3(cwd2, relPath);
688
- if (existsSync2(abs)) return;
766
+ const abs = join4(cwd2, relPath);
767
+ if (existsSync3(abs)) return;
689
768
  mkdirSync3(resolve2(abs, ".."), { recursive: true });
690
769
  writeFileSync3(abs, content, "utf8");
691
770
  }
692
771
  function appendGitignore(cwd2) {
693
- const giPath = join3(cwd2, ".gitignore");
694
- const existing = existsSync2(giPath) ? readFileSync3(giPath, "utf8") : "";
772
+ const giPath = join4(cwd2, ".gitignore");
773
+ const existing = existsSync3(giPath) ? readFileSync4(giPath, "utf8") : "";
695
774
  const toAdd = GITIGNORE_ENTRIES.split("\n").filter((line) => line && !existing.includes(line)).join("\n");
696
775
  if (toAdd.trim()) {
697
776
  writeFileSync3(giPath, existing + (existing.endsWith("\n") ? "" : "\n") + toAdd + "\n", "utf8");
@@ -703,13 +782,21 @@ function slugify(title) {
703
782
  function writeSkills(cwd2, skillsDir) {
704
783
  const skillNames = ["ahk-ask", "ahk-consultant", "ahk-triage", "ahk-review"];
705
784
  for (const skillName of skillNames) {
706
- const src = join3(__dirname2, "skills", skillName, "SKILL.md");
707
- const destDir = join3(cwd2, skillsDir, skillName);
708
- const dest = join3(destDir, "SKILL.md");
785
+ const src = join4(__dirname2, "skills", skillName, "SKILL.md");
786
+ const destDir = join4(cwd2, skillsDir, skillName);
787
+ const dest = join4(destDir, "SKILL.md");
709
788
  mkdirSync3(destDir, { recursive: true });
710
- writeFileSync3(dest, readFileSync3(src, "utf8"), "utf8");
789
+ writeFileSync3(dest, readFileSync4(src, "utf8"), "utf8");
711
790
  }
712
791
  }
792
+ function writeSkill(skillsRoot, skillName) {
793
+ const src = join4(__dirname2, "skills", skillName, "SKILL.md");
794
+ const destDir = join4(skillsRoot, skillName);
795
+ const dest = join4(destDir, "SKILL.md");
796
+ if (existsSync3(dest)) return;
797
+ mkdirSync3(destDir, { recursive: true });
798
+ writeFileSync3(dest, readFileSync4(src, "utf8"), "utf8");
799
+ }
713
800
 
714
801
  // src/core/materializer/claude-code.ts
715
802
  var ClaudeCodeMaterializer = class {
@@ -717,12 +804,12 @@ var ClaudeCodeMaterializer = class {
717
804
  const { cwd: cwd2 } = opts;
718
805
  write(cwd2, "AGENTS.md", agentsMd(config));
719
806
  write(cwd2, "CLAUDE.md", claudeMd(config));
720
- if (!existsSync3(join4(cwd2, "health.sh"))) {
807
+ if (!existsSync4(join5(cwd2, "health.sh"))) {
721
808
  write(cwd2, "health.sh", HEALTH_SH, 493);
722
809
  }
723
810
  const tasks = opts.firstTask ? [{ slug: slugify(opts.firstTask.title), ...opts.firstTask }] : [];
724
811
  write(cwd2, "feature_list.json", featureListJson(tasks));
725
- if (!existsSync3(join4(cwd2, config.storage.markdownFallback.path))) {
812
+ if (!existsSync4(join5(cwd2, config.storage.markdownFallback.path))) {
726
813
  write(
727
814
  cwd2,
728
815
  config.storage.markdownFallback.path,
@@ -748,15 +835,15 @@ No tasks in progress.
748
835
  writeAgentFile(cwd2, ".claude/agents/consultant.md", translateFrontmatterForClaudeCode(agentConsultant({ projectName }), "consultant", consultantModel));
749
836
  writeAgentFile(cwd2, ".claude/agents/builder.md", translateFrontmatterForClaudeCode(agentBuilder({ projectName, writablePaths }), "builder", builderModel));
750
837
  writeAgentFile(cwd2, ".claude/agents/reviewer.md", translateFrontmatterForClaudeCode(agentReviewer({ projectName }), "reviewer", reviewerModel));
751
- mergeClaudeMcpJson(join4(cwd2, ".mcp.json"), config.tools.mcp.port);
752
- mergeClaudeSettingsJson(join4(cwd2, ".claude/settings.json"));
753
- mergeClaudeSettingsLocalJson(join4(cwd2, ".claude/settings.local.json"));
838
+ mergeClaudeMcpJson(join5(cwd2, ".mcp.json"), config.tools.mcp.port, detectPackageManager(cwd2));
839
+ mergeClaudeSettingsJson(join5(cwd2, ".claude/settings.json"));
840
+ mergeClaudeSettingsLocalJson(join5(cwd2, ".claude/settings.local.json"));
754
841
  appendGitignore(cwd2);
755
842
  writeSkills(cwd2, ".claude/skills");
756
843
  }
757
844
  async build(config, cwd2) {
758
845
  const write2 = (relPath, content) => {
759
- const abs = join4(cwd2, relPath);
846
+ const abs = join5(cwd2, relPath);
760
847
  mkdirSync4(resolve3(abs, ".."), { recursive: true });
761
848
  writeFileSync4(abs, content, "utf8");
762
849
  };
@@ -775,9 +862,9 @@ No tasks in progress.
775
862
  write2(".claude/agents/consultant.md", translateFrontmatterForClaudeCode(agentConsultant({ projectName }), "consultant", consultantModel));
776
863
  write2(".claude/agents/builder.md", translateFrontmatterForClaudeCode(agentBuilder({ projectName, writablePaths }), "builder", builderModel));
777
864
  write2(".claude/agents/reviewer.md", translateFrontmatterForClaudeCode(agentReviewer({ projectName }), "reviewer", reviewerModel));
778
- mergeClaudeMcpJson(join4(cwd2, ".mcp.json"), config.tools.mcp.port);
779
- mergeClaudeSettingsJson(join4(cwd2, ".claude/settings.json"));
780
- mergeClaudeSettingsLocalJson(join4(cwd2, ".claude/settings.local.json"));
865
+ mergeClaudeMcpJson(join5(cwd2, ".mcp.json"), config.tools.mcp.port, detectPackageManager(cwd2));
866
+ mergeClaudeSettingsJson(join5(cwd2, ".claude/settings.json"));
867
+ mergeClaudeSettingsLocalJson(join5(cwd2, ".claude/settings.local.json"));
781
868
  writeSkills(cwd2, ".claude/skills");
782
869
  }
783
870
  async migrate(config, _to, _cwd) {
@@ -792,12 +879,12 @@ No tasks in progress.
792
879
  reviewer: [...MCP_CLAUDE_PERMISSIONS_REVIEWER]
793
880
  };
794
881
  for (const [agent, tools] of Object.entries(AGENT_TOOLS)) {
795
- const filePath = join4(cwd2, ".claude", "agents", `${agent}.md`);
796
- if (!existsSync3(filePath)) {
882
+ const filePath = join5(cwd2, ".claude", "agents", `${agent}.md`);
883
+ if (!existsSync4(filePath)) {
797
884
  console.log(` ${agent}.md not found \u2014 skipping`);
798
885
  continue;
799
886
  }
800
- const content = readFileSync4(filePath, "utf-8");
887
+ const content = readFileSync5(filePath, "utf-8");
801
888
  const updated = content.replace(
802
889
  /(tools:\n)((?: - [^\n]+\n)*)/m,
803
890
  (_match, header, toolsSection) => {
@@ -818,23 +905,23 @@ No tasks in progress.
818
905
  };
819
906
 
820
907
  // src/core/materializer/codex-cli.ts
821
- import { existsSync as existsSync4, mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "fs";
822
- import { join as join5, resolve as resolve4 } from "path";
908
+ import { existsSync as existsSync5, mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "fs";
909
+ import { join as join6, resolve as resolve4 } from "path";
823
910
  var CodexCliMaterializer = class {
824
911
  async scaffold(config, opts) {
825
912
  const { cwd: cwd2 } = opts;
826
913
  const write2 = (relPath, content, mode) => {
827
- const abs = join5(cwd2, relPath);
914
+ const abs = join6(cwd2, relPath);
828
915
  mkdirSync5(resolve4(abs, ".."), { recursive: true });
829
916
  writeFileSync5(abs, content, { encoding: "utf8", mode });
830
917
  };
831
918
  write2("AGENTS.md", agentsMd(config));
832
- if (!existsSync4(join5(cwd2, "health.sh"))) {
919
+ if (!existsSync5(join6(cwd2, "health.sh"))) {
833
920
  write2("health.sh", HEALTH_SH, 493);
834
921
  }
835
922
  const tasks = opts.firstTask ? [{ slug: slugify(opts.firstTask.title), ...opts.firstTask }] : [];
836
- write2(join5(config.storage.dir, "feature_list.json"), featureListJson(tasks));
837
- if (!existsSync4(join5(cwd2, config.storage.markdownFallback.path))) {
923
+ write2(join6(config.storage.dir, "feature_list.json"), featureListJson(tasks));
924
+ if (!existsSync5(join6(cwd2, config.storage.markdownFallback.path))) {
838
925
  write2(
839
926
  config.storage.markdownFallback.path,
840
927
  `<!-- AUTO-GENERATED by agent-harness-kit \u2014 DO NOT EDIT MANUALLY -->
@@ -860,13 +947,13 @@ No tasks in progress.
860
947
  writeAgentFile(cwd2, ".codex/agents/builder.toml", agentBuilderToml({ projectName, writablePaths, model: builderModel }));
861
948
  writeAgentFile(cwd2, ".codex/agents/reviewer.toml", agentReviewerToml({ projectName, model: reviewerModel }));
862
949
  writeAgentFile(cwd2, ".codex/agents/default.toml", agentLeadAsDefaultToml({ projectName, model: leadModel }));
863
- mergeCodexConfigToml(join5(cwd2, ".codex/config.toml"), config.tools.mcp.port);
950
+ mergeCodexConfigToml(join6(cwd2, ".codex/config.toml"), config.tools.mcp.port, detectPackageManager(cwd2));
864
951
  appendGitignore(cwd2);
865
952
  writeSkills(cwd2, ".agents/skills");
866
953
  }
867
954
  async build(config, cwd2) {
868
955
  const write2 = (relPath, content) => {
869
- const abs = join5(cwd2, relPath);
956
+ const abs = join6(cwd2, relPath);
870
957
  mkdirSync5(resolve4(abs, ".."), { recursive: true });
871
958
  writeFileSync5(abs, content, "utf8");
872
959
  };
@@ -885,7 +972,7 @@ No tasks in progress.
885
972
  writeAgentFile(cwd2, ".codex/agents/builder.toml", agentBuilderToml({ projectName, writablePaths, model: builderModel }));
886
973
  writeAgentFile(cwd2, ".codex/agents/reviewer.toml", agentReviewerToml({ projectName, model: reviewerModel }));
887
974
  writeAgentFile(cwd2, ".codex/agents/default.toml", agentLeadAsDefaultToml({ projectName, model: leadModel }));
888
- mergeCodexConfigToml(join5(cwd2, ".codex/config.toml"), config.tools.mcp.port);
975
+ mergeCodexConfigToml(join6(cwd2, ".codex/config.toml"), config.tools.mcp.port, detectPackageManager(cwd2));
889
976
  writeSkills(cwd2, ".agents/skills");
890
977
  }
891
978
  async migrate(config, _to, _cwd) {
@@ -897,23 +984,23 @@ No tasks in progress.
897
984
  };
898
985
 
899
986
  // src/core/materializer/opencode.ts
900
- import { existsSync as existsSync5, mkdirSync as mkdirSync6, writeFileSync as writeFileSync6 } from "fs";
901
- import { join as join6, resolve as resolve5 } from "path";
987
+ import { existsSync as existsSync6, mkdirSync as mkdirSync6, writeFileSync as writeFileSync6 } from "fs";
988
+ import { join as join7, resolve as resolve5 } from "path";
902
989
  var OpenCodeMaterializer = class {
903
990
  async scaffold(config, opts) {
904
991
  const { cwd: cwd2 } = opts;
905
992
  const write2 = (relPath, content, mode) => {
906
- const abs = join6(cwd2, relPath);
993
+ const abs = join7(cwd2, relPath);
907
994
  mkdirSync6(resolve5(abs, ".."), { recursive: true });
908
995
  writeFileSync6(abs, content, { encoding: "utf8", mode });
909
996
  };
910
997
  write2("AGENTS.md", agentsMd(config));
911
- if (!existsSync5(join6(cwd2, "health.sh"))) {
998
+ if (!existsSync6(join7(cwd2, "health.sh"))) {
912
999
  write2("health.sh", HEALTH_SH, 493);
913
1000
  }
914
1001
  const tasks = opts.firstTask ? [{ slug: slugify(opts.firstTask.title), ...opts.firstTask }] : [];
915
- write2(join6(config.storage.dir, "feature_list.json"), featureListJson(tasks));
916
- if (!existsSync5(join6(cwd2, config.storage.markdownFallback.path))) {
1002
+ write2(join7(config.storage.dir, "feature_list.json"), featureListJson(tasks));
1003
+ if (!existsSync6(join7(cwd2, config.storage.markdownFallback.path))) {
917
1004
  write2(
918
1005
  config.storage.markdownFallback.path,
919
1006
  `<!-- AUTO-GENERATED by agent-harness-kit \u2014 DO NOT EDIT MANUALLY -->
@@ -933,13 +1020,13 @@ No tasks in progress.
933
1020
  writeAgentFile(cwd2, ".opencode/agents/consultant.md", translateFrontmatterForOpenCode(agentConsultant({ projectName })));
934
1021
  writeAgentFile(cwd2, ".opencode/agents/builder.md", translateFrontmatterForOpenCode(agentBuilder({ projectName, writablePaths })));
935
1022
  writeAgentFile(cwd2, ".opencode/agents/reviewer.md", translateFrontmatterForOpenCode(agentReviewer({ projectName })));
936
- mergeOpencodeJson(join6(cwd2, "opencode.json"), config.tools.mcp.port);
1023
+ mergeOpencodeJson(join7(cwd2, "opencode.json"), config.tools.mcp.port, detectPackageManager(cwd2));
937
1024
  appendGitignore(cwd2);
938
1025
  writeSkills(cwd2, ".opencode/skills");
939
1026
  }
940
1027
  async build(config, cwd2) {
941
1028
  const write2 = (relPath, content) => {
942
- const abs = join6(cwd2, relPath);
1029
+ const abs = join7(cwd2, relPath);
943
1030
  mkdirSync6(resolve5(abs, ".."), { recursive: true });
944
1031
  writeFileSync6(abs, content, "utf8");
945
1032
  };
@@ -952,7 +1039,7 @@ No tasks in progress.
952
1039
  writeAgentFile(cwd2, ".opencode/agents/consultant.md", translateFrontmatterForOpenCode(agentConsultant({ projectName })));
953
1040
  writeAgentFile(cwd2, ".opencode/agents/builder.md", translateFrontmatterForOpenCode(agentBuilder({ projectName, writablePaths })));
954
1041
  writeAgentFile(cwd2, ".opencode/agents/reviewer.md", translateFrontmatterForOpenCode(agentReviewer({ projectName })));
955
- mergeOpencodeJson(join6(cwd2, "opencode.json"), config.tools.mcp.port);
1042
+ mergeOpencodeJson(join7(cwd2, "opencode.json"), config.tools.mcp.port, detectPackageManager(cwd2));
956
1043
  writeSkills(cwd2, ".opencode/skills");
957
1044
  }
958
1045
  async migrate(config, _to, _cwd) {
@@ -1018,14 +1105,14 @@ async function buildOnce(cwd2) {
1018
1105
  }
1019
1106
 
1020
1107
  // src/commands/dashboard.ts
1021
- import { dirname as dirname5, join as join9, resolve as resolve7 } from "path";
1108
+ import { dirname as dirname4, join as join9, resolve as resolve6 } from "path";
1022
1109
  import { fileURLToPath as fileURLToPath3 } from "url";
1023
1110
  import pc2 from "picocolors";
1024
1111
 
1025
1112
  // src/core/dashboard-server.ts
1026
1113
  import { watch as watch2 } from "fs";
1027
- import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
1028
- import { extname, join as join7 } from "path";
1114
+ import { existsSync as existsSync7, readFileSync as readFileSync6 } from "fs";
1115
+ import { extname, join as join8 } from "path";
1029
1116
  import { serve } from "@hono/node-server";
1030
1117
  import { Hono } from "hono";
1031
1118
  import { WebSocketServer } from "ws";
@@ -1067,7 +1154,7 @@ var MIME = {
1067
1154
  ".ttf": "font/ttf"
1068
1155
  };
1069
1156
  function fileResponse(filePath) {
1070
- const content = readFileSync5(filePath);
1157
+ const content = readFileSync6(filePath);
1071
1158
  const mime = MIME[extname(filePath)] ?? "application/octet-stream";
1072
1159
  return new Response(content, {
1073
1160
  headers: { "Content-Type": mime, "Cache-Control": "no-cache" }
@@ -1173,15 +1260,15 @@ async function startDashboardServer(db, dbPath, staticPath, port) {
1173
1260
  app.get("/*", (c) => {
1174
1261
  const urlPath = c.req.path;
1175
1262
  if (urlPath !== "/") {
1176
- const candidate = join7(staticPath, urlPath);
1177
- if (existsSync6(candidate)) {
1263
+ const candidate = join8(staticPath, urlPath);
1264
+ if (existsSync7(candidate)) {
1178
1265
  try {
1179
1266
  return fileResponse(candidate);
1180
1267
  } catch {
1181
1268
  }
1182
1269
  }
1183
1270
  }
1184
- return fileResponse(join7(staticPath, "index.html"));
1271
+ return fileResponse(join8(staticPath, "index.html"));
1185
1272
  });
1186
1273
  const resolvedPort = await findFreePort(port);
1187
1274
  if (resolvedPort !== port) {
@@ -1212,7 +1299,7 @@ async function startDashboardServer(db, dbPath, staticPath, port) {
1212
1299
  let watcher = null;
1213
1300
  if (dbPath) {
1214
1301
  const walPath = `${dbPath}-wal`;
1215
- const watchTarget = existsSync6(walPath) ? walPath : dbPath;
1302
+ const watchTarget = existsSync7(walPath) ? walPath : dbPath;
1216
1303
  watcher = watch2(watchTarget, broadcast);
1217
1304
  }
1218
1305
  return {
@@ -1226,635 +1313,12 @@ async function startDashboardServer(db, dbPath, staticPath, port) {
1226
1313
  };
1227
1314
  }
1228
1315
 
1229
- // src/core/db.ts
1230
- import { randomUUID } from "crypto";
1231
- import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync7 } from "fs";
1232
- import { dirname as dirname4, join as join8, resolve as resolve6 } from "path";
1233
-
1234
- // src/core/repositories/ActionRepository.ts
1235
- var ActionRepository = class {
1236
- constructor(driver) {
1237
- this.driver = driver;
1238
- }
1239
- driver;
1240
- async create(id, taskId, agent, now) {
1241
- await this.driver.exec(
1242
- `INSERT INTO actions (id, task_id, agent, status, created_at) VALUES (?, ?, ?, 'in_progress', ?)`,
1243
- [id, taskId, agent, now]
1244
- );
1245
- }
1246
- async complete(actionId, summary, now) {
1247
- await this.driver.exec(
1248
- `UPDATE actions SET status = 'completed', completed_at = ?, summary = ? WHERE id = ?`,
1249
- [now, summary, actionId]
1250
- );
1251
- }
1252
- async closeOrphaned(taskId, now) {
1253
- return this.driver.exec(
1254
- `UPDATE actions SET status = 'completed', completed_at = ?, summary = 'Auto-closed: task marked done' WHERE task_id = ? AND status = 'in_progress'`,
1255
- [now, taskId]
1256
- );
1257
- }
1258
- async getById(actionId) {
1259
- return this.driver.queryOne(`SELECT * FROM actions WHERE id = ?`, [actionId]);
1260
- }
1261
- async getForTask(taskId) {
1262
- return this.driver.query(
1263
- `SELECT * FROM actions WHERE task_id = ? ORDER BY created_at`,
1264
- [taskId]
1265
- );
1266
- }
1267
- async getAll() {
1268
- return this.driver.query(`SELECT * FROM actions ORDER BY created_at`);
1269
- }
1270
- async getWithDetails(taskId) {
1271
- const actions = await this.getForTask(taskId);
1272
- return Promise.all(
1273
- actions.map(async (action) => ({
1274
- ...action,
1275
- sections: await this.getSections(action.id),
1276
- files: await this.getFiles(action.id),
1277
- tools: await this.getTools(action.id)
1278
- }))
1279
- );
1280
- }
1281
- // ─── Sections ─────────────────────────────────────────────────────────────
1282
- async addSection(actionId, sectionType, content, now) {
1283
- await this.driver.exec(
1284
- `INSERT INTO action_sections (action_id, section_type, content, created_at) VALUES (?, ?, ?, ?)`,
1285
- [actionId, sectionType, content, now]
1286
- );
1287
- }
1288
- async getSections(actionId) {
1289
- return this.driver.query(
1290
- `SELECT * FROM action_sections WHERE action_id = ? ORDER BY created_at`,
1291
- [actionId]
1292
- );
1293
- }
1294
- async getAllSections() {
1295
- return this.driver.query(`SELECT * FROM action_sections ORDER BY created_at`);
1296
- }
1297
- // ─── Files ────────────────────────────────────────────────────────────────
1298
- async addFile(actionId, filePath, operation, notes) {
1299
- await this.driver.exec(
1300
- `INSERT INTO action_files (action_id, file_path, operation, notes) VALUES (?, ?, ?, ?)`,
1301
- [actionId, filePath, operation, notes]
1302
- );
1303
- }
1304
- async getFiles(actionId) {
1305
- return this.driver.query(
1306
- `SELECT * FROM action_files WHERE action_id = ?`,
1307
- [actionId]
1308
- );
1309
- }
1310
- async getFilesForTask(taskId) {
1311
- return this.driver.query(
1312
- `SELECT af.*, a.agent FROM action_files af JOIN actions a ON af.action_id = a.id WHERE a.task_id = ? ORDER BY a.agent, af.operation`,
1313
- [taskId]
1314
- );
1315
- }
1316
- // ─── Tools ────────────────────────────────────────────────────────────────
1317
- async addTool(actionId, toolName, argsJson, resultSummary, now) {
1318
- await this.driver.exec(
1319
- `INSERT INTO action_tools (action_id, tool_name, args_json, result_summary, called_at) VALUES (?, ?, ?, ?, ?)`,
1320
- [actionId, toolName, argsJson, resultSummary, now]
1321
- );
1322
- }
1323
- async getTools(actionId) {
1324
- return this.driver.query(
1325
- `SELECT * FROM action_tools WHERE action_id = ? ORDER BY called_at`,
1326
- [actionId]
1327
- );
1328
- }
1329
- async getTopTools(limit) {
1330
- return this.driver.query(
1331
- `SELECT tool_name, COUNT(*) as uses FROM action_tools GROUP BY tool_name ORDER BY uses DESC LIMIT ?`,
1332
- [limit]
1333
- );
1334
- }
1335
- };
1336
-
1337
- // src/core/repositories/StatsRepository.ts
1338
- var AGENT_ORDER = ["lead", "explorer", "builder", "reviewer"];
1339
- var StatsRepository = class {
1340
- constructor(driver) {
1341
- this.driver = driver;
1342
- }
1343
- driver;
1344
- async getCounts() {
1345
- const [{ total: totalActions }] = await this.driver.query(
1346
- `SELECT COUNT(*) as total FROM actions`
1347
- );
1348
- const [{ total: totalFiles }] = await this.driver.query(
1349
- `SELECT COUNT(*) as total FROM action_files`
1350
- );
1351
- const [{ total: uniqueTools }] = await this.driver.query(
1352
- `SELECT COUNT(DISTINCT tool_name) as total FROM action_tools`
1353
- );
1354
- const [{ total: activeAgents }] = await this.driver.query(
1355
- `SELECT COUNT(DISTINCT agent) as total FROM actions WHERE status = 'in_progress'`
1356
- );
1357
- return { totalActions, totalFiles, uniqueTools, activeAgents };
1358
- }
1359
- async getRecentTools(limit) {
1360
- return this.driver.query(
1361
- `SELECT at.*, t.id as task_id, t.title as task_title, t.slug as task_slug, a.agent
1362
- FROM action_tools at
1363
- JOIN actions a ON at.action_id = a.id
1364
- JOIN tasks t ON a.task_id = t.id
1365
- ORDER BY at.called_at DESC
1366
- LIMIT ?`,
1367
- [limit]
1368
- );
1369
- }
1370
- async getTopFiles(limit) {
1371
- return this.driver.query(
1372
- `SELECT
1373
- file_path,
1374
- COUNT(*) as total,
1375
- SUM(CASE WHEN operation='read' THEN 1 ELSE 0 END) as read,
1376
- SUM(CASE WHEN operation='created' THEN 1 ELSE 0 END) as created,
1377
- SUM(CASE WHEN operation='modified' THEN 1 ELSE 0 END) as modified,
1378
- SUM(CASE WHEN operation='deleted' THEN 1 ELSE 0 END) as deleted
1379
- FROM action_files
1380
- GROUP BY file_path
1381
- ORDER BY total DESC
1382
- LIMIT ?`,
1383
- [limit]
1384
- );
1385
- }
1386
- async getRecentFiles(limit) {
1387
- return this.driver.query(
1388
- `SELECT af.*, t.id as task_id, t.title as task_title, t.slug as task_slug,
1389
- a.agent, a.created_at as called_at
1390
- FROM action_files af
1391
- JOIN actions a ON af.action_id = a.id
1392
- JOIN tasks t ON a.task_id = t.id
1393
- ORDER BY a.created_at DESC
1394
- LIMIT ?`,
1395
- [limit]
1396
- );
1397
- }
1398
- async getAgentStats() {
1399
- const rows = await this.driver.query(
1400
- `SELECT
1401
- a.agent,
1402
- COUNT(*) as actions_total,
1403
- SUM(CASE WHEN a.status='completed' THEN 1 ELSE 0 END) as actions_done,
1404
- SUM(CASE WHEN a.status='blocked' THEN 1 ELSE 0 END) as actions_blocked,
1405
- COUNT(DISTINCT a.task_id) as tasks_worked,
1406
- COUNT(DISTINCT af.file_path) as files_touched
1407
- FROM actions a
1408
- LEFT JOIN action_files af ON af.action_id = a.id
1409
- GROUP BY a.agent
1410
- ORDER BY actions_total DESC`
1411
- );
1412
- return rows.sort((a, b) => {
1413
- const ai = AGENT_ORDER.indexOf(a.agent);
1414
- const bi = AGENT_ORDER.indexOf(b.agent);
1415
- if (ai === -1 && bi === -1) return 0;
1416
- if (ai === -1) return 1;
1417
- if (bi === -1) return -1;
1418
- return ai - bi;
1419
- });
1420
- }
1421
- async getTimeline(limit) {
1422
- return this.driver.query(
1423
- `SELECT a.*, t.title as task_title, t.slug as task_slug, t.status as task_status
1424
- FROM actions a
1425
- JOIN tasks t ON a.task_id = t.id
1426
- ORDER BY a.created_at DESC
1427
- LIMIT ?`,
1428
- [limit]
1429
- );
1430
- }
1431
- };
1432
-
1433
- // src/core/repositories/TaskRepository.ts
1434
- var TaskRepository = class {
1435
- constructor(driver) {
1436
- this.driver = driver;
1437
- }
1438
- driver;
1439
- async add(params) {
1440
- const now = (/* @__PURE__ */ new Date()).toISOString();
1441
- return this.driver.insert(
1442
- `INSERT INTO tasks (slug, title, description, status, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)`,
1443
- [params.slug, params.title, params.description ?? null, params.status ?? "pending", now, now]
1444
- );
1445
- }
1446
- async addAcceptance(taskId, criteria) {
1447
- for (const criterion of criteria) {
1448
- await this.driver.exec(
1449
- `INSERT INTO task_acceptance (task_id, criterion) VALUES (?, ?)`,
1450
- [taskId, criterion]
1451
- );
1452
- }
1453
- }
1454
- async getAll(status, includeArchived = false) {
1455
- let sql = `SELECT * FROM tasks`;
1456
- const params = [];
1457
- const conditions = [];
1458
- if (!includeArchived) {
1459
- conditions.push(`archived_at IS NULL`);
1460
- }
1461
- if (status) {
1462
- conditions.push(`status = ?`);
1463
- params.push(status);
1464
- }
1465
- if (conditions.length > 0) {
1466
- sql += ` WHERE ${conditions.join(" AND ")}`;
1467
- }
1468
- sql += ` ORDER BY CASE status WHEN 'pending' THEN 1 WHEN 'in_progress' THEN 2 WHEN 'blocked' THEN 3 WHEN 'done' THEN 4 ELSE 5 END, updated_at DESC`;
1469
- return this.driver.query(sql, params);
1470
- }
1471
- async getAllWithAcceptanceCounts(includeArchived = false) {
1472
- let sql = `
1473
- SELECT t.*,
1474
- COUNT(ta.id) as acceptance_total,
1475
- COALESCE(SUM(ta.met), 0) as acceptance_met
1476
- FROM tasks t
1477
- LEFT JOIN task_acceptance ta ON ta.task_id = t.id
1478
- `;
1479
- if (!includeArchived) {
1480
- sql += ` WHERE t.archived_at IS NULL`;
1481
- }
1482
- sql += ` GROUP BY t.id ORDER BY CASE t.status WHEN 'pending' THEN 1 WHEN 'in_progress' THEN 2 WHEN 'blocked' THEN 3 WHEN 'done' THEN 4 ELSE 5 END, t.updated_at DESC`;
1483
- return this.driver.query(sql);
1484
- }
1485
- async getById(id) {
1486
- return this.driver.queryOne(`SELECT * FROM tasks WHERE id = ?`, [id]);
1487
- }
1488
- async getBySlug(slug) {
1489
- return this.driver.queryOne(`SELECT * FROM tasks WHERE slug = ?`, [slug]);
1490
- }
1491
- async getAcceptance(taskId) {
1492
- return this.driver.query(
1493
- `SELECT * FROM task_acceptance WHERE task_id = ?`,
1494
- [taskId]
1495
- );
1496
- }
1497
- async setStatus(id, status, extra) {
1498
- const now = (/* @__PURE__ */ new Date()).toISOString();
1499
- if (extra?.started_at) {
1500
- await this.driver.exec(
1501
- `UPDATE tasks SET status = ?, started_at = ?, updated_at = ? WHERE id = ?`,
1502
- [status, extra.started_at, now, id]
1503
- );
1504
- } else if (extra?.completed_at) {
1505
- await this.driver.exec(
1506
- `UPDATE tasks SET status = ?, completed_at = ?, updated_at = ? WHERE id = ?`,
1507
- [status, extra.completed_at, now, id]
1508
- );
1509
- } else {
1510
- await this.driver.exec(`UPDATE tasks SET status = ?, updated_at = ? WHERE id = ?`, [status, now, id]);
1511
- }
1512
- }
1513
- async update(id, params) {
1514
- const sets = [];
1515
- const vals = [];
1516
- const now = (/* @__PURE__ */ new Date()).toISOString();
1517
- if (params.title !== void 0) {
1518
- sets.push("title = ?");
1519
- vals.push(params.title);
1520
- }
1521
- if (params.description !== void 0) {
1522
- sets.push("description = ?");
1523
- vals.push(params.description);
1524
- }
1525
- if (params.slug !== void 0) {
1526
- sets.push("slug = ?");
1527
- vals.push(params.slug);
1528
- }
1529
- if (sets.length === 0) return;
1530
- sets.push("updated_at = ?");
1531
- vals.push(now);
1532
- vals.push(id);
1533
- await this.driver.exec(`UPDATE tasks SET ${sets.join(", ")} WHERE id = ?`, vals);
1534
- }
1535
- async replaceAcceptance(taskId, criteria) {
1536
- await this.driver.exec(`DELETE FROM task_acceptance WHERE task_id = ?`, [taskId]);
1537
- for (const criterion of criteria) {
1538
- await this.driver.exec(
1539
- `INSERT INTO task_acceptance (task_id, criterion) VALUES (?, ?)`,
1540
- [taskId, criterion]
1541
- );
1542
- }
1543
- }
1544
- async archive(id) {
1545
- const now = (/* @__PURE__ */ new Date()).toISOString();
1546
- await this.driver.exec(`UPDATE tasks SET archived_at = ?, updated_at = ? WHERE id = ?`, [now, now, id]);
1547
- }
1548
- async unarchive(id) {
1549
- const now = (/* @__PURE__ */ new Date()).toISOString();
1550
- await this.driver.exec(`UPDATE tasks SET archived_at = NULL, updated_at = ? WHERE id = ?`, [now, id]);
1551
- }
1552
- async getArchived() {
1553
- return this.driver.query(
1554
- `SELECT * FROM tasks WHERE archived_at IS NOT NULL ORDER BY archived_at DESC`
1555
- );
1556
- }
1557
- async claim(id, agent, now) {
1558
- return this.driver.exec(
1559
- `UPDATE tasks SET status = 'in_progress', assigned_to = ?, started_at = ?, updated_at = ? WHERE id = ? AND status = 'pending'`,
1560
- [agent, now, now, id]
1561
- );
1562
- }
1563
- async markAcceptanceMet(criterionId) {
1564
- await this.driver.exec(`UPDATE task_acceptance SET met = 1 WHERE id = ?`, [criterionId]);
1565
- }
1566
- async getStatusSummary() {
1567
- return this.driver.query(
1568
- `SELECT status, COUNT(*) as total FROM tasks WHERE archived_at IS NULL GROUP BY status`
1569
- );
1570
- }
1571
- };
1572
-
1573
- // src/core/db.ts
1574
- var HarnessDB = class {
1575
- tasks;
1576
- actions;
1577
- stats;
1578
- driver;
1579
- config;
1580
- constructor(driver, config) {
1581
- this.driver = driver;
1582
- this.config = config;
1583
- this.tasks = new TaskRepository(driver);
1584
- this.actions = new ActionRepository(driver);
1585
- this.stats = new StatsRepository(driver);
1586
- }
1587
- // ─── Tasks (public facade — delegates to TaskRepository) ──────────────────
1588
- async addTask(params) {
1589
- const taskId = await this.tasks.add({
1590
- slug: params.slug,
1591
- title: params.title,
1592
- description: params.description
1593
- });
1594
- if (params.acceptance?.length) {
1595
- await this.tasks.addAcceptance(taskId, params.acceptance);
1596
- }
1597
- await this.regenerateCurrentMd();
1598
- return await this.tasks.getById(taskId);
1599
- }
1600
- async getTasks(status, includeArchived = false) {
1601
- return this.tasks.getAll(status, includeArchived);
1602
- }
1603
- async getTaskById(id) {
1604
- return this.tasks.getById(id);
1605
- }
1606
- async getTaskBySlug(slug) {
1607
- return this.tasks.getBySlug(slug);
1608
- }
1609
- async getTaskAcceptance(taskId) {
1610
- return this.tasks.getAcceptance(taskId);
1611
- }
1612
- async updateTaskStatus(idOrSlug, status) {
1613
- const now = (/* @__PURE__ */ new Date()).toISOString();
1614
- const task2 = typeof idOrSlug === "number" ? await this.tasks.getById(idOrSlug) : await this.tasks.getBySlug(idOrSlug);
1615
- if (!task2) throw new Error(`Task not found: ${idOrSlug}`);
1616
- if (status === "in_progress" && !task2.started_at) {
1617
- await this.tasks.setStatus(task2.id, status, { started_at: now });
1618
- } else if (status === "done") {
1619
- await this.tasks.setStatus(task2.id, status, { completed_at: now });
1620
- } else {
1621
- await this.tasks.setStatus(task2.id, status);
1622
- }
1623
- await this.regenerateCurrentMd();
1624
- return await this.tasks.getById(task2.id);
1625
- }
1626
- async claimTask(id, agent) {
1627
- const now = (/* @__PURE__ */ new Date()).toISOString();
1628
- return this.driver.transaction(async (tx) => {
1629
- const txTasks = new TaskRepository(tx);
1630
- const changed = await txTasks.claim(id, agent, now);
1631
- if (!changed) return null;
1632
- const task2 = await txTasks.getById(id);
1633
- if (!task2 || task2.status !== "in_progress" || task2.assigned_to !== agent) return null;
1634
- await this.regenerateCurrentMd();
1635
- return task2;
1636
- });
1637
- }
1638
- async markAcceptanceMet(criterionId) {
1639
- return this.tasks.markAcceptanceMet(criterionId);
1640
- }
1641
- async updateTask(id, params) {
1642
- await this.tasks.update(id, params);
1643
- await this.regenerateCurrentMd();
1644
- return await this.tasks.getById(id);
1645
- }
1646
- async updateTaskAcceptance(taskId, criteria) {
1647
- await this.tasks.replaceAcceptance(taskId, criteria);
1648
- await this.regenerateCurrentMd();
1649
- }
1650
- async archiveTask(id) {
1651
- await this.tasks.archive(id);
1652
- await this.regenerateCurrentMd();
1653
- return await this.tasks.getById(id);
1654
- }
1655
- async unarchiveTask(id) {
1656
- await this.tasks.unarchive(id);
1657
- await this.regenerateCurrentMd();
1658
- return await this.tasks.getById(id);
1659
- }
1660
- async getArchivedTasks() {
1661
- return this.tasks.getArchived();
1662
- }
1663
- async getStatusSummary() {
1664
- return this.tasks.getStatusSummary();
1665
- }
1666
- // ─── Actions (public facade — delegates to ActionRepository) ──────────────
1667
- async startAction(taskId, agent) {
1668
- const id = randomUUID();
1669
- const now = (/* @__PURE__ */ new Date()).toISOString();
1670
- await this.actions.create(id, taskId, agent, now);
1671
- await this.regenerateCurrentMd();
1672
- return await this.actions.getById(id);
1673
- }
1674
- async writeSection(actionId, sectionType, content) {
1675
- const now = (/* @__PURE__ */ new Date()).toISOString();
1676
- await this.actions.addSection(actionId, sectionType, content, now);
1677
- await this.regenerateCurrentMd();
1678
- }
1679
- async completeAction(actionId, summary) {
1680
- const now = (/* @__PURE__ */ new Date()).toISOString();
1681
- await this.actions.complete(actionId, summary, now);
1682
- await this.regenerateCurrentMd();
1683
- return await this.actions.getById(actionId);
1684
- }
1685
- async closeOrphanedActions(taskId) {
1686
- const now = (/* @__PURE__ */ new Date()).toISOString();
1687
- return this.actions.closeOrphaned(taskId, now);
1688
- }
1689
- async getAction(actionId) {
1690
- return this.actions.getById(actionId);
1691
- }
1692
- async getActionsForTask(taskId) {
1693
- return this.actions.getForTask(taskId);
1694
- }
1695
- async getActionSections(actionId) {
1696
- return this.actions.getSections(actionId);
1697
- }
1698
- async recordFile(actionId, filePath, operation, notes) {
1699
- return this.actions.addFile(actionId, filePath, operation, notes ?? null);
1700
- }
1701
- async recordTool(actionId, toolName, argsJson, resultSummary) {
1702
- const now = (/* @__PURE__ */ new Date()).toISOString();
1703
- return this.actions.addTool(actionId, toolName, argsJson ?? null, resultSummary ?? null, now);
1704
- }
1705
- async getFilesForTask(taskId) {
1706
- return this.actions.getFilesForTask(taskId);
1707
- }
1708
- async getTopTools(limit = 10) {
1709
- return this.actions.getTopTools(limit);
1710
- }
1711
- // ─── current.md fallback ──────────────────────────────────────────────────
1712
- async regenerateCurrentMd() {
1713
- if (!this.config.storage.markdownFallback.enabled) return;
1714
- const mdPath = resolve6(this.config.storage.markdownFallback.path);
1715
- mkdirSync7(dirname4(mdPath), { recursive: true });
1716
- const inProgress = await this.tasks.getAll("in_progress");
1717
- const now = (/* @__PURE__ */ new Date()).toISOString();
1718
- let md = `<!-- AUTO-GENERATED by agent-harness-kit \u2014 DO NOT EDIT MANUALLY -->
1719
- `;
1720
- md += `<!-- Last updated: ${now} -->
1721
-
1722
- `;
1723
- md += `# Current Session
1724
-
1725
- `;
1726
- if (inProgress.length === 0) {
1727
- md += `## No tasks in progress
1728
-
1729
- `;
1730
- const pending = await this.tasks.getAll("pending");
1731
- if (pending.length > 0) {
1732
- md += `### Next pending tasks
1733
- `;
1734
- for (const t of pending.slice(0, 5)) {
1735
- md += `- **#${t.id}** ${t.title} (\`${t.slug}\`)
1736
- `;
1737
- }
1738
- }
1739
- } else {
1740
- for (const task2 of inProgress) {
1741
- md += `## Active Task
1742
- `;
1743
- md += `- **ID:** ${task2.id}
1744
- `;
1745
- md += `- **Slug:** ${task2.slug}
1746
- `;
1747
- md += `- **Status:** ${task2.status}
1748
- `;
1749
- md += `- **Started:** ${task2.started_at ?? "unknown"}
1750
-
1751
- `;
1752
- const taskActions = await this.actions.getForTask(task2.id);
1753
- if (taskActions.length > 0) {
1754
- md += `## Actions this session
1755
- `;
1756
- md += `| Agent | Status | Summary | Started |
1757
- `;
1758
- md += `|----------|-------------|----------------------------------|-------------|
1759
- `;
1760
- for (const a of taskActions) {
1761
- const started = a.created_at.slice(11, 16);
1762
- const summary = (a.summary ?? "").slice(0, 34).padEnd(34);
1763
- md += `| ${a.agent.padEnd(8)} | ${a.status.padEnd(11)} | ${summary} | ${started} |
1764
- `;
1765
- }
1766
- md += `
1767
- `;
1768
- }
1769
- const acceptance = await this.tasks.getAcceptance(task2.id);
1770
- if (acceptance.length > 0) {
1771
- md += `## Acceptance Criteria
1772
- `;
1773
- for (const a of acceptance) {
1774
- md += `- [${a.met ? "x" : " "}] ${a.criterion}
1775
- `;
1776
- }
1777
- md += `
1778
- `;
1779
- }
1780
- }
1781
- }
1782
- writeFileSync7(mdPath, md, "utf8");
1783
- }
1784
- // ─── Raw query escape hatch ───────────────────────────────────────────────
1785
- async queryRaw(sql, ...params) {
1786
- return this.driver.query(sql, params);
1787
- }
1788
- // ─── Export helpers ───────────────────────────────────────────────────────
1789
- async exportJson() {
1790
- return {
1791
- tasks: await this.tasks.getAll(void 0, true),
1792
- actions: await this.actions.getAll(),
1793
- sections: await this.actions.getAllSections()
1794
- };
1795
- }
1796
- async reconnect() {
1797
- await this.driver.reconnect();
1798
- }
1799
- async close() {
1800
- await this.driver.close();
1801
- }
1802
- // ─── feature_list.json sync ───────────────────────────────────────────────
1803
- async syncFromFeatureList(seeds) {
1804
- let added = 0;
1805
- let skipped = 0;
1806
- for (const t of seeds) {
1807
- if (await this.tasks.getBySlug(t.slug)) {
1808
- skipped++;
1809
- continue;
1810
- }
1811
- await this.addTask(t);
1812
- added++;
1813
- }
1814
- return { added, skipped };
1815
- }
1816
- async writeFeatureList(cwd2) {
1817
- const allTasks = await this.tasks.getAll(void 0, true);
1818
- const list = await Promise.all(
1819
- allTasks.map(async (t) => ({
1820
- slug: t.slug,
1821
- title: t.title,
1822
- description: t.description ?? void 0,
1823
- acceptance: (await this.tasks.getAcceptance(t.id)).map((a) => a.criterion),
1824
- status: t.status
1825
- }))
1826
- );
1827
- const path = join8(resolve6(cwd2), this.config.storage.dir, "feature_list.json");
1828
- mkdirSync7(dirname4(path), { recursive: true });
1829
- writeFileSync7(path, JSON.stringify(list, null, 2) + "\n", "utf8");
1830
- }
1831
- };
1832
- async function openDB(config, cwd2) {
1833
- const dbConfig = config.database;
1834
- let driver;
1835
- if (dbConfig.type === "postgres") {
1836
- const { PostgresDriver } = await import("./postgres-IOQE32DM.js");
1837
- driver = new PostgresDriver(dbConfig);
1838
- } else if (dbConfig.type === "mysql") {
1839
- const { MySQLDriver } = await import("./mysql-THKQOXIS.js");
1840
- driver = new MySQLDriver(dbConfig);
1841
- } else {
1842
- const { SQLiteDriver } = await import("./sqlite-KWYK4IJW.js");
1843
- if (dbConfig.type !== "sqlite") {
1844
- throw new Error("Invalid database type");
1845
- }
1846
- driver = new SQLiteDriver(resolve6(cwd2, dbConfig.path));
1847
- }
1848
- await driver.ensureSchema();
1849
- return new HarnessDB(driver, config);
1850
- }
1851
-
1852
1316
  // src/commands/dashboard.ts
1853
- var __dirname3 = dirname5(fileURLToPath3(import.meta.url));
1317
+ var __dirname3 = dirname4(fileURLToPath3(import.meta.url));
1854
1318
  async function runDashboard(cwd2, opts) {
1855
1319
  const config = await loadConfig(cwd2);
1856
1320
  const db = await openDB(config, cwd2);
1857
- const dbPath = config.database.type === "sqlite" ? resolve7(cwd2, config.database.path) : null;
1321
+ const dbPath = config.database.type === "sqlite" ? resolve6(cwd2, config.database.path) : null;
1858
1322
  const staticPath = join9(__dirname3, "dashboard-dist");
1859
1323
  const { url } = await startDashboardServer(db, dbPath, staticPath, opts.port);
1860
1324
  console.log(pc2.green(`\u2713`) + ` Dashboard running at ${pc2.bold(pc2.cyan(url))}`);
@@ -1875,29 +1339,37 @@ async function runDashboard(cwd2, opts) {
1875
1339
  import pc3 from "picocolors";
1876
1340
 
1877
1341
  // src/core/doctor.ts
1878
- import { existsSync as existsSync8, readFileSync as readFileSync6 } from "fs";
1879
- import { dirname as dirname7, join as join11 } from "path";
1342
+ import { existsSync as existsSync9, readFileSync as readFileSync7 } from "fs";
1343
+ import { homedir } from "os";
1344
+ import { dirname as dirname6, join as join11 } from "path";
1880
1345
  import { fileURLToPath as fileURLToPath5 } from "url";
1881
1346
 
1882
1347
  // src/core/package-data.ts
1883
- import { existsSync as existsSync7 } from "fs";
1348
+ import { existsSync as existsSync8 } from "fs";
1884
1349
  import { createRequire } from "module";
1885
- import { dirname as dirname6, join as join10 } from "path";
1350
+ import { dirname as dirname5, join as join10 } from "path";
1886
1351
  import { fileURLToPath as fileURLToPath4 } from "url";
1887
1352
  var require2 = createRequire(import.meta.url);
1888
- var here = dirname6(fileURLToPath4(import.meta.url));
1353
+ var here = dirname5(fileURLToPath4(import.meta.url));
1889
1354
  var candidates = [join10(here, "..", "..", "package.json"), join10(here, "..", "package.json")];
1890
- var pkgPath = candidates.find((p8) => existsSync7(p8)) ?? candidates[0];
1355
+ var pkgPath = candidates.find((p8) => existsSync8(p8)) ?? candidates[0];
1891
1356
  var pkg = require2(pkgPath);
1892
1357
 
1893
1358
  // src/core/doctor.ts
1894
1359
  var REGISTRY_URL = `https://registry.npmjs.org/${pkg.name}/latest`;
1895
1360
  var TIMEOUT_MS = 2e3;
1361
+ var LIB_VERSION_CACHE_TTL_MS = 5 * 60 * 1e3;
1896
1362
  var AGENT_NAMES = ["lead", "explorer", "consultant", "builder", "reviewer"];
1897
1363
  var SKILL_NAMES = ["ahk-ask", "ahk-consultant", "ahk-triage", "ahk-review"];
1898
- var __dirname4 = dirname7(fileURLToPath5(import.meta.url));
1364
+ var __dirname4 = dirname6(fileURLToPath5(import.meta.url));
1365
+ var libVersionCache = null;
1899
1366
  async function checkLibVersion() {
1900
1367
  const current = pkg.version;
1368
+ if (libVersionCache && Date.now() - libVersionCache.fetchedAt < LIB_VERSION_CACHE_TTL_MS) {
1369
+ if (libVersionCache.status.current === current) {
1370
+ return libVersionCache.status;
1371
+ }
1372
+ }
1901
1373
  try {
1902
1374
  const controller = new AbortController();
1903
1375
  const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
@@ -1906,9 +1378,13 @@ async function checkLibVersion() {
1906
1378
  const data = await res.json();
1907
1379
  const latest = data.version;
1908
1380
  const outdated = isNewer(latest, current);
1909
- return { current, latest, outdated };
1381
+ const status = { current, latest, outdated };
1382
+ libVersionCache = { status, fetchedAt: Date.now() };
1383
+ return status;
1910
1384
  } catch {
1911
- return { current, latest: null, outdated: false };
1385
+ const status = { current, latest: null, outdated: false };
1386
+ libVersionCache = { status, fetchedAt: Date.now() };
1387
+ return status;
1912
1388
  }
1913
1389
  }
1914
1390
  function isNewer(latest, current) {
@@ -1962,15 +1438,14 @@ function generateExpectedAgentContent(agentName, provider, vars) {
1962
1438
  };
1963
1439
  return tomlFns[agentName]();
1964
1440
  }
1965
- function checkAgentFiles(cwd2, provider, projectName, allowedPaths, writablePaths, models) {
1966
- const { agentsDir, ext } = getProviderAgentInfo(provider);
1441
+ function checkAgentFilesAtRoot(agentsRoot, ext, provider, projectName, allowedPaths, writablePaths, models) {
1967
1442
  return AGENT_NAMES.map((name) => {
1968
- const filePath = join11(cwd2, agentsDir, `${name}${ext}`);
1969
- if (!existsSync8(filePath)) {
1443
+ const filePath = join11(agentsRoot, `${name}${ext}`);
1444
+ if (!existsSync9(filePath)) {
1970
1445
  return { name, status: "missing" };
1971
1446
  }
1972
1447
  try {
1973
- const live = readFileSync6(filePath, "utf8");
1448
+ const live = readFileSync7(filePath, "utf8");
1974
1449
  const expected = generateExpectedAgentContent(name, provider, {
1975
1450
  projectName,
1976
1451
  allowedPaths,
@@ -1983,6 +1458,18 @@ function checkAgentFiles(cwd2, provider, projectName, allowedPaths, writablePath
1983
1458
  }
1984
1459
  });
1985
1460
  }
1461
+ function checkAgentFiles(cwd2, provider, projectName, allowedPaths, writablePaths, models) {
1462
+ const { agentsDir, ext } = getProviderAgentInfo(provider);
1463
+ return checkAgentFilesAtRoot(
1464
+ join11(cwd2, agentsDir),
1465
+ ext,
1466
+ provider,
1467
+ projectName,
1468
+ allowedPaths,
1469
+ writablePaths,
1470
+ models
1471
+ );
1472
+ }
1986
1473
  function getProviderSkillsDir(provider) {
1987
1474
  switch (provider) {
1988
1475
  case "claude-code":
@@ -1995,24 +1482,76 @@ function getProviderSkillsDir(provider) {
1995
1482
  return ".claude/skills";
1996
1483
  }
1997
1484
  }
1998
- function checkSkills(cwd2, provider) {
1999
- const skillsDir = getProviderSkillsDir(provider);
1485
+ function checkSkillsAtRoot(skillsRoot) {
2000
1486
  const skillSourceBase = join11(__dirname4, "skills");
2001
1487
  return SKILL_NAMES.map((name) => {
2002
- const livePath = join11(cwd2, skillsDir, name, "SKILL.md");
1488
+ const livePath = join11(skillsRoot, name, "SKILL.md");
2003
1489
  const sourcePath = join11(skillSourceBase, name, "SKILL.md");
2004
- if (!existsSync8(livePath)) {
1490
+ if (!existsSync9(livePath)) {
2005
1491
  return { name, status: "missing" };
2006
1492
  }
2007
1493
  try {
2008
- const live = readFileSync6(livePath, "utf8");
2009
- const source = readFileSync6(sourcePath, "utf8");
1494
+ const live = readFileSync7(livePath, "utf8");
1495
+ const source = readFileSync7(sourcePath, "utf8");
2010
1496
  return { name, status: live === source ? "ok" : "outdated" };
2011
1497
  } catch {
2012
1498
  return { name, status: "outdated" };
2013
1499
  }
2014
1500
  });
2015
1501
  }
1502
+ function checkSkills(cwd2, provider) {
1503
+ const skillsDir = getProviderSkillsDir(provider);
1504
+ return checkSkillsAtRoot(join11(cwd2, skillsDir));
1505
+ }
1506
+ function getGlobalProviderAgentDir(provider, homeDir) {
1507
+ switch (provider) {
1508
+ case "claude-code":
1509
+ return { agentsDir: join11(homeDir, ".claude", "agents"), ext: ".md" };
1510
+ case "opencode":
1511
+ return { agentsDir: join11(homeDir, ".config", "opencode", "agents"), ext: ".md" };
1512
+ case "codex-cli":
1513
+ return { agentsDir: join11(homeDir, ".codex", "agents"), ext: ".toml" };
1514
+ default:
1515
+ return { agentsDir: join11(homeDir, ".claude", "agents"), ext: ".md" };
1516
+ }
1517
+ }
1518
+ function getGlobalProviderSkillsDir(provider, homeDir) {
1519
+ switch (provider) {
1520
+ case "claude-code":
1521
+ return join11(homeDir, ".claude", "skills");
1522
+ case "opencode":
1523
+ return join11(homeDir, ".config", "opencode", "skills");
1524
+ case "codex-cli":
1525
+ return join11(homeDir, ".agents", "skills");
1526
+ default:
1527
+ return join11(homeDir, ".claude", "skills");
1528
+ }
1529
+ }
1530
+ async function getGlobalDoctorStatus(provider, config, homeDir = homedir()) {
1531
+ const projectName = config.project.name;
1532
+ const allowedPaths = (config.agents.explorer.allowedPaths ?? []).join(", ");
1533
+ const writablePaths = (config.agents.builder.writablePaths ?? []).join(", ");
1534
+ const models = {
1535
+ lead: config.agents.lead.model,
1536
+ explorer: config.agents.explorer.model,
1537
+ consultant: config.agents.consultant?.model,
1538
+ builder: config.agents.builder.model,
1539
+ reviewer: config.agents.reviewer.model
1540
+ };
1541
+ const { agentsDir, ext } = getGlobalProviderAgentDir(provider, homeDir);
1542
+ const agents = checkAgentFilesAtRoot(
1543
+ agentsDir,
1544
+ ext,
1545
+ provider,
1546
+ projectName,
1547
+ allowedPaths,
1548
+ writablePaths,
1549
+ models
1550
+ );
1551
+ const skillsDir = getGlobalProviderSkillsDir(provider, homeDir);
1552
+ const skills = checkSkillsAtRoot(skillsDir);
1553
+ return { agents, skills };
1554
+ }
2016
1555
  async function getDoctorStatus(cwd2) {
2017
1556
  const lib = await checkLibVersion();
2018
1557
  let config;
@@ -2126,7 +1665,7 @@ async function runDoctor(cwd2) {
2126
1665
  }
2127
1666
 
2128
1667
  // src/commands/export.ts
2129
- import { writeFileSync as writeFileSync8 } from "fs";
1668
+ import { writeFileSync as writeFileSync7 } from "fs";
2130
1669
  import pc4 from "picocolors";
2131
1670
  async function runExport(cwd2, opts) {
2132
1671
  if (!opts.sql && !opts.json) {
@@ -2140,7 +1679,7 @@ async function runExport(cwd2, opts) {
2140
1679
  const data = await db.exportJson();
2141
1680
  const out = JSON.stringify(data, null, 2) + "\n";
2142
1681
  if (opts.output) {
2143
- writeFileSync8(opts.output, out, "utf8");
1682
+ writeFileSync7(opts.output, out, "utf8");
2144
1683
  console.log(pc4.green(`\u2713 Exported JSON \u2192 ${opts.output}`));
2145
1684
  } else {
2146
1685
  process.stdout.write(out);
@@ -2157,8 +1696,8 @@ async function runExport(cwd2, opts) {
2157
1696
 
2158
1697
  // src/commands/health.ts
2159
1698
  import { spawnSync } from "child_process";
2160
- import { existsSync as existsSync9 } from "fs";
2161
- import { join as join12, resolve as resolve8 } from "path";
1699
+ import { existsSync as existsSync10 } from "fs";
1700
+ import { join as join12, resolve as resolve7 } from "path";
2162
1701
  import pc5 from "picocolors";
2163
1702
  function checkLine(label, ok3, message, indent = 0) {
2164
1703
  const prefix = label ? pc5.cyan(`[${label}] `) : " ".repeat(indent);
@@ -2176,8 +1715,8 @@ async function runHealth(cwd2) {
2176
1715
  let allOk = true;
2177
1716
  let dbOk;
2178
1717
  if (config.database.type === "sqlite") {
2179
- const dbPath = resolve8(cwd2, config.database.path);
2180
- dbOk = existsSync9(dbPath);
1718
+ const dbPath = resolve7(cwd2, config.database.path);
1719
+ dbOk = existsSync10(dbPath);
2181
1720
  checkLine("checking DB", dbOk, `${config.database.path} reachable`);
2182
1721
  } else {
2183
1722
  dbOk = true;
@@ -2191,7 +1730,7 @@ async function runHealth(cwd2) {
2191
1730
  for (let i = 0; i < agentNames.length; i++) {
2192
1731
  const name = agentNames[i];
2193
1732
  const agentPath = join12(cwd2, agentsDir, `${name}${providerFiles.agentExtension}`);
2194
- const ok3 = existsSync9(agentPath);
1733
+ const ok3 = existsSync10(agentPath);
2195
1734
  checkLine(
2196
1735
  i === 0 ? "checking agents" : null,
2197
1736
  ok3,
@@ -2202,8 +1741,8 @@ async function runHealth(cwd2) {
2202
1741
  }
2203
1742
  if (config.tools.mcp.enabled) {
2204
1743
  const mcpFile = providerFiles.mcpFile;
2205
- const mcpPath = resolve8(cwd2, mcpFile);
2206
- const mcpOk = existsSync9(mcpPath);
1744
+ const mcpPath = resolve7(cwd2, mcpFile);
1745
+ const mcpOk = existsSync10(mcpPath);
2207
1746
  checkLine("checking MCP", mcpOk, `${mcpFile} valid`);
2208
1747
  if (!mcpOk) allOk = false;
2209
1748
  }
@@ -2212,8 +1751,8 @@ async function runHealth(cwd2) {
2212
1751
  console.error(pc5.red("\u2717 Harness checks failed \u2014 fix the above before running health.sh"));
2213
1752
  process.exit(1);
2214
1753
  }
2215
- const scriptPath = resolve8(cwd2, config.health.scriptPath);
2216
- if (!existsSync9(scriptPath)) {
1754
+ const scriptPath = resolve7(cwd2, config.health.scriptPath);
1755
+ if (!existsSync10(scriptPath)) {
2217
1756
  console.error(pc5.red(`\u2717 health.sh not found: ${scriptPath}`));
2218
1757
  console.error(" Run ahk init first.");
2219
1758
  process.exit(1);
@@ -2249,11 +1788,56 @@ function getProviderHealthFiles(provider) {
2249
1788
  }
2250
1789
 
2251
1790
  // src/commands/init.ts
2252
- import { mkdirSync as mkdirSync8, writeFileSync as writeFileSync9 } from "fs";
1791
+ import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync8 } from "fs";
2253
1792
  import { join as join14 } from "path";
2254
1793
  import * as p3 from "@clack/prompts";
2255
1794
  import pc7 from "picocolors";
2256
1795
 
1796
+ // src/core/materializer/global-sync.ts
1797
+ import { homedir as homedir2 } from "os";
1798
+ async function syncGlobalAgentsAndSkills(config, provider, homeDir = homedir2()) {
1799
+ const status = await getGlobalDoctorStatus(provider, config, homeDir);
1800
+ const missingAgents = status.agents.filter((a) => a.status === "missing");
1801
+ const missingSkills = status.skills.filter((s) => s.status === "missing");
1802
+ if (missingAgents.length === 0 && missingSkills.length === 0) {
1803
+ return { alreadySynced: true, createdAgents: [], createdSkills: [] };
1804
+ }
1805
+ const projectName = config.project.name;
1806
+ const allowedPaths = (config.agents.explorer.allowedPaths ?? []).join(", ");
1807
+ const writablePaths = (config.agents.builder.writablePaths ?? []).join(", ");
1808
+ const models = {
1809
+ lead: config.agents.lead.model,
1810
+ explorer: config.agents.explorer.model,
1811
+ consultant: config.agents.consultant?.model,
1812
+ builder: config.agents.builder.model,
1813
+ reviewer: config.agents.reviewer.model
1814
+ };
1815
+ const createdAgents = [];
1816
+ if (missingAgents.length > 0) {
1817
+ const { agentsDir, ext } = getGlobalProviderAgentDir(provider, homeDir);
1818
+ for (const agent of missingAgents) {
1819
+ const name = agent.name;
1820
+ const content = generateExpectedAgentContent(name, provider, {
1821
+ projectName,
1822
+ allowedPaths,
1823
+ writablePaths,
1824
+ model: models[name]
1825
+ });
1826
+ writeAgentFile(agentsDir, `${name}${ext}`, content);
1827
+ createdAgents.push(name);
1828
+ }
1829
+ }
1830
+ const createdSkills = [];
1831
+ if (missingSkills.length > 0) {
1832
+ const skillsDir = getGlobalProviderSkillsDir(provider, homeDir);
1833
+ for (const skill of missingSkills) {
1834
+ writeSkill(skillsDir, skill.name);
1835
+ createdSkills.push(skill.name);
1836
+ }
1837
+ }
1838
+ return { alreadySynced: false, createdAgents, createdSkills };
1839
+ }
1840
+
2257
1841
  // src/schema/init.ts
2258
1842
  import * as v from "valibot";
2259
1843
  var initNameSchema = v.pipe(
@@ -2305,14 +1889,15 @@ var cliFormWithRetry = async (formFn, schema) => {
2305
1889
  };
2306
1890
 
2307
1891
  // src/commands/init-helpers.ts
2308
- import { existsSync as existsSync10, readFileSync as readFileSync7 } from "fs";
1892
+ import { randomUUID } from "crypto";
1893
+ import { existsSync as existsSync11, readFileSync as readFileSync8 } from "fs";
2309
1894
  import { join as join13 } from "path";
2310
1895
  import pc6 from "picocolors";
2311
1896
  function readProjectNameFromPackageJson(cwd2) {
2312
1897
  try {
2313
1898
  const pkgPath2 = join13(cwd2, "package.json");
2314
- if (!existsSync10(pkgPath2)) return null;
2315
- const content = readFileSync7(pkgPath2, "utf8");
1899
+ if (!existsSync11(pkgPath2)) return null;
1900
+ const content = readFileSync8(pkgPath2, "utf8");
2316
1901
  const pkg2 = JSON.parse(content);
2317
1902
  const name = pkg2?.name;
2318
1903
  if (typeof name === "string" && name.trim()) return name.trim();
@@ -2323,10 +1908,10 @@ function readProjectNameFromPackageJson(cwd2) {
2323
1908
  }
2324
1909
  function detectConfigExtension(cwd2) {
2325
1910
  try {
2326
- if (existsSync10(join13(cwd2, "tsconfig.json"))) return "ts";
1911
+ if (existsSync11(join13(cwd2, "tsconfig.json"))) return "ts";
2327
1912
  const pkgPath2 = join13(cwd2, "package.json");
2328
- if (!existsSync10(pkgPath2)) return "mjs";
2329
- const pkg2 = JSON.parse(readFileSync7(pkgPath2, "utf8"));
1913
+ if (!existsSync11(pkgPath2)) return "mjs";
1914
+ const pkg2 = JSON.parse(readFileSync8(pkgPath2, "utf8"));
2330
1915
  if (pkg2?.type === "module") return "mjs";
2331
1916
  } catch {
2332
1917
  }
@@ -2369,7 +1954,9 @@ function applyConfigDefaults(params) {
2369
1954
  blockers: true,
2370
1955
  nextSteps: false
2371
1956
  },
2372
- markdownFallback: { enabled: true, path: ".harness/current.md" }
1957
+ markdownFallback: { enabled: true, path: ".harness/current.md" },
1958
+ scope: params.scope ?? "local",
1959
+ projectId: params.projectId ?? randomUUID()
2373
1960
  },
2374
1961
  health: {
2375
1962
  scriptPath: "./health.sh",
@@ -2553,6 +2140,24 @@ async function runInit(cwd2, flags) {
2553
2140
  return val;
2554
2141
  }, initDocsSchema);
2555
2142
  }
2143
+ let storageScope;
2144
+ if (flags.storageScope && ["local", "global"].includes(flags.storageScope)) {
2145
+ storageScope = flags.storageScope;
2146
+ } else {
2147
+ const val = await p3.select({
2148
+ message: "Storage scope",
2149
+ options: [
2150
+ { value: "local", label: "Local \u2014 .harness/harness.db lives in this project" },
2151
+ { value: "global", label: "Global \u2014 DB lives under ~/.harness/dbs/<projectId>/, outside the project" }
2152
+ ],
2153
+ initialValue: "local"
2154
+ });
2155
+ if (p3.isCancel(val)) {
2156
+ p3.cancel("Cancelled.");
2157
+ process.exit(0);
2158
+ }
2159
+ storageScope = val;
2160
+ }
2556
2161
  let tasksAdapter;
2557
2162
  if (flags.tasks && ["local", "jira", "linear"].includes(flags.tasks)) {
2558
2163
  tasksAdapter = flags.tasks;
@@ -2607,10 +2212,19 @@ async function runInit(cwd2, flags) {
2607
2212
  firstTask = { title: taskTitle, description: taskDesc, acceptance };
2608
2213
  }
2609
2214
  let configExt = "ts";
2215
+ let globalSyncResult = null;
2610
2216
  const spinner6 = p3.spinner();
2611
2217
  spinner6.start("Scaffolding...");
2612
2218
  try {
2613
- const config = applyConfigDefaults({ name, description, provider, docsPath, tasksAdapter, models: modelOverrides });
2219
+ const config = applyConfigDefaults({
2220
+ name,
2221
+ description,
2222
+ provider,
2223
+ docsPath,
2224
+ tasksAdapter,
2225
+ models: modelOverrides,
2226
+ scope: storageScope
2227
+ });
2614
2228
  const materializer = getMaterializer(provider);
2615
2229
  const installDir = cwd2;
2616
2230
  configExt = detectConfigExtension(cwd2);
@@ -2623,12 +2237,18 @@ async function runInit(cwd2, flags) {
2623
2237
  docsPath,
2624
2238
  tasksAdapter,
2625
2239
  port: config.tools.mcp.port,
2626
- models: modelOverrides
2240
+ models: modelOverrides,
2241
+ scope: config.storage.scope,
2242
+ projectId: config.storage.projectId
2627
2243
  });
2628
- writeFileSync9(join14(installDir, configFileName), configContent, "utf8");
2629
- mkdirSync8(join14(installDir, config.storage.dir), { recursive: true });
2244
+ writeFileSync8(join14(installDir, configFileName), configContent, "utf8");
2245
+ mkdirSync7(join14(installDir, config.storage.dir), { recursive: true });
2630
2246
  const db = await openDB(config, installDir);
2247
+ await db.writeStorageState(installDir);
2631
2248
  await materializer.scaffold(config, { cwd: installDir, firstTask });
2249
+ if (config.storage.scope === "global") {
2250
+ globalSyncResult = await syncGlobalAgentsAndSkills(config, provider);
2251
+ }
2632
2252
  if (firstTask) {
2633
2253
  const slug = slugify(firstTask.title);
2634
2254
  await db.addTask({
@@ -2652,14 +2272,28 @@ async function runInit(cwd2, flags) {
2652
2272
  console.log(pc7.green(`\u2713 agent-harness-kit.config.${configExt}`));
2653
2273
  console.log(pc7.green("\u2713 AGENTS.md"));
2654
2274
  console.log(pc7.green("\u2713 health.sh"));
2655
- console.log(pc7.green("\u2713 .harness/harness.db"));
2656
- console.log(pc7.green("\u2713 .harness/current.md"));
2275
+ console.log(pc7.green(storageScope === "global" ? "\u2713 ~/.harness/dbs/<projectId>/harness.db" : "\u2713 .harness/harness.db"));
2276
+ console.log(pc7.green(storageScope === "global" ? "\u2713 ~/.harness/dbs/<projectId>/current.md" : "\u2713 .harness/current.md"));
2277
+ console.log(pc7.green("\u2713 .harness/storage-state.json"));
2657
2278
  console.log(pc7.green(`\u2713 ${agentsDir}lead.md`));
2658
2279
  console.log(pc7.green(`\u2713 ${agentsDir}explorer.md`));
2659
2280
  console.log(pc7.green(`\u2713 ${agentsDir}builder.md`));
2660
2281
  console.log(pc7.green(`\u2713 ${agentsDir}reviewer.md`));
2661
2282
  console.log(pc7.green(`\u2713 ${mcpFile}`));
2662
2283
  console.log(pc7.green("\u2713 .gitignore entries added"));
2284
+ if (globalSyncResult) {
2285
+ console.log("");
2286
+ if (globalSyncResult.alreadySynced) {
2287
+ console.log(pc7.dim("\u2713 Global agents/skills already synced \u2014 skipped"));
2288
+ } else {
2289
+ for (const name2 of globalSyncResult.createdAgents) {
2290
+ console.log(pc7.green(`\u2713 ~global agent added: ${name2}`));
2291
+ }
2292
+ for (const name2 of globalSyncResult.createdSkills) {
2293
+ console.log(pc7.green(`\u2713 ~global skill added: ${name2}`));
2294
+ }
2295
+ }
2296
+ }
2663
2297
  console.log("");
2664
2298
  console.log(pc7.cyan("\u2192") + ` Edit ${pc7.cyan("health.sh")} with your project checks`);
2665
2299
  console.log(pc7.cyan("\u2192") + ` ${pc7.cyan("ahk task add")} to queue work for agents`);
@@ -2716,17 +2350,251 @@ async function runMigrate(cwd2, opts) {
2716
2350
  }
2717
2351
  }
2718
2352
 
2353
+ // src/commands/migrate-storage.ts
2354
+ import { copyFileSync, existsSync as existsSync12, mkdirSync as mkdirSync8, rmSync, writeFileSync as writeFileSync9 } from "fs";
2355
+ import { homedir as homedir3 } from "os";
2356
+ import { dirname as dirname7, join as join15, resolve as resolve8 } from "path";
2357
+ import pc9 from "picocolors";
2358
+ function log5(msg) {
2359
+ console.log(msg);
2360
+ }
2361
+ function fail(msg) {
2362
+ throw new Error(msg);
2363
+ }
2364
+ function currentMdPathForScope(scope, config, cwd2, homeDir) {
2365
+ return scope === "global" ? join15(resolveGlobalStorageDir(config, homeDir), "current.md") : resolve8(cwd2, config.storage.markdownFallback.path);
2366
+ }
2367
+ async function backupDestination(cwd2, storageDir, data) {
2368
+ const backupsDir = resolve8(cwd2, storageDir, "backups");
2369
+ const path = join15(backupsDir, `pre-migrate-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.json`);
2370
+ try {
2371
+ mkdirSync8(backupsDir, { recursive: true });
2372
+ writeFileSync9(path, JSON.stringify(data, null, 2) + "\n", "utf8");
2373
+ } catch (err) {
2374
+ throw new Error(
2375
+ `Could not write destination backup to ${path} (${err instanceof Error ? err.message : String(err)}). Aborting migration WITHOUT touching the destination \u2014 nothing was overwritten.`
2376
+ );
2377
+ }
2378
+ return path;
2379
+ }
2380
+ function copySqliteFile(srcPath, destPath) {
2381
+ mkdirSync8(dirname7(destPath), { recursive: true });
2382
+ copyFileSync(srcPath, destPath);
2383
+ for (const suffix of ["-wal", "-shm"]) {
2384
+ if (existsSync12(`${srcPath}${suffix}`)) {
2385
+ copyFileSync(`${srcPath}${suffix}`, `${destPath}${suffix}`);
2386
+ }
2387
+ }
2388
+ if (!existsSync12(destPath)) {
2389
+ throw new Error(`Copy verification failed: ${destPath} does not exist after copy.`);
2390
+ }
2391
+ }
2392
+ async function runMigrateStorage(cwd2, opts, homeDir = homedir3()) {
2393
+ const config = await loadConfig(cwd2);
2394
+ const storageDir = config.storage.dir;
2395
+ const state = readStorageStateFile(cwd2, storageDir);
2396
+ let realScope;
2397
+ let realDbType;
2398
+ if (!state) {
2399
+ const defaultSqlitePath = config.database.type === "sqlite" ? config.database.path : ".harness/harness.db";
2400
+ const localPath = resolveSqlitePathForScope("local", defaultSqlitePath, cwd2, config, homeDir);
2401
+ const globalPath = resolveSqlitePathForScope("global", defaultSqlitePath, cwd2, config, homeDir);
2402
+ const localCount = await probeTaskCount(localPath);
2403
+ const globalCount = await probeTaskCount(globalPath);
2404
+ if (localCount > 0 && globalCount > 0) {
2405
+ fail(
2406
+ `storage-state.json is missing and BOTH candidate locations have data \u2014 local (${localPath}): ${localCount} task(s); global (${globalPath}): ${globalCount} task(s). Refusing to guess which one is authoritative. Resolve manually (inspect both databases) or delete the one that should be discarded, then re-run this command.`
2407
+ );
2408
+ }
2409
+ if (localCount === 0 && globalCount === 0) {
2410
+ const db = await openDB(config, cwd2, homeDir);
2411
+ try {
2412
+ await db.writeStorageState(cwd2);
2413
+ } finally {
2414
+ await db.close();
2415
+ }
2416
+ log5(pc9.dim("storage-state.json was missing; no data found at either candidate location. Nothing to migrate \u2014 state recorded."));
2417
+ return;
2418
+ }
2419
+ realScope = localCount > 0 ? "local" : "global";
2420
+ realDbType = "sqlite";
2421
+ log5(
2422
+ pc9.yellow(
2423
+ `storage-state.json was missing. Detected real data at ${realScope} sqlite location (${realScope === "local" ? localPath : globalPath}) \u2014 using it as the migration source.`
2424
+ )
2425
+ );
2426
+ } else {
2427
+ realScope = state.scope;
2428
+ realDbType = state.dbType;
2429
+ }
2430
+ const desiredScope = config.storage.scope;
2431
+ const desiredDbType = config.database.type;
2432
+ if (realScope === desiredScope && realDbType === desiredDbType) {
2433
+ log5(pc9.green(`\u2713 Storage already matches config (scope=${desiredScope}, database=${desiredDbType}) \u2014 nothing to migrate.`));
2434
+ return;
2435
+ }
2436
+ if (realDbType !== "sqlite") {
2437
+ fail(
2438
+ `Cannot auto-locate the previous ${realDbType} database \u2014 storage-state.json does not retain connection credentials for security. Manually run "ahk export --json" while still connected to the old database (with the old config), then adjust agent-harness-kit.config.ts and re-import. This direction is out of scope for "ahk migrate storage".`
2439
+ );
2440
+ }
2441
+ if (desiredDbType === "sqlite" && realDbType === "sqlite") {
2442
+ return migrateScopeOnly(cwd2, config, homeDir, realScope, desiredScope, opts);
2443
+ }
2444
+ return migrateAcrossDbType(cwd2, config, homeDir, realScope, opts);
2445
+ }
2446
+ async function probeTaskCount(dbPath) {
2447
+ if (!existsSync12(dbPath)) return 0;
2448
+ const { SQLiteDriver } = await import("./sqlite-TR4D324R.js");
2449
+ const driver = new SQLiteDriver(dbPath);
2450
+ try {
2451
+ await driver.ensureSchema();
2452
+ const counts = await getRowCounts(driver);
2453
+ return counts.tasks;
2454
+ } finally {
2455
+ await driver.close();
2456
+ }
2457
+ }
2458
+ async function migrateScopeOnly(cwd2, config, homeDir, fromScope, toScope, opts) {
2459
+ const sqlitePath = config.database.type === "sqlite" ? config.database.path : ".harness/harness.db";
2460
+ const srcDb = resolveSqlitePathForScope(fromScope, sqlitePath, cwd2, config, homeDir);
2461
+ const destDb = resolveSqlitePathForScope(toScope, sqlitePath, cwd2, config, homeDir);
2462
+ const srcMd = currentMdPathForScope(fromScope, config, cwd2, homeDir);
2463
+ const destMd = currentMdPathForScope(toScope, config, cwd2, homeDir);
2464
+ if (!existsSync12(srcDb)) {
2465
+ fail(`Source database not found at ${srcDb} (expected ${fromScope} scope) \u2014 nothing to move.`);
2466
+ }
2467
+ const destExists = existsSync12(destDb);
2468
+ if (destExists) {
2469
+ const { SQLiteDriver } = await import("./sqlite-TR4D324R.js");
2470
+ const destDriver = new SQLiteDriver(destDb);
2471
+ let destEmpty;
2472
+ try {
2473
+ await destDriver.ensureSchema();
2474
+ destEmpty = await isEmptyDatabase(destDriver);
2475
+ } finally {
2476
+ await destDriver.close();
2477
+ }
2478
+ if (!destEmpty && !opts.force) {
2479
+ fail(
2480
+ `Destination (${toScope}, ${destDb}) already has data. Re-run with --force to overwrite it (a backup of the destination will be written first).`
2481
+ );
2482
+ }
2483
+ if (!destEmpty && opts.force) {
2484
+ const { SQLiteDriver: Driver } = await import("./sqlite-TR4D324R.js");
2485
+ const backupDriver = new Driver(destDb);
2486
+ let data;
2487
+ try {
2488
+ await backupDriver.ensureSchema();
2489
+ const { HarnessDB } = await import("./db-QQ7BR5K7.js");
2490
+ const tmpDb = new HarnessDB(backupDriver, { ...config, storage: { ...config.storage, scope: toScope } }, homeDir);
2491
+ data = await tmpDb.exportJson();
2492
+ } finally {
2493
+ await backupDriver.close();
2494
+ }
2495
+ const backupPath = await backupDestination(cwd2, config.storage.dir, data);
2496
+ log5(pc9.yellow(` Backed up existing destination data \u2192 ${backupPath}`));
2497
+ }
2498
+ }
2499
+ if (opts.dryRun) {
2500
+ log5(pc9.dim(`[dry-run] Would copy ${srcDb} \u2192 ${destDb} (scope ${fromScope} \u2192 ${toScope}), and move current.md.`));
2501
+ return;
2502
+ }
2503
+ copySqliteFile(srcDb, destDb);
2504
+ log5(pc9.green(`\u2713 Copied database ${srcDb} \u2192 ${destDb}`));
2505
+ if (existsSync12(srcMd)) {
2506
+ mkdirSync8(dirname7(destMd), { recursive: true });
2507
+ copyFileSync(srcMd, destMd);
2508
+ log5(pc9.green(`\u2713 Copied current.md ${srcMd} \u2192 ${destMd}`));
2509
+ }
2510
+ rmSync(srcDb, { force: true });
2511
+ rmSync(`${srcDb}-wal`, { force: true });
2512
+ rmSync(`${srcDb}-shm`, { force: true });
2513
+ if (existsSync12(srcMd) && srcMd !== destMd) rmSync(srcMd, { force: true });
2514
+ const db = await openDB(config, cwd2, homeDir);
2515
+ try {
2516
+ await db.writeStorageState(cwd2);
2517
+ } finally {
2518
+ await db.close();
2519
+ }
2520
+ log5(pc9.green(`\u2713 Storage migrated: scope ${fromScope} \u2192 ${toScope}`));
2521
+ }
2522
+ async function migrateAcrossDbType(cwd2, config, homeDir, sourceScope, opts) {
2523
+ const sqlitePath = config.database.type === "sqlite" ? config.database.path : ".harness/harness.db";
2524
+ const srcPath = resolveSqlitePathForScope(sourceScope, sqlitePath, cwd2, config, homeDir);
2525
+ if (!existsSync12(srcPath)) {
2526
+ fail(`Source sqlite database not found at ${srcPath} (expected ${sourceScope} scope) \u2014 nothing to migrate.`);
2527
+ }
2528
+ const { SQLiteDriver } = await import("./sqlite-TR4D324R.js");
2529
+ const srcDriver = new SQLiteDriver(srcPath);
2530
+ let sourceData;
2531
+ let sourceCounts;
2532
+ try {
2533
+ await srcDriver.ensureSchema();
2534
+ sourceCounts = await getRowCounts(srcDriver);
2535
+ const { HarnessDB } = await import("./db-QQ7BR5K7.js");
2536
+ const srcDb = new HarnessDB(srcDriver, { ...config, storage: { ...config.storage, scope: sourceScope }, database: { type: "sqlite", path: sqlitePath } }, homeDir);
2537
+ sourceData = await srcDb.exportJson();
2538
+ } finally {
2539
+ await srcDriver.close();
2540
+ }
2541
+ let destDb;
2542
+ try {
2543
+ destDb = await openDB(config, cwd2, homeDir);
2544
+ } catch (err) {
2545
+ fail(`Could not connect to destination (${config.database.type}): ${err instanceof Error ? err.message : String(err)}. Verify database configuration.`);
2546
+ }
2547
+ try {
2548
+ const destCounts = await destDb.getRowCounts();
2549
+ const destEmpty = Object.values(destCounts).every((n) => n === 0);
2550
+ const sourceEmpty = Object.values(sourceCounts).every((n) => n === 0);
2551
+ if (!destEmpty) {
2552
+ if (!opts.force) {
2553
+ const bothHaveData = !sourceEmpty;
2554
+ fail(
2555
+ bothHaveData ? `Both source (${sourceCounts.tasks} task(s)) and destination (${config.database.type}, ${destCounts.tasks} task(s)) have data that DIVERGE \u2014 this is not a first-time migration. Refusing to auto-merge. Review both manually, or re-run with --force to overwrite the destination (a JSON backup will be written first).` : `Destination (${config.database.type}) already has data (${destCounts.tasks} task(s)). Re-run with --force to overwrite it (a backup of the destination will be written first).`
2556
+ );
2557
+ }
2558
+ }
2559
+ if (opts.dryRun) {
2560
+ log5(
2561
+ pc9.dim(
2562
+ `[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)"}.`
2563
+ )
2564
+ );
2565
+ return;
2566
+ }
2567
+ let backupPath = null;
2568
+ if (!destEmpty && opts.force) {
2569
+ const currentDestData = await destDb.exportJson();
2570
+ backupPath = await backupDestination(cwd2, config.storage.dir, currentDestData);
2571
+ log5(pc9.yellow(` Backed up existing destination data \u2192 ${backupPath}`));
2572
+ }
2573
+ await destDb.importFullExport(sourceData, config.database.type, { truncateFirst: !destEmpty });
2574
+ await destDb.writeStorageState(cwd2);
2575
+ log5(
2576
+ pc9.green(
2577
+ `\u2713 Migrated ${sourceData.tasks.length} task(s), ${sourceData.actions.length} action(s) from sqlite (${sourceScope}) \u2192 ${config.database.type}.`
2578
+ )
2579
+ );
2580
+ if (backupPath) log5(pc9.dim(` Destination backup: ${backupPath}`));
2581
+ log5(pc9.yellow(` Note: the original sqlite file at ${srcPath} was NOT deleted \u2014 remove it manually once you've verified the migration.`));
2582
+ } finally {
2583
+ await destDb.close();
2584
+ }
2585
+ }
2586
+
2719
2587
  // src/commands/reset.ts
2720
- import { existsSync as existsSync11, readdirSync, rmSync } from "fs";
2721
- import { join as join15, resolve as resolve9 } from "path";
2588
+ import { existsSync as existsSync13, readdirSync, rmSync as rmSync2 } from "fs";
2589
+ import { join as join16, resolve as resolve9 } from "path";
2722
2590
  import * as p5 from "@clack/prompts";
2723
- import pc9 from "picocolors";
2591
+ import pc10 from "picocolors";
2724
2592
  var AGENT_MD_FILES = ["lead", "explorer", "consultant", "builder", "reviewer"];
2725
2593
  async function resetAgentMds(cwd2, provider) {
2726
2594
  const agentDir = provider === "claude-code" ? ".claude/agents" : ".opencode/agents";
2727
2595
  const agentDirPath = resolve9(cwd2, agentDir);
2728
- if (!existsSync11(agentDirPath)) {
2729
- console.log(pc9.yellow(` Skipping agent files \u2014 directory not found: ${agentDirPath}`));
2596
+ if (!existsSync13(agentDirPath)) {
2597
+ console.log(pc10.yellow(` Skipping agent files \u2014 directory not found: ${agentDirPath}`));
2730
2598
  return;
2731
2599
  }
2732
2600
  const existingFiles = [];
@@ -2738,11 +2606,11 @@ async function resetAgentMds(cwd2, provider) {
2738
2606
  }
2739
2607
  }
2740
2608
  } catch {
2741
- console.log(pc9.yellow(` Skipping agent files \u2014 ${agentDirPath} is not readable`));
2609
+ console.log(pc10.yellow(` Skipping agent files \u2014 ${agentDirPath} is not readable`));
2742
2610
  return;
2743
2611
  }
2744
2612
  if (existingFiles.length === 0) {
2745
- console.log(pc9.yellow(` No agent MD files found in ${agentDir}/`));
2613
+ console.log(pc10.yellow(` No agent MD files found in ${agentDir}/`));
2746
2614
  return;
2747
2615
  }
2748
2616
  for (const file of existingFiles) {
@@ -2751,19 +2619,19 @@ async function resetAgentMds(cwd2, provider) {
2751
2619
  initialValue: true
2752
2620
  });
2753
2621
  if (p5.isCancel(confirm3)) {
2754
- console.log(pc9.red(" Cancelled by user."));
2622
+ console.log(pc10.red(" Cancelled by user."));
2755
2623
  return;
2756
2624
  }
2757
2625
  if (confirm3) {
2758
2626
  try {
2759
- const filePath = join15(agentDirPath, file);
2760
- rmSync(filePath, { force: true });
2761
- console.log(pc9.green(` Removed ${file}`));
2627
+ const filePath = join16(agentDirPath, file);
2628
+ rmSync2(filePath, { force: true });
2629
+ console.log(pc10.green(` Removed ${file}`));
2762
2630
  } catch {
2763
- console.error(pc9.red(` Failed to remove ${file}`));
2631
+ console.error(pc10.red(` Failed to remove ${file}`));
2764
2632
  }
2765
2633
  } else {
2766
- console.log(pc9.cyan(` Skipped ${file}`));
2634
+ console.log(pc10.cyan(` Skipped ${file}`));
2767
2635
  }
2768
2636
  }
2769
2637
  }
@@ -2772,7 +2640,7 @@ async function runReset(cwd2, opts) {
2772
2640
  try {
2773
2641
  config = await loadConfig(cwd2);
2774
2642
  } catch {
2775
- console.error(pc9.red("\u2717 No agent-harness-kit.config found. Run: ahk init"));
2643
+ console.error(pc10.red("\u2717 No agent-harness-kit.config found. Run: ahk init"));
2776
2644
  process.exit(1);
2777
2645
  }
2778
2646
  const storageDir = config.storage.dir || ".harness";
@@ -2781,12 +2649,12 @@ async function runReset(cwd2, opts) {
2781
2649
  let resetDb = false;
2782
2650
  let resetFeatureList = false;
2783
2651
  let resetAgentMdsFlag = false;
2784
- if (dbPath && existsSync11(dbPath)) {
2652
+ if (dbPath && existsSync13(dbPath)) {
2785
2653
  if (opts.force) {
2786
2654
  resetDb = true;
2787
2655
  } else {
2788
2656
  if (config.database.type !== "sqlite") {
2789
- console.log(pc9.yellow(` Skipping DB reset \u2014 database type "${config.database.type}" is not managed by this command.`));
2657
+ console.log(pc10.yellow(` Skipping DB reset \u2014 database type "${config.database.type}" is not managed by this command.`));
2790
2658
  resetDb = false;
2791
2659
  } else {
2792
2660
  const confirm3 = await p5.confirm({
@@ -2794,16 +2662,16 @@ async function runReset(cwd2, opts) {
2794
2662
  initialValue: true
2795
2663
  });
2796
2664
  if (p5.isCancel(confirm3)) {
2797
- console.log(pc9.red(" Cancelled by user."));
2665
+ console.log(pc10.red(" Cancelled by user."));
2798
2666
  return;
2799
2667
  }
2800
2668
  resetDb = confirm3;
2801
2669
  }
2802
2670
  }
2803
2671
  } else if (!dbPath) {
2804
- console.log(pc9.dim(` Skipping DB reset \u2014 remote ${config.database.type} database is not managed by this command.`));
2672
+ console.log(pc10.dim(` Skipping DB reset \u2014 remote ${config.database.type} database is not managed by this command.`));
2805
2673
  }
2806
- if (existsSync11(featureListPath)) {
2674
+ if (existsSync13(featureListPath)) {
2807
2675
  if (opts.force) {
2808
2676
  resetFeatureList = true;
2809
2677
  } else {
@@ -2812,7 +2680,7 @@ async function runReset(cwd2, opts) {
2812
2680
  initialValue: true
2813
2681
  });
2814
2682
  if (p5.isCancel(confirm3)) {
2815
- console.log(pc9.red(" Cancelled by user."));
2683
+ console.log(pc10.red(" Cancelled by user."));
2816
2684
  return;
2817
2685
  }
2818
2686
  resetFeatureList = confirm3;
@@ -2823,20 +2691,20 @@ async function runReset(cwd2, opts) {
2823
2691
  }
2824
2692
  if (resetDb && dbPath) {
2825
2693
  try {
2826
- rmSync(dbPath, { force: true });
2827
- rmSync(`${dbPath}-wal`, { force: true });
2828
- rmSync(`${dbPath}-shm`, { force: true });
2829
- console.log(pc9.green(` \u2713 Removed ${dbPath}`));
2694
+ rmSync2(dbPath, { force: true });
2695
+ rmSync2(`${dbPath}-wal`, { force: true });
2696
+ rmSync2(`${dbPath}-shm`, { force: true });
2697
+ console.log(pc10.green(` \u2713 Removed ${dbPath}`));
2830
2698
  } catch {
2831
- console.error(pc9.red(` \u2717 Failed to remove ${dbPath}`));
2699
+ console.error(pc10.red(` \u2717 Failed to remove ${dbPath}`));
2832
2700
  }
2833
2701
  }
2834
2702
  if (resetFeatureList) {
2835
2703
  try {
2836
- rmSync(featureListPath, { force: true });
2837
- console.log(pc9.green(` \u2713 Removed ${storageDir}/feature_list.json`));
2704
+ rmSync2(featureListPath, { force: true });
2705
+ console.log(pc10.green(` \u2713 Removed ${storageDir}/feature_list.json`));
2838
2706
  } catch {
2839
- console.error(pc9.red(` \u2717 Failed to remove ${featureListPath}`));
2707
+ console.error(pc10.red(` \u2717 Failed to remove ${featureListPath}`));
2840
2708
  }
2841
2709
  }
2842
2710
  if (resetAgentMdsFlag) {
@@ -2844,16 +2712,16 @@ async function runReset(cwd2, opts) {
2844
2712
  await resetAgentMds(cwd2, opts.provider || "claude-code");
2845
2713
  }
2846
2714
  if (!resetDb && !resetFeatureList && !resetAgentMdsFlag) {
2847
- console.log(pc9.yellow(" Nothing to reset (all items missing or skipped)."));
2715
+ console.log(pc10.yellow(" Nothing to reset (all items missing or skipped)."));
2848
2716
  return;
2849
2717
  }
2850
2718
  console.log("");
2851
- console.log(pc9.green('\u2713 Reset complete. Run "ahk init" to scaffold a fresh harness.'));
2719
+ console.log(pc10.green('\u2713 Reset complete. Run "ahk init" to scaffold a fresh harness.'));
2852
2720
  }
2853
2721
 
2854
2722
  // src/core/mcp-server.ts
2855
- import { existsSync as existsSync13, mkdirSync as mkdirSync9, readdirSync as readdirSync2, readFileSync as readFileSync9, statSync, writeFileSync as writeFileSync10 } from "fs";
2856
- import { join as join17, resolve as resolve10 } from "path";
2723
+ import { existsSync as existsSync15, mkdirSync as mkdirSync9, readdirSync as readdirSync2, readFileSync as readFileSync10, statSync, writeFileSync as writeFileSync10 } from "fs";
2724
+ import { join as join18, resolve as resolve10 } from "path";
2857
2725
  import { Server } from "@modelcontextprotocol/sdk/server";
2858
2726
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
2859
2727
  import {
@@ -2862,8 +2730,8 @@ import {
2862
2730
  } from "@modelcontextprotocol/sdk/types.js";
2863
2731
 
2864
2732
  // src/core/permissions-check.ts
2865
- import { existsSync as existsSync12, readFileSync as readFileSync8 } from "fs";
2866
- import { join as join16 } from "path";
2733
+ import { existsSync as existsSync14, readFileSync as readFileSync9 } from "fs";
2734
+ import { join as join17 } from "path";
2867
2735
  var CANONICAL = {
2868
2736
  lead: [...MCP_CLAUDE_PERMISSIONS_LEAD],
2869
2737
  explorer: [...MCP_CLAUDE_PERMISSIONS_EXPLORER],
@@ -2886,14 +2754,14 @@ function checkPermissionsSync(cwd2, config) {
2886
2754
  const agents = {};
2887
2755
  let in_sync = true;
2888
2756
  for (const agent of ["lead", "explorer", "consultant", "builder", "reviewer"]) {
2889
- const filePath = join16(cwd2, ".claude", "agents", `${agent}.md`);
2890
- if (!existsSync12(filePath)) {
2757
+ const filePath = join17(cwd2, ".claude", "agents", `${agent}.md`);
2758
+ if (!existsSync14(filePath)) {
2891
2759
  const missing2 = CANONICAL[agent];
2892
2760
  agents[agent] = { ok: false, missing: missing2, extra: [] };
2893
2761
  in_sync = false;
2894
2762
  continue;
2895
2763
  }
2896
- const content = readFileSync8(filePath, "utf-8");
2764
+ const content = readFileSync9(filePath, "utf-8");
2897
2765
  const installed = parseToolsFromFrontmatter(content);
2898
2766
  const canonical = CANONICAL[agent];
2899
2767
  const missing = canonical.filter((t) => !installed.includes(t));
@@ -3313,19 +3181,19 @@ async function dispatch(name, args, db, docsPath, cwd2, config) {
3313
3181
  return ok2(JSON.stringify(result, null, 2));
3314
3182
  }
3315
3183
  case "deps.snapshot": {
3316
- const pkgPath2 = join17(cwd2, "package.json");
3317
- if (!existsSync13(pkgPath2)) {
3184
+ const pkgPath2 = join18(cwd2, "package.json");
3185
+ if (!existsSync15(pkgPath2)) {
3318
3186
  return ok2("package.json not found in project root", true);
3319
3187
  }
3320
- const pkg2 = JSON.parse(readFileSync9(pkgPath2, "utf8"));
3188
+ const pkg2 = JSON.parse(readFileSync10(pkgPath2, "utf8"));
3321
3189
  const snapshot = {
3322
3190
  capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
3323
3191
  dependencies: pkg2.dependencies ?? {},
3324
3192
  devDependencies: pkg2.devDependencies ?? {}
3325
3193
  };
3326
- const harnessDir = join17(cwd2, ".harness");
3194
+ const harnessDir = join18(cwd2, ".harness");
3327
3195
  mkdirSync9(harnessDir, { recursive: true });
3328
- writeFileSync10(join17(harnessDir, "deps-lock.json"), JSON.stringify(snapshot, null, 2), "utf8");
3196
+ writeFileSync10(join18(harnessDir, "deps-lock.json"), JSON.stringify(snapshot, null, 2), "utf8");
3329
3197
  return ok2(
3330
3198
  JSON.stringify({
3331
3199
  message: "Snapshot saved to .harness/deps-lock.json",
@@ -3334,12 +3202,12 @@ async function dispatch(name, args, db, docsPath, cwd2, config) {
3334
3202
  );
3335
3203
  }
3336
3204
  case "deps.check": {
3337
- const pkgPath2 = join17(cwd2, "package.json");
3338
- const lockPath = join17(cwd2, ".harness", "deps-lock.json");
3339
- if (!existsSync13(pkgPath2)) {
3205
+ const pkgPath2 = join18(cwd2, "package.json");
3206
+ const lockPath = join18(cwd2, ".harness", "deps-lock.json");
3207
+ if (!existsSync15(pkgPath2)) {
3340
3208
  return ok2("package.json not found in project root", true);
3341
3209
  }
3342
- if (!existsSync13(lockPath)) {
3210
+ if (!existsSync15(lockPath)) {
3343
3211
  return ok2(
3344
3212
  JSON.stringify({
3345
3213
  status: "no-snapshot",
@@ -3347,8 +3215,8 @@ async function dispatch(name, args, db, docsPath, cwd2, config) {
3347
3215
  })
3348
3216
  );
3349
3217
  }
3350
- const pkg2 = JSON.parse(readFileSync9(pkgPath2, "utf8"));
3351
- const lock = JSON.parse(readFileSync9(lockPath, "utf8"));
3218
+ const pkg2 = JSON.parse(readFileSync10(pkgPath2, "utf8"));
3219
+ const lock = JSON.parse(readFileSync10(lockPath, "utf8"));
3352
3220
  const current = { ...pkg2.dependencies ?? {}, ...pkg2.devDependencies ?? {} };
3353
3221
  const previous = { ...lock.dependencies ?? {}, ...lock.devDependencies ?? {} };
3354
3222
  const added = [];
@@ -3415,7 +3283,7 @@ function searchDocs(docsPath, query, maxResults = 10) {
3415
3283
  for (const file of files) {
3416
3284
  if (results.length >= maxResults) break;
3417
3285
  try {
3418
- const content = readFileSync9(file, "utf8");
3286
+ const content = readFileSync10(file, "utf8");
3419
3287
  const lines = content.split("\n");
3420
3288
  for (let i = 0; i < lines.length; i++) {
3421
3289
  const lower = lines[i].toLowerCase();
@@ -3440,7 +3308,7 @@ function collectMarkdownFiles(dir) {
3440
3308
  const files = [];
3441
3309
  try {
3442
3310
  for (const entry of readdirSync2(dir)) {
3443
- const full = join17(dir, entry);
3311
+ const full = join18(dir, entry);
3444
3312
  const stat = statSync(full);
3445
3313
  if (stat.isDirectory()) {
3446
3314
  files.push(...collectMarkdownFiles(full));
@@ -3491,12 +3359,12 @@ async function runServe(cwd2, opts) {
3491
3359
 
3492
3360
  // src/commands/status.ts
3493
3361
  import Table from "cli-table3";
3494
- import pc10 from "picocolors";
3362
+ import pc11 from "picocolors";
3495
3363
  var STATUS_COLOR = {
3496
- pending: (s) => pc10.dim(s),
3497
- in_progress: (s) => pc10.cyan(s),
3498
- done: (s) => pc10.green(s),
3499
- blocked: (s) => pc10.red(s)
3364
+ pending: (s) => pc11.dim(s),
3365
+ in_progress: (s) => pc11.cyan(s),
3366
+ done: (s) => pc11.green(s),
3367
+ blocked: (s) => pc11.red(s)
3500
3368
  };
3501
3369
  async function runStatus(cwd2, opts) {
3502
3370
  const config = await loadConfig(cwd2);
@@ -3517,11 +3385,11 @@ async function runStatus(cwd2, opts) {
3517
3385
  return;
3518
3386
  }
3519
3387
  if (tasks.length === 0) {
3520
- console.log(pc10.dim("No tasks yet. Run: ahk task add"));
3388
+ console.log(pc11.dim("No tasks yet. Run: ahk task add"));
3521
3389
  return;
3522
3390
  }
3523
3391
  const table = new Table({
3524
- head: ["ID", "Slug", "Title", "Status", "Assigned", "Started"].map((h) => pc10.bold(h)),
3392
+ head: ["ID", "Slug", "Title", "Status", "Assigned", "Started"].map((h) => pc11.bold(h)),
3525
3393
  style: { head: [], border: [] }
3526
3394
  });
3527
3395
  for (const t of tasks) {
@@ -3539,12 +3407,12 @@ async function runStatus(cwd2, opts) {
3539
3407
  const inProgress = tasks.filter((t) => t.status === "in_progress");
3540
3408
  if (inProgress.length > 0) {
3541
3409
  console.log("");
3542
- console.log(pc10.bold("Active actions:"));
3410
+ console.log(pc11.bold("Active actions:"));
3543
3411
  for (const t of inProgress) {
3544
3412
  const actions = await db.getActionsForTask(t.id);
3545
3413
  const active = actions.filter((a) => a.status === "in_progress");
3546
3414
  for (const a of active) {
3547
- console.log(` ${pc10.cyan(a.agent.padEnd(10))} \u2192 task #${t.id} ${t.slug}`);
3415
+ console.log(` ${pc11.cyan(a.agent.padEnd(10))} \u2192 task #${t.id} ${t.slug}`);
3548
3416
  }
3549
3417
  }
3550
3418
  }
@@ -3553,10 +3421,10 @@ async function runStatus(cwd2, opts) {
3553
3421
  const fn = STATUS_COLOR[s.status] ?? ((x) => x);
3554
3422
  return `${fn(s.status)}: ${s.total}`;
3555
3423
  });
3556
- console.log(pc10.dim("Tasks \u2014 ") + parts.join(pc10.dim(" | ")));
3424
+ console.log(pc11.dim("Tasks \u2014 ") + parts.join(pc11.dim(" | ")));
3557
3425
  const archivedTasks = await db.getArchivedTasks();
3558
3426
  if (archivedTasks.length > 0) {
3559
- console.log(pc10.dim(`${archivedTasks.length} archived (use \`ahk task list --archived\` to view)`));
3427
+ console.log(pc11.dim(`${archivedTasks.length} archived (use \`ahk task list --archived\` to view)`));
3560
3428
  }
3561
3429
  } finally {
3562
3430
  await db.close();
@@ -3564,13 +3432,13 @@ async function runStatus(cwd2, opts) {
3564
3432
  }
3565
3433
 
3566
3434
  // src/commands/sync.ts
3567
- import { existsSync as existsSync14, readFileSync as readFileSync10 } from "fs";
3568
- import { join as join18, resolve as resolve11 } from "path";
3569
- import pc11 from "picocolors";
3435
+ import { existsSync as existsSync16, readFileSync as readFileSync11 } from "fs";
3436
+ import { join as join19, resolve as resolve11 } from "path";
3437
+ import pc12 from "picocolors";
3570
3438
  async function runSync(cwd2, opts) {
3571
3439
  const config = await loadConfig(cwd2);
3572
3440
  const direction = opts.direction ?? "both";
3573
- const featureListPath = resolve11(join18(cwd2, config.storage.dir, "feature_list.json"));
3441
+ const featureListPath = resolve11(join19(cwd2, config.storage.dir, "feature_list.json"));
3574
3442
  const db = await openDB(config, cwd2);
3575
3443
  try {
3576
3444
  if (direction === "in" || direction === "both") {
@@ -3584,44 +3452,44 @@ async function runSync(cwd2, opts) {
3584
3452
  }
3585
3453
  }
3586
3454
  async function syncIn(featureListPath, db, dryRun) {
3587
- if (!existsSync14(featureListPath)) {
3588
- console.log(pc11.dim(`feature_list.json not found at ${featureListPath} \u2014 skipping in-sync`));
3455
+ if (!existsSync16(featureListPath)) {
3456
+ console.log(pc12.dim(`feature_list.json not found at ${featureListPath} \u2014 skipping in-sync`));
3589
3457
  return;
3590
3458
  }
3591
3459
  let seeds;
3592
3460
  try {
3593
- seeds = JSON.parse(readFileSync10(featureListPath, "utf8"));
3461
+ seeds = JSON.parse(readFileSync11(featureListPath, "utf8"));
3594
3462
  } catch (err) {
3595
- console.error(pc11.red(`Failed to parse feature_list.json: ${err}`));
3463
+ console.error(pc12.red(`Failed to parse feature_list.json: ${err}`));
3596
3464
  process.exit(1);
3597
3465
  }
3598
3466
  if (dryRun) {
3599
- console.log(pc11.bold("Dry run \u2014 in-sync (feature_list.json \u2192 SQLite):"));
3467
+ console.log(pc12.bold("Dry run \u2014 in-sync (feature_list.json \u2192 SQLite):"));
3600
3468
  for (const t of seeds) {
3601
3469
  const existing = await db.getTaskBySlug(t.slug);
3602
- console.log(` ${existing ? pc11.dim("skip") : pc11.green("add ")} ${t.slug}`);
3470
+ console.log(` ${existing ? pc12.dim("skip") : pc12.green("add ")} ${t.slug}`);
3603
3471
  }
3604
3472
  return;
3605
3473
  }
3606
3474
  const result = await db.syncFromFeatureList(seeds);
3607
- console.log(pc11.green(`\u2713 In-sync: ${result.added} added, ${result.skipped} already existed`));
3475
+ console.log(pc12.green(`\u2713 In-sync: ${result.added} added, ${result.skipped} already existed`));
3608
3476
  }
3609
3477
  async function syncOut(db, cwd2, dryRun) {
3610
3478
  if (dryRun) {
3611
3479
  const tasks = await db.getTasks();
3612
- console.log(pc11.bold("Dry run \u2014 out-sync (SQLite \u2192 feature_list.json):"));
3480
+ console.log(pc12.bold("Dry run \u2014 out-sync (SQLite \u2192 feature_list.json):"));
3613
3481
  console.log(` ${tasks.length} tasks would be written`);
3614
3482
  return;
3615
3483
  }
3616
3484
  await db.writeFeatureList(cwd2);
3617
- console.log(pc11.green("\u2713 Out-sync: feature_list.json updated"));
3485
+ console.log(pc12.green("\u2713 Out-sync: feature_list.json updated"));
3618
3486
  }
3619
3487
 
3620
3488
  // src/commands/task/add.ts
3621
3489
  import * as p6 from "@clack/prompts";
3622
- import pc12 from "picocolors";
3490
+ import pc13 from "picocolors";
3623
3491
  async function runTaskAdd(cwd2) {
3624
- p6.intro(pc12.bold("agent-harness-kit \u2014 add task"));
3492
+ p6.intro(pc13.bold("agent-harness-kit \u2014 add task"));
3625
3493
  const title = await cliFormWithRetry(
3626
3494
  async () => {
3627
3495
  const val = await p6.text({ message: "Task title" });
@@ -3664,10 +3532,10 @@ async function runTaskAdd(cwd2) {
3664
3532
  await db.writeFeatureList(cwd2);
3665
3533
  await db.close();
3666
3534
  spinner6.stop("");
3667
- console.log(pc12.green(`\u2713 Task #${task2.id} added \u2014 ${task2.slug} (pending)`));
3668
- console.log(pc12.cyan("\u2192") + " " + pc12.cyan("ahk status") + " to see all tasks");
3535
+ console.log(pc13.green(`\u2713 Task #${task2.id} added \u2014 ${task2.slug} (pending)`));
3536
+ console.log(pc13.cyan("\u2192") + " " + pc13.cyan("ahk status") + " to see all tasks");
3669
3537
  } catch (err) {
3670
- spinner6.stop(pc12.red("Failed"));
3538
+ spinner6.stop(pc13.red("Failed"));
3671
3539
  p6.log.error(err instanceof Error ? err.message : String(err));
3672
3540
  process.exit(1);
3673
3541
  }
@@ -3675,17 +3543,17 @@ async function runTaskAdd(cwd2) {
3675
3543
 
3676
3544
  // src/commands/task/done.ts
3677
3545
  import { spawnSync as spawnSync2 } from "child_process";
3678
- import { existsSync as existsSync15 } from "fs";
3546
+ import { existsSync as existsSync17 } from "fs";
3679
3547
  import { resolve as resolve12 } from "path";
3680
- import pc13 from "picocolors";
3548
+ import pc14 from "picocolors";
3681
3549
  async function runTaskDone(cwd2, idOrSlug) {
3682
3550
  const config = await loadConfig(cwd2);
3683
3551
  if (config.health.required) {
3684
3552
  const scriptPath = resolve12(cwd2, config.health.scriptPath);
3685
- if (existsSync15(scriptPath)) {
3553
+ if (existsSync17(scriptPath)) {
3686
3554
  const result = spawnSync2("bash", [scriptPath], { cwd: cwd2, stdio: "pipe", encoding: "utf8" });
3687
3555
  if (result.status !== 0) {
3688
- console.error(pc13.red("\u2717 Health check failed \u2014 cannot mark task as done."));
3556
+ console.error(pc14.red("\u2717 Health check failed \u2014 cannot mark task as done."));
3689
3557
  if (result.stdout) console.error(result.stdout);
3690
3558
  if (result.stderr) console.error(result.stderr);
3691
3559
  process.exit(1);
@@ -3698,16 +3566,16 @@ async function runTaskDone(cwd2, idOrSlug) {
3698
3566
  const isId = !isNaN(parsed);
3699
3567
  const task2 = isId ? await db.getTaskById(parsed) : await db.getTaskBySlug(idOrSlug);
3700
3568
  if (!task2) {
3701
- console.error(pc13.red(`Task not found: ${idOrSlug}`));
3569
+ console.error(pc14.red(`Task not found: ${idOrSlug}`));
3702
3570
  process.exit(1);
3703
3571
  }
3704
3572
  if (task2.status === "done") {
3705
- console.log(pc13.dim(`Task #${task2.id} is already done.`));
3573
+ console.log(pc14.dim(`Task #${task2.id} is already done.`));
3706
3574
  return;
3707
3575
  }
3708
3576
  await db.updateTaskStatus(task2.id, "done");
3709
3577
  await db.writeFeatureList(cwd2);
3710
- console.log(pc13.green(`\u2713 Task #${task2.id} \u2014 ${task2.slug} marked as done`));
3578
+ console.log(pc14.green(`\u2713 Task #${task2.id} \u2014 ${task2.slug} marked as done`));
3711
3579
  } finally {
3712
3580
  await db.close();
3713
3581
  }
@@ -3715,9 +3583,9 @@ async function runTaskDone(cwd2, idOrSlug) {
3715
3583
 
3716
3584
  // src/commands/task/edit.ts
3717
3585
  import * as p7 from "@clack/prompts";
3718
- import pc14 from "picocolors";
3586
+ import pc15 from "picocolors";
3719
3587
  async function runTaskEdit(cwd2) {
3720
- p7.intro(pc14.bold("agent-harness-kit \u2014 edit task"));
3588
+ p7.intro(pc15.bold("agent-harness-kit \u2014 edit task"));
3721
3589
  const config = await loadConfig(cwd2);
3722
3590
  const db = await openDB(config, cwd2);
3723
3591
  try {
@@ -3795,9 +3663,9 @@ async function runTaskEdit(cwd2) {
3795
3663
  await db.updateTaskAcceptance(task2.id, newAcceptance);
3796
3664
  await db.writeFeatureList(cwd2);
3797
3665
  spinner6.stop("");
3798
- console.log(pc14.green(`\u2713 Task #${task2.id} updated \u2014 ${newSlug}`));
3666
+ console.log(pc15.green(`\u2713 Task #${task2.id} updated \u2014 ${newSlug}`));
3799
3667
  } catch (err) {
3800
- spinner6.stop(pc14.red("Failed"));
3668
+ spinner6.stop(pc15.red("Failed"));
3801
3669
  p7.log.error(err instanceof Error ? err.message : String(err));
3802
3670
  process.exit(1);
3803
3671
  }
@@ -3808,12 +3676,12 @@ async function runTaskEdit(cwd2) {
3808
3676
 
3809
3677
  // src/commands/task/list.ts
3810
3678
  import Table2 from "cli-table3";
3811
- import pc15 from "picocolors";
3679
+ import pc16 from "picocolors";
3812
3680
  var STATUS_COLOR2 = {
3813
- pending: (s) => pc15.dim(s),
3814
- in_progress: (s) => pc15.cyan(s),
3815
- done: (s) => pc15.green(s),
3816
- blocked: (s) => pc15.red(s)
3681
+ pending: (s) => pc16.dim(s),
3682
+ in_progress: (s) => pc16.cyan(s),
3683
+ done: (s) => pc16.green(s),
3684
+ blocked: (s) => pc16.red(s)
3817
3685
  };
3818
3686
  async function runTaskList(cwd2, opts) {
3819
3687
  const config = await loadConfig(cwd2);
@@ -3830,11 +3698,11 @@ async function runTaskList(cwd2, opts) {
3830
3698
  let msg = "No tasks";
3831
3699
  if (filterStatus) msg += ` with status: ${filterStatus}`;
3832
3700
  if (opts.archived) msg += " (archived)";
3833
- console.log(pc15.dim(msg + "."));
3701
+ console.log(pc16.dim(msg + "."));
3834
3702
  return;
3835
3703
  }
3836
3704
  const table = new Table2({
3837
- head: ["ID", "Slug", "Title", "Status"].map((h) => pc15.bold(h)),
3705
+ head: ["ID", "Slug", "Title", "Status"].map((h) => pc16.bold(h)),
3838
3706
  style: { head: [], border: [] }
3839
3707
  });
3840
3708
  for (const t of tasks) {
@@ -3845,7 +3713,7 @@ async function runTaskList(cwd2, opts) {
3845
3713
  if (!opts.archived && !opts.includeArchived) {
3846
3714
  const archivedTasks = await db.getArchivedTasks();
3847
3715
  if (archivedTasks.length > 0) {
3848
- console.log(pc15.dim(`${archivedTasks.length} archived task${archivedTasks.length !== 1 ? "s" : ""} (use --archived to view)`));
3716
+ console.log(pc16.dim(`${archivedTasks.length} archived task${archivedTasks.length !== 1 ? "s" : ""} (use --archived to view)`));
3849
3717
  }
3850
3718
  }
3851
3719
  } finally {
@@ -3853,8 +3721,42 @@ async function runTaskList(cwd2, opts) {
3853
3721
  }
3854
3722
  }
3855
3723
 
3724
+ // src/core/local-install-guard.ts
3725
+ import { existsSync as existsSync18, readFileSync as readFileSync12 } from "fs";
3726
+ import { join as join20 } from "path";
3727
+ import pc17 from "picocolors";
3728
+ function isLocalInstallSatisfied(cwd2) {
3729
+ const selfPkgPath = join20(cwd2, "package.json");
3730
+ let projectPkg = null;
3731
+ if (existsSync18(selfPkgPath)) {
3732
+ try {
3733
+ const selfPkg = JSON.parse(readFileSync12(selfPkgPath, "utf8"));
3734
+ if (selfPkg?.name === pkg.name) return true;
3735
+ projectPkg = selfPkg;
3736
+ } catch {
3737
+ }
3738
+ }
3739
+ const [scope, name] = pkg.name.split("/");
3740
+ const localPath = pkg.name.startsWith("@") ? join20(cwd2, "node_modules", scope, name) : join20(cwd2, "node_modules", pkg.name);
3741
+ if (existsSync18(localPath)) return true;
3742
+ const isPnp = existsSync18(join20(cwd2, ".pnp.cjs")) || existsSync18(join20(cwd2, ".pnp.loader.mjs"));
3743
+ if (isPnp && projectPkg) {
3744
+ const deps = {
3745
+ ...projectPkg.dependencies ?? {},
3746
+ ...projectPkg.devDependencies ?? {}
3747
+ };
3748
+ if (Object.prototype.hasOwnProperty.call(deps, pkg.name)) return true;
3749
+ }
3750
+ return false;
3751
+ }
3752
+ function printLocalInstallWarning() {
3753
+ console.error(pc17.red(`\u2717 ${pkg.name} must be installed locally in this project.`));
3754
+ console.error(pc17.dim(` Run: npm install --save-dev ${pkg.name}`));
3755
+ console.error(pc17.dim(" (or the equivalent for your package manager: pnpm add -D, yarn add --dev, bun add -d)"));
3756
+ }
3757
+
3856
3758
  // src/core/update-check.ts
3857
- import pc16 from "picocolors";
3759
+ import pc18 from "picocolors";
3858
3760
  var REGISTRY_URL2 = `https://registry.npmjs.org/${pkg.name}/latest`;
3859
3761
  var TIMEOUT_MS2 = 2500;
3860
3762
  function checkForUpdate(currentVersion) {
@@ -3872,8 +3774,8 @@ function checkForUpdate(currentVersion) {
3872
3774
  }
3873
3775
  function printUpdateMessage({ current, latest }) {
3874
3776
  const lines = [
3875
- ` Update available ${pc16.dim(current)} \u2192 ${pc16.green(latest)} `,
3876
- ` Run: ${pc16.cyan(`pnpm i ${pkg.name}@${latest}`)} `
3777
+ ` Update available ${pc18.dim(current)} \u2192 ${pc18.green(latest)} `,
3778
+ ` Run: ${pc18.cyan(`pnpm i ${pkg.name}@${latest}`)} `
3877
3779
  ];
3878
3780
  drawBox(lines);
3879
3781
  }
@@ -3891,7 +3793,7 @@ var cwd = process.cwd();
3891
3793
  var updateCheck = checkForUpdate(pkg.version);
3892
3794
  var program = new Command();
3893
3795
  program.name("ahk").description("agent-harness-kit \u2014 CLI scaffolding for multi-agent harness systems").version(pkg.version, "-v, --version");
3894
- 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)").action(async (opts) => {
3796
+ 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) => {
3895
3797
  await runInit(cwd, opts);
3896
3798
  });
3897
3799
  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) => {
@@ -3925,9 +3827,20 @@ task.command("edit").description("Edit a task interactively").action(async () =>
3925
3827
  program.command("dashboard").description("Open web dashboard to visualize harness data").option("-p, --port <port>", "Port to listen on", "4242").option("--no-open", "Do not open browser automatically").action(async (opts) => {
3926
3828
  await runDashboard(cwd, { port: parseInt(opts.port), open: opts.open });
3927
3829
  });
3928
- program.command("migrate").description("Migrate provider-specific files to a different provider").option("--to <provider>", "Target provider: claude-code | opencode").action(async (opts) => {
3830
+ var migrate = program.command("migrate").description("Migrate provider files to a different provider, or migrate harness storage (see subcommands)");
3831
+ migrate.command("provider").description("Migrate provider-specific files to a different provider").option("--to <provider>", "Target provider: claude-code | opencode | codex-cli").action(async (opts) => {
3929
3832
  await runMigrate(cwd, opts);
3930
3833
  });
3834
+ migrate.command("storage").description(
3835
+ "Migrate harness DB storage between local/global scope or sqlite/postgres/mysql, based on agent-harness-kit.config.ts vs the real current state"
3836
+ ).option("--force", "Required to overwrite a non-empty destination (a backup is written first)").option("--dry-run", "Preview what would migrate without applying any changes").action(async (opts) => {
3837
+ try {
3838
+ await runMigrateStorage(cwd, { force: opts.force, dryRun: opts["dry-run"] });
3839
+ } catch (err) {
3840
+ console.error(pc19.red(`\u2717 ${err instanceof Error ? err.message : String(err)}`));
3841
+ process.exit(1);
3842
+ }
3843
+ });
3931
3844
  program.command("export").description("Export the database").option("--sql", "SQL dump").option("--json", "JSON export of tasks and actions").option("--output <path>", "Output file path (default: stdout)").action(async (opts) => {
3932
3845
  await runExport(cwd, opts);
3933
3846
  });
@@ -3937,9 +3850,25 @@ program.command("reset").description("Reset/clear harness data (DB, feature list
3937
3850
  program.command("doctor").description("Check lib version, agent files, and harness skills sync status").action(async () => {
3938
3851
  await runDoctor(cwd);
3939
3852
  });
3853
+ program.hook("preAction", () => {
3854
+ if (!isLocalInstallSatisfied(cwd)) {
3855
+ printLocalInstallWarning();
3856
+ process.exit(1);
3857
+ }
3858
+ });
3940
3859
  program.hook("postAction", async () => {
3941
3860
  const update = await updateCheck;
3942
3861
  if (update) printUpdateMessage(update);
3943
3862
  });
3944
- program.parse();
3863
+ function rewriteLegacyMigrateArgv(argv) {
3864
+ const migrateIdx = argv.indexOf("migrate");
3865
+ if (migrateIdx === -1) return argv;
3866
+ const next = argv[migrateIdx + 1];
3867
+ const isLegacyForm = next === void 0 || next === "--to";
3868
+ if (!isLegacyForm) return argv;
3869
+ const rewritten = [...argv];
3870
+ rewritten.splice(migrateIdx + 1, 0, "provider");
3871
+ return rewritten;
3872
+ }
3873
+ program.parse(rewriteLegacyMigrateArgv(process.argv));
3945
3874
  //# sourceMappingURL=cli.js.map