kanbot 0.3.0 → 0.4.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.
Files changed (53) hide show
  1. package/README.md +8 -8
  2. package/package.json +1 -1
  3. package/schema/config.schema.json +38 -8
  4. package/src/agents/load.ts +11 -0
  5. package/src/agents/sync.ts +8 -8
  6. package/src/capabilities.ts +157 -0
  7. package/src/commands/board.ts +14 -7
  8. package/src/commands/doctor.ts +80 -6
  9. package/src/commands/init.ts +15 -3
  10. package/src/commands/sync.ts +1 -1
  11. package/src/commands/upgrade.ts +20 -1
  12. package/src/config.ts +38 -8
  13. package/src/daemon/derive.ts +41 -0
  14. package/src/daemon/loop.ts +2 -0
  15. package/src/jobs/prompts.ts +25 -5
  16. package/src/jobs/run.ts +61 -22
  17. package/src/known-hashes.json +42 -57
  18. package/src/providers/claude.ts +40 -30
  19. package/src/providers/codex.ts +60 -30
  20. package/src/providers/mcp.ts +71 -0
  21. package/src/schemas/results.ts +2 -5
  22. package/templates/.agents/skills/approach-designer/SKILL.md +1 -1
  23. package/templates/.agents/skills/kanbot-admin/SKILL.md +21 -4
  24. package/templates/.agents/skills/kanbot-guide/SKILL.md +26 -7
  25. package/templates/.agents/skills/kanbot-idea/SKILL.md +1 -1
  26. package/templates/.agents/skills/plan-template/SKILL.md +14 -6
  27. package/templates/.agents/skills/pr-description/SKILL.md +7 -3
  28. package/templates/.agents/skills/project-conventions/SKILL.md +23 -35
  29. package/templates/.agents/skills/review-findings/SKILL.md +47 -0
  30. package/templates/.agents/skills/spec-review/SKILL.md +10 -27
  31. package/templates/.agents/skills/user-stories/SKILL.md +1 -1
  32. package/templates/.github/pull_request_template.md +6 -0
  33. package/templates/.kanbot/agents/acceptance-reviewer.md +43 -0
  34. package/templates/.kanbot/agents/fixer.md +7 -5
  35. package/templates/.kanbot/agents/implementer.md +49 -0
  36. package/templates/.kanbot/agents/plan-critic.md +3 -3
  37. package/templates/.kanbot/agents/planner.md +10 -9
  38. package/templates/.kanbot/agents/reviewer.md +42 -0
  39. package/templates/AGENTS.md +2 -3
  40. package/templates/.agents/skills/accessibility/SKILL.md +0 -76
  41. package/templates/.agents/skills/code-review/SKILL.md +0 -83
  42. package/templates/.agents/skills/frontend-design/LICENSE.txt +0 -177
  43. package/templates/.agents/skills/frontend-design/SKILL.md +0 -76
  44. package/templates/.agents/skills/tdd/SKILL.md +0 -51
  45. package/templates/.agents/skills/verify-ui/SKILL.md +0 -104
  46. package/templates/.kanbot/agents/implementer-backend.md +0 -57
  47. package/templates/.kanbot/agents/implementer-docs.md +0 -55
  48. package/templates/.kanbot/agents/implementer-frontend.md +0 -54
  49. package/templates/.kanbot/agents/implementer-technical.md +0 -53
  50. package/templates/.kanbot/agents/reviewer-code.md +0 -46
  51. package/templates/.kanbot/agents/reviewer-spec.md +0 -49
  52. package/templates/.kanbot/agents/test-writer.md +0 -35
  53. package/templates/.kanbot/agents/ui-verifier.md +0 -35
@@ -76,6 +76,7 @@ export type Action =
76
76
  | { kind: "merge"; item: Item; pr: PR; by: string }
77
77
  | { kind: "compare"; item: Item; prs: PR[] }
78
78
  | { kind: "needsHuman"; number: number; why: string }
79
+ | { kind: "openQuestions"; item: Item; questions: string[] }
79
80
  | { kind: "resetFailures"; prefix: string }
80
81
  | { kind: "cleanup"; number: number };
81
82
 
@@ -178,6 +179,36 @@ export function approvedBy(
178
179
  );
179
180
  }
180
181
 
