@cardor/agent-harness-kit 1.6.0 → 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 CHANGED
@@ -172,6 +172,7 @@ Regenerates `AGENTS.md` and provider-specific files from your `agent-harness-kit
172
172
  ```bash
173
173
  ahk build
174
174
  ahk build --watch # watch mode: rebuilds automatically on config changes
175
+ ahk build --sync # sync tools: frontmatter in .claude/agents/*.md to match current permission constants
175
176
  ```
176
177
 
177
178
  ---
@@ -186,6 +187,8 @@ ahk dashboard --port 8080 # custom port
186
187
  ahk dashboard --no-open # start server without opening browser
187
188
  ```
188
189
 
190
+ If the requested port (default `4242`) is already in use, `ahk dashboard` automatically tries up to 10 sequential ports (e.g. `4242 → 4243 → … → 4251`). The actual port opened is printed to the console. If all 10 ports are exhausted, the command exits with a clear error message showing which port range was attempted.
191
+
189
192
  The dashboard includes:
190
193
 
191
194
  | View | What it shows |
@@ -409,6 +412,12 @@ your-project/
409
412
 
410
413
  ---
411
414
 
415
+ ## Tasks schema
416
+
417
+ The `tasks` table includes an `updated_at` timestamp column, set on creation and automatically updated on every status change. On first run after upgrading from an older version, existing rows are backfilled with `COALESCE(completed_at, started_at, created_at)`. Tasks returned by `tasks.get` are ordered by status priority (pending → in_progress → blocked → done) then by `updated_at` descending.
418
+
419
+ ---
420
+
412
421
  ## What you can customize
413
422
 
414
423
  ### `agent-harness-kit.config.ts`
@@ -574,7 +583,7 @@ The harness exposes these tools via MCP. Agents use them instead of reading file
574
583
  | `tasks.claim` | `id, agent` | Atomically claim a pending task. Returns `task_already_claimed` if another agent got it first |
575
584
  | `tasks.update` | `id, status` | Change task status |
576
585
  | `tasks.add` | `title, slug?, description?, acceptance?` | Create a new task directly from MCP (agents can queue work on the fly) |
577
- | `tasks.acceptance.update` | `criterionId` | Mark an acceptance criterion as met. Criterion IDs come from `tasks.get` |
586
+ | `tasks.acceptance.update` | `criterionId` | Mark an acceptance criterion as met. Criterion IDs come from `tasks.acceptance_get` |
578
587
  | `actions.start` | `taskId, agent` | Start a new action, returns `actionId` |
579
588
  | `actions.write` | `actionId, sectionType, content` | Record a text section: `result \| tools_used \| blockers \| next_steps`. Does **not** populate the Files dashboard — use `actions.record_file` for that |
580
589
  | `actions.complete` | `actionId, summary` | Close an action with a one-line summary |
@@ -582,6 +591,7 @@ The harness exposes these tools via MCP. Agents use them instead of reading file
582
591
  | `actions.record_file` | `actionId, filePath, operation, notes?` | Register a file touch. The **only** way to populate the Files dashboard. `operation`: `read \| created \| modified \| deleted` |
583
592
  | `actions.record_tool` | `actionId, toolName, argsJson?, resultSummary?` | Register a tool call. The **only** way to populate the Tools dashboard |
584
593
  | `docs.search` | `query` | Search the `docsPath` folder for content matching the query |
594
+ | `tasks.acceptance_get` | `taskId` | Returns all acceptance criteria for a task with their `id`, `task_id`, `criterion` text, and `met` status. Use the returned `id` values with `tasks.acceptance.update` |
585
595
 
586
596
  ---
587
597
 
@@ -594,6 +604,30 @@ The harness exposes these tools via MCP. Agents use them instead of reading file
594
604
  | **builder** | Implements the plan. Only writes to `writablePaths`. Records every file modified. |
595
605
  | **reviewer** | Verifies all acceptance criteria are met. Approves or blocks. Runs health check before approving. |
596
606
 
