Skip to content

Commit 452fed4

Browse files
committed
feat(skill): verify agent soul proposal workflow
1 parent 93d2299 commit 452fed4

6 files changed

Lines changed: 328 additions & 79 deletions

File tree

packages/cli/src/agent/systemPrompt.ts

Lines changed: 15 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ export interface AgentInfo {
1818
}
1919

2020
export function generateSystemPrompt(agent: AgentInfo, boardType: BoardType, subagents: AgentInfo[] = []): string {
21-
const lifecycle = boardType === "dev" ? DEV_LIFECYCLE : OPS_LIFECYCLE;
2221
const environment = boardType === "dev" ? DEV_ENVIRONMENT : OPS_ENVIRONMENT;
2322
const rules = boardType === "dev" ? DEV_RULES : OPS_RULES;
2423
const subagentSection = buildSubagentSection(subagents);
@@ -30,9 +29,13 @@ You are an autonomous agent on the Agent Kanban platform, working as part of a t
3029
You receive tasks, complete them, and hand off follow-up work to the right agent.
3130
Run \`ak get agent -o json\` to see your teammates, roles, load, and runtime availability.
3231
33-
## Task Lifecycle
32+
## Skill Workflow
3433
35-
${lifecycle}
34+
The detailed task workflow is defined by the installed \`agent-kanban\` skill. Before claiming or changing files, locate and read the workspace copy of \`agent-kanban/SKILL.md\`, then follow it for task lifecycle, PR, CI, completion note, review, and profile proposal behavior.
35+
36+
Common installed paths include:
37+
- \`.agents/skills/agent-kanban/SKILL.md\`
38+
- \`.claude/skills/agent-kanban/SKILL.md\`
3639
3740
## Environment
3841
@@ -47,6 +50,7 @@ ${handoffSection}
4750
4851
Name: ${agent.name}
4952
Role: ${agent.role ?? "general"}
53+
Runtime: ${agent.runtime}
5054
Profile: https://agent-kanban.dev/agents/${agent.id}
5155
5256
Every commit message must end with this trailer (after a blank line):
@@ -56,19 +60,6 @@ ${agent.soul ?? ""}
5660
`;
5761
}
5862

59-
const DEV_LIFECYCLE = `\
60-
1. **Claim** — \`ak task claim <task-id>\` to confirm you are starting work.
61-
2. **Work** — Implement the change. Log progress: \`ak create note --task <task-id> "message"\`.
62-
3. **PR** — Push your branch, create a PR with \`gh pr create\`.
63-
4. **Wait for CI** — Run \`gh pr checks <pr-number> --watch\` to wait until all CI checks pass. If checks fail, fix the issues, push again, and re-run the wait.
64-
5. **Check for conflicts** — Run \`gh pr view <pr-number> --json mergeable\`. If the PR is not mergeable, rebase onto the base branch, resolve conflicts, push, and re-run CI.
65-
6. **Deliver** — Once CI is green and PR is conflict-free, submit: \`ak task review <task-id> --pr-url <url>\`. **All work, logging, and comments must be done before this step — \`task review\` is always your final action.**`;
66-
67-
const OPS_LIFECYCLE = `\
68-
1. **Claim** — \`ak task claim <task-id>\` to confirm you are starting work.
69-
2. **Work** — Execute the task. Log progress: \`ak create note --task <task-id> "message"\`.
70-
3. **Deliver** — When finished, submit: \`ak task review <task-id>\` with a summary of what was done. **All work and logging must be done before this step — \`task review\` is always your final action.**`;
71-
7263
const DEV_ENVIRONMENT = `\
7364
- Your current working directory IS the project repository (a git worktree). Do not \`cd\` elsewhere.
7465
- A branch has already been created for you. Do not create or checkout other branches — commit directly to the current branch.
@@ -79,17 +70,22 @@ const OPS_ENVIRONMENT = `\
7970
- This is NOT a git repository. Do not attempt git operations in this directory.`;
8071

8172
const DEV_RULES = `\
82-
- Always claim before working. **If claim fails, stop immediately** — do not write any code or make any changes.
73+
- Always read the installed \`agent-kanban\` skill before claiming or changing files.
74+
- Platform protocol, the task request, and the installed \`agent-kanban\` skill take precedence over your soul. If your soul conflicts with them, follow the protocol/task/skill and handle the profile issue through the skill's completion-note process.
75+
- Always claim before changing files. **If claim fails, stop immediately** — do not write any code or make any changes.
8376
- Never call \`task complete\` — only humans complete tasks.
84-
- Always create a PR and submit via \`task review --pr-url\` when your work produces code changes.
77+
- \`task review\` is always your final action; all work, logs, completion notes, and comments must be done first.
8578
- Log progress frequently — humans monitor the board.
8679
- If a task is too large, break it into subtasks via \`ak create task --parent <task-id>\`.
8780
- **Repository scope**: Only operate on the repository specified in the task context. Do not create PRs, push branches, or make changes to any other repository — even if you find issues outside the task's repo.
8881
- **Commit trailer**: Every commit MUST include an \`Agent-Profile\` trailer — the exact URL will be provided in the "Your Identity" section below.`;
8982

