@cardor/agent-harness-kit 1.11.0 → 2.1.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
@@ -12,20 +12,20 @@ import {
12
12
  resolveGlobalStorageDir,
13
13
  resolveSqlitePath,
14
14
  resolveSqlitePathForScope
15
- } from "./chunk-6PEIJ2D5.js";
15
+ } from "./chunk-JTACLEGM.js";
16
16
 
17
17
  // src/cli.ts
18
- import { Command } from "commander";
19
- import pc19 from "picocolors";
18
+ import { Command, InvalidArgumentError } from "commander";
19
+ import pc20 from "picocolors";
20
20
 
21
21
  // src/commands/build.ts
22
22
  import { watch } from "fs";
23
23
  import * as p from "@clack/prompts";
24
- import pc from "picocolors";
24
+ import pc2 from "picocolors";
25
25
 
26
26
  // src/core/materializer/claude-code.ts
27
- import { existsSync as existsSync4, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
28
- import { join as join5, resolve as resolve3 } from "path";
27
+ import { existsSync as existsSync6 } from "fs";
28
+ import { join as join7 } from "path";
29
29
 
30
30
  // src/utils/file.ts
31
31
  import { mkdirSync, writeFileSync } from "fs";
@@ -37,24 +37,76 @@ var write = (cwd2, relPath, content, mode) => {
37
37
  };
38
38
 
39
39
  // src/core/materializer/detect-package-manager.ts
40
- import { existsSync, readFileSync } from "fs";
41
- import { join as join2 } from "path";
40
+ import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
41
+ import { join as join4 } from "path";
42
+
43
+ // src/core/local-install-guard.ts
44
+ import { existsSync as existsSync2, readFileSync } from "fs";
45
+ import { join as join3 } from "path";
46
+ import pc from "picocolors";
47
+
48
+ // src/core/package-data.ts
49
+ import { existsSync } from "fs";
50
+ import { createRequire } from "module";
51
+ import { dirname, join as join2 } from "path";
52
+ import { fileURLToPath } from "url";
53
+ var require2 = createRequire(import.meta.url);
54
+ var here = dirname(fileURLToPath(import.meta.url));
55
+ var candidates = [join2(here, "..", "..", "package.json"), join2(here, "..", "package.json")];
56
+ var pkgPath = candidates.find((p8) => existsSync(p8)) ?? candidates[0];
57
+ var pkg = require2(pkgPath);
58
+
59
+ // src/core/local-install-guard.ts
60
+ function isLocalInstallSatisfied(cwd2) {
61
+ const selfPkgPath = join3(cwd2, "package.json");
62
+ let projectPkg = null;
63
+ if (existsSync2(selfPkgPath)) {
64
+ try {
65
+ const selfPkg = JSON.parse(readFileSync(selfPkgPath, "utf8"));
66
+ if (selfPkg?.name === pkg.name) return true;
67
+ projectPkg = selfPkg;
68
+ } catch {
69
+ }
70
+ }
71
+ const [scope, name] = pkg.name.split("/");
72
+ const localPath = pkg.name.startsWith("@") ? join3(cwd2, "node_modules", scope, name) : join3(cwd2, "node_modules", pkg.name);
73
+ if (existsSync2(localPath)) return true;
74
+ const isPnp = existsSync2(join3(cwd2, ".pnp.cjs")) || existsSync2(join3(cwd2, ".pnp.loader.mjs"));
75
+ if (isPnp && projectPkg) {
76
+ const deps = {
77
+ ...projectPkg.dependencies ?? {},
78
+ ...projectPkg.devDependencies ?? {}
79
+ };
80
+ if (Object.prototype.hasOwnProperty.call(deps, pkg.name)) return true;
81
+ }
82
+ return false;
83
+ }
84
+ function printLocalInstallWarning() {
85
+ console.error(pc.yellow(`\u26A0 ${pkg.name} is not installed locally in this project.`));
86
+ console.error(pc.dim(" This is only a recommendation for reproducibility: pinning a local"));
87
+ console.error(pc.dim(" version keeps behavior consistent across your team and CI, instead of"));
88
+ console.error(pc.dim(" drifting with whatever version is installed globally on each machine."));
89
+ console.error(pc.dim(` Run: npm install --save-dev ${pkg.name}`));
90
+ console.error(pc.dim(" (or the equivalent for your package manager: pnpm add -D, yarn add --dev, bun add -d)"));
91
+ }
92
+
93
+ // src/core/materializer/detect-package-manager.ts
42
94
  function detectPackageManager(cwd2) {
43
95
  const fromField = detectFromPackageManagerField(cwd2);
44
96
  if (fromField) return fromField;
45
- if (existsSync(join2(cwd2, "pnpm-lock.yaml"))) return "pnpm";
46
- if (existsSync(join2(cwd2, "bun.lockb")) || existsSync(join2(cwd2, "bun.lock"))) return "bun";
47
- if (existsSync(join2(cwd2, "yarn.lock"))) {
48
- return existsSync(join2(cwd2, ".yarnrc.yml")) ? "yarn-berry" : "yarn-classic";
97
+ if (existsSync3(join4(cwd2, "pnpm-lock.yaml"))) return "pnpm";
98
+ if (existsSync3(join4(cwd2, "bun.lockb")) || existsSync3(join4(cwd2, "bun.lock"))) return "bun";
99
+ if (existsSync3(join4(cwd2, "yarn.lock"))) {
100
+ return existsSync3(join4(cwd2, ".yarnrc.yml")) ? "yarn-berry" : "yarn-classic";
49
101
  }
50
- if (existsSync(join2(cwd2, "package-lock.json"))) return "npm";
102
+ if (existsSync3(join4(cwd2, "package-lock.json"))) return "npm";
51
103
  return "npm";
52
104
  }
53
105
  function detectFromPackageManagerField(cwd2) {
54
- const pkgPath2 = join2(cwd2, "package.json");
55
- if (!existsSync(pkgPath2)) return null;
106
+ const pkgPath2 = join4(cwd2, "package.json");
107
+ if (!existsSync3(pkgPath2)) return null;
56
108
  try {
57
- const pkg2 = JSON.parse(readFileSync(pkgPath2, "utf8"));
109
+ const pkg2 = JSON.parse(readFileSync2(pkgPath2, "utf8"));
58
110
  const field = pkg2?.packageManager;
59
111
  if (typeof field !== "string" || !field.trim()) return null;
60
112
  const match = field.match(/^([a-z]+)@(\d+)/i);
@@ -78,8 +130,11 @@ function detectFromPackageManagerField(cwd2) {
78
130
  return null;
79
131
  }
80
132
  }
81
- function getMcpCommandParts(pm, port) {
133
+ function getMcpCommandParts(pm, port, cwd2) {
82
134
  const portStr = String(port);
135
+ if (!isLocalInstallSatisfied(cwd2)) {
136
+ return ["ahk", "serve", "--port", portStr];
137
+ }
83
138
  switch (pm) {
84
139
  case "pnpm":
85
140
  return ["pnpm", "exec", "ahk", "serve", "--port", portStr];
@@ -95,21 +150,21 @@ function getMcpCommandParts(pm, port) {
95
150
  }
96
151
 
97
152
  // src/core/materializer/mcp-merge.ts
98
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
99
- import { dirname } from "path";
100
- function mergeClaudeMcpJson(filePath, port, pm = "npm") {
101
- const folderPath = dirname(filePath);
102
- if (!existsSync2(folderPath)) {
153
+ import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
154
+ import { dirname as dirname2 } from "path";
155
+ function mergeClaudeMcpJson(filePath, port, cwd2, pm = "npm") {
156
+ const folderPath = dirname2(filePath);
157
+ if (!existsSync4(folderPath)) {
103
158
  mkdirSync2(folderPath, { recursive: true });
104
159
  }
105
160
  let existing = {};
106
- if (existsSync2(filePath)) {
161
+ if (existsSync4(filePath)) {
107
162
  try {
108
- existing = JSON.parse(readFileSync2(filePath, "utf8"));
163
+ existing = JSON.parse(readFileSync3(filePath, "utf8"));
109
164
  } catch {
110
165
  }
111
166
  }
112
- const [command, ...args] = getMcpCommandParts(pm, port);
167
+ const [command, ...args] = getMcpCommandParts(pm, port, cwd2);
113
168
  const merged = {
114
169
  ...existing,
115
170
  mcpServers: {
@@ -121,15 +176,15 @@ function mergeClaudeMcpJson(filePath, port, pm = "npm") {
121
176
  }
122
177
  }
123
178
  };
124
- mkdirSync2(dirname(filePath), { recursive: true });
179
+ mkdirSync2(dirname2(filePath), { recursive: true });
125
180
  writeFileSync2(filePath, JSON.stringify(merged, null, 2) + "\n", "utf8");
126
181
  }
127
182
  function mergeClaudeSettingsJson(filePath) {
128
- mkdirSync2(dirname(filePath), { recursive: true });
183
+ mkdirSync2(dirname2(filePath), { recursive: true });
129
184
  let existing = {};
130
- if (existsSync2(filePath)) {
185
+ if (existsSync4(filePath)) {
131
186
  try {
132
- existing = JSON.parse(readFileSync2(filePath, "utf8"));
187
+ existing = JSON.parse(readFileSync3(filePath, "utf8"));
133
188
  } catch {
134
189
  }
135
190
  }
@@ -232,11 +287,11 @@ var MCP_CLAUDE_PERMISSIONS = [
232
287
  ])
233
288
  ];
234
289
  function mergeClaudeSettingsLocalJson(filePath) {
235
- mkdirSync2(dirname(filePath), { recursive: true });
290
+ mkdirSync2(dirname2(filePath), { recursive: true });
236
291
  let existing = {};
237
- if (existsSync2(filePath)) {
292
+ if (existsSync4(filePath)) {
238
293
  try {
239
- existing = JSON.parse(readFileSync2(filePath, "utf8"));
294
+ existing = JSON.parse(readFileSync3(filePath, "utf8"));
240
295
  } catch {
241
296
  }
242
297
  }
@@ -255,15 +310,15 @@ function mergeClaudeSettingsLocalJson(filePath) {
255
310
  };
256
311
  writeFileSync2(filePath, JSON.stringify(merged, null, 2) + "\n", "utf8");
257
312
  }
258
- function mergeOpencodeJson(filePath, port, pm = "npm") {
259
- const folderPath = dirname(filePath);
260
- if (!existsSync2(folderPath)) {
313
+ function mergeOpencodeJson(filePath, port, cwd2, pm = "npm") {
314
+ const folderPath = dirname2(filePath);
315
+ if (!existsSync4(folderPath)) {
261
316
  mkdirSync2(folderPath, { recursive: true });
262
317
  }
263
318
  let existing = {};
264
- if (existsSync2(filePath)) {
319
+ if (existsSync4(filePath)) {
265
320
  try {
266
- existing = JSON.parse(readFileSync2(filePath, "utf8"));
321
+ existing = JSON.parse(readFileSync3(filePath, "utf8"));
267
322
  } catch {
268
323
  }
269
324
  }
@@ -280,7 +335,7 @@ function mergeOpencodeJson(filePath, port, pm = "npm") {
280
335
  type: "local",
281
336
  // OpenCode's mcp.<name>.command field is a single array (unlike
282
337
  // Claude/Codex, which split command/args) — pass the full token list.
283
- command: getMcpCommandParts(pm, port)
338
+ command: getMcpCommandParts(pm, port, cwd2)
284
339
  }
285
340
  }
286
341
  };
@@ -310,13 +365,13 @@ function mergeTomlSection(content, sectionName, sectionBody) {
310
365
  ];
311
366
  return newLines.join("\n");
312
367
  }
313
- function mergeCodexConfigToml(filePath, port, pm = "npm") {
314
- mkdirSync2(dirname(filePath), { recursive: true });
368
+ function mergeCodexConfigToml(filePath, port, cwd2, pm = "npm") {
369
+ mkdirSync2(dirname2(filePath), { recursive: true });
315
370
  let content = "";
316
- if (existsSync2(filePath)) {
317
- content = readFileSync2(filePath, "utf8");
371
+ if (existsSync4(filePath)) {
372
+ content = readFileSync3(filePath, "utf8");
318
373
  }
319
- const [command, ...args] = getMcpCommandParts(pm, port);
374
+ const [command, ...args] = getMcpCommandParts(pm, port, cwd2);
320
375
  const sectionBody = [
321
376
  `command = ${JSON.stringify(command)}`,
322
377
  `args = ${JSON.stringify(args)}`,
@@ -325,16 +380,28 @@ function mergeCodexConfigToml(filePath, port, pm = "npm") {
325
380
  content = mergeTomlSection(content, "mcp_servers.agent-harness-kit", sectionBody);
326
381
  writeFileSync2(filePath, content, "utf8");
327
382
  }
383
+ function mergeGrokConfigToml(filePath, port, cwd2, pm = "npm") {
384
+ mkdirSync2(dirname2(filePath), { recursive: true });
385
+ let content = "";
386
+ if (existsSync4(filePath)) {
387
+ content = readFileSync3(filePath, "utf8");
388
+ }
389
+ const [command, ...args] = getMcpCommandParts(pm, port, cwd2);
390
+ const sectionBody = [`command = ${JSON.stringify(command)}`, `args = ${JSON.stringify(args)}`].join("\n");
391
+ content = mergeTomlSection(content, "mcp_servers.agent-harness-kit", sectionBody);
392
+ writeFileSync2(filePath, content, "utf8");
393
+ }
328
394
 
329
395
  // src/core/materializer/scaffold-utils.ts
330
- import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
331
- import { dirname as dirname3, join as join4, resolve as resolve2 } from "path";
332
- import { fileURLToPath as fileURLToPath2 } from "url";
396
+ import { createHash } from "crypto";
397
+ import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
398
+ import { dirname as dirname4, join as join6, resolve as resolve2 } from "path";
399
+ import { fileURLToPath as fileURLToPath3 } from "url";
333
400
 
334
401
  // src/core/materializer/templates.ts
335
- import { readFileSync as readFileSync3 } from "fs";
336
- import { dirname as dirname2, join as join3 } from "path";
337
- import { fileURLToPath } from "url";
402
+ import { readFileSync as readFileSync4 } from "fs";
403
+ import { dirname as dirname3, join as join5 } from "path";
404
+ import { fileURLToPath as fileURLToPath2 } from "url";
338
405
 
339
406
  // src/core/materializer/agent-restrictions.ts
340
407
  var AGENT_RESTRICTIONS = {
@@ -364,12 +431,15 @@ These tools may still appear available to you. The sandbox will reject the call.
364
431
  function codexRestrictionNotice(agentName) {
365
432
  return restrictionFor(agentName) === "no-write" ? CODEX_READ_ONLY_NOTICE : "";
366
433
  }
434
+ function grokToolsAllowlist(agentName) {
435
+ return restrictionFor(agentName) === "no-write" ? ["Bash", "Read", "NotebookRead", "Grep", "Glob", "WebFetch", "WebSearch", "search_tool", "use_tool"] : [];
436
+ }
367
437
 
368
438
  // src/core/materializer/templates.ts
369
- var __dirname = dirname2(fileURLToPath(import.meta.url));
370
- var TEMPLATES_DIR = join3(__dirname, "agent-templates");
439
+ var __dirname = dirname3(fileURLToPath2(import.meta.url));
440
+ var TEMPLATES_DIR = join5(__dirname, "agent-templates");
371
441
  function loadAgentTemplate(name, vars = {}) {
372
- const raw = readFileSync3(join3(TEMPLATES_DIR, `${name}.md`), "utf8");
442
+ const raw = readFileSync4(join5(TEMPLATES_DIR, `${name}.md`), "utf8");
373
443
  return raw.replace(/\{\{(\w+)\}\}/g, (_, key) => vars[key] ?? `{{${key}}}`);
374
444
  }
375
445
  var HEALTH_SH = `#!/usr/bin/env bash
@@ -424,10 +494,10 @@ If it exits non-zero, stop and report the issue. Do not proceed with codebase ch
424
494
  The harness exposes tools via MCP server on port ${port}. Use these instead of reading files directly.
425
495
 
426
496
  \`\`\`
427
- actions.start taskId agent \u2192 start an action, returns actionId
497
+ actions.start taskId agent \u2192 start an action, returns a numeric actionId
428
498
  actions.write actionId section text \u2192 record a section (result, blockers, ...)
429
- actions.record_tool actionId toolName [argsJson] [summary] \u2192 log a tool call to the Tools dashboard
430
- actions.record_file actionId filePath operation [notes] \u2192 log a file touch to the Files dashboard
499
+ actions.record_tool actionId calls[] \u2192 batch-log tool calls to the Tools dashboard (array, min 1)
500
+ actions.record_file actionId files[] \u2192 batch-log file touches to the Files dashboard (array, min 1)
431
501
  actions.complete actionId summary \u2192 close the action
432
502
  actions.get taskId \u2192 full action history for a task
433
503
  tasks.add title [slug] [description] [acceptance] \u2192 create a new task from natural language
@@ -447,9 +517,8 @@ docs.search query \u2192 search ${docs
447
517
  - tasks.get('pending') \u2192 pick lowest id
448
518
 
449
519
  2. WORK (lead \u2192 explorer \u2192 consultant \u2192 builder \u2192 reviewer)
450
- - Each agent calls actions.start(taskId, agentName) \u2192 actionId
451
- - After EVERY tool call: actions.record_tool(actionId, toolName, args, summary)
452
- - After EVERY file change: actions.record_file(actionId, filePath, operation, notes)
520
+ - Each agent calls actions.start(taskId, agentName) \u2192 numeric actionId
521
+ - Accumulate tool calls / file touches as you work; flush periodically (every few calls or at a phase boundary) via actions.record_tool(actionId, calls: [...]) and actions.record_file(actionId, files: [...]) \u2014 both are batch-only, even a single entry goes through as a one-element array
453
522
  - Closes with actions.complete(actionId, summary)
454
523
 
455
524
  3. CLOSE
@@ -508,10 +577,10 @@ If it exits non-zero, stop and report the issue. Do not proceed with codebase ch
508
577
  The harness exposes tools via MCP server on port ${port}. Use these instead of reading files directly.
509
578
 
510
579
  \`\`\`
511
- actions.start taskId agent \u2192 start an action, returns actionId
580
+ actions.start taskId agent \u2192 start an action, returns a numeric actionId
512
581
  actions.write actionId section text \u2192 record a section (result, blockers, ...)
513
- actions.record_tool actionId toolName [argsJson] [summary] \u2192 log a tool call to the Tools dashboard
514
- actions.record_file actionId filePath operation [notes] \u2192 log a file touch to the Files dashboard
582
+ actions.record_tool actionId calls[] \u2192 batch-log tool calls to the Tools dashboard (array, min 1)
583
+ actions.record_file actionId files[] \u2192 batch-log file touches to the Files dashboard (array, min 1)
515
584
  actions.complete actionId summary \u2192 close the action
516
585
  actions.get taskId \u2192 full action history for a task
517
586
  tasks.add title [slug] [description] [acceptance] \u2192 create a new task from natural language
@@ -532,9 +601,8 @@ docs.search query \u2192 search ${docs
532
601
  - No pending tasks? \u2192 ask user, infer fields, call tasks.add, then tasks.claim
533
602
 
534
603
  2. WORK (lead \u2192 explorer \u2192 consultant \u2192 builder \u2192 reviewer)
535
- - Each agent calls actions.start(taskId, agentName) \u2192 actionId
536
- - After EVERY tool call: actions.record_tool(actionId, toolName, args, summary)
537
- - After EVERY file change: actions.record_file(actionId, filePath, operation, notes)
604
+ - Each agent calls actions.start(taskId, agentName) \u2192 numeric actionId
605
+ - Accumulate tool calls / file touches as you work; flush periodically (every few calls or at a phase boundary) via actions.record_tool(actionId, calls: [...]) and actions.record_file(actionId, files: [...]) \u2014 both are batch-only, even a single entry goes through as a one-element array
538
606
  - Closes with actions.complete(actionId, summary)
539
607
 
540
608
  3. CLOSE
@@ -692,9 +760,6 @@ function agentConsultant(vars) {
692
760
  function agentReviewer(vars) {
693
761
  return loadAgentTemplate("reviewer", vars);
694
762
  }
695
- function featureListJson(tasks) {
696
- return JSON.stringify(tasks, null, 2) + "\n";
697
- }
698
763
  function stripFrontmatter(md) {
699
764
  const parts = md.split(/^---\s*$/m);
700
765
  if (parts.length < 3) return { description: "", body: md };
@@ -768,6 +833,13 @@ ${values.map((v4) => ` - ${v4}`).join("\n")}
768
833
  ${body}${block}---
769
834
  `);
770
835
  }
836
+ function appendFrontmatterScalar(md, key, value) {
837
+ const block = `${key}: ${value}
838
+ `;
839
+ return md.replace(/^---\n([\s\S]*?)^---\n/m, (_m, body) => `---
840
+ ${body}${block}---
841
+ `);
842
+ }
771
843
  function appendFrontmatterMapping(md, key, entries) {
772
844
  const keys = Object.keys(entries);
773
845
  if (keys.length === 0) return md;
@@ -778,9 +850,12 @@ ${keys.map((k) => ` ${k}: ${entries[k]}`).join("\n")}
778
850
  ${body}${block}---
779
851
  `);
780
852
  }
781
- function translateFrontmatterForClaudeCode(md, agentName) {
853
+ function translateFrontmatterForClaudeCode(md, agentName, opts) {
782
854
  let result = stripFrontmatterBlockSequence(md, "tools");
783
855
  result = stripFrontmatterBlockSequence(result, "disallowedTools");
856
+ if (opts?.model && opts.model !== "inherit") {
857
+ result = appendFrontmatterScalar(result, "model", opts.model);
858
+ }
784
859
  return appendFrontmatterBlockSequence(result, "disallowedTools", claudeDisallowedTools(agentName));
785
860
  }
786
861
  function translateFrontmatterForOpenCode(md, agentName) {
@@ -788,6 +863,11 @@ function translateFrontmatterForOpenCode(md, agentName) {
788
863
  result = stripFrontmatterBlockSequence(result, "disallowedTools");
789
864
  return appendFrontmatterMapping(result, "permission", opencodePermissions(agentName));
790
865
  }
866
+ function translateFrontmatterForGrok(md, agentName) {
867
+ let result = stripFrontmatterBlockSequence(md, "tools");
868
+ result = stripFrontmatterBlockSequence(result, "disallowedTools");
869
+ return appendFrontmatterBlockSequence(result, "tools", grokToolsAllowlist(agentName));
870
+ }
791
871
  var GITIGNORE_ENTRIES = `
792
872
  # agent-harness-kit
793
873
  .harness/harness.db
@@ -797,10 +877,10 @@ var GITIGNORE_ENTRIES = `
797
877
  `;
798
878
 
799
879
  // src/core/materializer/scaffold-utils.ts
800
- var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
880
+ var __dirname2 = dirname4(fileURLToPath3(import.meta.url));
801
881
  function writeAgentFiles(cwd2, entries, opts = {}) {
802
882
  const result = { created: [], overwritten: [], preserved: [] };
803
- const existing = entries.filter((e) => existsSync3(join4(cwd2, e.relPath)));
883
+ const existing = entries.filter((e) => existsSync5(join6(cwd2, e.relPath)));
804
884
  if (opts.force && existing.length > 0) {
805
885
  if (!opts.backupRoot) {
806
886
  throw new Error(
@@ -808,12 +888,12 @@ function writeAgentFiles(cwd2, entries, opts = {}) {
808
888
  );
809
889
  }
810
890
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
811
- const backupDir = join4(opts.backupRoot, `agents-${stamp}`);
891
+ const backupDir = join6(opts.backupRoot, `agents-${stamp}`);
812
892
  try {
813
893
  for (const entry of existing) {
814
- const dest = join4(backupDir, entry.relPath);
894
+ const dest = join6(backupDir, entry.relPath);
815
895
  mkdirSync3(resolve2(dest, ".."), { recursive: true });
816
- writeFileSync3(dest, readFileSync4(join4(cwd2, entry.relPath), "utf8"), "utf8");
896
+ writeFileSync3(dest, readFileSync5(join6(cwd2, entry.relPath), "utf8"), "utf8");
817
897
  }
818
898
  } catch (err) {
819
899
  throw new Error(
@@ -823,8 +903,8 @@ function writeAgentFiles(cwd2, entries, opts = {}) {
823
903
  result.backupDir = backupDir;
824
904
  }
825
905
  for (const entry of entries) {
826
- const abs = join4(cwd2, entry.relPath);
827
- const exists = existsSync3(abs);
906
+ const abs = join6(cwd2, entry.relPath);
907
+ const exists = existsSync5(abs);
828
908
  if (exists && !opts.force) {
829
909
  result.preserved.push(entry.relPath);
830
910
  continue;
@@ -836,9 +916,106 @@ function writeAgentFiles(cwd2, entries, opts = {}) {
836
916
  }
837
917
  return result;
838
918
  }
919
+ var GENERATED_MARKER_RE = /^([\s\S]*)\n<!-- ahk:generated ([0-9a-f]{64}) -->\n?$/;
920
+ function bodyFingerprint(body) {
921
+ return createHash("sha256").update(body, "utf8").digest("hex");
922
+ }
923
+ function stampGenerated(body) {
924
+ return `${body}
925
+ <!-- ahk:generated ${bodyFingerprint(body)} -->
926
+ `;
927
+ }
928
+ function readStamp(fileContent) {
929
+ const m = GENERATED_MARKER_RE.exec(fileContent);
930
+ if (!m) return null;
931
+ return { body: m[1], hash: m[2] };
932
+ }
933
+ function reconcileGeneratedFiles(cwd2, entries, opts = {}) {
934
+ const result = {
935
+ created: [],
936
+ current: [],
937
+ propagated: [],
938
+ preserved: [],
939
+ overwritten: []
940
+ };
941
+ const plans = [];
942
+ const toBackup = [];
943
+ for (const entry of entries) {
944
+ const abs = join6(cwd2, entry.relPath);
945
+ if (!existsSync5(abs)) {
946
+ plans.push({ entry, action: "create" });
947
+ continue;
948
+ }
949
+ const onDisk = readFileSync5(abs, "utf8");
950
+ const stamp = readStamp(onDisk);
951
+ const onDiskBody = stamp ? stamp.body : onDisk;
952
+ if (onDiskBody === entry.content) {
953
+ plans.push({ entry, action: "current" });
954
+ continue;
955
+ }
956
+ if (stamp && stamp.hash === bodyFingerprint(stamp.body)) {
957
+ plans.push({ entry, action: "propagate" });
958
+ continue;
959
+ }
960
+ if (opts.force) {
961
+ plans.push({ entry, action: "overwrite" });
962
+ toBackup.push(entry);
963
+ } else {
964
+ plans.push({ entry, action: "preserve" });
965
+ }
966
+ }
967
+ if (toBackup.length > 0) {
968
+ if (!opts.backupRoot) {
969
+ throw new Error(
970
+ "reconcileGeneratedFiles: force is set and hand-edited generated files would be overwritten, but no backupRoot was provided. Refusing to overwrite without a backup."
971
+ );
972
+ }
973
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
974
+ const backupDir = join6(opts.backupRoot, `derived-${stamp}`);
975
+ try {
976
+ for (const entry of toBackup) {
977
+ const dest = join6(backupDir, entry.relPath);
978
+ mkdirSync3(resolve2(dest, ".."), { recursive: true });
979
+ writeFileSync3(dest, readFileSync5(join6(cwd2, entry.relPath), "utf8"), "utf8");
980
+ }
981
+ } catch (err) {
982
+ throw new Error(
983
+ `Could not back up existing generated files to ${backupDir} (${err instanceof Error ? err.message : String(err)}). Aborting WITHOUT overwriting anything \u2014 no file was modified.`
984
+ );
985
+ }
986
+ result.backupDir = backupDir;
987
+ }
988
+ for (const { entry, action } of plans) {
989
+ const abs = join6(cwd2, entry.relPath);
990
+ switch (action) {
991
+ case "current":
992
+ result.current.push(entry.relPath);
993
+ break;
994
+ case "preserve":
995
+ result.preserved.push(entry.relPath);
996
+ break;
997
+ case "create":
998
+ mkdirSync3(resolve2(abs, ".."), { recursive: true });
999
+ writeFileSync3(abs, stampGenerated(entry.content), "utf8");
1000
+ result.created.push(entry.relPath);
1001
+ break;
1002
+ case "propagate":
1003
+ mkdirSync3(resolve2(abs, ".."), { recursive: true });
1004
+ writeFileSync3(abs, stampGenerated(entry.content), "utf8");
1005
+ result.propagated.push(entry.relPath);
1006
+ break;
1007
+ case "overwrite":
1008
+ mkdirSync3(resolve2(abs, ".."), { recursive: true });
1009
+ writeFileSync3(abs, stampGenerated(entry.content), "utf8");
1010
+ result.overwritten.push(entry.relPath);
1011
+ break;
1012
+ }
1013
+ }
1014
+ return result;
1015
+ }
839
1016
  function appendGitignore(cwd2) {
840
- const giPath = join4(cwd2, ".gitignore");
841
- const existing = existsSync3(giPath) ? readFileSync4(giPath, "utf8") : "";
1017
+ const giPath = join6(cwd2, ".gitignore");
1018
+ const existing = existsSync5(giPath) ? readFileSync5(giPath, "utf8") : "";
842
1019
  const toAdd = GITIGNORE_ENTRIES.split("\n").filter((line) => line && !existing.includes(line)).join("\n");
843
1020
  if (toAdd.trim()) {
844
1021
  writeFileSync3(giPath, existing + (existing.endsWith("\n") ? "" : "\n") + toAdd + "\n", "utf8");
@@ -850,36 +1027,34 @@ function slugify(title) {
850
1027
  function writeSkills(cwd2, skillsDir) {
851
1028
  const skillNames = ["ahk-ask", "ahk-consultant", "ahk-triage", "ahk-review"];
852
1029
  for (const skillName of skillNames) {
853
- const src = join4(__dirname2, "skills", skillName, "SKILL.md");
854
- const destDir = join4(cwd2, skillsDir, skillName);
855
- const dest = join4(destDir, "SKILL.md");
1030
+ const src = join6(__dirname2, "skills", skillName, "SKILL.md");
1031
+ const destDir = join6(cwd2, skillsDir, skillName);
1032
+ const dest = join6(destDir, "SKILL.md");
856
1033
  mkdirSync3(destDir, { recursive: true });
857
- writeFileSync3(dest, readFileSync4(src, "utf8"), "utf8");
1034
+ writeFileSync3(dest, readFileSync5(src, "utf8"), "utf8");
858
1035
  }
859
1036
  }
860
1037
 
861
1038
  // src/core/materializer/claude-code.ts
862
- function claudeAgentFiles(config) {
1039
+ function claudeAgentFiles(config, modelsByRole) {
863
1040
  const projectName = config.project.name;
864
1041
  return [
865
- { relPath: ".claude/agents/lead.md", content: translateFrontmatterForClaudeCode(agentLead({ projectName }), "lead") },
866
- { relPath: ".claude/agents/explorer.md", content: translateFrontmatterForClaudeCode(agentExplorer({ projectName }), "explorer") },
867
- { relPath: ".claude/agents/consultant.md", content: translateFrontmatterForClaudeCode(agentConsultant({ projectName }), "consultant") },
868
- { relPath: ".claude/agents/builder.md", content: translateFrontmatterForClaudeCode(agentBuilder({ projectName }), "builder") },
869
- { relPath: ".claude/agents/reviewer.md", content: translateFrontmatterForClaudeCode(agentReviewer({ projectName }), "reviewer") }
1042
+ { relPath: ".claude/agents/lead.md", content: translateFrontmatterForClaudeCode(agentLead({ projectName }), "lead", { model: modelsByRole?.lead }) },
1043
+ { relPath: ".claude/agents/explorer.md", content: translateFrontmatterForClaudeCode(agentExplorer({ projectName }), "explorer", { model: modelsByRole?.explorer }) },
1044
+ { relPath: ".claude/agents/consultant.md", content: translateFrontmatterForClaudeCode(agentConsultant({ projectName }), "consultant", { model: modelsByRole?.consultant }) },
1045
+ { relPath: ".claude/agents/builder.md", content: translateFrontmatterForClaudeCode(agentBuilder({ projectName }), "builder", { model: modelsByRole?.builder }) },
1046
+ { relPath: ".claude/agents/reviewer.md", content: translateFrontmatterForClaudeCode(agentReviewer({ projectName }), "reviewer", { model: modelsByRole?.reviewer }) }
870
1047
  ];
871
1048
  }
872
1049
  var ClaudeCodeMaterializer = class {
873
1050
  async scaffold(config, opts) {
874
- const { cwd: cwd2 } = opts;
875
- write(cwd2, "AGENTS.md", agentsMd(config));
876
- write(cwd2, "CLAUDE.md", claudeMd(config));
877
- if (!existsSync4(join5(cwd2, "health.sh"))) {
1051
+ const { cwd: cwd2, claudeAgentModels } = opts;
1052
+ write(cwd2, "AGENTS.md", stampGenerated(agentsMd(config)));
1053
+ write(cwd2, "CLAUDE.md", stampGenerated(claudeMd(config)));
1054
+ if (!existsSync6(join7(cwd2, "health.sh"))) {
878
1055
  write(cwd2, "health.sh", HEALTH_SH, 493);
879
1056
  }
880
- const tasks = opts.firstTask ? [{ slug: slugify(opts.firstTask.title), ...opts.firstTask }] : [];
881
- write(cwd2, "feature_list.json", featureListJson(tasks));
882
- if (config.storage.scope === "local" && !existsSync4(join5(cwd2, config.storage.markdownFallback.path))) {
1057
+ if (config.storage.scope === "local" && !existsSync6(join7(cwd2, config.storage.markdownFallback.path))) {
883
1058
  write(
884
1059
  cwd2,
885
1060
  config.storage.markdownFallback.path,
@@ -892,30 +1067,31 @@ No tasks in progress.
892
1067
  `
893
1068
  );
894
1069
  }
895
- writeAgentFiles(cwd2, claudeAgentFiles(config));
896
- mergeClaudeMcpJson(join5(cwd2, ".mcp.json"), config.tools.mcp.port, detectPackageManager(cwd2));
897
- mergeClaudeSettingsJson(join5(cwd2, ".claude/settings.json"));
898
- mergeClaudeSettingsLocalJson(join5(cwd2, ".claude/settings.local.json"));
1070
+ writeAgentFiles(cwd2, claudeAgentFiles(config, claudeAgentModels));
1071
+ mergeClaudeMcpJson(join7(cwd2, ".mcp.json"), config.tools.mcp.port, cwd2, detectPackageManager(cwd2));
1072
+ mergeClaudeSettingsJson(join7(cwd2, ".claude/settings.json"));
1073
+ mergeClaudeSettingsLocalJson(join7(cwd2, ".claude/settings.local.json"));
899
1074
  appendGitignore(cwd2);
900
1075
  writeSkills(cwd2, ".claude/skills");
901
1076
  }
902
1077
  async build(config, cwd2, opts = {}) {
903
- const write2 = (relPath, content) => {
904
- const abs = join5(cwd2, relPath);
905
- mkdirSync4(resolve3(abs, ".."), { recursive: true });
906
- writeFileSync4(abs, content, "utf8");
907
- };
908
- write2("AGENTS.md", agentsMd(config));
909
- write2("CLAUDE.md", claudeMd(config));
1078
+ const derived = reconcileGeneratedFiles(
1079
+ cwd2,
1080
+ [
1081
+ { relPath: "AGENTS.md", content: agentsMd(config) },
1082
+ { relPath: "CLAUDE.md", content: claudeMd(config) }
1083
+ ],
1084
+ { force: opts.force, backupRoot: join7(cwd2, config.storage.dir, "backups") }
1085
+ );
910
1086
  const agents = writeAgentFiles(cwd2, claudeAgentFiles(config), {
911
1087
  force: opts.force,
912
- backupRoot: join5(cwd2, config.storage.dir, "backups")
1088
+ backupRoot: join7(cwd2, config.storage.dir, "backups")
913
1089
  });
914
- mergeClaudeMcpJson(join5(cwd2, ".mcp.json"), config.tools.mcp.port, detectPackageManager(cwd2));
915
- mergeClaudeSettingsJson(join5(cwd2, ".claude/settings.json"));
916
- mergeClaudeSettingsLocalJson(join5(cwd2, ".claude/settings.local.json"));
1090
+ mergeClaudeMcpJson(join7(cwd2, ".mcp.json"), config.tools.mcp.port, cwd2, detectPackageManager(cwd2));
1091
+ mergeClaudeSettingsJson(join7(cwd2, ".claude/settings.json"));
1092
+ mergeClaudeSettingsLocalJson(join7(cwd2, ".claude/settings.local.json"));
917
1093
  writeSkills(cwd2, ".claude/skills");
918
- return { agents };
1094
+ return { agents, derived };
919
1095
  }
920
1096
  async migrate(config, _to, _cwd) {
921
1097
  void config;
@@ -942,8 +1118,8 @@ No tasks in progress.
942
1118
  };
943
1119
 
944
1120
  // src/core/materializer/codex-cli.ts
945
- import { existsSync as existsSync5, mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "fs";
946
- import { join as join6, resolve as resolve4 } from "path";
1121
+ import { existsSync as existsSync7, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
1122
+ import { join as join8, resolve as resolve3 } from "path";
947
1123
  function codexAgentFiles(config) {
948
1124
  const projectName = config.project.name;
949
1125
  return [
@@ -959,17 +1135,15 @@ var CodexCliMaterializer = class {
959
1135
  async scaffold(config, opts) {
960
1136
  const { cwd: cwd2 } = opts;
961
1137
  const write2 = (relPath, content, mode) => {
962
- const abs = join6(cwd2, relPath);
963
- mkdirSync5(resolve4(abs, ".."), { recursive: true });
964
- writeFileSync5(abs, content, { encoding: "utf8", mode });
1138
+ const abs = join8(cwd2, relPath);
1139
+ mkdirSync4(resolve3(abs, ".."), { recursive: true });
1140
+ writeFileSync4(abs, content, { encoding: "utf8", mode });
965
1141
  };
966
- write2("AGENTS.md", agentsMd(config));
967
- if (!existsSync5(join6(cwd2, "health.sh"))) {
1142
+ write2("AGENTS.md", stampGenerated(agentsMd(config)));
1143
+ if (!existsSync7(join8(cwd2, "health.sh"))) {
968
1144
  write2("health.sh", HEALTH_SH, 493);
969
1145
  }
970
- const tasks = opts.firstTask ? [{ slug: slugify(opts.firstTask.title), ...opts.firstTask }] : [];
971
- write2(join6(config.storage.dir, "feature_list.json"), featureListJson(tasks));
972
- if (config.storage.scope === "local" && !existsSync5(join6(cwd2, config.storage.markdownFallback.path))) {
1146
+ if (config.storage.scope === "local" && !existsSync7(join8(cwd2, config.storage.markdownFallback.path))) {
973
1147
  write2(
974
1148
  config.storage.markdownFallback.path,
975
1149
  `<!-- AUTO-GENERATED by agent-harness-kit \u2014 DO NOT EDIT MANUALLY -->
@@ -982,24 +1156,23 @@ No tasks in progress.
982
1156
  );
983
1157
  }
984
1158
  writeAgentFiles(cwd2, codexAgentFiles(config));
985
- mergeCodexConfigToml(join6(cwd2, ".codex/config.toml"), config.tools.mcp.port, detectPackageManager(cwd2));
1159
+ mergeCodexConfigToml(join8(cwd2, ".codex/config.toml"), config.tools.mcp.port, cwd2, detectPackageManager(cwd2));
986
1160
  appendGitignore(cwd2);
987
1161
  writeSkills(cwd2, ".agents/skills");
988
1162
  }
989
1163
  async build(config, cwd2, opts = {}) {
990
- const write2 = (relPath, content) => {
991
- const abs = join6(cwd2, relPath);
992
- mkdirSync5(resolve4(abs, ".."), { recursive: true });
993
- writeFileSync5(abs, content, "utf8");
994
- };
995
- write2("AGENTS.md", agentsMd(config));
1164
+ const derived = reconcileGeneratedFiles(
1165
+ cwd2,
1166
+ [{ relPath: "AGENTS.md", content: agentsMd(config) }],
1167
+ { force: opts.force, backupRoot: join8(cwd2, config.storage.dir, "backups") }
1168
+ );
996
1169
  const agents = writeAgentFiles(cwd2, codexAgentFiles(config), {
997
1170
  force: opts.force,
998
- backupRoot: join6(cwd2, config.storage.dir, "backups")
1171
+ backupRoot: join8(cwd2, config.storage.dir, "backups")
999
1172
  });
1000
- mergeCodexConfigToml(join6(cwd2, ".codex/config.toml"), config.tools.mcp.port, detectPackageManager(cwd2));
1173
+ mergeCodexConfigToml(join8(cwd2, ".codex/config.toml"), config.tools.mcp.port, cwd2, detectPackageManager(cwd2));
1001
1174
  writeSkills(cwd2, ".agents/skills");
1002
- return { agents };
1175
+ return { agents, derived };
1003
1176
  }
1004
1177
  async migrate(config, _to, _cwd) {
1005
1178
  void config;
@@ -1009,9 +1182,73 @@ No tasks in progress.
1009
1182
  }
1010
1183
  };
1011
1184
 
1185
+ // src/core/materializer/grok.ts
1186
+ import { existsSync as existsSync8, mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "fs";
1187
+ import { join as join9, resolve as resolve4 } from "path";
1188
+ function grokAgentFiles(config) {
1189
+ const projectName = config.project.name;
1190
+ return [
1191
+ { relPath: ".grok/agents/lead.md", content: translateFrontmatterForGrok(agentLead({ projectName }), "lead") },
1192
+ { relPath: ".grok/agents/explorer.md", content: translateFrontmatterForGrok(agentExplorer({ projectName }), "explorer") },
1193
+ { relPath: ".grok/agents/consultant.md", content: translateFrontmatterForGrok(agentConsultant({ projectName }), "consultant") },
1194
+ { relPath: ".grok/agents/builder.md", content: translateFrontmatterForGrok(agentBuilder({ projectName }), "builder") },
1195
+ { relPath: ".grok/agents/reviewer.md", content: translateFrontmatterForGrok(agentReviewer({ projectName }), "reviewer") }
1196
+ ];
1197
+ }
1198
+ var GrokMaterializer = class {
1199
+ async scaffold(config, opts) {
1200
+ const { cwd: cwd2 } = opts;
1201
+ const write2 = (relPath, content, mode) => {
1202
+ const abs = join9(cwd2, relPath);
1203
+ mkdirSync5(resolve4(abs, ".."), { recursive: true });
1204
+ writeFileSync5(abs, content, { encoding: "utf8", mode });
1205
+ };
1206
+ write2("AGENTS.md", stampGenerated(agentsMd(config)));
1207
+ if (!existsSync8(join9(cwd2, "health.sh"))) {
1208
+ write2("health.sh", HEALTH_SH, 493);
1209
+ }
1210
+ if (config.storage.scope === "local" && !existsSync8(join9(cwd2, config.storage.markdownFallback.path))) {
1211
+ write2(
1212
+ config.storage.markdownFallback.path,
1213
+ `<!-- AUTO-GENERATED by agent-harness-kit \u2014 DO NOT EDIT MANUALLY -->
1214
+ <!-- Run ahk status to refresh -->
1215
+
1216
+ # Current Session
1217
+
1218
+ No tasks in progress.
1219
+ `
1220
+ );
1221
+ }
1222
+ writeAgentFiles(cwd2, grokAgentFiles(config));
1223
+ mergeGrokConfigToml(join9(cwd2, ".grok/config.toml"), config.tools.mcp.port, cwd2, detectPackageManager(cwd2));
1224
+ appendGitignore(cwd2);
1225
+ writeSkills(cwd2, ".grok/skills");
1226
+ }
1227
+ async build(config, cwd2, opts = {}) {
1228
+ const derived = reconcileGeneratedFiles(
1229
+ cwd2,
1230
+ [{ relPath: "AGENTS.md", content: agentsMd(config) }],
1231
+ { force: opts.force, backupRoot: join9(cwd2, config.storage.dir, "backups") }
1232
+ );
1233
+ const agents = writeAgentFiles(cwd2, grokAgentFiles(config), {
1234
+ force: opts.force,
1235
+ backupRoot: join9(cwd2, config.storage.dir, "backups")
1236
+ });
1237
+ mergeGrokConfigToml(join9(cwd2, ".grok/config.toml"), config.tools.mcp.port, cwd2, detectPackageManager(cwd2));
1238
+ writeSkills(cwd2, ".grok/skills");
1239
+ return { agents, derived };
1240
+ }
1241
+ async migrate(config, _to, _cwd) {
1242
+ void config;
1243
+ }
1244
+ async syncPermissions(_cwd) {
1245
+ console.log(" Permissions sync not needed for grok-cli \u2014 skipping");
1246
+ }
1247
+ };
1248
+
1012
1249
  // src/core/materializer/opencode.ts
1013
- import { existsSync as existsSync6, mkdirSync as mkdirSync6, writeFileSync as writeFileSync6 } from "fs";
1014
- import { join as join7, resolve as resolve5 } from "path";
1250
+ import { existsSync as existsSync9, mkdirSync as mkdirSync6, writeFileSync as writeFileSync6 } from "fs";
1251
+ import { join as join10, resolve as resolve5 } from "path";
1015
1252
  function opencodeAgentFiles(config) {
1016
1253
  const projectName = config.project.name;
1017
1254
  return [
@@ -1026,17 +1263,15 @@ var OpenCodeMaterializer = class {
1026
1263
  async scaffold(config, opts) {
1027
1264
  const { cwd: cwd2 } = opts;
1028
1265
  const write2 = (relPath, content, mode) => {
1029
- const abs = join7(cwd2, relPath);
1266
+ const abs = join10(cwd2, relPath);
1030
1267
  mkdirSync6(resolve5(abs, ".."), { recursive: true });
1031
1268
  writeFileSync6(abs, content, { encoding: "utf8", mode });
1032
1269
  };
1033
- write2("AGENTS.md", agentsMd(config));
1034
- if (!existsSync6(join7(cwd2, "health.sh"))) {
1270
+ write2("AGENTS.md", stampGenerated(agentsMd(config)));
1271
+ if (!existsSync9(join10(cwd2, "health.sh"))) {
1035
1272
  write2("health.sh", HEALTH_SH, 493);
1036
1273
  }
1037
- const tasks = opts.firstTask ? [{ slug: slugify(opts.firstTask.title), ...opts.firstTask }] : [];
1038
- write2(join7(config.storage.dir, "feature_list.json"), featureListJson(tasks));
1039
- if (config.storage.scope === "local" && !existsSync6(join7(cwd2, config.storage.markdownFallback.path))) {
1274
+ if (config.storage.scope === "local" && !existsSync9(join10(cwd2, config.storage.markdownFallback.path))) {
1040
1275
  write2(
1041
1276
  config.storage.markdownFallback.path,
1042
1277
  `<!-- AUTO-GENERATED by agent-harness-kit \u2014 DO NOT EDIT MANUALLY -->
@@ -1049,24 +1284,23 @@ No tasks in progress.
1049
1284
  );
1050
1285
  }
1051
1286
  writeAgentFiles(cwd2, opencodeAgentFiles(config));
1052
- mergeOpencodeJson(join7(cwd2, "opencode.json"), config.tools.mcp.port, detectPackageManager(cwd2));
1287
+ mergeOpencodeJson(join10(cwd2, "opencode.json"), config.tools.mcp.port, cwd2, detectPackageManager(cwd2));
1053
1288
  appendGitignore(cwd2);
1054
1289
  writeSkills(cwd2, ".opencode/skills");
1055
1290
  }
1056
1291
  async build(config, cwd2, opts = {}) {
1057
- const write2 = (relPath, content) => {
1058
- const abs = join7(cwd2, relPath);
1059
- mkdirSync6(resolve5(abs, ".."), { recursive: true });
1060
- writeFileSync6(abs, content, "utf8");
1061
- };
1062
- write2("AGENTS.md", agentsMd(config));
1292
+ const derived = reconcileGeneratedFiles(
1293
+ cwd2,
1294
+ [{ relPath: "AGENTS.md", content: agentsMd(config) }],
1295
+ { force: opts.force, backupRoot: join10(cwd2, config.storage.dir, "backups") }
1296
+ );
1063
1297
  const agents = writeAgentFiles(cwd2, opencodeAgentFiles(config), {
1064
1298
  force: opts.force,
1065
- backupRoot: join7(cwd2, config.storage.dir, "backups")
1299
+ backupRoot: join10(cwd2, config.storage.dir, "backups")
1066
1300
  });
1067
- mergeOpencodeJson(join7(cwd2, "opencode.json"), config.tools.mcp.port, detectPackageManager(cwd2));
1301
+ mergeOpencodeJson(join10(cwd2, "opencode.json"), config.tools.mcp.port, cwd2, detectPackageManager(cwd2));
1068
1302
  writeSkills(cwd2, ".opencode/skills");
1069
- return { agents };
1303
+ return { agents, derived };
1070
1304
  }
1071
1305
  async migrate(config, _to, _cwd) {
1072
1306
  void config;
@@ -1085,6 +1319,8 @@ function getMaterializer(provider) {
1085
1319
  return new OpenCodeMaterializer();
1086
1320
  case "codex-cli":
1087
1321
  return new CodexCliMaterializer();
1322
+ case "grok-cli":
1323
+ return new GrokMaterializer();
1088
1324
  default:
1089
1325
  throw new Error(`Unknown provider: ${provider}`);
1090
1326
  }
@@ -1119,8 +1355,37 @@ async function buildOnce(cwd2, force) {
1119
1355
  spinner6.message("Rebuilding files...");
1120
1356
  const materializer = getMaterializer(config.provider);
1121
1357
  const report = await materializer.build(config, cwd2, { force });
1122
- spinner6.stop(pc.green("Build complete"));
1123
- p.log.success("AGENTS.md");
1358
+ spinner6.stop(pc2.green("Build complete"));
1359
+ const d = report.derived;
1360
+ const upToDate = [...d.created, ...d.current, ...d.propagated];
1361
+ if (upToDate.length > 0) {
1362
+ p.log.success(upToDate.join(", "));
1363
+ }
1364
+ if (d.propagated.length > 0) {
1365
+ p.log.info(`Propagated config changes to ${d.propagated.length} generated file(s):
1366
+ ${d.propagated.join("\n ")}`);
1367
+ }
1368
+ if (d.overwritten.length > 0) {
1369
+ p.log.warn(
1370
+ pc2.yellow(
1371
+ `--force REGENERATED ${d.overwritten.length} hand-edited generated file(s), discarding your edits:
1372
+ ` + d.overwritten.join("\n ")
1373
+ )
1374
+ );
1375
+ if (d.backupDir) {
1376
+ p.log.info(pc2.yellow(` Previous content backed up \u2192 ${d.backupDir}`));
1377
+ }
1378
+ }
1379
+ if (d.preserved.length > 0) {
1380
+ p.log.warn(
1381
+ pc2.yellow(
1382
+ `Left ${d.preserved.length} hand-edited generated file(s) UNTOUCHED \u2014 your edits are safe:
1383
+ ` + d.preserved.join("\n ") + `
1384
+ These no longer match the current config. Re-run with --force to regenerate them
1385
+ (this DESTROYS your edits; a backup is written first).`
1386
+ )
1387
+ );
1388
+ }
1124
1389
  p.log.success(`Agent definitions (${config.provider})`);
1125
1390
  p.log.success("MCP config");
1126
1391
  const { created, overwritten, preserved, backupDir } = report.agents;
@@ -1130,13 +1395,13 @@ async function buildOnce(cwd2, force) {
1130
1395
  }
1131
1396
  if (overwritten.length > 0) {
1132
1397
  p.log.warn(
1133
- pc.yellow(
1398
+ pc2.yellow(
1134
1399
  `--force REGENERATED ${overwritten.length} existing agent file(s), discarding any customizations:
1135
1400
  ` + overwritten.join("\n ")
1136
1401
  )
1137
1402
  );
1138
1403
  if (backupDir) {
1139
- p.log.info(pc.yellow(` Previous content backed up \u2192 ${backupDir}`));
1404
+ p.log.info(pc2.yellow(` Previous content backed up \u2192 ${backupDir}`));
1140
1405
  }
1141
1406
  }
1142
1407
  if (preserved.length > 0) {
@@ -1147,7 +1412,7 @@ async function buildOnce(cwd2, force) {
1147
1412
  );
1148
1413
  }
1149
1414
  } catch (err) {
1150
- spinner6.stop(pc.red("Build failed"));
1415
+ spinner6.stop(pc2.red("Build failed"));
1151
1416
  p.log.error(err instanceof Error ? err.message : String(err));
1152
1417
  process.exit(1);
1153
1418
  }
@@ -1155,34 +1420,41 @@ async function buildOnce(cwd2, force) {
1155
1420
 
1156
1421
  // src/commands/dashboard.ts
1157
1422
  import { homedir } from "os";
1158
- import { dirname as dirname4, join as join9 } from "path";
1159
- import { fileURLToPath as fileURLToPath3 } from "url";
1160
- import pc2 from "picocolors";
1423
+ import { dirname as dirname5, join as join12 } from "path";
1424
+ import { fileURLToPath as fileURLToPath4 } from "url";
1425
+ import pc3 from "picocolors";
1161
1426
 
1162
1427
  // src/core/dashboard-server.ts
1163
1428
  import { watch as watch2 } from "fs";
1164
- import { existsSync as existsSync7, readFileSync as readFileSync5 } from "fs";
1165
- import { extname, join as join8 } from "path";
1429
+ import { existsSync as existsSync10, readFileSync as readFileSync6 } from "fs";
1430
+ import { extname, join as join11 } from "path";
1166
1431
  import { serve } from "@hono/node-server";
1167
1432
  import { Hono } from "hono";
1168
1433
  import { WebSocketServer } from "ws";
1169
1434
 
1170
1435
  // src/core/port-utils.ts
1171
1436
  import { createServer } from "net";
1172
- function isPortFree(port) {
1437
+ var DASHBOARD_BIND_HOST = void 0;
1438
+ function isPortFree(port, host = DASHBOARD_BIND_HOST) {
1173
1439
  return new Promise((resolve12) => {
1174
1440
  const server = createServer();
1175
1441
  server.once("error", () => resolve12(false));
1176
1442
  server.once("listening", () => {
1177
1443
  server.close(() => resolve12(true));
1178
1444
  });
1179
- server.listen(port, "127.0.0.1");
1445
+ server.listen(port, host);
1180
1446
  });
1181
1447
  }
1182
- async function findFreePort(start, maxAttempts = 10) {
1448
+ async function findFreePort(start, options = {}) {
1449
+ const { maxAttempts = 10, host = DASHBOARD_BIND_HOST } = options;
1450
+ if (typeof start !== "number" || !Number.isInteger(start)) {
1451
+ throw new Error(
1452
+ `findFreePort requires an integer port number, received ${typeof start} ${JSON.stringify(start)}. The port must be coerced to a number before it reaches here (commander supplies --port as a string).`
1453
+ );
1454
+ }
1183
1455
  for (let i = 0; i < maxAttempts; i++) {
1184
1456
  const port = start + i;
1185
- if (await isPortFree(port)) return port;
1457
+ if (await isPortFree(port, host)) return port;
1186
1458
  }
1187
1459
  throw new Error(
1188
1460
  `Could not find a free port after ${maxAttempts} attempts (tried ${start}-${start + maxAttempts - 1}). Please free a port and try again.`
@@ -1204,12 +1476,42 @@ var MIME = {
1204
1476
  ".ttf": "font/ttf"
1205
1477
  };
1206
1478
  function fileResponse(filePath) {
1207
- const content = readFileSync5(filePath);
1479
+ const content = readFileSync6(filePath);
1208
1480
  const mime = MIME[extname(filePath)] ?? "application/octet-stream";
1209
1481
  return new Response(content, {
1210
1482
  headers: { "Content-Type": mime, "Cache-Control": "no-cache" }
1211
1483
  });
1212
1484
  }
1485
+ function awaitServerListening(server, port) {
1486
+ return new Promise((resolve12, reject) => {
1487
+ const closeQuietly = () => {
1488
+ try {
1489
+ server.close(() => {
1490
+ });
1491
+ } catch {
1492
+ }
1493
+ };
1494
+ const onError = (err) => {
1495
+ server.off("listening", onListening);
1496
+ closeQuietly();
1497
+ if (err.code === "EADDRINUSE") {
1498
+ reject(
1499
+ new Error(
1500
+ `Port ${port} was taken by another process while starting the dashboard. Please retry, or pick a different port with --port.`
1501
+ )
1502
+ );
1503
+ return;
1504
+ }
1505
+ reject(new Error(`Failed to start the dashboard on port ${port}: ${err.message}`));
1506
+ };
1507
+ const onListening = () => {
1508
+ server.off("error", onError);
1509
+ resolve12();
1510
+ };
1511
+ server.once("error", onError);
1512
+ server.once("listening", onListening);
1513
+ });
1514
+ }
1213
1515
  async function startDashboardServer(db, dbPath, staticPath, port) {
1214
1516
  const app = new Hono();
1215
1517
  const { tasks, actions, stats } = db;
@@ -1310,21 +1612,29 @@ async function startDashboardServer(db, dbPath, staticPath, port) {
1310
1612
  app.get("/*", (c) => {
1311
1613
  const urlPath = c.req.path;
1312
1614
  if (urlPath !== "/") {
1313
- const candidate = join8(staticPath, urlPath);
1314
- if (existsSync7(candidate)) {
1615
+ const candidate = join11(staticPath, urlPath);
1616
+ if (existsSync10(candidate)) {
1315
1617
  try {
1316
1618
  return fileResponse(candidate);
1317
1619
  } catch {
1318
1620
  }
1319
1621
  }
1320
1622
  }
1321
- return fileResponse(join8(staticPath, "index.html"));
1623
+ return fileResponse(join11(staticPath, "index.html"));
1322
1624
  });
1323
- const resolvedPort = await findFreePort(port);
1625
+ const resolvedPort = await findFreePort(port, { host: DASHBOARD_BIND_HOST });
1324
1626
  if (resolvedPort !== port) {
1325
1627
  console.log(`Port ${port} in use, using ${resolvedPort}`);
1326
1628
  }
1327
- const httpServer = serve({ fetch: app.fetch, port: resolvedPort });
1629
+ const httpServer = serve({
1630
+ fetch: app.fetch,
1631
+ port: resolvedPort,
1632
+ hostname: DASHBOARD_BIND_HOST
1633
+ });
1634
+ await awaitServerListening(httpServer, resolvedPort);
1635
+ httpServer.on("error", (err) => {
1636
+ console.error(`Dashboard server error: ${err.message}`);
1637
+ });
1328
1638
  const wss = new WebSocketServer({ noServer: true });
1329
1639
  httpServer.on("upgrade", (req, socket, head) => {
1330
1640
  if (req.url === "/ws") {
@@ -1349,7 +1659,7 @@ async function startDashboardServer(db, dbPath, staticPath, port) {
1349
1659
  let watcher = null;
1350
1660
  if (dbPath) {
1351
1661
  const walPath = `${dbPath}-wal`;
1352
- const watchTarget = existsSync7(walPath) ? walPath : dbPath;
1662
+ const watchTarget = existsSync10(walPath) ? walPath : dbPath;
1353
1663
  watcher = watch2(watchTarget, broadcast);
1354
1664
  }
1355
1665
  return {
@@ -1364,16 +1674,16 @@ async function startDashboardServer(db, dbPath, staticPath, port) {
1364
1674
  }
1365
1675
 
1366
1676
  // src/commands/dashboard.ts
1367
- var __dirname3 = dirname4(fileURLToPath3(import.meta.url));
1677
+ var __dirname3 = dirname5(fileURLToPath4(import.meta.url));
1368
1678
  async function runDashboard(cwd2, opts) {
1369
1679
  const config = await loadConfig(cwd2);
1370
1680
  const db = await openDB(config, cwd2);
1371
1681
  const dbPath = config.database.type === "sqlite" ? resolveSqlitePath(config, cwd2, homedir()) : null;
1372
- const staticPath = join9(__dirname3, "dashboard-dist");
1682
+ const staticPath = join12(__dirname3, "dashboard-dist");
1373
1683
  const { url } = await startDashboardServer(db, dbPath, staticPath, opts.port);
1374
- console.log(pc2.green(`\u2713`) + ` Dashboard running at ${pc2.bold(pc2.cyan(url))}`);
1375
- console.log(pc2.dim(` WebSocket live updates enabled`));
1376
- console.log(pc2.dim(` Press Ctrl+C to stop`));
1684
+ console.log(pc3.green(`\u2713`) + ` Dashboard running at ${pc3.bold(pc3.cyan(url))}`);
1685
+ console.log(pc3.dim(` WebSocket live updates enabled`));
1686
+ console.log(pc3.dim(` Press Ctrl+C to stop`));
1377
1687
  if (opts.open) {
1378
1688
  const { default: open } = await import("open");
1379
1689
  await open(url);
@@ -1386,25 +1696,12 @@ async function runDashboard(cwd2, opts) {
1386
1696
  }
1387
1697
 
1388
1698
  // src/commands/doctor.ts
1389
- import pc3 from "picocolors";
1699
+ import pc4 from "picocolors";
1390
1700
 
1391
1701
  // src/core/doctor.ts
1392
- import { existsSync as existsSync9, readFileSync as readFileSync6 } from "fs";
1393
- import { dirname as dirname6, join as join11 } from "path";
1702
+ import { existsSync as existsSync11, readFileSync as readFileSync7 } from "fs";
1703
+ import { dirname as dirname6, join as join13 } from "path";
1394
1704
  import { fileURLToPath as fileURLToPath5 } from "url";
1395
-
1396
- // src/core/package-data.ts
1397
- import { existsSync as existsSync8 } from "fs";
1398
- import { createRequire } from "module";
1399
- import { dirname as dirname5, join as join10 } from "path";
1400
- import { fileURLToPath as fileURLToPath4 } from "url";
1401
- var require2 = createRequire(import.meta.url);
1402
- var here = dirname5(fileURLToPath4(import.meta.url));
1403
- var candidates = [join10(here, "..", "..", "package.json"), join10(here, "..", "package.json")];
1404
- var pkgPath = candidates.find((p8) => existsSync8(p8)) ?? candidates[0];
1405
- var pkg = require2(pkgPath);
1406
-
1407
- // src/core/doctor.ts
1408
1705
  var REGISTRY_URL = `https://registry.npmjs.org/${pkg.name}/latest`;
1409
1706
  var TIMEOUT_MS = 2e3;
1410
1707
  var LIB_VERSION_CACHE_TTL_MS = 5 * 60 * 1e3;
@@ -1452,19 +1749,21 @@ function getProviderAgentInfo(provider) {
1452
1749
  return { agentsDir: ".opencode/agents", ext: ".md" };
1453
1750
  case "codex-cli":
1454
1751
  return { agentsDir: ".codex/agents", ext: ".toml" };
1752
+ case "grok-cli":
1753
+ return { agentsDir: ".grok/agents", ext: ".md" };
1455
1754
  default:
1456
1755
  return { agentsDir: ".claude/agents", ext: ".md" };
1457
1756
  }
1458
1757
  }
1459
1758
  function checkAgentFilesAtRoot(agentsRoot, ext) {
1460
1759
  return AGENT_NAMES.map((name) => {
1461
- const filePath = join11(agentsRoot, `${name}${ext}`);
1462
- return { name, status: existsSync9(filePath) ? "ok" : "missing" };
1760
+ const filePath = join13(agentsRoot, `${name}${ext}`);
1761
+ return { name, status: existsSync11(filePath) ? "ok" : "missing" };
1463
1762
  });
1464
1763
  }
1465
1764
  function checkAgentFiles(cwd2, provider) {
1466
1765
  const { agentsDir, ext } = getProviderAgentInfo(provider);
1467
- return checkAgentFilesAtRoot(join11(cwd2, agentsDir), ext);
1766
+ return checkAgentFilesAtRoot(join13(cwd2, agentsDir), ext);
1468
1767
  }
1469
1768
  function getProviderSkillsDir(provider) {
1470
1769
  switch (provider) {
@@ -1474,21 +1773,23 @@ function getProviderSkillsDir(provider) {
1474
1773
  return ".opencode/skills";
1475
1774
  case "codex-cli":
1476
1775
  return ".agents/skills";
1776
+ case "grok-cli":
1777
+ return ".grok/skills";
1477
1778
  default:
1478
1779
  return ".claude/skills";
1479
1780
  }
1480
1781
  }
1481
1782
  function checkSkillsAtRoot(skillsRoot) {
1482
- const skillSourceBase = join11(__dirname4, "skills");
1783
+ const skillSourceBase = join13(__dirname4, "skills");
1483
1784
  return SKILL_NAMES.map((name) => {
1484
- const livePath = join11(skillsRoot, name, "SKILL.md");
1485
- const sourcePath = join11(skillSourceBase, name, "SKILL.md");
1486
- if (!existsSync9(livePath)) {
1785
+ const livePath = join13(skillsRoot, name, "SKILL.md");
1786
+ const sourcePath = join13(skillSourceBase, name, "SKILL.md");
1787
+ if (!existsSync11(livePath)) {
1487
1788
  return { name, status: "missing" };
1488
1789
  }
1489
1790
  try {
1490
- const live = readFileSync6(livePath, "utf8");
1491
- const source = readFileSync6(sourcePath, "utf8");
1791
+ const live = readFileSync7(livePath, "utf8");
1792
+ const source = readFileSync7(sourcePath, "utf8");
1492
1793
  return { name, status: live === source ? "ok" : "outdated" };
1493
1794
  } catch {
1494
1795
  return { name, status: "outdated" };
@@ -1497,7 +1798,7 @@ function checkSkillsAtRoot(skillsRoot) {
1497
1798
  }
1498
1799
  function checkSkills(cwd2, provider) {
1499
1800
  const skillsDir = getProviderSkillsDir(provider);
1500
- return checkSkillsAtRoot(join11(cwd2, skillsDir));
1801
+ return checkSkillsAtRoot(join13(cwd2, skillsDir));
1501
1802
  }
1502
1803
  async function getDoctorStatus(cwd2) {
1503
1804
  const lib = await checkLibVersion();
@@ -1519,16 +1820,16 @@ async function getDoctorStatus(cwd2) {
1519
1820
 
1520
1821
  // src/commands/doctor.ts
1521
1822
  function ok(label, detail) {
1522
- console.log(` ${pc3.cyan(label.padEnd(16))}${pc3.green("[\u2713]")} ${detail}`);
1823
+ console.log(` ${pc4.cyan(label.padEnd(16))}${pc4.green("[\u2713]")} ${detail}`);
1523
1824
  }
1524
1825
  function warn(label, detail, hint) {
1525
- console.log(` ${pc3.cyan(label.padEnd(16))}${pc3.yellow("[!]")} ${detail}`);
1826
+ console.log(` ${pc4.cyan(label.padEnd(16))}${pc4.yellow("[!]")} ${detail}`);
1526
1827
  if (hint) {
1527
- console.log(` ${"".padEnd(16)} ${pc3.dim(hint)}`);
1828
+ console.log(` ${"".padEnd(16)} ${pc4.dim(hint)}`);
1528
1829
  }
1529
1830
  }
1530
1831
  function neutral(label, detail) {
1531
- console.log(` ${pc3.cyan(label.padEnd(16))}${pc3.dim("[~]")} ${detail}`);
1832
+ console.log(` ${pc4.cyan(label.padEnd(16))}${pc4.dim("[~]")} ${detail}`);
1532
1833
  }
1533
1834
  function printLibSection(lib) {
1534
1835
  if (lib.latest === null) {
@@ -1580,7 +1881,7 @@ async function runDoctor(cwd2) {
1580
1881
  configFound = false;
1581
1882
  }
1582
1883
  console.log("");
1583
- console.log(pc3.bold(`\u25CF ahk doctor ` + "\u2500".repeat(44)));
1884
+ console.log(pc4.bold(`\u25CF ahk doctor ` + "\u2500".repeat(44)));
1584
1885
  console.log("");
1585
1886
  const status = await getDoctorStatus(cwd2);
1586
1887
  printLibSection(status.lib);
@@ -1599,10 +1900,10 @@ async function runDoctor(cwd2) {
1599
1900
 
1600
1901
  // src/commands/export.ts
1601
1902
  import { writeFileSync as writeFileSync7 } from "fs";
1602
- import pc4 from "picocolors";
1903
+ import pc5 from "picocolors";
1603
1904
  async function runExport(cwd2, opts) {
1604
1905
  if (!opts.sql && !opts.json) {
1605
- console.error(pc4.red("Specify --sql or --json"));
1906
+ console.error(pc5.red("Specify --sql or --json"));
1606
1907
  process.exit(1);
1607
1908
  }
1608
1909
  const config = await loadConfig(cwd2);
@@ -1613,13 +1914,13 @@ async function runExport(cwd2, opts) {
1613
1914
  const out = JSON.stringify(data, null, 2) + "\n";
1614
1915
  if (opts.output) {
1615
1916
  writeFileSync7(opts.output, out, "utf8");
1616
- console.log(pc4.green(`\u2713 Exported JSON \u2192 ${opts.output}`));
1917
+ console.log(pc5.green(`\u2713 Exported JSON \u2192 ${opts.output}`));
1617
1918
  } else {
1618
1919
  process.stdout.write(out);
1619
1920
  }
1620
1921
  }
1621
1922
  if (opts.sql) {
1622
- console.error(pc4.dim("SQL dump requires direct SQLite access \u2014 use: sqlite3 .harness/harness.db .dump"));
1923
+ console.error(pc5.dim("SQL dump requires direct SQLite access \u2014 use: sqlite3 .harness/harness.db .dump"));
1623
1924
  process.exit(1);
1624
1925
  }
1625
1926
  } finally {
@@ -1629,28 +1930,28 @@ async function runExport(cwd2, opts) {
1629
1930
 
1630
1931
  // src/commands/health.ts
1631
1932
  import { spawnSync } from "child_process";
1632
- import { existsSync as existsSync10 } from "fs";
1933
+ import { existsSync as existsSync12 } from "fs";
1633
1934
  import { homedir as homedir2 } from "os";
1634
- import { join as join12, resolve as resolve6 } from "path";
1635
- import pc5 from "picocolors";
1935
+ import { join as join14, resolve as resolve6 } from "path";
1936
+ import pc6 from "picocolors";
1636
1937
  function checkLine(label, ok3, message, indent = 0) {
1637
- const prefix = label ? pc5.cyan(`[${label}] `) : " ".repeat(indent);
1638
- const icon = ok3 ? pc5.green("\u2713") : pc5.red("\u2717");
1639
- console.log(prefix + icon + " " + (ok3 ? pc5.green(message) : pc5.red(message)));
1938
+ const prefix = label ? pc6.cyan(`[${label}] `) : " ".repeat(indent);
1939
+ const icon = ok3 ? pc6.green("\u2713") : pc6.red("\u2717");
1940
+ console.log(prefix + icon + " " + (ok3 ? pc6.green(message) : pc6.red(message)));
1640
1941
  }
1641
1942
  async function runHealth(cwd2) {
1642
1943
  let config;
1643
1944
  try {
1644
1945
  config = await loadConfig(cwd2);
1645
1946
  } catch {
1646
- console.error(pc5.red("\u2717 No config found. Run: ahk init"));
1947
+ console.error(pc6.red("\u2717 No config found. Run: ahk init"));
1647
1948
  process.exit(1);
1648
1949
  }
1649
1950
  let allOk = true;
1650
1951
  let dbOk;
1651
1952
  if (config.database.type === "sqlite") {
1652
1953
  const dbPath = resolveSqlitePath(config, cwd2, homedir2());
1653
- dbOk = existsSync10(dbPath);
1954
+ dbOk = existsSync12(dbPath);
1654
1955
  checkLine("checking DB", dbOk, `${dbPath} reachable`);
1655
1956
  } else {
1656
1957
  dbOk = true;
@@ -1663,8 +1964,8 @@ async function runHealth(cwd2) {
1663
1964
  const agentsLabelWidth = "[checking agents] ".length;
1664
1965
  for (let i = 0; i < agentNames.length; i++) {
1665
1966
  const name = agentNames[i];
1666
- const agentPath = join12(cwd2, agentsDir, `${name}${providerFiles.agentExtension}`);
1667
- const ok3 = existsSync10(agentPath);
1967
+ const agentPath = join14(cwd2, agentsDir, `${name}${providerFiles.agentExtension}`);
1968
+ const ok3 = existsSync12(agentPath);
1668
1969
  checkLine(
1669
1970
  i === 0 ? "checking agents" : null,
1670
1971
  ok3,
@@ -1676,18 +1977,18 @@ async function runHealth(cwd2) {
1676
1977
  if (config.tools.mcp.enabled) {
1677
1978
  const mcpFile = providerFiles.mcpFile;
1678
1979
  const mcpPath = resolve6(cwd2, mcpFile);
1679
- const mcpOk = existsSync10(mcpPath);
1980
+ const mcpOk = existsSync12(mcpPath);
1680
1981
  checkLine("checking MCP", mcpOk, `${mcpFile} valid`);
1681
1982
  if (!mcpOk) allOk = false;
1682
1983
  }
1683
1984
  if (!allOk) {
1684
1985
  console.log("");
1685
- console.error(pc5.red("\u2717 Harness checks failed \u2014 fix the above before running health.sh"));
1986
+ console.error(pc6.red("\u2717 Harness checks failed \u2014 fix the above before running health.sh"));
1686
1987
  process.exit(1);
1687
1988
  }
1688
1989
  const scriptPath = resolve6(cwd2, config.health.scriptPath);
1689
- if (!existsSync10(scriptPath)) {
1690
- console.error(pc5.red(`\u2717 health.sh not found: ${scriptPath}`));
1990
+ if (!existsSync12(scriptPath)) {
1991
+ console.error(pc6.red(`\u2717 health.sh not found: ${scriptPath}`));
1691
1992
  console.error(" Run ahk init first.");
1692
1993
  process.exit(1);
1693
1994
  }
@@ -1697,14 +1998,14 @@ async function runHealth(cwd2) {
1697
1998
  encoding: "utf8"
1698
1999
  });
1699
2000
  if (result.error) {
1700
- console.error(pc5.red(`\u2717 Failed to run health.sh: ${result.error.message}`));
2001
+ console.error(pc6.red(`\u2717 Failed to run health.sh: ${result.error.message}`));
1701
2002
  process.exit(1);
1702
2003
  }
1703
2004
  if (result.status === 0) {
1704
- console.log(pc5.green("\u2713 Health check passed"));
2005
+ console.log(pc6.green("\u2713 Health check passed"));
1705
2006
  process.exit(0);
1706
2007
  } else {
1707
- console.error(pc5.red(`\u2717 Health check failed (exit ${result.status ?? "unknown"})`));
2008
+ console.error(pc6.red(`\u2717 Health check failed (exit ${result.status ?? "unknown"})`));
1708
2009
  process.exit(result.status ?? 1);
1709
2010
  }
1710
2011
  }
@@ -1716,14 +2017,16 @@ function getProviderHealthFiles(provider) {
1716
2017
  return { agentsDir: ".opencode/agents", agentExtension: ".md", mcpFile: "opencode.json" };
1717
2018
  case "codex-cli":
1718
2019
  return { agentsDir: ".codex/agents", agentExtension: ".toml", mcpFile: ".codex/config.toml" };
2020
+ case "grok-cli":
2021
+ return { agentsDir: ".grok/agents", agentExtension: ".md", mcpFile: ".grok/config.toml" };
1719
2022
  default:
1720
2023
  throw new Error(`Unknown provider: ${provider}`);
1721
2024
  }
1722
2025
  }
1723
2026
 
1724
2027
  // src/commands/init.ts
1725
- import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync8 } from "fs";
1726
- import { join as join15 } from "path";
2028
+ import { existsSync as existsSync14, mkdirSync as mkdirSync7, readFileSync as readFileSync9, writeFileSync as writeFileSync8 } from "fs";
2029
+ import { join as join16 } from "path";
1727
2030
  import * as p3 from "@clack/prompts";
1728
2031
  import pc8 from "picocolors";
1729
2032
 
@@ -1778,52 +2081,13 @@ var cliFormWithRetry = async (formFn, schema) => {
1778
2081
 
1779
2082
  // src/commands/init-helpers.ts
1780
2083
  import { randomUUID } from "crypto";
1781
- import { existsSync as existsSync12, readFileSync as readFileSync8 } from "fs";
1782
- import { join as join14 } from "path";
2084
+ import { existsSync as existsSync13, readFileSync as readFileSync8 } from "fs";
2085
+ import { join as join15 } from "path";
1783
2086
  import pc7 from "picocolors";
1784
-
1785
- // src/core/local-install-guard.ts
1786
- import { existsSync as existsSync11, readFileSync as readFileSync7 } from "fs";
1787
- import { join as join13 } from "path";
1788
- import pc6 from "picocolors";
1789
- function isLocalInstallSatisfied(cwd2) {
1790
- const selfPkgPath = join13(cwd2, "package.json");
1791
- let projectPkg = null;
1792
- if (existsSync11(selfPkgPath)) {
1793
- try {
1794
- const selfPkg = JSON.parse(readFileSync7(selfPkgPath, "utf8"));
1795
- if (selfPkg?.name === pkg.name) return true;
1796
- projectPkg = selfPkg;
1797
- } catch {
1798
- }
1799
- }
1800
- const [scope, name] = pkg.name.split("/");
1801
- const localPath = pkg.name.startsWith("@") ? join13(cwd2, "node_modules", scope, name) : join13(cwd2, "node_modules", pkg.name);
1802
- if (existsSync11(localPath)) return true;
1803
- const isPnp = existsSync11(join13(cwd2, ".pnp.cjs")) || existsSync11(join13(cwd2, ".pnp.loader.mjs"));
1804
- if (isPnp && projectPkg) {
1805
- const deps = {
1806
- ...projectPkg.dependencies ?? {},
1807
- ...projectPkg.devDependencies ?? {}
1808
- };
1809
- if (Object.prototype.hasOwnProperty.call(deps, pkg.name)) return true;
1810
- }
1811
- return false;
1812
- }
1813
- function printLocalInstallWarning() {
1814
- console.error(pc6.yellow(`\u26A0 ${pkg.name} is not installed locally in this project.`));
1815
- console.error(pc6.dim(" This is only a recommendation for reproducibility: pinning a local"));
1816
- console.error(pc6.dim(" version keeps behavior consistent across your team and CI, instead of"));
1817
- console.error(pc6.dim(" drifting with whatever version is installed globally on each machine."));
1818
- console.error(pc6.dim(` Run: npm install --save-dev ${pkg.name}`));
1819
- console.error(pc6.dim(" (or the equivalent for your package manager: pnpm add -D, yarn add --dev, bun add -d)"));
1820
- }
1821
-
1822
- // src/commands/init-helpers.ts
1823
2087
  function readProjectNameFromPackageJson(cwd2) {
1824
2088
  try {
1825
- const pkgPath2 = join14(cwd2, "package.json");
1826
- if (!existsSync12(pkgPath2)) return null;
2089
+ const pkgPath2 = join15(cwd2, "package.json");
2090
+ if (!existsSync13(pkgPath2)) return null;
1827
2091
  const content = readFileSync8(pkgPath2, "utf8");
1828
2092
  const pkg2 = JSON.parse(content);
1829
2093
  const name = pkg2?.name;
@@ -1836,9 +2100,9 @@ function readProjectNameFromPackageJson(cwd2) {
1836
2100
  function detectConfigExtension(cwd2) {
1837
2101
  if (!isLocalInstallSatisfied(cwd2)) return "json";
1838
2102
  try {
1839
- if (existsSync12(join14(cwd2, "tsconfig.json"))) return "ts";
1840
- const pkgPath2 = join14(cwd2, "package.json");
1841
- if (!existsSync12(pkgPath2)) return "mjs";
2103
+ if (existsSync13(join15(cwd2, "tsconfig.json"))) return "ts";
2104
+ const pkgPath2 = join15(cwd2, "package.json");
2105
+ if (!existsSync13(pkgPath2)) return "mjs";
1842
2106
  const pkg2 = JSON.parse(readFileSync8(pkgPath2, "utf8"));
1843
2107
  if (pkg2?.type === "module") return "mjs";
1844
2108
  } catch {
@@ -1918,6 +2182,34 @@ function printWelcomeMessage(projectName) {
1918
2182
  }
1919
2183
 
1920
2184
  // src/commands/init.ts
2185
+ async function reconcileFeatureList(db, installDir, storageDir, firstTask) {
2186
+ const featureListPath = join16(installDir, storageDir, "feature_list.json");
2187
+ let existingSeeds = [];
2188
+ let parseFailed = false;
2189
+ if (existsSync14(featureListPath)) {
2190
+ try {
2191
+ const parsed = JSON.parse(readFileSync9(featureListPath, "utf8"));
2192
+ if (!Array.isArray(parsed)) throw new Error("feature_list.json is not a JSON array");
2193
+ existingSeeds = parsed;
2194
+ } catch {
2195
+ parseFailed = true;
2196
+ }
2197
+ }
2198
+ const firstTaskSeed = firstTask ? {
2199
+ slug: slugify(firstTask.title),
2200
+ title: firstTask.title,
2201
+ description: firstTask.description,
2202
+ acceptance: firstTask.acceptance
2203
+ } : void 0;
2204
+ if (parseFailed) {
2205
+ if (firstTaskSeed) await db.syncFromFeatureList([firstTaskSeed]);
2206
+ } else {
2207
+ const seeds = firstTaskSeed ? [...existingSeeds, firstTaskSeed] : existingSeeds;
2208
+ await db.syncFromFeatureList(seeds);
2209
+ await db.writeFeatureList(installDir);
2210
+ }
2211
+ return { parseFailed };
2212
+ }
1921
2213
  async function runInit(cwd2, flags) {
1922
2214
  const existingConfig = findConfigFile(cwd2);
1923
2215
  if (existingConfig) {
@@ -1968,7 +2260,7 @@ async function runInit(cwd2, flags) {
1968
2260
  return val;
1969
2261
  }, initDescriptionSchema);
1970
2262
  let provider;
1971
- if (flags.provider && ["claude-code", "opencode"].includes(flags.provider)) {
2263
+ if (flags.provider && ["claude-code", "opencode", "codex-cli", "grok-cli"].includes(flags.provider)) {
1972
2264
  provider = flags.provider;
1973
2265
  } else {
1974
2266
  const val = await p3.select({
@@ -1976,7 +2268,8 @@ async function runInit(cwd2, flags) {
1976
2268
  options: [
1977
2269
  { value: "opencode", label: "OpenCode" },
1978
2270
  { value: "claude-code", label: "Claude Code" },
1979
- { value: "codex-cli", label: "Codex CLI" }
2271
+ { value: "codex-cli", label: "Codex CLI" },
2272
+ { value: "grok-cli", label: "Grok CLI" }
1980
2273
  ]
1981
2274
  });
1982
2275
  if (p3.isCancel(val)) {
@@ -1985,6 +2278,34 @@ async function runInit(cwd2, flags) {
1985
2278
  }
1986
2279
  provider = val;
1987
2280
  }
2281
+ const AGENT_LABELS = [
2282
+ { key: "lead", label: "Lead" },
2283
+ { key: "explorer", label: "Explorer" },
2284
+ { key: "consultant", label: "Consultant" },
2285
+ { key: "builder", label: "Builder" },
2286
+ { key: "reviewer", label: "Reviewer" }
2287
+ ];
2288
+ const claudeAgentModels = {};
2289
+ if (provider === "claude-code") {
2290
+ for (const agent of AGENT_LABELS) {
2291
+ const val = await p3.select({
2292
+ message: `Model for ${agent.label}`,
2293
+ options: [
2294
+ { value: "inherit", label: "inherit (default)" },
2295
+ { value: "haiku", label: "haiku" },
2296
+ { value: "sonnet", label: "sonnet" },
2297
+ { value: "opus", label: "opus" },
2298
+ { value: "fable", label: "fable" }
2299
+ ],
2300
+ initialValue: "inherit"
2301
+ });
2302
+ if (p3.isCancel(val)) {
2303
+ p3.cancel("Cancelled.");
2304
+ process.exit(0);
2305
+ }
2306
+ claudeAgentModels[agent.key] = val;
2307
+ }
2308
+ }
1988
2309
  let docsPath;
1989
2310
  if (flags.docs) {
1990
2311
  docsPath = flags.docs;
@@ -2076,6 +2397,7 @@ async function runInit(cwd2, flags) {
2076
2397
  firstTask = { title: taskTitle, description: taskDesc, acceptance };
2077
2398
  }
2078
2399
  let configExt = "ts";
2400
+ let featureListParseFailedPath = null;
2079
2401
  const spinner6 = p3.spinner();
2080
2402
  spinner6.start("Scaffolding...");
2081
2403
  try {
@@ -2102,19 +2424,14 @@ async function runInit(cwd2, flags) {
2102
2424
  scope: config.storage.scope,
2103
2425
  projectId: config.storage.projectId
2104
2426
  });
2105
- writeFileSync8(join15(installDir, configFileName), configContent, "utf8");
2106
- mkdirSync7(join15(installDir, config.storage.dir), { recursive: true });
2427
+ writeFileSync8(join16(installDir, configFileName), configContent, "utf8");
2428
+ mkdirSync7(join16(installDir, config.storage.dir), { recursive: true });
2107
2429
  const db = await openDB(config, installDir);
2108
2430
  await db.writeStorageState(installDir);
2109
- await materializer.scaffold(config, { cwd: installDir, firstTask });
2110
- if (firstTask) {
2111
- const slug = slugify(firstTask.title);
2112
- await db.addTask({
2113
- slug,
2114
- title: firstTask.title,
2115
- description: firstTask.description,
2116
- acceptance: firstTask.acceptance
2117
- });
2431
+ await materializer.scaffold(config, { cwd: installDir, firstTask, claudeAgentModels });
2432
+ const { parseFailed } = await reconcileFeatureList(db, installDir, config.storage.dir, firstTask);
2433
+ if (parseFailed) {
2434
+ featureListParseFailedPath = join16(config.storage.dir, "feature_list.json");
2118
2435
  }
2119
2436
  await db.close();
2120
2437
  spinner6.stop("");
@@ -2123,9 +2440,19 @@ async function runInit(cwd2, flags) {
2123
2440
  p3.log.error(err instanceof Error ? err.message : String(err));
2124
2441
  throw err;
2125
2442
  }
2443
+ if (featureListParseFailedPath) {
2444
+ console.log(
2445
+ pc8.yellow("\u26A0") + " Existing " + pc8.bold(featureListParseFailedPath) + " is not valid JSON \u2014 left untouched. Fix it and run `ahk sync`."
2446
+ );
2447
+ }
2126
2448
  console.log(pc8.green("\u2713 Scaffolded harness in current directory"));
2127
- const agentsDir = provider === "claude-code" ? ".claude/agents/" : ".opencode/agents/";
2128
- const mcpFile = provider === "claude-code" ? ".claude/mcp.json" : "./opencode.json";
2449
+ const PROVIDER_SUMMARY_INFO = {
2450
+ "claude-code": { agentsDir: ".claude/agents/", mcpFile: ".mcp.json" },
2451
+ opencode: { agentsDir: ".opencode/agents/", mcpFile: "./opencode.json" },
2452
+ "codex-cli": { agentsDir: ".codex/agents/", mcpFile: ".codex/config.toml" },
2453
+ "grok-cli": { agentsDir: ".grok/agents/", mcpFile: ".grok/config.toml" }
2454
+ };
2455
+ const { agentsDir, mcpFile } = PROVIDER_SUMMARY_INFO[provider];
2129
2456
  console.log("");
2130
2457
  console.log(pc8.green(`\u2713 agent-harness-kit.config.${configExt}`));
2131
2458
  console.log(pc8.green("\u2713 AGENTS.md"));
@@ -2167,7 +2494,7 @@ import pc9 from "picocolors";
2167
2494
  async function runMigrate(cwd2, opts) {
2168
2495
  const config = await loadConfig(cwd2);
2169
2496
  let target;
2170
- if (opts.to && ["claude-code", "opencode", "codex-cli"].includes(opts.to)) {
2497
+ if (opts.to && ["claude-code", "opencode", "codex-cli", "grok-cli"].includes(opts.to)) {
2171
2498
  target = opts.to;
2172
2499
  } else {
2173
2500
  const val = await p4.select({
@@ -2175,7 +2502,8 @@ async function runMigrate(cwd2, opts) {
2175
2502
  options: [
2176
2503
  { value: "claude-code", label: "Claude Code" },
2177
2504
  { value: "opencode", label: "OpenCode" },
2178
- { value: "codex-cli", label: "Codex CLI" }
2505
+ { value: "codex-cli", label: "Codex CLI" },
2506
+ { value: "grok-cli", label: "Grok CLI" }
2179
2507
  ]
2180
2508
  });
2181
2509
  if (p4.isCancel(val)) {
@@ -2204,9 +2532,9 @@ async function runMigrate(cwd2, opts) {
2204
2532
  }
2205
2533
 
2206
2534
  // src/commands/migrate-storage.ts
2207
- import { copyFileSync, existsSync as existsSync13, mkdirSync as mkdirSync8, rmSync, writeFileSync as writeFileSync9 } from "fs";
2535
+ import { copyFileSync, existsSync as existsSync15, mkdirSync as mkdirSync8, rmSync, writeFileSync as writeFileSync9 } from "fs";
2208
2536
  import { homedir as homedir3 } from "os";
2209
- import { dirname as dirname7, join as join16, resolve as resolve7 } from "path";
2537
+ import { dirname as dirname7, join as join17, resolve as resolve7 } from "path";
2210
2538
  import pc10 from "picocolors";
2211
2539
  function log5(msg) {
2212
2540
  console.log(msg);
@@ -2218,14 +2546,14 @@ function defaultMarkdownPathForConfig(config) {
2218
2546
  return config.storage.scope === "local" ? config.storage.markdownFallback.path : DEFAULT_MARKDOWN_PATH;
2219
2547
  }
2220
2548
  function currentMdPathForScope(scope, config, cwd2, homeDir) {
2221
- return scope === "global" ? join16(resolveGlobalStorageDir(config, homeDir), "current.md") : resolve7(cwd2, defaultMarkdownPathForConfig(config));
2549
+ return scope === "global" ? join17(resolveGlobalStorageDir(config, homeDir), "current.md") : resolve7(cwd2, defaultMarkdownPathForConfig(config));
2222
2550
  }
2223
2551
  function defaultSqlitePathForConfig(config) {
2224
2552
  return config.storage.scope === "local" && config.database.type === "sqlite" ? config.storage.sqlitePath ?? DEFAULT_SQLITE_PATH : DEFAULT_SQLITE_PATH;
2225
2553
  }
2226
2554
  async function backupDestination(cwd2, storageDir, data) {
2227
2555
  const backupsDir = resolve7(cwd2, storageDir, "backups");
2228
- const path = join16(backupsDir, `pre-migrate-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.json`);
2556
+ const path = join17(backupsDir, `pre-migrate-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.json`);
2229
2557
  try {
2230
2558
  mkdirSync8(backupsDir, { recursive: true });
2231
2559
  writeFileSync9(path, JSON.stringify(data, null, 2) + "\n", "utf8");
@@ -2240,11 +2568,11 @@ function copySqliteFile(srcPath, destPath) {
2240
2568
  mkdirSync8(dirname7(destPath), { recursive: true });
2241
2569
  copyFileSync(srcPath, destPath);
2242
2570
  for (const suffix of ["-wal", "-shm"]) {
2243
- if (existsSync13(`${srcPath}${suffix}`)) {
2571
+ if (existsSync15(`${srcPath}${suffix}`)) {
2244
2572
  copyFileSync(`${srcPath}${suffix}`, `${destPath}${suffix}`);
2245
2573
  }
2246
2574
  }
2247
- if (!existsSync13(destPath)) {
2575
+ if (!existsSync15(destPath)) {
2248
2576
  throw new Error(`Copy verification failed: ${destPath} does not exist after copy.`);
2249
2577
  }
2250
2578
  }
@@ -2303,8 +2631,8 @@ async function runMigrateStorage(cwd2, opts, homeDir = homedir3()) {
2303
2631
  return migrateAcrossDbType(cwd2, config, homeDir, realScope, opts);
2304
2632
  }
2305
2633
  async function probeTaskCount(dbPath) {
2306
- if (!existsSync13(dbPath)) return 0;
2307
- const { SQLiteDriver } = await import("./sqlite-TR4D324R.js");
2634
+ if (!existsSync15(dbPath)) return 0;
2635
+ const { SQLiteDriver } = await import("./sqlite-5OWKTUUZ.js");
2308
2636
  const driver = new SQLiteDriver(dbPath);
2309
2637
  try {
2310
2638
  await driver.ensureSchema();
@@ -2320,12 +2648,12 @@ async function migrateScopeOnly(cwd2, config, homeDir, fromScope, toScope, opts)
2320
2648
  const destDb = resolveSqlitePathForScope(toScope, sqlitePath, cwd2, config, homeDir);
2321
2649
  const srcMd = currentMdPathForScope(fromScope, config, cwd2, homeDir);
2322
2650
  const destMd = currentMdPathForScope(toScope, config, cwd2, homeDir);
2323
- if (!existsSync13(srcDb)) {
2651
+ if (!existsSync15(srcDb)) {
2324
2652
  fail(`Source database not found at ${srcDb} (expected ${fromScope} scope) \u2014 nothing to move.`);
2325
2653
  }
2326
- const destExists = existsSync13(destDb);
2654
+ const destExists = existsSync15(destDb);
2327
2655
  if (destExists) {
2328
- const { SQLiteDriver } = await import("./sqlite-TR4D324R.js");
2656
+ const { SQLiteDriver } = await import("./sqlite-5OWKTUUZ.js");
2329
2657
  const destDriver = new SQLiteDriver(destDb);
2330
2658
  let destEmpty;
2331
2659
  try {
@@ -2340,12 +2668,12 @@ async function migrateScopeOnly(cwd2, config, homeDir, fromScope, toScope, opts)
2340
2668
  );
2341
2669
  }
2342
2670
  if (!destEmpty && opts.force) {
2343
- const { SQLiteDriver: Driver } = await import("./sqlite-TR4D324R.js");
2671
+ const { SQLiteDriver: Driver } = await import("./sqlite-5OWKTUUZ.js");
2344
2672
  const backupDriver = new Driver(destDb);
2345
2673
  let data;
2346
2674
  try {
2347
2675
  await backupDriver.ensureSchema();
2348
- const { HarnessDB } = await import("./db-3OXHRFAR.js");
2676
+ const { HarnessDB } = await import("./db-L3AADJF5.js");
2349
2677
  const tmpDb = new HarnessDB(backupDriver, config, homeDir);
2350
2678
  data = await tmpDb.exportJson();
2351
2679
  } finally {
@@ -2361,7 +2689,7 @@ async function migrateScopeOnly(cwd2, config, homeDir, fromScope, toScope, opts)
2361
2689
  }
2362
2690
  copySqliteFile(srcDb, destDb);
2363
2691
  log5(pc10.green(`\u2713 Copied database ${srcDb} \u2192 ${destDb}`));
2364
- if (existsSync13(srcMd)) {
2692
+ if (existsSync15(srcMd)) {
2365
2693
  mkdirSync8(dirname7(destMd), { recursive: true });
2366
2694
  copyFileSync(srcMd, destMd);
2367
2695
  log5(pc10.green(`\u2713 Copied current.md ${srcMd} \u2192 ${destMd}`));
@@ -2369,7 +2697,7 @@ async function migrateScopeOnly(cwd2, config, homeDir, fromScope, toScope, opts)
2369
2697
  rmSync(srcDb, { force: true });
2370
2698
  rmSync(`${srcDb}-wal`, { force: true });
2371
2699
  rmSync(`${srcDb}-shm`, { force: true });
2372
- if (existsSync13(srcMd) && srcMd !== destMd) rmSync(srcMd, { force: true });
2700
+ if (existsSync15(srcMd) && srcMd !== destMd) rmSync(srcMd, { force: true });
2373
2701
  const db = await openDB(config, cwd2, homeDir);
2374
2702
  try {
2375
2703
  await db.writeStorageState(cwd2);
@@ -2381,17 +2709,17 @@ async function migrateScopeOnly(cwd2, config, homeDir, fromScope, toScope, opts)
2381
2709
  async function migrateAcrossDbType(cwd2, config, homeDir, sourceScope, opts) {
2382
2710
  const sqlitePath = defaultSqlitePathForConfig(config);
2383
2711
  const srcPath = resolveSqlitePathForScope(sourceScope, sqlitePath, cwd2, config, homeDir);
2384
- if (!existsSync13(srcPath)) {
2712
+ if (!existsSync15(srcPath)) {
2385
2713
  fail(`Source sqlite database not found at ${srcPath} (expected ${sourceScope} scope) \u2014 nothing to migrate.`);
2386
2714
  }
2387
- const { SQLiteDriver } = await import("./sqlite-TR4D324R.js");
2715
+ const { SQLiteDriver } = await import("./sqlite-5OWKTUUZ.js");
2388
2716
  const srcDriver = new SQLiteDriver(srcPath);
2389
2717
  let sourceData;
2390
2718
  let sourceCounts;
2391
2719
  try {
2392
2720
  await srcDriver.ensureSchema();
2393
2721
  sourceCounts = await getRowCounts(srcDriver);
2394
- const { HarnessDB } = await import("./db-3OXHRFAR.js");
2722
+ const { HarnessDB } = await import("./db-L3AADJF5.js");
2395
2723
  const srcDb = new HarnessDB(srcDriver, config, homeDir);
2396
2724
  sourceData = await srcDb.exportJson();
2397
2725
  } finally {
@@ -2444,16 +2772,29 @@ async function migrateAcrossDbType(cwd2, config, homeDir, sourceScope, opts) {
2444
2772
  }
2445
2773
 
2446
2774
  // src/commands/reset.ts
2447
- import { existsSync as existsSync14, readdirSync, rmSync as rmSync2 } from "fs";
2775
+ import { existsSync as existsSync16, readdirSync, rmSync as rmSync2 } from "fs";
2448
2776
  import { homedir as homedir4 } from "os";
2449
- import { join as join17, resolve as resolve8 } from "path";
2777
+ import { join as join18, resolve as resolve8 } from "path";
2450
2778
  import * as p5 from "@clack/prompts";
2451
2779
  import pc11 from "picocolors";
2452
2780
  var AGENT_MD_FILES = ["lead", "explorer", "consultant", "builder", "reviewer"];
2781
+ var PROVIDER_AGENT_DIRS = {
2782
+ "claude-code": ".claude/agents",
2783
+ opencode: ".opencode/agents",
2784
+ "codex-cli": ".codex/agents",
2785
+ "grok-cli": ".grok/agents"
2786
+ };
2787
+ var PROVIDER_AGENT_EXT = {
2788
+ "claude-code": ".md",
2789
+ opencode: ".md",
2790
+ "codex-cli": ".toml",
2791
+ "grok-cli": ".md"
2792
+ };
2453
2793
  async function resetAgentMds(cwd2, provider) {
2454
- const agentDir = provider === "claude-code" ? ".claude/agents" : ".opencode/agents";
2794
+ const agentDir = PROVIDER_AGENT_DIRS[provider];
2455
2795
  const agentDirPath = resolve8(cwd2, agentDir);
2456
- if (!existsSync14(agentDirPath)) {
2796
+ const agentExt = PROVIDER_AGENT_EXT[provider];
2797
+ if (!existsSync16(agentDirPath)) {
2457
2798
  console.log(pc11.yellow(` Skipping agent files \u2014 directory not found: ${agentDirPath}`));
2458
2799
  return;
2459
2800
  }
@@ -2461,7 +2802,7 @@ async function resetAgentMds(cwd2, provider) {
2461
2802
  try {
2462
2803
  const files = readdirSync(agentDirPath);
2463
2804
  for (const f of files) {
2464
- if (f.endsWith(".md") && AGENT_MD_FILES.includes(f.replace(".md", ""))) {
2805
+ if (f.endsWith(agentExt) && AGENT_MD_FILES.includes(f.replace(agentExt, ""))) {
2465
2806
  existingFiles.push(f);
2466
2807
  }
2467
2808
  }
@@ -2484,7 +2825,7 @@ async function resetAgentMds(cwd2, provider) {
2484
2825
  }
2485
2826
  if (confirm3) {
2486
2827
  try {
2487
- const filePath = join17(agentDirPath, file);
2828
+ const filePath = join18(agentDirPath, file);
2488
2829
  rmSync2(filePath, { force: true });
2489
2830
  console.log(pc11.green(` Removed ${file}`));
2490
2831
  } catch {
@@ -2509,7 +2850,7 @@ async function runReset(cwd2, opts) {
2509
2850
  let resetDb = false;
2510
2851
  let resetFeatureList = false;
2511
2852
  let resetAgentMdsFlag = false;
2512
- if (dbPath && existsSync14(dbPath)) {
2853
+ if (dbPath && existsSync16(dbPath)) {
2513
2854
  if (opts.force) {
2514
2855
  resetDb = true;
2515
2856
  } else {
@@ -2531,7 +2872,7 @@ async function runReset(cwd2, opts) {
2531
2872
  } else if (!dbPath) {
2532
2873
  console.log(pc11.dim(` Skipping DB reset \u2014 remote ${config.database.type} database is not managed by this command.`));
2533
2874
  }
2534
- if (existsSync14(featureListPath)) {
2875
+ if (existsSync16(featureListPath)) {
2535
2876
  if (opts.force) {
2536
2877
  resetFeatureList = true;
2537
2878
  } else {
@@ -2580,8 +2921,8 @@ async function runReset(cwd2, opts) {
2580
2921
  }
2581
2922
 
2582
2923
  // src/core/mcp-server.ts
2583
- import { existsSync as existsSync16, mkdirSync as mkdirSync9, readdirSync as readdirSync2, readFileSync as readFileSync9, statSync, writeFileSync as writeFileSync10 } from "fs";
2584
- import { join as join19, resolve as resolve9 } from "path";
2924
+ import { existsSync as existsSync18, mkdirSync as mkdirSync9, readdirSync as readdirSync2, readFileSync as readFileSync10, statSync, writeFileSync as writeFileSync10 } from "fs";
2925
+ import { join as join20, resolve as resolve9 } from "path";
2585
2926
  import { Server } from "@modelcontextprotocol/sdk/server";
2586
2927
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
2587
2928
  import {
@@ -2590,8 +2931,8 @@ import {
2590
2931
  } from "@modelcontextprotocol/sdk/types.js";
2591
2932
 
2592
2933
  // src/core/permissions-check.ts
2593
- import { existsSync as existsSync15 } from "fs";
2594
- import { join as join18 } from "path";
2934
+ import { existsSync as existsSync17 } from "fs";
2935
+ import { join as join19 } from "path";
2595
2936
  var AGENTS = ["lead", "explorer", "consultant", "builder", "reviewer"];
2596
2937
  function checkPermissionsSync(cwd2, config) {
2597
2938
  if (config.provider !== "claude-code") {
@@ -2600,8 +2941,8 @@ function checkPermissionsSync(cwd2, config) {
2600
2941
  const agents = {};
2601
2942
  let in_sync = true;
2602
2943
  for (const agent of AGENTS) {
2603
- const filePath = join18(cwd2, ".claude", "agents", `${agent}.md`);
2604
- const exists = existsSync15(filePath);
2944
+ const filePath = join19(cwd2, ".claude", "agents", `${agent}.md`);
2945
+ const exists = existsSync17(filePath);
2605
2946
  if (!exists) in_sync = false;
2606
2947
  agents[agent] = exists ? { ok: true } : { ok: false, reason: "missing_file" };
2607
2948
  }
@@ -2613,7 +2954,7 @@ var VERSION = "0.1.0";
2613
2954
  var TOOLS = [
2614
2955
  {
2615
2956
  name: "actions.start",
2616
- description: "Start a new action for a task. Returns an actionId (UUID).",
2957
+ description: "Start a new action for a task. Returns an actionId.",
2617
2958
  inputSchema: {
2618
2959
  type: "object",
2619
2960
  properties: {
@@ -2628,18 +2969,18 @@ var TOOLS = [
2628
2969
  },
2629
2970
  {
2630
2971
  name: "actions.write",
2631
- description: "Record a section in an action. Standard sections: result, tools_used, blockers, next_steps. Note: files_modified is a plain-text note only \u2014 it does NOT populate the files dashboard. Use actions.record_file to register files in the dashboard.",
2972
+ description: "Record a section in an action. Standard sections: result, tools_used, blockers, next_steps.",
2632
2973
  inputSchema: {
2633
2974
  type: "object",
2634
2975
  properties: {
2635
- actionId: { type: "string", description: "UUID returned by actions.start" },
2976
+ actionId: { type: "number", description: "The actionId returned by actions.start" },
2636
2977
  sectionType: {
2637
2978
  type: "string",
2638
2979
  description: "Section name: result | tools_used | blockers | next_steps | <custom>. Do NOT use files_modified to track files \u2014 it is stored as plain text only. Use actions.record_file instead."
2639
2980
  },
2640
2981
  content: {
2641
2982
  type: "string",
2642
- description: "Content for this section. No length limit \u2014 include all information that's relevant and necessary, but avoid unnecessary padding to prevent context bottlenecks between agents."
2983
+ description: "Content for this section. No length limit; avoid padding \u2014 it costs shared context for other agents."
2643
2984
  }
2644
2985
  },
2645
2986
  required: ["actionId", "sectionType", "content"]
@@ -2651,7 +2992,7 @@ var TOOLS = [
2651
2992
  inputSchema: {
2652
2993
  type: "object",
2653
2994
  properties: {
2654
- actionId: { type: "string", description: "UUID of the action to close" },
2995
+ actionId: { type: "number", description: "The actionId of the action to close" },
2655
2996
  summary: { type: "string", description: "One-line summary of what was done" }
2656
2997
  },
2657
2998
  required: ["actionId", "summary"]
@@ -2726,20 +3067,31 @@ var TOOLS = [
2726
3067
  },
2727
3068
  {
2728
3069
  name: "actions.record_file",
2729
- description: "Record a file touched during an action. This is the only way to populate the files-touched count shown in the dashboard. Call once per file.",
3070
+ description: "Record one or more files touched during an action, atomically (all-or-nothing). This is the only way to populate the files-touched count shown in the dashboard. Batch every file from a step of work into a single call \u2014 a single-element array is correct when only one file was touched.",
2730
3071
  inputSchema: {
2731
3072
  type: "object",
2732
3073
  properties: {
2733
- actionId: { type: "string", description: "UUID returned by actions.start" },
2734
- filePath: { type: "string", description: "Absolute or repo-relative path of the file" },
2735
- operation: {
2736
- type: "string",
2737
- enum: ["read", "created", "modified", "deleted"],
2738
- description: "What was done to the file"
2739
- },
2740
- notes: { type: "string", description: "Optional short note about the change" }
3074
+ actionId: { type: "number", description: "The actionId returned by actions.start" },
3075
+ files: {
3076
+ type: "array",
3077
+ minItems: 1,
3078
+ description: "Files touched, recorded atomically in one transaction.",
3079
+ items: {
3080
+ type: "object",
3081
+ properties: {
3082
+ filePath: { type: "string", description: "Absolute or repo-relative path of the file" },
3083
+ operation: {
3084
+ type: "string",
3085
+ enum: ["read", "created", "modified", "deleted"],
3086
+ description: "What was done to the file"
3087
+ },
3088
+ notes: { type: "string", description: "Optional short note about the change" }
3089
+ },
3090
+ required: ["filePath", "operation"]
3091
+ }
3092
+ }
2741
3093
  },
2742
- required: ["actionId", "filePath", "operation"]
3094
+ required: ["actionId", "files"]
2743
3095
  }
2744
3096
  },
2745
3097
  {
@@ -2780,12 +3132,12 @@ var TOOLS = [
2780
3132
  },
2781
3133
  description: {
2782
3134
  type: "string",
2783
- description: "Longer description of the task goal. No length limit \u2014 include all information that's relevant and necessary, but avoid unnecessary padding to prevent context bottlenecks between agents."
3135
+ description: "Longer description of the task goal. No length limit; avoid padding \u2014 it costs shared context for other agents."
2784
3136
  },
2785
3137
  acceptance: {
2786
3138
  type: "array",
2787
3139
  items: { type: "string" },
2788
- description: "List of acceptance criteria (plain sentences). No length limit \u2014 include all information that's relevant and necessary, but avoid unnecessary padding to prevent context bottlenecks between agents."
3140
+ description: "List of acceptance criteria (plain sentences). No length limit; avoid padding \u2014 it costs shared context for other agents."
2789
3141
  }
2790
3142
  },
2791
3143
  required: ["title"]
@@ -2793,22 +3145,36 @@ var TOOLS = [
2793
3145
  },
2794
3146
  {
2795
3147
  name: "actions.record_tool",
2796
- description: "Record a tool call made during an action. This is the only way to populate the Tools dashboard. Call once per tool invocation.",
3148
+ description: "Record one or more tool calls made during an action, atomically (all-or-nothing). This is the only way to populate the Tools dashboard. Batch every tool call from a step of work into a single call \u2014 a single-element array is correct when only one call was made.",
2797
3149
  inputSchema: {
2798
3150
  type: "object",
2799
3151
  properties: {
2800
- actionId: { type: "string", description: "UUID returned by actions.start" },
2801
- toolName: {
2802
- type: "string",
2803
- description: "Name of the tool that was called (e.g. Read, Bash, Edit)"
2804
- },
2805
- argsJson: {
2806
- type: "string",
2807
- description: "Optional JSON string of the arguments passed to the tool"
2808
- },
2809
- resultSummary: { type: "string", description: "Optional short summary of the tool result" }
3152
+ actionId: { type: "number", description: "The actionId returned by actions.start" },
3153
+ calls: {
3154
+ type: "array",
3155
+ minItems: 1,
3156
+ description: "Tool calls made, recorded atomically in one transaction.",
3157
+ items: {
3158
+ type: "object",
3159
+ properties: {
3160
+ toolName: {
3161
+ type: "string",
3162
+ description: "Name of the tool that was called (e.g. Read, Bash, Edit)"
3163
+ },
3164
+ argsJson: {
3165
+ type: "string",
3166
+ description: "Optional JSON string of the arguments passed to the tool"
3167
+ },
3168
+ resultSummary: {
3169
+ type: "string",
3170
+ description: "Optional short summary of the tool result"
3171
+ }
3172
+ },
3173
+ required: ["toolName"]
3174
+ }
3175
+ }
2810
3176
  },
2811
- required: ["actionId", "toolName"]
3177
+ required: ["actionId", "calls"]
2812
3178
  }
2813
3179
  },
2814
3180
  {
@@ -2899,39 +3265,50 @@ async function dispatch(name, args, db, docsPath, cwd2, config) {
2899
3265
  const taskId = num(args, "taskId");
2900
3266
  const agent = str(args, "agent");
2901
3267
  const action = await db.startAction(taskId, agent);
2902
- return ok2(JSON.stringify({ actionId: action.id, taskId, agent, status: "in_progress" }));
3268
+ return ok2(JSON.stringify({ actionId: action.id }));
2903
3269
  }
2904
3270
  case "actions.write": {
2905
- const actionId = str(args, "actionId");
3271
+ const actionId = num(args, "actionId");
2906
3272
  const sectionType = str(args, "sectionType");
2907
3273
  const content = str(args, "content");
2908
3274
  await db.writeSection(actionId, sectionType, content);
2909
- return ok2(JSON.stringify({ actionId, sectionType, recorded: true }));
3275
+ return ok2(JSON.stringify({ recorded: true }));
2910
3276
  }
2911
3277
  case "actions.complete": {
2912
- const actionId = str(args, "actionId");
3278
+ const actionId = num(args, "actionId");
2913
3279
  const summary = str(args, "summary");
2914
3280
  const action = await db.completeAction(actionId, summary);
2915
- return ok2(
2916
- JSON.stringify({ actionId, status: action.status, completedAt: action.completed_at })
2917
- );
3281
+ return ok2(JSON.stringify({ status: action.status, completedAt: action.completed_at }));
2918
3282
  }
2919
3283
  case "actions.get": {
2920
3284
  const taskId = num(args, "taskId");
2921
3285
  const actions = await db.getActionsForTask(taskId);
2922
3286
  const full = await Promise.all(
2923
- actions.map(async (a) => ({
2924
- ...a,
2925
- sections: await db.getActionSections(a.id)
2926
- }))
3287
+ actions.map(async (a) => {
3288
+ const sections = await db.getActionSections(a.id);
3289
+ return {
3290
+ id: a.id,
3291
+ agent: a.agent,
3292
+ status: a.status,
3293
+ created_at: a.created_at,
3294
+ completed_at: a.completed_at,
3295
+ summary: a.summary,
3296
+ sections: sections.map((s) => ({
3297
+ id: s.id,
3298
+ section_type: s.section_type,
3299
+ content: s.content,
3300
+ created_at: s.created_at
3301
+ }))
3302
+ };
3303
+ })
2927
3304
  );
2928
- return ok2(JSON.stringify(full, null, 2));
3305
+ return ok2(JSON.stringify(full));
2929
3306
  }
2930
3307
  case "tasks.get": {
2931
3308
  const status = args["status"];
2932
3309
  const includeArchived = args["includeArchived"];
2933
3310
  const tasks = status ? await db.getTasks(status, includeArchived ?? false) : await db.getTasks(void 0, includeArchived ?? false);
2934
- return ok2(JSON.stringify(tasks, null, 2));
3311
+ return ok2(JSON.stringify(tasks));
2935
3312
  }
2936
3313
  case "tasks.claim": {
2937
3314
  const id = num(args, "id");
@@ -2962,15 +3339,17 @@ async function dispatch(name, args, db, docsPath, cwd2, config) {
2962
3339
  case "docs.search": {
2963
3340
  const query = str(args, "query");
2964
3341
  const results = searchDocs(docsPath, query);
2965
- return ok2(JSON.stringify(results, null, 2));
3342
+ return ok2(JSON.stringify(results));
2966
3343
  }
2967
3344
  case "actions.record_file": {
2968
- const actionId = str(args, "actionId");
2969
- const filePath = str(args, "filePath");
2970
- const operation = str(args, "operation");
2971
- const notes = args["notes"];
2972
- await db.recordFile(actionId, filePath, operation, notes);
2973
- return ok2(JSON.stringify({ actionId, filePath, operation, recorded: true }));
3345
+ const actionId = num(args, "actionId");
3346
+ const files = nonEmptyArray(args, "files").map((f) => ({
3347
+ filePath: str(f, "filePath"),
3348
+ operation: str(f, "operation"),
3349
+ notes: f["notes"]
3350
+ }));
3351
+ const recorded = await db.recordFiles(actionId, files);
3352
+ return ok2(JSON.stringify({ recorded }));
2974
3353
  }
2975
3354
  case "tasks.acceptance.update": {
2976
3355
  const criterionId = num(args, "criterionId");
@@ -2980,15 +3359,17 @@ async function dispatch(name, args, db, docsPath, cwd2, config) {
2980
3359
  case "tasks.acceptance.get": {
2981
3360
  const taskId = num(args, "taskId");
2982
3361
  const criteria = await db.getTaskAcceptance(taskId);
2983
- return ok2(JSON.stringify(criteria, null, 2));
3362
+ return ok2(JSON.stringify(criteria));
2984
3363
  }
2985
3364
  case "actions.record_tool": {
2986
- const actionId = str(args, "actionId");
2987
- const toolName = str(args, "toolName");
2988
- const argsJson = args["argsJson"];
2989
- const resultSummary = args["resultSummary"];
2990
- await db.recordTool(actionId, toolName, argsJson, resultSummary);
2991
- return ok2(JSON.stringify({ actionId, toolName, recorded: true }));
3365
+ const actionId = num(args, "actionId");
3366
+ const calls = nonEmptyArray(args, "calls").map((c) => ({
3367
+ toolName: str(c, "toolName"),
3368
+ argsJson: c["argsJson"],
3369
+ resultSummary: c["resultSummary"]
3370
+ }));
3371
+ const recorded = await db.recordTools(actionId, calls);
3372
+ return ok2(JSON.stringify({ recorded }));
2992
3373
  }
2993
3374
  case "tasks.edit": {
2994
3375
  const id = num(args, "id");
@@ -3019,22 +3400,22 @@ async function dispatch(name, args, db, docsPath, cwd2, config) {
3019
3400
  }
3020
3401
  case "permissions.check": {
3021
3402
  const result = checkPermissionsSync(cwd2, config);
3022
- return ok2(JSON.stringify(result, null, 2));
3403
+ return ok2(JSON.stringify(result));
3023
3404
  }
3024
3405
  case "deps.snapshot": {
3025
- const pkgPath2 = join19(cwd2, "package.json");
3026
- if (!existsSync16(pkgPath2)) {
3406
+ const pkgPath2 = join20(cwd2, "package.json");
3407
+ if (!existsSync18(pkgPath2)) {
3027
3408
  return ok2("package.json not found in project root", true);
3028
3409
  }
3029
- const pkg2 = JSON.parse(readFileSync9(pkgPath2, "utf8"));
3410
+ const pkg2 = JSON.parse(readFileSync10(pkgPath2, "utf8"));
3030
3411
  const snapshot = {
3031
3412
  capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
3032
3413
  dependencies: pkg2.dependencies ?? {},
3033
3414
  devDependencies: pkg2.devDependencies ?? {}
3034
3415
  };
3035
- const harnessDir = join19(cwd2, ".harness");
3416
+ const harnessDir = join20(cwd2, ".harness");
3036
3417
  mkdirSync9(harnessDir, { recursive: true });
3037
- writeFileSync10(join19(harnessDir, "deps-lock.json"), JSON.stringify(snapshot, null, 2), "utf8");
3418
+ writeFileSync10(join20(harnessDir, "deps-lock.json"), JSON.stringify(snapshot, null, 2), "utf8");
3038
3419
  return ok2(
3039
3420
  JSON.stringify({
3040
3421
  message: "Snapshot saved to .harness/deps-lock.json",
@@ -3043,12 +3424,12 @@ async function dispatch(name, args, db, docsPath, cwd2, config) {
3043
3424
  );
3044
3425
  }
3045
3426
  case "deps.check": {
3046
- const pkgPath2 = join19(cwd2, "package.json");
3047
- const lockPath = join19(cwd2, ".harness", "deps-lock.json");
3048
- if (!existsSync16(pkgPath2)) {
3427
+ const pkgPath2 = join20(cwd2, "package.json");
3428
+ const lockPath = join20(cwd2, ".harness", "deps-lock.json");
3429
+ if (!existsSync18(pkgPath2)) {
3049
3430
  return ok2("package.json not found in project root", true);
3050
3431
  }
3051
- if (!existsSync16(lockPath)) {
3432
+ if (!existsSync18(lockPath)) {
3052
3433
  return ok2(
3053
3434
  JSON.stringify({
3054
3435
  status: "no-snapshot",
@@ -3056,8 +3437,8 @@ async function dispatch(name, args, db, docsPath, cwd2, config) {
3056
3437
  })
3057
3438
  );
3058
3439
  }
3059
- const pkg2 = JSON.parse(readFileSync9(pkgPath2, "utf8"));
3060
- const lock = JSON.parse(readFileSync9(lockPath, "utf8"));
3440
+ const pkg2 = JSON.parse(readFileSync10(pkgPath2, "utf8"));
3441
+ const lock = JSON.parse(readFileSync10(lockPath, "utf8"));
3061
3442
  const current = { ...pkg2.dependencies ?? {}, ...pkg2.devDependencies ?? {} };
3062
3443
  const previous = { ...lock.dependencies ?? {}, ...lock.devDependencies ?? {} };
3063
3444
  const added = [];
@@ -3111,7 +3492,7 @@ async function dispatch(name, args, db, docsPath, cwd2, config) {
3111
3492
  ok: status.skills.filter((s) => s.status === "ok").map((s) => s.name)
3112
3493
  }
3113
3494
  };
3114
- return ok2(JSON.stringify(result, null, 2));
3495
+ return ok2(JSON.stringify(result));
3115
3496
  }
3116
3497
  default:
3117
3498
  return ok2(`Unknown tool: ${name}`, true);
@@ -3125,7 +3506,7 @@ function searchDocs(docsPath, query, maxResults = 10) {
3125
3506
  for (const file of files) {
3126
3507
  if (results.length >= maxResults) break;
3127
3508
  try {
3128
- const content = readFileSync9(file, "utf8");
3509
+ const content = readFileSync10(file, "utf8");
3129
3510
  const lines = content.split("\n");
3130
3511
  for (let i = 0; i < lines.length; i++) {
3131
3512
  const lower = lines[i].toLowerCase();
@@ -3150,7 +3531,7 @@ function collectMarkdownFiles(dir) {
3150
3531
  const files = [];
3151
3532
  try {
3152
3533
  for (const entry of readdirSync2(dir)) {
3153
- const full = join19(dir, entry);
3534
+ const full = join20(dir, entry);
3154
3535
  const stat = statSync(full);
3155
3536
  if (stat.isDirectory()) {
3156
3537
  files.push(...collectMarkdownFiles(full));
@@ -3175,6 +3556,18 @@ function num(args, key) {
3175
3556
  if (typeof v4 !== "number") throw new Error(`${key} must be a number`);
3176
3557
  return v4;
3177
3558
  }
3559
+ function nonEmptyArray(args, key) {
3560
+ const v4 = args[key];
3561
+ if (!Array.isArray(v4) || v4.length === 0) {
3562
+ throw new Error(`${key} must be a non-empty array`);
3563
+ }
3564
+ for (const item of v4) {
3565
+ if (typeof item !== "object" || item === null) {
3566
+ throw new Error(`${key} entries must be objects`);
3567
+ }
3568
+ }
3569
+ return v4;
3570
+ }
3178
3571
 
3179
3572
  // src/commands/serve.ts
3180
3573
  async function runServe(cwd2, opts) {
@@ -3268,13 +3661,13 @@ async function runStatus(cwd2, opts) {
3268
3661
  }
3269
3662
 
3270
3663
  // src/commands/sync.ts
3271
- import { existsSync as existsSync17, readFileSync as readFileSync10 } from "fs";
3272
- import { join as join20, resolve as resolve10 } from "path";
3664
+ import { existsSync as existsSync19, readFileSync as readFileSync11 } from "fs";
3665
+ import { join as join21, resolve as resolve10 } from "path";
3273
3666
  import pc13 from "picocolors";
3274
3667
  async function runSync(cwd2, opts) {
3275
3668
  const config = await loadConfig(cwd2);
3276
3669
  const direction = opts.direction ?? "both";
3277
- const featureListPath = resolve10(join20(cwd2, config.storage.dir, "feature_list.json"));
3670
+ const featureListPath = resolve10(join21(cwd2, config.storage.dir, "feature_list.json"));
3278
3671
  const db = await openDB(config, cwd2);
3279
3672
  try {
3280
3673
  if (direction === "in" || direction === "both") {
@@ -3288,13 +3681,13 @@ async function runSync(cwd2, opts) {
3288
3681
  }
3289
3682
  }
3290
3683
  async function syncIn(featureListPath, db, dryRun) {
3291
- if (!existsSync17(featureListPath)) {
3684
+ if (!existsSync19(featureListPath)) {
3292
3685
  console.log(pc13.dim(`feature_list.json not found at ${featureListPath} \u2014 skipping in-sync`));
3293
3686
  return;
3294
3687
  }
3295
3688
  let seeds;
3296
3689
  try {
3297
- seeds = JSON.parse(readFileSync10(featureListPath, "utf8"));
3690
+ seeds = JSON.parse(readFileSync11(featureListPath, "utf8"));
3298
3691
  } catch (err) {
3299
3692
  console.error(pc13.red(`Failed to parse feature_list.json: ${err}`));
3300
3693
  process.exit(1);
@@ -3379,14 +3772,14 @@ async function runTaskAdd(cwd2) {
3379
3772
 
3380
3773
  // src/commands/task/done.ts
3381
3774
  import { spawnSync as spawnSync2 } from "child_process";
3382
- import { existsSync as existsSync18 } from "fs";
3775
+ import { existsSync as existsSync20 } from "fs";
3383
3776
  import { resolve as resolve11 } from "path";
3384
3777
  import pc15 from "picocolors";
3385
3778
  async function runTaskDone(cwd2, idOrSlug) {
3386
3779
  const config = await loadConfig(cwd2);
3387
3780
  if (config.health.required) {
3388
3781
  const scriptPath = resolve11(cwd2, config.health.scriptPath);
3389
- if (existsSync18(scriptPath)) {
3782
+ if (existsSync20(scriptPath)) {
3390
3783
  const result = spawnSync2("bash", [scriptPath], { cwd: cwd2, stdio: "pipe", encoding: "utf8" });
3391
3784
  if (result.status !== 0) {
3392
3785
  console.error(pc15.red("\u2717 Health check failed \u2014 cannot mark task as done."));
@@ -3557,8 +3950,74 @@ async function runTaskList(cwd2, opts) {
3557
3950
  }
3558
3951
  }
3559
3952
 
3560
- // src/core/update-check.ts
3953
+ // src/core/path-probe.ts
3954
+ import { accessSync, constants, readdirSync as readdirSync3 } from "fs";
3955
+ import { join as join22 } from "path";
3561
3956
  import pc18 from "picocolors";
3957
+ var DEFAULT_PATHEXT = [".COM", ".EXE", ".BAT", ".CMD"];
3958
+ function defaultIsExecutable(filePath) {
3959
+ try {
3960
+ accessSync(filePath, constants.X_OK);
3961
+ return true;
3962
+ } catch {
3963
+ return false;
3964
+ }
3965
+ }
3966
+ function normalizeExts(pathext) {
3967
+ const raw = pathext && pathext.trim().length > 0 ? pathext : DEFAULT_PATHEXT.join(";");
3968
+ return raw.split(";").map((ext) => ext.trim().toLowerCase()).filter((ext) => ext.length > 0).map((ext) => ext.startsWith(".") ? ext : `.${ext}`);
3969
+ }
3970
+ function resolveOnPath(name, options = {}) {
3971
+ const {
3972
+ pathValue = process.env.PATH,
3973
+ pathext = process.env.PATHEXT,
3974
+ platform = process.platform,
3975
+ isExecutable = defaultIsExecutable
3976
+ } = options;
3977
+ if (!pathValue) return false;
3978
+ const isWindows = platform === "win32";
3979
+ const sep = isWindows ? ";" : ":";
3980
+ const dirs = pathValue.split(sep).filter((dir) => dir.length > 0);
3981
+ if (dirs.length === 0) return false;
3982
+ if (isWindows) {
3983
+ const lowerName = name.toLowerCase();
3984
+ const candidates2 = /* @__PURE__ */ new Set([lowerName, ...normalizeExts(pathext).map((ext) => `${lowerName}${ext}`)]);
3985
+ for (const dir of dirs) {
3986
+ let entries;
3987
+ try {
3988
+ entries = readdirSync3(dir);
3989
+ } catch {
3990
+ continue;
3991
+ }
3992
+ for (const entry of entries) {
3993
+ if (candidates2.has(entry.toLowerCase())) return true;
3994
+ }
3995
+ }
3996
+ return false;
3997
+ }
3998
+ for (const dir of dirs) {
3999
+ try {
4000
+ if (isExecutable(join22(dir, name))) return true;
4001
+ } catch {
4002
+ continue;
4003
+ }
4004
+ }
4005
+ return false;
4006
+ }
4007
+ function isExecutableOnPath(name) {
4008
+ return resolveOnPath(name);
4009
+ }
4010
+ function printMissingGlobalBinaryWarning() {
4011
+ console.error(pc18.yellow("\u26A0 `ahk` was not found on your PATH."));
4012
+ console.error(pc18.dim(" Your project has no local install, so the generated MCP config launches"));
4013
+ console.error(pc18.dim(" `ahk serve` directly. Without `ahk` on your PATH, starting the MCP server"));
4014
+ console.error(pc18.dim(" from that config will fail. This is only a warning \u2014 the command continues."));
4015
+ console.error(pc18.dim(` Run: npm i -g ${pkg.name} (install globally)`));
4016
+ console.error(pc18.dim(` or: npm install --save-dev ${pkg.name} (install locally in this project)`));
4017
+ }
4018
+
4019
+ // src/core/update-check.ts
4020
+ import pc19 from "picocolors";
3562
4021
  var REGISTRY_URL2 = `https://registry.npmjs.org/${pkg.name}/latest`;
3563
4022
  var TIMEOUT_MS2 = 2500;
3564
4023
  function checkForUpdate(currentVersion) {
@@ -3576,8 +4035,8 @@ function checkForUpdate(currentVersion) {
3576
4035
  }
3577
4036
  function printUpdateMessage({ current, latest }) {
3578
4037
  const lines = [
3579
- ` Update available ${pc18.dim(current)} \u2192 ${pc18.green(latest)} `,
3580
- ` Run: ${pc18.cyan(`pnpm i ${pkg.name}@${latest}`)} `
4038
+ ` Update available ${pc19.dim(current)} \u2192 ${pc19.green(latest)} `,
4039
+ ` Run: ${pc19.cyan(`pnpm i ${pkg.name}@${latest}`)} `
3581
4040
  ];
3582
4041
  drawBox(lines);
3583
4042
  }
@@ -3592,10 +4051,22 @@ function isNewer2(latest, current) {
3592
4051
 
3593
4052
  // src/cli.ts
3594
4053
  var cwd = process.cwd();
4054
+ function parsePort(raw) {
4055
+ const trimmed = raw.trim();
4056
+ const rangeHint = "must be an integer between 1 and 65535";
4057
+ if (!/^\d+$/.test(trimmed)) {
4058
+ throw new InvalidArgumentError(`--port ${rangeHint} (received "${raw}").`);
4059
+ }
4060
+ const port = Number(trimmed);
4061
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
4062
+ throw new InvalidArgumentError(`--port ${rangeHint} (received "${raw}").`);
4063
+ }
4064
+ return port;
4065
+ }
3595
4066
  var updateCheck = checkForUpdate(pkg.version);
3596
4067
  var program = new Command();
3597
4068
  program.name("ahk").description("agent-harness-kit \u2014 CLI scaffolding for multi-agent harness systems").version(pkg.version, "-v, --version");
3598
- program.command("init").description("Scaffold a harness interactively in the current directory").option("--name <name>", "Project name (skip prompt)").option("--provider <provider>", "AI provider: claude-code | opencode (skip prompt)").option("--docs <path>", "Docs folder path (skip prompt)").option("--tasks <adapter>", "Task adapter: local | jira | linear (skip prompt)").option("--storage-scope <scope>", "Storage scope: local | global (skip prompt)").action(async (opts) => {
4069
+ 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 | codex-cli | grok-cli (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) => {
3599
4070
  await runInit(cwd, opts);
3600
4071
  });
3601
4072
  program.command("build").description("Regenerate AGENTS.md and provider files from agent-harness-kit.config.ts").option("--watch", "Rebuild on config changes").option("--sync", "Sync tools: frontmatter in existing .claude/agents/*.md to match current permission constants").option(
@@ -3613,7 +4084,7 @@ program.command("status").description("Show task table and active actions").opti
3613
4084
  program.command("sync").description("Sync feature_list.json \u2194 SQLite").option("--dry-run", "Show what would change without applying").option("--direction <direction>", "in | out | both (default: both)").action(async (opts) => {
3614
4085
  await runSync(cwd, { dryRun: opts["dry-run"], direction: opts.direction });
3615
4086
  });
3616
- program.command("serve").description("Start the MCP server (stdio)").option("--port <port>", "Port hint stored in config (default: 3742)", parseInt).action(async (opts) => {
4087
+ program.command("serve").description("Start the MCP server (stdio)").option("--port <port>", "Port hint stored in config (default: 3742)", parsePort).action(async (opts) => {
3617
4088
  await runServe(cwd, { port: opts.port });
3618
4089
  });
3619
4090
  var task = program.command("task").description("Manage tasks");
@@ -3629,11 +4100,11 @@ task.command("done <id|slug>").description("Mark a task as done").action(async (
3629
4100
  task.command("edit").description("Edit a task interactively").action(async () => {
3630
4101
  await runTaskEdit(cwd);
3631
4102
  });
3632
- 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) => {
3633
- await runDashboard(cwd, { port: parseInt(opts.port), open: opts.open });
4103
+ program.command("dashboard").description("Open web dashboard to visualize harness data").option("-p, --port <port>", "Port to listen on", parsePort, 4242).option("--no-open", "Do not open browser automatically").action(async (opts) => {
4104
+ await runDashboard(cwd, { port: opts.port, open: opts.open });
3634
4105
  });
3635
4106
  var migrate = program.command("migrate").description("Migrate provider files to a different provider, or migrate harness storage (see subcommands)");
3636
- 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) => {
4107
+ migrate.command("provider").description("Migrate provider-specific files to a different provider").option("--to <provider>", "Target provider: claude-code | opencode | codex-cli | grok-cli").action(async (opts) => {
3637
4108
  await runMigrate(cwd, opts);
3638
4109
  });
3639
4110
  migrate.command("storage").description(
@@ -3642,14 +4113,14 @@ migrate.command("storage").description(
3642
4113
  try {
3643
4114
  await runMigrateStorage(cwd, { force: opts.force, dryRun: opts["dry-run"] });
3644
4115
  } catch (err) {
3645
- console.error(pc19.red(`\u2717 ${err instanceof Error ? err.message : String(err)}`));
4116
+ console.error(pc20.red(`\u2717 ${err instanceof Error ? err.message : String(err)}`));
3646
4117
  process.exit(1);
3647
4118
  }
3648
4119
  });
3649
4120
  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) => {
3650
4121
  await runExport(cwd, opts);
3651
4122
  });
3652
- program.command("reset").description("Reset/clear harness data (DB, feature list, agent files)").option("--force", "Skip confirmation prompts").option("--provider <claude-code|opencode>", "Reset agent MD files for specified provider").action(async (opts) => {
4123
+ program.command("reset").description("Reset/clear harness data (DB, feature list, agent files)").option("--force", "Skip confirmation prompts").option("--provider <claude-code|opencode|codex-cli|grok-cli>", "Reset agent MD files for specified provider").action(async (opts) => {
3653
4124
  await runReset(cwd, opts);
3654
4125
  });
3655
4126
  program.command("doctor").description("Check lib version, agent files, and harness skills sync status").action(async () => {
@@ -3658,6 +4129,9 @@ program.command("doctor").description("Check lib version, agent files, and harne
3658
4129
  program.hook("preAction", () => {
3659
4130
  if (!isLocalInstallSatisfied(cwd)) {
3660
4131
  printLocalInstallWarning();
4132
+ if (!isExecutableOnPath("ahk")) {
4133
+ printMissingGlobalBinaryWarning();
4134
+ }
3661
4135
  }
3662
4136
  });
3663
4137
  program.hook("postAction", async () => {