@cardor/agent-harness-kit 1.10.5 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  findConfigFile,
3
3
  loadConfig
4
- } from "./chunk-D64KK6UU.js";
4
+ } from "./chunk-U3O77CGE.js";
5
5
  import {
6
6
  DEFAULT_MARKDOWN_PATH,
7
7
  DEFAULT_SQLITE_PATH,
@@ -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, readFileSync as readFileSync5, 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)}`,
@@ -327,18 +382,50 @@ function mergeCodexConfigToml(filePath, port, pm = "npm") {
327
382
  }
328
383
 
329
384
  // 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";
385
+ import { createHash } from "crypto";
386
+ import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
387
+ import { dirname as dirname4, join as join6, resolve as resolve2 } from "path";
388
+ import { fileURLToPath as fileURLToPath3 } from "url";
389
+
390
+ // src/core/materializer/templates.ts
391
+ import { readFileSync as readFileSync4 } from "fs";
392
+ import { dirname as dirname3, join as join5 } from "path";
332
393
  import { fileURLToPath as fileURLToPath2 } from "url";
333
394
 
395
+ // src/core/materializer/agent-restrictions.ts
396
+ var AGENT_RESTRICTIONS = {
397
+ lead: "no-write",
398
+ explorer: "no-write",
399
+ consultant: "no-write",
400
+ builder: "none",
401
+ reviewer: "no-write"
402
+ };
403
+ function restrictionFor(agentName) {
404
+ return AGENT_RESTRICTIONS[agentName] ?? "no-write";
405
+ }
406
+ function claudeDisallowedTools(agentName) {
407
+ return restrictionFor(agentName) === "no-write" ? ["Write", "Edit"] : [];
408
+ }
409
+ function opencodePermissions(agentName) {
410
+ return restrictionFor(agentName) === "no-write" ? { edit: "deny" } : {};
411
+ }
412
+ function codexSandboxMode(agentName) {
413
+ return restrictionFor(agentName) === "no-write" ? "read-only" : "workspace-write";
414
+ }
415
+ var CODEX_READ_ONLY_NOTICE = `## Tool restrictions (enforced by the sandbox)
416
+
417
+ This agent runs with \`sandbox_mode = "read-only"\`. You MUST NOT create, modify, or delete any file: no \`Write\`, no \`Edit\`, no \`apply_patch\`, and no shell command that writes to disk (\`>\`, \`tee\`, \`sed -i\`, \`mv\`, \`rm\`, ...).
418
+
419
+ These tools may still appear available to you. The sandbox will reject the call. Do not retry a rejected write \u2014 report it as a blocker instead.`;
420
+ function codexRestrictionNotice(agentName) {
421
+ return restrictionFor(agentName) === "no-write" ? CODEX_READ_ONLY_NOTICE : "";
422
+ }
423
+
334
424
  // 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";
338
- var __dirname = dirname2(fileURLToPath(import.meta.url));
339
- var TEMPLATES_DIR = join3(__dirname, "agent-templates");
425
+ var __dirname = dirname3(fileURLToPath2(import.meta.url));
426
+ var TEMPLATES_DIR = join5(__dirname, "agent-templates");
340
427
  function loadAgentTemplate(name, vars = {}) {
341
- const raw = readFileSync3(join3(TEMPLATES_DIR, `${name}.md`), "utf8");
428
+ const raw = readFileSync4(join5(TEMPLATES_DIR, `${name}.md`), "utf8");
342
429
  return raw.replace(/\{\{(\w+)\}\}/g, (_, key) => vars[key] ?? `{{${key}}}`);
343
430
  }
344
431
  var HEALTH_SH = `#!/usr/bin/env bash
@@ -393,10 +480,10 @@ If it exits non-zero, stop and report the issue. Do not proceed with codebase ch
393
480
  The harness exposes tools via MCP server on port ${port}. Use these instead of reading files directly.
394
481
 
395
482
  \`\`\`
396
- actions.start taskId agent \u2192 start an action, returns actionId
483
+ actions.start taskId agent \u2192 start an action, returns a numeric actionId
397
484
  actions.write actionId section text \u2192 record a section (result, blockers, ...)
398
- actions.record_tool actionId toolName [argsJson] [summary] \u2192 log a tool call to the Tools dashboard
399
- actions.record_file actionId filePath operation [notes] \u2192 log a file touch to the Files dashboard
485
+ actions.record_tool actionId calls[] \u2192 batch-log tool calls to the Tools dashboard (array, min 1)
486
+ actions.record_file actionId files[] \u2192 batch-log file touches to the Files dashboard (array, min 1)
400
487
  actions.complete actionId summary \u2192 close the action
401
488
  actions.get taskId \u2192 full action history for a task
402
489
  tasks.add title [slug] [description] [acceptance] \u2192 create a new task from natural language
@@ -416,9 +503,8 @@ docs.search query \u2192 search ${docs
416
503
  - tasks.get('pending') \u2192 pick lowest id
417
504
 
418
505
  2. WORK (lead \u2192 explorer \u2192 consultant \u2192 builder \u2192 reviewer)
419
- - Each agent calls actions.start(taskId, agentName) \u2192 actionId
420
- - After EVERY tool call: actions.record_tool(actionId, toolName, args, summary)
421
- - After EVERY file change: actions.record_file(actionId, filePath, operation, notes)
506
+ - Each agent calls actions.start(taskId, agentName) \u2192 numeric actionId
507
+ - 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
422
508
  - Closes with actions.complete(actionId, summary)
423
509
 
424
510
  3. CLOSE
@@ -477,10 +563,10 @@ If it exits non-zero, stop and report the issue. Do not proceed with codebase ch
477
563
  The harness exposes tools via MCP server on port ${port}. Use these instead of reading files directly.
478
564
 
479
565
  \`\`\`
480
- actions.start taskId agent \u2192 start an action, returns actionId
566
+ actions.start taskId agent \u2192 start an action, returns a numeric actionId
481
567
  actions.write actionId section text \u2192 record a section (result, blockers, ...)
482
- actions.record_tool actionId toolName [argsJson] [summary] \u2192 log a tool call to the Tools dashboard
483
- actions.record_file actionId filePath operation [notes] \u2192 log a file touch to the Files dashboard
568
+ actions.record_tool actionId calls[] \u2192 batch-log tool calls to the Tools dashboard (array, min 1)
569
+ actions.record_file actionId files[] \u2192 batch-log file touches to the Files dashboard (array, min 1)
484
570
  actions.complete actionId summary \u2192 close the action
485
571
  actions.get taskId \u2192 full action history for a task
486
572
  tasks.add title [slug] [description] [acceptance] \u2192 create a new task from natural language
@@ -501,9 +587,8 @@ docs.search query \u2192 search ${docs
501
587
  - No pending tasks? \u2192 ask user, infer fields, call tasks.add, then tasks.claim
502
588
 
503
589
  2. WORK (lead \u2192 explorer \u2192 consultant \u2192 builder \u2192 reviewer)
504
- - Each agent calls actions.start(taskId, agentName) \u2192 actionId
505
- - After EVERY tool call: actions.record_tool(actionId, toolName, args, summary)
506
- - After EVERY file change: actions.record_file(actionId, filePath, operation, notes)
590
+ - Each agent calls actions.start(taskId, agentName) \u2192 numeric actionId
591
+ - 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
507
592
  - Closes with actions.complete(actionId, summary)
508
593
 
509
594
  3. CLOSE
@@ -530,11 +615,7 @@ If orchestrating: Agent definition files in .claude/agents/
530
615
  \`\`\`
531
616
  `;
532
617
  }
533
- function modelField(model) {
534
- return model ? `, model: ${JSON.stringify(model)}` : "";
535
- }
536
618
  function configObjectBody(params) {
537
- const models = params.models ?? {};
538
619
  const isGlobal = params.scope === "global";
539
620
  const markdownFallbackLine = isGlobal ? `markdownFallback: { enabled: true },` : `markdownFallback: { enabled: true, path: '.harness/current.md' },`;
540
621
  return ` project: {
@@ -545,14 +626,13 @@ function configObjectBody(params) {
545
626
 
546
627
  provider: '${params.provider}',
547
628
 
548
- agents: {
549
- lead: { instructionsPath: null${modelField(models.lead)} },
550
- explorer: { instructionsPath: null, allowedPaths: ['${params.docsPath}', './src']${modelField(models.explorer)} },
551
- builder: { instructionsPath: null, writablePaths: ['./src', './tests']${modelField(models.builder)} },
552
- reviewer: { instructionsPath: null${modelField(models.reviewer)} },
553
- ${models.consultant ? `consultant: { instructionsPath: null${modelField(models.consultant)} },
554
- ` : ""}custom: [],
555
- },
629
+ // There is no 'agents' key. Agent files are yours: edit the role prompt and
630
+ // the 'model:' frontmatter line directly in the generated file. 'ahk build'
631
+ // creates them when missing and never overwrites them \u2014 use
632
+ // 'ahk build --force' to regenerate them from the packaged templates.
633
+ // What each role may NOT do is enforced per-tool inside those files
634
+ // (disallowedTools / permission.edit / sandbox_mode); see
635
+ // src/core/materializer/agent-restrictions.ts.
556
636
 
557
637
  // SQLite (default). Switch to postgres/mysql by changing database.type.
558
638
  // database: { type: 'postgres', connectionString: process.env.DATABASE_URL },
@@ -587,6 +667,47 @@ function configObjectBody(params) {
587
667
  },
588
668
  `;
589
669
  }
670
+ function configObject(params) {
671
+ const isGlobal = params.scope === "global";
672
+ return {
673
+ project: {
674
+ name: params.name,
675
+ description: params.description,
676
+ docsPath: params.docsPath
677
+ },
678
+ provider: params.provider,
679
+ // There is no 'agents' key here either — it was removed from the config
680
+ // entirely, so the JSON variant must not reintroduce it.
681
+ database: { type: "sqlite" },
682
+ storage: {
683
+ dir: ".harness",
684
+ tasks: { adapter: params.tasksAdapter },
685
+ sections: {
686
+ toolsUsed: true,
687
+ filesModified: true,
688
+ result: true,
689
+ blockers: true,
690
+ nextSteps: false
691
+ },
692
+ // Same scope rule as configObjectBody(): 'global' has no local path to
693
+ // declare, so markdownFallback.path is omitted for it.
694
+ markdownFallback: isGlobal ? { enabled: true } : { enabled: true, path: ".harness/current.md" },
695
+ scope: params.scope,
696
+ projectId: params.projectId
697
+ },
698
+ health: {
699
+ scriptPath: "./health.sh",
700
+ required: true
701
+ },
702
+ tools: {
703
+ mcp: { enabled: true, port: params.port },
704
+ scripts: { enabled: true, outputDir: "./.harness/scripts" }
705
+ }
706
+ };
707
+ }
708
+ function configJson(params) {
709
+ return JSON.stringify(configObject(params), null, 2) + "\n";
710
+ }
590
711
  function configTs(params) {
591
712
  return `import type { HarnessConfig } from '@cardor/agent-harness-kit'
592
713
 
@@ -625,9 +746,6 @@ function agentConsultant(vars) {
625
746
  function agentReviewer(vars) {
626
747
  return loadAgentTemplate("reviewer", vars);
627
748
  }
628
- function featureListJson(tasks) {
629
- return JSON.stringify(tasks, null, 2) + "\n";
630
- }
631
749
  function stripFrontmatter(md) {
632
750
  const parts = md.split(/^---\s*$/m);
633
751
  if (parts.length < 3) return { description: "", body: md };
@@ -643,81 +761,93 @@ function stripFrontmatter(md) {
643
761
  }
644
762
  return { description, body };
645
763
  }
646
- function toCodexToml(name, description, body, sandboxMode, model) {
764
+ function toCodexToml(tomlName, agentName, description, body) {
647
765
  const safe = (s) => s.replace(/"""/g, '""\\u0022');
648
- const trimmedModel = model?.trim() ?? "";
649
- const modelLine = trimmedModel.length >= 3 ? `model = "${trimmedModel}"
650
- ` : "";
651
- return `name = "${name}"
766
+ const sandboxMode = codexSandboxMode(agentName);
767
+ const notice = codexRestrictionNotice(agentName);
768
+ const instructions = notice ? `${body.trimEnd()}
769
+
770
+ ---
771
+
772
+ ${notice}` : body.trimEnd();
773
+ return `name = "${tomlName}"
652
774
  sandbox_mode = "${sandboxMode}"
653
- ${modelLine}
775
+
654
776
  description = """
655
777
  ${safe(description)}
656
778
  """
657
779
 
658
780
  developer_instructions = """
659
- ${safe(body.trimEnd())}
781
+ ${safe(instructions)}
660
782
  """
661
783
  `;
662
784
  }
663
785
  function agentLeadToml(vars) {
664
786
  const { description, body } = stripFrontmatter(loadAgentTemplate("lead", vars));
665
- return toCodexToml("lead", description, body, "read-only", vars.model);
787
+ return toCodexToml("lead", "lead", description, body);
666
788
  }
667
789
  function agentLeadAsDefaultToml(vars) {
668
790
  const { description, body } = stripFrontmatter(loadAgentTemplate("lead", vars));
669
- return toCodexToml("default", description, body, "read-only", vars.model);
791
+ return toCodexToml("default", "lead", description, body);
670
792
  }
671
793
  function agentExplorerToml(vars) {
672
794
  const { description, body } = stripFrontmatter(loadAgentTemplate("explorer", vars));
673
- return toCodexToml("explorer", description, body, "read-only", vars.model);
795
+ return toCodexToml("explorer", "explorer", description, body);
674
796
  }
675
797
  function agentBuilderToml(vars) {
676
798
  const { description, body } = stripFrontmatter(loadAgentTemplate("builder", vars));
677
- return toCodexToml("builder", description, body, "workspace-write", vars.model);
799
+ return toCodexToml("builder", "builder", description, body);
678
800
  }
679
801
  function agentReviewerToml(vars) {
680
802
  const { description, body } = stripFrontmatter(loadAgentTemplate("reviewer", vars));
681
- return toCodexToml("reviewer", description, body, "read-only", vars.model);
803
+ return toCodexToml("reviewer", "reviewer", description, body);
682
804
  }
