@cardor/agent-harness-kit 1.5.2 → 1.6.2
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 +35 -1
- package/dist/agent-templates/builder.md +26 -5
- package/dist/agent-templates/lead.md +5 -0
- package/dist/agent-templates/reviewer.md +1 -0
- package/dist/cli.js +247 -41
- package/dist/cli.js.map +1 -1
- package/dist/dashboard-dist/assets/{index-Tnz4xXel.js → index-CoqlHfTu.js} +4 -4
- package/dist/dashboard-dist/index.html +9 -6
- package/dist/dashboard-dist/logo-512.png +0 -0
- package/dist/dashboard-dist/logo.png +0 -0
- package/dist/index.d.ts +1 -0
- package/dist/{mysql-NXLYFD2H.js → mysql-THKQOXIS.js} +8 -2
- package/dist/mysql-THKQOXIS.js.map +1 -0
- package/dist/{postgres-6BXN7ZH4.js → postgres-IOQE32DM.js} +8 -2
- package/dist/postgres-IOQE32DM.js.map +1 -0
- package/dist/{sqlite-M65L55DA.js → sqlite-KWYK4IJW.js} +8 -2
- package/dist/sqlite-KWYK4IJW.js.map +1 -0
- package/package.json +2 -2
- package/dist/mysql-NXLYFD2H.js.map +0 -1
- package/dist/postgres-6BXN7ZH4.js.map +0 -1
- package/dist/sqlite-M65L55DA.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -67,7 +67,7 @@ function mergeClaudeSettingsJson(filePath) {
|
|
|
67
67
|
};
|
|
68
68
|
writeFileSync2(filePath, JSON.stringify(merged, null, 2) + "\n", "utf8");
|
|
69
69
|
}
|
|
70
|
-
var
|
|
70
|
+
var MCP_CLAUDE_PERMISSIONS_LEAD = [
|
|
71
71
|
"mcp__agent-harness-kit__actions_start",
|
|
72
72
|
"mcp__agent-harness-kit__actions_write",
|
|
73
73
|
"mcp__agent-harness-kit__actions_complete",
|
|
@@ -76,14 +76,69 @@ var MCP_CLAUDE_PERMISSIONS = [
|
|
|
76
76
|
"mcp__agent-harness-kit__actions_record_tool",
|
|
77
77
|
"mcp__agent-harness-kit__tasks_get",
|
|
78
78
|
"mcp__agent-harness-kit__tasks_claim",
|
|
79
|
+
"mcp__agent-harness-kit__tasks_add",
|
|
79
80
|
"mcp__agent-harness-kit__tasks_update",
|
|
81
|
+
"mcp__agent-harness-kit__tasks_edit",
|
|
82
|
+
"mcp__agent-harness-kit__tasks_archive",
|
|
83
|
+
"mcp__agent-harness-kit__tasks_unarchive",
|
|
84
|
+
"mcp__agent-harness-kit__tasks_acceptance_get",
|
|
85
|
+
"mcp__agent-harness-kit__docs_search"
|
|
86
|
+
];
|
|
87
|
+
var MCP_CLAUDE_PERMISSIONS_EXPLORER = [
|
|
88
|
+
"mcp__agent-harness-kit__actions_start",
|
|
89
|
+
"mcp__agent-harness-kit__actions_write",
|
|
90
|
+
"mcp__agent-harness-kit__actions_complete",
|
|
91
|
+
"mcp__agent-harness-kit__actions_get",
|
|
92
|
+
"mcp__agent-harness-kit__actions_record_file",
|
|
93
|
+
"mcp__agent-harness-kit__actions_record_tool",
|
|
94
|
+
"mcp__agent-harness-kit__tasks_get",
|
|
95
|
+
"mcp__agent-harness-kit__tasks_claim",
|
|
96
|
+
"mcp__agent-harness-kit__tasks_acceptance_get",
|
|
97
|
+
"mcp__agent-harness-kit__docs_search"
|
|
98
|
+
];
|
|
99
|
+
var MCP_CLAUDE_PERMISSIONS_BUILDER = [
|
|
100
|
+
"mcp__agent-harness-kit__actions_start",
|
|
101
|
+
"mcp__agent-harness-kit__actions_write",
|
|
102
|
+
"mcp__agent-harness-kit__actions_complete",
|
|
103
|
+
"mcp__agent-harness-kit__actions_get",
|
|
104
|
+
"mcp__agent-harness-kit__actions_record_file",
|
|
105
|
+
"mcp__agent-harness-kit__actions_record_tool",
|
|
106
|
+
"mcp__agent-harness-kit__tasks_get",
|
|
107
|
+
"mcp__agent-harness-kit__tasks_claim",
|
|
80
108
|
"mcp__agent-harness-kit__tasks_add",
|
|
81
|
-
"mcp__agent-harness-
|
|
109
|
+
"mcp__agent-harness-kit__tasks_update",
|
|
110
|
+
"mcp__agent-harness-kit__tasks_edit",
|
|
111
|
+
"mcp__agent-harness-kit__tasks_archive",
|
|
112
|
+
"mcp__agent-harness-kit__tasks_unarchive",
|
|
113
|
+
"mcp__agent-harness-kit__tasks_acceptance_get",
|
|
114
|
+
"mcp__agent-harness-kit__docs_search"
|
|
115
|
+
];
|
|
116
|
+
var MCP_CLAUDE_PERMISSIONS_REVIEWER = [
|
|
117
|
+
"mcp__agent-harness-kit__actions_start",
|
|
118
|
+
"mcp__agent-harness-kit__actions_write",
|
|
119
|
+
"mcp__agent-harness-kit__actions_complete",
|
|
120
|
+
"mcp__agent-harness-kit__actions_get",
|
|
121
|
+
"mcp__agent-harness-kit__actions_record_file",
|
|
122
|
+
"mcp__agent-harness-kit__actions_record_tool",
|
|
123
|
+
"mcp__agent-harness-kit__tasks_get",
|
|
124
|
+
"mcp__agent-harness-kit__tasks_claim",
|
|
125
|
+
"mcp__agent-harness-kit__tasks_add",
|
|
126
|
+
"mcp__agent-harness-kit__tasks_update",
|
|
82
127
|
"mcp__agent-harness-kit__tasks_edit",
|
|
83
128
|
"mcp__agent-harness-kit__tasks_archive",
|
|
84
129
|
"mcp__agent-harness-kit__tasks_unarchive",
|
|
130
|
+
"mcp__agent-harness-kit__tasks_acceptance_update",
|
|
131
|
+
"mcp__agent-harness-kit__tasks_acceptance_get",
|
|
85
132
|
"mcp__agent-harness-kit__docs_search"
|
|
86
133
|
];
|
|
134
|
+
var MCP_CLAUDE_PERMISSIONS = [
|
|
135
|
+
.../* @__PURE__ */ new Set([
|
|
136
|
+
...MCP_CLAUDE_PERMISSIONS_LEAD,
|
|
137
|
+
...MCP_CLAUDE_PERMISSIONS_EXPLORER,
|
|
138
|
+
...MCP_CLAUDE_PERMISSIONS_BUILDER,
|
|
139
|
+
...MCP_CLAUDE_PERMISSIONS_REVIEWER
|
|
140
|
+
])
|
|
141
|
+
];
|
|
87
142
|
function mergeClaudeSettingsLocalJson(filePath) {
|
|
88
143
|
mkdirSync2(dirname(filePath), { recursive: true });
|
|
89
144
|
let existing = {};
|
|
@@ -491,8 +546,15 @@ function agentReviewerToml(vars) {
|
|
|
491
546
|
const { description, body } = stripFrontmatter(loadAgentTemplate("reviewer", vars));
|
|
492
547
|
return toCodexToml("reviewer", description, body, "read-only");
|
|
493
548
|
}
|
|
494
|
-
function translateFrontmatterForClaudeCode(md,
|
|
495
|
-
const
|
|
549
|
+
function translateFrontmatterForClaudeCode(md, agentName) {
|
|
550
|
+
const permissionsMap = {
|
|
551
|
+
lead: MCP_CLAUDE_PERMISSIONS_LEAD,
|
|
552
|
+
explorer: MCP_CLAUDE_PERMISSIONS_EXPLORER,
|
|
553
|
+
builder: MCP_CLAUDE_PERMISSIONS_BUILDER,
|
|
554
|
+
reviewer: MCP_CLAUDE_PERMISSIONS_REVIEWER
|
|
555
|
+
};
|
|
556
|
+
const permissions = permissionsMap[agentName] ?? MCP_CLAUDE_PERMISSIONS;
|
|
557
|
+
const mcpLines = permissions.map((t) => ` - ${t}`).join("\n");
|
|
496
558
|
return md.replace(/(tools:\n(?: - (?!mcp__)[^\n]+\n)+)/, (match) => {
|
|
497
559
|
const trimmed = match.trimEnd();
|
|
498
560
|
return `${trimmed}
|
|
@@ -527,6 +589,32 @@ function appendGitignore(cwd2) {
|
|
|
527
589
|
function slugify(title) {
|
|
528
590
|
return title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64);
|
|
529
591
|
}
|
|
592
|
+
var AGENT_TOOLS = {
|
|
593
|
+
lead: MCP_CLAUDE_PERMISSIONS_LEAD,
|
|
594
|
+
explorer: MCP_CLAUDE_PERMISSIONS_EXPLORER,
|
|
595
|
+
builder: MCP_CLAUDE_PERMISSIONS_BUILDER,
|
|
596
|
+
reviewer: MCP_CLAUDE_PERMISSIONS_REVIEWER
|
|
597
|
+
};
|
|
598
|
+
async function syncAgentPermissions(cwd2) {
|
|
599
|
+
for (const [agent, tools] of Object.entries(AGENT_TOOLS)) {
|
|
600
|
+
const filePath = join3(cwd2, ".claude", "agents", `${agent}.md`);
|
|
601
|
+
if (!existsSync2(filePath)) {
|
|
602
|
+
console.log(` ${agent}.md not found \u2014 skipping`);
|
|
603
|
+
continue;
|
|
604
|
+
}
|
|
605
|
+
const content = readFileSync3(filePath, "utf-8");
|
|
606
|
+
const toolsBlock = `tools:
|
|
607
|
+
${tools.map((t) => ` - ${t}`).join("\n")}
|
|
608
|
+
`;
|
|
609
|
+
const updated = content.replace(/tools:\n(?: - [^\n]+\n)*/m, toolsBlock);
|
|
610
|
+
if (updated === content) {
|
|
611
|
+
console.log(` ${agent}.md already in sync`);
|
|
612
|
+
} else {
|
|
613
|
+
writeFileSync3(filePath, updated, "utf-8");
|
|
614
|
+
console.log(` ${agent}.md updated`);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
530
618
|
|
|
531
619
|
// src/core/materializer/claude-code.ts
|
|
532
620
|
var ClaudeCodeMaterializer = class {
|
|
@@ -727,6 +815,9 @@ function getMaterializer(provider) {
|
|
|
727
815
|
// src/commands/build.ts
|
|
728
816
|
async function runBuild(cwd2, opts) {
|
|
729
817
|
await buildOnce(cwd2);
|
|
818
|
+
if (opts.sync) {
|
|
819
|
+
await syncAgentPermissions(cwd2);
|
|
820
|
+
}
|
|
730
821
|
if (opts.watch) {
|
|
731
822
|
p.log.info(`Watching agent-harness-kit.config.ts for changes...`);
|
|
732
823
|
watch(cwd2, { recursive: false }, async (_, filename) => {
|
|
@@ -770,6 +861,30 @@ import { extname, join as join7 } from "path";
|
|
|
770
861
|
import { serve } from "@hono/node-server";
|
|
771
862
|
import { Hono } from "hono";
|
|
772
863
|
import { WebSocketServer } from "ws";
|
|
864
|
+
|
|
865
|
+
// src/core/port-utils.ts
|
|
866
|
+
import { createServer } from "net";
|
|
867
|
+
function isPortFree(port) {
|
|
868
|
+
return new Promise((resolve13) => {
|
|
869
|
+
const server = createServer();
|
|
870
|
+
server.once("error", () => resolve13(false));
|
|
871
|
+
server.once("listening", () => {
|
|
872
|
+
server.close(() => resolve13(true));
|
|
873
|
+
});
|
|
874
|
+
server.listen(port, "127.0.0.1");
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
async function findFreePort(start, maxAttempts = 10) {
|
|
878
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
879
|
+
const port = start + i;
|
|
880
|
+
if (await isPortFree(port)) return port;
|
|
881
|
+
}
|
|
882
|
+
throw new Error(
|
|
883
|
+
`Could not find a free port after ${maxAttempts} attempts (tried ${start}-${start + maxAttempts - 1}). Please free a port and try again.`
|
|
884
|
+
);
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
// src/core/dashboard-server.ts
|
|
773
888
|
var MIME = {
|
|
774
889
|
".html": "text/html; charset=utf-8",
|
|
775
890
|
".js": "application/javascript; charset=utf-8",
|
|
@@ -790,7 +905,7 @@ function fileResponse(filePath) {
|
|
|
790
905
|
headers: { "Content-Type": mime, "Cache-Control": "no-cache" }
|
|
791
906
|
});
|
|
792
907
|
}
|
|
793
|
-
function startDashboardServer(db, dbPath, staticPath, port) {
|
|
908
|
+
async function startDashboardServer(db, dbPath, staticPath, port) {
|
|
794
909
|
const app = new Hono();
|
|
795
910
|
const { tasks, actions, stats } = db;
|
|
796
911
|
app.use("/api/*", async (c, next) => {
|
|
@@ -832,7 +947,10 @@ function startDashboardServer(db, dbPath, staticPath, port) {
|
|
|
832
947
|
if (body.description !== void 0) updateParams.description = body.description;
|
|
833
948
|
await db.updateTask(id, updateParams);
|
|
834
949
|
if (body.acceptance !== void 0 && Array.isArray(body.acceptance)) {
|
|
835
|
-
await db.updateTaskAcceptance(
|
|
950
|
+
await db.updateTaskAcceptance(
|
|
951
|
+
id,
|
|
952
|
+
body.acceptance.map((a) => a.trim()).filter(Boolean)
|
|
953
|
+
);
|
|
836
954
|
}
|
|
837
955
|
const updated = await tasks.getById(id);
|
|
838
956
|
const acceptance = await tasks.getAcceptance(id);
|
|
@@ -897,7 +1015,11 @@ function startDashboardServer(db, dbPath, staticPath, port) {
|
|
|
897
1015
|
}
|
|
898
1016
|
return fileResponse(join7(staticPath, "index.html"));
|
|
899
1017
|
});
|
|
900
|
-
const
|
|
1018
|
+
const resolvedPort = await findFreePort(port);
|
|
1019
|
+
if (resolvedPort !== port) {
|
|
1020
|
+
console.log(`Port ${port} in use, using ${resolvedPort}`);
|
|
1021
|
+
}
|
|
1022
|
+
const httpServer = serve({ fetch: app.fetch, port: resolvedPort });
|
|
901
1023
|
const wss = new WebSocketServer({ noServer: true });
|
|
902
1024
|
httpServer.on("upgrade", (req, socket, head) => {
|
|
903
1025
|
if (req.url === "/ws") {
|
|
@@ -926,7 +1048,7 @@ function startDashboardServer(db, dbPath, staticPath, port) {
|
|
|
926
1048
|
watcher = watch2(watchTarget, broadcast);
|
|
927
1049
|
}
|
|
928
1050
|
return {
|
|
929
|
-
url: `http://localhost:${
|
|
1051
|
+
url: `http://localhost:${resolvedPort}`,
|
|
930
1052
|
close: () => {
|
|
931
1053
|
clearTimeout(debounce);
|
|
932
1054
|
watcher?.close();
|
|
@@ -1149,8 +1271,8 @@ var TaskRepository = class {
|
|
|
1149
1271
|
async add(params) {
|
|
1150
1272
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1151
1273
|
return this.driver.insert(
|
|
1152
|
-
`INSERT INTO tasks (slug, title, description, status, created_at) VALUES (?, ?, ?, ?, ?)`,
|
|
1153
|
-
[params.slug, params.title, params.description ?? null, params.status ?? "pending", now]
|
|
1274
|
+
`INSERT INTO tasks (slug, title, description, status, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)`,
|
|
1275
|
+
[params.slug, params.title, params.description ?? null, params.status ?? "pending", now, now]
|
|
1154
1276
|
);
|
|
1155
1277
|
}
|
|
1156
1278
|
async addAcceptance(taskId, criteria) {
|
|
@@ -1175,7 +1297,7 @@ var TaskRepository = class {
|
|
|
1175
1297
|
if (conditions.length > 0) {
|
|
1176
1298
|
sql += ` WHERE ${conditions.join(" AND ")}`;
|
|
1177
1299
|
}
|
|
1178
|
-
sql += ` ORDER BY
|
|
1300
|
+
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`;
|
|
1179
1301
|
return this.driver.query(sql, params);
|
|
1180
1302
|
}
|
|
1181
1303
|
async getAllWithAcceptanceCounts(includeArchived = false) {
|
|
@@ -1189,7 +1311,7 @@ var TaskRepository = class {
|
|
|
1189
1311
|
if (!includeArchived) {
|
|
1190
1312
|
sql += ` WHERE t.archived_at IS NULL`;
|
|
1191
1313
|
}
|
|
1192
|
-
sql += ` GROUP BY t.id ORDER BY t.
|
|
1314
|
+
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`;
|
|
1193
1315
|
return this.driver.query(sql);
|
|
1194
1316
|
}
|
|
1195
1317
|
async getById(id) {
|
|
@@ -1205,23 +1327,25 @@ var TaskRepository = class {
|
|
|
1205
1327
|
);
|
|
1206
1328
|
}
|
|
1207
1329
|
async setStatus(id, status, extra) {
|
|
1330
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1208
1331
|
if (extra?.started_at) {
|
|
1209
1332
|
await this.driver.exec(
|
|
1210
|
-
`UPDATE tasks SET status = ?, started_at = ? WHERE id = ?`,
|
|
1211
|
-
[status, extra.started_at, id]
|
|
1333
|
+
`UPDATE tasks SET status = ?, started_at = ?, updated_at = ? WHERE id = ?`,
|
|
1334
|
+
[status, extra.started_at, now, id]
|
|
1212
1335
|
);
|
|
1213
1336
|
} else if (extra?.completed_at) {
|
|
1214
1337
|
await this.driver.exec(
|
|
1215
|
-
`UPDATE tasks SET status = ?, completed_at = ? WHERE id = ?`,
|
|
1216
|
-
[status, extra.completed_at, id]
|
|
1338
|
+
`UPDATE tasks SET status = ?, completed_at = ?, updated_at = ? WHERE id = ?`,
|
|
1339
|
+
[status, extra.completed_at, now, id]
|
|
1217
1340
|
);
|
|
1218
1341
|
} else {
|
|
1219
|
-
await this.driver.exec(`UPDATE tasks SET status = ? WHERE id = ?`, [status, id]);
|
|
1342
|
+
await this.driver.exec(`UPDATE tasks SET status = ?, updated_at = ? WHERE id = ?`, [status, now, id]);
|
|
1220
1343
|
}
|
|
1221
1344
|
}
|
|
1222
1345
|
async update(id, params) {
|
|
1223
1346
|
const sets = [];
|
|
1224
1347
|
const vals = [];
|
|
1348
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1225
1349
|
if (params.title !== void 0) {
|
|
1226
1350
|
sets.push("title = ?");
|
|
1227
1351
|
vals.push(params.title);
|
|
@@ -1235,6 +1359,8 @@ var TaskRepository = class {
|
|
|
1235
1359
|
vals.push(params.slug);
|
|
1236
1360
|
}
|
|
1237
1361
|
if (sets.length === 0) return;
|
|
1362
|
+
sets.push("updated_at = ?");
|
|
1363
|
+
vals.push(now);
|
|
1238
1364
|
vals.push(id);
|
|
1239
1365
|
await this.driver.exec(`UPDATE tasks SET ${sets.join(", ")} WHERE id = ?`, vals);
|
|
1240
1366
|
}
|
|
@@ -1249,10 +1375,11 @@ var TaskRepository = class {
|
|
|
1249
1375
|
}
|
|
1250
1376
|
async archive(id) {
|
|
1251
1377
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1252
|
-
await this.driver.exec(`UPDATE tasks SET archived_at = ? WHERE id = ?`, [now, id]);
|
|
1378
|
+
await this.driver.exec(`UPDATE tasks SET archived_at = ?, updated_at = ? WHERE id = ?`, [now, now, id]);
|
|
1253
1379
|
}
|
|
1254
1380
|
async unarchive(id) {
|
|
1255
|
-
|
|
1381
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1382
|
+
await this.driver.exec(`UPDATE tasks SET archived_at = NULL, updated_at = ? WHERE id = ?`, [now, id]);
|
|
1256
1383
|
}
|
|
1257
1384
|
async getArchived() {
|
|
1258
1385
|
return this.driver.query(
|
|
@@ -1261,8 +1388,8 @@ var TaskRepository = class {
|
|
|
1261
1388
|
}
|
|
1262
1389
|
async claim(id, agent, now) {
|
|
1263
1390
|
return this.driver.exec(
|
|
1264
|
-
`UPDATE tasks SET status = 'in_progress', assigned_to = ?, started_at = ? WHERE id = ? AND status = 'pending'`,
|
|
1265
|
-
[agent, now, id]
|
|
1391
|
+
`UPDATE tasks SET status = 'in_progress', assigned_to = ?, started_at = ?, updated_at = ? WHERE id = ? AND status = 'pending'`,
|
|
1392
|
+
[agent, now, now, id]
|
|
1266
1393
|
);
|
|
1267
1394
|
}
|
|
1268
1395
|
async markAcceptanceMet(criterionId) {
|
|
@@ -1538,13 +1665,13 @@ async function openDB(config, cwd2) {
|
|
|
1538
1665
|
const dbConfig = config.database;
|
|
1539
1666
|
let driver;
|
|
1540
1667
|
if (dbConfig.type === "postgres") {
|
|
1541
|
-
const { PostgresDriver } = await import("./postgres-
|
|
1668
|
+
const { PostgresDriver } = await import("./postgres-IOQE32DM.js");
|
|
1542
1669
|
driver = new PostgresDriver(dbConfig);
|
|
1543
1670
|
} else if (dbConfig.type === "mysql") {
|
|
1544
|
-
const { MySQLDriver } = await import("./mysql-
|
|
1671
|
+
const { MySQLDriver } = await import("./mysql-THKQOXIS.js");
|
|
1545
1672
|
driver = new MySQLDriver(dbConfig);
|
|
1546
1673
|
} else {
|
|
1547
|
-
const { SQLiteDriver } = await import("./sqlite-
|
|
1674
|
+
const { SQLiteDriver } = await import("./sqlite-KWYK4IJW.js");
|
|
1548
1675
|
if (dbConfig.type !== "sqlite") {
|
|
1549
1676
|
throw new Error("Invalid database type");
|
|
1550
1677
|
}
|
|
@@ -1561,7 +1688,7 @@ async function runDashboard(cwd2, opts) {
|
|
|
1561
1688
|
const db = await openDB(config, cwd2);
|
|
1562
1689
|
const dbPath = config.database.type === "sqlite" ? resolve7(cwd2, config.database.path) : null;
|
|
1563
1690
|
const staticPath = join9(__dirname2, "dashboard-dist");
|
|
1564
|
-
const { url } = startDashboardServer(db, dbPath, staticPath, opts.port);
|
|
1691
|
+
const { url } = await startDashboardServer(db, dbPath, staticPath, opts.port);
|
|
1565
1692
|
console.log(pc2.green(`\u2713`) + ` Dashboard running at ${pc2.bold(pc2.cyan(url))}`);
|
|
1566
1693
|
console.log(pc2.dim(` WebSocket live updates enabled`));
|
|
1567
1694
|
console.log(pc2.dim(` Press Ctrl+C to stop`));
|
|
@@ -2226,14 +2353,56 @@ async function runReset(cwd2, opts) {
|
|
|
2226
2353
|
}
|
|
2227
2354
|
|
|
2228
2355
|
// src/core/mcp-server.ts
|
|
2229
|
-
import { readdirSync as readdirSync2, readFileSync as
|
|
2230
|
-
import { join as
|
|
2356
|
+
import { readdirSync as readdirSync2, readFileSync as readFileSync7, statSync } from "fs";
|
|
2357
|
+
import { join as join15, resolve as resolve10 } from "path";
|
|
2231
2358
|
import { Server } from "@modelcontextprotocol/sdk/server";
|
|
2232
2359
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
2233
2360
|
import {
|
|
2234
2361
|
CallToolRequestSchema,
|
|
2235
2362
|
ListToolsRequestSchema
|
|
2236
2363
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
2364
|
+
|
|
2365
|
+
// src/core/permissions-check.ts
|
|
2366
|
+
import { existsSync as existsSync10, readFileSync as readFileSync6 } from "fs";
|
|
2367
|
+
import { join as join14 } from "path";
|
|
2368
|
+
var CANONICAL = {
|
|
2369
|
+
lead: MCP_CLAUDE_PERMISSIONS_LEAD,
|
|
2370
|
+
explorer: MCP_CLAUDE_PERMISSIONS_EXPLORER,
|
|
2371
|
+
builder: MCP_CLAUDE_PERMISSIONS_BUILDER,
|
|
2372
|
+
reviewer: MCP_CLAUDE_PERMISSIONS_REVIEWER
|
|
2373
|
+
};
|
|
2374
|
+
function parseToolsFromFrontmatter(content) {
|
|
2375
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/m);
|
|
2376
|
+
if (!match) return [];
|
|
2377
|
+
const fm = match[1];
|
|
2378
|
+
const toolsMatch = fm.match(/^tools:\n((?: - [^\n]+\n?)*)/m);
|
|
2379
|
+
if (!toolsMatch) return [];
|
|
2380
|
+
return toolsMatch[1].split("\n").map((l) => l.trim().replace(/^- /, "")).filter((l) => l.startsWith("mcp__"));
|
|
2381
|
+
}
|
|
2382
|
+
function checkPermissionsSync(cwd2) {
|
|
2383
|
+
const agents = {};
|
|
2384
|
+
let in_sync = true;
|
|
2385
|
+
for (const agent of ["lead", "explorer", "builder", "reviewer"]) {
|
|
2386
|
+
const filePath = join14(cwd2, ".claude", "agents", `${agent}.md`);
|
|
2387
|
+
if (!existsSync10(filePath)) {
|
|
2388
|
+
const missing2 = CANONICAL[agent];
|
|
2389
|
+
agents[agent] = { ok: false, missing: missing2, extra: [] };
|
|
2390
|
+
in_sync = false;
|
|
2391
|
+
continue;
|
|
2392
|
+
}
|
|
2393
|
+
const content = readFileSync6(filePath, "utf-8");
|
|
2394
|
+
const installed = parseToolsFromFrontmatter(content);
|
|
2395
|
+
const canonical = CANONICAL[agent];
|
|
2396
|
+
const missing = canonical.filter((t) => !installed.includes(t));
|
|
2397
|
+
const extra = installed.filter((t) => !canonical.includes(t));
|
|
2398
|
+
const ok2 = missing.length === 0 && extra.length === 0;
|
|
2399
|
+
if (!ok2) in_sync = false;
|
|
2400
|
+
agents[agent] = { ok: ok2, missing, extra };
|
|
2401
|
+
}
|
|
2402
|
+
return { in_sync, agents };
|
|
2403
|
+
}
|
|
2404
|
+
|
|
2405
|
+
// src/core/mcp-server.ts
|
|
2237
2406
|
var VERSION = "0.1.0";
|
|
2238
2407
|
var TOOLS = [
|
|
2239
2408
|
{
|
|
@@ -2375,6 +2544,17 @@ var TOOLS = [
|
|
|
2375
2544
|
required: ["criterionId"]
|
|
2376
2545
|
}
|
|
2377
2546
|
},
|
|
2547
|
+
{
|
|
2548
|
+
name: "tasks.acceptance.get",
|
|
2549
|
+
description: "Given a taskId, returns all acceptance criteria for that task with their id, task_id, criterion text, and met status. Use the returned id values to call tasks.acceptance_update(criterionId).",
|
|
2550
|
+
inputSchema: {
|
|
2551
|
+
type: "object",
|
|
2552
|
+
properties: {
|
|
2553
|
+
taskId: { type: "number", description: "Task ID" }
|
|
2554
|
+
},
|
|
2555
|
+
required: ["taskId"]
|
|
2556
|
+
}
|
|
2557
|
+
},
|
|
2378
2558
|
{
|
|
2379
2559
|
name: "tasks.add",
|
|
2380
2560
|
description: "Create a new task in the harness. Use this when the user describes work in natural language. Infer slug, title, description, and acceptance criteria from the conversation. Ask for missing critical info before calling.",
|
|
@@ -2446,6 +2626,11 @@ var TOOLS = [
|
|
|
2446
2626
|
},
|
|
2447
2627
|
required: ["id"]
|
|
2448
2628
|
}
|
|
2629
|
+
},
|
|
2630
|
+
{
|
|
2631
|
+
name: "permissions.check",
|
|
2632
|
+
description: "Check whether the .claude/agents/*.md tool permission lists are in sync with the current canonical permission constants. Returns per-agent diff with missing and extra tools. Call this at session start to detect outdated agent files after an ahk upgrade.",
|
|
2633
|
+
inputSchema: { type: "object", properties: {}, required: [] }
|
|
2449
2634
|
}
|
|
2450
2635
|
];
|
|
2451
2636
|
async function startMcpServer(config, cwd2) {
|
|
@@ -2460,7 +2645,7 @@ async function startMcpServer(config, cwd2) {
|
|
|
2460
2645
|
const { name, arguments: args } = request.params;
|
|
2461
2646
|
const a = args ?? {};
|
|
2462
2647
|
try {
|
|
2463
|
-
const result = await dispatch(name, a, db, docsPath);
|
|
2648
|
+
const result = await dispatch(name, a, db, docsPath, cwd2);
|
|
2464
2649
|
return result;
|
|
2465
2650
|
} catch (err) {
|
|
2466
2651
|
return ok(`Error: ${err instanceof Error ? err.message : String(err)}`, true);
|
|
@@ -2469,7 +2654,7 @@ async function startMcpServer(config, cwd2) {
|
|
|
2469
2654
|
const transport = new StdioServerTransport();
|
|
2470
2655
|
await server.connect(transport);
|
|
2471
2656
|
}
|
|
2472
|
-
async function dispatch(name, args, db, docsPath) {
|
|
2657
|
+
async function dispatch(name, args, db, docsPath, cwd2) {
|
|
2473
2658
|
switch (name) {
|
|
2474
2659
|
case "actions.start": {
|
|
2475
2660
|
const taskId = num(args, "taskId");
|
|
@@ -2551,6 +2736,11 @@ async function dispatch(name, args, db, docsPath) {
|
|
|
2551
2736
|
await db.markAcceptanceMet(criterionId);
|
|
2552
2737
|
return ok(JSON.stringify({ criterionId, met: true }));
|
|
2553
2738
|
}
|
|
2739
|
+
case "tasks.acceptance.get": {
|
|
2740
|
+
const taskId = num(args, "taskId");
|
|
2741
|
+
const criteria = await db.getTaskAcceptance(taskId);
|
|
2742
|
+
return ok(JSON.stringify(criteria, null, 2));
|
|
2743
|
+
}
|
|
2554
2744
|
case "actions.record_tool": {
|
|
2555
2745
|
const actionId = str(args, "actionId");
|
|
2556
2746
|
const toolName = str(args, "toolName");
|
|
@@ -2583,6 +2773,10 @@ async function dispatch(name, args, db, docsPath) {
|
|
|
2583
2773
|
const task2 = await db.unarchiveTask(id);
|
|
2584
2774
|
return ok(JSON.stringify(task2));
|
|
2585
2775
|
}
|
|
2776
|
+
case "permissions.check": {
|
|
2777
|
+
const result = checkPermissionsSync(cwd2);
|
|
2778
|
+
return ok(JSON.stringify(result, null, 2));
|
|
2779
|
+
}
|
|
2586
2780
|
default:
|
|
2587
2781
|
return ok(`Unknown tool: ${name}`, true);
|
|
2588
2782
|
}
|
|
@@ -2595,7 +2789,7 @@ function searchDocs(docsPath, query, maxResults = 10) {
|
|
|
2595
2789
|
for (const file of files) {
|
|
2596
2790
|
if (results.length >= maxResults) break;
|
|
2597
2791
|
try {
|
|
2598
|
-
const content =
|
|
2792
|
+
const content = readFileSync7(file, "utf8");
|
|
2599
2793
|
const lines = content.split("\n");
|
|
2600
2794
|
for (let i = 0; i < lines.length; i++) {
|
|
2601
2795
|
const lower = lines[i].toLowerCase();
|
|
@@ -2616,7 +2810,7 @@ function collectMarkdownFiles(dir) {
|
|
|
2616
2810
|
const files = [];
|
|
2617
2811
|
try {
|
|
2618
2812
|
for (const entry of readdirSync2(dir)) {
|
|
2619
|
-
const full =
|
|
2813
|
+
const full = join15(dir, entry);
|
|
2620
2814
|
const stat = statSync(full);
|
|
2621
2815
|
if (stat.isDirectory()) {
|
|
2622
2816
|
files.push(...collectMarkdownFiles(full));
|
|
@@ -2650,6 +2844,18 @@ async function runServe(cwd2, opts) {
|
|
|
2650
2844
|
}
|
|
2651
2845
|
process.stderr.write(`[agent-harness-kit] MCP server starting (stdio)
|
|
2652
2846
|
`);
|
|
2847
|
+
const syncResult = checkPermissionsSync(cwd2);
|
|
2848
|
+
if (!syncResult.in_sync) {
|
|
2849
|
+
const affected = Object.entries(syncResult.agents).filter(([, r]) => !r.ok).map(([name, r]) => {
|
|
2850
|
+
const parts = [];
|
|
2851
|
+
if (r.missing.length) parts.push(`missing: ${r.missing.map((t) => t.replace("mcp__agent-harness-kit__", "")).join(", ")}`);
|
|
2852
|
+
if (r.extra.length) parts.push(`extra: ${r.extra.map((t) => t.replace("mcp__agent-harness-kit__", "")).join(", ")}`);
|
|
2853
|
+
return `${name} (${parts.join("; ")})`;
|
|
2854
|
+
}).join("\n ");
|
|
2855
|
+
process.stderr.write(`[agent-harness-kit] Agent permissions out of sync. Run: ahk build --sync
|
|
2856
|
+
${affected}
|
|
2857
|
+
`);
|
|
2858
|
+
}
|
|
2653
2859
|
await startMcpServer(config, cwd2);
|
|
2654
2860
|
}
|
|
2655
2861
|
|
|
@@ -2728,13 +2934,13 @@ async function runStatus(cwd2, opts) {
|
|
|
2728
2934
|
}
|
|
2729
2935
|
|
|
2730
2936
|
// src/commands/sync.ts
|
|
2731
|
-
import { existsSync as
|
|
2732
|
-
import { join as
|
|
2937
|
+
import { existsSync as existsSync11, readFileSync as readFileSync8 } from "fs";
|
|
2938
|
+
import { join as join16, resolve as resolve11 } from "path";
|
|
2733
2939
|
import pc10 from "picocolors";
|
|
2734
2940
|
async function runSync(cwd2, opts) {
|
|
2735
2941
|
const config = await loadConfig(cwd2);
|
|
2736
2942
|
const direction = opts.direction ?? "both";
|
|
2737
|
-
const featureListPath = resolve11(
|
|
2943
|
+
const featureListPath = resolve11(join16(cwd2, config.storage.dir, "feature_list.json"));
|
|
2738
2944
|
const db = await openDB(config, cwd2);
|
|
2739
2945
|
try {
|
|
2740
2946
|
if (direction === "in" || direction === "both") {
|
|
@@ -2748,13 +2954,13 @@ async function runSync(cwd2, opts) {
|
|
|
2748
2954
|
}
|
|
2749
2955
|
}
|
|
2750
2956
|
async function syncIn(featureListPath, db, dryRun) {
|
|
2751
|
-
if (!
|
|
2957
|
+
if (!existsSync11(featureListPath)) {
|
|
2752
2958
|
console.log(pc10.dim(`feature_list.json not found at ${featureListPath} \u2014 skipping in-sync`));
|
|
2753
2959
|
return;
|
|
2754
2960
|
}
|
|
2755
2961
|
let seeds;
|
|
2756
2962
|
try {
|
|
2757
|
-
seeds = JSON.parse(
|
|
2963
|
+
seeds = JSON.parse(readFileSync8(featureListPath, "utf8"));
|
|
2758
2964
|
} catch (err) {
|
|
2759
2965
|
console.error(pc10.red(`Failed to parse feature_list.json: ${err}`));
|
|
2760
2966
|
process.exit(1);
|
|
@@ -2839,14 +3045,14 @@ async function runTaskAdd(cwd2) {
|
|
|
2839
3045
|
|
|
2840
3046
|
// src/commands/task/done.ts
|
|
2841
3047
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
2842
|
-
import { existsSync as
|
|
3048
|
+
import { existsSync as existsSync12 } from "fs";
|
|
2843
3049
|
import { resolve as resolve12 } from "path";
|
|
2844
3050
|
import pc12 from "picocolors";
|
|
2845
3051
|
async function runTaskDone(cwd2, idOrSlug) {
|
|
2846
3052
|
const config = await loadConfig(cwd2);
|
|
2847
3053
|
if (config.health.required) {
|
|
2848
3054
|
const scriptPath = resolve12(cwd2, config.health.scriptPath);
|
|
2849
|
-
if (
|
|
3055
|
+
if (existsSync12(scriptPath)) {
|
|
2850
3056
|
const result = spawnSync2("bash", [scriptPath], { cwd: cwd2, stdio: "pipe", encoding: "utf8" });
|
|
2851
3057
|
if (result.status !== 0) {
|
|
2852
3058
|
console.error(pc12.red("\u2717 Health check failed \u2014 cannot mark task as done."));
|
|
@@ -3019,10 +3225,10 @@ async function runTaskList(cwd2, opts) {
|
|
|
3019
3225
|
|
|
3020
3226
|
// src/core/package-data.ts
|
|
3021
3227
|
import { createRequire } from "module";
|
|
3022
|
-
import { dirname as dirname5, join as
|
|
3228
|
+
import { dirname as dirname5, join as join17 } from "path";
|
|
3023
3229
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
3024
3230
|
var require2 = createRequire(import.meta.url);
|
|
3025
|
-
var pkgPath =
|
|
3231
|
+
var pkgPath = join17(dirname5(fileURLToPath3(import.meta.url)), "..", "package.json");
|
|
3026
3232
|
var pkg = require2(pkgPath);
|
|
3027
3233
|
|
|
3028
3234
|
// src/core/update-check.ts
|
|
@@ -3066,7 +3272,7 @@ program.name("ahk").description("agent-harness-kit \u2014 CLI scaffolding for mu
|
|
|
3066
3272
|
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) => {
|
|
3067
3273
|
await runInit(cwd, opts);
|
|
3068
3274
|
});
|
|
3069
|
-
program.command("build").description("Regenerate AGENTS.md and provider files from agent-harness-kit.config.ts").option("--watch", "Rebuild on config changes").action(async (opts) => {
|
|
3275
|
+
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) => {
|
|
3070
3276
|
await runBuild(cwd, opts);
|
|
3071
3277
|
});
|
|
3072
3278
|
program.command("health").description("Run health.sh and report result").action(async () => {
|