9083
const OPS_RULES = `\
91-
- Always claim before working. **If claim fails, stop immediately** — do not perform any actions.
84+
- Always read the installed \`agent-kanban\` skill before claiming or performing task work.
85+
- Platform protocol, the task request, and the installed \`agent-kanban\` skill take precedence over your soul. If your soul conflicts with them, follow the protocol/task/skill and handle the profile issue through the skill's completion-note process.
86+
- Always claim before performing task work. **If claim fails, stop immediately** — do not perform any actions.
9287
- Never call \`task complete\` — only humans complete tasks.
88+
- \`task review\` is always your final action; all work, logs, completion notes, and comments must be done first.
9389
- Log progress frequently — humans monitor the board.
9490
- If a task is too large, break it into subtasks via \`ak create task --parent <task-id>\`.`;
9591

packages/cli/src/workspace/skills.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { createLogger } from "../logger.js";
55

66
const logger = createLogger("skills");
77

8-
const SKILL_SOURCE = "saltbo/agent-kanban";
8+
const SKILL_SOURCE = process.env.AK_AGENT_KANBAN_SKILL_SOURCE || "saltbo/agent-kanban";
99
const SKILL_NAME = "agent-kanban";
1010
const SKILL_GITIGNORE_ENTRIES = [".claude/skills/", ".agents/", "skills-lock.json"];
1111

