@cardor/agent-harness-kit 1.8.1 → 1.10.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/LICENSE +80 -58
- package/README.md +126 -44
- package/dist/agent-templates/builder.md +13 -0
- package/dist/{chunk-OEPZRC7J.js → chunk-ADV7OPU2.js} +4 -1
- package/dist/chunk-ADV7OPU2.js.map +1 -0
- package/dist/chunk-DNFFWQWR.js +821 -0
- package/dist/chunk-DNFFWQWR.js.map +1 -0
- package/dist/cli.js +952 -905
- package/dist/cli.js.map +1 -1
- package/dist/dashboard-dist/assets/index-CyU-X1yO.js +9 -0
- package/dist/dashboard-dist/assets/index-CzEB2a6I.css +1 -0
- package/dist/dashboard-dist/index.html +2 -2
- package/dist/db-QQ7BR5K7.js +23 -0
- package/dist/db-QQ7BR5K7.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +1 -1
- package/dist/skills/ahk-review/SKILL.md +45 -0
- package/dist/{sqlite-KWYK4IJW.js → sqlite-TR4D324R.js} +5 -5
- package/dist/{sqlite-KWYK4IJW.js.map → sqlite-TR4D324R.js.map} +1 -1
- package/package.json +3 -2
- package/dist/chunk-OEPZRC7J.js.map +0 -1
- package/dist/dashboard-dist/assets/index-6UCLKb-M.css +0 -1
- package/dist/dashboard-dist/assets/index-CoqlHfTu.js +0 -9
package/dist/cli.js
CHANGED
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
import {
|
|
2
2
|
findConfigFile,
|
|
3
3
|
loadConfig
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-ADV7OPU2.js";
|
|
5
|
+
import {
|
|
6
|
+
getRowCounts,
|
|
7
|
+
isEmptyDatabase,
|
|
8
|
+
openDB,
|
|
9
|
+
readStorageStateFile,
|
|
10
|
+
resolveGlobalStorageDir,
|
|
11
|
+
resolveSqlitePathForScope
|
|
12
|
+
} from "./chunk-DNFFWQWR.js";
|
|
5
13
|
|
|
6
14
|
// src/cli.ts
|
|
7
15
|
import { Command } from "commander";
|
|
16
|
+
import pc19 from "picocolors";
|
|
8
17
|
|
|
9
18
|
// src/commands/build.ts
|
|
10
19
|
import { watch } from "fs";
|
|
@@ -12,8 +21,8 @@ import * as p from "@clack/prompts";
|
|
|
12
21
|
import pc from "picocolors";
|
|
13
22
|
|
|
14
23
|
// src/core/materializer/claude-code.ts
|
|
15
|
-
import { existsSync as
|
|
16
|
-
import { join as
|
|
24
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
|
|
25
|
+
import { join as join5, resolve as resolve3 } from "path";
|
|
17
26
|
|
|
18
27
|
// src/utils/file.ts
|
|
19
28
|
import { mkdirSync, writeFileSync } from "fs";
|
|
@@ -24,29 +33,88 @@ var write = (cwd2, relPath, content, mode) => {
|
|
|
24
33
|
writeFileSync(abs, content, { encoding: "utf8", mode });
|
|
25
34
|
};
|
|
26
35
|
|
|
36
|
+
// src/core/materializer/detect-package-manager.ts
|
|
37
|
+
import { existsSync, readFileSync } from "fs";
|
|
38
|
+
import { join as join2 } from "path";
|
|
39
|
+
function detectPackageManager(cwd2) {
|
|
40
|
+
const fromField = detectFromPackageManagerField(cwd2);
|
|
41
|
+
if (fromField) return fromField;
|
|
42
|
+
if (existsSync(join2(cwd2, "pnpm-lock.yaml"))) return "pnpm";
|
|
43
|
+
if (existsSync(join2(cwd2, "bun.lockb")) || existsSync(join2(cwd2, "bun.lock"))) return "bun";
|
|
44
|
+
if (existsSync(join2(cwd2, "yarn.lock"))) {
|
|
45
|
+
return existsSync(join2(cwd2, ".yarnrc.yml")) ? "yarn-berry" : "yarn-classic";
|
|
46
|
+
}
|
|
47
|
+
if (existsSync(join2(cwd2, "package-lock.json"))) return "npm";
|
|
48
|
+
return "npm";
|
|
49
|
+
}
|
|
50
|
+
function detectFromPackageManagerField(cwd2) {
|
|
51
|
+
const pkgPath2 = join2(cwd2, "package.json");
|
|
52
|
+
if (!existsSync(pkgPath2)) return null;
|
|
53
|
+
try {
|
|
54
|
+
const pkg2 = JSON.parse(readFileSync(pkgPath2, "utf8"));
|
|
55
|
+
const field = pkg2?.packageManager;
|
|
56
|
+
if (typeof field !== "string" || !field.trim()) return null;
|
|
57
|
+
const match = field.match(/^([a-z]+)@(\d+)/i);
|
|
58
|
+
if (!match) return null;
|
|
59
|
+
const [, rawName, majorStr] = match;
|
|
60
|
+
const name = rawName.toLowerCase();
|
|
61
|
+
const major = Number(majorStr);
|
|
62
|
+
switch (name) {
|
|
63
|
+
case "npm":
|
|
64
|
+
return "npm";
|
|
65
|
+
case "pnpm":
|
|
66
|
+
return "pnpm";
|
|
67
|
+
case "bun":
|
|
68
|
+
return "bun";
|
|
69
|
+
case "yarn":
|
|
70
|
+
return major >= 2 ? "yarn-berry" : "yarn-classic";
|
|
71
|
+
default:
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
} catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function getMcpCommandParts(pm, port) {
|
|
79
|
+
const portStr = String(port);
|
|
80
|
+
switch (pm) {
|
|
81
|
+
case "pnpm":
|
|
82
|
+
return ["pnpm", "exec", "ahk", "serve", "--port", portStr];
|
|
83
|
+
case "yarn-classic":
|
|
84
|
+
case "yarn-berry":
|
|
85
|
+
return ["yarn", "run", "ahk", "serve", "--port", portStr];
|
|
86
|
+
case "bun":
|
|
87
|
+
return ["bunx", "--no-install", "ahk", "serve", "--port", portStr];
|
|
88
|
+
case "npm":
|
|
89
|
+
default:
|
|
90
|
+
return ["npx", "--no", "ahk", "serve", "--port", portStr];
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
27
94
|
// src/core/materializer/mcp-merge.ts
|
|
28
|
-
import { existsSync, mkdirSync as mkdirSync2, readFileSync, writeFileSync as writeFileSync2 } from "fs";
|
|
95
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
29
96
|
import { dirname } from "path";
|
|
30
|
-
function mergeClaudeMcpJson(filePath, port) {
|
|
97
|
+
function mergeClaudeMcpJson(filePath, port, pm = "npm") {
|
|
31
98
|
const folderPath = dirname(filePath);
|
|
32
|
-
if (!
|
|
99
|
+
if (!existsSync2(folderPath)) {
|
|
33
100
|
mkdirSync2(folderPath, { recursive: true });
|
|
34
101
|
}
|
|
35
102
|
let existing = {};
|
|
36
|
-
if (
|
|
103
|
+
if (existsSync2(filePath)) {
|
|
37
104
|
try {
|
|
38
|
-
existing = JSON.parse(
|
|
105
|
+
existing = JSON.parse(readFileSync2(filePath, "utf8"));
|
|
39
106
|
} catch {
|
|
40
107
|
}
|
|
41
108
|
}
|
|
109
|
+
const [command, ...args] = getMcpCommandParts(pm, port);
|
|
42
110
|
const merged = {
|
|
43
111
|
...existing,
|
|
44
112
|
mcpServers: {
|
|
45
113
|
...existing.mcpServers ?? {},
|
|
46
114
|
"agent-harness-kit": {
|
|
47
115
|
type: "stdio",
|
|
48
|
-
command
|
|
49
|
-
args
|
|
116
|
+
command,
|
|
117
|
+
args
|
|
50
118
|
}
|
|
51
119
|
}
|
|
52
120
|
};
|
|
@@ -56,9 +124,9 @@ function mergeClaudeMcpJson(filePath, port) {
|
|
|
56
124
|
function mergeClaudeSettingsJson(filePath) {
|
|
57
125
|
mkdirSync2(dirname(filePath), { recursive: true });
|
|
58
126
|
let existing = {};
|
|
59
|
-
if (
|
|
127
|
+
if (existsSync2(filePath)) {
|
|
60
128
|
try {
|
|
61
|
-
existing = JSON.parse(
|
|
129
|
+
existing = JSON.parse(readFileSync2(filePath, "utf8"));
|
|
62
130
|
} catch {
|
|
63
131
|
}
|
|
64
132
|
}
|
|
@@ -163,9 +231,9 @@ var MCP_CLAUDE_PERMISSIONS = [
|
|
|
163
231
|
function mergeClaudeSettingsLocalJson(filePath) {
|
|
164
232
|
mkdirSync2(dirname(filePath), { recursive: true });
|
|
165
233
|
let existing = {};
|
|
166
|
-
if (
|
|
234
|
+
if (existsSync2(filePath)) {
|
|
167
235
|
try {
|
|
168
|
-
existing = JSON.parse(
|
|
236
|
+
existing = JSON.parse(readFileSync2(filePath, "utf8"));
|
|
169
237
|
} catch {
|
|
170
238
|
}
|
|
171
239
|
}
|
|
@@ -184,15 +252,15 @@ function mergeClaudeSettingsLocalJson(filePath) {
|
|
|
184
252
|
};
|
|
185
253
|
writeFileSync2(filePath, JSON.stringify(merged, null, 2) + "\n", "utf8");
|
|
186
254
|
}
|
|
187
|
-
function mergeOpencodeJson(filePath, port) {
|
|
255
|
+
function mergeOpencodeJson(filePath, port, pm = "npm") {
|
|
188
256
|
const folderPath = dirname(filePath);
|
|
189
|
-
if (!
|
|
257
|
+
if (!existsSync2(folderPath)) {
|
|
190
258
|
mkdirSync2(folderPath, { recursive: true });
|
|
191
259
|
}
|
|
192
260
|
let existing = {};
|
|
193
|
-
if (
|
|
261
|
+
if (existsSync2(filePath)) {
|
|
194
262
|
try {
|
|
195
|
-
existing = JSON.parse(
|
|
263
|
+
existing = JSON.parse(readFileSync2(filePath, "utf8"));
|
|
196
264
|
} catch {
|
|
197
265
|
}
|
|
198
266
|
}
|
|
@@ -207,7 +275,9 @@ function mergeOpencodeJson(filePath, port) {
|
|
|
207
275
|
"agent-harness-kit": {
|
|
208
276
|
enabled: true,
|
|
209
277
|
type: "local",
|
|
210
|
-
command
|
|
278
|
+
// OpenCode's mcp.<name>.command field is a single array (unlike
|
|
279
|
+
// Claude/Codex, which split command/args) — pass the full token list.
|
|
280
|
+
command: getMcpCommandParts(pm, port)
|
|
211
281
|
}
|
|
212
282
|
}
|
|
213
283
|
};
|
|
@@ -237,15 +307,16 @@ function mergeTomlSection(content, sectionName, sectionBody) {
|
|
|
237
307
|
];
|
|
238
308
|
return newLines.join("\n");
|
|
239
309
|
}
|
|
240
|
-
function mergeCodexConfigToml(filePath, port) {
|
|
310
|
+
function mergeCodexConfigToml(filePath, port, pm = "npm") {
|
|
241
311
|
mkdirSync2(dirname(filePath), { recursive: true });
|
|
242
312
|
let content = "";
|
|
243
|
-
if (
|
|
244
|
-
content =
|
|
313
|
+
if (existsSync2(filePath)) {
|
|
314
|
+
content = readFileSync2(filePath, "utf8");
|
|
245
315
|
}
|
|
316
|
+
const [command, ...args] = getMcpCommandParts(pm, port);
|
|
246
317
|
const sectionBody = [
|
|
247
|
-
|
|
248
|
-
`args =
|
|
318
|
+
`command = ${JSON.stringify(command)}`,
|
|
319
|
+
`args = ${JSON.stringify(args)}`,
|
|
249
320
|
'default_tools_approval_mode = "auto"'
|
|
250
321
|
].join("\n");
|
|
251
322
|
content = mergeTomlSection(content, "mcp_servers.agent-harness-kit", sectionBody);
|
|
@@ -253,18 +324,18 @@ function mergeCodexConfigToml(filePath, port) {
|
|
|
253
324
|
}
|
|
254
325
|
|
|
255
326
|
// src/core/materializer/scaffold-utils.ts
|
|
256
|
-
import { existsSync as
|
|
257
|
-
import { dirname as dirname3, join as
|
|
327
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
|
|
328
|
+
import { dirname as dirname3, join as join4, resolve as resolve2 } from "path";
|
|
258
329
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
259
330
|
|
|
260
331
|
// src/core/materializer/templates.ts
|
|
261
|
-
import { readFileSync as
|
|
262
|
-
import { dirname as dirname2, join as
|
|
332
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
333
|
+
import { dirname as dirname2, join as join3 } from "path";
|
|
263
334
|
import { fileURLToPath } from "url";
|
|
264
335
|
var __dirname = dirname2(fileURLToPath(import.meta.url));
|
|
265
|
-
var TEMPLATES_DIR =
|
|
336
|
+
var TEMPLATES_DIR = join3(__dirname, "agent-templates");
|
|
266
337
|
function loadAgentTemplate(name, vars = {}) {
|
|
267
|
-
const raw =
|
|
338
|
+
const raw = readFileSync3(join3(TEMPLATES_DIR, `${name}.md`), "utf8");
|
|
268
339
|
return raw.replace(/\{\{(\w+)\}\}/g, (_, key) => vars[key] ?? `{{${key}}}`);
|
|
269
340
|
}
|
|
270
341
|
var HEALTH_SH = `#!/usr/bin/env bash
|
|
@@ -456,7 +527,11 @@ If orchestrating: Agent definition files in .claude/agents/
|
|
|
456
527
|
\`\`\`
|
|
457
528
|
`;
|
|
458
529
|
}
|
|
530
|
+
function modelField(model) {
|
|
531
|
+
return model ? `, model: ${JSON.stringify(model)}` : "";
|
|
532
|
+
}
|
|
459
533
|
function configTs(params) {
|
|
534
|
+
const models = params.models ?? {};
|
|
460
535
|
return `import { defineHarness } from '@cardor/agent-harness-kit'
|
|
461
536
|
|
|
462
537
|
export default defineHarness({
|
|
@@ -469,11 +544,12 @@ export default defineHarness({
|
|
|
469
544
|
provider: '${params.provider}',
|
|
470
545
|
|
|
471
546
|
agents: {
|
|
472
|
-
lead: { instructionsPath: null },
|
|
473
|
-
explorer: { instructionsPath: null, allowedPaths: ['${params.docsPath}', './src'] },
|
|
474
|
-
builder: { instructionsPath: null, writablePaths: ['./src', './tests'] },
|
|
475
|
-
reviewer: { instructionsPath: null },
|
|
476
|
-
|
|
547
|
+
lead: { instructionsPath: null${modelField(models.lead)} },
|
|
548
|
+
explorer: { instructionsPath: null, allowedPaths: ['${params.docsPath}', './src']${modelField(models.explorer)} },
|
|
549
|
+
builder: { instructionsPath: null, writablePaths: ['./src', './tests']${modelField(models.builder)} },
|
|
550
|
+
reviewer: { instructionsPath: null${modelField(models.reviewer)} },
|
|
551
|
+
${models.consultant ? `consultant: { instructionsPath: null${modelField(models.consultant)} },
|
|
552
|
+
` : ""}custom: [],
|
|
477
553
|
},
|
|
478
554
|
|
|
479
555
|
// SQLite (default). Switch to postgres/mysql by changing database.type.
|
|
@@ -492,6 +568,10 @@ export default defineHarness({
|
|
|
492
568
|
nextSteps: false,
|
|
493
569
|
},
|
|
494
570
|
markdownFallback: { enabled: true, path: '.harness/current.md' },
|
|
571
|
+
// 'local' \u2014 DB lives in .harness/ (project-relative). 'global' \u2014 DB lives
|
|
572
|
+
// under ~/.harness/dbs/<projectId>/, outside the project tree.
|
|
573
|
+
scope: '${params.scope}',
|
|
574
|
+
projectId: '${params.projectId}',
|
|
495
575
|
},
|
|
496
576
|
|
|
497
577
|
health: {
|
|
@@ -508,6 +588,7 @@ export default defineHarness({
|
|
|
508
588
|
}
|
|
509
589
|
var configMjs = configTs;
|
|
510
590
|
function configCjs(params) {
|
|
591
|
+
const models = params.models ?? {};
|
|
511
592
|
return `const { defineHarness } = require('@cardor/agent-harness-kit')
|
|
512
593
|
|
|
513
594
|
module.exports = defineHarness({
|
|
@@ -520,11 +601,12 @@ module.exports = defineHarness({
|
|
|
520
601
|
provider: '${params.provider}',
|
|
521
602
|
|
|
522
603
|
agents: {
|
|
523
|
-
lead: { instructionsPath: null },
|
|
524
|
-
explorer: { instructionsPath: null, allowedPaths: ['${params.docsPath}', './src'] },
|
|
525
|
-
builder: { instructionsPath: null, writablePaths: ['./src', './tests'] },
|
|
526
|
-
reviewer: { instructionsPath: null },
|
|
527
|
-
|
|
604
|
+
lead: { instructionsPath: null${modelField(models.lead)} },
|
|
605
|
+
explorer: { instructionsPath: null, allowedPaths: ['${params.docsPath}', './src']${modelField(models.explorer)} },
|
|
606
|
+
builder: { instructionsPath: null, writablePaths: ['./src', './tests']${modelField(models.builder)} },
|
|
607
|
+
reviewer: { instructionsPath: null${modelField(models.reviewer)} },
|
|
608
|
+
${models.consultant ? `consultant: { instructionsPath: null${modelField(models.consultant)} },
|
|
609
|
+
` : ""}custom: [],
|
|
528
610
|
},
|
|
529
611
|
|
|
530
612
|
// SQLite (default). Switch to postgres/mysql by changing database.type.
|
|
@@ -543,6 +625,10 @@ module.exports = defineHarness({
|
|
|
543
625
|
nextSteps: false,
|
|
544
626
|
},
|
|
545
627
|
markdownFallback: { enabled: true, path: '.harness/current.md' },
|
|
628
|
+
// 'local' \u2014 DB lives in .harness/ (project-relative). 'global' \u2014 DB lives
|
|
629
|
+
// under ~/.harness/dbs/<projectId>/, outside the project tree.
|
|
630
|
+
scope: '${params.scope}',
|
|
631
|
+
projectId: '${params.projectId}',
|
|
546
632
|
},
|
|
547
633
|
|
|
548
634
|
health: {
|
|
@@ -590,11 +676,14 @@ function stripFrontmatter(md) {
|
|
|
590
676
|
}
|
|
591
677
|
return { description, body };
|
|
592
678
|
}
|
|
593
|
-
function toCodexToml(name, description, body, sandboxMode) {
|
|
679
|
+
function toCodexToml(name, description, body, sandboxMode, model) {
|
|
594
680
|
const safe = (s) => s.replace(/"""/g, '""\\u0022');
|
|
681
|
+
const trimmedModel = model?.trim() ?? "";
|
|
682
|
+
const modelLine = trimmedModel.length >= 3 ? `model = "${trimmedModel}"
|
|
683
|
+
` : "";
|
|
595
684
|
return `name = "${name}"
|
|
596
685
|
sandbox_mode = "${sandboxMode}"
|
|
597
|
-
|
|
686
|
+
${modelLine}
|
|
598
687
|
description = """
|
|
599
688
|
${safe(description)}
|
|
600
689
|
"""
|
|
@@ -606,29 +695,29 @@ ${safe(body.trimEnd())}
|
|
|
606
695
|
}
|
|
607
696
|
function agentLeadToml(vars) {
|
|
608
697
|
const { description, body } = stripFrontmatter(loadAgentTemplate("lead", vars));
|
|
609
|
-
return toCodexToml("lead", description, body, "read-only");
|
|
698
|
+
return toCodexToml("lead", description, body, "read-only", vars.model);
|
|
610
699
|
}
|
|
611
700
|
function agentLeadAsDefaultToml(vars) {
|
|
612
701
|
const { description, body } = stripFrontmatter(loadAgentTemplate("lead", vars));
|
|
613
|
-
return toCodexToml("default", description, body, "read-only");
|
|
702
|
+
return toCodexToml("default", description, body, "read-only", vars.model);
|
|
614
703
|
}
|
|
615
704
|
function agentExplorerToml(vars) {
|
|
616
705
|
const { description, body } = stripFrontmatter(loadAgentTemplate("explorer", vars));
|
|
617
|
-
return toCodexToml("explorer", description, body, "read-only");
|
|
706
|
+
return toCodexToml("explorer", description, body, "read-only", vars.model);
|
|
618
707
|
}
|
|
619
708
|
function agentBuilderToml(vars) {
|
|
620
709
|
const { description, body } = stripFrontmatter(loadAgentTemplate("builder", vars));
|
|
621
|
-
return toCodexToml("builder", description, body, "workspace-write");
|
|
710
|
+
return toCodexToml("builder", description, body, "workspace-write", vars.model);
|
|
622
711
|
}
|
|
623
712
|
function agentReviewerToml(vars) {
|
|
624
713
|
const { description, body } = stripFrontmatter(loadAgentTemplate("reviewer", vars));
|
|
625
|
-
return toCodexToml("reviewer", description, body, "read-only");
|
|
714
|
+
return toCodexToml("reviewer", description, body, "read-only", vars.model);
|
|
626
715
|
}
|
|
627
716
|
function agentConsultantToml(vars) {
|
|
628
717
|
const { description, body } = stripFrontmatter(loadAgentTemplate("consultant", vars));
|
|
629
|
-
return toCodexToml("consultant", description, body, "read-only");
|
|
718
|
+
return toCodexToml("consultant", description, body, "read-only", vars.model);
|
|
630
719
|
}
|
|
631
|
-
function translateFrontmatterForClaudeCode(md, agentName) {
|
|
720
|
+
function translateFrontmatterForClaudeCode(md, agentName, model) {
|
|
632
721
|
const permissionsMap = {
|
|
633
722
|
lead: [...MCP_CLAUDE_PERMISSIONS_LEAD],
|
|
634
723
|
explorer: [...MCP_CLAUDE_PERMISSIONS_EXPLORER],
|
|
@@ -638,13 +727,24 @@ function translateFrontmatterForClaudeCode(md, agentName) {
|
|
|
638
727
|
};
|
|
639
728
|
const permissions = permissionsMap[agentName] ?? MCP_CLAUDE_PERMISSIONS;
|
|
640
729
|
const mcpLines = permissions.map((t) => ` - ${t}`).join("\n");
|
|
641
|
-
|
|
730
|
+
let result = md.replace(/(tools:\n(?: - (?!mcp__)[^\n]+\n)+)/, (match) => {
|
|
642
731
|
const trimmed = match.trimEnd();
|
|
643
732
|
return `${trimmed}
|
|
644
733
|
- Task
|
|
645
734
|
${mcpLines}
|
|
646
735
|
`;
|
|
647
736
|
});
|
|
737
|
+
if (model) {
|
|
738
|
+
result = injectModelFrontmatterLine(result, model);
|
|
739
|
+
}
|
|
740
|
+
return result;
|
|
741
|
+
}
|
|
742
|
+
function injectModelFrontmatterLine(md, model) {
|
|
743
|
+
if (/^model:\s*.*$/m.test(md)) {
|
|
744
|
+
return md.replace(/^model:\s*.*$/m, `model: ${model}`);
|
|
745
|
+
}
|
|
746
|
+
return md.replace(/^(name:.*)$/m, `$1
|
|
747
|
+
model: ${model}`);
|
|
648
748
|
}
|
|
649
749
|
function translateFrontmatterForOpenCode(md) {
|
|
650
750
|
return md.replace(/(tools:\n(?: - [^\n]+\n)+)/, (match) => {
|
|
@@ -663,14 +763,14 @@ var GITIGNORE_ENTRIES = `
|
|
|
663
763
|
// src/core/materializer/scaffold-utils.ts
|
|
664
764
|
var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
|
|
665
765
|
function writeAgentFile(cwd2, relPath, content) {
|
|
666
|
-
const abs =
|
|
667
|
-
if (
|
|
766
|
+
const abs = join4(cwd2, relPath);
|
|
767
|
+
if (existsSync3(abs)) return;
|
|
668
768
|
mkdirSync3(resolve2(abs, ".."), { recursive: true });
|
|
669
769
|
writeFileSync3(abs, content, "utf8");
|
|
670
770
|
}
|
|
671
771
|
function appendGitignore(cwd2) {
|
|
672
|
-
const giPath =
|
|
673
|
-
const existing =
|
|
772
|
+
const giPath = join4(cwd2, ".gitignore");
|
|
773
|
+
const existing = existsSync3(giPath) ? readFileSync4(giPath, "utf8") : "";
|
|
674
774
|
const toAdd = GITIGNORE_ENTRIES.split("\n").filter((line) => line && !existing.includes(line)).join("\n");
|
|
675
775
|
if (toAdd.trim()) {
|
|
676
776
|
writeFileSync3(giPath, existing + (existing.endsWith("\n") ? "" : "\n") + toAdd + "\n", "utf8");
|
|
@@ -680,15 +780,23 @@ function slugify(title) {
|
|
|
680
780
|
return title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64);
|
|
681
781
|
}
|
|
682
782
|
function writeSkills(cwd2, skillsDir) {
|
|
683
|
-
const skillNames = ["ahk-ask", "ahk-consultant", "ahk-triage"];
|
|
783
|
+
const skillNames = ["ahk-ask", "ahk-consultant", "ahk-triage", "ahk-review"];
|
|
684
784
|
for (const skillName of skillNames) {
|
|
685
|
-
const src =
|
|
686
|
-
const destDir =
|
|
687
|
-
const dest =
|
|
785
|
+
const src = join4(__dirname2, "skills", skillName, "SKILL.md");
|
|
786
|
+
const destDir = join4(cwd2, skillsDir, skillName);
|
|
787
|
+
const dest = join4(destDir, "SKILL.md");
|
|
688
788
|
mkdirSync3(destDir, { recursive: true });
|
|
689
|
-
writeFileSync3(dest,
|
|
789
|
+
writeFileSync3(dest, readFileSync4(src, "utf8"), "utf8");
|
|
690
790
|
}
|
|
691
791
|
}
|
|
792
|
+
function writeSkill(skillsRoot, skillName) {
|
|
793
|
+
const src = join4(__dirname2, "skills", skillName, "SKILL.md");
|
|
794
|
+
const destDir = join4(skillsRoot, skillName);
|
|
795
|
+
const dest = join4(destDir, "SKILL.md");
|
|
796
|
+
if (existsSync3(dest)) return;
|
|
797
|
+
mkdirSync3(destDir, { recursive: true });
|
|
798
|
+
writeFileSync3(dest, readFileSync4(src, "utf8"), "utf8");
|
|
799
|
+
}
|
|
692
800
|
|
|
693
801
|
// src/core/materializer/claude-code.ts
|
|
694
802
|
var ClaudeCodeMaterializer = class {
|
|
@@ -696,12 +804,12 @@ var ClaudeCodeMaterializer = class {
|
|
|
696
804
|
const { cwd: cwd2 } = opts;
|
|
697
805
|
write(cwd2, "AGENTS.md", agentsMd(config));
|
|
698
806
|
write(cwd2, "CLAUDE.md", claudeMd(config));
|
|
699
|
-
if (!
|
|
807
|
+
if (!existsSync4(join5(cwd2, "health.sh"))) {
|
|
700
808
|
write(cwd2, "health.sh", HEALTH_SH, 493);
|
|
701
809
|
}
|
|
702
810
|
const tasks = opts.firstTask ? [{ slug: slugify(opts.firstTask.title), ...opts.firstTask }] : [];
|
|
703
811
|
write(cwd2, "feature_list.json", featureListJson(tasks));
|
|
704
|
-
if (!
|
|
812
|
+
if (!existsSync4(join5(cwd2, config.storage.markdownFallback.path))) {
|
|
705
813
|
write(
|
|
706
814
|
cwd2,
|
|
707
815
|
config.storage.markdownFallback.path,
|
|
@@ -717,20 +825,25 @@ No tasks in progress.
|
|
|
717
825
|
const projectName = config.project.name;
|
|
718
826
|
const allowedPaths = (config.agents.explorer.allowedPaths ?? []).join(", ");
|
|
719
827
|
const writablePaths = (config.agents.builder.writablePaths ?? []).join(", ");
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
828
|
+
const leadModel = config.agents.lead.model;
|
|
829
|
+
const explorerModel = config.agents.explorer.model;
|
|
830
|
+
const consultantModel = config.agents.consultant?.model;
|
|
831
|
+
const builderModel = config.agents.builder.model;
|
|
832
|
+
const reviewerModel = config.agents.reviewer.model;
|
|
833
|
+
writeAgentFile(cwd2, ".claude/agents/lead.md", translateFrontmatterForClaudeCode(agentLead({ projectName }), "lead", leadModel));
|
|
834
|
+
writeAgentFile(cwd2, ".claude/agents/explorer.md", translateFrontmatterForClaudeCode(agentExplorer({ projectName, allowedPaths }), "explorer", explorerModel));
|
|
835
|
+
writeAgentFile(cwd2, ".claude/agents/consultant.md", translateFrontmatterForClaudeCode(agentConsultant({ projectName }), "consultant", consultantModel));
|
|
836
|
+
writeAgentFile(cwd2, ".claude/agents/builder.md", translateFrontmatterForClaudeCode(agentBuilder({ projectName, writablePaths }), "builder", builderModel));
|
|
837
|
+
writeAgentFile(cwd2, ".claude/agents/reviewer.md", translateFrontmatterForClaudeCode(agentReviewer({ projectName }), "reviewer", reviewerModel));
|
|
838
|
+
mergeClaudeMcpJson(join5(cwd2, ".mcp.json"), config.tools.mcp.port, detectPackageManager(cwd2));
|
|
839
|
+
mergeClaudeSettingsJson(join5(cwd2, ".claude/settings.json"));
|
|
840
|
+
mergeClaudeSettingsLocalJson(join5(cwd2, ".claude/settings.local.json"));
|
|
728
841
|
appendGitignore(cwd2);
|
|
729
842
|
writeSkills(cwd2, ".claude/skills");
|
|
730
843
|
}
|
|
731
844
|
async build(config, cwd2) {
|
|
732
845
|
const write2 = (relPath, content) => {
|
|
733
|
-
const abs =
|
|
846
|
+
const abs = join5(cwd2, relPath);
|
|
734
847
|
mkdirSync4(resolve3(abs, ".."), { recursive: true });
|
|
735
848
|
writeFileSync4(abs, content, "utf8");
|
|
736
849
|
};
|
|
@@ -739,14 +852,19 @@ No tasks in progress.
|
|
|
739
852
|
const projectName = config.project.name;
|
|
740
853
|
const allowedPaths = (config.agents.explorer.allowedPaths ?? []).join(", ");
|
|
741
854
|
const writablePaths = (config.agents.builder.writablePaths ?? []).join(", ");
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
855
|
+
const leadModel = config.agents.lead.model;
|
|
856
|
+
const explorerModel = config.agents.explorer.model;
|
|
857
|
+
const consultantModel = config.agents.consultant?.model;
|
|
858
|
+
const builderModel = config.agents.builder.model;
|
|
859
|
+
const reviewerModel = config.agents.reviewer.model;
|
|
860
|
+
write2(".claude/agents/lead.md", translateFrontmatterForClaudeCode(agentLead({ projectName }), "lead", leadModel));
|
|
861
|
+
write2(".claude/agents/explorer.md", translateFrontmatterForClaudeCode(agentExplorer({ projectName, allowedPaths }), "explorer", explorerModel));
|
|
862
|
+
write2(".claude/agents/consultant.md", translateFrontmatterForClaudeCode(agentConsultant({ projectName }), "consultant", consultantModel));
|
|
863
|
+
write2(".claude/agents/builder.md", translateFrontmatterForClaudeCode(agentBuilder({ projectName, writablePaths }), "builder", builderModel));
|
|
864
|
+
write2(".claude/agents/reviewer.md", translateFrontmatterForClaudeCode(agentReviewer({ projectName }), "reviewer", reviewerModel));
|
|
865
|
+
mergeClaudeMcpJson(join5(cwd2, ".mcp.json"), config.tools.mcp.port, detectPackageManager(cwd2));
|
|
866
|
+
mergeClaudeSettingsJson(join5(cwd2, ".claude/settings.json"));
|
|
867
|
+
mergeClaudeSettingsLocalJson(join5(cwd2, ".claude/settings.local.json"));
|
|
750
868
|
writeSkills(cwd2, ".claude/skills");
|
|
751
869
|
}
|
|
752
870
|
async migrate(config, _to, _cwd) {
|
|
@@ -761,12 +879,12 @@ No tasks in progress.
|
|
|
761
879
|
reviewer: [...MCP_CLAUDE_PERMISSIONS_REVIEWER]
|
|
762
880
|
};
|
|
763
881
|
for (const [agent, tools] of Object.entries(AGENT_TOOLS)) {
|
|
764
|
-
const filePath =
|
|
765
|
-
if (!
|
|
882
|
+
const filePath = join5(cwd2, ".claude", "agents", `${agent}.md`);
|
|
883
|
+
if (!existsSync4(filePath)) {
|
|
766
884
|
console.log(` ${agent}.md not found \u2014 skipping`);
|
|
767
885
|
continue;
|
|
768
886
|
}
|
|
769
|
-
const content =
|
|
887
|
+
const content = readFileSync5(filePath, "utf-8");
|
|
770
888
|
const updated = content.replace(
|
|
771
889
|
/(tools:\n)((?: - [^\n]+\n)*)/m,
|
|
772
890
|
(_match, header, toolsSection) => {
|
|
@@ -787,23 +905,23 @@ No tasks in progress.
|
|
|
787
905
|
};
|
|
788
906
|
|
|
789
907
|
// src/core/materializer/codex-cli.ts
|
|
790
|
-
import { existsSync as
|
|
791
|
-
import { join as
|
|
908
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "fs";
|
|
909
|
+
import { join as join6, resolve as resolve4 } from "path";
|
|
792
910
|
var CodexCliMaterializer = class {
|
|
793
911
|
async scaffold(config, opts) {
|
|
794
912
|
const { cwd: cwd2 } = opts;
|
|
795
913
|
const write2 = (relPath, content, mode) => {
|
|
796
|
-
const abs =
|
|
914
|
+
const abs = join6(cwd2, relPath);
|
|
797
915
|
mkdirSync5(resolve4(abs, ".."), { recursive: true });
|
|
798
916
|
writeFileSync5(abs, content, { encoding: "utf8", mode });
|
|
799
917
|
};
|
|
800
918
|
write2("AGENTS.md", agentsMd(config));
|
|
801
|
-
if (!
|
|
919
|
+
if (!existsSync5(join6(cwd2, "health.sh"))) {
|
|
802
920
|
write2("health.sh", HEALTH_SH, 493);
|
|
803
921
|
}
|
|
804
922
|
const tasks = opts.firstTask ? [{ slug: slugify(opts.firstTask.title), ...opts.firstTask }] : [];
|
|
805
|
-
write2(
|
|
806
|
-
if (!
|
|
923
|
+
write2(join6(config.storage.dir, "feature_list.json"), featureListJson(tasks));
|
|
924
|
+
if (!existsSync5(join6(cwd2, config.storage.markdownFallback.path))) {
|
|
807
925
|
write2(
|
|
808
926
|
config.storage.markdownFallback.path,
|
|
809
927
|
`<!-- AUTO-GENERATED by agent-harness-kit \u2014 DO NOT EDIT MANUALLY -->
|
|
@@ -818,19 +936,24 @@ No tasks in progress.
|
|
|
818
936
|
const projectName = config.project.name;
|
|
819
937
|
const allowedPaths = (config.agents.explorer.allowedPaths ?? []).join(", ");
|
|
820
938
|
const writablePaths = (config.agents.builder.writablePaths ?? []).join(", ");
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
writeAgentFile(cwd2, ".codex/agents/
|
|
827
|
-
|
|
939
|
+
const leadModel = config.agents.lead.model;
|
|
940
|
+
const explorerModel = config.agents.explorer.model;
|
|
941
|
+
const consultantModel = config.agents.consultant?.model;
|
|
942
|
+
const builderModel = config.agents.builder.model;
|
|
943
|
+
const reviewerModel = config.agents.reviewer.model;
|
|
944
|
+
writeAgentFile(cwd2, ".codex/agents/lead.toml", agentLeadToml({ projectName, model: leadModel }));
|
|
945
|
+
writeAgentFile(cwd2, ".codex/agents/explorer.toml", agentExplorerToml({ projectName, allowedPaths, model: explorerModel }));
|
|
946
|
+
writeAgentFile(cwd2, ".codex/agents/consultant.toml", agentConsultantToml({ projectName, model: consultantModel }));
|
|
947
|
+
writeAgentFile(cwd2, ".codex/agents/builder.toml", agentBuilderToml({ projectName, writablePaths, model: builderModel }));
|
|
948
|
+
writeAgentFile(cwd2, ".codex/agents/reviewer.toml", agentReviewerToml({ projectName, model: reviewerModel }));
|
|
949
|
+
writeAgentFile(cwd2, ".codex/agents/default.toml", agentLeadAsDefaultToml({ projectName, model: leadModel }));
|
|
950
|
+
mergeCodexConfigToml(join6(cwd2, ".codex/config.toml"), config.tools.mcp.port, detectPackageManager(cwd2));
|
|
828
951
|
appendGitignore(cwd2);
|
|
829
952
|
writeSkills(cwd2, ".agents/skills");
|
|
830
953
|
}
|
|
831
954
|
async build(config, cwd2) {
|
|
832
955
|
const write2 = (relPath, content) => {
|
|
833
|
-
const abs =
|
|
956
|
+
const abs = join6(cwd2, relPath);
|
|
834
957
|
mkdirSync5(resolve4(abs, ".."), { recursive: true });
|
|
835
958
|
writeFileSync5(abs, content, "utf8");
|
|
836
959
|
};
|
|
@@ -838,13 +961,18 @@ No tasks in progress.
|
|
|
838
961
|
const projectName = config.project.name;
|
|
839
962
|
const allowedPaths = (config.agents.explorer.allowedPaths ?? []).join(", ");
|
|
840
963
|
const writablePaths = (config.agents.builder.writablePaths ?? []).join(", ");
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
writeAgentFile(cwd2, ".codex/agents/
|
|
847
|
-
|
|
964
|
+
const leadModel = config.agents.lead.model;
|
|
965
|
+
const explorerModel = config.agents.explorer.model;
|
|
966
|
+
const consultantModel = config.agents.consultant?.model;
|
|
967
|
+
const builderModel = config.agents.builder.model;
|
|
968
|
+
const reviewerModel = config.agents.reviewer.model;
|
|
969
|
+
writeAgentFile(cwd2, ".codex/agents/lead.toml", agentLeadToml({ projectName, model: leadModel }));
|
|
970
|
+
writeAgentFile(cwd2, ".codex/agents/explorer.toml", agentExplorerToml({ projectName, allowedPaths, model: explorerModel }));
|
|
971
|
+
writeAgentFile(cwd2, ".codex/agents/consultant.toml", agentConsultantToml({ projectName, model: consultantModel }));
|
|
972
|
+
writeAgentFile(cwd2, ".codex/agents/builder.toml", agentBuilderToml({ projectName, writablePaths, model: builderModel }));
|
|
973
|
+
writeAgentFile(cwd2, ".codex/agents/reviewer.toml", agentReviewerToml({ projectName, model: reviewerModel }));
|
|
974
|
+
writeAgentFile(cwd2, ".codex/agents/default.toml", agentLeadAsDefaultToml({ projectName, model: leadModel }));
|
|
975
|
+
mergeCodexConfigToml(join6(cwd2, ".codex/config.toml"), config.tools.mcp.port, detectPackageManager(cwd2));
|
|
848
976
|
writeSkills(cwd2, ".agents/skills");
|
|
849
977
|
}
|
|
850
978
|
async migrate(config, _to, _cwd) {
|
|
@@ -856,23 +984,23 @@ No tasks in progress.
|
|
|
856
984
|
};
|
|
857
985
|
|
|
858
986
|
// src/core/materializer/opencode.ts
|
|
859
|
-
import { existsSync as
|
|
860
|
-
import { join as
|
|
987
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync6, writeFileSync as writeFileSync6 } from "fs";
|
|
988
|
+
import { join as join7, resolve as resolve5 } from "path";
|
|
861
989
|
var OpenCodeMaterializer = class {
|
|
862
990
|
async scaffold(config, opts) {
|
|
863
991
|
const { cwd: cwd2 } = opts;
|
|
864
992
|
const write2 = (relPath, content, mode) => {
|
|
865
|
-
const abs =
|
|
993
|
+
const abs = join7(cwd2, relPath);
|
|
866
994
|
mkdirSync6(resolve5(abs, ".."), { recursive: true });
|
|
867
995
|
writeFileSync6(abs, content, { encoding: "utf8", mode });
|
|
868
996
|
};
|
|
869
997
|
write2("AGENTS.md", agentsMd(config));
|
|
870
|
-
if (!
|
|
998
|
+
if (!existsSync6(join7(cwd2, "health.sh"))) {
|
|
871
999
|
write2("health.sh", HEALTH_SH, 493);
|
|
872
1000
|
}
|
|
873
1001
|
const tasks = opts.firstTask ? [{ slug: slugify(opts.firstTask.title), ...opts.firstTask }] : [];
|
|
874
|
-
write2(
|
|
875
|
-
if (!
|
|
1002
|
+
write2(join7(config.storage.dir, "feature_list.json"), featureListJson(tasks));
|
|
1003
|
+
if (!existsSync6(join7(cwd2, config.storage.markdownFallback.path))) {
|
|
876
1004
|
write2(
|
|
877
1005
|
config.storage.markdownFallback.path,
|
|
878
1006
|
`<!-- AUTO-GENERATED by agent-harness-kit \u2014 DO NOT EDIT MANUALLY -->
|
|
@@ -892,13 +1020,13 @@ No tasks in progress.
|
|
|
892
1020
|
writeAgentFile(cwd2, ".opencode/agents/consultant.md", translateFrontmatterForOpenCode(agentConsultant({ projectName })));
|
|
893
1021
|
writeAgentFile(cwd2, ".opencode/agents/builder.md", translateFrontmatterForOpenCode(agentBuilder({ projectName, writablePaths })));
|
|
894
1022
|
writeAgentFile(cwd2, ".opencode/agents/reviewer.md", translateFrontmatterForOpenCode(agentReviewer({ projectName })));
|
|
895
|
-
mergeOpencodeJson(
|
|
1023
|
+
mergeOpencodeJson(join7(cwd2, "opencode.json"), config.tools.mcp.port, detectPackageManager(cwd2));
|
|
896
1024
|
appendGitignore(cwd2);
|
|
897
1025
|
writeSkills(cwd2, ".opencode/skills");
|
|
898
1026
|
}
|
|
899
1027
|
async build(config, cwd2) {
|
|
900
1028
|
const write2 = (relPath, content) => {
|
|
901
|
-
const abs =
|
|
1029
|
+
const abs = join7(cwd2, relPath);
|
|
902
1030
|
mkdirSync6(resolve5(abs, ".."), { recursive: true });
|
|
903
1031
|
writeFileSync6(abs, content, "utf8");
|
|
904
1032
|
};
|
|
@@ -911,7 +1039,7 @@ No tasks in progress.
|
|
|
911
1039
|
writeAgentFile(cwd2, ".opencode/agents/consultant.md", translateFrontmatterForOpenCode(agentConsultant({ projectName })));
|
|
912
1040
|
writeAgentFile(cwd2, ".opencode/agents/builder.md", translateFrontmatterForOpenCode(agentBuilder({ projectName, writablePaths })));
|
|
913
1041
|
writeAgentFile(cwd2, ".opencode/agents/reviewer.md", translateFrontmatterForOpenCode(agentReviewer({ projectName })));
|
|
914
|
-
mergeOpencodeJson(
|
|
1042
|
+
mergeOpencodeJson(join7(cwd2, "opencode.json"), config.tools.mcp.port, detectPackageManager(cwd2));
|
|
915
1043
|
writeSkills(cwd2, ".opencode/skills");
|
|
916
1044
|
}
|
|
917
1045
|
async migrate(config, _to, _cwd) {
|
|
@@ -977,14 +1105,14 @@ async function buildOnce(cwd2) {
|
|
|
977
1105
|
}
|
|
978
1106
|
|
|
979
1107
|
// src/commands/dashboard.ts
|
|
980
|
-
import { dirname as
|
|
1108
|
+
import { dirname as dirname4, join as join9, resolve as resolve6 } from "path";
|
|
981
1109
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
982
1110
|
import pc2 from "picocolors";
|
|
983
1111
|
|
|
984
1112
|
// src/core/dashboard-server.ts
|
|
985
1113
|
import { watch as watch2 } from "fs";
|
|
986
|
-
import { existsSync as
|
|
987
|
-
import { extname, join as
|
|
1114
|
+
import { existsSync as existsSync7, readFileSync as readFileSync6 } from "fs";
|
|
1115
|
+
import { extname, join as join8 } from "path";
|
|
988
1116
|
import { serve } from "@hono/node-server";
|
|
989
1117
|
import { Hono } from "hono";
|
|
990
1118
|
import { WebSocketServer } from "ws";
|
|
@@ -1026,7 +1154,7 @@ var MIME = {
|
|
|
1026
1154
|
".ttf": "font/ttf"
|
|
1027
1155
|
};
|
|
1028
1156
|
function fileResponse(filePath) {
|
|
1029
|
-
const content =
|
|
1157
|
+
const content = readFileSync6(filePath);
|
|
1030
1158
|
const mime = MIME[extname(filePath)] ?? "application/octet-stream";
|
|
1031
1159
|
return new Response(content, {
|
|
1032
1160
|
headers: { "Content-Type": mime, "Cache-Control": "no-cache" }
|
|
@@ -1132,15 +1260,15 @@ async function startDashboardServer(db, dbPath, staticPath, port) {
|
|
|
1132
1260
|
app.get("/*", (c) => {
|
|
1133
1261
|
const urlPath = c.req.path;
|
|
1134
1262
|
if (urlPath !== "/") {
|
|
1135
|
-
const candidate =
|
|
1136
|
-
if (
|
|
1263
|
+
const candidate = join8(staticPath, urlPath);
|
|
1264
|
+
if (existsSync7(candidate)) {
|
|
1137
1265
|
try {
|
|
1138
1266
|
return fileResponse(candidate);
|
|
1139
1267
|
} catch {
|
|
1140
1268
|
}
|
|
1141
1269
|
}
|
|
1142
1270
|
}
|
|
1143
|
-
return fileResponse(
|
|
1271
|
+
return fileResponse(join8(staticPath, "index.html"));
|
|
1144
1272
|
});
|
|
1145
1273
|
const resolvedPort = await findFreePort(port);
|
|
1146
1274
|
if (resolvedPort !== port) {
|
|
@@ -1171,7 +1299,7 @@ async function startDashboardServer(db, dbPath, staticPath, port) {
|
|
|
1171
1299
|
let watcher = null;
|
|
1172
1300
|
if (dbPath) {
|
|
1173
1301
|
const walPath = `${dbPath}-wal`;
|
|
1174
|
-
const watchTarget =
|
|
1302
|
+
const watchTarget = existsSync7(walPath) ? walPath : dbPath;
|
|
1175
1303
|
watcher = watch2(watchTarget, broadcast);
|
|
1176
1304
|
}
|
|
1177
1305
|
return {
|
|
@@ -1185,635 +1313,12 @@ async function startDashboardServer(db, dbPath, staticPath, port) {
|
|
|
1185
1313
|
};
|
|
1186
1314
|
}
|
|
1187
1315
|
|
|
1188
|
-
// src/core/db.ts
|
|
1189
|
-
import { randomUUID } from "crypto";
|
|
1190
|
-
import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync7 } from "fs";
|
|
1191
|
-
import { dirname as dirname4, join as join8, resolve as resolve6 } from "path";
|
|
1192
|
-
|
|
1193
|
-
// src/core/repositories/ActionRepository.ts
|
|
1194
|
-
var ActionRepository = class {
|
|
1195
|
-
constructor(driver) {
|
|
1196
|
-
this.driver = driver;
|
|
1197
|
-
}
|
|
1198
|
-
driver;
|
|
1199
|
-
async create(id, taskId, agent, now) {
|
|
1200
|
-
await this.driver.exec(
|
|
1201
|
-
`INSERT INTO actions (id, task_id, agent, status, created_at) VALUES (?, ?, ?, 'in_progress', ?)`,
|
|
1202
|
-
[id, taskId, agent, now]
|
|
1203
|
-
);
|
|
1204
|
-
}
|
|
1205
|
-
async complete(actionId, summary, now) {
|
|
1206
|
-
await this.driver.exec(
|
|
1207
|
-
`UPDATE actions SET status = 'completed', completed_at = ?, summary = ? WHERE id = ?`,
|
|
1208
|
-
[now, summary, actionId]
|
|
1209
|
-
);
|
|
1210
|
-
}
|
|
1211
|
-
async closeOrphaned(taskId, now) {
|
|
1212
|
-
return this.driver.exec(
|
|
1213
|
-
`UPDATE actions SET status = 'completed', completed_at = ?, summary = 'Auto-closed: task marked done' WHERE task_id = ? AND status = 'in_progress'`,
|
|
1214
|
-
[now, taskId]
|
|
1215
|
-
);
|
|
1216
|
-
}
|
|
1217
|
-
async getById(actionId) {
|
|
1218
|
-
return this.driver.queryOne(`SELECT * FROM actions WHERE id = ?`, [actionId]);
|
|
1219
|
-
}
|
|
1220
|
-
async getForTask(taskId) {
|
|
1221
|
-
return this.driver.query(
|
|
1222
|
-
`SELECT * FROM actions WHERE task_id = ? ORDER BY created_at`,
|
|
1223
|
-
[taskId]
|
|
1224
|
-
);
|
|
1225
|
-
}
|
|
1226
|
-
async getAll() {
|
|
1227
|
-
return this.driver.query(`SELECT * FROM actions ORDER BY created_at`);
|
|
1228
|
-
}
|
|
1229
|
-
async getWithDetails(taskId) {
|
|
1230
|
-
const actions = await this.getForTask(taskId);
|
|
1231
|
-
return Promise.all(
|
|
1232
|
-
actions.map(async (action) => ({
|
|
1233
|
-
...action,
|
|
1234
|
-
sections: await this.getSections(action.id),
|
|
1235
|
-
files: await this.getFiles(action.id),
|
|
1236
|
-
tools: await this.getTools(action.id)
|
|
1237
|
-
}))
|
|
1238
|
-
);
|
|
1239
|
-
}
|
|
1240
|
-
// ─── Sections ─────────────────────────────────────────────────────────────
|
|
1241
|
-
async addSection(actionId, sectionType, content, now) {
|
|
1242
|
-
await this.driver.exec(
|
|
1243
|
-
`INSERT INTO action_sections (action_id, section_type, content, created_at) VALUES (?, ?, ?, ?)`,
|
|
1244
|
-
[actionId, sectionType, content, now]
|
|
1245
|
-
);
|
|
1246
|
-
}
|
|
1247
|
-
async getSections(actionId) {
|
|
1248
|
-
return this.driver.query(
|
|
1249
|
-
`SELECT * FROM action_sections WHERE action_id = ? ORDER BY created_at`,
|
|
1250
|
-
[actionId]
|
|
1251
|
-
);
|
|
1252
|
-
}
|
|
1253
|
-
async getAllSections() {
|
|
1254
|
-
return this.driver.query(`SELECT * FROM action_sections ORDER BY created_at`);
|
|
1255
|
-
}
|
|
1256
|
-
// ─── Files ────────────────────────────────────────────────────────────────
|
|
1257
|
-
async addFile(actionId, filePath, operation, notes) {
|
|
1258
|
-
await this.driver.exec(
|
|
1259
|
-
`INSERT INTO action_files (action_id, file_path, operation, notes) VALUES (?, ?, ?, ?)`,
|
|
1260
|
-
[actionId, filePath, operation, notes]
|
|
1261
|
-
);
|
|
1262
|
-
}
|
|
1263
|
-
async getFiles(actionId) {
|
|
1264
|
-
return this.driver.query(
|
|
1265
|
-
`SELECT * FROM action_files WHERE action_id = ?`,
|
|
1266
|
-
[actionId]
|
|
1267
|
-
);
|
|
1268
|
-
}
|
|
1269
|
-
async getFilesForTask(taskId) {
|
|
1270
|
-
return this.driver.query(
|
|
1271
|
-
`SELECT af.*, a.agent FROM action_files af JOIN actions a ON af.action_id = a.id WHERE a.task_id = ? ORDER BY a.agent, af.operation`,
|
|
1272
|
-
[taskId]
|
|
1273
|
-
);
|
|
1274
|
-
}
|
|
1275
|
-
// ─── Tools ────────────────────────────────────────────────────────────────
|
|
1276
|
-
async addTool(actionId, toolName, argsJson, resultSummary, now) {
|
|
1277
|
-
await this.driver.exec(
|
|
1278
|
-
`INSERT INTO action_tools (action_id, tool_name, args_json, result_summary, called_at) VALUES (?, ?, ?, ?, ?)`,
|
|
1279
|
-
[actionId, toolName, argsJson, resultSummary, now]
|
|
1280
|
-
);
|
|
1281
|
-
}
|
|
1282
|
-
async getTools(actionId) {
|
|
1283
|
-
return this.driver.query(
|
|
1284
|
-
`SELECT * FROM action_tools WHERE action_id = ? ORDER BY called_at`,
|
|
1285
|
-
[actionId]
|
|
1286
|
-
);
|
|
1287
|
-
}
|
|
1288
|
-
async getTopTools(limit) {
|
|
1289
|
-
return this.driver.query(
|
|
1290
|
-
`SELECT tool_name, COUNT(*) as uses FROM action_tools GROUP BY tool_name ORDER BY uses DESC LIMIT ?`,
|
|
1291
|
-
[limit]
|
|
1292
|
-
);
|
|
1293
|
-
}
|
|
1294
|
-
};
|
|
1295
|
-
|
|
1296
|
-
// src/core/repositories/StatsRepository.ts
|
|
1297
|
-
var AGENT_ORDER = ["lead", "explorer", "builder", "reviewer"];
|
|
1298
|
-
var StatsRepository = class {
|
|
1299
|
-
constructor(driver) {
|
|
1300
|
-
this.driver = driver;
|
|
1301
|
-
}
|
|
1302
|
-
driver;
|
|
1303
|
-
async getCounts() {
|
|
1304
|
-
const [{ total: totalActions }] = await this.driver.query(
|
|
1305
|
-
`SELECT COUNT(*) as total FROM actions`
|
|
1306
|
-
);
|
|
1307
|
-
const [{ total: totalFiles }] = await this.driver.query(
|
|
1308
|
-
`SELECT COUNT(*) as total FROM action_files`
|
|
1309
|
-
);
|
|
1310
|
-
const [{ total: uniqueTools }] = await this.driver.query(
|
|
1311
|
-
`SELECT COUNT(DISTINCT tool_name) as total FROM action_tools`
|
|
1312
|
-
);
|
|
1313
|
-
const [{ total: activeAgents }] = await this.driver.query(
|
|
1314
|
-
`SELECT COUNT(DISTINCT agent) as total FROM actions WHERE status = 'in_progress'`
|
|
1315
|
-
);
|
|
1316
|
-
return { totalActions, totalFiles, uniqueTools, activeAgents };
|
|
1317
|
-
}
|
|
1318
|
-
async getRecentTools(limit) {
|
|
1319
|
-
return this.driver.query(
|
|
1320
|
-
`SELECT at.*, t.id as task_id, t.title as task_title, t.slug as task_slug, a.agent
|
|
1321
|
-
FROM action_tools at
|
|
1322
|
-
JOIN actions a ON at.action_id = a.id
|
|
1323
|
-
JOIN tasks t ON a.task_id = t.id
|
|
1324
|
-
ORDER BY at.called_at DESC
|
|
1325
|
-
LIMIT ?`,
|
|
1326
|
-
[limit]
|
|
1327
|
-
);
|
|
1328
|
-
}
|
|
1329
|
-
async getTopFiles(limit) {
|
|
1330
|
-
return this.driver.query(
|
|
1331
|
-
`SELECT
|
|
1332
|
-
file_path,
|
|
1333
|
-
COUNT(*) as total,
|
|
1334
|
-
SUM(CASE WHEN operation='read' THEN 1 ELSE 0 END) as read,
|
|
1335
|
-
SUM(CASE WHEN operation='created' THEN 1 ELSE 0 END) as created,
|
|
1336
|
-
SUM(CASE WHEN operation='modified' THEN 1 ELSE 0 END) as modified,
|
|
1337
|
-
SUM(CASE WHEN operation='deleted' THEN 1 ELSE 0 END) as deleted
|
|
1338
|
-
FROM action_files
|
|
1339
|
-
GROUP BY file_path
|
|
1340
|
-
ORDER BY total DESC
|
|
1341
|
-
LIMIT ?`,
|
|
1342
|
-
[limit]
|
|
1343
|
-
);
|
|
1344
|
-
}
|
|
1345
|
-
async getRecentFiles(limit) {
|
|
1346
|
-
return this.driver.query(
|
|
1347
|
-
`SELECT af.*, t.id as task_id, t.title as task_title, t.slug as task_slug,
|
|
1348
|
-
a.agent, a.created_at as called_at
|
|
1349
|
-
FROM action_files af
|
|
1350
|
-
JOIN actions a ON af.action_id = a.id
|
|
1351
|
-
JOIN tasks t ON a.task_id = t.id
|
|
1352
|
-
ORDER BY a.created_at DESC
|
|
1353
|
-
LIMIT ?`,
|
|
1354
|
-
[limit]
|
|
1355
|
-
);
|
|
1356
|
-
}
|
|
1357
|
-
async getAgentStats() {
|
|
1358
|
-
const rows = await this.driver.query(
|
|
1359
|
-
`SELECT
|
|
1360
|
-
a.agent,
|
|
1361
|
-
COUNT(*) as actions_total,
|
|
1362
|
-
SUM(CASE WHEN a.status='completed' THEN 1 ELSE 0 END) as actions_done,
|
|
1363
|
-
SUM(CASE WHEN a.status='blocked' THEN 1 ELSE 0 END) as actions_blocked,
|
|
1364
|
-
COUNT(DISTINCT a.task_id) as tasks_worked,
|
|
1365
|
-
COUNT(DISTINCT af.file_path) as files_touched
|
|
1366
|
-
FROM actions a
|
|
1367
|
-
LEFT JOIN action_files af ON af.action_id = a.id
|
|
1368
|
-
GROUP BY a.agent
|
|
1369
|
-
ORDER BY actions_total DESC`
|
|
1370
|
-
);
|
|
1371
|
-
return rows.sort((a, b) => {
|
|
1372
|
-
const ai = AGENT_ORDER.indexOf(a.agent);
|
|
1373
|
-
const bi = AGENT_ORDER.indexOf(b.agent);
|
|
1374
|
-
if (ai === -1 && bi === -1) return 0;
|
|
1375
|
-
if (ai === -1) return 1;
|
|
1376
|
-
if (bi === -1) return -1;
|
|
1377
|
-
return ai - bi;
|
|
1378
|
-
});
|
|
1379
|
-
}
|
|
1380
|
-
async getTimeline(limit) {
|
|
1381
|
-
return this.driver.query(
|
|
1382
|
-
`SELECT a.*, t.title as task_title, t.slug as task_slug, t.status as task_status
|
|
1383
|
-
FROM actions a
|
|
1384
|
-
JOIN tasks t ON a.task_id = t.id
|
|
1385
|
-
ORDER BY a.created_at DESC
|
|
1386
|
-
LIMIT ?`,
|
|
1387
|
-
[limit]
|
|
1388
|
-
);
|
|
1389
|
-
}
|
|
1390
|
-
};
|
|
1391
|
-
|
|
1392
|
-
// src/core/repositories/TaskRepository.ts
|
|
1393
|
-
var TaskRepository = class {
|
|
1394
|
-
constructor(driver) {
|
|
1395
|
-
this.driver = driver;
|
|
1396
|
-
}
|
|
1397
|
-
driver;
|
|
1398
|
-
async add(params) {
|
|
1399
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1400
|
-
return this.driver.insert(
|
|
1401
|
-
`INSERT INTO tasks (slug, title, description, status, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)`,
|
|
1402
|
-
[params.slug, params.title, params.description ?? null, params.status ?? "pending", now, now]
|
|
1403
|
-
);
|
|
1404
|
-
}
|
|
1405
|
-
async addAcceptance(taskId, criteria) {
|
|
1406
|
-
for (const criterion of criteria) {
|
|
1407
|
-
await this.driver.exec(
|
|
1408
|
-
`INSERT INTO task_acceptance (task_id, criterion) VALUES (?, ?)`,
|
|
1409
|
-
[taskId, criterion]
|
|
1410
|
-
);
|
|
1411
|
-
}
|
|
1412
|
-
}
|
|
1413
|
-
async getAll(status, includeArchived = false) {
|
|
1414
|
-
let sql = `SELECT * FROM tasks`;
|
|
1415
|
-
const params = [];
|
|
1416
|
-
const conditions = [];
|
|
1417
|
-
if (!includeArchived) {
|
|
1418
|
-
conditions.push(`archived_at IS NULL`);
|
|
1419
|
-
}
|
|
1420
|
-
if (status) {
|
|
1421
|
-
conditions.push(`status = ?`);
|
|
1422
|
-
params.push(status);
|
|
1423
|
-
}
|
|
1424
|
-
if (conditions.length > 0) {
|
|
1425
|
-
sql += ` WHERE ${conditions.join(" AND ")}`;
|
|
1426
|
-
}
|
|
1427
|
-
sql += ` ORDER BY CASE status WHEN 'pending' THEN 1 WHEN 'in_progress' THEN 2 WHEN 'blocked' THEN 3 WHEN 'done' THEN 4 ELSE 5 END, updated_at DESC`;
|
|
1428
|
-
return this.driver.query(sql, params);
|
|
1429
|
-
}
|
|
1430
|
-
async getAllWithAcceptanceCounts(includeArchived = false) {
|
|
1431
|
-
let sql = `
|
|
1432
|
-
SELECT t.*,
|
|
1433
|
-
COUNT(ta.id) as acceptance_total,
|
|
1434
|
-
COALESCE(SUM(ta.met), 0) as acceptance_met
|
|
1435
|
-
FROM tasks t
|
|
1436
|
-
LEFT JOIN task_acceptance ta ON ta.task_id = t.id
|
|
1437
|
-
`;
|
|
1438
|
-
if (!includeArchived) {
|
|
1439
|
-
sql += ` WHERE t.archived_at IS NULL`;
|
|
1440
|
-
}
|
|
1441
|
-
sql += ` GROUP BY t.id ORDER BY CASE t.status WHEN 'pending' THEN 1 WHEN 'in_progress' THEN 2 WHEN 'blocked' THEN 3 WHEN 'done' THEN 4 ELSE 5 END, t.updated_at DESC`;
|
|
1442
|
-
return this.driver.query(sql);
|
|
1443
|
-
}
|
|
1444
|
-
async getById(id) {
|
|
1445
|
-
return this.driver.queryOne(`SELECT * FROM tasks WHERE id = ?`, [id]);
|
|
1446
|
-
}
|
|
1447
|
-
async getBySlug(slug) {
|
|
1448
|
-
return this.driver.queryOne(`SELECT * FROM tasks WHERE slug = ?`, [slug]);
|
|
1449
|
-
}
|
|
1450
|
-
async getAcceptance(taskId) {
|
|
1451
|
-
return this.driver.query(
|
|
1452
|
-
`SELECT * FROM task_acceptance WHERE task_id = ?`,
|
|
1453
|
-
[taskId]
|
|
1454
|
-
);
|
|
1455
|
-
}
|
|
1456
|
-
async setStatus(id, status, extra) {
|
|
1457
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1458
|
-
if (extra?.started_at) {
|
|
1459
|
-
await this.driver.exec(
|
|
1460
|
-
`UPDATE tasks SET status = ?, started_at = ?, updated_at = ? WHERE id = ?`,
|
|
1461
|
-
[status, extra.started_at, now, id]
|
|
1462
|
-
);
|
|
1463
|
-
} else if (extra?.completed_at) {
|
|
1464
|
-
await this.driver.exec(
|
|
1465
|
-
`UPDATE tasks SET status = ?, completed_at = ?, updated_at = ? WHERE id = ?`,
|
|
1466
|
-
[status, extra.completed_at, now, id]
|
|
1467
|
-
);
|
|
1468
|
-
} else {
|
|
1469
|
-
await this.driver.exec(`UPDATE tasks SET status = ?, updated_at = ? WHERE id = ?`, [status, now, id]);
|
|
1470
|
-
}
|
|
1471
|
-
}
|
|
1472
|
-
async update(id, params) {
|
|
1473
|
-
const sets = [];
|
|
1474
|
-
const vals = [];
|
|
1475
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1476
|
-
if (params.title !== void 0) {
|
|
1477
|
-
sets.push("title = ?");
|
|
1478
|
-
vals.push(params.title);
|
|
1479
|
-
}
|
|
1480
|
-
if (params.description !== void 0) {
|
|
1481
|
-
sets.push("description = ?");
|
|
1482
|
-
vals.push(params.description);
|
|
1483
|
-
}
|
|
1484
|
-
if (params.slug !== void 0) {
|
|
1485
|
-
sets.push("slug = ?");
|
|
1486
|
-
vals.push(params.slug);
|
|
1487
|
-
}
|
|
1488
|
-
if (sets.length === 0) return;
|
|
1489
|
-
sets.push("updated_at = ?");
|
|
1490
|
-
vals.push(now);
|
|
1491
|
-
vals.push(id);
|
|
1492
|
-
await this.driver.exec(`UPDATE tasks SET ${sets.join(", ")} WHERE id = ?`, vals);
|
|
1493
|
-
}
|
|
1494
|
-
async replaceAcceptance(taskId, criteria) {
|
|
1495
|
-
await this.driver.exec(`DELETE FROM task_acceptance WHERE task_id = ?`, [taskId]);
|
|
1496
|
-
for (const criterion of criteria) {
|
|
1497
|
-
await this.driver.exec(
|
|
1498
|
-
`INSERT INTO task_acceptance (task_id, criterion) VALUES (?, ?)`,
|
|
1499
|
-
[taskId, criterion]
|
|
1500
|
-
);
|
|
1501
|
-
}
|
|
1502
|
-
}
|
|
1503
|
-
async archive(id) {
|
|
1504
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1505
|
-
await this.driver.exec(`UPDATE tasks SET archived_at = ?, updated_at = ? WHERE id = ?`, [now, now, id]);
|
|
1506
|
-
}
|
|
1507
|
-
async unarchive(id) {
|
|
1508
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1509
|
-
await this.driver.exec(`UPDATE tasks SET archived_at = NULL, updated_at = ? WHERE id = ?`, [now, id]);
|
|
1510
|
-
}
|
|
1511
|
-
async getArchived() {
|
|
1512
|
-
return this.driver.query(
|
|
1513
|
-
`SELECT * FROM tasks WHERE archived_at IS NOT NULL ORDER BY archived_at DESC`
|
|
1514
|
-
);
|
|
1515
|
-
}
|
|
1516
|
-
async claim(id, agent, now) {
|
|
1517
|
-
return this.driver.exec(
|
|
1518
|
-
`UPDATE tasks SET status = 'in_progress', assigned_to = ?, started_at = ?, updated_at = ? WHERE id = ? AND status = 'pending'`,
|
|
1519
|
-
[agent, now, now, id]
|
|
1520
|
-
);
|
|
1521
|
-
}
|
|
1522
|
-
async markAcceptanceMet(criterionId) {
|
|
1523
|
-
await this.driver.exec(`UPDATE task_acceptance SET met = 1 WHERE id = ?`, [criterionId]);
|
|
1524
|
-
}
|
|
1525
|
-
async getStatusSummary() {
|
|
1526
|
-
return this.driver.query(
|
|
1527
|
-
`SELECT status, COUNT(*) as total FROM tasks WHERE archived_at IS NULL GROUP BY status`
|
|
1528
|
-
);
|
|
1529
|
-
}
|
|
1530
|
-
};
|
|
1531
|
-
|
|
1532
|
-
// src/core/db.ts
|
|
1533
|
-
var HarnessDB = class {
|
|
1534
|
-
tasks;
|
|
1535
|
-
actions;
|
|
1536
|
-
stats;
|
|
1537
|
-
driver;
|
|
1538
|
-
config;
|
|
1539
|
-
constructor(driver, config) {
|
|
1540
|
-
this.driver = driver;
|
|
1541
|
-
this.config = config;
|
|
1542
|
-
this.tasks = new TaskRepository(driver);
|
|
1543
|
-
this.actions = new ActionRepository(driver);
|
|
1544
|
-
this.stats = new StatsRepository(driver);
|
|
1545
|
-
}
|
|
1546
|
-
// ─── Tasks (public facade — delegates to TaskRepository) ──────────────────
|
|
1547
|
-
async addTask(params) {
|
|
1548
|
-
const taskId = await this.tasks.add({
|
|
1549
|
-
slug: params.slug,
|
|
1550
|
-
title: params.title,
|
|
1551
|
-
description: params.description
|
|
1552
|
-
});
|
|
1553
|
-
if (params.acceptance?.length) {
|
|
1554
|
-
await this.tasks.addAcceptance(taskId, params.acceptance);
|
|
1555
|
-
}
|
|
1556
|
-
await this.regenerateCurrentMd();
|
|
1557
|
-
return await this.tasks.getById(taskId);
|
|
1558
|
-
}
|
|
1559
|
-
async getTasks(status, includeArchived = false) {
|
|
1560
|
-
return this.tasks.getAll(status, includeArchived);
|
|
1561
|
-
}
|
|
1562
|
-
async getTaskById(id) {
|
|
1563
|
-
return this.tasks.getById(id);
|
|
1564
|
-
}
|
|
1565
|
-
async getTaskBySlug(slug) {
|
|
1566
|
-
return this.tasks.getBySlug(slug);
|
|
1567
|
-
}
|
|
1568
|
-
async getTaskAcceptance(taskId) {
|
|
1569
|
-
return this.tasks.getAcceptance(taskId);
|
|
1570
|
-
}
|
|
1571
|
-
async updateTaskStatus(idOrSlug, status) {
|
|
1572
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1573
|
-
const task2 = typeof idOrSlug === "number" ? await this.tasks.getById(idOrSlug) : await this.tasks.getBySlug(idOrSlug);
|
|
1574
|
-
if (!task2) throw new Error(`Task not found: ${idOrSlug}`);
|
|
1575
|
-
if (status === "in_progress" && !task2.started_at) {
|
|
1576
|
-
await this.tasks.setStatus(task2.id, status, { started_at: now });
|
|
1577
|
-
} else if (status === "done") {
|
|
1578
|
-
await this.tasks.setStatus(task2.id, status, { completed_at: now });
|
|
1579
|
-
} else {
|
|
1580
|
-
await this.tasks.setStatus(task2.id, status);
|
|
1581
|
-
}
|
|
1582
|
-
await this.regenerateCurrentMd();
|
|
1583
|
-
return await this.tasks.getById(task2.id);
|
|
1584
|
-
}
|
|
1585
|
-
async claimTask(id, agent) {
|
|
1586
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1587
|
-
return this.driver.transaction(async (tx) => {
|
|
1588
|
-
const txTasks = new TaskRepository(tx);
|
|
1589
|
-
const changed = await txTasks.claim(id, agent, now);
|
|
1590
|
-
if (!changed) return null;
|
|
1591
|
-
const task2 = await txTasks.getById(id);
|
|
1592
|
-
if (!task2 || task2.status !== "in_progress" || task2.assigned_to !== agent) return null;
|
|
1593
|
-
await this.regenerateCurrentMd();
|
|
1594
|
-
return task2;
|
|
1595
|
-
});
|
|
1596
|
-
}
|
|
1597
|
-
async markAcceptanceMet(criterionId) {
|
|
1598
|
-
return this.tasks.markAcceptanceMet(criterionId);
|
|
1599
|
-
}
|
|
1600
|
-
async updateTask(id, params) {
|
|
1601
|
-
await this.tasks.update(id, params);
|
|
1602
|
-
await this.regenerateCurrentMd();
|
|
1603
|
-
return await this.tasks.getById(id);
|
|
1604
|
-
}
|
|
1605
|
-
async updateTaskAcceptance(taskId, criteria) {
|
|
1606
|
-
await this.tasks.replaceAcceptance(taskId, criteria);
|
|
1607
|
-
await this.regenerateCurrentMd();
|
|
1608
|
-
}
|
|
1609
|
-
async archiveTask(id) {
|
|
1610
|
-
await this.tasks.archive(id);
|
|
1611
|
-
await this.regenerateCurrentMd();
|
|
1612
|
-
return await this.tasks.getById(id);
|
|
1613
|
-
}
|
|
1614
|
-
async unarchiveTask(id) {
|
|
1615
|
-
await this.tasks.unarchive(id);
|
|
1616
|
-
await this.regenerateCurrentMd();
|
|
1617
|
-
return await this.tasks.getById(id);
|
|
1618
|
-
}
|
|
1619
|
-
async getArchivedTasks() {
|
|
1620
|
-
return this.tasks.getArchived();
|
|
1621
|
-
}
|
|
1622
|
-
async getStatusSummary() {
|
|
1623
|
-
return this.tasks.getStatusSummary();
|
|
1624
|
-
}
|
|
1625
|
-
// ─── Actions (public facade — delegates to ActionRepository) ──────────────
|
|
1626
|
-
async startAction(taskId, agent) {
|
|
1627
|
-
const id = randomUUID();
|
|
1628
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1629
|
-
await this.actions.create(id, taskId, agent, now);
|
|
1630
|
-
await this.regenerateCurrentMd();
|
|
1631
|
-
return await this.actions.getById(id);
|
|
1632
|
-
}
|
|
1633
|
-
async writeSection(actionId, sectionType, content) {
|
|
1634
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1635
|
-
await this.actions.addSection(actionId, sectionType, content, now);
|
|
1636
|
-
await this.regenerateCurrentMd();
|
|
1637
|
-
}
|
|
1638
|
-
async completeAction(actionId, summary) {
|
|
1639
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1640
|
-
await this.actions.complete(actionId, summary, now);
|
|
1641
|
-
await this.regenerateCurrentMd();
|
|
1642
|
-
return await this.actions.getById(actionId);
|
|
1643
|
-
}
|
|
1644
|
-
async closeOrphanedActions(taskId) {
|
|
1645
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1646
|
-
return this.actions.closeOrphaned(taskId, now);
|
|
1647
|
-
}
|
|
1648
|
-
async getAction(actionId) {
|
|
1649
|
-
return this.actions.getById(actionId);
|
|
1650
|
-
}
|
|
1651
|
-
async getActionsForTask(taskId) {
|
|
1652
|
-
return this.actions.getForTask(taskId);
|
|
1653
|
-
}
|
|
1654
|
-
async getActionSections(actionId) {
|
|
1655
|
-
return this.actions.getSections(actionId);
|
|
1656
|
-
}
|
|
1657
|
-
async recordFile(actionId, filePath, operation, notes) {
|
|
1658
|
-
return this.actions.addFile(actionId, filePath, operation, notes ?? null);
|
|
1659
|
-
}
|
|
1660
|
-
async recordTool(actionId, toolName, argsJson, resultSummary) {
|
|
1661
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1662
|
-
return this.actions.addTool(actionId, toolName, argsJson ?? null, resultSummary ?? null, now);
|
|
1663
|
-
}
|
|
1664
|
-
async getFilesForTask(taskId) {
|
|
1665
|
-
return this.actions.getFilesForTask(taskId);
|
|
1666
|
-
}
|
|
1667
|
-
async getTopTools(limit = 10) {
|
|
1668
|
-
return this.actions.getTopTools(limit);
|
|
1669
|
-
}
|
|
1670
|
-
// ─── current.md fallback ──────────────────────────────────────────────────
|
|
1671
|
-
async regenerateCurrentMd() {
|
|
1672
|
-
if (!this.config.storage.markdownFallback.enabled) return;
|
|
1673
|
-
const mdPath = resolve6(this.config.storage.markdownFallback.path);
|
|
1674
|
-
mkdirSync7(dirname4(mdPath), { recursive: true });
|
|
1675
|
-
const inProgress = await this.tasks.getAll("in_progress");
|
|
1676
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1677
|
-
let md = `<!-- AUTO-GENERATED by agent-harness-kit \u2014 DO NOT EDIT MANUALLY -->
|
|
1678
|
-
`;
|
|
1679
|
-
md += `<!-- Last updated: ${now} -->
|
|
1680
|
-
|
|
1681
|
-
`;
|
|
1682
|
-
md += `# Current Session
|
|
1683
|
-
|
|
1684
|
-
`;
|
|
1685
|
-
if (inProgress.length === 0) {
|
|
1686
|
-
md += `## No tasks in progress
|
|
1687
|
-
|
|
1688
|
-
`;
|
|
1689
|
-
const pending = await this.tasks.getAll("pending");
|
|
1690
|
-
if (pending.length > 0) {
|
|
1691
|
-
md += `### Next pending tasks
|
|
1692
|
-
`;
|
|
1693
|
-
for (const t of pending.slice(0, 5)) {
|
|
1694
|
-
md += `- **#${t.id}** ${t.title} (\`${t.slug}\`)
|
|
1695
|
-
`;
|
|
1696
|
-
}
|
|
1697
|
-
}
|
|
1698
|
-
} else {
|
|
1699
|
-
for (const task2 of inProgress) {
|
|
1700
|
-
md += `## Active Task
|
|
1701
|
-
`;
|
|
1702
|
-
md += `- **ID:** ${task2.id}
|
|
1703
|
-
`;
|
|
1704
|
-
md += `- **Slug:** ${task2.slug}
|
|
1705
|
-
`;
|
|
1706
|
-
md += `- **Status:** ${task2.status}
|
|
1707
|
-
`;
|
|
1708
|
-
md += `- **Started:** ${task2.started_at ?? "unknown"}
|
|
1709
|
-
|
|
1710
|
-
`;
|
|
1711
|
-
const taskActions = await this.actions.getForTask(task2.id);
|
|
1712
|
-
if (taskActions.length > 0) {
|
|
1713
|
-
md += `## Actions this session
|
|
1714
|
-
`;
|
|
1715
|
-
md += `| Agent | Status | Summary | Started |
|
|
1716
|
-
`;
|
|
1717
|
-
md += `|----------|-------------|----------------------------------|-------------|
|
|
1718
|
-
`;
|
|
1719
|
-
for (const a of taskActions) {
|
|
1720
|
-
const started = a.created_at.slice(11, 16);
|
|
1721
|
-
const summary = (a.summary ?? "").slice(0, 34).padEnd(34);
|
|
1722
|
-
md += `| ${a.agent.padEnd(8)} | ${a.status.padEnd(11)} | ${summary} | ${started} |
|
|
1723
|
-
`;
|
|
1724
|
-
}
|
|
1725
|
-
md += `
|
|
1726
|
-
`;
|
|
1727
|
-
}
|
|
1728
|
-
const acceptance = await this.tasks.getAcceptance(task2.id);
|
|
1729
|
-
if (acceptance.length > 0) {
|
|
1730
|
-
md += `## Acceptance Criteria
|
|
1731
|
-
`;
|
|
1732
|
-
for (const a of acceptance) {
|
|
1733
|
-
md += `- [${a.met ? "x" : " "}] ${a.criterion}
|
|
1734
|
-
`;
|
|
1735
|
-
}
|
|
1736
|
-
md += `
|
|
1737
|
-
`;
|
|
1738
|
-
}
|
|
1739
|
-
}
|
|
1740
|
-
}
|
|
1741
|
-
writeFileSync7(mdPath, md, "utf8");
|
|
1742
|
-
}
|
|
1743
|
-
// ─── Raw query escape hatch ───────────────────────────────────────────────
|
|
1744
|
-
async queryRaw(sql, ...params) {
|
|
1745
|
-
return this.driver.query(sql, params);
|
|
1746
|
-
}
|
|
1747
|
-
// ─── Export helpers ───────────────────────────────────────────────────────
|
|
1748
|
-
async exportJson() {
|
|
1749
|
-
return {
|
|
1750
|
-
tasks: await this.tasks.getAll(void 0, true),
|
|
1751
|
-
actions: await this.actions.getAll(),
|
|
1752
|
-
sections: await this.actions.getAllSections()
|
|
1753
|
-
};
|
|
1754
|
-
}
|
|
1755
|
-
async reconnect() {
|
|
1756
|
-
await this.driver.reconnect();
|
|
1757
|
-
}
|
|
1758
|
-
async close() {
|
|
1759
|
-
await this.driver.close();
|
|
1760
|
-
}
|
|
1761
|
-
// ─── feature_list.json sync ───────────────────────────────────────────────
|
|
1762
|
-
async syncFromFeatureList(seeds) {
|
|
1763
|
-
let added = 0;
|
|
1764
|
-
let skipped = 0;
|
|
1765
|
-
for (const t of seeds) {
|
|
1766
|
-
if (await this.tasks.getBySlug(t.slug)) {
|
|
1767
|
-
skipped++;
|
|
1768
|
-
continue;
|
|
1769
|
-
}
|
|
1770
|
-
await this.addTask(t);
|
|
1771
|
-
added++;
|
|
1772
|
-
}
|
|
1773
|
-
return { added, skipped };
|
|
1774
|
-
}
|
|
1775
|
-
async writeFeatureList(cwd2) {
|
|
1776
|
-
const allTasks = await this.tasks.getAll(void 0, true);
|
|
1777
|
-
const list = await Promise.all(
|
|
1778
|
-
allTasks.map(async (t) => ({
|
|
1779
|
-
slug: t.slug,
|
|
1780
|
-
title: t.title,
|
|
1781
|
-
description: t.description ?? void 0,
|
|
1782
|
-
acceptance: (await this.tasks.getAcceptance(t.id)).map((a) => a.criterion),
|
|
1783
|
-
status: t.status
|
|
1784
|
-
}))
|
|
1785
|
-
);
|
|
1786
|
-
const path = join8(resolve6(cwd2), this.config.storage.dir, "feature_list.json");
|
|
1787
|
-
mkdirSync7(dirname4(path), { recursive: true });
|
|
1788
|
-
writeFileSync7(path, JSON.stringify(list, null, 2) + "\n", "utf8");
|
|
1789
|
-
}
|
|
1790
|
-
};
|
|
1791
|
-
async function openDB(config, cwd2) {
|
|
1792
|
-
const dbConfig = config.database;
|
|
1793
|
-
let driver;
|
|
1794
|
-
if (dbConfig.type === "postgres") {
|
|
1795
|
-
const { PostgresDriver } = await import("./postgres-IOQE32DM.js");
|
|
1796
|
-
driver = new PostgresDriver(dbConfig);
|
|
1797
|
-
} else if (dbConfig.type === "mysql") {
|
|
1798
|
-
const { MySQLDriver } = await import("./mysql-THKQOXIS.js");
|
|
1799
|
-
driver = new MySQLDriver(dbConfig);
|
|
1800
|
-
} else {
|
|
1801
|
-
const { SQLiteDriver } = await import("./sqlite-KWYK4IJW.js");
|
|
1802
|
-
if (dbConfig.type !== "sqlite") {
|
|
1803
|
-
throw new Error("Invalid database type");
|
|
1804
|
-
}
|
|
1805
|
-
driver = new SQLiteDriver(resolve6(cwd2, dbConfig.path));
|
|
1806
|
-
}
|
|
1807
|
-
await driver.ensureSchema();
|
|
1808
|
-
return new HarnessDB(driver, config);
|
|
1809
|
-
}
|
|
1810
|
-
|
|
1811
1316
|
// src/commands/dashboard.ts
|
|
1812
|
-
var __dirname3 =
|
|
1317
|
+
var __dirname3 = dirname4(fileURLToPath3(import.meta.url));
|
|
1813
1318
|
async function runDashboard(cwd2, opts) {
|
|
1814
1319
|
const config = await loadConfig(cwd2);
|
|
1815
1320
|
const db = await openDB(config, cwd2);
|
|
1816
|
-
const dbPath = config.database.type === "sqlite" ?
|
|
1321
|
+
const dbPath = config.database.type === "sqlite" ? resolve6(cwd2, config.database.path) : null;
|
|
1817
1322
|
const staticPath = join9(__dirname3, "dashboard-dist");
|
|
1818
1323
|
const { url } = await startDashboardServer(db, dbPath, staticPath, opts.port);
|
|
1819
1324
|
console.log(pc2.green(`\u2713`) + ` Dashboard running at ${pc2.bold(pc2.cyan(url))}`);
|
|
@@ -1834,26 +1339,37 @@ async function runDashboard(cwd2, opts) {
|
|
|
1834
1339
|
import pc3 from "picocolors";
|
|
1835
1340
|
|
|
1836
1341
|
// src/core/doctor.ts
|
|
1837
|
-
import { existsSync as
|
|
1838
|
-
import {
|
|
1342
|
+
import { existsSync as existsSync9, readFileSync as readFileSync7 } from "fs";
|
|
1343
|
+
import { homedir } from "os";
|
|
1344
|
+
import { dirname as dirname6, join as join11 } from "path";
|
|
1839
1345
|
import { fileURLToPath as fileURLToPath5 } from "url";
|
|
1840
1346
|
|
|
1841
1347
|
// src/core/package-data.ts
|
|
1348
|
+
import { existsSync as existsSync8 } from "fs";
|
|
1842
1349
|
import { createRequire } from "module";
|
|
1843
|
-
import { dirname as
|
|
1350
|
+
import { dirname as dirname5, join as join10 } from "path";
|
|
1844
1351
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
1845
1352
|
var require2 = createRequire(import.meta.url);
|
|
1846
|
-
var
|
|
1353
|
+
var here = dirname5(fileURLToPath4(import.meta.url));
|
|
1354
|
+
var candidates = [join10(here, "..", "..", "package.json"), join10(here, "..", "package.json")];
|
|
1355
|
+
var pkgPath = candidates.find((p8) => existsSync8(p8)) ?? candidates[0];
|
|
1847
1356
|
var pkg = require2(pkgPath);
|
|
1848
1357
|
|
|
1849
1358
|
// src/core/doctor.ts
|
|
1850
1359
|
var REGISTRY_URL = `https://registry.npmjs.org/${pkg.name}/latest`;
|
|
1851
1360
|
var TIMEOUT_MS = 2e3;
|
|
1361
|
+
var LIB_VERSION_CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
1852
1362
|
var AGENT_NAMES = ["lead", "explorer", "consultant", "builder", "reviewer"];
|
|
1853
|
-
var SKILL_NAMES = ["ahk-ask", "ahk-consultant", "ahk-triage"];
|
|
1854
|
-
var __dirname4 =
|
|
1363
|
+
var SKILL_NAMES = ["ahk-ask", "ahk-consultant", "ahk-triage", "ahk-review"];
|
|
1364
|
+
var __dirname4 = dirname6(fileURLToPath5(import.meta.url));
|
|
1365
|
+
var libVersionCache = null;
|
|
1855
1366
|
async function checkLibVersion() {
|
|
1856
1367
|
const current = pkg.version;
|
|
1368
|
+
if (libVersionCache && Date.now() - libVersionCache.fetchedAt < LIB_VERSION_CACHE_TTL_MS) {
|
|
1369
|
+
if (libVersionCache.status.current === current) {
|
|
1370
|
+
return libVersionCache.status;
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1857
1373
|
try {
|
|
1858
1374
|
const controller = new AbortController();
|
|
1859
1375
|
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
@@ -1862,9 +1378,13 @@ async function checkLibVersion() {
|
|
|
1862
1378
|
const data = await res.json();
|
|
1863
1379
|
const latest = data.version;
|
|
1864
1380
|
const outdated = isNewer(latest, current);
|
|
1865
|
-
|
|
1381
|
+
const status = { current, latest, outdated };
|
|
1382
|
+
libVersionCache = { status, fetchedAt: Date.now() };
|
|
1383
|
+
return status;
|
|
1866
1384
|
} catch {
|
|
1867
|
-
|
|
1385
|
+
const status = { current, latest: null, outdated: false };
|
|
1386
|
+
libVersionCache = { status, fetchedAt: Date.now() };
|
|
1387
|
+
return status;
|
|
1868
1388
|
}
|
|
1869
1389
|
}
|
|
1870
1390
|
function isNewer(latest, current) {
|
|
@@ -1888,7 +1408,7 @@ function getProviderAgentInfo(provider) {
|
|
|
1888
1408
|
}
|
|
1889
1409
|
}
|
|
1890
1410
|
function generateExpectedAgentContent(agentName, provider, vars) {
|
|
1891
|
-
const { projectName, allowedPaths, writablePaths } = vars;
|
|
1411
|
+
const { projectName, allowedPaths, writablePaths, model } = vars;
|
|
1892
1412
|
if (provider === "claude-code") {
|
|
1893
1413
|
const templateFns = {
|
|
1894
1414
|
lead: () => agentLead({ projectName }),
|
|
@@ -1897,7 +1417,7 @@ function generateExpectedAgentContent(agentName, provider, vars) {
|
|
|
1897
1417
|
builder: () => agentBuilder({ projectName, writablePaths }),
|
|
1898
1418
|
reviewer: () => agentReviewer({ projectName })
|
|
1899
1419
|
};
|
|
1900
|
-
return translateFrontmatterForClaudeCode(templateFns[agentName](), agentName);
|
|
1420
|
+
return translateFrontmatterForClaudeCode(templateFns[agentName](), agentName, model);
|
|
1901
1421
|
}
|
|
1902
1422
|
if (provider === "opencode") {
|
|
1903
1423
|
const templateFns = {
|
|
@@ -1910,27 +1430,27 @@ function generateExpectedAgentContent(agentName, provider, vars) {
|
|
|
1910
1430
|
return translateFrontmatterForOpenCode(templateFns[agentName]());
|
|
1911
1431
|
}
|
|
1912
1432
|
const tomlFns = {
|
|
1913
|
-
lead: () => agentLeadToml({ projectName }),
|
|
1914
|
-
explorer: () => agentExplorerToml({ projectName, allowedPaths }),
|
|
1915
|
-
consultant: () => agentConsultantToml({ projectName }),
|
|
1916
|
-
builder: () => agentBuilderToml({ projectName, writablePaths }),
|
|
1917
|
-
reviewer: () => agentReviewerToml({ projectName })
|
|
1433
|
+
lead: () => agentLeadToml({ projectName, model }),
|
|
1434
|
+
explorer: () => agentExplorerToml({ projectName, allowedPaths, model }),
|
|
1435
|
+
consultant: () => agentConsultantToml({ projectName, model }),
|
|
1436
|
+
builder: () => agentBuilderToml({ projectName, writablePaths, model }),
|
|
1437
|
+
reviewer: () => agentReviewerToml({ projectName, model })
|
|
1918
1438
|
};
|
|
1919
1439
|
return tomlFns[agentName]();
|
|
1920
1440
|
}
|
|
1921
|
-
function
|
|
1922
|
-
const { agentsDir, ext } = getProviderAgentInfo(provider);
|
|
1441
|
+
function checkAgentFilesAtRoot(agentsRoot, ext, provider, projectName, allowedPaths, writablePaths, models) {
|
|
1923
1442
|
return AGENT_NAMES.map((name) => {
|
|
1924
|
-
const filePath = join11(
|
|
1925
|
-
if (!
|
|
1443
|
+
const filePath = join11(agentsRoot, `${name}${ext}`);
|
|
1444
|
+
if (!existsSync9(filePath)) {
|
|
1926
1445
|
return { name, status: "missing" };
|
|
1927
1446
|
}
|
|
1928
1447
|
try {
|
|
1929
|
-
const live =
|
|
1448
|
+
const live = readFileSync7(filePath, "utf8");
|
|
1930
1449
|
const expected = generateExpectedAgentContent(name, provider, {
|
|
1931
1450
|
projectName,
|
|
1932
1451
|
allowedPaths,
|
|
1933
|
-
writablePaths
|
|
1452
|
+
writablePaths,
|
|
1453
|
+
model: models[name]
|
|
1934
1454
|
});
|
|
1935
1455
|
return { name, status: live === expected ? "ok" : "outdated" };
|
|
1936
1456
|
} catch {
|
|
@@ -1938,6 +1458,18 @@ function checkAgentFiles(cwd2, provider, projectName, allowedPaths, writablePath
|
|
|
1938
1458
|
}
|
|
1939
1459
|
});
|
|
1940
1460
|
}
|
|
1461
|
+
function checkAgentFiles(cwd2, provider, projectName, allowedPaths, writablePaths, models) {
|
|
1462
|
+
const { agentsDir, ext } = getProviderAgentInfo(provider);
|
|
1463
|
+
return checkAgentFilesAtRoot(
|
|
1464
|
+
join11(cwd2, agentsDir),
|
|
1465
|
+
ext,
|
|
1466
|
+
provider,
|
|
1467
|
+
projectName,
|
|
1468
|
+
allowedPaths,
|
|
1469
|
+
writablePaths,
|
|
1470
|
+
models
|
|
1471
|
+
);
|
|
1472
|
+
}
|
|
1941
1473
|
function getProviderSkillsDir(provider) {
|
|
1942
1474
|
switch (provider) {
|
|
1943
1475
|
case "claude-code":
|
|
@@ -1950,24 +1482,76 @@ function getProviderSkillsDir(provider) {
|
|
|
1950
1482
|
return ".claude/skills";
|
|
1951
1483
|
}
|
|
1952
1484
|
}
|
|
1953
|
-
function
|
|
1954
|
-
const skillsDir = getProviderSkillsDir(provider);
|
|
1485
|
+
function checkSkillsAtRoot(skillsRoot) {
|
|
1955
1486
|
const skillSourceBase = join11(__dirname4, "skills");
|
|
1956
1487
|
return SKILL_NAMES.map((name) => {
|
|
1957
|
-
const livePath = join11(
|
|
1488
|
+
const livePath = join11(skillsRoot, name, "SKILL.md");
|
|
1958
1489
|
const sourcePath = join11(skillSourceBase, name, "SKILL.md");
|
|
1959
|
-
if (!
|
|
1490
|
+
if (!existsSync9(livePath)) {
|
|
1960
1491
|
return { name, status: "missing" };
|
|
1961
1492
|
}
|
|
1962
1493
|
try {
|
|
1963
|
-
const live =
|
|
1964
|
-
const source =
|
|
1494
|
+
const live = readFileSync7(livePath, "utf8");
|
|
1495
|
+
const source = readFileSync7(sourcePath, "utf8");
|
|
1965
1496
|
return { name, status: live === source ? "ok" : "outdated" };
|
|
1966
1497
|
} catch {
|
|
1967
1498
|
return { name, status: "outdated" };
|
|
1968
1499
|
}
|
|
1969
1500
|
});
|
|
1970
1501
|
}
|
|
1502
|
+
function checkSkills(cwd2, provider) {
|
|
1503
|
+
const skillsDir = getProviderSkillsDir(provider);
|
|
1504
|
+
return checkSkillsAtRoot(join11(cwd2, skillsDir));
|
|
1505
|
+
}
|
|
1506
|
+
function getGlobalProviderAgentDir(provider, homeDir) {
|
|
1507
|
+
switch (provider) {
|
|
1508
|
+
case "claude-code":
|
|
1509
|
+
return { agentsDir: join11(homeDir, ".claude", "agents"), ext: ".md" };
|
|
1510
|
+
case "opencode":
|
|
1511
|
+
return { agentsDir: join11(homeDir, ".config", "opencode", "agents"), ext: ".md" };
|
|
1512
|
+
case "codex-cli":
|
|
1513
|
+
return { agentsDir: join11(homeDir, ".codex", "agents"), ext: ".toml" };
|
|
1514
|
+
default:
|
|
1515
|
+
return { agentsDir: join11(homeDir, ".claude", "agents"), ext: ".md" };
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
function getGlobalProviderSkillsDir(provider, homeDir) {
|
|
1519
|
+
switch (provider) {
|
|
1520
|
+
case "claude-code":
|
|
1521
|
+
return join11(homeDir, ".claude", "skills");
|
|
1522
|
+
case "opencode":
|
|
1523
|
+
return join11(homeDir, ".config", "opencode", "skills");
|
|
1524
|
+
case "codex-cli":
|
|
1525
|
+
return join11(homeDir, ".agents", "skills");
|
|
1526
|
+
default:
|
|
1527
|
+
return join11(homeDir, ".claude", "skills");
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
async function getGlobalDoctorStatus(provider, config, homeDir = homedir()) {
|
|
1531
|
+
const projectName = config.project.name;
|
|
1532
|
+
const allowedPaths = (config.agents.explorer.allowedPaths ?? []).join(", ");
|
|
1533
|
+
const writablePaths = (config.agents.builder.writablePaths ?? []).join(", ");
|
|
1534
|
+
const models = {
|
|
1535
|
+
lead: config.agents.lead.model,
|
|
1536
|
+
explorer: config.agents.explorer.model,
|
|
1537
|
+
consultant: config.agents.consultant?.model,
|
|
1538
|
+
builder: config.agents.builder.model,
|
|
1539
|
+
reviewer: config.agents.reviewer.model
|
|
1540
|
+
};
|
|
1541
|
+
const { agentsDir, ext } = getGlobalProviderAgentDir(provider, homeDir);
|
|
1542
|
+
const agents = checkAgentFilesAtRoot(
|
|
1543
|
+
agentsDir,
|
|
1544
|
+
ext,
|
|
1545
|
+
provider,
|
|
1546
|
+
projectName,
|
|
1547
|
+
allowedPaths,
|
|
1548
|
+
writablePaths,
|
|
1549
|
+
models
|
|
1550
|
+
);
|
|
1551
|
+
const skillsDir = getGlobalProviderSkillsDir(provider, homeDir);
|
|
1552
|
+
const skills = checkSkillsAtRoot(skillsDir);
|
|
1553
|
+
return { agents, skills };
|
|
1554
|
+
}
|
|
1971
1555
|
async function getDoctorStatus(cwd2) {
|
|
1972
1556
|
const lib = await checkLibVersion();
|
|
1973
1557
|
let config;
|
|
@@ -1984,7 +1568,14 @@ async function getDoctorStatus(cwd2) {
|
|
|
1984
1568
|
const projectName = config.project.name;
|
|
1985
1569
|
const allowedPaths = (config.agents.explorer.allowedPaths ?? []).join(", ");
|
|
1986
1570
|
const writablePaths = (config.agents.builder.writablePaths ?? []).join(", ");
|
|
1987
|
-
const
|
|
1571
|
+
const models = {
|
|
1572
|
+
lead: config.agents.lead.model,
|
|
1573
|
+
explorer: config.agents.explorer.model,
|
|
1574
|
+
consultant: config.agents.consultant?.model,
|
|
1575
|
+
builder: config.agents.builder.model,
|
|
1576
|
+
reviewer: config.agents.reviewer.model
|
|
1577
|
+
};
|
|
1578
|
+
const agents = checkAgentFiles(cwd2, provider, projectName, allowedPaths, writablePaths, models);
|
|
1988
1579
|
const skills = checkSkills(cwd2, provider);
|
|
1989
1580
|
return { lib, agents, skills };
|
|
1990
1581
|
}
|
|
@@ -2074,7 +1665,7 @@ async function runDoctor(cwd2) {
|
|
|
2074
1665
|
}
|
|
2075
1666
|
|
|
2076
1667
|
// src/commands/export.ts
|
|
2077
|
-
import { writeFileSync as
|
|
1668
|
+
import { writeFileSync as writeFileSync7 } from "fs";
|
|
2078
1669
|
import pc4 from "picocolors";
|
|
2079
1670
|
async function runExport(cwd2, opts) {
|
|
2080
1671
|
if (!opts.sql && !opts.json) {
|
|
@@ -2088,7 +1679,7 @@ async function runExport(cwd2, opts) {
|
|
|
2088
1679
|
const data = await db.exportJson();
|
|
2089
1680
|
const out = JSON.stringify(data, null, 2) + "\n";
|
|
2090
1681
|
if (opts.output) {
|
|
2091
|
-
|
|
1682
|
+
writeFileSync7(opts.output, out, "utf8");
|
|
2092
1683
|
console.log(pc4.green(`\u2713 Exported JSON \u2192 ${opts.output}`));
|
|
2093
1684
|
} else {
|
|
2094
1685
|
process.stdout.write(out);
|
|
@@ -2105,8 +1696,8 @@ async function runExport(cwd2, opts) {
|
|
|
2105
1696
|
|
|
2106
1697
|
// src/commands/health.ts
|
|
2107
1698
|
import { spawnSync } from "child_process";
|
|
2108
|
-
import { existsSync as
|
|
2109
|
-
import { join as join12, resolve as
|
|
1699
|
+
import { existsSync as existsSync10 } from "fs";
|
|
1700
|
+
import { join as join12, resolve as resolve7 } from "path";
|
|
2110
1701
|
import pc5 from "picocolors";
|
|
2111
1702
|
function checkLine(label, ok3, message, indent = 0) {
|
|
2112
1703
|
const prefix = label ? pc5.cyan(`[${label}] `) : " ".repeat(indent);
|
|
@@ -2124,8 +1715,8 @@ async function runHealth(cwd2) {
|
|
|
2124
1715
|
let allOk = true;
|
|
2125
1716
|
let dbOk;
|
|
2126
1717
|
if (config.database.type === "sqlite") {
|
|
2127
|
-
const dbPath =
|
|
2128
|
-
dbOk =
|
|
1718
|
+
const dbPath = resolve7(cwd2, config.database.path);
|
|
1719
|
+
dbOk = existsSync10(dbPath);
|
|
2129
1720
|
checkLine("checking DB", dbOk, `${config.database.path} reachable`);
|
|
2130
1721
|
} else {
|
|
2131
1722
|
dbOk = true;
|
|
@@ -2139,7 +1730,7 @@ async function runHealth(cwd2) {
|
|
|
2139
1730
|
for (let i = 0; i < agentNames.length; i++) {
|
|
2140
1731
|
const name = agentNames[i];
|
|
2141
1732
|
const agentPath = join12(cwd2, agentsDir, `${name}${providerFiles.agentExtension}`);
|
|
2142
|
-
const ok3 =
|
|
1733
|
+
const ok3 = existsSync10(agentPath);
|
|
2143
1734
|
checkLine(
|
|
2144
1735
|
i === 0 ? "checking agents" : null,
|
|
2145
1736
|
ok3,
|
|
@@ -2150,8 +1741,8 @@ async function runHealth(cwd2) {
|
|
|
2150
1741
|
}
|
|
2151
1742
|
if (config.tools.mcp.enabled) {
|
|
2152
1743
|
const mcpFile = providerFiles.mcpFile;
|
|
2153
|
-
const mcpPath =
|
|
2154
|
-
const mcpOk =
|
|
1744
|
+
const mcpPath = resolve7(cwd2, mcpFile);
|
|
1745
|
+
const mcpOk = existsSync10(mcpPath);
|
|
2155
1746
|
checkLine("checking MCP", mcpOk, `${mcpFile} valid`);
|
|
2156
1747
|
if (!mcpOk) allOk = false;
|
|
2157
1748
|
}
|
|
@@ -2160,8 +1751,8 @@ async function runHealth(cwd2) {
|
|
|
2160
1751
|
console.error(pc5.red("\u2717 Harness checks failed \u2014 fix the above before running health.sh"));
|
|
2161
1752
|
process.exit(1);
|
|
2162
1753
|
}
|
|
2163
|
-
const scriptPath =
|
|
2164
|
-
if (!
|
|
1754
|
+
const scriptPath = resolve7(cwd2, config.health.scriptPath);
|
|
1755
|
+
if (!existsSync10(scriptPath)) {
|
|
2165
1756
|
console.error(pc5.red(`\u2717 health.sh not found: ${scriptPath}`));
|
|
2166
1757
|
console.error(" Run ahk init first.");
|
|
2167
1758
|
process.exit(1);
|
|
@@ -2197,11 +1788,56 @@ function getProviderHealthFiles(provider) {
|
|
|
2197
1788
|
}
|
|
2198
1789
|
|
|
2199
1790
|
// src/commands/init.ts
|
|
2200
|
-
import { mkdirSync as
|
|
1791
|
+
import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync8 } from "fs";
|
|
2201
1792
|
import { join as join14 } from "path";
|
|
2202
1793
|
import * as p3 from "@clack/prompts";
|
|
2203
1794
|
import pc7 from "picocolors";
|
|
2204
1795
|
|
|
1796
|
+
// src/core/materializer/global-sync.ts
|
|
1797
|
+
import { homedir as homedir2 } from "os";
|
|
1798
|
+
async function syncGlobalAgentsAndSkills(config, provider, homeDir = homedir2()) {
|
|
1799
|
+
const status = await getGlobalDoctorStatus(provider, config, homeDir);
|
|
1800
|
+
const missingAgents = status.agents.filter((a) => a.status === "missing");
|
|
1801
|
+
const missingSkills = status.skills.filter((s) => s.status === "missing");
|
|
1802
|
+
if (missingAgents.length === 0 && missingSkills.length === 0) {
|
|
1803
|
+
return { alreadySynced: true, createdAgents: [], createdSkills: [] };
|
|
1804
|
+
}
|
|
1805
|
+
const projectName = config.project.name;
|
|
1806
|
+
const allowedPaths = (config.agents.explorer.allowedPaths ?? []).join(", ");
|
|
1807
|
+
const writablePaths = (config.agents.builder.writablePaths ?? []).join(", ");
|
|
1808
|
+
const models = {
|
|
1809
|
+
lead: config.agents.lead.model,
|
|
1810
|
+
explorer: config.agents.explorer.model,
|
|
1811
|
+
consultant: config.agents.consultant?.model,
|
|
1812
|
+
builder: config.agents.builder.model,
|
|
1813
|
+
reviewer: config.agents.reviewer.model
|
|
1814
|
+
};
|
|
1815
|
+
const createdAgents = [];
|
|
1816
|
+
if (missingAgents.length > 0) {
|
|
1817
|
+
const { agentsDir, ext } = getGlobalProviderAgentDir(provider, homeDir);
|
|
1818
|
+
for (const agent of missingAgents) {
|
|
1819
|
+
const name = agent.name;
|
|
1820
|
+
const content = generateExpectedAgentContent(name, provider, {
|
|
1821
|
+
projectName,
|
|
1822
|
+
allowedPaths,
|
|
1823
|
+
writablePaths,
|
|
1824
|
+
model: models[name]
|
|
1825
|
+
});
|
|
1826
|
+
writeAgentFile(agentsDir, `${name}${ext}`, content);
|
|
1827
|
+
createdAgents.push(name);
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
const createdSkills = [];
|
|
1831
|
+
if (missingSkills.length > 0) {
|
|
1832
|
+
const skillsDir = getGlobalProviderSkillsDir(provider, homeDir);
|
|
1833
|
+
for (const skill of missingSkills) {
|
|
1834
|
+
writeSkill(skillsDir, skill.name);
|
|
1835
|
+
createdSkills.push(skill.name);
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
return { alreadySynced: false, createdAgents, createdSkills };
|
|
1839
|
+
}
|
|
1840
|
+
|
|
2205
1841
|
// src/schema/init.ts
|
|
2206
1842
|
import * as v from "valibot";
|
|
2207
1843
|
var initNameSchema = v.pipe(
|
|
@@ -2253,14 +1889,15 @@ var cliFormWithRetry = async (formFn, schema) => {
|
|
|
2253
1889
|
};
|
|
2254
1890
|
|
|
2255
1891
|
// src/commands/init-helpers.ts
|
|
2256
|
-
import {
|
|
1892
|
+
import { randomUUID } from "crypto";
|
|
1893
|
+
import { existsSync as existsSync11, readFileSync as readFileSync8 } from "fs";
|
|
2257
1894
|
import { join as join13 } from "path";
|
|
2258
1895
|
import pc6 from "picocolors";
|
|
2259
1896
|
function readProjectNameFromPackageJson(cwd2) {
|
|
2260
1897
|
try {
|
|
2261
1898
|
const pkgPath2 = join13(cwd2, "package.json");
|
|
2262
|
-
if (!
|
|
2263
|
-
const content =
|
|
1899
|
+
if (!existsSync11(pkgPath2)) return null;
|
|
1900
|
+
const content = readFileSync8(pkgPath2, "utf8");
|
|
2264
1901
|
const pkg2 = JSON.parse(content);
|
|
2265
1902
|
const name = pkg2?.name;
|
|
2266
1903
|
if (typeof name === "string" && name.trim()) return name.trim();
|
|
@@ -2271,16 +1908,17 @@ function readProjectNameFromPackageJson(cwd2) {
|
|
|
2271
1908
|
}
|
|
2272
1909
|
function detectConfigExtension(cwd2) {
|
|
2273
1910
|
try {
|
|
2274
|
-
if (
|
|
1911
|
+
if (existsSync11(join13(cwd2, "tsconfig.json"))) return "ts";
|
|
2275
1912
|
const pkgPath2 = join13(cwd2, "package.json");
|
|
2276
|
-
if (!
|
|
2277
|
-
const pkg2 = JSON.parse(
|
|
1913
|
+
if (!existsSync11(pkgPath2)) return "mjs";
|
|
1914
|
+
const pkg2 = JSON.parse(readFileSync8(pkgPath2, "utf8"));
|
|
2278
1915
|
if (pkg2?.type === "module") return "mjs";
|
|
2279
1916
|
} catch {
|
|
2280
1917
|
}
|
|
2281
1918
|
return "mjs";
|
|
2282
1919
|
}
|
|
2283
1920
|
function applyConfigDefaults(params) {
|
|
1921
|
+
const models = params.models ?? {};
|
|
2284
1922
|
return {
|
|
2285
1923
|
provider: params.provider,
|
|
2286
1924
|
project: {
|
|
@@ -2290,10 +1928,19 @@ function applyConfigDefaults(params) {
|
|
|
2290
1928
|
agentsMd: "./AGENTS.md"
|
|
2291
1929
|
},
|
|
2292
1930
|
agents: {
|
|
2293
|
-
lead: { instructionsPath: null },
|
|
2294
|
-
explorer: {
|
|
2295
|
-
|
|
2296
|
-
|
|
1931
|
+
lead: { instructionsPath: null, ...models.lead && { model: models.lead } },
|
|
1932
|
+
explorer: {
|
|
1933
|
+
instructionsPath: null,
|
|
1934
|
+
allowedPaths: [params.docsPath, "./src"],
|
|
1935
|
+
...models.explorer && { model: models.explorer }
|
|
1936
|
+
},
|
|
1937
|
+
builder: {
|
|
1938
|
+
instructionsPath: null,
|
|
1939
|
+
writablePaths: ["./src", "./tests"],
|
|
1940
|
+
...models.builder && { model: models.builder }
|
|
1941
|
+
},
|
|
1942
|
+
reviewer: { instructionsPath: null, ...models.reviewer && { model: models.reviewer } },
|
|
1943
|
+
...models.consultant && { consultant: { instructionsPath: null, model: models.consultant } },
|
|
2297
1944
|
custom: []
|
|
2298
1945
|
},
|
|
2299
1946
|
database: { type: "sqlite", path: ".harness/harness.db" },
|
|
@@ -2307,7 +1954,9 @@ function applyConfigDefaults(params) {
|
|
|
2307
1954
|
blockers: true,
|
|
2308
1955
|
nextSteps: false
|
|
2309
1956
|
},
|
|
2310
|
-
markdownFallback: { enabled: true, path: ".harness/current.md" }
|
|
1957
|
+
markdownFallback: { enabled: true, path: ".harness/current.md" },
|
|
1958
|
+
scope: params.scope ?? "local",
|
|
1959
|
+
projectId: params.projectId ?? randomUUID()
|
|
2311
1960
|
},
|
|
2312
1961
|
health: {
|
|
2313
1962
|
scriptPath: "./health.sh",
|
|
@@ -2420,6 +2069,61 @@ async function runInit(cwd2, flags) {
|
|
|
2420
2069
|
}
|
|
2421
2070
|
provider = val;
|
|
2422
2071
|
}
|
|
2072
|
+
const AGENT_LABELS = [
|
|
2073
|
+
{ key: "lead", label: "Lead" },
|
|
2074
|
+
{ key: "explorer", label: "Explorer" },
|
|
2075
|
+
{ key: "consultant", label: "Consultant" },
|
|
2076
|
+
{ key: "builder", label: "Builder" },
|
|
2077
|
+
{ key: "reviewer", label: "Reviewer" }
|
|
2078
|
+
];
|
|
2079
|
+
const modelOverrides = {};
|
|
2080
|
+
if (provider === "claude-code" || provider === "codex-cli") {
|
|
2081
|
+
const wantsModelCustomization = await p3.confirm({
|
|
2082
|
+
message: "\xBFPersonalizar el modelo por agente?",
|
|
2083
|
+
initialValue: false
|
|
2084
|
+
});
|
|
2085
|
+
if (p3.isCancel(wantsModelCustomization)) {
|
|
2086
|
+
p3.cancel("Cancelled.");
|
|
2087
|
+
process.exit(0);
|
|
2088
|
+
}
|
|
2089
|
+
if (wantsModelCustomization) {
|
|
2090
|
+
if (provider === "claude-code") {
|
|
2091
|
+
for (const agent of AGENT_LABELS) {
|
|
2092
|
+
const val = await p3.select({
|
|
2093
|
+
message: `Modelo para ${agent.label}`,
|
|
2094
|
+
options: [
|
|
2095
|
+
{ value: "inherit", label: "inherit (default)" },
|
|
2096
|
+
{ value: "haiku", label: "haiku" },
|
|
2097
|
+
{ value: "sonnet", label: "sonnet" },
|
|
2098
|
+
{ value: "opus", label: "opus" },
|
|
2099
|
+
{ value: "fable", label: "fable" }
|
|
2100
|
+
],
|
|
2101
|
+
initialValue: "inherit"
|
|
2102
|
+
});
|
|
2103
|
+
if (p3.isCancel(val)) {
|
|
2104
|
+
p3.cancel("Cancelled.");
|
|
2105
|
+
process.exit(0);
|
|
2106
|
+
}
|
|
2107
|
+
modelOverrides[agent.key] = val;
|
|
2108
|
+
}
|
|
2109
|
+
} else {
|
|
2110
|
+
for (const agent of AGENT_LABELS) {
|
|
2111
|
+
const val = await p3.text({
|
|
2112
|
+
message: `Modelo para ${agent.label} (Codex no valida este valor)`,
|
|
2113
|
+
placeholder: "ej. gpt-5 (vac\xEDo o <3 caracteres = sin override)"
|
|
2114
|
+
});
|
|
2115
|
+
if (p3.isCancel(val)) {
|
|
2116
|
+
p3.cancel("Cancelled.");
|
|
2117
|
+
process.exit(0);
|
|
2118
|
+
}
|
|
2119
|
+
const trimmed = val.trim();
|
|
2120
|
+
if (trimmed.length >= 3) {
|
|
2121
|
+
modelOverrides[agent.key] = trimmed;
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2423
2127
|
let docsPath;
|
|
2424
2128
|
if (flags.docs) {
|
|
2425
2129
|
docsPath = flags.docs;
|
|
@@ -2436,6 +2140,24 @@ async function runInit(cwd2, flags) {
|
|
|
2436
2140
|
return val;
|
|
2437
2141
|
}, initDocsSchema);
|
|
2438
2142
|
}
|
|
2143
|
+
let storageScope;
|
|
2144
|
+
if (flags.storageScope && ["local", "global"].includes(flags.storageScope)) {
|
|
2145
|
+
storageScope = flags.storageScope;
|
|
2146
|
+
} else {
|
|
2147
|
+
const val = await p3.select({
|
|
2148
|
+
message: "Storage scope",
|
|
2149
|
+
options: [
|
|
2150
|
+
{ value: "local", label: "Local \u2014 .harness/harness.db lives in this project" },
|
|
2151
|
+
{ value: "global", label: "Global \u2014 DB lives under ~/.harness/dbs/<projectId>/, outside the project" }
|
|
2152
|
+
],
|
|
2153
|
+
initialValue: "local"
|
|
2154
|
+
});
|
|
2155
|
+
if (p3.isCancel(val)) {
|
|
2156
|
+
p3.cancel("Cancelled.");
|
|
2157
|
+
process.exit(0);
|
|
2158
|
+
}
|
|
2159
|
+
storageScope = val;
|
|
2160
|
+
}
|
|
2439
2161
|
let tasksAdapter;
|
|
2440
2162
|
if (flags.tasks && ["local", "jira", "linear"].includes(flags.tasks)) {
|
|
2441
2163
|
tasksAdapter = flags.tasks;
|
|
@@ -2490,10 +2212,19 @@ async function runInit(cwd2, flags) {
|
|
|
2490
2212
|
firstTask = { title: taskTitle, description: taskDesc, acceptance };
|
|
2491
2213
|
}
|
|
2492
2214
|
let configExt = "ts";
|
|
2215
|
+
let globalSyncResult = null;
|
|
2493
2216
|
const spinner6 = p3.spinner();
|
|
2494
2217
|
spinner6.start("Scaffolding...");
|
|
2495
2218
|
try {
|
|
2496
|
-
const config = applyConfigDefaults({
|
|
2219
|
+
const config = applyConfigDefaults({
|
|
2220
|
+
name,
|
|
2221
|
+
description,
|
|
2222
|
+
provider,
|
|
2223
|
+
docsPath,
|
|
2224
|
+
tasksAdapter,
|
|
2225
|
+
models: modelOverrides,
|
|
2226
|
+
scope: storageScope
|
|
2227
|
+
});
|
|
2497
2228
|
const materializer = getMaterializer(provider);
|
|
2498
2229
|
const installDir = cwd2;
|
|
2499
2230
|
configExt = detectConfigExtension(cwd2);
|
|
@@ -2505,12 +2236,19 @@ async function runInit(cwd2, flags) {
|
|
|
2505
2236
|
provider,
|
|
2506
2237
|
docsPath,
|
|
2507
2238
|
tasksAdapter,
|
|
2508
|
-
port: config.tools.mcp.port
|
|
2239
|
+
port: config.tools.mcp.port,
|
|
2240
|
+
models: modelOverrides,
|
|
2241
|
+
scope: config.storage.scope,
|
|
2242
|
+
projectId: config.storage.projectId
|
|
2509
2243
|
});
|
|
2510
|
-
|
|
2511
|
-
|
|
2244
|
+
writeFileSync8(join14(installDir, configFileName), configContent, "utf8");
|
|
2245
|
+
mkdirSync7(join14(installDir, config.storage.dir), { recursive: true });
|
|
2512
2246
|
const db = await openDB(config, installDir);
|
|
2247
|
+
await db.writeStorageState(installDir);
|
|
2513
2248
|
await materializer.scaffold(config, { cwd: installDir, firstTask });
|
|
2249
|
+
if (config.storage.scope === "global") {
|
|
2250
|
+
globalSyncResult = await syncGlobalAgentsAndSkills(config, provider);
|
|
2251
|
+
}
|
|
2514
2252
|
if (firstTask) {
|
|
2515
2253
|
const slug = slugify(firstTask.title);
|
|
2516
2254
|
await db.addTask({
|
|
@@ -2534,14 +2272,28 @@ async function runInit(cwd2, flags) {
|
|
|
2534
2272
|
console.log(pc7.green(`\u2713 agent-harness-kit.config.${configExt}`));
|
|
2535
2273
|
console.log(pc7.green("\u2713 AGENTS.md"));
|
|
2536
2274
|
console.log(pc7.green("\u2713 health.sh"));
|
|
2537
|
-
console.log(pc7.green("\u2713 .harness/harness.db"));
|
|
2538
|
-
console.log(pc7.green("\u2713 .harness/current.md"));
|
|
2275
|
+
console.log(pc7.green(storageScope === "global" ? "\u2713 ~/.harness/dbs/<projectId>/harness.db" : "\u2713 .harness/harness.db"));
|
|
2276
|
+
console.log(pc7.green(storageScope === "global" ? "\u2713 ~/.harness/dbs/<projectId>/current.md" : "\u2713 .harness/current.md"));
|
|
2277
|
+
console.log(pc7.green("\u2713 .harness/storage-state.json"));
|
|
2539
2278
|
console.log(pc7.green(`\u2713 ${agentsDir}lead.md`));
|
|
2540
2279
|
console.log(pc7.green(`\u2713 ${agentsDir}explorer.md`));
|
|
2541
2280
|
console.log(pc7.green(`\u2713 ${agentsDir}builder.md`));
|
|
2542
2281
|
console.log(pc7.green(`\u2713 ${agentsDir}reviewer.md`));
|
|
2543
2282
|
console.log(pc7.green(`\u2713 ${mcpFile}`));
|
|
2544
2283
|
console.log(pc7.green("\u2713 .gitignore entries added"));
|
|
2284
|
+
if (globalSyncResult) {
|
|
2285
|
+
console.log("");
|
|
2286
|
+
if (globalSyncResult.alreadySynced) {
|
|
2287
|
+
console.log(pc7.dim("\u2713 Global agents/skills already synced \u2014 skipped"));
|
|
2288
|
+
} else {
|
|
2289
|
+
for (const name2 of globalSyncResult.createdAgents) {
|
|
2290
|
+
console.log(pc7.green(`\u2713 ~global agent added: ${name2}`));
|
|
2291
|
+
}
|
|
2292
|
+
for (const name2 of globalSyncResult.createdSkills) {
|
|
2293
|
+
console.log(pc7.green(`\u2713 ~global skill added: ${name2}`));
|
|
2294
|
+
}
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2545
2297
|
console.log("");
|
|
2546
2298
|
console.log(pc7.cyan("\u2192") + ` Edit ${pc7.cyan("health.sh")} with your project checks`);
|
|
2547
2299
|
console.log(pc7.cyan("\u2192") + ` ${pc7.cyan("ahk task add")} to queue work for agents`);
|
|
@@ -2598,17 +2350,251 @@ async function runMigrate(cwd2, opts) {
|
|
|
2598
2350
|
}
|
|
2599
2351
|
}
|
|
2600
2352
|
|
|
2353
|
+
// src/commands/migrate-storage.ts
|
|
2354
|
+
import { copyFileSync, existsSync as existsSync12, mkdirSync as mkdirSync8, rmSync, writeFileSync as writeFileSync9 } from "fs";
|
|
2355
|
+
import { homedir as homedir3 } from "os";
|
|
2356
|
+
import { dirname as dirname7, join as join15, resolve as resolve8 } from "path";
|
|
2357
|
+
import pc9 from "picocolors";
|
|
2358
|
+
function log5(msg) {
|
|
2359
|
+
console.log(msg);
|
|
2360
|
+
}
|
|
2361
|
+
function fail(msg) {
|
|
2362
|
+
throw new Error(msg);
|
|
2363
|
+
}
|
|
2364
|
+
function currentMdPathForScope(scope, config, cwd2, homeDir) {
|
|
2365
|
+
return scope === "global" ? join15(resolveGlobalStorageDir(config, homeDir), "current.md") : resolve8(cwd2, config.storage.markdownFallback.path);
|
|
2366
|
+
}
|
|
2367
|
+
async function backupDestination(cwd2, storageDir, data) {
|
|
2368
|
+
const backupsDir = resolve8(cwd2, storageDir, "backups");
|
|
2369
|
+
const path = join15(backupsDir, `pre-migrate-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.json`);
|
|
2370
|
+
try {
|
|
2371
|
+
mkdirSync8(backupsDir, { recursive: true });
|
|
2372
|
+
writeFileSync9(path, JSON.stringify(data, null, 2) + "\n", "utf8");
|
|
2373
|
+
} catch (err) {
|
|
2374
|
+
throw new Error(
|
|
2375
|
+
`Could not write destination backup to ${path} (${err instanceof Error ? err.message : String(err)}). Aborting migration WITHOUT touching the destination \u2014 nothing was overwritten.`
|
|
2376
|
+
);
|
|
2377
|
+
}
|
|
2378
|
+
return path;
|
|
2379
|
+
}
|
|
2380
|
+
function copySqliteFile(srcPath, destPath) {
|
|
2381
|
+
mkdirSync8(dirname7(destPath), { recursive: true });
|
|
2382
|
+
copyFileSync(srcPath, destPath);
|
|
2383
|
+
for (const suffix of ["-wal", "-shm"]) {
|
|
2384
|
+
if (existsSync12(`${srcPath}${suffix}`)) {
|
|
2385
|
+
copyFileSync(`${srcPath}${suffix}`, `${destPath}${suffix}`);
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2388
|
+
if (!existsSync12(destPath)) {
|
|
2389
|
+
throw new Error(`Copy verification failed: ${destPath} does not exist after copy.`);
|
|
2390
|
+
}
|
|
2391
|
+
}
|
|
2392
|
+
async function runMigrateStorage(cwd2, opts, homeDir = homedir3()) {
|
|
2393
|
+
const config = await loadConfig(cwd2);
|
|
2394
|
+
const storageDir = config.storage.dir;
|
|
2395
|
+
const state = readStorageStateFile(cwd2, storageDir);
|
|
2396
|
+
let realScope;
|
|
2397
|
+
let realDbType;
|
|
2398
|
+
if (!state) {
|
|
2399
|
+
const defaultSqlitePath = config.database.type === "sqlite" ? config.database.path : ".harness/harness.db";
|
|
2400
|
+
const localPath = resolveSqlitePathForScope("local", defaultSqlitePath, cwd2, config, homeDir);
|
|
2401
|
+
const globalPath = resolveSqlitePathForScope("global", defaultSqlitePath, cwd2, config, homeDir);
|
|
2402
|
+
const localCount = await probeTaskCount(localPath);
|
|
2403
|
+
const globalCount = await probeTaskCount(globalPath);
|
|
2404
|
+
if (localCount > 0 && globalCount > 0) {
|
|
2405
|
+
fail(
|
|
2406
|
+
`storage-state.json is missing and BOTH candidate locations have data \u2014 local (${localPath}): ${localCount} task(s); global (${globalPath}): ${globalCount} task(s). Refusing to guess which one is authoritative. Resolve manually (inspect both databases) or delete the one that should be discarded, then re-run this command.`
|
|
2407
|
+
);
|
|
2408
|
+
}
|
|
2409
|
+
if (localCount === 0 && globalCount === 0) {
|
|
2410
|
+
const db = await openDB(config, cwd2, homeDir);
|
|
2411
|
+
try {
|
|
2412
|
+
await db.writeStorageState(cwd2);
|
|
2413
|
+
} finally {
|
|
2414
|
+
await db.close();
|
|
2415
|
+
}
|
|
2416
|
+
log5(pc9.dim("storage-state.json was missing; no data found at either candidate location. Nothing to migrate \u2014 state recorded."));
|
|
2417
|
+
return;
|
|
2418
|
+
}
|
|
2419
|
+
realScope = localCount > 0 ? "local" : "global";
|
|
2420
|
+
realDbType = "sqlite";
|
|
2421
|
+
log5(
|
|
2422
|
+
pc9.yellow(
|
|
2423
|
+
`storage-state.json was missing. Detected real data at ${realScope} sqlite location (${realScope === "local" ? localPath : globalPath}) \u2014 using it as the migration source.`
|
|
2424
|
+
)
|
|
2425
|
+
);
|
|
2426
|
+
} else {
|
|
2427
|
+
realScope = state.scope;
|
|
2428
|
+
realDbType = state.dbType;
|
|
2429
|
+
}
|
|
2430
|
+
const desiredScope = config.storage.scope;
|
|
2431
|
+
const desiredDbType = config.database.type;
|
|
2432
|
+
if (realScope === desiredScope && realDbType === desiredDbType) {
|
|
2433
|
+
log5(pc9.green(`\u2713 Storage already matches config (scope=${desiredScope}, database=${desiredDbType}) \u2014 nothing to migrate.`));
|
|
2434
|
+
return;
|
|
2435
|
+
}
|
|
2436
|
+
if (realDbType !== "sqlite") {
|
|
2437
|
+
fail(
|
|
2438
|
+
`Cannot auto-locate the previous ${realDbType} database \u2014 storage-state.json does not retain connection credentials for security. Manually run "ahk export --json" while still connected to the old database (with the old config), then adjust agent-harness-kit.config.ts and re-import. This direction is out of scope for "ahk migrate storage".`
|
|
2439
|
+
);
|
|
2440
|
+
}
|
|
2441
|
+
if (desiredDbType === "sqlite" && realDbType === "sqlite") {
|
|
2442
|
+
return migrateScopeOnly(cwd2, config, homeDir, realScope, desiredScope, opts);
|
|
2443
|
+
}
|
|
2444
|
+
return migrateAcrossDbType(cwd2, config, homeDir, realScope, opts);
|
|
2445
|
+
}
|
|
2446
|
+
async function probeTaskCount(dbPath) {
|
|
2447
|
+
if (!existsSync12(dbPath)) return 0;
|
|
2448
|
+
const { SQLiteDriver } = await import("./sqlite-TR4D324R.js");
|
|
2449
|
+
const driver = new SQLiteDriver(dbPath);
|
|
2450
|
+
try {
|
|
2451
|
+
await driver.ensureSchema();
|
|
2452
|
+
const counts = await getRowCounts(driver);
|
|
2453
|
+
return counts.tasks;
|
|
2454
|
+
} finally {
|
|
2455
|
+
await driver.close();
|
|
2456
|
+
}
|
|
2457
|
+
}
|
|
2458
|
+
async function migrateScopeOnly(cwd2, config, homeDir, fromScope, toScope, opts) {
|
|
2459
|
+
const sqlitePath = config.database.type === "sqlite" ? config.database.path : ".harness/harness.db";
|
|
2460
|
+
const srcDb = resolveSqlitePathForScope(fromScope, sqlitePath, cwd2, config, homeDir);
|
|
2461
|
+
const destDb = resolveSqlitePathForScope(toScope, sqlitePath, cwd2, config, homeDir);
|
|
2462
|
+
const srcMd = currentMdPathForScope(fromScope, config, cwd2, homeDir);
|
|
2463
|
+
const destMd = currentMdPathForScope(toScope, config, cwd2, homeDir);
|
|
2464
|
+
if (!existsSync12(srcDb)) {
|
|
2465
|
+
fail(`Source database not found at ${srcDb} (expected ${fromScope} scope) \u2014 nothing to move.`);
|
|
2466
|
+
}
|
|
2467
|
+
const destExists = existsSync12(destDb);
|
|
2468
|
+
if (destExists) {
|
|
2469
|
+
const { SQLiteDriver } = await import("./sqlite-TR4D324R.js");
|
|
2470
|
+
const destDriver = new SQLiteDriver(destDb);
|
|
2471
|
+
let destEmpty;
|
|
2472
|
+
try {
|
|
2473
|
+
await destDriver.ensureSchema();
|
|
2474
|
+
destEmpty = await isEmptyDatabase(destDriver);
|
|
2475
|
+
} finally {
|
|
2476
|
+
await destDriver.close();
|
|
2477
|
+
}
|
|
2478
|
+
if (!destEmpty && !opts.force) {
|
|
2479
|
+
fail(
|
|
2480
|
+
`Destination (${toScope}, ${destDb}) already has data. Re-run with --force to overwrite it (a backup of the destination will be written first).`
|
|
2481
|
+
);
|
|
2482
|
+
}
|
|
2483
|
+
if (!destEmpty && opts.force) {
|
|
2484
|
+
const { SQLiteDriver: Driver } = await import("./sqlite-TR4D324R.js");
|
|
2485
|
+
const backupDriver = new Driver(destDb);
|
|
2486
|
+
let data;
|
|
2487
|
+
try {
|
|
2488
|
+
await backupDriver.ensureSchema();
|
|
2489
|
+
const { HarnessDB } = await import("./db-QQ7BR5K7.js");
|
|
2490
|
+
const tmpDb = new HarnessDB(backupDriver, { ...config, storage: { ...config.storage, scope: toScope } }, homeDir);
|
|
2491
|
+
data = await tmpDb.exportJson();
|
|
2492
|
+
} finally {
|
|
2493
|
+
await backupDriver.close();
|
|
2494
|
+
}
|
|
2495
|
+
const backupPath = await backupDestination(cwd2, config.storage.dir, data);
|
|
2496
|
+
log5(pc9.yellow(` Backed up existing destination data \u2192 ${backupPath}`));
|
|
2497
|
+
}
|
|
2498
|
+
}
|
|
2499
|
+
if (opts.dryRun) {
|
|
2500
|
+
log5(pc9.dim(`[dry-run] Would copy ${srcDb} \u2192 ${destDb} (scope ${fromScope} \u2192 ${toScope}), and move current.md.`));
|
|
2501
|
+
return;
|
|
2502
|
+
}
|
|
2503
|
+
copySqliteFile(srcDb, destDb);
|
|
2504
|
+
log5(pc9.green(`\u2713 Copied database ${srcDb} \u2192 ${destDb}`));
|
|
2505
|
+
if (existsSync12(srcMd)) {
|
|
2506
|
+
mkdirSync8(dirname7(destMd), { recursive: true });
|
|
2507
|
+
copyFileSync(srcMd, destMd);
|
|
2508
|
+
log5(pc9.green(`\u2713 Copied current.md ${srcMd} \u2192 ${destMd}`));
|
|
2509
|
+
}
|
|
2510
|
+
rmSync(srcDb, { force: true });
|
|
2511
|
+
rmSync(`${srcDb}-wal`, { force: true });
|
|
2512
|
+
rmSync(`${srcDb}-shm`, { force: true });
|
|
2513
|
+
if (existsSync12(srcMd) && srcMd !== destMd) rmSync(srcMd, { force: true });
|
|
2514
|
+
const db = await openDB(config, cwd2, homeDir);
|
|
2515
|
+
try {
|
|
2516
|
+
await db.writeStorageState(cwd2);
|
|
2517
|
+
} finally {
|
|
2518
|
+
await db.close();
|
|
2519
|
+
}
|
|
2520
|
+
log5(pc9.green(`\u2713 Storage migrated: scope ${fromScope} \u2192 ${toScope}`));
|
|
2521
|
+
}
|
|
2522
|
+
async function migrateAcrossDbType(cwd2, config, homeDir, sourceScope, opts) {
|
|
2523
|
+
const sqlitePath = config.database.type === "sqlite" ? config.database.path : ".harness/harness.db";
|
|
2524
|
+
const srcPath = resolveSqlitePathForScope(sourceScope, sqlitePath, cwd2, config, homeDir);
|
|
2525
|
+
if (!existsSync12(srcPath)) {
|
|
2526
|
+
fail(`Source sqlite database not found at ${srcPath} (expected ${sourceScope} scope) \u2014 nothing to migrate.`);
|
|
2527
|
+
}
|
|
2528
|
+
const { SQLiteDriver } = await import("./sqlite-TR4D324R.js");
|
|
2529
|
+
const srcDriver = new SQLiteDriver(srcPath);
|
|
2530
|
+
let sourceData;
|
|
2531
|
+
let sourceCounts;
|
|
2532
|
+
try {
|
|
2533
|
+
await srcDriver.ensureSchema();
|
|
2534
|
+
sourceCounts = await getRowCounts(srcDriver);
|
|
2535
|
+
const { HarnessDB } = await import("./db-QQ7BR5K7.js");
|
|
2536
|
+
const srcDb = new HarnessDB(srcDriver, { ...config, storage: { ...config.storage, scope: sourceScope }, database: { type: "sqlite", path: sqlitePath } }, homeDir);
|
|
2537
|
+
sourceData = await srcDb.exportJson();
|
|
2538
|
+
} finally {
|
|
2539
|
+
await srcDriver.close();
|
|
2540
|
+
}
|
|
2541
|
+
let destDb;
|
|
2542
|
+
try {
|
|
2543
|
+
destDb = await openDB(config, cwd2, homeDir);
|
|
2544
|
+
} catch (err) {
|
|
2545
|
+
fail(`Could not connect to destination (${config.database.type}): ${err instanceof Error ? err.message : String(err)}. Verify database configuration.`);
|
|
2546
|
+
}
|
|
2547
|
+
try {
|
|
2548
|
+
const destCounts = await destDb.getRowCounts();
|
|
2549
|
+
const destEmpty = Object.values(destCounts).every((n) => n === 0);
|
|
2550
|
+
const sourceEmpty = Object.values(sourceCounts).every((n) => n === 0);
|
|
2551
|
+
if (!destEmpty) {
|
|
2552
|
+
if (!opts.force) {
|
|
2553
|
+
const bothHaveData = !sourceEmpty;
|
|
2554
|
+
fail(
|
|
2555
|
+
bothHaveData ? `Both source (${sourceCounts.tasks} task(s)) and destination (${config.database.type}, ${destCounts.tasks} task(s)) have data that DIVERGE \u2014 this is not a first-time migration. Refusing to auto-merge. Review both manually, or re-run with --force to overwrite the destination (a JSON backup will be written first).` : `Destination (${config.database.type}) already has data (${destCounts.tasks} task(s)). Re-run with --force to overwrite it (a backup of the destination will be written first).`
|
|
2556
|
+
);
|
|
2557
|
+
}
|
|
2558
|
+
}
|
|
2559
|
+
if (opts.dryRun) {
|
|
2560
|
+
log5(
|
|
2561
|
+
pc9.dim(
|
|
2562
|
+
`[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)"}.`
|
|
2563
|
+
)
|
|
2564
|
+
);
|
|
2565
|
+
return;
|
|
2566
|
+
}
|
|
2567
|
+
let backupPath = null;
|
|
2568
|
+
if (!destEmpty && opts.force) {
|
|
2569
|
+
const currentDestData = await destDb.exportJson();
|
|
2570
|
+
backupPath = await backupDestination(cwd2, config.storage.dir, currentDestData);
|
|
2571
|
+
log5(pc9.yellow(` Backed up existing destination data \u2192 ${backupPath}`));
|
|
2572
|
+
}
|
|
2573
|
+
await destDb.importFullExport(sourceData, config.database.type, { truncateFirst: !destEmpty });
|
|
2574
|
+
await destDb.writeStorageState(cwd2);
|
|
2575
|
+
log5(
|
|
2576
|
+
pc9.green(
|
|
2577
|
+
`\u2713 Migrated ${sourceData.tasks.length} task(s), ${sourceData.actions.length} action(s) from sqlite (${sourceScope}) \u2192 ${config.database.type}.`
|
|
2578
|
+
)
|
|
2579
|
+
);
|
|
2580
|
+
if (backupPath) log5(pc9.dim(` Destination backup: ${backupPath}`));
|
|
2581
|
+
log5(pc9.yellow(` Note: the original sqlite file at ${srcPath} was NOT deleted \u2014 remove it manually once you've verified the migration.`));
|
|
2582
|
+
} finally {
|
|
2583
|
+
await destDb.close();
|
|
2584
|
+
}
|
|
2585
|
+
}
|
|
2586
|
+
|
|
2601
2587
|
// src/commands/reset.ts
|
|
2602
|
-
import { existsSync as
|
|
2603
|
-
import { join as
|
|
2588
|
+
import { existsSync as existsSync13, readdirSync, rmSync as rmSync2 } from "fs";
|
|
2589
|
+
import { join as join16, resolve as resolve9 } from "path";
|
|
2604
2590
|
import * as p5 from "@clack/prompts";
|
|
2605
|
-
import
|
|
2591
|
+
import pc10 from "picocolors";
|
|
2606
2592
|
var AGENT_MD_FILES = ["lead", "explorer", "consultant", "builder", "reviewer"];
|
|
2607
2593
|
async function resetAgentMds(cwd2, provider) {
|
|
2608
2594
|
const agentDir = provider === "claude-code" ? ".claude/agents" : ".opencode/agents";
|
|
2609
2595
|
const agentDirPath = resolve9(cwd2, agentDir);
|
|
2610
|
-
if (!
|
|
2611
|
-
console.log(
|
|
2596
|
+
if (!existsSync13(agentDirPath)) {
|
|
2597
|
+
console.log(pc10.yellow(` Skipping agent files \u2014 directory not found: ${agentDirPath}`));
|
|
2612
2598
|
return;
|
|
2613
2599
|
}
|
|
2614
2600
|
const existingFiles = [];
|
|
@@ -2620,11 +2606,11 @@ async function resetAgentMds(cwd2, provider) {
|
|
|
2620
2606
|
}
|
|
2621
2607
|
}
|
|
2622
2608
|
} catch {
|
|
2623
|
-
console.log(
|
|
2609
|
+
console.log(pc10.yellow(` Skipping agent files \u2014 ${agentDirPath} is not readable`));
|
|
2624
2610
|
return;
|
|
2625
2611
|
}
|
|
2626
2612
|
if (existingFiles.length === 0) {
|
|
2627
|
-
console.log(
|
|
2613
|
+
console.log(pc10.yellow(` No agent MD files found in ${agentDir}/`));
|
|
2628
2614
|
return;
|
|
2629
2615
|
}
|
|
2630
2616
|
for (const file of existingFiles) {
|
|
@@ -2633,19 +2619,19 @@ async function resetAgentMds(cwd2, provider) {
|
|
|
2633
2619
|
initialValue: true
|
|
2634
2620
|
});
|
|
2635
2621
|
if (p5.isCancel(confirm3)) {
|
|
2636
|
-
console.log(
|
|
2622
|
+
console.log(pc10.red(" Cancelled by user."));
|
|
2637
2623
|
return;
|
|
2638
2624
|
}
|
|
2639
2625
|
if (confirm3) {
|
|
2640
2626
|
try {
|
|
2641
|
-
const filePath =
|
|
2642
|
-
|
|
2643
|
-
console.log(
|
|
2627
|
+
const filePath = join16(agentDirPath, file);
|
|
2628
|
+
rmSync2(filePath, { force: true });
|
|
2629
|
+
console.log(pc10.green(` Removed ${file}`));
|
|
2644
2630
|
} catch {
|
|
2645
|
-
console.error(
|
|
2631
|
+
console.error(pc10.red(` Failed to remove ${file}`));
|
|
2646
2632
|
}
|
|
2647
2633
|
} else {
|
|
2648
|
-
console.log(
|
|
2634
|
+
console.log(pc10.cyan(` Skipped ${file}`));
|
|
2649
2635
|
}
|
|
2650
2636
|
}
|
|
2651
2637
|
}
|
|
@@ -2654,7 +2640,7 @@ async function runReset(cwd2, opts) {
|
|
|
2654
2640
|
try {
|
|
2655
2641
|
config = await loadConfig(cwd2);
|
|
2656
2642
|
} catch {
|
|
2657
|
-
console.error(
|
|
2643
|
+
console.error(pc10.red("\u2717 No agent-harness-kit.config found. Run: ahk init"));
|
|
2658
2644
|
process.exit(1);
|
|
2659
2645
|
}
|
|
2660
2646
|
const storageDir = config.storage.dir || ".harness";
|
|
@@ -2663,12 +2649,12 @@ async function runReset(cwd2, opts) {
|
|
|
2663
2649
|
let resetDb = false;
|
|
2664
2650
|
let resetFeatureList = false;
|
|
2665
2651
|
let resetAgentMdsFlag = false;
|
|
2666
|
-
if (dbPath &&
|
|
2652
|
+
if (dbPath && existsSync13(dbPath)) {
|
|
2667
2653
|
if (opts.force) {
|
|
2668
2654
|
resetDb = true;
|
|
2669
2655
|
} else {
|
|
2670
2656
|
if (config.database.type !== "sqlite") {
|
|
2671
|
-
console.log(
|
|
2657
|
+
console.log(pc10.yellow(` Skipping DB reset \u2014 database type "${config.database.type}" is not managed by this command.`));
|
|
2672
2658
|
resetDb = false;
|
|
2673
2659
|
} else {
|
|
2674
2660
|
const confirm3 = await p5.confirm({
|
|
@@ -2676,16 +2662,16 @@ async function runReset(cwd2, opts) {
|
|
|
2676
2662
|
initialValue: true
|
|
2677
2663
|
});
|
|
2678
2664
|
if (p5.isCancel(confirm3)) {
|
|
2679
|
-
console.log(
|
|
2665
|
+
console.log(pc10.red(" Cancelled by user."));
|
|
2680
2666
|
return;
|
|
2681
2667
|
}
|
|
2682
2668
|
resetDb = confirm3;
|
|
2683
2669
|
}
|
|
2684
2670
|
}
|
|
2685
2671
|
} else if (!dbPath) {
|
|
2686
|
-
console.log(
|
|
2672
|
+
console.log(pc10.dim(` Skipping DB reset \u2014 remote ${config.database.type} database is not managed by this command.`));
|
|
2687
2673
|
}
|
|
2688
|
-
if (
|
|
2674
|
+
if (existsSync13(featureListPath)) {
|
|
2689
2675
|
if (opts.force) {
|
|
2690
2676
|
resetFeatureList = true;
|
|
2691
2677
|
} else {
|
|
@@ -2694,7 +2680,7 @@ async function runReset(cwd2, opts) {
|
|
|
2694
2680
|
initialValue: true
|
|
2695
2681
|
});
|
|
2696
2682
|
if (p5.isCancel(confirm3)) {
|
|
2697
|
-
console.log(
|
|
2683
|
+
console.log(pc10.red(" Cancelled by user."));
|
|
2698
2684
|
return;
|
|
2699
2685
|
}
|
|
2700
2686
|
resetFeatureList = confirm3;
|
|
@@ -2705,20 +2691,20 @@ async function runReset(cwd2, opts) {
|
|
|
2705
2691
|
}
|
|
2706
2692
|
if (resetDb && dbPath) {
|
|
2707
2693
|
try {
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
console.log(
|
|
2694
|
+
rmSync2(dbPath, { force: true });
|
|
2695
|
+
rmSync2(`${dbPath}-wal`, { force: true });
|
|
2696
|
+
rmSync2(`${dbPath}-shm`, { force: true });
|
|
2697
|
+
console.log(pc10.green(` \u2713 Removed ${dbPath}`));
|
|
2712
2698
|
} catch {
|
|
2713
|
-
console.error(
|
|
2699
|
+
console.error(pc10.red(` \u2717 Failed to remove ${dbPath}`));
|
|
2714
2700
|
}
|
|
2715
2701
|
}
|
|
2716
2702
|
if (resetFeatureList) {
|
|
2717
2703
|
try {
|
|
2718
|
-
|
|
2719
|
-
console.log(
|
|
2704
|
+
rmSync2(featureListPath, { force: true });
|
|
2705
|
+
console.log(pc10.green(` \u2713 Removed ${storageDir}/feature_list.json`));
|
|
2720
2706
|
} catch {
|
|
2721
|
-
console.error(
|
|
2707
|
+
console.error(pc10.red(` \u2717 Failed to remove ${featureListPath}`));
|
|
2722
2708
|
}
|
|
2723
2709
|
}
|
|
2724
2710
|
if (resetAgentMdsFlag) {
|
|
@@ -2726,16 +2712,16 @@ async function runReset(cwd2, opts) {
|
|
|
2726
2712
|
await resetAgentMds(cwd2, opts.provider || "claude-code");
|
|
2727
2713
|
}
|
|
2728
2714
|
if (!resetDb && !resetFeatureList && !resetAgentMdsFlag) {
|
|
2729
|
-
console.log(
|
|
2715
|
+
console.log(pc10.yellow(" Nothing to reset (all items missing or skipped)."));
|
|
2730
2716
|
return;
|
|
2731
2717
|
}
|
|
2732
2718
|
console.log("");
|
|
2733
|
-
console.log(
|
|
2719
|
+
console.log(pc10.green('\u2713 Reset complete. Run "ahk init" to scaffold a fresh harness.'));
|
|
2734
2720
|
}
|
|
2735
2721
|
|
|
2736
2722
|
// src/core/mcp-server.ts
|
|
2737
|
-
import { existsSync as
|
|
2738
|
-
import { join as
|
|
2723
|
+
import { existsSync as existsSync15, mkdirSync as mkdirSync9, readdirSync as readdirSync2, readFileSync as readFileSync10, statSync, writeFileSync as writeFileSync10 } from "fs";
|
|
2724
|
+
import { join as join18, resolve as resolve10 } from "path";
|
|
2739
2725
|
import { Server } from "@modelcontextprotocol/sdk/server";
|
|
2740
2726
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
2741
2727
|
import {
|
|
@@ -2744,8 +2730,8 @@ import {
|
|
|
2744
2730
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
2745
2731
|
|
|
2746
2732
|
// src/core/permissions-check.ts
|
|
2747
|
-
import { existsSync as
|
|
2748
|
-
import { join as
|
|
2733
|
+
import { existsSync as existsSync14, readFileSync as readFileSync9 } from "fs";
|
|
2734
|
+
import { join as join17 } from "path";
|
|
2749
2735
|
var CANONICAL = {
|
|
2750
2736
|
lead: [...MCP_CLAUDE_PERMISSIONS_LEAD],
|
|
2751
2737
|
explorer: [...MCP_CLAUDE_PERMISSIONS_EXPLORER],
|
|
@@ -2768,14 +2754,14 @@ function checkPermissionsSync(cwd2, config) {
|
|
|
2768
2754
|
const agents = {};
|
|
2769
2755
|
let in_sync = true;
|
|
2770
2756
|
for (const agent of ["lead", "explorer", "consultant", "builder", "reviewer"]) {
|
|
2771
|
-
const filePath =
|
|
2772
|
-
if (!
|
|
2757
|
+
const filePath = join17(cwd2, ".claude", "agents", `${agent}.md`);
|
|
2758
|
+
if (!existsSync14(filePath)) {
|
|
2773
2759
|
const missing2 = CANONICAL[agent];
|
|
2774
2760
|
agents[agent] = { ok: false, missing: missing2, extra: [] };
|
|
2775
2761
|
in_sync = false;
|
|
2776
2762
|
continue;
|
|
2777
2763
|
}
|
|
2778
|
-
const content =
|
|
2764
|
+
const content = readFileSync9(filePath, "utf-8");
|
|
2779
2765
|
const installed = parseToolsFromFrontmatter(content);
|
|
2780
2766
|
const canonical = CANONICAL[agent];
|
|
2781
2767
|
const missing = canonical.filter((t) => !installed.includes(t));
|
|
@@ -3195,19 +3181,19 @@ async function dispatch(name, args, db, docsPath, cwd2, config) {
|
|
|
3195
3181
|
return ok2(JSON.stringify(result, null, 2));
|
|
3196
3182
|
}
|
|
3197
3183
|
case "deps.snapshot": {
|
|
3198
|
-
const pkgPath2 =
|
|
3199
|
-
if (!
|
|
3184
|
+
const pkgPath2 = join18(cwd2, "package.json");
|
|
3185
|
+
if (!existsSync15(pkgPath2)) {
|
|
3200
3186
|
return ok2("package.json not found in project root", true);
|
|
3201
3187
|
}
|
|
3202
|
-
const pkg2 = JSON.parse(
|
|
3188
|
+
const pkg2 = JSON.parse(readFileSync10(pkgPath2, "utf8"));
|
|
3203
3189
|
const snapshot = {
|
|
3204
3190
|
capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3205
3191
|
dependencies: pkg2.dependencies ?? {},
|
|
3206
3192
|
devDependencies: pkg2.devDependencies ?? {}
|
|
3207
3193
|
};
|
|
3208
|
-
const harnessDir =
|
|
3194
|
+
const harnessDir = join18(cwd2, ".harness");
|
|
3209
3195
|
mkdirSync9(harnessDir, { recursive: true });
|
|
3210
|
-
writeFileSync10(
|
|
3196
|
+
writeFileSync10(join18(harnessDir, "deps-lock.json"), JSON.stringify(snapshot, null, 2), "utf8");
|
|
3211
3197
|
return ok2(
|
|
3212
3198
|
JSON.stringify({
|
|
3213
3199
|
message: "Snapshot saved to .harness/deps-lock.json",
|
|
@@ -3216,12 +3202,12 @@ async function dispatch(name, args, db, docsPath, cwd2, config) {
|
|
|
3216
3202
|
);
|
|
3217
3203
|
}
|
|
3218
3204
|
case "deps.check": {
|
|
3219
|
-
const pkgPath2 =
|
|
3220
|
-
const lockPath =
|
|
3221
|
-
if (!
|
|
3205
|
+
const pkgPath2 = join18(cwd2, "package.json");
|
|
3206
|
+
const lockPath = join18(cwd2, ".harness", "deps-lock.json");
|
|
3207
|
+
if (!existsSync15(pkgPath2)) {
|
|
3222
3208
|
return ok2("package.json not found in project root", true);
|
|
3223
3209
|
}
|
|
3224
|
-
if (!
|
|
3210
|
+
if (!existsSync15(lockPath)) {
|
|
3225
3211
|
return ok2(
|
|
3226
3212
|
JSON.stringify({
|
|
3227
3213
|
status: "no-snapshot",
|
|
@@ -3229,8 +3215,8 @@ async function dispatch(name, args, db, docsPath, cwd2, config) {
|
|
|
3229
3215
|
})
|
|
3230
3216
|
);
|
|
3231
3217
|
}
|
|
3232
|
-
const pkg2 = JSON.parse(
|
|
3233
|
-
const lock = JSON.parse(
|
|
3218
|
+
const pkg2 = JSON.parse(readFileSync10(pkgPath2, "utf8"));
|
|
3219
|
+
const lock = JSON.parse(readFileSync10(lockPath, "utf8"));
|
|
3234
3220
|
const current = { ...pkg2.dependencies ?? {}, ...pkg2.devDependencies ?? {} };
|
|
3235
3221
|
const previous = { ...lock.dependencies ?? {}, ...lock.devDependencies ?? {} };
|
|
3236
3222
|
const added = [];
|
|
@@ -3297,7 +3283,7 @@ function searchDocs(docsPath, query, maxResults = 10) {
|
|
|
3297
3283
|
for (const file of files) {
|
|
3298
3284
|
if (results.length >= maxResults) break;
|
|
3299
3285
|
try {
|
|
3300
|
-
const content =
|
|
3286
|
+
const content = readFileSync10(file, "utf8");
|
|
3301
3287
|
const lines = content.split("\n");
|
|
3302
3288
|
for (let i = 0; i < lines.length; i++) {
|
|
3303
3289
|
const lower = lines[i].toLowerCase();
|
|
@@ -3322,7 +3308,7 @@ function collectMarkdownFiles(dir) {
|
|
|
3322
3308
|
const files = [];
|
|
3323
3309
|
try {
|
|
3324
3310
|
for (const entry of readdirSync2(dir)) {
|
|
3325
|
-
const full =
|
|
3311
|
+
const full = join18(dir, entry);
|
|
3326
3312
|
const stat = statSync(full);
|
|
3327
3313
|
if (stat.isDirectory()) {
|
|
3328
3314
|
files.push(...collectMarkdownFiles(full));
|
|
@@ -3373,12 +3359,12 @@ async function runServe(cwd2, opts) {
|
|
|
3373
3359
|
|
|
3374
3360
|
// src/commands/status.ts
|
|
3375
3361
|
import Table from "cli-table3";
|
|
3376
|
-
import
|
|
3362
|
+
import pc11 from "picocolors";
|
|
3377
3363
|
var STATUS_COLOR = {
|
|
3378
|
-
pending: (s) =>
|
|
3379
|
-
in_progress: (s) =>
|
|
3380
|
-
done: (s) =>
|
|
3381
|
-
blocked: (s) =>
|
|
3364
|
+
pending: (s) => pc11.dim(s),
|
|
3365
|
+
in_progress: (s) => pc11.cyan(s),
|
|
3366
|
+
done: (s) => pc11.green(s),
|
|
3367
|
+
blocked: (s) => pc11.red(s)
|
|
3382
3368
|
};
|
|
3383
3369
|
async function runStatus(cwd2, opts) {
|
|
3384
3370
|
const config = await loadConfig(cwd2);
|
|
@@ -3399,11 +3385,11 @@ async function runStatus(cwd2, opts) {
|
|
|
3399
3385
|
return;
|
|
3400
3386
|
}
|
|
3401
3387
|
if (tasks.length === 0) {
|
|
3402
|
-
console.log(
|
|
3388
|
+
console.log(pc11.dim("No tasks yet. Run: ahk task add"));
|
|
3403
3389
|
return;
|
|
3404
3390
|
}
|
|
3405
3391
|
const table = new Table({
|
|
3406
|
-
head: ["ID", "Slug", "Title", "Status", "Assigned", "Started"].map((h) =>
|
|
3392
|
+
head: ["ID", "Slug", "Title", "Status", "Assigned", "Started"].map((h) => pc11.bold(h)),
|
|
3407
3393
|
style: { head: [], border: [] }
|
|
3408
3394
|
});
|
|
3409
3395
|
for (const t of tasks) {
|
|
@@ -3421,12 +3407,12 @@ async function runStatus(cwd2, opts) {
|
|
|
3421
3407
|
const inProgress = tasks.filter((t) => t.status === "in_progress");
|
|
3422
3408
|
if (inProgress.length > 0) {
|
|
3423
3409
|
console.log("");
|
|
3424
|
-
console.log(
|
|
3410
|
+
console.log(pc11.bold("Active actions:"));
|
|
3425
3411
|
for (const t of inProgress) {
|
|
3426
3412
|
const actions = await db.getActionsForTask(t.id);
|
|
3427
3413
|
const active = actions.filter((a) => a.status === "in_progress");
|
|
3428
3414
|
for (const a of active) {
|
|
3429
|
-
console.log(` ${
|
|
3415
|
+
console.log(` ${pc11.cyan(a.agent.padEnd(10))} \u2192 task #${t.id} ${t.slug}`);
|
|
3430
3416
|
}
|
|
3431
3417
|
}
|
|
3432
3418
|
}
|
|
@@ -3435,10 +3421,10 @@ async function runStatus(cwd2, opts) {
|
|
|
3435
3421
|
const fn = STATUS_COLOR[s.status] ?? ((x) => x);
|
|
3436
3422
|
return `${fn(s.status)}: ${s.total}`;
|
|
3437
3423
|
});
|
|
3438
|
-
console.log(
|
|
3424
|
+
console.log(pc11.dim("Tasks \u2014 ") + parts.join(pc11.dim(" | ")));
|
|
3439
3425
|
const archivedTasks = await db.getArchivedTasks();
|
|
3440
3426
|
if (archivedTasks.length > 0) {
|
|
3441
|
-
console.log(
|
|
3427
|
+
console.log(pc11.dim(`${archivedTasks.length} archived (use \`ahk task list --archived\` to view)`));
|
|
3442
3428
|
}
|
|
3443
3429
|
} finally {
|
|
3444
3430
|
await db.close();
|
|
@@ -3446,13 +3432,13 @@ async function runStatus(cwd2, opts) {
|
|
|
3446
3432
|
}
|
|
3447
3433
|
|
|
3448
3434
|
// src/commands/sync.ts
|
|
3449
|
-
import { existsSync as
|
|
3450
|
-
import { join as
|
|
3451
|
-
import
|
|
3435
|
+
import { existsSync as existsSync16, readFileSync as readFileSync11 } from "fs";
|
|
3436
|
+
import { join as join19, resolve as resolve11 } from "path";
|
|
3437
|
+
import pc12 from "picocolors";
|
|
3452
3438
|
async function runSync(cwd2, opts) {
|
|
3453
3439
|
const config = await loadConfig(cwd2);
|
|
3454
3440
|
const direction = opts.direction ?? "both";
|
|
3455
|
-
const featureListPath = resolve11(
|
|
3441
|
+
const featureListPath = resolve11(join19(cwd2, config.storage.dir, "feature_list.json"));
|
|
3456
3442
|
const db = await openDB(config, cwd2);
|
|
3457
3443
|
try {
|
|
3458
3444
|
if (direction === "in" || direction === "both") {
|
|
@@ -3466,44 +3452,44 @@ async function runSync(cwd2, opts) {
|
|
|
3466
3452
|
}
|
|
3467
3453
|
}
|
|
3468
3454
|
async function syncIn(featureListPath, db, dryRun) {
|
|
3469
|
-
if (!
|
|
3470
|
-
console.log(
|
|
3455
|
+
if (!existsSync16(featureListPath)) {
|
|
3456
|
+
console.log(pc12.dim(`feature_list.json not found at ${featureListPath} \u2014 skipping in-sync`));
|
|
3471
3457
|
return;
|
|
3472
3458
|
}
|
|
3473
3459
|
let seeds;
|
|
3474
3460
|
try {
|
|
3475
|
-
seeds = JSON.parse(
|
|
3461
|
+
seeds = JSON.parse(readFileSync11(featureListPath, "utf8"));
|
|
3476
3462
|
} catch (err) {
|
|
3477
|
-
console.error(
|
|
3463
|
+
console.error(pc12.red(`Failed to parse feature_list.json: ${err}`));
|
|
3478
3464
|
process.exit(1);
|
|
3479
3465
|
}
|
|
3480
3466
|
if (dryRun) {
|
|
3481
|
-
console.log(
|
|
3467
|
+
console.log(pc12.bold("Dry run \u2014 in-sync (feature_list.json \u2192 SQLite):"));
|
|
3482
3468
|
for (const t of seeds) {
|
|
3483
3469
|
const existing = await db.getTaskBySlug(t.slug);
|
|
3484
|
-
console.log(` ${existing ?
|
|
3470
|
+
console.log(` ${existing ? pc12.dim("skip") : pc12.green("add ")} ${t.slug}`);
|
|
3485
3471
|
}
|
|
3486
3472
|
return;
|
|
3487
3473
|
}
|
|
3488
3474
|
const result = await db.syncFromFeatureList(seeds);
|
|
3489
|
-
console.log(
|
|
3475
|
+
console.log(pc12.green(`\u2713 In-sync: ${result.added} added, ${result.skipped} already existed`));
|
|
3490
3476
|
}
|
|
3491
3477
|
async function syncOut(db, cwd2, dryRun) {
|
|
3492
3478
|
if (dryRun) {
|
|
3493
3479
|
const tasks = await db.getTasks();
|
|
3494
|
-
console.log(
|
|
3480
|
+
console.log(pc12.bold("Dry run \u2014 out-sync (SQLite \u2192 feature_list.json):"));
|
|
3495
3481
|
console.log(` ${tasks.length} tasks would be written`);
|
|
3496
3482
|
return;
|
|
3497
3483
|
}
|
|
3498
3484
|
await db.writeFeatureList(cwd2);
|
|
3499
|
-
console.log(
|
|
3485
|
+
console.log(pc12.green("\u2713 Out-sync: feature_list.json updated"));
|
|
3500
3486
|
}
|
|
3501
3487
|
|
|
3502
3488
|
// src/commands/task/add.ts
|
|
3503
3489
|
import * as p6 from "@clack/prompts";
|
|
3504
|
-
import
|
|
3490
|
+
import pc13 from "picocolors";
|
|
3505
3491
|
async function runTaskAdd(cwd2) {
|
|
3506
|
-
p6.intro(
|
|
3492
|
+
p6.intro(pc13.bold("agent-harness-kit \u2014 add task"));
|
|
3507
3493
|
const title = await cliFormWithRetry(
|
|
3508
3494
|
async () => {
|
|
3509
3495
|
const val = await p6.text({ message: "Task title" });
|
|
@@ -3546,10 +3532,10 @@ async function runTaskAdd(cwd2) {
|
|
|
3546
3532
|
await db.writeFeatureList(cwd2);
|
|
3547
3533
|
await db.close();
|
|
3548
3534
|
spinner6.stop("");
|
|
3549
|
-
console.log(
|
|
3550
|
-
console.log(
|
|
3535
|
+
console.log(pc13.green(`\u2713 Task #${task2.id} added \u2014 ${task2.slug} (pending)`));
|
|
3536
|
+
console.log(pc13.cyan("\u2192") + " " + pc13.cyan("ahk status") + " to see all tasks");
|
|
3551
3537
|
} catch (err) {
|
|
3552
|
-
spinner6.stop(
|
|
3538
|
+
spinner6.stop(pc13.red("Failed"));
|
|
3553
3539
|
p6.log.error(err instanceof Error ? err.message : String(err));
|
|
3554
3540
|
process.exit(1);
|
|
3555
3541
|
}
|
|
@@ -3557,17 +3543,17 @@ async function runTaskAdd(cwd2) {
|
|
|
3557
3543
|
|
|
3558
3544
|
// src/commands/task/done.ts
|
|
3559
3545
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
3560
|
-
import { existsSync as
|
|
3546
|
+
import { existsSync as existsSync17 } from "fs";
|
|
3561
3547
|
import { resolve as resolve12 } from "path";
|
|
3562
|
-
import
|
|
3548
|
+
import pc14 from "picocolors";
|
|
3563
3549
|
async function runTaskDone(cwd2, idOrSlug) {
|
|
3564
3550
|
const config = await loadConfig(cwd2);
|
|
3565
3551
|
if (config.health.required) {
|
|
3566
3552
|
const scriptPath = resolve12(cwd2, config.health.scriptPath);
|
|
3567
|
-
if (
|
|
3553
|
+
if (existsSync17(scriptPath)) {
|
|
3568
3554
|
const result = spawnSync2("bash", [scriptPath], { cwd: cwd2, stdio: "pipe", encoding: "utf8" });
|
|
3569
3555
|
if (result.status !== 0) {
|
|
3570
|
-
console.error(
|
|
3556
|
+
console.error(pc14.red("\u2717 Health check failed \u2014 cannot mark task as done."));
|
|
3571
3557
|
if (result.stdout) console.error(result.stdout);
|
|
3572
3558
|
if (result.stderr) console.error(result.stderr);
|
|
3573
3559
|
process.exit(1);
|
|
@@ -3580,16 +3566,16 @@ async function runTaskDone(cwd2, idOrSlug) {
|
|
|
3580
3566
|
const isId = !isNaN(parsed);
|
|
3581
3567
|
const task2 = isId ? await db.getTaskById(parsed) : await db.getTaskBySlug(idOrSlug);
|
|
3582
3568
|
if (!task2) {
|
|
3583
|
-
console.error(
|
|
3569
|
+
console.error(pc14.red(`Task not found: ${idOrSlug}`));
|
|
3584
3570
|
process.exit(1);
|
|
3585
3571
|
}
|
|
3586
3572
|
if (task2.status === "done") {
|
|
3587
|
-
console.log(
|
|
3573
|
+
console.log(pc14.dim(`Task #${task2.id} is already done.`));
|
|
3588
3574
|
return;
|
|
3589
3575
|
}
|
|
3590
3576
|
await db.updateTaskStatus(task2.id, "done");
|
|
3591
3577
|
await db.writeFeatureList(cwd2);
|
|
3592
|
-
console.log(
|
|
3578
|
+
console.log(pc14.green(`\u2713 Task #${task2.id} \u2014 ${task2.slug} marked as done`));
|
|
3593
3579
|
} finally {
|
|
3594
3580
|
await db.close();
|
|
3595
3581
|
}
|
|
@@ -3597,9 +3583,9 @@ async function runTaskDone(cwd2, idOrSlug) {
|
|
|
3597
3583
|
|
|
3598
3584
|
// src/commands/task/edit.ts
|
|
3599
3585
|
import * as p7 from "@clack/prompts";
|
|
3600
|
-
import
|
|
3586
|
+
import pc15 from "picocolors";
|
|
3601
3587
|
async function runTaskEdit(cwd2) {
|
|
3602
|
-
p7.intro(
|
|
3588
|
+
p7.intro(pc15.bold("agent-harness-kit \u2014 edit task"));
|
|
3603
3589
|
const config = await loadConfig(cwd2);
|
|
3604
3590
|
const db = await openDB(config, cwd2);
|
|
3605
3591
|
try {
|
|
@@ -3677,9 +3663,9 @@ async function runTaskEdit(cwd2) {
|
|
|
3677
3663
|
await db.updateTaskAcceptance(task2.id, newAcceptance);
|
|
3678
3664
|
await db.writeFeatureList(cwd2);
|
|
3679
3665
|
spinner6.stop("");
|
|
3680
|
-
console.log(
|
|
3666
|
+
console.log(pc15.green(`\u2713 Task #${task2.id} updated \u2014 ${newSlug}`));
|
|
3681
3667
|
} catch (err) {
|
|
3682
|
-
spinner6.stop(
|
|
3668
|
+
spinner6.stop(pc15.red("Failed"));
|
|
3683
3669
|
p7.log.error(err instanceof Error ? err.message : String(err));
|
|
3684
3670
|
process.exit(1);
|
|
3685
3671
|
}
|
|
@@ -3690,12 +3676,12 @@ async function runTaskEdit(cwd2) {
|
|
|
3690
3676
|
|
|
3691
3677
|
// src/commands/task/list.ts
|
|
3692
3678
|
import Table2 from "cli-table3";
|
|
3693
|
-
import
|
|
3679
|
+
import pc16 from "picocolors";
|
|
3694
3680
|
var STATUS_COLOR2 = {
|
|
3695
|
-
pending: (s) =>
|
|
3696
|
-
in_progress: (s) =>
|
|
3697
|
-
done: (s) =>
|
|
3698
|
-
blocked: (s) =>
|
|
3681
|
+
pending: (s) => pc16.dim(s),
|
|
3682
|
+
in_progress: (s) => pc16.cyan(s),
|
|
3683
|
+
done: (s) => pc16.green(s),
|
|
3684
|
+
blocked: (s) => pc16.red(s)
|
|
3699
3685
|
};
|
|
3700
3686
|
async function runTaskList(cwd2, opts) {
|
|
3701
3687
|
const config = await loadConfig(cwd2);
|
|
@@ -3712,11 +3698,11 @@ async function runTaskList(cwd2, opts) {
|
|
|
3712
3698
|
let msg = "No tasks";
|
|
3713
3699
|
if (filterStatus) msg += ` with status: ${filterStatus}`;
|
|
3714
3700
|
if (opts.archived) msg += " (archived)";
|
|
3715
|
-
console.log(
|
|
3701
|
+
console.log(pc16.dim(msg + "."));
|
|
3716
3702
|
return;
|
|
3717
3703
|
}
|
|
3718
3704
|
const table = new Table2({
|
|
3719
|
-
head: ["ID", "Slug", "Title", "Status"].map((h) =>
|
|
3705
|
+
head: ["ID", "Slug", "Title", "Status"].map((h) => pc16.bold(h)),
|
|
3720
3706
|
style: { head: [], border: [] }
|
|
3721
3707
|
});
|
|
3722
3708
|
for (const t of tasks) {
|
|
@@ -3727,7 +3713,7 @@ async function runTaskList(cwd2, opts) {
|
|
|
3727
3713
|
if (!opts.archived && !opts.includeArchived) {
|
|
3728
3714
|
const archivedTasks = await db.getArchivedTasks();
|
|
3729
3715
|
if (archivedTasks.length > 0) {
|
|
3730
|
-
console.log(
|
|
3716
|
+
console.log(pc16.dim(`${archivedTasks.length} archived task${archivedTasks.length !== 1 ? "s" : ""} (use --archived to view)`));
|
|
3731
3717
|
}
|
|
3732
3718
|
}
|
|
3733
3719
|
} finally {
|
|
@@ -3735,8 +3721,42 @@ async function runTaskList(cwd2, opts) {
|
|
|
3735
3721
|
}
|
|
3736
3722
|
}
|
|
3737
3723
|
|
|
3724
|
+
// src/core/local-install-guard.ts
|
|
3725
|
+
import { existsSync as existsSync18, readFileSync as readFileSync12 } from "fs";
|
|
3726
|
+
import { join as join20 } from "path";
|
|
3727
|
+
import pc17 from "picocolors";
|
|
3728
|
+
function isLocalInstallSatisfied(cwd2) {
|
|
3729
|
+
const selfPkgPath = join20(cwd2, "package.json");
|
|
3730
|
+
let projectPkg = null;
|
|
3731
|
+
if (existsSync18(selfPkgPath)) {
|
|
3732
|
+
try {
|
|
3733
|
+
const selfPkg = JSON.parse(readFileSync12(selfPkgPath, "utf8"));
|
|
3734
|
+
if (selfPkg?.name === pkg.name) return true;
|
|
3735
|
+
projectPkg = selfPkg;
|
|
3736
|
+
} catch {
|
|
3737
|
+
}
|
|
3738
|
+
}
|
|
3739
|
+
const [scope, name] = pkg.name.split("/");
|
|
3740
|
+
const localPath = pkg.name.startsWith("@") ? join20(cwd2, "node_modules", scope, name) : join20(cwd2, "node_modules", pkg.name);
|
|
3741
|
+
if (existsSync18(localPath)) return true;
|
|
3742
|
+
const isPnp = existsSync18(join20(cwd2, ".pnp.cjs")) || existsSync18(join20(cwd2, ".pnp.loader.mjs"));
|
|
3743
|
+
if (isPnp && projectPkg) {
|
|
3744
|
+
const deps = {
|
|
3745
|
+
...projectPkg.dependencies ?? {},
|
|
3746
|
+
...projectPkg.devDependencies ?? {}
|
|
3747
|
+
};
|
|
3748
|
+
if (Object.prototype.hasOwnProperty.call(deps, pkg.name)) return true;
|
|
3749
|
+
}
|
|
3750
|
+
return false;
|
|
3751
|
+
}
|
|
3752
|
+
function printLocalInstallWarning() {
|
|
3753
|
+
console.error(pc17.red(`\u2717 ${pkg.name} must be installed locally in this project.`));
|
|
3754
|
+
console.error(pc17.dim(` Run: npm install --save-dev ${pkg.name}`));
|
|
3755
|
+
console.error(pc17.dim(" (or the equivalent for your package manager: pnpm add -D, yarn add --dev, bun add -d)"));
|
|
3756
|
+
}
|
|
3757
|
+
|
|
3738
3758
|
// src/core/update-check.ts
|
|
3739
|
-
import
|
|
3759
|
+
import pc18 from "picocolors";
|
|
3740
3760
|
var REGISTRY_URL2 = `https://registry.npmjs.org/${pkg.name}/latest`;
|
|
3741
3761
|
var TIMEOUT_MS2 = 2500;
|
|
3742
3762
|
function checkForUpdate(currentVersion) {
|
|
@@ -3754,8 +3774,8 @@ function checkForUpdate(currentVersion) {
|
|
|
3754
3774
|
}
|
|
3755
3775
|
function printUpdateMessage({ current, latest }) {
|
|
3756
3776
|
const lines = [
|
|
3757
|
-
` Update available ${
|
|
3758
|
-
` Run: ${
|
|
3777
|
+
` Update available ${pc18.dim(current)} \u2192 ${pc18.green(latest)} `,
|
|
3778
|
+
` Run: ${pc18.cyan(`pnpm i ${pkg.name}@${latest}`)} `
|
|
3759
3779
|
];
|
|
3760
3780
|
drawBox(lines);
|
|
3761
3781
|
}
|
|
@@ -3773,7 +3793,7 @@ var cwd = process.cwd();
|
|
|
3773
3793
|
var updateCheck = checkForUpdate(pkg.version);
|
|
3774
3794
|
var program = new Command();
|
|
3775
3795
|
program.name("ahk").description("agent-harness-kit \u2014 CLI scaffolding for multi-agent harness systems").version(pkg.version, "-v, --version");
|
|
3776
|
-
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)").action(async (opts) => {
|
|
3796
|
+
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) => {
|
|
3777
3797
|
await runInit(cwd, opts);
|
|
3778
3798
|
});
|
|
3779
3799
|
program.command("build").description("Regenerate AGENTS.md and provider files from agent-harness-kit.config.ts").option("--watch", "Rebuild on config changes").option("--sync", "Sync tools: frontmatter in existing .claude/agents/*.md to match current permission constants").action(async (opts) => {
|
|
@@ -3807,9 +3827,20 @@ task.command("edit").description("Edit a task interactively").action(async () =>
|
|
|
3807
3827
|
program.command("dashboard").description("Open web dashboard to visualize harness data").option("-p, --port <port>", "Port to listen on", "4242").option("--no-open", "Do not open browser automatically").action(async (opts) => {
|
|
3808
3828
|
await runDashboard(cwd, { port: parseInt(opts.port), open: opts.open });
|
|
3809
3829
|
});
|
|
3810
|
-
program.command("migrate").description("Migrate provider
|
|
3830
|
+
var migrate = program.command("migrate").description("Migrate provider files to a different provider, or migrate harness storage (see subcommands)");
|
|
3831
|
+
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) => {
|
|
3811
3832
|
await runMigrate(cwd, opts);
|
|
3812
3833
|
});
|
|
3834
|
+
migrate.command("storage").description(
|
|
3835
|
+
"Migrate harness DB storage between local/global scope or sqlite/postgres/mysql, based on agent-harness-kit.config.ts vs the real current state"
|
|
3836
|
+
).option("--force", "Required to overwrite a non-empty destination (a backup is written first)").option("--dry-run", "Preview what would migrate without applying any changes").action(async (opts) => {
|
|
3837
|
+
try {
|
|
3838
|
+
await runMigrateStorage(cwd, { force: opts.force, dryRun: opts["dry-run"] });
|
|
3839
|
+
} catch (err) {
|
|
3840
|
+
console.error(pc19.red(`\u2717 ${err instanceof Error ? err.message : String(err)}`));
|
|
3841
|
+
process.exit(1);
|
|
3842
|
+
}
|
|
3843
|
+
});
|
|
3813
3844
|
program.command("export").description("Export the database").option("--sql", "SQL dump").option("--json", "JSON export of tasks and actions").option("--output <path>", "Output file path (default: stdout)").action(async (opts) => {
|
|
3814
3845
|
await runExport(cwd, opts);
|
|
3815
3846
|
});
|
|
@@ -3819,9 +3850,25 @@ program.command("reset").description("Reset/clear harness data (DB, feature list
|
|
|
3819
3850
|
program.command("doctor").description("Check lib version, agent files, and harness skills sync status").action(async () => {
|
|
3820
3851
|
await runDoctor(cwd);
|
|
3821
3852
|
});
|
|
3853
|
+
program.hook("preAction", () => {
|
|
3854
|
+
if (!isLocalInstallSatisfied(cwd)) {
|
|
3855
|
+
printLocalInstallWarning();
|
|
3856
|
+
process.exit(1);
|
|
3857
|
+
}
|
|
3858
|
+
});
|
|
3822
3859
|
program.hook("postAction", async () => {
|
|
3823
3860
|
const update = await updateCheck;
|
|
3824
3861
|
if (update) printUpdateMessage(update);
|
|
3825
3862
|
});
|
|
3826
|
-
|
|
3863
|
+
function rewriteLegacyMigrateArgv(argv) {
|
|
3864
|
+
const migrateIdx = argv.indexOf("migrate");
|
|
3865
|
+
if (migrateIdx === -1) return argv;
|
|
3866
|
+
const next = argv[migrateIdx + 1];
|
|
3867
|
+
const isLegacyForm = next === void 0 || next === "--to";
|
|
3868
|
+
if (!isLegacyForm) return argv;
|
|
3869
|
+
const rewritten = [...argv];
|
|
3870
|
+
rewritten.splice(migrateIdx + 1, 0, "provider");
|
|
3871
|
+
return rewritten;
|
|
3872
|
+
}
|
|
3873
|
+
program.parse(rewriteLegacyMigrateArgv(process.argv));
|
|
3827
3874
|
//# sourceMappingURL=cli.js.map
|