kanbot 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -8
- package/package.json +1 -1
- package/schema/config.schema.json +38 -8
- package/src/agents/load.ts +11 -0
- package/src/agents/sync.ts +8 -8
- package/src/capabilities.ts +157 -0
- package/src/commands/board.ts +14 -7
- package/src/commands/doctor.ts +80 -6
- package/src/commands/init.ts +15 -3
- package/src/commands/sync.ts +1 -1
- package/src/commands/upgrade.ts +20 -1
- package/src/config.ts +38 -8
- package/src/daemon/derive.ts +41 -0
- package/src/daemon/loop.ts +2 -0
- package/src/jobs/prompts.ts +25 -5
- package/src/jobs/run.ts +61 -22
- package/src/known-hashes.json +42 -57
- package/src/providers/claude.ts +40 -30
- package/src/providers/codex.ts +60 -30
- package/src/providers/mcp.ts +71 -0
- package/src/schemas/results.ts +2 -5
- package/templates/.agents/skills/approach-designer/SKILL.md +1 -1
- package/templates/.agents/skills/kanbot-admin/SKILL.md +21 -4
- package/templates/.agents/skills/kanbot-guide/SKILL.md +26 -7
- package/templates/.agents/skills/kanbot-idea/SKILL.md +1 -1
- package/templates/.agents/skills/plan-template/SKILL.md +14 -6
- package/templates/.agents/skills/pr-description/SKILL.md +7 -3
- package/templates/.agents/skills/project-conventions/SKILL.md +23 -35
- package/templates/.agents/skills/review-findings/SKILL.md +47 -0
- package/templates/.agents/skills/spec-review/SKILL.md +10 -27
- package/templates/.agents/skills/user-stories/SKILL.md +1 -1
- package/templates/.github/pull_request_template.md +6 -0
- package/templates/.kanbot/agents/acceptance-reviewer.md +43 -0
- package/templates/.kanbot/agents/fixer.md +7 -5
- package/templates/.kanbot/agents/implementer.md +49 -0
- package/templates/.kanbot/agents/plan-critic.md +3 -3
- package/templates/.kanbot/agents/planner.md +10 -9
- package/templates/.kanbot/agents/reviewer.md +42 -0
- package/templates/AGENTS.md +2 -3
- package/templates/.agents/skills/accessibility/SKILL.md +0 -76
- package/templates/.agents/skills/code-review/SKILL.md +0 -83
- package/templates/.agents/skills/frontend-design/LICENSE.txt +0 -177
- package/templates/.agents/skills/frontend-design/SKILL.md +0 -76
- package/templates/.agents/skills/tdd/SKILL.md +0 -51
- package/templates/.agents/skills/verify-ui/SKILL.md +0 -104
- package/templates/.kanbot/agents/implementer-backend.md +0 -57
- package/templates/.kanbot/agents/implementer-docs.md +0 -55
- package/templates/.kanbot/agents/implementer-frontend.md +0 -54
- package/templates/.kanbot/agents/implementer-technical.md +0 -53
- package/templates/.kanbot/agents/reviewer-code.md +0 -46
- package/templates/.kanbot/agents/reviewer-spec.md +0 -49
- package/templates/.kanbot/agents/test-writer.md +0 -35
- package/templates/.kanbot/agents/ui-verifier.md +0 -35
package/README.md
CHANGED
|
@@ -52,14 +52,13 @@ For a guided setup, run `bunx kanbot skills install` and ask your agent "help me
|
|
|
52
52
|
|
|
53
53
|
## Labels
|
|
54
54
|
|
|
55
|
-
| Label
|
|
56
|
-
|
|
|
57
|
-
| `technical`
|
|
58
|
-
| `
|
|
59
|
-
| `
|
|
60
|
-
| `
|
|
61
|
-
| `
|
|
62
|
-
| `kanbot-review` | Agent review for your own PR, without a card |
|
|
55
|
+
| Label | Meaning |
|
|
56
|
+
| ---------------- | -------------------------------------------- |
|
|
57
|
+
| `technical` | Plan as a technical goal, not a user story |
|
|
58
|
+
| `explore:2..4` | Build N alternative approaches as N PRs |
|
|
59
|
+
| `agent-reviewed` | Passed agent review; ready for you |
|
|
60
|
+
| `needs-human` | Agents gave up. Remove it to retry. |
|
|
61
|
+
| `kanbot-review` | Agent review for your own PR, without a card |
|
|
63
62
|
|
|
64
63
|
## Beyond the board
|
|
65
64
|
|
|
@@ -73,6 +72,7 @@ For a guided setup, run `bunx kanbot skills install` and ask your agent "help me
|
|
|
73
72
|
- `.kanbot/config.yml`: concurrency, timeouts, notifications, column names.
|
|
74
73
|
- `.kanbot/agents/*.md`: agent definitions. Run `kanbot sync` after editing; don't edit the generated `.claude/` or `.codex/` files.
|
|
75
74
|
- `.agents/skills/`: skills shared by agents. Fill in `project-conventions` first.
|
|
75
|
+
- `capabilities` in config: your own skills and agents for building, testing and reviewing. [More](./docs/13-capabilities.md)
|
|
76
76
|
|
|
77
77
|
## Upgrading
|
|
78
78
|
|
package/package.json
CHANGED
|
@@ -126,14 +126,7 @@
|
|
|
126
126
|
}
|
|
127
127
|
},
|
|
128
128
|
"implementers": {
|
|
129
|
-
"
|
|
130
|
-
"frontend": "implementer-frontend",
|
|
131
|
-
"backend": "implementer-backend",
|
|
132
|
-
"docs": "implementer-docs",
|
|
133
|
-
"infra": "implementer-technical",
|
|
134
|
-
"default": "implementer-backend"
|
|
135
|
-
},
|
|
136
|
-
"description": "Maps a type:<key> label to an agent. `default` is the fallback.",
|
|
129
|
+
"description": "Deprecated: implement always uses `implementer`. While set, maps a type:<key> label to an agent, with `default` as the fallback. Use capabilities instead.",
|
|
137
130
|
"type": "object",
|
|
138
131
|
"propertyNames": {
|
|
139
132
|
"type": "string"
|
|
@@ -142,6 +135,43 @@
|
|
|
142
135
|
"type": "string"
|
|
143
136
|
}
|
|
144
137
|
},
|
|
138
|
+
"capabilities": {
|
|
139
|
+
"description": "Needs mapped to the repo's own skills or agents. Reserved: verify (prove acceptance criteria), review (technical review), preview (something a human can try). Other names are yours.",
|
|
140
|
+
"type": "object",
|
|
141
|
+
"propertyNames": {
|
|
142
|
+
"type": "string",
|
|
143
|
+
"pattern": "^[a-z][a-z0-9-]*$"
|
|
144
|
+
},
|
|
145
|
+
"additionalProperties": {
|
|
146
|
+
"anyOf": [
|
|
147
|
+
{
|
|
148
|
+
"type": "string",
|
|
149
|
+
"minLength": 1,
|
|
150
|
+
"description": "Skill or agent name, short for `{ use: <name> }`."
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
"type": "object",
|
|
154
|
+
"properties": {
|
|
155
|
+
"use": {
|
|
156
|
+
"type": "string",
|
|
157
|
+
"minLength": 1,
|
|
158
|
+
"description": "Skill in .agents/skills or agent in .kanbot/agents that does the work."
|
|
159
|
+
},
|
|
160
|
+
"when": {
|
|
161
|
+
"description": "When it applies, e.g. `Changes to UI users see`.",
|
|
162
|
+
"type": "string"
|
|
163
|
+
},
|
|
164
|
+
"required": {
|
|
165
|
+
"default": false,
|
|
166
|
+
"description": "A miss when it applies blocks acceptance review.",
|
|
167
|
+
"type": "boolean"
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
"required": ["use"]
|
|
171
|
+
}
|
|
172
|
+
]
|
|
173
|
+
}
|
|
174
|
+
},
|
|
145
175
|
"review": {
|
|
146
176
|
"default": {},
|
|
147
177
|
"type": "object",
|
package/src/agents/load.ts
CHANGED
|
@@ -63,6 +63,17 @@ export async function loadAgents(root: string): Promise<AgentDef[]> {
|
|
|
63
63
|
return agents;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
/** `agent: skill` for each skill an agent lists that has no .agents/skills/<skill>/SKILL.md. */
|
|
67
|
+
export function missingSkills(root: string, agents: AgentDef[]): string[] {
|
|
68
|
+
return agents.flatMap((a) =>
|
|
69
|
+
a.skills
|
|
70
|
+
.filter(
|
|
71
|
+
(s) => !existsSync(join(root, ".agents", "skills", s, "SKILL.md")),
|
|
72
|
+
)
|
|
73
|
+
.map((s) => `${a.name}: ${s}`),
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
66
77
|
function validate(agents: AgentDef[]) {
|
|
67
78
|
const names = new Set(agents.map((a) => a.name));
|
|
68
79
|
if (names.size !== agents.length) {
|
package/src/agents/sync.ts
CHANGED
|
@@ -42,7 +42,7 @@ export const CLAUDE_ATTRIBUTION = {
|
|
|
42
42
|
export function claudeTools(agent: AgentDef): string[] {
|
|
43
43
|
const tools = agent.tools.flatMap((t) => CLAUDE_TOOLS[t] ?? []);
|
|
44
44
|
if (agent.skills.length) tools.push("Skill");
|
|
45
|
-
if (agent.subagents.length) tools.push("
|
|
45
|
+
if (agent.subagents.length) tools.push("Agent");
|
|
46
46
|
return [...new Set(tools)];
|
|
47
47
|
}
|
|
48
48
|
|
|
@@ -118,14 +118,14 @@ export type SyncFile = { path: string; content: string };
|
|
|
118
118
|
const COPILOT_INSTRUCTIONS = join(".github", "copilot-instructions.md");
|
|
119
119
|
|
|
120
120
|
/** Points Copilot code review at the same rules the kanbot reviewers use. */
|
|
121
|
-
export const renderCopilotInstructions =
|
|
122
|
-
|
|
121
|
+
export const renderCopilotInstructions = (review?: string) =>
|
|
122
|
+
`<!-- generated by kanbot from review.copilot in .kanbot/config.yml -->
|
|
123
123
|
|
|
124
124
|
# Code review
|
|
125
125
|
|
|
126
126
|
AI agents write most changes in this repo, from a plan in the linked issue. When you review a pull request:
|
|
127
127
|
|
|
128
|
-
- Use the
|
|
128
|
+
- Use the severity rubric and finding format in \`.agents/skills/review-findings/SKILL.md\`.${review ? `\n- Review the way the repo's \`review\` capability does: \`${review}\` in \`.agents/skills/\` or \`.kanbot/agents/\`.` : ""}
|
|
129
129
|
- Check the change against the plan in the linked issue and the PR description.
|
|
130
130
|
- Hold it to \`AGENTS.md\`, \`.agents/skills/project-conventions/SKILL.md\` and the facts in \`.agents/skills/project-memory/\`.
|
|
131
131
|
- Comment on real problems: bugs, missing tests, security, broken conventions. Leave style to the linter.
|
|
@@ -155,12 +155,12 @@ export async function sync(
|
|
|
155
155
|
{ check = false } = {},
|
|
156
156
|
): Promise<string[]> {
|
|
157
157
|
const files = renderAll(agents);
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
if (copilot)
|
|
158
|
+
const config = existsSync(configPath(root)) ? await loadConfig(root) : null;
|
|
159
|
+
const copilot = config?.review.copilot ?? false;
|
|
160
|
+
if (config && copilot)
|
|
161
161
|
files.push({
|
|
162
162
|
path: COPILOT_INSTRUCTIONS,
|
|
163
|
-
content: renderCopilotInstructions(),
|
|
163
|
+
content: renderCopilotInstructions(config.capabilities.review?.use),
|
|
164
164
|
});
|
|
165
165
|
const changed: string[] = [];
|
|
166
166
|
for (const f of files) {
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { readdir } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import type { AgentDef } from "./agents/load.ts";
|
|
5
|
+
import type { Config, RESERVED_CAPABILITIES } from "./config.ts";
|
|
6
|
+
import { TEMPLATES } from "./templates.ts";
|
|
7
|
+
|
|
8
|
+
export type Capabilities = Config["capabilities"];
|
|
9
|
+
|
|
10
|
+
/** Where a capability's `use` lives. `claude`/`codex` agents work on one provider only. */
|
|
11
|
+
export type CapabilitySource = "skill" | "agent" | "claude" | "codex";
|
|
12
|
+
|
|
13
|
+
export function findCapability(
|
|
14
|
+
root: string,
|
|
15
|
+
agents: AgentDef[],
|
|
16
|
+
use: string,
|
|
17
|
+
): CapabilitySource | null {
|
|
18
|
+
if (existsSync(join(root, ".agents", "skills", use, "SKILL.md")))
|
|
19
|
+
return "skill";
|
|
20
|
+
if (agents.some((a) => a.name === use)) return "agent";
|
|
21
|
+
if (existsSync(join(root, ".claude", "agents", `${use}.md`))) return "claude";
|
|
22
|
+
if (existsSync(join(root, ".codex", "agents", `${use}.toml`))) return "codex";
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Prompt section listing the repo's capabilities. Empty when none are mapped. */
|
|
27
|
+
export function capabilitiesSection(caps: Capabilities, extra = ""): string {
|
|
28
|
+
const rows = Object.entries(caps);
|
|
29
|
+
if (!rows.length) return "";
|
|
30
|
+
const cell = (s: string) => s.replace(/\|/g, "\\|").replace(/\n/g, " ");
|
|
31
|
+
return [
|
|
32
|
+
"## Capabilities",
|
|
33
|
+
`The repo maps these needs to its own skills or agents. Use one when its "when" fits the task. Required ones must be used when they apply. For needs not listed, decide yourself.${extra ? ` ${extra}` : ""}`,
|
|
34
|
+
"",
|
|
35
|
+
"| Name | When | Use | Required |",
|
|
36
|
+
"| --- | --- | --- | --- |",
|
|
37
|
+
...rows.map(
|
|
38
|
+
([name, c]) =>
|
|
39
|
+
`| ${name} | ${cell(c.when ?? "When it fits the task")} | \`${c.use}\` | ${c.required ? "yes" : "no"} |`,
|
|
40
|
+
),
|
|
41
|
+
].join("\n");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Craft skills earlier kanbot versions installed, and the capability each maps to. */
|
|
45
|
+
const LEGACY: [name: string, use: string, when?: string][] = [
|
|
46
|
+
["verify", "verify-ui", "Changes to UI users see"],
|
|
47
|
+
["review", "code-review"],
|
|
48
|
+
["tdd", "tdd", "Writing or changing behaviour"],
|
|
49
|
+
["ui-design", "frontend-design", "Building or restyling UI"],
|
|
50
|
+
["accessibility", "accessibility", "Changes to UI users see"],
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* A capabilities block for craft skills an earlier kanbot installed, so
|
|
55
|
+
* upgrades keep using them. Null when config already maps capabilities or
|
|
56
|
+
* none of them are present.
|
|
57
|
+
*/
|
|
58
|
+
export function legacyCapabilities(
|
|
59
|
+
root: string,
|
|
60
|
+
current: Capabilities,
|
|
61
|
+
): Capabilities | null {
|
|
62
|
+
if (Object.keys(current).length) return null;
|
|
63
|
+
const found = LEGACY.filter(([, use]) =>
|
|
64
|
+
existsSync(join(root, ".agents", "skills", use, "SKILL.md")),
|
|
65
|
+
);
|
|
66
|
+
if (!found.length) return null;
|
|
67
|
+
return Object.fromEntries(
|
|
68
|
+
found.map(([name, use, when]) => [
|
|
69
|
+
name,
|
|
70
|
+
{ use, ...(when ? { when } : {}), required: false },
|
|
71
|
+
]),
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export const RESERVED: Record<(typeof RESERVED_CAPABILITIES)[number], string> =
|
|
76
|
+
{
|
|
77
|
+
verify: "prove acceptance criteria",
|
|
78
|
+
review: "technical review",
|
|
79
|
+
preview: "something a human can try",
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/** Skills and agents in the repo that kanbot didn't ship. */
|
|
83
|
+
export async function ownWorkers(root: string): Promise<string[]> {
|
|
84
|
+
const list = async (dir: string, ext?: string) =>
|
|
85
|
+
existsSync(dir)
|
|
86
|
+
? (await readdir(dir, { withFileTypes: true }))
|
|
87
|
+
.filter((e) => (ext ? e.name.endsWith(ext) : e.isDirectory()))
|
|
88
|
+
.map((e) => (ext ? e.name.slice(0, -ext.length) : e.name))
|
|
89
|
+
: [];
|
|
90
|
+
const shipped = new Set([
|
|
91
|
+
...(await list(join(TEMPLATES, ".agents", "skills"))),
|
|
92
|
+
...(await list(join(TEMPLATES, ".kanbot", "agents"), ".md")),
|
|
93
|
+
]);
|
|
94
|
+
const all = [
|
|
95
|
+
...(await list(join(root, ".agents", "skills"))),
|
|
96
|
+
...(await list(join(root, ".kanbot", "agents"), ".md")),
|
|
97
|
+
];
|
|
98
|
+
return [...new Set(all)].filter((n) => !shipped.has(n)).sort();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Asks which of the repo's own skills or agents fill each reserved need.
|
|
103
|
+
* `ask` returns the answer, or null/empty to skip.
|
|
104
|
+
*/
|
|
105
|
+
export function askCapabilities(
|
|
106
|
+
workers: string[],
|
|
107
|
+
ask: (question: string) => string | null,
|
|
108
|
+
say: (line: string) => void = console.log,
|
|
109
|
+
): Capabilities {
|
|
110
|
+
const caps: Capabilities = {};
|
|
111
|
+
if (!workers.length) return caps;
|
|
112
|
+
say(
|
|
113
|
+
`Your skills and agents:\n${workers.map((w, i) => ` ${i + 1}. ${w}`).join("\n")}`,
|
|
114
|
+
);
|
|
115
|
+
const yes = ask("Map them to kanbot capabilities now? [y/N]");
|
|
116
|
+
if (!/^y/i.test(yes?.trim() ?? "")) return caps;
|
|
117
|
+
for (const [name, what] of Object.entries(RESERVED)) {
|
|
118
|
+
const answer = ask(
|
|
119
|
+
`${name} (${what}): number or name, Enter to skip`,
|
|
120
|
+
)?.trim();
|
|
121
|
+
if (!answer) continue;
|
|
122
|
+
const use = workers[Number(answer) - 1] ?? answer;
|
|
123
|
+
if (workers.includes(use)) caps[name] = { use, required: false };
|
|
124
|
+
else say(` "${answer}" not found, skipped ${name}.`);
|
|
125
|
+
}
|
|
126
|
+
return caps;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** One line: what is mapped, and which reserved needs aren't. */
|
|
130
|
+
export function capabilitySummary(caps: Capabilities): string {
|
|
131
|
+
const mapped = Object.entries(caps).map(([n, c]) => `${n} → ${c.use}`);
|
|
132
|
+
const missing = Object.keys(RESERVED).filter((n) => !caps[n]);
|
|
133
|
+
return [
|
|
134
|
+
mapped.join(", ") || "none mapped",
|
|
135
|
+
missing.length
|
|
136
|
+
? `could add ${missing.join(", ")} (kanbot-guide can help)`
|
|
137
|
+
: "",
|
|
138
|
+
]
|
|
139
|
+
.filter(Boolean)
|
|
140
|
+
.join("; ");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Capabilities as written to config: a bare name when that says it all. */
|
|
144
|
+
export function compact(caps: Capabilities) {
|
|
145
|
+
return Object.fromEntries(
|
|
146
|
+
Object.entries(caps).map(([name, c]) => [
|
|
147
|
+
name,
|
|
148
|
+
!c.when && !c.required
|
|
149
|
+
? c.use
|
|
150
|
+
: {
|
|
151
|
+
use: c.use,
|
|
152
|
+
...(c.when ? { when: c.when } : {}),
|
|
153
|
+
...(c.required ? { required: true } : {}),
|
|
154
|
+
},
|
|
155
|
+
]),
|
|
156
|
+
);
|
|
157
|
+
}
|
package/src/commands/board.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { defineCommand } from "citty";
|
|
2
2
|
import { LABELS, loadConfig, saveConfig } from "../config.ts";
|
|
3
|
+
import { UserError } from "../errors.ts";
|
|
3
4
|
import { ensureCopilotRuleset, ensureLabel } from "../github/api.ts";
|
|
4
5
|
import { Gh } from "../github/gh.ts";
|
|
5
6
|
import { createBoard, loadBoard, missingColumns } from "../github/project.ts";
|
|
@@ -12,10 +13,6 @@ export const REPO_LABELS: [name: string, color: string, description: string][] =
|
|
|
12
13
|
"5319e7",
|
|
13
14
|
"Technical work; planned as a technical goal instead of a user story",
|
|
14
15
|
],
|
|
15
|
-
["type:frontend", "1d76db", "User-facing UI work"],
|
|
16
|
-
["type:backend", "0e8a16", "Server, data or API work"],
|
|
17
|
-
["type:docs", "c5def5", "Documentation"],
|
|
18
|
-
["type:infra", "bfd4f2", "Tooling, CI, infrastructure"],
|
|
19
16
|
[
|
|
20
17
|
"explore:2",
|
|
21
18
|
"fbca04",
|
|
@@ -62,7 +59,7 @@ const create = defineCommand({
|
|
|
62
59
|
meta: {
|
|
63
60
|
name: "create",
|
|
64
61
|
description:
|
|
65
|
-
"Create a GitHub Project board for this repo with the kanbot columns",
|
|
62
|
+
"Create a GitHub Project board for this repo with the kanbot columns and labels",
|
|
66
63
|
},
|
|
67
64
|
args: {
|
|
68
65
|
title: {
|
|
@@ -109,7 +106,11 @@ ${board.url}`);
|
|
|
109
106
|
});
|
|
110
107
|
|
|
111
108
|
const link = defineCommand({
|
|
112
|
-
meta: {
|
|
109
|
+
meta: {
|
|
110
|
+
name: "link",
|
|
111
|
+
description:
|
|
112
|
+
"Use an existing project board, e.g. github.com/users/<owner>/projects/<number>",
|
|
113
|
+
},
|
|
113
114
|
args: {
|
|
114
115
|
owner: {
|
|
115
116
|
type: "string",
|
|
@@ -120,8 +121,14 @@ const link = defineCommand({
|
|
|
120
121
|
},
|
|
121
122
|
async run({ args }) {
|
|
122
123
|
const root = process.cwd();
|
|
124
|
+
const number = Number(args.number);
|
|
125
|
+
if (!Number.isInteger(number))
|
|
126
|
+
throw new UserError("--number must be a number.", {
|
|
127
|
+
why: "It's the number at the end of the project URL.",
|
|
128
|
+
docs: "getting-started#create-the-board",
|
|
129
|
+
});
|
|
123
130
|
await saveConfig(root, {
|
|
124
|
-
project: { owner: args.owner, number
|
|
131
|
+
project: { owner: args.owner, number },
|
|
125
132
|
});
|
|
126
133
|
const config = await loadConfig(root);
|
|
127
134
|
const board = await loadBoard(new Gh(config.repo), config);
|
package/src/commands/doctor.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { defineCommand } from "citty";
|
|
4
|
-
import { loadAgents } from "../agents/load.ts";
|
|
4
|
+
import { loadAgents, missingSkills } from "../agents/load.ts";
|
|
5
5
|
import { sync } from "../agents/sync.ts";
|
|
6
|
+
import { capabilitySummary, findCapability } from "../capabilities.ts";
|
|
6
7
|
import { loadConfig, writeConfigSchema } from "../config.ts";
|
|
7
8
|
import { git } from "../daemon/git.ts";
|
|
8
9
|
import { describeError } from "../errors.ts";
|
|
@@ -14,10 +15,22 @@ import {
|
|
|
14
15
|
loadBoard,
|
|
15
16
|
missingColumns,
|
|
16
17
|
} from "../github/project.ts";
|
|
18
|
+
import {
|
|
19
|
+
codexSupports,
|
|
20
|
+
type McpServer,
|
|
21
|
+
readMcpServers,
|
|
22
|
+
} from "../providers/mcp.ts";
|
|
17
23
|
import { NEW_SUFFIX, pendingMerges, planUpgrade } from "../templates.ts";
|
|
18
24
|
import { REPO_LABELS } from "./board.ts";
|
|
19
25
|
|
|
20
|
-
type Check = {
|
|
26
|
+
type Check = {
|
|
27
|
+
name: string;
|
|
28
|
+
ok: boolean;
|
|
29
|
+
/** Shown with a warning mark; never fails doctor. */
|
|
30
|
+
warn?: boolean;
|
|
31
|
+
detail?: string;
|
|
32
|
+
fix?: string;
|
|
33
|
+
};
|
|
21
34
|
|
|
22
35
|
async function cmd(args: string[]) {
|
|
23
36
|
const p = Bun.spawn(args, { stdout: "pipe", stderr: "pipe" });
|
|
@@ -50,7 +63,7 @@ export default defineCommand({
|
|
|
50
63
|
const add = (c: Check) => {
|
|
51
64
|
checks.push(c);
|
|
52
65
|
console.log(
|
|
53
|
-
`${c.ok ? "✔" : "✖"} ${c.name}${c.detail ? ` — ${c.detail}` : ""}${!c.ok && c.fix ? `\n fix: ${c.fix}` : ""}`,
|
|
66
|
+
`${c.warn ? "!" : c.ok ? "✔" : "✖"} ${c.name}${c.detail ? ` — ${c.detail}` : ""}${!c.ok && c.fix ? `\n fix: ${c.fix}` : ""}`,
|
|
54
67
|
);
|
|
55
68
|
};
|
|
56
69
|
|
|
@@ -94,6 +107,12 @@ export default defineCommand({
|
|
|
94
107
|
detail: s.out.split("\n")[0],
|
|
95
108
|
fix: "codex login",
|
|
96
109
|
});
|
|
110
|
+
add({
|
|
111
|
+
name: "codex workers ignore ~/.codex/config.toml",
|
|
112
|
+
ok: true,
|
|
113
|
+
detail:
|
|
114
|
+
"models come from agent frontmatter, MCP servers from .mcp.json",
|
|
115
|
+
});
|
|
97
116
|
}
|
|
98
117
|
|
|
99
118
|
try {
|
|
@@ -105,10 +124,11 @@ export default defineCommand({
|
|
|
105
124
|
});
|
|
106
125
|
const needed = [
|
|
107
126
|
"planner",
|
|
108
|
-
"reviewer
|
|
109
|
-
"reviewer
|
|
127
|
+
"reviewer",
|
|
128
|
+
"acceptance-reviewer",
|
|
110
129
|
"fixer",
|
|
111
|
-
|
|
130
|
+
"implementer",
|
|
131
|
+
...Object.values(config.implementers ?? {}),
|
|
112
132
|
];
|
|
113
133
|
const missing = [...new Set(needed)].filter(
|
|
114
134
|
(n) => !agents.some((a) => a.name === n),
|
|
@@ -117,7 +137,43 @@ export default defineCommand({
|
|
|
117
137
|
name: "agents referenced by config exist",
|
|
118
138
|
ok: !missing.length,
|
|
119
139
|
detail: missing.join(", ") || undefined,
|
|
140
|
+
fix: "kanbot upgrade",
|
|
120
141
|
});
|
|
142
|
+
if (config.implementers)
|
|
143
|
+
add({
|
|
144
|
+
name: "implementers is deprecated",
|
|
145
|
+
ok: true,
|
|
146
|
+
warn: true,
|
|
147
|
+
detail:
|
|
148
|
+
"implement uses `implementer`; map craft with capabilities instead",
|
|
149
|
+
fix: "remove implementers from .kanbot/config.yml",
|
|
150
|
+
});
|
|
151
|
+
const noSkill = missingSkills(root, agents);
|
|
152
|
+
add({
|
|
153
|
+
name: "skills referenced by agents exist",
|
|
154
|
+
ok: !noSkill.length,
|
|
155
|
+
detail: noSkill.join(", ") || undefined,
|
|
156
|
+
fix: "add them to .agents/skills or remove them from the agent's `skills`",
|
|
157
|
+
});
|
|
158
|
+
add({
|
|
159
|
+
name: "capabilities",
|
|
160
|
+
ok: true,
|
|
161
|
+
detail: capabilitySummary(config.capabilities),
|
|
162
|
+
});
|
|
163
|
+
// Only problems get their own line.
|
|
164
|
+
for (const [name, c] of Object.entries(config.capabilities)) {
|
|
165
|
+
const found = findCapability(root, agents, c.use);
|
|
166
|
+
if (found === "skill" || found === "agent") continue;
|
|
167
|
+
add({
|
|
168
|
+
name: `capability ${name} → ${c.use}`,
|
|
169
|
+
ok: found !== null,
|
|
170
|
+
warn: found !== null,
|
|
171
|
+
detail: found
|
|
172
|
+
? `${found} agent, works on ${found} only`
|
|
173
|
+
: "not found",
|
|
174
|
+
fix: "add the skill to .agents/skills or the agent to .kanbot/agents",
|
|
175
|
+
});
|
|
176
|
+
}
|
|
121
177
|
const stale = await sync(root, agents, { check: true });
|
|
122
178
|
add({
|
|
123
179
|
name: "generated agent files up to date",
|
|
@@ -134,6 +190,24 @@ export default defineCommand({
|
|
|
134
190
|
add({ name: "agents", ok: false, ...explain(e) });
|
|
135
191
|
}
|
|
136
192
|
|
|
193
|
+
const servers = await readMcpServers(root);
|
|
194
|
+
const names = Object.keys(servers);
|
|
195
|
+
const skipped =
|
|
196
|
+
config.provider === "codex"
|
|
197
|
+
? names.filter((n) => !codexSupports(servers[n] as McpServer))
|
|
198
|
+
: [];
|
|
199
|
+
add({
|
|
200
|
+
name: "MCP servers for workers (.mcp.json)",
|
|
201
|
+
ok: true,
|
|
202
|
+
warn: skipped.length > 0,
|
|
203
|
+
detail: [
|
|
204
|
+
names.join(", ") || "none",
|
|
205
|
+
skipped.length ? `codex can't run: ${skipped.join(", ")}` : "",
|
|
206
|
+
]
|
|
207
|
+
.filter(Boolean)
|
|
208
|
+
.join("; "),
|
|
209
|
+
});
|
|
210
|
+
|
|
137
211
|
const plan = await planUpgrade(root);
|
|
138
212
|
const behind = plan.changes.filter((c) =>
|
|
139
213
|
["add", "update", "conflict"].includes(c.action),
|
package/src/commands/init.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { join } from "node:path";
|
|
|
3
3
|
import { defineCommand } from "citty";
|
|
4
4
|
import { loadAgents } from "../agents/load.ts";
|
|
5
5
|
import { sync } from "../agents/sync.ts";
|
|
6
|
+
import { askCapabilities, compact, ownWorkers } from "../capabilities.ts";
|
|
6
7
|
import { configPath, saveConfig, writeConfigSchema } from "../config.ts";
|
|
7
8
|
import { UserError } from "../errors.ts";
|
|
8
9
|
import { run } from "../github/gh.ts";
|
|
@@ -79,6 +80,16 @@ export default defineCommand({
|
|
|
79
80
|
log.info(
|
|
80
81
|
`+ .kanbot/config.yml (${repo.nameWithOwner}, base ${repo.defaultBranchRef.name})`,
|
|
81
82
|
);
|
|
83
|
+
// Only ask when someone can answer.
|
|
84
|
+
if (process.stdin.isTTY) {
|
|
85
|
+
const caps = askCapabilities(await ownWorkers(root), prompt);
|
|
86
|
+
if (Object.keys(caps).length) {
|
|
87
|
+
await saveConfig(root, { capabilities: compact(caps) } as never);
|
|
88
|
+
log.info(
|
|
89
|
+
`~ .kanbot/config.yml (capabilities: ${Object.keys(caps).join(", ")})`,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
82
93
|
}
|
|
83
94
|
|
|
84
95
|
const codeowners = join(root, ".github", "CODEOWNERS");
|
|
@@ -101,10 +112,11 @@ export default defineCommand({
|
|
|
101
112
|
Next steps:
|
|
102
113
|
1. Review AGENTS.md and fill in .agents/skills/project-conventions/SKILL.md
|
|
103
114
|
2. Adjust .github/workflows/ci.yml to your stack (agents fix failing CI)
|
|
104
|
-
3.
|
|
105
|
-
4. kanbot
|
|
115
|
+
3. Map your own skills under capabilities in .kanbot/config.yml (kanbot-guide can help)
|
|
116
|
+
4. kanbot board create # board and labels; enable the workflows it prints
|
|
106
117
|
5. Commit and push these files to the default branch (agents work from it)
|
|
107
|
-
6. kanbot
|
|
118
|
+
6. kanbot doctor # checks auth, board, generated files
|
|
119
|
+
7. kanbot run # start the loop (or kanbot run --once --dry-run)`);
|
|
108
120
|
},
|
|
109
121
|
});
|
|
110
122
|
|
package/src/commands/sync.ts
CHANGED
|
@@ -8,7 +8,7 @@ export default defineCommand({
|
|
|
8
8
|
meta: {
|
|
9
9
|
name: "sync",
|
|
10
10
|
description:
|
|
11
|
-
"Generate .claude/agents, .codex/agents, permissions and the config schema",
|
|
11
|
+
"Generate .claude/agents, .codex/agents, permissions and the config schema from .kanbot/agents",
|
|
12
12
|
},
|
|
13
13
|
args: {
|
|
14
14
|
check: {
|
package/src/commands/upgrade.ts
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
1
2
|
import { defineCommand } from "citty";
|
|
2
3
|
import pkg from "../../package.json" with { type: "json" };
|
|
3
4
|
import { loadAgents } from "../agents/load.ts";
|
|
4
5
|
import { sync } from "../agents/sync.ts";
|
|
5
|
-
import {
|
|
6
|
+
import { compact, legacyCapabilities } from "../capabilities.ts";
|
|
7
|
+
import {
|
|
8
|
+
configPath,
|
|
9
|
+
loadConfig,
|
|
10
|
+
saveConfig,
|
|
11
|
+
writeConfigSchema,
|
|
12
|
+
} from "../config.ts";
|
|
6
13
|
import { log } from "../log.ts";
|
|
7
14
|
import {
|
|
8
15
|
applyUpgrade,
|
|
@@ -48,9 +55,21 @@ export default defineCommand({
|
|
|
48
55
|
);
|
|
49
56
|
}
|
|
50
57
|
if (!plan.changes.length) log.info("Templates are up to date.");
|
|
58
|
+
const caps = existsSync(configPath(root))
|
|
59
|
+
? legacyCapabilities(root, (await loadConfig(root)).capabilities)
|
|
60
|
+
: null;
|
|
61
|
+
if (caps)
|
|
62
|
+
console.log(
|
|
63
|
+
`\nMapped skills you have to capabilities in .kanbot/config.yml:\n${Object.entries(
|
|
64
|
+
caps,
|
|
65
|
+
)
|
|
66
|
+
.map(([n, c]) => ` ${n}: ${c.use}`)
|
|
67
|
+
.join("\n")}`,
|
|
68
|
+
);
|
|
51
69
|
if (args["dry-run"]) return;
|
|
52
70
|
|
|
53
71
|
await applyUpgrade(root, plan);
|
|
72
|
+
if (caps) await saveConfig(root, { capabilities: compact(caps) } as never);
|
|
54
73
|
const generated = await sync(root, await loadAgents(root));
|
|
55
74
|
if (await writeConfigSchema(root))
|
|
56
75
|
generated.push(".kanbot/config.schema.json");
|
package/src/config.ts
CHANGED
|
@@ -17,6 +17,36 @@ export type ColumnKey = (typeof COLUMN_KEYS)[number];
|
|
|
17
17
|
export const JOB_KINDS = ["fix", "review", "plan", "implement"] as const;
|
|
18
18
|
export type JobKind = (typeof JOB_KINDS)[number];
|
|
19
19
|
|
|
20
|
+
export const RESERVED_CAPABILITIES = ["verify", "review", "preview"] as const;
|
|
21
|
+
|
|
22
|
+
const CapabilityObject = z.object({
|
|
23
|
+
use: z
|
|
24
|
+
.string()
|
|
25
|
+
.min(1)
|
|
26
|
+
.describe(
|
|
27
|
+
"Skill in .agents/skills or agent in .kanbot/agents that does the work.",
|
|
28
|
+
),
|
|
29
|
+
when: z
|
|
30
|
+
.string()
|
|
31
|
+
.optional()
|
|
32
|
+
.describe("When it applies, e.g. `Changes to UI users see`."),
|
|
33
|
+
required: z
|
|
34
|
+
.boolean()
|
|
35
|
+
.default(false)
|
|
36
|
+
.describe("A miss when it applies blocks acceptance review."),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
/** `verify: verify-ui` is short for `verify: { use: verify-ui }`. */
|
|
40
|
+
export const Capability = z.union([
|
|
41
|
+
z
|
|
42
|
+
.string()
|
|
43
|
+
.min(1)
|
|
44
|
+
.describe("Skill or agent name, short for `{ use: <name> }`.")
|
|
45
|
+
.transform((use) => CapabilityObject.parse({ use })),
|
|
46
|
+
CapabilityObject,
|
|
47
|
+
]);
|
|
48
|
+
export type Capability = z.output<typeof Capability>;
|
|
49
|
+
|
|
20
50
|
export const ConfigSchema = z.object({
|
|
21
51
|
provider: z
|
|
22
52
|
.enum(["claude", "codex"])
|
|
@@ -78,15 +108,15 @@ export const ConfigSchema = z.object({
|
|
|
78
108
|
.describe("Per-job timeout in minutes."),
|
|
79
109
|
implementers: z
|
|
80
110
|
.record(z.string(), z.string())
|
|
81
|
-
.
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
})
|
|
111
|
+
.optional()
|
|
112
|
+
.describe(
|
|
113
|
+
"Deprecated: implement always uses `implementer`. While set, maps a type:<key> label to an agent, with `default` as the fallback. Use capabilities instead.",
|
|
114
|
+
),
|
|
115
|
+
capabilities: z
|
|
116
|
+
.record(z.string().regex(/^[a-z][a-z0-9-]*$/), Capability)
|
|
117
|
+
.default({})
|
|
88
118
|
.describe(
|
|
89
|
-
"
|
|
119
|
+
"Needs mapped to the repo's own skills or agents. Reserved: verify (prove acceptance criteria), review (technical review), preview (something a human can try). Other names are yours.",
|
|
90
120
|
),
|
|
91
121
|
review: z
|
|
92
122
|
.object({
|