kanbot 0.0.0-stage → 0.2.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 (80) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +112 -2
  3. package/package.json +75 -5
  4. package/schema/config.schema.json +206 -0
  5. package/src/agents/load.ts +105 -0
  6. package/src/agents/sync.ts +219 -0
  7. package/src/cli.ts +27 -0
  8. package/src/commands/board.ts +140 -0
  9. package/src/commands/completion.ts +30 -0
  10. package/src/commands/doctor.ts +260 -0
  11. package/src/commands/init.ts +119 -0
  12. package/src/commands/run.ts +32 -0
  13. package/src/commands/skills.ts +63 -0
  14. package/src/commands/status.ts +70 -0
  15. package/src/commands/sync.ts +31 -0
  16. package/src/commands/upgrade.ts +70 -0
  17. package/src/completion.ts +217 -0
  18. package/src/config.ts +240 -0
  19. package/src/daemon/derive.ts +413 -0
  20. package/src/daemon/git.ts +187 -0
  21. package/src/daemon/lock.ts +29 -0
  22. package/src/daemon/loop.ts +212 -0
  23. package/src/daemon/notify.ts +44 -0
  24. package/src/daemon/snapshot.ts +109 -0
  25. package/src/daemon/state.ts +60 -0
  26. package/src/daemon/types.ts +73 -0
  27. package/src/errors.ts +171 -0
  28. package/src/github/api.ts +332 -0
  29. package/src/github/gh.ts +110 -0
  30. package/src/github/markers.ts +82 -0
  31. package/src/github/project.ts +288 -0
  32. package/src/github/trust.ts +38 -0
  33. package/src/jobs/prompts.ts +205 -0
  34. package/src/jobs/run.ts +569 -0
  35. package/src/known-hashes.json +132 -0
  36. package/src/log.ts +11 -0
  37. package/src/main.ts +21 -0
  38. package/src/providers/claude.ts +122 -0
  39. package/src/providers/codex.ts +98 -0
  40. package/src/providers/types.ts +94 -0
  41. package/src/schemas/results.ts +116 -0
  42. package/src/templates.ts +192 -0
  43. package/templates/.agents/skills/accessibility/SKILL.md +76 -0
  44. package/templates/.agents/skills/approach-designer/SKILL.md +79 -0
  45. package/templates/.agents/skills/code-review/SKILL.md +83 -0
  46. package/templates/.agents/skills/frontend-design/LICENSE.txt +177 -0
  47. package/templates/.agents/skills/frontend-design/SKILL.md +76 -0
  48. package/templates/.agents/skills/issue-splitting/SKILL.md +78 -0
  49. package/templates/.agents/skills/kanbot-admin/SKILL.md +57 -0
  50. package/templates/.agents/skills/kanbot-idea/SKILL.md +56 -0
  51. package/templates/.agents/skills/plan-template/SKILL.md +85 -0
  52. package/templates/.agents/skills/pr-description/SKILL.md +44 -0
  53. package/templates/.agents/skills/project-conventions/SKILL.md +79 -0
  54. package/templates/.agents/skills/project-memory/SKILL.md +39 -0
  55. package/templates/.agents/skills/project-memory/facts/.gitkeep +0 -0
  56. package/templates/.agents/skills/spec-review/SKILL.md +73 -0
  57. package/templates/.agents/skills/tdd/SKILL.md +51 -0
  58. package/templates/.agents/skills/user-stories/SKILL.md +82 -0
  59. package/templates/.agents/skills/verify-ui/SKILL.md +104 -0
  60. package/templates/.github/ISSUE_TEMPLATE/config.yml +1 -0
  61. package/templates/.github/ISSUE_TEMPLATE/technical.yml +15 -0
  62. package/templates/.github/ISSUE_TEMPLATE/user-story.yml +15 -0
  63. package/templates/.github/pull_request_template.md +15 -0
  64. package/templates/.github/workflows/ci.yml +23 -0
  65. package/templates/.github/workflows/kanbot-merged.yml +30 -0
  66. package/templates/.kanbot/agents/approach-designer.md +31 -0
  67. package/templates/.kanbot/agents/explorer.md +35 -0
  68. package/templates/.kanbot/agents/fixer.md +54 -0
  69. package/templates/.kanbot/agents/implementer-backend.md +57 -0
  70. package/templates/.kanbot/agents/implementer-docs.md +55 -0
  71. package/templates/.kanbot/agents/implementer-frontend.md +54 -0
  72. package/templates/.kanbot/agents/implementer-technical.md +53 -0
  73. package/templates/.kanbot/agents/plan-critic.md +36 -0
  74. package/templates/.kanbot/agents/planner.md +52 -0
  75. package/templates/.kanbot/agents/reviewer-code.md +46 -0
  76. package/templates/.kanbot/agents/reviewer-spec.md +49 -0
  77. package/templates/.kanbot/agents/test-writer.md +35 -0
  78. package/templates/.kanbot/agents/ui-verifier.md +35 -0
  79. package/templates/AGENTS.md +39 -0
  80. package/templates/CLAUDE.md +1 -0
