@cardor/agent-harness-kit 1.5.1 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -119,6 +119,7 @@ Then notify lead so the builder can be re-assigned.
119
119
 
120
120
  - **Run health.sh before approving.** No exceptions.
121
121
  - **Check every acceptance criterion.** Not just the obvious ones.
122
+ - **Use `tasks.acceptance.get(taskId)` to retrieve criterion ids.** Call this before `tasks.acceptance.update()` when you do not already have criterion ids from `tasks.get`.
122
123
  - **Call `tasks.acceptance.update()` for each criterion.** Never skip this step.
123
124
  - **Never self-approve partial work.** All criteria must be met, not most.
124
125
  - **Be specific when blocking.** The builder must know exactly what to fix.
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 MCP_PERMISSIONS = [
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_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-kit__tasks_acceptance_update",
109
+ "mcp__agent-harness-kit__tasks_update",
82
110
  "mcp__agent-harness-kit__tasks_edit",
83
111
  "mcp__agent-harness-kit__tasks_archive",
84
112
  "mcp__agent-harness-kit__tasks_unarchive",
113
+ "mcp__agent-harness-kit__tasks_acceptance_get",
85
114
  "mcp__agent-harness-kit__docs_search"
86
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",
127
+ "mcp__agent-harness-kit__tasks_edit",
128
+ "mcp__agent-harness-kit__tasks_archive",
129
+ "mcp__agent-harness-kit__tasks_unarchive",
130
+ "mcp__agent-harness-kit__tasks_acceptance_update",
131
+ "mcp__agent-harness-kit__tasks_acceptance_get",
132
+ "mcp__agent-harness-kit__docs_search"
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 = {};
@@ -96,7 +151,7 @@ function mergeClaudeSettingsLocalJson(filePath) {
96
151
  const existingPermissions = existing.permissions ?? {};
97
152
  const existingAllow = existingPermissions.allow ?? [];
98
153
  const existingServers = existing.enabledMcpjsonServers ?? [];
99
- const mergedAllow = Array.from(/* @__PURE__ */ new Set([...existingAllow, ...MCP_PERMISSIONS]));
154
+ const mergedAllow = Array.from(/* @__PURE__ */ new Set([...existingAllow, ...MCP_CLAUDE_PERMISSIONS]));
100
155
  const mergedServers = Array.from(/* @__PURE__ */ new Set([...existingServers, "agent-harness-kit"]));
101
156
  const merged = {
102
157
  ...existing,
@@ -491,47 +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
- var CLAUDE_CODE_MCP_TOOLS = {
495
- lead: [
496
- // 'mcp__agent-harness-kit__actions.start',
497
- // 'mcp__agent-harness-kit__actions.write',
498
- // 'mcp__agent-harness-kit__actions.complete',
499
- // 'mcp__agent-harness-kit__actions.get',
500
- // 'mcp__agent-harness-kit__actions.record_tool',
501
- // 'mcp__agent-harness-kit__tasks.get',
502
- // 'mcp__agent-harness-kit__tasks.claim',
503
- // 'mcp__agent-harness-kit__tasks.update',
504
- // 'mcp__agent-harness-kit__tasks.add',
505
- ],
506
- explorer: [
507
- // 'mcp__agent-harness-kit__actions.start',
508
- // 'mcp__agent-harness-kit__actions.write',
509
- // 'mcp__agent-harness-kit__actions.complete',
510
- // 'mcp__agent-harness-kit__actions.get',
511
- // 'mcp__agent-harness-kit__actions.record_tool',
512
- // 'mcp__agent-harness-kit__docs.search',
513
- ],
514
- builder: [
515
- // 'mcp__agent-harness-kit__actions.start',
516
- // 'mcp__agent-harness-kit__actions.write',
517
- // 'mcp__agent-harness-kit__actions.complete',
518
- // 'mcp__agent-harness-kit__actions.get',
519
- // 'mcp__agent-harness-kit__actions.record_tool',
520
- // 'mcp__agent-harness-kit__actions.record_file',
521
- ],
522
- reviewer: [
523
- // 'mcp__agent-harness-kit__actions.start',
524
- // 'mcp__agent-harness-kit__actions.write',
525
- // 'mcp__agent-harness-kit__actions.complete',
526
- // 'mcp__agent-harness-kit__actions.get',
527
- // 'mcp__agent-harness-kit__actions.record_tool',
528
- // 'mcp__agent-harness-kit__tasks.acceptance.update',
529
- // 'mcp__agent-harness-kit__tasks.update',
530
- ]
531
- };
532
549
  function translateFrontmatterForClaudeCode(md, agentName) {
533
- const mcpTools = CLAUDE_CODE_MCP_TOOLS[agentName] ?? [];
534
- const mcpLines = mcpTools.map((t) => ` - ${t}`).join("\n");
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");
535
558
  return md.replace(/(tools:\n(?: - (?!mcp__)[^\n]+\n)+)/, (match) => {
536
559
  const trimmed = match.trimEnd();
537
560
  return `${trimmed}
@@ -809,6 +832,30 @@ import { extname, join as join7 } from "path";
809
832
  import { serve } from "@hono/node-server";
810
833
  import { Hono } from "hono";
811
834
  import { WebSocketServer } from "ws";
835
+
836
+ // src/core/port-utils.ts
837
+ import { createServer } from "net";
838
+ function isPortFree(port) {
839
+ return new Promise((resolve13) => {
840
+ const server = createServer();
841
+ server.once("error", () => resolve13(false));
842
+ server.once("listening", () => {
843
+ server.close(() => resolve13(true));
844
+ });
845
+ server.listen(port, "127.0.0.1");
846
+ });
847
+ }
848
+ async function findFreePort(start, maxAttempts = 10) {
849
+ for (let i = 0; i < maxAttempts; i++) {
850
+ const port = start + i;
851
+ if (await isPortFree(port)) return port;
852
+ }
853
+ throw new Error(
854
+ `Could not find a free port after ${maxAttempts} attempts (tried ${start}-${start + maxAttempts - 1}). Please free a port and try again.`
855
+ );
856
+ }
857
+
858
+ // src/core/dashboard-server.ts
812
859
  var MIME = {
813
860
  ".html": "text/html; charset=utf-8",
814
861
  ".js": "application/javascript; charset=utf-8",
@@ -829,7 +876,7 @@ function fileResponse(filePath) {
829
876
  headers: { "Content-Type": mime, "Cache-Control": "no-cache" }
830
877
  });
831
878
  }
832
- function startDashboardServer(db, dbPath, staticPath, port) {
879
+ async function startDashboardServer(db, dbPath, staticPath, port) {
833
880
  const app = new Hono();
834
881
  const { tasks, actions, stats } = db;
835
882
  app.use("/api/*", async (c, next) => {
@@ -871,7 +918,10 @@ function startDashboardServer(db, dbPath, staticPath, port) {
871
918
  if (body.description !== void 0) updateParams.description = body.description;
872
919
  await db.updateTask(id, updateParams);
873
920
  if (body.acceptance !== void 0 && Array.isArray(body.acceptance)) {
874
- await db.updateTaskAcceptance(id, body.acceptance.map((a) => a.trim()).filter(Boolean));
921
+ await db.updateTaskAcceptance(
922
+ id,
923
+ body.acceptance.map((a) => a.trim()).filter(Boolean)
924
+ );
875
925
  }
876
926
  const updated = await tasks.getById(id);
877
927
  const acceptance = await tasks.getAcceptance(id);
@@ -936,7 +986,11 @@ function startDashboardServer(db, dbPath, staticPath, port) {
936
986
  }
937
987
  return fileResponse(join7(staticPath, "index.html"));
938
988
  });
939
- const httpServer = serve({ fetch: app.fetch, port });
989
+ const resolvedPort = await findFreePort(port);
990
+ if (resolvedPort !== port) {
991
+ console.log(`Port ${port} in use, using ${resolvedPort}`);
992
+ }
993
+ const httpServer = serve({ fetch: app.fetch, port: resolvedPort });
940
994
  const wss = new WebSocketServer({ noServer: true });
941
995
  httpServer.on("upgrade", (req, socket, head) => {
942
996
  if (req.url === "/ws") {
@@ -965,7 +1019,7 @@ function startDashboardServer(db, dbPath, staticPath, port) {
965
1019
  watcher = watch2(watchTarget, broadcast);
966
1020
  }
967
1021
  return {
968
- url: `http://localhost:${port}`,
1022
+ url: `http://localhost:${resolvedPort}`,
969
1023
  close: () => {
970
1024
  clearTimeout(debounce);
971
1025
  watcher?.close();
@@ -1188,8 +1242,8 @@ var TaskRepository = class {
1188
1242
  async add(params) {
1189
1243
  const now = (/* @__PURE__ */ new Date()).toISOString();
1190
1244
  return this.driver.insert(
1191
- `INSERT INTO tasks (slug, title, description, status, created_at) VALUES (?, ?, ?, ?, ?)`,
1192
- [params.slug, params.title, params.description ?? null, params.status ?? "pending", now]
1245
+ `INSERT INTO tasks (slug, title, description, status, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)`,
1246
+ [params.slug, params.title, params.description ?? null, params.status ?? "pending", now, now]
1193
1247
  );
1194
1248
  }
1195
1249
  async addAcceptance(taskId, criteria) {
@@ -1214,7 +1268,7 @@ var TaskRepository = class {
1214
1268
  if (conditions.length > 0) {
1215
1269
  sql += ` WHERE ${conditions.join(" AND ")}`;
1216
1270
  }
1217
- sql += ` ORDER BY id`;
1271
+ 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`;
1218
1272
  return this.driver.query(sql, params);
1219
1273
  }
1220
1274
  async getAllWithAcceptanceCounts(includeArchived = false) {
@@ -1228,7 +1282,7 @@ var TaskRepository = class {
1228
1282
  if (!includeArchived) {
1229
1283
  sql += ` WHERE t.archived_at IS NULL`;
1230
1284
  }
1231
- sql += ` GROUP BY t.id ORDER BY t.id`;
1285
+ 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`;
1232
1286
  return this.driver.query(sql);
1233
1287
  }
1234
1288
  async getById(id) {
@@ -1244,23 +1298,25 @@ var TaskRepository = class {
1244
1298
  );
1245
1299
  }
1246
1300
  async setStatus(id, status, extra) {
1301
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1247
1302
  if (extra?.started_at) {
1248
1303
  await this.driver.exec(
1249
- `UPDATE tasks SET status = ?, started_at = ? WHERE id = ?`,
1250
- [status, extra.started_at, id]
1304
+ `UPDATE tasks SET status = ?, started_at = ?, updated_at = ? WHERE id = ?`,
1305
+ [status, extra.started_at, now, id]
1251
1306
  );
1252
1307
  } else if (extra?.completed_at) {
1253
1308
  await this.driver.exec(
1254
- `UPDATE tasks SET status = ?, completed_at = ? WHERE id = ?`,
1255
- [status, extra.completed_at, id]
1309
+ `UPDATE tasks SET status = ?, completed_at = ?, updated_at = ? WHERE id = ?`,
1310
+ [status, extra.completed_at, now, id]
1256
1311
  );
1257
1312
  } else {
1258
- await this.driver.exec(`UPDATE tasks SET status = ? WHERE id = ?`, [status, id]);
1313
+ await this.driver.exec(`UPDATE tasks SET status = ?, updated_at = ? WHERE id = ?`, [status, now, id]);
1259
1314
  }
1260
1315
  }
1261
1316
  async update(id, params) {
1262
1317
  const sets = [];
1263
1318
  const vals = [];
1319
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1264
1320
  if (params.title !== void 0) {
1265
1321
  sets.push("title = ?");
1266
1322
  vals.push(params.title);
@@ -1274,6 +1330,8 @@ var TaskRepository = class {
1274
1330
  vals.push(params.slug);
1275
1331
  }
1276
1332
  if (sets.length === 0) return;
1333
+ sets.push("updated_at = ?");
1334
+ vals.push(now);
1277
1335
  vals.push(id);
1278
1336
  await this.driver.exec(`UPDATE tasks SET ${sets.join(", ")} WHERE id = ?`, vals);
1279
1337
  }
@@ -1288,10 +1346,11 @@ var TaskRepository = class {
1288
1346
  }
1289
1347
  async archive(id) {
1290
1348
  const now = (/* @__PURE__ */ new Date()).toISOString();
1291
- await this.driver.exec(`UPDATE tasks SET archived_at = ? WHERE id = ?`, [now, id]);
1349
+ await this.driver.exec(`UPDATE tasks SET archived_at = ?, updated_at = ? WHERE id = ?`, [now, now, id]);
1292
1350
  }
1293
1351
  async unarchive(id) {
1294
- await this.driver.exec(`UPDATE tasks SET archived_at = NULL WHERE id = ?`, [id]);
1352
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1353
+ await this.driver.exec(`UPDATE tasks SET archived_at = NULL, updated_at = ? WHERE id = ?`, [now, id]);
1295
1354
  }
1296
1355
  async getArchived() {
1297
1356
  return this.driver.query(
@@ -1300,8 +1359,8 @@ var TaskRepository = class {
1300
1359
  }
1301
1360
  async claim(id, agent, now) {
1302
1361
  return this.driver.exec(
1303
- `UPDATE tasks SET status = 'in_progress', assigned_to = ?, started_at = ? WHERE id = ? AND status = 'pending'`,
1304
- [agent, now, id]
1362
+ `UPDATE tasks SET status = 'in_progress', assigned_to = ?, started_at = ?, updated_at = ? WHERE id = ? AND status = 'pending'`,
1363
+ [agent, now, now, id]
1305
1364
  );
1306
1365
  }
1307
1366
  async markAcceptanceMet(criterionId) {
@@ -1577,13 +1636,13 @@ async function openDB(config, cwd2) {
1577
1636
  const dbConfig = config.database;
1578
1637
  let driver;
1579
1638
  if (dbConfig.type === "postgres") {
1580
- const { PostgresDriver } = await import("./postgres-6BXN7ZH4.js");
1639
+ const { PostgresDriver } = await import("./postgres-IOQE32DM.js");
1581
1640
  driver = new PostgresDriver(dbConfig);
1582
1641
  } else if (dbConfig.type === "mysql") {
1583
- const { MySQLDriver } = await import("./mysql-NXLYFD2H.js");
1642
+ const { MySQLDriver } = await import("./mysql-THKQOXIS.js");
1584
1643
  driver = new MySQLDriver(dbConfig);
1585
1644
  } else {
1586
- const { SQLiteDriver } = await import("./sqlite-M65L55DA.js");
1645
+ const { SQLiteDriver } = await import("./sqlite-XBEJJ5T2.js");
1587
1646
  if (dbConfig.type !== "sqlite") {
1588
1647
  throw new Error("Invalid database type");
1589
1648
  }
@@ -1600,7 +1659,7 @@ async function runDashboard(cwd2, opts) {
1600
1659
  const db = await openDB(config, cwd2);
1601
1660
  const dbPath = config.database.type === "sqlite" ? resolve7(cwd2, config.database.path) : null;
1602
1661
  const staticPath = join9(__dirname2, "dashboard-dist");
1603
- const { url } = startDashboardServer(db, dbPath, staticPath, opts.port);
1662
+ const { url } = await startDashboardServer(db, dbPath, staticPath, opts.port);
1604
1663
  console.log(pc2.green(`\u2713`) + ` Dashboard running at ${pc2.bold(pc2.cyan(url))}`);
1605
1664
  console.log(pc2.dim(` WebSocket live updates enabled`));
1606
1665
  console.log(pc2.dim(` Press Ctrl+C to stop`));
@@ -2414,6 +2473,17 @@ var TOOLS = [
2414
2473
  required: ["criterionId"]
2415
2474
  }
2416
2475
  },
2476
+ {
2477
+ name: "tasks.acceptance.get",
2478
+ 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).",
2479
+ inputSchema: {
2480
+ type: "object",
2481
+ properties: {
2482
+ taskId: { type: "number", description: "Task ID" }
2483
+ },
2484
+ required: ["taskId"]
2485
+ }
2486
+ },
2417
2487
  {
2418
2488
  name: "tasks.add",
2419
2489
  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.",
@@ -2590,6 +2660,11 @@ async function dispatch(name, args, db, docsPath) {
2590
2660
  await db.markAcceptanceMet(criterionId);
2591
2661
  return ok(JSON.stringify({ criterionId, met: true }));
2592
2662
  }
2663
+ case "tasks.acceptance.get": {
2664
+ const taskId = num(args, "taskId");
2665
+ const criteria = await db.getTaskAcceptance(taskId);
2666
+ return ok(JSON.stringify(criteria, null, 2));
2667
+ }
2593
2668
  case "actions.record_tool": {
2594
2669
  const actionId = str(args, "actionId");
2595
2670
  const toolName = str(args, "toolName");