@cardor/agent-harness-kit 1.5.2 → 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_CLAUDE_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_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-kit__tasks_acceptance_update",
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, _agentName) {
495
- const mcpLines = MCP_CLAUDE_PERMISSIONS.map((t) => ` - ${t}`).join("\n");
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}
@@ -770,6 +832,30 @@ import { extname, join as join7 } from "path";
770
832
  import { serve } from "@hono/node-server";
771
833
  import { Hono } from "hono";
772
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
773
859
  var MIME = {
774
860
  ".html": "text/html; charset=utf-8",
775
861
  ".js": "application/javascript; charset=utf-8",
@@ -790,7 +876,7 @@ function fileResponse(filePath) {
790
876
  headers: { "Content-Type": mime, "Cache-Control": "no-cache" }
791
877
  });
792
878
  }
793
- function startDashboardServer(db, dbPath, staticPath, port) {
879
+ async function startDashboardServer(db, dbPath, staticPath, port) {
794
880
  const app = new Hono();
795
881
  const { tasks, actions, stats } = db;
796
882
  app.use("/api/*", async (c, next) => {
@@ -832,7 +918,10 @@ function startDashboardServer(db, dbPath, staticPath, port) {
832
918
  if (body.description !== void 0) updateParams.description = body.description;
833
919
  await db.updateTask(id, updateParams);
834
920
  if (body.acceptance !== void 0 && Array.isArray(body.acceptance)) {
835
- 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
+ );
836
925
  }
837
926
  const updated = await tasks.getById(id);
838
927
  const acceptance = await tasks.getAcceptance(id);
@@ -897,7 +986,11 @@ function startDashboardServer(db, dbPath, staticPath, port) {
897
986
  }
898
987
  return fileResponse(join7(staticPath, "index.html"));
899
988
  });
900
- 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 });
901
994
  const wss = new WebSocketServer({ noServer: true });
902
995
  httpServer.on("upgrade", (req, socket, head) => {
903
996
  if (req.url === "/ws") {
@@ -926,7 +1019,7 @@ function startDashboardServer(db, dbPath, staticPath, port) {
926
1019
  watcher = watch2(watchTarget, broadcast);
927
1020
  }
928
1021
  return {
929
- url: `http://localhost:${port}`,
1022
+ url: `http://localhost:${resolvedPort}`,
930
1023
  close: () => {
931
1024
  clearTimeout(debounce);
932
1025
  watcher?.close();
@@ -1149,8 +1242,8 @@ var TaskRepository = class {
1149
1242
  async add(params) {
1150
1243
  const now = (/* @__PURE__ */ new Date()).toISOString();
1151
1244
  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]
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]
1154
1247
  );
1155
1248
  }
1156
1249
  async addAcceptance(taskId, criteria) {
@@ -1175,7 +1268,7 @@ var TaskRepository = class {
1175
1268
  if (conditions.length > 0) {
1176
1269
  sql += ` WHERE ${conditions.join(" AND ")}`;
1177
1270
  }
1178
- 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`;
1179
1272
  return this.driver.query(sql, params);
1180
1273
  }
1181
1274
  async getAllWithAcceptanceCounts(includeArchived = false) {
@@ -1189,7 +1282,7 @@ var TaskRepository = class {
1189
1282
  if (!includeArchived) {
1190
1283
  sql += ` WHERE t.archived_at IS NULL`;
1191
1284
  }
1192
- 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`;
1193
1286
  return this.driver.query(sql);
1194
1287
  }
1195
1288
  async getById(id) {
@@ -1205,23 +1298,25 @@ var TaskRepository = class {
1205
1298
  );
1206
1299
  }
1207
1300
  async setStatus(id, status, extra) {
1301
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1208
1302
  if (extra?.started_at) {
1209
1303
  await this.driver.exec(
1210
- `UPDATE tasks SET status = ?, started_at = ? WHERE id = ?`,
1211
- [status, extra.started_at, id]
1304
+ `UPDATE tasks SET status = ?, started_at = ?, updated_at = ? WHERE id = ?`,
1305
+ [status, extra.started_at, now, id]
1212
1306
  );
1213
1307
  } else if (extra?.completed_at) {
1214
1308
  await this.driver.exec(
1215
- `UPDATE tasks SET status = ?, completed_at = ? WHERE id = ?`,
1216
- [status, extra.completed_at, id]
1309
+ `UPDATE tasks SET status = ?, completed_at = ?, updated_at = ? WHERE id = ?`,
1310
+ [status, extra.completed_at, now, id]
1217
1311
  );
1218
1312
  } else {
1219
- 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]);
1220
1314
  }
1221
1315
  }
1222
1316
  async update(id, params) {
1223
1317
  const sets = [];
1224
1318
  const vals = [];
1319
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1225
1320
  if (params.title !== void 0) {
1226
1321
  sets.push("title = ?");
1227
1322
  vals.push(params.title);
@@ -1235,6 +1330,8 @@ var TaskRepository = class {
1235
1330
  vals.push(params.slug);
1236
1331
  }
1237
1332
  if (sets.length === 0) return;
1333
+ sets.push("updated_at = ?");
1334
+ vals.push(now);
1238
1335
  vals.push(id);
1239
1336
  await this.driver.exec(`UPDATE tasks SET ${sets.join(", ")} WHERE id = ?`, vals);
1240
1337
  }
@@ -1249,10 +1346,11 @@ var TaskRepository = class {
1249
1346
  }
1250
1347
  async archive(id) {
1251
1348
  const now = (/* @__PURE__ */ new Date()).toISOString();
1252
- 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]);
1253
1350
  }
1254
1351
  async unarchive(id) {
1255
- 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]);
1256
1354
  }