182
+ const OPEN_QUESTIONS = /^##\s+Open questions\s*$[\s\S]*?(?=^##\s|(?![\s\S]))/im;
183
+
184
+ /** The plan with its "## Open questions" section set to `questions`, last. */
185
+ export function withOpenQuestions(plan: string, questions: string[]): string {
186
+ const rest = plan.replace(OPEN_QUESTIONS, "").trimEnd();
187
+ if (!questions.length) return rest;
188
+ return `${rest}\n\n## Open questions\n\n${questions.map((q) => `- ${q}`).join("\n")}`;
189
+ }
190
+
191
+ /** Bullets under the plan's "## Open questions". */
192
+ export function openQuestions(plan: string | null): string[] {
193
+ const m = plan?.match(OPEN_QUESTIONS);
194
+ if (!m) return [];
195
+ return m[0]
196
+ .split("\n")
197
+ .slice(1)
198
+ .map((l) => l.match(/^\s*[-*]\s+(.+)$/)?.[1]?.trim())
199
+ .filter((q): q is string => Boolean(q));
200
+ }
201
+
202
+ /** Was the open-questions gate already posted for the latest plan? */
203
+ function gatePosted(comments: Comment[]) {
204
+ for (let i = comments.length - 1; i >= 0; i--) {
205
+ const body = (comments[i] as Comment).body;
206
+ if (hasMarker(body, "questions")) return true;
207
+ if (hasMarker(body, "plan")) return false;
208
+ }
209
+ return false;
210
+ }
211
+
181
212
  const fixCount = (pr: PR, kind: "ci" | "conflict") =>
182
213
  pr.comments.reduce((n, c) => n + markerValues(c.body, kind).length, 0);
183
214
 
