@cardor/agent-harness-kit 1.11.0 → 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
@@ -12,20 +12,20 @@ import {
12
12
  resolveGlobalStorageDir,
13
13
  resolveSqlitePath,
14
14
  resolveSqlitePathForScope
15
- } from "./chunk-6PEIJ2D5.js";
15
+ } from "./chunk-JTACLEGM.js";
16
16
 
17
17
  // src/cli.ts
18
- import { Command } from "commander";
19
- import pc19 from "picocolors";
18
+ import { Command, InvalidArgumentError } from "commander";
19
+ import pc20 from "picocolors";
20
20
 
21
21
  // src/commands/build.ts
22
22
  import { watch } from "fs";
23
23
  import * as p from "@clack/prompts";
24
- import pc from "picocolors";
24
+ import pc2 from "picocolors";
25
25
 
26
26
  // src/core/materializer/claude-code.ts
27
- import { existsSync as existsSync4, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
28
- import { join as join5, resolve as resolve3 } from "path";
27
+ import { existsSync as existsSync6 } from "fs";
28
+ import { join as join7 } from "path";
29
29
 
30
30
  // src/utils/file.ts
31
31
  import { mkdirSync, writeFileSync } from "fs";
@@ -37,24 +37,76 @@ var write = (cwd2, relPath, content, mode) => {
37
37
  };
38
38
 
39
39
  // src/core/materializer/detect-package-manager.ts
40
- import { existsSync, readFileSync } from "fs";
41
- import { join as join2 } from "path";
40
+ import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
41
+ import { join as join4 } from "path";
42
+
43
+ // src/core/local-install-guard.ts
44
+ import { existsSync as existsSync2, readFileSync } from "fs";
45
+ import { join as join3 } from "path";
46
+ import pc from "picocolors";
47
+
48
+ // src/core/package-data.ts
49
+ import { existsSync } from "fs";
50
+ import { createRequire } from "module";
51
+ import { dirname, join as join2 } from "path";
52
+ import { fileURLToPath } from "url";
53
+ var require2 = createRequire(import.meta.url);
54
+ var here = dirname(fileURLToPath(import.meta.url));
55
+ var candidates = [join2(here, "..", "..", "package.json"), join2(here, "..", "package.json")];
56
+ var pkgPath = candidates.find((p8) => existsSync(p8)) ?? candidates[0];
57
+ var pkg = require2(pkgPath);
58
+
59
+ // src/core/local-install-guard.ts
60
+ function isLocalInstallSatisfied(cwd2) {
61
+ const selfPkgPath = join3(cwd2, "package.json");
62
+ let projectPkg = null;
63
+ if (existsSync2(selfPkgPath)) {
64
+ try {
65
+ const selfPkg = JSON.parse(readFileSync(selfPkgPath, "utf8"));
66
+ if (selfPkg?.name === pkg.name) return true;
67
+ projectPkg = selfPkg;
68
+ } catch {
69
+ }
70
+ }
71
+ const [scope, name] = pkg.name.split("/");
72
+ const localPath = pkg.name.startsWith("@") ? join3(cwd2, "node_modules", scope, name) : join3(cwd2, "node_modules", pkg.name);
73
+ if (existsSync2(localPath)) return true;
74
+ const isPnp = existsSync2(join3(cwd2, ".pnp.cjs")) || existsSync2(join3(cwd2, ".pnp.loader.mjs"));
75
+ if (isPnp && projectPkg) {
76
+ const deps = {
77
+ ...projectPkg.dependencies ?? {},
78
+ ...projectPkg.devDependencies ?? {}
79
+ };
80
+ if (Object.prototype.hasOwnProperty.call(deps, pkg.name)) return true;
81
+ }
82
+ return false;
83
+ }
84
+ function printLocalInstallWarning() {
85
+ console.error(pc.yellow(`\u26A0 ${pkg.name} is not installed locally in this project.`));
86
+ console.error(pc.dim(" This is only a recommendation for reproducibility: pinning a local"));
87
+ console.error(pc.dim(" version keeps behavior consistent across your team and CI, instead of"));
88
+ console.error(pc.dim(" drifting with whatever version is installed globally on each machine."));
89
+ console.error(pc.dim(` Run: npm install --save-dev ${pkg.name}`));
90
+ console.error(pc.dim(" (or the equivalent for your package manager: pnpm add -D, yarn add --dev, bun add -d)"));
91
+ }
92
+
93
+ // src/core/materializer/detect-package-manager.ts
42
94
  function detectPackageManager(cwd2) {
43
95
  const fromField = detectFromPackageManagerField(cwd2);
44
96
  if (fromField) return fromField;
45
- if (existsSync(join2(cwd2, "pnpm-lock.yaml"))) return "pnpm";
46
- if (existsSync(join2(cwd2, "bun.lockb")) || existsSync(join2(cwd2, "bun.lock"))) return "bun";
47
- if (existsSync(join2(cwd2, "yarn.lock"))) {
48
- return existsSync(join2(cwd2, ".yarnrc.yml")) ? "yarn-berry" : "yarn-classic";
97
+ if (existsSync3(join4(cwd2, "pnpm-lock.yaml"))) return "pnpm";
98
+ if (existsSync3(join4(cwd2, "bun.lockb")) || existsSync3(join4(cwd2, "bun.lock"))) return "bun";
99
+ if (existsSync3(join4(cwd2, "yarn.lock"))) {
100
+ return existsSync3(join4(cwd2, ".yarnrc.yml")) ? "yarn-berry" : "yarn-classic";
49
101
  }
50
- if (existsSync(join2(cwd2, "package-lock.json"))) return "npm";
102
+ if (existsSync3(join4(cwd2, "package-lock.json"))) return "npm";
51
103
  return "npm";
52
104
  }
53
105
  function detectFromPackageManagerField(cwd2) {
54
- const pkgPath2 = join2(cwd2, "package.json");
55
- if (!existsSync(pkgPath2)) return null;
106
+ const pkgPath2 = join4(cwd2, "package.json");
107
+ if (!existsSync3(pkgPath2)) return null;
56
108
  try {
57
- const pkg2 = JSON.parse(readFileSync(pkgPath2, "utf8"));
109
+ const pkg2 = JSON.parse(readFileSync2(pkgPath2, "utf8"));
58
110
  const field = pkg2?.packageManager;
59
111
  if (typeof field !== "string" || !field.trim()) return null;
60
112
  const match = field.match(/^([a-z]+)@(\d+)/i);
@@ -78,8 +130,11 @@ function detectFromPackageManagerField(cwd2) {
78
130
  return null;
79
131
  }
80
132
  }
81
- function getMcpCommandParts(pm, port) {
133
+ function getMcpCommandParts(pm, port, cwd2) {
82
134
  const portStr = String(port);
135
+ if (!isLocalInstallSatisfied(cwd2)) {
136
+ return ["ahk", "serve", "--port", portStr];
137
+ }
83
138
  switch (pm) {
84
139
  case "pnpm":
85
140
  return ["pnpm", "exec", "ahk", "serve", "--port", portStr];
@@ -95,21 +150,21 @@ function getMcpCommandParts(pm, port) {
95
150
  }
96
151
 
97
152
  // src/core/materializer/mcp-merge.ts
98
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
99
- import { dirname } from "path";
100
- function mergeClaudeMcpJson(filePath, port, pm = "npm") {
101
- const folderPath = dirname(filePath);
102
- if (!existsSync2(folderPath)) {
153
+ import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
154
+ import { dirname as dirname2 } from "path";
155
+ function mergeClaudeMcpJson(filePath, port, cwd2, pm = "npm") {
156
+ const folderPath = dirname2(filePath);
157
+ if (!existsSync4(folderPath)) {
103
158
  mkdirSync2(folderPath, { recursive: true });
104
159
  }
105
160
  let existing = {};
106
- if (existsSync2(filePath)) {
161
+ if (existsSync4(filePath)) {
107
162
  try {
108
- existing = JSON.parse(readFileSync2(filePath, "utf8"));
163
+ existing = JSON.parse(readFileSync3(filePath, "utf8"));
109
164
  } catch {
110
165
  }
111
166
  }
112
- const [command, ...args] = getMcpCommandParts(pm, port);
167
+ const [command, ...args] = getMcpCommandParts(pm, port, cwd2);
113
168
  const merged = {
114
169
  ...existing,
115
170
  mcpServers: {
@@ -121,15 +176,15 @@ function mergeClaudeMcpJson(filePath, port, pm = "npm") {
121
176
  }
122
177
  }
123
178
  };
124
- mkdirSync2(dirname(filePath), { recursive: true });
179
+ mkdirSync2(dirname2(filePath), { recursive: true });
125
180
  writeFileSync2(filePath, JSON.stringify(merged, null, 2) + "\n", "utf8");
126
181
  }
127
182
  function mergeClaudeSettingsJson(filePath) {
128
- mkdirSync2(dirname(filePath), { recursive: true });
183
+ mkdirSync2(dirname2(filePath), { recursive: true });
129
184
  let existing = {};
130
- if (existsSync2(filePath)) {
185
+ if (existsSync4(filePath)) {
131
186
  try {
132
- existing = JSON.parse(readFileSync2(filePath, "utf8"));
187
+ existing = JSON.parse(readFileSync3(filePath, "utf8"));
133
188
  } catch {
134
189
  }
135
190
  }
@@ -232,11 +287,11 @@ var MCP_CLAUDE_PERMISSIONS = [
232
287
  ])
233
288
  ];
234
289
  function mergeClaudeSettingsLocalJson(filePath) {
235
- mkdirSync2(dirname(filePath), { recursive: true });
290
+ mkdirSync2(dirname2(filePath), { recursive: true });
236
291
  let existing = {};
237
- if (existsSync2(filePath)) {
292
+ if (existsSync4(filePath)) {
238
293
  try {
239
- existing = JSON.parse(readFileSync2(filePath, "utf8"));
294
+ existing = JSON.parse(readFileSync3(filePath, "utf8"));
240
295
  } catch {
241
296
  }
242
297
  }
@@ -255,15 +310,15 @@ function mergeClaudeSettingsLocalJson(filePath) {
255
310
  };
256
311
  writeFileSync2(filePath, JSON.stringify(merged, null, 2) + "\n", "utf8");
257
312
  }
258
- function mergeOpencodeJson(filePath, port, pm = "npm") {
259
- const folderPath = dirname(filePath);
260
- if (!existsSync2(folderPath)) {
313
+ function mergeOpencodeJson(filePath, port, cwd2, pm = "npm") {
314
+ const folderPath = dirname2(filePath);
315
+ if (!existsSync4(folderPath)) {
261
316
  mkdirSync2(folderPath, { recursive: true });
262
317
  }
263
318
  let existing = {};
264
- if (existsSync2(filePath)) {
319
+ if (existsSync4(filePath)) {
265
320
  try {
266
- existing = JSON.parse(readFileSync2(filePath, "utf8"));
321
+ existing = JSON.parse(readFileSync3(filePath, "utf8"));
267
322
  } catch {
268
323
  }
269
324
  }
@@ -280,7 +335,7 @@ function mergeOpencodeJson(filePath, port, pm = "npm") {
280
335
  type: "local",
281
336
  // OpenCode's mcp.<name>.command field is a single array (unlike
282
337
  // Claude/Codex, which split command/args) — pass the full token list.
283
- command: getMcpCommandParts(pm, port)
338
+ command: getMcpCommandParts(pm, port, cwd2)
284
339
  }
285
340
  }
286
341
  };
@@ -310,13 +365,13 @@ function mergeTomlSection(content, sectionName, sectionBody) {
310
365
  ];
311
366
  return newLines.join("\n");
312
367
  }
313
- function mergeCodexConfigToml(filePath, port, pm = "npm") {
314
- mkdirSync2(dirname(filePath), { recursive: true });
368
+ function mergeCodexConfigToml(filePath, port, cwd2, pm = "npm") {
369
+ mkdirSync2(dirname2(filePath), { recursive: true });
315
370
  let content = "";
316
- if (existsSync2(filePath)) {
317
- content = readFileSync2(filePath, "utf8");
371
+ if (existsSync4(filePath)) {
372
+ content = readFileSync3(filePath, "utf8");
318
373
  }
319
- const [command, ...args] = getMcpCommandParts(pm, port);
374
+ const [command, ...args] = getMcpCommandParts(pm, port, cwd2);
320
375
  const sectionBody = [
321
376
  `command = ${JSON.stringify(command)}`,
322
377
  `args = ${JSON.stringify(args)}`,
@@ -327,14 +382,15 @@ 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";
332
- import { fileURLToPath as fileURLToPath2 } from "url";
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";
333
389
 
334
390
  // 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";
391
+ import { readFileSync as readFileSync4 } from "fs";
392
+ import { dirname as dirname3, join as join5 } from "path";
393
+ import { fileURLToPath as fileURLToPath2 } from "url";
338
394
 
339
395
  // src/core/materializer/agent-restrictions.ts
340
396
  var AGENT_RESTRICTIONS = {
@@ -366,10 +422,10 @@ function codexRestrictionNotice(agentName) {
366
422
  }
367
423
 
368
424
  // src/core/materializer/templates.ts
369
- var __dirname = dirname2(fileURLToPath(import.meta.url));
370
- var TEMPLATES_DIR = join3(__dirname, "agent-templates");
425
+ var __dirname = dirname3(fileURLToPath2(import.meta.url));
426
+ var TEMPLATES_DIR = join5(__dirname, "agent-templates");
371
427
  function loadAgentTemplate(name, vars = {}) {
372
- const raw = readFileSync3(join3(TEMPLATES_DIR, `${name}.md`), "utf8");
428
+ const raw = readFileSync4(join5(TEMPLATES_DIR, `${name}.md`), "utf8");
373
429
  return raw.replace(/\{\{(\w+)\}\}/g, (_, key) => vars[key] ?? `{{${key}}}`);
374
430
  }
375
431
  var HEALTH_SH = `#!/usr/bin/env bash
@@ -424,10 +480,10 @@ If it exits non-zero, stop and report the issue. Do not proceed with codebase ch
424
480
  The harness exposes tools via MCP server on port ${port}. Use these instead of reading files directly.
425
481
 
426
482
  \`\`\`
427
- actions.start taskId agent \u2192 start an action, returns actionId
483
+ actions.start taskId agent \u2192 start an action, returns a numeric actionId
428
484
  actions.write actionId section text \u2192 record a section (result, blockers, ...)
429
- actions.record_tool actionId toolName [argsJson] [summary] \u2192 log a tool call to the Tools dashboard
430
- actions.record_file actionId filePath operation [notes] \u2192 log a file touch to the Files dashboard
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)
431
487
  actions.complete actionId summary \u2192 close the action
432
488
  actions.get taskId \u2192 full action history for a task
433
489
  tasks.add title [slug] [description] [acceptance] \u2192 create a new task from natural language
@@ -447,9 +503,8 @@ docs.search query \u2192 search ${docs
447
503
  - tasks.get('pending') \u2192 pick lowest id
448
504
 
449
505
  2. WORK (lead \u2192 explorer \u2192 consultant \u2192 builder \u2192 reviewer)
450
- - Each agent calls actions.start(taskId, agentName) \u2192 actionId
451
- - After EVERY tool call: actions.record_tool(actionId, toolName, args, summary)
452
- - After EVERY file change: actions.record_file(actionId, filePath, operation, notes)
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
453
508
  - Closes with actions.complete(actionId, summary)
454
509
 
455
510
  3. CLOSE
@@ -508,10 +563,10 @@ If it exits non-zero, stop and report the issue. Do not proceed with codebase ch
508
563
  The harness exposes tools via MCP server on port ${port}. Use these instead of reading files directly.
509
564
 
510
565
  \`\`\`
511
- actions.start taskId agent \u2192 start an action, returns actionId
566
+ actions.start taskId agent \u2192 start an action, returns a numeric actionId
512
567
  actions.write actionId section text \u2192 record a section (result, blockers, ...)
513
- actions.record_tool actionId toolName [argsJson] [summary] \u2192 log a tool call to the Tools dashboard
514
- actions.record_file actionId filePath operation [notes] \u2192 log a file touch to the Files dashboard
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)
515
570
  actions.complete actionId summary \u2192 close the action
516
571
  actions.get taskId \u2192 full action history for a task
517
572
  tasks.add title [slug] [description] [acceptance] \u2192 create a new task from natural language
@@ -532,9 +587,8 @@ docs.search query \u2192 search ${docs
532
587
  - No pending tasks? \u2192 ask user, infer fields, call tasks.add, then tasks.claim
533
588
 
534
589
  2. WORK (lead \u2192 explorer \u2192 consultant \u2192 builder \u2192 reviewer)
535
- - Each agent calls actions.start(taskId, agentName) \u2192 actionId
536
- - After EVERY tool call: actions.record_tool(actionId, toolName, args, summary)
537
- - After EVERY file change: actions.record_file(actionId, filePath, operation, notes)
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
538
592
  - Closes with actions.complete(actionId, summary)
539
593
 
540
594
  3. CLOSE
@@ -692,9 +746,6 @@ function agentConsultant(vars) {
692
746
  function agentReviewer(vars) {
693
747
  return loadAgentTemplate("reviewer", vars);
694
748
  }
695
- function featureListJson(tasks) {
696
- return JSON.stringify(tasks, null, 2) + "\n";
697
- }
698
749
  function stripFrontmatter(md) {
699
750
  const parts = md.split(/^---\s*$/m);
700
751
  if (parts.length < 3) return { description: "", body: md };
@@ -768,6 +819,13 @@ ${values.map((v4) => ` - ${v4}`).join("\n")}
768
819
  ${body}${block}---
769
820
  `);
770
821
  }
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
+ }
771
829
  function appendFrontmatterMapping(md, key, entries) {
772
830
  const keys = Object.keys(entries);
773
831
  if (keys.length === 0) return md;
@@ -778,9 +836,12 @@ ${keys.map((k) => ` ${k}: ${entries[k]}`).join("\n")}
778
836
  ${body}${block}---
779
837
  `);
780
838
  }
781
- function translateFrontmatterForClaudeCode(md, agentName) {
839
+ function translateFrontmatterForClaudeCode(md, agentName, opts) {
782
840
  let result = stripFrontmatterBlockSequence(md, "tools");
783
841
  result = stripFrontmatterBlockSequence(result, "disallowedTools");
842
+ if (opts?.model && opts.model !== "inherit") {
843
+ result = appendFrontmatterScalar(result, "model", opts.model);
844
+ }
784
845
  return appendFrontmatterBlockSequence(result, "disallowedTools", claudeDisallowedTools(agentName));
785
846
  }
786
847
  function translateFrontmatterForOpenCode(md, agentName) {
@@ -797,10 +858,10 @@ var GITIGNORE_ENTRIES = `
797
858
  `;
798
859
 
799
860
  // src/core/materializer/scaffold-utils.ts
800
- var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
861
+ var __dirname2 = dirname4(fileURLToPath3(import.meta.url));
801
862
  function writeAgentFiles(cwd2, entries, opts = {}) {
802
863
  const result = { created: [], overwritten: [], preserved: [] };
803
- const existing = entries.filter((e) => existsSync3(join4(cwd2, e.relPath)));
864
+ const existing = entries.filter((e) => existsSync5(join6(cwd2, e.relPath)));
804
865
  if (opts.force && existing.length > 0) {
805
866
  if (!opts.backupRoot) {
806
867
  throw new Error(
@@ -808,12 +869,12 @@ function writeAgentFiles(cwd2, entries, opts = {}) {
808
869
  );
809
870
  }
810
871
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
811
- const backupDir = join4(opts.backupRoot, `agents-${stamp}`);
872
+ const backupDir = join6(opts.backupRoot, `agents-${stamp}`);
812
873
  try {
813
874
  for (const entry of existing) {
814
- const dest = join4(backupDir, entry.relPath);
875
+ const dest = join6(backupDir, entry.relPath);
815
876
  mkdirSync3(resolve2(dest, ".."), { recursive: true });
816
- writeFileSync3(dest, readFileSync4(join4(cwd2, entry.relPath), "utf8"), "utf8");
877
+ writeFileSync3(dest, readFileSync5(join6(cwd2, entry.relPath), "utf8"), "utf8");
817
878
  }
818
879
  } catch (err) {
819
880
  throw new Error(
@@ -823,8 +884,8 @@ function writeAgentFiles(cwd2, entries, opts = {}) {
823
884
  result.backupDir = backupDir;
824
885
  }
825
886
  for (const entry of entries) {
826
- const abs = join4(cwd2, entry.relPath);
827
- const exists = existsSync3(abs);
887
+ const abs = join6(cwd2, entry.relPath);
888
+ const exists = existsSync5(abs);
828
889
  if (exists && !opts.force) {
829
890
  result.preserved.push(entry.relPath);
830
891
  continue;
@@ -836,9 +897,106 @@ function writeAgentFiles(cwd2, entries, opts = {}) {
836
897
  }
837
898
  return result;
838
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;
996
+ }
839
997
  function appendGitignore(cwd2) {
840
- const giPath = join4(cwd2, ".gitignore");
841
- const existing = existsSync3(giPath) ? readFileSync4(giPath, "utf8") : "";
998
+ const giPath = join6(cwd2, ".gitignore");
999
+ const existing = existsSync5(giPath) ? readFileSync5(giPath, "utf8") : "";
842
1000
  const toAdd = GITIGNORE_ENTRIES.split("\n").filter((line) => line && !existing.includes(line)).join("\n");
843
1001
  if (toAdd.trim()) {
844
1002
  writeFileSync3(giPath, existing + (existing.endsWith("\n") ? "" : "\n") + toAdd + "\n", "utf8");
@@ -850,36 +1008,34 @@ function slugify(title) {
850
1008
  function writeSkills(cwd2, skillsDir) {
851
1009
  const skillNames = ["ahk-ask", "ahk-consultant", "ahk-triage", "ahk-review"];
852
1010
  for (const skillName of skillNames) {
853
- const src = join4(__dirname2, "skills", skillName, "SKILL.md");
854
- const destDir = join4(cwd2, skillsDir, skillName);
855
- const dest = join4(destDir, "SKILL.md");
1011
+ const src = join6(__dirname2, "skills", skillName, "SKILL.md");
1012
+ const destDir = join6(cwd2, skillsDir, skillName);
1013
+ const dest = join6(destDir, "SKILL.md");
856
1014
  mkdirSync3(destDir, { recursive: true });
857
- writeFileSync3(dest, readFileSync4(src, "utf8"), "utf8");
1015
+ writeFileSync3(dest, readFileSync5(src, "utf8"), "utf8");
858
1016
  }
859
1017
  }
860
1018
 
861
1019
  // src/core/materializer/claude-code.ts
862
- function claudeAgentFiles(config) {
1020
+ function claudeAgentFiles(config, modelsByRole) {
863
1021
  const projectName = config.project.name;
864
1022
  return [
865
- { relPath: ".claude/agents/lead.md", content: translateFrontmatterForClaudeCode(agentLead({ projectName }), "lead") },
866
- { relPath: ".claude/agents/explorer.md", content: translateFrontmatterForClaudeCode(agentExplorer({ projectName }), "explorer") },
867
- { relPath: ".claude/agents/consultant.md", content: translateFrontmatterForClaudeCode(agentConsultant({ projectName }), "consultant") },
868
- { relPath: ".claude/agents/builder.md", content: translateFrontmatterForClaudeCode(agentBuilder({ projectName }), "builder") },
869
- { relPath: ".claude/agents/reviewer.md", content: translateFrontmatterForClaudeCode(agentReviewer({ projectName }), "reviewer") }
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 }) }
870
1028
  ];
871
1029
  }
872
1030
  var ClaudeCodeMaterializer = class {
873
1031
  async scaffold(config, opts) {
874
- const { cwd: cwd2 } = opts;
875
- write(cwd2, "AGENTS.md", agentsMd(config));
876
- write(cwd2, "CLAUDE.md", claudeMd(config));
877
- if (!existsSync4(join5(cwd2, "health.sh"))) {
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"))) {
878
1036
  write(cwd2, "health.sh", HEALTH_SH, 493);
879
1037
  }
880
- const tasks = opts.firstTask ? [{ slug: slugify(opts.firstTask.title), ...opts.firstTask }] : [];
881
- write(cwd2, "feature_list.json", featureListJson(tasks));
882
- if (config.storage.scope === "local" && !existsSync4(join5(cwd2, config.storage.markdownFallback.path))) {
1038
+ if (config.storage.scope === "local" && !existsSync6(join7(cwd2, config.storage.markdownFallback.path))) {
883
1039
  write(
884
1040
  cwd2,
885
1041
  config.storage.markdownFallback.path,
@@ -892,30 +1048,31 @@ No tasks in progress.
892
1048
  `
893
1049
  );
894
1050
  }
895
- writeAgentFiles(cwd2, claudeAgentFiles(config));
896
- mergeClaudeMcpJson(join5(cwd2, ".mcp.json"), config.tools.mcp.port, detectPackageManager(cwd2));
897
- mergeClaudeSettingsJson(join5(cwd2, ".claude/settings.json"));
898
- mergeClaudeSettingsLocalJson(join5(cwd2, ".claude/settings.local.json"));
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"));
899
1055
  appendGitignore(cwd2);
900
1056
  writeSkills(cwd2, ".claude/skills");
901
1057
  }
902
1058
  async build(config, cwd2, opts = {}) {
903
- const write2 = (relPath, content) => {
904
- const abs = join5(cwd2, relPath);
905
- mkdirSync4(resolve3(abs, ".."), { recursive: true });
906
- writeFileSync4(abs, content, "utf8");
907
- };
908
- write2("AGENTS.md", agentsMd(config));
909
- write2("CLAUDE.md", claudeMd(config));
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
+ );
910
1067
  const agents = writeAgentFiles(cwd2, claudeAgentFiles(config), {
911
1068
  force: opts.force,
912
- backupRoot: join5(cwd2, config.storage.dir, "backups")
1069
+ backupRoot: join7(cwd2, config.storage.dir, "backups")
913
1070
  });
914
- mergeClaudeMcpJson(join5(cwd2, ".mcp.json"), config.tools.mcp.port, detectPackageManager(cwd2));
915
- mergeClaudeSettingsJson(join5(cwd2, ".claude/settings.json"));
916
- mergeClaudeSettingsLocalJson(join5(cwd2, ".claude/settings.local.json"));
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"));
917
1074
  writeSkills(cwd2, ".claude/skills");
918
- return { agents };
1075
+ return { agents, derived };
919
1076
  }
920
1077
  async migrate(config, _to, _cwd) {
921
1078
  void config;
@@ -942,8 +1099,8 @@ No tasks in progress.
942
1099
  };
943
1100
 
944
1101
  // src/core/materializer/codex-cli.ts
945
- import { existsSync as existsSync5, mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "fs";
946
- import { join as join6, resolve as resolve4 } from "path";
1102
+ import { existsSync as existsSync7, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
1103
+ import { join as join8, resolve as resolve3 } from "path";
947
1104
  function codexAgentFiles(config) {
948
1105
  const projectName = config.project.name;
949
1106
  return [
@@ -959,17 +1116,15 @@ var CodexCliMaterializer = class {
959
1116
  async scaffold(config, opts) {
960
1117
  const { cwd: cwd2 } = opts;
961
1118
  const write2 = (relPath, content, mode) => {
962
- const abs = join6(cwd2, relPath);
963
- mkdirSync5(resolve4(abs, ".."), { recursive: true });
964
- 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 });
965
1122
  };
966
- write2("AGENTS.md", agentsMd(config));
967
- if (!existsSync5(join6(cwd2, "health.sh"))) {
1123
+ write2("AGENTS.md", stampGenerated(agentsMd(config)));
1124
+ if (!existsSync7(join8(cwd2, "health.sh"))) {
968
1125
  write2("health.sh", HEALTH_SH, 493);
969
1126
  }
970
- const tasks = opts.firstTask ? [{ slug: slugify(opts.firstTask.title), ...opts.firstTask }] : [];
971
- write2(join6(config.storage.dir, "feature_list.json"), featureListJson(tasks));
972
- if (config.storage.scope === "local" && !existsSync5(join6(cwd2, config.storage.markdownFallback.path))) {
1127
+ if (config.storage.scope === "local" && !existsSync7(join8(cwd2, config.storage.markdownFallback.path))) {
973
1128
  write2(
974
1129
  config.storage.markdownFallback.path,
975
1130
  `<!-- AUTO-GENERATED by agent-harness-kit \u2014 DO NOT EDIT MANUALLY -->
@@ -982,24 +1137,23 @@ No tasks in progress.
982
1137
  );
983
1138
  }
984
1139
  writeAgentFiles(cwd2, codexAgentFiles(config));
985
- mergeCodexConfigToml(join6(cwd2, ".codex/config.toml"), config.tools.mcp.port, detectPackageManager(cwd2));
1140
+ mergeCodexConfigToml(join8(cwd2, ".codex/config.toml"), config.tools.mcp.port, cwd2, detectPackageManager(cwd2));
986
1141
  appendGitignore(cwd2);
987
1142
  writeSkills(cwd2, ".agents/skills");
988
1143
  }
989
1144
  async build(config, cwd2, opts = {}) {
990
- const write2 = (relPath, content) => {
991
- const abs = join6(cwd2, relPath);
992
- mkdirSync5(resolve4(abs, ".."), { recursive: true });
993
- writeFileSync5(abs, content, "utf8");
994
- };
995
- write2("AGENTS.md", agentsMd(config));
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
+ );
996
1150
  const agents = writeAgentFiles(cwd2, codexAgentFiles(config), {
997
1151
  force: opts.force,
998
- backupRoot: join6(cwd2, config.storage.dir, "backups")
1152
+ backupRoot: join8(cwd2, config.storage.dir, "backups")
999
1153
  });
1000
- mergeCodexConfigToml(join6(cwd2, ".codex/config.toml"), config.tools.mcp.port, detectPackageManager(cwd2));
1154
+ mergeCodexConfigToml(join8(cwd2, ".codex/config.toml"), config.tools.mcp.port, cwd2, detectPackageManager(cwd2));
1001
1155
  writeSkills(cwd2, ".agents/skills");
1002
- return { agents };
1156
+ return { agents, derived };
1003
1157
  }
1004
1158
  async migrate(config, _to, _cwd) {
1005
1159
  void config;
@@ -1010,8 +1164,8 @@ No tasks in progress.
1010
1164
  };
1011
1165
 
1012
1166
  // src/core/materializer/opencode.ts
1013
- import { existsSync as existsSync6, mkdirSync as mkdirSync6, writeFileSync as writeFileSync6 } from "fs";
1014
- import { join as join7, resolve as resolve5 } from "path";
1167
+ import { existsSync as existsSync8, mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "fs";
1168
+ import { join as join9, resolve as resolve4 } from "path";
1015
1169
  function opencodeAgentFiles(config) {
1016
1170
  const projectName = config.project.name;
1017
1171
  return [
@@ -1026,17 +1180,15 @@ var OpenCodeMaterializer = class {
1026
1180
  async scaffold(config, opts) {
1027
1181
  const { cwd: cwd2 } = opts;
1028
1182
  const write2 = (relPath, content, mode) => {
1029
- const abs = join7(cwd2, relPath);
1030
- mkdirSync6(resolve5(abs, ".."), { recursive: true });
1031
- 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 });
1032
1186
  };
1033
- write2("AGENTS.md", agentsMd(config));
1034
- if (!existsSync6(join7(cwd2, "health.sh"))) {
1187
+ write2("AGENTS.md", stampGenerated(agentsMd(config)));
1188
+ if (!existsSync8(join9(cwd2, "health.sh"))) {
1035
1189
  write2("health.sh", HEALTH_SH, 493);
1036
1190
  }
1037
- const tasks = opts.firstTask ? [{ slug: slugify(opts.firstTask.title), ...opts.firstTask }] : [];
1038
- write2(join7(config.storage.dir, "feature_list.json"), featureListJson(tasks));
1039
- if (config.storage.scope === "local" && !existsSync6(join7(cwd2, config.storage.markdownFallback.path))) {
1191
+ if (config.storage.scope === "local" && !existsSync8(join9(cwd2, config.storage.markdownFallback.path))) {
1040
1192
  write2(
1041
1193
  config.storage.markdownFallback.path,
1042
1194
  `<!-- AUTO-GENERATED by agent-harness-kit \u2014 DO NOT EDIT MANUALLY -->
@@ -1049,24 +1201,23 @@ No tasks in progress.
1049
1201
  );
1050
1202
  }
1051
1203
  writeAgentFiles(cwd2, opencodeAgentFiles(config));
1052
- mergeOpencodeJson(join7(cwd2, "opencode.json"), config.tools.mcp.port, detectPackageManager(cwd2));
1204
+ mergeOpencodeJson(join9(cwd2, "opencode.json"), config.tools.mcp.port, cwd2, detectPackageManager(cwd2));
1053
1205
  appendGitignore(cwd2);
1054
1206
  writeSkills(cwd2, ".opencode/skills");
1055
1207
  }
1056
1208
  async build(config, cwd2, opts = {}) {
1057
- const write2 = (relPath, content) => {
1058
- const abs = join7(cwd2, relPath);
1059
- mkdirSync6(resolve5(abs, ".."), { recursive: true });
1060
- writeFileSync6(abs, content, "utf8");
1061
- };
1062
- write2("AGENTS.md", agentsMd(config));
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
+ );
1063
1214
  const agents = writeAgentFiles(cwd2, opencodeAgentFiles(config), {
1064
1215
  force: opts.force,
1065
- backupRoot: join7(cwd2, config.storage.dir, "backups")
1216
+ backupRoot: join9(cwd2, config.storage.dir, "backups")
1066
1217
  });
1067
- mergeOpencodeJson(join7(cwd2, "opencode.json"), config.tools.mcp.port, detectPackageManager(cwd2));
1218
+ mergeOpencodeJson(join9(cwd2, "opencode.json"), config.tools.mcp.port, cwd2, detectPackageManager(cwd2));
1068
1219
  writeSkills(cwd2, ".opencode/skills");
1069
- return { agents };
1220
+ return { agents, derived };
1070
1221
  }
1071
1222
  async migrate(config, _to, _cwd) {
1072
1223
  void config;
@@ -1119,8 +1270,37 @@ async function buildOnce(cwd2, force) {
1119
1270
  spinner6.message("Rebuilding files...");
1120
1271
  const materializer = getMaterializer(config.provider);
1121
1272
  const report = await materializer.build(config, cwd2, { force });
1122
- spinner6.stop(pc.green("Build complete"));
1123
- p.log.success("AGENTS.md");
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
+ }
1124
1304
  p.log.success(`Agent definitions (${config.provider})`);
1125
1305
  p.log.success("MCP config");
1126
1306
  const { created, overwritten, preserved, backupDir } = report.agents;
@@ -1130,13 +1310,13 @@ async function buildOnce(cwd2, force) {
1130
1310
  }
1131
1311
  if (overwritten.length > 0) {
1132
1312
  p.log.warn(
1133
- pc.yellow(
1313
+ pc2.yellow(
1134
1314
  `--force REGENERATED ${overwritten.length} existing agent file(s), discarding any customizations:
1135
1315
  ` + overwritten.join("\n ")
1136
1316
  )
1137
1317
  );
1138
1318
  if (backupDir) {
1139
- p.log.info(pc.yellow(` Previous content backed up \u2192 ${backupDir}`));
1319
+ p.log.info(pc2.yellow(` Previous content backed up \u2192 ${backupDir}`));
1140
1320
  }
1141
1321
  }
1142
1322
  if (preserved.length > 0) {
@@ -1147,7 +1327,7 @@ async function buildOnce(cwd2, force) {
1147
1327
  );
1148
1328
  }
1149
1329
  } catch (err) {
1150
- spinner6.stop(pc.red("Build failed"));
1330
+ spinner6.stop(pc2.red("Build failed"));
1151
1331
  p.log.error(err instanceof Error ? err.message : String(err));
1152
1332
  process.exit(1);
1153
1333
  }
@@ -1155,34 +1335,41 @@ async function buildOnce(cwd2, force) {
1155
1335
 
1156
1336
  // src/commands/dashboard.ts
1157
1337
  import { homedir } from "os";
1158
- import { dirname as dirname4, join as join9 } from "path";
1159
- import { fileURLToPath as fileURLToPath3 } from "url";
1160
- import pc2 from "picocolors";
1338
+ import { dirname as dirname5, join as join11 } from "path";
1339
+ import { fileURLToPath as fileURLToPath4 } from "url";
1340
+ import pc3 from "picocolors";
1161
1341
 
1162
1342
  // src/core/dashboard-server.ts
1163
1343
  import { watch as watch2 } from "fs";
1164
- import { existsSync as existsSync7, readFileSync as readFileSync5 } from "fs";
1165
- 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";
1166
1346
  import { serve } from "@hono/node-server";
1167
1347
  import { Hono } from "hono";
1168
1348
  import { WebSocketServer } from "ws";
1169
1349
 
1170
1350
  // src/core/port-utils.ts
1171
1351
  import { createServer } from "net";
1172
- function isPortFree(port) {
1173
- return new Promise((resolve12) => {
1352
+ var DASHBOARD_BIND_HOST = void 0;
1353
+ function isPortFree(port, host = DASHBOARD_BIND_HOST) {
1354
+ return new Promise((resolve11) => {
1174
1355
  const server = createServer();
1175
- server.once("error", () => resolve12(false));
1356
+ server.once("error", () => resolve11(false));
1176
1357
  server.once("listening", () => {
1177
- server.close(() => resolve12(true));
1358
+ server.close(() => resolve11(true));
1178
1359
  });
1179
- server.listen(port, "127.0.0.1");
1360
+ server.listen(port, host);
1180
1361
  });
1181
1362
  }
1182
- 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
+ }
1183
1370
  for (let i = 0; i < maxAttempts; i++) {
1184
1371
  const port = start + i;
1185
- if (await isPortFree(port)) return port;
1372
+ if (await isPortFree(port, host)) return port;
1186
1373
  }
1187
1374
  throw new Error(
1188
1375
  `Could not find a free port after ${maxAttempts} attempts (tried ${start}-${start + maxAttempts - 1}). Please free a port and try again.`
@@ -1204,12 +1391,42 @@ var MIME = {
1204
1391
  ".ttf": "font/ttf"
1205
1392
  };
1206
1393
  function fileResponse(filePath) {
1207
- const content = readFileSync5(filePath);
1394
+ const content = readFileSync6(filePath);
1208
1395
  const mime = MIME[extname(filePath)] ?? "application/octet-stream";
1209
1396
  return new Response(content, {
1210
1397
  headers: { "Content-Type": mime, "Cache-Control": "no-cache" }
1211
1398
  });
1212
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
+ }
1213
1430
  async function startDashboardServer(db, dbPath, staticPath, port) {
1214
1431
  const app = new Hono();
1215
1432
  const { tasks, actions, stats } = db;
@@ -1310,21 +1527,29 @@ async function startDashboardServer(db, dbPath, staticPath, port) {
1310
1527
  app.get("/*", (c) => {
1311
1528
  const urlPath = c.req.path;
1312
1529
  if (urlPath !== "/") {
1313
- const candidate = join8(staticPath, urlPath);
1314
- if (existsSync7(candidate)) {
1530
+ const candidate = join10(staticPath, urlPath);
1531
+ if (existsSync9(candidate)) {
1315
1532
  try {
1316
1533
  return fileResponse(candidate);
1317
1534
  } catch {
1318
1535
  }
1319
1536
  }
1320
1537
  }
1321
- return fileResponse(join8(staticPath, "index.html"));
1538
+ return fileResponse(join10(staticPath, "index.html"));
1322
1539
  });
1323
- const resolvedPort = await findFreePort(port);
1540
+ const resolvedPort = await findFreePort(port, { host: DASHBOARD_BIND_HOST });
1324
1541
  if (resolvedPort !== port) {
1325
1542
  console.log(`Port ${port} in use, using ${resolvedPort}`);
1326
1543
  }
1327
- 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
+ });
1328
1553
  const wss = new WebSocketServer({ noServer: true });
1329
1554
  httpServer.on("upgrade", (req, socket, head) => {
1330
1555
  if (req.url === "/ws") {
@@ -1349,7 +1574,7 @@ async function startDashboardServer(db, dbPath, staticPath, port) {
1349
1574
  let watcher = null;
1350
1575
  if (dbPath) {
1351
1576
  const walPath = `${dbPath}-wal`;
1352
- const watchTarget = existsSync7(walPath) ? walPath : dbPath;
1577
+ const watchTarget = existsSync9(walPath) ? walPath : dbPath;
1353
1578
  watcher = watch2(watchTarget, broadcast);
1354
1579
  }
1355
1580
  return {
@@ -1364,16 +1589,16 @@ async function startDashboardServer(db, dbPath, staticPath, port) {
1364
1589
  }
1365
1590
 
1366
1591
  // src/commands/dashboard.ts
1367
- var __dirname3 = dirname4(fileURLToPath3(import.meta.url));
1592
+ var __dirname3 = dirname5(fileURLToPath4(import.meta.url));
1368
1593
  async function runDashboard(cwd2, opts) {
1369
1594
  const config = await loadConfig(cwd2);
1370
1595
  const db = await openDB(config, cwd2);
1371
1596
  const dbPath = config.database.type === "sqlite" ? resolveSqlitePath(config, cwd2, homedir()) : null;
1372
- const staticPath = join9(__dirname3, "dashboard-dist");
1597
+ const staticPath = join11(__dirname3, "dashboard-dist");
1373
1598
  const { url } = await startDashboardServer(db, dbPath, staticPath, opts.port);
1374
- console.log(pc2.green(`\u2713`) + ` Dashboard running at ${pc2.bold(pc2.cyan(url))}`);
1375
- console.log(pc2.dim(` WebSocket live updates enabled`));
1376
- console.log(pc2.dim(` Press Ctrl+C to stop`));
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`));
1377
1602
  if (opts.open) {
1378
1603
  const { default: open } = await import("open");
1379
1604
  await open(url);
@@ -1386,25 +1611,12 @@ async function runDashboard(cwd2, opts) {
1386
1611
  }
1387
1612
 
1388
1613
  // src/commands/doctor.ts
1389
- import pc3 from "picocolors";
1614
+ import pc4 from "picocolors";
1390
1615
 
1391
1616
  // src/core/doctor.ts
1392
- import { existsSync as existsSync9, readFileSync as readFileSync6 } from "fs";
1393
- 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";
1394
1619
  import { fileURLToPath as fileURLToPath5 } from "url";
1395
-
1396
- // src/core/package-data.ts
1397
- import { existsSync as existsSync8 } from "fs";
1398
- import { createRequire } from "module";
1399
- import { dirname as dirname5, join as join10 } from "path";
1400
- import { fileURLToPath as fileURLToPath4 } from "url";
1401
- var require2 = createRequire(import.meta.url);
1402
- var here = dirname5(fileURLToPath4(import.meta.url));
1403
- var candidates = [join10(here, "..", "..", "package.json"), join10(here, "..", "package.json")];
1404
- var pkgPath = candidates.find((p8) => existsSync8(p8)) ?? candidates[0];
1405
- var pkg = require2(pkgPath);
1406
-
1407
- // src/core/doctor.ts
1408
1620
  var REGISTRY_URL = `https://registry.npmjs.org/${pkg.name}/latest`;
1409
1621
  var TIMEOUT_MS = 2e3;
1410
1622
  var LIB_VERSION_CACHE_TTL_MS = 5 * 60 * 1e3;
@@ -1458,13 +1670,13 @@ function getProviderAgentInfo(provider) {
1458
1670
  }
1459
1671
  function checkAgentFilesAtRoot(agentsRoot, ext) {
1460
1672
  return AGENT_NAMES.map((name) => {
1461
- const filePath = join11(agentsRoot, `${name}${ext}`);
1462
- return { name, status: existsSync9(filePath) ? "ok" : "missing" };
1673
+ const filePath = join12(agentsRoot, `${name}${ext}`);
1674
+ return { name, status: existsSync10(filePath) ? "ok" : "missing" };
1463
1675
  });
1464
1676
  }
1465
1677
  function checkAgentFiles(cwd2, provider) {
1466
1678
  const { agentsDir, ext } = getProviderAgentInfo(provider);
1467
- return checkAgentFilesAtRoot(join11(cwd2, agentsDir), ext);
1679
+ return checkAgentFilesAtRoot(join12(cwd2, agentsDir), ext);
1468
1680
  }
1469
1681
  function getProviderSkillsDir(provider) {
1470
1682
  switch (provider) {
@@ -1479,16 +1691,16 @@ function getProviderSkillsDir(provider) {
1479
1691
  }
1480
1692
  }
1481
1693
  function checkSkillsAtRoot(skillsRoot) {
1482
- const skillSourceBase = join11(__dirname4, "skills");
1694
+ const skillSourceBase = join12(__dirname4, "skills");
1483
1695
  return SKILL_NAMES.map((name) => {
1484
- const livePath = join11(skillsRoot, name, "SKILL.md");
1485
- const sourcePath = join11(skillSourceBase, name, "SKILL.md");
1486
- if (!existsSync9(livePath)) {
1696
+ const livePath = join12(skillsRoot, name, "SKILL.md");
1697
+ const sourcePath = join12(skillSourceBase, name, "SKILL.md");
1698
+ if (!existsSync10(livePath)) {
1487
1699
  return { name, status: "missing" };
1488
1700
  }
1489
1701
  try {
1490
- const live = readFileSync6(livePath, "utf8");
1491
- const source = readFileSync6(sourcePath, "utf8");
1702
+ const live = readFileSync7(livePath, "utf8");
1703
+ const source = readFileSync7(sourcePath, "utf8");
1492
1704
  return { name, status: live === source ? "ok" : "outdated" };
1493
1705
  } catch {
1494
1706
  return { name, status: "outdated" };
@@ -1497,7 +1709,7 @@ function checkSkillsAtRoot(skillsRoot) {
1497
1709
  }
1498
1710
  function checkSkills(cwd2, provider) {
1499
1711
  const skillsDir = getProviderSkillsDir(provider);
1500
- return checkSkillsAtRoot(join11(cwd2, skillsDir));
1712
+ return checkSkillsAtRoot(join12(cwd2, skillsDir));
1501
1713
  }
1502
1714
  async function getDoctorStatus(cwd2) {
1503
1715
  const lib = await checkLibVersion();
@@ -1519,16 +1731,16 @@ async function getDoctorStatus(cwd2) {
1519
1731
 
1520
1732
  // src/commands/doctor.ts
1521
1733
  function ok(label, detail) {
1522
- console.log(` ${pc3.cyan(label.padEnd(16))}${pc3.green("[\u2713]")} ${detail}`);
1734
+ console.log(` ${pc4.cyan(label.padEnd(16))}${pc4.green("[\u2713]")} ${detail}`);
1523
1735
  }
1524
1736
  function warn(label, detail, hint) {
1525
- console.log(` ${pc3.cyan(label.padEnd(16))}${pc3.yellow("[!]")} ${detail}`);
1737
+ console.log(` ${pc4.cyan(label.padEnd(16))}${pc4.yellow("[!]")} ${detail}`);
1526
1738
  if (hint) {
1527
- console.log(` ${"".padEnd(16)} ${pc3.dim(hint)}`);
1739
+ console.log(` ${"".padEnd(16)} ${pc4.dim(hint)}`);
1528
1740
  }
1529
1741
  }
1530
1742
  function neutral(label, detail) {
1531
- console.log(` ${pc3.cyan(label.padEnd(16))}${pc3.dim("[~]")} ${detail}`);
1743
+ console.log(` ${pc4.cyan(label.padEnd(16))}${pc4.dim("[~]")} ${detail}`);
1532
1744
  }
1533
1745
  function printLibSection(lib) {
1534
1746
  if (lib.latest === null) {
@@ -1580,7 +1792,7 @@ async function runDoctor(cwd2) {
1580
1792
  configFound = false;
1581
1793
  }
1582
1794
  console.log("");
1583
- console.log(pc3.bold(`\u25CF ahk doctor ` + "\u2500".repeat(44)));
1795
+ console.log(pc4.bold(`\u25CF ahk doctor ` + "\u2500".repeat(44)));
1584
1796
  console.log("");
1585
1797
  const status = await getDoctorStatus(cwd2);
1586
1798
  printLibSection(status.lib);
@@ -1598,11 +1810,11 @@ async function runDoctor(cwd2) {
1598
1810
  }
1599
1811
 
1600
1812
  // src/commands/export.ts
1601
- import { writeFileSync as writeFileSync7 } from "fs";
1602
- import pc4 from "picocolors";
1813
+ import { writeFileSync as writeFileSync6 } from "fs";
1814
+ import pc5 from "picocolors";
1603
1815
  async function runExport(cwd2, opts) {
1604
1816
  if (!opts.sql && !opts.json) {
1605
- console.error(pc4.red("Specify --sql or --json"));
1817
+ console.error(pc5.red("Specify --sql or --json"));
1606
1818
  process.exit(1);
1607
1819
  }
1608
1820
  const config = await loadConfig(cwd2);
@@ -1612,14 +1824,14 @@ async function runExport(cwd2, opts) {
1612
1824
  const data = await db.exportJson();
1613
1825
  const out = JSON.stringify(data, null, 2) + "\n";
1614
1826
  if (opts.output) {
1615
- writeFileSync7(opts.output, out, "utf8");
1616
- 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}`));
1617
1829
  } else {
1618
1830
  process.stdout.write(out);
1619
1831
  }
1620
1832
  }
1621
1833
  if (opts.sql) {
1622
- 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"));
1623
1835
  process.exit(1);
1624
1836
  }
1625
1837
  } finally {
@@ -1629,28 +1841,28 @@ async function runExport(cwd2, opts) {
1629
1841
 
1630
1842
  // src/commands/health.ts
1631
1843
  import { spawnSync } from "child_process";
1632
- import { existsSync as existsSync10 } from "fs";
1844
+ import { existsSync as existsSync11 } from "fs";
1633
1845
  import { homedir as homedir2 } from "os";
1634
- import { join as join12, resolve as resolve6 } from "path";
1635
- import pc5 from "picocolors";
1846
+ import { join as join13, resolve as resolve5 } from "path";
1847
+ import pc6 from "picocolors";
1636
1848
  function checkLine(label, ok3, message, indent = 0) {
1637
- const prefix = label ? pc5.cyan(`[${label}] `) : " ".repeat(indent);
1638
- const icon = ok3 ? pc5.green("\u2713") : pc5.red("\u2717");
1639
- console.log(prefix + icon + " " + (ok3 ? pc5.green(message) : pc5.red(message)));
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)));
1640
1852
  }
1641
1853
  async function runHealth(cwd2) {
1642
1854
  let config;
1643
1855
  try {
1644
1856
  config = await loadConfig(cwd2);
1645
1857
  } catch {
1646
- console.error(pc5.red("\u2717 No config found. Run: ahk init"));
1858
+ console.error(pc6.red("\u2717 No config found. Run: ahk init"));
1647
1859
  process.exit(1);
1648
1860
  }
1649
1861
  let allOk = true;
1650
1862
  let dbOk;
1651
1863
  if (config.database.type === "sqlite") {
1652
1864
  const dbPath = resolveSqlitePath(config, cwd2, homedir2());
1653
- dbOk = existsSync10(dbPath);
1865
+ dbOk = existsSync11(dbPath);
1654
1866
  checkLine("checking DB", dbOk, `${dbPath} reachable`);
1655
1867
  } else {
1656
1868
  dbOk = true;
@@ -1663,8 +1875,8 @@ async function runHealth(cwd2) {
1663
1875
  const agentsLabelWidth = "[checking agents] ".length;
1664
1876
  for (let i = 0; i < agentNames.length; i++) {
1665
1877
  const name = agentNames[i];
1666
- const agentPath = join12(cwd2, agentsDir, `${name}${providerFiles.agentExtension}`);
1667
- const ok3 = existsSync10(agentPath);
1878
+ const agentPath = join13(cwd2, agentsDir, `${name}${providerFiles.agentExtension}`);
1879
+ const ok3 = existsSync11(agentPath);
1668
1880
  checkLine(
1669
1881
  i === 0 ? "checking agents" : null,
1670
1882
  ok3,
@@ -1675,19 +1887,19 @@ async function runHealth(cwd2) {
1675
1887
  }
1676
1888
  if (config.tools.mcp.enabled) {
1677
1889
  const mcpFile = providerFiles.mcpFile;
1678
- const mcpPath = resolve6(cwd2, mcpFile);
1679
- const mcpOk = existsSync10(mcpPath);
1890
+ const mcpPath = resolve5(cwd2, mcpFile);
1891
+ const mcpOk = existsSync11(mcpPath);
1680
1892
  checkLine("checking MCP", mcpOk, `${mcpFile} valid`);
1681
1893
  if (!mcpOk) allOk = false;
1682
1894
  }
1683
1895
  if (!allOk) {
1684
1896
  console.log("");
1685
- 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"));
1686
1898
  process.exit(1);
1687
1899
  }
1688
- const scriptPath = resolve6(cwd2, config.health.scriptPath);
1689
- if (!existsSync10(scriptPath)) {
1690
- 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}`));
1691
1903
  console.error(" Run ahk init first.");
1692
1904
  process.exit(1);
1693
1905
  }
@@ -1697,14 +1909,14 @@ async function runHealth(cwd2) {
1697
1909
  encoding: "utf8"
1698
1910
  });
1699
1911
  if (result.error) {
1700
- 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}`));
1701
1913
  process.exit(1);
1702
1914
  }
1703
1915
  if (result.status === 0) {
1704
- console.log(pc5.green("\u2713 Health check passed"));
1916
+ console.log(pc6.green("\u2713 Health check passed"));
1705
1917
  process.exit(0);
1706
1918
  } else {
1707
- 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"})`));
1708
1920
  process.exit(result.status ?? 1);
1709
1921
  }
1710
1922
  }
@@ -1722,7 +1934,7 @@ function getProviderHealthFiles(provider) {
1722
1934
  }
1723
1935
 
1724
1936
  // src/commands/init.ts
1725
- import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync8 } from "fs";
1937
+ import { existsSync as existsSync13, mkdirSync as mkdirSync6, readFileSync as readFileSync9, writeFileSync as writeFileSync7 } from "fs";
1726
1938
  import { join as join15 } from "path";
1727
1939
  import * as p3 from "@clack/prompts";
1728
1940
  import pc8 from "picocolors";
@@ -1781,45 +1993,6 @@ import { randomUUID } from "crypto";
1781
1993
  import { existsSync as existsSync12, readFileSync as readFileSync8 } from "fs";
1782
1994
  import { join as join14 } from "path";
1783
1995
  import pc7 from "picocolors";
1784
-
1785
- // src/core/local-install-guard.ts
1786
- import { existsSync as existsSync11, readFileSync as readFileSync7 } from "fs";
1787
- import { join as join13 } from "path";
1788
- import pc6 from "picocolors";
1789
- function isLocalInstallSatisfied(cwd2) {
1790
- const selfPkgPath = join13(cwd2, "package.json");
1791
- let projectPkg = null;
1792
- if (existsSync11(selfPkgPath)) {
1793
- try {
1794
- const selfPkg = JSON.parse(readFileSync7(selfPkgPath, "utf8"));
1795
- if (selfPkg?.name === pkg.name) return true;
1796
- projectPkg = selfPkg;
1797
- } catch {
1798
- }
1799
- }
1800
- const [scope, name] = pkg.name.split("/");
1801
- const localPath = pkg.name.startsWith("@") ? join13(cwd2, "node_modules", scope, name) : join13(cwd2, "node_modules", pkg.name);
1802
- if (existsSync11(localPath)) return true;
1803
- const isPnp = existsSync11(join13(cwd2, ".pnp.cjs")) || existsSync11(join13(cwd2, ".pnp.loader.mjs"));
1804
- if (isPnp && projectPkg) {
1805
- const deps = {
1806
- ...projectPkg.dependencies ?? {},
1807
- ...projectPkg.devDependencies ?? {}
1808
- };
1809
- if (Object.prototype.hasOwnProperty.call(deps, pkg.name)) return true;
1810
- }
1811
- return false;
1812
- }
1813
- function printLocalInstallWarning() {
1814
- console.error(pc6.yellow(`\u26A0 ${pkg.name} is not installed locally in this project.`));
1815
- console.error(pc6.dim(" This is only a recommendation for reproducibility: pinning a local"));
1816
- console.error(pc6.dim(" version keeps behavior consistent across your team and CI, instead of"));
1817
- console.error(pc6.dim(" drifting with whatever version is installed globally on each machine."));
1818
- console.error(pc6.dim(` Run: npm install --save-dev ${pkg.name}`));
1819
- console.error(pc6.dim(" (or the equivalent for your package manager: pnpm add -D, yarn add --dev, bun add -d)"));
1820
- }
1821
-
1822
- // src/commands/init-helpers.ts
1823
1996
  function readProjectNameFromPackageJson(cwd2) {
1824
1997
  try {
1825
1998
  const pkgPath2 = join14(cwd2, "package.json");
@@ -1918,6 +2091,34 @@ function printWelcomeMessage(projectName) {
1918
2091
  }
1919
2092
 
1920
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
+ }
1921
2122
  async function runInit(cwd2, flags) {
1922
2123
  const existingConfig = findConfigFile(cwd2);
1923
2124
  if (existingConfig) {
@@ -1985,6 +2186,34 @@ async function runInit(cwd2, flags) {
1985
2186
  }
1986
2187
  provider = val;
1987
2188
  }
2189
+ const AGENT_LABELS = [
2190
+ { key: "lead", label: "Lead" },
2191
+ { key: "explorer", label: "Explorer" },
2192
+ { key: "consultant", label: "Consultant" },
2193
+ { key: "builder", label: "Builder" },
2194
+ { key: "reviewer", label: "Reviewer" }
2195
+ ];
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);
2213
+ }
2214
+ claudeAgentModels[agent.key] = val;
2215
+ }
2216
+ }
1988
2217
  let docsPath;
1989
2218
  if (flags.docs) {
1990
2219
  docsPath = flags.docs;
@@ -2076,6 +2305,7 @@ async function runInit(cwd2, flags) {
2076
2305
  firstTask = { title: taskTitle, description: taskDesc, acceptance };
2077
2306
  }
2078
2307
  let configExt = "ts";
2308
+ let featureListParseFailedPath = null;
2079
2309
  const spinner6 = p3.spinner();
2080
2310
  spinner6.start("Scaffolding...");
2081
2311
  try {
@@ -2102,19 +2332,14 @@ async function runInit(cwd2, flags) {
2102
2332
  scope: config.storage.scope,
2103
2333
  projectId: config.storage.projectId
2104
2334
  });
2105
- writeFileSync8(join15(installDir, configFileName), configContent, "utf8");
2106
- mkdirSync7(join15(installDir, config.storage.dir), { recursive: true });
2335
+ writeFileSync7(join15(installDir, configFileName), configContent, "utf8");
2336
+ mkdirSync6(join15(installDir, config.storage.dir), { recursive: true });
2107
2337
  const db = await openDB(config, installDir);
2108
2338
  await db.writeStorageState(installDir);
2109
- await materializer.scaffold(config, { cwd: installDir, firstTask });
2110
- if (firstTask) {
2111
- const slug = slugify(firstTask.title);
2112
- await db.addTask({
2113
- slug,
2114
- title: firstTask.title,
2115
- description: firstTask.description,
2116
- acceptance: firstTask.acceptance
2117
- });
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");
2118
2343
  }
2119
2344
  await db.close();
2120
2345
  spinner6.stop("");
@@ -2123,6 +2348,11 @@ async function runInit(cwd2, flags) {
2123
2348
  p3.log.error(err instanceof Error ? err.message : String(err));
2124
2349
  throw err;
2125
2350
  }
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
+ }
2126
2356
  console.log(pc8.green("\u2713 Scaffolded harness in current directory"));
2127
2357
  const agentsDir = provider === "claude-code" ? ".claude/agents/" : ".opencode/agents/";
2128
2358
  const mcpFile = provider === "claude-code" ? ".claude/mcp.json" : "./opencode.json";
@@ -2204,9 +2434,9 @@ async function runMigrate(cwd2, opts) {
2204
2434
  }
2205
2435
 
2206
2436
  // src/commands/migrate-storage.ts
2207
- import { copyFileSync, existsSync as existsSync13, mkdirSync as mkdirSync8, rmSync, writeFileSync as writeFileSync9 } from "fs";
2437
+ import { copyFileSync, existsSync as existsSync14, mkdirSync as mkdirSync7, rmSync, writeFileSync as writeFileSync8 } from "fs";
2208
2438
  import { homedir as homedir3 } from "os";
2209
- import { dirname as dirname7, join as join16, resolve as resolve7 } from "path";
2439
+ import { dirname as dirname7, join as join16, resolve as resolve6 } from "path";
2210
2440
  import pc10 from "picocolors";
2211
2441
  function log5(msg) {
2212
2442
  console.log(msg);
@@ -2218,17 +2448,17 @@ function defaultMarkdownPathForConfig(config) {
2218
2448
  return config.storage.scope === "local" ? config.storage.markdownFallback.path : DEFAULT_MARKDOWN_PATH;
2219
2449
  }
2220
2450
  function currentMdPathForScope(scope, config, cwd2, homeDir) {
2221
- return scope === "global" ? join16(resolveGlobalStorageDir(config, homeDir), "current.md") : resolve7(cwd2, defaultMarkdownPathForConfig(config));
2451
+ return scope === "global" ? join16(resolveGlobalStorageDir(config, homeDir), "current.md") : resolve6(cwd2, defaultMarkdownPathForConfig(config));
2222
2452
  }
2223
2453
  function defaultSqlitePathForConfig(config) {
2224
2454
  return config.storage.scope === "local" && config.database.type === "sqlite" ? config.storage.sqlitePath ?? DEFAULT_SQLITE_PATH : DEFAULT_SQLITE_PATH;
2225
2455
  }
2226
2456
  async function backupDestination(cwd2, storageDir, data) {
2227
- const backupsDir = resolve7(cwd2, storageDir, "backups");
2457
+ const backupsDir = resolve6(cwd2, storageDir, "backups");
2228
2458
  const path = join16(backupsDir, `pre-migrate-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.json`);
2229
2459
  try {
2230
- mkdirSync8(backupsDir, { recursive: true });
2231
- writeFileSync9(path, JSON.stringify(data, null, 2) + "\n", "utf8");
2460
+ mkdirSync7(backupsDir, { recursive: true });
2461
+ writeFileSync8(path, JSON.stringify(data, null, 2) + "\n", "utf8");
2232
2462
  } catch (err) {
2233
2463
  throw new Error(
2234
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.`
@@ -2237,14 +2467,14 @@ async function backupDestination(cwd2, storageDir, data) {
2237
2467
  return path;
2238
2468
  }
2239
2469
  function copySqliteFile(srcPath, destPath) {
2240
- mkdirSync8(dirname7(destPath), { recursive: true });
2470
+ mkdirSync7(dirname7(destPath), { recursive: true });
2241
2471
  copyFileSync(srcPath, destPath);
2242
2472
  for (const suffix of ["-wal", "-shm"]) {
2243
- if (existsSync13(`${srcPath}${suffix}`)) {
2473
+ if (existsSync14(`${srcPath}${suffix}`)) {
2244
2474
  copyFileSync(`${srcPath}${suffix}`, `${destPath}${suffix}`);
2245
2475
  }
2246
2476
  }
2247
- if (!existsSync13(destPath)) {
2477
+ if (!existsSync14(destPath)) {
2248
2478
  throw new Error(`Copy verification failed: ${destPath} does not exist after copy.`);
2249
2479
  }
2250
2480
  }
@@ -2303,8 +2533,8 @@ async function runMigrateStorage(cwd2, opts, homeDir = homedir3()) {
2303
2533
  return migrateAcrossDbType(cwd2, config, homeDir, realScope, opts);
2304
2534
  }
2305
2535
  async function probeTaskCount(dbPath) {
2306
- if (!existsSync13(dbPath)) return 0;
2307
- const { SQLiteDriver } = await import("./sqlite-TR4D324R.js");
2536
+ if (!existsSync14(dbPath)) return 0;
2537
+ const { SQLiteDriver } = await import("./sqlite-5OWKTUUZ.js");
2308
2538
  const driver = new SQLiteDriver(dbPath);
2309
2539
  try {
2310
2540
  await driver.ensureSchema();
@@ -2320,12 +2550,12 @@ async function migrateScopeOnly(cwd2, config, homeDir, fromScope, toScope, opts)
2320
2550
  const destDb = resolveSqlitePathForScope(toScope, sqlitePath, cwd2, config, homeDir);
2321
2551
  const srcMd = currentMdPathForScope(fromScope, config, cwd2, homeDir);
2322
2552
  const destMd = currentMdPathForScope(toScope, config, cwd2, homeDir);
2323
- if (!existsSync13(srcDb)) {
2553
+ if (!existsSync14(srcDb)) {
2324
2554
  fail(`Source database not found at ${srcDb} (expected ${fromScope} scope) \u2014 nothing to move.`);
2325
2555
  }
2326
- const destExists = existsSync13(destDb);
2556
+ const destExists = existsSync14(destDb);
2327
2557
  if (destExists) {
2328
- const { SQLiteDriver } = await import("./sqlite-TR4D324R.js");
2558
+ const { SQLiteDriver } = await import("./sqlite-5OWKTUUZ.js");
2329
2559
  const destDriver = new SQLiteDriver(destDb);
2330
2560
  let destEmpty;
2331
2561
  try {
@@ -2340,12 +2570,12 @@ async function migrateScopeOnly(cwd2, config, homeDir, fromScope, toScope, opts)
2340
2570
  );
2341
2571
  }
2342
2572
  if (!destEmpty && opts.force) {
2343
- const { SQLiteDriver: Driver } = await import("./sqlite-TR4D324R.js");
2573
+ const { SQLiteDriver: Driver } = await import("./sqlite-5OWKTUUZ.js");
2344
2574
  const backupDriver = new Driver(destDb);
2345
2575
  let data;
2346
2576
  try {
2347
2577
  await backupDriver.ensureSchema();
2348
- const { HarnessDB } = await import("./db-3OXHRFAR.js");
2578
+ const { HarnessDB } = await import("./db-L3AADJF5.js");
2349
2579
  const tmpDb = new HarnessDB(backupDriver, config, homeDir);
2350
2580
  data = await tmpDb.exportJson();
2351
2581
  } finally {
@@ -2361,15 +2591,15 @@ async function migrateScopeOnly(cwd2, config, homeDir, fromScope, toScope, opts)
2361
2591
  }
2362
2592
  copySqliteFile(srcDb, destDb);
2363
2593
  log5(pc10.green(`\u2713 Copied database ${srcDb} \u2192 ${destDb}`));
2364
- if (existsSync13(srcMd)) {
2365
- mkdirSync8(dirname7(destMd), { recursive: true });
2594
+ if (existsSync14(srcMd)) {
2595
+ mkdirSync7(dirname7(destMd), { recursive: true });
2366
2596
  copyFileSync(srcMd, destMd);
2367
2597
  log5(pc10.green(`\u2713 Copied current.md ${srcMd} \u2192 ${destMd}`));
2368
2598
  }
2369
2599
  rmSync(srcDb, { force: true });
2370
2600
  rmSync(`${srcDb}-wal`, { force: true });
2371
2601
  rmSync(`${srcDb}-shm`, { force: true });
2372
- if (existsSync13(srcMd) && srcMd !== destMd) rmSync(srcMd, { force: true });
2602
+ if (existsSync14(srcMd) && srcMd !== destMd) rmSync(srcMd, { force: true });
2373
2603
  const db = await openDB(config, cwd2, homeDir);
2374
2604
  try {
2375
2605
  await db.writeStorageState(cwd2);
@@ -2381,17 +2611,17 @@ async function migrateScopeOnly(cwd2, config, homeDir, fromScope, toScope, opts)
2381
2611
  async function migrateAcrossDbType(cwd2, config, homeDir, sourceScope, opts) {
2382
2612
  const sqlitePath = defaultSqlitePathForConfig(config);
2383
2613
  const srcPath = resolveSqlitePathForScope(sourceScope, sqlitePath, cwd2, config, homeDir);
2384
- if (!existsSync13(srcPath)) {
2614
+ if (!existsSync14(srcPath)) {
2385
2615
  fail(`Source sqlite database not found at ${srcPath} (expected ${sourceScope} scope) \u2014 nothing to migrate.`);
2386
2616
  }
2387
- const { SQLiteDriver } = await import("./sqlite-TR4D324R.js");
2617
+ const { SQLiteDriver } = await import("./sqlite-5OWKTUUZ.js");
2388
2618
  const srcDriver = new SQLiteDriver(srcPath);
2389
2619
  let sourceData;
2390
2620
  let sourceCounts;
2391
2621
  try {
2392
2622
  await srcDriver.ensureSchema();
2393
2623
  sourceCounts = await getRowCounts(srcDriver);
2394
- const { HarnessDB } = await import("./db-3OXHRFAR.js");
2624
+ const { HarnessDB } = await import("./db-L3AADJF5.js");
2395
2625
  const srcDb = new HarnessDB(srcDriver, config, homeDir);
2396
2626
  sourceData = await srcDb.exportJson();
2397
2627
  } finally {
@@ -2444,16 +2674,16 @@ async function migrateAcrossDbType(cwd2, config, homeDir, sourceScope, opts) {
2444
2674
  }
2445
2675
 
2446
2676
  // src/commands/reset.ts
2447
- import { existsSync as existsSync14, readdirSync, rmSync as rmSync2 } from "fs";
2677
+ import { existsSync as existsSync15, readdirSync, rmSync as rmSync2 } from "fs";
2448
2678
  import { homedir as homedir4 } from "os";
2449
- import { join as join17, resolve as resolve8 } from "path";
2679
+ import { join as join17, resolve as resolve7 } from "path";
2450
2680
  import * as p5 from "@clack/prompts";
2451
2681
  import pc11 from "picocolors";
2452
2682
  var AGENT_MD_FILES = ["lead", "explorer", "consultant", "builder", "reviewer"];
2453
2683
  async function resetAgentMds(cwd2, provider) {
2454
2684
  const agentDir = provider === "claude-code" ? ".claude/agents" : ".opencode/agents";
2455
- const agentDirPath = resolve8(cwd2, agentDir);
2456
- if (!existsSync14(agentDirPath)) {
2685
+ const agentDirPath = resolve7(cwd2, agentDir);
2686
+ if (!existsSync15(agentDirPath)) {
2457
2687
  console.log(pc11.yellow(` Skipping agent files \u2014 directory not found: ${agentDirPath}`));
2458
2688
  return;
2459
2689
  }
@@ -2505,11 +2735,11 @@ async function runReset(cwd2, opts) {
2505
2735
  }
2506
2736
  const storageDir = config.storage.dir || ".harness";
2507
2737
  const dbPath = config.database.type === "sqlite" ? resolveSqlitePath(config, cwd2, homedir4()) : null;
2508
- const featureListPath = resolve8(cwd2, storageDir, "feature_list.json");
2738
+ const featureListPath = resolve7(cwd2, storageDir, "feature_list.json");
2509
2739
  let resetDb = false;
2510
2740
  let resetFeatureList = false;
2511
2741
  let resetAgentMdsFlag = false;
2512
- if (dbPath && existsSync14(dbPath)) {
2742
+ if (dbPath && existsSync15(dbPath)) {
2513
2743
  if (opts.force) {
2514
2744
  resetDb = true;
2515
2745
  } else {
@@ -2531,7 +2761,7 @@ async function runReset(cwd2, opts) {
2531
2761
  } else if (!dbPath) {
2532
2762
  console.log(pc11.dim(` Skipping DB reset \u2014 remote ${config.database.type} database is not managed by this command.`));
2533
2763
  }
2534
- if (existsSync14(featureListPath)) {
2764
+ if (existsSync15(featureListPath)) {
2535
2765
  if (opts.force) {
2536
2766
  resetFeatureList = true;
2537
2767
  } else {
@@ -2580,8 +2810,8 @@ async function runReset(cwd2, opts) {
2580
2810
  }
2581
2811
 
2582
2812
  // src/core/mcp-server.ts
2583
- import { existsSync as existsSync16, mkdirSync as mkdirSync9, readdirSync as readdirSync2, readFileSync as readFileSync9, statSync, writeFileSync as writeFileSync10 } from "fs";
2584
- import { join as join19, resolve as resolve9 } from "path";
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";
2585
2815
  import { Server } from "@modelcontextprotocol/sdk/server";
2586
2816
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
2587
2817
  import {
@@ -2590,7 +2820,7 @@ import {
2590
2820
  } from "@modelcontextprotocol/sdk/types.js";
2591
2821
 
2592
2822
  // src/core/permissions-check.ts
2593
- import { existsSync as existsSync15 } from "fs";
2823
+ import { existsSync as existsSync16 } from "fs";
2594
2824
  import { join as join18 } from "path";
2595
2825
  var AGENTS = ["lead", "explorer", "consultant", "builder", "reviewer"];
2596
2826
  function checkPermissionsSync(cwd2, config) {
@@ -2601,7 +2831,7 @@ function checkPermissionsSync(cwd2, config) {
2601
2831
  let in_sync = true;
2602
2832
  for (const agent of AGENTS) {
2603
2833
  const filePath = join18(cwd2, ".claude", "agents", `${agent}.md`);
2604
- const exists = existsSync15(filePath);
2834
+ const exists = existsSync16(filePath);
2605
2835
  if (!exists) in_sync = false;
2606
2836
  agents[agent] = exists ? { ok: true } : { ok: false, reason: "missing_file" };
2607
2837
  }
@@ -2613,7 +2843,7 @@ var VERSION = "0.1.0";
2613
2843
  var TOOLS = [
2614
2844
  {
2615
2845
  name: "actions.start",
2616
- description: "Start a new action for a task. Returns an actionId (UUID).",
2846
+ description: "Start a new action for a task. Returns an actionId.",
2617
2847
  inputSchema: {
2618
2848
  type: "object",
2619
2849
  properties: {
@@ -2628,18 +2858,18 @@ var TOOLS = [
2628
2858
  },
2629
2859
  {
2630
2860
  name: "actions.write",
2631
- description: "Record a section in an action. Standard sections: result, tools_used, blockers, next_steps. Note: files_modified is a plain-text note only \u2014 it does NOT populate the files dashboard. Use actions.record_file to register files in the dashboard.",
2861
+ description: "Record a section in an action. Standard sections: result, tools_used, blockers, next_steps.",
2632
2862
  inputSchema: {
2633
2863
  type: "object",
2634
2864
  properties: {
2635
- actionId: { type: "string", description: "UUID returned by actions.start" },
2865
+ actionId: { type: "number", description: "The actionId returned by actions.start" },
2636
2866
  sectionType: {
2637
2867
  type: "string",
2638
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."
2639
2869
  },
2640
2870
  content: {
2641
2871
  type: "string",
2642
- description: "Content for this section. No length limit \u2014 include all information that's relevant and necessary, but avoid unnecessary padding to prevent context bottlenecks between agents."
2872
+ description: "Content for this section. No length limit; avoid padding \u2014 it costs shared context for other agents."
2643
2873
  }
2644
2874
  },
2645
2875
  required: ["actionId", "sectionType", "content"]
@@ -2651,7 +2881,7 @@ var TOOLS = [
2651
2881
  inputSchema: {
2652
2882
  type: "object",
2653
2883
  properties: {
2654
- actionId: { type: "string", description: "UUID of the action to close" },
2884
+ actionId: { type: "number", description: "The actionId of the action to close" },
2655
2885
  summary: { type: "string", description: "One-line summary of what was done" }
2656
2886
  },
2657
2887
  required: ["actionId", "summary"]
@@ -2726,20 +2956,31 @@ var TOOLS = [
2726
2956
  },
2727
2957
  {
2728
2958
  name: "actions.record_file",
2729
- description: "Record a file touched during an action. This is the only way to populate the files-touched count shown in the dashboard. Call once per file.",
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.",
2730
2960
  inputSchema: {
2731
2961
  type: "object",
2732
2962
  properties: {
2733
- actionId: { type: "string", description: "UUID returned by actions.start" },
2734
- filePath: { type: "string", description: "Absolute or repo-relative path of the file" },
2735
- operation: {
2736
- type: "string",
2737
- enum: ["read", "created", "modified", "deleted"],
2738
- description: "What was done to the file"
2739
- },
2740
- notes: { type: "string", description: "Optional short note about the change" }
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
+ }
2741
2982
  },
2742
- required: ["actionId", "filePath", "operation"]
2983
+ required: ["actionId", "files"]
2743
2984
  }
2744
2985
  },
2745
2986
  {
@@ -2780,12 +3021,12 @@ var TOOLS = [
2780
3021
  },
2781
3022
  description: {
2782
3023
  type: "string",
2783
- description: "Longer description of the task goal. No length limit \u2014 include all information that's relevant and necessary, but avoid unnecessary padding to prevent context bottlenecks between agents."
3024
+ description: "Longer description of the task goal. No length limit; avoid padding \u2014 it costs shared context for other agents."
2784
3025
  },
2785
3026
  acceptance: {
2786
3027
  type: "array",
2787
3028
  items: { type: "string" },
2788
- description: "List of acceptance criteria (plain sentences). No length limit \u2014 include all information that's relevant and necessary, but avoid unnecessary padding to prevent context bottlenecks between agents."
3029
+ description: "List of acceptance criteria (plain sentences). No length limit; avoid padding \u2014 it costs shared context for other agents."
2789
3030
  }
2790
3031
  },
2791
3032
  required: ["title"]
@@ -2793,22 +3034,36 @@ var TOOLS = [
2793
3034
  },
2794
3035
  {
2795
3036
  name: "actions.record_tool",
2796
- description: "Record a tool call made during an action. This is the only way to populate the Tools dashboard. Call once per tool invocation.",
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.",
2797
3038
  inputSchema: {
2798
3039
  type: "object",
2799
3040
  properties: {
2800
- actionId: { type: "string", description: "UUID returned by actions.start" },
2801
- toolName: {
2802
- type: "string",
2803
- description: "Name of the tool that was called (e.g. Read, Bash, Edit)"
2804
- },
2805
- argsJson: {
2806
- type: "string",
2807
- description: "Optional JSON string of the arguments passed to the tool"
2808
- },
2809
- resultSummary: { type: "string", description: "Optional short summary of the tool result" }
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
+ }
2810
3065
  },
2811
- required: ["actionId", "toolName"]
3066
+ required: ["actionId", "calls"]
2812
3067
  }
2813
3068
  },
2814
3069
  {
@@ -2874,7 +3129,7 @@ var TOOLS = [
2874
3129
  ];
2875
3130
  async function startMcpServer(config, cwd2) {
2876
3131
  const db = await openDB(config, cwd2);
2877
- const docsPath = resolve9(cwd2, config.project.docsPath);
3132
+ const docsPath = resolve8(cwd2, config.project.docsPath);
2878
3133
  const server = new Server(
2879
3134
  { name: "agent-harness-kit", version: VERSION },
2880
3135
  { capabilities: { tools: {} } }
@@ -2899,39 +3154,50 @@ async function dispatch(name, args, db, docsPath, cwd2, config) {
2899
3154
  const taskId = num(args, "taskId");
2900
3155
  const agent = str(args, "agent");
2901
3156
  const action = await db.startAction(taskId, agent);
2902
- return ok2(JSON.stringify({ actionId: action.id, taskId, agent, status: "in_progress" }));
3157
+ return ok2(JSON.stringify({ actionId: action.id }));
2903
3158
  }
2904
3159
  case "actions.write": {
2905
- const actionId = str(args, "actionId");
3160
+ const actionId = num(args, "actionId");
2906
3161
  const sectionType = str(args, "sectionType");
2907
3162
  const content = str(args, "content");
2908
3163
  await db.writeSection(actionId, sectionType, content);
2909
- return ok2(JSON.stringify({ actionId, sectionType, recorded: true }));
3164
+ return ok2(JSON.stringify({ recorded: true }));
2910
3165
  }
2911
3166
  case "actions.complete": {
2912
- const actionId = str(args, "actionId");
3167
+ const actionId = num(args, "actionId");
2913
3168
  const summary = str(args, "summary");
2914
3169
  const action = await db.completeAction(actionId, summary);
2915
- return ok2(
2916
- JSON.stringify({ actionId, status: action.status, completedAt: action.completed_at })
2917
- );
3170
+ return ok2(JSON.stringify({ status: action.status, completedAt: action.completed_at }));
2918
3171
  }
2919
3172
  case "actions.get": {
2920
3173
  const taskId = num(args, "taskId");
2921
3174
  const actions = await db.getActionsForTask(taskId);
2922
3175
  const full = await Promise.all(
2923
- actions.map(async (a) => ({
2924
- ...a,
2925
- sections: await db.getActionSections(a.id)
2926
- }))
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
+ })
2927
3193
  );
2928
- return ok2(JSON.stringify(full, null, 2));
3194
+ return ok2(JSON.stringify(full));
2929
3195
  }
2930
3196
  case "tasks.get": {
2931
3197
  const status = args["status"];
2932
3198
  const includeArchived = args["includeArchived"];
2933
3199
  const tasks = status ? await db.getTasks(status, includeArchived ?? false) : await db.getTasks(void 0, includeArchived ?? false);
2934
- return ok2(JSON.stringify(tasks, null, 2));
3200
+ return ok2(JSON.stringify(tasks));
2935
3201
  }
2936
3202
  case "tasks.claim": {
2937
3203
  const id = num(args, "id");
@@ -2962,15 +3228,17 @@ async function dispatch(name, args, db, docsPath, cwd2, config) {
2962
3228
  case "docs.search": {
2963
3229
  const query = str(args, "query");
2964
3230
  const results = searchDocs(docsPath, query);
2965
- return ok2(JSON.stringify(results, null, 2));
3231
+ return ok2(JSON.stringify(results));
2966
3232
  }
2967
3233
  case "actions.record_file": {
2968
- const actionId = str(args, "actionId");
2969
- const filePath = str(args, "filePath");
2970
- const operation = str(args, "operation");
2971
- const notes = args["notes"];
2972
- await db.recordFile(actionId, filePath, operation, notes);
2973
- return ok2(JSON.stringify({ actionId, filePath, operation, recorded: true }));
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 }));
2974
3242
  }
2975
3243
  case "tasks.acceptance.update": {
2976
3244
  const criterionId = num(args, "criterionId");
@@ -2980,15 +3248,17 @@ async function dispatch(name, args, db, docsPath, cwd2, config) {
2980
3248
  case "tasks.acceptance.get": {
2981
3249
  const taskId = num(args, "taskId");
2982
3250
  const criteria = await db.getTaskAcceptance(taskId);
2983
- return ok2(JSON.stringify(criteria, null, 2));
3251
+ return ok2(JSON.stringify(criteria));
2984
3252
  }
2985
3253
  case "actions.record_tool": {
2986
- const actionId = str(args, "actionId");
2987
- const toolName = str(args, "toolName");
2988
- const argsJson = args["argsJson"];
2989
- const resultSummary = args["resultSummary"];
2990
- await db.recordTool(actionId, toolName, argsJson, resultSummary);
2991
- return ok2(JSON.stringify({ actionId, toolName, recorded: true }));
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 }));
2992
3262
  }
2993
3263
  case "tasks.edit": {
2994
3264
  const id = num(args, "id");
@@ -3019,22 +3289,22 @@ async function dispatch(name, args, db, docsPath, cwd2, config) {
3019
3289
  }
3020
3290
  case "permissions.check": {
3021
3291
  const result = checkPermissionsSync(cwd2, config);
3022
- return ok2(JSON.stringify(result, null, 2));
3292
+ return ok2(JSON.stringify(result));
3023
3293
  }
3024
3294
  case "deps.snapshot": {
3025
3295
  const pkgPath2 = join19(cwd2, "package.json");
3026
- if (!existsSync16(pkgPath2)) {
3296
+ if (!existsSync17(pkgPath2)) {
3027
3297
  return ok2("package.json not found in project root", true);
3028
3298
  }
3029
- const pkg2 = JSON.parse(readFileSync9(pkgPath2, "utf8"));
3299
+ const pkg2 = JSON.parse(readFileSync10(pkgPath2, "utf8"));
3030
3300
  const snapshot = {
3031
3301
  capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
3032
3302
  dependencies: pkg2.dependencies ?? {},
3033
3303
  devDependencies: pkg2.devDependencies ?? {}
3034
3304
  };
3035
3305
  const harnessDir = join19(cwd2, ".harness");
3036
- mkdirSync9(harnessDir, { recursive: true });
3037
- writeFileSync10(join19(harnessDir, "deps-lock.json"), JSON.stringify(snapshot, null, 2), "utf8");
3306
+ mkdirSync8(harnessDir, { recursive: true });
3307
+ writeFileSync9(join19(harnessDir, "deps-lock.json"), JSON.stringify(snapshot, null, 2), "utf8");
3038
3308
  return ok2(
3039
3309
  JSON.stringify({
3040
3310
  message: "Snapshot saved to .harness/deps-lock.json",
@@ -3045,10 +3315,10 @@ async function dispatch(name, args, db, docsPath, cwd2, config) {
3045
3315
  case "deps.check": {
3046
3316
  const pkgPath2 = join19(cwd2, "package.json");
3047
3317
  const lockPath = join19(cwd2, ".harness", "deps-lock.json");
3048
- if (!existsSync16(pkgPath2)) {
3318
+ if (!existsSync17(pkgPath2)) {
3049
3319
  return ok2("package.json not found in project root", true);
3050
3320
  }
3051
- if (!existsSync16(lockPath)) {
3321
+ if (!existsSync17(lockPath)) {
3052
3322
  return ok2(
3053
3323
  JSON.stringify({
3054
3324
  status: "no-snapshot",
@@ -3056,8 +3326,8 @@ async function dispatch(name, args, db, docsPath, cwd2, config) {
3056
3326
  })
3057
3327
  );
3058
3328
  }
3059
- const pkg2 = JSON.parse(readFileSync9(pkgPath2, "utf8"));
3060
- const lock = JSON.parse(readFileSync9(lockPath, "utf8"));
3329
+ const pkg2 = JSON.parse(readFileSync10(pkgPath2, "utf8"));
3330
+ const lock = JSON.parse(readFileSync10(lockPath, "utf8"));
3061
3331
  const current = { ...pkg2.dependencies ?? {}, ...pkg2.devDependencies ?? {} };
3062
3332
  const previous = { ...lock.dependencies ?? {}, ...lock.devDependencies ?? {} };
3063
3333
  const added = [];
@@ -3111,7 +3381,7 @@ async function dispatch(name, args, db, docsPath, cwd2, config) {
3111
3381
  ok: status.skills.filter((s) => s.status === "ok").map((s) => s.name)
3112
3382
  }
3113
3383
  };
3114
- return ok2(JSON.stringify(result, null, 2));
3384
+ return ok2(JSON.stringify(result));
3115
3385
  }
3116
3386
  default:
3117
3387
  return ok2(`Unknown tool: ${name}`, true);
@@ -3125,7 +3395,7 @@ function searchDocs(docsPath, query, maxResults = 10) {
3125
3395
  for (const file of files) {
3126
3396
  if (results.length >= maxResults) break;
3127
3397
  try {
3128
- const content = readFileSync9(file, "utf8");
3398
+ const content = readFileSync10(file, "utf8");
3129
3399
  const lines = content.split("\n");
3130
3400
  for (let i = 0; i < lines.length; i++) {
3131
3401
  const lower = lines[i].toLowerCase();
@@ -3175,6 +3445,18 @@ function num(args, key) {
3175
3445
  if (typeof v4 !== "number") throw new Error(`${key} must be a number`);
3176
3446
  return v4;
3177
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
+ }
3178
3460
 
3179
3461
  // src/commands/serve.ts
3180
3462
  async function runServe(cwd2, opts) {
@@ -3268,13 +3550,13 @@ async function runStatus(cwd2, opts) {
3268
3550
  }
3269
3551
 
3270
3552
  // src/commands/sync.ts
3271
- import { existsSync as existsSync17, readFileSync as readFileSync10 } from "fs";
3272
- import { join as join20, resolve as resolve10 } from "path";
3553
+ import { existsSync as existsSync18, readFileSync as readFileSync11 } from "fs";
3554
+ import { join as join20, resolve as resolve9 } from "path";
3273
3555
  import pc13 from "picocolors";
3274
3556
  async function runSync(cwd2, opts) {
3275
3557
  const config = await loadConfig(cwd2);
3276
3558
  const direction = opts.direction ?? "both";
3277
- const featureListPath = resolve10(join20(cwd2, config.storage.dir, "feature_list.json"));
3559
+ const featureListPath = resolve9(join20(cwd2, config.storage.dir, "feature_list.json"));
3278
3560
  const db = await openDB(config, cwd2);
3279
3561
  try {
3280
3562
  if (direction === "in" || direction === "both") {
@@ -3288,13 +3570,13 @@ async function runSync(cwd2, opts) {
3288
3570
  }
3289
3571
  }
3290
3572
  async function syncIn(featureListPath, db, dryRun) {
3291
- if (!existsSync17(featureListPath)) {
3573
+ if (!existsSync18(featureListPath)) {
3292
3574
  console.log(pc13.dim(`feature_list.json not found at ${featureListPath} \u2014 skipping in-sync`));
3293
3575
  return;
3294
3576
  }
3295
3577
  let seeds;
3296
3578
  try {
3297
- seeds = JSON.parse(readFileSync10(featureListPath, "utf8"));
3579
+ seeds = JSON.parse(readFileSync11(featureListPath, "utf8"));
3298
3580
  } catch (err) {
3299
3581
  console.error(pc13.red(`Failed to parse feature_list.json: ${err}`));
3300
3582
  process.exit(1);
@@ -3379,14 +3661,14 @@ async function runTaskAdd(cwd2) {
3379
3661
 
3380
3662
  // src/commands/task/done.ts
3381
3663
  import { spawnSync as spawnSync2 } from "child_process";
3382
- import { existsSync as existsSync18 } from "fs";
3383
- import { resolve as resolve11 } from "path";
3664
+ import { existsSync as existsSync19 } from "fs";
3665
+ import { resolve as resolve10 } from "path";
3384
3666
  import pc15 from "picocolors";
3385
3667
  async function runTaskDone(cwd2, idOrSlug) {
3386
3668
  const config = await loadConfig(cwd2);
3387
3669
  if (config.health.required) {
3388
- const scriptPath = resolve11(cwd2, config.health.scriptPath);
3389
- if (existsSync18(scriptPath)) {
3670
+ const scriptPath = resolve10(cwd2, config.health.scriptPath);
3671
+ if (existsSync19(scriptPath)) {
3390
3672
  const result = spawnSync2("bash", [scriptPath], { cwd: cwd2, stdio: "pipe", encoding: "utf8" });
3391
3673
  if (result.status !== 0) {
3392
3674
  console.error(pc15.red("\u2717 Health check failed \u2014 cannot mark task as done."));
@@ -3557,27 +3839,93 @@ async function runTaskList(cwd2, opts) {
3557
3839
  }
3558
3840
  }
3559
3841
 
3560
- // src/core/update-check.ts
3842
+ // src/core/path-probe.ts
3843
+ import { accessSync, constants, readdirSync as readdirSync3 } from "fs";
3844
+ import { join as join21 } from "path";
3561
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) {
3888
+ try {
3889
+ if (isExecutable(join21(dir, name))) return true;
3890
+ } catch {
3891
+ continue;
3892
+ }
3893
+ }
3894
+ return false;
3895
+ }
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)`));
3906
+ }
3907
+
3908
+ // src/core/update-check.ts
3909
+ import pc19 from "picocolors";
3562
3910
  var REGISTRY_URL2 = `https://registry.npmjs.org/${pkg.name}/latest`;
3563
3911
  var TIMEOUT_MS2 = 2500;
3564
3912
  function checkForUpdate(currentVersion) {
3565
- return new Promise((resolve12) => {
3566
- const timer = setTimeout(() => resolve12(null), TIMEOUT_MS2);
3913
+ return new Promise((resolve11) => {
3914
+ const timer = setTimeout(() => resolve11(null), TIMEOUT_MS2);
3567
3915
  fetch(REGISTRY_URL2).then((res) => res.json()).then((data) => {
3568
3916
  clearTimeout(timer);
3569
3917
  const latest = data.version;
3570
- resolve12(isNewer2(latest, currentVersion) ? { current: currentVersion, latest } : null);
3918
+ resolve11(isNewer2(latest, currentVersion) ? { current: currentVersion, latest } : null);
3571
3919
  }).catch(() => {
3572
3920
  clearTimeout(timer);
3573
- resolve12(null);
3921
+ resolve11(null);
3574
3922
  });
3575
3923
  });
3576
3924
  }
3577
3925
  function printUpdateMessage({ current, latest }) {
3578
3926
  const lines = [
3579
- ` Update available ${pc18.dim(current)} \u2192 ${pc18.green(latest)} `,
3580
- ` 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}`)} `
3581
3929
  ];
3582
3930
  drawBox(lines);
3583
3931
  }
@@ -3592,6 +3940,18 @@ function isNewer2(latest, current) {
3592
3940
 
3593
3941
  // src/cli.ts
3594
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
+ }
3595
3955
  var updateCheck = checkForUpdate(pkg.version);
3596
3956
  var program = new Command();
3597
3957
  program.name("ahk").description("agent-harness-kit \u2014 CLI scaffolding for multi-agent harness systems").version(pkg.version, "-v, --version");
@@ -3613,7 +3973,7 @@ program.command("status").description("Show task table and active actions").opti
3613
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) => {
3614
3974
  await runSync(cwd, { dryRun: opts["dry-run"], direction: opts.direction });
3615
3975
  });
3616
- 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) => {
3617
3977
  await runServe(cwd, { port: opts.port });
3618
3978
  });
3619
3979
  var task = program.command("task").description("Manage tasks");
@@ -3629,8 +3989,8 @@ task.command("done <id|slug>").description("Mark a task as done").action(async (
3629
3989
  task.command("edit").description("Edit a task interactively").action(async () => {
3630
3990
  await runTaskEdit(cwd);
3631
3991
  });
3632
- program.command("dashboard").description("Open web dashboard to visualize harness data").option("-p, --port <port>", "Port to listen on", "4242").option("--no-open", "Do not open browser automatically").action(async (opts) => {
3633
- await runDashboard(cwd, { port: parseInt(opts.port), open: opts.open });
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 });
3634
3994
  });
3635
3995
  var migrate = program.command("migrate").description("Migrate provider files to a different provider, or migrate harness storage (see subcommands)");
3636
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) => {
@@ -3642,7 +4002,7 @@ migrate.command("storage").description(
3642
4002
  try {
3643
4003
  await runMigrateStorage(cwd, { force: opts.force, dryRun: opts["dry-run"] });
3644
4004
  } catch (err) {
3645
- 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)}`));
3646
4006
  process.exit(1);
3647
4007
  }
3648
4008
  });
@@ -3658,6 +4018,9 @@ program.command("doctor").description("Check lib version, agent files, and harne
3658
4018
  program.hook("preAction", () => {
3659
4019
  if (!isLocalInstallSatisfied(cwd)) {
3660
4020
  printLocalInstallWarning();
4021
+ if (!isExecutableOnPath("ahk")) {
4022
+ printMissingGlobalBinaryWarning();
4023
+ }
3661
4024
  }
3662
4025
  });
3663
4026
  program.hook("postAction", async () => {