683
805
  function agentConsultantToml(vars) {
684
806
  const { description, body } = stripFrontmatter(loadAgentTemplate("consultant", vars));
685
- return toCodexToml("consultant", description, body, "read-only", vars.model);
686
- }
687
- function translateFrontmatterForClaudeCode(md, agentName, model) {
688
- const permissionsMap = {
689
- lead: [...MCP_CLAUDE_PERMISSIONS_LEAD],
690
- explorer: [...MCP_CLAUDE_PERMISSIONS_EXPLORER],
691
- consultant: [...MCP_CLAUDE_PERMISSIONS_CONSULTANT],
692
- builder: [...MCP_CLAUDE_PERMISSIONS_BUILDER],
693
- reviewer: [...MCP_CLAUDE_PERMISSIONS_REVIEWER]
694
- };
695
- const permissions = permissionsMap[agentName] ?? MCP_CLAUDE_PERMISSIONS;
696
- const mcpLines = permissions.map((t) => ` - ${t}`).join("\n");
697
- let result = md.replace(/(tools:\n(?: - (?!mcp__)[^\n]+\n)+)/, (match) => {
698
- const trimmed = match.trimEnd();
699
- return `${trimmed}
700
- - Task
701
- ${mcpLines}
807
+ return toCodexToml("consultant", "consultant", description, body);
808
+ }
809
+ function stripFrontmatterBlockSequence(md, key) {
810
+ const re = new RegExp(`^${key}:\\n(?: - [^\\n]+\\n)+`, "m");
811
+ return md.replace(re, "");
812
+ }
813
+ function appendFrontmatterBlockSequence(md, key, values) {
814
+ if (values.length === 0) return md;
815
+ const block = `${key}:
816
+ ${values.map((v4) => ` - ${v4}`).join("\n")}
702
817
  `;
703
- });
704
- if (model) {
705
- result = injectModelFrontmatterLine(result, model);
706
- }
707
- return result;
818
+ return md.replace(/^---\n([\s\S]*?)^---\n/m, (_m, body) => `---
819
+ ${body}${block}---
820
+ `);
708
821
  }
709
- function injectModelFrontmatterLine(md, model) {
710
- if (/^model:\s*.*$/m.test(md)) {
711
- return md.replace(/^model:\s*.*$/m, `model: ${model}`);
822
+ function appendFrontmatterScalar(md, key, value) {
823
+ const block = `${key}: ${value}
824
+ `;
825
+ return md.replace(/^---\n([\s\S]*?)^---\n/m, (_m, body) => `---
826
+ ${body}${block}---
827
+ `);
828
+ }
829
+ function appendFrontmatterMapping(md, key, entries) {
830
+ const keys = Object.keys(entries);
831
+ if (keys.length === 0) return md;
832
+ const block = `${key}:
833
+ ${keys.map((k) => ` ${k}: ${entries[k]}`).join("\n")}
834
+ `;
835
+ return md.replace(/^---\n([\s\S]*?)^---\n/m, (_m, body) => `---
836
+ ${body}${block}---
837
+ `);
838
+ }
839
+ function translateFrontmatterForClaudeCode(md, agentName, opts) {
840
+ let result = stripFrontmatterBlockSequence(md, "tools");
841
+ result = stripFrontmatterBlockSequence(result, "disallowedTools");
842
+ if (opts?.model && opts.model !== "inherit") {
843
+ result = appendFrontmatterScalar(result, "model", opts.model);
712
844
  }
713
- return md.replace(/^(name:.*)$/m, `$1
714
- model: ${model}`);
845
+ return appendFrontmatterBlockSequence(result, "disallowedTools", claudeDisallowedTools(agentName));
715
846
  }
716
- function translateFrontmatterForOpenCode(md) {
717
- return md.replace(/(tools:\n(?: - [^\n]+\n)+)/, (match) => {
718
- const tools = [...match.matchAll(/ - ([^\n]+)/g)].map((m) => m[1].trim());
719
- return "tools:\n" + tools.map((t) => ` ${t.toLocaleLowerCase()}: true`).join("\n") + "\n";
720
- });
847
+ function translateFrontmatterForOpenCode(md, agentName) {
848
+ let result = stripFrontmatterBlockSequence(md, "tools");
849
+ result = stripFrontmatterBlockSequence(result, "disallowedTools");
850
+ return appendFrontmatterMapping(result, "permission", opencodePermissions(agentName));
721
851
  }
722
852
  var GITIGNORE_ENTRIES = `
723
853
  # agent-harness-kit
@@ -728,16 +858,145 @@ var GITIGNORE_ENTRIES = `
728
858
  `;
729
859
 
730
860
  // src/core/materializer/scaffold-utils.ts
731
- var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
732
- function writeAgentFile(cwd2, relPath, content) {
733
- const abs = join4(cwd2, relPath);
734
- if (existsSync3(abs)) return;
735
- mkdirSync3(resolve2(abs, ".."), { recursive: true });
736
- writeFileSync3(abs, content, "utf8");
861
+ var __dirname2 = dirname4(fileURLToPath3(import.meta.url));
862
+ function writeAgentFiles(cwd2, entries, opts = {}) {
863
+ const result = { created: [], overwritten: [], preserved: [] };
864
+ const existing = entries.filter((e) => existsSync5(join6(cwd2, e.relPath)));
865
+ if (opts.force && existing.length > 0) {
866
+ if (!opts.backupRoot) {
867
+ throw new Error(
868
+ "writeAgentFiles: force is set and existing agent files would be overwritten, but no backupRoot was provided. Refusing to overwrite without a backup."
869
+ );
870
+ }
871
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
872
+ const backupDir = join6(opts.backupRoot, `agents-${stamp}`);
873
+ try {
874
+ for (const entry of existing) {
875
+ const dest = join6(backupDir, entry.relPath);
876
+ mkdirSync3(resolve2(dest, ".."), { recursive: true });
877
+ writeFileSync3(dest, readFileSync5(join6(cwd2, entry.relPath), "utf8"), "utf8");
878
+ }
879
+ } catch (err) {
880
+ throw new Error(
881
+ `Could not back up existing agent files to ${backupDir} (${err instanceof Error ? err.message : String(err)}). Aborting WITHOUT overwriting anything \u2014 no agent file was modified.`
882
+ );
883
+ }
884
+ result.backupDir = backupDir;
885
+ }
886
+ for (const entry of entries) {
887
+ const abs = join6(cwd2, entry.relPath);
888
+ const exists = existsSync5(abs);
889
+ if (exists && !opts.force) {
890
+ result.preserved.push(entry.relPath);
891
+ continue;
892
+ }
893
+ mkdirSync3(resolve2(abs, ".."), { recursive: true });
894
+ writeFileSync3(abs, entry.content, "utf8");
895
+ if (exists) result.overwritten.push(entry.relPath);
896
+ else result.created.push(entry.relPath);
897
+ }
898
+ return result;
899
+ }
900
+ var GENERATED_MARKER_RE = /^([\s\S]*)\n<!-- ahk:generated ([0-9a-f]{64}) -->\n?$/;
901
+ function bodyFingerprint(body) {
902
+ return createHash("sha256").update(body, "utf8").digest("hex");
903
+ }
904
+ function stampGenerated(body) {
905
+ return `${body}
906
+ <!-- ahk:generated ${bodyFingerprint(body)} -->
907
+ `;
908
+ }
909
+ function readStamp(fileContent) {
910
+ const m = GENERATED_MARKER_RE.exec(fileContent);
911
+ if (!m) return null;
912
+ return { body: m[1], hash: m[2] };
913
+ }
914
+ function reconcileGeneratedFiles(cwd2, entries, opts = {}) {
915
+ const result = {
916
+ created: [],
917
+ current: [],
918
+ propagated: [],
919
+ preserved: [],
920
+ overwritten: []
921
+ };
922
+ const plans = [];
923
+ const toBackup = [];
924
+ for (const entry of entries) {
925
+ const abs = join6(cwd2, entry.relPath);
926
+ if (!existsSync5(abs)) {
927
+ plans.push({ entry, action: "create" });
928
+ continue;
929
+ }
930
+ const onDisk = readFileSync5(abs, "utf8");
931
+ const stamp = readStamp(onDisk);
932
+ const onDiskBody = stamp ? stamp.body : onDisk;
933
+ if (onDiskBody === entry.content) {
934
+ plans.push({ entry, action: "current" });
935
+ continue;
936
+ }
937
+ if (stamp && stamp.hash === bodyFingerprint(stamp.body)) {
938
+ plans.push({ entry, action: "propagate" });
939
+ continue;
940
+ }
941
+ if (opts.force) {
942
+ plans.push({ entry, action: "overwrite" });
943
+ toBackup.push(entry);
944
+ } else {
945
+ plans.push({ entry, action: "preserve" });
946
+ }
947
+ }
948
+ if (toBackup.length > 0) {
949
+ if (!opts.backupRoot) {
950
+ throw new Error(
951
+ "reconcileGeneratedFiles: force is set and hand-edited generated files would be overwritten, but no backupRoot was provided. Refusing to overwrite without a backup."
952
+ );
953
+ }
954
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
955
+ const backupDir = join6(opts.backupRoot, `derived-${stamp}`);
956
+ try {
957
+ for (const entry of toBackup) {
958
+ const dest = join6(backupDir, entry.relPath);
959
+ mkdirSync3(resolve2(dest, ".."), { recursive: true });
960
+ writeFileSync3(dest, readFileSync5(join6(cwd2, entry.relPath), "utf8"), "utf8");
961
+ }
962
+ } catch (err) {
963
+ throw new Error(
964
+ `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.`
965
+ );
966
+ }
967
+ result.backupDir = backupDir;
968
+ }
969
+ for (const { entry, action } of plans) {
970
+ const abs = join6(cwd2, entry.relPath);
971
+ switch (action) {
972
+ case "current":
973
+ result.current.push(entry.relPath);
974
+ break;
975
+ case "preserve":
976
+ result.preserved.push(entry.relPath);
977
+ break;
978
+ case "create":
979
+ mkdirSync3(resolve2(abs, ".."), { recursive: true });
980
+ writeFileSync3(abs, stampGenerated(entry.content), "utf8");
981
+ result.created.push(entry.relPath);
982
+ break;
983
+ case "propagate":
984
+ mkdirSync3(resolve2(abs, ".."), { recursive: true });
985
+ writeFileSync3(abs, stampGenerated(entry.content), "utf8");
986
+ result.propagated.push(entry.relPath);
987
+ break;
988
+ case "overwrite":
989
+ mkdirSync3(resolve2(abs, ".."), { recursive: true });
990
+ writeFileSync3(abs, stampGenerated(entry.content), "utf8");
991
+ result.overwritten.push(entry.relPath);
992
+ break;
993
+ }
994
+ }
995
+ return result;
737
996
  }
738
997
  function appendGitignore(cwd2) {
739
- const giPath = join4(cwd2, ".gitignore");
740
- const existing = existsSync3(giPath) ? readFileSync4(giPath, "utf8") : "";
998
+ const giPath = join6(cwd2, ".gitignore");
999
+ const existing = existsSync5(giPath) ? readFileSync5(giPath, "utf8") : "";
741
1000
  const toAdd = GITIGNORE_ENTRIES.split("\n").filter((line) => line && !existing.includes(line)).join("\n");
742
1001
  if (toAdd.trim()) {
743
1002
  writeFileSync3(giPath, existing + (existing.endsWith("\n") ? "" : "\n") + toAdd + "\n", "utf8");
@@ -749,26 +1008,34 @@ function slugify(title) {
749
1008
  function writeSkills(cwd2, skillsDir) {
750
1009
  const skillNames = ["ahk-ask", "ahk-consultant", "ahk-triage", "ahk-review"];
751
1010
  for (const skillName of skillNames) {
752
- const src = join4(__dirname2, "skills", skillName, "SKILL.md");
753
- const destDir = join4(cwd2, skillsDir, skillName);
754
- const dest = join4(destDir, "SKILL.md");
1011
+ const src = join6(__dirname2, "skills", skillName, "SKILL.md");
1012
+ const destDir = join6(cwd2, skillsDir, skillName);
1013
+ const dest = join6(destDir, "SKILL.md");
755
1014
  mkdirSync3(destDir, { recursive: true });
756
- writeFileSync3(dest, readFileSync4(src, "utf8"), "utf8");
1015
+ writeFileSync3(dest, readFileSync5(src, "utf8"), "utf8");
757
1016
  }
758
1017
  }
759
1018
 
760
1019
  // src/core/materializer/claude-code.ts
1020
+ function claudeAgentFiles(config, modelsByRole) {
1021
+ const projectName = config.project.name;
1022
+ return [
1023
+ { relPath: ".claude/agents/lead.md", content: translateFrontmatterForClaudeCode(agentLead({ projectName }), "lead", { model: modelsByRole?.lead }) },
1024
+ { relPath: ".claude/agents/explorer.md", content: translateFrontmatterForClaudeCode(agentExplorer({ projectName }), "explorer", { model: modelsByRole?.explorer }) },
1025
+ { relPath: ".claude/agents/consultant.md", content: translateFrontmatterForClaudeCode(agentConsultant({ projectName }), "consultant", { model: modelsByRole?.consultant }) },
1026
+ { relPath: ".claude/agents/builder.md", content: translateFrontmatterForClaudeCode(agentBuilder({ projectName }), "builder", { model: modelsByRole?.builder }) },
1027
+ { relPath: ".claude/agents/reviewer.md", content: translateFrontmatterForClaudeCode(agentReviewer({ projectName }), "reviewer", { model: modelsByRole?.reviewer }) }
1028
+ ];
1029
+ }
761
1030
  var ClaudeCodeMaterializer = class {
762
1031
  async scaffold(config, opts) {
763
- const { cwd: cwd2 } = opts;
764
- write(cwd2, "AGENTS.md", agentsMd(config));
765
- write(cwd2, "CLAUDE.md", claudeMd(config));
766
- if (!existsSync4(join5(cwd2, "health.sh"))) {
1032
+ const { cwd: cwd2, claudeAgentModels } = opts;
1033
+ write(cwd2, "AGENTS.md", stampGenerated(agentsMd(config)));
1034
+ write(cwd2, "CLAUDE.md", stampGenerated(claudeMd(config)));
1035
+ if (!existsSync6(join7(cwd2, "health.sh"))) {
767
1036
  write(cwd2, "health.sh", HEALTH_SH, 493);
768
1037
  }
769
- const tasks = opts.firstTask ? [{ slug: slugify(opts.firstTask.title), ...opts.firstTask }] : [];
770
- write(cwd2, "feature_list.json", featureListJson(tasks));
771
- if (config.storage.scope === "local" && !existsSync4(join5(cwd2, config.storage.markdownFallback.path))) {
1038
+ if (config.storage.scope === "local" && !existsSync6(join7(cwd2, config.storage.markdownFallback.path))) {
772
1039
  write(
773
1040
  cwd2,
774
1041
  config.storage.markdownFallback.path,
@@ -781,106 +1048,83 @@ No tasks in progress.
781
1048
  `
782
1049
  );
783
1050
  }
784
- const projectName = config.project.name;
785
- const allowedPaths = (config.agents.explorer.allowedPaths ?? []).join(", ");
786
- const writablePaths = (config.agents.builder.writablePaths ?? []).join(", ");
787
- const leadModel = config.agents.lead.model;
788
- const explorerModel = config.agents.explorer.model;
789
- const consultantModel = config.agents.consultant?.model;
790
- const builderModel = config.agents.builder.model;
791
- const reviewerModel = config.agents.reviewer.model;
792
- writeAgentFile(cwd2, ".claude/agents/lead.md", translateFrontmatterForClaudeCode(agentLead({ projectName }), "lead", leadModel));
793
- writeAgentFile(cwd2, ".claude/agents/explorer.md", translateFrontmatterForClaudeCode(agentExplorer({ projectName, allowedPaths }), "explorer", explorerModel));
794
- writeAgentFile(cwd2, ".claude/agents/consultant.md", translateFrontmatterForClaudeCode(agentConsultant({ projectName }), "consultant", consultantModel));
795
- writeAgentFile(cwd2, ".claude/agents/builder.md", translateFrontmatterForClaudeCode(agentBuilder({ projectName, writablePaths }), "builder", builderModel));
796
- writeAgentFile(cwd2, ".claude/agents/reviewer.md", translateFrontmatterForClaudeCode(agentReviewer({ projectName }), "reviewer", reviewerModel));
797
- mergeClaudeMcpJson(join5(cwd2, ".mcp.json"), config.tools.mcp.port, detectPackageManager(cwd2));
798
- mergeClaudeSettingsJson(join5(cwd2, ".claude/settings.json"));
799
- mergeClaudeSettingsLocalJson(join5(cwd2, ".claude/settings.local.json"));
1051
+ writeAgentFiles(cwd2, claudeAgentFiles(config, claudeAgentModels));
1052
+ mergeClaudeMcpJson(join7(cwd2, ".mcp.json"), config.tools.mcp.port, cwd2, detectPackageManager(cwd2));
1053
+ mergeClaudeSettingsJson(join7(cwd2, ".claude/settings.json"));
1054
+ mergeClaudeSettingsLocalJson(join7(cwd2, ".claude/settings.local.json"));
800
1055
  appendGitignore(cwd2);
801
1056
  writeSkills(cwd2, ".claude/skills");
802
1057
  }
803
- async build(config, cwd2) {
804
- const write2 = (relPath, content) => {
805
- const abs = join5(cwd2, relPath);
806
- mkdirSync4(resolve3(abs, ".."), { recursive: true });
807
- writeFileSync4(abs, content, "utf8");
808
- };
809
- write2("AGENTS.md", agentsMd(config));
810
- write2("CLAUDE.md", claudeMd(config));
811
- const projectName = config.project.name;
812
- const allowedPaths = (config.agents.explorer.allowedPaths ?? []).join(", ");
813
- const writablePaths = (config.agents.builder.writablePaths ?? []).join(", ");
814
- const leadModel = config.agents.lead.model;
815
- const explorerModel = config.agents.explorer.model;
816
- const consultantModel = config.agents.consultant?.model;
817
- const builderModel = config.agents.builder.model;
818
- const reviewerModel = config.agents.reviewer.model;
819
- write2(".claude/agents/lead.md", translateFrontmatterForClaudeCode(agentLead({ projectName }), "lead", leadModel));
820
- write2(".claude/agents/explorer.md", translateFrontmatterForClaudeCode(agentExplorer({ projectName, allowedPaths }), "explorer", explorerModel));
821
- write2(".claude/agents/consultant.md", translateFrontmatterForClaudeCode(agentConsultant({ projectName }), "consultant", consultantModel));
822
- write2(".claude/agents/builder.md", translateFrontmatterForClaudeCode(agentBuilder({ projectName, writablePaths }), "builder", builderModel));
823
- write2(".claude/agents/reviewer.md", translateFrontmatterForClaudeCode(agentReviewer({ projectName }), "reviewer", reviewerModel));
824
- mergeClaudeMcpJson(join5(cwd2, ".mcp.json"), config.tools.mcp.port, detectPackageManager(cwd2));
825
- mergeClaudeSettingsJson(join5(cwd2, ".claude/settings.json"));
826
- mergeClaudeSettingsLocalJson(join5(cwd2, ".claude/settings.local.json"));
1058
+ async build(config, cwd2, opts = {}) {
1059
+ const derived = reconcileGeneratedFiles(
1060
+ cwd2,
1061
+ [
1062
+ { relPath: "AGENTS.md", content: agentsMd(config) },
1063
+ { relPath: "CLAUDE.md", content: claudeMd(config) }
1064
+ ],
1065
+ { force: opts.force, backupRoot: join7(cwd2, config.storage.dir, "backups") }
1066
+ );
1067
+ const agents = writeAgentFiles(cwd2, claudeAgentFiles(config), {
1068
+ force: opts.force,
1069
+ backupRoot: join7(cwd2, config.storage.dir, "backups")
1070
+ });
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"));
827
1074
  writeSkills(cwd2, ".claude/skills");
1075
+ return { agents, derived };
828
1076
  }
829
1077
  async migrate(config, _to, _cwd) {
830
1078
  void config;
831
1079
  }
832
- async syncPermissions(cwd2) {
833
- const AGENT_TOOLS = {
834
- lead: [...MCP_CLAUDE_PERMISSIONS_LEAD],
835
- explorer: [...MCP_CLAUDE_PERMISSIONS_EXPLORER],
836
- consultant: [...MCP_CLAUDE_PERMISSIONS_CONSULTANT],
837
- builder: [...MCP_CLAUDE_PERMISSIONS_BUILDER],
838
- reviewer: [...MCP_CLAUDE_PERMISSIONS_REVIEWER]
839
- };
840
- for (const [agent, tools] of Object.entries(AGENT_TOOLS)) {
841
- const filePath = join5(cwd2, ".claude", "agents", `${agent}.md`);
842
- if (!existsSync4(filePath)) {
843
- console.log(` ${agent}.md not found \u2014 skipping`);
844
- continue;
845
- }
846
- const content = readFileSync5(filePath, "utf-8");
847
- const updated = content.replace(
848
- /(tools:\n)((?: - [^\n]+\n)*)/m,
849
- (_match, header, toolsSection) => {
850
- const nativeLines = toolsSection.split("\n").filter((line) => line.trim() && !line.includes("mcp__"));
851
- const nativeSection = nativeLines.length ? nativeLines.join("\n") + "\n" : "";
852
- const mcpSection = tools.map((t) => ` - ${t}`).join("\n") + "\n";
853
- return header + nativeSection + mcpSection;
854
- }
855
- );
856
- if (updated === content) {
857
- console.log(` ${agent}.md already in sync`);
858
- } else {
859
- writeFileSync4(filePath, updated, "utf-8");
860
- console.log(` ${agent}.md updated`);
861
- }
862
- }
1080
+ /**
1081
+ * No-op by design.
1082
+ *
1083
+ * This used to rewrite the `tools:` frontmatter block of every agent file,
1084
+ * re-injecting the canonical `mcp__agent-harness-kit__*` allowlist. Agent
1085
+ * files no longer declare `tools` at all: they inherit the full tool set
1086
+ * (Task and every MCP tool included) and express restrictions as a
1087
+ * `disallowedTools` denylist instead.
1088
+ *
1089
+ * Keeping the old rewrite alive would be actively harmful — on a project
1090
+ * upgraded from an older version, whose agent files still carry a legacy
1091
+ * `tools:` block, it would re-inject the allowlist and silently undo the
1092
+ * migration on exactly the files that most need it. Run `ahk build` to
1093
+ * regenerate agent files in the current shape.
1094
+ */
1095
+ async syncPermissions(_cwd) {
1096
+ console.log(" Agent files inherit tools and declare a disallowedTools denylist \u2014");
1097
+ console.log(" there is no permission allowlist to sync. Run `ahk build` to regenerate them.");
863
1098
  }
864
1099
  };
865
1100
 
866
1101
  // src/core/materializer/codex-cli.ts
867
- import { existsSync as existsSync5, mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "fs";
868
- import { join as join6, resolve as resolve4 } from "path";
1102
+ import { existsSync as existsSync7, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
1103
+ import { join as join8, resolve as resolve3 } from "path";
1104
+ function codexAgentFiles(config) {
1105
+ const projectName = config.project.name;
1106
+ return [
1107
+ { relPath: ".codex/agents/lead.toml", content: agentLeadToml({ projectName }) },
1108
+ { relPath: ".codex/agents/explorer.toml", content: agentExplorerToml({ projectName }) },
1109
+ { relPath: ".codex/agents/consultant.toml", content: agentConsultantToml({ projectName }) },
1110
+ { relPath: ".codex/agents/builder.toml", content: agentBuilderToml({ projectName }) },
1111
+ { relPath: ".codex/agents/reviewer.toml", content: agentReviewerToml({ projectName }) },
1112
+ { relPath: ".codex/agents/default.toml", content: agentLeadAsDefaultToml({ projectName }) }
1113
+ ];
1114
+ }
869
1115
  var CodexCliMaterializer = class {
870
1116
  async scaffold(config, opts) {
871
1117
  const { cwd: cwd2 } = opts;
872
1118
  const write2 = (relPath, content, mode) => {
873
- const abs = join6(cwd2, relPath);
874
- mkdirSync5(resolve4(abs, ".."), { recursive: true });
875
- writeFileSync5(abs, content, { encoding: "utf8", mode });
1119
+ const abs = join8(cwd2, relPath);
1120
+ mkdirSync4(resolve3(abs, ".."), { recursive: true });
1121
+ writeFileSync4(abs, content, { encoding: "utf8", mode });
876
1122
  };
877
- write2("AGENTS.md", agentsMd(config));
878
- if (!existsSync5(join6(cwd2, "health.sh"))) {
1123
+ write2("AGENTS.md", stampGenerated(agentsMd(config)));
1124
+ if (!existsSync7(join8(cwd2, "health.sh"))) {
879
1125
  write2("health.sh", HEALTH_SH, 493);
880
1126
  }
881
- const tasks = opts.firstTask ? [{ slug: slugify(opts.firstTask.title), ...opts.firstTask }] : [];
882
- write2(join6(config.storage.dir, "feature_list.json"), featureListJson(tasks));
883
- if (config.storage.scope === "local" && !existsSync5(join6(cwd2, config.storage.markdownFallback.path))) {
1127
+ if (config.storage.scope === "local" && !existsSync7(join8(cwd2, config.storage.markdownFallback.path))) {
884
1128
  write2(
885
1129
  config.storage.markdownFallback.path,
886
1130
  `<!-- AUTO-GENERATED by agent-harness-kit \u2014 DO NOT EDIT MANUALLY -->
@@ -892,47 +1136,24 @@ No tasks in progress.
892
1136
  `
893
1137
  );
894
1138
  }
895
- const projectName = config.project.name;
896
- const allowedPaths = (config.agents.explorer.allowedPaths ?? []).join(", ");
897
- const writablePaths = (config.agents.builder.writablePaths ?? []).join(", ");
898
- const leadModel = config.agents.lead.model;
899
- const explorerModel = config.agents.explorer.model;
900
- const consultantModel = config.agents.consultant?.model;
901
- const builderModel = config.agents.builder.model;
902
- const reviewerModel = config.agents.reviewer.model;
903
- writeAgentFile(cwd2, ".codex/agents/lead.toml", agentLeadToml({ projectName, model: leadModel }));
904
- writeAgentFile(cwd2, ".codex/agents/explorer.toml", agentExplorerToml({ projectName, allowedPaths, model: explorerModel }));
905
- writeAgentFile(cwd2, ".codex/agents/consultant.toml", agentConsultantToml({ projectName, model: consultantModel }));
906
- writeAgentFile(cwd2, ".codex/agents/builder.toml", agentBuilderToml({ projectName, writablePaths, model: builderModel }));
907
- writeAgentFile(cwd2, ".codex/agents/reviewer.toml", agentReviewerToml({ projectName, model: reviewerModel }));
908
- writeAgentFile(cwd2, ".codex/agents/default.toml", agentLeadAsDefaultToml({ projectName, model: leadModel }));
909
- mergeCodexConfigToml(join6(cwd2, ".codex/config.toml"), config.tools.mcp.port, detectPackageManager(cwd2));
1139
+ writeAgentFiles(cwd2, codexAgentFiles(config));
1140
+ mergeCodexConfigToml(join8(cwd2, ".codex/config.toml"), config.tools.mcp.port, cwd2, detectPackageManager(cwd2));
910
1141
  appendGitignore(cwd2);
911
1142
  writeSkills(cwd2, ".agents/skills");
912
1143
  }
913
- async build(config, cwd2) {
914
- const write2 = (relPath, content) => {
915
- const abs = join6(cwd2, relPath);
916
- mkdirSync5(resolve4(abs, ".."), { recursive: true });
917
- writeFileSync5(abs, content, "utf8");
918
- };
919
- write2("AGENTS.md", agentsMd(config));
920
- const projectName = config.project.name;
921
- const allowedPaths = (config.agents.explorer.allowedPaths ?? []).join(", ");
922
- const writablePaths = (config.agents.builder.writablePaths ?? []).join(", ");
923
- const leadModel = config.agents.lead.model;
924
- const explorerModel = config.agents.explorer.model;
925
- const consultantModel = config.agents.consultant?.model;
926
- const builderModel = config.agents.builder.model;
927
- const reviewerModel = config.agents.reviewer.model;
928
- writeAgentFile(cwd2, ".codex/agents/lead.toml", agentLeadToml({ projectName, model: leadModel }));
929
- writeAgentFile(cwd2, ".codex/agents/explorer.toml", agentExplorerToml({ projectName, allowedPaths, model: explorerModel }));
930
- writeAgentFile(cwd2, ".codex/agents/consultant.toml", agentConsultantToml({ projectName, model: consultantModel }));
931
- writeAgentFile(cwd2, ".codex/agents/builder.toml", agentBuilderToml({ projectName, writablePaths, model: builderModel }));
932
- writeAgentFile(cwd2, ".codex/agents/reviewer.toml", agentReviewerToml({ projectName, model: reviewerModel }));
933
- writeAgentFile(cwd2, ".codex/agents/default.toml", agentLeadAsDefaultToml({ projectName, model: leadModel }));
934
- mergeCodexConfigToml(join6(cwd2, ".codex/config.toml"), config.tools.mcp.port, detectPackageManager(cwd2));
1144
+ async build(config, cwd2, opts = {}) {
1145
+ const derived = reconcileGeneratedFiles(
1146
+ cwd2,
1147
+ [{ relPath: "AGENTS.md", content: agentsMd(config) }],
1148
+ { force: opts.force, backupRoot: join8(cwd2, config.storage.dir, "backups") }
1149
+ );
1150
+ const agents = writeAgentFiles(cwd2, codexAgentFiles(config), {
1151
+ force: opts.force,
1152
+ backupRoot: join8(cwd2, config.storage.dir, "backups")
1153
+ });
1154
+ mergeCodexConfigToml(join8(cwd2, ".codex/config.toml"), config.tools.mcp.port, cwd2, detectPackageManager(cwd2));
935
1155
  writeSkills(cwd2, ".agents/skills");
1156
+ return { agents, derived };
936
1157
  }
937
1158
  async migrate(config, _to, _cwd) {
938
1159
  void config;
@@ -943,23 +1164,31 @@ No tasks in progress.
943
1164
  };
944
1165
 
945
1166
  // src/core/materializer/opencode.ts
946
- import { existsSync as existsSync6, mkdirSync as mkdirSync6, writeFileSync as writeFileSync6 } from "fs";
947
- import { join as join7, resolve as resolve5 } from "path";
1167
+ import { existsSync as existsSync8, mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "fs";
1168
+ import { join as join9, resolve as resolve4 } from "path";
1169
+ function opencodeAgentFiles(config) {
1170
+ const projectName = config.project.name;
1171
+ return [
1172
+ { relPath: ".opencode/agents/lead.md", content: translateFrontmatterForOpenCode(agentLead({ projectName }), "lead") },
1173
+ { relPath: ".opencode/agents/explorer.md", content: translateFrontmatterForOpenCode(agentExplorer({ projectName }), "explorer") },
1174
+ { relPath: ".opencode/agents/consultant.md", content: translateFrontmatterForOpenCode(agentConsultant({ projectName }), "consultant") },
1175
+ { relPath: ".opencode/agents/builder.md", content: translateFrontmatterForOpenCode(agentBuilder({ projectName }), "builder") },
1176
+ { relPath: ".opencode/agents/reviewer.md", content: translateFrontmatterForOpenCode(agentReviewer({ projectName }), "reviewer") }
1177
+ ];
1178
+ }
948
1179
  var OpenCodeMaterializer = class {
949
1180
  async scaffold(config, opts) {
950
1181
  const { cwd: cwd2 } = opts;
951
1182
  const write2 = (relPath, content, mode) => {
952
- const abs = join7(cwd2, relPath);
953
- mkdirSync6(resolve5(abs, ".."), { recursive: true });
954
- writeFileSync6(abs, content, { encoding: "utf8", mode });
1183
+ const abs = join9(cwd2, relPath);
1184
+ mkdirSync5(resolve4(abs, ".."), { recursive: true });
1185
+ writeFileSync5(abs, content, { encoding: "utf8", mode });
955
1186
  };
956
- write2("AGENTS.md", agentsMd(config));
957
- if (!existsSync6(join7(cwd2, "health.sh"))) {
1187
+ write2("AGENTS.md", stampGenerated(agentsMd(config)));
1188
+ if (!existsSync8(join9(cwd2, "health.sh"))) {
958
1189
  write2("health.sh", HEALTH_SH, 493);
959
1190
  }
960
- const tasks = opts.firstTask ? [{ slug: slugify(opts.firstTask.title), ...opts.firstTask }] : [];
961
- write2(join7(config.storage.dir, "feature_list.json"), featureListJson(tasks));
962
- if (config.storage.scope === "local" && !existsSync6(join7(cwd2, config.storage.markdownFallback.path))) {
1191
+ if (config.storage.scope === "local" && !existsSync8(join9(cwd2, config.storage.markdownFallback.path))) {
963
1192
  write2(
964
1193
  config.storage.markdownFallback.path,
965
1194
  `<!-- AUTO-GENERATED by agent-harness-kit \u2014 DO NOT EDIT MANUALLY -->
@@ -971,35 +1200,24 @@ No tasks in progress.
971
1200
  `
972
1201
  );
973
1202
  }
974
- const projectName = config.project.name;
975
- const allowedPaths = (config.agents.explorer.allowedPaths ?? []).join(", ");
976
- const writablePaths = (config.agents.builder.writablePaths ?? []).join(", ");
977
- writeAgentFile(cwd2, ".opencode/agents/lead.md", translateFrontmatterForOpenCode(agentLead({ projectName })));
978
- writeAgentFile(cwd2, ".opencode/agents/explorer.md", translateFrontmatterForOpenCode(agentExplorer({ projectName, allowedPaths })));
979
- writeAgentFile(cwd2, ".opencode/agents/consultant.md", translateFrontmatterForOpenCode(agentConsultant({ projectName })));
980
- writeAgentFile(cwd2, ".opencode/agents/builder.md", translateFrontmatterForOpenCode(agentBuilder({ projectName, writablePaths })));
981
- writeAgentFile(cwd2, ".opencode/agents/reviewer.md", translateFrontmatterForOpenCode(agentReviewer({ projectName })));
982
- mergeOpencodeJson(join7(cwd2, "opencode.json"), config.tools.mcp.port, detectPackageManager(cwd2));
1203
+ writeAgentFiles(cwd2, opencodeAgentFiles(config));
1204
+ mergeOpencodeJson(join9(cwd2, "opencode.json"), config.tools.mcp.port, cwd2, detectPackageManager(cwd2));
983
1205
  appendGitignore(cwd2);
984
1206
  writeSkills(cwd2, ".opencode/skills");
985
1207
  }
986
- async build(config, cwd2) {
987
- const write2 = (relPath, content) => {
988
- const abs = join7(cwd2, relPath);
989
- mkdirSync6(resolve5(abs, ".."), { recursive: true });
990
- writeFileSync6(abs, content, "utf8");
991
- };
992
- write2("AGENTS.md", agentsMd(config));
993
- const projectName = config.project.name;
994
- const allowedPaths = (config.agents.explorer.allowedPaths ?? []).join(", ");
995
- const writablePaths = (config.agents.builder.writablePaths ?? []).join(", ");
996
- writeAgentFile(cwd2, ".opencode/agents/lead.md", translateFrontmatterForOpenCode(agentLead({ projectName })));
997
- writeAgentFile(cwd2, ".opencode/agents/explorer.md", translateFrontmatterForOpenCode(agentExplorer({ projectName, allowedPaths })));
998
- writeAgentFile(cwd2, ".opencode/agents/consultant.md", translateFrontmatterForOpenCode(agentConsultant({ projectName })));
999
- writeAgentFile(cwd2, ".opencode/agents/builder.md", translateFrontmatterForOpenCode(agentBuilder({ projectName, writablePaths })));
1000
- writeAgentFile(cwd2, ".opencode/agents/reviewer.md", translateFrontmatterForOpenCode(agentReviewer({ projectName })));
1001
- mergeOpencodeJson(join7(cwd2, "opencode.json"), config.tools.mcp.port, detectPackageManager(cwd2));
1208
+ async build(config, cwd2, opts = {}) {
1209
+ const derived = reconcileGeneratedFiles(
1210
+ cwd2,
1211
+ [{ relPath: "AGENTS.md", content: agentsMd(config) }],
1212
+ { force: opts.force, backupRoot: join9(cwd2, config.storage.dir, "backups") }
1213
+ );
1214
+ const agents = writeAgentFiles(cwd2, opencodeAgentFiles(config), {
1215
+ force: opts.force,
1216
+ backupRoot: join9(cwd2, config.storage.dir, "backups")
1217
+ });
1218
+ mergeOpencodeJson(join9(cwd2, "opencode.json"), config.tools.mcp.port, cwd2, detectPackageManager(cwd2));
1002
1219
  writeSkills(cwd2, ".opencode/skills");
1220
+ return { agents, derived };
1003
1221
  }
1004
1222
  async migrate(config, _to, _cwd) {
1005
1223
  void config;
@@ -1025,7 +1243,7 @@ function getMaterializer(provider) {
1025
1243
 
1026
1244
  // src/commands/build.ts
1027
1245
  async function runBuild(cwd2, opts) {
1028
- await buildOnce(cwd2);
1246
+ await buildOnce(cwd2, opts.force);
1029
1247
  if (opts.sync) {
1030
1248
  p.log.step("Syncing agent permissions...");
1031
1249
  const config = await loadConfig(cwd2);
@@ -1037,27 +1255,79 @@ async function runBuild(cwd2, opts) {
1037
1255
  watch(cwd2, { recursive: false }, async (_, filename) => {
1038
1256
  if (filename?.startsWith("agent-harness-kit.config")) {
1039
1257
  p.log.step("Config changed \u2014 rebuilding...");
1040
- await buildOnce(cwd2);
1258
+ await buildOnce(cwd2, false);
1041
1259
  }
1042
1260
  });
1043
1261
  await new Promise(() => {
1044
1262
  });
1045
1263
  }
1046
1264
  }
1047
- async function buildOnce(cwd2) {
1265
+ async function buildOnce(cwd2, force) {
1048
1266
  const spinner6 = p.spinner();
1049
1267
  spinner6.start("Loading config...");
1050
1268
  try {
1051
1269
  const config = await loadConfig(cwd2);
1052
1270
  spinner6.message("Rebuilding files...");
1053
1271
  const materializer = getMaterializer(config.provider);
1054
- await materializer.build(config, cwd2);
1055
- spinner6.stop(pc.green("Build complete"));
1056
- p.log.success("AGENTS.md");
1272
+ const report = await materializer.build(config, cwd2, { force });
1273
+ spinner6.stop(pc2.green("Build complete"));
1274
+ const d = report.derived;
1275
+ const upToDate = [...d.created, ...d.current, ...d.propagated];
1276
+ if (upToDate.length > 0) {
1277
+ p.log.success(upToDate.join(", "));
1278
+ }
1279
+ if (d.propagated.length > 0) {
1280
+ p.log.info(`Propagated config changes to ${d.propagated.length} generated file(s):
1281
+ ${d.propagated.join("\n ")}`);
1282
+ }
1283
+ if (d.overwritten.length > 0) {
1284
+ p.log.warn(
1285
+ pc2.yellow(
1286
+ `--force REGENERATED ${d.overwritten.length} hand-edited generated file(s), discarding your edits:
1287
+ ` + d.overwritten.join("\n ")
1288
+ )
1289
+ );
1290
+ if (d.backupDir) {
1291
+ p.log.info(pc2.yellow(` Previous content backed up \u2192 ${d.backupDir}`));
1292
+ }
1293
+ }
1294
+ if (d.preserved.length > 0) {
1295
+ p.log.warn(
1296
+ pc2.yellow(
1297
+ `Left ${d.preserved.length} hand-edited generated file(s) UNTOUCHED \u2014 your edits are safe:
1298
+ ` + d.preserved.join("\n ") + `
1299
+ These no longer match the current config. Re-run with --force to regenerate them
1300
+ (this DESTROYS your edits; a backup is written first).`
1301
+ )
1302
+ );
1303
+ }
1057
1304
  p.log.success(`Agent definitions (${config.provider})`);
1058
1305
  p.log.success("MCP config");
1306
+ const { created, overwritten, preserved, backupDir } = report.agents;
1307
+ if (created.length > 0) {
1308
+ p.log.info(`Created ${created.length} missing agent file(s):
1309
+ ${created.join("\n ")}`);
1310
+ }
1311
+ if (overwritten.length > 0) {
1312
+ p.log.warn(
1313
+ pc2.yellow(
1314
+ `--force REGENERATED ${overwritten.length} existing agent file(s), discarding any customizations:
1315
+ ` + overwritten.join("\n ")
1316
+ )
1317
+ );
1318
+ if (backupDir) {
1319
+ p.log.info(pc2.yellow(` Previous content backed up \u2192 ${backupDir}`));
1320
+ }
1321
+ }
1322
+ if (preserved.length > 0) {
1323
+ p.log.info(
1324
+ `Left ${preserved.length} existing agent file(s) untouched \u2014 agent files are yours to edit.
1325
+ Re-run with --force to regenerate them from the packaged templates (this DESTROYS your edits;
1326
+ a backup is written first).`
1327
+ );
1328
+ }
1059
1329
  } catch (err) {
1060
- spinner6.stop(pc.red("Build failed"));
1330
+ spinner6.stop(pc2.red("Build failed"));
1061
1331
  p.log.error(err instanceof Error ? err.message : String(err));
1062
1332
  process.exit(1);
1063
1333
  }
@@ -1065,34 +1335,41 @@ async function buildOnce(cwd2) {
1065
1335
 
1066
1336
  // src/commands/dashboard.ts
1067
1337
  import { homedir } from "os";
1068
- import { dirname as dirname4, join as join9 } from "path";
1069
- import { fileURLToPath as fileURLToPath3 } from "url";
1070
- import pc2 from "picocolors";
1338
+ import { dirname as dirname5, join as join11 } from "path";
1339
+ import { fileURLToPath as fileURLToPath4 } from "url";
1340
+ import pc3 from "picocolors";
1071
1341
 
1072
1342
  // src/core/dashboard-server.ts
1073
1343
  import { watch as watch2 } from "fs";
1074
- import { existsSync as existsSync7, readFileSync as readFileSync6 } from "fs";
1075
- import { extname, join as join8 } from "path";
1344
+ import { existsSync as existsSync9, readFileSync as readFileSync6 } from "fs";
1345
+ import { extname, join as join10 } from "path";
1076
1346
  import { serve } from "@hono/node-server";
1077
1347
  import { Hono } from "hono";
1078
1348
  import { WebSocketServer } from "ws";
1079
1349
 
1080
1350
  // src/core/port-utils.ts
1081
1351
  import { createServer } from "net";
1082
- function isPortFree(port) {
1083
- return new Promise((resolve12) => {
1352
+ var DASHBOARD_BIND_HOST = void 0;
1353
+ function isPortFree(port, host = DASHBOARD_BIND_HOST) {
1354
+ return new Promise((resolve11) => {
1084
1355
  const server = createServer();
1085
- server.once("error", () => resolve12(false));
1356
+ server.once("error", () => resolve11(false));
1086
1357
  server.once("listening", () => {
1087
- server.close(() => resolve12(true));
1358
+ server.close(() => resolve11(true));
1088
1359
  });
1089
- server.listen(port, "127.0.0.1");
1360
+ server.listen(port, host);
1090
1361
  });
1091
1362
  }
1092
- async function findFreePort(start, maxAttempts = 10) {
1363
+ async function findFreePort(start, options = {}) {
1364
+ const { maxAttempts = 10, host = DASHBOARD_BIND_HOST } = options;
1365
+ if (typeof start !== "number" || !Number.isInteger(start)) {
1366
+ throw new Error(
1367
+ `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).`
1368
+ );
1369
+ }
1093
1370
  for (let i = 0; i < maxAttempts; i++) {
1094
1371
  const port = start + i;
1095
- if (await isPortFree(port)) return port;
1372
+ if (await isPortFree(port, host)) return port;
1096
1373
  }
1097
1374
  throw new Error(
1098
1375
  `Could not find a free port after ${maxAttempts} attempts (tried ${start}-${start + maxAttempts - 1}). Please free a port and try again.`
@@ -1120,6 +1397,36 @@ function fileResponse(filePath) {
1120
1397
  headers: { "Content-Type": mime, "Cache-Control": "no-cache" }
1121
1398
  });
1122
1399
  }
1400
+ function awaitServerListening(server, port) {
1401
+ return new Promise((resolve11, reject) => {
1402
+ const closeQuietly = () => {
1403
+ try {
1404
+ server.close(() => {
1405
+ });
1406
+ } catch {
1407
+ }
1408
+ };
1409
+ const onError = (err) => {
1410
+ server.off("listening", onListening);
1411
+ closeQuietly();
1412
+ if (err.code === "EADDRINUSE") {
1413
+ reject(
1414
+ new Error(
1415
+ `Port ${port} was taken by another process while starting the dashboard. Please retry, or pick a different port with --port.`
1416
+ )
1417
+ );
1418
+ return;
1419
+ }
1420
+ reject(new Error(`Failed to start the dashboard on port ${port}: ${err.message}`));
1421
+ };
1422
+ const onListening = () => {
1423
+ server.off("error", onError);
1424
+ resolve11();
1425
+ };
1426
+ server.once("error", onError);
1427
+ server.once("listening", onListening);
1428
+ });
1429
+ }
1123
1430
  async function startDashboardServer(db, dbPath, staticPath, port) {
1124
1431
  const app = new Hono();
1125
1432
  const { tasks, actions, stats } = db;
@@ -1220,21 +1527,29 @@ async function startDashboardServer(db, dbPath, staticPath, port) {
1220
1527
  app.get("/*", (c) => {
1221
1528
  const urlPath = c.req.path;
1222
1529
  if (urlPath !== "/") {
1223
- const candidate = join8(staticPath, urlPath);
1224
- if (existsSync7(candidate)) {
1530
+ const candidate = join10(staticPath, urlPath);
1531
+ if (existsSync9(candidate)) {
1225
1532
  try {
1226
1533
  return fileResponse(candidate);
1227
1534
  } catch {
1228
1535
  }
1229
1536
  }
1230
1537
  }
1231
- return fileResponse(join8(staticPath, "index.html"));
1538
+ return fileResponse(join10(staticPath, "index.html"));
1232
1539
  });
1233
- const resolvedPort = await findFreePort(port);
1540
+ const resolvedPort = await findFreePort(port, { host: DASHBOARD_BIND_HOST });
1234
1541
  if (resolvedPort !== port) {
1235
1542
  console.log(`Port ${port} in use, using ${resolvedPort}`);
1236
1543
  }
1237
- const httpServer = serve({ fetch: app.fetch, port: resolvedPort });
1544
+ const httpServer = serve({
1545
+ fetch: app.fetch,
1546
+ port: resolvedPort,
1547
+ hostname: DASHBOARD_BIND_HOST
1548
+ });
1549
+ await awaitServerListening(httpServer, resolvedPort);
1550
+ httpServer.on("error", (err) => {
1551
+ console.error(`Dashboard server error: ${err.message}`);
1552
+ });
1238
1553
  const wss = new WebSocketServer({ noServer: true });
1239
1554
  httpServer.on("upgrade", (req, socket, head) => {
1240
1555
  if (req.url === "/ws") {
@@ -1259,7 +1574,7 @@ async function startDashboardServer(db, dbPath, staticPath, port) {
1259
1574
  let watcher = null;
1260
1575
  if (dbPath) {
1261
1576
  const walPath = `${dbPath}-wal`;
1262
- const watchTarget = existsSync7(walPath) ? walPath : dbPath;
1577
+ const watchTarget = existsSync9(walPath) ? walPath : dbPath;
1263
1578
  watcher = watch2(watchTarget, broadcast);
1264
1579
  }
1265
1580
  return {
@@ -1274,16 +1589,16 @@ async function startDashboardServer(db, dbPath, staticPath, port) {
1274
1589
  }
1275
1590
 
1276
1591
  // src/commands/dashboard.ts
1277
- var __dirname3 = dirname4(fileURLToPath3(import.meta.url));
1592
+ var __dirname3 = dirname5(fileURLToPath4(import.meta.url));
1278
1593
  async function runDashboard(cwd2, opts) {
1279
1594
  const config = await loadConfig(cwd2);
1280
1595
  const db = await openDB(config, cwd2);
1281
1596
  const dbPath = config.database.type === "sqlite" ? resolveSqlitePath(config, cwd2, homedir()) : null;
1282
- const staticPath = join9(__dirname3, "dashboard-dist");
1597
+ const staticPath = join11(__dirname3, "dashboard-dist");
1283
1598
  const { url } = await startDashboardServer(db, dbPath, staticPath, opts.port);
1284
- console.log(pc2.green(`\u2713`) + ` Dashboard running at ${pc2.bold(pc2.cyan(url))}`);
1285
- console.log(pc2.dim(` WebSocket live updates enabled`));
1286
- console.log(pc2.dim(` Press Ctrl+C to stop`));
1599
+ console.log(pc3.green(`\u2713`) + ` Dashboard running at ${pc3.bold(pc3.cyan(url))}`);
1600
+ console.log(pc3.dim(` WebSocket live updates enabled`));
1601
+ console.log(pc3.dim(` Press Ctrl+C to stop`));
1287
1602
  if (opts.open) {
1288
1603
  const { default: open } = await import("open");
1289
1604
  await open(url);
@@ -1296,25 +1611,12 @@ async function runDashboard(cwd2, opts) {
1296
1611
  }
1297
1612
 
1298
1613
  // src/commands/doctor.ts
1299
- import pc3 from "picocolors";
1614
+ import pc4 from "picocolors";
1300
1615
 
1301
1616
  // src/core/doctor.ts
1302
- import { existsSync as existsSync9, readFileSync as readFileSync7 } from "fs";
1303
- import { dirname as dirname6, join as join11 } from "path";
1617
+ import { existsSync as existsSync10, readFileSync as readFileSync7 } from "fs";
1618
+ import { dirname as dirname6, join as join12 } from "path";
1304
1619
  import { fileURLToPath as fileURLToPath5 } from "url";
1305
-
1306
- // src/core/package-data.ts
1307
- import { existsSync as existsSync8 } from "fs";
1308
- import { createRequire } from "module";
1309
- import { dirname as dirname5, join as join10 } from "path";
1310
- import { fileURLToPath as fileURLToPath4 } from "url";
1311
- var require2 = createRequire(import.meta.url);
1312
- var here = dirname5(fileURLToPath4(import.meta.url));
1313
- var candidates = [join10(here, "..", "..", "package.json"), join10(here, "..", "package.json")];
1314
- var pkgPath = candidates.find((p8) => existsSync8(p8)) ?? candidates[0];
1315
- var pkg = require2(pkgPath);
1316
-
1317
- // src/core/doctor.ts
1318
1620
  var REGISTRY_URL = `https://registry.npmjs.org/${pkg.name}/latest`;
1319
1621
  var TIMEOUT_MS = 2e3;
1320
1622
  var LIB_VERSION_CACHE_TTL_MS = 5 * 60 * 1e3;
@@ -1366,68 +1668,15 @@ function getProviderAgentInfo(provider) {
1366
1668
  return { agentsDir: ".claude/agents", ext: ".md" };
1367
1669
  }
1368
1670
  }
1369
- function generateExpectedAgentContent(agentName, provider, vars) {
1370
- const { projectName, allowedPaths, writablePaths, model } = vars;
1371
- if (provider === "claude-code") {
1372
- const templateFns = {
1373
- lead: () => agentLead({ projectName }),
1374
- explorer: () => agentExplorer({ projectName, allowedPaths }),
1375
- consultant: () => agentConsultant({ projectName }),
1376
- builder: () => agentBuilder({ projectName, writablePaths }),
1377
- reviewer: () => agentReviewer({ projectName })
1378
- };
1379
- return translateFrontmatterForClaudeCode(templateFns[agentName](), agentName, model);
1380
- }
1381
- if (provider === "opencode") {
1382
- const templateFns = {
1383
- lead: () => agentLead({ projectName }),
1384
- explorer: () => agentExplorer({ projectName, allowedPaths }),
1385
- consultant: () => agentConsultant({ projectName }),
1386
- builder: () => agentBuilder({ projectName, writablePaths }),
1387
- reviewer: () => agentReviewer({ projectName })
1388
- };
1389
- return translateFrontmatterForOpenCode(templateFns[agentName]());
1390
- }
1391
- const tomlFns = {
1392
- lead: () => agentLeadToml({ projectName, model }),
1393
- explorer: () => agentExplorerToml({ projectName, allowedPaths, model }),
1394
- consultant: () => agentConsultantToml({ projectName, model }),
1395
- builder: () => agentBuilderToml({ projectName, writablePaths, model }),
1396
- reviewer: () => agentReviewerToml({ projectName, model })
1397
- };
1398
- return tomlFns[agentName]();
1399
- }
1400
- function checkAgentFilesAtRoot(agentsRoot, ext, provider, projectName, allowedPaths, writablePaths, models) {
1671
+ function checkAgentFilesAtRoot(agentsRoot, ext) {
1401
1672
  return AGENT_NAMES.map((name) => {
1402
- const filePath = join11(agentsRoot, `${name}${ext}`);
1403
- if (!existsSync9(filePath)) {
1404
- return { name, status: "missing" };
1405
- }
1406
- try {
1407
- const live = readFileSync7(filePath, "utf8");
1408
- const expected = generateExpectedAgentContent(name, provider, {
1409
- projectName,
1410
- allowedPaths,
1411
- writablePaths,
1412
- model: models[name]
1413
- });
1414
- return { name, status: live === expected ? "ok" : "outdated" };
1415
- } catch {
1416
- return { name, status: "outdated" };
1417
- }
1673
+ const filePath = join12(agentsRoot, `${name}${ext}`);
1674
+ return { name, status: existsSync10(filePath) ? "ok" : "missing" };
1418
1675
  });
1419
1676
  }
1420
- function checkAgentFiles(cwd2, provider, projectName, allowedPaths, writablePaths, models) {
1677
+ function checkAgentFiles(cwd2, provider) {
1421
1678
  const { agentsDir, ext } = getProviderAgentInfo(provider);
1422
- return checkAgentFilesAtRoot(
1423
- join11(cwd2, agentsDir),
1424
- ext,
1425
- provider,
1426
- projectName,
1427
- allowedPaths,
1428
- writablePaths,
1429
- models
1430
- );
1679
+ return checkAgentFilesAtRoot(join12(cwd2, agentsDir), ext);
1431
1680
  }
1432
1681
  function getProviderSkillsDir(provider) {
1433
1682
  switch (provider) {
@@ -1442,11 +1691,11 @@ function getProviderSkillsDir(provider) {
1442
1691
  }
1443
1692
  }
1444
1693
  function checkSkillsAtRoot(skillsRoot) {
1445
- const skillSourceBase = join11(__dirname4, "skills");
1694
+ const skillSourceBase = join12(__dirname4, "skills");
1446
1695
  return SKILL_NAMES.map((name) => {
1447
- const livePath = join11(skillsRoot, name, "SKILL.md");
1448
- const sourcePath = join11(skillSourceBase, name, "SKILL.md");
1449
- if (!existsSync9(livePath)) {
1696
+ const livePath = join12(skillsRoot, name, "SKILL.md");
1697
+ const sourcePath = join12(skillSourceBase, name, "SKILL.md");
1698
+ if (!existsSync10(livePath)) {
1450
1699
  return { name, status: "missing" };
1451
1700
  }
1452
1701
  try {
@@ -1460,7 +1709,7 @@ function checkSkillsAtRoot(skillsRoot) {
1460
1709
  }
1461
1710
  function checkSkills(cwd2, provider) {
1462
1711
  const skillsDir = getProviderSkillsDir(provider);
1463
- return checkSkillsAtRoot(join11(cwd2, skillsDir));
1712
+ return checkSkillsAtRoot(join12(cwd2, skillsDir));
1464
1713
  }
1465
1714
  async function getDoctorStatus(cwd2) {
1466
1715
  const lib = await checkLibVersion();
@@ -1475,33 +1724,23 @@ async function getDoctorStatus(cwd2) {
1475
1724
  };
1476
1725
  }
1477
1726
  const provider = config.provider;
1478
- const projectName = config.project.name;
1479
- const allowedPaths = (config.agents.explorer.allowedPaths ?? []).join(", ");
1480
- const writablePaths = (config.agents.builder.writablePaths ?? []).join(", ");
1481
- const models = {
1482
- lead: config.agents.lead.model,
1483
- explorer: config.agents.explorer.model,
1484
- consultant: config.agents.consultant?.model,
1485
- builder: config.agents.builder.model,
1486
- reviewer: config.agents.reviewer.model
1487
- };
1488
- const agents = checkAgentFiles(cwd2, provider, projectName, allowedPaths, writablePaths, models);
1727
+ const agents = checkAgentFiles(cwd2, provider);
1489
1728
  const skills = checkSkills(cwd2, provider);
1490
1729
  return { lib, agents, skills };
1491
1730
  }
1492
1731
 
1493
1732
  // src/commands/doctor.ts
1494
1733
  function ok(label, detail) {
1495
- console.log(` ${pc3.cyan(label.padEnd(16))}${pc3.green("[\u2713]")} ${detail}`);
1734
+ console.log(` ${pc4.cyan(label.padEnd(16))}${pc4.green("[\u2713]")} ${detail}`);
1496
1735
  }
1497
1736
  function warn(label, detail, hint) {
1498
- console.log(` ${pc3.cyan(label.padEnd(16))}${pc3.yellow("[!]")} ${detail}`);
1737
+ console.log(` ${pc4.cyan(label.padEnd(16))}${pc4.yellow("[!]")} ${detail}`);
1499
1738
  if (hint) {
1500
- console.log(` ${"".padEnd(16)} ${pc3.dim(hint)}`);
1739
+ console.log(` ${"".padEnd(16)} ${pc4.dim(hint)}`);
1501
1740
  }
1502
1741
  }
1503
1742
  function neutral(label, detail) {
1504
- console.log(` ${pc3.cyan(label.padEnd(16))}${pc3.dim("[~]")} ${detail}`);
1743
+ console.log(` ${pc4.cyan(label.padEnd(16))}${pc4.dim("[~]")} ${detail}`);
1505
1744
  }
1506
1745
  function printLibSection(lib) {
1507
1746
  if (lib.latest === null) {
@@ -1522,17 +1761,13 @@ function printAgentsSection(agents) {
1522
1761
  return;
1523
1762
  }
1524
1763
  const missing = agents.filter((a) => a.status === "missing");
1525
- const outdated = agents.filter((a) => a.status === "outdated");
1526
- if (missing.length === 0 && outdated.length === 0) {
1527
- ok("agent files", "all up to date");
1764
+ if (missing.length === 0) {
1765
+ ok("agent files", "all present");
1528
1766
  return;
1529
1767
  }
1530
1768
  for (const agent of missing) {
1531
1769
  warn("agent files", `${agent.name} missing`, "run: ahk build");
1532
1770
  }
1533
- for (const agent of outdated) {
1534
- warn("agent files", `${agent.name} outdated`, "run: ahk build");
1535
- }
1536
1771
  }
1537
1772
  function printSkillsSection(skills) {
1538
1773
  if (skills.length === 0) {
@@ -1557,7 +1792,7 @@ async function runDoctor(cwd2) {
1557
1792
  configFound = false;
1558
1793
  }
1559
1794
  console.log("");
1560
- console.log(pc3.bold(`\u25CF ahk doctor ` + "\u2500".repeat(44)));
1795
+ console.log(pc4.bold(`\u25CF ahk doctor ` + "\u2500".repeat(44)));
1561
1796
  console.log("");
1562
1797
  const status = await getDoctorStatus(cwd2);
1563
1798
  printLibSection(status.lib);
@@ -1575,11 +1810,11 @@ async function runDoctor(cwd2) {
1575
1810
  }
1576
1811
 
1577
1812
  // src/commands/export.ts
1578
- import { writeFileSync as writeFileSync7 } from "fs";
1579
- import pc4 from "picocolors";
1813
+ import { writeFileSync as writeFileSync6 } from "fs";
1814
+ import pc5 from "picocolors";
1580
1815
  async function runExport(cwd2, opts) {
1581
1816
  if (!opts.sql && !opts.json) {
1582
- console.error(pc4.red("Specify --sql or --json"));
1817
+ console.error(pc5.red("Specify --sql or --json"));
1583
1818
  process.exit(1);
1584
1819
  }
1585
1820
  const config = await loadConfig(cwd2);
@@ -1589,14 +1824,14 @@ async function runExport(cwd2, opts) {
1589
1824
  const data = await db.exportJson();
1590
1825
  const out = JSON.stringify(data, null, 2) + "\n";
1591
1826
  if (opts.output) {
1592
- writeFileSync7(opts.output, out, "utf8");
1593
- console.log(pc4.green(`\u2713 Exported JSON \u2192 ${opts.output}`));
1827
+ writeFileSync6(opts.output, out, "utf8");
1828
+ console.log(pc5.green(`\u2713 Exported JSON \u2192 ${opts.output}`));
1594
1829
  } else {
1595
1830
  process.stdout.write(out);
1596
1831
  }
1597
1832
  }
1598
1833
  if (opts.sql) {
1599
- console.error(pc4.dim("SQL dump requires direct SQLite access \u2014 use: sqlite3 .harness/harness.db .dump"));
1834
+ console.error(pc5.dim("SQL dump requires direct SQLite access \u2014 use: sqlite3 .harness/harness.db .dump"));
1600
1835
  process.exit(1);
1601
1836
  }
1602
1837
  } finally {
@@ -1606,28 +1841,28 @@ async function runExport(cwd2, opts) {
1606
1841
 
1607
1842
  // src/commands/health.ts
1608
1843
  import { spawnSync } from "child_process";
1609
- import { existsSync as existsSync10 } from "fs";
1844
+ import { existsSync as existsSync11 } from "fs";
1610
1845
  import { homedir as homedir2 } from "os";
1611
- import { join as join12, resolve as resolve6 } from "path";
1612
- import pc5 from "picocolors";
1846
+ import { join as join13, resolve as resolve5 } from "path";
1847
+ import pc6 from "picocolors";
1613
1848
  function checkLine(label, ok3, message, indent = 0) {
1614
- const prefix = label ? pc5.cyan(`[${label}] `) : " ".repeat(indent);
1615
- const icon = ok3 ? pc5.green("\u2713") : pc5.red("\u2717");
1616
- console.log(prefix + icon + " " + (ok3 ? pc5.green(message) : pc5.red(message)));
1849
+ const prefix = label ? pc6.cyan(`[${label}] `) : " ".repeat(indent);
1850
+ const icon = ok3 ? pc6.green("\u2713") : pc6.red("\u2717");
1851
+ console.log(prefix + icon + " " + (ok3 ? pc6.green(message) : pc6.red(message)));
1617
1852
  }
1618
1853
  async function runHealth(cwd2) {
1619
1854
  let config;
1620
1855
  try {
1621
1856
  config = await loadConfig(cwd2);
1622
1857
  } catch {
1623
- console.error(pc5.red("\u2717 No config found. Run: ahk init"));
1858
+ console.error(pc6.red("\u2717 No config found. Run: ahk init"));
1624
1859
  process.exit(1);
1625
1860
  }
1626
1861
  let allOk = true;
1627
1862
  let dbOk;
1628
1863
  if (config.database.type === "sqlite") {
1629
1864
  const dbPath = resolveSqlitePath(config, cwd2, homedir2());
1630
- dbOk = existsSync10(dbPath);
1865
+ dbOk = existsSync11(dbPath);
1631
1866
  checkLine("checking DB", dbOk, `${dbPath} reachable`);
1632
1867
  } else {
1633
1868
  dbOk = true;
@@ -1640,8 +1875,8 @@ async function runHealth(cwd2) {
1640
1875
  const agentsLabelWidth = "[checking agents] ".length;
1641
1876
  for (let i = 0; i < agentNames.length; i++) {
1642
1877
  const name = agentNames[i];
1643
- const agentPath = join12(cwd2, agentsDir, `${name}${providerFiles.agentExtension}`);
1644
- const ok3 = existsSync10(agentPath);
1878
+ const agentPath = join13(cwd2, agentsDir, `${name}${providerFiles.agentExtension}`);
1879
+ const ok3 = existsSync11(agentPath);
1645
1880
  checkLine(
1646
1881
  i === 0 ? "checking agents" : null,
1647
1882
  ok3,
@@ -1652,19 +1887,19 @@ async function runHealth(cwd2) {
1652
1887
  }
1653
1888
  if (config.tools.mcp.enabled) {
1654
1889
  const mcpFile = providerFiles.mcpFile;
1655
- const mcpPath = resolve6(cwd2, mcpFile);
1656
- const mcpOk = existsSync10(mcpPath);
1890
+ const mcpPath = resolve5(cwd2, mcpFile);
1891
+ const mcpOk = existsSync11(mcpPath);
1657
1892
  checkLine("checking MCP", mcpOk, `${mcpFile} valid`);
1658
1893
  if (!mcpOk) allOk = false;
1659
1894
  }
1660
1895
  if (!allOk) {
1661
1896
  console.log("");
1662
- console.error(pc5.red("\u2717 Harness checks failed \u2014 fix the above before running health.sh"));
1897
+ console.error(pc6.red("\u2717 Harness checks failed \u2014 fix the above before running health.sh"));
1663
1898
  process.exit(1);
1664
1899
  }
1665
- const scriptPath = resolve6(cwd2, config.health.scriptPath);
1666
- if (!existsSync10(scriptPath)) {
1667
- console.error(pc5.red(`\u2717 health.sh not found: ${scriptPath}`));
1900
+ const scriptPath = resolve5(cwd2, config.health.scriptPath);
1901
+ if (!existsSync11(scriptPath)) {
1902
+ console.error(pc6.red(`\u2717 health.sh not found: ${scriptPath}`));
1668
1903
  console.error(" Run ahk init first.");
1669
1904
  process.exit(1);
1670
1905
  }
@@ -1674,14 +1909,14 @@ async function runHealth(cwd2) {
1674
1909
  encoding: "utf8"
1675
1910
  });
1676
1911
  if (result.error) {
1677
- console.error(pc5.red(`\u2717 Failed to run health.sh: ${result.error.message}`));
1912
+ console.error(pc6.red(`\u2717 Failed to run health.sh: ${result.error.message}`));
1678
1913
  process.exit(1);
1679
1914
  }
1680
1915
  if (result.status === 0) {
1681
- console.log(pc5.green("\u2713 Health check passed"));
1916
+ console.log(pc6.green("\u2713 Health check passed"));
1682
1917
  process.exit(0);
1683
1918
  } else {
1684
- console.error(pc5.red(`\u2717 Health check failed (exit ${result.status ?? "unknown"})`));
1919
+ console.error(pc6.red(`\u2717 Health check failed (exit ${result.status ?? "unknown"})`));
1685
1920
  process.exit(result.status ?? 1);
1686
1921
  }
1687
1922
  }
@@ -1699,10 +1934,10 @@ function getProviderHealthFiles(provider) {
1699
1934
  }
1700
1935
 
1701
1936
  // src/commands/init.ts
1702
- import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync8 } from "fs";
1703
- import { join as join14 } from "path";
1937
+ import { existsSync as existsSync13, mkdirSync as mkdirSync6, readFileSync as readFileSync9, writeFileSync as writeFileSync7 } from "fs";
1938
+ import { join as join15 } from "path";
1704
1939
  import * as p3 from "@clack/prompts";
1705
- import pc7 from "picocolors";
1940
+ import pc8 from "picocolors";
1706
1941
 
1707
1942
  // src/schema/init.ts
1708
1943
  import * as v from "valibot";
@@ -1755,13 +1990,13 @@ var cliFormWithRetry = async (formFn, schema) => {
1755
1990
 
1756
1991
  // src/commands/init-helpers.ts
1757
1992
  import { randomUUID } from "crypto";
1758
- import { existsSync as existsSync11, readFileSync as readFileSync8 } from "fs";
1759
- import { join as join13 } from "path";
1760
- import pc6 from "picocolors";
1993
+ import { existsSync as existsSync12, readFileSync as readFileSync8 } from "fs";
1994
+ import { join as join14 } from "path";
1995
+ import pc7 from "picocolors";
1761
1996
  function readProjectNameFromPackageJson(cwd2) {
1762
1997
  try {
1763
- const pkgPath2 = join13(cwd2, "package.json");
1764
- if (!existsSync11(pkgPath2)) return null;
1998
+ const pkgPath2 = join14(cwd2, "package.json");
1999
+ if (!existsSync12(pkgPath2)) return null;
1765
2000
  const content = readFileSync8(pkgPath2, "utf8");
1766
2001
  const pkg2 = JSON.parse(content);
1767
2002
  const name = pkg2?.name;
@@ -1772,10 +2007,11 @@ function readProjectNameFromPackageJson(cwd2) {
1772
2007
  }
1773
2008
  }
1774
2009
  function detectConfigExtension(cwd2) {
2010
+ if (!isLocalInstallSatisfied(cwd2)) return "json";
1775
2011
  try {
1776
- if (existsSync11(join13(cwd2, "tsconfig.json"))) return "ts";
1777
- const pkgPath2 = join13(cwd2, "package.json");
1778
- if (!existsSync11(pkgPath2)) return "mjs";
2012
+ if (existsSync12(join14(cwd2, "tsconfig.json"))) return "ts";
2013
+ const pkgPath2 = join14(cwd2, "package.json");
2014
+ if (!existsSync12(pkgPath2)) return "mjs";
1779
2015
  const pkg2 = JSON.parse(readFileSync8(pkgPath2, "utf8"));
1780
2016
  if (pkg2?.type === "module") return "mjs";
1781
2017
  } catch {
@@ -1783,7 +2019,6 @@ function detectConfigExtension(cwd2) {
1783
2019
  return "mjs";
1784
2020
  }
1785
2021
  function applyConfigDefaults(params) {
1786
- const models = params.models ?? {};
1787
2022
  const scope = params.scope ?? "local";
1788
2023
  const projectId = params.projectId ?? randomUUID();
1789
2024
  const baseStorage = {
@@ -1806,22 +2041,10 @@ function applyConfigDefaults(params) {
1806
2041
  docsPath: params.docsPath,
1807
2042
  agentsMd: "./AGENTS.md"
1808
2043
  },
1809
- agents: {
1810
- lead: { instructionsPath: null, ...models.lead && { model: models.lead } },
1811
- explorer: {
1812
- instructionsPath: null,
1813
- allowedPaths: [params.docsPath, "./src"],
1814
- ...models.explorer && { model: models.explorer }
1815
- },
1816
- builder: {
1817
- instructionsPath: null,
1818
- writablePaths: ["./src", "./tests"],
1819
- ...models.builder && { model: models.builder }
1820
- },
1821
- reviewer: { instructionsPath: null, ...models.reviewer && { model: models.reviewer } },
1822
- ...models.consultant && { consultant: { instructionsPath: null, model: models.consultant } },
1823
- custom: []
1824
- },
2044
+ // No `agents` key: per-agent settings (model, role instructions) live in
2045
+ // the generated agent file, which is user-owned. This object is the runtime
2046
+ // twin of the config body emitted by configObjectBody() in templates.ts —
2047
+ // drift between the two is the bug this pairing has to keep out.
1825
2048
  database: { type: "sqlite" },
1826
2049
  storage,
1827
2050
  health: {
@@ -1840,27 +2063,27 @@ function stripAnsi(str2) {
1840
2063
  function drawBox(lines) {
1841
2064
  const width = Math.max(...lines.map((l) => stripAnsi(l).length));
1842
2065
  const border = "\u2500".repeat(width);
1843
- console.log(pc6.yellow(`\u250C${border}\u2510`));
2066
+ console.log(pc7.yellow(`\u250C${border}\u2510`));
1844
2067
  for (const line of lines) {
1845
2068
  const pad = width - stripAnsi(line).length;
1846
2069
  const padStr = pad > 0 ? " ".repeat(pad) : "";
1847
- console.log(pc6.yellow("\u2502") + line + padStr + pc6.yellow("\u2502"));
2070
+ console.log(pc7.yellow("\u2502") + line + padStr + pc7.yellow("\u2502"));
1848
2071
  }
1849
- console.log(pc6.yellow(`\u2514${border}\u2518`));
2072
+ console.log(pc7.yellow(`\u2514${border}\u2518`));
1850
2073
  }
1851
2074
  function printWelcomeMessage(projectName) {
1852
2075
  const sep = "\u2500".repeat(38);
1853
2076
  const lines = [
1854
- ` ${pc6.bold(pc6.white("agent-harness-kit"))} `,
1855
- ` ${pc6.gray("\u2014")} harness scaffolding ${pc6.gray("\u2014")} `,
1856
- ` ${pc6.gray(sep)} `,
1857
- ` ${pc6.bold("Project:")} ${projectName || "\u2014"} `,
1858
- ` ${pc6.bold("Status:")} ${pc6.green("ready to configure")} `,
1859
- ` ${pc6.gray(sep)} `,
1860
- ` ${pc6.gray("Next steps:")} `,
1861
- ` ${pc6.gray("\u2192")} ${pc6.gray("Set up your AI provider config")} `,
1862
- ` ${pc6.gray("\u2192")} ${pc6.gray("Run your health check to verify")} `,
1863
- ` ${pc6.gray("\u2192")} ${pc6.gray("Start adding tasks for your agents")} `
2077
+ ` ${pc7.bold(pc7.white("agent-harness-kit"))} `,
2078
+ ` ${pc7.gray("\u2014")} harness scaffolding ${pc7.gray("\u2014")} `,
2079
+ ` ${pc7.gray(sep)} `,
2080
+ ` ${pc7.bold("Project:")} ${projectName || "\u2014"} `,
2081
+ ` ${pc7.bold("Status:")} ${pc7.green("ready to configure")} `,
2082
+ ` ${pc7.gray(sep)} `,
2083
+ ` ${pc7.gray("Next steps:")} `,
2084
+ ` ${pc7.gray("\u2192")} ${pc7.gray("Set up your AI provider config")} `,
2085
+ ` ${pc7.gray("\u2192")} ${pc7.gray("Run your health check to verify")} `,
2086
+ ` ${pc7.gray("\u2192")} ${pc7.gray("Start adding tasks for your agents")} `
1864
2087
  ];
1865
2088
  console.log();
1866
2089
  drawBox(lines);
@@ -1868,22 +2091,50 @@ function printWelcomeMessage(projectName) {
1868
2091
  }
1869
2092
 
1870
2093
  // src/commands/init.ts
2094
+ async function reconcileFeatureList(db, installDir, storageDir, firstTask) {
2095
+ const featureListPath = join15(installDir, storageDir, "feature_list.json");
2096
+ let existingSeeds = [];
2097
+ let parseFailed = false;
2098
+ if (existsSync13(featureListPath)) {
2099
+ try {
2100
+ const parsed = JSON.parse(readFileSync9(featureListPath, "utf8"));
2101
+ if (!Array.isArray(parsed)) throw new Error("feature_list.json is not a JSON array");
2102
+ existingSeeds = parsed;
2103
+ } catch {
2104
+ parseFailed = true;
2105
+ }
2106
+ }
2107
+ const firstTaskSeed = firstTask ? {
2108
+ slug: slugify(firstTask.title),
2109
+ title: firstTask.title,
2110
+ description: firstTask.description,
2111
+ acceptance: firstTask.acceptance
2112
+ } : void 0;
2113
+ if (parseFailed) {
2114
+ if (firstTaskSeed) await db.syncFromFeatureList([firstTaskSeed]);
2115
+ } else {
2116
+ const seeds = firstTaskSeed ? [...existingSeeds, firstTaskSeed] : existingSeeds;
2117
+ await db.syncFromFeatureList(seeds);
2118
+ await db.writeFeatureList(installDir);
2119
+ }
2120
+ return { parseFailed };
2121
+ }
1871
2122
  async function runInit(cwd2, flags) {
1872
2123
  const existingConfig = findConfigFile(cwd2);
1873
2124
  if (existingConfig) {
1874
2125
  console.log(
1875
- pc7.yellow("\u26A0") + " " + pc7.bold("Project already initialized.") + pc7.dim(` (${existingConfig})`)
2126
+ pc8.yellow("\u26A0") + " " + pc8.bold("Project already initialized.") + pc8.dim(` (${existingConfig})`)
1876
2127
  );
1877
2128
  console.log();
1878
- console.log(pc7.dim("Suggested next steps:"));
2129
+ console.log(pc8.dim("Suggested next steps:"));
1879
2130
  console.log(
1880
- " " + pc7.cyan("ahk build") + pc7.dim(" \u2014 re-sync agent files after updating the library")
2131
+ " " + pc8.cyan("ahk build") + pc8.dim(" \u2014 re-sync agent files after updating the library")
1881
2132
  );
1882
- console.log(" " + pc7.cyan("ahk build --sync") + pc7.dim(" \u2014 also sync agent permissions"));
2133
+ console.log(" " + pc8.cyan("ahk build --sync") + pc8.dim(" \u2014 also sync agent permissions"));
1883
2134
  console.log(
1884
- " " + pc7.cyan("ahk reset") + pc7.dim(" \u2014 wipe and re-initialize from scratch")
2135
+ " " + pc8.cyan("ahk reset") + pc8.dim(" \u2014 wipe and re-initialize from scratch")
1885
2136
  );
1886
- console.log(" " + pc7.cyan("ahk dashboard") + pc7.dim(" \u2014 open the harness dashboard"));
2137
+ console.log(" " + pc8.cyan("ahk dashboard") + pc8.dim(" \u2014 open the harness dashboard"));
1887
2138
  process.exit(0);
1888
2139
  }
1889
2140
  const detectedName = flags.name ?? readProjectNameFromPackageJson(cwd2);
@@ -1942,52 +2193,25 @@ async function runInit(cwd2, flags) {
1942
2193
  { key: "builder", label: "Builder" },
1943
2194
  { key: "reviewer", label: "Reviewer" }
1944
2195
  ];
1945
- const modelOverrides = {};
1946
- if (provider === "claude-code" || provider === "codex-cli") {
1947
- const wantsModelCustomization = await p3.confirm({
1948
- message: "Customize the model per agent?",
1949
- initialValue: false
1950
- });
1951
- if (p3.isCancel(wantsModelCustomization)) {
1952
- p3.cancel("Cancelled.");
1953
- process.exit(0);
1954
- }
1955
- if (wantsModelCustomization) {
1956
- if (provider === "claude-code") {
1957
- for (const agent of AGENT_LABELS) {
1958
- const val = await p3.select({
1959
- message: `Model for ${agent.label}`,
1960
- options: [
1961
- { value: "inherit", label: "inherit (default)" },
1962
- { value: "haiku", label: "haiku" },
1963
- { value: "sonnet", label: "sonnet" },
1964
- { value: "opus", label: "opus" },
1965
- { value: "fable", label: "fable" }
1966
- ],
1967
- initialValue: "inherit"
1968
- });
1969
- if (p3.isCancel(val)) {
1970
- p3.cancel("Cancelled.");
1971
- process.exit(0);
1972
- }
1973
- modelOverrides[agent.key] = val;
1974
- }
1975
- } else {
1976
- for (const agent of AGENT_LABELS) {
1977
- const val = await p3.text({
1978
- message: `Model for ${agent.label} (Codex does not validate this value)`,
1979
- placeholder: "e.g. gpt-5 (empty or <3 chars = no override)"
1980
- });
1981
- if (p3.isCancel(val)) {
1982
- p3.cancel("Cancelled.");
1983
- process.exit(0);
1984
- }
1985
- const trimmed = val.trim();
1986
- if (trimmed.length >= 3) {
1987
- modelOverrides[agent.key] = trimmed;
1988
- }
1989
- }
2196
+ const claudeAgentModels = {};
2197
+ if (provider === "claude-code") {
2198
+ for (const agent of AGENT_LABELS) {
2199
+ const val = await p3.select({
2200
+ message: `Model for ${agent.label}`,
2201
+ options: [
2202
+ { value: "inherit", label: "inherit (default)" },
2203
+ { value: "haiku", label: "haiku" },
2204
+ { value: "sonnet", label: "sonnet" },
2205
+ { value: "opus", label: "opus" },
2206
+ { value: "fable", label: "fable" }
2207
+ ],
2208
+ initialValue: "inherit"
2209
+ });
2210
+ if (p3.isCancel(val)) {
2211
+ p3.cancel("Cancelled.");
2212
+ process.exit(0);
1990
2213
  }
2214
+ claudeAgentModels[agent.key] = val;
1991
2215
  }
1992
2216
  }
1993
2217
  let docsPath;
@@ -2081,6 +2305,7 @@ async function runInit(cwd2, flags) {
2081
2305
  firstTask = { title: taskTitle, description: taskDesc, acceptance };
2082
2306
  }
2083
2307
  let configExt = "ts";
2308
+ let featureListParseFailedPath = null;
2084
2309
  const spinner6 = p3.spinner();
2085
2310
  spinner6.start("Scaffolding...");
2086
2311
  try {
@@ -2090,14 +2315,13 @@ async function runInit(cwd2, flags) {
2090
2315
  provider,
2091
2316
  docsPath,
2092
2317
  tasksAdapter,
2093
- models: modelOverrides,
2094
2318
  scope: storageScope
2095
2319
  });
2096
2320
  const materializer = getMaterializer(provider);
2097
2321
  const installDir = cwd2;
2098
2322
  configExt = detectConfigExtension(cwd2);
2099
2323
  const configFileName = `agent-harness-kit.config.${configExt}`;
2100
- const templateFn = configExt === "ts" ? configTs : configExt === "mjs" ? configMjs : configCjs;
2324
+ const templateFn = configExt === "json" ? configJson : configExt === "ts" ? configTs : configExt === "mjs" ? configMjs : configCjs;
2101
2325
  const configContent = templateFn({
2102
2326
  name,
2103
2327
  description,
@@ -2105,23 +2329,17 @@ async function runInit(cwd2, flags) {
2105
2329
  docsPath,
2106
2330
  tasksAdapter,
2107
2331
  port: config.tools.mcp.port,
2108
- models: modelOverrides,
2109
2332
  scope: config.storage.scope,
2110
2333
  projectId: config.storage.projectId
2111
2334
  });
2112
- writeFileSync8(join14(installDir, configFileName), configContent, "utf8");
2113
- mkdirSync7(join14(installDir, config.storage.dir), { recursive: true });
2335
+ writeFileSync7(join15(installDir, configFileName), configContent, "utf8");
2336
+ mkdirSync6(join15(installDir, config.storage.dir), { recursive: true });
2114
2337
  const db = await openDB(config, installDir);
2115
2338
  await db.writeStorageState(installDir);
2116
- await materializer.scaffold(config, { cwd: installDir, firstTask });
2117
- if (firstTask) {
2118
- const slug = slugify(firstTask.title);
2119
- await db.addTask({
2120
- slug,
2121
- title: firstTask.title,
2122
- description: firstTask.description,
2123
- acceptance: firstTask.acceptance
2124
- });
2339
+ await materializer.scaffold(config, { cwd: installDir, firstTask, claudeAgentModels });
2340
+ const { parseFailed } = await reconcileFeatureList(db, installDir, config.storage.dir, firstTask);
2341
+ if (parseFailed) {
2342
+ featureListParseFailedPath = join15(config.storage.dir, "feature_list.json");
2125
2343
  }
2126
2344
  await db.close();
2127
2345
  spinner6.stop("");
@@ -2130,39 +2348,44 @@ async function runInit(cwd2, flags) {
2130
2348
  p3.log.error(err instanceof Error ? err.message : String(err));
2131
2349
  throw err;
2132
2350
  }
2133
- console.log(pc7.green("\u2713 Scaffolded harness in current directory"));
2351
+ if (featureListParseFailedPath) {
2352
+ console.log(
2353
+ pc8.yellow("\u26A0") + " Existing " + pc8.bold(featureListParseFailedPath) + " is not valid JSON \u2014 left untouched. Fix it and run `ahk sync`."
2354
+ );
2355
+ }
2356
+ console.log(pc8.green("\u2713 Scaffolded harness in current directory"));
2134
2357
  const agentsDir = provider === "claude-code" ? ".claude/agents/" : ".opencode/agents/";
2135
2358
  const mcpFile = provider === "claude-code" ? ".claude/mcp.json" : "./opencode.json";
2136
2359
  console.log("");
2137
- console.log(pc7.green(`\u2713 agent-harness-kit.config.${configExt}`));
2138
- console.log(pc7.green("\u2713 AGENTS.md"));
2139
- console.log(pc7.green("\u2713 health.sh"));
2360
+ console.log(pc8.green(`\u2713 agent-harness-kit.config.${configExt}`));
2361
+ console.log(pc8.green("\u2713 AGENTS.md"));
2362
+ console.log(pc8.green("\u2713 health.sh"));
2140
2363
  console.log(
2141
- pc7.green(
2364
+ pc8.green(
2142
2365
  storageScope === "global" ? "\u2713 ~/.harness/dbs/<projectId>/harness.db" : "\u2713 .harness/harness.db"
2143
2366
  )
2144
2367
  );
2145
2368
  console.log(
2146
- pc7.green(
2369
+ pc8.green(
2147
2370
  storageScope === "global" ? "\u2713 ~/.harness/dbs/<projectId>/current.md" : "\u2713 .harness/current.md"
2148
2371
  )
2149
2372
  );
2150
- console.log(pc7.green("\u2713 .harness/storage-state.json"));
2151
- console.log(pc7.green(`\u2713 ${agentsDir}lead.md`));
2152
- console.log(pc7.green(`\u2713 ${agentsDir}explorer.md`));
2153
- console.log(pc7.green(`\u2713 ${agentsDir}builder.md`));
2154
- console.log(pc7.green(`\u2713 ${agentsDir}reviewer.md`));
2155
- console.log(pc7.green(`\u2713 ${mcpFile}`));
2156
- console.log(pc7.green("\u2713 .gitignore entries added"));
2373
+ console.log(pc8.green("\u2713 .harness/storage-state.json"));
2374
+ console.log(pc8.green(`\u2713 ${agentsDir}lead.md`));
2375
+ console.log(pc8.green(`\u2713 ${agentsDir}explorer.md`));
2376
+ console.log(pc8.green(`\u2713 ${agentsDir}builder.md`));
2377
+ console.log(pc8.green(`\u2713 ${agentsDir}reviewer.md`));
2378
+ console.log(pc8.green(`\u2713 ${mcpFile}`));
2379
+ console.log(pc8.green("\u2713 .gitignore entries added"));
2157
2380
  console.log("");
2158
- console.log(pc7.cyan("\u2192") + ` Edit ${pc7.cyan("health.sh")} with your project checks`);
2159
- console.log(pc7.cyan("\u2192") + ` ${pc7.cyan("ahk task add")} to queue work for agents`);
2381
+ console.log(pc8.cyan("\u2192") + ` Edit ${pc8.cyan("health.sh")} with your project checks`);
2382
+ console.log(pc8.cyan("\u2192") + ` ${pc8.cyan("ahk task add")} to queue work for agents`);
2160
2383
  console.log(
2161
- pc7.cyan("\u2192") + ` Enrich your docs with knowledge graphs: ${pc7.cyan("https://github.com/safishamsi/graphify")}`
2384
+ pc8.cyan("\u2192") + ` Enrich your docs with knowledge graphs: ${pc8.cyan("https://github.com/safishamsi/graphify")}`
2162
2385
  );
2163
2386
  const recommendations = [
2164
2387
  ` Give a try to Heimdall MCP: Transparent proxy that traces every MCP tool call with OpenTelemetry. `,
2165
- ` Learn more: ${pc7.cyan("https://github.com/enmanuelmag/heimdall-mcp")} `
2388
+ ` Learn more: ${pc8.cyan("https://github.com/enmanuelmag/heimdall-mcp")} `
2166
2389
  ];
2167
2390
  console.log("");
2168
2391
  drawBox(recommendations);
@@ -2170,7 +2393,7 @@ async function runInit(cwd2, flags) {
2170
2393
 
2171
2394
  // src/commands/migrate.ts
2172
2395
  import * as p4 from "@clack/prompts";
2173
- import pc8 from "picocolors";
2396
+ import pc9 from "picocolors";
2174
2397
  async function runMigrate(cwd2, opts) {
2175
2398
  const config = await loadConfig(cwd2);
2176
2399
  let target;
@@ -2192,7 +2415,7 @@ async function runMigrate(cwd2, opts) {
2192
2415
  target = val;
2193
2416
  }
2194
2417
  if (target === config.provider) {
2195
- console.log(pc8.dim(`Already on ${target} \u2014 nothing to migrate.`));
2418
+ console.log(pc9.dim(`Already on ${target} \u2014 nothing to migrate.`));
2196
2419
  return;
2197
2420
  }
2198
2421
  const spinner6 = p4.spinner();
@@ -2200,21 +2423,21 @@ async function runMigrate(cwd2, opts) {
2200
2423
  try {
2201
2424
  const targetMaterializer = getMaterializer(target);
2202
2425
  await targetMaterializer.build(config, cwd2);
2203
- spinner6.stop(pc8.green(`Migrated to ${target}`));
2426
+ spinner6.stop(pc9.green(`Migrated to ${target}`));
2204
2427
  p4.log.warn(`Update agent-harness-kit.config.ts: set provider: '${target}'`);
2205
2428
  p4.log.warn(`Then run: ahk build`);
2206
2429
  } catch (err) {
2207
- spinner6.stop(pc8.red("Migration failed"));
2430
+ spinner6.stop(pc9.red("Migration failed"));
2208
2431
  p4.log.error(err instanceof Error ? err.message : String(err));
2209
2432
  process.exit(1);
2210
2433
  }
2211
2434
  }
2212
2435
 
2213
2436
  // src/commands/migrate-storage.ts
2214
- import { copyFileSync, existsSync as existsSync12, mkdirSync as mkdirSync8, rmSync, writeFileSync as writeFileSync9 } from "fs";
2437
+ import { copyFileSync, existsSync as existsSync14, mkdirSync as mkdirSync7, rmSync, writeFileSync as writeFileSync8 } from "fs";
2215
2438
  import { homedir as homedir3 } from "os";
2216
- import { dirname as dirname7, join as join15, resolve as resolve7 } from "path";
2217
- import pc9 from "picocolors";
2439
+ import { dirname as dirname7, join as join16, resolve as resolve6 } from "path";
2440
+ import pc10 from "picocolors";
2218
2441
  function log5(msg) {
2219
2442
  console.log(msg);
2220
2443
  }
@@ -2225,17 +2448,17 @@ function defaultMarkdownPathForConfig(config) {
2225
2448
  return config.storage.scope === "local" ? config.storage.markdownFallback.path : DEFAULT_MARKDOWN_PATH;
2226
2449
  }
2227
2450
  function currentMdPathForScope(scope, config, cwd2, homeDir) {
2228
- return scope === "global" ? join15(resolveGlobalStorageDir(config, homeDir), "current.md") : resolve7(cwd2, defaultMarkdownPathForConfig(config));
2451
+ return scope === "global" ? join16(resolveGlobalStorageDir(config, homeDir), "current.md") : resolve6(cwd2, defaultMarkdownPathForConfig(config));
2229
2452
  }
2230
2453
  function defaultSqlitePathForConfig(config) {
2231
2454
  return config.storage.scope === "local" && config.database.type === "sqlite" ? config.storage.sqlitePath ?? DEFAULT_SQLITE_PATH : DEFAULT_SQLITE_PATH;
2232
2455
  }
2233
2456
  async function backupDestination(cwd2, storageDir, data) {
2234
- const backupsDir = resolve7(cwd2, storageDir, "backups");
2235
- const path = join15(backupsDir, `pre-migrate-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.json`);
2457
+ const backupsDir = resolve6(cwd2, storageDir, "backups");
2458
+ const path = join16(backupsDir, `pre-migrate-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.json`);
2236
2459
  try {
2237
- mkdirSync8(backupsDir, { recursive: true });
2238
- writeFileSync9(path, JSON.stringify(data, null, 2) + "\n", "utf8");
2460
+ mkdirSync7(backupsDir, { recursive: true });
2461
+ writeFileSync8(path, JSON.stringify(data, null, 2) + "\n", "utf8");
2239
2462
  } catch (err) {
2240
2463
  throw new Error(
2241
2464
  `Could not write destination backup to ${path} (${err instanceof Error ? err.message : String(err)}). Aborting migration WITHOUT touching the destination \u2014 nothing was overwritten.`
@@ -2244,14 +2467,14 @@ async function backupDestination(cwd2, storageDir, data) {
2244
2467
  return path;
2245
2468
  }
2246
2469
  function copySqliteFile(srcPath, destPath) {
2247
- mkdirSync8(dirname7(destPath), { recursive: true });
2470
+ mkdirSync7(dirname7(destPath), { recursive: true });
2248
2471
  copyFileSync(srcPath, destPath);
2249
2472
  for (const suffix of ["-wal", "-shm"]) {
2250
- if (existsSync12(`${srcPath}${suffix}`)) {
2473
+ if (existsSync14(`${srcPath}${suffix}`)) {
2251
2474
  copyFileSync(`${srcPath}${suffix}`, `${destPath}${suffix}`);
2252
2475
  }
2253
2476
  }
2254
- if (!existsSync12(destPath)) {
2477
+ if (!existsSync14(destPath)) {
2255
2478
  throw new Error(`Copy verification failed: ${destPath} does not exist after copy.`);
2256
2479
  }
2257
2480
  }
@@ -2279,13 +2502,13 @@ async function runMigrateStorage(cwd2, opts, homeDir = homedir3()) {
2279
2502
  } finally {
2280
2503
  await db.close();
2281
2504
  }
2282
- log5(pc9.dim("storage-state.json was missing; no data found at either candidate location. Nothing to migrate \u2014 state recorded."));
2505
+ log5(pc10.dim("storage-state.json was missing; no data found at either candidate location. Nothing to migrate \u2014 state recorded."));
2283
2506
  return;
2284
2507
  }
2285
2508
  realScope = localCount > 0 ? "local" : "global";
2286
2509
  realDbType = "sqlite";
2287
2510
  log5(
2288
- pc9.yellow(
2511
+ pc10.yellow(
2289
2512
  `storage-state.json was missing. Detected real data at ${realScope} sqlite location (${realScope === "local" ? localPath : globalPath}) \u2014 using it as the migration source.`
2290
2513
  )
2291
2514
  );
@@ -2296,7 +2519,7 @@ async function runMigrateStorage(cwd2, opts, homeDir = homedir3()) {
2296
2519
  const desiredScope = config.storage.scope;
2297
2520
  const desiredDbType = config.database.type;
2298
2521
  if (realScope === desiredScope && realDbType === desiredDbType) {
2299
- log5(pc9.green(`\u2713 Storage already matches config (scope=${desiredScope}, database=${desiredDbType}) \u2014 nothing to migrate.`));
2522
+ log5(pc10.green(`\u2713 Storage already matches config (scope=${desiredScope}, database=${desiredDbType}) \u2014 nothing to migrate.`));
2300
2523
  return;
2301
2524
  }
2302
2525
  if (realDbType !== "sqlite") {
@@ -2310,8 +2533,8 @@ async function runMigrateStorage(cwd2, opts, homeDir = homedir3()) {
2310
2533
  return migrateAcrossDbType(cwd2, config, homeDir, realScope, opts);
2311
2534
  }
2312
2535
  async function probeTaskCount(dbPath) {
2313
- if (!existsSync12(dbPath)) return 0;
2314
- const { SQLiteDriver } = await import("./sqlite-TR4D324R.js");
2536
+ if (!existsSync14(dbPath)) return 0;
2537
+ const { SQLiteDriver } = await import("./sqlite-5OWKTUUZ.js");
2315
2538
  const driver = new SQLiteDriver(dbPath);
2316
2539
  try {
2317
2540
  await driver.ensureSchema();
@@ -2327,12 +2550,12 @@ async function migrateScopeOnly(cwd2, config, homeDir, fromScope, toScope, opts)
2327
2550
  const destDb = resolveSqlitePathForScope(toScope, sqlitePath, cwd2, config, homeDir);
2328
2551
  const srcMd = currentMdPathForScope(fromScope, config, cwd2, homeDir);
2329
2552
  const destMd = currentMdPathForScope(toScope, config, cwd2, homeDir);
2330
- if (!existsSync12(srcDb)) {
2553
+ if (!existsSync14(srcDb)) {
2331
2554
  fail(`Source database not found at ${srcDb} (expected ${fromScope} scope) \u2014 nothing to move.`);
2332
2555
  }
2333
- const destExists = existsSync12(destDb);
2556
+ const destExists = existsSync14(destDb);
2334
2557
  if (destExists) {
2335
- const { SQLiteDriver } = await import("./sqlite-TR4D324R.js");
2558
+ const { SQLiteDriver } = await import("./sqlite-5OWKTUUZ.js");
2336
2559
  const destDriver = new SQLiteDriver(destDb);
2337
2560
  let destEmpty;
2338
2561
  try {
@@ -2347,58 +2570,58 @@ async function migrateScopeOnly(cwd2, config, homeDir, fromScope, toScope, opts)
2347
2570
  );
2348
2571
  }
2349
2572
  if (!destEmpty && opts.force) {
2350
- const { SQLiteDriver: Driver } = await import("./sqlite-TR4D324R.js");
2573
+ const { SQLiteDriver: Driver } = await import("./sqlite-5OWKTUUZ.js");
2351
2574
  const backupDriver = new Driver(destDb);
2352
2575
  let data;
2353
2576
  try {
2354
2577
  await backupDriver.ensureSchema();
2355
- const { HarnessDB } = await import("./db-3OXHRFAR.js");
2578
+ const { HarnessDB } = await import("./db-L3AADJF5.js");
2356
2579
  const tmpDb = new HarnessDB(backupDriver, config, homeDir);
2357
2580
  data = await tmpDb.exportJson();
2358
2581
  } finally {
2359
2582
  await backupDriver.close();
2360
2583
  }
2361
2584
  const backupPath = await backupDestination(cwd2, config.storage.dir, data);
2362
- log5(pc9.yellow(` Backed up existing destination data \u2192 ${backupPath}`));
2585
+ log5(pc10.yellow(` Backed up existing destination data \u2192 ${backupPath}`));
2363
2586
  }
2364
2587
  }
2365
2588
  if (opts.dryRun) {
2366
- log5(pc9.dim(`[dry-run] Would copy ${srcDb} \u2192 ${destDb} (scope ${fromScope} \u2192 ${toScope}), and move current.md.`));
2589
+ log5(pc10.dim(`[dry-run] Would copy ${srcDb} \u2192 ${destDb} (scope ${fromScope} \u2192 ${toScope}), and move current.md.`));
2367
2590
  return;
2368
2591
  }
2369
2592
  copySqliteFile(srcDb, destDb);
2370
- log5(pc9.green(`\u2713 Copied database ${srcDb} \u2192 ${destDb}`));
2371
- if (existsSync12(srcMd)) {
2372
- mkdirSync8(dirname7(destMd), { recursive: true });
2593
+ log5(pc10.green(`\u2713 Copied database ${srcDb} \u2192 ${destDb}`));
2594
+ if (existsSync14(srcMd)) {
2595
+ mkdirSync7(dirname7(destMd), { recursive: true });
2373
2596
  copyFileSync(srcMd, destMd);
2374
- log5(pc9.green(`\u2713 Copied current.md ${srcMd} \u2192 ${destMd}`));
2597
+ log5(pc10.green(`\u2713 Copied current.md ${srcMd} \u2192 ${destMd}`));
2375
2598
  }
2376
2599
  rmSync(srcDb, { force: true });
2377
2600
  rmSync(`${srcDb}-wal`, { force: true });
2378
2601
  rmSync(`${srcDb}-shm`, { force: true });
2379
- if (existsSync12(srcMd) && srcMd !== destMd) rmSync(srcMd, { force: true });
2602
+ if (existsSync14(srcMd) && srcMd !== destMd) rmSync(srcMd, { force: true });
2380
2603
  const db = await openDB(config, cwd2, homeDir);
2381
2604
  try {
2382
2605
  await db.writeStorageState(cwd2);
2383
2606
  } finally {
2384
2607
  await db.close();
2385
2608
  }
2386
- log5(pc9.green(`\u2713 Storage migrated: scope ${fromScope} \u2192 ${toScope}`));
2609
+ log5(pc10.green(`\u2713 Storage migrated: scope ${fromScope} \u2192 ${toScope}`));
2387
2610
  }
2388
2611
  async function migrateAcrossDbType(cwd2, config, homeDir, sourceScope, opts) {
2389
2612
  const sqlitePath = defaultSqlitePathForConfig(config);
2390
2613
  const srcPath = resolveSqlitePathForScope(sourceScope, sqlitePath, cwd2, config, homeDir);
2391
- if (!existsSync12(srcPath)) {
2614
+ if (!existsSync14(srcPath)) {
2392
2615
  fail(`Source sqlite database not found at ${srcPath} (expected ${sourceScope} scope) \u2014 nothing to migrate.`);
2393
2616
  }
2394
- const { SQLiteDriver } = await import("./sqlite-TR4D324R.js");
2617
+ const { SQLiteDriver } = await import("./sqlite-5OWKTUUZ.js");
2395
2618
  const srcDriver = new SQLiteDriver(srcPath);
2396
2619
  let sourceData;
2397
2620
  let sourceCounts;
2398
2621
  try {
2399
2622
  await srcDriver.ensureSchema();
2400
2623
  sourceCounts = await getRowCounts(srcDriver);
2401
- const { HarnessDB } = await import("./db-3OXHRFAR.js");
2624
+ const { HarnessDB } = await import("./db-L3AADJF5.js");
2402
2625
  const srcDb = new HarnessDB(srcDriver, config, homeDir);
2403
2626
  sourceData = await srcDb.exportJson();
2404
2627
  } finally {
@@ -2424,7 +2647,7 @@ async function migrateAcrossDbType(cwd2, config, homeDir, sourceScope, opts) {
2424
2647
  }
2425
2648
  if (opts.dryRun) {
2426
2649
  log5(
2427
- pc9.dim(
2650
+ pc10.dim(
2428
2651
  `[dry-run] Would migrate ${sourceCounts.tasks} task(s) from sqlite (${sourceScope}, ${srcPath}) to ${config.database.type}${destEmpty ? "" : " (destination has data \u2014 would back up first, then overwrite)"}.`
2429
2652
  )
2430
2653
  );
@@ -2434,34 +2657,34 @@ async function migrateAcrossDbType(cwd2, config, homeDir, sourceScope, opts) {
2434
2657
  if (!destEmpty && opts.force) {
2435
2658
  const currentDestData = await destDb.exportJson();
2436
2659
  backupPath = await backupDestination(cwd2, config.storage.dir, currentDestData);
2437
- log5(pc9.yellow(` Backed up existing destination data \u2192 ${backupPath}`));
2660
+ log5(pc10.yellow(` Backed up existing destination data \u2192 ${backupPath}`));
2438
2661
  }
2439
2662
  await destDb.importFullExport(sourceData, config.database.type, { truncateFirst: !destEmpty });
2440
2663
  await destDb.writeStorageState(cwd2);
2441
2664
  log5(
2442
- pc9.green(
2665
+ pc10.green(
2443
2666
  `\u2713 Migrated ${sourceData.tasks.length} task(s), ${sourceData.actions.length} action(s) from sqlite (${sourceScope}) \u2192 ${config.database.type}.`
2444
2667
  )
2445
2668
  );
2446
- if (backupPath) log5(pc9.dim(` Destination backup: ${backupPath}`));
2447
- log5(pc9.yellow(` Note: the original sqlite file at ${srcPath} was NOT deleted \u2014 remove it manually once you've verified the migration.`));
2669
+ if (backupPath) log5(pc10.dim(` Destination backup: ${backupPath}`));
2670
+ log5(pc10.yellow(` Note: the original sqlite file at ${srcPath} was NOT deleted \u2014 remove it manually once you've verified the migration.`));
2448
2671
  } finally {
2449
2672
  await destDb.close();
2450
2673
  }
2451
2674
  }
2452
2675
 
2453
2676
  // src/commands/reset.ts
2454
- import { existsSync as existsSync13, readdirSync, rmSync as rmSync2 } from "fs";
2677
+ import { existsSync as existsSync15, readdirSync, rmSync as rmSync2 } from "fs";
2455
2678
  import { homedir as homedir4 } from "os";
2456
- import { join as join16, resolve as resolve8 } from "path";
2679
+ import { join as join17, resolve as resolve7 } from "path";
2457
2680
  import * as p5 from "@clack/prompts";
2458
- import pc10 from "picocolors";
2681
+ import pc11 from "picocolors";
2459
2682
  var AGENT_MD_FILES = ["lead", "explorer", "consultant", "builder", "reviewer"];
2460
2683
  async function resetAgentMds(cwd2, provider) {
2461
2684
  const agentDir = provider === "claude-code" ? ".claude/agents" : ".opencode/agents";
2462
- const agentDirPath = resolve8(cwd2, agentDir);
2463
- if (!existsSync13(agentDirPath)) {
2464
- console.log(pc10.yellow(` Skipping agent files \u2014 directory not found: ${agentDirPath}`));
2685
+ const agentDirPath = resolve7(cwd2, agentDir);
2686
+ if (!existsSync15(agentDirPath)) {
2687
+ console.log(pc11.yellow(` Skipping agent files \u2014 directory not found: ${agentDirPath}`));
2465
2688
  return;
2466
2689
  }
2467
2690
  const existingFiles = [];
@@ -2473,11 +2696,11 @@ async function resetAgentMds(cwd2, provider) {
2473
2696
  }
2474
2697
  }
2475
2698
  } catch {
2476
- console.log(pc10.yellow(` Skipping agent files \u2014 ${agentDirPath} is not readable`));
2699
+ console.log(pc11.yellow(` Skipping agent files \u2014 ${agentDirPath} is not readable`));
2477
2700
  return;
2478
2701
  }
2479
2702
  if (existingFiles.length === 0) {
2480
- console.log(pc10.yellow(` No agent MD files found in ${agentDir}/`));
2703
+ console.log(pc11.yellow(` No agent MD files found in ${agentDir}/`));
2481
2704
  return;
2482
2705
  }
2483
2706
  for (const file of existingFiles) {
@@ -2486,19 +2709,19 @@ async function resetAgentMds(cwd2, provider) {
2486
2709
  initialValue: true
2487
2710
  });
2488
2711
  if (p5.isCancel(confirm3)) {
2489
- console.log(pc10.red(" Cancelled by user."));
2712
+ console.log(pc11.red(" Cancelled by user."));
2490
2713
  return;
2491
2714
  }
2492
2715
  if (confirm3) {
2493
2716
  try {
2494
- const filePath = join16(agentDirPath, file);
2717
+ const filePath = join17(agentDirPath, file);
2495
2718
  rmSync2(filePath, { force: true });
2496
- console.log(pc10.green(` Removed ${file}`));
2719
+ console.log(pc11.green(` Removed ${file}`));
2497
2720
  } catch {
2498
- console.error(pc10.red(` Failed to remove ${file}`));
2721
+ console.error(pc11.red(` Failed to remove ${file}`));
2499
2722
  }
2500
2723
  } else {
2501
- console.log(pc10.cyan(` Skipped ${file}`));
2724
+ console.log(pc11.cyan(` Skipped ${file}`));
2502
2725
  }
2503
2726
  }
2504
2727
  }
@@ -2507,21 +2730,21 @@ async function runReset(cwd2, opts) {
2507
2730
  try {
2508
2731
  config = await loadConfig(cwd2);
2509
2732
  } catch {
2510
- console.error(pc10.red("\u2717 No agent-harness-kit.config found. Run: ahk init"));
2733
+ console.error(pc11.red("\u2717 No agent-harness-kit.config found. Run: ahk init"));
2511
2734
  process.exit(1);
2512
2735
  }
2513
2736
  const storageDir = config.storage.dir || ".harness";
2514
2737
  const dbPath = config.database.type === "sqlite" ? resolveSqlitePath(config, cwd2, homedir4()) : null;
2515
- const featureListPath = resolve8(cwd2, storageDir, "feature_list.json");
2738
+ const featureListPath = resolve7(cwd2, storageDir, "feature_list.json");
2516
2739
  let resetDb = false;
2517
2740
  let resetFeatureList = false;
2518
2741
  let resetAgentMdsFlag = false;
2519
- if (dbPath && existsSync13(dbPath)) {
2742
+ if (dbPath && existsSync15(dbPath)) {
2520
2743
  if (opts.force) {
2521
2744
  resetDb = true;
2522
2745
  } else {
2523
2746
  if (config.database.type !== "sqlite") {
2524
- console.log(pc10.yellow(` Skipping DB reset \u2014 database type "${config.database.type}" is not managed by this command.`));
2747
+ console.log(pc11.yellow(` Skipping DB reset \u2014 database type "${config.database.type}" is not managed by this command.`));
2525
2748
  resetDb = false;
2526
2749
  } else {
2527
2750
  const confirm3 = await p5.confirm({
@@ -2529,16 +2752,16 @@ async function runReset(cwd2, opts) {
2529
2752
  initialValue: true
2530
2753
  });
2531
2754
  if (p5.isCancel(confirm3)) {
2532
- console.log(pc10.red(" Cancelled by user."));
2755
+ console.log(pc11.red(" Cancelled by user."));
2533
2756
  return;
2534
2757
  }
2535
2758
  resetDb = confirm3;
2536
2759
  }
2537
2760
  }
2538
2761
  } else if (!dbPath) {
2539
- console.log(pc10.dim(` Skipping DB reset \u2014 remote ${config.database.type} database is not managed by this command.`));
2762
+ console.log(pc11.dim(` Skipping DB reset \u2014 remote ${config.database.type} database is not managed by this command.`));
2540
2763
  }
2541
- if (existsSync13(featureListPath)) {
2764
+ if (existsSync15(featureListPath)) {
2542
2765
  if (opts.force) {
2543
2766
  resetFeatureList = true;
2544
2767
  } else {
@@ -2547,7 +2770,7 @@ async function runReset(cwd2, opts) {
2547
2770
  initialValue: true
2548
2771
  });
2549
2772
  if (p5.isCancel(confirm3)) {
2550
- console.log(pc10.red(" Cancelled by user."));
2773
+ console.log(pc11.red(" Cancelled by user."));
2551
2774
  return;
2552
2775
  }
2553
2776
  resetFeatureList = confirm3;
@@ -2561,17 +2784,17 @@ async function runReset(cwd2, opts) {
2561
2784
  rmSync2(dbPath, { force: true });
2562
2785
  rmSync2(`${dbPath}-wal`, { force: true });
2563
2786
  rmSync2(`${dbPath}-shm`, { force: true });
2564
- console.log(pc10.green(` \u2713 Removed ${dbPath}`));
2787
+ console.log(pc11.green(` \u2713 Removed ${dbPath}`));
2565
2788
  } catch {
2566
- console.error(pc10.red(` \u2717 Failed to remove ${dbPath}`));
2789
+ console.error(pc11.red(` \u2717 Failed to remove ${dbPath}`));
2567
2790
  }
2568
2791
  }
2569
2792
  if (resetFeatureList) {
2570
2793
  try {
2571
2794
  rmSync2(featureListPath, { force: true });
2572
- console.log(pc10.green(` \u2713 Removed ${storageDir}/feature_list.json`));
2795
+ console.log(pc11.green(` \u2713 Removed ${storageDir}/feature_list.json`));
2573
2796
  } catch {
2574
- console.error(pc10.red(` \u2717 Failed to remove ${featureListPath}`));
2797
+ console.error(pc11.red(` \u2717 Failed to remove ${featureListPath}`));
2575
2798
  }
2576
2799
  }
2577
2800
  if (resetAgentMdsFlag) {
@@ -2579,16 +2802,16 @@ async function runReset(cwd2, opts) {
2579
2802
  await resetAgentMds(cwd2, opts.provider || "claude-code");
2580
2803
  }
2581
2804
  if (!resetDb && !resetFeatureList && !resetAgentMdsFlag) {
2582
- console.log(pc10.yellow(" Nothing to reset (all items missing or skipped)."));
2805
+ console.log(pc11.yellow(" Nothing to reset (all items missing or skipped)."));
2583
2806
  return;
2584
2807
  }
2585
2808
  console.log("");
2586
- console.log(pc10.green('\u2713 Reset complete. Run "ahk init" to scaffold a fresh harness.'));
2809
+ console.log(pc11.green('\u2713 Reset complete. Run "ahk init" to scaffold a fresh harness.'));
2587
2810
  }
2588
2811
 
2589
2812
  // src/core/mcp-server.ts
2590
- import { existsSync as existsSync15, mkdirSync as mkdirSync9, readdirSync as readdirSync2, readFileSync as readFileSync10, statSync, writeFileSync as writeFileSync10 } from "fs";
2591
- import { join as join18, resolve as resolve9 } from "path";
2813
+ import { existsSync as existsSync17, mkdirSync as mkdirSync8, readdirSync as readdirSync2, readFileSync as readFileSync10, statSync, writeFileSync as writeFileSync9 } from "fs";
2814
+ import { join as join19, resolve as resolve8 } from "path";
2592
2815
  import { Server } from "@modelcontextprotocol/sdk/server";
2593
2816
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
2594
2817
  import {
@@ -2597,45 +2820,20 @@ import {
2597
2820
  } from "@modelcontextprotocol/sdk/types.js";
2598
2821
 
2599
2822
  // src/core/permissions-check.ts
2600
- import { existsSync as existsSync14, readFileSync as readFileSync9 } from "fs";
2601
- import { join as join17 } from "path";
2602
- var CANONICAL = {
2603
- lead: [...MCP_CLAUDE_PERMISSIONS_LEAD],
2604
- explorer: [...MCP_CLAUDE_PERMISSIONS_EXPLORER],
2605
- consultant: [...MCP_CLAUDE_PERMISSIONS_CONSULTANT],
2606
- builder: [...MCP_CLAUDE_PERMISSIONS_BUILDER],
2607
- reviewer: [...MCP_CLAUDE_PERMISSIONS_REVIEWER]
2608
- };
2609
- function parseToolsFromFrontmatter(content) {
2610
- const match = content.match(/^---\n([\s\S]*?)\n---/m);
2611
- if (!match) return [];
2612
- const fm = match[1];
2613
- const toolsMatch = fm.match(/^tools:\n((?: - [^\n]+\n?)*)/m);
2614
- if (!toolsMatch) return [];
2615
- return toolsMatch[1].split("\n").map((l) => l.trim().replace(/^- /, "")).filter((l) => l.startsWith("mcp__"));
2616
- }
2823
+ import { existsSync as existsSync16 } from "fs";
2824
+ import { join as join18 } from "path";
2825
+ var AGENTS = ["lead", "explorer", "consultant", "builder", "reviewer"];
2617
2826
  function checkPermissionsSync(cwd2, config) {
2618
2827
  if (config.provider !== "claude-code") {
2619
2828
  return { in_sync: true };
2620
2829
  }
2621
2830
  const agents = {};
2622
2831
  let in_sync = true;
2623
- for (const agent of ["lead", "explorer", "consultant", "builder", "reviewer"]) {
2624
- const filePath = join17(cwd2, ".claude", "agents", `${agent}.md`);
2625
- if (!existsSync14(filePath)) {
2626
- const missing2 = CANONICAL[agent];
2627
- agents[agent] = { ok: false, missing: missing2, extra: [] };
2628
- in_sync = false;
2629
- continue;
2630
- }
2631
- const content = readFileSync9(filePath, "utf-8");
2632
- const installed = parseToolsFromFrontmatter(content);
2633
- const canonical = CANONICAL[agent];
2634
- const missing = canonical.filter((t) => !installed.includes(t));
2635
- const extra = installed.filter((t) => !canonical.includes(t));
2636
- const ok3 = missing.length === 0 && extra.length === 0;
2637
- if (!ok3) in_sync = false;
2638
- agents[agent] = { ok: ok3, missing, extra };
2832
+ for (const agent of AGENTS) {
2833
+ const filePath = join18(cwd2, ".claude", "agents", `${agent}.md`);
2834
+ const exists = existsSync16(filePath);
2835
+ if (!exists) in_sync = false;
2836
+ agents[agent] = exists ? { ok: true } : { ok: false, reason: "missing_file" };
2639
2837
  }
2640
2838
  return { in_sync, agents };
2641
2839
  }
@@ -2645,7 +2843,7 @@ var VERSION = "0.1.0";
2645
2843
  var TOOLS = [
2646
2844
  {
2647
2845
  name: "actions.start",
2648
- description: "Start a new action for a task. Returns an actionId (UUID).",
2846
+ description: "Start a new action for a task. Returns an actionId.",
2649
2847
  inputSchema: {
2650
2848
  type: "object",
2651
2849
  properties: {
@@ -2660,18 +2858,18 @@ var TOOLS = [
2660
2858
  },
2661
2859
  {
2662
2860
  name: "actions.write",
2663
- 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.",
2861
+ description: "Record a section in an action. Standard sections: result, tools_used, blockers, next_steps.",
2664
2862
  inputSchema: {
2665
2863
  type: "object",
2666
2864
  properties: {
2667
- actionId: { type: "string", description: "UUID returned by actions.start" },
2865
+ actionId: { type: "number", description: "The actionId returned by actions.start" },
2668
2866
  sectionType: {
2669
2867
  type: "string",
2670
2868
  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."
2671
2869
  },
2672
2870
  content: {
2673
2871
  type: "string",
2674
- 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."
2872
+ description: "Content for this section. No length limit; avoid padding \u2014 it costs shared context for other agents."
2675
2873
  }
2676
2874
  },
2677
2875
  required: ["actionId", "sectionType", "content"]
@@ -2683,7 +2881,7 @@ var TOOLS = [
2683
2881
  inputSchema: {
2684
2882
  type: "object",
2685
2883
  properties: {
2686
- actionId: { type: "string", description: "UUID of the action to close" },
2884
+ actionId: { type: "number", description: "The actionId of the action to close" },
2687
2885
  summary: { type: "string", description: "One-line summary of what was done" }
2688
2886
  },
2689
2887
  required: ["actionId", "summary"]
@@ -2758,20 +2956,31 @@ var TOOLS = [
2758
2956
  },
2759
2957
  {
2760
2958
  name: "actions.record_file",
2761
- 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.",
2959
+ 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.",
2762
2960
  inputSchema: {
2763
2961
  type: "object",
2764
2962
  properties: {
2765
- actionId: { type: "string", description: "UUID returned by actions.start" },
2766
- filePath: { type: "string", description: "Absolute or repo-relative path of the file" },
2767
- operation: {
2768
- type: "string",
2769
- enum: ["read", "created", "modified", "deleted"],
2770
- description: "What was done to the file"
2771
- },
2772
- notes: { type: "string", description: "Optional short note about the change" }
2963
+ actionId: { type: "number", description: "The actionId returned by actions.start" },
2964
+ files: {
2965
+ type: "array",
2966
+ minItems: 1,
2967
+ description: "Files touched, recorded atomically in one transaction.",
2968
+ items: {
2969
+ type: "object",
2970
+ properties: {
2971
+ filePath: { type: "string", description: "Absolute or repo-relative path of the file" },
2972
+ operation: {
2973
+ type: "string",
2974
+ enum: ["read", "created", "modified", "deleted"],
2975
+ description: "What was done to the file"
2976
+ },
2977
+ notes: { type: "string", description: "Optional short note about the change" }
2978
+ },
2979
+ required: ["filePath", "operation"]
2980
+ }
2981
+ }
2773
2982
  },
2774
- required: ["actionId", "filePath", "operation"]
2983
+ required: ["actionId", "files"]
2775
2984
  }
2776
2985
  },
2777
2986
  {
@@ -2812,12 +3021,12 @@ var TOOLS = [
2812
3021
  },
2813
3022
  description: {
2814
3023
  type: "string",
2815
- 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."
3024
+ description: "Longer description of the task goal. No length limit; avoid padding \u2014 it costs shared context for other agents."
2816
3025
  },
2817
3026
  acceptance: {
2818
3027
  type: "array",
2819
3028
  items: { type: "string" },
2820
- 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."
3029
+ description: "List of acceptance criteria (plain sentences). No length limit; avoid padding \u2014 it costs shared context for other agents."
2821
3030
  }
2822
3031
  },
2823
3032
  required: ["title"]
@@ -2825,22 +3034,36 @@ var TOOLS = [
2825
3034
  },
2826
3035
  {
2827
3036
  name: "actions.record_tool",
2828
- description: "Record a tool call made during an action. This is the only way to populate the Tools dashboard. Call once per tool invocation.",
3037
+ 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.",
2829
3038
  inputSchema: {
2830
3039
  type: "object",
2831
3040
  properties: {
2832
- actionId: { type: "string", description: "UUID returned by actions.start" },
2833
- toolName: {
2834
- type: "string",
2835
- description: "Name of the tool that was called (e.g. Read, Bash, Edit)"
2836
- },
2837
- argsJson: {
2838
- type: "string",
2839
- description: "Optional JSON string of the arguments passed to the tool"
2840
- },
2841
- resultSummary: { type: "string", description: "Optional short summary of the tool result" }
3041
+ actionId: { type: "number", description: "The actionId returned by actions.start" },
3042
+ calls: {
3043
+ type: "array",
3044
+ minItems: 1,
3045
+ description: "Tool calls made, recorded atomically in one transaction.",
3046
+ items: {
3047
+ type: "object",
3048
+ properties: {
3049
+ toolName: {
3050
+ type: "string",
3051
+ description: "Name of the tool that was called (e.g. Read, Bash, Edit)"
3052
+ },
3053
+ argsJson: {
3054
+ type: "string",
3055
+ description: "Optional JSON string of the arguments passed to the tool"
3056
+ },
3057
+ resultSummary: {
3058
+ type: "string",
3059
+ description: "Optional short summary of the tool result"
3060
+ }
3061
+ },
3062
+ required: ["toolName"]
3063
+ }
3064
+ }
2842
3065
  },
2843
- required: ["actionId", "toolName"]
3066
+ required: ["actionId", "calls"]
2844
3067
  }
2845
3068
  },
2846
3069
  {
@@ -2885,7 +3108,7 @@ var TOOLS = [
2885
3108
  },
2886
3109
  {
2887
3110
  name: "permissions.check",
2888
- description: "Check whether the .claude/agents/*.md tool permission lists are in sync with the current canonical permission constants. Returns per-agent diff with missing and extra tools. Call this at session start to detect outdated agent files after an ahk upgrade.",
3111
+ description: 'Check that a .claude/agents/*.md definition file exists for every role. Returns { in_sync, agents } where each agent is { ok } or { ok: false, reason: "missing_file" }. Agent file CONTENTS are never inspected \u2014 they are meant to be customised freely \u2014 so this never reports drift, only absence. Run `ahk build` to restore a missing file.',
2889
3112
  inputSchema: { type: "object", properties: {}, required: [] }
2890
3113
  },
2891
3114
  {
@@ -2906,7 +3129,7 @@ var TOOLS = [
2906
3129
  ];
2907
3130
  async function startMcpServer(config, cwd2) {
2908
3131
  const db = await openDB(config, cwd2);
2909
- const docsPath = resolve9(cwd2, config.project.docsPath);
3132
+ const docsPath = resolve8(cwd2, config.project.docsPath);
2910
3133
  const server = new Server(
2911
3134
  { name: "agent-harness-kit", version: VERSION },
2912
3135
  { capabilities: { tools: {} } }
@@ -2931,39 +3154,50 @@ async function dispatch(name, args, db, docsPath, cwd2, config) {
2931
3154
  const taskId = num(args, "taskId");
2932
3155
  const agent = str(args, "agent");
2933
3156
  const action = await db.startAction(taskId, agent);
2934
- return ok2(JSON.stringify({ actionId: action.id, taskId, agent, status: "in_progress" }));
3157
+ return ok2(JSON.stringify({ actionId: action.id }));
2935
3158
  }
2936
3159
  case "actions.write": {
2937
- const actionId = str(args, "actionId");
3160
+ const actionId = num(args, "actionId");
2938
3161
  const sectionType = str(args, "sectionType");
2939
3162
  const content = str(args, "content");
2940
3163
  await db.writeSection(actionId, sectionType, content);
2941
- return ok2(JSON.stringify({ actionId, sectionType, recorded: true }));
3164
+ return ok2(JSON.stringify({ recorded: true }));
2942
3165
  }
2943
3166
  case "actions.complete": {
2944
- const actionId = str(args, "actionId");
3167
+ const actionId = num(args, "actionId");
2945
3168
  const summary = str(args, "summary");
2946
3169
  const action = await db.completeAction(actionId, summary);
2947
- return ok2(
2948
- JSON.stringify({ actionId, status: action.status, completedAt: action.completed_at })
2949
- );
3170
+ return ok2(JSON.stringify({ status: action.status, completedAt: action.completed_at }));
2950
3171
  }
2951
3172
  case "actions.get": {
2952
3173
  const taskId = num(args, "taskId");
2953
3174
  const actions = await db.getActionsForTask(taskId);
2954
3175
  const full = await Promise.all(
2955
- actions.map(async (a) => ({
2956
- ...a,
2957
- sections: await db.getActionSections(a.id)
2958
- }))
3176
+ actions.map(async (a) => {
3177
+ const sections = await db.getActionSections(a.id);
3178
+ return {
3179
+ id: a.id,
3180
+ agent: a.agent,
3181
+ status: a.status,
3182
+ created_at: a.created_at,
3183
+ completed_at: a.completed_at,
3184
+ summary: a.summary,
3185
+ sections: sections.map((s) => ({
3186
+ id: s.id,
3187
+ section_type: s.section_type,
3188
+ content: s.content,
3189
+ created_at: s.created_at
3190
+ }))
3191
+ };
3192
+ })
2959
3193
  );
2960
- return ok2(JSON.stringify(full, null, 2));
3194
+ return ok2(JSON.stringify(full));
2961
3195
  }
2962
3196
  case "tasks.get": {
2963
3197
  const status = args["status"];
2964
3198
  const includeArchived = args["includeArchived"];
2965
3199
  const tasks = status ? await db.getTasks(status, includeArchived ?? false) : await db.getTasks(void 0, includeArchived ?? false);
2966
- return ok2(JSON.stringify(tasks, null, 2));
3200
+ return ok2(JSON.stringify(tasks));
2967
3201
  }
2968
3202
  case "tasks.claim": {
2969
3203
  const id = num(args, "id");
@@ -2994,15 +3228,17 @@ async function dispatch(name, args, db, docsPath, cwd2, config) {
2994
3228
  case "docs.search": {
2995
3229
  const query = str(args, "query");
2996
3230
  const results = searchDocs(docsPath, query);
2997
- return ok2(JSON.stringify(results, null, 2));
3231
+ return ok2(JSON.stringify(results));
2998
3232
  }
2999
3233
  case "actions.record_file": {
3000
- const actionId = str(args, "actionId");
3001
- const filePath = str(args, "filePath");
3002
- const operation = str(args, "operation");
3003
- const notes = args["notes"];
3004
- await db.recordFile(actionId, filePath, operation, notes);
3005
- return ok2(JSON.stringify({ actionId, filePath, operation, recorded: true }));
3234
+ const actionId = num(args, "actionId");
3235
+ const files = nonEmptyArray(args, "files").map((f) => ({
3236
+ filePath: str(f, "filePath"),
3237
+ operation: str(f, "operation"),
3238
+ notes: f["notes"]
3239
+ }));
3240
+ const recorded = await db.recordFiles(actionId, files);
3241
+ return ok2(JSON.stringify({ recorded }));
3006
3242
  }
3007
3243
  case "tasks.acceptance.update": {
3008
3244
  const criterionId = num(args, "criterionId");
@@ -3012,15 +3248,17 @@ async function dispatch(name, args, db, docsPath, cwd2, config) {
3012
3248
  case "tasks.acceptance.get": {
3013
3249
  const taskId = num(args, "taskId");
3014
3250
  const criteria = await db.getTaskAcceptance(taskId);
3015
- return ok2(JSON.stringify(criteria, null, 2));
3251
+ return ok2(JSON.stringify(criteria));
3016
3252
  }
3017
3253
  case "actions.record_tool": {
3018
- const actionId = str(args, "actionId");
3019
- const toolName = str(args, "toolName");
3020
- const argsJson = args["argsJson"];
3021
- const resultSummary = args["resultSummary"];
3022
- await db.recordTool(actionId, toolName, argsJson, resultSummary);
3023
- return ok2(JSON.stringify({ actionId, toolName, recorded: true }));
3254
+ const actionId = num(args, "actionId");
3255
+ const calls = nonEmptyArray(args, "calls").map((c) => ({
3256
+ toolName: str(c, "toolName"),
3257
+ argsJson: c["argsJson"],
3258
+ resultSummary: c["resultSummary"]
3259
+ }));
3260
+ const recorded = await db.recordTools(actionId, calls);
3261
+ return ok2(JSON.stringify({ recorded }));
3024
3262
  }
3025
3263
  case "tasks.edit": {
3026
3264
  const id = num(args, "id");
@@ -3051,11 +3289,11 @@ async function dispatch(name, args, db, docsPath, cwd2, config) {
3051
3289
  }
3052
3290
  case "permissions.check": {
3053
3291
  const result = checkPermissionsSync(cwd2, config);
3054
- return ok2(JSON.stringify(result, null, 2));
3292
+ return ok2(JSON.stringify(result));
3055
3293
  }
3056
3294
  case "deps.snapshot": {
3057
- const pkgPath2 = join18(cwd2, "package.json");
3058
- if (!existsSync15(pkgPath2)) {
3295
+ const pkgPath2 = join19(cwd2, "package.json");
3296
+ if (!existsSync17(pkgPath2)) {
3059
3297
  return ok2("package.json not found in project root", true);
3060
3298
  }
3061
3299
  const pkg2 = JSON.parse(readFileSync10(pkgPath2, "utf8"));
@@ -3064,9 +3302,9 @@ async function dispatch(name, args, db, docsPath, cwd2, config) {
3064
3302
  dependencies: pkg2.dependencies ?? {},
3065
3303
  devDependencies: pkg2.devDependencies ?? {}
3066
3304
  };
3067
- const harnessDir = join18(cwd2, ".harness");
3068
- mkdirSync9(harnessDir, { recursive: true });
3069
- writeFileSync10(join18(harnessDir, "deps-lock.json"), JSON.stringify(snapshot, null, 2), "utf8");
3305
+ const harnessDir = join19(cwd2, ".harness");
3306
+ mkdirSync8(harnessDir, { recursive: true });
3307
+ writeFileSync9(join19(harnessDir, "deps-lock.json"), JSON.stringify(snapshot, null, 2), "utf8");
3070
3308
  return ok2(
3071
3309
  JSON.stringify({
3072
3310
  message: "Snapshot saved to .harness/deps-lock.json",
@@ -3075,12 +3313,12 @@ async function dispatch(name, args, db, docsPath, cwd2, config) {
3075
3313
  );
3076
3314
  }
3077
3315
  case "deps.check": {
3078
- const pkgPath2 = join18(cwd2, "package.json");
3079
- const lockPath = join18(cwd2, ".harness", "deps-lock.json");
3080
- if (!existsSync15(pkgPath2)) {
3316
+ const pkgPath2 = join19(cwd2, "package.json");
3317
+ const lockPath = join19(cwd2, ".harness", "deps-lock.json");
3318
+ if (!existsSync17(pkgPath2)) {
3081
3319
  return ok2("package.json not found in project root", true);
3082
3320
  }
3083
- if (!existsSync15(lockPath)) {
3321
+ if (!existsSync17(lockPath)) {
3084
3322
  return ok2(
3085
3323
  JSON.stringify({
3086
3324
  status: "no-snapshot",
@@ -3131,9 +3369,10 @@ async function dispatch(name, args, db, docsPath, cwd2, config) {
3131
3369
  const status = await getDoctorStatus(cwd2);
3132
3370
  const result = {
3133
3371
  lib: { current: status.lib.current, latest: status.lib.latest, outdated: status.lib.outdated },
3372
+ // Agents are existence-checked only — there is no `outdated` bucket.
3373
+ // Hand-edited agent definitions are supported and must not be flagged.
3134
3374
  agents: {
3135
3375
  missing: status.agents.filter((a) => a.status === "missing").map((a) => a.name),
3136
- outdated: status.agents.filter((a) => a.status === "outdated").map((a) => a.name),
3137
3376
  ok: status.agents.filter((a) => a.status === "ok").map((a) => a.name)
3138
3377
  },
3139
3378
  skills: {
@@ -3142,7 +3381,7 @@ async function dispatch(name, args, db, docsPath, cwd2, config) {
3142
3381
  ok: status.skills.filter((s) => s.status === "ok").map((s) => s.name)
3143
3382
  }
3144
3383
  };
3145
- return ok2(JSON.stringify(result, null, 2));
3384
+ return ok2(JSON.stringify(result));
3146
3385
  }
3147
3386
  default:
3148
3387
  return ok2(`Unknown tool: ${name}`, true);
@@ -3181,7 +3420,7 @@ function collectMarkdownFiles(dir) {
3181
3420
  const files = [];
3182
3421
  try {
3183
3422
  for (const entry of readdirSync2(dir)) {
3184
- const full = join18(dir, entry);
3423
+ const full = join19(dir, entry);
3185
3424
  const stat = statSync(full);
3186
3425
  if (stat.isDirectory()) {
3187
3426
  files.push(...collectMarkdownFiles(full));
@@ -3206,6 +3445,18 @@ function num(args, key) {
3206
3445
  if (typeof v4 !== "number") throw new Error(`${key} must be a number`);
3207
3446
  return v4;
3208
3447
  }
3448
+ function nonEmptyArray(args, key) {
3449
+ const v4 = args[key];
3450
+ if (!Array.isArray(v4) || v4.length === 0) {
3451
+ throw new Error(`${key} must be a non-empty array`);
3452
+ }
3453
+ for (const item of v4) {
3454
+ if (typeof item !== "object" || item === null) {
3455
+ throw new Error(`${key} entries must be objects`);
3456
+ }
3457
+ }
3458
+ return v4;
3459
+ }
3209
3460
 
3210
3461
  // src/commands/serve.ts
3211
3462
  async function runServe(cwd2, opts) {
@@ -3217,14 +3468,8 @@ async function runServe(cwd2, opts) {
3217
3468
  `);
3218
3469
  const syncResult = checkPermissionsSync(cwd2, config);
3219
3470
  if (!syncResult.in_sync && syncResult.agents) {
3220
- const affected = Object.entries(syncResult.agents).filter(([, r]) => !r.ok).map(([name, r]) => {
3221
- const parts = [];
3222
- if (r.missing.length) parts.push(`missing: ${r.missing.map((t) => t.replace("mcp__agent-harness-kit__", "")).join(", ")}`);
3223
- if (r.extra.length) parts.push(`extra: ${r.extra.map((t) => t.replace("mcp__agent-harness-kit__", "")).join(", ")}`);
3224
- return `${name} (${parts.join("; ")})`;
3225
- }).join("\n ");
3226
- process.stderr.write(`[agent-harness-kit] Agent permissions out of sync. Run: ahk build --sync
3227
- ${affected}
3471
+ const affected = Object.entries(syncResult.agents).filter(([, r]) => !r.ok).map(([name]) => name).join(", ");
3472
+ process.stderr.write(`[agent-harness-kit] Agent definition files missing: ${affected}. Run: ahk build
3228
3473
  `);
3229
3474
  }
3230
3475
  await startMcpServer(config, cwd2);
@@ -3232,12 +3477,12 @@ async function runServe(cwd2, opts) {
3232
3477
 
3233
3478
  // src/commands/status.ts
3234
3479
  import Table from "cli-table3";
3235
- import pc11 from "picocolors";
3480
+ import pc12 from "picocolors";
3236
3481
  var STATUS_COLOR = {
3237
- pending: (s) => pc11.dim(s),
3238
- in_progress: (s) => pc11.cyan(s),
3239
- done: (s) => pc11.green(s),
3240
- blocked: (s) => pc11.red(s)
3482
+ pending: (s) => pc12.dim(s),
3483
+ in_progress: (s) => pc12.cyan(s),
3484
+ done: (s) => pc12.green(s),
3485
+ blocked: (s) => pc12.red(s)
3241
3486
  };
3242
3487
  async function runStatus(cwd2, opts) {
3243
3488
  const config = await loadConfig(cwd2);
@@ -3258,11 +3503,11 @@ async function runStatus(cwd2, opts) {
3258
3503
  return;
3259
3504
  }
3260
3505
  if (tasks.length === 0) {
3261
- console.log(pc11.dim("No tasks yet. Run: ahk task add"));
3506
+ console.log(pc12.dim("No tasks yet. Run: ahk task add"));
3262
3507
  return;
3263
3508
  }
3264
3509
  const table = new Table({
3265
- head: ["ID", "Slug", "Title", "Status", "Assigned", "Started"].map((h) => pc11.bold(h)),
3510
+ head: ["ID", "Slug", "Title", "Status", "Assigned", "Started"].map((h) => pc12.bold(h)),
3266
3511
  style: { head: [], border: [] }
3267
3512
  });
3268
3513
  for (const t of tasks) {
@@ -3280,12 +3525,12 @@ async function runStatus(cwd2, opts) {
3280
3525
  const inProgress = tasks.filter((t) => t.status === "in_progress");
3281
3526
  if (inProgress.length > 0) {
3282
3527
  console.log("");
3283
- console.log(pc11.bold("Active actions:"));
3528
+ console.log(pc12.bold("Active actions:"));
3284
3529
  for (const t of inProgress) {
3285
3530
  const actions = await db.getActionsForTask(t.id);
3286
3531
  const active = actions.filter((a) => a.status === "in_progress");
3287
3532
  for (const a of active) {
3288
- console.log(` ${pc11.cyan(a.agent.padEnd(10))} \u2192 task #${t.id} ${t.slug}`);
3533
+ console.log(` ${pc12.cyan(a.agent.padEnd(10))} \u2192 task #${t.id} ${t.slug}`);
3289
3534
  }
3290
3535
  }
3291
3536
  }
@@ -3294,10 +3539,10 @@ async function runStatus(cwd2, opts) {
3294
3539
  const fn = STATUS_COLOR[s.status] ?? ((x) => x);
3295
3540
  return `${fn(s.status)}: ${s.total}`;
3296
3541
  });
3297
- console.log(pc11.dim("Tasks \u2014 ") + parts.join(pc11.dim(" | ")));
3542
+ console.log(pc12.dim("Tasks \u2014 ") + parts.join(pc12.dim(" | ")));
3298
3543
  const archivedTasks = await db.getArchivedTasks();
3299
3544
  if (archivedTasks.length > 0) {
3300
- console.log(pc11.dim(`${archivedTasks.length} archived (use \`ahk task list --archived\` to view)`));
3545
+ console.log(pc12.dim(`${archivedTasks.length} archived (use \`ahk task list --archived\` to view)`));
3301
3546
  }
3302
3547
  } finally {
3303
3548
  await db.close();
@@ -3305,13 +3550,13 @@ async function runStatus(cwd2, opts) {
3305
3550
  }
3306
3551
 
3307
3552
  // src/commands/sync.ts
3308
- import { existsSync as existsSync16, readFileSync as readFileSync11 } from "fs";
3309
- import { join as join19, resolve as resolve10 } from "path";
3310
- import pc12 from "picocolors";
3553
+ import { existsSync as existsSync18, readFileSync as readFileSync11 } from "fs";
3554
+ import { join as join20, resolve as resolve9 } from "path";
3555
+ import pc13 from "picocolors";
3311
3556
  async function runSync(cwd2, opts) {
3312
3557
  const config = await loadConfig(cwd2);
3313
3558
  const direction = opts.direction ?? "both";
3314
- const featureListPath = resolve10(join19(cwd2, config.storage.dir, "feature_list.json"));
3559
+ const featureListPath = resolve9(join20(cwd2, config.storage.dir, "feature_list.json"));
3315
3560
  const db = await openDB(config, cwd2);
3316
3561
  try {
3317
3562
  if (direction === "in" || direction === "both") {
@@ -3325,44 +3570,44 @@ async function runSync(cwd2, opts) {
3325
3570
  }
3326
3571
  }
3327
3572
  async function syncIn(featureListPath, db, dryRun) {
3328
- if (!existsSync16(featureListPath)) {
3329
- console.log(pc12.dim(`feature_list.json not found at ${featureListPath} \u2014 skipping in-sync`));
3573
+ if (!existsSync18(featureListPath)) {
3574
+ console.log(pc13.dim(`feature_list.json not found at ${featureListPath} \u2014 skipping in-sync`));
3330
3575
  return;
3331
3576
  }
3332
3577
  let seeds;
3333
3578
  try {
3334
3579
  seeds = JSON.parse(readFileSync11(featureListPath, "utf8"));
3335
3580
  } catch (err) {
3336
- console.error(pc12.red(`Failed to parse feature_list.json: ${err}`));
3581
+ console.error(pc13.red(`Failed to parse feature_list.json: ${err}`));
3337
3582
  process.exit(1);
3338
3583
  }
3339
3584
  if (dryRun) {
3340
- console.log(pc12.bold("Dry run \u2014 in-sync (feature_list.json \u2192 SQLite):"));
3585
+ console.log(pc13.bold("Dry run \u2014 in-sync (feature_list.json \u2192 SQLite):"));
3341
3586
  for (const t of seeds) {
3342
3587
  const existing = await db.getTaskBySlug(t.slug);
3343
- console.log(` ${existing ? pc12.dim("skip") : pc12.green("add ")} ${t.slug}`);
3588
+ console.log(` ${existing ? pc13.dim("skip") : pc13.green("add ")} ${t.slug}`);
3344
3589
  }
3345
3590
  return;
3346
3591
  }
3347
3592
  const result = await db.syncFromFeatureList(seeds);
3348
- console.log(pc12.green(`\u2713 In-sync: ${result.added} added, ${result.skipped} already existed`));
3593
+ console.log(pc13.green(`\u2713 In-sync: ${result.added} added, ${result.skipped} already existed`));
3349
3594
  }
3350
3595
  async function syncOut(db, cwd2, dryRun) {
3351
3596
  if (dryRun) {
3352
3597
  const tasks = await db.getTasks();
3353
- console.log(pc12.bold("Dry run \u2014 out-sync (SQLite \u2192 feature_list.json):"));
3598
+ console.log(pc13.bold("Dry run \u2014 out-sync (SQLite \u2192 feature_list.json):"));
3354
3599
  console.log(` ${tasks.length} tasks would be written`);
3355
3600
  return;
3356
3601
  }
3357
3602
  await db.writeFeatureList(cwd2);
3358
- console.log(pc12.green("\u2713 Out-sync: feature_list.json updated"));
3603
+ console.log(pc13.green("\u2713 Out-sync: feature_list.json updated"));
3359
3604
  }
3360
3605
 
3361
3606
  // src/commands/task/add.ts
3362
3607
  import * as p6 from "@clack/prompts";
3363
- import pc13 from "picocolors";
3608
+ import pc14 from "picocolors";
3364
3609
  async function runTaskAdd(cwd2) {
3365
- p6.intro(pc13.bold("agent-harness-kit \u2014 add task"));
3610
+ p6.intro(pc14.bold("agent-harness-kit \u2014 add task"));
3366
3611
  const title = await cliFormWithRetry(
3367
3612
  async () => {
3368
3613
  const val = await p6.text({ message: "Task title" });
@@ -3405,10 +3650,10 @@ async function runTaskAdd(cwd2) {
3405
3650
  await db.writeFeatureList(cwd2);
3406
3651
  await db.close();
3407
3652
  spinner6.stop("");
3408
- console.log(pc13.green(`\u2713 Task #${task2.id} added \u2014 ${task2.slug} (pending)`));
3409
- console.log(pc13.cyan("\u2192") + " " + pc13.cyan("ahk status") + " to see all tasks");
3653
+ console.log(pc14.green(`\u2713 Task #${task2.id} added \u2014 ${task2.slug} (pending)`));
3654
+ console.log(pc14.cyan("\u2192") + " " + pc14.cyan("ahk status") + " to see all tasks");
3410
3655
  } catch (err) {
3411
- spinner6.stop(pc13.red("Failed"));
3656
+ spinner6.stop(pc14.red("Failed"));
3412
3657
  p6.log.error(err instanceof Error ? err.message : String(err));
3413
3658
  process.exit(1);
3414
3659
  }
@@ -3416,17 +3661,17 @@ async function runTaskAdd(cwd2) {
3416
3661
 
3417
3662
  // src/commands/task/done.ts
3418
3663
  import { spawnSync as spawnSync2 } from "child_process";
3419
- import { existsSync as existsSync17 } from "fs";
3420
- import { resolve as resolve11 } from "path";
3421
- import pc14 from "picocolors";
3664
+ import { existsSync as existsSync19 } from "fs";
3665
+ import { resolve as resolve10 } from "path";
3666
+ import pc15 from "picocolors";
3422
3667
  async function runTaskDone(cwd2, idOrSlug) {
3423
3668
  const config = await loadConfig(cwd2);
3424
3669
  if (config.health.required) {
3425
- const scriptPath = resolve11(cwd2, config.health.scriptPath);
3426
- if (existsSync17(scriptPath)) {
3670
+ const scriptPath = resolve10(cwd2, config.health.scriptPath);
3671
+ if (existsSync19(scriptPath)) {
3427
3672
  const result = spawnSync2("bash", [scriptPath], { cwd: cwd2, stdio: "pipe", encoding: "utf8" });
3428
3673
  if (result.status !== 0) {
3429
- console.error(pc14.red("\u2717 Health check failed \u2014 cannot mark task as done."));
3674
+ console.error(pc15.red("\u2717 Health check failed \u2014 cannot mark task as done."));
3430
3675
  if (result.stdout) console.error(result.stdout);
3431
3676
  if (result.stderr) console.error(result.stderr);
3432
3677
  process.exit(1);
@@ -3439,16 +3684,16 @@ async function runTaskDone(cwd2, idOrSlug) {
3439
3684
  const isId = !isNaN(parsed);
3440
3685
  const task2 = isId ? await db.getTaskById(parsed) : await db.getTaskBySlug(idOrSlug);
3441
3686
  if (!task2) {
3442
- console.error(pc14.red(`Task not found: ${idOrSlug}`));
3687
+ console.error(pc15.red(`Task not found: ${idOrSlug}`));
3443
3688
  process.exit(1);
3444
3689
  }
3445
3690
  if (task2.status === "done") {
3446
- console.log(pc14.dim(`Task #${task2.id} is already done.`));
3691
+ console.log(pc15.dim(`Task #${task2.id} is already done.`));
3447
3692
  return;
3448
3693
  }
3449
3694
  await db.updateTaskStatus(task2.id, "done");
3450
3695
  await db.writeFeatureList(cwd2);
3451
- console.log(pc14.green(`\u2713 Task #${task2.id} \u2014 ${task2.slug} marked as done`));
3696
+ console.log(pc15.green(`\u2713 Task #${task2.id} \u2014 ${task2.slug} marked as done`));
3452
3697
  } finally {
3453
3698
  await db.close();
3454
3699
  }
@@ -3456,9 +3701,9 @@ async function runTaskDone(cwd2, idOrSlug) {
3456
3701
 
3457
3702
  // src/commands/task/edit.ts
3458
3703
  import * as p7 from "@clack/prompts";
3459
- import pc15 from "picocolors";
3704
+ import pc16 from "picocolors";
3460
3705
  async function runTaskEdit(cwd2) {
3461
- p7.intro(pc15.bold("agent-harness-kit \u2014 edit task"));
3706
+ p7.intro(pc16.bold("agent-harness-kit \u2014 edit task"));
3462
3707
  const config = await loadConfig(cwd2);
3463
3708
  const db = await openDB(config, cwd2);
3464
3709
  try {
@@ -3536,9 +3781,9 @@ async function runTaskEdit(cwd2) {
3536
3781
  await db.updateTaskAcceptance(task2.id, newAcceptance);
3537
3782
  await db.writeFeatureList(cwd2);
3538
3783
  spinner6.stop("");
3539
- console.log(pc15.green(`\u2713 Task #${task2.id} updated \u2014 ${newSlug}`));
3784
+ console.log(pc16.green(`\u2713 Task #${task2.id} updated \u2014 ${newSlug}`));
3540
3785
  } catch (err) {
3541
- spinner6.stop(pc15.red("Failed"));
3786
+ spinner6.stop(pc16.red("Failed"));
3542
3787
  p7.log.error(err instanceof Error ? err.message : String(err));
3543
3788
  process.exit(1);
3544
3789
  }
@@ -3549,12 +3794,12 @@ async function runTaskEdit(cwd2) {
3549
3794
 
3550
3795
  // src/commands/task/list.ts
3551
3796
  import Table2 from "cli-table3";
3552
- import pc16 from "picocolors";
3797
+ import pc17 from "picocolors";
3553
3798
  var STATUS_COLOR2 = {
3554
- pending: (s) => pc16.dim(s),
3555
- in_progress: (s) => pc16.cyan(s),
3556
- done: (s) => pc16.green(s),
3557
- blocked: (s) => pc16.red(s)
3799
+ pending: (s) => pc17.dim(s),
3800
+ in_progress: (s) => pc17.cyan(s),
3801
+ done: (s) => pc17.green(s),
3802
+ blocked: (s) => pc17.red(s)
3558
3803
  };
3559
3804
  async function runTaskList(cwd2, opts) {
3560
3805
  const config = await loadConfig(cwd2);
@@ -3571,11 +3816,11 @@ async function runTaskList(cwd2, opts) {
3571
3816
  let msg = "No tasks";
3572
3817
  if (filterStatus) msg += ` with status: ${filterStatus}`;
3573
3818
  if (opts.archived) msg += " (archived)";
3574
- console.log(pc16.dim(msg + "."));
3819
+ console.log(pc17.dim(msg + "."));
3575
3820
  return;
3576
3821
  }
3577
3822
  const table = new Table2({
3578
- head: ["ID", "Slug", "Title", "Status"].map((h) => pc16.bold(h)),
3823
+ head: ["ID", "Slug", "Title", "Status"].map((h) => pc17.bold(h)),
3579
3824
  style: { head: [], border: [] }
3580
3825
  });
3581
3826
  for (const t of tasks) {
@@ -3586,7 +3831,7 @@ async function runTaskList(cwd2, opts) {
3586
3831
  if (!opts.archived && !opts.includeArchived) {
3587
3832
  const archivedTasks = await db.getArchivedTasks();
3588
3833
  if (archivedTasks.length > 0) {
3589
- console.log(pc16.dim(`${archivedTasks.length} archived task${archivedTasks.length !== 1 ? "s" : ""} (use --archived to view)`));
3834
+ console.log(pc17.dim(`${archivedTasks.length} archived task${archivedTasks.length !== 1 ? "s" : ""} (use --archived to view)`));
3590
3835
  }
3591
3836
  }
3592
3837
  } finally {
@@ -3594,64 +3839,93 @@ async function runTaskList(cwd2, opts) {
3594
3839
  }
3595
3840
  }
3596
3841
 
3597
- // src/core/local-install-guard.ts
3598
- import { existsSync as existsSync18, readFileSync as readFileSync12 } from "fs";
3599
- import { join as join20 } from "path";
3600
- import pc17 from "picocolors";
3601
- function isLocalInstallSatisfied(cwd2) {
3602
- const selfPkgPath = join20(cwd2, "package.json");
3603
- let projectPkg = null;
3604
- if (existsSync18(selfPkgPath)) {
3842
+ // src/core/path-probe.ts
3843
+ import { accessSync, constants, readdirSync as readdirSync3 } from "fs";
3844
+ import { join as join21 } from "path";
3845
+ import pc18 from "picocolors";
3846
+ var DEFAULT_PATHEXT = [".COM", ".EXE", ".BAT", ".CMD"];
3847
+ function defaultIsExecutable(filePath) {
3848
+ try {
3849
+ accessSync(filePath, constants.X_OK);
3850
+ return true;
3851
+ } catch {
3852
+ return false;
3853
+ }
3854
+ }
3855
+ function normalizeExts(pathext) {
3856
+ const raw = pathext && pathext.trim().length > 0 ? pathext : DEFAULT_PATHEXT.join(";");
3857
+ return raw.split(";").map((ext) => ext.trim().toLowerCase()).filter((ext) => ext.length > 0).map((ext) => ext.startsWith(".") ? ext : `.${ext}`);
3858
+ }
3859
+ function resolveOnPath(name, options = {}) {
3860
+ const {
3861
+ pathValue = process.env.PATH,
3862
+ pathext = process.env.PATHEXT,
3863
+ platform = process.platform,
3864
+ isExecutable = defaultIsExecutable
3865
+ } = options;
3866
+ if (!pathValue) return false;
3867
+ const isWindows = platform === "win32";
3868
+ const sep = isWindows ? ";" : ":";
3869
+ const dirs = pathValue.split(sep).filter((dir) => dir.length > 0);
3870
+ if (dirs.length === 0) return false;
3871
+ if (isWindows) {
3872
+ const lowerName = name.toLowerCase();
3873
+ const candidates2 = /* @__PURE__ */ new Set([lowerName, ...normalizeExts(pathext).map((ext) => `${lowerName}${ext}`)]);
3874
+ for (const dir of dirs) {
3875
+ let entries;
3876
+ try {
3877
+ entries = readdirSync3(dir);
3878
+ } catch {
3879
+ continue;
3880
+ }
3881
+ for (const entry of entries) {
3882
+ if (candidates2.has(entry.toLowerCase())) return true;
3883
+ }
3884
+ }
3885
+ return false;
3886
+ }
3887
+ for (const dir of dirs) {
3605
3888
  try {
3606
- const selfPkg = JSON.parse(readFileSync12(selfPkgPath, "utf8"));
3607
- if (selfPkg?.name === pkg.name) return true;
3608
- projectPkg = selfPkg;
3889
+ if (isExecutable(join21(dir, name))) return true;
3609
3890
  } catch {
3891
+ continue;
3610
3892
  }
3611
3893
  }
3612
- const [scope, name] = pkg.name.split("/");
3613
- const localPath = pkg.name.startsWith("@") ? join20(cwd2, "node_modules", scope, name) : join20(cwd2, "node_modules", pkg.name);
3614
- if (existsSync18(localPath)) return true;
3615
- const isPnp = existsSync18(join20(cwd2, ".pnp.cjs")) || existsSync18(join20(cwd2, ".pnp.loader.mjs"));
3616
- if (isPnp && projectPkg) {
3617
- const deps = {
3618
- ...projectPkg.dependencies ?? {},
3619
- ...projectPkg.devDependencies ?? {}
3620
- };
3621
- if (Object.prototype.hasOwnProperty.call(deps, pkg.name)) return true;
3622
- }
3623
3894
  return false;
3624
3895
  }
3625
- function printLocalInstallWarning() {
3626
- console.error(pc17.yellow(`\u26A0 ${pkg.name} is not installed locally in this project.`));
3627
- console.error(pc17.dim(" This is only a recommendation for reproducibility: pinning a local"));
3628
- console.error(pc17.dim(" version keeps behavior consistent across your team and CI, instead of"));
3629
- console.error(pc17.dim(" drifting with whatever version is installed globally on each machine."));
3630
- console.error(pc17.dim(` Run: npm install --save-dev ${pkg.name}`));
3631
- console.error(pc17.dim(" (or the equivalent for your package manager: pnpm add -D, yarn add --dev, bun add -d)"));
3896
+ function isExecutableOnPath(name) {
3897
+ return resolveOnPath(name);
3898
+ }
3899
+ function printMissingGlobalBinaryWarning() {
3900
+ console.error(pc18.yellow("\u26A0 `ahk` was not found on your PATH."));
3901
+ console.error(pc18.dim(" Your project has no local install, so the generated MCP config launches"));
3902
+ console.error(pc18.dim(" `ahk serve` directly. Without `ahk` on your PATH, starting the MCP server"));
3903
+ console.error(pc18.dim(" from that config will fail. This is only a warning \u2014 the command continues."));
3904
+ console.error(pc18.dim(` Run: npm i -g ${pkg.name} (install globally)`));
3905
+ console.error(pc18.dim(` or: npm install --save-dev ${pkg.name} (install locally in this project)`));
3632
3906
  }
3633
3907
 
3634
3908
  // src/core/update-check.ts
3635
- import pc18 from "picocolors";
3909
+ import pc19 from "picocolors";
3636
3910
  var REGISTRY_URL2 = `https://registry.npmjs.org/${pkg.name}/latest`;
3637
3911
  var TIMEOUT_MS2 = 2500;
3638
3912
  function checkForUpdate(currentVersion) {
3639
- return new Promise((resolve12) => {
3640
- const timer = setTimeout(() => resolve12(null), TIMEOUT_MS2);
3913
+ return new Promise((resolve11) => {
3914
+ const timer = setTimeout(() => resolve11(null), TIMEOUT_MS2);
3641
3915
  fetch(REGISTRY_URL2).then((res) => res.json()).then((data) => {
3642
3916
  clearTimeout(timer);
3643
3917
  const latest = data.version;
3644
- resolve12(isNewer2(latest, currentVersion) ? { current: currentVersion, latest } : null);
3918
+ resolve11(isNewer2(latest, currentVersion) ? { current: currentVersion, latest } : null);
3645
3919
  }).catch(() => {
3646
3920
  clearTimeout(timer);
3647
- resolve12(null);
3921
+ resolve11(null);
3648
3922
  });
3649
3923
  });
3650
3924
  }
3651
3925
  function printUpdateMessage({ current, latest }) {
3652
3926
  const lines = [
3653
- ` Update available ${pc18.dim(current)} \u2192 ${pc18.green(latest)} `,
3654
- ` Run: ${pc18.cyan(`pnpm i ${pkg.name}@${latest}`)} `
3927
+ ` Update available ${pc19.dim(current)} \u2192 ${pc19.green(latest)} `,
3928
+ ` Run: ${pc19.cyan(`pnpm i ${pkg.name}@${latest}`)} `
3655
3929
  ];
3656
3930
  drawBox(lines);
3657
3931
  }
@@ -3666,13 +3940,28 @@ function isNewer2(latest, current) {
3666
3940
 
3667
3941
  // src/cli.ts
3668
3942
  var cwd = process.cwd();
3943
+ function parsePort(raw) {
3944
+ const trimmed = raw.trim();
3945
+ const rangeHint = "must be an integer between 1 and 65535";
3946
+ if (!/^\d+$/.test(trimmed)) {
3947
+ throw new InvalidArgumentError(`--port ${rangeHint} (received "${raw}").`);
3948
+ }
3949
+ const port = Number(trimmed);
3950
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
3951
+ throw new InvalidArgumentError(`--port ${rangeHint} (received "${raw}").`);
3952
+ }
3953
+ return port;
3954
+ }
3669
3955
  var updateCheck = checkForUpdate(pkg.version);
3670
3956
  var program = new Command();
3671
3957
  program.name("ahk").description("agent-harness-kit \u2014 CLI scaffolding for multi-agent harness systems").version(pkg.version, "-v, --version");
3672
3958
  program.command("init").description("Scaffold a harness interactively in the current directory").option("--name <name>", "Project name (skip prompt)").option("--provider <provider>", "AI provider: claude-code | opencode (skip prompt)").option("--docs <path>", "Docs folder path (skip prompt)").option("--tasks <adapter>", "Task adapter: local | jira | linear (skip prompt)").option("--storage-scope <scope>", "Storage scope: local | global (skip prompt)").action(async (opts) => {
3673
3959
  await runInit(cwd, opts);
3674
3960
  });
3675
- program.command("build").description("Regenerate AGENTS.md and provider files from agent-harness-kit.config.ts").option("--watch", "Rebuild on config changes").option("--sync", "Sync tools: frontmatter in existing .claude/agents/*.md to match current permission constants").action(async (opts) => {
3961
+ 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(
3962
+ "--force",
3963
+ "Regenerate agent files that already exist, DESTROYING your customizations (a backup is written first). Without this flag, build only creates agent files that are missing and never modifies existing ones."
3964
+ ).action(async (opts) => {
3676
3965
  await runBuild(cwd, opts);
3677
3966
  });
3678
3967
  program.command("health").description("Run health.sh and report result").action(async () => {
@@ -3684,7 +3973,7 @@ program.command("status").description("Show task table and active actions").opti
3684
3973
  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) => {
3685
3974
  await runSync(cwd, { dryRun: opts["dry-run"], direction: opts.direction });
3686
3975
  });
3687
- program.command("serve").description("Start the MCP server (stdio)").option("--port <port>", "Port hint stored in config (default: 3742)", parseInt).action(async (opts) => {
3976
+ program.command("serve").description("Start the MCP server (stdio)").option("--port <port>", "Port hint stored in config (default: 3742)", parsePort).action(async (opts) => {
3688
3977
  await runServe(cwd, { port: opts.port });
3689
3978
  });
3690
3979
  var task = program.command("task").description("Manage tasks");
@@ -3700,8 +3989,8 @@ task.command("done <id|slug>").description("Mark a task as done").action(async (
3700
3989
  task.command("edit").description("Edit a task interactively").action(async () => {
3701
3990
  await runTaskEdit(cwd);
3702
3991
  });
3703
- 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) => {
3704
- await runDashboard(cwd, { port: parseInt(opts.port), open: opts.open });
3992
+ 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) => {
3993
+ await runDashboard(cwd, { port: opts.port, open: opts.open });
3705
3994
  });
3706
3995
  var migrate = program.command("migrate").description("Migrate provider files to a different provider, or migrate harness storage (see subcommands)");
3707
3996
  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) => {
@@ -3713,7 +4002,7 @@ migrate.command("storage").description(
3713
4002
  try {
3714
4003
  await runMigrateStorage(cwd, { force: opts.force, dryRun: opts["dry-run"] });
3715
4004
  } catch (err) {
3716
- console.error(pc19.red(`\u2717 ${err instanceof Error ? err.message : String(err)}`));
4005
+ console.error(pc20.red(`\u2717 ${err instanceof Error ? err.message : String(err)}`));
3717
4006
  process.exit(1);
3718
4007
  }
3719
4008
  });
@@ -3729,6 +4018,9 @@ program.command("doctor").description("Check lib version, agent files, and harne
3729
4018
  program.hook("preAction", () => {
3730
4019
  if (!isLocalInstallSatisfied(cwd)) {
3731
4020
  printLocalInstallWarning();
4021
+ if (!isExecutableOnPath("ahk")) {
4022
+ printMissingGlobalBinaryWarning();
4023
+ }
3732
4024
  }
3733
4025
  });
3734
4026
  program.hook("postAction", async () => {