@cardor/agent-harness-kit 1.9.0 → 1.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +100 -24
- 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 +779 -850
- 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 +8 -0
- package/dist/index.js +1 -1
- 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
|
|
@@ -497,6 +568,10 @@ export default defineHarness({
|
|
|
497
568
|
nextSteps: false,
|
|
498
569
|
},
|
|
499
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}',
|
|
500
575
|
},
|
|
501
576
|
|
|
502
577
|
health: {
|
|
@@ -550,6 +625,10 @@ module.exports = defineHarness({
|
|
|
550
625
|
nextSteps: false,
|
|
551
626
|
},
|
|
552
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}',
|
|
553
632
|
},
|
|
554
633
|
|
|
555
634
|
health: {
|
|
@@ -684,14 +763,14 @@ var GITIGNORE_ENTRIES = `
|
|
|
684
763
|
// src/core/materializer/scaffold-utils.ts
|
|
685
764
|
var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
|
|
686
765
|
function writeAgentFile(cwd2, relPath, content) {
|
|
687
|
-
const abs =
|
|
688
|
-
if (
|
|
766
|
+
const abs = join4(cwd2, relPath);
|
|
767
|
+
if (existsSync3(abs)) return;
|
|
689
768
|
mkdirSync3(resolve2(abs, ".."), { recursive: true });
|
|
690
769
|
writeFileSync3(abs, content, "utf8");
|
|
691
770
|
}
|
|
692
771
|
function appendGitignore(cwd2) {
|
|
693
|
-
const giPath =
|
|
694
|
-
const existing =
|
|
772
|
+
const giPath = join4(cwd2, ".gitignore");
|
|
773
|
+
const existing = existsSync3(giPath) ? readFileSync4(giPath, "utf8") : "";
|
|
695
774
|
const toAdd = GITIGNORE_ENTRIES.split("\n").filter((line) => line && !existing.includes(line)).join("\n");
|
|
696
775
|
if (toAdd.trim()) {
|
|
697
776
|
writeFileSync3(giPath, existing + (existing.endsWith("\n") ? "" : "\n") + toAdd + "\n", "utf8");
|
|
@@ -703,13 +782,21 @@ function slugify(title) {
|
|
|
703
782
|
function writeSkills(cwd2, skillsDir) {
|
|
704
783
|
const skillNames = ["ahk-ask", "ahk-consultant", "ahk-triage", "ahk-review"];
|
|
705
784
|
for (const skillName of skillNames) {
|
|
706
|
-
const src =
|
|
707
|
-
const destDir =
|
|
708
|
-
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");
|
|
709
788
|
mkdirSync3(destDir, { recursive: true });
|
|
710
|
-
writeFileSync3(dest,
|
|
789
|
+
writeFileSync3(dest, readFileSync4(src, "utf8"), "utf8");
|
|
711
790
|
}
|
|
712
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
|
+
}
|
|
713
800
|
|
|
714
801
|
// src/core/materializer/claude-code.ts
|
|
715
802
|
var ClaudeCodeMaterializer = class {
|
|
@@ -717,12 +804,12 @@ var ClaudeCodeMaterializer = class {
|
|
|
717
804
|
const { cwd: cwd2 } = opts;
|
|
718
805
|
write(cwd2, "AGENTS.md", agentsMd(config));
|
|
719
806
|
write(cwd2, "CLAUDE.md", claudeMd(config));
|
|
720
|
-
if (!
|
|
807
|
+
if (!existsSync4(join5(cwd2, "health.sh"))) {
|
|
721
808
|
write(cwd2, "health.sh", HEALTH_SH, 493);
|
|
722
809
|
}
|
|
723
810
|
const tasks = opts.firstTask ? [{ slug: slugify(opts.firstTask.title), ...opts.firstTask }] : [];
|
|
724
811
|
write(cwd2, "feature_list.json", featureListJson(tasks));
|
|
725
|
-
if (!
|
|
812
|
+
if (!existsSync4(join5(cwd2, config.storage.markdownFallback.path))) {
|
|
726
813
|
write(
|
|
727
814
|
cwd2,
|
|
728
815
|
config.storage.markdownFallback.path,
|
|
@@ -748,15 +835,15 @@ No tasks in progress.
|
|
|
748
835
|
writeAgentFile(cwd2, ".claude/agents/consultant.md", translateFrontmatterForClaudeCode(agentConsultant({ projectName }), "consultant", consultantModel));
|
|
749
836
|
writeAgentFile(cwd2, ".claude/agents/builder.md", translateFrontmatterForClaudeCode(agentBuilder({ projectName, writablePaths }), "builder", builderModel));
|
|
750
837
|
writeAgentFile(cwd2, ".claude/agents/reviewer.md", translateFrontmatterForClaudeCode(agentReviewer({ projectName }), "reviewer", reviewerModel));
|
|
751
|
-
mergeClaudeMcpJson(
|
|
752
|
-
mergeClaudeSettingsJson(
|
|
753
|
-
mergeClaudeSettingsLocalJson(
|
|
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"));
|
|
754
841
|
appendGitignore(cwd2);
|
|
755
842
|
writeSkills(cwd2, ".claude/skills");
|
|
756
843
|
}
|
|
757
844
|
async build(config, cwd2) {
|
|
758
845
|
const write2 = (relPath, content) => {
|
|
759
|
-
const abs =
|
|
846
|
+
const abs = join5(cwd2, relPath);
|
|
760
847
|
mkdirSync4(resolve3(abs, ".."), { recursive: true });
|
|
761
848
|
writeFileSync4(abs, content, "utf8");
|
|
762
849
|
};
|
|
@@ -775,9 +862,9 @@ No tasks in progress.
|
|
|
775
862
|
write2(".claude/agents/consultant.md", translateFrontmatterForClaudeCode(agentConsultant({ projectName }), "consultant", consultantModel));
|
|
776
863
|
write2(".claude/agents/builder.md", translateFrontmatterForClaudeCode(agentBuilder({ projectName, writablePaths }), "builder", builderModel));
|
|
777
864
|
write2(".claude/agents/reviewer.md", translateFrontmatterForClaudeCode(agentReviewer({ projectName }), "reviewer", reviewerModel));
|
|
778
|
-
mergeClaudeMcpJson(
|
|
779
|
-
mergeClaudeSettingsJson(
|
|
780
|
-
mergeClaudeSettingsLocalJson(
|
|
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"));
|
|
781
868
|
writeSkills(cwd2, ".claude/skills");
|
|
782
869
|
}
|
|
783
870
|
async migrate(config, _to, _cwd) {
|
|
@@ -792,12 +879,12 @@ No tasks in progress.
|
|
|
792
879
|
reviewer: [...MCP_CLAUDE_PERMISSIONS_REVIEWER]
|
|
793
880
|
};
|
|
794
881
|
for (const [agent, tools] of Object.entries(AGENT_TOOLS)) {
|
|
795
|
-
const filePath =
|
|
796
|
-
if (!
|
|
882
|
+
const filePath = join5(cwd2, ".claude", "agents", `${agent}.md`);
|
|
883
|
+
if (!existsSync4(filePath)) {
|
|
797
884
|
console.log(` ${agent}.md not found \u2014 skipping`);
|
|
798
885
|
continue;
|
|
799
886
|
}
|
|
800
|
-
const content =
|
|
887
|
+
const content = readFileSync5(filePath, "utf-8");
|
|
801
888
|
const updated = content.replace(
|
|
802
889
|
/(tools:\n)((?: - [^\n]+\n)*)/m,
|
|
803
890
|
(_match, header, toolsSection) => {
|
|
@@ -818,23 +905,23 @@ No tasks in progress.
|
|
|
818
905
|
};
|
|
819
906
|
|
|
820
907
|
// src/core/materializer/codex-cli.ts
|
|
821
|
-
import { existsSync as
|
|
822
|
-
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";
|
|
823
910
|
var CodexCliMaterializer = class {
|
|
824
911
|
async scaffold(config, opts) {
|
|
825
912
|
const { cwd: cwd2 } = opts;
|
|
826
913
|
const write2 = (relPath, content, mode) => {
|
|
827
|
-
const abs =
|
|
914
|
+
const abs = join6(cwd2, relPath);
|
|
828
915
|
mkdirSync5(resolve4(abs, ".."), { recursive: true });
|
|
829
916
|
writeFileSync5(abs, content, { encoding: "utf8", mode });
|
|
830
917
|
};
|
|
831
918
|
write2("AGENTS.md", agentsMd(config));
|
|
832
|
-
if (!
|
|
919
|
+
if (!existsSync5(join6(cwd2, "health.sh"))) {
|
|
833
920
|
write2("health.sh", HEALTH_SH, 493);
|
|
834
921
|
}
|
|
835
922
|
const tasks = opts.firstTask ? [{ slug: slugify(opts.firstTask.title), ...opts.firstTask }] : [];
|
|
836
|
-
write2(
|
|
837
|
-
if (!
|
|
923
|
+
write2(join6(config.storage.dir, "feature_list.json"), featureListJson(tasks));
|
|
924
|
+
if (!existsSync5(join6(cwd2, config.storage.markdownFallback.path))) {
|
|
838
925
|
write2(
|
|
839
926
|
config.storage.markdownFallback.path,
|
|
840
927
|
`<!-- AUTO-GENERATED by agent-harness-kit \u2014 DO NOT EDIT MANUALLY -->
|
|
@@ -860,13 +947,13 @@ No tasks in progress.
|
|
|
860
947
|
writeAgentFile(cwd2, ".codex/agents/builder.toml", agentBuilderToml({ projectName, writablePaths, model: builderModel }));
|
|
861
948
|
writeAgentFile(cwd2, ".codex/agents/reviewer.toml", agentReviewerToml({ projectName, model: reviewerModel }));
|
|
862
949
|
writeAgentFile(cwd2, ".codex/agents/default.toml", agentLeadAsDefaultToml({ projectName, model: leadModel }));
|
|
863
|
-
mergeCodexConfigToml(
|
|
950
|
+
mergeCodexConfigToml(join6(cwd2, ".codex/config.toml"), config.tools.mcp.port, detectPackageManager(cwd2));
|
|
864
951
|
appendGitignore(cwd2);
|
|
865
952
|
writeSkills(cwd2, ".agents/skills");
|
|
866
953
|
}
|
|
867
954
|
async build(config, cwd2) {
|
|
868
955
|
const write2 = (relPath, content) => {
|
|
869
|
-
const abs =
|
|
956
|
+
const abs = join6(cwd2, relPath);
|
|
870
957
|
mkdirSync5(resolve4(abs, ".."), { recursive: true });
|
|
871
958
|
writeFileSync5(abs, content, "utf8");
|
|
872
959
|
};
|
|
@@ -885,7 +972,7 @@ No tasks in progress.
|
|
|
885
972
|
writeAgentFile(cwd2, ".codex/agents/builder.toml", agentBuilderToml({ projectName, writablePaths, model: builderModel }));
|
|
886
973
|
writeAgentFile(cwd2, ".codex/agents/reviewer.toml", agentReviewerToml({ projectName, model: reviewerModel }));
|
|
887
974
|
writeAgentFile(cwd2, ".codex/agents/default.toml", agentLeadAsDefaultToml({ projectName, model: leadModel }));
|
|
888
|
-
mergeCodexConfigToml(
|
|
975
|
+
mergeCodexConfigToml(join6(cwd2, ".codex/config.toml"), config.tools.mcp.port, detectPackageManager(cwd2));
|
|
889
976
|
writeSkills(cwd2, ".agents/skills");
|
|
890
977
|
}
|
|
891
978
|
async migrate(config, _to, _cwd) {
|
|
@@ -897,23 +984,23 @@ No tasks in progress.
|
|
|
897
984
|
};
|
|
898
985
|
|
|
899
986
|
// src/core/materializer/opencode.ts
|
|
900
|
-
import { existsSync as
|
|
901
|
-
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";
|
|
902
989
|
var OpenCodeMaterializer = class {
|
|
903
990
|
async scaffold(config, opts) {
|
|
904
991
|
const { cwd: cwd2 } = opts;
|
|
905
992
|
const write2 = (relPath, content, mode) => {
|
|
906
|
-
const abs =
|
|
993
|
+
const abs = join7(cwd2, relPath);
|
|
907
994
|
mkdirSync6(resolve5(abs, ".."), { recursive: true });
|
|
908
995
|
writeFileSync6(abs, content, { encoding: "utf8", mode });
|
|
909
996
|
};
|
|
910
997
|
write2("AGENTS.md", agentsMd(config));
|
|
911
|
-
if (!
|
|
998
|
+
if (!existsSync6(join7(cwd2, "health.sh"))) {
|
|
912
999
|
write2("health.sh", HEALTH_SH, 493);
|
|
913
1000
|
}
|
|
914
1001
|
const tasks = opts.firstTask ? [{ slug: slugify(opts.firstTask.title), ...opts.firstTask }] : [];
|
|
915
|
-
write2(
|
|
916
|
-
if (!
|
|
1002
|
+
write2(join7(config.storage.dir, "feature_list.json"), featureListJson(tasks));
|
|
1003
|
+
if (!existsSync6(join7(cwd2, config.storage.markdownFallback.path))) {
|
|
917
1004
|
write2(
|
|
918
1005
|
config.storage.markdownFallback.path,
|
|
919
1006
|
`<!-- AUTO-GENERATED by agent-harness-kit \u2014 DO NOT EDIT MANUALLY -->
|
|
@@ -933,13 +1020,13 @@ No tasks in progress.
|
|
|
933
1020
|
writeAgentFile(cwd2, ".opencode/agents/consultant.md", translateFrontmatterForOpenCode(agentConsultant({ projectName })));
|
|
934
1021
|
writeAgentFile(cwd2, ".opencode/agents/builder.md", translateFrontmatterForOpenCode(agentBuilder({ projectName, writablePaths })));
|
|
935
1022
|
writeAgentFile(cwd2, ".opencode/agents/reviewer.md", translateFrontmatterForOpenCode(agentReviewer({ projectName })));
|
|
936
|
-
mergeOpencodeJson(
|
|
1023
|
+
mergeOpencodeJson(join7(cwd2, "opencode.json"), config.tools.mcp.port, detectPackageManager(cwd2));
|
|
937
1024
|
appendGitignore(cwd2);
|
|
938
1025
|
writeSkills(cwd2, ".opencode/skills");
|
|
939
1026
|
}
|
|
940
1027
|
async build(config, cwd2) {
|
|
941
1028
|
const write2 = (relPath, content) => {
|
|
942
|
-
const abs =
|
|
1029
|
+
const abs = join7(cwd2, relPath);
|
|
943
1030
|
mkdirSync6(resolve5(abs, ".."), { recursive: true });
|
|
944
1031
|
writeFileSync6(abs, content, "utf8");
|
|
945
1032
|
};
|
|
@@ -952,7 +1039,7 @@ No tasks in progress.
|
|
|
952
1039
|
writeAgentFile(cwd2, ".opencode/agents/consultant.md", translateFrontmatterForOpenCode(agentConsultant({ projectName })));
|
|
953
1040
|
writeAgentFile(cwd2, ".opencode/agents/builder.md", translateFrontmatterForOpenCode(agentBuilder({ projectName, writablePaths })));
|
|
954
1041
|
writeAgentFile(cwd2, ".opencode/agents/reviewer.md", translateFrontmatterForOpenCode(agentReviewer({ projectName })));
|
|
955
|
-
mergeOpencodeJson(
|
|
1042
|
+
mergeOpencodeJson(join7(cwd2, "opencode.json"), config.tools.mcp.port, detectPackageManager(cwd2));
|
|
956
1043
|
writeSkills(cwd2, ".opencode/skills");
|
|
957
1044
|
}
|
|
958
1045
|
async migrate(config, _to, _cwd) {
|
|
@@ -1018,14 +1105,14 @@ async function buildOnce(cwd2) {
|
|
|
1018
1105
|
}
|
|
1019
1106
|
|
|
1020
1107
|
// src/commands/dashboard.ts
|
|
1021
|
-
import { dirname as
|
|
1108
|
+
import { dirname as dirname4, join as join9, resolve as resolve6 } from "path";
|
|
1022
1109
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
1023
1110
|
import pc2 from "picocolors";
|
|
1024
1111
|
|
|
1025
1112
|
// src/core/dashboard-server.ts
|
|
1026
1113
|
import { watch as watch2 } from "fs";
|
|
1027
|
-
import { existsSync as
|
|
1028
|
-
import { extname, join as
|
|
1114
|
+
import { existsSync as existsSync7, readFileSync as readFileSync6 } from "fs";
|
|
1115
|
+
import { extname, join as join8 } from "path";
|
|
1029
1116
|
import { serve } from "@hono/node-server";
|
|
1030
1117
|
import { Hono } from "hono";
|
|
1031
1118
|
import { WebSocketServer } from "ws";
|
|
@@ -1067,7 +1154,7 @@ var MIME = {
|
|
|
1067
1154
|
".ttf": "font/ttf"
|
|
1068
1155
|
};
|
|
1069
1156
|
function fileResponse(filePath) {
|
|
1070
|
-
const content =
|
|
1157
|
+
const content = readFileSync6(filePath);
|
|
1071
1158
|
const mime = MIME[extname(filePath)] ?? "application/octet-stream";
|
|
1072
1159
|
return new Response(content, {
|
|
1073
1160
|
headers: { "Content-Type": mime, "Cache-Control": "no-cache" }
|
|
@@ -1173,15 +1260,15 @@ async function startDashboardServer(db, dbPath, staticPath, port) {
|
|
|
1173
1260
|
app.get("/*", (c) => {
|
|
1174
1261
|
const urlPath = c.req.path;
|
|
1175
1262
|
if (urlPath !== "/") {
|
|
1176
|
-
const candidate =
|
|
1177
|
-
if (
|
|
1263
|
+
const candidate = join8(staticPath, urlPath);
|
|
1264
|
+
if (existsSync7(candidate)) {
|
|
1178
1265
|
try {
|
|
1179
1266
|
return fileResponse(candidate);
|
|
1180
1267
|
} catch {
|
|
1181
1268
|
}
|
|
1182
1269
|
}
|
|
1183
1270
|
}
|
|
1184
|
-
return fileResponse(
|
|
1271
|
+
return fileResponse(join8(staticPath, "index.html"));
|
|
1185
1272
|
});
|
|
1186
1273
|
const resolvedPort = await findFreePort(port);
|
|
1187
1274
|
if (resolvedPort !== port) {
|
|
@@ -1212,7 +1299,7 @@ async function startDashboardServer(db, dbPath, staticPath, port) {
|
|
|
1212
1299
|
let watcher = null;
|
|
1213
1300
|
if (dbPath) {
|
|
1214
1301
|
const walPath = `${dbPath}-wal`;
|
|
1215
|
-
const watchTarget =
|
|
1302
|
+
const watchTarget = existsSync7(walPath) ? walPath : dbPath;
|
|
1216
1303
|
watcher = watch2(watchTarget, broadcast);
|
|
1217
1304
|
}
|
|
1218
1305
|
return {
|
|
@@ -1226,635 +1313,12 @@ async function startDashboardServer(db, dbPath, staticPath, port) {
|
|
|
1226
1313
|
};
|
|
1227
1314
|
}
|
|
1228
1315
|
|
|
1229
|
-
// src/core/db.ts
|
|
1230
|
-
import { randomUUID } from "crypto";
|
|
1231
|
-
import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync7 } from "fs";
|
|
1232
|
-
import { dirname as dirname4, join as join8, resolve as resolve6 } from "path";
|
|
1233
|
-
|
|
1234
|
-
// src/core/repositories/ActionRepository.ts
|
|
1235
|
-
var ActionRepository = class {
|
|
1236
|
-
constructor(driver) {
|
|
1237
|
-
this.driver = driver;
|
|
1238
|
-
}
|
|
1239
|
-
driver;
|
|
1240
|
-
async create(id, taskId, agent, now) {
|
|
1241
|
-
await this.driver.exec(
|
|
1242
|
-
`INSERT INTO actions (id, task_id, agent, status, created_at) VALUES (?, ?, ?, 'in_progress', ?)`,
|
|
1243
|
-
[id, taskId, agent, now]
|
|
1244
|
-
);
|
|
1245
|
-
}
|
|
1246
|
-
async complete(actionId, summary, now) {
|
|
1247
|
-
await this.driver.exec(
|
|
1248
|
-
`UPDATE actions SET status = 'completed', completed_at = ?, summary = ? WHERE id = ?`,
|
|
1249
|
-
[now, summary, actionId]
|
|
1250
|
-
);
|
|
1251
|
-
}
|
|
1252
|
-
async closeOrphaned(taskId, now) {
|
|
1253
|
-
return this.driver.exec(
|
|
1254
|
-
`UPDATE actions SET status = 'completed', completed_at = ?, summary = 'Auto-closed: task marked done' WHERE task_id = ? AND status = 'in_progress'`,
|
|
1255
|
-
[now, taskId]
|
|
1256
|
-
);
|
|
1257
|
-
}
|
|
1258
|
-
async getById(actionId) {
|
|
1259
|
-
return this.driver.queryOne(`SELECT * FROM actions WHERE id = ?`, [actionId]);
|
|
1260
|
-
}
|
|
1261
|
-
async getForTask(taskId) {
|
|
1262
|
-
return this.driver.query(
|
|
1263
|
-
`SELECT * FROM actions WHERE task_id = ? ORDER BY created_at`,
|
|
1264
|
-
[taskId]
|
|
1265
|
-
);
|
|
1266
|
-
}
|
|
1267
|
-
async getAll() {
|
|
1268
|
-
return this.driver.query(`SELECT * FROM actions ORDER BY created_at`);
|
|
1269
|
-
}
|
|
1270
|
-
async getWithDetails(taskId) {
|
|
1271
|
-
const actions = await this.getForTask(taskId);
|
|
1272
|
-
return Promise.all(
|
|
1273
|
-
actions.map(async (action) => ({
|
|
1274
|
-
...action,
|
|
1275
|
-
sections: await this.getSections(action.id),
|
|
1276
|
-
files: await this.getFiles(action.id),
|
|
1277
|
-
tools: await this.getTools(action.id)
|
|
1278
|
-
}))
|
|
1279
|
-
);
|
|
1280
|
-
}
|
|
1281
|
-
// ─── Sections ─────────────────────────────────────────────────────────────
|
|
1282
|
-
async addSection(actionId, sectionType, content, now) {
|
|
1283
|
-
await this.driver.exec(
|
|
1284
|
-
`INSERT INTO action_sections (action_id, section_type, content, created_at) VALUES (?, ?, ?, ?)`,
|
|
1285
|
-
[actionId, sectionType, content, now]
|
|
1286
|
-
);
|
|
1287
|
-
}
|
|
1288
|
-
async getSections(actionId) {
|
|
1289
|
-
return this.driver.query(
|
|
1290
|
-
`SELECT * FROM action_sections WHERE action_id = ? ORDER BY created_at`,
|
|
1291
|
-
[actionId]
|
|
1292
|
-
);
|
|
1293
|
-
}
|
|
1294
|
-
async getAllSections() {
|
|
1295
|
-
return this.driver.query(`SELECT * FROM action_sections ORDER BY created_at`);
|
|
1296
|
-
}
|
|
1297
|
-
// ─── Files ────────────────────────────────────────────────────────────────
|
|
1298
|
-
async addFile(actionId, filePath, operation, notes) {
|
|
1299
|
-
await this.driver.exec(
|
|
1300
|
-
`INSERT INTO action_files (action_id, file_path, operation, notes) VALUES (?, ?, ?, ?)`,
|
|
1301
|
-
[actionId, filePath, operation, notes]
|
|
1302
|
-
);
|
|
1303
|
-
}
|
|
1304
|
-
async getFiles(actionId) {
|
|
1305
|
-
return this.driver.query(
|
|
1306
|
-
`SELECT * FROM action_files WHERE action_id = ?`,
|
|
1307
|
-
[actionId]
|
|
1308
|
-
);
|
|
1309
|
-
}
|
|
1310
|
-
async getFilesForTask(taskId) {
|
|
1311
|
-
return this.driver.query(
|
|
1312
|
-
`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`,
|
|
1313
|
-
[taskId]
|
|
1314
|
-
);
|
|
1315
|
-
}
|
|
1316
|
-
// ─── Tools ────────────────────────────────────────────────────────────────
|
|
1317
|
-
async addTool(actionId, toolName, argsJson, resultSummary, now) {
|
|
1318
|
-
await this.driver.exec(
|
|
1319
|
-
`INSERT INTO action_tools (action_id, tool_name, args_json, result_summary, called_at) VALUES (?, ?, ?, ?, ?)`,
|
|
1320
|
-
[actionId, toolName, argsJson, resultSummary, now]
|
|
1321
|
-
);
|
|
1322
|
-
}
|
|
1323
|
-
async getTools(actionId) {
|
|
1324
|
-
return this.driver.query(
|
|
1325
|
-
`SELECT * FROM action_tools WHERE action_id = ? ORDER BY called_at`,
|
|
1326
|
-
[actionId]
|
|
1327
|
-
);
|
|
1328
|
-
}
|
|
1329
|
-
async getTopTools(limit) {
|
|
1330
|
-
return this.driver.query(
|
|
1331
|
-
`SELECT tool_name, COUNT(*) as uses FROM action_tools GROUP BY tool_name ORDER BY uses DESC LIMIT ?`,
|
|
1332
|
-
[limit]
|
|
1333
|
-
);
|
|
1334
|
-
}
|
|
1335
|
-
};
|
|
1336
|
-
|
|
1337
|
-
// src/core/repositories/StatsRepository.ts
|
|
1338
|
-
var AGENT_ORDER = ["lead", "explorer", "builder", "reviewer"];
|
|
1339
|
-
var StatsRepository = class {
|
|
1340
|
-
constructor(driver) {
|
|
1341
|
-
this.driver = driver;
|
|
1342
|
-
}
|
|
1343
|
-
driver;
|
|
1344
|
-
async getCounts() {
|
|
1345
|
-
const [{ total: totalActions }] = await this.driver.query(
|
|
1346
|
-
`SELECT COUNT(*) as total FROM actions`
|
|
1347
|
-
);
|
|
1348
|
-
const [{ total: totalFiles }] = await this.driver.query(
|
|
1349
|
-
`SELECT COUNT(*) as total FROM action_files`
|
|
1350
|
-
);
|
|
1351
|
-
const [{ total: uniqueTools }] = await this.driver.query(
|
|
1352
|
-
`SELECT COUNT(DISTINCT tool_name) as total FROM action_tools`
|
|
1353
|
-
);
|
|
1354
|
-
const [{ total: activeAgents }] = await this.driver.query(
|
|
1355
|
-
`SELECT COUNT(DISTINCT agent) as total FROM actions WHERE status = 'in_progress'`
|
|
1356
|
-
);
|
|
1357
|
-
return { totalActions, totalFiles, uniqueTools, activeAgents };
|
|
1358
|
-
}
|
|
1359
|
-
async getRecentTools(limit) {
|
|
1360
|
-
return this.driver.query(
|
|
1361
|
-
`SELECT at.*, t.id as task_id, t.title as task_title, t.slug as task_slug, a.agent
|
|
1362
|
-
FROM action_tools at
|
|
1363
|
-
JOIN actions a ON at.action_id = a.id
|
|
1364
|
-
JOIN tasks t ON a.task_id = t.id
|
|
1365
|
-
ORDER BY at.called_at DESC
|
|
1366
|
-
LIMIT ?`,
|
|
1367
|
-
[limit]
|
|
1368
|
-
);
|
|
1369
|
-
}
|
|
1370
|
-
async getTopFiles(limit) {
|
|
1371
|
-
return this.driver.query(
|
|
1372
|
-
`SELECT
|
|
1373
|
-
file_path,
|
|
1374
|
-
COUNT(*) as total,
|
|
1375
|
-
SUM(CASE WHEN operation='read' THEN 1 ELSE 0 END) as read,
|
|
1376
|
-
SUM(CASE WHEN operation='created' THEN 1 ELSE 0 END) as created,
|
|
1377
|
-
SUM(CASE WHEN operation='modified' THEN 1 ELSE 0 END) as modified,
|
|
1378
|
-
SUM(CASE WHEN operation='deleted' THEN 1 ELSE 0 END) as deleted
|
|
1379
|
-
FROM action_files
|
|
1380
|
-
GROUP BY file_path
|
|
1381
|
-
ORDER BY total DESC
|
|
1382
|
-
LIMIT ?`,
|
|
1383
|
-
[limit]
|
|
1384
|
-
);
|
|
1385
|
-
}
|
|
1386
|
-
async getRecentFiles(limit) {
|
|
1387
|
-
return this.driver.query(
|
|
1388
|
-
`SELECT af.*, t.id as task_id, t.title as task_title, t.slug as task_slug,
|
|
1389
|
-
a.agent, a.created_at as called_at
|
|
1390
|
-
FROM action_files af
|
|
1391
|
-
JOIN actions a ON af.action_id = a.id
|
|
1392
|
-
JOIN tasks t ON a.task_id = t.id
|
|
1393
|
-
ORDER BY a.created_at DESC
|
|
1394
|
-
LIMIT ?`,
|
|
1395
|
-
[limit]
|
|
1396
|
-
);
|
|
1397
|
-
}
|
|
1398
|
-
async getAgentStats() {
|
|
1399
|
-
const rows = await this.driver.query(
|
|
1400
|
-
`SELECT
|
|
1401
|
-
a.agent,
|
|
1402
|
-
COUNT(*) as actions_total,
|
|
1403
|
-
SUM(CASE WHEN a.status='completed' THEN 1 ELSE 0 END) as actions_done,
|
|
1404
|
-
SUM(CASE WHEN a.status='blocked' THEN 1 ELSE 0 END) as actions_blocked,
|
|
1405
|
-
COUNT(DISTINCT a.task_id) as tasks_worked,
|
|
1406
|
-
COUNT(DISTINCT af.file_path) as files_touched
|
|
1407
|
-
FROM actions a
|
|
1408
|
-
LEFT JOIN action_files af ON af.action_id = a.id
|
|
1409
|
-
GROUP BY a.agent
|
|
1410
|
-
ORDER BY actions_total DESC`
|
|
1411
|
-
);
|
|
1412
|
-
return rows.sort((a, b) => {
|
|
1413
|
-
const ai = AGENT_ORDER.indexOf(a.agent);
|
|
1414
|
-
const bi = AGENT_ORDER.indexOf(b.agent);
|
|
1415
|
-
if (ai === -1 && bi === -1) return 0;
|
|
1416
|
-
if (ai === -1) return 1;
|
|
1417
|
-
if (bi === -1) return -1;
|
|
1418
|
-
return ai - bi;
|
|
1419
|
-
});
|
|
1420
|
-
}
|
|
1421
|
-
async getTimeline(limit) {
|
|
1422
|
-
return this.driver.query(
|
|
1423
|
-
`SELECT a.*, t.title as task_title, t.slug as task_slug, t.status as task_status
|
|
1424
|
-
FROM actions a
|
|
1425
|
-
JOIN tasks t ON a.task_id = t.id
|
|
1426
|
-
ORDER BY a.created_at DESC
|
|
1427
|
-
LIMIT ?`,
|
|
1428
|
-
[limit]
|
|
1429
|
-
);
|
|
1430
|
-
}
|
|
1431
|
-
};
|
|
1432
|
-
|
|
1433
|
-
// src/core/repositories/TaskRepository.ts
|
|
1434
|
-
var TaskRepository = class {
|
|
1435
|
-
constructor(driver) {
|
|
1436
|
-
this.driver = driver;
|
|
1437
|
-
}
|
|
1438
|
-
driver;
|
|
1439
|
-
async add(params) {
|
|
1440
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1441
|
-
return this.driver.insert(
|
|
1442
|
-
`INSERT INTO tasks (slug, title, description, status, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)`,
|
|
1443
|
-
[params.slug, params.title, params.description ?? null, params.status ?? "pending", now, now]
|
|
1444
|
-
);
|
|
1445
|
-
}
|
|
1446
|
-
async addAcceptance(taskId, criteria) {
|
|
1447
|
-
for (const criterion of criteria) {
|
|
1448
|
-
await this.driver.exec(
|
|
1449
|
-
`INSERT INTO task_acceptance (task_id, criterion) VALUES (?, ?)`,
|
|
1450
|
-
[taskId, criterion]
|
|
1451
|
-
);
|
|
1452
|
-
}
|
|
1453
|
-
}
|
|
1454
|
-
async getAll(status, includeArchived = false) {
|
|
1455
|
-
let sql = `SELECT * FROM tasks`;
|
|
1456
|
-
const params = [];
|
|
1457
|
-
const conditions = [];
|
|
1458
|
-
if (!includeArchived) {
|
|
1459
|
-
conditions.push(`archived_at IS NULL`);
|
|
1460
|
-
}
|
|
1461
|
-
if (status) {
|
|
1462
|
-
conditions.push(`status = ?`);
|
|
1463
|
-
params.push(status);
|
|
1464
|
-
}
|
|
1465
|
-
if (conditions.length > 0) {
|
|
1466
|
-
sql += ` WHERE ${conditions.join(" AND ")}`;
|
|
1467
|
-
}
|
|
1468
|
-
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`;
|
|
1469
|
-
return this.driver.query(sql, params);
|
|
1470
|
-
}
|
|
1471
|
-
async getAllWithAcceptanceCounts(includeArchived = false) {
|
|
1472
|
-
let sql = `
|
|
1473
|
-
SELECT t.*,
|
|
1474
|
-
COUNT(ta.id) as acceptance_total,
|
|
1475
|
-
COALESCE(SUM(ta.met), 0) as acceptance_met
|
|
1476
|
-
FROM tasks t
|
|
1477
|
-
LEFT JOIN task_acceptance ta ON ta.task_id = t.id
|
|
1478
|
-
`;
|
|
1479
|
-
if (!includeArchived) {
|
|
1480
|
-
sql += ` WHERE t.archived_at IS NULL`;
|
|
1481
|
-
}
|
|
1482
|
-
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`;
|
|
1483
|
-
return this.driver.query(sql);
|
|
1484
|
-
}
|
|
1485
|
-
async getById(id) {
|
|
1486
|
-
return this.driver.queryOne(`SELECT * FROM tasks WHERE id = ?`, [id]);
|
|
1487
|
-
}
|
|
1488
|
-
async getBySlug(slug) {
|
|
1489
|
-
return this.driver.queryOne(`SELECT * FROM tasks WHERE slug = ?`, [slug]);
|
|
1490
|
-
}
|
|
1491
|
-
async getAcceptance(taskId) {
|
|
1492
|
-
return this.driver.query(
|
|
1493
|
-
`SELECT * FROM task_acceptance WHERE task_id = ?`,
|
|
1494
|
-
[taskId]
|
|
1495
|
-
);
|
|
1496
|
-
}
|
|
1497
|
-
async setStatus(id, status, extra) {
|
|
1498
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1499
|
-
if (extra?.started_at) {
|
|
1500
|
-
await this.driver.exec(
|
|
1501
|
-
`UPDATE tasks SET status = ?, started_at = ?, updated_at = ? WHERE id = ?`,
|
|
1502
|
-
[status, extra.started_at, now, id]
|
|
1503
|
-
);
|
|
1504
|
-
} else if (extra?.completed_at) {
|
|
1505
|
-
await this.driver.exec(
|
|
1506
|
-
`UPDATE tasks SET status = ?, completed_at = ?, updated_at = ? WHERE id = ?`,
|
|
1507
|
-
[status, extra.completed_at, now, id]
|
|
1508
|
-
);
|
|
1509
|
-
} else {
|
|
1510
|
-
await this.driver.exec(`UPDATE tasks SET status = ?, updated_at = ? WHERE id = ?`, [status, now, id]);
|
|
1511
|
-
}
|
|
1512
|
-
}
|
|
1513
|
-
async update(id, params) {
|
|
1514
|
-
const sets = [];
|
|
1515
|
-
const vals = [];
|
|
1516
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1517
|
-
if (params.title !== void 0) {
|
|
1518
|
-
sets.push("title = ?");
|
|
1519
|
-
vals.push(params.title);
|
|
1520
|
-
}
|
|
1521
|
-
if (params.description !== void 0) {
|
|
1522
|
-
sets.push("description = ?");
|
|
1523
|
-
vals.push(params.description);
|
|
1524
|
-
}
|
|
1525
|
-
if (params.slug !== void 0) {
|
|
1526
|
-
sets.push("slug = ?");
|
|
1527
|
-
vals.push(params.slug);
|
|
1528
|
-
}
|
|
1529
|
-
if (sets.length === 0) return;
|
|
1530
|
-
sets.push("updated_at = ?");
|
|
1531
|
-
vals.push(now);
|
|
1532
|
-
vals.push(id);
|
|
1533
|
-
await this.driver.exec(`UPDATE tasks SET ${sets.join(", ")} WHERE id = ?`, vals);
|
|
1534
|
-
}
|
|
1535
|
-
async replaceAcceptance(taskId, criteria) {
|
|
1536
|
-
await this.driver.exec(`DELETE FROM task_acceptance WHERE task_id = ?`, [taskId]);
|
|
1537
|
-
for (const criterion of criteria) {
|
|
1538
|
-
await this.driver.exec(
|
|
1539
|
-
`INSERT INTO task_acceptance (task_id, criterion) VALUES (?, ?)`,
|
|
1540
|
-
[taskId, criterion]
|
|
1541
|
-
);
|
|
1542
|
-
}
|
|
1543
|
-
}
|
|
1544
|
-
async archive(id) {
|
|
1545
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1546
|
-
await this.driver.exec(`UPDATE tasks SET archived_at = ?, updated_at = ? WHERE id = ?`, [now, now, id]);
|
|
1547
|
-
}
|
|
1548
|
-
async unarchive(id) {
|
|
1549
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1550
|
-
await this.driver.exec(`UPDATE tasks SET archived_at = NULL, updated_at = ? WHERE id = ?`, [now, id]);
|
|
1551
|
-
}
|
|
1552
|
-
async getArchived() {
|
|
1553
|
-
return this.driver.query(
|
|
1554
|
-
`SELECT * FROM tasks WHERE archived_at IS NOT NULL ORDER BY archived_at DESC`
|
|
1555
|
-
);
|
|
1556
|
-
}
|
|
1557
|
-
async claim(id, agent, now) {
|
|
1558
|
-
return this.driver.exec(
|
|
1559
|
-
`UPDATE tasks SET status = 'in_progress', assigned_to = ?, started_at = ?, updated_at = ? WHERE id = ? AND status = 'pending'`,
|
|
1560
|
-
[agent, now, now, id]
|
|
1561
|
-
);
|
|
1562
|
-
}
|
|
1563
|
-
async markAcceptanceMet(criterionId) {
|
|
1564
|
-
await this.driver.exec(`UPDATE task_acceptance SET met = 1 WHERE id = ?`, [criterionId]);
|
|
1565
|
-
}
|
|
1566
|
-
async getStatusSummary() {
|
|
1567
|
-
return this.driver.query(
|
|
1568
|
-
`SELECT status, COUNT(*) as total FROM tasks WHERE archived_at IS NULL GROUP BY status`
|
|
1569
|
-
);
|
|
1570
|
-
}
|
|
1571
|
-
};
|
|
1572
|
-
|
|
1573
|
-
// src/core/db.ts
|
|
1574
|
-
var HarnessDB = class {
|
|
1575
|
-
tasks;
|
|
1576
|
-
actions;
|
|
1577
|
-
stats;
|
|
1578
|
-
driver;
|
|
1579
|
-
config;
|
|
1580
|
-
constructor(driver, config) {
|
|
1581
|
-
this.driver = driver;
|
|
1582
|
-
this.config = config;
|
|
1583
|
-
this.tasks = new TaskRepository(driver);
|
|
1584
|
-
this.actions = new ActionRepository(driver);
|
|
1585
|
-
this.stats = new StatsRepository(driver);
|
|
1586
|
-
}
|
|
1587
|
-
// ─── Tasks (public facade — delegates to TaskRepository) ──────────────────
|
|
1588
|
-
async addTask(params) {
|
|
1589
|
-
const taskId = await this.tasks.add({
|
|
1590
|
-
slug: params.slug,
|
|
1591
|
-
title: params.title,
|
|
1592
|
-
description: params.description
|
|
1593
|
-
});
|
|
1594
|
-
if (params.acceptance?.length) {
|
|
1595
|
-
await this.tasks.addAcceptance(taskId, params.acceptance);
|
|
1596
|
-
}
|
|
1597
|
-
await this.regenerateCurrentMd();
|
|
1598
|
-
return await this.tasks.getById(taskId);
|
|
1599
|
-
}
|
|
1600
|
-
async getTasks(status, includeArchived = false) {
|
|
1601
|
-
return this.tasks.getAll(status, includeArchived);
|
|
1602
|
-
}
|
|
1603
|
-
async getTaskById(id) {
|
|
1604
|
-
return this.tasks.getById(id);
|
|
1605
|
-
}
|
|
1606
|
-
async getTaskBySlug(slug) {
|
|
1607
|
-
return this.tasks.getBySlug(slug);
|
|
1608
|
-
}
|
|
1609
|
-
async getTaskAcceptance(taskId) {
|
|
1610
|
-
return this.tasks.getAcceptance(taskId);
|
|
1611
|
-
}
|
|
1612
|
-
async updateTaskStatus(idOrSlug, status) {
|
|
1613
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1614
|
-
const task2 = typeof idOrSlug === "number" ? await this.tasks.getById(idOrSlug) : await this.tasks.getBySlug(idOrSlug);
|
|
1615
|
-
if (!task2) throw new Error(`Task not found: ${idOrSlug}`);
|
|
1616
|
-
if (status === "in_progress" && !task2.started_at) {
|
|
1617
|
-
await this.tasks.setStatus(task2.id, status, { started_at: now });
|
|
1618
|
-
} else if (status === "done") {
|
|
1619
|
-
await this.tasks.setStatus(task2.id, status, { completed_at: now });
|
|
1620
|
-
} else {
|
|
1621
|
-
await this.tasks.setStatus(task2.id, status);
|
|
1622
|
-
}
|
|
1623
|
-
await this.regenerateCurrentMd();
|
|
1624
|
-
return await this.tasks.getById(task2.id);
|
|
1625
|
-
}
|
|
1626
|
-
async claimTask(id, agent) {
|
|
1627
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1628
|
-
return this.driver.transaction(async (tx) => {
|
|
1629
|
-
const txTasks = new TaskRepository(tx);
|
|
1630
|
-
const changed = await txTasks.claim(id, agent, now);
|
|
1631
|
-
if (!changed) return null;
|
|
1632
|
-
const task2 = await txTasks.getById(id);
|
|
1633
|
-
if (!task2 || task2.status !== "in_progress" || task2.assigned_to !== agent) return null;
|
|
1634
|
-
await this.regenerateCurrentMd();
|
|
1635
|
-
return task2;
|
|
1636
|
-
});
|
|
1637
|
-
}
|
|
1638
|
-
async markAcceptanceMet(criterionId) {
|
|
1639
|
-
return this.tasks.markAcceptanceMet(criterionId);
|
|
1640
|
-
}
|
|
1641
|
-
async updateTask(id, params) {
|
|
1642
|
-
await this.tasks.update(id, params);
|
|
1643
|
-
await this.regenerateCurrentMd();
|
|
1644
|
-
return await this.tasks.getById(id);
|
|
1645
|
-
}
|
|
1646
|
-
async updateTaskAcceptance(taskId, criteria) {
|
|
1647
|
-
await this.tasks.replaceAcceptance(taskId, criteria);
|
|
1648
|
-
await this.regenerateCurrentMd();
|
|
1649
|
-
}
|
|
1650
|
-
async archiveTask(id) {
|
|
1651
|
-
await this.tasks.archive(id);
|
|
1652
|
-
await this.regenerateCurrentMd();
|
|
1653
|
-
return await this.tasks.getById(id);
|
|
1654
|
-
}
|
|
1655
|
-
async unarchiveTask(id) {
|
|
1656
|
-
await this.tasks.unarchive(id);
|
|
1657
|
-
await this.regenerateCurrentMd();
|
|
1658
|
-
return await this.tasks.getById(id);
|
|
1659
|
-
}
|
|
1660
|
-
async getArchivedTasks() {
|
|
1661
|
-
return this.tasks.getArchived();
|
|
1662
|
-
}
|
|
1663
|
-
async getStatusSummary() {
|
|
1664
|
-
return this.tasks.getStatusSummary();
|
|
1665
|
-
}
|
|
1666
|
-
// ─── Actions (public facade — delegates to ActionRepository) ──────────────
|
|
1667
|
-
async startAction(taskId, agent) {
|
|
1668
|
-
const id = randomUUID();
|
|
1669
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1670
|
-
await this.actions.create(id, taskId, agent, now);
|
|
1671
|
-
await this.regenerateCurrentMd();
|
|
1672
|
-
return await this.actions.getById(id);
|
|
1673
|
-
}
|
|
1674
|
-
async writeSection(actionId, sectionType, content) {
|
|
1675
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1676
|
-
await this.actions.addSection(actionId, sectionType, content, now);
|
|
1677
|
-
await this.regenerateCurrentMd();
|
|
1678
|
-
}
|
|
1679
|
-
async completeAction(actionId, summary) {
|
|
1680
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1681
|
-
await this.actions.complete(actionId, summary, now);
|
|
1682
|
-
await this.regenerateCurrentMd();
|
|
1683
|
-
return await this.actions.getById(actionId);
|
|
1684
|
-
}
|
|
1685
|
-
async closeOrphanedActions(taskId) {
|
|
1686
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1687
|
-
return this.actions.closeOrphaned(taskId, now);
|
|
1688
|
-
}
|
|
1689
|
-
async getAction(actionId) {
|
|
1690
|
-
return this.actions.getById(actionId);
|
|
1691
|
-
}
|
|
1692
|
-
async getActionsForTask(taskId) {
|
|
1693
|
-
return this.actions.getForTask(taskId);
|
|
1694
|
-
}
|
|
1695
|
-
async getActionSections(actionId) {
|
|
1696
|
-
return this.actions.getSections(actionId);
|
|
1697
|
-
}
|
|
1698
|
-
async recordFile(actionId, filePath, operation, notes) {
|
|
1699
|
-
return this.actions.addFile(actionId, filePath, operation, notes ?? null);
|
|
1700
|
-
}
|
|
1701
|
-
async recordTool(actionId, toolName, argsJson, resultSummary) {
|
|
1702
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1703
|
-
return this.actions.addTool(actionId, toolName, argsJson ?? null, resultSummary ?? null, now);
|
|
1704
|
-
}
|
|
1705
|
-
async getFilesForTask(taskId) {
|
|
1706
|
-
return this.actions.getFilesForTask(taskId);
|
|
1707
|
-
}
|
|
1708
|
-
async getTopTools(limit = 10) {
|
|
1709
|
-
return this.actions.getTopTools(limit);
|
|
1710
|
-
}
|
|
1711
|
-
// ─── current.md fallback ──────────────────────────────────────────────────
|
|
1712
|
-
async regenerateCurrentMd() {
|
|
1713
|
-
if (!this.config.storage.markdownFallback.enabled) return;
|
|
1714
|
-
const mdPath = resolve6(this.config.storage.markdownFallback.path);
|
|
1715
|
-
mkdirSync7(dirname4(mdPath), { recursive: true });
|
|
1716
|
-
const inProgress = await this.tasks.getAll("in_progress");
|
|
1717
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1718
|
-
let md = `<!-- AUTO-GENERATED by agent-harness-kit \u2014 DO NOT EDIT MANUALLY -->
|
|
1719
|
-
`;
|
|
1720
|
-
md += `<!-- Last updated: ${now} -->
|
|
1721
|
-
|
|
1722
|
-
`;
|
|
1723
|
-
md += `# Current Session
|
|
1724
|
-
|
|
1725
|
-
`;
|
|
1726
|
-
if (inProgress.length === 0) {
|
|
1727
|
-
md += `## No tasks in progress
|
|
1728
|
-
|
|
1729
|
-
`;
|
|
1730
|
-
const pending = await this.tasks.getAll("pending");
|
|
1731
|
-
if (pending.length > 0) {
|
|
1732
|
-
md += `### Next pending tasks
|
|
1733
|
-
`;
|
|
1734
|
-
for (const t of pending.slice(0, 5)) {
|
|
1735
|
-
md += `- **#${t.id}** ${t.title} (\`${t.slug}\`)
|
|
1736
|
-
`;
|
|
1737
|
-
}
|
|
1738
|
-
}
|
|
1739
|
-
} else {
|
|
1740
|
-
for (const task2 of inProgress) {
|
|
1741
|
-
md += `## Active Task
|
|
1742
|
-
`;
|
|
1743
|
-
md += `- **ID:** ${task2.id}
|
|
1744
|
-
`;
|
|
1745
|
-
md += `- **Slug:** ${task2.slug}
|
|
1746
|
-
`;
|
|
1747
|
-
md += `- **Status:** ${task2.status}
|
|
1748
|
-
`;
|
|
1749
|
-
md += `- **Started:** ${task2.started_at ?? "unknown"}
|
|
1750
|
-
|
|
1751
|
-
`;
|
|
1752
|
-
const taskActions = await this.actions.getForTask(task2.id);
|
|
1753
|
-
if (taskActions.length > 0) {
|
|
1754
|
-
md += `## Actions this session
|
|
1755
|
-
`;
|
|
1756
|
-
md += `| Agent | Status | Summary | Started |
|
|
1757
|
-
`;
|
|
1758
|
-
md += `|----------|-------------|----------------------------------|-------------|
|
|
1759
|
-
`;
|
|
1760
|
-
for (const a of taskActions) {
|
|
1761
|
-
const started = a.created_at.slice(11, 16);
|
|
1762
|
-
const summary = (a.summary ?? "").slice(0, 34).padEnd(34);
|
|
1763
|
-
md += `| ${a.agent.padEnd(8)} | ${a.status.padEnd(11)} | ${summary} | ${started} |
|
|
1764
|
-
`;
|
|
1765
|
-
}
|
|
1766
|
-
md += `
|
|
1767
|
-
`;
|
|
1768
|
-
}
|
|
1769
|
-
const acceptance = await this.tasks.getAcceptance(task2.id);
|
|
1770
|
-
if (acceptance.length > 0) {
|
|
1771
|
-
md += `## Acceptance Criteria
|
|
1772
|
-
`;
|
|
1773
|
-
for (const a of acceptance) {
|
|
1774
|
-
md += `- [${a.met ? "x" : " "}] ${a.criterion}
|
|
1775
|
-
`;
|
|
1776
|
-
}
|
|
1777
|
-
md += `
|
|
1778
|
-
`;
|
|
1779
|
-
}
|
|
1780
|
-
}
|
|
1781
|
-
}
|
|
1782
|
-
writeFileSync7(mdPath, md, "utf8");
|
|
1783
|
-
}
|
|
1784
|
-
// ─── Raw query escape hatch ───────────────────────────────────────────────
|
|
1785
|
-
async queryRaw(sql, ...params) {
|
|
1786
|
-
return this.driver.query(sql, params);
|
|
1787
|
-
}
|
|
1788
|
-
// ─── Export helpers ───────────────────────────────────────────────────────
|
|
1789
|
-
async exportJson() {
|
|
1790
|
-
return {
|
|
1791
|
-
tasks: await this.tasks.getAll(void 0, true),
|
|
1792
|
-
actions: await this.actions.getAll(),
|
|
1793
|
-
sections: await this.actions.getAllSections()
|
|
1794
|
-
};
|
|
1795
|
-
}
|
|
1796
|
-
async reconnect() {
|
|
1797
|
-
await this.driver.reconnect();
|
|
1798
|
-
}
|
|
1799
|
-
async close() {
|
|
1800
|
-
await this.driver.close();
|
|
1801
|
-
}
|
|
1802
|
-
// ─── feature_list.json sync ───────────────────────────────────────────────
|
|
1803
|
-
async syncFromFeatureList(seeds) {
|
|
1804
|
-
let added = 0;
|
|
1805
|
-
let skipped = 0;
|
|
1806
|
-
for (const t of seeds) {
|
|
1807
|
-
if (await this.tasks.getBySlug(t.slug)) {
|
|
1808
|
-
skipped++;
|
|
1809
|
-
continue;
|
|
1810
|
-
}
|
|
1811
|
-
await this.addTask(t);
|
|
1812
|
-
added++;
|
|
1813
|
-
}
|
|
1814
|
-
return { added, skipped };
|
|
1815
|
-
}
|
|
1816
|
-
async writeFeatureList(cwd2) {
|
|
1817
|
-
const allTasks = await this.tasks.getAll(void 0, true);
|
|
1818
|
-
const list = await Promise.all(
|
|
1819
|
-
allTasks.map(async (t) => ({
|
|
1820
|
-
slug: t.slug,
|
|
1821
|
-
title: t.title,
|
|
1822
|
-
description: t.description ?? void 0,
|
|
1823
|
-
acceptance: (await this.tasks.getAcceptance(t.id)).map((a) => a.criterion),
|
|
1824
|
-
status: t.status
|
|
1825
|
-
}))
|
|
1826
|
-
);
|
|
1827
|
-
const path = join8(resolve6(cwd2), this.config.storage.dir, "feature_list.json");
|
|
1828
|
-
mkdirSync7(dirname4(path), { recursive: true });
|
|
1829
|
-
writeFileSync7(path, JSON.stringify(list, null, 2) + "\n", "utf8");
|
|
1830
|
-
}
|
|
1831
|
-
};
|
|
1832
|
-
async function openDB(config, cwd2) {
|
|
1833
|
-
const dbConfig = config.database;
|
|
1834
|
-
let driver;
|
|
1835
|
-
if (dbConfig.type === "postgres") {
|
|
1836
|
-
const { PostgresDriver } = await import("./postgres-IOQE32DM.js");
|
|
1837
|
-
driver = new PostgresDriver(dbConfig);
|
|
1838
|
-
} else if (dbConfig.type === "mysql") {
|
|
1839
|
-
const { MySQLDriver } = await import("./mysql-THKQOXIS.js");
|
|
1840
|
-
driver = new MySQLDriver(dbConfig);
|
|
1841
|
-
} else {
|
|
1842
|
-
const { SQLiteDriver } = await import("./sqlite-KWYK4IJW.js");
|
|
1843
|
-
if (dbConfig.type !== "sqlite") {
|
|
1844
|
-
throw new Error("Invalid database type");
|
|
1845
|
-
}
|
|
1846
|
-
driver = new SQLiteDriver(resolve6(cwd2, dbConfig.path));
|
|
1847
|
-
}
|
|
1848
|
-
await driver.ensureSchema();
|
|
1849
|
-
return new HarnessDB(driver, config);
|
|
1850
|
-
}
|
|
1851
|
-
|
|
1852
1316
|
// src/commands/dashboard.ts
|
|
1853
|
-
var __dirname3 =
|
|
1317
|
+
var __dirname3 = dirname4(fileURLToPath3(import.meta.url));
|
|
1854
1318
|
async function runDashboard(cwd2, opts) {
|
|
1855
1319
|
const config = await loadConfig(cwd2);
|
|
1856
1320
|
const db = await openDB(config, cwd2);
|
|
1857
|
-
const dbPath = config.database.type === "sqlite" ?
|
|
1321
|
+
const dbPath = config.database.type === "sqlite" ? resolve6(cwd2, config.database.path) : null;
|
|
1858
1322
|
const staticPath = join9(__dirname3, "dashboard-dist");
|
|
1859
1323
|
const { url } = await startDashboardServer(db, dbPath, staticPath, opts.port);
|
|
1860
1324
|
console.log(pc2.green(`\u2713`) + ` Dashboard running at ${pc2.bold(pc2.cyan(url))}`);
|
|
@@ -1875,29 +1339,37 @@ async function runDashboard(cwd2, opts) {
|
|
|
1875
1339
|
import pc3 from "picocolors";
|
|
1876
1340
|
|
|
1877
1341
|
// src/core/doctor.ts
|
|
1878
|
-
import { existsSync as
|
|
1879
|
-
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";
|
|
1880
1345
|
import { fileURLToPath as fileURLToPath5 } from "url";
|
|
1881
1346
|
|
|
1882
1347
|
// src/core/package-data.ts
|
|
1883
|
-
import { existsSync as
|
|
1348
|
+
import { existsSync as existsSync8 } from "fs";
|
|
1884
1349
|
import { createRequire } from "module";
|
|
1885
|
-
import { dirname as
|
|
1350
|
+
import { dirname as dirname5, join as join10 } from "path";
|
|
1886
1351
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
1887
1352
|
var require2 = createRequire(import.meta.url);
|
|
1888
|
-
var here =
|
|
1353
|
+
var here = dirname5(fileURLToPath4(import.meta.url));
|
|
1889
1354
|
var candidates = [join10(here, "..", "..", "package.json"), join10(here, "..", "package.json")];
|
|
1890
|
-
var pkgPath = candidates.find((p8) =>
|
|
1355
|
+
var pkgPath = candidates.find((p8) => existsSync8(p8)) ?? candidates[0];
|
|
1891
1356
|
var pkg = require2(pkgPath);
|
|
1892
1357
|
|
|
1893
1358
|
// src/core/doctor.ts
|
|
1894
1359
|
var REGISTRY_URL = `https://registry.npmjs.org/${pkg.name}/latest`;
|
|
1895
1360
|
var TIMEOUT_MS = 2e3;
|
|
1361
|
+
var LIB_VERSION_CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
1896
1362
|
var AGENT_NAMES = ["lead", "explorer", "consultant", "builder", "reviewer"];
|
|
1897
1363
|
var SKILL_NAMES = ["ahk-ask", "ahk-consultant", "ahk-triage", "ahk-review"];
|
|
1898
|
-
var __dirname4 =
|
|
1364
|
+
var __dirname4 = dirname6(fileURLToPath5(import.meta.url));
|
|
1365
|
+
var libVersionCache = null;
|
|
1899
1366
|
async function checkLibVersion() {
|
|
1900
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
|
+
}
|
|
1901
1373
|
try {
|
|
1902
1374
|
const controller = new AbortController();
|
|
1903
1375
|
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
@@ -1906,9 +1378,13 @@ async function checkLibVersion() {
|
|
|
1906
1378
|
const data = await res.json();
|
|
1907
1379
|
const latest = data.version;
|
|
1908
1380
|
const outdated = isNewer(latest, current);
|
|
1909
|
-
|
|
1381
|
+
const status = { current, latest, outdated };
|
|
1382
|
+
libVersionCache = { status, fetchedAt: Date.now() };
|
|
1383
|
+
return status;
|
|
1910
1384
|
} catch {
|
|
1911
|
-
|
|
1385
|
+
const status = { current, latest: null, outdated: false };
|
|
1386
|
+
libVersionCache = { status, fetchedAt: Date.now() };
|
|
1387
|
+
return status;
|
|
1912
1388
|
}
|
|
1913
1389
|
}
|
|
1914
1390
|
function isNewer(latest, current) {
|
|
@@ -1962,15 +1438,14 @@ function generateExpectedAgentContent(agentName, provider, vars) {
|
|
|
1962
1438
|
};
|
|
1963
1439
|
return tomlFns[agentName]();
|
|
1964
1440
|
}
|
|
1965
|
-
function
|
|
1966
|
-
const { agentsDir, ext } = getProviderAgentInfo(provider);
|
|
1441
|
+
function checkAgentFilesAtRoot(agentsRoot, ext, provider, projectName, allowedPaths, writablePaths, models) {
|
|
1967
1442
|
return AGENT_NAMES.map((name) => {
|
|
1968
|
-
const filePath = join11(
|
|
1969
|
-
if (!
|
|
1443
|
+
const filePath = join11(agentsRoot, `${name}${ext}`);
|
|
1444
|
+
if (!existsSync9(filePath)) {
|
|
1970
1445
|
return { name, status: "missing" };
|
|
1971
1446
|
}
|
|
1972
1447
|
try {
|
|
1973
|
-
const live =
|
|
1448
|
+
const live = readFileSync7(filePath, "utf8");
|
|
1974
1449
|
const expected = generateExpectedAgentContent(name, provider, {
|
|
1975
1450
|
projectName,
|
|
1976
1451
|
allowedPaths,
|
|
@@ -1983,6 +1458,18 @@ function checkAgentFiles(cwd2, provider, projectName, allowedPaths, writablePath
|
|
|
1983
1458
|
}
|
|
1984
1459
|
});
|
|
1985
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
|
+
}
|
|
1986
1473
|
function getProviderSkillsDir(provider) {
|
|
1987
1474
|
switch (provider) {
|
|
1988
1475
|
case "claude-code":
|
|
@@ -1995,24 +1482,76 @@ function getProviderSkillsDir(provider) {
|
|
|
1995
1482
|
return ".claude/skills";
|
|
1996
1483
|
}
|
|
1997
1484
|
}
|
|
1998
|
-
function
|
|
1999
|
-
const skillsDir = getProviderSkillsDir(provider);
|
|
1485
|
+
function checkSkillsAtRoot(skillsRoot) {
|
|
2000
1486
|
const skillSourceBase = join11(__dirname4, "skills");
|
|
2001
1487
|
return SKILL_NAMES.map((name) => {
|
|
2002
|
-
const livePath = join11(
|
|
1488
|
+
const livePath = join11(skillsRoot, name, "SKILL.md");
|
|
2003
1489
|
const sourcePath = join11(skillSourceBase, name, "SKILL.md");
|
|
2004
|
-
if (!
|
|
1490
|
+
if (!existsSync9(livePath)) {
|
|
2005
1491
|
return { name, status: "missing" };
|
|
2006
1492
|
}
|
|
2007
1493
|
try {
|
|
2008
|
-
const live =
|
|
2009
|
-
const source =
|
|
1494
|
+
const live = readFileSync7(livePath, "utf8");
|
|
1495
|
+
const source = readFileSync7(sourcePath, "utf8");
|
|
2010
1496
|
return { name, status: live === source ? "ok" : "outdated" };
|
|
2011
1497
|
} catch {
|
|
2012
1498
|
return { name, status: "outdated" };
|
|
2013
1499
|
}
|
|
2014
1500
|
});
|
|
2015
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
|
+
}
|
|
2016
1555
|
async function getDoctorStatus(cwd2) {
|
|
2017
1556
|
const lib = await checkLibVersion();
|
|
2018
1557
|
let config;
|
|
@@ -2126,7 +1665,7 @@ async function runDoctor(cwd2) {
|
|
|
2126
1665
|
}
|
|
2127
1666
|
|
|
2128
1667
|
// src/commands/export.ts
|
|
2129
|
-
import { writeFileSync as
|
|
1668
|
+
import { writeFileSync as writeFileSync7 } from "fs";
|
|
2130
1669
|
import pc4 from "picocolors";
|
|
2131
1670
|
async function runExport(cwd2, opts) {
|
|
2132
1671
|
if (!opts.sql && !opts.json) {
|
|
@@ -2140,7 +1679,7 @@ async function runExport(cwd2, opts) {
|
|
|
2140
1679
|
const data = await db.exportJson();
|
|
2141
1680
|
const out = JSON.stringify(data, null, 2) + "\n";
|
|
2142
1681
|
if (opts.output) {
|
|
2143
|
-
|
|
1682
|
+
writeFileSync7(opts.output, out, "utf8");
|
|
2144
1683
|
console.log(pc4.green(`\u2713 Exported JSON \u2192 ${opts.output}`));
|
|
2145
1684
|
} else {
|
|
2146
1685
|
process.stdout.write(out);
|
|
@@ -2157,8 +1696,8 @@ async function runExport(cwd2, opts) {
|
|
|
2157
1696
|
|
|
2158
1697
|
// src/commands/health.ts
|
|
2159
1698
|
import { spawnSync } from "child_process";
|
|
2160
|
-
import { existsSync as
|
|
2161
|
-
import { join as join12, resolve as
|
|
1699
|
+
import { existsSync as existsSync10 } from "fs";
|
|
1700
|
+
import { join as join12, resolve as resolve7 } from "path";
|
|
2162
1701
|
import pc5 from "picocolors";
|
|
2163
1702
|
function checkLine(label, ok3, message, indent = 0) {
|
|
2164
1703
|
const prefix = label ? pc5.cyan(`[${label}] `) : " ".repeat(indent);
|
|
@@ -2176,8 +1715,8 @@ async function runHealth(cwd2) {
|
|
|
2176
1715
|
let allOk = true;
|
|
2177
1716
|
let dbOk;
|
|
2178
1717
|
if (config.database.type === "sqlite") {
|
|
2179
|
-
const dbPath =
|
|
2180
|
-
dbOk =
|
|
1718
|
+
const dbPath = resolve7(cwd2, config.database.path);
|
|
1719
|
+
dbOk = existsSync10(dbPath);
|
|
2181
1720
|
checkLine("checking DB", dbOk, `${config.database.path} reachable`);
|
|
2182
1721
|
} else {
|
|
2183
1722
|
dbOk = true;
|
|
@@ -2191,7 +1730,7 @@ async function runHealth(cwd2) {
|
|
|
2191
1730
|
for (let i = 0; i < agentNames.length; i++) {
|
|
2192
1731
|
const name = agentNames[i];
|
|
2193
1732
|
const agentPath = join12(cwd2, agentsDir, `${name}${providerFiles.agentExtension}`);
|
|
2194
|
-
const ok3 =
|
|
1733
|
+
const ok3 = existsSync10(agentPath);
|
|
2195
1734
|
checkLine(
|
|
2196
1735
|
i === 0 ? "checking agents" : null,
|
|
2197
1736
|
ok3,
|
|
@@ -2202,8 +1741,8 @@ async function runHealth(cwd2) {
|
|
|
2202
1741
|
}
|
|
2203
1742
|
if (config.tools.mcp.enabled) {
|
|
2204
1743
|
const mcpFile = providerFiles.mcpFile;
|
|
2205
|
-
const mcpPath =
|
|
2206
|
-
const mcpOk =
|
|
1744
|
+
const mcpPath = resolve7(cwd2, mcpFile);
|
|
1745
|
+
const mcpOk = existsSync10(mcpPath);
|
|
2207
1746
|
checkLine("checking MCP", mcpOk, `${mcpFile} valid`);
|
|
2208
1747
|
if (!mcpOk) allOk = false;
|
|
2209
1748
|
}
|
|
@@ -2212,8 +1751,8 @@ async function runHealth(cwd2) {
|
|
|
2212
1751
|
console.error(pc5.red("\u2717 Harness checks failed \u2014 fix the above before running health.sh"));
|
|
2213
1752
|
process.exit(1);
|
|
2214
1753
|
}
|
|
2215
|
-
const scriptPath =
|
|
2216
|
-
if (!
|
|
1754
|
+
const scriptPath = resolve7(cwd2, config.health.scriptPath);
|
|
1755
|
+
if (!existsSync10(scriptPath)) {
|
|
2217
1756
|
console.error(pc5.red(`\u2717 health.sh not found: ${scriptPath}`));
|
|
2218
1757
|
console.error(" Run ahk init first.");
|
|
2219
1758
|
process.exit(1);
|
|
@@ -2249,11 +1788,56 @@ function getProviderHealthFiles(provider) {
|
|
|
2249
1788
|
}
|
|
2250
1789
|
|
|
2251
1790
|
// src/commands/init.ts
|
|
2252
|
-
import { mkdirSync as
|
|
1791
|
+
import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync8 } from "fs";
|
|
2253
1792
|
import { join as join14 } from "path";
|
|
2254
1793
|
import * as p3 from "@clack/prompts";
|
|
2255
1794
|
import pc7 from "picocolors";
|
|
2256
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
|
+
|
|
2257
1841
|
// src/schema/init.ts
|
|
2258
1842
|
import * as v from "valibot";
|
|
2259
1843
|
var initNameSchema = v.pipe(
|
|
@@ -2305,14 +1889,15 @@ var cliFormWithRetry = async (formFn, schema) => {
|
|
|
2305
1889
|
};
|
|
2306
1890
|
|
|
2307
1891
|
// src/commands/init-helpers.ts
|
|
2308
|
-
import {
|
|
1892
|
+
import { randomUUID } from "crypto";
|
|
1893
|
+
import { existsSync as existsSync11, readFileSync as readFileSync8 } from "fs";
|
|
2309
1894
|
import { join as join13 } from "path";
|
|
2310
1895
|
import pc6 from "picocolors";
|
|
2311
1896
|
function readProjectNameFromPackageJson(cwd2) {
|
|
2312
1897
|
try {
|
|
2313
1898
|
const pkgPath2 = join13(cwd2, "package.json");
|
|
2314
|
-
if (!
|
|
2315
|
-
const content =
|
|
1899
|
+
if (!existsSync11(pkgPath2)) return null;
|
|
1900
|
+
const content = readFileSync8(pkgPath2, "utf8");
|
|
2316
1901
|
const pkg2 = JSON.parse(content);
|
|
2317
1902
|
const name = pkg2?.name;
|
|
2318
1903
|
if (typeof name === "string" && name.trim()) return name.trim();
|
|
@@ -2323,10 +1908,10 @@ function readProjectNameFromPackageJson(cwd2) {
|
|
|
2323
1908
|
}
|
|
2324
1909
|
function detectConfigExtension(cwd2) {
|
|
2325
1910
|
try {
|
|
2326
|
-
if (
|
|
1911
|
+
if (existsSync11(join13(cwd2, "tsconfig.json"))) return "ts";
|
|
2327
1912
|
const pkgPath2 = join13(cwd2, "package.json");
|
|
2328
|
-
if (!
|
|
2329
|
-
const pkg2 = JSON.parse(
|
|
1913
|
+
if (!existsSync11(pkgPath2)) return "mjs";
|
|
1914
|
+
const pkg2 = JSON.parse(readFileSync8(pkgPath2, "utf8"));
|
|
2330
1915
|
if (pkg2?.type === "module") return "mjs";
|
|
2331
1916
|
} catch {
|
|
2332
1917
|
}
|
|
@@ -2369,7 +1954,9 @@ function applyConfigDefaults(params) {
|
|
|
2369
1954
|
blockers: true,
|
|
2370
1955
|
nextSteps: false
|
|
2371
1956
|
},
|
|
2372
|
-
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()
|
|
2373
1960
|
},
|
|
2374
1961
|
health: {
|
|
2375
1962
|
scriptPath: "./health.sh",
|
|
@@ -2553,6 +2140,24 @@ async function runInit(cwd2, flags) {
|
|
|
2553
2140
|
return val;
|
|
2554
2141
|
}, initDocsSchema);
|
|
2555
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
|
+
}
|
|
2556
2161
|
let tasksAdapter;
|
|
2557
2162
|
if (flags.tasks && ["local", "jira", "linear"].includes(flags.tasks)) {
|
|
2558
2163
|
tasksAdapter = flags.tasks;
|
|
@@ -2607,10 +2212,19 @@ async function runInit(cwd2, flags) {
|
|
|
2607
2212
|
firstTask = { title: taskTitle, description: taskDesc, acceptance };
|
|
2608
2213
|
}
|
|
2609
2214
|
let configExt = "ts";
|
|
2215
|
+
let globalSyncResult = null;
|
|
2610
2216
|
const spinner6 = p3.spinner();
|
|
2611
2217
|
spinner6.start("Scaffolding...");
|
|
2612
2218
|
try {
|
|
2613
|
-
const config = applyConfigDefaults({
|
|
2219
|
+
const config = applyConfigDefaults({
|
|
2220
|
+
name,
|
|
2221
|
+
description,
|
|
2222
|
+
provider,
|
|
2223
|
+
docsPath,
|
|
2224
|
+
tasksAdapter,
|
|
2225
|
+
models: modelOverrides,
|
|
2226
|
+
scope: storageScope
|
|
2227
|
+
});
|
|
2614
2228
|
const materializer = getMaterializer(provider);
|
|
2615
2229
|
const installDir = cwd2;
|
|
2616
2230
|
configExt = detectConfigExtension(cwd2);
|
|
@@ -2623,12 +2237,18 @@ async function runInit(cwd2, flags) {
|
|
|
2623
2237
|
docsPath,
|
|
2624
2238
|
tasksAdapter,
|
|
2625
2239
|
port: config.tools.mcp.port,
|
|
2626
|
-
models: modelOverrides
|
|
2240
|
+
models: modelOverrides,
|
|
2241
|
+
scope: config.storage.scope,
|
|
2242
|
+
projectId: config.storage.projectId
|
|
2627
2243
|
});
|
|
2628
|
-
|
|
2629
|
-
|
|
2244
|
+
writeFileSync8(join14(installDir, configFileName), configContent, "utf8");
|
|
2245
|
+
mkdirSync7(join14(installDir, config.storage.dir), { recursive: true });
|
|
2630
2246
|
const db = await openDB(config, installDir);
|
|
2247
|
+
await db.writeStorageState(installDir);
|
|
2631
2248
|
await materializer.scaffold(config, { cwd: installDir, firstTask });
|
|
2249
|
+
if (config.storage.scope === "global") {
|
|
2250
|
+
globalSyncResult = await syncGlobalAgentsAndSkills(config, provider);
|
|
2251
|
+
}
|
|
2632
2252
|
if (firstTask) {
|
|
2633
2253
|
const slug = slugify(firstTask.title);
|
|
2634
2254
|
await db.addTask({
|
|
@@ -2652,14 +2272,28 @@ async function runInit(cwd2, flags) {
|
|
|
2652
2272
|
console.log(pc7.green(`\u2713 agent-harness-kit.config.${configExt}`));
|
|
2653
2273
|
console.log(pc7.green("\u2713 AGENTS.md"));
|
|
2654
2274
|
console.log(pc7.green("\u2713 health.sh"));
|
|
2655
|
-
console.log(pc7.green("\u2713 .harness/harness.db"));
|
|
2656
|
-
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"));
|
|
2657
2278
|
console.log(pc7.green(`\u2713 ${agentsDir}lead.md`));
|
|
2658
2279
|
console.log(pc7.green(`\u2713 ${agentsDir}explorer.md`));
|
|
2659
2280
|
console.log(pc7.green(`\u2713 ${agentsDir}builder.md`));
|
|
2660
2281
|
console.log(pc7.green(`\u2713 ${agentsDir}reviewer.md`));
|
|
2661
2282
|
console.log(pc7.green(`\u2713 ${mcpFile}`));
|
|
2662
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
|
+
}
|
|
2663
2297
|
console.log("");
|
|
2664
2298
|
console.log(pc7.cyan("\u2192") + ` Edit ${pc7.cyan("health.sh")} with your project checks`);
|
|
2665
2299
|
console.log(pc7.cyan("\u2192") + ` ${pc7.cyan("ahk task add")} to queue work for agents`);
|
|
@@ -2716,17 +2350,251 @@ async function runMigrate(cwd2, opts) {
|
|
|
2716
2350
|
}
|
|
2717
2351
|
}
|
|
2718
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
|
+
|
|
2719
2587
|
// src/commands/reset.ts
|
|
2720
|
-
import { existsSync as
|
|
2721
|
-
import { join as
|
|
2588
|
+
import { existsSync as existsSync13, readdirSync, rmSync as rmSync2 } from "fs";
|
|
2589
|
+
import { join as join16, resolve as resolve9 } from "path";
|
|
2722
2590
|
import * as p5 from "@clack/prompts";
|
|
2723
|
-
import
|
|
2591
|
+
import pc10 from "picocolors";
|
|
2724
2592
|
var AGENT_MD_FILES = ["lead", "explorer", "consultant", "builder", "reviewer"];
|
|
2725
2593
|
async function resetAgentMds(cwd2, provider) {
|
|
2726
2594
|
const agentDir = provider === "claude-code" ? ".claude/agents" : ".opencode/agents";
|
|
2727
2595
|
const agentDirPath = resolve9(cwd2, agentDir);
|
|
2728
|
-
if (!
|
|
2729
|
-
console.log(
|
|
2596
|
+
if (!existsSync13(agentDirPath)) {
|
|
2597
|
+
console.log(pc10.yellow(` Skipping agent files \u2014 directory not found: ${agentDirPath}`));
|
|
2730
2598
|
return;
|
|
2731
2599
|
}
|
|
2732
2600
|
const existingFiles = [];
|
|
@@ -2738,11 +2606,11 @@ async function resetAgentMds(cwd2, provider) {
|
|
|
2738
2606
|
}
|
|
2739
2607
|
}
|
|
2740
2608
|
} catch {
|
|
2741
|
-
console.log(
|
|
2609
|
+
console.log(pc10.yellow(` Skipping agent files \u2014 ${agentDirPath} is not readable`));
|
|
2742
2610
|
return;
|
|
2743
2611
|
}
|
|
2744
2612
|
if (existingFiles.length === 0) {
|
|
2745
|
-
console.log(
|
|
2613
|
+
console.log(pc10.yellow(` No agent MD files found in ${agentDir}/`));
|
|
2746
2614
|
return;
|
|
2747
2615
|
}
|
|
2748
2616
|
for (const file of existingFiles) {
|
|
@@ -2751,19 +2619,19 @@ async function resetAgentMds(cwd2, provider) {
|
|
|
2751
2619
|
initialValue: true
|
|
2752
2620
|
});
|
|
2753
2621
|
if (p5.isCancel(confirm3)) {
|
|
2754
|
-
console.log(
|
|
2622
|
+
console.log(pc10.red(" Cancelled by user."));
|
|
2755
2623
|
return;
|
|
2756
2624
|
}
|
|
2757
2625
|
if (confirm3) {
|
|
2758
2626
|
try {
|
|
2759
|
-
const filePath =
|
|
2760
|
-
|
|
2761
|
-
console.log(
|
|
2627
|
+
const filePath = join16(agentDirPath, file);
|
|
2628
|
+
rmSync2(filePath, { force: true });
|
|
2629
|
+
console.log(pc10.green(` Removed ${file}`));
|
|
2762
2630
|
} catch {
|
|
2763
|
-
console.error(
|
|
2631
|
+
console.error(pc10.red(` Failed to remove ${file}`));
|
|
2764
2632
|
}
|
|
2765
2633
|
} else {
|
|
2766
|
-
console.log(
|
|
2634
|
+
console.log(pc10.cyan(` Skipped ${file}`));
|
|
2767
2635
|
}
|
|
2768
2636
|
}
|
|
2769
2637
|
}
|
|
@@ -2772,7 +2640,7 @@ async function runReset(cwd2, opts) {
|
|
|
2772
2640
|
try {
|
|
2773
2641
|
config = await loadConfig(cwd2);
|
|
2774
2642
|
} catch {
|
|
2775
|
-
console.error(
|
|
2643
|
+
console.error(pc10.red("\u2717 No agent-harness-kit.config found. Run: ahk init"));
|
|
2776
2644
|
process.exit(1);
|
|
2777
2645
|
}
|
|
2778
2646
|
const storageDir = config.storage.dir || ".harness";
|
|
@@ -2781,12 +2649,12 @@ async function runReset(cwd2, opts) {
|
|
|
2781
2649
|
let resetDb = false;
|
|
2782
2650
|
let resetFeatureList = false;
|
|
2783
2651
|
let resetAgentMdsFlag = false;
|
|
2784
|
-
if (dbPath &&
|
|
2652
|
+
if (dbPath && existsSync13(dbPath)) {
|
|
2785
2653
|
if (opts.force) {
|
|
2786
2654
|
resetDb = true;
|
|
2787
2655
|
} else {
|
|
2788
2656
|
if (config.database.type !== "sqlite") {
|
|
2789
|
-
console.log(
|
|
2657
|
+
console.log(pc10.yellow(` Skipping DB reset \u2014 database type "${config.database.type}" is not managed by this command.`));
|
|
2790
2658
|
resetDb = false;
|
|
2791
2659
|
} else {
|
|
2792
2660
|
const confirm3 = await p5.confirm({
|
|
@@ -2794,16 +2662,16 @@ async function runReset(cwd2, opts) {
|
|
|
2794
2662
|
initialValue: true
|
|
2795
2663
|
});
|
|
2796
2664
|
if (p5.isCancel(confirm3)) {
|
|
2797
|
-
console.log(
|
|
2665
|
+
console.log(pc10.red(" Cancelled by user."));
|
|
2798
2666
|
return;
|
|
2799
2667
|
}
|
|
2800
2668
|
resetDb = confirm3;
|
|
2801
2669
|
}
|
|
2802
2670
|
}
|
|
2803
2671
|
} else if (!dbPath) {
|
|
2804
|
-
console.log(
|
|
2672
|
+
console.log(pc10.dim(` Skipping DB reset \u2014 remote ${config.database.type} database is not managed by this command.`));
|
|
2805
2673
|
}
|
|
2806
|
-
if (
|
|
2674
|
+
if (existsSync13(featureListPath)) {
|
|
2807
2675
|
if (opts.force) {
|
|
2808
2676
|
resetFeatureList = true;
|
|
2809
2677
|
} else {
|
|
@@ -2812,7 +2680,7 @@ async function runReset(cwd2, opts) {
|
|
|
2812
2680
|
initialValue: true
|
|
2813
2681
|
});
|
|
2814
2682
|
if (p5.isCancel(confirm3)) {
|
|
2815
|
-
console.log(
|
|
2683
|
+
console.log(pc10.red(" Cancelled by user."));
|
|
2816
2684
|
return;
|
|
2817
2685
|
}
|
|
2818
2686
|
resetFeatureList = confirm3;
|
|
@@ -2823,20 +2691,20 @@ async function runReset(cwd2, opts) {
|
|
|
2823
2691
|
}
|
|
2824
2692
|
if (resetDb && dbPath) {
|
|
2825
2693
|
try {
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
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}`));
|
|
2830
2698
|
} catch {
|
|
2831
|
-
console.error(
|
|
2699
|
+
console.error(pc10.red(` \u2717 Failed to remove ${dbPath}`));
|
|
2832
2700
|
}
|
|
2833
2701
|
}
|
|
2834
2702
|
if (resetFeatureList) {
|
|
2835
2703
|
try {
|
|
2836
|
-
|
|
2837
|
-
console.log(
|
|
2704
|
+
rmSync2(featureListPath, { force: true });
|
|
2705
|
+
console.log(pc10.green(` \u2713 Removed ${storageDir}/feature_list.json`));
|
|
2838
2706
|
} catch {
|
|
2839
|
-
console.error(
|
|
2707
|
+
console.error(pc10.red(` \u2717 Failed to remove ${featureListPath}`));
|
|
2840
2708
|
}
|
|
2841
2709
|
}
|
|
2842
2710
|
if (resetAgentMdsFlag) {
|
|
@@ -2844,16 +2712,16 @@ async function runReset(cwd2, opts) {
|
|
|
2844
2712
|
await resetAgentMds(cwd2, opts.provider || "claude-code");
|
|
2845
2713
|
}
|
|
2846
2714
|
if (!resetDb && !resetFeatureList && !resetAgentMdsFlag) {
|
|
2847
|
-
console.log(
|
|
2715
|
+
console.log(pc10.yellow(" Nothing to reset (all items missing or skipped)."));
|
|
2848
2716
|
return;
|
|
2849
2717
|
}
|
|
2850
2718
|
console.log("");
|
|
2851
|
-
console.log(
|
|
2719
|
+
console.log(pc10.green('\u2713 Reset complete. Run "ahk init" to scaffold a fresh harness.'));
|
|
2852
2720
|
}
|
|
2853
2721
|
|
|
2854
2722
|
// src/core/mcp-server.ts
|
|
2855
|
-
import { existsSync as
|
|
2856
|
-
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";
|
|
2857
2725
|
import { Server } from "@modelcontextprotocol/sdk/server";
|
|
2858
2726
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
2859
2727
|
import {
|
|
@@ -2862,8 +2730,8 @@ import {
|
|
|
2862
2730
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
2863
2731
|
|
|
2864
2732
|
// src/core/permissions-check.ts
|
|
2865
|
-
import { existsSync as
|
|
2866
|
-
import { join as
|
|
2733
|
+
import { existsSync as existsSync14, readFileSync as readFileSync9 } from "fs";
|
|
2734
|
+
import { join as join17 } from "path";
|
|
2867
2735
|
var CANONICAL = {
|
|
2868
2736
|
lead: [...MCP_CLAUDE_PERMISSIONS_LEAD],
|
|
2869
2737
|
explorer: [...MCP_CLAUDE_PERMISSIONS_EXPLORER],
|
|
@@ -2886,14 +2754,14 @@ function checkPermissionsSync(cwd2, config) {
|
|
|
2886
2754
|
const agents = {};
|
|
2887
2755
|
let in_sync = true;
|
|
2888
2756
|
for (const agent of ["lead", "explorer", "consultant", "builder", "reviewer"]) {
|
|
2889
|
-
const filePath =
|
|
2890
|
-
if (!
|
|
2757
|
+
const filePath = join17(cwd2, ".claude", "agents", `${agent}.md`);
|
|
2758
|
+
if (!existsSync14(filePath)) {
|
|
2891
2759
|
const missing2 = CANONICAL[agent];
|
|
2892
2760
|
agents[agent] = { ok: false, missing: missing2, extra: [] };
|
|
2893
2761
|
in_sync = false;
|
|
2894
2762
|
continue;
|
|
2895
2763
|
}
|
|
2896
|
-
const content =
|
|
2764
|
+
const content = readFileSync9(filePath, "utf-8");
|
|
2897
2765
|
const installed = parseToolsFromFrontmatter(content);
|
|
2898
2766
|
const canonical = CANONICAL[agent];
|
|
2899
2767
|
const missing = canonical.filter((t) => !installed.includes(t));
|
|
@@ -3313,19 +3181,19 @@ async function dispatch(name, args, db, docsPath, cwd2, config) {
|
|
|
3313
3181
|
return ok2(JSON.stringify(result, null, 2));
|
|
3314
3182
|
}
|
|
3315
3183
|
case "deps.snapshot": {
|
|
3316
|
-
const pkgPath2 =
|
|
3317
|
-
if (!
|
|
3184
|
+
const pkgPath2 = join18(cwd2, "package.json");
|
|
3185
|
+
if (!existsSync15(pkgPath2)) {
|
|
3318
3186
|
return ok2("package.json not found in project root", true);
|
|
3319
3187
|
}
|
|
3320
|
-
const pkg2 = JSON.parse(
|
|
3188
|
+
const pkg2 = JSON.parse(readFileSync10(pkgPath2, "utf8"));
|
|
3321
3189
|
const snapshot = {
|
|
3322
3190
|
capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3323
3191
|
dependencies: pkg2.dependencies ?? {},
|
|
3324
3192
|
devDependencies: pkg2.devDependencies ?? {}
|
|
3325
3193
|
};
|
|
3326
|
-
const harnessDir =
|
|
3194
|
+
const harnessDir = join18(cwd2, ".harness");
|
|
3327
3195
|
mkdirSync9(harnessDir, { recursive: true });
|
|
3328
|
-
writeFileSync10(
|
|
3196
|
+
writeFileSync10(join18(harnessDir, "deps-lock.json"), JSON.stringify(snapshot, null, 2), "utf8");
|
|
3329
3197
|
return ok2(
|
|
3330
3198
|
JSON.stringify({
|
|
3331
3199
|
message: "Snapshot saved to .harness/deps-lock.json",
|
|
@@ -3334,12 +3202,12 @@ async function dispatch(name, args, db, docsPath, cwd2, config) {
|
|
|
3334
3202
|
);
|
|
3335
3203
|
}
|
|
3336
3204
|
case "deps.check": {
|
|
3337
|
-
const pkgPath2 =
|
|
3338
|
-
const lockPath =
|
|
3339
|
-
if (!
|
|
3205
|
+
const pkgPath2 = join18(cwd2, "package.json");
|
|
3206
|
+
const lockPath = join18(cwd2, ".harness", "deps-lock.json");
|
|
3207
|
+
if (!existsSync15(pkgPath2)) {
|
|
3340
3208
|
return ok2("package.json not found in project root", true);
|
|
3341
3209
|
}
|
|
3342
|
-
if (!
|
|
3210
|
+
if (!existsSync15(lockPath)) {
|
|
3343
3211
|
return ok2(
|
|
3344
3212
|
JSON.stringify({
|
|
3345
3213
|
status: "no-snapshot",
|
|
@@ -3347,8 +3215,8 @@ async function dispatch(name, args, db, docsPath, cwd2, config) {
|
|
|
3347
3215
|
})
|
|
3348
3216
|
);
|
|
3349
3217
|
}
|
|
3350
|
-
const pkg2 = JSON.parse(
|
|
3351
|
-
const lock = JSON.parse(
|
|
3218
|
+
const pkg2 = JSON.parse(readFileSync10(pkgPath2, "utf8"));
|
|
3219
|
+
const lock = JSON.parse(readFileSync10(lockPath, "utf8"));
|
|
3352
3220
|
const current = { ...pkg2.dependencies ?? {}, ...pkg2.devDependencies ?? {} };
|
|
3353
3221
|
const previous = { ...lock.dependencies ?? {}, ...lock.devDependencies ?? {} };
|
|
3354
3222
|
const added = [];
|
|
@@ -3415,7 +3283,7 @@ function searchDocs(docsPath, query, maxResults = 10) {
|
|
|
3415
3283
|
for (const file of files) {
|
|
3416
3284
|
if (results.length >= maxResults) break;
|
|
3417
3285
|
try {
|
|
3418
|
-
const content =
|
|
3286
|
+
const content = readFileSync10(file, "utf8");
|
|
3419
3287
|
const lines = content.split("\n");
|
|
3420
3288
|
for (let i = 0; i < lines.length; i++) {
|
|
3421
3289
|
const lower = lines[i].toLowerCase();
|
|
@@ -3440,7 +3308,7 @@ function collectMarkdownFiles(dir) {
|
|
|
3440
3308
|
const files = [];
|
|
3441
3309
|
try {
|
|
3442
3310
|
for (const entry of readdirSync2(dir)) {
|
|
3443
|
-
const full =
|
|
3311
|
+
const full = join18(dir, entry);
|
|
3444
3312
|
const stat = statSync(full);
|
|
3445
3313
|
if (stat.isDirectory()) {
|
|
3446
3314
|
files.push(...collectMarkdownFiles(full));
|
|
@@ -3491,12 +3359,12 @@ async function runServe(cwd2, opts) {
|
|
|
3491
3359
|
|
|
3492
3360
|
// src/commands/status.ts
|
|
3493
3361
|
import Table from "cli-table3";
|
|
3494
|
-
import
|
|
3362
|
+
import pc11 from "picocolors";
|
|
3495
3363
|
var STATUS_COLOR = {
|
|
3496
|
-
pending: (s) =>
|
|
3497
|
-
in_progress: (s) =>
|
|
3498
|
-
done: (s) =>
|
|
3499
|
-
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)
|
|
3500
3368
|
};
|
|
3501
3369
|
async function runStatus(cwd2, opts) {
|
|
3502
3370
|
const config = await loadConfig(cwd2);
|
|
@@ -3517,11 +3385,11 @@ async function runStatus(cwd2, opts) {
|
|
|
3517
3385
|
return;
|
|
3518
3386
|
}
|
|
3519
3387
|
if (tasks.length === 0) {
|
|
3520
|
-
console.log(
|
|
3388
|
+
console.log(pc11.dim("No tasks yet. Run: ahk task add"));
|
|
3521
3389
|
return;
|
|
3522
3390
|
}
|
|
3523
3391
|
const table = new Table({
|
|
3524
|
-
head: ["ID", "Slug", "Title", "Status", "Assigned", "Started"].map((h) =>
|
|
3392
|
+
head: ["ID", "Slug", "Title", "Status", "Assigned", "Started"].map((h) => pc11.bold(h)),
|
|
3525
3393
|
style: { head: [], border: [] }
|
|
3526
3394
|
});
|
|
3527
3395
|
for (const t of tasks) {
|
|
@@ -3539,12 +3407,12 @@ async function runStatus(cwd2, opts) {
|
|
|
3539
3407
|
const inProgress = tasks.filter((t) => t.status === "in_progress");
|
|
3540
3408
|
if (inProgress.length > 0) {
|
|
3541
3409
|
console.log("");
|
|
3542
|
-
console.log(
|
|
3410
|
+
console.log(pc11.bold("Active actions:"));
|
|
3543
3411
|
for (const t of inProgress) {
|
|
3544
3412
|
const actions = await db.getActionsForTask(t.id);
|
|
3545
3413
|
const active = actions.filter((a) => a.status === "in_progress");
|
|
3546
3414
|
for (const a of active) {
|
|
3547
|
-
console.log(` ${
|
|
3415
|
+
console.log(` ${pc11.cyan(a.agent.padEnd(10))} \u2192 task #${t.id} ${t.slug}`);
|
|
3548
3416
|
}
|
|
3549
3417
|
}
|
|
3550
3418
|
}
|
|
@@ -3553,10 +3421,10 @@ async function runStatus(cwd2, opts) {
|
|
|
3553
3421
|
const fn = STATUS_COLOR[s.status] ?? ((x) => x);
|
|
3554
3422
|
return `${fn(s.status)}: ${s.total}`;
|
|
3555
3423
|
});
|
|
3556
|
-
console.log(
|
|
3424
|
+
console.log(pc11.dim("Tasks \u2014 ") + parts.join(pc11.dim(" | ")));
|
|
3557
3425
|
const archivedTasks = await db.getArchivedTasks();
|
|
3558
3426
|
if (archivedTasks.length > 0) {
|
|
3559
|
-
console.log(
|
|
3427
|
+
console.log(pc11.dim(`${archivedTasks.length} archived (use \`ahk task list --archived\` to view)`));
|
|
3560
3428
|
}
|
|
3561
3429
|
} finally {
|
|
3562
3430
|
await db.close();
|
|
@@ -3564,13 +3432,13 @@ async function runStatus(cwd2, opts) {
|
|
|
3564
3432
|
}
|
|
3565
3433
|
|
|
3566
3434
|
// src/commands/sync.ts
|
|
3567
|
-
import { existsSync as
|
|
3568
|
-
import { join as
|
|
3569
|
-
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";
|
|
3570
3438
|
async function runSync(cwd2, opts) {
|
|
3571
3439
|
const config = await loadConfig(cwd2);
|
|
3572
3440
|
const direction = opts.direction ?? "both";
|
|
3573
|
-
const featureListPath = resolve11(
|
|
3441
|
+
const featureListPath = resolve11(join19(cwd2, config.storage.dir, "feature_list.json"));
|
|
3574
3442
|
const db = await openDB(config, cwd2);
|
|
3575
3443
|
try {
|
|
3576
3444
|
if (direction === "in" || direction === "both") {
|
|
@@ -3584,44 +3452,44 @@ async function runSync(cwd2, opts) {
|
|
|
3584
3452
|
}
|
|
3585
3453
|
}
|
|
3586
3454
|
async function syncIn(featureListPath, db, dryRun) {
|
|
3587
|
-
if (!
|
|
3588
|
-
console.log(
|
|
3455
|
+
if (!existsSync16(featureListPath)) {
|
|
3456
|
+
console.log(pc12.dim(`feature_list.json not found at ${featureListPath} \u2014 skipping in-sync`));
|
|
3589
3457
|
return;
|
|
3590
3458
|
}
|
|
3591
3459
|
let seeds;
|
|
3592
3460
|
try {
|
|
3593
|
-
seeds = JSON.parse(
|
|
3461
|
+
seeds = JSON.parse(readFileSync11(featureListPath, "utf8"));
|
|
3594
3462
|
} catch (err) {
|
|
3595
|
-
console.error(
|
|
3463
|
+
console.error(pc12.red(`Failed to parse feature_list.json: ${err}`));
|
|
3596
3464
|
process.exit(1);
|
|
3597
3465
|
}
|
|
3598
3466
|
if (dryRun) {
|
|
3599
|
-
console.log(
|
|
3467
|
+
console.log(pc12.bold("Dry run \u2014 in-sync (feature_list.json \u2192 SQLite):"));
|
|
3600
3468
|
for (const t of seeds) {
|
|
3601
3469
|
const existing = await db.getTaskBySlug(t.slug);
|
|
3602
|
-
console.log(` ${existing ?
|
|
3470
|
+
console.log(` ${existing ? pc12.dim("skip") : pc12.green("add ")} ${t.slug}`);
|
|
3603
3471
|
}
|
|
3604
3472
|
return;
|
|
3605
3473
|
}
|
|
3606
3474
|
const result = await db.syncFromFeatureList(seeds);
|
|
3607
|
-
console.log(
|
|
3475
|
+
console.log(pc12.green(`\u2713 In-sync: ${result.added} added, ${result.skipped} already existed`));
|
|
3608
3476
|
}
|
|
3609
3477
|
async function syncOut(db, cwd2, dryRun) {
|
|
3610
3478
|
if (dryRun) {
|
|
3611
3479
|
const tasks = await db.getTasks();
|
|
3612
|
-
console.log(
|
|
3480
|
+
console.log(pc12.bold("Dry run \u2014 out-sync (SQLite \u2192 feature_list.json):"));
|
|
3613
3481
|
console.log(` ${tasks.length} tasks would be written`);
|
|
3614
3482
|
return;
|
|
3615
3483
|
}
|
|
3616
3484
|
await db.writeFeatureList(cwd2);
|
|
3617
|
-
console.log(
|
|
3485
|
+
console.log(pc12.green("\u2713 Out-sync: feature_list.json updated"));
|
|
3618
3486
|
}
|
|
3619
3487
|
|
|
3620
3488
|
// src/commands/task/add.ts
|
|
3621
3489
|
import * as p6 from "@clack/prompts";
|
|
3622
|
-
import
|
|
3490
|
+
import pc13 from "picocolors";
|
|
3623
3491
|
async function runTaskAdd(cwd2) {
|
|
3624
|
-
p6.intro(
|
|
3492
|
+
p6.intro(pc13.bold("agent-harness-kit \u2014 add task"));
|
|
3625
3493
|
const title = await cliFormWithRetry(
|
|
3626
3494
|
async () => {
|
|
3627
3495
|
const val = await p6.text({ message: "Task title" });
|
|
@@ -3664,10 +3532,10 @@ async function runTaskAdd(cwd2) {
|
|
|
3664
3532
|
await db.writeFeatureList(cwd2);
|
|
3665
3533
|
await db.close();
|
|
3666
3534
|
spinner6.stop("");
|
|
3667
|
-
console.log(
|
|
3668
|
-
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");
|
|
3669
3537
|
} catch (err) {
|
|
3670
|
-
spinner6.stop(
|
|
3538
|
+
spinner6.stop(pc13.red("Failed"));
|
|
3671
3539
|
p6.log.error(err instanceof Error ? err.message : String(err));
|
|
3672
3540
|
process.exit(1);
|
|
3673
3541
|
}
|
|
@@ -3675,17 +3543,17 @@ async function runTaskAdd(cwd2) {
|
|
|
3675
3543
|
|
|
3676
3544
|
// src/commands/task/done.ts
|
|
3677
3545
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
3678
|
-
import { existsSync as
|
|
3546
|
+
import { existsSync as existsSync17 } from "fs";
|
|
3679
3547
|
import { resolve as resolve12 } from "path";
|
|
3680
|
-
import
|
|
3548
|
+
import pc14 from "picocolors";
|
|
3681
3549
|
async function runTaskDone(cwd2, idOrSlug) {
|
|
3682
3550
|
const config = await loadConfig(cwd2);
|
|
3683
3551
|
if (config.health.required) {
|
|
3684
3552
|
const scriptPath = resolve12(cwd2, config.health.scriptPath);
|
|
3685
|
-
if (
|
|
3553
|
+
if (existsSync17(scriptPath)) {
|
|
3686
3554
|
const result = spawnSync2("bash", [scriptPath], { cwd: cwd2, stdio: "pipe", encoding: "utf8" });
|
|
3687
3555
|
if (result.status !== 0) {
|
|
3688
|
-
console.error(
|
|
3556
|
+
console.error(pc14.red("\u2717 Health check failed \u2014 cannot mark task as done."));
|
|
3689
3557
|
if (result.stdout) console.error(result.stdout);
|
|
3690
3558
|
if (result.stderr) console.error(result.stderr);
|
|
3691
3559
|
process.exit(1);
|
|
@@ -3698,16 +3566,16 @@ async function runTaskDone(cwd2, idOrSlug) {
|
|
|
3698
3566
|
const isId = !isNaN(parsed);
|
|
3699
3567
|
const task2 = isId ? await db.getTaskById(parsed) : await db.getTaskBySlug(idOrSlug);
|
|
3700
3568
|
if (!task2) {
|
|
3701
|
-
console.error(
|
|
3569
|
+
console.error(pc14.red(`Task not found: ${idOrSlug}`));
|
|
3702
3570
|
process.exit(1);
|
|
3703
3571
|
}
|
|
3704
3572
|
if (task2.status === "done") {
|
|
3705
|
-
console.log(
|
|
3573
|
+
console.log(pc14.dim(`Task #${task2.id} is already done.`));
|
|
3706
3574
|
return;
|
|
3707
3575
|
}
|
|
3708
3576
|
await db.updateTaskStatus(task2.id, "done");
|
|
3709
3577
|
await db.writeFeatureList(cwd2);
|
|
3710
|
-
console.log(
|
|
3578
|
+
console.log(pc14.green(`\u2713 Task #${task2.id} \u2014 ${task2.slug} marked as done`));
|
|
3711
3579
|
} finally {
|
|
3712
3580
|
await db.close();
|
|
3713
3581
|
}
|
|
@@ -3715,9 +3583,9 @@ async function runTaskDone(cwd2, idOrSlug) {
|
|
|
3715
3583
|
|
|
3716
3584
|
// src/commands/task/edit.ts
|
|
3717
3585
|
import * as p7 from "@clack/prompts";
|
|
3718
|
-
import
|
|
3586
|
+
import pc15 from "picocolors";
|
|
3719
3587
|
async function runTaskEdit(cwd2) {
|
|
3720
|
-
p7.intro(
|
|
3588
|
+
p7.intro(pc15.bold("agent-harness-kit \u2014 edit task"));
|
|
3721
3589
|
const config = await loadConfig(cwd2);
|
|
3722
3590
|
const db = await openDB(config, cwd2);
|
|
3723
3591
|
try {
|
|
@@ -3795,9 +3663,9 @@ async function runTaskEdit(cwd2) {
|
|
|
3795
3663
|
await db.updateTaskAcceptance(task2.id, newAcceptance);
|
|
3796
3664
|
await db.writeFeatureList(cwd2);
|
|
3797
3665
|
spinner6.stop("");
|
|
3798
|
-
console.log(
|
|
3666
|
+
console.log(pc15.green(`\u2713 Task #${task2.id} updated \u2014 ${newSlug}`));
|
|
3799
3667
|
} catch (err) {
|
|
3800
|
-
spinner6.stop(
|
|
3668
|
+
spinner6.stop(pc15.red("Failed"));
|
|
3801
3669
|
p7.log.error(err instanceof Error ? err.message : String(err));
|
|
3802
3670
|
process.exit(1);
|
|
3803
3671
|
}
|
|
@@ -3808,12 +3676,12 @@ async function runTaskEdit(cwd2) {
|
|
|
3808
3676
|
|
|
3809
3677
|
// src/commands/task/list.ts
|
|
3810
3678
|
import Table2 from "cli-table3";
|
|
3811
|
-
import
|
|
3679
|
+
import pc16 from "picocolors";
|
|
3812
3680
|
var STATUS_COLOR2 = {
|
|
3813
|
-
pending: (s) =>
|
|
3814
|
-
in_progress: (s) =>
|
|
3815
|
-
done: (s) =>
|
|
3816
|
-
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)
|
|
3817
3685
|
};
|
|
3818
3686
|
async function runTaskList(cwd2, opts) {
|
|
3819
3687
|
const config = await loadConfig(cwd2);
|
|
@@ -3830,11 +3698,11 @@ async function runTaskList(cwd2, opts) {
|
|
|
3830
3698
|
let msg = "No tasks";
|
|
3831
3699
|
if (filterStatus) msg += ` with status: ${filterStatus}`;
|
|
3832
3700
|
if (opts.archived) msg += " (archived)";
|
|
3833
|
-
console.log(
|
|
3701
|
+
console.log(pc16.dim(msg + "."));
|
|
3834
3702
|
return;
|
|
3835
3703
|
}
|
|
3836
3704
|
const table = new Table2({
|
|
3837
|
-
head: ["ID", "Slug", "Title", "Status"].map((h) =>
|
|
3705
|
+
head: ["ID", "Slug", "Title", "Status"].map((h) => pc16.bold(h)),
|
|
3838
3706
|
style: { head: [], border: [] }
|
|
3839
3707
|
});
|
|
3840
3708
|
for (const t of tasks) {
|
|
@@ -3845,7 +3713,7 @@ async function runTaskList(cwd2, opts) {
|
|
|
3845
3713
|
if (!opts.archived && !opts.includeArchived) {
|
|
3846
3714
|
const archivedTasks = await db.getArchivedTasks();
|
|
3847
3715
|
if (archivedTasks.length > 0) {
|
|
3848
|
-
console.log(
|
|
3716
|
+
console.log(pc16.dim(`${archivedTasks.length} archived task${archivedTasks.length !== 1 ? "s" : ""} (use --archived to view)`));
|
|
3849
3717
|
}
|
|
3850
3718
|
}
|
|
3851
3719
|
} finally {
|
|
@@ -3853,8 +3721,42 @@ async function runTaskList(cwd2, opts) {
|
|
|
3853
3721
|
}
|
|
3854
3722
|
}
|
|
3855
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
|
+
|
|
3856
3758
|
// src/core/update-check.ts
|
|
3857
|
-
import
|
|
3759
|
+
import pc18 from "picocolors";
|
|
3858
3760
|
var REGISTRY_URL2 = `https://registry.npmjs.org/${pkg.name}/latest`;
|
|
3859
3761
|
var TIMEOUT_MS2 = 2500;
|
|
3860
3762
|
function checkForUpdate(currentVersion) {
|
|
@@ -3872,8 +3774,8 @@ function checkForUpdate(currentVersion) {
|
|
|
3872
3774
|
}
|
|
3873
3775
|
function printUpdateMessage({ current, latest }) {
|
|
3874
3776
|
const lines = [
|
|
3875
|
-
` Update available ${
|
|
3876
|
-
` Run: ${
|
|
3777
|
+
` Update available ${pc18.dim(current)} \u2192 ${pc18.green(latest)} `,
|
|
3778
|
+
` Run: ${pc18.cyan(`pnpm i ${pkg.name}@${latest}`)} `
|
|
3877
3779
|
];
|
|
3878
3780
|
drawBox(lines);
|
|
3879
3781
|
}
|
|
@@ -3891,7 +3793,7 @@ var cwd = process.cwd();
|
|
|
3891
3793
|
var updateCheck = checkForUpdate(pkg.version);
|
|
3892
3794
|
var program = new Command();
|
|
3893
3795
|
program.name("ahk").description("agent-harness-kit \u2014 CLI scaffolding for multi-agent harness systems").version(pkg.version, "-v, --version");
|
|
3894
|
-
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) => {
|
|
3895
3797
|
await runInit(cwd, opts);
|
|
3896
3798
|
});
|
|
3897
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) => {
|
|
@@ -3925,9 +3827,20 @@ task.command("edit").description("Edit a task interactively").action(async () =>
|
|
|
3925
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) => {
|
|
3926
3828
|
await runDashboard(cwd, { port: parseInt(opts.port), open: opts.open });
|
|
3927
3829
|
});
|
|
3928
|
-
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) => {
|
|
3929
3832
|
await runMigrate(cwd, opts);
|
|
3930
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
|
+
});
|
|
3931
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) => {
|
|
3932
3845
|
await runExport(cwd, opts);
|
|
3933
3846
|
});
|
|
@@ -3937,9 +3850,25 @@ program.command("reset").description("Reset/clear harness data (DB, feature list
|
|
|
3937
3850
|
program.command("doctor").description("Check lib version, agent files, and harness skills sync status").action(async () => {
|
|
3938
3851
|
await runDoctor(cwd);
|
|
3939
3852
|
});
|
|
3853
|
+
program.hook("preAction", () => {
|
|
3854
|
+
if (!isLocalInstallSatisfied(cwd)) {
|
|
3855
|
+
printLocalInstallWarning();
|
|
3856
|
+
process.exit(1);
|
|
3857
|
+
}
|
|
3858
|
+
});
|
|
3940
3859
|
program.hook("postAction", async () => {
|
|
3941
3860
|
const update = await updateCheck;
|
|
3942
3861
|
if (update) printUpdateMessage(update);
|
|
3943
3862
|
});
|
|
3944
|
-
|
|
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));
|
|
3945
3874
|
//# sourceMappingURL=cli.js.map
|