kanbot 0.1.0 → 0.3.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.
package/src/github/gh.ts CHANGED
@@ -1,15 +1,29 @@
1
1
  import { log } from "../log.ts";
2
2
 
3
+ /** Returns a GitHub token to use instead of `gh`'s own login. */
4
+ export type TokenSource = () => Promise<string>;
5
+
3
6
  /**
4
7
  * Thin wrapper over the `gh` CLI so kanbot reuses the user's existing auth.
8
+ * With a `token`, calls run as that identity instead (the kanbot app).
5
9
  * Reads always run; writes are skipped (and logged) in dry-run mode.
6
10
  */
7
11
  export class Gh {
8
12
  constructor(
9
13
  readonly repo: string,
10
14
  readonly dryRun = false,
15
+ private readonly token?: TokenSource,
11
16
  ) {}
12
17
 
18
+ /** True when calls run as the kanbot app, not the user. */
19
+ get isBot() {
20
+ return this.token !== undefined;
21
+ }
22
+
23
+ private async env() {
24
+ return this.token ? { GH_TOKEN: await this.token() } : undefined;
25
+ }
26
+
13
27
  get owner() {
14
28
  return this.repo.split("/")[0] as string;
15
29
  }
@@ -35,7 +49,11 @@ export class Gh {
35
49
  }
36
50
 
37
51
  private async queryOnce<T>(input: string): Promise<T> {
38
- const out = await run(["api", "graphql", "--input", "-"], input);
52
+ const out = await run(
53
+ ["api", "graphql", "--input", "-"],
54
+ input,
55
+ await this.env(),
56
+ );
39
57
  const json = JSON.parse(out) as { data: T; errors?: { message: string }[] };
40
58
  if (json.errors?.length) {
41
59
  throw new Error(
@@ -58,7 +76,9 @@ export class Gh {
58
76
  }
59
77
 
60
78
  async rest<T = unknown>(path: string): Promise<T> {
61
- return JSON.parse(await run(["api", path])) as T;
79
+ return JSON.parse(
80
+ await run(["api", path], undefined, await this.env()),
81
+ ) as T;
62
82
  }
63
83
 
64
84
  async restWrite<T = unknown>(
@@ -80,6 +100,7 @@ export class Gh {
80
100
  ...(body === undefined ? [] : ["--input", "-"]),
81
101
  ],
82
102
  body === undefined ? undefined : JSON.stringify(body),
103
+ await this.env(),
83
104
  );
84
105
  return (out.trim() ? JSON.parse(out) : null) as T;
85
106
  }
@@ -90,9 +111,14 @@ export class Gh {
90
111
  }
91
112
  }
92
113
 
93
- export async function run(args: string[], input?: string): Promise<string> {
114
+ export async function run(
115
+ args: string[],
116
+ input?: string,
117
+ env?: Record<string, string>,
118
+ ): Promise<string> {
94
119
  const proc = Bun.spawn(["gh", ...args], {
95
120
  stdin: input === undefined ? "ignore" : new Blob([input]),
121
+ env: env ? { ...process.env, ...env } : undefined,
96
122
  stdout: "pipe",
97
123
  stderr: "pipe",
98
124
  });
@@ -1,4 +1,5 @@
1
1
  import { COLUMN_KEYS, type ColumnKey, type Config } from "../config.ts";
2
+ import { UserError } from "../errors.ts";
2
3
  import type { Gh } from "./gh.ts";
3
4
 
4
5
  export type Board = {
@@ -60,7 +61,11 @@ function toBoard(p: RawProject, config: Config): Board {
60
61
 
61
62
  export async function loadBoard(gh: Gh, config: Config): Promise<Board> {
62
63
  if (!config.project) {
63
- throw new Error("No project configured. Run `kanbot board create`.");
64
+ throw new UserError("No project board configured.", {
65
+ why: "`project` is not set in .kanbot/config.yml.",
66
+ fix: "kanbot board create (or kanbot board link --owner <login> --number <n>)",
67
+ docs: "getting-started#create-the-board",
68
+ });
64
69
  }
65
70
  const data = await gh.query<{
66
71
  repositoryOwner: { projectV2: RawProject | null } | null;
@@ -74,8 +79,13 @@ export async function loadBoard(gh: Gh, config: Config): Promise<Board> {
74
79
  );
75
80
  const p = data.repositoryOwner?.projectV2;
76
81
  if (!p) {
77
- throw new Error(
78
- `Project ${config.project.owner}#${config.project.number} not found (does gh have the "project" scope? run: gh auth refresh -s project)`,
82
+ throw new UserError(
83
+ `Project ${config.project.owner}#${config.project.number} not found.`,
84
+ {
85
+ why: "It does not exist, or your gh token lacks the `project` scope.",
86
+ fix: "gh auth refresh -s project, then check `project` in .kanbot/config.yml",
87
+ docs: "getting-started#create-the-board",
88
+ },
79
89
  );
80
90
  }
81
91
  return toBoard(p, config);
@@ -6,6 +6,7 @@ import {
6
6
  reviewPasses,
7
7
  } from "../daemon/derive.ts";
8
8
  import type { Comment, Issue, PR } from "../daemon/types.ts";
9
+ import { SIGNATURE } from "../github/api.ts";
9
10
  import { extractPlan, isKanbot, stripPlan } from "../github/markers.ts";
10
11
  import type { Finding } from "../schemas/results.ts";
11
12
 
@@ -36,7 +37,10 @@ function conversation(comments: Comment[], limit = 30) {
36
37
  }
37
38
 
38
39
  function stripMarkers(body: string) {
39
- return body.replace(/<!--\s*kanbot[^>]*-->/g, "").trim();
40
+ return body
41
+ .replace(SIGNATURE, "")
42
+ .replace(/<!--\s*kanbot[^>]*-->/g, "")
43
+ .trim();
40
44
  }
41
45
 
42
46
  /**
package/src/jobs/run.ts CHANGED
@@ -24,10 +24,12 @@ import {
24
24
  createPR,
25
25
  failedCheckLogs,
26
26
  fetchIssue,
27
+ mergePR,
27
28
  postComment,
28
29
  react,
29
30
  removeLabel,
30
31
  replyToThread,
32
+ requestReview,
31
33
  resolveThread,
32
34
  updateIssueBody,
33
35
  updatePRBody,
@@ -62,7 +64,12 @@ import {
62
64
  export type JobDeps = {
63
65
  root: string;
64
66
  config: Config;
67
+ /** The user's own client: reads, board moves, merges. */
65
68
  gh: Gh;
69
+ /** Who kanbot talks as: comments, labels, PRs. Same as `gh` in user mode. */
70
+ bot: Gh;
71
+ /** Login of the user running the daemon. */
72
+ me: string;
66
73
  board: Board;
67
74
  agents: AgentDef[];
68
75
  provider: Provider;
@@ -145,9 +152,9 @@ export function runJob(job: Job, deps: JobDeps): Promise<void> {
145
152
  // ---- plan -------------------------------------------------------------------
146
153
 
147
154
  async function plan(job: PlanJob, deps: JobDeps) {
148
- const { gh } = deps;
155
+ const { gh, bot } = deps;
149
156
  const { issue } = job.item;
150
- await Promise.all(job.comments.map((c) => react(gh, c.id, "EYES")));
157
+ await Promise.all(job.comments.map((c) => react(bot, c.id, "EYES")));
151
158
  const cwd = await deps.worktrees.detached(worktreeName(job));
152
159
  const result = await runAgent(
153
160
  deps,
@@ -161,13 +168,13 @@ async function plan(job: PlanJob, deps: JobDeps) {
161
168
 
162
169
  // Re-read so concurrent human edits to the description are kept.
163
170
  const fresh = await fetchIssue(gh, issue.number);
164
- await updateIssueBody(gh, issue.number, withPlan(fresh.body, result.plan));
171
+ await updateIssueBody(bot, issue.number, withPlan(fresh.body, result.plan));
165
172
 
166
173
  const allowed = result.labels.filter(
167
174
  (l) => l === LABELS.technical || l.startsWith(LABELS.typePrefix),
168
175
  );
169
176
  await addLabels(
170
- gh,
177
+ bot,
171
178
  issue.number,
172
179
  allowed.filter((l) => !fresh.labels.includes(l)),
173
180
  );
@@ -175,12 +182,12 @@ async function plan(job: PlanJob, deps: JobDeps) {
175
182
  const created: string[] = [];
176
183
  for (const ni of result.newIssues) {
177
184
  const res = await createIssue(
178
- gh,
185
+ bot,
179
186
  ni.title,
180
187
  `${ni.body}\n\n_Split from #${issue.number} by kanbot._\n${marker("created", issue.number)}`,
181
188
  );
182
189
  if (!res) continue;
183
- if (ni.subIssue) await addSubIssue(gh, issue.id, res.node_id);
190
+ if (ni.subIssue) await addSubIssue(bot, issue.id, res.node_id);
184
191
  await addToBoard(gh, deps.board, res.node_id, ni.column);
185
192
  created.push(`#${res.number}`);
186
193
  }
@@ -206,18 +213,18 @@ async function plan(job: PlanJob, deps: JobDeps) {
206
213
  lines.push(
207
214
  [marker("plan"), ...job.comments.map((c) => marker("re", c.id))].join("\n"),
208
215
  );
209
- await postComment(gh, issue.number, lines.join("\n\n"));
210
- await Promise.all(job.comments.map((c) => react(gh, c.id, "THUMBS_UP")));
216
+ await postComment(bot, issue.number, lines.join("\n\n"));
217
+ await Promise.all(job.comments.map((c) => react(bot, c.id, "THUMBS_UP")));
211
218
  }
212
219
 
213
220
  // ---- implement ----------------------------------------------------------------
214
221
 
215
222
  async function implement(job: ImplementJob, deps: JobDeps) {
216
- const { gh, config, worktrees } = deps;
223
+ const { gh, bot, config, worktrees } = deps;
217
224
  const { issue } = job.item;
218
225
  if (job.item.column === "ready") {
219
226
  await moveItem(gh, deps.board, job.item.itemId, "inProgress");
220
- await addLabels(gh, issue.number, [LABELS.working]);
227
+ await addLabels(bot, issue.number, [LABELS.working]);
221
228
  }
222
229
 
223
230
  const resumed = await worktrees.remoteHas(job.branch);
@@ -240,9 +247,9 @@ async function implement(job: ImplementJob, deps: JobDeps) {
240
247
  implementPrompt(job, config.baseBranch),
241
248
  );
242
249
  if (result.status === "blocked") {
243
- await addLabels(gh, issue.number, [LABELS.needsHuman]);
250
+ await addLabels(bot, issue.number, [LABELS.needsHuman]);
244
251
  await postComment(
245
- gh,
252
+ bot,
246
253
  issue.number,
247
254
  `**Implementation blocked${job.variant ? ` (approach ${job.variant})` : ""}.** ${result.blockedReason ?? ""}\n\nRemove \`${LABELS.needsHuman}\` once resolved and kanbot will retry.\n${marker("blocked", job.branch)}`,
248
255
  );
@@ -292,7 +299,7 @@ async function implement(job: ImplementJob, deps: JobDeps) {
292
299
  ]
293
300
  .filter(Boolean)
294
301
  .join("\n\n");
295
- const pr = await createPR(gh, {
302
+ const pr = await createPR(bot, {
296
303
  title: `${result?.prTitle ?? issue.title}${variant}`,
297
304
  body,
298
305
  head: job.branch,
@@ -304,7 +311,7 @@ async function implement(job: ImplementJob, deps: JobDeps) {
304
311
  );
305
312
  if (job.variant !== null)
306
313
  labels.push(`${LABELS.variantPrefix}${job.variant}`);
307
- await addLabels(gh, pr.number, labels);
314
+ await addLabels(bot, pr.number, labels);
308
315
  log.info(`✔ opened ${pr.html_url}`);
309
316
  }
310
317
  }
@@ -354,13 +361,13 @@ async function review(job: ReviewJob, deps: JobDeps) {
354
361
  marker("review", job.pass),
355
362
  embedJson("findings", result.findings),
356
363
  ].join("\n\n");
357
- await postComment(deps.gh, job.pr.number, body);
364
+ await postComment(deps.bot, job.pr.number, body);
358
365
  }
359
366
 
360
367
  // ---- fix ----------------------------------------------------------------------
361
368
 
362
369
  async function fix(job: FixJob, deps: JobDeps) {
363
- const { gh, worktrees, config } = deps;
370
+ const { gh, bot, worktrees, config } = deps;
364
371
  const { pr } = job;
365
372
  const cwd = await worktrees.checkout(worktreeName(job), pr.headRefName);
366
373
  if (job.reason.type === "conflict") return mergeBase(job, deps, cwd);
@@ -387,7 +394,7 @@ async function fix(job: FixJob, deps: JobDeps) {
387
394
  if (changed) await worktrees.push(cwd, pr.headRefName);
388
395
  if (result.prBody) {
389
396
  const desc = absoluteImages(result.prBody, config.repo, pr.headRefName);
390
- await updatePRBody(gh, pr.number, withDescription(pr.body, desc));
397
+ await updatePRBody(bot, pr.number, withDescription(pr.body, desc));
391
398
  }
392
399
  const by = new Map(result.addressed.map((a) => [a.ref, a]));
393
400
  const pushed = changed ? "Pushed fixes." : "No code changes.";
@@ -399,7 +406,7 @@ async function fix(job: FixJob, deps: JobDeps) {
399
406
  return `- ${a?.resolved ? "✅" : "⏭️"} **${f.id}** ${a?.reply ?? "Not addressed."}`;
400
407
  });
401
408
  await postComment(
402
- gh,
409
+ bot,
403
410
  pr.number,
404
411
  [
405
412
  `**Addressed agent review ${r.pass}/${reviewPasses(job.item.issue).length}.** ${result.summary} ${pushed}`,
@@ -411,7 +418,7 @@ async function fix(job: FixJob, deps: JobDeps) {
411
418
  }
412
419
  if (r.type === "ci") {
413
420
  await postComment(
414
- gh,
421
+ bot,
415
422
  pr.number,
416
423
  `**CI fix attempt.** ${result.summary} ${pushed}\n\n${marker("ci", pr.headSha)}`,
417
424
  );
@@ -423,11 +430,11 @@ async function fix(job: FixJob, deps: JobDeps) {
423
430
  for (const t of r.threads) {
424
431
  const a = by.get(t.id);
425
432
  await replyToThread(
426
- gh,
433
+ bot,
427
434
  t.id,
428
435
  `${a?.reply ?? "Looked at this but made no change."}\n\n${marker("re", t.id)}`,
429
436
  );
430
- if (a?.resolved) await resolveThread(gh, t.id);
437
+ if (a?.resolved) await resolveThread(bot, t.id);
431
438
  else open++;
432
439
  }
433
440
  if (r.type === "copilot") return;
@@ -438,7 +445,7 @@ async function fix(job: FixJob, deps: JobDeps) {
438
445
  return `> ${excerpt(c.body)}\n\n${a?.reply ?? "Noted."}`;
439
446
  });
440
447
  await postComment(
441
- gh,
448
+ bot,
442
449
  pr.number,
443
450
  [
444
451
  `**Feedback round.** ${result.summary} ${pushed}`,
@@ -452,7 +459,7 @@ async function fix(job: FixJob, deps: JobDeps) {
452
459
  : "All feedback addressed — ready for another look.";
453
460
  if (!r.comments.length)
454
461
  await postComment(
455
- gh,
462
+ bot,
456
463
  pr.number,
457
464
  `${msg} ${pushed}\n\n${marker("feedback-round")}`,
458
465
  );
@@ -469,7 +476,7 @@ const CONFLICT_MARKER = /^(<{7}|>{7})( |$)/m;
469
476
 
470
477
  /** Merge the base branch into a conflicting PR; the fixer resolves conflicts. */
471
478
  async function mergeBase(job: FixJob, deps: JobDeps, cwd: string) {
472
- const { gh, worktrees, config } = deps;
479
+ const { bot, worktrees, config } = deps;
473
480
  const { pr } = job;
474
481
  const base = `origin/${config.baseBranch}`;
475
482
  const files = await worktrees.merge(cwd, base);
@@ -503,7 +510,7 @@ async function mergeBase(job: FixJob, deps: JobDeps, cwd: string) {
503
510
  throw new Error(`${base} was not merged`);
504
511
  await worktrees.push(cwd, pr.headRefName);
505
512
  await postComment(
506
- gh,
513
+ bot,
507
514
  pr.number,
508
515
  `**Merged \`${config.baseBranch}\`.** ${summary}\n\n${marker("conflict", pr.headSha)}`,
509
516
  );
@@ -512,35 +519,68 @@ async function mergeBase(job: FixJob, deps: JobDeps, cwd: string) {
512
519
  // ---- deterministic actions ----------------------------------------------------
513
520
 
514
521
  export async function applyAction(action: Action, deps: JobDeps) {
515
- const { gh } = deps;
522
+ const { gh, bot, config } = deps;
516
523
  switch (action.kind) {
517
524
  case "move":
518
525
  log.info(`#${action.item.issue.number} → ${action.to} (${action.why})`);
519
526
  return moveItem(gh, deps.board, action.item.itemId, action.to);
520
527
  case "label":
521
- await addLabels(gh, action.number, action.add);
522
- for (const l of action.remove) await removeLabel(gh, action.number, l);
528
+ await addLabels(bot, action.number, action.add);
529
+ for (const l of action.remove) await removeLabel(bot, action.number, l);
523
530
  return;
524
- case "agentReviewed":
525
- await addLabels(gh, action.pr.number, [LABELS.agentReviewed]);
531
+ case "agentReviewed": {
532
+ await addLabels(bot, action.pr.number, [LABELS.agentReviewed]);
533
+ const next =
534
+ config.merge.onApproval && action.pr.headRefName.startsWith("kanbot/")
535
+ ? "Approve to merge."
536
+ : "Ready for you to test and merge.";
526
537
  await postComment(
527
- gh,
538
+ bot,
528
539
  action.pr.number,
529
- `**Agent review complete.** Ready for you to test and merge.\n\n${marker("ready")}`,
540
+ `**Agent review complete.** ${next}\n\n${marker("ready")}`,
530
541
  );
542
+ // The bot opened the PR, so the user can be asked to review it.
543
+ if (bot.isBot)
544
+ await requestReview(bot, action.pr.number, [deps.me]).catch((e) =>
545
+ log.warn(`request review on #${action.pr.number}: ${e}`),
546
+ );
531
547
  deps.notify.send(
532
548
  `Ready to test: PR #${action.pr.number}`,
533
549
  action.pr.title,
534
550
  action.pr.url,
535
551
  );
536
552
  return;
553
+ }
554
+ case "merge": {
555
+ const { pr } = action;
556
+ log.info(`merging #${pr.number}, approved by ${action.by}`);
557
+ try {
558
+ await mergePR(gh, pr.number, pr.headSha, config.merge.method);
559
+ } catch (e) {
560
+ return applyAction(
561
+ {
562
+ kind: "needsHuman",
563
+ number: action.item.issue.number,
564
+ why: `Approved, but merging #${pr.number} failed:\n\n\`\`\`\n${String(e).slice(0, 1500)}\n\`\`\``,
565
+ },
566
+ deps,
567
+ );
568
+ }
569
+ await postComment(
570
+ bot,
571
+ pr.number,
572
+ `**Merged** after approval by @${action.by}.\n\n${marker("merged")}`,
573
+ );
574
+ deps.notify.send(`Merged PR #${pr.number}`, pr.title, pr.url);
575
+ return;
576
+ }
537
577
  case "compare": {
538
578
  const rows = action.prs.map(
539
579
  (p) =>
540
580
  `| ${p.labels.find((l) => l.startsWith(LABELS.variantPrefix)) ?? "?"} | #${p.number} | ${p.title} |`,
541
581
  );
542
582
  await postComment(
543
- gh,
583
+ bot,
544
584
  action.item.issue.number,
545
585
  `**All ${action.prs.length} approaches are agent-reviewed.** Try them and merge the one you want; the others are closed automatically.\n\n| Variant | PR | Title |\n|---|---|---|\n${rows.join("\n")}\n\n${marker("compare")}`,
546
586
  );
@@ -552,9 +592,9 @@ export async function applyAction(action: Action, deps: JobDeps) {
552
592
  return;
553
593
  }
554
594
  case "needsHuman":
555
- await addLabels(gh, action.number, [LABELS.needsHuman]);
595
+ await addLabels(bot, action.number, [LABELS.needsHuman]);
556
596
  await postComment(
557
- gh,
597
+ bot,
558
598
  action.number,
559
599
  `**kanbot needs a human.** ${action.why}\n\nRemove \`${LABELS.needsHuman}\` to let agents retry.\n${marker("needs-human")}`,
560
600
  );
@@ -19,8 +19,12 @@
19
19
  ],
20
20
  ".agents/skills/kanbot-admin/SKILL.md": [
21
21
  "3789c559be9fb6a78f82708ae9857395a5556f6b",
22
+ "557532f241c0fc1e04f03ce8edb69efaa7b5165b",
22
23
  "e427c711b532963c0916f07ecc6b11e9b47cb9fa"
23
24
  ],
25
+ ".agents/skills/kanbot-guide/SKILL.md": [
26
+ "cf3e95d137a5a58a273262bca6ba25fbc95fa037"
27
+ ],
24
28
  ".agents/skills/kanbot-idea/SKILL.md": [
25
29
  "8ab28cec8d3f30250ffe080106d0de8e3c2eaa8a"
26
30
  ],
@@ -125,6 +129,7 @@
125
129
  "AGENTS.md": [
126
130
  "3da9115cbf52b7b7d0feb8325503a8caf43ff2c7",
127
131
  "b821013b5be7051327b349eda603218590a41f33",
132
+ "c688ed7f42f7a6f6a5ac600de857b42fb04aa613",
128
133
  "cf7ef593ccfda953055dfb76b3e9c267fc9637f6",
129
134
  "e5b76bcbaf155b79a8ff3f2e70977cc0c589566b"
130
135
  ],
package/src/main.ts CHANGED
@@ -10,6 +10,7 @@ export const main = defineCommand({
10
10
  subCommands: {
11
11
  init: () => import("./commands/init.ts").then((m) => m.default),
12
12
  board: () => import("./commands/board.ts").then((m) => m.default),
13
+ app: () => import("./commands/app.ts").then((m) => m.default),
13
14
  upgrade: () => import("./commands/upgrade.ts").then((m) => m.default),
14
15
  skills: () => import("./commands/skills.ts").then((m) => m.default),
15
16
  sync: () => import("./commands/sync.ts").then((m) => m.default),
package/src/templates.ts CHANGED
@@ -24,6 +24,7 @@ export type FileClass = "kanbot" | "user" | "shared";
24
24
  const CLASSES: [prefix: string, FileClass][] = [
25
25
  [".github/workflows/kanbot-", "kanbot"],
26
26
  [".agents/skills/kanbot-admin/", "kanbot"],
27
+ [".agents/skills/kanbot-guide/", "kanbot"],
27
28
  [".agents/skills/kanbot-idea/", "kanbot"],
28
29
  ["AGENTS.md", "user"],
29
30
  ["CLAUDE.md", "user"],
@@ -1,12 +1,12 @@
1
1
  ---
2
2
 
3
3
  name: kanbot-admin
4
- description: Maintain this repo's kanbot setup with a human. Use to add or edit kanbot agents, skills or project memory, change `.kanbot/config.yml`, run `kanbot upgrade` or merge `.kanbot-new` files. Not for board jobs.
4
+ description: Change this repo's kanbot setup with a human. Use to add or edit kanbot agents, skills or project memory, change `.kanbot/config.yml`, switch identity mode, turn on merge on approval, run `kanbot upgrade` or merge `.kanbot-new` files. Not for board jobs. For setup walkthroughs and explaining options, use kanbot-guide.
5
5
  ---
6
6
 
7
7
  # kanbot admin
8
8
 
9
- You are helping a human maintain kanbot, the board-driven agent team in this repo. Board agents (planner, implementers, reviewers, fixer) never use this skill.
9
+ You are helping a human maintain kanbot, the board-driven agent team in this repo. Board agents (planner, implementers, reviewers, fixer) never use this skill. To explain options or set kanbot up from scratch, use `kanbot-guide`.
10
10
 
11
11
  ## Map
12
12
 
@@ -14,15 +14,30 @@ You are helping a human maintain kanbot, the board-driven agent team in this rep
14
14
  | -------------------------- | -------------------------------------- | ------------------------------- |
15
15
  | `.kanbot/agents/<name>.md` | Agent definitions, the source of truth | Yes, then `kanbot sync` |
16
16
  | `.agents/skills/<name>/` | Skills shared by all agents | Yes |
17
- | `.kanbot/config.yml` | Daemon config; schema next to it | Yes |
17
+ | `.kanbot/config.yml` | Daemon config; schema next to it | Yes, then restart the daemon |
18
18
  | `.kanbot/manifest.json` | Template hashes from the last upgrade | Never by hand |
19
19
  | `.claude/`, `.codex/` | Generated by `kanbot sync` | Never; edit the source and sync |
20
+ | `~/.kanbot/apps/<id>.pem` | Private key of the kanbot GitHub App | Never commit |
20
21
 
21
- `kanbot <command> --help` lists every flag. Agent frontmatter fields and output schemas: read an existing agent in `.kanbot/agents/` and copy its shape.
22
+ `kanbot <command> --help` lists every flag. `.kanbot/config.schema.json` describes every config field. Docs: https://kanbot.mib.im.
22
23
 
23
24
  ## Recipes
24
25
 
25
- Finish every recipe with `kanbot sync` when agents changed, then `kanbot doctor`, then show the human the diff. The human commits and pushes; worktrees are made from the pushed base branch, so agents see nothing until then.
26
+ Finish every recipe with `kanbot sync` when agents changed, then `kanbot doctor`, then show the human the diff. The human commits and pushes; worktrees are made from the pushed base branch, so agents see nothing until then. Restart the daemon after config changes.
27
+
28
+ ### Switch to app mode
29
+
30
+ 1. `kanbot app create`. The human clicks **Create GitHub App** on GitHub, then installs it on the repo.
31
+ 2. Already have an app from another repo? `kanbot app link --id <id> --slug <slug>`, add the repo to its installation, and copy `~/.kanbot/apps/<id>.pem` if on another machine.
32
+ 3. `kanbot doctor` checks the app, its key and the install.
33
+
34
+ Back to user mode: set `identity.mode: user`.
35
+
36
+ ### Turn on merge on approval
37
+
38
+ 1. Set `merge.onApproval: true` and pick `merge.method` (`squash`, `merge` or `rebase`).
39
+ 2. Recommend app mode, so they can approve their own kanbot PRs.
40
+ 3. Warn: never enable the branch rule "Require approval of the most recent reviewable push". It blocks their own approval.
26
41
 
27
42
  ### Add or change an agent
28
43
 
@@ -0,0 +1,91 @@
1
+ ---
2
+
3
+ name: kanbot-guide
4
+ description: Explain kanbot and guide a human through it. Use when someone asks what kanbot is, how to set it up in a repo, which config options to pick (provider, bot or user identity, merge on approval, Copilot, notifications), how the board flow works, where something is documented, or why something isn't working. Works before kanbot is installed in the repo. For changing agents, skills or upgrading, use kanbot-admin.
5
+ ---
6
+
7
+ # kanbot guide
8
+
9
+ You are helping a human understand and set up kanbot: a daemon that runs a team of Claude Code or Codex agents from a GitHub Project board. They write and prioritize issues; agents plan, build, review and fix; they approve plans and merge.
10
+
11
+ Explain in plain words. Ask before choosing for them; give a recommendation with one line of why. Docs live at https://kanbot.mib.im/<slug>. Link the page instead of repeating it.
12
+
13
+ ## Docs
14
+
15
+ | Question | Slug |
16
+ | --------------------------------------------- | ------------------- |
17
+ | How do I install and set up a repo? | `getting-started` |
18
+ | How does a card move across the board? | `workflow` |
19
+ | How do I file ideas by chatting? | `capturing-ideas` |
20
+ | My own PRs, local sessions, direct commits? | `outside-the-board` |
21
+ | Which agents are there, how do I change them? | `agents` |
22
+ | Skills and project memory? | `skills` |
23
+ | Claude or Codex? | `providers` |
24
+ | Every config field, labels, notifications | `configuration` |
25
+ | Copilot as an extra reviewer | `copilot-review` |
26
+ | Bot or user identity, merge by approving | `identity` |
27
+ | Upgrading and `.kanbot-new` files | `upgrading` |
28
+ | Every command and flag | `usage` |
29
+
30
+ ## Board flow
31
+
32
+ Ideas (you write) → Planning (agent plans, you comment) → Ready (you approve by moving the card) → In Progress (agent builds, opens a PR) → Review (two agent reviews with fixes; you comment) → Done (you merge, or approve and kanbot merges).
33
+
34
+ Agents never get a GitHub token. The daemon does every comment, push and PR. Only collaborators with write access can steer agents.
35
+
36
+ ## Guided setup
37
+
38
+ Go step by step. Run each command, check its output, then go on. If `.kanbot/config.yml` exists, run `kanbot doctor` first and skip what passes.
39
+
40
+ 1. **Requirements.** Bun 1.3+, `gh` logged in, `claude` or `codex` logged in with a subscription (not an API key). Run `gh auth refresh -s project`.
41
+ 2. **Provider.** Ask which subscription they have, then `bunx kanbot init --provider claude|codex` in the repo root.
42
+ 3. **Conventions.** Fill in `.agents/skills/project-conventions/SKILL.md` with them: stack, commands to test, lint and typecheck. Adjust `.github/workflows/ci.yml` to run the same. Every agent reads this first.
43
+ 4. **Board.** `kanbot board create`, or `kanbot board link --owner <login> --number <n>` for an existing project. Have them enable the four built-in project workflows it prints; they can't be set through the API.
44
+ 5. **Options.** Go through [Choose options](#choose-options). For app mode, run `kanbot app create` and have them install the app on the repo.
45
+ 6. **Commit and push** the kanbot files. Agents only see what's on the pushed base branch.
46
+ 7. **Check.** `kanbot doctor` until it passes, then `kanbot run --once --dry-run`, then `kanbot run`.
47
+ 8. **First card.** Have them write an issue, put it in Planning and read the plan. Point to `workflow`.
48
+
49
+ ## Choose options
50
+
51
+ Ask only about what matters to them. Defaults are fine for the rest. Options live in `.kanbot/config.yml`; restart the daemon after changes.
52
+
53
+ | Option | Ask | Recommend |
54
+ | ---------------------------------- | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
55
+ | `identity.mode` (`user` \| `app`) | Should kanbot talk as you, or as its own bot? | `app`: clear who said what, GitHub notifies you, and you can approve its PRs. `user` for zero setup. Commits are yours in both. |
56
+ | `merge.onApproval`, `merge.method` | Merge by approving the PR, or press merge yourself? | `true` with `app` mode. In `user` mode only another collaborator can approve. |
57
+ | `provider` | Claude or ChatGPT subscription? | Whichever they pay for. |
58
+ | `concurrency` | How many agents at once? | Default 3. Lower on a small machine or near usage limits. |
59
+ | `review.copilot` | Want GitHub Copilot as an extra reviewer? (costs credits) | Off unless they have a paid Copilot plan. |
60
+ | `notify.desktop`, `notify.webhook` | Where do you want to hear that a PR is ready? | Desktop. Add an ntfy.sh webhook for phone alerts. |
61
+ | `codex.networkAccess` | Do agents need to install packages? (Codex only) | Off unless installs fail. |
62
+ | `explore:N` label | Want several approaches built for some issues? | Per issue, not config. |
63
+
64
+ ### Who does what
65
+
66
+ | Action | `user` mode | `app` mode |
67
+ | ----------------------------------------- | ---------------------- | ------------ |
68
+ | Comments, PRs, labels, issue and PR edits | User, `🤖 kanbot` line | `<app>[bot]` |
69
+ | Commits, pushes, merges, board moves | User | User |
70
+
71
+ Board moves stay on the user's account: GitHub Apps can't use boards owned by a personal account.
72
+
73
+ ### App mode
74
+
75
+ - `kanbot app create` opens GitHub; they click **Create GitHub App**, then install it on the repo. The key goes in `~/.kanbot/apps/<id>.pem`; never commit it.
76
+ - Same app in another repo: `kanbot app link --id <id> --slug <slug>`, then add the repo to the app's installation.
77
+ - Back to user mode: `identity.mode: user`.
78
+
79
+ ### Merge on approval
80
+
81
+ kanbot merges, as the user, only `kanbot/*` PRs that are agent-reviewed, approved on the latest commit by a collaborator, with no open change request, passing checks, no conflicts and no `needs-human`. A push after approval needs a new approval.
82
+
83
+ Warn: never enable the branch rule "Require approval of the most recent reviewable push". The user's account pushes kanbot's commits, so it blocks their own approval.
84
+
85
+ ## Troubleshoot
86
+
87
+ 1. `kanbot doctor`: prints a fix for each failed check.
88
+ 2. `kanbot status`: board by column, daemon, failures, rate-limit pause.
89
+ 3. `.kanbot/logs/*.jsonl`: full agent transcripts.
90
+ 4. `needs-human` label: the issue has a comment saying why. Fix the cause, then remove the label to retry.
91
+ 5. Nothing happens: is the daemon running, are kanbot files pushed to the base branch, is the card in an active column?
@@ -13,7 +13,7 @@ Board flow:
13
13
  3. **Ready**: a human approves the plan by moving the card here.
14
14
  4. **In Progress**: an `implementer-*` agent builds the plan on a branch and commits.
15
15
  5. **Review**: `reviewer-code`, then `reviewer-spec`, then `fixer` for findings, human review comments, failing CI and merge conflicts.
16
- 6. **Done**: a human merges.
16
+ 6. **Done**: a human merges, or approves and the daemon merges.
17
17
 
18
18
  ## Golden rules
19
19