607
+ ### MCP tool permissions by role
608
+
609
+ Each agent role has a scoped set of MCP tools enforced through the agent definition files.
610
+
611
+ | Tool | lead | explorer | builder | reviewer |
612
+ |---|:---:|:---:|:---:|:---:|
613
+ | `tasks.get` | ✅ | ✅ | ✅ | ✅ |
614
+ | `tasks.claim` | ✅ | ✅ | ✅ | ✅ |
615
+ | `tasks.add` | ✅ | ❌ | ✅ | ✅ |
616
+ | `tasks.update` | ✅ | ❌ | ✅ | ✅ |
617
+ | `tasks.edit` | ✅ | ❌ | ✅ | ✅ |
618
+ | `tasks.archive` / `unarchive` | ✅ | ❌ | ✅ | ✅ |
619
+ | `tasks.acceptance_get` | ✅ | ✅ | ✅ | ✅ |
620
+ | `tasks.acceptance.update` | ❌ | ❌ | ❌ | ✅ |
621
+ | `actions.*` (all 6) | ✅ | ✅ | ✅ | ✅ |
622
+ | `docs.search` | ✅ | ✅ | ✅ | ✅ |
623
+ | `permissions.check` | ✅ | ✅ | ✅ | ✅ |
624
+
625
+ **explorer** is read-only for task state — can query but cannot mutate status or mark criteria.
626
+ **reviewer** is the only role that can mark acceptance criteria as met (`tasks.acceptance.update`).
627
+ **lead** and **builder** have identical access, both excluding `tasks.acceptance.update`.
628
+
629
+ `permissions.check` compares each `.claude/agents/*.md` tool list against the canonical constants in the package. Returns `{ in_sync: bool, agents: { lead, explorer, builder, reviewer } }` with per-agent `missing` and `extra` arrays. Run `ahk build --sync` to fix any drift.
630
+
597
631
  ---
598
632
 
599
633
  ## What to commit
@@ -99,13 +99,34 @@ The explorer identified how this codebase works. Use those patterns. Do not intr
99
99
 
100
100
  If tests fail, fix them before completing your action. Do not leave the codebase in a broken state.
101
101
 
102
- ### 6. Sync README and docs after codebase changes
102
+ ### 6. Sync README and docs MANDATORY
103
103
 
104
- If your changes affect public APIs, CLI commands, configuration, or any user-facing behavior, update the relevant sections of `README.md` and any files under `./docs/` to reflect the new state.
104
+ Before completing your action, you **must** check whether any user-facing behavior changed and update docs accordingly. This step is not optional.
105
105
 
106
- - Do not leave docs describing behavior that no longer exists.
107
- - Do not add implementation details that belong in code comments, not docs.
108
- - If no user-facing behavior changed, you may skip this step — but note that explicitly in your result.
106
+ **Step 1 Search actively:**
107
+ ```bash
108
+ grep -n "your-feature-keyword" README.md docs/**/*.md 2>/dev/null
109
+ ```
110
+ Search for keywords related to the files you changed (CLI commands, MCP tool names, config keys, DB columns, agent behavior). Read any matching sections.
111
+
112
+ **Step 2 — Update or justify:**
113
+ - If a matching section exists → update it to reflect the new behavior.
114
+ - If no section exists but the change is user-facing → add one in the appropriate location.
115
+ - If nothing is user-facing (internal refactor, tests only) → explicitly state that in your result section.
116
+
117
+ **What counts as user-facing:**
118
+ - New or changed CLI commands or flags
119
+ - New or changed MCP tools
120
+ - Changes to DB schema visible to users
121
+ - Changes to agent permissions or behavior
122
+ - New config options
123
+
124
+ **Step 3 — Report in your result section:**
125
+ Always end your result with one of:
126
+ - `Docs updated: README.md lines X–Y (description of what changed)`
127
+ - `No docs update needed: this change is internal only ([specific reason])`
128
+
129
+ Never leave this blank or skip it silently.
109
130
 
110
131
  ### 7. Record your result
111
132
 
@@ -87,6 +87,10 @@ bash health.sh
87
87
 
88
88
  If exit code ≠ 0 → **stop immediately**. Report the health failure and do not proceed.
89
89
 
90
+ Then call `permissions.check` — if `in_sync: false`, inform the user before proceeding:
91
+ > "Your agent permissions are outdated. Run `ahk build --sync` to update, or I can guide you."
92
+ Wait for the user to acknowledge before continuing the session.
93
+
90
94
  Then check session state via MCP:
91
95
 