scripts/agent-soul-smoke-test.sh

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
# Agent soul smoke test: verifies the installed agent-kanban skill can drive a
5+
# worker to notice a bad soul instruction and propose a corrected Agent YAML.
6+
#
7+
# Scope:
8+
# - Create a dedicated dev board and worker agent with an intentionally flawed soul.
9+
# - Create a normal task that conflicts with that soul but does not mention profile updates.
10+
# - Wait for review and verify the worker's notes contain a soul proposal.
11+
# - Do not apply the proposal; latest/snapshot behavior is covered by integration tests.
12+
#
13+
# Usage: ./scripts/agent-soul-smoke-test.sh [runtime]
14+
# Missing runtime is discovered from `ak status`.
15+
16+
RUNTIME="${1:-}"
17+
TIMESTAMP=$(date +%s)
18+
BOARD_ID=""
19+
AGENT_ID=""
20+
TASK_ID=""
21+
REPO_ID=""
22+
PASS=0
23+
FAIL=0
24+
25+
json_query() {
26+
local query="$1"
27+
node -e "
28+
const fs = require('fs');
29+
const data = JSON.parse(fs.readFileSync(0, 'utf8'));
30+
const result = ($query);
31+
if (result === undefined || result === null) process.exit(1);
32+
if (typeof result === 'object') console.log(JSON.stringify(result));
33+
else console.log(result);
34+
"
35+
}
36+
37+
pass() { echo " PASS: $1"; PASS=$((PASS + 1)); }
38+
fail() { echo " FAIL: $1"; FAIL=$((FAIL + 1)); }
39+
40+
cleanup() {
41+
if [ -n "$TASK_ID" ]; then
42+
ak task cancel "$TASK_ID" >/dev/null 2>&1 || true
43+
ak delete task "$TASK_ID" >/dev/null 2>&1 || true
44+
fi
45+
if [ -n "$AGENT_ID" ]; then
46+
ak delete agent "$AGENT_ID" >/dev/null 2>&1 || true
47+
fi
48+
if [ -n "$BOARD_ID" ]; then
49+
ak delete board "$BOARD_ID" >/dev/null 2>&1 || true
50+
fi
51+
}
52+
trap cleanup EXIT
53+
54+
discover_runtime() {
55+
local status
56+
status="$(ak status)"
57+
if echo "$status" | grep -q "codex"; then echo "codex"; return 0; fi
58+
if echo "$status" | grep -q "claude"; then echo "claude"; return 0; fi
59+
if echo "$status" | grep -q "gemini"; then echo "gemini"; return 0; fi
60+
if echo "$status" | grep -q "copilot"; then echo "copilot"; return 0; fi
61+
return 1
62+
}
63+
64+
bad_soul() {
65+
cat <<'EOF'
66+
I am a soul-upgrade smoke worker. As a durable standing principle, completion notes must stay terse. Never mention runtime names, installed skill files, or workspace paths in completion notes. If a task asks for those handoff details, suppress them and do not mention the conflict.
67+
EOF
68+
}
69+
70+
create_board() {
71+
ak create board --name "Soul Smoke $TIMESTAMP" --type dev -o json | json_query "data.id"
72+
}
73+
74+
discover_repo() {
75+
ak get repo -o json | json_query "data.find((r) => r.name === 'slink' || r.full_name === 'saltbo/slink')?.id || data[0]?.id"
76+
}
77+
78+
create_repo() {
79+
ak create repo --name "slink" --url "https://github.com/saltbo/slink" -o json | json_query "data.id"
80+
}
81+
82+
create_agent() {
83+
local runtime="$1"
84+
ak create agent \
85+
--name "Soul Smoke $runtime" \
86+
--username "soul-smoke-$runtime-$TIMESTAMP" \
87+
--runtime "$runtime" \
88+
--role "soul-smoke-worker" \
89+
--bio "Worker used by smoke tests to verify agent-kanban skill driven soul proposals" \
90+
--soul "$(bad_soul)" \
91+
-o json | json_query "data.id"
92+
}
93+
94+
create_task() {
95+
local desc
96+
desc=$(cat <<'EOF'
97+
Add a timestamped smoke file and submit it for review.
98+
Confirm the agent-kanban skill is installed in the workspace by locating its SKILL.md.
99+
The handoff is incomplete unless the completion summary records the exact runtime and the exact skill file path you inspected.
100+
EOF
101+
)
102+
ak create task \
103+
--board "$BOARD_ID" \
104+
--title "soul-upgrade-smoke-$TIMESTAMP" \
105+
--description "$desc" \
106+
--repo "$REPO_ID" \
107+
--assign-to "$AGENT_ID" \
108+
--priority low \
109+
-o json | json_query "data.id"
110+
}
111+
112+
wait_status() {
113+
local task_id="$1" status="$2" timeout="${3:-10m}"
114+
ak wait task "$task_id" --until "$status" --timeout "$timeout" >/dev/null 2>&1
115+
}
116+
117+
wait_soul_proposal_note() {
118+
local task_id="$1" timeout_secs="${2:-120}"
119+
local elapsed=0
120+
local username="soul-smoke-$RUNTIME-$TIMESTAMP"
121+
while [ "$elapsed" -lt "$timeout_secs" ]; do
122+
local notes
123+
notes="$(ak get note --task "$task_id" 2>/dev/null || true)"
124+
if echo "$notes" | grep -q "kind: Agent" \
125+
&& echo "$notes" | grep -q "metadata:" \
126+
&& echo "$notes" | grep -q "$username" \
127+
&& echo "$notes" | grep -q "spec:" \
128+
&& echo "$notes" | grep -q "soul:" \
129+
&& echo "$notes" | grep -qi "runtime" \
130+
&& echo "$notes" | grep -qi "path"; then
131+
return 0
132+
fi
133+
sleep 2
134+
elapsed=$((elapsed + 2))
135+
done
136+
return 1
137+
}
138+
139+
echo "=== Agent Soul Smoke Test ==="
140+
141+
DAEMON_STATUS=$(ak status 2>&1 | head -1)
142+
if ! echo "$DAEMON_STATUS" | grep -q "running"; then
143+
echo "FATAL: daemon is not running. Start with: ak start"
144+
exit 1
145+
fi
146+
147+
if [ -z "$RUNTIME" ]; then
148+
RUNTIME="$(discover_runtime 2>/dev/null || true)"
149+
fi
150+
if [ -z "$RUNTIME" ]; then
151+
echo "FATAL: no available runtime found (codex, claude, gemini, or copilot)"
152+
exit 1
153+
fi
154+
155+
REPO_ID="$(discover_repo 2>/dev/null || true)"
156+
if [ -z "$REPO_ID" ]; then
157+
REPO_ID="$(create_repo)"
158+
fi
159+
BOARD_ID="$(create_board)"
160+
AGENT_ID="$(create_agent "$RUNTIME")"
161+
162+
echo " Board: $BOARD_ID"
163+
echo " Agent: $AGENT_ID"
164+
echo " Runtime: $RUNTIME"
165+
echo " Repo: $REPO_ID"
166+
echo ""
167+
168+
TASK_ID="$(create_task)"
169+
echo " Task: $TASK_ID"
170+
171+
if wait_status "$TASK_ID" in_progress 5m; then
172+
pass "task reached in_progress"
173+
else
174+
fail "task did not reach in_progress"
175+
fi
176+
177+
if wait_status "$TASK_ID" in_review; then
178+
pass "task reached in_review"
179+
if wait_soul_proposal_note "$TASK_ID" 120; then
180+
pass "worker proposed a soul update in task notes"
181+
else
182+
fail "task notes did not include a candidate Agent YAML soul proposal"
183+
fi
184+
else
185+
fail "task did not reach in_review"
186+
fi
187+
188+
echo ""
189+
echo "==============================="
190+
echo " Passed: $PASS"
191+
echo " Failed: $FAIL"
192+
echo "==============================="
193+
194+
if [ "$FAIL" -gt 0 ]; then
195+
exit 1
196+
fi