@@ -228,6 +259,16 @@ export function derive(snapshot: Snapshot, ctx: DeriveContext) {
228
259
 
229
260
  case "ready":
230
261
  case "inProgress": {
262
+ // Open questions keep a plan out of Ready. Once started, it runs on.
263
+ const questions =
264
+ item.column === "ready" && !issue.prs.length
265
+ ? openQuestions(extractPlan(issue.body))
266
+ : [];
267
+ if (questions.length) {
268
+ if (!gatePosted(issue.comments))
269
+ actions.push({ kind: "openQuestions", item, questions });
270
+ break;
271
+ }
231
272
  const n = exploreCount(issue.labels);
232
273
  const variants =
233
274
  n === null ? [null] : Array.from({ length: n }, (_, i) => i + 1);
@@ -211,6 +211,8 @@ function describeAction(a: Action) {
211
211
  return `post comparison on #${a.item.issue.number}`;
212
212
  case "needsHuman":
213
213
  return `needs-human #${a.number}`;
214
+ case "openQuestions":
215
+ return `open questions on #${a.item.issue.number}`;
214
216
  case "resetFailures":
215
217
  return `reset failures ${a.prefix}`;
216
218
  case "cleanup":
@@ -1,3 +1,4 @@
1
+ import { type Capabilities, capabilitiesSection } from "../capabilities.ts";
1
2
  import {
2
3
  type FixJob,
3
4
  type ImplementJob,
@@ -21,6 +22,8 @@ const RULES = `## Ground rules
21
22
  - Your final answer must be the structured JSON result. Put everything the human should see in it.
22
23
  - Write short and plain: only what the reader needs to act or understand. Don't restate the input or narrate your process.`;
23
24
 
25
+ const VALIDATION = "List what you used under Validation in the PR body.";
26
+
24
27
  function quote(text: string) {
25
28
  return text.trim() ? text.trim() : "_(empty)_";
26
29
  }
@@ -109,7 +112,11 @@ export function planPrompt(job: PlanJob, explore: number | null) {
109
112
  return parts.join("\n\n");
110
113
  }
111
114
 
112
- export function implementPrompt(job: ImplementJob, base: string) {
115
+ export function implementPrompt(
116
+ job: ImplementJob,
117
+ base: string,
118
+ capabilities: Capabilities = {},
119
+ ) {
113
120
  const { issue } = job.item;
114
121
  const parts = [
115
122
  issueHeader(issue),
@@ -126,13 +133,18 @@ export function implementPrompt(job: ImplementJob, base: string) {
126
133
  `## Recent human comments on the issue\n${conversation(humanNotes)}`,
127
134
  );
128
135
  parts.push(
136
+ capabilitiesSection(capabilities, VALIDATION),
129
137
  `## Workspace\n- Branch \`${job.branch}\`, created from \`origin/${base}\`.\n- Commit your work with conventional commit messages. Uncommitted changes are committed for you, but prefer small commits.\n- Run the project's tests and linters before finishing and report them in \`testsRun\`.`,
130
138
  RULES,
131
139
  );
132
- return parts.join("\n\n");
140
+ return parts.filter(Boolean).join("\n\n");
133
141
  }
134
142
 
135
- export function reviewPrompt(job: ReviewJob, base: string) {
143
+ export function reviewPrompt(
144
+ job: ReviewJob,
145
+ base: string,
146
+ capabilities: Capabilities = {},
147
+ ) {
136
148
  const { issue } = job.item;
137
149
  const { pr } = job;
138
150
  return [
@@ -140,7 +152,8 @@ export function reviewPrompt(job: ReviewJob, base: string) {
140
152
  `## Plan / acceptance criteria\n${extractPlan(issue.body) ?? stripPlan(issue.body)}`,
141
153
  `## Pull request #${pr.number}: ${pr.title}\nBranch \`${pr.headRefName}\` (checked out). Diff with: \`git diff origin/${base}...HEAD\`. CI status: ${pr.checks ?? "none"}.\n\n### PR description\n${quote(stripMarkers(pr.body))}`,
142
154
  humanDecisions(pr),
143
- `## Review pass ${job.pass} of ${reviewPasses(issue).length}\nYou only review. Do not edit files or commit.`,
155
+ capabilitiesSection(capabilities),
156
+ `## Review pass ${job.pass} of ${reviewPasses(issue).length}: ${job.pass === 1 ? "technical" : "acceptance"}\nYou only review. Do not edit files or commit.${job.pass === 2 && Object.values(capabilities).some((c) => c.required) ? " A required capability that applies but is missing from the PR body's Validation section is a blocker." : ""}`,
144
157
  RULES,
145
158
  ]
146
159
  .filter(Boolean)
@@ -148,7 +161,12 @@ export function reviewPrompt(job: ReviewJob, base: string) {
148
161
  }
149
162
 
150
163
  /** `details`: CI logs for a CI fix, the conflicted files for a conflict fix. */
151
- export function fixPrompt(job: FixJob, base: string, details: string | null) {
164
+ export function fixPrompt(
165
+ job: FixJob,
166
+ base: string,
167
+ details: string | null,
168
+ capabilities: Capabilities = {},
169
+ ) {
152
170
  const { issue } = job.item;
153
171
  const { pr } = job;
154
172
  const parts = [
@@ -156,6 +174,8 @@ export function fixPrompt(job: FixJob, base: string, details: string | null) {
156
174
  `## Plan / acceptance criteria\n${extractPlan(issue.body) ?? stripPlan(issue.body)}`,
157
175
  `## Pull request #${pr.number}: ${pr.title}\nBranch \`${pr.headRefName}\` (checked out). Diff with: \`git diff origin/${base}...HEAD\`.\n\n### Current PR description\n${quote(stripMarkers(pr.body))}`,
158
176
  ];
177
+ const caps = capabilitiesSection(capabilities, VALIDATION);
178
+ if (caps) parts.push(caps);
159
179
  const r = job.reason;
160
180
  const decisions = r.type === "feedback" ? null : humanDecisions(pr);
161
181
  if (decisions) parts.push(decisions);
package/src/jobs/run.ts CHANGED
@@ -13,6 +13,7 @@ import {
13
13
  type ReviewJob,
14
14
  reviewPasses,
15
15
  variantOf,
16
+ withOpenQuestions,
16
17
  } from "../daemon/derive.ts";
17
18
  import type { Worktrees } from "../daemon/git.ts";
18
19
  import type { Notifier } from "../daemon/notify.ts";
@@ -168,11 +169,13 @@ async function plan(job: PlanJob, deps: JobDeps) {
168
169
 
169
170
  // Re-read so concurrent human edits to the description are kept.
170
171
  const fresh = await fetchIssue(gh, issue.number);
171
- await updateIssueBody(bot, issue.number, withPlan(fresh.body, result.plan));
172
-
173
- const allowed = result.labels.filter(
174
- (l) => l === LABELS.technical || l.startsWith(LABELS.typePrefix),
172
+ await updateIssueBody(
173
+ bot,
174
+ issue.number,
175
+ withPlan(fresh.body, withOpenQuestions(result.plan, result.questions)),
175
176
  );
177
+
178
+ const allowed = result.labels.filter((l) => l === LABELS.technical);
176
179
  await addLabels(
177
180
  bot,
178
181
  issue.number,
@@ -208,7 +211,9 @@ async function plan(job: PlanJob, deps: JobDeps) {
208
211
  );
209
212
  }
210
213
  lines.push(
211
- "_Comment to refine. Drag the card to **Ready** when the plan is approved._",
214
+ result.questions.length
215
+ ? "_Answer the questions to refine. kanbot won't start while the plan has open questions._"
216
+ : "_Comment to refine. Drag the card to **Ready** when the plan is approved._",
212
217
  );
213
218
  lines.push(
214
219
  [marker("plan"), ...job.comments.map((c) => marker("re", c.id))].join("\n"),
@@ -219,6 +224,28 @@ async function plan(job: PlanJob, deps: JobDeps) {
219
224
 
220
225
  // ---- implement ----------------------------------------------------------------
221
226
 
227
+ /**
228
+ * Always `implementer`. The deprecated `implementers` map still routes by
229
+ * type:<key> label, and `implementer-backend` serves installs not yet upgraded.
230
+ */
231
+ export function implementerFor(
232
+ config: Config,
233
+ agents: AgentDef[],
234
+ labels: string[],
235
+ ): string {
236
+ if (config.implementers) {
237
+ const type = labels
238
+ .find((l) => l.startsWith(LABELS.typePrefix))
239
+ ?.slice(LABELS.typePrefix.length);
240
+ const name =
241
+ config.implementers[type ?? "default"] ?? config.implementers.default;
242
+ if (name) return name;
243
+ }
244
+ return agents.some((a) => a.name === "implementer")
245
+ ? "implementer"
246
+ : "implementer-backend";
247
+ }
248
+
222
249
  async function implement(job: ImplementJob, deps: JobDeps) {
223
250
  const { gh, bot, config, worktrees } = deps;
224
251
  const { issue } = job.item;
@@ -231,20 +258,14 @@ async function implement(job: ImplementJob, deps: JobDeps) {
231
258
  const cwd = await worktrees.checkout(worktreeName(job), job.branch);
232
259
  let result: z.infer<typeof ImplementResult> | null = null;
233
260
  if (!resumed) {
234
- const typeLabel = issue.labels
235
- .find((l) => l.startsWith(LABELS.typePrefix))
236
- ?.slice(LABELS.typePrefix.length);
237
- const agentName =
238
- config.implementers[typeLabel ?? "default"] ??
239
- config.implementers.default ??
240
- "implementer-backend";
261
+ const agentName = implementerFor(config, deps.agents, issue.labels);
241
262
  result = await runAgent(
242
263
  deps,
243
264
  job,
244
265
  agentName,
245
266
  "implement-result",
246
267
  cwd,
247
- implementPrompt(job, config.baseBranch),
268
+ implementPrompt(job, config.baseBranch, config.capabilities),
248
269
  );
249
270
  if (result.status === "blocked") {
250
271
  await addLabels(bot, issue.number, [LABELS.needsHuman]);
@@ -306,9 +327,7 @@ async function implement(job: ImplementJob, deps: JobDeps) {
306
327
  base: config.baseBranch,
307
328
  });
308
329
  if (pr) {
309
- const labels = issue.labels.filter(
310
- (l) => l.startsWith(LABELS.typePrefix) || l === LABELS.technical,
311
- );
330
+ const labels: string[] = issue.labels.filter((l) => l === LABELS.technical);
312
331
  if (job.variant !== null)
313
332
  labels.push(`${LABELS.variantPrefix}${job.variant}`);
314
333
  await addLabels(bot, pr.number, labels);
@@ -336,8 +355,17 @@ export function absoluteImages(md: string, repo: string, branch: string) {
336
355
 
337
356
  // ---- review -------------------------------------------------------------------
338
357
 
339
- const PASS_AGENT = { 1: "reviewer-code", 2: "reviewer-spec" } as const;
340
- const PASS_NAME = { 1: "code", 2: "spec" } as const;
358
+ /** Agent per review pass. The second name serves installs not yet upgraded. */
359
+ export const PASS_AGENTS = {
360
+ 1: ["reviewer", "reviewer-code"],
361
+ 2: ["acceptance-reviewer", "reviewer-spec"],
362
+ } as const;
363
+ const PASS_NAME = { 1: "technical", 2: "acceptance" } as const;
364
+
365
+ export function passAgent(agents: AgentDef[], pass: 1 | 2): string {
366
+ const [name, legacy] = PASS_AGENTS[pass];
367
+ return agents.some((a) => a.name === name) ? name : legacy;
368
+ }
341
369
 
342
370
  async function review(job: ReviewJob, deps: JobDeps) {
343
371
  const cwd = await deps.worktrees.checkout(
@@ -347,10 +375,10 @@ async function review(job: ReviewJob, deps: JobDeps) {
347
375
  const result = await runAgent(
348
376
  deps,
349
377
  job,
350
- PASS_AGENT[job.pass],
378
+ passAgent(deps.agents, job.pass),
351
379
  "review-result",
352
380
  cwd,
353
- reviewPrompt(job, deps.config.baseBranch),
381
+ reviewPrompt(job, deps.config.baseBranch, deps.config.capabilities),
354
382
  );
355
383
  ReviewResult.parse(result);
356
384
  const icon = result.verdict === "approve" ? "✅" : "🛠️";
@@ -382,7 +410,7 @@ async function fix(job: FixJob, deps: JobDeps) {
382
410
  "fixer",
383
411
  "fix-result",
384
412
  cwd,
385
- fixPrompt(job, config.baseBranch, logs),
413
+ fixPrompt(job, config.baseBranch, logs, config.capabilities),
386
414
  );
387
415
  FixResult.parse(result);
388
416
 
@@ -488,7 +516,12 @@ async function mergeBase(job: FixJob, deps: JobDeps, cwd: string) {
488
516
  "fixer",
489
517
  "fix-result",
490
518
  cwd,
491
- fixPrompt(job, config.baseBranch, files.map((f) => `- ${f}`).join("\n")),
519
+ fixPrompt(
520
+ job,
521
+ config.baseBranch,
522
+ files.map((f) => `- ${f}`).join("\n"),
523
+ config.capabilities,
524
+ ),
492
525
  );
493
526
  FixResult.parse(result);
494
527
  const left: string[] = [];
@@ -551,6 +584,12 @@ export async function applyAction(action: Action, deps: JobDeps) {
551
584
  );
552
585
  return;
553
586
  }
587
+ case "openQuestions":
588
+ return postComment(
589
+ bot,
590
+ action.item.issue.number,
591
+ `**Not started: the plan has open questions.**\n\n${action.questions.map((q) => `- ${q}`).join("\n")}\n\nMove the card to **Planning** and answer them, or remove them from the plan. Then move it back to **Ready**.\n${marker("questions")}`,
592
+ );
554
593
  case "merge": {
555
594
  const { pr } = action;
556
595
  log.info(`merging #${pr.number}, approved by ${action.by}`);
@@ -1,44 +1,46 @@
1
1
  {
2
- ".agents/skills/accessibility/SKILL.md": [
3
- "bb8c5148899685168324b8fda3524937e3943b32"
4
- ],
5
2
  ".agents/skills/approach-designer/SKILL.md": [
6
- "7fbf6e80562b59e94c8b9615d777afca8a769d29"
7
- ],
8
- ".agents/skills/code-review/SKILL.md": [
9
- "a35bdecaeb1b6a2469a6330d2cef42a9d61409fe"
10
- ],
11
- ".agents/skills/frontend-design/LICENSE.txt": [
12
- "f433b1a53f5b830a205fd2df78e2b34974656c7b"
13
- ],
14
- ".agents/skills/frontend-design/SKILL.md": [
15
- "8ac4bf9b37f7b42ead4b65c35e0e9890fb5354e8"
3
+ "7fbf6e80562b59e94c8b9615d777afca8a769d29",
4
+ "d932ed30a58cb32dd91ec03ddd2b1c6c59f77714"
16
5
  ],
17
6
  ".agents/skills/issue-splitting/SKILL.md": [
18
7
  "01625372d68a81347bf401e8603084420922d04e"
19
8
  ],
20
9
  ".agents/skills/kanbot-admin/SKILL.md": [
21
10
  "3789c559be9fb6a78f82708ae9857395a5556f6b",
11
+ "47b16ee8a6c6847450589bbb954d5a08c154f52a",
22
12
  "557532f241c0fc1e04f03ce8edb69efaa7b5165b",
13
+ "a7a7b2f55be7235ea17ffd092755af8493c8ef76",
14
+ "b09b9ccc5ca90585418a6645919c10d9a487bad9",
15
+ "c29ed25fa7f825e64b58c54ed2240823fb22ef6c",
23
16
  "e427c711b532963c0916f07ecc6b11e9b47cb9fa"
24
17
  ],
25
18
  ".agents/skills/kanbot-guide/SKILL.md": [
19
+ "1251194ba34a8b6eae8508037b0a76c60d7a26b8",
20
+ "2c6c77a225c5b880eff023d4b3c1305bfc91bfb3",
21
+ "2ccb86b12e5af121b79ef9f53c2a35dbbdb2ddeb",
22
+ "a24c0a655d6485e65bd5dea2a71f3afe67f04404",
26
23
  "cf3e95d137a5a58a273262bca6ba25fbc95fa037"
27
24
  ],
28
25
  ".agents/skills/kanbot-idea/SKILL.md": [
29
- "8ab28cec8d3f30250ffe080106d0de8e3c2eaa8a"
26
+ "8ab28cec8d3f30250ffe080106d0de8e3c2eaa8a",
27
+ "9d47113a299a75514a00993060bf6afebb049c6c"
30
28
  ],
31
29
  ".agents/skills/plan-template/SKILL.md": [
32
30
  "47a5dbbd2870883ca42dd9410d224ef9c0c9979f",
33
31
  "79cfd0bf36110b5d6f452e6400161609b11df0d6",
34
- "7bfde45995efaf515e45ccec09c204178203fdb7"
32
+ "7bfde45995efaf515e45ccec09c204178203fdb7",
33
+ "841446c304ffe9783c79f293e050a28ceb540b3d",
34
+ "e6b6e55a42bfc42d0c9ff1a5cdf3a0573ab665c0"
35
35
  ],
36
36
  ".agents/skills/pr-description/SKILL.md": [
37
37
  "1db6eb751e0bb1ea0014a2b414e04f6af59b1b2d",
38
38
  "6b0cca1be3abe8d031159c94e5d3b4f20307d600",
39
- "7e477762c9c5f09ce6d5d51ccd9138ce9743baa8"
39
+ "7e477762c9c5f09ce6d5d51ccd9138ce9743baa8",
40
+ "830c94888620de0a5bb7030672c017c5fafa63e5"
40
41
  ],
41
42
  ".agents/skills/project-conventions/SKILL.md": [
43
+ "668d904ff8fdf33edbe9768cd6bf223813380c9d",
42
44
  "81d9ff980a15a6f901010646b800b3e6e379e4b0",
43
45
  "dabf1dadc148c020496d8f2f9bb8de6269f56479"
44
46
  ],
@@ -48,18 +50,18 @@
48
50
  ".agents/skills/project-memory/SKILL.md": [
49
51
  "d354e11310db01362ec2bf5eec1080db77ac053d"
50
52
  ],
53
+ ".agents/skills/review-findings/SKILL.md": [
54
+ "4ba188751544038ca5ab3fdeaf61f40455a3bcec"
55
+ ],
51
56
  ".agents/skills/spec-review/SKILL.md": [
52
57
  "03e44fe4f23b04f1b11c907936ee9f6c7c8131f0",
58
+ "84fbc57a8b8f4fe473258402772185944a54a81c",
53
59
  "f455d1007d589602e5afce0dac4f00d78aa7c67a"
54
60
  ],
55
- ".agents/skills/tdd/SKILL.md": ["5e6b20868e02ae24f6d25824bac431143bd43021"],
56
61
  ".agents/skills/user-stories/SKILL.md": [
57
62
  "1b37f8e59c15b934d555070a63d4f0d3939a007f",
58
- "1b5d229d043c1bfbd84aecd9ee6191b1d473f1d8"
59
- ],
60
- ".agents/skills/verify-ui/SKILL.md": [
61
- "c67a0e1d6a1c1d9adf59dd154160ccf58519ea72",
62
- "f4d3d07c0ec28a9961d919356d0b4e572b724b43"
63
+ "1b5d229d043c1bfbd84aecd9ee6191b1d473f1d8",
64
+ "73455bec36bd5677cdf3c081677ad2856c434954"
63
65
  ],
64
66
  ".github/ISSUE_TEMPLATE/config.yml": [
65
67
  "0086358db1eb971c0cfa8739c27518bbc18a5ff4"
@@ -71,12 +73,16 @@
71
73
  "d26a148970ba6323d9cfdd86924cb2e82e68b705"
72
74
  ],
73
75
  ".github/pull_request_template.md": [
74
- "57cfbc21472a94ccccb19ef6f19e87a473925649"
76
+ "57cfbc21472a94ccccb19ef6f19e87a473925649",
77
+ "b8060c2599769dd12477081b6ec51733a2a82c7b"
75
78
  ],
76
79
  ".github/workflows/ci.yml": ["dce7bcf80cef91c10923829eb5893d2b311b58e5"],
77
80
  ".github/workflows/kanbot-merged.yml": [
78
81
  "8daaea63e23cf1e0d37821bfadaf60018a448de2"
79
82
  ],
83
+ ".kanbot/agents/acceptance-reviewer.md": [
84
+ "e6b65950345c686b4d219026d8d2d185e9735c12"
85
+ ],
80
86
  ".kanbot/agents/approach-designer.md": [
81
87
  "7e5f8c16c4d4a4e261bccc5dc8b4fff2d3046a72"
82
88
  ],
@@ -86,52 +92,31 @@
86
92
  ],
87
93
  ".kanbot/agents/fixer.md": [
88
94
  "109c8974fe2d70f5f514f33d97761740f589a6c4",
95
+ "16c16692f6d2ee29bc0e4d0ca4a1b3cfc4f1ba72",
89
96
  "7c98cf1dc3e798e0e5856fd95a3d78d378884def",
90
97
  "810871ec49fd7be56d111c7df176d22c66748a0f"
91
98
  ],
92
- ".kanbot/agents/implementer-backend.md": [
93
- "5d454e29260ba8df0ef6e21f1ece9d02a472d4ed",
94
- "6fefffb02bb5551631839abd0863379e58bc7dcc"
99
+ ".kanbot/agents/implementer.md": ["f6000b529b1a8bed0def57af9bfa55c1ca5cd3b6"],
100
+ ".kanbot/agents/plan-critic.md": [
101
+ "3d1c38922a94712d0f3a725902e3df9cee87bcda",
102
+ "cd6dd133f4611bd86fe27f8539a62a805e4df6c9"
95
103
  ],
96
- ".kanbot/agents/implementer-docs.md": [
97
- "082da045a20bc914f29839e026027b6715c184a9",
98
- "c5f01ac3d377133caab5b13422f1e73608d6357e"
99
- ],
100
- ".kanbot/agents/implementer-frontend.md": [
101
- "0aa9b0e70c3057138b4255ee3d270ac04fc8ab0e",
102
- "45a6e3b8f6e6f5a95dac865126c5649dab115fc5"
103
- ],
104
- ".kanbot/agents/implementer-technical.md": [
105
- "c95dff48eed5db4aadf10d0a47e7ec1285937ebf",
106
- "e4bbddb0a810313fb3a6ed3983b3b9335279feef"
107
- ],
108
- ".kanbot/agents/plan-critic.md": ["cd6dd133f4611bd86fe27f8539a62a805e4df6c9"],
109
104
  ".kanbot/agents/planner.md": [
110
105
  "2283cf8bf6d27b8bbf721f97bb45cd40eaa6d39d",
111
- "bdb111213cced70583157edad658247972dcbe45"
112
- ],
113
- ".kanbot/agents/reviewer-code.md": [
114
- "398035214b63ceb3469a3b3384b07209e4cd63dd",
115
- "61f3a45642ca954c6d4a5f41083f0a25f78d2b51"
116
- ],
117
- ".kanbot/agents/reviewer-spec.md": [
118
- "11c86a654a0b715777b23bfb699a4f0d6cfe91de",
119
- "ab8951ab296e96b01f419243d8acfc99c7e64b1c"
120
- ],
121
- ".kanbot/agents/test-writer.md": [
122
- "dcef7f611a307229974fef4098d53816d7db0555",
123
- "ec81c44c021053fae0612683a61e8414634359a0"
124
- ],
125
- ".kanbot/agents/ui-verifier.md": [
126
- "38615159a8fe7b1e99cacedc49c0be53c69b90d4",
127
- "a73abf7e72c654fde41df51b43a5879170978cb0"
106
+ "6500bea6691685d2b74ea73095f11643e0fcc937",
107
+ "a4a65d240ab4b23737f4d68d8765cca89cf08864",
108
+ "bdb111213cced70583157edad658247972dcbe45",
109
+ "dc50d048164ade2926b58dee48c3919c6b7d9558"
128
110
  ],
111
+ ".kanbot/agents/reviewer.md": ["7ff1fdd21df4442d3b82784d3e6077195b32ba5b"],
129
112
  "AGENTS.md": [
113
+ "0dd205961831f0082e74a3c03b79036e2114c072",
130
114
  "3da9115cbf52b7b7d0feb8325503a8caf43ff2c7",
131
115
  "b821013b5be7051327b349eda603218590a41f33",
132
116
  "c688ed7f42f7a6f6a5ac600de857b42fb04aa613",
133
117
  "cf7ef593ccfda953055dfb76b3e9c267fc9637f6",
134
- "e5b76bcbaf155b79a8ff3f2e70977cc0c589566b"
118
+ "e5b76bcbaf155b79a8ff3f2e70977cc0c589566b",
119
+ "f820c2f5a9d3bc299eed35a6012bbc5d19615aef"
135
120
  ],
136
121
  "CLAUDE.md": ["43c994c2d3617f947bcb5adf1933e21dabe46bb5"]
137
122
  }
@@ -8,6 +8,7 @@ import {
8
8
  CLAUDE_DENY,
9
9
  claudeTools,
10
10
  } from "../agents/sync.ts";
11
+ import { mcpConfigPath } from "./mcp.ts";
11
12
  import {
12
13
  looksRateLimited,
13
14
  type Provider,
@@ -25,45 +26,54 @@ type ResultEvent = {
25
26
  api_error_status?: number | null;
26
27
  };
27
28
 
29
+ /** CLI args for a headless run. */
30
+ export function claudeArgs(input: RunInput, agentsFile: string): string[] {
31
+ const { agent } = input;
32
+ const cmd = [
33
+ "claude",
34
+ "-p",
35
+ "--agents",
36
+ agentsFile,
37
+ "--agent",
38
+ agent.name,
39
+ "--output-format",
40
+ "stream-json",
41
+ "--verbose",
42
+ "--json-schema",
43
+ JSON.stringify(input.schema),
44
+ "--permission-mode",
45
+ "acceptEdits",
46
+ "--allowedTools",
47
+ ...headlessTools(agent),
48
+ "--disallowedTools",
49
+ ...CLAUDE_DENY,
50
+ "--no-session-persistence",
51
+ // Isolate from the user's own setup: no user-level plugins, skills or
52
+ // MCP servers, so workers only see the repo's agents and skills.
53
+ "--setting-sources",
54
+ "project,local",
55
+ "--strict-mcp-config",
56
+ "--settings",
57
+ JSON.stringify(CLAUDE_ATTRIBUTION),
58
+ ];
59
+ // The repo's committed MCP servers only.
60
+ const mcp = mcpConfigPath(input.cwd);
61
+ if (mcp) cmd.push("--mcp-config", mcp);
62
+ if (agent.models.claude) cmd.push("--model", agent.models.claude);
63
+ if (agent.effort) cmd.push("--effort", agent.effort);
64
+ return cmd;
65
+ }
66
+
28
67
  /** Claude Code headless (`claude -p`), using the logged-in subscription. */
29
68
  export const claude: Provider = {
30
69
  name: "claude",
31
70
  async run(input: RunInput): Promise<RunResult> {
32
- const { agent } = input;
33
71
  // Pass agent definitions explicitly so runs don't depend on what is
34
72
  // committed in the worktree's .claude/agents.
35
73
  const tmp = await mkdtemp(join(tmpdir(), "kanbot-claude-"));
36
74
  const agentsFile = join(tmp, "agents.json");
37
75
  await Bun.write(agentsFile, JSON.stringify(claudeAgentsJson(input.agents)));
38
- const cmd = [
39
- "claude",
40
- "-p",
41
- "--agents",
42
- agentsFile,
43
- "--agent",
44
- agent.name,
45
- "--output-format",
46
- "stream-json",
47
- "--verbose",
48
- "--json-schema",
49
- JSON.stringify(input.schema),
50
- "--permission-mode",
51
- "acceptEdits",
52
- "--allowedTools",
53
- ...headlessTools(agent),
54
- "--disallowedTools",
55
- ...CLAUDE_DENY,
56
- "--no-session-persistence",
57
- // Isolate from the user's own setup: no user-level plugins, skills or
58
- // MCP servers, so workers only see the repo's agents and skills.
59
- "--setting-sources",
60
- "project,local",
61
- "--strict-mcp-config",
62
- "--settings",
63
- JSON.stringify(CLAUDE_ATTRIBUTION),
64
- ];
65
- if (agent.models.claude) cmd.push("--model", agent.models.claude);
66
- if (agent.effort) cmd.push("--effort", agent.effort);
76
+ const cmd = claudeArgs(input, agentsFile);
67
77
 
68
78
  const res = await spawnWorker(cmd, {
69
79
  cwd: input.cwd,