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