Skip to content

Commit d042ee6

Browse files
committed
feat(cli): tighten agent creation workflow
1 parent cedd18a commit d042ee6

13 files changed

Lines changed: 530 additions & 88 deletions

File tree

apps/video/src/PromoVideo.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,13 +101,13 @@ const claudeUIEvents: ClaudeEvent[] = [
101101
},
102102
{
103103
type: "tool",
104-
command: 'ak create agent --template fullstack-developer --name "Atlas"',
104+
command: "ak apply -f agents/atlas.yaml",
105105
output: "Created agent a_atlas01: Atlas (developer)",
106106
at: P2 + 120,
107107
},
108108
{
109109
type: "tool",
110-
command: 'ak create agent --template fullstack-developer --name "Nova"',
110+
command: "ak apply -f agents/nova.yaml",
111111
output: "Created agent a_nova02: Nova (developer)",
112112
at: P2 + 140,
113113
},

packages/cli/src/apply/kinds.ts

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,37 @@ async function resolveRepoField(client: ApiClient, spec: Record<string, unknown>
2121
}
2222
}
2323

24-
export async function applyResource(client: ApiClient, kind: string, spec: Record<string, unknown>, fmt: OutputFormat): Promise<void> {
24+
function agentBody(spec: Record<string, unknown>, metadata: Record<string, unknown> | undefined): Record<string, unknown> {
25+
if ("kind" in spec) {
26+
console.error("Agent resources create worker agents only. Remove spec.kind.");
27+
process.exit(1);
28+
}
29+
if ("username" in spec || "name" in spec) {
30+
console.error("Agent identity belongs in metadata.name and metadata.annotations, not spec.");
31+
process.exit(1);
32+
}
33+
const username = metadata?.name;
34+
if (typeof username !== "string" || username.length === 0) {
35+
console.error("Agent resources require metadata.name.");
36+
process.exit(1);
37+
}
38+
const annotations = metadata?.annotations as Record<string, unknown> | undefined;
39+
const name = annotations?.["agent-kanban.dev/display-name"];
40+
return {
41+
...spec,
42+
username,
43+
...(typeof name === "string" && name.length > 0 ? { name } : {}),
44+
kind: "worker",
45+
};
46+
}
47+
48+
export async function applyResource(
49+
client: ApiClient,
50+
kind: string,
51+
spec: Record<string, unknown>,
52+
fmt: OutputFormat,
53+
metadata?: Record<string, unknown>,
54+
): Promise<void> {
2555
const id = spec.id as string | undefined;
2656

2757
switch (kind.toLowerCase()) {
@@ -49,12 +79,13 @@ export async function applyResource(client: ApiClient, kind: string, spec: Recor
4979
break;
5080
}
5181
case "agent": {
82+
const body = agentBody(spec, metadata);
5283
if (id) {
53-
const { id: _, ...body } = spec;
54-
const agent = (await client.updateAgent(id, body)) as any;
84+
const { id: _, username: _username, kind: _kind, ...updates } = body;
85+
const agent = (await client.updateAgent(id, updates)) as any;
5586
output(agent, fmt, (a) => `Updated agent ${a.id}: ${a.name}`, { kind: "agent" });
5687
} else {
57-
const agent = (await client.createAgent(spec as any)) as any;
88+
const agent = (await client.createAgent(body as any)) as any;
5889
output(agent, fmt, (a) => `Created agent ${a.id}: ${a.name} (${a.role || "no role"})`, { kind: "agent" });
5990
}
6091
break;

packages/cli/src/apply/parser.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { parseAllDocuments } from "yaml";
33

44
export interface ResourceDoc {
55
kind: string;
6+
metadata?: Record<string, unknown>;
67
spec: Record<string, unknown>;
78
}
89

@@ -40,15 +41,17 @@ export function parseResourceDocs(file: string): ResourceDoc[] {
4041
const docs = Array.isArray(parsed) ? parsed : [parsed];
4142
return docs.map((d: any) => ({
4243
kind: d.kind as string,
44+
metadata: d.metadata as Record<string, unknown> | undefined,
4345
spec: convertSpec(d.spec as Record<string, unknown>),
4446
}));
4547
}
4648

4749
const documents = parseAllDocuments(content);
4850
return documents.map((doc) => {
49-
const data = doc.toJS() as { kind: string; spec: Record<string, unknown> };
51+
const data = doc.toJS() as { kind: string; metadata?: Record<string, unknown>; spec: Record<string, unknown> };
5052
return {
5153
kind: data.kind,
54+
metadata: data.metadata,
5255
spec: convertSpec(data.spec),
5356
};
5457
});

packages/cli/src/commands/apply.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export function registerApplyCommand(program: Command) {
2323
console.error(`Missing or invalid 'spec' field in document (kind: ${doc.kind})`);
2424
process.exit(1);
2525
}
26-
await applyResource(client, doc.kind, doc.spec, fmt);
26+
await applyResource(client, doc.kind, doc.spec, fmt, doc.metadata);
2727
}
2828
});
2929
}