92
96
  ```
@@ -134,6 +138,7 @@ Think through:
134
138
  - What exactly should the builder implement?
135
139
  - What are the acceptance criteria the reviewer will check?
136
140
  - If codebase changes are involved: does the builder need to update README or `docs/` files?
141
+ - Does this task touch user-facing behavior (CLI commands, MCP tools, DB schema, config, agent permissions)? If yes, add an acceptance criterion: `README.md and/or docs/ updated to reflect the change`
137
142
 
138
143
  Record it:
139
144
 
package/dist/cli.js CHANGED
@@ -589,6 +589,32 @@ function appendGitignore(cwd2) {
589
589
  function slugify(title) {
590
590
  return title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64);
591
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
+ }
592
618
 
593
619
  // src/core/materializer/claude-code.ts
594
620
  var ClaudeCodeMaterializer = class {
@@ -789,6 +815,9 @@ function getMaterializer(provider) {
789
815
  // src/commands/build.ts
790
816
  async function runBuild(cwd2, opts) {
791
817
  await buildOnce(cwd2);
818
+ if (opts.sync) {
819
+ await syncAgentPermissions(cwd2);
820
+ }
792
821
  if (opts.watch) {
793
822
  p.log.info(`Watching agent-harness-kit.config.ts for changes...`);
794
823
  watch(cwd2, { recursive: false }, async (_, filename) => {
@@ -1642,7 +1671,7 @@ async function openDB(config, cwd2) {
1642
1671
  const { MySQLDriver } = await import("./mysql-THKQOXIS.js");
1643
1672
  driver = new MySQLDriver(dbConfig);
1644
1673
  } else {
1645
- const { SQLiteDriver } = await import("./sqlite-XBEJJ5T2.js");
1674
+ const { SQLiteDriver } = await import("./sqlite-KWYK4IJW.js");
1646
1675
  if (dbConfig.type !== "sqlite") {
1647
1676
  throw new Error("Invalid database type");
1648
1677
  }
@@ -2324,14 +2353,56 @@ async function runReset(cwd2, opts) {
2324
2353
  }
2325
2354
 
2326
2355
  // src/core/mcp-server.ts
2327
- import { readdirSync as readdirSync2, readFileSync as readFileSync6, statSync } from "fs";
2328
- 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";
2329
2358
  import { Server } from "@modelcontextprotocol/sdk/server";
2330
2359
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
2331
2360
  import {
2332
2361
  CallToolRequestSchema,
2333
2362
  ListToolsRequestSchema
2334
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
2335
2406
  var VERSION = "0.1.0";
2336
2407
  var TOOLS = [
2337
2408
  {
@@ -2555,6 +2626,11 @@ var TOOLS = [
2555
2626
  },
2556
2627
  required: ["id"]
2557
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: [] }
2558
2634
  }
2559
2635
  ];
2560
2636
  async function startMcpServer(config, cwd2) {
@@ -2569,7 +2645,7 @@ async function startMcpServer(config, cwd2) {
2569
2645
  const { name, arguments: args } = request.params;
2570
2646
  const a = args ?? {};
2571
2647
  try {
2572
- const result = await dispatch(name, a, db, docsPath);
2648
+ const result = await dispatch(name, a, db, docsPath, cwd2);
2573
2649
  return result;
2574
2650
  } catch (err) {
2575
2651
  return ok(`Error: ${err instanceof Error ? err.message : String(err)}`, true);
@@ -2578,7 +2654,7 @@ async function startMcpServer(config, cwd2) {
2578
2654
  const transport = new StdioServerTransport();
2579
2655
  await server.connect(transport);
2580
2656
  }
2581
- async function dispatch(name, args, db, docsPath) {
2657
+ async function dispatch(name, args, db, docsPath, cwd2) {
2582
2658
  switch (name) {
2583
2659
  case "actions.start": {
2584
2660
  const taskId = num(args, "taskId");
@@ -2697,6 +2773,10 @@ async function dispatch(name, args, db, docsPath) {
2697
2773
  const task2 = await db.unarchiveTask(id);
2698
2774
  return ok(JSON.stringify(task2));
2699
2775
  }
2776
+ case "permissions.check": {
2777
+ const result = checkPermissionsSync(cwd2);
2778
+ return ok(JSON.stringify(result, null, 2));
2779
+ }
2700
2780
  default:
2701
2781
  return ok(`Unknown tool: ${name}`, true);
2702
2782
  }
@@ -2709,7 +2789,7 @@ function searchDocs(docsPath, query, maxResults = 10) {
2709
2789
  for (const file of files) {
2710
2790
  if (results.length >= maxResults) break;
2711
2791
  try {
2712
- const content = readFileSync6(file, "utf8");
2792
+ const content = readFileSync7(file, "utf8");
2713
2793
  const lines = content.split("\n");
2714
2794
  for (let i = 0; i < lines.length; i++) {
2715
2795
  const lower = lines[i].toLowerCase();
@@ -2730,7 +2810,7 @@ function collectMarkdownFiles(dir) {
2730
2810
  const files = [];
2731
2811
  try {
2732
2812
  for (const entry of readdirSync2(dir)) {
2733
- const full = join14(dir, entry);
2813
+ const full = join15(dir, entry);
2734
2814
  const stat = statSync(full);
2735
2815
  if (stat.isDirectory()) {
2736
2816
  files.push(...collectMarkdownFiles(full));
@@ -2764,6 +2844,18 @@ async function runServe(cwd2, opts) {
2764
2844
  }
2765
2845
  process.stderr.write(`[agent-harness-kit] MCP server starting (stdio)
