kanbot 0.5.2 → 0.6.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/README.md +1 -1
- package/package.json +1 -1
- package/schema/config.schema.json +21 -0
- package/src/ask.ts +88 -0
- package/src/commands/app.ts +73 -80
- package/src/commands/board.ts +84 -48
- package/src/commands/doctor.ts +20 -18
- package/src/commands/init.ts +246 -56
- package/src/commands/report.ts +74 -0
- package/src/commands/status.ts +10 -2
- package/src/config.ts +14 -0
- package/src/daemon/derive.ts +10 -0
- package/src/daemon/loop.ts +11 -1
- package/src/github/api.ts +46 -0
- package/src/github/project.ts +55 -0
- package/src/known-hashes.json +3 -0
- package/src/main.ts +1 -0
- package/src/providers/login.ts +46 -0
- package/src/report.ts +245 -0
- package/src/setup.ts +170 -0
- package/templates/.agents/skills/kanbot-guide/SKILL.md +20 -18
package/src/main.ts
CHANGED
|
@@ -17,6 +17,7 @@ export const main = defineCommand({
|
|
|
17
17
|
doctor: () => import("./commands/doctor.ts").then((m) => m.default),
|
|
18
18
|
run: () => import("./commands/run.ts").then((m) => m.default),
|
|
19
19
|
status: () => import("./commands/status.ts").then((m) => m.default),
|
|
20
|
+
report: () => import("./commands/report.ts").then((m) => m.default),
|
|
20
21
|
completion: () => import("./commands/completion.ts").then((m) => m.default),
|
|
21
22
|
},
|
|
22
23
|
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Config } from "../config.ts";
|
|
2
|
+
|
|
3
|
+
export type Login = {
|
|
4
|
+
/** The CLI is on PATH. */
|
|
5
|
+
installed: boolean;
|
|
6
|
+
/** Logged in with a subscription, not an API key. */
|
|
7
|
+
ok: boolean;
|
|
8
|
+
detail?: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
async function cmd(args: string[]) {
|
|
12
|
+
const p = Bun.spawn(args, { stdout: "pipe", stderr: "pipe" });
|
|
13
|
+
const [out, err, code] = await Promise.all([
|
|
14
|
+
new Response(p.stdout).text(),
|
|
15
|
+
new Response(p.stderr).text(),
|
|
16
|
+
p.exited,
|
|
17
|
+
]);
|
|
18
|
+
return { ok: code === 0, out: `${out}${err}`.trim() };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Whether the provider's CLI is installed and logged in with a subscription. */
|
|
22
|
+
export async function providerLogin(
|
|
23
|
+
provider: Config["provider"],
|
|
24
|
+
): Promise<Login> {
|
|
25
|
+
if (!Bun.which(provider)) return { installed: false, ok: false };
|
|
26
|
+
if (provider === "claude") {
|
|
27
|
+
const s = await cmd(["claude", "auth", "status"]);
|
|
28
|
+
const sub = /"authMethod":\s*"claude\.ai"/.test(s.out);
|
|
29
|
+
return {
|
|
30
|
+
installed: true,
|
|
31
|
+
ok: s.ok && sub,
|
|
32
|
+
detail: sub ? undefined : "Not using a claude.ai login.",
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
const s = await cmd(["codex", "login", "status"]);
|
|
36
|
+
return {
|
|
37
|
+
installed: true,
|
|
38
|
+
ok: s.ok && /ChatGPT/i.test(s.out),
|
|
39
|
+
detail: s.out.split("\n")[0],
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const LOGIN_FIX: Record<Config["provider"], string> = {
|
|
44
|
+
claude: "claude auth login",
|
|
45
|
+
codex: "codex login",
|
|
46
|
+
};
|
package/src/report.ts
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { UserError } from "./errors.ts";
|
|
2
|
+
|
|
3
|
+
/** A PR or issue touched in the report period, with its comments. */
|
|
4
|
+
export type Activity = {
|
|
5
|
+
kind: "pr" | "issue";
|
|
6
|
+
number: number;
|
|
7
|
+
title: string;
|
|
8
|
+
url: string;
|
|
9
|
+
/** PRs only. */
|
|
10
|
+
headRefName?: string;
|
|
11
|
+
createdAt: string;
|
|
12
|
+
mergedAt?: string | null;
|
|
13
|
+
comments: { body: string; createdAt: string }[];
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/** The board right now, from the snapshot. */
|
|
17
|
+
export type BoardNow = {
|
|
18
|
+
waiting: number;
|
|
19
|
+
limit: number | null;
|
|
20
|
+
needsHuman: number;
|
|
21
|
+
/** Open cards in Planning, Ready, In Progress or Review. */
|
|
22
|
+
active: number;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type Loops = {
|
|
26
|
+
review: number;
|
|
27
|
+
ci: number;
|
|
28
|
+
conflict: number;
|
|
29
|
+
feedback: number;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export type Health = "ON_TRACK" | "AT_RISK" | "OFF_TRACK" | "INACTIVE";
|
|
33
|
+
|
|
34
|
+
export type Report = {
|
|
35
|
+
since: string;
|
|
36
|
+
until: string;
|
|
37
|
+
merged: {
|
|
38
|
+
number: number;
|
|
39
|
+
title: string;
|
|
40
|
+
url: string;
|
|
41
|
+
cycleMs: number;
|
|
42
|
+
waitMs: number | null;
|
|
43
|
+
}[];
|
|
44
|
+
loops: Loops;
|
|
45
|
+
needsHuman: { number: number; url: string; reason: string }[];
|
|
46
|
+
now: BoardNow;
|
|
47
|
+
health: Health;
|
|
48
|
+
why: string[];
|
|
49
|
+
next: string[];
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const LOOP_MARKERS: Record<keyof Loops, string> = {
|
|
53
|
+
review: "fixed",
|
|
54
|
+
ci: "ci",
|
|
55
|
+
conflict: "conflict",
|
|
56
|
+
feedback: "feedback-round",
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/** True for `<!-- kanbot:kind -->` and `<!-- kanbot:kind:… -->`. */
|
|
60
|
+
const has = (body: string, kind: string) =>
|
|
61
|
+
body.includes(`<!-- kanbot:${kind} `) ||
|
|
62
|
+
body.includes(`<!-- kanbot:${kind}:`);
|
|
63
|
+
|
|
64
|
+
/** `7d`, `2w`, `12h` or `YYYY-MM-DD` → start of the period. */
|
|
65
|
+
export function parseSince(value: string, now = Date.now()): Date {
|
|
66
|
+
const m = value.match(/^(\d+)([hdw])$/);
|
|
67
|
+
if (m) {
|
|
68
|
+
const hours = { h: 1, d: 24, w: 24 * 7 }[m[2] as "h" | "d" | "w"];
|
|
69
|
+
return new Date(now - Number(m[1]) * hours * 3_600_000);
|
|
70
|
+
}
|
|
71
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(value)) {
|
|
72
|
+
const d = new Date(`${value}T00:00:00Z`);
|
|
73
|
+
if (!Number.isNaN(d.getTime())) return d;
|
|
74
|
+
}
|
|
75
|
+
throw new UserError(`Can't read --since "${value}".`, {
|
|
76
|
+
why: "Use a duration like 7d, 2w or 12h, or a date like 2026-10-01.",
|
|
77
|
+
fix: "kanbot report --since 7d",
|
|
78
|
+
more: "",
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** The reason line from a `needs-human` or `blocked` comment. */
|
|
83
|
+
export function reasonOf(body: string): string {
|
|
84
|
+
const line =
|
|
85
|
+
body
|
|
86
|
+
.replace(/<sub>.*?<\/sub>/g, "")
|
|
87
|
+
.replace(/<!--.*?-->/g, "")
|
|
88
|
+
.replace(/^\s*\*\*[^*]+\*\*\s*/m, "")
|
|
89
|
+
.split("\n")
|
|
90
|
+
.map((l) => l.trim())
|
|
91
|
+
.find((l) => l && !l.startsWith("Remove `")) ?? "";
|
|
92
|
+
return line.length > 90 ? `${line.slice(0, 89)}…` : line || "no reason given";
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function median(xs: number[]): number | null {
|
|
96
|
+
if (!xs.length) return null;
|
|
97
|
+
const s = [...xs].sort((a, b) => a - b);
|
|
98
|
+
const mid = Math.floor(s.length / 2);
|
|
99
|
+
return s.length % 2
|
|
100
|
+
? (s[mid] as number)
|
|
101
|
+
: ((s[mid - 1] as number) + (s[mid] as number)) / 2;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Short duration like `45m`, `6h` or `2d 4h`. */
|
|
105
|
+
export function duration(ms: number): string {
|
|
106
|
+
const m = Math.max(0, Math.floor(ms / 60_000));
|
|
107
|
+
if (m < 60) return `${m}m`;
|
|
108
|
+
const h = Math.floor(m / 60);
|
|
109
|
+
if (h < 24) return `${h}h`;
|
|
110
|
+
const d = Math.floor(h / 24);
|
|
111
|
+
return h % 24 ? `${d}d ${h % 24}h` : `${d}d`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function buildReport(
|
|
115
|
+
activity: Activity[],
|
|
116
|
+
now: BoardNow,
|
|
117
|
+
since: Date,
|
|
118
|
+
until = new Date(),
|
|
119
|
+
): Report {
|
|
120
|
+
const inPeriod = (at: string) => Date.parse(at) >= since.getTime();
|
|
121
|
+
const loops: Loops = { review: 0, ci: 0, conflict: 0, feedback: 0 };
|
|
122
|
+
const merged: Report["merged"] = [];
|
|
123
|
+
|
|
124
|
+
for (const a of activity) {
|
|
125
|
+
if (a.kind !== "pr" || !a.headRefName?.startsWith("kanbot/")) continue;
|
|
126
|
+
if (!a.mergedAt || !inPeriod(a.mergedAt)) continue;
|
|
127
|
+
const mergedAt = Date.parse(a.mergedAt);
|
|
128
|
+
for (const c of a.comments)
|
|
129
|
+
for (const [k, kind] of Object.entries(LOOP_MARKERS))
|
|
130
|
+
if (has(c.body, kind)) loops[k as keyof Loops]++;
|
|
131
|
+
// The last "agent review complete" before merge starts the final wait.
|
|
132
|
+
const ready = a.comments
|
|
133
|
+
.filter(
|
|
134
|
+
(c) => has(c.body, "ready") && Date.parse(c.createdAt) <= mergedAt,
|
|
135
|
+
)
|
|
136
|
+
.at(-1);
|
|
137
|
+
merged.push({
|
|
138
|
+
number: a.number,
|
|
139
|
+
title: a.title,
|
|
140
|
+
url: a.url,
|
|
141
|
+
cycleMs: mergedAt - Date.parse(a.createdAt),
|
|
142
|
+
waitMs: ready ? mergedAt - Date.parse(ready.createdAt) : null,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const needsHuman = activity.flatMap((a) =>
|
|
147
|
+
a.comments
|
|
148
|
+
.filter(
|
|
149
|
+
(c) =>
|
|
150
|
+
inPeriod(c.createdAt) &&
|
|
151
|
+
(has(c.body, "needs-human") || has(c.body, "blocked")),
|
|
152
|
+
)
|
|
153
|
+
.map((c) => ({ number: a.number, url: a.url, reason: reasonOf(c.body) })),
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
const report = {
|
|
157
|
+
since: since.toISOString(),
|
|
158
|
+
until: until.toISOString(),
|
|
159
|
+
merged,
|
|
160
|
+
loops,
|
|
161
|
+
needsHuman,
|
|
162
|
+
now,
|
|
163
|
+
};
|
|
164
|
+
return { ...report, ...healthOf(report), next: nextSteps(report) };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
type Base = Omit<Report, "health" | "why" | "next">;
|
|
168
|
+
|
|
169
|
+
/** Board health from fixed rules, so it means the same every week. */
|
|
170
|
+
export function healthOf(r: Base): { health: Health; why: string[] } {
|
|
171
|
+
const { now } = r;
|
|
172
|
+
if (!r.merged.length && !now.active)
|
|
173
|
+
return { health: "INACTIVE", why: ["Nothing merged and no active cards."] };
|
|
174
|
+
const why: string[] = [];
|
|
175
|
+
if (!r.merged.length && (now.waiting || now.needsHuman)) {
|
|
176
|
+
why.push(`Nothing merged, ${now.waiting} item(s) waiting on you.`);
|
|
177
|
+
return { health: "OFF_TRACK", why };
|
|
178
|
+
}
|
|
179
|
+
if (now.limit !== null && now.waiting >= now.limit)
|
|
180
|
+
why.push(`WIP limit reached (${now.waiting}/${now.limit}).`);
|
|
181
|
+
if (now.needsHuman)
|
|
182
|
+
why.push(`${now.needsHuman} card(s) labeled needs-human.`);
|
|
183
|
+
return { health: why.length ? "AT_RISK" : "ON_TRACK", why };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** At most two concrete changes to try. */
|
|
187
|
+
export function nextSteps(r: Base): string[] {
|
|
188
|
+
const out: string[] = [];
|
|
189
|
+
const n = r.merged.length;
|
|
190
|
+
const cycle = median(r.merged.map((m) => m.cycleMs));
|
|
191
|
+
const wait = median(
|
|
192
|
+
r.merged.flatMap((m) => (m.waitMs === null ? [] : [m.waitMs])),
|
|
193
|
+
);
|
|
194
|
+
if (r.now.needsHuman)
|
|
195
|
+
out.push("Clear needs-human cards: fix the cause, then remove the label.");
|
|
196
|
+
if (cycle && wait !== null && wait > cycle / 2)
|
|
197
|
+
out.push(
|
|
198
|
+
"Most cycle time is waiting on you. Run `kanbot status` and clear the queue.",
|
|
199
|
+
);
|
|
200
|
+
if (n && r.loops.ci > n)
|
|
201
|
+
out.push(
|
|
202
|
+
"CI fails often. Map a `verify` capability that runs the same checks.",
|
|
203
|
+
);
|
|
204
|
+
if (n && r.loops.review / n > 2)
|
|
205
|
+
out.push(
|
|
206
|
+
"Reviews often need fixes. Add what they catch to project-memory.",
|
|
207
|
+
);
|
|
208
|
+
if (n && r.loops.conflict / n > 1)
|
|
209
|
+
out.push(
|
|
210
|
+
"Conflicts are frequent. Lower `concurrency` or split cards smaller.",
|
|
211
|
+
);
|
|
212
|
+
return out.slice(0, 2);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const HEALTH_LABEL: Record<Health, string> = {
|
|
216
|
+
ON_TRACK: "On track",
|
|
217
|
+
AT_RISK: "At risk",
|
|
218
|
+
OFF_TRACK: "Off track",
|
|
219
|
+
INACTIVE: "Inactive",
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
/** Markdown for the terminal and for the board's status update. */
|
|
223
|
+
export function renderReport(r: Report): string {
|
|
224
|
+
const day = (iso: string) => iso.slice(0, 10);
|
|
225
|
+
const med = (xs: number[]) => {
|
|
226
|
+
const m = median(xs);
|
|
227
|
+
return m === null ? "—" : duration(m);
|
|
228
|
+
};
|
|
229
|
+
const waits = r.merged.flatMap((m) => (m.waitMs === null ? [] : [m.waitMs]));
|
|
230
|
+
const { loops, now } = r;
|
|
231
|
+
const lines = [
|
|
232
|
+
`**${HEALTH_LABEL[r.health]}** · ${day(r.since)} → ${day(r.until)}`,
|
|
233
|
+
...r.why.map((w) => `> ${w}`),
|
|
234
|
+
"",
|
|
235
|
+
`- Merged: ${r.merged.length} PR(s)`,
|
|
236
|
+
`- Cycle, PR opened → merged: median ${med(r.merged.map((m) => m.cycleMs))}`,
|
|
237
|
+
`- Waiting on you, agent-reviewed → merged: median ${med(waits)}`,
|
|
238
|
+
`- Fix loops: ${loops.review} review, ${loops.ci} CI, ${loops.conflict} conflict, ${loops.feedback} feedback`,
|
|
239
|
+
`- Waiting on you now: ${now.waiting}${now.limit === null ? "" : ` of ${now.limit}`}`,
|
|
240
|
+
`- needs-human: ${r.needsHuman.length} this period`,
|
|
241
|
+
...r.needsHuman.slice(0, 5).map((h) => ` - #${h.number} ${h.reason}`),
|
|
242
|
+
];
|
|
243
|
+
if (r.next.length) lines.push("", "**Next**", ...r.next.map((s) => `- ${s}`));
|
|
244
|
+
return lines.join("\n");
|
|
245
|
+
}
|
package/src/setup.ts
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type Ask,
|
|
3
|
+
askNumber,
|
|
4
|
+
askText,
|
|
5
|
+
choose,
|
|
6
|
+
confirm,
|
|
7
|
+
type Say,
|
|
8
|
+
} from "./ask.ts";
|
|
9
|
+
import type { Config } from "./config.ts";
|
|
10
|
+
import type { Login } from "./providers/login.ts";
|
|
11
|
+
|
|
12
|
+
type Provider = Config["provider"];
|
|
13
|
+
|
|
14
|
+
const loginHint = (l: Login) =>
|
|
15
|
+
l.ok ? "logged in" : l.installed ? "not logged in" : "not installed";
|
|
16
|
+
|
|
17
|
+
/** Picks the provider, defaulting to the one that is logged in. */
|
|
18
|
+
export function askProvider(
|
|
19
|
+
ask: Ask,
|
|
20
|
+
say: Say,
|
|
21
|
+
logins: Record<Provider, Login>,
|
|
22
|
+
): Provider {
|
|
23
|
+
const def = !logins.claude.ok && logins.codex.ok ? 1 : 0;
|
|
24
|
+
return choose<Provider>(
|
|
25
|
+
ask,
|
|
26
|
+
say,
|
|
27
|
+
"Which agent CLI runs the workers?",
|
|
28
|
+
[
|
|
29
|
+
{
|
|
30
|
+
value: "claude",
|
|
31
|
+
label: "Claude Code",
|
|
32
|
+
hint: `Claude subscription, ${loginHint(logins.claude)}`,
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
value: "codex",
|
|
36
|
+
label: "Codex",
|
|
37
|
+
hint: `ChatGPT plan, ${loginHint(logins.codex)}`,
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
def,
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export type Identity = Config["identity"]["mode"];
|
|
45
|
+
|
|
46
|
+
export function askIdentity(ask: Ask, say: Say): Identity {
|
|
47
|
+
return choose<Identity>(ask, say, "Who should kanbot talk as on GitHub?", [
|
|
48
|
+
{
|
|
49
|
+
value: "user",
|
|
50
|
+
label: "You",
|
|
51
|
+
hint: "no setup; comments end with a 🤖 kanbot line",
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
value: "app",
|
|
55
|
+
label: "Its own bot",
|
|
56
|
+
hint: "creates a GitHub App; clear who said what, and you can approve its PRs",
|
|
57
|
+
},
|
|
58
|
+
]);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Merge on approval. Only useful when someone other than you can approve. */
|
|
62
|
+
export function askMerge(
|
|
63
|
+
ask: Ask,
|
|
64
|
+
say: Say,
|
|
65
|
+
identity: Identity,
|
|
66
|
+
): Partial<Config["merge"]> {
|
|
67
|
+
if (identity === "user")
|
|
68
|
+
say(
|
|
69
|
+
" As you, kanbot's PRs are yours, so only another collaborator can approve them.",
|
|
70
|
+
);
|
|
71
|
+
return confirm(
|
|
72
|
+
ask,
|
|
73
|
+
"Merge a kanbot PR when it's approved and checks pass?",
|
|
74
|
+
identity === "app",
|
|
75
|
+
)
|
|
76
|
+
? { onApproval: true }
|
|
77
|
+
: {};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Config keys that differ from defaults. Fields left out keep defaults. */
|
|
81
|
+
export type OptionsPatch = Partial<
|
|
82
|
+
Pick<Config, "concurrency" | "wip" | "notify" | "review" | "codex">
|
|
83
|
+
>;
|
|
84
|
+
|
|
85
|
+
/** Offers the defaults, and asks for each only if the user wants to change them. */
|
|
86
|
+
export function askOptions(
|
|
87
|
+
ask: Ask,
|
|
88
|
+
say: Say,
|
|
89
|
+
provider: Provider,
|
|
90
|
+
): OptionsPatch {
|
|
91
|
+
say("These defaults suit most repos:");
|
|
92
|
+
say(" 3 agents at once");
|
|
93
|
+
say(" No limit on items waiting for you");
|
|
94
|
+
say(" Desktop notifications, no webhook");
|
|
95
|
+
say(" No Copilot review");
|
|
96
|
+
if (provider === "codex") say(" Codex workers have no network access");
|
|
97
|
+
if (!confirm(ask, "Change any of these?", false)) return {};
|
|
98
|
+
|
|
99
|
+
const patch: OptionsPatch = {};
|
|
100
|
+
const concurrency = askNumber(ask, say, "Agents at once?", 3) ?? 3;
|
|
101
|
+
if (concurrency !== 3) patch.concurrency = concurrency;
|
|
102
|
+
|
|
103
|
+
say(
|
|
104
|
+
" New work pauses when this many PRs and plans wait on you. 5 suits one person.",
|
|
105
|
+
);
|
|
106
|
+
const wip = askNumber(ask, say, "Limit on items waiting for you?", null, {
|
|
107
|
+
none: "none",
|
|
108
|
+
});
|
|
109
|
+
if (wip !== null) patch.wip = { awaitingHuman: wip };
|
|
110
|
+
|
|
111
|
+
const webhook = askText(
|
|
112
|
+
ask,
|
|
113
|
+
say,
|
|
114
|
+
"Webhook for phone alerts, e.g. https://ntfy.sh/<topic> (Enter to skip):",
|
|
115
|
+
(s) => (URL.canParse(s) ? null : "Not a URL. Try again, or Enter to skip."),
|
|
116
|
+
);
|
|
117
|
+
if (webhook) patch.notify = { webhook } as Config["notify"];
|
|
118
|
+
|
|
119
|
+
if (
|
|
120
|
+
confirm(
|
|
121
|
+
ask,
|
|
122
|
+
"Have Copilot review PRs too? Needs a paid plan; each review uses credits.",
|
|
123
|
+
false,
|
|
124
|
+
)
|
|
125
|
+
)
|
|
126
|
+
patch.review = { copilot: true } as Config["review"];
|
|
127
|
+
|
|
128
|
+
if (
|
|
129
|
+
provider === "codex" &&
|
|
130
|
+
confirm(
|
|
131
|
+
ask,
|
|
132
|
+
"Let Codex workers reach the network (package installs)?",
|
|
133
|
+
false,
|
|
134
|
+
)
|
|
135
|
+
)
|
|
136
|
+
patch.codex = { networkAccess: true };
|
|
137
|
+
return patch;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export type BoardChoice =
|
|
141
|
+
| { kind: "create" }
|
|
142
|
+
| { kind: "link"; owner: string; number: number }
|
|
143
|
+
| { kind: "skip" };
|
|
144
|
+
|
|
145
|
+
export function askBoard(
|
|
146
|
+
ask: Ask,
|
|
147
|
+
say: Say,
|
|
148
|
+
parse: (url: string) => { owner: string; number: number } | null,
|
|
149
|
+
): BoardChoice {
|
|
150
|
+
const kind = choose<BoardChoice["kind"]>(
|
|
151
|
+
ask,
|
|
152
|
+
say,
|
|
153
|
+
"kanbot runs from a GitHub Project board with six columns.",
|
|
154
|
+
[
|
|
155
|
+
{ value: "create", label: "Create a new board", hint: "recommended" },
|
|
156
|
+
{ value: "link", label: "Use an existing board" },
|
|
157
|
+
{ value: "skip", label: "Skip", hint: "run `kanbot board create` later" },
|
|
158
|
+
],
|
|
159
|
+
);
|
|
160
|
+
if (kind !== "link") return { kind };
|
|
161
|
+
const url = askText(
|
|
162
|
+
ask,
|
|
163
|
+
say,
|
|
164
|
+
"Project URL (github.com/users/<owner>/projects/<n>):",
|
|
165
|
+
(s) =>
|
|
166
|
+
parse(s) ? null : "Not a project URL. Try again, or Enter to skip.",
|
|
167
|
+
);
|
|
168
|
+
const p = url ? parse(url) : null;
|
|
169
|
+
return p ? { kind, ...p } : { kind: "skip" };
|
|
170
|
+
}
|
|
@@ -43,11 +43,11 @@ Agents never get a GitHub token. The daemon does every comment, push and PR. Onl
|
|
|
43
43
|
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.
|
|
44
44
|
|
|
45
45
|
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`.
|
|
46
|
-
2. **
|
|
46
|
+
2. **Init.** Best: have them run `bunx kanbot init` in their own terminal. It asks for provider, identity, options, capabilities and the board. Without a terminal, ask which subscription they have and run `bunx kanbot init --yes --provider claude|codex`; then go on with the steps below.
|
|
47
47
|
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. Draft it from manifests and CI, ask what you can't find, and have them run each command. Link `project-conventions`.
|
|
48
48
|
4. **Capabilities.** kanbot runs the process; the repo's own skills and agents do the work. Ask which skills they have for proving acceptance criteria (`verify`), technical review (`review`) and something a human can try (`preview`), and map them under `capabilities` in `.kanbot/config.yml`. See [Capabilities](#capabilities).
|
|
49
|
-
5. **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.
|
|
50
|
-
6. **Options.**
|
|
49
|
+
5. **Board.** Skip if `init` made one. Otherwise `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.
|
|
50
|
+
6. **Options.** Skip what `init` asked. Otherwise go through [Choose options](#choose-options). For app mode, run `kanbot app create` and have them install the app on the repo.
|
|
51
51
|
7. **Commit and push** the kanbot files. Agents only see what's on the pushed base branch.
|
|
52
52
|
8. **Check.** `kanbot doctor` until it passes, then `kanbot run --once --dry-run`, then `kanbot run`.
|
|
53
53
|
9. **First card.** Have them write an issue, put it in Planning and read the plan. Point to `workflow`.
|
|
@@ -69,16 +69,17 @@ capabilities:
|
|
|
69
69
|
|
|
70
70
|
Ask only about what matters to them. Defaults are fine for the rest. Options live in `.kanbot/config.yml`; restart the daemon after changes.
|
|
71
71
|
|
|
72
|
-
| Option | Ask
|
|
73
|
-
| ---------------------------------- |
|
|
74
|
-
| `identity.mode` (`user` \| `app`) | Should kanbot talk as you, or as its own bot?
|
|
75
|
-
| `merge.onApproval`, `merge.method` | Merge by approving the PR, or press merge yourself?
|
|
76
|
-
| `provider` | Claude or ChatGPT subscription?
|
|
77
|
-
| `concurrency` | How many agents at once?
|
|
78
|
-
| `
|
|
79
|
-
| `
|
|
80
|
-
| `
|
|
81
|
-
| `
|
|
72
|
+
| Option | Ask | Recommend |
|
|
73
|
+
| ---------------------------------- | -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
|
74
|
+
| `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. |
|
|
75
|
+
| `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. |
|
|
76
|
+
| `provider` | Claude or ChatGPT subscription? | Whichever they pay for. |
|
|
77
|
+
| `concurrency` | How many agents at once? | Default 3. Lower on a small machine or near usage limits. |
|
|
78
|
+
| `wip.awaitingHuman` | How many items can wait on you before agents stop starting new work? | 5 for a solo builder. Off (`null`) by default. Optional: same limit on the board's Review column (column menu → Set limit). |
|
|
79
|
+
| `review.copilot` | Want GitHub Copilot as an extra reviewer? (costs credits) | Off unless they have a paid Copilot plan. |
|
|
80
|
+
| `notify.desktop`, `notify.webhook` | Where do you want to hear that a PR is ready? | Desktop. Add an ntfy.sh webhook for phone alerts. |
|
|
81
|
+
| `codex.networkAccess` | Do agents need to install packages? (Codex only) | Off unless installs fail. |
|
|
82
|
+
| `explore:N` label | Want several approaches built for some issues? | Per issue, not config. |
|
|
82
83
|
|
|
83
84
|
### Who does what
|
|
84
85
|
|
|
@@ -105,8 +106,9 @@ Warn: never enable the branch rule "Require approval of the most recent reviewab
|
|
|
105
106
|
|
|
106
107
|
1. `kanbot doctor`: prints a fix for each failed check.
|
|
107
108
|
2. `kanbot status`: board by column, what waits on the user (oldest first), daemon, failures, rate-limit pause.
|
|
108
|
-
3.
|
|
109
|
-
4.
|
|
110
|
-
5.
|
|
111
|
-
6.
|
|
112
|
-
7.
|
|
109
|
+
3. `kanbot report`: merged PRs, cycle time, wait on the user, fix loops and `needs-human` causes for the last 7 days. `--post` adds it to the board's Status updates.
|
|
110
|
+
4. `.kanbot/logs/*.jsonl`: full agent transcripts.
|
|
111
|
+
5. `needs-human` label: the issue has a comment saying why. Fix the cause, then remove the label to retry.
|
|
112
|
+
6. Nothing happens: is the daemon running, are kanbot files pushed to the base branch, is the card in an active column?
|
|
113
|
+
7. Card stays in Ready: the plan has open questions, or the issue is blocked by open issues. kanbot commented why. Answer questions in Planning; blocked cards start when the blockers close.
|
|
114
|
+
8. Agent didn't use a skill: is it mapped under `capabilities`, does its `when` fit, and is it committed?
|