@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/README.md +42 -10
- package/dist/agent-templates/builder.md +20 -14
- package/dist/agent-templates/explorer.md +11 -8
- package/dist/agent-templates/lead.md +10 -7
- package/dist/agent-templates/reviewer.md +9 -5
- package/dist/{chunk-6PEIJ2D5.js → chunk-JTACLEGM.js} +45 -16
- package/dist/chunk-JTACLEGM.js.map +1 -0
- package/dist/chunk-URMVLD2S.js +147 -0
- package/dist/chunk-URMVLD2S.js.map +1 -0
- package/dist/cli.js +770 -407
- package/dist/cli.js.map +1 -1
- package/dist/{db-3OXHRFAR.js → db-L3AADJF5.js} +4 -2
- package/dist/index.d.ts +1 -1
- package/dist/{mysql-THKQOXIS.js → mysql-AUPKARWA.js} +11 -5
- package/dist/mysql-AUPKARWA.js.map +1 -0
- package/dist/{postgres-IOQE32DM.js → postgres-BB4GY4PN.js} +11 -5
- package/dist/postgres-BB4GY4PN.js.map +1 -0
- package/dist/{sqlite-TR4D324R.js → sqlite-5OWKTUUZ.js} +11 -5
- package/dist/sqlite-5OWKTUUZ.js.map +1 -0
- package/package.json +2 -1
- package/dist/chunk-6PEIJ2D5.js.map +0 -1
- package/dist/mysql-THKQOXIS.js.map +0 -1
- package/dist/postgres-IOQE32DM.js.map +0 -1
- package/dist/sqlite-TR4D324R.js.map +0 -1
- /package/dist/{db-3OXHRFAR.js.map → db-L3AADJF5.js.map} +0 -0
package/dist/cli.js
CHANGED
|
@@ -12,20 +12,20 @@ import {
|
|
|
12
12
|
resolveGlobalStorageDir,
|
|
13
13
|
resolveSqlitePath,
|
|
14
14
|
resolveSqlitePathForScope
|
|
15
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-JTACLEGM.js";
|
|
16
16
|
|
|
17
17
|
// src/cli.ts
|
|
18
|
-
import { Command } from "commander";
|
|
19
|
-
import
|
|
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
|
|
24
|
+
import pc2 from "picocolors";
|
|
25
25
|
|
|
26
26
|
// src/core/materializer/claude-code.ts
|
|
27
|
-
import { existsSync as
|
|
28
|
-
import { join as
|
|
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
|
|
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 (
|
|
46
|
-
if (
|
|
47
|
-
if (
|
|
48
|
-
return
|
|
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 (
|
|
102
|
+
if (existsSync3(join4(cwd2, "package-lock.json"))) return "npm";
|
|
51
103
|
return "npm";
|
|
52
104
|
}
|
|
53
105
|
function detectFromPackageManagerField(cwd2) {
|
|
54
|
-
const pkgPath2 =
|
|
55
|
-
if (!
|
|
106
|
+
const pkgPath2 = join4(cwd2, "package.json");
|
|
107
|
+
if (!existsSync3(pkgPath2)) return null;
|
|
56
108
|
try {
|
|
57
|
-
const pkg2 = JSON.parse(
|
|
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
|
|
99
|
-
import { dirname } from "path";
|
|
100
|
-
function mergeClaudeMcpJson(filePath, port, pm = "npm") {
|
|
101
|
-
const folderPath =
|
|
102
|
-
if (!
|
|
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 (
|
|
161
|
+
if (existsSync4(filePath)) {
|
|
107
162
|
try {
|
|
108
|
-
existing = JSON.parse(
|
|
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(
|
|
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(
|
|
183
|
+
mkdirSync2(dirname2(filePath), { recursive: true });
|
|
129
184
|
let existing = {};
|
|
130
|
-
if (
|
|
185
|
+
if (existsSync4(filePath)) {
|
|
131
186
|
try {
|
|
132
|
-
existing = JSON.parse(
|
|
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(
|
|
290
|
+
mkdirSync2(dirname2(filePath), { recursive: true });
|
|
236
291
|
let existing = {};
|
|
237
|
-
if (
|
|
292
|
+
if (existsSync4(filePath)) {
|
|
238
293
|
try {
|
|
239
|
-
existing = JSON.parse(
|
|
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 =
|
|
260
|
-
if (!
|
|
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 (
|
|
319
|
+
if (existsSync4(filePath)) {
|
|
265
320
|
try {
|
|
266
|
-
existing = JSON.parse(
|
|
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(
|
|
368
|
+
function mergeCodexConfigToml(filePath, port, cwd2, pm = "npm") {
|
|
369
|
+
mkdirSync2(dirname2(filePath), { recursive: true });
|
|
315
370
|
let content = "";
|
|
316
|
-
if (
|
|
317
|
-
content =
|
|
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 {
|
|
331
|
-
import {
|
|
332
|
-
import {
|
|
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
|
|
336
|
-
import { dirname as
|
|
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 =
|
|
370
|
-
var TEMPLATES_DIR =
|
|
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 =
|
|
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
|
|
430
|
-
actions.record_file actionId
|
|
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
|
-
-
|
|
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
|
|
514
|
-
actions.record_file actionId
|
|
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
|
-
-
|
|
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 =
|
|
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) =>
|
|
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 =
|
|
872
|
+
const backupDir = join6(opts.backupRoot, `agents-${stamp}`);
|
|
812
873
|
try {
|
|
813
874
|
for (const entry of existing) {
|
|
814
|
-
const dest =
|
|
875
|
+
const dest = join6(backupDir, entry.relPath);
|
|
815
876
|
mkdirSync3(resolve2(dest, ".."), { recursive: true });
|
|
816
|
-
writeFileSync3(dest,
|
|
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 =
|
|
827
|
-
const exists =
|
|
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 =
|
|
841
|
-
const existing =
|
|
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 =
|
|
854
|
-
const destDir =
|
|
855
|
-
const dest =
|
|
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,
|
|
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 (!
|
|
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
|
-
|
|
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(
|
|
897
|
-
mergeClaudeSettingsJson(
|
|
898
|
-
mergeClaudeSettingsLocalJson(
|
|
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
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
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:
|
|
1069
|
+
backupRoot: join7(cwd2, config.storage.dir, "backups")
|
|
913
1070
|
});
|
|
914
|
-
mergeClaudeMcpJson(
|
|
915
|
-
mergeClaudeSettingsJson(
|
|
916
|
-
mergeClaudeSettingsLocalJson(
|
|
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
|
|
946
|
-
import { join as
|
|
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 =
|
|
963
|
-
|
|
964
|
-
|
|
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 (!
|
|
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
|
-
|
|
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(
|
|
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
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
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:
|
|
1152
|
+
backupRoot: join8(cwd2, config.storage.dir, "backups")
|
|
999
1153
|
});
|
|
1000
|
-
mergeCodexConfigToml(
|
|
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
|
|
1014
|
-
import { join as
|
|
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 =
|
|
1030
|
-
|
|
1031
|
-
|
|
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 (!
|
|
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
|
-
|
|
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(
|
|
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
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
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:
|
|
1216
|
+
backupRoot: join9(cwd2, config.storage.dir, "backups")
|
|
1066
1217
|
});
|
|
1067
|
-
mergeOpencodeJson(
|
|
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(
|
|
1123
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
|
1159
|
-
import { fileURLToPath as
|
|
1160
|
-
import
|
|
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
|
|
1165
|
-
import { extname, join as
|
|
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
|
-
|
|
1173
|
-
|
|
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", () =>
|
|
1356
|
+
server.once("error", () => resolve11(false));
|
|
1176
1357
|
server.once("listening", () => {
|
|
1177
|
-
server.close(() =>
|
|
1358
|
+
server.close(() => resolve11(true));
|
|
1178
1359
|
});
|
|
1179
|
-
server.listen(port,
|
|
1360
|
+
server.listen(port, host);
|
|
1180
1361
|
});
|
|
1181
1362
|
}
|
|
1182
|
-
async function findFreePort(start,
|
|
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 =
|
|
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 =
|
|
1314
|
-
if (
|
|
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(
|
|
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({
|
|
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 =
|
|
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 =
|
|
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 =
|
|
1597
|
+
const staticPath = join11(__dirname3, "dashboard-dist");
|
|
1373
1598
|
const { url } = await startDashboardServer(db, dbPath, staticPath, opts.port);
|
|
1374
|
-
console.log(
|
|
1375
|
-
console.log(
|
|
1376
|
-
console.log(
|
|
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
|
|
1614
|
+
import pc4 from "picocolors";
|
|
1390
1615
|
|
|
1391
1616
|
// src/core/doctor.ts
|
|
1392
|
-
import { existsSync as
|
|
1393
|
-
import { dirname as dirname6, join as
|
|
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 =
|
|
1462
|
-
return { name, status:
|
|
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(
|
|
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 =
|
|
1694
|
+
const skillSourceBase = join12(__dirname4, "skills");
|
|
1483
1695
|
return SKILL_NAMES.map((name) => {
|
|
1484
|
-
const livePath =
|
|
1485
|
-
const sourcePath =
|
|
1486
|
-
if (!
|
|
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 =
|
|
1491
|
-
const source =
|
|
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(
|
|
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(` ${
|
|
1734
|
+
console.log(` ${pc4.cyan(label.padEnd(16))}${pc4.green("[\u2713]")} ${detail}`);
|
|
1523
1735
|
}
|
|
1524
1736
|
function warn(label, detail, hint) {
|
|
1525
|
-
console.log(` ${
|
|
1737
|
+
console.log(` ${pc4.cyan(label.padEnd(16))}${pc4.yellow("[!]")} ${detail}`);
|
|
1526
1738
|
if (hint) {
|
|
1527
|
-
console.log(` ${"".padEnd(16)} ${
|
|
1739
|
+
console.log(` ${"".padEnd(16)} ${pc4.dim(hint)}`);
|
|
1528
1740
|
}
|
|
1529
1741
|
}
|
|
1530
1742
|
function neutral(label, detail) {
|
|
1531
|
-
console.log(` ${
|
|
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(
|
|
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
|
|
1602
|
-
import
|
|
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(
|
|
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
|
-
|
|
1616
|
-
console.log(
|
|
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(
|
|
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
|
|
1844
|
+
import { existsSync as existsSync11 } from "fs";
|
|
1633
1845
|
import { homedir as homedir2 } from "os";
|
|
1634
|
-
import { join as
|
|
1635
|
-
import
|
|
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 ?
|
|
1638
|
-
const icon = ok3 ?
|
|
1639
|
-
console.log(prefix + icon + " " + (ok3 ?
|
|
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(
|
|
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 =
|
|
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 =
|
|
1667
|
-
const ok3 =
|
|
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 =
|
|
1679
|
-
const mcpOk =
|
|
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(
|
|
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 =
|
|
1689
|
-
if (!
|
|
1690
|
-
console.error(
|
|
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(
|
|
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(
|
|
1916
|
+
console.log(pc6.green("\u2713 Health check passed"));
|
|
1705
1917
|
process.exit(0);
|
|
1706
1918
|
} else {
|
|
1707
|
-
console.error(
|
|
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
|
|
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
|
-
|
|
2106
|
-
|
|
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
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
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
|
|
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
|
|
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") :
|
|
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 =
|
|
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
|
-
|
|
2231
|
-
|
|
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
|
-
|
|
2470
|
+
mkdirSync7(dirname7(destPath), { recursive: true });
|
|
2241
2471
|
copyFileSync(srcPath, destPath);
|
|
2242
2472
|
for (const suffix of ["-wal", "-shm"]) {
|
|
2243
|
-
if (
|
|
2473
|
+
if (existsSync14(`${srcPath}${suffix}`)) {
|
|
2244
2474
|
copyFileSync(`${srcPath}${suffix}`, `${destPath}${suffix}`);
|
|
2245
2475
|
}
|
|
2246
2476
|
}
|
|
2247
|
-
if (!
|
|
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 (!
|
|
2307
|
-
const { SQLiteDriver } = await import("./sqlite-
|
|
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 (!
|
|
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 =
|
|
2556
|
+
const destExists = existsSync14(destDb);
|
|
2327
2557
|
if (destExists) {
|
|
2328
|
-
const { SQLiteDriver } = await import("./sqlite-
|
|
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-
|
|
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-
|
|
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 (
|
|
2365
|
-
|
|
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 (
|
|
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 (!
|
|
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-
|
|
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-
|
|
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
|
|
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
|
|
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 =
|
|
2456
|
-
if (!
|
|
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 =
|
|
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 &&
|
|
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 (
|
|
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
|
|
2584
|
-
import { join as join19, resolve as
|
|
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
|
|
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 =
|
|
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
|
|
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.
|
|
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: "
|
|
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
|
|
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: "
|
|
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
|
|
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: "
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
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", "
|
|
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
|
|
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
|
|
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
|
|
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: "
|
|
2801
|
-
|
|
2802
|
-
type: "
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
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", "
|
|
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 =
|
|
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
|
|
3157
|
+
return ok2(JSON.stringify({ actionId: action.id }));
|
|
2903
3158
|
}
|
|
2904
3159
|
case "actions.write": {
|
|
2905
|
-
const 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({
|
|
3164
|
+
return ok2(JSON.stringify({ recorded: true }));
|
|
2910
3165
|
}
|
|
2911
3166
|
case "actions.complete": {
|
|
2912
|
-
const 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
|
-
|
|
2925
|
-
|
|
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
|
|
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
|
|
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
|
|
3231
|
+
return ok2(JSON.stringify(results));
|
|
2966
3232
|
}
|
|
2967
3233
|
case "actions.record_file": {
|
|
2968
|
-
const actionId =
|
|
2969
|
-
const
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
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
|
|
3251
|
+
return ok2(JSON.stringify(criteria));
|
|
2984
3252
|
}
|
|
2985
3253
|
case "actions.record_tool": {
|
|
2986
|
-
const actionId =
|
|
2987
|
-
const
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
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
|
|
3292
|
+
return ok2(JSON.stringify(result));
|
|
3023
3293
|
}
|
|
3024
3294
|
case "deps.snapshot": {
|
|
3025
3295
|
const pkgPath2 = join19(cwd2, "package.json");
|
|
3026
|
-
if (!
|
|
3296
|
+
if (!existsSync17(pkgPath2)) {
|
|
3027
3297
|
return ok2("package.json not found in project root", true);
|
|
3028
3298
|
}
|
|
3029
|
-
const pkg2 = JSON.parse(
|
|
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
|
-
|
|
3037
|
-
|
|
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 (!
|
|
3318
|
+
if (!existsSync17(pkgPath2)) {
|
|
3049
3319
|
return ok2("package.json not found in project root", true);
|
|
3050
3320
|
}
|
|
3051
|
-
if (!
|
|
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(
|
|
3060
|
-
const lock = JSON.parse(
|
|
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
|
|
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 =
|
|
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
|
|
3272
|
-
import { join as join20, resolve as
|
|
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 =
|
|
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 (!
|
|
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(
|
|
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
|
|
3383
|
-
import { resolve as
|
|
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 =
|
|
3389
|
-
if (
|
|
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/
|
|
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((
|
|
3566
|
-
const timer = setTimeout(() =>
|
|
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
|
-
|
|
3918
|
+
resolve11(isNewer2(latest, currentVersion) ? { current: currentVersion, latest } : null);
|
|
3571
3919
|
}).catch(() => {
|
|
3572
3920
|
clearTimeout(timer);
|
|
3573
|
-
|
|
3921
|
+
resolve11(null);
|
|
3574
3922
|
});
|
|
3575
3923
|
});
|
|
3576
3924
|
}
|
|
3577
3925
|
function printUpdateMessage({ current, latest }) {
|
|
3578
3926
|
const lines = [
|
|
3579
|
-
` Update available ${
|
|
3580
|
-
` Run: ${
|
|
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)",
|
|
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",
|
|
3633
|
-
await runDashboard(cwd, { port:
|
|
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(
|
|
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 () => {
|