2766
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
+ }
2767
2859
  await startMcpServer(config, cwd2);
2768
2860
  }
2769
2861
 
@@ -2842,13 +2934,13 @@ async function runStatus(cwd2, opts) {
2842
2934
  }
2843
2935
 
2844
2936
  // src/commands/sync.ts
2845
- import { existsSync as existsSync10, readFileSync as readFileSync7 } from "fs";
2846
- 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";
2847
2939
  import pc10 from "picocolors";
2848
2940
  async function runSync(cwd2, opts) {
2849
2941
  const config = await loadConfig(cwd2);
2850
2942
  const direction = opts.direction ?? "both";
2851
- const featureListPath = resolve11(join15(cwd2, config.storage.dir, "feature_list.json"));
2943
+ const featureListPath = resolve11(join16(cwd2, config.storage.dir, "feature_list.json"));
2852
2944
  const db = await openDB(config, cwd2);
2853
2945
  try {
2854
2946
  if (direction === "in" || direction === "both") {
@@ -2862,13 +2954,13 @@ async function runSync(cwd2, opts) {
2862
2954
  }
2863
2955
  }
2864
2956
  async function syncIn(featureListPath, db, dryRun) {
2865
- if (!existsSync10(featureListPath)) {
2957
+ if (!existsSync11(featureListPath)) {
2866
2958
  console.log(pc10.dim(`feature_list.json not found at ${featureListPath} \u2014 skipping in-sync`));
2867
2959
  return;
2868
2960
  }
2869
2961
  let seeds;
2870
2962
  try {
2871
- seeds = JSON.parse(readFileSync7(featureListPath, "utf8"));
2963
+ seeds = JSON.parse(readFileSync8(featureListPath, "utf8"));
2872
2964
  } catch (err) {
2873
2965
  console.error(pc10.red(`Failed to parse feature_list.json: ${err}`));
2874
2966
  process.exit(1);
@@ -2953,14 +3045,14 @@ async function runTaskAdd(cwd2) {
2953
3045
 
2954
3046
  // src/commands/task/done.ts
2955
3047
  import { spawnSync as spawnSync2 } from "child_process";
2956
- import { existsSync as existsSync11 } from "fs";
3048
+ import { existsSync as existsSync12 } from "fs";
2957
3049
  import { resolve as resolve12 } from "path";
2958
3050
  import pc12 from "picocolors";
2959
3051
  async function runTaskDone(cwd2, idOrSlug) {
2960
3052
  const config = await loadConfig(cwd2);
2961
3053
  if (config.health.required) {
2962
3054
  const scriptPath = resolve12(cwd2, config.health.scriptPath);
2963
- if (existsSync11(scriptPath)) {
3055
+ if (existsSync12(scriptPath)) {
2964
3056
  const result = spawnSync2("bash", [scriptPath], { cwd: cwd2, stdio: "pipe", encoding: "utf8" });
2965
3057
  if (result.status !== 0) {
2966
3058
  console.error(pc12.red("\u2717 Health check failed \u2014 cannot mark task as done."));
@@ -3133,10 +3225,10 @@ async function runTaskList(cwd2, opts) {
3133
3225
 
3134
3226
  // src/core/package-data.ts
3135
3227
  import { createRequire } from "module";
3136
- import { dirname as dirname5, join as join16 } from "path";
3228
+ import { dirname as dirname5, join as join17 } from "path";
3137
3229
  import { fileURLToPath as fileURLToPath3 } from "url";
3138
3230
  var require2 = createRequire(import.meta.url);
3139
- var pkgPath = join16(dirname5(fileURLToPath3(import.meta.url)), "..", "package.json");
3231
+ var pkgPath = join17(dirname5(fileURLToPath3(import.meta.url)), "..", "package.json");
3140
3232
  var pkg = require2(pkgPath);
3141
3233
 
3142
3234
  // src/core/update-check.ts
@@ -3180,7 +3272,7 @@ program.name("ahk").description("agent-harness-kit \u2014 CLI scaffolding for mu
3180
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) => {
3181
3273
  await runInit(cwd, opts);
3182
3274
  });
3183
- 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) => {
3184
3276
  await runBuild(cwd, opts);
3185
3277
  });
3186
3278
  program.command("health").description("Run health.sh and report result").action(async () => {