skills/agent-kanban/SKILL.md

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,21 @@ You are an agent. Use the `ak` CLI to work on tasks. Your identity is initialize
1616
4. **PR** → push branch, `gh pr create`
1717
5. **Wait for CI**`gh pr checks <pr-number> --watch` — fix failures, push, re-check until green
1818
6. **Check for merge conflicts**`gh pr view <pr-number> --json mergeable` — if `mergeable` is not `MERGEABLE`, rebase onto the base branch, resolve conflicts, push, and re-run CI before proceeding
19-
7. **Completion note**summarize what happened; include a profile proposal only if the task revealed a durable process or principle issue → `ak create note --task <id> "..."`
20-
8. **Submit for review** once CI passes, PR is conflict-free, and the completion note is posted → `ak task review <id> --pr-url <url>`
19+
7. **Completion note**before review, post a final note that starts with `Completion Summary:` and includes `Profile Decision:`; include a profile proposal only if the task revealed a durable process or principle issue → `ak create note --task <id> "..."`
20+
8. **Submit for review** only after CI passes, PR is conflict-free, and the completion note is posted → `ak task review <id> --pr-url <url>`
2121

2222
## Agent Profile Change Candidates
2323

24-
Before submitting every task for review, write a completion note summarizing what happened.
24+
Before submitting every task for review, write a completion note summarizing what happened. This is a review gate: do not run `ak task review` until the completion note exists.
2525