@@ -0,0 +1,569 @@
1
+ import { mkdir } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import type { z } from "zod";
4
+ import { type AgentDef, getAgent } from "../agents/load.ts";
5
+ import { type Config, LABELS } from "../config.ts";
6
+ import {
7
+ type Action,
8
+ exploreCount,
9
+ type FixJob,
10
+ type ImplementJob,
11
+ type Job,
12
+ type PlanJob,
13
+ type ReviewJob,
14
+ reviewPasses,
15
+ variantOf,
16
+ } from "../daemon/derive.ts";
17
+ import type { Worktrees } from "../daemon/git.ts";
18
+ import type { Notifier } from "../daemon/notify.ts";
19
+ import type { State } from "../daemon/state.ts";
20
+ import {
21
+ addLabels,
22
+ addSubIssue,
23
+ createIssue,
24
+ createPR,
25
+ failedCheckLogs,
26
+ fetchIssue,
27
+ postComment,
28
+ react,
29
+ removeLabel,
30
+ replyToThread,
31
+ resolveThread,
32
+ updateIssueBody,
33
+ updatePRBody,
34
+ } from "../github/api.ts";
35
+ import type { Gh } from "../github/gh.ts";
36
+ import {
37
+ embedJson,
38
+ marker,
39
+ withDescription,
40
+ withPlan,
41
+ } from "../github/markers.ts";
42
+ import { addToBoard, type Board, moveItem } from "../github/project.ts";
43
+ import { log } from "../log.ts";
44
+ import type { Provider } from "../providers/types.ts";
45
+ import {
46
+ FixResult,
47
+ type ImplementResult,
48
+ jsonSchema,
49
+ PlanResult,
50
+ RESULT_SCHEMAS,
51
+ type ResultSchemaName,
52
+ ReviewResult,
53
+ } from "../schemas/results.ts";
54
+ import {
55
+ findingsList,
56
+ fixPrompt,
57
+ implementPrompt,
58
+ planPrompt,
59
+ reviewPrompt,
60
+ } from "./prompts.ts";
61
+
62
+ export type JobDeps = {
63
+ root: string;
64
+ config: Config;
65
+ gh: Gh;
66
+ board: Board;
67
+ agents: AgentDef[];
68
+ provider: Provider;
69
+ worktrees: Worktrees;
70
+ notify: Notifier;
71
+ state: State;
72
+ };
73
+
74
+ export class RateLimited extends Error {}
75
+
76
+ /** Thrown when a job ended in a state the human must resolve; not retried. */
77
+ export class NeedsHuman extends Error {}
78
+
79
+ async function runAgent<N extends ResultSchemaName>(
80
+ deps: JobDeps,
81
+ job: Job,
82
+ agentName: string,
83
+ schema: N,
84
+ cwd: string,
85
+ prompt: string,
86
+ ): Promise<z.infer<(typeof RESULT_SCHEMAS)[N]>> {
87
+ const agent = getAgent(deps.agents, agentName);
88
+ const logDir = join(deps.root, ".kanbot", "logs");
89
+ await mkdir(logDir, { recursive: true });
90
+ const logFile = join(
91
+ logDir,
92
+ `${new Date().toISOString().replace(/[:.]/g, "-")}_${job.key.replace(/[^a-zA-Z0-9-]/g, "_")}.jsonl`,
93
+ );
94
+ log.info(
95
+ `▶ ${job.key} → ${agent.name} (${deps.provider.name}) log: ${logFile}`,
96
+ );
97
+ const res = await deps.provider.run({
98
+ agent,
99
+ agents: deps.agents,
100
+ prompt,
101
+ cwd,
102
+ schema: jsonSchema(schema),
103
+ timeoutMs: deps.config.timeoutMinutes[job.kind] * 60_000,
104
+ logFile,
105
+ repoRoot: deps.root,
106
+ });
107
+ if (!res.ok) {
108
+ if (res.rateLimited) throw new RateLimited(res.error);
109
+ throw new Error(res.error);
110
+ }
111
+ return RESULT_SCHEMAS[schema].parse(res.output) as z.infer<
112
+ (typeof RESULT_SCHEMAS)[N]
113
+ >;
114
+ }
115
+
116
+ /**
117
+ * One worktree per branch, not per job: git refuses to check out a branch that
118
+ * another worktree holds, and derive never runs two jobs on one branch at once.
119
+ */
120
+ export function worktreeName(job: Job) {
121
+ const n = job.item.issue.number;
122
+ switch (job.kind) {
123
+ case "plan":
124
+ return `i${n}-plan`;
125
+ case "implement":
126
+ return `i${n}-v${job.variant ?? 1}`;
127
+ default:
128
+ return `i${n}-v${variantOf(job.pr.headRefName) ?? 1}`;
129
+ }
130
+ }
131
+
132
+ export function runJob(job: Job, deps: JobDeps): Promise<void> {
133
+ switch (job.kind) {
134
+ case "plan":
135
+ return plan(job, deps);
136
+ case "implement":
137
+ return implement(job, deps);
138
+ case "review":
139
+ return review(job, deps);
140
+ case "fix":
141
+ return fix(job, deps);
142
+ }
143
+ }
144
+
145
+ // ---- plan -------------------------------------------------------------------
146
+
147
+ async function plan(job: PlanJob, deps: JobDeps) {
148
+ const { gh } = deps;
149
+ const { issue } = job.item;
150
+ await Promise.all(job.comments.map((c) => react(gh, c.id, "EYES")));
151
+ const cwd = await deps.worktrees.detached(worktreeName(job));
152
+ const result = await runAgent(
153
+ deps,
154
+ job,
155
+ "planner",
156
+ "plan-result",
157
+ cwd,
158
+ planPrompt(job, exploreCount(issue.labels)),
159
+ );
160
+ PlanResult.parse(result);
161
+
162
+ // Re-read so concurrent human edits to the description are kept.
163
+ const fresh = await fetchIssue(gh, issue.number);
164
+ await updateIssueBody(gh, issue.number, withPlan(fresh.body, result.plan));
165
+
166
+ const allowed = result.labels.filter(
167
+ (l) => l === LABELS.technical || l.startsWith(LABELS.typePrefix),
168
+ );
169
+ await addLabels(
170
+ gh,
171
+ issue.number,
172
+ allowed.filter((l) => !fresh.labels.includes(l)),
173
+ );
174
+
175
+ const created: string[] = [];
176
+ for (const ni of result.newIssues) {
177
+ const res = await createIssue(
178
+ gh,
179
+ ni.title,
180
+ `${ni.body}\n\n_Split from #${issue.number} by kanbot._\n${marker("created", issue.number)}`,
181
+ );
182
+ if (!res) continue;
183
+ if (ni.subIssue) await addSubIssue(gh, issue.id, res.node_id);
184
+ await addToBoard(gh, deps.board, res.node_id, ni.column);
185
+ created.push(`#${res.number}`);
186
+ }
187
+
188
+ const lines = [
189
+ `**Plan ${job.initial ? "drafted" : "updated"}.** ${result.summary}`,
190
+ ];
191
+ const replyFor = new Map(result.replies.map((r) => [r.commentId, r.body]));
192
+ for (const c of job.comments) {
193
+ const reply =
194
+ replyFor.get(c.id) ?? "Noted; reflected in the plan where relevant.";
195
+ lines.push(`> ${excerpt(c.body)}\n\n${reply}`);
196
+ }
197
+ if (created.length) lines.push(`Created ${created.join(", ")}.`);
198
+ if (result.questions.length) {
199
+ lines.push(
200
+ `**Questions for you:**\n${result.questions.map((q) => `- ${q}`).join("\n")}`,
201
+ );
202
+ }
203
+ lines.push(
204
+ "_Comment to refine. Drag the card to **Ready** when the plan is approved._",
205
+ );
206
+ lines.push(
207
+ [marker("plan"), ...job.comments.map((c) => marker("re", c.id))].join("\n"),
208
+ );
209
+ await postComment(gh, issue.number, lines.join("\n\n"));
210
+ await Promise.all(job.comments.map((c) => react(gh, c.id, "THUMBS_UP")));
211
+ }
212
+
213
+ // ---- implement ----------------------------------------------------------------
214
+
215
+ async function implement(job: ImplementJob, deps: JobDeps) {
216
+ const { gh, config, worktrees } = deps;
217
+ const { issue } = job.item;
218
+ if (job.item.column === "ready") {
219
+ await moveItem(gh, deps.board, job.item.itemId, "inProgress");
220
+ await addLabels(gh, issue.number, [LABELS.working]);
221
+ }
222
+
223
+ const resumed = await worktrees.remoteHas(job.branch);
224
+ const cwd = await worktrees.checkout(worktreeName(job), job.branch);
225
+ let result: z.infer<typeof ImplementResult> | null = null;
226
+ if (!resumed) {
227
+ const typeLabel = issue.labels
228
+ .find((l) => l.startsWith(LABELS.typePrefix))
229
+ ?.slice(LABELS.typePrefix.length);
230
+ const agentName =
231
+ config.implementers[typeLabel ?? "default"] ??
232
+ config.implementers.default ??
233
+ "implementer-backend";
234
+ result = await runAgent(
235
+ deps,
236
+ job,
237
+ agentName,
238
+ "implement-result",
239
+ cwd,
240
+ implementPrompt(job, config.baseBranch),
241
+ );
242
+ if (result.status === "blocked") {
243
+ await addLabels(gh, issue.number, [LABELS.needsHuman]);
244
+ await postComment(
245
+ gh,
246
+ issue.number,
247
+ `**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
+ );
249
+ deps.notify.send(
250
+ `Blocked: #${issue.number}`,
251
+ result.blockedReason ?? issue.title,
252
+ issue.url,
253
+ );
254
+ throw new NeedsHuman("agent reported blocked");
255
+ }
256
+ await worktrees.commitAll(
257
+ cwd,
258
+ `feat: ${result.prTitle} (#${issue.number})`,
259
+ );
260
+ if (
261
+ (await worktrees.commitsAhead(cwd, `origin/${config.baseBranch}`)) === 0
262
+ ) {
263
+ throw new Error("agent finished without any changes");
264
+ }
265
+ await worktrees.push(cwd, job.branch);
266
+ } else {
267
+ log.info(
268
+ `${job.key}: branch ${job.branch} already pushed; opening PR only`,
269
+ );
270
+ }
271
+
272
+ const variant =
273
+ job.variant === null ? "" : ` [approach ${job.variant}/${job.variants}]`;
274
+ const body = [
275
+ withDescription(
276
+ "",
277
+ result
278
+ ? absoluteImages(result.prBody, config.repo, job.branch)
279
+ : `Implements #${issue.number}.`,
280
+ ),
281
+ result?.risks.length
282
+ ? `### Risks\n${result.risks.map((r) => `- ${r}`).join("\n")}`
283
+ : "",
284
+ result?.testsRun.length
285
+ ? `### Checks\n${result.testsRun.map((t) => `- ${t}`).join("\n")}`
286
+ : "",
287
+ job.variant === null
288
+ ? ""
289
+ : `> Exploratory: approach ${job.variant} of ${job.variants}. Merging one closes the others.`,
290
+ `Closes #${issue.number}`,
291
+ marker("pr", issue.number),
292
+ ]
293
+ .filter(Boolean)
294
+ .join("\n\n");
295
+ const pr = await createPR(gh, {
296
+ title: `${result?.prTitle ?? issue.title}${variant}`,
297
+ body,
298
+ head: job.branch,
299
+ base: config.baseBranch,
300
+ });
301
+ if (pr) {
302
+ const labels = issue.labels.filter(
303
+ (l) => l.startsWith(LABELS.typePrefix) || l === LABELS.technical,
304
+ );
305
+ if (job.variant !== null)
306
+ labels.push(`${LABELS.variantPrefix}${job.variant}`);
307
+ await addLabels(gh, pr.number, labels);
308
+ log.info(`✔ opened ${pr.html_url}`);
309
+ }
310
+ }
311
+
312
+ /** First line of a comment, cut at a word boundary, for quoting in replies. */
313
+ export function excerpt(body: string, max = 120) {
314
+ const line = body.trim().split("\n")[0] ?? "";
315
+ if (line.length <= max) return line;
316
+ const cut = line.slice(0, max);
317
+ const space = cut.lastIndexOf(" ");
318
+ return `${(space > max / 2 ? cut.slice(0, space) : cut).trimEnd()}…`;
319
+ }
320
+
321
+ /** PR bodies don't render repo-relative images; point them at the branch. */
322
+ export function absoluteImages(md: string, repo: string, branch: string) {
323
+ return md.replace(
324
+ /!\[([^\]]*)\]\((?!https?:|\/\/)(?:\.\/)?([^)\s]+)\)/g,
325
+ (_, alt, path) =>
326
+ `![${alt}](https://github.com/${repo}/blob/${branch}/${path}?raw=true)`,
327
+ );
328
+ }
329
+
330
+ // ---- review -------------------------------------------------------------------
331
+
332
+ const PASS_AGENT = { 1: "reviewer-code", 2: "reviewer-spec" } as const;
333
+ const PASS_NAME = { 1: "code", 2: "spec" } as const;
334
+
335
+ async function review(job: ReviewJob, deps: JobDeps) {
336
+ const cwd = await deps.worktrees.checkout(
337
+ worktreeName(job),
338
+ job.pr.headRefName,
339
+ );
340
+ const result = await runAgent(
341
+ deps,
342
+ job,
343
+ PASS_AGENT[job.pass],
344
+ "review-result",
345
+ cwd,
346
+ reviewPrompt(job, deps.config.baseBranch),
347
+ );
348
+ ReviewResult.parse(result);
349
+ const icon = result.verdict === "approve" ? "✅" : "🛠️";
350
+ const body = [
351
+ `### ${icon} Agent review ${job.pass}/${reviewPasses(job.item.issue).length} (${PASS_NAME[job.pass]}): ${result.verdict === "approve" ? "approve" : "changes requested"}`,
352
+ result.summary,
353
+ result.findings.length ? findingsList(result.findings) : "_No findings._",
354
+ marker("review", job.pass),
355
+ embedJson("findings", result.findings),
356
+ ].join("\n\n");
357
+ await postComment(deps.gh, job.pr.number, body);
358
+ }
359
+
360
+ // ---- fix ----------------------------------------------------------------------
361
+
362
+ async function fix(job: FixJob, deps: JobDeps) {
363
+ const { gh, worktrees, config } = deps;
364
+ const { pr } = job;
365
+ const cwd = await worktrees.checkout(worktreeName(job), pr.headRefName);
366
+ if (job.reason.type === "conflict") return mergeBase(job, deps, cwd);
367
+ const before = await worktrees.head(cwd);
368
+ const logs =
369
+ job.reason.type === "ci"
370
+ ? await failedCheckLogs(gh, pr.headSha).catch(() => null)
371
+ : null;
372
+ const result = await runAgent(
373
+ deps,
374
+ job,
375
+ "fixer",
376
+ "fix-result",
377
+ cwd,
378
+ fixPrompt(job, config.baseBranch, logs),
379
+ );
380
+ FixResult.parse(result);
381
+
382
+ await worktrees.commitAll(
383
+ cwd,
384
+ `fix: address ${job.reason.type === "ci" ? "CI failure" : "review feedback"} (#${job.item.issue.number})`,
385
+ );
386
+ const changed = (await worktrees.head(cwd)) !== before;
387
+ if (changed) await worktrees.push(cwd, pr.headRefName);
388
+ if (result.prBody) {
389
+ const desc = absoluteImages(result.prBody, config.repo, pr.headRefName);
390
+ await updatePRBody(gh, pr.number, withDescription(pr.body, desc));
391
+ }
392
+ const by = new Map(result.addressed.map((a) => [a.ref, a]));
393
+ const pushed = changed ? "Pushed fixes." : "No code changes.";
394
+
395
+ const r = job.reason;
396
+ if (r.type === "review") {
397
+ const lines = r.findings.map((f) => {
398
+ const a = by.get(f.id);
399
+ return `- ${a?.resolved ? "✅" : "⏭️"} **${f.id}** ${a?.reply ?? "Not addressed."}`;
400
+ });
401
+ await postComment(
402
+ gh,
403
+ pr.number,
404
+ [
405
+ `**Addressed agent review ${r.pass}/${reviewPasses(job.item.issue).length}.** ${result.summary} ${pushed}`,
406
+ lines.join("\n"),
407
+ marker("fixed", r.pass),
408
+ ].join("\n\n"),
409
+ );
410
+ return;
411
+ }
412
+ if (r.type === "ci") {
413
+ await postComment(
414
+ gh,
415
+ pr.number,
416
+ `**CI fix attempt.** ${result.summary} ${pushed}\n\n${marker("ci", pr.headSha)}`,
417
+ );
418
+ return;
419
+ }
420
+
421
+ // Answer every thread so it no longer has a human or Copilot as last word.
422
+ let open = 0;
423
+ for (const t of r.threads) {
424
+ const a = by.get(t.id);
425
+ await replyToThread(
426
+ gh,
427
+ t.id,
428
+ `${a?.reply ?? "Looked at this but made no change."}\n\n${marker("re", t.id)}`,
429
+ );
430
+ if (a?.resolved) await resolveThread(gh, t.id);
431
+ else open++;
432
+ }
433
+ if (r.type === "copilot") return;
434
+ if (r.comments.length) {
435
+ const lines = r.comments.map((c) => {
436
+ const a = by.get(c.id);
437
+ if (!a?.resolved) open++;
438
+ return `> ${excerpt(c.body)}\n\n${a?.reply ?? "Noted."}`;
439
+ });
440
+ await postComment(
441
+ gh,
442
+ pr.number,
443
+ [
444
+ `**Feedback round.** ${result.summary} ${pushed}`,
445
+ ...lines,
446
+ r.comments.map((c) => marker("re", c.id)).join("\n"),
447
+ ].join("\n\n"),
448
+ );
449
+ }
450
+ const msg = open
451
+ ? `${open} item(s) need your input.`
452
+ : "All feedback addressed — ready for another look.";
453
+ if (!r.comments.length)
454
+ await postComment(
455
+ gh,
456
+ pr.number,
457
+ `${msg} ${pushed}\n\n${marker("feedback-round")}`,
458
+ );
459
+ deps.notify.send(
460
+ `PR #${pr.number}: ${open ? "needs input" : "feedback addressed"}`,
461
+ msg,
462
+ pr.url,
463
+ );
464
+ }
465
+
466
+ // ---- merge base ---------------------------------------------------------------
467
+
468
+ const CONFLICT_MARKER = /^(<{7}|>{7})( |$)/m;
469
+
470
+ /** Merge the base branch into a conflicting PR; the fixer resolves conflicts. */
471
+ async function mergeBase(job: FixJob, deps: JobDeps, cwd: string) {
472
+ const { gh, worktrees, config } = deps;
473
+ const { pr } = job;
474
+ const base = `origin/${config.baseBranch}`;
475
+ const files = await worktrees.merge(cwd, base);
476
+ let summary = "It merged cleanly.";
477
+ if (files.length) {
478
+ const result = await runAgent(
479
+ deps,
480
+ job,
481
+ "fixer",
482
+ "fix-result",
483
+ cwd,
484
+ fixPrompt(job, config.baseBranch, files.map((f) => `- ${f}`).join("\n")),
485
+ );
486
+ FixResult.parse(result);
487
+ const left: string[] = [];
488
+ for (const f of files) {
489
+ const file = Bun.file(join(cwd, f));
490
+ if ((await file.exists()) && CONFLICT_MARKER.test(await file.text()))
491
+ left.push(f);
492
+ }
493
+ if (left.length)
494
+ throw new Error(`conflict markers left in ${left.join(", ")}`);
495
+ summary = `${result.summary} Resolved conflicts in ${files.map((f) => `\`${f}\``).join(", ")}.`;
496
+ }
497
+ await worktrees.concludeMerge(cwd);
498
+ await worktrees.commitAll(
499
+ cwd,
500
+ `fix: resolve merge conflicts (#${job.item.issue.number})`,
501
+ );
502
+ if (!(await worktrees.isAncestor(cwd, base)))
503
+ throw new Error(`${base} was not merged`);
504
+ await worktrees.push(cwd, pr.headRefName);
505
+ await postComment(
506
+ gh,
507
+ pr.number,
508
+ `**Merged \`${config.baseBranch}\`.** ${summary}\n\n${marker("conflict", pr.headSha)}`,
509
+ );
510
+ }
511
+
512
+ // ---- deterministic actions ----------------------------------------------------
513
+
514
+ export async function applyAction(action: Action, deps: JobDeps) {
515
+ const { gh } = deps;
516
+ switch (action.kind) {
517
+ case "move":
518
+ log.info(`#${action.item.issue.number} → ${action.to} (${action.why})`);
519
+ return moveItem(gh, deps.board, action.item.itemId, action.to);
520
+ case "label":
521
+ await addLabels(gh, action.number, action.add);
522
+ for (const l of action.remove) await removeLabel(gh, action.number, l);
523
+ return;
524
+ case "agentReviewed":
525
+ await addLabels(gh, action.pr.number, [LABELS.agentReviewed]);
526
+ await postComment(
527
+ gh,
528
+ action.pr.number,
529
+ `**Agent review complete.** Ready for you to test and merge.\n\n${marker("ready")}`,
530
+ );
531
+ deps.notify.send(
532
+ `Ready to test: PR #${action.pr.number}`,
533
+ action.pr.title,
534
+ action.pr.url,
535
+ );
536
+ return;
537
+ case "compare": {
538
+ const rows = action.prs.map(
539
+ (p) =>
540
+ `| ${p.labels.find((l) => l.startsWith(LABELS.variantPrefix)) ?? "?"} | #${p.number} | ${p.title} |`,
541
+ );
542
+ await postComment(
543
+ gh,
544
+ action.item.issue.number,
545
+ `**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
+ );
547
+ deps.notify.send(
548
+ `Compare approaches: #${action.item.issue.number}`,
549
+ action.item.issue.title,
550
+ action.item.issue.url,
551
+ );
552
+ return;
553
+ }
554
+ case "needsHuman":
555
+ await addLabels(gh, action.number, [LABELS.needsHuman]);
556
+ await postComment(
557
+ gh,
558
+ action.number,
559
+ `**kanbot needs a human.** ${action.why}\n\nRemove \`${LABELS.needsHuman}\` to let agents retry.\n${marker("needs-human")}`,
560
+ );
561
+ deps.notify.send(`Needs human: #${action.number}`, action.why);
562
+ return;
563
+ case "resetFailures":
564
+ deps.state.resetFailures(action.prefix);
565
+ return;
566
+ case "cleanup":
567
+ return deps.worktrees.remove(`i${action.number}-`);
568
+ }
569
+ }
@@ -0,0 +1,132 @@
1
+ {
2
+ ".agents/skills/accessibility/SKILL.md": [
3
+ "bb8c5148899685168324b8fda3524937e3943b32"
4
+ ],
5
+ ".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"
16
+ ],
17
+ ".agents/skills/issue-splitting/SKILL.md": [
18
+ "01625372d68a81347bf401e8603084420922d04e"
19
+ ],
20
+ ".agents/skills/kanbot-admin/SKILL.md": [
21
+ "3789c559be9fb6a78f82708ae9857395a5556f6b",
22
+ "e427c711b532963c0916f07ecc6b11e9b47cb9fa"
23
+ ],
24
+ ".agents/skills/kanbot-idea/SKILL.md": [
25
+ "8ab28cec8d3f30250ffe080106d0de8e3c2eaa8a"
26
+ ],
27
+ ".agents/skills/plan-template/SKILL.md": [
28
+ "47a5dbbd2870883ca42dd9410d224ef9c0c9979f",
29
+ "79cfd0bf36110b5d6f452e6400161609b11df0d6",
30
+ "7bfde45995efaf515e45ccec09c204178203fdb7"
31
+ ],
32
+ ".agents/skills/pr-description/SKILL.md": [
33
+ "1db6eb751e0bb1ea0014a2b414e04f6af59b1b2d",
34
+ "6b0cca1be3abe8d031159c94e5d3b4f20307d600",
35
+ "7e477762c9c5f09ce6d5d51ccd9138ce9743baa8"
36
+ ],
37
+ ".agents/skills/project-conventions/SKILL.md": [
38
+ "81d9ff980a15a6f901010646b800b3e6e379e4b0",
39
+ "dabf1dadc148c020496d8f2f9bb8de6269f56479"
40
+ ],
41
+ ".agents/skills/project-memory/facts/.gitkeep": [
42
+ "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"
43
+ ],
44
+ ".agents/skills/project-memory/SKILL.md": [
45
+ "d354e11310db01362ec2bf5eec1080db77ac053d"
46
+ ],
47
+ ".agents/skills/spec-review/SKILL.md": [
48
+ "03e44fe4f23b04f1b11c907936ee9f6c7c8131f0",
49
+ "f455d1007d589602e5afce0dac4f00d78aa7c67a"
50
+ ],
51
+ ".agents/skills/tdd/SKILL.md": ["5e6b20868e02ae24f6d25824bac431143bd43021"],
52
+ ".agents/skills/user-stories/SKILL.md": [
53
+ "1b37f8e59c15b934d555070a63d4f0d3939a007f",
54
+ "1b5d229d043c1bfbd84aecd9ee6191b1d473f1d8"
55
+ ],
56
+ ".agents/skills/verify-ui/SKILL.md": [
57
+ "c67a0e1d6a1c1d9adf59dd154160ccf58519ea72",
58
+ "f4d3d07c0ec28a9961d919356d0b4e572b724b43"
59
+ ],
60
+ ".github/ISSUE_TEMPLATE/config.yml": [
61
+ "0086358db1eb971c0cfa8739c27518bbc18a5ff4"
62
+ ],
63
+ ".github/ISSUE_TEMPLATE/technical.yml": [
64
+ "7e9d95fbb7192f8116ecfc870e6eb07c06e235a6"
65
+ ],
66
+ ".github/ISSUE_TEMPLATE/user-story.yml": [
67
+ "d26a148970ba6323d9cfdd86924cb2e82e68b705"
68
+ ],
69
+ ".github/pull_request_template.md": [
70
+ "57cfbc21472a94ccccb19ef6f19e87a473925649"
71
+ ],
72
+ ".github/workflows/ci.yml": ["dce7bcf80cef91c10923829eb5893d2b311b58e5"],
73
+ ".github/workflows/kanbot-merged.yml": [
74
+ "8daaea63e23cf1e0d37821bfadaf60018a448de2"
75
+ ],
76
+ ".kanbot/agents/approach-designer.md": [
77
+ "7e5f8c16c4d4a4e261bccc5dc8b4fff2d3046a72"
78
+ ],
79
+ ".kanbot/agents/explorer.md": [
80
+ "4346bd34d438932f0a912ad0aad3d5f789e1e7dd",
81
+ "f85fe4782bdce4a691fa4ab41fb8195d52b70209"
82
+ ],
83
+ ".kanbot/agents/fixer.md": [
84
+ "109c8974fe2d70f5f514f33d97761740f589a6c4",
85
+ "7c98cf1dc3e798e0e5856fd95a3d78d378884def",
86
+ "810871ec49fd7be56d111c7df176d22c66748a0f"
87
+ ],
88
+ ".kanbot/agents/implementer-backend.md": [
89
+ "5d454e29260ba8df0ef6e21f1ece9d02a472d4ed",
90
+ "6fefffb02bb5551631839abd0863379e58bc7dcc"
91
+ ],
92
+ ".kanbot/agents/implementer-docs.md": [
93
+ "082da045a20bc914f29839e026027b6715c184a9",
94
+ "c5f01ac3d377133caab5b13422f1e73608d6357e"
95
+ ],
96
+ ".kanbot/agents/implementer-frontend.md": [
97
+ "0aa9b0e70c3057138b4255ee3d270ac04fc8ab0e",
98
+ "45a6e3b8f6e6f5a95dac865126c5649dab115fc5"
99
+ ],
100
+ ".kanbot/agents/implementer-technical.md": [
101
+ "c95dff48eed5db4aadf10d0a47e7ec1285937ebf",
102
+ "e4bbddb0a810313fb3a6ed3983b3b9335279feef"
103
+ ],
104
+ ".kanbot/agents/plan-critic.md": ["cd6dd133f4611bd86fe27f8539a62a805e4df6c9"],
105
+ ".kanbot/agents/planner.md": [
106
+ "2283cf8bf6d27b8bbf721f97bb45cd40eaa6d39d",
107
+ "bdb111213cced70583157edad658247972dcbe45"
108
+ ],
109
+ ".kanbot/agents/reviewer-code.md": [
110
+ "398035214b63ceb3469a3b3384b07209e4cd63dd",
111
+ "61f3a45642ca954c6d4a5f41083f0a25f78d2b51"
112
+ ],
113
+ ".kanbot/agents/reviewer-spec.md": [
114
+ "11c86a654a0b715777b23bfb699a4f0d6cfe91de",
115
+ "ab8951ab296e96b01f419243d8acfc99c7e64b1c"
116
+ ],
117
+ ".kanbot/agents/test-writer.md": [
118
+ "dcef7f611a307229974fef4098d53816d7db0555",
119
+ "ec81c44c021053fae0612683a61e8414634359a0"
120
+ ],
121
+ ".kanbot/agents/ui-verifier.md": [
122
+ "38615159a8fe7b1e99cacedc49c0be53c69b90d4",
123
+ "a73abf7e72c654fde41df51b43a5879170978cb0"
124
+ ],
125
+ "AGENTS.md": [
126
+ "3da9115cbf52b7b7d0feb8325503a8caf43ff2c7",
127
+ "b821013b5be7051327b349eda603218590a41f33",
128
+ "cf7ef593ccfda953055dfb76b3e9c267fc9637f6",
129
+ "e5b76bcbaf155b79a8ff3f2e70977cc0c589566b"
130
+ ],
131
+ "CLAUDE.md": ["43c994c2d3617f947bcb5adf1933e21dabe46bb5"]
132
+ }
package/src/log.ts ADDED
@@ -0,0 +1,11 @@
1
+ const ts = () => new Date().toISOString().slice(11, 19);
2
+
3
+ export const log = {
4
+ verbose: false,
5
+ info: (msg: string) => console.log(`${ts()} ${msg}`),
6
+ warn: (msg: string) => console.warn(`${ts()} ⚠ ${msg}`),
7
+ error: (msg: string) => console.error(`${ts()} ✖ ${msg}`),
8
+ debug: (msg: string) => {
9
+ if (log.verbose) console.log(`${ts()} · ${msg}`);
10
+ },
11
+ };