packages/cli/src/commands/create.ts

Lines changed: 15 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { fetchTemplate, isBoardType, parseScheduledAt } from "@agent-kanban/shared";
1+
import { isBoardType, parseScheduledAt } from "@agent-kanban/shared";
22
import type { Command } from "commander";
33
import { createClient } from "../agent/leader.js";
44
import type { ApiClient } from "../client/index.js";
@@ -100,62 +100,31 @@ export function registerCreateCommand(program: Command) {
100100
.description("Create an agent")
101101
.option("--name <name>", "Agent display name")
102102
.option("--username <username>", "Agent username")
103-
.option("--template <slug>", "Agent template slug")
104103
.option("--bio <bio>", "Agent bio")
105104
.option("--soul <soul>", "Agent soul — persistent behavior instructions")
106105
.option("--role <role>", "Agent role")
107106
.option("--runtime <runtime>", "Agent runtime")
108107
.option("--model <model>", "Model to use")
109-
.option("--kind <kind>", "Agent kind: worker, leader")
110108
.option("--handoff-to <ids>", "Comma-separated agent IDs for handoff")
111-
.option("--skills <skills>", "Comma-separated skill slugs")
109+
.option("--skills <skills>", "Comma-separated installable skill refs (<source>@<skill>)")
112110
.option("--subagents <ids>", "Comma-separated worker agent IDs to install as task-local subagents")
113111
.option("-o, --output <format>", "Output format (json, yaml, text)")
114112
.action(async (opts) => {
115113
const client = await createClient();
116-
let body: Record<string, unknown>;
117-
118-
if (opts.template) {
119-
const template = await fetchTemplate(opts.template);
120-
const runtime = opts.runtime || template.runtime;
121-
if (!runtime) {
122-
console.error("Template has no runtime. Pass --runtime explicitly.");
123-
process.exit(1);
124-
}
125-
const username = opts.username || template.username;
126-
if (!username) {
127-
console.error("--username is required (template has no default username)");
128-
process.exit(1);
129-
}
130-
body = {
131-
name: opts.name || template.name || username,
132-
username,
133-
bio: opts.bio || template.bio,
134-
soul: opts.soul || template.soul,
135-
role: opts.role || template.role,
136-
kind: opts.kind,
137-
handoff_to: opts.handoffTo ? opts.handoffTo.split(",").map((s: string) => s.trim()) : template.handoff_to,
138-
runtime,
139-
model: opts.model || template.model,
140-
skills: opts.skills ? opts.skills.split(",").map((s: string) => s.trim()) : template.skills,
141-
subagents: opts.subagents ? opts.subagents.split(",").map((s: string) => s.trim()) : undefined,
142-
};
143-
} else {
144-
if (!opts.username) {
145-
console.error("--username is required");
146-
process.exit(1);
147-
}
148-
const runtime = opts.runtime || detectRuntime();
149-
body = { name: opts.name || opts.username, username: opts.username, runtime };
150-
if (opts.bio) body.bio = opts.bio;
151-
if (opts.soul) body.soul = opts.soul;
152-
if (opts.role) body.role = opts.role;
153-
if (opts.kind) body.kind = opts.kind;
154-
if (opts.handoffTo) body.handoff_to = opts.handoffTo.split(",").map((s: string) => s.trim());
155-
if (opts.model) body.model = opts.model;
156-
if (opts.skills) body.skills = opts.skills.split(",").map((s: string) => s.trim());
157-
if (opts.subagents) body.subagents = opts.subagents.split(",").map((s: string) => s.trim());
114+
if (!opts.username) {
115+
console.error("--username is required");
116+
process.exit(1);
158117
}
118+
const runtime = opts.runtime || detectRuntime();
119+
const body: Record<string, unknown> = { name: opts.name || opts.username, username: opts.username, runtime };
120+
if (opts.bio) body.bio = opts.bio;
121+
if (opts.soul) body.soul = opts.soul;
122+
if (opts.role) body.role = opts.role;
123+
body.kind = "worker";
124+
if (opts.handoffTo) body.handoff_to = opts.handoffTo.split(",").map((s: string) => s.trim());
125+
if (opts.model) body.model = opts.model;
126+
if (opts.skills) body.skills = opts.skills.split(",").map((s: string) => s.trim());
127+
if (opts.subagents) body.subagents = opts.subagents.split(",").map((s: string) => s.trim());
159128

160129
const agent = await client.createAgent(body as any);
161130
const fmt = getOutputFormat(opts.output);

packages/cli/src/commands/update.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -99,9 +99,8 @@ export function registerUpdateCommand(program: Command) {
9999
.option("--role <role>", "Agent role")
100100
.option("--runtime <runtime>", "Agent runtime")
101101
.option("--model <model>", "Agent model")
102-
.option("--kind <kind>", "Agent kind: worker, leader")
103102
.option("--handoff-to <ids>", "Comma-separated agent IDs for handoff")
104-
.option("--skills <skills>", "Comma-separated skill slugs")
103+
.option("--skills <skills>", "Comma-separated installable skill refs (<source>@<skill>)")
105104
.option("--subagents <ids>", "Comma-separated worker agent IDs to install as task-local subagents")
106105
.option("-o, --output <format>", "Output format (json, yaml, text)")
107106
.action(async (id: string, opts) => {
@@ -113,12 +112,11 @@ export function registerUpdateCommand(program: Command) {
113112
if (opts.role) body.role = opts.role;
114113
if (opts.runtime) body.runtime = opts.runtime;
115114
if (opts.model) body.model = opts.model;
116-
if (opts.kind) body.kind = opts.kind;
117115
if (opts.handoffTo) body.handoff_to = opts.handoffTo.split(",").map((s: string) => s.trim());
118116
if (opts.skills) body.skills = opts.skills.split(",").map((s: string) => s.trim());
119117
if (opts.subagents) body.subagents = opts.subagents.split(",").map((s: string) => s.trim());
120118
if (Object.keys(body).length === 0) {
121-
console.error("Nothing to update. Provide at least one option (--name, --bio, --role, --runtime, --model, --kind, --skills, --subagents).");
119+
console.error("Nothing to update. Provide at least one option (--name, --bio, --role, --runtime, --model, --skills, --subagents).");
122120
process.exit(1);
123121
}
124122
const agent = await client.updateAgent(id, body);

skills/ak-plan/SKILL.md

Lines changed: 79 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ Create all tasks? (y/n)
141141

142142
The user must confirm before any `ak create task` calls are made. If the user requests changes, adjust and re-preview.
143143

144-
## Phase 3: Create Board & Tasks
144+
## Phase 3: Create Board, Workers & Tasks
145145

146146
Use the existing board for the project. One project = one board.
147147

@@ -150,6 +150,32 @@ ak get board # find the project board
150150
# Only create a new board if this is a new product with no board yet
151151
```
152152

153+
Before creating tasks, choose or create the workers that will own them. Read `references/runtime-delegation.md`.
154+
155+
Check existing agents. For a typical project you need:
156+
- **fullstack-developer** or backend + frontend split
157+
158+
Only assign work to agents whose `runtime_available` is `true`. If the best role exists only on an unavailable runtime, create a new worker with the same role, soul, skills, and handoff settings on an available runtime.
159+
160+
Create missing agents before task creation:
161+
```yaml
162+
kind: Agent
163+
metadata:
164+
name: <human-username>
165+
annotations:
166+
agent-kanban.dev/display-name: "<Human Name>"
167+
spec:
168+
runtime: <available-runtime>
169+
role: "<role>"
170+
bio: "<durable responsibility>"
171+
skills:
172+
- <source>@<skill>
173+
subagents:
174+
- <worker-agent-id>
175+
```
176+
177+
The leader must generate the Agent YAML from the project context and apply it with `ak apply -f <file>`. Do not use role templates. After creation, run `ak get agent -o json` and confirm the new worker is visible and `runtime_available: true` before assigning tasks.
178+
153179
Create tasks with full specs. For each task:
154180

155181
1. **`--title`** — concise action phrase
@@ -160,14 +186,25 @@ Create tasks with full specs. For each task:
160186
3. **`--repo <id>`** — from `ak repo list`
161187
4. **`--priority`** — urgent/high/medium/low
162188
5. **`--labels`** — include version label (e.g. `v1.4.0`) plus category (backend, frontend, cli, etc.)
163-
6. **`--depends-on`** — task IDs this depends on
189+
6. **`--assign-to <agent-id>`** — worker chosen before task creation
190+
7. **`--depends-on`** — task IDs this depends on
164191

165192
Create tasks in dependency order so earlier task IDs can be referenced:
166193
```bash
167-
T1=$(ak create task --board $BOARD --title "..." --repo $REPO --priority high -o json | jq -r .id)
168-
T2=$(ak create task --board $BOARD --title "..." --repo $REPO --depends-on $T1 -o json | jq -r .id)
194+
T1=$(ak create task --board $BOARD --title "..." --repo $REPO --assign-to $AGENT --priority high -o json | jq -r .id)
195+
T2=$(ak create task --board $BOARD --title "..." --repo $REPO --assign-to $AGENT --depends-on $T1 -o json | jq -r .id)
169196
```
170197

198+
### Task Creation Best Practices
199+
200+
- Create one task for one reviewable outcome. Split unrelated backend, frontend, CLI, and infra work.
201+
- Make each task independently claimable: no hidden chat context, no "continue from above" descriptions.
202+
- Put the exact files, APIs, commands, UI states, and acceptance checks in `--description`.
203+
- Assign every task at creation with `--assign-to`.
204+
- Use `--depends-on` for real blockers or overlapping files. Tasks touching the same files should be sequential.
205+
- Keep parallel tasks independent by file ownership and data model boundary.
206+
- Use stable labels: version plus area, such as `v1.4.0,backend` or `v1.4.0,cli`.
207+
171208
### Task Description Quality
172209

173210
Agents are autonomous — the description is their only input. A good description:
@@ -195,23 +232,7 @@ POST /api/items — create item
195232

196233
Vague descriptions produce vague code. Be specific.
197234

198-
## Phase 4: Assign
199-
200-
Before choosing or creating workers, read `references/runtime-delegation.md`.
201-
202-
Tasks should already be assigned via `--assign-to` on create. If not, use `ak update task <id>` or recreate.
203-
204-
Check existing agents. For a typical project you need:
205-
- **fullstack-developer** or backend + frontend split
206-
207-
Only assign work to agents whose `runtime_available` is `true`. If the best role exists only on an unavailable runtime, create a new worker with the same role, soul, skills, and handoff settings on an available runtime.
208-
209-
Create missing agents if needed:
210-
```bash
211-
ak create agent --template <template> --name "<Name>"
212-
```
213-
214-
## Phase 5: Monitor & Merge
235+
## Phase 4: Monitor & Merge
215236

216237
**Block on `ak wait board` instead of writing polling loops.** It streams tasks one at a time as they reach the filter status. Exit codes: 0 condition met, 2 task cancelled, 124 timeout.
217238

@@ -312,6 +333,43 @@ rm -rf /tmp/ak-review-* playwright-report/ test-results/
312333
### Completion:
313334
When all tasks are done, report the final summary to the user.
314335

336+
## AK Command or Product Issues
337+
338+
If the blocker appears to be an `ak` bug, missing capability, confusing UX, or documentation gap, file an issue in the official repo after collecting a minimal reproduction:
339+
340+
```bash
341+
gh issue create \
342+
--repo saltbo/agent-kanban \
343+
--title "ak: <short problem summary>" \
344+
--body "$(cat <<'EOF'
345+
## Summary
346+
<what failed or what capability is missing>
347+
348+
## Command
349+
ak <command and flags>
350+
351+
## Expected
352+
<what should have happened>
353+
354+
## Actual
355+
<exact error text or observed behavior>
356+
357+
## Context
358+
- ak version:
359+
- OS:
360+
- Runtime:
361+
- Auth type: user | machine | agent
362+
- Board/task/repo IDs, if relevant:
363+
364+
## Reproduction
365+
1. <step>
366+
2. <step>
367+
EOF
368+
)"
369+
```
370+
371+
Never include API keys, session tokens, private keys, `.env` contents, or private repository data. If `gh` is unavailable, open `https://github.com/saltbo/agent-kanban/issues/new` and paste the same content.
372+
315373
## Rules
316374

317375
- **Workflow completion is mandatory** — once this skill is invoked, the full lifecycle (plan → create → assign → monitor → review → merge all) MUST run to completion. If you are interrupted mid-workflow (user asks a side question, chat drifts to another topic, tool fails, etc.), handle the interruption and then **immediately resume the workflow from where you left off**. Never ask "should I continue monitoring?" or "do you want me to keep going?" — the answer is always yes. The only way to exit the workflow early is if the user explicitly says to stop, cancel, or abort.

skills/ak-plan/references/runtime-delegation.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,42 @@ Create workers only when needed for the current plan:
3636

3737
Do not create duplicate workers for hypothetical future work.
3838

39+
Create workers by generating an Agent YAML from the current project context. Do not use role templates.
40+
41+
```yaml
42+
kind: Agent
43+
metadata:
44+
name: alex-chen
45+
annotations:
46+
agent-kanban.dev/display-name: "Alex Chen"
47+
spec:
48+
runtime: codex
49+
role: frontend reviewer
50+
bio: Frontend reviewer focused on React, Tailwind, accessibility, and visual consistency.
51+
skills:
52+
- <source>@<skill>
53+
handoff_to:
54+
- <agent-id>
55+
subagents:
56+
- <worker-agent-id>
57+
```
58+
59+
```bash
60+
ak apply -f agent.yaml
61+
ak get agent -o json
62+
```
63+
64+
Agent creation rules:
65+
66+
- `metadata.name` is the stable username. Use a human-like username such as `alex-chen`, not a role slug or temporary task name.
67+
- `metadata.annotations["agent-kanban.dev/display-name"]` is the human display name, such as `Alex Chen`.
68+
- `spec.role` carries the job responsibility. Do not encode the role into the name.
69+
- `role`, `bio`, and `soul` describe durable responsibility.
70+
- `skills` must be installable skill refs in `<source>@<skill>` format, matching what `npx skills add <source> --skill <skill>` can install.
71+
- `handoff_to` should list real delegation targets.
72+
- `subagents` should list worker agent IDs to install as task-local subagents for this agent.
73+
- Verify `runtime_available: true` before assigning any task to the new worker.
74+
3975
## Runtime Failure Handling
4076

4177
If an assignment fails because the runtime is unavailable, refresh agent data and choose again:

0 commit comments

Comments
 (0)