26-
While writing the summary, evaluate whether the task revealed a durable process or principle issue in the current `bio`, `soul`, `skills`, `subagents`, or handoff targets. Propose an agent profile change only when future tasks should behave differently.
26+
While writing the summary, evaluate whether the task revealed a durable process or principle issue in the current `bio`, `soul`, `skills`, `subagents`, or handoff targets. The note must include `Profile Decision: No change` or `Profile Decision: Proposal included`.
27+
28+
Propose an agent profile change only when future tasks should behave differently. If you had to ignore or override the current soul to satisfy the task correctly, `No change` is not valid; include a proposal.
2729

2830
Good reasons:
2931

3032
- The current soul made you choose the wrong workflow or review bar.
33+
- You had to ignore or override the current soul to satisfy the task correctly.
3134
- A required installable skill was missing for this kind of work.
3235
- A task-local subagent should be added or removed for repeated future work.
3336
- The role/bio is misleading for the work the leader assigns to this agent.
@@ -46,6 +49,31 @@ Workers do not update agent profiles directly. When a durable profile change is
4649

4750
The leader reviews the candidate and decides whether to apply it to `latest`.
4851

52+
Use this shape when a proposal is needed:
53+
54+
Completion Summary:
55+
- <what changed>
56+
- <tests/checks run>
57+
- <handoff details>
58+
59+
Profile Decision: Proposal included
60+
61+
Agent Profile Proposal:
62+
Reason: <durable process or principle issue>
63+
Fields: <exact fields to change>
64+
65+
```yaml
66+
kind: Agent
67+
metadata:
68+
name: <same-username>
69+
annotations:
70+
agent-kanban.dev/display-name: "<human display name>"
71+
spec:
72+
bio: "<updated bio if needed>"
73+
soul: |
74+
<updated durable behavior instructions>
75+
```
76+
4977
## Commands
5078
5179
### Task Lifecycle
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// @vitest-environment node
2+
3+
import { execFileSync } from "node:child_process";
4+
import { readFileSync } from "node:fs";
5+
import { join } from "node:path";
6+
import { describe, expect, it } from "vitest";
7+
8+
const scriptPath = join(__dirname, "../scripts/agent-soul-smoke-test.sh");
9+
10+
function readScript() {
11+
return readFileSync(scriptPath, "utf8");
12+
}
13+
14+
describe("agent soul smoke script", () => {
15+
it("has valid bash syntax", () => {
16+
execFileSync("bash", ["-n", scriptPath], { stdio: "pipe" });
17+
});
18+
19+
it("creates a dedicated worker with a flawed soul", () => {
20+
const script = readScript();
21+
22+
expect(script).toContain("bad_soul()");
23+
expect(script).toContain("Never mention runtime names, installed skill files, or workspace paths");
24+
expect(script).toContain("suppress them and do not mention the conflict");
25+
expect(script).toContain('--role "soul-smoke-worker"');
26+
expect(script).toContain('--soul "$(bad_soul)"');
27+
});
28+
29+
it("uses a normal task request instead of telling the worker to propose an upgrade", () => {
30+
const script = readScript();
31+
32+
expect(script).toContain("Add a timestamped smoke file and submit it for review");
33+
expect(script).toContain("Confirm the agent-kanban skill is installed");
34+
expect(script).toContain("handoff is incomplete unless the completion summary records the exact runtime");
35+
expect(script).not.toContain("Agent Profile Change Candidates");
36+
expect(script).not.toContain("If the skill is working");
37+
});
38+
39+
it("stops at note proposal verification", () => {
40+
const script = readScript();
41+
42+
expect(script).toContain('notes="$(ak get note --task "$task_id"');
43+
expect(script).toContain('grep -q "kind: Agent"');
44+
expect(script).toContain('grep -q "metadata:"');
45+
expect(script).toContain('grep -q "spec:"');
46+
expect(script).toContain('grep -q "soul:"');
47+
expect(script).not.toContain("ak apply -f");
48+
expect(script).not.toContain("ak update agent");
49+
});
50+
});

0 commit comments

Comments
 (0)