1257
1355
  async getArchived() {
1258
1356
  return this.driver.query(
@@ -1261,8 +1359,8 @@ var TaskRepository = class {
1261
1359
  }
1262
1360
  async claim(id, agent, now) {
1263
1361
  return this.driver.exec(
1264
- `UPDATE tasks SET status = 'in_progress', assigned_to = ?, started_at = ? WHERE id = ? AND status = 'pending'`,
1265
- [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]
1266
1364
  );
1267
1365
  }
1268
1366
  async markAcceptanceMet(criterionId) {
@@ -1538,13 +1636,13 @@ async function openDB(config, cwd2) {
1538
1636
  const dbConfig = config.database;
1539
1637
  let driver;
1540
1638
  if (dbConfig.type === "postgres") {
1541
- const { PostgresDriver } = await import("./postgres-6BXN7ZH4.js");
1639
+ const { PostgresDriver } = await import("./postgres-IOQE32DM.js");
1542
1640
  driver = new PostgresDriver(dbConfig);
1543
1641
  } else if (dbConfig.type === "mysql") {
1544
- const { MySQLDriver } = await import("./mysql-NXLYFD2H.js");
1642
+ const { MySQLDriver } = await import("./mysql-THKQOXIS.js");
1545
1643
  driver = new MySQLDriver(dbConfig);
1546
1644
  } else {
1547
- const { SQLiteDriver } = await import("./sqlite-M65L55DA.js");
1645
+ const { SQLiteDriver } = await import("./sqlite-XBEJJ5T2.js");
1548
1646
  if (dbConfig.type !== "sqlite") {
1549
1647
  throw new Error("Invalid database type");
1550
1648
  }
@@ -1561,7 +1659,7 @@ async function runDashboard(cwd2, opts) {
1561
1659
  const db = await openDB(config, cwd2);
1562
1660
  const dbPath = config.database.type === "sqlite" ? resolve7(cwd2, config.database.path) : null;
1563
1661
  const staticPath = join9(__dirname2, "dashboard-dist");
1564
- const { url } = startDashboardServer(db, dbPath, staticPath, opts.port);
1662
+ const { url } = await startDashboardServer(db, dbPath, staticPath, opts.port);
1565
1663
  console.log(pc2.green(`\u2713`) + ` Dashboard running at ${pc2.bold(pc2.cyan(url))}`);
1566
1664
  console.log(pc2.dim(` WebSocket live updates enabled`));
1567
1665
  console.log(pc2.dim(` Press Ctrl+C to stop`));
@@ -2375,6 +2473,17 @@ var TOOLS = [
2375
2473
  required: ["criterionId"]
2376
2474
  }
2377
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
+ },
2378
2487
  {
2379
2488
  name: "tasks.add",
2380
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.",
@@ -2551,6 +2660,11 @@ async function dispatch(name, args, db, docsPath) {
2551
2660
  await db.markAcceptanceMet(criterionId);
2552
2661
  return ok(JSON.stringify({ criterionId, met: true }));
2553
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
+ }
2554
2668
  case "actions.record_tool": {
2555
2669
  const actionId = str(args, "actionId");
2556
2670
  const toolName = str(args, "toolName");