kanbot 0.3.0 → 0.4.1

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 (54) 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/color.ts +16 -0
  8. package/src/commands/board.ts +14 -7
  9. package/src/commands/doctor.ts +206 -19
  10. package/src/commands/init.ts +15 -3
  11. package/src/commands/sync.ts +1 -1
  12. package/src/commands/upgrade.ts +20 -1
  13. package/src/config.ts +38 -8
  14. package/src/daemon/derive.ts +41 -0
  15. package/src/daemon/loop.ts +2 -0
  16. package/src/jobs/prompts.ts +25 -5
  17. package/src/jobs/run.ts +61 -22
  18. package/src/known-hashes.json +42 -57
  19. package/src/providers/claude.ts +40 -30
  20. package/src/providers/codex.ts +60 -30
  21. package/src/providers/mcp.ts +71 -0
  22. package/src/schemas/results.ts +2 -5
  23. package/templates/.agents/skills/approach-designer/SKILL.md +1 -1
  24. package/templates/.agents/skills/kanbot-admin/SKILL.md +21 -4
  25. package/templates/.agents/skills/kanbot-guide/SKILL.md +26 -7
  26. package/templates/.agents/skills/kanbot-idea/SKILL.md +1 -1
  27. package/templates/.agents/skills/plan-template/SKILL.md +14 -6
  28. package/templates/.agents/skills/pr-description/SKILL.md +7 -3
  29. package/templates/.agents/skills/project-conventions/SKILL.md +23 -35
  30. package/templates/.agents/skills/review-findings/SKILL.md +47 -0
  31. package/templates/.agents/skills/spec-review/SKILL.md +10 -27
  32. package/templates/.agents/skills/user-stories/SKILL.md +1 -1
  33. package/templates/.github/pull_request_template.md +6 -0
  34. package/templates/.kanbot/agents/acceptance-reviewer.md +43 -0
  35. package/templates/.kanbot/agents/fixer.md +7 -5
  36. package/templates/.kanbot/agents/implementer.md +49 -0
  37. package/templates/.kanbot/agents/plan-critic.md +3 -3
  38. package/templates/.kanbot/agents/planner.md +10 -9
  39. package/templates/.kanbot/agents/reviewer.md +42 -0
  40. package/templates/AGENTS.md +2 -3
  41. package/templates/.agents/skills/accessibility/SKILL.md +0 -76
  42. package/templates/.agents/skills/code-review/SKILL.md +0 -83
  43. package/templates/.agents/skills/frontend-design/LICENSE.txt +0 -177
  44. package/templates/.agents/skills/frontend-design/SKILL.md +0 -76
  45. package/templates/.agents/skills/tdd/SKILL.md +0 -51
  46. package/templates/.agents/skills/verify-ui/SKILL.md +0 -104
  47. package/templates/.kanbot/agents/implementer-backend.md +0 -57
  48. package/templates/.kanbot/agents/implementer-docs.md +0 -55
  49. package/templates/.kanbot/agents/implementer-frontend.md +0 -54
  50. package/templates/.kanbot/agents/implementer-technical.md +0 -53
  51. package/templates/.kanbot/agents/reviewer-code.md +0 -46
  52. package/templates/.kanbot/agents/reviewer-spec.md +0 -49
  53. package/templates/.kanbot/agents/test-writer.md +0 -35
  54. package/templates/.kanbot/agents/ui-verifier.md +0 -35
@@ -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,
@@ -1,8 +1,10 @@
1
1
  import { mkdtemp, rm } from "node:fs/promises";
2
2
  import { tmpdir } from "node:os";
3
3
  import { join } from "node:path";
4
+ import type { AgentDef } from "../agents/load.ts";
4
5
  import { agentInstructions } from "../agents/sync.ts";
5
6
  import { git } from "../daemon/git.ts";
