@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/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}
@@ -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(id, body.acceptance.map((a) => a.trim()).filter(Boolean));
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 httpServer = serve({ fetch: app.fetch, port });
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:${port}`,
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 id`;
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.id`;
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
- await this.driver.exec(`UPDATE tasks SET archived_at = NULL WHERE id = ?`, [id]);
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-6BXN7ZH4.js");
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-NXLYFD2H.js");
1671
+ const { MySQLDriver } = await import("./mysql-THKQOXIS.js");
1545
1672
  driver = new MySQLDriver(dbConfig);
1546
1673
  } else {
1547
- const { SQLiteDriver } = await import("./sqlite-M65L55DA.js");
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 readFileSync6, statSync } from "fs";
2230
- import { join as join14, resolve as resolve10 } from "path";
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 = readFileSync6(file, "utf8");
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 = join14(dir, entry);
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 existsSync10, readFileSync as readFileSync7 } from "fs";
2732
- import { join as join15, resolve as resolve11 } from "path";
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(join15(cwd2, config.storage.dir, "feature_list.json"));
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 (!existsSync10(featureListPath)) {
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(readFileSync7(featureListPath, "utf8"));
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 existsSync11 } from "fs";
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 (existsSync11(scriptPath)) {
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 join16 } from "path";
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 = join16(dirname5(fileURLToPath3(import.meta.url)), "..", "package.json");
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 () => {