7
+ import { codexMcpArgs, type McpServer, readMcpServers } from "./mcp.ts";
6
8
  import {
7
9
  looksRateLimited,
8
10
  type Provider,
@@ -11,6 +13,58 @@ import {
11
13
  spawnWorker,
12
14
  } from "./types.ts";
13
15
 
16
+ /** CLI args for a headless run. */
17
+ export function codexArgs(o: {
18
+ agent: AgentDef;
19
+ cwd: string;
20
+ gitDir: string;
21
+ schemaFile: string;
22
+ lastFile: string;
23
+ networkAccess: boolean;
24
+ mcpServers: Record<string, McpServer>;
25
+ }): string[] {
26
+ const { agent } = o;
27
+ const sandbox =
28
+ agent.tools.includes("edit") || agent.tools.includes("bash")
29
+ ? "workspace-write"
30
+ : "read-only";
31
+ const cmd = [
32
+ "codex",
33
+ "exec",
34
+ "--json",
35
+ "--ephemeral",
36
+ // Isolate from the user's own setup: no user-level plugins or MCP
37
+ // servers. Auth still comes from CODEX_HOME.
38
+ "--ignore-user-config",
39
+ "--sandbox",
40
+ sandbox,
41
+ "--cd",
42
+ o.cwd,
43
+ "--add-dir",
44
+ o.gitDir,
45
+ "--output-schema",
46
+ o.schemaFile,
47
+ "--output-last-message",
48
+ o.lastFile,
49
+ "-c",
50
+ `developer_instructions=${JSON.stringify(agentInstructions(agent))}`,
51
+ // The repo's committed MCP servers only.
52
+ ...codexMcpArgs(o.mcpServers),
53
+ ];
54
+ if (o.networkAccess)
55
+ cmd.push("-c", "sandbox_workspace_write.network_access=true");
56
+ if (agent.models.codex && agent.models.codex !== "default")
57
+ cmd.push("--model", agent.models.codex);
58
+ if (agent.effort) {
59
+ cmd.push(
60
+ "-c",
61
+ `model_reasoning_effort="${agent.effort === "max" ? "xhigh" : agent.effort}"`,
62
+ );
63
+ }
64
+ cmd.push("-");
65
+ return cmd;
66
+ }
67
+
14
68
  /** Codex CLI headless (`codex exec`), using the logged-in ChatGPT plan. */
15
69
  export function codex({ networkAccess }: { networkAccess: boolean }): Provider {
16
70
  return {
@@ -29,39 +83,15 @@ export function codex({ networkAccess }: { networkAccess: boolean }): Provider {
29
83
  "--path-format=absolute",
30
84
  "--git-common-dir",
31
85
  );
32
- const sandbox =
33
- agent.tools.includes("edit") || agent.tools.includes("bash")
34
- ? "workspace-write"
35
- : "read-only";
36
- const cmd = [
37
- "codex",
38
- "exec",
39
- "--json",
40
- "--ephemeral",
41
- "--sandbox",
42
- sandbox,
43
- "--cd",
44
- input.cwd,
45
- "--add-dir",
86
+ const cmd = codexArgs({
87
+ agent,
88
+ cwd: input.cwd,
46
89
  gitDir,
47
- "--output-schema",
48
90
  schemaFile,
49
- "--output-last-message",
50
91
  lastFile,
51
- "-c",
52
- `developer_instructions=${JSON.stringify(agentInstructions(agent))}`,
53
- ];
54
- if (networkAccess)
55
- cmd.push("-c", "sandbox_workspace_write.network_access=true");
56
- if (agent.models.codex && agent.models.codex !== "default")
57
- cmd.push("--model", agent.models.codex);
58
- if (agent.effort) {
59
- cmd.push(
60
- "-c",
61
- `model_reasoning_effort="${agent.effort === "max" ? "xhigh" : agent.effort}"`,
62
- );
63
- }
64
- cmd.push("-");
92
+ networkAccess,
93
+ mcpServers: await readMcpServers(input.cwd),
94
+ });
65
95
 
66
96
  const res = await spawnWorker(cmd, {
67
97
  cwd: input.cwd,