2 Commits
Author SHA1 Message Date
Kevin Veen-Birkenbach 478ef1ac9f deleted file
🧪 Test / 🧪 Lock + lint (push) Has been cancelled
🔄 Update / 🧠 Update skills-lock.json (push) Has been cancelled
2026-08-10 17:14:39 +02:00
Kevin Veen-Birkenbach 1e90ac1737 updated lock
🧪 Test / 🧪 Lock + lint (push) Has been cancelled
2026-08-10 17:13:29 +02:00
81 changed files with 543 additions and 5623 deletions

No files matched your search

-6
View File
@@ -1,6 +0,0 @@
version: 2
updates:
- package-ecosystem: pip
directory: /
schedule:
interval: daily
+1 -2
View File
@@ -24,8 +24,7 @@ jobs:
- name: 📦 Install lint tools - name: 📦 Install lint tools
run: | run: |
python -m pip install --upgrade pip pip install ruff
pip install --group lint
sudo apt-get update && sudo apt-get install -y shellcheck sudo apt-get update && sudo apt-get install -y shellcheck
npm install -g markdownlint-cli2 npm install -g markdownlint-cli2
+1 -2
View File
@@ -29,8 +29,7 @@ jobs:
- name: 📦 Install lint tools - name: 📦 Install lint tools
run: | run: |
python -m pip install --upgrade pip pip install ruff
pip install --group lint
sudo apt-get update && sudo apt-get install -y shellcheck sudo apt-get update && sudo apt-get install -y shellcheck
npm install -g markdownlint-cli2 npm install -g markdownlint-cli2
-1
View File
@@ -15,7 +15,6 @@ npm-debug.log*
# Rendered skill install targets # Rendered skill install targets
.agents/ .agents/
.claude/ .claude/
.mcp.json
# Editor / OS # Editor / OS
.DS_Store .DS_Store
-67
View File
@@ -1,67 +0,0 @@
# Changelog
## [0.2.0] - 2026-10-31
Added:
- Git: *branch-gc* clears merged branches and worktrees with *pkgmgr*
- Agents: *subagent-model* uses a cheaper model only at 90 % same result
- Agents: *subagent-model* hooks log every spawn and its rated quality
Changed:
- Coordination: never cut a running job or agent short, not even at its cap
- Resources: *watch.py admit* grants test slots only below CPU and RAM limits
- Coordination: a relayer writes an operator rule to shared memory first
Fixed:
- Resources: stop-stage cleanup now skips worktrees where *make* still runs
- Resources: the block hook ignores heavy commands in quotes and heredocs
- Update: *make update* now updates every locked source, not just the first
- Update: *make update* installs for Claude Code only
- CI: *ci-artifacts* signs lint failures by finding, ignores later steps
- Release: *changelog* warns when a workflow on HEAD already failed
- CI: ruff pinned in *pyproject.toml*; local and CI lint now agree
Upgrade notes:
- 0.1.0 was never published (its CI failed); its upgrade notes follow
- Install: restart the agent after *make install* to load skills and hooks
- Resources: *watch.py* needs *~/.config/agents-council/config.json*
- Resources: add *busy_processes* and *load_limits* to that config
## [0.1.0] - 2026-10-30
Added:
- Install: *make install* and *make project* install every locked skill
- Install: enables caveman and ponytail; adds autotune and dream reminders
- Commit: *commit-next* commits a bundle and stages the next one
- Commit: *comments-clean* and *advocate* audit every commit before it lands
- Commit: *confidence* and *break-risk* report measured fix and break numbers
- Coordination: *democracy* elects the first agent to claim work as its owner
- Coordination: *council.py* claims work; a hook blocks edits without a claim
- Coordination: *funnel* keeps agent work in worktrees until it clears 90/5
- Resources: *watch.py* blocks heavy jobs and cleans up before the disk fills
- Cleanup: *cleanup* frees disk space after asking every peer agent
- CI: *triage*, *observe* and *ci-artifacts* with *ci_failures.py* clustering
- CI: *tune* finds speed-ups that never cut logic, coverage or correctness
- Git: *merge-fix* with *find_fix.py* finds a fix on any branch and merges it
- Reasoning: *active-listening* and *dialectic* pin requirements and causes
- Review: *criticize* and *sceptic* give critique hardened by dialectic
- Autonomy: *plan* hands an objective-oriented plan to *robot*
- Quality: *test* runs Makefile targets; *--fix* loops until they are green
- Quality: *refactor*, *no-defaults* and *scope-guard* keep changes lean
- Skills: *autotune*, *autoskill*, *dream* and *essence* improve the skills
- Release: *changelog* writes the pkgmgr release message since the last tag
- Status: *status-matrix* lists everything in flight with its confidence
- Help: *explain* and *help-skills* explain code and the skill set
- Chat: *shortcuts* expands operator tokens such as *sobu* and *socon*
- Security: *medusa* runs the MEDUSA scanner on repos and secret stores
- Docs: the README skill map shows how the skills invoke each other
Upgrade notes:
- Install: restart the agent after *make install* to load skills and hooks
- Resources: *watch.py* needs *~/.config/agents-council/config.json*
+1 -214
View File
@@ -7,219 +7,6 @@ Every skill is pinned in `skills-lock.json` for reproducible installs.
First-party skills live under `skills/` and are layered on top of the locked ones at install time. First-party skills live under `skills/` and are layered on top of the locked ones at install time.
Compatible with Claude Code, Codex, Gemini CLI, Cursor, Copilot, Windsurf, Cline, and other agents. Compatible with Claude Code, Codex, Gemini CLI, Cursor, Copilot, Windsurf, Cline, and other agents.
## Naming 🏷️
Every first-party skill is named `kvb-<category>-<name>` with one of these categories: `think`, `create`, `code`, `ci`, `git`, `check`, `org`, `skills`. Only `kvb-cleanup` and `kvb-help` stand without a category. Names use `a-z`, `0-9` and single hyphens, at most 64 characters, and equal their directory name. `tests/test_naming.py` enforces all of it, including that the organigram below lists every skill under its category.
## Organigram 🏛️
Every first-party skill under its category.
```mermaid
flowchart LR
kvb((kvb))
kvb --> think[think]
kvb --> create[create]
kvb --> code[code]
kvb --> ci[ci]
kvb --> git[git]
kvb --> check[check]
kvb --> org[org]
kvb --> skills[skills]
kvb --> none[no category]
think --> think_listen[kvb-think-listen]
think --> think_explain[kvb-think-explain]
think --> think_dialectic[kvb-think-dialectic]
think --> think_criticize[kvb-think-criticize]
create --> create_plan[kvb-create-plan]
create --> create_feature[kvb-create-feature]
code --> code_scope_guard[kvb-code-scope-guard]
code --> code_no_defaults[kvb-code-no-defaults]
code --> code_no_comments[kvb-code-no-comments]
code --> code_refactor[kvb-code-refactor]
code --> code_test[kvb-code-test]
code --> code_tune[kvb-code-tune]
ci --> ci_triage[kvb-ci-triage]
ci --> ci_artifacts[kvb-ci-artifacts]
ci --> ci_observe[kvb-ci-observe]
git --> git_commit_next[kvb-git-commit-next]
git --> git_commit_no_verify[kvb-git-commit-no-verify]
git --> git_merge_fix[kvb-git-merge-fix]
git --> git_branch_gc[kvb-git-branch-gc]
git --> git_changelog[kvb-git-changelog]
check --> check_legal[kvb-check-legal]
check --> check_security[kvb-check-security]
check --> check_principles[kvb-check-principles]
check --> check_confidence[kvb-check-confidence]
check --> check_break_risk[kvb-check-break-risk]
org --> org_organize[kvb-org-organize]
org --> org_split[kvb-org-split]
org --> org_coordinate[kvb-org-coordinate]
org --> org_perform[kvb-org-perform]
org --> org_matrix[kvb-org-matrix]
org --> org_rasci[kvb-org-rasci]
org --> org_artefacts[kvb-org-artefacts]
org --> org_wsjf[kvb-org-wsjf]
org --> org_robot[kvb-org-robot]
skills --> skills_dream[kvb-skills-dream]
skills --> skills_tune[kvb-skills-tune]
skills --> skills_auto[kvb-skills-auto]
skills --> skills_essence[kvb-skills-essence]
skills --> skills_shortcuts[kvb-skills-shortcuts]
none --> cleanup[kvb-cleanup]
none --> help[kvb-help]
```
## Skill map 🗺️
How the first-party skills work together. A solid arrow means the skill invokes or relies on the other one; a dotted arrow means it fires automatically. Every skill that reports a result closes with the `kvb-check-confidence` block; those arrows are left out. The locked third-party skills (caveman, ponytail, archify) are not drawn.
```mermaid
flowchart LR
subgraph triggers [Triggers]
commit_req([commit request])
stage_req([staging or fix ready])
edit_hook([edit in a git worktree])
reminders([reminder hooks])
code_task([code task])
spawn([subagent spawn])
end
subgraph cat_think [think]
active_listening[kvb-think-listen]
explain[kvb-think-explain]
dialectic[kvb-think-dialectic]
criticize[kvb-think-criticize]
end
subgraph cat_create [create]
plan[kvb-create-plan]
auto_feature[kvb-create-feature]
end
subgraph cat_code [code]
scope_guard[kvb-code-scope-guard]
no_defaults[kvb-code-no-defaults]
refactor[kvb-code-refactor]
test[kvb-code-test]
tune[kvb-code-tune]
comments_clean[kvb-code-no-comments]
end
subgraph cat_ci [ci]
triage[kvb-ci-triage]
ci_artifacts[kvb-ci-artifacts]
observe[kvb-ci-observe]
end
subgraph cat_git [git]
commit_next[kvb-git-commit-next]
commit_no_verify[kvb-git-commit-no-verify]
merge_fix[kvb-git-merge-fix]
branch_gc[kvb-git-branch-gc]
changelog[kvb-git-changelog]
end
subgraph cat_check [check]
advocate[kvb-check-legal]
medusa[kvb-check-security]
sceptic[kvb-check-principles]
confidence[kvb-check-confidence]
break_risk[kvb-check-break-risk]
end
subgraph cat_org [org]
democracy[kvb-org-organize]
funnel[kvb-org-split]
coordinate[kvb-org-coordinate]
subagent_model[kvb-org-perform]
status_matrix[kvb-org-matrix]
rasci[kvb-org-rasci]
artefacts[kvb-org-artefacts]
wsjf[kvb-org-wsjf]
robot[kvb-org-robot]
end
subgraph cat_skills [skills]
dream[kvb-skills-dream]
autotune[kvb-skills-tune]
autoskill[kvb-skills-auto]
essence[kvb-skills-essence]
shortcuts[kvb-skills-shortcuts]
end
subgraph cat_none [no category]
cleanup[kvb-cleanup]
help_skills[kvb-help]
end
commit_req -.-> commit_next
commit_req -.-> commit_no_verify
stage_req -.-> confidence
edit_hook -. council gate .-> democracy
reminders -.-> dream
reminders -.-> autotune
code_task -.-> no_defaults
code_task -.-> scope_guard
spawn -.-> subagent_model
commit_next --> merge_fix
commit_next --> commit_no_verify
commit_next --> explain
commit_next --> dialectic
commit_no_verify --> comments_clean
commit_no_verify --> advocate
advocate --> dialectic
confidence --> break_risk
confidence -. objective .-> autotune
coordinate --> democracy
cleanup --> democracy
cleanup --> branch_gc
branch_gc --> democracy
funnel --> democracy
funnel --> dialectic
funnel --> subagent_model
dialectic --> subagent_model
democracy --> merge_fix
democracy -. claim and release .-> rasci
funnel --> rasci
status_matrix --> rasci
cleanup --> artefacts
artefacts --> rasci
democracy --> wsjf
wsjf --> rasci
dream --> rasci
observe --> triage
observe --> ci_artifacts
triage --> ci_artifacts
triage --> democracy
triage --> dialectic
triage --> funnel
triage --> tune
tune --> democracy
tune --> funnel
tune --> merge_fix
tune --> dialectic
dialectic --> active_listening
criticize --> dialectic
criticize --> active_listening
sceptic --> dialectic
sceptic --> no_defaults
auto_feature --> sceptic
auto_feature --> active_listening
plan --> robot
plan --> active_listening
robot --> dialectic
dream --> dialectic
dream --> autotune
dream --> autoskill
autoskill --> autotune
autoskill --> shortcuts
autotune --> essence
test --> triage
test --> dialectic
test --> no_defaults
test --> comments_clean
refactor --> comments_clean
```
## Requirements 📦 ## Requirements 📦
- Node.js (provides `npx skills`). - Node.js (provides `npx skills`).
@@ -239,7 +26,7 @@ Copy the skills into a specific project (`<repo>/.agents/skills` and `<repo>/.cl
make project TARGET=/path/to/repo make project TARGET=/path/to/repo
``` ```
Both commands also patch the target's `.claude/settings.local.json` — the machine-local, untracked file, so a project's reviewed `.claude/settings.json` is never rewritten by an install: the caveman and ponytail plugins are enabled so their modes auto-activate on session start, and two reminder hooks are registered — `kvb-skills-tune` points you at `/kvb-skills-tune` at most once an hour, `kvb-skills-dream` points you at `/kvb-skills-dream` at most once every twelve hours per repository (stamped in that repository's git dir). A PreToolUse gate from the `kvb-org-organize` skill blocks edits to files in a git worktree until the session holds a claim (`skills/kvb-org-organize/scripts/council.py`), and its resource watch (`skills/kvb-org-organize/scripts/watch.py`) measures disk, Docker and caches, cleans up low-risk data and blocks new heavy jobs before the disk runs out; it stays inactive until `~/.config/agents-council/config.json` exists. Two hooks of the `kvb-org-perform` skill (`skills/kvb-org-perform/scripts/log.py`) log every Agent spawn with its task class, model and effort, and store the quality feedback a sandboxed session prints, in `~/.local/state/agents-council/subagent-model.jsonl`. Existing settings are preserved. Both commands also patch the target's `.claude/settings.json`: the caveman and ponytail plugins are enabled so their modes auto-activate on session start, and two reminder hooks are registered — `autotune` points you at `/autotune` at most once an hour, `dream` points you at `/dream` at most once every twelve hours per repository (stamped in that repository's git dir). Existing settings are preserved.
Restart your agent afterwards so it loads the new skills. Restart your agent afterwards so it loads the new skills.
-5
View File
@@ -1,5 +0,0 @@
[dependency-groups]
lint = ["ruff==0.17.3"]
[tool.ruff]
target-version = "py312"
+5 -50
View File
@@ -1,8 +1,8 @@
#!/usr/bin/env node #!/usr/bin/env node
// Enable the caveman and ponytail plugins in a Claude Code settings.json so // Enable the caveman and ponytail plugins in a Claude Code settings.json so
// their SessionStart hooks auto-activate both modes, register the kvb-skills-tune // their SessionStart hooks auto-activate both modes, and register the autotune
// and kvb-skills-dream reminder hooks, the kvb-org-organize claim gate, the resource watch and the kvb-org-perform log. Merges non-destructively: existing marketplaces, // and dream reminder hooks. Merges non-destructively: existing marketplaces,
// plugins and hooks are preserved, hooks of renamed skills are repointed, unparseable files are left untouched. // plugins and hooks are preserved, unparseable files are left untouched.
"use strict"; "use strict";
const fs = require("fs"); const fs = require("fs");
@@ -19,14 +19,7 @@ const MARKETPLACES = {
ponytail: { source: { source: "github", repo: "DietrichGebert/ponytail" } }, ponytail: { source: { source: "github", repo: "DietrichGebert/ponytail" } },
}; };
const PLUGINS = { "caveman@caveman": true, "ponytail@ponytail": true }; const PLUGINS = { "caveman@caveman": true, "ponytail@ponytail": true };
const REMINDERS = ["kvb-skills-tune", "kvb-skills-dream"]; const REMINDERS = ["autotune", "dream"];
const RENAMED = {
"autotune/hooks/reminder.sh": "kvb-skills-tune/hooks/reminder.sh",
"dream/hooks/reminder.sh": "kvb-skills-dream/hooks/reminder.sh",
"democracy/scripts/council.py": "kvb-org-organize/scripts/council.py",
"democracy/scripts/watch.py": "kvb-org-organize/scripts/watch.py",
"subagent-model/scripts/log.py": "kvb-org-perform/scripts/log.py",
};
let data = {}; let data = {};
if (fs.existsSync(settingsPath)) { if (fs.existsSync(settingsPath)) {
@@ -48,17 +41,6 @@ for (const [name, entry] of Object.entries(MARKETPLACES)) {
data.enabledPlugins = Object.assign(data.enabledPlugins || {}, PLUGINS); data.enabledPlugins = Object.assign(data.enabledPlugins || {}, PLUGINS);
data.hooks = data.hooks || {}; data.hooks = data.hooks || {};
for (const entries of Object.values(data.hooks)) {
if (!Array.isArray(entries)) continue;
for (const entry of entries) {
for (const hook of entry.hooks || []) {
if (typeof hook.command !== "string") continue;
for (const [from, to] of Object.entries(RENAMED)) {
hook.command = hook.command.replace(`/skills/${from}`, `/skills/${to}`);
}
}
}
}
data.hooks.UserPromptSubmit = data.hooks.UserPromptSubmit || []; data.hooks.UserPromptSubmit = data.hooks.UserPromptSubmit || [];
const alreadyRegistered = JSON.stringify(data.hooks.UserPromptSubmit); const alreadyRegistered = JSON.stringify(data.hooks.UserPromptSubmit);
for (const skill of REMINDERS) { for (const skill of REMINDERS) {
@@ -69,32 +51,5 @@ for (const skill of REMINDERS) {
}); });
} }
data.hooks.PreToolUse = data.hooks.PreToolUse || [];
if (!JSON.stringify(data.hooks.PreToolUse).includes("kvb-org-organize/scripts/council.py")) {
const council = path.join(path.dirname(settingsPath), "skills", "kvb-org-organize", "scripts", "council.py");
data.hooks.PreToolUse.push({
matcher: "Edit|Write|MultiEdit|NotebookEdit",
hooks: [{ type: "command", command: `python3 ${JSON.stringify(council)} gate` }],
});
}
const SCRIPT_HOOKS = [
["kvb-org-organize/scripts/watch.py", "UserPromptSubmit", null, "watch"],
["kvb-org-organize/scripts/watch.py", "PostToolUse", "Bash", "watch"],
["kvb-org-organize/scripts/watch.py", "PreToolUse", "Bash", "block"],
["kvb-org-perform/scripts/log.py", "PreToolUse", "Agent|Task", "spawn"],
["kvb-org-perform/scripts/log.py", "PostToolUse", "Bash", "persist"],
];
for (const [script, event, matcher, sub] of SCRIPT_HOOKS) {
data.hooks[event] = data.hooks[event] || [];
const registered = data.hooks[event].some((entry) =>
(entry.hooks || []).some((hook) =>
String(hook.command).includes(script) && String(hook.command).endsWith(` ${sub}`)));
if (registered) continue;
const file = path.join(path.dirname(settingsPath), "skills", ...script.split("/"));
const entry = { hooks: [{ type: "command", command: `python3 ${JSON.stringify(file)} ${sub}` }] };
data.hooks[event].push(matcher ? { matcher, ...entry } : entry);
}
fs.writeFileSync(settingsPath, JSON.stringify(data, null, 2) + "\n"); fs.writeFileSync(settingsPath, JSON.stringify(data, null, 2) + "\n");
console.log(`skills: enabled caveman + ponytail plugins, the ${REMINDERS.join(" + ")} reminders, the kvb-org-organize claim gate, the resource watch and the kvb-org-perform log in ${settingsPath}`); console.log(`skills: enabled caveman + ponytail plugins and the ${REMINDERS.join(" + ")} reminders in ${settingsPath}`);
+2 -3
View File
@@ -23,11 +23,10 @@ if [[ ! -d "${TARGET}" ]]; then
fi fi
cd "${REPO_ROOT}" cd "${REPO_ROOT}"
src="${REPO_ROOT}/.agents/skills"
rm -rf "${src}"
log "skills: restoring from skills-lock.json..." log "skills: restoring from skills-lock.json..."
npx --yes skills experimental_install npx --yes skills experimental_install
src="${REPO_ROOT}/.agents/skills"
if [[ ! -d "${src}" ]]; then if [[ ! -d "${src}" ]]; then
warn "skills: ${src} not found after install." warn "skills: ${src} not found after install."
exit 1 exit 1
@@ -45,7 +44,7 @@ for dst in "${TARGET}/.agents/skills" "${TARGET}/.claude/skills"; do
cp -a "${src}/." "${dst}/" cp -a "${src}/." "${dst}/"
done done
settings="${TARGET}/.claude/settings.local.json" settings="${TARGET}/.claude/settings.json"
if command -v node &>/dev/null; then if command -v node &>/dev/null; then
log "skills: auto-enabling caveman + ponytail plugins in ${settings}" log "skills: auto-enabling caveman + ponytail plugins in ${settings}"
node "${REPO_ROOT}/scripts/enable-plugins.js" "${settings}" node "${REPO_ROOT}/scripts/enable-plugins.js" "${settings}"
+1 -1
View File
@@ -30,6 +30,6 @@ fi
log "skills: updating from ${lockfile}..." log "skills: updating from ${lockfile}..."
jq -r '.skills[].source' "${lockfile}" | sort -u | while read -r source; do jq -r '.skills[].source' "${lockfile}" | sort -u | while read -r source; do
log "skills: updating ${source}..." log "skills: updating ${source}..."
npx --yes skills add --yes --agent claude-code "${source}" </dev/null npx --yes skills add --yes "${source}"
done done
log "skills: update complete." log "skills: update complete."
+13 -19
View File
@@ -1,89 +1,83 @@
{ {
"version": 1, "version": 1,
"skills": { "skills": {
"archify": {
"source": "tt-a1i/archify",
"sourceType": "github",
"skillPath": "archify/SKILL.md",
"computedHash": "e9ef0923cda908886e667eb48e36b29e81d0a7a7b9fe2bc05f3779be271e5119"
},
"cavecrew": { "cavecrew": {
"source": "JuliusBrussee/caveman", "source": "JuliusBrussee/caveman",
"sourceType": "github", "sourceType": "github",
"skillPath": "skills/cavecrew/SKILL.md", "skillPath": "skills/cavecrew/SKILL.md",
"computedHash": "2e122d819a812bef0e4958706f306e4346e78ecb5a4893997b99cbc19866f514" "computedHash": "c5527c994fbd4c22b36714e3b124a0f167a533d114ab164fb1d35e2123533917"
}, },
"caveman": { "caveman": {
"source": "JuliusBrussee/caveman", "source": "JuliusBrussee/caveman",
"sourceType": "github", "sourceType": "github",
"skillPath": "skills/caveman/SKILL.md", "skillPath": "skills/caveman/SKILL.md",
"computedHash": "71b600c88c8d1d36f18c4875db23b53c5d6cb9ba3195e19451d6b1bb67db07c4" "computedHash": "59e1fe0d3eeb4189ee5c467efde567672e5cacb41f157c477a6152ca907d44ea"
}, },
"caveman-commit": { "caveman-commit": {
"source": "JuliusBrussee/caveman", "source": "JuliusBrussee/caveman",
"sourceType": "github", "sourceType": "github",
"skillPath": "skills/caveman-commit/SKILL.md", "skillPath": "skills/caveman-commit/SKILL.md",
"computedHash": "b416cf134b0f33ddadeca7977aebdbb2d3db79a166d0fce2e1aabeec98132766" "computedHash": "790a4eeace0be35c6691faf923518ba5bd50f1f1305d1101d09dd4971be94e00"
}, },
"caveman-compress": { "caveman-compress": {
"source": "JuliusBrussee/caveman", "source": "JuliusBrussee/caveman",
"sourceType": "github", "sourceType": "github",
"skillPath": "skills/caveman-compress/SKILL.md", "skillPath": "skills/caveman-compress/SKILL.md",
"computedHash": "9c8d35882a93e387a584cc4efaacb74433d379a2bd02e3e6f6ad4f7c30ff55ed" "computedHash": "52f2301832b376a765b0ed02445c8bf05874b624052bf9a9c3861d9c3dfcee4b"
}, },
"caveman-help": { "caveman-help": {
"source": "JuliusBrussee/caveman", "source": "JuliusBrussee/caveman",
"sourceType": "github", "sourceType": "github",
"skillPath": "skills/caveman-help/SKILL.md", "skillPath": "skills/caveman-help/SKILL.md",
"computedHash": "c7a73a8c6afb6f871690666449209106aba847b4a9b0698686648960ece80012" "computedHash": "c76fd4aa86ad557eee62aacbd4b9dd46499fe3913910e7d596d20d094296b984"
}, },
"caveman-review": { "caveman-review": {
"source": "JuliusBrussee/caveman", "source": "JuliusBrussee/caveman",
"sourceType": "github", "sourceType": "github",
"skillPath": "skills/caveman-review/SKILL.md", "skillPath": "skills/caveman-review/SKILL.md",
"computedHash": "36a4007c5c0c6b90b5accfee1cd769a86e4963b651dece2ce5f17a223a0abb4b" "computedHash": "fb7214a1c5793bae6ba8b1be4329e2e6f40dbec6dd911dfb335ad29f09c316a1"
}, },
"caveman-stats": { "caveman-stats": {
"source": "JuliusBrussee/caveman", "source": "JuliusBrussee/caveman",
"sourceType": "github", "sourceType": "github",
"skillPath": "skills/caveman-stats/SKILL.md", "skillPath": "skills/caveman-stats/SKILL.md",
"computedHash": "cc9fcd21f2a30cff5596b3cbb197fe0f4b1b30b58084a864350853c217ad301a" "computedHash": "57c7db449641379e2afd1389fdb17cec8f33d9c8e25b1d28093937b4431895e8"
}, },
"ponytail": { "ponytail": {
"source": "DietrichGebert/ponytail", "source": "DietrichGebert/ponytail",
"sourceType": "github", "sourceType": "github",
"skillPath": "skills/ponytail/SKILL.md", "skillPath": "skills/ponytail/SKILL.md",
"computedHash": "061f887d9e3e735f85db3be3655a8936a00f24d5fb65ac4ab74a968a770fe342" "computedHash": "210fa0831f649ce9f8a305f7b7c8cf2a0dd09ac4dd53067109941b96e7811d4e"
}, },
"ponytail-audit": { "ponytail-audit": {
"source": "DietrichGebert/ponytail", "source": "DietrichGebert/ponytail",
"sourceType": "github", "sourceType": "github",
"skillPath": "skills/ponytail-audit/SKILL.md", "skillPath": "skills/ponytail-audit/SKILL.md",
"computedHash": "1d718f893dabc01caad0dacef86444b5b4b3e1e59409527d66e8b5dd0cd85ae0" "computedHash": "8b8d72e6ff70b65b6c9518d470fcf29dc696eea93d28f3efa10fdcc60cbd696d"
}, },
"ponytail-debt": { "ponytail-debt": {
"source": "DietrichGebert/ponytail", "source": "DietrichGebert/ponytail",
"sourceType": "github", "sourceType": "github",
"skillPath": "skills/ponytail-debt/SKILL.md", "skillPath": "skills/ponytail-debt/SKILL.md",
"computedHash": "f5d1d2ab891ff0a650a90b67235e487828e7d2a189de08a1ae073032de379407" "computedHash": "0099b82174e3a4261828b4ec04d2c068b1b294fdab14f76701c2acd1f7c99539"
}, },
"ponytail-gain": { "ponytail-gain": {
"source": "DietrichGebert/ponytail", "source": "DietrichGebert/ponytail",
"sourceType": "github", "sourceType": "github",
"skillPath": "skills/ponytail-gain/SKILL.md", "skillPath": "skills/ponytail-gain/SKILL.md",
"computedHash": "fca2297cf7b5fd26692f6bd94d398b522e19fa81ad6e2aaebf1933575f7a6803" "computedHash": "c45ecba7906960eea904218b623abf097ebff84ecbacca6d68da1d0687435f75"
}, },
"ponytail-help": { "ponytail-help": {
"source": "DietrichGebert/ponytail", "source": "DietrichGebert/ponytail",
"sourceType": "github", "sourceType": "github",
"skillPath": "skills/ponytail-help/SKILL.md", "skillPath": "skills/ponytail-help/SKILL.md",
"computedHash": "4582025c3923072fb6c35b8cbd75adf4f7ce47df0eff43cc9a2a39808537fd61" "computedHash": "10e8382b1033c9a0fec0073de6d3de3846b4f5dca7d78b6df2d476a1f4cd43c6"
}, },
"ponytail-review": { "ponytail-review": {
"source": "DietrichGebert/ponytail", "source": "DietrichGebert/ponytail",
"sourceType": "github", "sourceType": "github",
"skillPath": "skills/ponytail-review/SKILL.md", "skillPath": "skills/ponytail-review/SKILL.md",
"computedHash": "800dc0c86402978e12359f701b9e0c81ba4607a0cd0027f4596d0f835a67eadc" "computedHash": "4646594fcd75455533f7ff1425f4f1c5ba0fa65f88241bf262b1c4fc275f049a"
} }
} }
} }
@@ -1,10 +1,11 @@
--- ---
name: kvb-think-listen name: active-listening
description: > description: >
Ask focused questions until every requirement is pinned down, then reflect Gather every requirement before acting. Trigger at the start of any task
the task back. Trigger at the start of any task with unclear scope, whose scope, constraints, or success criteria are not fully pinned down, and
constraints or success criteria, and when another skill needs an whenever another skill needs an operator-only fact — ask focused questions
operator-only fact. until nothing needed is still open, then reflect the understanding back.
Portable across projects.
--- ---
Do not act on a half-understood request. Listen actively until you could state Do not act on a half-understood request. Listen actively until you could state
@@ -1,14 +1,15 @@
--- ---
name: kvb-skills-auto name: autoskill
description: > description: >
Turn operator repetition into a skill. Trigger when the operator repeats an Detect operator repetition and turn it into a skill. Trigger when the
instruction, correction or prompt pattern a third time in a conversation, or operator gives an instruction, correction, or prompt pattern for roughly
says they keep repeating themselves, and the pattern is reusable. the third time in a conversation (or says they keep repeating themselves),
and the pattern is general enough to reuse. Portable across projects.
--- ---
Its objective is the `kvb-skills-tune` skill's **Objective** section: a skill is worth Its objective is the `optimize` skill's: a skill is worth creating only when it
creating only when it cuts tokens or time and raises quality; judge every cuts tokens or time and raises quality — judge every candidate against that
candidate against its three axes and rules. skill's three axes and rules.
When you notice the operator repeating the same kind of instruction, When you notice the operator repeating the same kind of instruction,
correction, or prompt pattern (about three occurrences, exact wording may correction, or prompt pattern (about three occurrences, exact wording may
@@ -19,9 +20,9 @@ vary), do the following:
a skill name suggestion, the trigger phrasing, and what the skill would a skill name suggestion, the trigger phrasing, and what the skill would
do. Ask whether to create it. do. Ask whether to create it.
3. On approval, write the skill: 3. On approval, write the skill:
- a conversation shortcut or expansion: add it to the `kvb-skills-shortcuts` skill - a conversation shortcut or expansion: add it to the `shortcuts` skill
table instead of creating a new skill. table instead of creating a new skill.
- anything else: persist it exactly as the `kvb-skills-tune` skill's - anything else: persist it exactly as the `autotune` skill's
**Persistence** section specifies — locate the operator's skills **Persistence** section specifies — locate the operator's skills
repository, write `skills/<name>/SKILL.md` there (or in the current repository, write `skills/<name>/SKILL.md` there (or in the current
project for project-specific behavior), then mirror the skill directory project for project-specific behavior), then mirror the skill directory
@@ -1,49 +1,25 @@
--- ---
name: kvb-skills-tune name: autotune
description: > description: >
Generate new skills or optimize existing ones (fewer tokens, fewer errors, Generate new agent skills and optimize existing ones so the agent works more
persistent learnings), each proposal confirmed by single-select question. efficiently: fewer input and output tokens, fewer errors, more focus, and
Trigger ONLY on an explicit request: /kvb-skills-tune, "autotune", "tune the learnings that persist across sessions. Every proposal is confirmed with the
skills"; never on your own initiative. operator through single-select (radio button) questions. Trigger ONLY on an
explicit operator request (/autotune, "autotune", "tune the skills"); never
run it on your own initiative. Portable across projects.
--- ---
Autotune turns observed friction into durable skills. It runs on demand only, Autotune turns observed friction into durable skills. It runs on demand only,
it changes nothing without an explicit answer from the operator, and every it changes nothing without an explicit answer from the operator, and every
skill it writes or rewrites lands in the operator's skills repository. skill it writes or rewrites lands in the operator's skills repository.
## Objective Its objective is the `optimize` skill's: every proposal must cut tokens or time
and raise quality — follow that skill's three axes and rules, and drop any
The shared objective of the self-improving skills (kvb-skills-tune, candidate that moves none of them.
kvb-skills-auto, kvb-check-confidence): every skill, rule, or report the agent produces about its own
working is judged on three axes at once. A proposal that cannot name the axis
it moves is not an optimization, it is a preference.
- **Tokens** — fewer input and output tokens for the same result: read the range
instead of the whole file, grep the saved log instead of re-running the
command, route to an authoritative doc instead of duplicating it, report in
lines instead of paragraphs.
- **Time** — fewer round trips and less waiting: independent calls in parallel,
one command that answers the question instead of three that circle it, no
polling for work the harness will report on its own.
- **Quality** — fewer errors and less rework: verification instead of
assumption, and learnings that persist so the same mistake is not paid for
twice.
- A change must improve at least one axis and damage none. Output that got
cheaper but lost correctness is a regression, not an optimization.
- Quality is never the currency: validation, error handling, tests, and security
steps are not what gets cut to save tokens or time.
- Measure, do not guess: name the observed friction — the re-read, the retry,
the correction the operator had to give twice — and what it cost. A guessed
percentage is not a measurement.
- The cheapest step is the one that does not run: drop work before optimizing
it.
- Persist it or pay again: an optimization that lives only in this session is
re-derived in the next one, so it belongs in a skill or a memory entry.
## Trigger discipline ## Trigger discipline
- Run **only** when the operator asks: `/kvb-skills-tune`, "autotune", "tune the - Run **only** when the operator asks: `/autotune`, "autotune", "tune the
skills", or an equivalent instruction. skills", or an equivalent instruction.
- The 60-minute reminder hook is a hint for the operator, not a trigger. When it - The 60-minute reminder hook is a hint for the operator, not a trigger. When it
fires, print the hint and continue the operator's actual request. fires, print the hint and continue the operator's actual request.
@@ -75,14 +51,14 @@ option, and never write a file before the answers are in.
Ask in this order, at most four questions per call: Ask in this order, at most four questions per call:
1. **Scope** - "What should kvb-skills-tune do this run?" Options: `Create a new 1. **Scope** - "What should autotune do this run?" Options: `Create a new
skill`, `Optimize an existing skill`, `Both`, plus the ranked findings if skill`, `Optimize an existing skill`, `Both`, plus the ranked findings if
more than one candidate exists. more than one candidate exists.
2. **Target** - which finding becomes a skill, or which existing skill gets 2. **Target** - which finding becomes a skill, or which existing skill gets
optimized. One option per candidate, each with its measured cost in the optimized. One option per candidate, each with its measured cost in the
description. description.
3. **Shape** - for a new skill: `Standalone skill`, `Row in the 3. **Shape** - for a new skill: `Standalone skill`, `Row in the shortcuts
kvb-skills-shortcuts skill`, `Extend an existing skill`, `Memory entry instead of a skill`. skill`, `Extend an existing skill`, `Memory entry instead of a skill`.
For an optimization: `Tighten wording only`, `Change the trigger`, `Add For an optimization: `Tighten wording only`, `Change the trigger`, `Add
rules`, `Split into two skills`, `Merge into another skill`. rules`, `Split into two skills`, `Merge into another skill`.
4. **Placement** - `Operator skills repository (portable)` or 4. **Placement** - `Operator skills repository (portable)` or
@@ -129,20 +105,16 @@ reverse — remove the mirrors too, or the old name keeps firing.
behaviour. Route to an authoritative doc instead of duplicating it. behaviour. Route to an authoritative doc instead of duplicating it.
- Optimizing an existing skill means the file gets shorter or sharper, not - Optimizing an existing skill means the file gets shorter or sharper, not
longer. Delete rules the agent already follows, merge duplicated ones, and longer. Delete rules the agent already follows, merge duplicated ones, and
cut every sentence that does not change an action. Route that compaction cut every sentence that does not change an action.
through the `kvb-skills-essence` skill instead of trimming by hand: it runs a health
pass for dead pointers and rules duplicated across skills before cutting
anything, ranks by what loads in every session, and shows the diff before it
writes. It asks for its own scope — answer with the skill being optimized.
- Report what changed in at most three lines, then refresh the reminder stamp - Report what changed in at most three lines, then refresh the reminder stamp
so the next hint is an hour out: `date +%s > ~/.claude/.autotune-stamp`. so the next hint is an hour out: `date +%s > ~/.claude/.autotune-stamp`.
## Rules ## Rules
- One skill per run unless the operator picked `Both`; a broad rewrite of the - One skill per run unless the operator picked `Both`; a broad rewrite of the
whole collection is not what kvb-skills-tune is for. whole collection is not what autotune is for.
- Never write a skill for one-off work, for secrets, or for behaviour an - Never write a skill for one-off work, for secrets, or for behaviour an
existing skill already covers - propose extending that skill instead. existing skill already covers - propose extending that skill instead.
- The **Objective** section decides what counts as an improvement: at least - The `optimize` skill's rules decide what counts as an improvement: at least
one axis better, none damaged, and the claim named as observed friction rather one axis better, none damaged, and the claim named as observed friction rather
than a guessed percentage. than a guessed percentage.
@@ -1,7 +1,7 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# UserPromptSubmit hook: emit the kvb-skills-tune hint at most once per interval. # UserPromptSubmit hook: emit the autotune hint at most once per interval.
# Stdout lands in the agent's context, which is why the payload tells the agent # Stdout lands in the agent's context, which is why the payload tells the agent
# to show the hint and keep going instead of running kvb-skills-tune by itself. # to show the hint and keep going instead of running autotune by itself.
set -euo pipefail set -euo pipefail
interval="${AUTOTUNE_INTERVAL_SECONDS:-3600}" interval="${AUTOTUNE_INTERVAL_SECONDS:-3600}"
@@ -24,9 +24,9 @@ printf '%s\n' "${now}" >"${stamp}"
cat <<'HINT' cat <<'HINT'
AUTOTUNE HINT: over an hour since the last hint. Show the operator this notice AUTOTUNE HINT: over an hour since the last hint. Show the operator this notice
verbatim as a short block, then continue with their request. Do NOT run verbatim as a short block, then continue with their request. Do NOT run
kvb-skills-tune, do not propose skills, do not ask questions about it. autotune, do not propose skills, do not ask questions about it.
💡 /kvb-skills-tune - generate a new skill or optimize an existing one 💡 /autotune - generate a new skill or optimize an existing one
(fewer tokens, fewer errors, more focus, learnings that persist). (fewer tokens, fewer errors, more focus, learnings that persist).
Runs only when you trigger it. Runs only when you trigger it.
HINT HINT
+33
View File
@@ -0,0 +1,33 @@
---
name: comments-clean
description: >
Purge explanatory comments from staged and unstaged changes. Run
AUTOMATICALLY, without being asked, every time the operator requests a
commit or stage in any wording (commit, commit no-verify, stage next,
socon, sobu, socobu, "commite ..."), BEFORE executing that request; also
on /comments-clean or when the operator complains about comments.
Explanatory comments are bad practice: they narrate code instead of
letting it speak. Portable across projects.
---
When the operator asks to commit or stage, run this audit first and only
then perform the requested git action; never ask whether to run it.
Audit every staged and unstaged change (`git diff` plus `git diff --cached`,
added lines only) and delete every explanatory comment:
1. Delete: restated code, step narration, section banners, "Note that",
rationale prose, apology comments, and any comment whose information the
code, task name, or commit message already carries. When in doubt,
delete.
2. Keep only: parameter/exit-code docs, nocheck/noqa/shellcheck directives,
and project-sanctioned exception markers that name a concrete trip-wire.
A relabeled narration is not an exception; delete it instead.
3. Re-run the project's comment lint if one exists and re-stage the files
that were already staged.
4. Penalty: if this audit finds anything you wrote yourself, admit it in
the report (file and count, no excuses), lower your confidence numbers
for the affected bundle, and write the incident to persistent agent
memory (extend the existing minimal-comments feedback entry with the
date and the pattern you slipped on) so the calibration survives the
session.
+20
View File
@@ -0,0 +1,20 @@
---
name: commit-next
description: >
Commit the staged bundle with --no-verify, then stage the next logical
bundle and brief the operator on it. Trigger on /commit-next or when the
operator asks to commit and stage the next bundle in one step. Portable
across projects.
---
1. Run the commit-no-verify skill on the currently staged changes
(comments-clean audit first, then `git commit --no-verify`, exact
staging index only).
2. Group the remaining uncommitted changes into logical bundles (one
concern per bundle: same failure class, same mechanism, or same role)
and stage the next one; never mix concerns to save a commit.
3. For the newly staged bundle run the explain skill (what the changes do
and how the pieces interact, file:line-anchored) followed by the
confidence skill (fix confidence, break risk, evidence, residuals).
4. Close with what remains unstaged and wait for the operator's commit
decision; never auto-commit the newly staged bundle.
+15
View File
@@ -0,0 +1,15 @@
---
name: commit-no-verify
description: >
Commit the currently staged changes with --no-verify (skip the pre-commit
hooks). Trigger on /commit-no-verify or when the operator asks for a
no-verify commit in any wording. Invoking this skill IS the explicit
per-commit authorization that --no-verify requires; it never carries over
to later commits. Portable across projects.
---
1. Run the comments-clean audit over the staged and unstaged changes first.
2. Commit exactly the staging index with `git commit --no-verify` and a
Conventional-Commits message derived from the staged diff; never add
unstaged files, never push.
3. Report the created commit hash and what remains uncommitted.
@@ -1,21 +1,18 @@
--- ---
name: kvb-check-confidence name: confidence
description: > description: >
Report calibrated fix confidence and break risk unprompted whenever you Report calibrated confidence unprompted. Trigger whenever you stage
stage changes, declare a fix or bundle ready, or summarize work the operator changes, declare a fix or bundle ready, or summarize completed work the
may commit or deploy. operator may commit or deploy — the operator must never have to ask "how
sure are you?". Portable across projects.
--- ---
Every stage summary, fix report, or ready-to-commit bundle ends with a Every stage summary, fix report, or ready-to-commit bundle ends with a
confidence block. confidence block:
Invoke the `kvb-check-break-risk` skill every time, before writing the block; this skill
reports the number it derives and never estimates one itself.
1. **Fix confidence** (percent): probability the change does what it 1. **Fix confidence** (percent): probability the change does what it
claims. **Break risk** (percent): probability it breaks anything else, claims. **Break risk** (percent): probability it breaks anything else.
derived via the `kvb-check-break-risk` skill. Two separate numbers, never one Two separate numbers, never one blended figure.
blended figure.
2. **Evidence**: the strongest proof lines only — what was executed, 2. **Evidence**: the strongest proof lines only — what was executed,
reproduced, enumerated, or read from authoritative source. Distinguish reproduced, enumerated, or read from authoritative source. Distinguish
proven (ran it, saw it) from derived (reasoned statically). proven (ran it, saw it) from derived (reasoned statically).
@@ -24,7 +21,6 @@ reports the number it derives and never estimates one itself.
Never a bare percentage without its residuals. Never a bare percentage without its residuals.
Calibration rules: Calibration rules:
- Number the claim you verified, not the effort you spent; discount after - Number the claim you verified, not the effort you spent; discount after
any error the operator caught in the same area. any error the operator caught in the same area.
- Distinguish "fixes the observed failure" from "makes the job green" when - Distinguish "fixes the observed failure" from "makes the job green" when
@@ -32,9 +28,8 @@ Calibration rules:
- Static analysis alone caps at the low nineties; only executed evidence - Static analysis alone caps at the low nineties; only executed evidence
(test, reproduction, live probe) justifies more. (test, reproduction, live probe) justifies more.
A threshold the operator named ("95% confidence, break risk ≤5%") is a gate on Objective: the `optimize` skill's. The number exists to save the operator a
the work, handled as the `kvb-check-break-risk` skill's last section says, for both verification round trip (time), to stop a rework cycle before it starts
numbers. (quality), and to replace a long hedging paragraph with two figures and their
residuals (tokens). A confidence block that costs more than it saves — padding,
Objective: the `kvb-skills-tune` skill's **Objective** section. Two figures with their residuals replace a repeated caveats, a number without residuals — misses its own objective.
hedging paragraph and save the operator a verification round trip.
@@ -1,10 +1,11 @@
--- ---
name: kvb-think-dialectic name: dialectic
description: > description: >
Reach a defensible conclusion by thesis, independent skeptic antithesis and Reach a defensible conclusion through a dialectical process — thesis,
synthesis, iterated to ~99% confidence. Trigger for any consequential root adversarial antithesis, synthesis — iterated until one thesis survives every
cause, design or decision where being wrong is expensive and the answer is attack at ~99% confidence. Trigger for any consequential root-cause, design,
uncertain. or decision where being wrong is expensive and the answer is not yet certain.
Deep-inspects every accessible source. Portable across projects.
--- ---
Reach the truth by dialectic, not by first guess. Do not stop at a plausible Reach the truth by dialectic, not by first guess. Do not stop at a plausible
@@ -12,16 +13,12 @@ answer; drive the loop until a thesis survives every attack at ~99% confidence.
## Before you start: listen ## Before you start: listen
Invoke the `kvb-think-listen` skill first. Do not begin the dialectic until you Invoke the `active-listening` skill first. Do not begin the dialectic until you
have gathered every requirement, constraint, and success criterion the task have gathered every requirement, constraint, and success criterion the task
needs. A dialectic run on the wrong question wastes the whole loop. Return to needs. A dialectic run on the wrong question wastes the whole loop. Return to
`kvb-think-listen` mid-loop whenever an objection turns on a fact only the `active-listening` mid-loop whenever an objection turns on a fact only the
operator holds. operator holds.
When another skill or an autonomous run (kvb-org-robot, kvb-ci-observe, a loop) invoked this
skill, derive the requirements from the task and the evidence instead of
asking; ask only when an operator-only fact blocks the conclusion.
## The loop ## The loop
1. **Thesis.** Form the strongest initial claim you can, grounded in evidence, 1. **Thesis.** Form the strongest initial claim you can, grounded in evidence,
@@ -30,8 +27,7 @@ asking; ask only when an operator-only fact blocks the conclusion.
make or CLI helpers the project exposes); CI logs and artifacts; tests; docs. make or CLI helpers the project exposes); CI logs and artifacts; tests; docs.
Cite `file:line` and command output — never a bare assertion. Cite `file:line` and command output — never a bare assertion.
2. **Antithesis.** Spawn MULTIPLE independent skeptics (parallel subagents or a 2. **Antithesis.** Spawn MULTIPLE independent skeptics (parallel subagents or a
workflow, on the default model as the `kvb-org-perform` skill requires) workflow) whose only job is to REFUTE the thesis. Each re-inspects the
whose only job is to REFUTE the thesis. Each re-inspects the
sources itself and defaults to "refuted" on any concrete hole. Give diverse sources itself and defaults to "refuted" on any concrete hole. Give diverse
skeptics diverse lenses: correctness, an alternate mechanism, does-it- skeptics diverse lenses: correctness, an alternate mechanism, does-it-
reproduce, does-the-fix-break-something-else. reproduce, does-the-fix-break-something-else.
@@ -41,7 +37,7 @@ asking; ask only when an operator-only fact blocks the conclusion.
4. **Iterate.** Repeat thesis → antithesis → synthesis until a full skeptic 4. **Iterate.** Repeat thesis → antithesis → synthesis until a full skeptic
round finds no new hole and the thesis holds at ~99%. If confidence stalls round finds no new hole and the thesis holds at ~99%. If confidence stalls
below 99%, name the exact residual and the evidence that would close it, then below 99%, name the exact residual and the evidence that would close it, then
go get that evidence (more inspection, or `kvb-think-listen` for an go get that evidence (more inspection, or `active-listening` for an
operator-only fact) and loop again. operator-only fact) and loop again.
## Rules ## Rules
@@ -1,10 +1,16 @@
--- ---
name: kvb-skills-dream name: dream
description: > description: >
Consolidate the last 24 hours of work in this repository (what held, what Consolidate the last 24 hours of work in the current repository: what the
broke) into memory and improvement proposals. agent did, where it was right, where it was wrong, and which hypotheses
Trigger on /kvb-skills-dream or a request to reflect on or learn from recent follow. Every hypothesis is hardened with the dialectic skill, the survivors
work. are written to memory, the derived improvements to the repository's code and
to its agent instructions (CLAUDE.md, AGENTS.md) are offered as single-select
questions, and the run ends in autotune and autoskill. The objective is to
raise the confidence of future runs until those mistakes stop recurring.
Trigger on /dream, or
when the operator asks to reflect on, consolidate, or learn from recent work.
Portable across projects.
--- ---
Sleep on the work: replay what happened in this repository, separate what held Sleep on the work: replay what happened in this repository, separate what held
@@ -24,13 +30,13 @@ A class of error counts as closed only when something structural prevents it —
a test, a lint rule, a CI check, a hook, a written rule in the agent a test, a lint rule, a CI check, a hook, a written rule in the agent
instructions, or a skill. "Be more careful next time" closes nothing. Report the instructions, or a skill. "Be more careful next time" closes nothing. Report the
confidence the next run can now hold on each closed class, calibrated as the confidence the next run can now hold on each closed class, calibrated as the
`kvb-check-confidence` skill requires, and name what is still open. `confidence` skill requires, and name what is still open.
## Scope ## Scope
Everything is read from the repository the skill runs in (`git rev-parse Everything is read from the repository the skill runs in (`git rev-parse
--show-toplevel`); never mix in another project's history or memory. The default --show-toplevel`); never mix in another project's history or memory. The default
window is the last 24 hours — the operator may widen it (`/kvb-skills-dream 7d`). State the window is the last 24 hours — the operator may widen it (`/dream 7d`). State the
repository and the window in one line before you start. repository and the window in one line before you start.
Run only when the operator asks. The 12-hour reminder hook Run only when the operator asks. The 12-hour reminder hook
@@ -48,9 +54,6 @@ Gather the window's actual record, not your recollection of it:
logs of the red ones — the failing log line, not the job title. logs of the red ones — the failing log line, not the job title.
- **Reviews**: pull requests and issues touched in the window, and the review - **Reviews**: pull requests and issues touched in the window, and the review
comments on them. comments on them.
- **Register**: the `kvb-org-rasci` register: `rasci.py tools` ranks the
scripts and tools the window's tasks ran, the refactoring candidates;
`get <task>` shows a task's objective, roles, workdir and artifacts.
- **Memory**: this project's memory directory and its `MEMORY.md` index — the - **Memory**: this project's memory directory and its `MEMORY.md` index — the
findings already recorded, so the dream extends them instead of repeating them. findings already recorded, so the dream extends them instead of repeating them.
- **Instructions**: `CLAUDE.md`, `AGENTS.md`, and the project's own skills, to - **Instructions**: `CLAUDE.md`, `AGENTS.md`, and the project's own skills, to
@@ -80,7 +83,7 @@ that nothing could disprove is not a hypothesis — cut it.
## Step 4: dialectic ## Step 4: dialectic
Put every surviving hypothesis through the `kvb-think-dialectic` skill: thesis, Put every surviving hypothesis through the `dialectic` skill: thesis,
independent skeptics, synthesis, iterated to ~99%. What survives becomes a independent skeptics, synthesis, iterated to ~99%. What survives becomes a
finding. What gets refuted is recorded as discarded **with the refutation**, so finding. What gets refuted is recorded as discarded **with the refutation**, so
the next dream does not re-derive it. the next dream does not re-derive it.
@@ -109,15 +112,15 @@ as radio buttons: the improvement, the evidence behind it, and the options
`Apply now`, `Record as a task`, `Skip`. Ranked by evidence, one question per `Apply now`, `Record as a task`, `Skip`. Ranked by evidence, one question per
improvement. No repository file changes before an answer comes back. improvement. No repository file changes before an answer comes back.
## Step 7: kvb-skills-tune, then kvb-skills-auto ## Step 7: autotune, then autoskill
With the dialectic done and memory written, run `kvb-skills-tune` — hand it this run's With the dialectic done and memory written, run `autotune` — hand it this run's
findings as its evidence so it does not re-collect them — and then `kvb-skills-auto` findings as its evidence so it does not re-collect them — and then `autoskill`
for the operator corrections the window revealed. Both keep their own for the operator corrections the window revealed. Both keep their own
confirmation steps; the dream never writes a skill on their behalf. confirmation steps; the dream never writes a skill on their behalf.
Close the run by refreshing this repository's reminder stamp, so the next hint Close the run by refreshing this repository's reminder stamp, so the next hint
is twelve hours out: `date +%s > "$(git rev-parse --path-format=absolute --git-common-dir)/.dream-stamp"`. is twelve hours out: `date +%s > "$(git rev-parse --absolute-git-dir)/.dream-stamp"`.
## Rules ## Rules
@@ -1,16 +1,16 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# UserPromptSubmit hook: emit the kvb-skills-dream hint at most once per interval and per # UserPromptSubmit hook: emit the dream hint at most once per interval and per
# repository. The stamp lives in the repository's git dir, so every repository # repository. The stamp lives in the repository's git dir, so every repository
# keeps its own cadence; outside a git repository the hook stays silent. # keeps its own cadence; outside a git repository the hook stays silent.
# Stdout lands in the agent's context, which is why the payload tells the agent # Stdout lands in the agent's context, which is why the payload tells the agent
# to show the hint and keep going instead of running kvb-skills-dream by itself. # to show the hint and keep going instead of running dream by itself.
set -euo pipefail set -euo pipefail
interval="${DREAM_INTERVAL_SECONDS:-43200}" interval="${DREAM_INTERVAL_SECONDS:-43200}"
if [[ -n "${DREAM_STAMP:-}" ]]; then if [[ -n "${DREAM_STAMP:-}" ]]; then
stamp="${DREAM_STAMP}" stamp="${DREAM_STAMP}"
else else
git_dir="$(git rev-parse --path-format=absolute --git-common-dir 2>/dev/null)" || exit 0 git_dir="$(git rev-parse --absolute-git-dir 2>/dev/null)" || exit 0
stamp="${git_dir}/.dream-stamp" stamp="${git_dir}/.dream-stamp"
fi fi
@@ -31,11 +31,10 @@ printf '%s\n' "${now}" >"${stamp}"
cat <<'HINT' cat <<'HINT'
DREAM HINT: over twelve hours since the last hint for this repository. Show the DREAM HINT: over twelve hours since the last hint for this repository. Show the
operator this notice verbatim as a short block, then continue with their operator this notice verbatim as a short block, then continue with their
request. Do NOT run kvb-skills-dream, do not start replaying the history, do request. Do NOT run dream, do not start replaying the history, do not ask
not ask questions about it. questions about it.
💤 /kvb-skills-dream - consolidate the last 24h in this repository: what 💤 /dream - consolidate the last 24h in this repository: what held, what
held, what broke, which hypotheses follow, into memory and improvement broke, which hypotheses follow, into memory and improvement proposals.
proposals.
Runs only when you trigger it. Runs only when you trigger it.
HINT HINT
@@ -1,9 +1,10 @@
--- ---
name: kvb-think-explain name: explain
description: > description: >
Explain code and its interactions. Trigger when the operator asks what code Explain code and its interactions. Trigger when the operator asks what a
does, how a mechanism works, why components interact, or invokes /kvb-think-explain on piece of code does, how a mechanism works, why components interact the way
a file, function, role or module. they do, or invokes /explain on a file, function, role, or module.
Portable across projects.
--- ---
Explain the named code so an engineer who has never seen it can follow it: Explain the named code so an engineer who has never seen it can follow it:
@@ -1,9 +1,10 @@
--- ---
name: kvb-help name: help-skills
description: > description: >
List and explain every skill in the session. Trigger on /kvb-help, List and explain every skill available in the current session. Trigger on
"welche skills gibt es", "skill übersicht", or when the operator asks what /help-skills, "welche skills gibt es", "skill übersicht", or when the
skills exist or what one does. operator asks what skills exist or what a skill does. Portable across
projects.
--- ---
Produce a one-shot overview of every skill the session can invoke: Produce a one-shot overview of every skill the session can invoke:
@@ -17,8 +18,8 @@ Produce a one-shot overview of every skill the session can invoke:
— compress the description, do not paste it. — compress the description, do not paste it.
3. When the operator names a specific skill or topic, explain only the 3. When the operator names a specific skill or topic, explain only the
matching skills in more depth: what they do, when they fire on their matching skills in more depth: what they do, when they fire on their
own, and how they compose (e.g. kvb-git-commit-next → kvb-git-commit-no-verify → own, and how they compose (e.g. commit-next → commit-no-verify →
kvb-code-no-comments). comments-clean).
4. End with the activation hint: project installers or `make install` in 4. End with the activation hint: project installers or `make install` in
the skills repository refresh the live copies; a restart loads renamed the skills repository refresh the live copies; a restart loads renamed
or new skills. or new skills.
-64
View File
@@ -1,64 +0,0 @@
---
name: kvb-check-break-risk
description: >
Derive the break-risk percentage from measurement, not feel. Called by
`kvb-check-confidence` on every report; also when the operator asks "what could this
break?" or names a break-risk threshold.
---
Break risk is the probability the change breaks something it did not intend to
touch. It is not the inverse of fix confidence, and it is not a mood. Derive it
by the steps below; a number produced without them is a guess wearing a
percent sign.
## Fast path
A change whose files nothing builds, deploys, or runs (documentation and
comments only, or tests that ran green with it) has no consumers outside
itself. Name the files (`git diff --cached --name-only`), report break risk
under 1%, and skip the method. Any other file takes the full method.
## The method
1. **Consumers**: enumerate every consumer of everything the change alters, in
the committed state (`git grep <symbol> HEAD`) and of its output; say the
count. A caveat never licenses a low number; an enumeration does.
2. **Delta**: run the old and the new code on today's inputs and compare at
the surface where the difference shows (rendered output, emitted config,
returned set, wire bytes), not as the source reads.
3. **Live or latent**: per finding, whether a shipped configuration reaches it.
Only live findings move the number materially.
4. **Matrix**: a verdict per deployment axis the project has, never averaged.
5. **Failure mode**: fails closed and loud, or open and silent; the latter can
outrank a likelier failure.
6. **Evidence**: executed beats derived; a report, a summary or your own
earlier message is not evidence; negative-control every check you cite,
including one you did not write.
`reference.md` next to this file explains each step with the trap it defeats.
Read it when a step is new to you or its result is contested.
## Producing the number
State it as a percentage next to fix confidence, never blended, and always with
its residuals. Anchor roughly:
- **Under 5%**: consumers enumerated, delta measured at the surface, every axis
of the matrix covered by executed evidence, failure mode closed.
- **5 to 15%**: measured, but one axis or one layer rests on reasoning — no live
run, a mode not exercised, a runtime the render cannot prove.
- **15 to 40%**: a real behaviour change whose consumers are enumerated but
whose effect is unmeasured, or a latent finding with a plausible trigger.
- **Above 40%**: a known live defect, an unenumerated consumer set, or a
fail-open exposure without a verified control. Fix it rather than report it.
Discount yourself after any error the operator caught in the same area during
this session, and say that you are doing so. A region where you have already
been wrong twice deserves a worse number than its paper evidence suggests.
## When the operator names a threshold
A named threshold is a gate on the work, not a field in the report. Missing it
is a work item: name the cheapest evidence that would close the gap, go get it,
re-measure. Report a miss only when the evidence is genuinely out of reach, and
name which piece.
-81
View File
@@ -1,81 +0,0 @@
# Break risk: the method in detail
Each step of the `kvb-check-break-risk` skill, with the trap it defeats.
## The consumer question, first and always
Enumerate every consumer of every thing the change alters, before naming a
number. A written caveat never licenses a low number; an enumeration does.
- Search the **committed** state, not just the working tree. A symbol that looks
unused in your branch may still be called from a file you have not opened
(`git grep <symbol> HEAD`). Removing a function with zero external callers is
a different risk class from removing one with nine.
- Enumerate consumers of the **output**, not only of the code: who reads the
variable, renders the template, parses the emitted block, imports the
constant.
- Say the count out loud. "Two consumers, both inside this change set" is
evidence. "I don't think anything else uses it" is not.
## Measure the delta at the surface that matters
Compare what the old code **actually produced** against what the new code
produces, at the point where the difference becomes observable: rendered output,
emitted config, returned set, wire bytes.
The trap this defeats: reasoning about what the old code *looks like* it did. A
sweep that appeared to forward sixteen ports forwarded **zero** in production,
because the flag it gated on was unrendered template text. Had the delta been
argued from the source rather than measured, the risk would have been sized
against a behaviour that never existed.
Extract the old implementation from version control and run it on today's
inputs. If `DROPPED` is empty, nothing can regress; say so and move on.
## Classify every finding as live or latent
For each way the change could break something, decide which it is and say so:
- **Live**: a shipped configuration, inventory, variant or default reaches it.
- **Latent**: reachable only under a configuration nobody ships today.
Both belong in the report. Only live findings drive the number up materially. A
latent finding with a named trigger is worth more to the operator than a vague
worry, and it is honest about why the number is not zero.
## Walk the deployment matrix
A change verified in one mode is unverified in the others. Enumerate the axes
the project actually has, and state the verdict per cell rather than averaging
them away. Typical axes: deployment mode, node role, topology, distro,
filesystem, single versus multi host.
Two numbers for two modes beats one blended number that is wrong in both.
## Weigh the failure mode, not only the probability
A low-probability failure that **fails open** can outrank a likely one that
fails closed. Ask what the world looks like when the change misbehaves:
- Fails closed and loud: the service refuses to start, a gate goes red, a
request errors. Cheap to detect, bounded blast radius.
- Fails open and silent: a port stays reachable, a check passes vacuously, a
credential keeps working, traffic is forwarded to an unintended place.
Say which one applies. When a design choice is between an unverifiable control
over an exposure and not creating the exposure, prefer the second and say why.
## Evidence rules
- Executed beats derived. Run it, render it, read the output.
- A subagent's report, a conversation summary, or your own earlier message is
**not** evidence. Re-verify the claim yourself before it changes a number.
- Negative control before claiming a check works: make the thing it should
catch, watch it fail, restore. A check never observed failing is not known to
work.
- The same applies to a check you did not write. Citing an existing lint, test
or gate as the reason a change is safe means reading its pattern, not its
docstring, and watching it fail on the case you are trusting it to catch. A
green gate whose rule never matched your case is silence, not a pass.
- After a context compaction, rebuild attributions from the source; conclusions
survive compaction, the evidence under them does not.
-88
View File
@@ -1,88 +0,0 @@
---
name: kvb-check-legal
description: >
Check a commit for legal soundness: licence compatibility of added code and
dependencies, attribution, copied snippets, GDPR, trademarks. Run
AUTOMATICALLY before every commit the operator requests; also on /kvb-check-legal
[ref|range], "rechtlich prüfen", "lizenz prüfen".
---
The stance is the project's counsel: find what would make the change unlawful
to distribute before it is distributed. This is not legal advice but a reading
of the licence texts, with German and EU law where jurisdiction matters; every
report says so in one line. Leaked secrets belong to `kvb-check-security`, engineering
principles to `kvb-check-principles`.
## Target
- Before a commit: the staging index (`git diff --cached`, plus every added or
changed file in full where origin or licence is in question).
- `/kvb-check-legal <ref>` or `/kvb-check-legal <a>..<b>`: that commit or range.
- `/kvb-check-legal` alone: the index if it is non-empty, else `HEAD`.
## Fast exit
The target is in scope only if it adds or changes at least one of: a
dependency manifest or lockfile; a vendored, generated, or third-party file; a
LICENSE, NOTICE, or COPYING file or a licence header; code with a foreign
origin (a source URL, a foreign copyright line, a block that reads unlike its
surroundings); an external request (CDN, web font, analytics, embed, API);
handling of personal data; a logo, icon, or third-party brand name. Nothing in
scope: report `advocate: nothing licence-relevant in <target>` in one line and
let the commit proceed.
## Project licence
Read every declaration: LICENSE/COPYING, the manifest's licence field
(`package.json`, `pyproject.toml`, `Cargo.toml`, `composer.json`, ...), SPDX
headers, the README's licence section. Declarations that disagree are a
finding. A project without any licence is a finding (all rights reserved),
never a blocker of the commit at hand.
## The checks
Only for a target in scope: read `checks.md` next to this file and apply its
seven checks (inbound code, dependency licences, project licence fit,
attribution, copied snippets, data protection, trademarks).
## Severity
- **Blocker** - the commit would distribute what the project may not
distribute: an incompatible inbound or dependency licence, or non-trivial
code or assets without a licence.
- **Finding** - fixable while keeping the change: missing attribution, NOTICE,
or header; a data-protection gap; trademark use; a block of unclear origin.
- **Note** - worth knowing, no action required.
## Blockers stop the commit
Harden every blocker with the `kvb-think-dialectic` skill before reporting it. The
antithesis tries: the code is trivial or the operator's own; the dependency is
build- or test-only and never distributed; an exception applies (linking or
Classpath exception, dual licensing, a licence grant in the issue or commit
history). A blocker that survives stops the commit: present it with the
options (remove, replace with a compatible alternative, relicense, obtain
permission) and wait. The operator's explicit acceptance lets this one commit
proceed and never carries over to the next. Findings and notes never stop a
commit.
## Output
One line with the target, the project licence, and "Keine Rechtsberatung" (in
the operator's language). Then one table, blockers first:
| Severity | Check | Location | What is wrong | Licence or law | Fix | Evidence |
`Evidence` is `proven` (licence text, metadata, or file read; cite path:line
or the registry URL) or `derived` (inferred). Close with the verdict (commit
stops or proceeds) and the `kvb-check-confidence` skill's numbers.
## Rules
- Never infer a licence from a package's name or reputation; read it. An
unreachable registry makes the row `derived`, never clean.
- Never invent a licence clause, a statute, or a court ruling; cite only what
you read.
- Read-only apart from stopping the commit: never edit licence files, add
headers, or remove dependencies on your own. Propose the fix; the operator
decides.
-37
View File
@@ -1,37 +0,0 @@
# Advocate: the checks
Apply each check to a target the `kvb-check-legal` skill found in scope.
1. **Inbound code** - every file or block of foreign origin carries a licence
that the project licence can absorb in the direction of distribution. GPL
code cannot enter a project distributed under MIT without relicensing the
whole; Apache-2.0 cannot enter GPL-2.0-only; code without a licence is all
rights reserved.
2. **Dependency licences** - for each added or bumped dependency, read the
licence of the resolved version (lockfile, registry metadata, or the
package's own LICENSE), including the transitive dependencies the lockfile
newly pulls. Watch copyleft reach (GPL, AGPL's network clause, LGPL static
versus dynamic linking), source-available terms that are not open source
(SSPL, BUSL, Commons Clause, "non-commercial"), and an upstream relicensing
between the old and the new version.
3. **Project licence fit** - the project's declarations still hold against
what checks 1 and 2 found: a dependency that forces copyleft forces either
the project licence or the dependency's removal.
4. **Attribution** - retained copyright notices, the licence text shipped
where required, an Apache-2.0 NOTICE propagated, modified-file notices
(Apache-2.0 §4(b), GPL-3.0 §5(a)), SPDX headers where the project uses them,
credit for CC BY material.
5. **Copied snippets** - Stack Overflow content is CC BY-SA (attribution plus
share-alike); gists, blog posts, other repositories, and verbatim
reproductions of well-known code need a traceable origin and licence.
Trivial code without originality (one-liners, usage dictated by an API) is
not a finding.
6. **Data protection (GDPR, TDDDG)** - new collection, storage, logging, or
transfer of personal data; IP addresses sent to third parties through
external fonts, CDNs, analytics, or embeds; storage on the user's device
without consent (§ 25 TDDDG); transfers outside the EU. Name the data flow
and the missing legal basis or consent mechanism.
7. **Trademarks and branding** - third-party logos, icons, and product names
used beyond plain reference: implied endorsement, use in the project's own
name, modified logos. Icon sets carry their own licence, and a free licence
on an icon does not license the trademark it depicts.
-86
View File
@@ -1,86 +0,0 @@
---
name: kvb-check-principles
description: >
Analyse a repository, path or diff for violated principles: SPOT, DRY, KISS,
SRP, security at trust boundaries. Trigger on /kvb-check-principles, "sei
skeptisch", "kritische analyse", "review the architecture", or "what is
wrong with the code".
---
The stance is adversarial: the code is guilty until its structure proves
otherwise. This skill hunts violated principles, not bugs (`code-review`),
not bloat (`ponytail-audit`), not leaked secrets (`kvb-check-security`). Read-only —
report the findings, change nothing unless the operator asks afterwards.
## Scope
The operator names the target: a repository, a directory, a role, or a diff.
Without a target, take the whole repository and rank by blast radius. State
the scope in one line before reporting, and name explicitly what you did not
read — an audit that silently covered half the tree reads as clean.
## The lenses
Apply each independently; a finding belongs to exactly one:
1. **SPOT** — one truth, one place. A value, a version, a hostname, a
schema, a list of names that exists twice is a divergence waiting to
happen. Name both locations and the drift they will cause.
2. **DRY** — repeated *knowledge*, not repeated text. Two blocks that look
alike but change for different reasons are correctly separate; merging
them is the violation. Prove the shared reason to change, or drop the
finding.
3. **SRP** — one reason to change per unit. Look for the module that both
parses and persists, the function whose name contains "and", the role
that configures and deploys and monitors. The test: name the two
stakeholders who would each demand a change here.
4. **KISS** — mechanism the problem never asked for: an abstraction with one
implementation, a config value that never varies, a cache with no
measured pressure, a retry loop around a deterministic call.
5. **Security** — only at trust boundaries: unvalidated input crossing into
a query, a shell, a template, or a path; authorization checked in the
caller instead of the resource; a secret in a default, a log, or an
image layer; a permission wider than the operation needs. Defensive
findings only.
6. **Silent defaults** — a fallback that substitutes a guess for a value the
caller failed to supply, turning a misconfiguration into wrong behaviour
instead of a loud failure. See the `kvb-code-no-defaults` skill.
## Method
- **Evidence before verdict.** Every finding carries `file:line` you actually
read. Grep for the sibling occurrences before calling something a
duplicate — one hit is not a pattern.
- **Name the failure.** A finding must state what concretely goes wrong:
which value drifts, which caller breaks, which input reaches which sink.
A finding whose worst case is "feels wrong" gets deleted, not softened.
- **Attack your own finding.** For anything consequential or expensive to
fix, run it through the `kvb-think-dialectic` skill before reporting it — the
strongest counter-argument is usually "these two are not the same
knowledge" or "this boundary is already checked upstream".
- **Respect intent.** A deliberate marker (`ponytail:`, a documented
exception, a nocheck directive with a reason) is a known ceiling, not a
finding. Report it only when the ceiling has since been exceeded.
## Output
One ranked table, worst blast radius first:
| Severity | Principle | Location | What is wrong | Smallest fix |
Then three closing lines at most: what the scope excluded, which findings
survived `kvb-think-dialectic` versus which are still derived-only, and the single
finding to fix first. No per-finding paragraphs — the operator asks when a
row needs unpacking.
## Rules
- Never report formatting, naming taste, or missing comments; a linter owns
those and the operator did not ask.
- Distinguish proven (grepped, read, reproduced) from derived (reasoned
statically) per finding, and close with the `kvb-check-confidence` skill's numbers
for the audit as a whole.
- No silent caps: if you ranked and cut, say how many findings were dropped
and by what rule.
- Skepticism ends at the report. Do not fix, stage, or refactor as part of
this skill.
-78
View File
@@ -1,78 +0,0 @@
---
name: kvb-ci-artifacts
description: >
Get the evidence out of a CI run: failed job logs (also mid-run) clustered
by error line, and the artifacts that hold the cause. Trigger whenever a CI
failure must be explained, and from triage.
---
A job log tells you what broke. An artifact tells you why. Investigating a CI
failure without downloading its artifacts is guessing with extra steps.
## Getting the log
For a GitHub Actions run, one call does it:
${CLAUDE_SKILL_DIR}/scripts/ci_failures.py <RUN_ID> [--repo <owner>/<repo>] [--artifacts]
It prints how far HEAD moved since the run's commit, saves every finished
failed job's log to `/tmp/ci-logs-<run>/` (colour stripped, even while the run
is still in progress), and clusters the jobs by the normalised error line that
ended them. Jobs still running are listed, not fetched: their API log can stop hours
early. The clustering is a heuristic; check each boundary against the logs.
Grep the saved files repeatedly. Re-fetching a log to find the line you just
lost costs minutes; grepping the saved copy costs milliseconds.
## Getting the artifacts
gh api /repos/<owner>/<repo>/actions/runs/<RUN_ID>/artifacts --paginate \
--jq '.artifacts[] | "\(.name)\t\(.size_in_bytes)"'
gh run download <RUN_ID> --repo <owner>/<repo> -n <NAME> -D <DIR>
Choose by content, not by name length:
- **Rescue / diagnostics bundles** carry the reverse-proxy access and error log,
the per-service and per-container logs, the journal, and the event stream.
This is where causes live. Prefer them.
- **Test-report bundles** (Playwright and friends) are mostly video and traces
and can run to hundreds of megabytes for one assertion string. Pull one only
when you need the assertion text or the error-context file.
- **Inventory / runtime dumps** settle "what was actually deployed" questions.
Inside a rescue bundle, read in this order: the reverse-proxy log (it shows
status code, upstream, and time-to-response for the exact failing request), the
application container log, then the journal and event stream for restarts and
OOM kills.
An access-log line is a timing witness. A `499` means the *client* gave up; the
server was still working. A `504` or an `upstream timed out ... while reading
upstream` means your backend never answered. `ReqTime` versus `UpstreamTime`
tells you whether the proxy or the upstream burned the seconds.
## Grep hygiene
Two failure modes turn a search into a false conclusion:
- **Clustered flags silently change the pattern.** Write them separately —
`grep -r -a -i -l -F "needle" dir/`, not `-ariel`. A cluster where one letter
takes an argument consumes your pattern and searches for something else, and
the run still exits successfully.
- **Substring matches lie.** A protocol token like `h2c` matches inside
`veth2cb3f8c`; a short id matches inside a hash. Before concluding anything
from a hit, print its context (`grep -o ".\{30\}needle.\{30\}"`) and look at
it. Before concluding anything from a *miss*, prove the command was
well-formed by searching for a string you know is present.
Binary-ish logs need `-a`, or grep reports "binary file matches" and you see
nothing.
## Rules
- Download before theorising. Do it even when the job log looks conclusive: a
log that explains the symptom rarely explains the cause.
- Quote literally, with the file path and line number. Never paraphrase a log
line you did not read, and never reconstruct one from memory.
- A negative result is a finding only when the search was verified sound.
- "The artifact does not settle this" is a legitimate answer. Say it instead of
filling the gap with a plausible mechanism.
@@ -1,170 +0,0 @@
#!/usr/bin/env python3
"""Fetch the failed jobs of a GitHub Actions run and cluster them by error line.
Usage:
ci_failures.py RUN_ID [--repo OWNER/REPO] [--out DIR] [--artifacts]
Prints how far HEAD moved since the run's commit, saves each finished failed
job's log (colour stripped) under DIR (default /tmp/ci-logs-RUN_ID), and
groups the jobs by the normalised error line that ended them (the last one). Jobs still running are
listed but not fetched: their API log can stop hours early. The clustering is
a heuristic; confirm a cluster's boundary against the logs.
"""
from __future__ import annotations
import argparse
import json
import re
import subprocess
import sys
from pathlib import Path
FAILED = {"failure", "cancelled", "timed_out"}
ANSI = re.compile(r"\x1b\[[0-9;]*[A-Za-z]")
STAMP = re.compile(r"^\d{4}-\d\d-\d\dT[\d:.]+Z\s?")
GENERIC = re.compile(r"failed=0\b|FAILED - RETRYING|^make(\[\d+\])?: \*\*\* \[.*\] Error \d+")
PROCESS_END = re.compile(r"##\[error\]Process completed with exit code")
ANNOTATION = re.compile(r"##\[error\]")
FATAL = re.compile(r"fatal: \[|FAILED!|Traceback|AssertionError|panic:")
LINT_AT = re.compile(r"\S+:\d+:\d+:? ((error|warning) )?[A-Z]{1,4}\d{3,4}\b")
LINT_CODE = re.compile(r"^[A-Z]{1,4}\d{3,4}\b")
LINT_ARROW = re.compile(r"^-->\s+\S+:\d+:\d+")
SIGNAL = re.compile(r"\b(ERROR|Error|FAILED|FAIL|fatal|Exception|failed)\b")
TASK = re.compile(r"^TASK \[")
REFUSAL = "terminal escape sequences"
def run(*cmd: str) -> subprocess.CompletedProcess:
return subprocess.run(list(cmd), capture_output=True, text=True, check=False)
def clean(line: str) -> str:
return STAMP.sub("", ANSI.sub("", line)).strip()
def normalise(line: str) -> str:
line = re.sub(r"\b[0-9a-f]{7,}\b", "<hex>", line)
line = re.sub(r"\d+", "<n>", line)
return line[:200]
def output_lines(text: str) -> list[tuple[str, str]]:
kept, echo, task = [], False, ""
for raw in text.splitlines():
line = clean(raw)
if PROCESS_END.search(line):
break
if line.startswith("##[group]Run "):
echo = True
elif line.startswith("##[endgroup]"):
echo = False
elif TASK.match(line):
task = line
elif line and not echo and not GENERIC.search(line):
kept.append((task, line))
return kept
def is_lint(lines: list[tuple[str, str]], index: int) -> bool:
line = lines[index][1]
if LINT_AT.search(line):
return True
following = lines[index + 1][1] if index + 1 < len(lines) else ""
return bool(LINT_CODE.match(line) and LINT_ARROW.match(following))
def signature(text: str) -> str:
lines = output_lines(text)
tiers = (
(lambda i: ANNOTATION.search(lines[i][1]), False),
(lambda i: FATAL.search(lines[i][1]), True),
(lambda i: is_lint(lines, i), False),
(lambda i: SIGNAL.search(lines[i][1]), False),
)
for matches, with_task in tiers:
for index in reversed(range(len(lines))):
if matches(index):
task, line = lines[index]
found = ANNOTATION.sub("", line).strip()
return normalise(f"{task} {found}" if with_task and task else found)
return normalise(lines[-1][1]) if lines else "<empty log>"
def cluster(jobs: list[tuple[str, str]]) -> dict[str, list[str]]:
groups: dict[str, list[str]] = {}
for job, text in jobs:
groups.setdefault(signature(text), []).append(job)
return dict(sorted(groups.items(), key=lambda item: -len(item[1])))
def behind(sha: str) -> str:
if run("git", "cat-file", "-e", f"{sha}^{{commit}}").returncode:
return "behind=unknown (run commit not in this clone)"
count = run("git", "rev-list", "--count", f"{sha}..HEAD").stdout.strip()
moved = {p.split("/")[0] for p in run("git", "diff", "--name-only", f"{sha}..HEAD").stdout.split()}
return f"behind={count} moved={','.join(sorted(moved)) or '-'}"
def main() -> int:
parser = argparse.ArgumentParser(prog="ci_failures")
parser.add_argument("run_id")
parser.add_argument("--repo")
parser.add_argument("--out")
parser.add_argument("--artifacts", action="store_true")
args = parser.parse_args()
repo = args.repo or run("gh", "repo", "view", "--json", "nameWithOwner", "-q", ".nameWithOwner").stdout.strip()
if not repo:
parser.error("no --repo and gh cannot resolve the current repository")
meta = run("gh", "api", f"/repos/{repo}/actions/runs/{args.run_id}")
# per_page=100 on a large matrix answers HTTP 502; 30 per page goes through.
listing = run("gh", "api", "--paginate", f"/repos/{repo}/actions/runs/{args.run_id}/jobs?per_page=30",
"--jq", ".jobs[] | {id, name, status, conclusion}")
if meta.returncode or listing.returncode:
print((meta.stderr or listing.stderr).strip(), file=sys.stderr)
return 1
data = json.loads(meta.stdout)
jobs = [json.loads(line) for line in listing.stdout.splitlines() if line]
out = Path(args.out or f"/tmp/ci-logs-{args.run_id}")
out.mkdir(parents=True, exist_ok=True)
print(f"run={args.run_id} repo={repo} head={data['head_sha'][:10]} status={data['status']} "
f"jobs={len(jobs)} {behind(data['head_sha'])}")
running = [j["name"] for j in jobs if j["status"] != "completed"]
if running:
print(f"running ({len(running)}, not fetched): {', '.join(running)}")
fetched, missing = [], []
for job in jobs:
if job["status"] != "completed" or job["conclusion"] not in FAILED:
continue
log = run("gh", "api", "--allow-escape-sequences", f"/repos/{repo}/actions/jobs/{job['id']}/logs")
if log.returncode or REFUSAL in log.stdout[:200]:
missing.append(f"{job['conclusion']}\t{job['name']}\t{log.stderr.strip()[:80]}")
continue
slug = re.sub(r"[^A-Za-z0-9._-]+", "_", job["name"])[:80]
path = out / f"{job['id']}-{slug}.log"
path.write_text("\n".join(ANSI.sub("", line) for line in log.stdout.splitlines()), encoding="utf-8")
text = "##[error]cancelled" if job["conclusion"] == "cancelled" else log.stdout
fetched.append((f"{job['conclusion']}\t{job['name']}\t{path}", text))
if missing:
(out / "no-log.txt").write_text("\n".join(missing) + "\n", encoding="utf-8")
counts = {c: sum(m.startswith(c) for m in missing) for c in FAILED}
summary = ", ".join(f"{n} {c}" for c, n in sorted(counts.items()) if n)
print(f"no log for {len(missing)} jobs ({summary}; never started or expired): {out / 'no-log.txt'}")
for number, (sig, jobs) in enumerate(cluster(fetched).items(), 1):
print(f"cluster {number} ({len(jobs)} jobs): {sig}")
for job in jobs[:3]:
print(f" {job}")
if len(jobs) > 3:
members = out / f"cluster-{number}.txt"
members.write_text("\n".join(jobs) + "\n", encoding="utf-8")
print(f" ... +{len(jobs) - 3} more: {members}")
if args.artifacts:
listing = run("gh", "api", f"/repos/{repo}/actions/runs/{args.run_id}/artifacts", "--paginate",
"--jq", '.artifacts[] | "\\(.name)\\t\\(.size_in_bytes)"')
print("artifacts:")
print(listing.stdout.rstrip() or " none")
return 0
if __name__ == "__main__":
sys.exit(main())
-114
View File
@@ -1,114 +0,0 @@
---
name: kvb-ci-triage
description: >
Triage a failing CI run, pipeline or job set to a verified root cause and a
real fix, one cluster per worktree. Trigger when a build, test or deploy
pipeline has failed jobs.
---
Drive a red pipeline to green by root cause, not by guesswork. Treat every
failing job as its own investigation, and do not declare a fix until that job's
root cause is proven.
## Procedure
For a GitHub Actions run, the kvb-ci-artifacts skill's `ci_failures.py <run>`
does the mechanics of steps 0 to 2 in one call: distance to HEAD, the failed
jobs' logs, and a first clustering. The judgement in those steps stays yours.
0. **Check the run against HEAD before anything else.** A CI run is a snapshot
of one commit, and the branch has moved since. Run
`git log --oneline <run-sha>..HEAD | wc -l` as the first command of the
triage. Zero means the run speaks for the current tree and you proceed.
Non-zero means it does not, and you scope the job set down **before**
enumerating, clustering, or spawning anything — every job you investigate
against stale code is work thrown away, and a fix proposed there risks
reverting someone else's. Two cheap discriminators do it without opening an
artifact: `git diff --name-only <run-sha>..HEAD -- <source dir> | cut -d/ -f2 | sort -u`
for which components already moved, and a grep of the saved job logs for the
literal error signature of each cause already fixed in that range. What
survives both is the job set the rest of this procedure applies to; say how
many jobs you dropped and why.
1. **Enumerate every failing job.** Pull the run's job list and isolate each job
whose conclusion is failure, cancelled, or timed-out. A downstream aggregate
job that fails only because an upstream job did is not a separate root cause —
note it and move on.
2. **Cluster the failures by error message.** Group the failing jobs by their
actual failing line, not by job name. Jobs whose messages differ in anything
but the interpolated identifiers (host, app, port, path) belong in separate
clusters.
3. **Coordinate every cluster.** Run the `kvb-org-organize` skill's Lineage and Peer
protocol for each cluster before fixing it: claim it, ask the lineage
whether someone already fixes it, and take their fix or keep yours. Keep
investigating while you wait for answers. In the `kvb-org-rasci` register
the cluster's objective is the root cause fixed, its key results are the
failing jobs passing again, one per job.
4. **Download artifacts per cluster.** Use the `kvb-ci-artifacts` skill for the
mechanics — it covers per-job logs while a run is still in progress, which
bundle to pull, and the grep hygiene that keeps a false positive from
becoming a root cause. For each cluster, fetch every artifact of
one representative job: reports, rescue diagnostics, inventories, container
logs. Pull a second member's artifacts only when the representative's
evidence does not explain the whole cluster. The job log usually names an
artifact path and nothing more; the assertion text, the server-side
exception and the state dumps are inside the artifact. Do this even when the
log looks conclusive, because a log that explains the symptom rarely
explains the cause.
5. **One funnel per cluster.** Hand every cluster you claimed in step 3 to the
`kvb-org-split` skill: one agent per cluster, working in its own worktree on its own
branch created from the exact current HEAD, with that cluster as its only
concern, the artifact paths of step 4 in its brief and a time cap for its
interim report.
Steps 6 to 8 run inside that sidetree, so no cluster's edits land under
another cluster's work or under a running gate or deploy. Run clusters in
parallel only when their fixes touch different files and the machine has
room. A split cluster (step 7) gets a funnel of its own.
6. **Dialectic per cluster.** For EACH cluster, invoke the `kvb-think-dialectic` skill:
form a thesis about the root cause from evidence (the job log, its artifacts,
the code at the run's commit, git history), attack it with independent
skeptics, and iterate to a ~99% thesis.
7. **Distinguish shared vs hidden causes.** A cluster is a hypothesis, not a
proof: if the dialectic shows one cluster splitting into two causes, split it.
When a job hides a second failure behind the first, keep going until the job
is actually green, not just past the first error.
8. **Fix at the root, through the funnel.** Apply the real fix for each proven
root cause in the cluster's sidetree, with a test and a negative control as
the `kvb-org-split` skill demands. The fix enters the main line only at the funnel's
bar (at least 90 % fix confidence, under 5 % break risk) after its
independent review; below the bar the cluster ends as a proposal for the
operator, never as a merge. Never mask a failure: no retry-until-pass, no
disabling the check, no soft-skip. If a failure is genuinely external
(upstream outage, flaky infra), confirm that with evidence and surface it
honestly instead of fixing around it. Report every fix, every proposal, and
every abandoned attempt to the same agents through the `kvb-org-organize` Peer protocol,
then remove the sidetree.
9. **Tune what ran out of time.** Only for a cluster whose jobs died on their
time budget (timed out, or cancelled by the runner's limit), or when the
operator asks: hand the job log that ran furthest to the `kvb-code-tune` skill. Its
candidates run through funnels of their own, separate from the cluster's fix
sidetree, and never delay or block the fix.
10. **Follow the run.** While the run is still in progress, re-check it
periodically and triage each newly failed job as it appears. The run is done
only when it has finished and every failure has a verified fix.
## Rules
- Executed evidence beats reasoning: read the actual failing log line and the
artifact, not the job title.
- A derived source is not evidence. A conversation summary, your own earlier
message, a cluster table you built ten minutes ago, or a subagent's report
proves nothing on its own. Only the job log, the artifact, the code at the
run's commit, and command output you just ran count. Quote them literally; if
you cannot quote it, you do not know it.
- Re-derive after a context compaction. A summary carried across a compaction
boundary preserves your conclusions but not the evidence under them, and a
cluster assignment that was wrong stays wrong while reading as settled fact.
Before acting on any pre-compaction attribution, rebuild it from the job logs.
- Classify each failure: a real defect (fix it), a flake or infra hiccup (prove
it is transient before dismissing), or already-fixed-by-a-later-commit. For
that last class the comparison is `<run-sha>..HEAD`, never the run's commit
alone: the run cannot contain a fix that landed after it, so looking there
can only ever confirm the failure. Name the commit that carries the fix.
- Report a per-job verdict: root cause, the artifact path and line that prove
it, the fix, the cluster's sidetree branch, and the funnel verdict (merged or
proposal). A verdict that cites only the job log has not finished step 4.
-187
View File
@@ -1,187 +0,0 @@
---
name: kvb-cleanup
description: >
Free disk space agents filled: ask peers what they still need,
inventory /tmp, Docker and agent dirs, and delete only what the operator
picks. Trigger on /kvb-cleanup, "aufräumen", "bereinigen", "platte voll", "no
space left on device".
---
Nothing is deleted before the operator has picked it. Every candidate carries
its owner, its size, and its risk.
## Step 0: the resource chair
Shared resources belong to the resource chair of the `kvb-org-organize` skill. If
another session holds the chair, send it the cleanup request and stop, unless
the operator ordered the cleanup in this session; then tell the chair before
step 1, so its grants and queue stay consistent. The `kvb-org-organize` resource
watch already runs the configured low-risk cleanup at its stop threshold; this
skill covers everything that needs the operator's pick.
## Step 1: ask the peers
List every agent the host can message (in Claude Code: `ListAgents`). Only
peers sharing this machine's filesystem and Docker daemon count: local
sessions and your own subagents. Cloud sessions and remote sessions on other
machines hold nothing here; skip them.
Send each local peer one message (`SendMessage`) and go on with step 2 while
they answer:
```text
Cleanup planned by <own session name>. Reply to me with everything you still
need, one item per line:
- paths under /tmp, caches, worktrees
- containers, compose or stack projects, volumes, images, networks
- running jobs (deploys, tests, builds) and when they finish
Reply "nothing" if you need none of it. Items you do not answer for stay.
```
## Step 2: inventory
Read-only, in parallel, sizes included:
- **Scope**: the directories agents may write to, taken from the host's
sandbox or permission config (in Claude Code:
`sandbox.filesystem.allowWrite` in `.claude/settings.json`) plus the session
scratch dirs. Anything outside is out of scope, even when readable.
- **/tmp**: `du -sh` per top-level entry the current user owns. System entries
(`.X11-unix`, `.ICE-unix`, `systemd-private-*`, `ssh-*`, `tmux-*`, sockets)
are never candidates.
- **Docker**: `docker system df -v`; containers with state and compose project
label, volumes with the containers using them
(`docker ps -a --filter volume=<name>`), dangling and unused images, build
cache, unused networks. For every stopped container (`exited`, `created`)
record its volumes (`{{.Mounts}}`); for every volume no running container
uses, record size, `CreatedAt` (`docker volume inspect`) and compose project
label, anonymous ones included.
- **Other agent dirs**: package and tool caches (pip, npm, pre-commit, act,
gh, buildx), worktrees with `git status --short` and whether their branch is
merged or pushed, Ansible and build temp dirs. With `pkgmgr` installed,
merged branches and worktrees go to the `kvb-git-branch-gc` skill instead.
- **Register**: `rasci.py artifacts` of the `kvb-org-artefacts` skill:
workdir, branch and artifacts of every task in the `kvb-org-rasci` register,
with its status and worker; `--ended` narrows it to the candidates.
- **Live use**: open handles (`fuser`, `lsof`) and running processes by name
only (`ps -o pid,etime,comm`). Never print full command lines or
environments: process arguments carry credentials. A sandboxed agent often
sees only its own processes, so an empty handle list proves nothing there;
then only entries untouched for more than 24 hours (`find -mtime +0`) are
candidates, and newer ones stay as `keep (recent, possibly in use)`.
- **Archive duties**: memory and project instructions that require copying
something out before deletion (logs, reports, generated maps). Such an item
is `archive first`, never a plain delete.
Prefer the project's own cleanup and teardown targets (Makefile, scripts) over
raw commands where they exist, and obey project rules that forbid raw
commands.
## Step 3: attribute
Give every candidate an owner: a peer's reply, a session id in the path, a
worktree or compose project name, a container label, an open handle, or
`unknown`. Then apply, first match wins:
1. Claimed by a peer: `keep`.
2. Listed in the register under a task that is not `done`, `stopped` or `stale`:
`keep (task <task>)`. Under an ended task it is a candidate whose owner is
the task's worker.
3. A job running on it (deploy, gate, test, swarm run, build): `keep
(running)`.
4. Attributable to a peer that has not answered: `keep (no answer from
<peer>)`. Silence is not consent.
5. Archive duty: `archive first`.
6. Everything else is a candidate with a risk class:
| Risk | Meaning | Typical items |
| ---- | ------- | ------------- |
| low | regenerated automatically, cheap | stopped containers without volumes, dangling images, unused networks, scratch dirs and logs of finished sessions |
| medium | regenerated, but slow, rate-limited or network-bound | unused tagged images (re-pull, registry limits), build cache (cold rebuilds), package caches |
| high | data loss or a broken run possible | volumes, stopped containers with volumes, running unclaimed containers or stacks, worktrees with uncommitted or unpushed work, unread reports, anything `unknown` |
## Step 4: propose
One table, biggest first:
| Area | Item | Size | Owner | Risk | Proposal | What breaks if wrong |
Below it: reclaimable size per risk class, and every `keep` row grouped by
reason, so the operator sees what was protected and why.
Then ask with `AskUserQuestion` in two calls.
**Call 1, the selection** (`multiSelect: true`, checklists, at most four
questions): `/tmp`, `Docker`, `Other dirs`, and a separate `High risk`
question that holds every high-risk candidate. Size, risk and worst case go
into each option's description. An area with more than four options is
grouped by owner or risk, or split into a further call. Only ticked items are
selected; nothing is preselected.
**Stopped containers and volumes** always get their own checklist
(`multiSelect: true`, header `⚠ Volumes`, in a further call when call 1 is
full). Every stopped container and every volume without a running container
appears there unless a peer claimed it; none is dropped as "not proposed",
`unknown` and anonymous ones included. The question text carries the warning:
`Deleting a volume is irreversible; its data does not come back unless the
description says how.` One option per stopped container together with its
volumes (removing the container alone frees nothing of them), one per
unattached volume, anonymous ones grouped. Each description gives size,
`CreatedAt`, owner or compose project, and what is lost.
**Call 2, the confirmation** (`multiSelect: false`, radio buttons), computed
from the ticks: `Execute: <n> items, <size> (Recommended)`, `Revise the
selection`, `Abort`. The execute description names the worst case of the
ticked items. Revise repeats call 1; nothing is deleted before `Execute`.
## Step 5: execute
- Re-check claims, open handles and running state right before each delete: a
peer may have started something since the inventory.
- Delete exactly the selected items, by name or path. No blanket prune
(`docker system prune -a --volumes`, `rm -rf /tmp/*`) that sweeps beyond the
selection. For more than a handful of paths, write the list to a file under
`/tmp`, grep it against every claim once more, then delete with
`xargs -d '\n' rm -rf -- < <list>`.
- Locally built images free little on their own: their layers stay behind as
build cache. After removing images, re-measure `docker system df`; the
layers show up as reclaimable build cache. Offer
`docker builder prune --filter until=24h` as a follow-up candidate, ask the
peers that claimed build cache first, and run it outside their running
builds.
- A host permission layer (an auto-mode classifier, a deny rule) may refuse a
delete. Never route around it with another command or a peer. Finish the
rest, then put the refused item to the operator again as its own option: an
explicit renewed selection may pass; if it is refused again, hand over the
exact command with its list file for the operator to run.
- Never stop, take down or kill a running job, and never ask its owner to, to
free space: not for a priority slot, not at a disk stop. Free idle items,
ask the operator for an exception, or wait; an owner's own disk guard only
alerts the chair, and only the operator can order a running job down.
A hung job is not freed through cleanup: the `kvb-org-organize` skill's hang rule
lets only its owner end it.
- Idle running stacks go down through the project's teardown target, never by
removing their containers or volumes underneath them.
- Archive-first items: copy out, verify the copy, then delete.
- Drop every deleted item from its task as the `kvb-org-artefacts` skill
says (`set <task> --drop-artifact <kind>:<ref>`), so `artifacts` stays true.
- A delete failing on permissions (files owned by a container UID): use the
project's container helper. Never sudo, never bypass the sandbox; without a
helper, report the path.
- Stop on the first unexpected error and report it.
## Step 6: report
`df -h` and `docker system df` before and after, freed size per area, every
skipped or failed item with its reason. Tell each peer that answered which of
the removed items lay near its own.
## Rules
- Questions, inventory and proposal change nothing. Deletion needs the
operator's selection in this run; a selection from an earlier run does not
carry over.
- Never delete outside the agent write scope, another user's files, or system
entries in `/tmp`.
- Never invent a peer's answer or a size. Unmeasured is `unknown`, and
`unknown` is high risk.
-43
View File
@@ -1,43 +0,0 @@
---
name: kvb-code-no-comments
description: >
Purge explanatory comments from staged and unstaged changes. Run
AUTOMATICALLY before every commit or stage request (commit, commit
no-verify, stage next, socon, sobu, socobu, "commite ..."); also on
/kvb-code-no-comments or comment complaints.
---
When the operator asks to commit or stage, run this audit first and only
then perform the requested git action; never ask whether to run it.
Audit every staged and unstaged change (`git diff` plus `git diff --cached`,
added lines only) and delete every explanatory comment:
1. Delete: restated code, step narration, section banners, "Note that",
rationale prose, apology comments, and any comment whose information the
code, task name, or commit message already carries. When in doubt,
delete.
2. Keep only: parameter/exit-code docs, nocheck/noqa/shellcheck directives,
and project-sanctioned exception markers that name a concrete trip-wire.
A relabeled narration is not an exception; delete it instead.
3. Block-comment languages are not line-oriented. In Jinja (`{# … #}`), C-like
(`/* … */`), HTML (`<!-- … -->`) and their kin, a closing delimiter alone on
its own line looks exactly like a comment line and gets deleted with the
rest — after which the opening delimiter swallows every live statement down
to the next closer. Never strip these languages with a line filter. Delete
the whole block by hand, then count the delimiters (`grep -c '{#'` against
`grep -c '#}'`) and re-parse the file.
4. Re-run the project's comment lint if one exists. A green lint does NOT prove
the deletion was safe: a swallowed block is still syntactically valid, so
both the lint and the language parser can pass over code that no longer
executes. Diff against the pre-edit revision (`git show HEAD:<path>`) and
confirm that only comment lines left.
5. Do not stage in order to run a check (the `kvb-git-commit-next` skill's step 4
says why). Re-stage after the audit exactly the files that were staged
before it.
6. Penalty: if this audit finds anything you wrote yourself, admit it in
the report (file and count, no excuses), lower your confidence numbers
for the affected bundle, and write the incident to persistent agent
memory (extend the existing minimal-comments feedback entry with the
date and the pattern you slipped on) so the calibration survives the
session.
-34
View File
@@ -1,34 +0,0 @@
---
name: kvb-code-scope-guard
description: >
Deliver exactly what was asked and nothing more. Trigger on every task that
creates files, commands or commits, on unreviewed autonomous runs, on
hand-over requests, and when the operator asks why something unrequested
exists.
---
The requested scope is the deliverable. A problem you noticed behind the
request is not an invitation to solve it.
Before creating anything the request did not name, answer one question: which
sentence of the request asks for this? No sentence, no artifact.
1. No permanent scaffolding for a one-off need. A migration you run once needs
a throwaway script, not a CLI subcommand that ships forever. Do the thing,
then delete the scaffolding.
2. Path-scope every `git add`. Never `-A`, never `.`. The operator's tree
carries their own edits and their local config, and sweeping those into
your bundle makes them undo your work by hand.
3. An adjacent defect gets reported in one line, not fixed — unless it blocks
the request. Fixing it quietly puts an unreviewed change in someone else's
commit.
4. An instruction to hand something over is complete on its own. Print the
artifact in the shape asked for: do not optimize, reorder or deduplicate it,
do not wrap it in a command that consumes it, and do not append commentary,
next steps, confidence blocks or offers. A narrowing qualifier ("just the
line") re-shapes the earlier request; re-emit narrower without re-deriving
the content. A problem with the artifact is worth one sentence before it,
never a rewrite of it.
When something genuinely seems missing, say so in one sentence and finish the
requested scope anyway. Widening the scope is the operator's call.
-130
View File
@@ -1,130 +0,0 @@
---
name: kvb-code-test
description: >
Discover the Makefile's test targets, let the operator pick, run and report;
fix mode loops until green. Trigger on /kvb-code-test [--fix], or asked to
run the tests, make them pass, or get the suite green.
---
The Makefile is the source of truth for how this project runs tests. Never
invent a command (`pytest`, `npm test`, `go test`) while a Makefile target
exists - the target carries the project's env vars, build dependencies and
container setup.
## Step 1: find the targets
From the repository root:
```bash
grep -nE '^test[A-Za-z0-9_.-]*:' Makefile
```
Read the matched rules plus their prerequisites, so the selection list can say
what each one actually does (delegated script, container build, sub-targets).
Edge cases, handled explicitly rather than guessed around:
- **No Makefile**: say so in one line, name the test runner the project does
use (from `pyproject.toml`, `package.json`, `tox.ini`, CI workflow), and ask
before running anything.
- **No `test*` target**: report the targets that do exist and stop.
- **Included makefiles** (`include foo.mk`): grep those too.
- **Aggregate targets**: a target whose recipe is only other test targets (e.g.
`test: test-unit test-integration`) is the "run everything" entry - mark it as
such in the list, do not expand it into its parts silently.
## Step 2: offer the selection
Ask with `AskUserQuestion`, `multiSelect: true`, one question. Options are the
discovered targets, each with a one-line description of what it runs. Order the
aggregate/full target first and label it as the complete suite.
The tool takes at most 4 options. With more targets than that:
- print the **complete** discovered list as text first, one line per target, so
nothing is hidden, then
- offer the 4 most useful entries (aggregate first, then the ones matching the
operator's stated intent or the files currently uncommitted), and note that
"Other" accepts any target name from the printed list.
Never silently truncate. If the operator already named a target in their
request, skip the question and run it.
## Step 3: run
Run the selected targets in the order the operator listed them, one `make`
invocation per target, each as its own command so a failure is attributable:
```bash
make <target>
```
Do not add `-k`, do not reorder, do not substitute a faster equivalent. If a
target needs a long timeout (container builds, e2e), set it on the Bash call
rather than backgrounding blindly.
Stop after the first failing target unless the operator asked for all of them -
a later suite running against a broken build produces noise, not information.
## Step 4: report
Per target: pass/fail plus the failing test names and the exact error output,
quoted verbatim. Never paraphrase a failure. Then one line with the exact
command to reproduce the failure alone.
Do not fix what failed unless the operator asks - report first. A request to
make the tests pass, fix them, or get the suite green (`/kvb-code-test --fix`) switches
to fix mode.
## Fix mode
The contract is narrow: the code becomes correct, the tests stay honest.
One iteration is: run -> read the failure -> one root cause -> one fix ->
re-run. Never batch several speculative fixes into one iteration; a green run
after three simultaneous changes proves nothing about which one mattered.
- **Run** the failing target alone, the fastest command that reproduces the
failure. Widen back to the full selection only when that target is green.
- **Diagnose** from the actual error output before touching a file. Apply the
`kvb-ci-triage` skill to reach a verified root cause; for a cause still unclear
after one look, apply `kvb-think-dialectic` rather than guessing twice. Say in one line
which side is wrong:
- **Code**: the test states the intended behaviour. Fix the code.
- **Test**: it encodes an outdated or incorrect expectation. Fix it only with
a stated reason why the old expectation was wrong, never because the code
disagrees with it.
- **Environment**: missing dependency, stale container, absent env var,
unbuilt artefact. Fix the environment or report it; do not patch code
around it.
- **Fix** with the smallest change that addresses the root cause, in the
project's idioms and under the `kvb-code-no-defaults` and `kvb-code-no-comments` rules.
Forbidden without explicit operator approval: deleting, skipping, `xfail`-ing
or commenting out a failing test; loosening an assertion to what the code
produces; swallowing the exception the test surfaces; retries, sleeps or
reruns over a flaky failure; disabling a linter rule, a type check or a
target. Any of these is a report item, not a fix: a suite that is green
because the failing test no longer runs is a regression disguised as success.
- **Re-run** the same target, then every target that was already green; only
a full green selection ends the loop.
Stop and report, without asking for permission to stop:
- **Green**: the full selection passes.
- **No progress**: the same failure survives 3 iterations, or the failure count
stops falling across 3 iterations. Report the root cause reached, what was
tried, and why each attempt failed.
- **Beyond the mandate**: the real fix is an API change, a dependency bump, a
schema migration, or a redesign. Name it with its minimal diff and let the
operator decide.
- **Oscillation**: fixing A re-breaks B and vice versa. Report both; that is a
design problem, not a test problem.
- **Flaky**: a target passes and fails without any change. Report both
outputs; never rerun until green.
Emit one line per iteration: target, failure, hypothesis, change. The final
report gives per iteration what failed, the root cause, the fix and the
result; the final state of every selected target with the verbatim output of
anything still failing; everything deliberately not done; and the command
reproducing the final state. State confidence per the `kvb-check-confidence` skill. Do
not commit unless the operator asked for a commit.
-218
View File
@@ -1,218 +0,0 @@
---
name: kvb-code-tune
description: >
Find and apply performance optimizations (CI, deploy, build, gate, hot
paths) without reducing logic, coverage or correctness. Trigger on
/kvb-code-tune, a job dying on its time budget, a slower pipeline or deploy,
or asked where time goes.
---
# Tune
One rule outranks every optimization: **nothing that reduces logic, coverage or
correctness counts as a speed-up.**
## Once per session: listen first
The first time this skill runs in a session, invoke the `kvb-think-listen`
skill before measuring anything, and ask three questions through the host's
structured question mechanism. Offer each as a checklist (multi-select) with
suggestions, and allow free text:
1. **What to examine.** Suggestions: CI wall-clock, deploy duration, gate and
test runtime, image pulls and build caches, per-item loops and process
spawns, idempotency (work repeated on a second run), network round trips,
and resource use (CPU, memory, disk, I/O). Language, framework and
recursion are checked on every run anyway (see Always check).
2. **Which performance boundaries to keep.** Suggestions: time budgets per job,
deploy or gate; resource caps (memory, CPU, disk headroom); a minimum saving
per change (default 10 s per run); the measurement source that counts as
the verdict.
3. **Which limits apply to optimizations.** Suggestions: the scope that may be
changed (files, roles, services); whether new dependencies, toolchains or
languages may be introduced; whether infrastructure or CI configuration may
change; the merge bar (default: the `kvb-org-split` skill's 90 % confidence and
under 5 % break risk); a time cap per delegated agent (default 60 minutes).
Keep the answers for the rest of the session and do not ask again. Work only
inside them; a candidate outside them becomes a proposal.
When another skill (kvb-ci-triage, kvb-ci-observe) or an autonomous run invoked
this skill, skip
the questions: examine the handed-over log, apply the defaults above, and ask
only when an operator-only fact blocks.
## Forbidden, whatever it saves
- Dropping a test, a spec, a play, a drill or a verification pass.
- Raising a timeout or budget so a slow thing fits instead of getting faster.
- Swallowed failures, retry-until-pass, a soft skip, a narrowed assertion.
- Trading error semantics, credential masking, concurrency safety or a lint's
intent for speed. A per-item failure stays loud and names its item.
A change that makes the clock smaller by testing less is reported to the
operator as a coverage cut for them to decide, never applied as a tuning.
## Measure first
Never tune what has not been measured, and read numbers from logs, not by eye.
- **Task profilers** (Ansible `profile_tasks` and similar) print the duration
of the *previous* task under the *next* task's header. Pair each header with
the following timestamp line, or read the end-of-run recap.
- **Build logs** (BuildKit): pair `#N [service x/y] RUN …` with `#N DONE Xs`
by step number; `CACHED` marks a hit.
- **Gates**: per-target wall-clock tables and failed-target lines. An exit code
piped through `tee` is not a verdict.
- **Code paths**: a profiler (py-spy, cProfile, perf, flame graphs) tells
whether time goes to CPU, to I/O and waits, or to process start-up.
- Stream every run to a log file and compare runs by their logs; never re-run
to recover a lost number.
- **Rank by fixed cost x count.** Measure the per-call fixed cost (one
application console boot, one package-manager call, one container exec) and
multiply by how often the run pays it. A one-second boot paid twenty-eight
times is a candidate; a sixty-second download paid once is not.
- **The baseline is the newest log of the current code.** An older log may
still carry the step a previous optimization replaced, and a saving measured
against it is fiction.
## Invalid comparisons
Discard a before/after pair, not just its noise, when:
- Another gate, deploy or heavy job shared the machine during either run.
- The two runs differ in image or cache state. A cold build after a cache prune
or a Dockerfile edit is expected: before calling a cold build a defect,
compare the instruction text and the base image digest across both logs.
- One run pays a one-time cost (first install, migration, purge, model
download) the other does not.
- A long wait hides retries or a failure; count retry lines before calling it
slow.
- A fresh-install pass is compared with an update pass. Analyse each pass of a
multi-pass run separately.
## Before you optimize
- **Search every branch** (the kvb-git-merge-fix skill's `find_fix.py -S <symbol> --path <path>`) for a change
that already removes or reshapes the step. A fix on a sibling branch can make
the whole candidate worth zero.
- A step that is long by design (a backup and restore drill, an end-to-end
suite) is not a candidate. Its cost buys its guarantee.
- Apply the session's saving floor: a smaller saving is listed as "below
floor" and dropped.
- Pruning build caches to free disk costs a cold rebuild of every custom image
on the next run. Name that cost before anyone prunes.
## Coordinate every cost cluster
A cost cluster is one place the time goes: one step, one hot path, one
repeated call, named by its log line and its number. Before investigating or
implementing a cluster, run the `kvb-org-organize` skill's Peer protocol for it with
every agent session on this machine, not only the branch lineage.
- **The first to identify a cluster works it.** "First" is the `kvb-org-organize`
claim `cluster/<slug>`, whose `--work` names the cluster's log line and
number; between machines, the earliest announcement by message time. A
claim without a measurement does not count. In the `kvb-org-rasci` register
the cluster's key result is the measured target (`<step> from <before> to
under <after>`), met only by the re-measurement.
- **Someone was earlier:** send them your measurements and findings, then move
on to the next cluster. Never build a second optimization of the same
cluster.
- **A claim ends** when its holder reports the result, a proposal or an
abandoned attempt and releases it, or when the operator reassigns it.
## Always check
Every run examines these two on each measured hot path, whatever the operator
picked in the first question, and lists the result in the output table, a
clean result included:
- **Language and framework**: whether the hot path's language and framework
are the most efficient fit for its work (see Language and framework).
- **Unnecessary recursion**: a recursive function that recomputes the same
subproblem (memoize it or turn it into a loop), recursion that only walks a
list or a fixed depth (a loop), a tree walk that visits or reads the same
node more than once, depth that grows with the input (stack limits, Python's
1000 frames), and recursive tool calls: a make target calling make, a role
or playbook including itself, a script re-invoking itself per item. Count
the calls first (a profiler's call graph, cProfile's `ncalls`, log lines);
recursion that is bounded, rare and cheap stays.
## Levers that usually pay
- **Batch per-item process spawns**: N calls that each boot an interpreter or
an application console become one process running N commands (one app
console boot, one module, one batched API request). Keep each item's failure
named and the exit code honest.
- **Skip no-op work** a previous step already did, such as a separate enable
after an install that already enabled.
- **Idempotency flip-flops**: any `changed` on a second consecutive run.
- **Image pulls and builds**: pull only what the host lacks; keep build caches
warm.
- **Overlap waits with work** only where concurrent execution is proven safe.
Schema migrations and lock-holding steps are usually not.
## Language and framework
Check the language and the framework of every measured hot path, for example
replacing a Python hot path with Rust, or a heavy framework layer with a
lighter one or the standard library:
- **Only for measured CPU-bound work.** When the profile shows the time in
computation inside the process (parsing, hashing, rendering, tight loops),
a compiled language can win by an order of magnitude. When the time goes to
process start-up, network round trips or waiting, batching in the current
language wins first, and a rewrite changes nothing.
- **Framework overhead**: when the profile puts the time inside the framework
(import and start-up, middleware, ORM query building, serialization),
compare a lighter framework or the standard library on the same input. An
ORM issuing one query per item is fixed by batching the query, not by
swapping the framework.
- **Prefer the narrowest cut**: rewrite the hot function as an extension
module (for Python and Rust: PyO3 and maturin) or a small binary behind the
same interface, before rewriting a whole tool.
- **Count the total cost**: a toolchain in CI, builds per target architecture,
shipping binaries to every host, the team's ability to maintain the code,
and test parity with the old implementation.
- **Prove it on a prototype**: the same input, the same machine state, a before
and an after number, and the old test suite passing against the new code.
- A language or framework change is applied only when the session's limits
allow new languages, frameworks or toolchains; otherwise it is submitted as
a proposal with the prototype's numbers.
## Delegating an optimization
Hand every optimization to an agent through the `kvb-org-split` skill: one sidetree
per candidate, branched from an exact commit, with a time cap and "measure
first, report the time split before implementing". The agent probes live
systems read-only and never starts gates or deploys. A broad brief without a
cap can run for hours without a word; at its cap, collect the agent's interim
findings and let it run on.
Before merging, run independent skeptics through the `kvb-think-dialectic` skill. The
antithesis must try to show that the candidate removes coverage, hides a
failure, or only moves the cost somewhere the measurement does not look.
## The loop
1. Measure, and name the single biggest cost with its log line and number.
2. Coordinate the cluster with the other agents; work it only when you
identified it first, otherwise hand over your findings and take the next.
Then form one candidate for it, after the branch search and the floor check.
3. Harden it with the `kvb-think-dialectic` skill.
4. Funnel it through a sidetree; merge what reaches the bar, submit the rest as
proposals. Re-measure from the same source under the same conditions and
report a before and an after number.
5. Repeat until the next candidate is worth less than the run that measures it.
## Output
One table, biggest saving first:
| Candidate | Before | After | Source | Verdict |
Rejected candidates stay in the table with the reason they fell: below floor,
coverage cut, outside the session's limits, invalid comparison, made moot by
another branch, or cap reached without a measured result. Proposals carry what
would close their gap.
-98
View File
@@ -1,98 +0,0 @@
---
name: kvb-create-feature
description: >
Derive missing work (principles pass for structural flaws, usability pass for
missing features) and write requirement documents after confirmation.
Trigger on /kvb-create-feature, "was fehlt noch", "welche features brauchen
wir", or asked what to build next.
---
Two blind spots produce different work: the code has defects of structure,
the product has gaps of use. This skill runs both lenses, ranks what comes
out, and lets the operator choose — it writes requirement documents, never
implementations.
## Step 1: fix the scope
Name the target in one line: repository, subsystem, role, or user-facing
surface. Read the project's existing requirement documents first — their
location, filename convention, and section structure are the template, and
an already-written requirement is not a finding.
## Step 2: the principles pass
Invoke the `kvb-check-principles` skill on the target. Its findings become requirement
candidates by severity: a violated SPOT, SRP, or trust boundary is work the
product needs whether or not a user ever notices. Carry each finding's
`file:line` evidence forward — a requirement without it is a guess.
## Step 3: the usability pass
Independently, look for what the product does not do yet. Anchor every gap
in something observable in the repository, never in an invented persona:
- **Entry** — can someone install, configure, and run it without reading the
source? Missing `--help`, missing example config, an undocumented required
variable.
- **Recovery** — does a failure tell the user what to do next? Errors that
print a stack trace instead of the remediation, no dry-run, no way to undo
or uninstall, no idempotent re-run.
- **Feedback** — does a long operation say anything while it works? Silent
minutes, no progress, no exit summary.
- **Empty and edge states** — first run with no data, zero results, an
expired credential, a partially completed run.
- **Discoverability** — a capability that exists but nothing surfaces:
undocumented flags, features reachable only by reading tests.
- **Accessibility and locale** — output that assumes colour, a terminal
width, or one language.
Each gap gets one line: `observed evidence -> who is blocked -> smallest
change that unblocks them`. Drop anything you cannot anchor.
## Step 4: rank and ask
Merge both lists, drop duplicates where a structural finding and a usability
gap describe the same defect, and rank by blocked users first, blast radius
second.
Then apply `kvb-think-listen`: answer everything you can from the code, the
docs, and the git history yourself, and put only the genuinely open decisions
to the operator with `AskUserQuestion`, `multiSelect: false`, so every
question renders as radio buttons. At most four questions per call, each
option a real alternative — never one option plus filler:
1. **Which candidates** become requirements this run, ranked, each option
carrying its evidence in the description.
2. **Which shape** the chosen one takes — for a feature typically
`Minimal version that unblocks the user`, `Full version`, `Fix the
underlying structure instead`, `Document the limitation and defer`.
3. **Scope boundary** — what is explicitly out of scope, when the candidate
could grow in more than one direction.
4. **Placement** — which requirement file or directory receives it, when the
project has more than one.
Never assume an answer, never fold two decisions into one option, and never
write a file before the answers are in.
## Step 5: write the requirements
One document per feature — a requirement that needs "and" in its title is
two requirements. Follow the project's existing template; when there is
none, keep it to: problem with its evidence, the decided scope and its
boundary, definition of done as checkable criteria, and the alternatives the
operator rejected with the reason.
Show the drafted document and ask for a final `Write it` / `Revise it` /
`Discard` before touching disk.
## Rules
- Requirements only: no implementation, no refactor, no staging as part of
this skill.
- Every requirement traces to evidence — a `file:line`, a failing path, a
missing artifact. A requirement whose justification is "would be nice" is
not written.
- Record the rejected alternatives inside the document; the next reader
needs to know what was already considered and dismissed.
- Close with the `kvb-check-confidence` skill's numbers for the analysis: how sure the
gaps are real, and what the passes did not cover.
-108
View File
@@ -1,108 +0,0 @@
---
name: kvb-git-branch-gc
description: >
Remove merged branches and worktrees with `pkgmgr branch gc`: dry run,
operator picks from the plan, then apply; unmerged items only one by one.
Trigger on /kvb-git-branch-gc, "branches aufräumen", "worktrees aufräumen",
"garbage collect".
---
`pkgmgr branch gc` decides what is merged and does the removing; this skill
decides what may go and who has to agree first. The dry run changes nothing,
and nothing is removed before the operator's pick in this run.
## Step 0: claim and chair
1. Claim `work/branch-gc-<repo>`, or `work/branch-gc-all` for every
repository, through the `kvb-org-organize` skill.
2. The apply takes stacks down and ends with a host-wide
`docker image prune -f` and `docker builder prune -f`, which reach every
session's images and build cache. When another session holds the chair,
send it what, where and how long, and wait for the grant; the prune never
runs during a peer's build.
## Step 1: dry run
`pkgmgr branch gc --json`, with `--all` only when the operator asked for every
repository, output to `/tmp/branch-gc-<N>.json`. No `git fetch` of your own
before it: gc fetches without auto-maintenance, while a plain fetch on git
2.50 or later may prune the sandbox's bind-mount stubs. Every repository's
`warnings` (a failed fetch) go to the top of the plan. Sort the items:
- **Candidates**: `state` `merged-clean`, non-empty `actions`, and `skip`
null or `lock: liveness unknown`.
- **Step 5**: `state` `unmerged` or `dirty`.
- **Reported only**: `protected`, every other `skip`, and `stale` as one count
per repository.
Never override a state: a branch without own commits is `unmerged` even
though git sees its tip on the default branch, because it is fresh work.
`host_pid_namespace: false` means the dry run ran inside a sandbox: it saw no
processes and could not tell whether a lock is live. Say so above the plan;
the apply re-checks both on the host.
## Step 2: claims
Read `council.py list`. For every candidate whose branch or worktree a peer's
claim names, send that peer one message (Peer protocol) and wait up to 5
minutes. A kept or unanswered item leaves the plan as `keep (<peer>)`;
silence is not consent.
## Step 3: plan
One table:
| Repo | Branch or worktree | Merged by | Actions | Note |
Then ask with `AskUserQuestion`:
- **Call 1** (`multiSelect: true`): the candidates, grouped by repository,
nothing preselected. Each description names `merged_by` and every action
(stack with volumes, the `images`, worktree, local and remote branches), and
`lock checked at apply` where liveness was unknown.
- **Call 2** (`multiSelect: false`): `Apply: <n> items (Recommended)`,
`Revise the selection`, `Abort`. Revise repeats call 1.
## Step 4: apply
1. Re-read `council.py list` and drop every item claimed since the dry run.
2. `--apply` refuses outside the host PID namespace (exit 2) and deletes
remote branches, which is a push. Unless this session runs in the host PID
namespace and the project lets agents push, hand the operator one command
per repository, run from inside it, one `--branch` per picked item (the
worktree path for a detached worktree):
```bash
pkgmgr branch gc --apply --json --branch <name> --branch <name> | tee /tmp/branch-gc-apply-<N>.json
```
3. Read every item's `result` in that file: `done`, `done; image kept: <img>`
(still in use elsewhere), or `failed: <why>`.
Report a failure as it stands; never remove by hand what gc skipped or
refused.
4. For every `done` item, drop its worktree and branch from the task that
lists them in the `kvb-org-rasci` register (`rasci.py artifacts` names
it): `set <task> --drop-artifact worktree:<path>`, and `--branch ''` where
the branch is gone.
## Step 5: unmerged and dirty
Never part of the apply. One `AskUserQuestion` question per item
(`multiSelect: false`): `Keep (Recommended)` and `Delete`, the description
naming what is lost (the item's `reason`: unique commits, dirty paths). For
each `Delete`, hand the operator
`pkgmgr branch purge --branch <name>`: it asks y/N per item and answers no
without a terminal, so it runs only in the operator's shell.
## Step 6: report
Removed, kept and skipped items, each with its reason, and
`docker system df` before and after. Report the release to the chair, tell
each peer whose line an item touched what went, and release the claim.
## Rules
- A pick from an earlier run does not carry over.
- Never widen the selection, and never route around a refusal through another
command or a peer.
-143
View File
@@ -1,143 +0,0 @@
---
name: kvb-git-changelog
description: >
Write the release message since the last release for `pkgmgr release`;
prints it only, never edits CHANGELOG.md, tags or releases. Trigger on
/kvb-git-changelog, "release message", "changelog entry", before cutting a release,
or on pkgmgr's markdown-lint rejection.
---
The message is one text that pkgmgr writes to four places: the CHANGELOG.md
entry, the annotated release tag, `debian/changelog` and the RPM `%changelog`.
Every rule below exists because one of those four would otherwise break or
mangle it.
## 1. Find the range
```bash
git fetch --tags
git describe --tags --abbrev=0 --match 'v[0-9]*.[0-9]*.[0-9]*'
git log --no-merges --format='%h %s%n%b' <tag>..HEAD
```
- pkgmgr tags every release `vX.Y.Z`; the newest one reachable from HEAD is
the base. No such tag: the range is the whole history, say so.
- Drop the `Release version X.Y.Z` commits and any revert together with the
commit it reverts inside the range.
- A subject is a lead, not the change: read the diff (`git show --stat`, then
the hunks) when the subject does not say what changed for the user.
- When `gh` is available, read the CI of the previous tag's commit and of HEAD,
every workflow (`gh run list --commit <sha> --json
workflowName,status,conclusion`), not one picked by `-L 1`.
- Previous tag: a workflow that finished as a failure means that version was
never published: say so under *Upgrade notes* and carry its upgrade notes
forward to this release.
- HEAD: a workflow that finished as a failure means pkgmgr would tag a
release that never goes stable, because its CI gate runs after the tag.
Queued, running or missing runs are normal; report only the failure, in
step 5.
## 2. Sort into sections
| Commit | Section |
| --- | --- |
| `feat` | Added, or Changed when it alters existing behaviour |
| `fix`, `perf` | Fixed |
| `!` or `BREAKING CHANGE:` | Breaking, plus one Upgrade notes line per required action |
| `docs`, `style`, `refactor`, `test`, `ci`, `build`, `chore` | omitted unless a user sees the effect |
| dependency bumps | one collapsed line, or omitted |
When nothing user-visible remains (a test- or CI-only release), keep those
commits under Fixed with an area prefix (`Tests:`, `CI:`) so the entry is not
empty.
Order: Breaking, Added, Changed, Fixed, Upgrade notes. Leave out empty
sections.
## 3. Format
```text
Fixed:
- Backup: rsync -a stored no xattrs, ACLs or hardlinks; now -aHAX
- Restore: *--empty* no longer aborts on a database with an extension
Upgrade notes:
- First backup re-copies every attribute-carrying volume once in full
```
1. **Section labels are plain lines ending in `:`**, never `#` headings and
never a bold-only line. pkgmgr creates the tag with `git tag -a -m`, whose
default `strip` cleanup deletes every line starting with `#`. A bold-only
line fails MD036.
2. **No version heading, date or "Release X.Y.Z" line.** pkgmgr adds
`## [X.Y.Z] - YYYY-MM-DD` itself.
3. **One bullet, one line. Too long means shorter, never wrapped.** RPM turns
every non-empty line into its own `-` entry and Debian indents each line,
so a continuation line becomes a separate, broken entry.
4. **Line length: the repository's MD013 limit**, which is markdownlint's
default of 80 unless the repo's config changes it. Aim for 78 or less, so
lines stay within 80 after Debian's two-space indent and RPM's two-character
`-` prefix.
5. **Lists use the marker CHANGELOG.md already uses**, no nesting, with a
blank line before and after every list (MD004, MD032). MD004's default
*consistent* style takes the marker of the first list in the file, and
pkgmgr puts the new entry on top, so a marker that differs from the older
entries turns every older list into a finding. Take the marker of the first
list item in the existing CHANGELOG.md; use `-` only when there is no
CHANGELOG.md yet, and the configured one when the repo's MD004 pins a style.
6. **No backticks and no code blocks.** pkgmgr rewrites inline code to
*italic* in all four targets, and a fenced block fails its fallback lint.
Write *names*, *--flags* and *paths* in `*` italics yourself; use `*`, not
`_`, for emphasis (MD049).
7. **Bullet text is `Area: effect`**: the component the user knows (Backup,
Restore, CLI, Tests), then what changed for them, not how the code
changed. No commit SHAs, no em dashes: use `:` or `;`.
8. **Plain hygiene**: no trailing whitespace (MD009), no double blank lines
(MD012), no bare URLs: wrap them as `<https://...>` (MD034).
## 4. Lint it the way pkgmgr does
pkgmgr wraps the message as `# Changelog`, blank line, `## [X.Y.Z] - date`,
blank line, message, then runs `markdownlint-cli2` with the target repo's own
config. Reproduce that exactly:
1. Write that wrapped document to a scratch file outside the repository.
2. From the repository root, so its config is picked up, run:
```bash
markdownlint-cli2 --no-globs - < <scratch-file>
```
*--no-globs* keeps the config's own globs from linting the whole tree too.
3. A finding on line N points at message line N - 4. Shorten that line and
lint again; print nothing until the result is 0 errors.
4. The standalone check cannot see rules that compare the entry with the rest
of the file (MD004 *consistent*, MD024 duplicate headings), but the
repository's own markdown lint and pre-push gate do. Copy CHANGELOG.md to a
scratch file, insert the wrapped heading and message below its
`# Changelog` line, and lint that copy the same way. A finding in the older
entries that the unmodified CHANGELOG.md does not produce comes from the
new entry; fix the message, never the older entries.
Without `markdownlint-cli2`, check by hand what pkgmgr's fallback checks plus
the length limit: no line over the limit, no trailing whitespace, no
backtick, no double blank line.
When the operator pastes pkgmgr's rejection (`changelog entry:7:81 error
MD013/line-length ...`), apply the same N - 4 mapping to the message they
used, shorten only the lines it names, and lint again.
## 5. Output
1. The message in one fenced `text` block, nothing else inside it.
2. One line: `Bump: patch | minor | major` and why (breaking → major,
`feat` → minor, otherwise patch).
3. One line with the lint result (`markdownlint-cli2: 0 errors` or the manual
check that ran).
4. Only when a workflow on HEAD finished as a failure: one line naming it and
its run, and the advice not to release before it is green.
Then stop. Never edit CHANGELOG.md or a version file, never run
`pkgmgr release`, never tag: the operator releases.
-36
View File
@@ -1,36 +0,0 @@
---
name: kvb-git-commit-next
description: >
Check every branch for the staged fix, commit the staged bundle with
--no-verify, then stage and brief the next bundle. Trigger on /kvb-git-commit-next
or a request to commit and stage the next bundle in one step.
---
1. `git fetch --all --prune --tags`, then hunt the staged fix across every
ref with the kvb-git-merge-fix skill's `find_fix.py` (by `--grep`, by `-S`, by
`--path`). Describe the staged bundle in one line first so a
wrong search intent is visible. Only the search runs here — never
cherry-pick or merge, and never touch the staging index or the tree.
2. A similar fix on another branch is a stop: run the kvb-think-dialectic skill on the
two candidates (staged version as thesis, foreign commit as antithesis)
until one survives, then present sha, branch, both diffs, the verdict and
the recommended move (keep staged / take theirs / combine). Wait for the
operator's decision; do not commit.
3. Nothing similar found, or the operator picked the staged version: run the
kvb-git-commit-no-verify skill on the currently staged changes (kvb-code-no-comments
audit and kvb-check-legal check first, then `git commit --no-verify`, exact
staging index only).
4. Group the remaining uncommitted changes into logical bundles (one
concern per bundle: same failure class, same mechanism, or same role)
and stage the next one; never mix concerns to save a commit. Staging
happens here and nowhere else: never stage merely to run a lint or a
test. Repository checks that enumerate through `git ls-files` already
see tracked, modified files and read the worktree; only an untracked
NEW file needs `git add` to become visible. Path-scope every `git add`
to the bundle — `git add -A` sweeps up the operator's own edits and
their local config.
5. For the newly staged bundle run the kvb-think-explain skill (what the changes do
and how the pieces interact, file:line-anchored) followed by the
kvb-check-confidence skill (fix confidence, break risk, evidence, residuals).
6. Close with what remains unstaged and wait for the operator's commit
decision; never auto-commit the newly staged bundle.
-15
View File
@@ -1,15 +0,0 @@
---
name: kvb-git-commit-no-verify
description: >
Commit the staged changes with --no-verify. Trigger on /kvb-git-commit-no-verify or
any request for a no-verify commit; invoking it IS the per-commit
authorization --no-verify requires, never carried over to later commits.
---
1. Run the kvb-code-no-comments audit over the staged and unstaged changes first,
then the advocate check over the staging index. An advocate blocker stops
here.
2. Commit exactly the staging index with `git commit --no-verify` and a
Conventional-Commits message derived from the staged diff; never add
unstaged files, never push.
3. Report the created commit hash and what remains uncommitted.
-80
View File
@@ -1,80 +0,0 @@
---
name: kvb-git-merge-fix
description: >
Hunt down the commits on any branch that carry a described fix and merge it
into the current branch. Trigger on /kvb-git-merge-fix <description> or when the
operator asks to bring over a fix from another branch.
---
The free text after `/kvb-git-merge-fix` is the fix description, exactly like `/loop`
takes its prompt. Everything after the skill name is the search intent; there is
no other input. If it is missing, ask for it and stop.
## 1. Refuse to work on a dirty tree
`git status --porcelain` must be empty. If it is not, stop and let the operator
commit or open a worktree. Never stash, never `git checkout -- <file>`, never
`git restore --staged`: those discard uncommitted work that other agents in the
same tree may own.
## 2. Fetch everything
```bash
git fetch --all --prune --tags
```
That is the "pull all branches" step. Do not check branches out one by one; all
searching below runs against the fetched refs.
## 3. Turn the description into search terms
Derive from the operator's text: 2-4 keywords for commit messages, plus any
symbol, string, or path the fix must have touched. State the derived terms in
one line so a wrong reading is visible before the search.
## 4. Search every ref
Search by keyword, by literal and by path in one call; they find different
things, and a commit matching any one of them is a hit, so `--path` widens
the search instead of narrowing `--grep`:
```bash
${CLAUDE_SKILL_DIR}/scripts/find_fix.py --grep '<keyword>' -S '<symbol or literal>' --path '<path or glob>'
```
It drops every hit already in HEAD (`in_head` counts them; all in HEAD means
the fix is already in, say so and stop) and prints the rest with their
branches and files. Then read the actual change: `git show --stat <sha>` and
the diff. A matching commit subject is a lead, not evidence, and `-S` also
matches the commit that removed the literal; the diff decides.
## 5. Present the candidates
One table: sha, branch(es), subject, files touched, one line on what the diff
really does. Let the operator pick unless exactly one candidate survives step 4
and its diff plainly is the described fix.
## 6. Bring it over
- One or a few commits, rest of the branch unwanted: `git cherry-pick -x <sha>…`
(`-x` records the origin).
- The whole branch is wanted: `git merge --no-ff <branch>`.
- Conflicts: resolve them by reading both sides. Never `-X ours`/`-X theirs`,
never `git checkout --ours/--theirs` wholesale — that silently drops one
side's fix. Unresolvable → `git cherry-pick --abort` / `git merge --abort`,
report what conflicted, change nothing else.
## 7. Report
Show `git log --oneline <base>..HEAD` and the resulting diffstat, then apply the
kvb-check-confidence skill: what came over, what did not, what still has to be verified.
Run the project's test or quality gate only when the operator asks for it.
## Rules
- Never push and never force anything; the merge stays local until the operator
ships it.
- Never rewrite history to make a cherry-pick fit (no rebase of the target
branch, no amend of foreign commits).
- If the search finds nothing, say so plainly and list the terms tried. Do not
reimplement the fix under this skill — that is a different task.
@@ -1,67 +0,0 @@
#!/usr/bin/env python3
"""Find commits on any ref that match a fix, skipping those already in HEAD.
Usage:
find_fix.py [--grep KEYWORD]... [-S LITERAL]... [--path PATH]... [--fetch]
Prints one line per candidate: sha, up to three branches, subject, files
touched. -S also matches commits that removed the literal; read the diff.
Run it inside the repository; it never checks anything out.
"""
from __future__ import annotations
import argparse
import subprocess
import sys
LIMIT = 30
def git(*args: str) -> subprocess.CompletedProcess:
return subprocess.run(["git", *args], capture_output=True, text=True, check=False)
def search(args: argparse.Namespace) -> list[str]:
queries = [["--regexp-ignore-case", f"--grep={k}"] for k in args.grep]
queries += [[f"-S{s}"] for s in args.S]
queries += [["--", p] for p in args.path]
found: list[str] = []
for query in queries:
for sha in git("log", "--all", "--format=%H", *query).stdout.split():
if sha not in found:
found.append(sha)
return found
def describe(sha: str) -> str:
subject = git("log", "-1", "--format=%h %s", sha).stdout.strip()
branches = git("branch", "-a", "--contains", sha, "--format=%(refname:short)").stdout.split()
files = [f for f in git("show", "--name-only", "--format=", sha).stdout.split("\n") if f]
shown = ",".join(branches[:3]) + (f",+{len(branches) - 3}" if len(branches) > 3 else "")
return f"{subject}\t{shown or '-'}\t{len(files)} files: {','.join(files[:5])}"
def main() -> int:
parser = argparse.ArgumentParser(prog="find_fix")
parser.add_argument("--grep", action="append", default=[])
parser.add_argument("-S", action="append", default=[])
parser.add_argument("--path", action="append", default=[])
parser.add_argument("--fetch", action="store_true")
args = parser.parse_args()
if not (args.grep or args.S or args.path):
parser.error("give at least one --grep, -S or --path")
if args.fetch:
git("fetch", "--all", "--prune", "--tags")
hits = search(args)
outside = [sha for sha in hits if git("merge-base", "--is-ancestor", sha, "HEAD").returncode]
print(f"hits={len(hits)} in_head={len(hits) - len(outside)} candidates={len(outside)}")
for sha in outside[:LIMIT]:
print(describe(sha))
if len(outside) > LIMIT:
print(f"... {len(outside) - LIMIT} more, narrow the search")
return 0
if __name__ == "__main__":
sys.exit(main())
-31
View File
@@ -1,31 +0,0 @@
---
name: kvb-org-artefacts
description: >
Artifacts of the kvb-org-rasci register per task: images, volumes,
worktrees. Trigger on /kvb-org-artefacts, "artefakte".
---
Artifacts live in the same table as the tasks of the `kvb-org-rasci`
register: each one belongs to exactly one task, carries its kind, reference
and the time it was recorded, and takes status and worker from its task. The
script is the register's `${CLAUDE_SKILL_DIR}/../kvb-org-rasci/scripts/rasci.py`.
```bash
rasci.py set <task> --artifact <kind>:<ref>
rasci.py set <task> --drop-artifact <kind>:<ref>
rasci.py artifacts [--ended] [--task <task>]
```
Kinds: `image:`, `container:`, `volume:`, `network:`, `stack:`, `worktree:`,
`dir:`, `log:`. `artifacts` lists each task's workdir and branch beside them.
## Rules
- Record an artifact the moment it exists, under the task that created it;
one left unrecorded is invisible to cleanup.
- `artifacts --ended` lists what belongs to `done`, `stopped` or `stale`
tasks: the cleanup candidates, owned by the task's worker. Everything of an
open task stays.
- Deleting goes through the `kvb-cleanup` skill, where the operator picks;
drop the artifact here once it is gone.
- The register's `show` counts each task's artifacts in its Artifacts column.
-12
View File
@@ -1,12 +0,0 @@
---
name: kvb-org-coordinate
description: >
Coordinate a failure or fix with the agents on related branches through
merges: claim, ask, report. Trigger on /kvb-org-coordinate and when a fix touches
code other branches share.
---
Run the `kvb-org-organize` skill's **Lineage** and **Peer protocol** sections for the
failure or fix at hand. They hold the whole procedure, including the entries
in the `kvb-org-rasci` register; this skill only keeps `/kvb-org-coordinate`
working.
-61
View File
@@ -1,61 +0,0 @@
---
name: kvb-org-matrix
description: >
Print one matrix of everything in flight: tasks, workflows, background jobs,
with confidence and break risk per item. Trigger on /kvb-org-matrix,
"status", "übersicht", "wo stehen wir", or what is done, left or running.
---
One-shot overview, no side effects: this skill reads state and reports it. It
never edits files, stages, commits, deploys, or starts work — the operator
decides what to act on after reading the matrix.
## Collect the state
Gather in parallel, skip any source the session does not have, and never invent
a row for a source you could not read:
1. **Tasks** — the `kvb-org-rasci` register first, then the session's todo list, the operator's stated goals this
session, and any requirement or plan documents the work follows.
2. **Working tree** — `git status --short` and `git log --oneline -10`:
uncommitted files are open work, commits since the branch point are closed
work. Untracked files count as open.
3. **Running** — background bash jobs, spawned agents not yet reported,
scheduled `/loop` wake-ups, and remote runs (CI, deploys). Poll their live
state; a job you started is not "running" because you started it.
4. **Remote** — open PRs and issues for the branch, and their check
conclusions, when a forge CLI is available.
## The matrix
One table, most-blocking first, one row per item:
| Item | State | Conf. | Break risk | Evidence / next step |
- **State**: `open`, `running`, `blocked`, `done`. `done` means verified —
a passing run, a green check, a reproduction that now succeeds. Work that
merely looks finished is `open`.
- **Conf.** and **Break risk**: two separate percentages per the `kvb-check-confidence`
skill — probability the item does what it claims, and probability it breaks
something else. Only for rows that changed something; `—` for pure
observation rows.
- **Evidence / next step**: for `done`, the strongest proof line (what ran,
what it returned). For everything else, the single next action.
Then at most three lines below the table:
- **Blocked on**: what needs the operator (a decision, a credential, an
approval) — omit when nothing does.
- **Residuals**: what keeps the lowest confidence number below 100.
- **Running**: what will report back on its own, so the operator does not wait
on it manually.
## Rules
- Cover everything in scope, including work the operator did not ask about —
a forgotten background job or an unpushed commit is exactly what the matrix
exists to surface.
- Report what the poll returned, never a predicted result for work still in
flight; a running item has no confidence number yet.
- Compress: one row per item, no per-item paragraphs. If a row needs
explaining, the operator will ask.
-211
View File
@@ -1,211 +0,0 @@
---
name: kvb-org-organize
description: >
Keep parallel agents from duplicating work: the first to claim it owns it;
line representatives and a resource chair coordinate. Run AUTOMATICALLY
before work another agent might also do (failure, cluster, feature, gate,
deploy, cleanup), and on /kvb-org-organize.
---
Two agents doing the same work waste the second run and then collide in the
merge. The remedy is an election nobody has to hold: whoever starts first owns
the work. The structure is anarcho-syndicalist: councils (soviets) of
delegates with a bound mandate, organised as a matrix, recallable by the
operator at any time.
## Offices
- **President of a piece of work**: the first agent that starts it. It does
the whole piece. Every later agent hands its findings to the president and
moves on to other work; it never builds a second version.
- **Representative of an organisational line**: the first agent that works in
the line. Lines come in as many dimensions as are useful: functional domain
(security, GUI, identity, i18n), repository, branch line, host, cloud
account. An agent belongs to one line per dimension.
- **Organiser of a work cluster**: the first agent that starts a cluster
cutting across lines (a failure cluster from `kvb-ci-triage`, a cost cluster from
`kvb-code-tune`, a feature touching several domains). It works the strand end to end
and informs the representative of every line the strand touches before it
changes that line's files.
- **Chair**: the earliest-elected president still running. It chairs the
council of all representatives and organisers and allocates the resources of
the whole system everyone runs on. There is exactly one allocator at any
time.
Every office is a mandate, not a rank: it coordinates and allocates, it never
takes over or directs the content of another agent's work. The operator
outranks every office and appoints or recalls at will; an operator
appointment outranks the election.
## Election
The first to claim wins, and the claim is atomic, so two agents can never both
be first. `${CLAUDE_SKILL_DIR}/scripts/council.py` keeps the claims in
`/tmp/agents-council`; other runtimes call the same script from their skills
directory with `--session <id>`.
1. **Name the office**: `work/<slug>`, `cluster/<slug>`,
`<dimension>/<line>` for a representative, or `chair`. The slug names the
work concretely: the issue number, the failing line, the cost's log line.
2. **Look first**: `council.py list`. A claim for the same work under another
slug counts as first.
3. **Claim**: `council.py claim <office> --work "<what, evidence, branch or
worktree>"`. Exit 0: elected, or already yours. Exit 3: a live agent holds
it, and its claim is printed. Exit 4: the holder's session is dead (see
Succession).
4. **Elected**: the claim entered the task in the `kvb-org-rasci` register,
the organisation's single point of truth; complete it there (objective,
description, priority, roles), then continue with the Peer protocol; its
message is the announcement.
5. **Not elected**: send the holder your findings and stop on that work.
A Claude Code session cannot edit a file in a git worktree before it holds a
claim: a PreToolUse hook (`council.py gate`) blocks the edit.
Peers on other machines cannot see this host's claims. Between machines the
earliest announcement by message time wins, and each host's claim directory
records the result.
## Chair and resource allocation
- The first agent elected on the system while `chair` is vacant also claims
`chair`, in the same way.
- An operator-appointed allocator holds the chair: its claim says
`appointed: operator`, and no election replaces it while its session runs and
the operator has not ended the appointment.
- Before starting anything that consumes shared capacity (gates, deploys,
iteration loops, swarm or cloud resources, cleanup of shared data), an agent
sends the chair what, where, for how long and its expected disk need, waits
for the grant, and reports the release. The chair keeps one queue, ordered
by the WSJF rank of the `kvb-org-wsjf` skill (it asks its subordinates for
the Fibonacci estimates), answers every request with its position, enforces the limits the operator set,
checks every grant with `watch.py admit --need-gb <N>` (or `--kind <slot>`;
test kinds also need CPU and RAM below the configured limits).
- A running job (deploy, gate, test, swarm run, build) is never cut short to
free capacity. The chair neither asks nor orders its owner to stop, take
down or kill it, not for an operator-prioritised slot and not for a
projected disk stop. It grants nothing new, frees idle items through the
`kvb-cleanup` flow, asks the operator for an exception, or waits. An owner's
own disk guard only alerts the chair and takes nothing down; only the
operator can order a running job down. The one exception is a hang: an
agent may end its own job, a stuck step inside it, or a process its own
ended session left behind, once at least 60 minutes passed without log
progress and a probe shows standstill (no fresh child process, no network
traffic, CPU time not moving). It secures the live state first and then
reports the timestamp, the step, the probe and the command. A peer's hung
job goes to its owner, and to the operator when the owner does not answer.
- Handover is explicit: the outgoing chair sends every peer the new holder,
the queue and the running grants, and keeps allocating until the incoming
chair confirms. Two allocators never run at once.
## Resource watch
The chair keeps the system from running out instead of reacting once it has.
`${CLAUDE_SKILL_DIR}/scripts/watch.py` does the measuring as hooks in every
session, so no agent probes the disk by hand; it reads its thresholds, job
patterns, cleanup commands and cache directories from
`~/.config/agents-council/config.json` (keys in the script's docstring) and
stays silent below the thresholds.
- **Warn**: the chair and every session get the biggest consumers; the chair
plans a cleanup and grants no large slot.
- **Stop**: new heavy jobs are blocked in every session (teardown commands
never), and the hook runs the configured low-risk cleanup by itself; it
skips every worktree where a gate or deploy process still runs.
- **Critical**: additionally a desktop alarm to the operator. Running jobs
and their stacks still keep running; only the operator can make an
exception.
- A measured fall that would reach the critical threshold within the
projection window raises the stage by one at once.
- Caches together stay under their budget; the least recently used go first.
`watch.py status` shows the last measurement. A `RESOURCE` line in a hook's
context is addressed to the chair; any other session only adjusts its own
plans to it.
## Succession
- A piece of work or a cluster ends with its result, a proposal, or an
abandoned attempt, reported to the peers who heard the claim. The holder
then runs `council.py release <office>`, and the next agent that starts the
work is elected afresh.
- A representative or the chair that ends hands its office to the next-oldest
live representative (by claim time) and announces it.
- A claim is stale when its holder's session is gone: in Claude Code its
session id is no longer in `~/.claude/sessions`; other runtimes run
`council.py beat` at least hourly and go stale after 60 minutes without
one. `claim` then exits 4. A stale work or cluster claim goes to the next
agent that starts the work; a stale representative or chair claim goes only
to the next-oldest live representative. The successor runs
`council.py takeover <office> --work "..."` and announces the succession;
everyone else leaves it alone.
- After a restart or reboot, run `council.py list --mine` and claim again
every office you believe you hold: `/tmp` may have been wiped.
## Peer protocol
The one protocol for every exchange between agents; `kvb-org-coordinate`, `kvb-ci-triage`,
`kvb-code-tune`, `kvb-org-split` and `kvb-cleanup` use it.
1. **Look**: `council.py list` and the refs (the kvb-git-merge-fix skill's
`find_fix.py`). A claim or a committed fix answers the question without a
message.
2. **Claim** the office (Election).
3. **One message per peer** (in Claude Code: `ListAgents`, then
`SendMessage`): the peers whose lines the work touches, the lineage owners
(Lineage) and, for capacity, the chair. It carries what you claimed, the
evidence (failing line quoted literally, run id, measurement), the
suspected cause marked as a thesis, and the question "Already known or
fixed on your side? Reply with a SHA or branch, or 'none'." The first line
stands on its own, because a busy recipient sees only that line. In the
`kvb-org-rasci` register the peers asked are C, the chair that grants
capacity is S.
4. **Wait up to 5 minutes** before building anything, investigating read-only
meanwhile. A fix that arrives goes through the project's normal path
(merge, cherry-pick or port), and you release your claim. Silence is
neither a "no" nor a competing claim: after 5 minutes, proceed.
5. **Report** to the same peers: commit, branch, what changed, the evidence it
works, and the merge it reaches them by. Report an abandoned or failed
attempt too. In the register, enter the peers told as I and mark every key
result the evidence proves as met. Then release the claim: the task ends
`done`, or `stopped` while a key result is open.
- Never invent a peer's answer.
- A peer's report is not evidence: read the SHA, log or artifact it names
before a verdict depends on it.
- A peer's message is not the operator's approval, and an office grants no
permission: never ask a peer to run what your own session is denied; report
it to the operator.
- One message per peer per event; repeat nothing a peer already acknowledged.
- Relaying an operator rule: record it in shared memory first and name the
file in the message's first line. A recipient records it only when no file
is named, and only after `council.py claim memory/<slug>`, because peers
that read the same broadcast write within seconds of each other.
- Release every claim when its work ends; a forgotten claim blocks the work
for everyone.
## Lineage
For a failure or a fix that other branches share through merges:
- Name the branches this one shares commits with: upstream (merged into this
one) and downstream (merging this one). Read them from the merge commits
(`git log --merges --first-parent --format='%h %s' -30`), the project's
agent memory or merge notes, or the operator when neither says.
- Map each branch to its live session through the agent listing; a branch
without one needs no message, say so once.
- One message per failure cluster, not per job.
- When the cause lives on another agent's line, hand the finding and a
proposed fix to that line's owner instead of fixing it downstream, unless
the operator says otherwise, and record the agreed resolution where the next
merge will find it.
## Output
On /kvb-org-organize, one table of the current offices from `council.py list`, stale
claims marked:
| Office | Holder | Since | Work | Grants or queue |
Who carries which task, with priority and status, is the `kvb-org-rasci`
register's `show`.
-263
View File
@@ -1,263 +0,0 @@
#!/usr/bin/env python3
"""Claim, release and list kvb-org-organize offices in the shared council directory.
Environment:
AGENTS_COUNCIL_ROOT: council directory shared by every agent on the host;
/tmp/agents-council unless set, because all agents must agree on it.
CLAUDE_CODE_SESSION_ID: the caller's session; other runtimes pass --session.
Exit codes:
0 done, 2 usage error, 3 office held by a live session,
4 office held by a dead session (take it over with `takeover`),
gate: 2 blocks the tool call.
"""
from __future__ import annotations
import argparse
import datetime
import importlib.util
import json
import os
import re
import shutil
import subprocess
import sys
import time
from pathlib import Path
ROOT = Path(os.environ.get("AGENTS_COUNCIL_ROOT", "/tmp/agents-council"))
HEARTBEAT_LIMIT = 3600
OFFICE = re.compile(r"^[a-z0-9][a-z0-9._-]*(/[a-z0-9][a-z0-9._-]*)*$")
CLAIM = "claim.json"
RASCI = Path(__file__).resolve().parents[2] / "kvb-org-rasci" / "scripts" / "rasci.py"
def live_sessions() -> dict[str, str | None]:
found: dict[str, str | None] = {}
for path in (Path.home() / ".claude" / "sessions").glob("*.json"):
try:
data = json.loads(path.read_text(encoding="utf-8"))
except (OSError, ValueError):
continue
if data.get("sessionId"):
found[data["sessionId"]] = data.get("name")
return found
def caller(session: str | None) -> tuple[str, str]:
if session:
return session, "other"
env = os.environ.get("CLAUDE_CODE_SESSION_ID")
if env:
return env, "claude-code"
print("council: no session id; pass --session", file=sys.stderr)
sys.exit(2)
def office_dir(office: str) -> Path:
if not OFFICE.match(office):
print(f"council: invalid office {office!r}", file=sys.stderr)
sys.exit(2)
return ROOT / office
def read_claim(path: Path) -> dict | None:
try:
return json.loads((path / CLAIM).read_text(encoding="utf-8"))
except (FileNotFoundError, ValueError):
return None
def is_stale(claim: dict | None, live: dict[str, str | None]) -> bool:
if claim is None:
return False
if claim["runtime"] == "claude-code":
return claim["session"] not in live
return time.time() - claim["beat"] > HEARTBEAT_LIMIT
def claims() -> list[tuple[str, dict]]:
found = []
for path in sorted(ROOT.glob(f"**/{CLAIM}")):
claim = read_claim(path.parent)
if claim is not None:
found.append((path.parent.relative_to(ROOT).as_posix(), claim))
return found
def write_claim(path: Path, office: str, sid: str, runtime: str, work: str) -> dict:
claim = {
"office": office,
"session": sid,
"runtime": runtime,
"name": live_sessions().get(sid),
"since": datetime.datetime.now(datetime.UTC).isoformat(timespec="seconds"),
"beat": time.time(),
"work": work,
}
(path / CLAIM).write_text(json.dumps(claim), encoding="utf-8")
return claim
def here() -> dict[str, str | None]:
try:
head = subprocess.run(
["git", "rev-parse", "--abbrev-ref", "HEAD"], capture_output=True, text=True, check=False
)
except OSError:
head = None
branch = head.stdout.strip() if head and head.returncode == 0 else ""
return {"workdir": os.getcwd(), "branch": branch if branch not in ("", "HEAD") else None}
def register(office: str, holder: str, status: str, **extra: str | None) -> None:
if not office.startswith(("work/", "cluster/")):
return
try:
spec = importlib.util.spec_from_file_location("rasci", RASCI)
rasci = importlib.util.module_from_spec(spec)
spec.loader.exec_module(rasci)
rasci.track(office, holder, status, **extra)
except (OSError, SystemExit) as exc:
print(f"council: {office} is missing from the rasci register ({exc})", file=sys.stderr)
def cmd_claim(args: argparse.Namespace) -> int:
sid, runtime = caller(args.session)
path = office_dir(args.office)
path.parent.mkdir(parents=True, exist_ok=True)
try:
path.mkdir()
except FileExistsError:
claim = read_claim(path)
if claim is not None and claim["session"] == sid:
claim = write_claim(path, args.office, sid, runtime, args.work)
register(args.office, claim["name"] or sid, "running", description=args.work, **here())
print(f"held {args.office}")
return 0
print(json.dumps(claim) if claim else f"{args.office}: claim in progress")
return 4 if is_stale(claim, live_sessions()) else 3
claim = write_claim(path, args.office, sid, runtime, args.work)
register(
args.office,
claim["name"] or sid,
"running",
description=args.work,
previous=getattr(args, "previous", None),
**here(),
)
print(f"elected {args.office}")
return 0
def cmd_takeover(args: argparse.Namespace) -> int:
path = office_dir(args.office)
claim = read_claim(path)
if path.exists() and not is_stale(claim, live_sessions()):
print(json.dumps(claim) if claim else f"{args.office}: claim in progress")
return 3
if claim is not None:
args.previous = claim["name"] or claim["session"]
# Two successors racing between this check and the rename can both win;
# kvb-org-organize names exactly one successor per office to keep that window closed.
if path.exists():
tomb = path.with_name(f"{path.name}.stale-{os.getpid()}-{time.time_ns()}")
try:
path.rename(tomb)
except FileNotFoundError:
pass
else:
shutil.rmtree(tomb)
return cmd_claim(args)
def cmd_release(args: argparse.Namespace) -> int:
sid, _ = caller(args.session)
path = office_dir(args.office)
claim = read_claim(path)
if claim is None:
print(f"{args.office}: not claimed")
return 0
if claim["session"] != sid and not is_stale(claim, live_sessions()):
print(json.dumps(claim))
return 3
shutil.rmtree(path)
register(args.office, claim["name"] or claim["session"], "done")
print(f"released {args.office}")
return 0
def cmd_beat(args: argparse.Namespace) -> int:
sid, _ = caller(args.session)
for office, claim in claims():
if claim["session"] == sid:
claim["beat"] = time.time()
(ROOT / office / CLAIM).write_text(json.dumps(claim), encoding="utf-8")
return 0
def cmd_list(args: argparse.Namespace) -> int:
live = live_sessions()
sid = None
if args.mine:
sid, _ = caller(args.session)
for office, claim in claims():
if sid is not None and claim["session"] != sid:
continue
state = "stale" if is_stale(claim, live) else "live"
holder = claim["name"] or claim["session"]
print(f"{office}\t{holder}\t{claim['since']}\t{state}\t{claim['work']}")
return 0
def in_git_worktree(target: Path) -> bool:
return any((path / ".git").exists() for path in (target, *target.parents))
def cmd_gate(_: argparse.Namespace) -> int:
payload = json.load(sys.stdin)
tool_input = payload.get("tool_input") or {}
target = tool_input.get("file_path") or tool_input.get("notebook_path")
if not target or not in_git_worktree(Path(target).resolve()):
return 0
if any(claim["session"] == payload.get("session_id") for _, claim in claims()):
return 0
script = Path(__file__).resolve()
print(
"kvb-org-organize: this session holds no claim, so it may not edit files in a git "
f"worktree. Claim the work first: python3 {script} claim work/<slug> "
"--work '<one line>'. Exit 3 or 4 means another agent holds it; read the "
"kvb-org-organize skill.",
file=sys.stderr,
)
return 2
def main() -> int:
parser = argparse.ArgumentParser(prog="council")
sub = parser.add_subparsers(dest="cmd", required=True)
for name, func in (("claim", cmd_claim), ("takeover", cmd_takeover)):
cmd = sub.add_parser(name)
cmd.add_argument("office")
cmd.add_argument("--work", required=True)
cmd.add_argument("--session")
cmd.set_defaults(func=func)
release = sub.add_parser("release")
release.add_argument("office")
release.add_argument("--session")
release.set_defaults(func=cmd_release)
beat = sub.add_parser("beat")
beat.add_argument("--session")
beat.set_defaults(func=cmd_beat)
listing = sub.add_parser("list")
listing.add_argument("--mine", action="store_true")
listing.add_argument("--session")
listing.set_defaults(func=cmd_list)
sub.add_parser("gate").set_defaults(func=cmd_gate)
args = parser.parse_args()
return args.func(args)
if __name__ == "__main__":
sys.exit(main())
-422
View File
@@ -1,422 +0,0 @@
#!/usr/bin/env python3
"""Watch disk, Docker and caches for the kvb-org-organize resource chair.
Usage:
watch.py watch hook (UserPromptSubmit, PostToolUse): measure at most once
per interval machine-wide, act on the stage, show alerts
watch.py block hook (PreToolUse on Bash): exit 2 for a heavy job while the
stage is stop or critical
watch.py admit (--need-gb N | --kind KIND)
exit 0 when the grant leaves at least the stop threshold
free, 3 otherwise
watch.py cleanup (--caches | --full)
trim caches to their budget; --full also runs the
configured low-risk cleanup commands
watch.py status print the last measurement
Environment:
AGENTS_COUNCIL_ROOT: shared state directory; /tmp/agents-council unless set,
because every agent on the host must agree on it.
AGENTS_COUNCIL_CONFIG: config file; ~/.config/agents-council/config.json
unless set. Without a config file every command exits 0 and does nothing.
Config keys, all required: disk, thresholds_gb {warn, stop, critical},
projection_minutes, interval_seconds, cache_interval_seconds, heavy_commands,
never_block (regexes), cleanup (shell commands), worktree_cleanup (shell
commands with {worktree}), worktrees_command (shell command printing the
running stacks' worktree paths, one per line), worktree_marker (path relative
to a worktree that must exist), busy_processes (process names; a worktree
where one of them runs is skipped), cache_dirs, cache_budget_percent,
slot_need_gb {kind: GB}, notify (argv the alert text is appended to),
load_limits {cpu_percent, mem_percent, sample_seconds, kinds}: admit for a
kind listed in kinds also needs CPU busy and RAM used below the limits.
"""
from __future__ import annotations
import argparse
import json
import os
import re
import shlex
import subprocess
import sys
import time
from pathlib import Path
ROOT = Path(os.environ.get("AGENTS_COUNCIL_ROOT", "/tmp/agents-council"))
CONFIG = Path(
os.environ.get(
"AGENTS_COUNCIL_CONFIG",
str(Path.home() / ".config" / "agents-council" / "config.json"),
)
)
STAGES = ("ok", "warn", "stop", "critical")
STATE = ROOT / "watch.json"
ALERTS = ROOT / "alerts.jsonl"
ALERT_TTL = 3600
LOCK_TTL = 1800
GB = 1e9
def load_config() -> dict | None:
if not CONFIG.is_file():
return None
return json.loads(CONFIG.read_text(encoding="utf-8"))
def disk(cfg: dict) -> tuple[float, float]:
st = os.statvfs(cfg["disk"])
return st.f_bavail * st.f_frsize / GB, st.f_blocks * st.f_frsize / GB
def read_json(path: Path) -> dict:
try:
return json.loads(path.read_text(encoding="utf-8"))
except (OSError, ValueError):
return {}
def write_json(path: Path, data: dict) -> None:
# A full disk refuses the write; the stage must still act, so it is dropped.
try:
path.parent.mkdir(parents=True, exist_ok=True)
tmp = path.with_name(f"{path.name}.{os.getpid()}")
tmp.write_text(json.dumps(data), encoding="utf-8")
tmp.replace(path)
except OSError:
pass
def stage_for(free: float, samples: list[list[float]], cfg: dict, now: float) -> str:
limits = cfg["thresholds_gb"]
level = 0
for index, name in ((1, "warn"), (2, "stop"), (3, "critical")):
if free < limits[name]:
level = index
window = cfg["projection_minutes"] * 60
recent = [s for s in samples if now - s[0] <= window and now > s[0]]
if recent and level < 3:
then, before = recent[0]
rate = (before - free) / (now - then)
if rate > 0 and (free - limits["critical"]) / rate < window:
level += 1
return STAGES[level]
def alert(stage: str, text: str) -> None:
try:
ROOT.mkdir(parents=True, exist_ok=True)
with ALERTS.open("a", encoding="utf-8") as handle:
handle.write(json.dumps({"id": time.time_ns(), "stage": stage, "text": text}) + "\n")
except OSError:
pass
def unseen_alerts(session: str) -> list[str]:
seen_path = ROOT / "seen" / session
try:
seen = int(seen_path.read_text(encoding="utf-8"))
except (OSError, ValueError):
seen = 0
try:
lines = ALERTS.read_text(encoding="utf-8").splitlines()
except OSError:
return []
cutoff = time.time_ns() - ALERT_TTL * 10**9
fresh = [json.loads(line) for line in lines if line]
fresh = [a for a in fresh if a["id"] > max(seen, cutoff)]
if fresh:
write_text(seen_path, str(fresh[-1]["id"]))
return [a["text"] for a in fresh]
def write_text(path: Path, text: str) -> None:
try:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(text, encoding="utf-8")
except OSError:
pass
def consumers() -> str:
try:
out = subprocess.run(
["docker", "system", "df", "--format", "{{.Type}}: {{.Size}} ({{.Reclaimable}} reclaimable)"],
capture_output=True, text=True, timeout=30, check=False,
).stdout.strip()
except (OSError, subprocess.TimeoutExpired):
return "docker system df unavailable"
return "; ".join(out.splitlines()) or "docker system df printed nothing"
def notify(cfg: dict, text: str) -> None:
try:
subprocess.run([*cfg["notify"], text], capture_output=True, timeout=10, check=False)
except (OSError, subprocess.TimeoutExpired):
alert("critical", f"RESOURCE: desktop notification failed: {cfg['notify'][0]}")
def spawn_cleanup(mode: str) -> None:
subprocess.Popen(
[sys.executable, str(Path(__file__).resolve()), "cleanup", mode],
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
start_new_session=True,
)
def act(cfg: dict, stage: str, previous: str, free: float) -> None:
limits = cfg["thresholds_gb"]
head = f"/ has {free:.1f} GB free (warn < {limits['warn']}, stop < {limits['stop']}, critical < {limits['critical']} GB)"
if STAGES.index(stage) < STAGES.index(previous):
alert(stage, f"RESOURCE {stage.upper()}: recovered, {head}.")
return
if stage == "warn":
alert(stage, f"RESOURCE WARN: {head}. Biggest consumers: {consumers()}. Chair: plan a cleanup; peers: request no large slot.")
elif stage in ("stop", "critical"):
alert(stage, f"RESOURCE {stage.upper()}: {head}. New heavy jobs are blocked; low-risk cleanup is running. {consumers()}")
spawn_cleanup("--full")
if stage == "critical":
notify(cfg, f"Agents council: {head}. Heavy jobs blocked; a running stack needs the operator's OK to go.")
def cmd_watch(cfg: dict) -> int:
payload = json.load(sys.stdin)
now = time.time()
state = read_json(STATE)
if now - state.get("checked", 0) >= cfg["interval_seconds"]:
free, _ = disk(cfg)
samples = [s for s in state.get("samples", []) if now - s[0] <= cfg["projection_minutes"] * 60]
stage = stage_for(free, samples, cfg, now)
previous = state.get("stage", "ok")
samples.append([now, free])
state.update(checked=now, free=free, stage=stage, samples=samples)
due = now - state.get("caches_checked", 0) >= cfg["cache_interval_seconds"]
if due:
state["caches_checked"] = now
write_json(STATE, state)
if stage != previous:
act(cfg, stage, previous, free)
elif due:
spawn_cleanup("--caches")
texts = unseen_alerts(payload["session_id"])
if texts:
print(json.dumps({"hookSpecificOutput": {
"hookEventName": payload["hook_event_name"],
"additionalContext": "\n".join(texts),
}}))
return 0
HEREDOC = re.compile(r"<<-?\s*(['\"]?)(\w+)\1[^\n]*\n.*?\n\s*\2(?=\s|\)|$)", re.DOTALL)
QUOTED = re.compile(r"'[^']*'|\"(?:\\.|[^\"\\])*\"")
def executable_text(command: str) -> str:
return QUOTED.sub("''", HEREDOC.sub("", command))
def cmd_block(cfg: dict) -> int:
payload = json.load(sys.stdin)
command = executable_text((payload.get("tool_input") or {}).get("command", ""))
stage = read_json(STATE).get("stage", "ok")
if stage not in ("stop", "critical"):
return 0
if any(re.search(p, command) for p in cfg["never_block"]):
return 0
if any(re.search(p, command) for p in cfg["heavy_commands"]):
print(
f"kvb-org-organize: resource stage is {stage}; new heavy jobs are blocked until "
"the disk recovers. Teardown commands still run. Ask the resource chair.",
file=sys.stderr,
)
return 2
return 0
def cpu_times() -> tuple[int, int]:
fields = [int(v) for v in Path("/proc/stat").read_text(encoding="utf-8").split("\n", 1)[0].split()[1:]]
return sum(fields), fields[3] + fields[4]
def cpu_busy_percent(before: tuple[int, int], after: tuple[int, int]) -> float:
total = after[0] - before[0]
idle = after[1] - before[1]
return 100.0 * (total - idle) / total
def mem_used_percent(meminfo: str) -> float:
values = {}
for line in meminfo.splitlines():
key, _, rest = line.partition(":")
if rest:
values[key] = int(rest.split()[0])
return 100.0 * (1 - values["MemAvailable"] / values["MemTotal"])
def load(cfg: dict) -> tuple[float, float]:
before = cpu_times()
time.sleep(cfg["load_limits"]["sample_seconds"])
cpu = cpu_busy_percent(before, cpu_times())
return cpu, mem_used_percent(Path("/proc/meminfo").read_text(encoding="utf-8"))
def cmd_admit(cfg: dict, args: argparse.Namespace) -> int:
need = args.need_gb if args.need_gb is not None else cfg["slot_need_gb"][args.kind]
free, _ = disk(cfg)
left = free - need
floor = cfg["thresholds_gb"]["stop"]
passed = [left >= floor]
report = [f"free {free:.1f} GB, need {need} GB, leaves {left:.1f} GB (floor {floor} GB)"]
limits = cfg["load_limits"]
if args.kind in limits["kinds"]:
cpu, mem = load(cfg)
passed += [cpu < limits["cpu_percent"], mem < limits["mem_percent"]]
report.append(
f"cpu {cpu:.0f} % (limit {limits['cpu_percent']}), ram {mem:.0f} % (limit {limits['mem_percent']})"
)
verdict = "grant" if all(passed) else "deny"
print(f"{verdict}: {'; '.join(report)}")
return 0 if verdict == "grant" else 3
def cache_files(dirs: list[str]) -> list[tuple[float, int, str]]:
files = []
for root in dirs:
for base, _, names in os.walk(os.path.expanduser(root)):
for name in names:
path = os.path.join(base, name)
try:
st = os.lstat(path)
except OSError:
continue
files.append((st.st_atime, st.st_size, path))
return files
def trim_caches(cfg: dict) -> float:
_, total = disk(cfg)
budget = total * GB * cfg["cache_budget_percent"] / 100
files = sorted(cache_files(cfg["cache_dirs"]))
used = sum(size for _, size, _ in files)
freed = 0
for _, size, path in files:
if used - freed <= budget:
break
try:
os.remove(path)
freed += size
except OSError:
continue
return freed / GB
def running_worktrees(cfg: dict) -> list[str]:
try:
out = subprocess.run(
cfg["worktrees_command"], shell=True, capture_output=True, text=True, timeout=60, check=False
).stdout
except subprocess.TimeoutExpired:
return []
trees = sorted({line.strip() for line in out.splitlines() if line.strip()})
return [t for t in trees if os.path.exists(os.path.join(t, cfg["worktree_marker"]))]
def busy(tree: str, names: list[str]) -> bool:
root = os.path.realpath(tree)
for pid in filter(str.isdigit, os.listdir("/proc")):
try:
if Path(f"/proc/{pid}/comm").read_text(encoding="utf-8").strip() not in names:
continue
cwd = os.readlink(f"/proc/{pid}/cwd")
except OSError:
continue
if cwd == root or cwd.startswith(root + os.sep):
return True
return False
def run_cleanup(cfg: dict) -> list[str]:
commands = list(cfg["cleanup"])
for tree in running_worktrees(cfg):
if busy(tree, cfg["busy_processes"]):
continue
commands += [t.format(worktree=shlex.quote(tree)) for t in cfg["worktree_cleanup"]]
results = []
for command in commands:
try:
code = subprocess.run(command, shell=True, capture_output=True, timeout=900, check=False).returncode
except subprocess.TimeoutExpired:
code = "timeout"
results.append(f"{'ok' if code == 0 else f'failed ({code})'}: {command}")
return results
def cmd_cleanup(cfg: dict, args: argparse.Namespace) -> int:
lock = ROOT / "cleanup.lock"
ROOT.mkdir(parents=True, exist_ok=True)
try:
if time.time() - lock.stat().st_mtime > LOCK_TTL:
lock.rmdir()
except FileNotFoundError:
pass
try:
lock.mkdir()
except FileExistsError:
return 0
try:
before, _ = disk(cfg)
results = run_cleanup(cfg) if args.full else []
cache_gb = trim_caches(cfg)
after, _ = disk(cfg)
failed = [r for r in results if not r.startswith("ok")]
if args.full or cache_gb:
alert(
"cleanup",
f"RESOURCE CLEANUP: freed {after - before:.1f} GB ({cache_gb:.1f} GB caches), "
f"/ now {after:.1f} GB free; {len(results) - len(failed)} commands ok"
+ (f", failed: {' | '.join(failed)}" if failed else "."),
)
finally:
lock.rmdir()
return 0
def cmd_status(cfg: dict) -> int:
state = read_json(STATE)
free, total = disk(cfg)
print(f"stage={state.get('stage', 'unmeasured')} free={free:.1f}GB total={total:.1f}GB samples={len(state.get('samples', []))}")
return 0
def main() -> int:
parser = argparse.ArgumentParser(prog="watch")
sub = parser.add_subparsers(dest="cmd", required=True)
sub.add_parser("watch")
sub.add_parser("block")
admit = sub.add_parser("admit")
need = admit.add_mutually_exclusive_group(required=True)
need.add_argument("--need-gb", type=float)
need.add_argument("--kind")
cleanup = sub.add_parser("cleanup")
mode = cleanup.add_mutually_exclusive_group(required=True)
mode.add_argument("--caches", action="store_true")
mode.add_argument("--full", action="store_true")
sub.add_parser("status")
args = parser.parse_args()
cfg = load_config()
if cfg is None:
return 0
if args.cmd == "watch":
return cmd_watch(cfg)
if args.cmd == "block":
return cmd_block(cfg)
if args.cmd == "admit":
return cmd_admit(cfg, args)
if args.cmd == "cleanup":
return cmd_cleanup(cfg, args)
return cmd_status(cfg)
if __name__ == "__main__":
sys.exit(main())
-74
View File
@@ -1,74 +0,0 @@
---
name: kvb-org-perform
description: >
Pick `model` and `effort` per subagent from a fixed table: cheaper only at
90 % confidence that the result equals the default's; log spawns and
quality. Apply AUTOMATICALLY before every Agent tool call.
---
A subagent runs on the main model and effort unless the spawn names others.
Set `model` and `effort` on an Agent call only when the task clearly matches a
row below and you are at least 90 % sure the cheaper setting delivers the same
result as the default: no lost findings, no rework. Cost counts per finished
task, retries included. Otherwise set neither, and the default stays.
## Table
| Task class | Subagent's task | `model` | `effort` | Evidence |
| --- | --- | --- | --- | --- |
| `lookup` | Read-only: find files, symbols or call sites, grep, list, read and quote, pull a log line or field, summarise a given text without judging it | `haiku` | `low` | Haiku 5.5 costs 1/40 of Opus 5.5 per token and is the speed tier; Anthropic names Haiku at low effort for simple, speed-critical and reading-heavy worker tasks; Explore and the cavecrew investigator already run on it |
| `mechanical-edit` | An edit the brief fully specifies (exact old and new text, a rename, deleting listed lines) in one or two files | `haiku` | `low` | nothing is left to decide, and the parent checks the diff |
| `judgement` | Everything else: root cause, skeptic or dialectic antithesis, review, security or legal checks, design, new or multi-file code, writing tests, performance work | omit | omit | no evidence at 90 % that a cheaper setting gives the same result |
Prices per million tokens in and out (claude-api reference, cached
2026-10-06): Haiku 5.5 $0.10 / $0.50, Sonnet 5.5 $2 / $10, Opus 5.5 $4 / $20,
Fable 5.1 $10 / $50.
## Spawn
1. Pick the class. A task that mixes classes, or whose class you hesitate
over, is `judgement`.
2. Start the brief with the line `task-class: <class>`; the spawn hook logs
it together with `model`, `effort` and the Agent `description`.
3. Set `model` and `effort` from the row; for `judgement` set neither.
4. Enter the spawn in the `kvb-org-rasci` register: the subagent as R and as
`--worker "subagent <type> (<model>, <effort>)"` of the task it serves.
- An agent type whose definition sets its own model (Explore, Plan, the
cavecrew agents) keeps it; never pass `model` or `effort` for it.
- Never pass `fable` or `opus`: neither is cheaper than the default.
- A Haiku agent that reports the task needs judgement, or returns an
incomplete result, is not retried on Haiku: the default continues it.
- Never change the default itself (settings, `CLAUDE_CODE_SUBAGENT_MODEL`,
agent definitions); this skill only chooses per call.
## Feedback
Once you have judged the subagent's result, rate it under the same Agent
`description`:
```bash
${CLAUDE_SKILL_DIR}/scripts/log.py feedback --description "<description>" --completed yes|no --tests pass|fail|none --redo yes|no
```
`--redo yes` when the default had to redo or finish the work. When the
operator later asks a follow-up question about that result or corrects it:
```bash
${CLAUDE_SKILL_DIR}/scripts/log.py followup --description "<description>" --text "<question or correction>"
```
All sessions share `~/.local/state/agents-council/subagent-model.jsonl`.
Inside a sandbox that cannot write it, the script prints the line as
`pending:` and the PostToolUse hook stores it.
## Evaluation
`log.py report` shows per task class, model and effort the runs, the rated
runs and the share with the same result: completed, not redone, no failing
test, no follow-up. A row is added, or a cheaper setting extended to a class,
only when the report shows at least 90 % over at least 20 rated runs, and only
after the operator confirms it (for example through `/kvb-skills-tune`). A row whose
rate falls below 90 % over 20 rated runs goes back to the default. `sonnet`
enters the table only this way, and a trial run on a setting the table does
not hold needs the operator's order.
-205
View File
@@ -1,205 +0,0 @@
#!/usr/bin/env python3
"""Log subagent spawns with their quality feedback, and evaluate the table.
Usage:
log.py spawn hook (PreToolUse on Agent): append the spawn line
log.py feedback --description D --completed yes|no --tests pass|fail|none --redo yes|no
rate the latest spawn with this Agent description
log.py followup --description D --text T
record an operator follow-up question or correction on it
log.py persist hook (PostToolUse on Bash): append the one line a sandboxed
feedback or followup printed as `pending: <json>`, only
when the command was that bare call and the line is
well-formed and from the hook's own session
log.py report runs, rated runs and same-result rate per task class,
model and effort; a row qualifies at >= 90 % over >= 20
Environment:
SUBAGENT_MODEL_LOG: the log shared by every session;
~/.local/state/agents-council/subagent-model.jsonl unless set.
CLAUDE_CODE_SESSION_ID: the session that rates its spawns.
"""
from __future__ import annotations
import argparse
import datetime
import json
import os
import re
import shlex
import sys
from pathlib import Path
LOG = Path(
os.environ.get(
"SUBAGENT_MODEL_LOG",
str(Path.home() / ".local" / "state" / "agents-council" / "subagent-model.jsonl"),
)
)
TASK_CLASS = re.compile(r"^task-class:\s*(\S+)", re.MULTILINE)
PENDING = "pending: "
SCRIPT = "kvb-org-perform/scripts/log.py"
CONTROL = re.compile(r"^[();<>|&]+$")
KEYS = {
"feedback": {"kind", "time", "session", "description", "completed", "tests", "redo"},
"followup": {"kind", "time", "session", "description", "text"},
}
QUALIFY_RUNS = 20
QUALIFY_RATE = 0.9
def now() -> str:
return datetime.datetime.now(datetime.UTC).isoformat(timespec="seconds")
def append(line: dict) -> None:
LOG.parent.mkdir(parents=True, exist_ok=True)
with LOG.open("a", encoding="utf-8") as handle:
handle.write(json.dumps(line, sort_keys=True) + "\n")
def record(line: dict) -> int:
try:
append(line)
except OSError:
print(PENDING + json.dumps(line, sort_keys=True))
return 0
def cmd_spawn() -> int:
payload = json.load(sys.stdin)
tool = payload.get("tool_input") or {}
match = TASK_CLASS.search(tool.get("prompt", ""))
append({
"kind": "spawn",
"time": now(),
"session": payload.get("session_id", ""),
"description": tool.get("description", ""),
"subagent_type": tool.get("subagent_type") or "general-purpose",
"task_class": match.group(1) if match else "unclassified",
"model": tool.get("model") or "default",
"effort": tool.get("effort") or "default",
})
return 0
def rated(kind: str, args: argparse.Namespace, **fields: object) -> dict:
session = os.environ.get("CLAUDE_CODE_SESSION_ID")
if not session:
sys.exit("log.py: CLAUDE_CODE_SESSION_ID is not set")
return {"kind": kind, "time": now(), "session": session, "description": args.description, **fields}
def invoked_kind(command: str) -> str | None:
if "$(" in command or "`" in command:
return None
lexer = shlex.shlex(command, posix=True, punctuation_chars=True)
lexer.whitespace_split = True
try:
tokens = list(lexer)
except ValueError:
return None
if any(CONTROL.match(token) for token in tokens):
return None
if tokens[:1] in (["python3"], ["python"]):
tokens = tokens[1:]
if len(tokens) < 2 or not tokens[0].endswith(SCRIPT) or tokens[1] not in KEYS:
return None
return tokens[1]
def well_formed(line: object, kind: str, session: str) -> bool:
if not isinstance(line, dict) or set(line) != KEYS[kind] or line["kind"] != kind or line["session"] != session:
return False
if not all(isinstance(line[key], str) for key in ("time", "description")):
return False
if kind == "feedback":
return isinstance(line["completed"], bool) and isinstance(line["redo"], bool) and line["tests"] in ("pass", "fail", "none")
return isinstance(line["text"], str)
def cmd_persist() -> int:
payload = json.load(sys.stdin)
kind = invoked_kind((payload.get("tool_input") or {}).get("command", ""))
if not kind:
return 0
response = payload.get("tool_response")
stdout = response.get("stdout", "") if isinstance(response, dict) else str(response or "")
pending = [text[len(PENDING):] for text in stdout.splitlines() if text.startswith(PENDING)]
if len(pending) != 1:
return 0
try:
line = json.loads(pending[0])
except json.JSONDecodeError:
return 0
if well_formed(line, kind, payload.get("session_id", "")):
append(line)
return 0
def runs() -> list[dict]:
current: dict[tuple[str, str], dict] = {}
found: list[dict] = []
for text in LOG.read_text(encoding="utf-8").splitlines() if LOG.exists() else []:
line = json.loads(text)
key = (line["session"], line["description"])
if line["kind"] == "spawn":
current[key] = {**line, "feedback": None, "followups": 0}
found.append(current[key])
elif key in current and line["kind"] == "feedback":
current[key]["feedback"] = line
elif key in current and line["kind"] == "followup":
current[key]["followups"] += 1
return found
def same_result(run: dict) -> bool:
feedback = run["feedback"]
return feedback["completed"] and not feedback["redo"] and feedback["tests"] != "fail" and not run["followups"]
def cmd_report() -> int:
rows: dict[tuple[str, str, str], list[dict]] = {}
for run in runs():
rows.setdefault((run["task_class"], run["model"], run["effort"]), []).append(run)
print("task_class\tmodel\teffort\truns\trated\tsame\trate\tqualifies")
for (task_class, model, effort), group in sorted(rows.items()):
rated_runs = [r for r in group if r["feedback"]]
same = sum(map(same_result, rated_runs))
rate = same / len(rated_runs) if rated_runs else 0.0
qualifies = len(rated_runs) >= QUALIFY_RUNS and rate >= QUALIFY_RATE
print(f"{task_class}\t{model}\t{effort}\t{len(group)}\t{len(rated_runs)}\t{same}\t{rate:.0%}\t{'yes' if qualifies else 'no'}")
return 0
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
sub = parser.add_subparsers(dest="command", required=True)
sub.add_parser("spawn")
feedback = sub.add_parser("feedback")
feedback.add_argument("--description", required=True)
feedback.add_argument("--completed", required=True, choices=("yes", "no"))
feedback.add_argument("--tests", required=True, choices=("pass", "fail", "none"))
feedback.add_argument("--redo", required=True, choices=("yes", "no"))
followup = sub.add_parser("followup")
followup.add_argument("--description", required=True)
followup.add_argument("--text", required=True)
sub.add_parser("persist")
sub.add_parser("report")
args = parser.parse_args()
if args.command == "spawn":
return cmd_spawn()
if args.command == "feedback":
return record(rated(
"feedback", args, completed=args.completed == "yes", tests=args.tests, redo=args.redo == "yes"
))
if args.command == "followup":
return record(rated("followup", args, text=args.text))
if args.command == "persist":
return cmd_persist()
return cmd_report()
if __name__ == "__main__":
sys.exit(main())
-63
View File
@@ -1,63 +0,0 @@
---
name: kvb-org-rasci
description: >
RASCI/OKR register of tasks: roles, key results, worker, status.
Trigger on /kvb-org-rasci, "rasci", "raci", "zuständigkeiten".
---
The register is the single point of truth of the organisation: every agent
records here what it works on, and claims, queue, handover and reports run
over it. `${CLAUDE_SKILL_DIR}/scripts/rasci.py` keeps it in
`<council root>/rasci.json`. A `kvb-org-organize` claim enters its task by
itself (holder as R and A, `running`, start, workdir, branch); its release
sets `done` and the end, or `stopped` while a key result is still open.
```bash
rasci.py set <task> --title <t> --objective <o> --description <d> --prio <n> --status queued|running|blocked|done|stopped --worker "<agent | subagent <type> (<model>)>" --workdir <dir> --branch <b> --artifact <kind>:<ref> --tool <script or command> [--start <iso>] [--end <iso>] [--drop-artifact <a>] [--drop-tool <t>]
rasci.py kr <task> add "<measurable result>" | met <n> | open <n> | rm <n>
rasci.py role <task> "<party>" <letters>
rasci.py get <task>
rasci.py rm <task>
rasci.py show [--all]
rasci.py artifacts [--ended] [--task <task>]
rasci.py estimate <task> "<party>" --value <fib> --complexity <fib>
rasci.py wsjf [--all]
rasci.py tools
```
## Objective and key results
The objective says what the task is for; its key results are the measurable
outcomes that prove it reached (a test green, a run passing, a release
published). `done` is refused while one is open; a task ended short of them is
`stopped`. `show` puts both in the OKR column as `<objective> (<met>/<all> KR)`.
## Roles
- **R Responsible**: does the task: the claim holder, or the subagent it runs.
- **A Accountable**: exactly one; takes the final decision and carries the
overall responsibility. The claim holder, unless the operator decides.
- **S Supportive**: actively helps carry it out: a peer's fix, the chair's
grant, a stack or runner.
- **C Consulted**: asked for input before acting: line representatives,
lineage owners, the operator on a pending decision.
- **I Informed**: kept up to date on progress or result, without a say.
`-` clears a party; the operator is the party `Operator`.
## Rules
- On start, `set` title, objective, description, priority and worker, `kr
add` every key result, and `role` every party beyond R and A; on each
change, update the status. The worker is the one session or subagent
executing the task right now.
- Artifacts share this table and follow the `kvb-org-artefacts` skill:
record each one when it appears, drop it once deleted. Add every script or
tool the task ran (`--tool`): `tools` ranks them for `kvb-skills-dream`.
- Priority is the rank of the `kvb-org-wsjf` skill: an operator's pin
(`--prio`) first, then WSJF from the estimates the chair collects; `show`
prints the rank as Prio and the score as WSJF.
- Write only your own rows; another session's task belongs to that session.
- On /kvb-org-rasci print `show` unchanged; its `gaps:` line names tasks
without exactly one A, without R, or open without a key result. `get`
prints one task in full, key results numbered.
-497
View File
@@ -1,497 +0,0 @@
#!/usr/bin/env python3
"""Keep the RASCI register of the organisation's tasks in a local JSON file.
Environment:
AGENTS_COUNCIL_ROOT: council directory shared by every agent on the host;
the register is <root>/rasci.json, and `show` also lists every work/
and cluster/ claim of the council there. /tmp/agents-council unless set.
Exit codes:
0 done, 2 usage error.
"""
from __future__ import annotations
import argparse
import contextlib
import datetime
import fcntl
import importlib.util
import json
import os
import re
import statistics
import sys
import tempfile
from collections.abc import Iterator
from pathlib import Path
ROOT = Path(os.environ.get("AGENTS_COUNCIL_ROOT", "/tmp/agents-council"))
DB = ROOT / "rasci.json"
LOCK = ROOT / "rasci.lock"
COUNCIL = Path(__file__).resolve().parents[2] / "kvb-org-organize" / "scripts" / "council.py"
TASK = re.compile(r"^[a-z0-9][a-z0-9._-]*(/[a-z0-9][a-z0-9._-]*)*$")
LETTERS = re.compile(r"^(?!.*(.).*\1)[RASCI]+$")
ORDER = "RASCI"
STATUSES = ("queued", "running", "blocked", "done", "stopped")
ENDED = ("done", "stopped")
FIELDS = (
"title", "objective", "description", "prio", "status", "worker", "start", "end", "workdir",
"branch",
)
TEXT_FIELDS = ("title", "objective", "description", "worker", "workdir", "branch")
LIST_FIELDS = {"tools": "tool"}
ARTIFACT = re.compile(r"^[a-z]+:\S")
FIBONACCI = (1, 2, 3, 5, 8, 13, 21, 34, 55, 89)
OPERATOR = "Operator"
def fail(message: str) -> None:
print(f"rasci: {message}", file=sys.stderr)
sys.exit(2)
def check_task(task: str) -> None:
if not TASK.match(task):
fail(f"invalid task {task!r}; use a slug like work/<slug>")
def stamp(value: str = "now") -> str:
if value == "now":
return datetime.datetime.now(datetime.UTC).isoformat(timespec="seconds")
try:
parsed = datetime.datetime.fromisoformat(value)
except ValueError:
fail(f"invalid time {value!r}; use ISO 8601 or now")
if parsed.tzinfo is None:
parsed = parsed.astimezone()
return parsed.astimezone(datetime.UTC).isoformat(timespec="seconds")
def letters_of(*parts: str) -> str:
return "".join(letter for letter in ORDER if any(letter in part for part in parts))
def load() -> dict[str, dict]:
try:
return json.loads(DB.read_text(encoding="utf-8"))
except FileNotFoundError:
return {}
except ValueError:
fail(f"{DB} is not valid JSON")
@contextlib.contextmanager
def database() -> Iterator[dict[str, dict]]:
ROOT.mkdir(parents=True, exist_ok=True)
with LOCK.open("a") as lock:
fcntl.flock(lock, fcntl.LOCK_EX)
tasks = load()
yield tasks
with tempfile.NamedTemporaryFile(
"w", dir=ROOT, prefix=".rasci-", delete=False, encoding="utf-8"
) as tmp:
json.dump(tasks, tmp, indent=1, sort_keys=True)
os.replace(tmp.name, DB)
def open_results(entry: dict) -> int:
return sum(not kr["met"] for kr in entry.get("key_results", []))
def apply_status(entry: dict, status: str) -> None:
if status not in ENDED:
entry.pop("end", None)
entry["status"] = status
if status == "running":
entry.setdefault("start", stamp())
if status in ENDED:
entry.setdefault("end", stamp())
def track(task: str, holder: str, status: str, *, description: str | None = None,
previous: str | None = None, workdir: str | None = None,
branch: str | None = None) -> None:
"""Write a council claim (status running) or release (status done) through.
Args:
task: the claimed office, e.g. work/<slug>.
holder: the session holding the claim.
status: running on claim and takeover, done on release; a release with
open key results ends as stopped.
description: the claim's --work line, kept unless one is recorded.
previous: the stale holder a takeover replaces; loses its R and A.
workdir: the claiming process's directory, kept unless one is recorded.
branch: the checked-out branch there, kept unless one is recorded.
"""
with database() as tasks:
entry = tasks.setdefault(task, {"roles": {}})
roles = entry.setdefault("roles", {})
if previous and previous != holder:
left = roles.pop(previous, "").replace("R", "").replace("A", "")
if left:
roles[previous] = left
if entry.get("worker") == previous:
entry["worker"] = holder
entry.setdefault("worker", holder)
for field, value in (("description", description), ("workdir", workdir), ("branch", branch)):
if value:
entry.setdefault(field, value)
if status == "done" and open_results(entry):
status = "stopped"
if status in ENDED or entry.get("status") in (None, "queued", *ENDED):
apply_status(entry, status)
if status not in ENDED:
accountable = any("A" in v for p, v in roles.items() if p != holder)
roles[holder] = letters_of(roles.get(holder, ""), "R" if accountable else "RA")
def claimed() -> dict[str, dict]:
if not COUNCIL.is_file():
return {}
spec = importlib.util.spec_from_file_location("council", COUNCIL)
council = importlib.util.module_from_spec(spec)
spec.loader.exec_module(council)
live = council.live_sessions()
rows = {}
for office, claim in council.claims():
if not office.startswith(("work/", "cluster/")):
continue
holder = claim["name"] or claim["session"]
rows[office] = {
"description": claim["work"],
"status": "claimed",
"worker": holder,
"start": claim["since"],
"roles": {holder: "RA"},
"stale": council.is_stale(claim, live),
}
return rows
def merged(include_done: bool) -> dict[str, dict]:
rows = claimed()
for task, entry in load().items():
base = rows.get(task, {"status": "queued", "roles": {}})
fields = {k: v for k, v in entry.items() if k != "roles"}
rows[task] = {**base, **fields, "roles": {**base["roles"], **entry.get("roles", {})}}
for row in rows.values():
if row.get("stale"):
row["status"] = "stale"
return {
task: row for task, row in rows.items() if include_done or row["status"] not in ENDED
}
def cell(text: object, width: int) -> str:
text = " ".join(str(text).split()).replace("|", "\\|")
return text if len(text) <= width else text[: width - 1] + "…"
def when(value: str | None) -> str:
if not value:
return "-"
return datetime.datetime.fromisoformat(value).astimezone().strftime("%m-%d %H:%M")
def wsjf(row: dict) -> tuple[float, float, float] | None:
estimates = list(row.get("estimates", {}).values())
if not estimates:
return None
value = statistics.median(e["value"] for e in estimates)
complexity = statistics.median(e["complexity"] for e in estimates)
return value, complexity, round(value / complexity, 2)
def ranked(rows: dict[str, dict]) -> list[str]:
def key(task: str) -> tuple:
row = rows[task]
score = wsjf(row)
pinned = row.get("prio")
return (pinned is None, pinned or 0, score is None, -(score[2] if score else 0), task)
return sorted(rows, key=key)
def okr(row: dict) -> str:
results = row.get("key_results", [])
text = cell(row.get("objective") or "-", 40)
if results:
text += f" ({sum(kr['met'] for kr in results)}/{len(results)} KR)"
return text
def gaps(task: str, row: dict) -> list[str]:
found = []
roles = row["roles"]
accountable = sum("A" in letters for letters in roles.values())
if accountable != 1:
found.append(f"{task} has {accountable} A")
if not any("R" in letters for letters in roles.values()):
found.append(f"{task} has no R")
if not row.get("key_results") and row["status"] not in ENDED:
found.append(f"{task} has no key result")
return found
def cmd_set(args: argparse.Namespace) -> int:
check_task(args.task)
if args.prio is not None and args.prio < 1:
fail("--prio must be 1 or higher")
times = {f: stamp(getattr(args, f)) for f in ("start", "end") if getattr(args, f)}
for ref in args.artifact:
if not ARTIFACT.match(ref):
fail(f"invalid artifact {ref!r}; use <kind>:<ref>, e.g. image:<name>:<tag>")
with database() as tasks:
entry = tasks.setdefault(args.task, {"roles": {}})
if args.status == "done" and open_results(entry):
fail(f"{args.task}: {open_results(entry)} key result(s) open; mark them met or set stopped")
for field in (*TEXT_FIELDS, "prio"):
value = getattr(args, field)
if value is not None:
entry[field] = value
if args.status:
apply_status(entry, args.status)
entry.update(times)
for field, flag in LIST_FIELDS.items():
drop = getattr(args, f"drop_{flag}")
items = [i for i in entry.get(field, []) if i not in drop]
items += [i for i in dict.fromkeys(getattr(args, flag)) if i not in items]
if items:
entry[field] = items
else:
entry.pop(field, None)
kept = [a for a in entry.get("artifacts", []) if a["ref"] not in args.drop_artifact]
known = {a["ref"] for a in kept}
kept += [{"ref": r, "added": stamp()} for r in dict.fromkeys(args.artifact) if r not in known]
if kept:
entry["artifacts"] = kept
else:
entry.pop("artifacts", None)
print(f"set {args.task}")
return 0
def cmd_role(args: argparse.Namespace) -> int:
check_task(args.task)
letters = args.letters.upper()
if letters != "-" and not LETTERS.match(letters):
fail("letters must be distinct ones of R, A, S, C, I, or - to clear")
with database() as tasks:
roles = tasks.setdefault(args.task, {"roles": {}}).setdefault("roles", {})
if letters == "-":
roles.pop(args.party, None)
else:
roles[args.party] = letters_of(letters)
print(f"{args.task}: {args.party} {letters}")
return 0
def cmd_kr(args: argparse.Namespace) -> int:
check_task(args.task)
with database() as tasks:
entry = tasks.setdefault(args.task, {"roles": {}})
results = entry.setdefault("key_results", [])
if args.action == "add":
if all(kr["text"] != args.value for kr in results):
results.append({"text": args.value, "met": False})
else:
if not args.value.isdigit() or not 1 <= int(args.value) <= len(results):
fail(f"{args.task}: no key result {args.value!r}; `get` lists them by number")
index = int(args.value) - 1
if args.action == "rm":
results.pop(index)
else:
results[index]["met"] = args.action == "met"
if not results:
entry.pop("key_results")
print(f"{args.task}: key result {args.action} {args.value}")
return 0
def cmd_get(args: argparse.Namespace) -> int:
row = merged(True).get(args.task)
if row is None:
fail(f"{args.task}: not in the register")
print(f"task: {args.task}")
for field in FIELDS:
if row.get(field) not in (None, ""):
print(f"{field}: {row[field]}")
for number, kr in enumerate(row.get("key_results", []), 1):
print(f"key result {number} [{'x' if kr['met'] else ' '}]: {kr['text']}")
for artifact in row.get("artifacts", []):
print(f"artifact: {artifact['ref']} (since {when(artifact['added'])})")
for field, flag in LIST_FIELDS.items():
for item in row.get(field, []):
print(f"{flag}: {item}")
roles = ", ".join(f"{p}={v}" for p, v in sorted(row["roles"].items()))
print(f"roles: {roles or '-'}")
for party, estimate in sorted(row.get("estimates", {}).items()):
print(f"estimate {party}: business value {estimate['value']}, complexity {estimate['complexity']}")
score = wsjf(row)
if score:
print(f"wsjf: {score[2]:g}")
return 0
def cmd_estimate(args: argparse.Namespace) -> int:
check_task(args.task)
if not args.drop:
for name in ("value", "complexity"):
if getattr(args, name) not in FIBONACCI:
fail(f"--{name} must be one of {', '.join(map(str, FIBONACCI))}")
with database() as tasks:
entry = tasks.setdefault(args.task, {"roles": {}})
estimates = entry.setdefault("estimates", {})
if args.drop:
estimates.pop(args.party, None)
else:
estimates[args.party] = {"value": args.value, "complexity": args.complexity, "at": stamp()}
if not estimates:
entry.pop("estimates")
print(f"{args.task}: estimate {'dropped' if args.drop else 'recorded'} for {args.party}")
return 0
def cmd_wsjf(args: argparse.Namespace) -> int:
rows = merged(args.all)
if not rows:
print("no tasks")
return 0
header = ["Rank", "Task", "OKR", "Business value", "Complexity", "WSJF", "Estimators", "Status"]
print("| " + " | ".join(header) + " |")
print("|" + "---|" * len(header))
unestimated = []
for rank, task in enumerate(ranked(rows), 1):
row = rows[task]
score = wsjf(row)
if score is None:
unestimated.append(task)
figures = [f"{n:g}" for n in score] if score else ["-", "-", "-"]
estimators = ", ".join(sorted(row.get("estimates", {}), key=str.lower)) or "-"
values = [rank, task, okr(row), *figures, cell(estimators, 40), row["status"]]
print("| " + " | ".join(str(v) for v in values) + " |")
if unestimated:
print("unestimated: " + ", ".join(unestimated))
return 0
def cmd_artifacts(args: argparse.Namespace) -> int:
rows = merged(True)
for task in sorted(rows):
row = rows[task]
if args.task and task != args.task:
continue
if args.ended and row["status"] not in (*ENDED, "stale"):
continue
places = [(f"{f}:{row[f]}", None) for f in ("workdir", "branch") if row.get(f)]
recorded = [(a["ref"], a["added"]) for a in row.get("artifacts", [])]
for ref, added in (*places, *recorded):
print(f"{row['status']}\t{task}\t{row.get('worker') or '-'}\t{ref}\t{when(added)}")
return 0
def cmd_tools(_: argparse.Namespace) -> int:
used: dict[str, list[str]] = {}
for task, row in merged(True).items():
for tool in row.get("tools", []):
used.setdefault(tool, []).append(task)
for tool, tasks in sorted(used.items(), key=lambda item: (-len(item[1]), item[0])):
print(f"{len(tasks)}\t{tool}\t{' '.join(sorted(tasks))}")
return 0
def cmd_rm(args: argparse.Namespace) -> int:
check_task(args.task)
with database() as tasks:
removed = tasks.pop(args.task, None)
print(f"removed {args.task}" if removed is not None else f"{args.task}: not in the register")
return 0
def cmd_show(args: argparse.Namespace) -> int:
rows = merged(args.all)
if not rows:
print("no tasks")
return 0
header = ["Prio", "WSJF", "Task", "OKR", "Status", "Worker", "Artifacts", "Start", "End", *ORDER]
print("| " + " | ".join(header) + " |")
print("|" + "---|" * len(header))
problems = []
for rank, task in enumerate(ranked(rows), 1):
row = rows[task]
title = f"{task}: {row['title']}" if row.get("title") else task
score = wsjf(row)
values = [
rank,
f"{score[2]:g}" if score else "-",
cell(title, 50),
okr(row),
row["status"],
cell(row.get("worker") or "-", 30),
len(row.get("artifacts", [])) or "-",
when(row.get("start")),
when(row.get("end")),
]
parties = sorted(row["roles"], key=lambda p: (p != OPERATOR, p.lower()))
values += [
cell(", ".join(p for p in parties if letter in row["roles"][p]), 30) for letter in ORDER
]
print("| " + " | ".join(str(v) for v in values) + " |")
problems += gaps(task, row)
if problems:
print("gaps: " + "; ".join(problems))
return 0
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
sub = parser.add_subparsers(dest="cmd", required=True)
p = sub.add_parser("set")
p.add_argument("task")
for field in (*TEXT_FIELDS, "start", "end"):
p.add_argument(f"--{field}")
p.add_argument("--prio", type=int)
for flag in (*LIST_FIELDS.values(), "artifact"):
p.add_argument(f"--{flag}", action="append", default=[])
p.add_argument(f"--drop-{flag}", action="append", default=[])
p.add_argument("--status", choices=STATUSES)
p.set_defaults(func=cmd_set)
p = sub.add_parser("estimate")
p.add_argument("task")
p.add_argument("party")
p.add_argument("--value", type=int)
p.add_argument("--complexity", type=int)
p.add_argument("--drop", action="store_true")
p.set_defaults(func=cmd_estimate)
p = sub.add_parser("wsjf")
p.add_argument("--all", action="store_true")
p.set_defaults(func=cmd_wsjf)
p = sub.add_parser("kr")
p.add_argument("task")
p.add_argument("action", choices=("add", "met", "open", "rm"))
p.add_argument("value")
p.set_defaults(func=cmd_kr)
p = sub.add_parser("role")
p.add_argument("task")
p.add_argument("party")
p.add_argument("letters")
p.set_defaults(func=cmd_role)
for name, func in (("get", cmd_get), ("rm", cmd_rm)):
p = sub.add_parser(name)
p.add_argument("task")
p.set_defaults(func=func)
p = sub.add_parser("show")
p.add_argument("--all", action="store_true")
p.set_defaults(func=cmd_show)
p = sub.add_parser("artifacts")
p.add_argument("--ended", action="store_true")
p.add_argument("--task")
p.set_defaults(func=cmd_artifacts)
sub.add_parser("tools").set_defaults(func=cmd_tools)
args = parser.parse_args()
return args.func(args)
if __name__ == "__main__":
sys.exit(main())
-120
View File
@@ -1,120 +0,0 @@
---
name: kvb-org-split
description: >
Keep agent work in its own git worktree until it reaches 90% fix confidence
and under 5% break risk; below that bar it becomes a proposal. Trigger when
delegating work to agents, when agents work in parallel, or on /kvb-org-split.
---
# Funnel
The main line only receives changes that have earned it. Everything else stays
in a sidetree until it does, or leaves as a proposal.
## The bar
- **Fix confidence of at least 90 %** and **break risk under 5 %**, both
reported by the `kvb-check-confidence` skill, the break risk derived through the
`kvb-check-break-risk` skill.
- The operator may set a different bar. Never lower it to make a change fit.
## The stages
1. **Brief.** One concern per agent, on the model the `kvb-org-perform` skill
picks, entered in the `kvb-org-rasci` register as R with `--worker "subagent
<type> (<model>)"`. Name the goal, the exact base commit, the
scope that may change, the evidence expected back, a time cap (default
60 minutes) at which it reports its interim state, and what the agent must
not do: no pushes, no gates or deploys
unless the resource chair of the `kvb-org-organize` skill granted them, read-only
probes on shared live systems.
2. **Sidetree.** The agent works in its own worktree on its own branch,
created from the exact base commit, so its edits never land under a running
gate or deploy in the main line. Use the host's worktree isolation when it
has one.
3. **Evidence.** The agent runs targeted tests, adds a test for the new
behaviour, and makes a negative control: the test fails without the change.
Numbers come from the same source before and after.
4. **Self-assessment.** The agent reports fix confidence and break risk with
their residuals and commits on its branch. Its parent marks the key results
that evidence proves as met in the `kvb-org-rasci` register and records
the branch and worktree as `--branch` and `--artifact worktree:<path>`.
5. **Independent review.** Before anything is merged, run the `kvb-think-dialectic` skill
with skeptics who did not write the change. A hole they find returns the
change to stage 3.
6. **Merge.** At or above the bar, merge the sidetree branch into the main line
following the repository's commit rules, then re-verify on the main line
with the project's gate.
7. **Tear everything down.** Once the branch is merged, remove everything the
sidetree brought into being, so the next round starts from a clean
machine. See "After the merge" below.
## After the merge
A merged sidetree leaves nothing behind. Work through the list in this order,
and verify each step instead of assuming it:
1. **Stop its jobs**: background runs, watchers, scheduled loops and agents
that belong to the sidetree.
2. **Tear down its stacks**: compose projects and swarm or test clusters
together with their volumes and networks, and the containers and images
built only for it. Leave anything another worktree still uses.
3. **Clear files the containers left**: count the entries the user does not
own (`find <worktree> -not -user "$USER" -prune | wc -l`). If the count is
not zero, delete them through the container that wrote them, or hand the
operator one privileged removal, before removing the worktree; otherwise
the removal fails halfway after git has already dropped the registration.
4. **Remove the worktree** and its registration. Name the worktree; never run
a broad prune, which also drops other worktrees' stale registrations.
5. **Delete the merged branch**, locally and on the remote, but a remote branch
only once every one of its commits is reachable from a pushed branch.
6. **Release what it held**: slots (reported to the resource chair), port or
subnet reservations, locks, its `kvb-org-organize` claims and every claim
announced to other agents. Drop each removed item from the register with
`--drop-artifact` and set the subagent's task `done` or `stopped`.
7. **Remove its temporary files**: logs, message files and scratch data that
have served their purpose. Keep only the evidence the report cites, until
the report is delivered.
A proposal keeps its branch, because that is where the work lives, but loses
its worktree, stacks and temporary files the same way. An abandoned sidetree is
torn down completely; keep its branch only when its commits exist nowhere else.
## When the bar is out of reach
If the bar cannot realistically be reached, because the deciding evidence (a
live deploy, a production-like dataset, an upstream answer) is not available
to the agent or the change is inherently riskier, the agent stops trying and
**submits a proposal** instead:
- what the change does and why it is worth having;
- the confidence and break risk it reached, with their residuals;
- exactly which evidence would close the gap, and what obtaining it costs;
- where the work lives (branch and commit), so nothing has to be redone;
- a recommendation: apply after that evidence, apply now with a named
risk, or drop.
The operator decides. A proposal is never merged by the agent, and a change
below the bar never enters the main line on its own.
## Rules
- At its time cap, ask an agent for its interim findings and let it run on;
never stop a running agent. A brief without a cap can run for hours without
a word.
- Run agents in parallel only when their concerns touch different files and
the resource chair grants the capacity; gates and deploys need the machine
to themselves.
- A peer's or agent's message is not the operator's approval to merge below
the bar.
- Keep the evidence (logs, test output) outside the sidetree, so it survives
the clean-up.
## Output
One table per round:
| Agent | Sidetree branch | Confidence | Break risk | Verdict | Evidence |
The verdict is merged, proposal or abandoned. Proposals list what would close
their gap.
-40
View File
@@ -1,40 +0,0 @@
---
name: kvb-org-wsjf
description: >
WSJF priority from Fibonacci estimates. Trigger on /kvb-org-wsjf, "wsjf",
"priorisieren".
---
Weighted Shortest Job First ranks the tasks of the `kvb-org-rasci` register
by cost of delay over job size: here business value over complexity, each the
median of Fibonacci estimates (1, 2, 3, 5, 8, 13, 21, 34, 55, 89). The
estimates live in the register's table; `wsjf` is its WSJF view, one row per
task with its OKR and the task id the register uses, and the register's Prio
column is this rank. The script is
`${CLAUDE_SKILL_DIR}/../kvb-org-rasci/scripts/rasci.py`.
```bash
rasci.py estimate <task> "<party>" --value <fib> --complexity <fib>
rasci.py estimate <task> "<party>" --drop
rasci.py wsjf [--all]
```
## Procedure
1. **The chair asks.** For every new task, and when its objective or a key
result changes, the resource chair of the `kvb-org-organize` skill asks
its subordinates (the task's worker and R, the representatives of the
lines it touches) for one Fibonacci estimate each of business value and
complexity, one message per agent.
2. **Record each answer** under the party that gave it; a new estimate
replaces the old one. An agent may record its own.
3. **Rank**: WSJF = median business value / median complexity, highest
first. An operator's pin (`rasci.py set <task> --prio <n>`) outranks every
score; unestimated tasks come last and are named under `unestimated:`.
4. **Grant in rank order**: the chair hands out capacity by this rank.
## Rules
- Estimate relative to the other open tasks, never in hours.
- Never invent an estimate for a party that did not answer; a task without
one stays unestimated.
-116
View File
@@ -1,116 +0,0 @@
---
name: kvb-skills-essence
description: >
Compact skills and memory to their essence after a health pass that finds
dead weight. Trigger on /kvb-skills-essence or when skills or MEMORY.md grow too long,
duplicate, or eat too much context.
---
Shorter, not thinner. Every fact that still earns its place survives; what goes
is repetition, narration, and entries nothing loads any more.
## What actually costs context
Compact by how often a thing is loaded, not by how large it is:
1. **`MEMORY.md`** and every skill's **`description`** frontmatter load into
*every* session. A wasted line here is paid on every single turn. Highest
leverage by a wide margin.
2. **Skill bodies** load when the skill fires. Worth tightening, paid only on use.
3. **Memory files** load on recall. Lowest leverage — and the place where
cutting evidence does the most damage.
A 200-line skill body that fires twice a month costs less than four bloated
`MEMORY.md` pointers. Rank the work accordingly and say so in the report.
## Step 1: ask for the scope
Never assume the target. Ask once with `AskUserQuestion` and
`multiSelect: true`, so the operator can tick several:
- **Skills repository** — the portable `skills/*/SKILL.md` set.
- **This project** — its `.claude/skills` and `.agents/skills`, plus the
project's memory directory and `MEMORY.md`.
- **Home** — `~/.claude` and `~/.agents` (skills, rules, commands, agents).
Take nothing outside what was ticked.
## Step 2: doctor
Before compacting anything, find what is already broken or dead. Report each
finding with its path:
- A `MEMORY.md` pointer whose file is gone, and a memory file no pointer names.
- A dead `[[wikilink]]` — no memory of that name exists.
- A memory asserting a `file:line`, function, or flag that no longer exists.
Verify against the current tree; a stale citation is worse than no memory,
because it reads as checked.
- The same rule stated in two or more skills. One is authoritative, the rest
are drift.
- A first-party skill shadowing a locked/vendored one.
- Frontmatter missing `name` or `description`, or a `description` that says
what the skill does but never when it fires — such a skill costs context in
every session and never triggers.
Dead entries are deleted, not compacted. Fix the broken ones or report them;
never compact around a stale citation, or the error survives in denser form.
## Step 3: compact
Cut:
- Anything the repository already records — code structure, git history, what
`CLAUDE.md`/`AGENTS.md` already say.
- A rule the agent follows anyway without being told.
- The second and third statement of one rule; keep the authoritative one and
point at it.
- Narration, hedging, and worked examples beyond the one that carries the point.
Keep, always:
- The **why** and the evidence behind a memory — the date, the incident, the
quoted correction. A rule stripped of its evidence degrades into an
exhortation and stops changing behaviour.
- Any fact with no other home. If it exists nowhere else, it is not redundancy.
- Trigger wording in a `description`. Tighten it, never drop the "when".
Dates stay absolute. Never introduce "recently" while shortening.
## Step 4: show the diff, then write
Writing happens in place — an essence that lives beside the original saves
nothing, because the original is still what loads.
Preconditions, per target:
- A git repository must have a clean status for the paths being rewritten, so
every edit is revertable with git alone. Not clean: report and stop.
- `~/.claude` and `~/.agents` are usually not under git. Copy each file to
`<file>.pre-essence` first.
Then present, per file: the diff, and the lines and characters saved. Ask for
one confirmation covering the batch. Write only after it.
## Step 5: verify
- Re-run the target's own validation if it has one (in a skills repository,
its test suite: frontmatter, lock, shadowing).
- Re-read each rewritten file and confirm every kept fact is still stated, not
merely implied by something shorter.
- Report the before/after loaded-context figure for the always-loaded surface
(`MEMORY.md` plus all descriptions) separately from the rest. That number is
the point of the run.
## Rules
- Deleting is the goal; rewriting shorter is the fallback. Prefer removing an
entry over polishing it.
- Never merge two memories whose evidence differs. Two incidents supporting one
rule are stronger than one; two rules sharing one incident is a fabrication.
- A skill that has never fired is a candidate for deletion, not compaction —
say so and let the operator decide.
- Nothing is written without the Step 4 confirmation, and nothing outside the
scope ticked in Step 1.
- `caveman-compress` is not a rival: it rewrites the wording of one named file,
this removes what should not be there at all. Run this one first — compressing
a dead entry is pure waste.
-93
View File
@@ -1,93 +0,0 @@
---
name: kvb-think-criticize
description: >
Give constructive criticism of a problem, plan, design, decision, text or
code, each point hardened by dialectic. Trigger on /kvb-think-criticize,
"kritisiere", "kritisch betrachten", "konstruktive Kritik", "was haeltst du
davon", or requests for honest feedback.
---
The stance is critical but constructive: the goal is a better result, not a
verdict on its author. This skill judges any artifact from many angles; it is
not limited to code (`kvb-check-principles` hunts violated engineering principles in code,
`code-review` hunts bugs). Read-only: report the critique, change nothing
unless the operator asks afterwards.
## Before you start: listen
Invoke the `kvb-think-listen` skill. Pin down what is being criticized, what it
is meant to achieve, for whom, and under which constraints. Derive what you can
from the artifact and its surroundings first; ask only what only the operator
knows. A critique against the wrong goal is noise.
## Steelman first
Restate the artifact and its intent in its strongest form, in two or three
sentences. Criticize that version, never a weaker one you made up. If you cannot
steelman it, you have not understood it yet; go back to listening.
## The aspects
Sweep every aspect that applies, and name the ones you skipped and why:
1. **Goal fit** - does it solve the stated problem for the stated audience, or
a neighbouring one?
2. **Correctness** - are the facts, the logic, and the claimed cause-effect
chains right? Which claim lacks evidence?
3. **Completeness** - missing cases, missing stakeholders, missing failure
paths, missing definition of done.
4. **Consistency** - contradictions inside the artifact or against its
neighbours; the same truth stated twice in two versions.
5. **Simplicity** - mechanism, scope, or wording the problem never asked for;
the smaller design that does the same.
6. **Feasibility and cost** - effort, dependencies, order of steps, what blocks
what, what it costs to run and to maintain.
7. **Risk** - security at trust boundaries, data loss, reversibility, blast
radius, what happens when it fails halfway.
8. **People** - usability, accessibility, the reader's or user's actual path,
who gains, who carries the burden.
9. **Alternatives** - at least one materially different approach, compared
honestly on the aspects above.
10. **Ethics and compliance** - only where it touches personal data, licences,
law, or fairness.
## Harden every point with dialectic
Run each candidate critique through the `kvb-think-dialectic` skill before reporting it:
- **Thesis**: the critique point with its evidence.
- **Antithesis**: independent defenders argue that the artifact is right as it
is, or that the constraint behind it is real; independent skeptics attack the
suggested improvement for new problems it would cause.
- **Synthesis**: keep the point, refine it, or drop it.
Batch minor points into one dialectic round; run consequential points (high
severity, expensive to act on, or challenging a premise) one by one.
## Output
One line of scope, then the steelman. Then:
- **Keep**: up to three strengths that must survive any change.
- **Critique**: one ranked table, highest impact first:
| Severity | Aspect | Observation | Why it matters | Suggestion | Evidence |
`Evidence` is `proven` (read, run, reproduced, cited) or `derived` (reasoned).
Close with at most three lines: how many candidate points dialectic dropped and
why, the single change that helps most, and the `kvb-check-confidence` skill's numbers for
the critique as a whole.
## Rules
- Every critique carries a concrete suggestion. A complaint without one becomes
an open question or is dropped.
- Criticize the work, never the person. Direct wording, no sarcasm, no
softening of a real problem, no inflation of a minor one.
- A point that attacks a stated constraint or goal is labelled "challenges a
premise" and kept apart from points that work within it.
- No taste or formatting nitpicks unless the operator asks for them.
- Answer in the operator's language.
- The critique ends at the report. Do not fix, stage, or rewrite as part of
this skill.
@@ -1,10 +1,11 @@
--- ---
name: kvb-check-security name: medusa
description: > description: >
Run the MEDUSA security scanner (medusa-security, PyPI) on a repository, Run the MEDUSA AI security scanner (medusa-security on PyPI) against a
working tree or secret stores. Use for security scans, vetting a repo before repository, working tree, or secret stores. Use when the user asks for a
cloning, leaked-secret detection in chat or shell history, or /kvb-check-security. security scan, supply-chain vetting of a repo before cloning, leaked-secret
Defensive only. detection in AI chat or shell history, or invokes /medusa. Defensive scanning
only.
--- ---
MEDUSA is a defensive AI-first security scanner (40,000+ patterns: CVEs, prompt injection, MCP tool poisoning, leaked secrets). MEDUSA is a defensive AI-first security scanner (40,000+ patterns: CVEs, prompt injection, MCP tool poisoning, leaked secrets).
@@ -1,10 +1,15 @@
--- ---
name: kvb-code-no-defaults name: no-defaults
description: > description: >
Forbid default values for parameters, settings and environment lookups; Forbid default values for parameters, settings, and environment lookups in
demand explicit values and fail loudly. Apply AUTOMATICALLY on every task every language. Load and apply AUTOMATICALLY on every task that writes,
that writes, edits, generates, refactors or reviews code, config, templates, edits, generates, refactors, or reviews code, config, templates, schemas,
schemas or infrastructure. or infrastructure — never wait to be asked. A default silently substitutes
a guess for a value the caller failed to supply, so a misconfiguration
becomes wrong behaviour instead of a loud failure. Demand strict, explicit
values and fail loudly when one is missing. `false`, `null` and `""` are the
only free defaults; every other default needs a written justification.
Portable across projects.
--- ---
A default value is a guess, written at the point where the caller's intent is A default value is a guess, written at the point where the caller's intent is
@@ -1,9 +1,10 @@
--- ---
name: kvb-ci-observe name: observe
description: > description: >
Watch a named target (CI run, pipeline, build, log) on a cadence and triage Watch a named target (a CI run, a pipeline, a build, a log) on a fixed
new failures as they appear. Trigger on /kvb-ci-observe <target> or when the cadence and apply the triage skill whenever new failures appear. Trigger on
operator asks to keep monitoring and fix regressions. /observe <target> or when the operator asks to keep monitoring something and
fix regressions as they surface. Portable across projects.
--- ---
Keep a target under continuous watch and act only when it degrades. The loop is Keep a target under continuous watch and act only when it degrades. The loop is
@@ -23,18 +24,17 @@ baseline, so a stale baseline re-triages solved failures.
Drive this with `/loop` in dynamic mode (no fixed interval unless the operator Drive this with `/loop` in dynamic mode (no fixed interval unless the operator
gave one): after each observation, schedule the next wake-up yourself and pass gave one): after each observation, schedule the next wake-up yourself and pass
the same `/kvb-ci-observe` prompt back so the next firing repeats the watch. the same `/observe` prompt back so the next firing repeats the watch.
1. **Poll the target.** Pull its current state (job list, check conclusions, the 1. **Poll the target.** Pull its current state (job list, check conclusions, the
log's tail); for a GitHub Actions run, the kvb-ci-artifacts skill's log's tail). Enumerate the failures exactly as the `triage` skill's step 1
`ci_failures.py <run>`. Enumerate the failures exactly as the `kvb-ci-triage` skill's step 1
does: failure, cancelled, timed-out; a downstream aggregate that fails only does: failure, cancelled, timed-out; a downstream aggregate that fails only
because an upstream did is not its own failure. because an upstream did is not its own failure.
2. **Diff against the baseline.** A failure is *new* only if it is not in the 2. **Diff against the baseline.** A failure is *new* only if it is not in the
baseline and was not already triaged this session. If nothing is new, log one baseline and was not already triaged this session. If nothing is new, log one
short line ("still green" / "N known failures, no change") and reschedule. short line ("still green" / "N known failures, no change") and reschedule.
3. **Triage each new failure.** Invoke the `kvb-ci-triage` skill on the new set only — 3. **Triage each new failure.** Invoke the `triage` skill on the new set only —
which applies `kvb-think-dialectic` per failing job to a ~99% root cause and a real which applies `dialectic` per failing job to a ~99% root cause and a real
fix. Never mask (no retry-until-pass, no disabled check, no soft-skip). Fold fix. Never mask (no retry-until-pass, no disabled check, no soft-skip). Fold
each newly fixed failure into the baseline so it is not re-triaged. each newly fixed failure into the baseline so it is not re-triaged.
4. **Reschedule.** Pick the next delay from what you are waiting on: an active 4. **Reschedule.** Pick the next delay from what you are waiting on: an active
@@ -50,7 +50,7 @@ the same `/kvb-ci-observe` prompt back so the next firing repeats the watch.
notification instead of polling for it; reserve the timed wake-up for external notification instead of polling for it; reserve the timed wake-up for external
state the harness cannot see (a remote CI run, a deploy queue). state the harness cannot see (a remote CI run, a deploy queue).
- Executed evidence beats reasoning: read the actual failing log line and the - Executed evidence beats reasoning: read the actual failing log line and the
artifact, not the job title — kvb-ci-triage and kvb-think-dialectic carry this through. artifact, not the job title — triage and dialectic carry this through.
- One baseline, updated in place: dedup every observation against all failures - One baseline, updated in place: dedup every observation against all failures
seen so far, or a flaky-then-fixed failure reappears every cycle and the watch seen so far, or a flaky-then-fixed failure reappears every cycle and the watch
never converges. never converges.
+40
View File
@@ -0,0 +1,40 @@
---
name: optimize
description: >
The shared objective of the self-improving skills: raise efficiency — fewer
tokens, less time — and raise quality at the same time. Trigger from autotune,
autoskill and confidence, which route their objective here, and on /optimize
when the operator asks how to make the agent's own work cheaper, faster or
better. Portable across projects.
---
Every skill, rule, or report the agent produces about its own working is judged
on three axes at once. A proposal that cannot name the axis it moves is not an
optimization, it is a preference.
## The three axes
- **Tokens** — fewer input and output tokens for the same result: read the range
instead of the whole file, grep the saved log instead of re-running the
command, route to an authoritative doc instead of duplicating it, report in
lines instead of paragraphs.
- **Time** — fewer round trips and less waiting: independent calls in parallel,
one command that answers the question instead of three that circle it, no
polling for work the harness will report on its own.
- **Quality** — fewer errors and less rework: verification instead of
assumption, and learnings that persist so the same mistake is not paid for
twice.
## Rules
- A change must improve at least one axis and damage none. Output that got
cheaper but lost correctness is a regression, not an optimization.
- Quality is never the currency: validation, error handling, tests, and security
steps are not what gets cut to save tokens or time.
- Measure, do not guess: name the observed friction — the re-read, the retry,
the correction the operator had to give twice — and what it cost. A guessed
percentage is not a measurement.
- The cheapest step is the one that does not run: drop work before optimizing
it.
- Persist it or pay again: an optimization that lives only in this session is
re-derived in the next one, so it belongs in a skill or a memory entry.
@@ -1,9 +1,10 @@
--- ---
name: kvb-create-plan name: plan
description: > description: >
Turn a request into an objective-oriented plan and hand it to the kvb-org-robot Turn a request into an objective-oriented plan and hand it to the robot skill
skill. Trigger on /kvb-create-plan or when the operator asks for a plan, breakdown or for autonomous execution. Trigger on /plan or when the operator asks for a
strategy before work starts. plan, a breakdown, or a strategy for a task before any work starts. Portable
across projects.
--- ---
Produce a plan whose every item is an outcome with a check, then execute it Produce a plan whose every item is an outcome with a check, then execute it
@@ -12,7 +13,7 @@ verified, and an unverifiable plan cannot be handed to a robot.
## Procedure ## Procedure
1. **Interview first.** Open every plan with the `kvb-think-listen` skill: ask 1. **Interview first.** Open every plan with the `active-listening` skill: ask
until scope, constraints, success criteria, and the operator-only facts are until scope, constraints, success criteria, and the operator-only facts are
all pinned, then reflect the understanding back. This is the only point in all pinned, then reflect the understanding back. This is the only point in
the run where questions are allowed, so leave nothing open here. the run where questions are allowed, so leave nothing open here.
@@ -28,13 +29,11 @@ verified, and an unverifiable plan cannot be handed to a robot.
the items that are independent, so they can run in parallel. the items that are independent, so they can run in parallel.
5. **Resolve the open decisions now.** Any root cause, design choice, or 5. **Resolve the open decisions now.** Any root cause, design choice, or
trade-off the plan rests on gets settled before execution, escalating to the trade-off the plan rests on gets settled before execution, escalating to the
`kvb-think-dialectic` skill where being wrong is expensive. The robot does not ask, so `dialectic` skill where being wrong is expensive. The robot does not ask, so
an unresolved decision becomes a guess at runtime. an unresolved decision becomes a guess at runtime.
6. **Record the plan in the register.** Enter the objective in the 6. **Record the plan as a todo list.** Write the sub-objectives into the
`kvb-org-rasci` register with each sub-objective's verification as a key harness's own todo tracking, one entry per item, phrased as the outcome. Keep
result, and mirror the sub-objectives into the harness's own todo tracking, a plan file only when the operator asks for one.
one entry per item, phrased as the outcome. Keep a plan file only when the
operator asks for one.
7. **Present it and wait.** Show the objective, the ordered sub-objectives with 7. **Present it and wait.** Show the objective, the ordered sub-objectives with
their verifications, what is deliberately out of scope, and the assumptions their verifications, what is deliberately out of scope, and the assumptions
the plan rests on. Stop here: the plan is the deliverable of this step. the plan rests on. Stop here: the plan is the deliverable of this step.
@@ -44,8 +43,8 @@ verified, and an unverifiable plan cannot be handed to a robot.
read the diff, run the linters, the unit tests, and whatever dry-run or read the diff, run the linters, the unit tests, and whatever dry-run or
syntax check the project offers. This step ends only when the whole plan syntax check the project offers. This step ends only when the whole plan
exists on disk, with no sub-objective left unwritten. exists on disk, with no sub-objective left unwritten.
9. **Then iterate under `kvb-org-robot`.** With the static implementation complete, 9. **Then iterate under `robot`.** With the static implementation complete,
invoke the `kvb-org-robot` skill with the objective as its goal for the dynamic part: invoke the `robot` skill with the objective as its goal for the dynamic part:
run, deploy, observe, fix, repeat until every verification passes. It drives run, deploy, observe, fix, repeat until every verification passes. It drives
the loop without check-ins, verifies each outcome instead of assuming it, and the loop without check-ins, verifies each outcome instead of assuming it, and
reports the items that fall outside its clearance for the operator to run. reports the items that fall outside its clearance for the operator to run.
@@ -1,9 +1,11 @@
--- ---
name: kvb-code-refactor name: refactor
description: > description: >
Refactor every uncommitted file (staged, unstaged, untracked) up to the Refactor every file that is currently uncommitted in the git working tree -
project's standards without changing behaviour. Trigger on /kvb-code-refactor or when staged, unstaged and untracked - up to the project's coding standards,
the operator asks to clean up, tidy or polish the current changes. without changing behaviour. Trigger on /refactor or when the operator asks to
clean up, tidy, or polish the current changes before committing. Portable
across projects.
--- ---
The unit of work is the uncommitted change set, not the repository. Behaviour The unit of work is the uncommitted change set, not the repository. Behaviour
@@ -80,7 +82,7 @@ Concretely:
returning early; replace magic values with a named constant at its use site. returning early; replace magic values with a named constant at its use site.
- **Errors**: never widen a caught exception, never swallow one, never remove a - **Errors**: never widen a caught exception, never swallow one, never remove a
validation or a security check to make the code shorter. validation or a security check to make the code shorter.
- **Comments**: invoke the `kvb-code-no-comments` skill rather than reimplementing its - **Comments**: invoke the `comments-clean` skill rather than reimplementing its
rules here. rules here.
Do not reformat regions you have no substantive fix for - cosmetic churn buries Do not reformat regions you have no substantive fix for - cosmetic churn buries
@@ -1,9 +1,10 @@
--- ---
name: kvb-org-robot name: robot
description: > description: >
Work fully autonomously to a goal: no check-ins, no stopping before the goal, Work fully autonomously to a goal. Trigger when the operator wants hands-off,
only commands already cleared. Trigger when the operator wants hands-off, uninterrupted execution — the agent drives the task to completion without
uninterrupted execution. check-ins, never stops before the goal is reached, and uses only commands it
is already cleared to run. Portable across projects.
--- ---
Operate as an autonomous robot: given a goal, reach it without hand-holding. Operate as an autonomous robot: given a goal, reach it without hand-holding.
@@ -11,7 +12,7 @@ Operate as an autonomous robot: given a goal, reach it without hand-holding.
## Contract ## Contract
- **No questions once the goal is clear.** Clarify only genuinely-blocking - **No questions once the goal is clear.** Clarify only genuinely-blocking
unknowns up front (via the `kvb-think-listen` skill), then proceed without unknowns up front (via the `active-listening` skill), then proceed without
further check-ins. Pick the cleanest option and continue; never pause to further check-ins. Pick the cleanest option and continue; never pause to
confirm a choice you can make yourself. confirm a choice you can make yourself.
- **Never stop before the goal.** Do not end the run, hand back, or declare done - **Never stop before the goal.** Do not end the run, hand back, or declare done
@@ -28,14 +29,10 @@ Operate as an autonomous robot: given a goal, reach it without hand-holding.
- **Verify, do not assume.** Reaching the goal means proving it: run it, read the - **Verify, do not assume.** Reaching the goal means proving it: run it, read the
output, confirm the end state. Never report success on a step you did not output, confirm the end state. Never report success on a step you did not
actually verify. actually verify.
- **Run it over the register.** Before the first step, enter the goal in the
`kvb-org-rasci` register as the task's objective and every check that proves
it as a key result; mark each one met as its proof comes in. The run is
finished when the task can be set `done`.
## Rules ## Rules
- Escalate an uncertain root cause or design decision to the `kvb-think-dialectic` skill - Escalate an uncertain root cause or design decision to the `dialectic` skill
rather than guessing; uncertainty is never a reason to stop. rather than guessing; uncertainty is never a reason to stop.
- Sustain the loop across long, multi-step work — the run is finished only when - Sustain the loop across long, multi-step work — the run is finished only when
the whole goal is verifiably met. the whole goal is verifiably met.
@@ -1,9 +1,10 @@
--- ---
name: kvb-skills-shortcuts name: shortcuts
description: > description: >
Operator conversation shortcuts. Trigger when the message is exactly one of Portable operator conversation shortcuts, reusable across every project.
these tokens, optionally with arguments: sobu, soco, socobu, socon, sona, Trigger when the user's message is exactly one of these tokens (optionally
sote. Expand it and act on the expanded instruction. followed by arguments): sobu, soco, socobu, socon, sona, sote. Expand the
shortcut to the listed meaning and act on the expanded instruction.
--- ---
When an operator message is one of the shortcuts below, expand it to the meaning and act on it. When an operator message is one of the shortcuts below, expand it to the meaning and act on it.
+95
View File
@@ -0,0 +1,95 @@
---
name: test-fix
description: >
Run the project's Makefile test targets in a loop - run, diagnose, fix, re-run -
until every selected target is green or the loop is provably stuck. Trigger on
/test-fix or when the operator asks to make the tests pass, fix the failing
tests, or get the suite green. Portable across projects.
---
Same target discovery and selection as the `test` skill - invoke it for step 1
and step 2 rather than reimplementing the grep and the selection list. This
skill owns what happens after the first red result: the fix loop.
The contract is narrow: the code becomes correct, the tests stay honest.
## The loop
One iteration is: run -> read the failure -> one root cause -> one fix ->
re-run. Never batch several speculative fixes into one iteration; a green run
after three simultaneous changes proves nothing about which one mattered.
### Run
Run the failing target alone, not the whole selection - the fastest command
that reproduces the failure. Widen back to the full selection only when that
target is green.
### Diagnose
Read the actual error output before touching a file. Apply the `triage` skill
to reach a verified root cause; for a failure whose cause is genuinely unclear
after one look, apply `dialectic` rather than guessing twice.
Decide explicitly which side is wrong, and say so in one line before editing:
- **Code is wrong**: the test states the intended behaviour. Fix the code.
- **Test is wrong**: the test encodes an outdated or incorrect expectation.
Fixing it is allowed *only* with a stated reason for why the old expectation
was wrong - never because the code disagrees with it.
- **Environment is wrong**: missing dependency, stale container, absent env var,
unbuilt artefact. Fix the environment or report it; do not patch code around
a broken environment.
### Fix
Smallest change that addresses the root cause. Follow the project's existing
idioms and the `no-defaults` and `comments-clean` rules.
Forbidden, in every iteration, without explicit operator approval:
- deleting, skipping, `xfail`-ing, or commenting out a failing test
- loosening an assertion to whatever the code currently produces
- catching or swallowing the exception the test was written to surface
- adding retries, sleeps, or reruns to paper over a flaky failure
- disabling a linter rule, a type check, or a test target from the selection
Any of these is a report item, not a fix. A suite that is green because the
failing test no longer runs is a regression disguised as success.
### Re-run
Re-run the same target. Then, once it passes, re-run every target that was
already green - a fix that breaks a neighbouring suite is not a fix. Only
after the full selection is green is the loop done.
## Stopping
Stop and report, without asking for permission to stop:
- **Green**: full selection passes. Report and stop.
- **No progress**: the same failure survives 3 iterations, or the failure count
stops falling across 3 iterations. Report the root cause reached so far, what
was tried, and why each attempt failed.
- **Fix exceeds the mandate**: the real fix is an API change, a dependency bump,
a schema migration, or a redesign. Name it, show the minimal diff it would
need, and let the operator decide.
- **Oscillation**: fixing A re-breaks B and vice versa. Report both with the
conflict between them - that is a design problem, not a test problem.
- **Flaky**: a target passes and fails without any change between runs. Report
it as flaky with both outputs; never "fix" it by rerunning until green.
Never loop silently. Emit one line per iteration: target, failure, hypothesis,
change made.
## Report
- Per iteration: what failed, the root cause, the fix, the result.
- Final state of every selected target, pass or fail, with the verbatim output
of anything still failing.
- Everything deliberately not done: tests judged wrong but left alone,
environment issues, out-of-mandate fixes.
- The exact command reproducing the final state.
State calibrated confidence in the fixes per the `confidence` skill. Do not
commit unless the operator asked for a commit.
+77
View File
@@ -0,0 +1,77 @@
---
name: test
description: >
Discover the test targets of the project's Makefile, let the operator pick
which ones to run, then run them and report. Trigger on /test or when the
operator asks to run the tests, the test suite, or a specific test target
without naming the exact command. Portable across projects.
---
The Makefile is the source of truth for how this project runs tests. Never
invent a command (`pytest`, `npm test`, `go test`) while a Makefile target
exists - the target carries the project's env vars, build dependencies and
container setup.
## Step 1: find the targets
From the repository root:
```bash
grep -nE '^test[A-Za-z0-9_.-]*:' Makefile
```
Read the matched rules plus their prerequisites, so the selection list can say
what each one actually does (delegated script, container build, sub-targets).
Edge cases, handled explicitly rather than guessed around:
- **No Makefile**: say so in one line, name the test runner the project does
use (from `pyproject.toml`, `package.json`, `tox.ini`, CI workflow), and ask
before running anything.
- **No `test*` target**: report the targets that do exist and stop.
- **Included makefiles** (`include foo.mk`): grep those too.
- **Aggregate targets**: a target whose recipe is only other test targets (e.g.
`test: test-unit test-integration`) is the "run everything" entry - mark it as
such in the list, do not expand it into its parts silently.
## Step 2: offer the selection
Ask with `AskUserQuestion`, `multiSelect: true`, one question. Options are the
discovered targets, each with a one-line description of what it runs. Order the
aggregate/full target first and label it as the complete suite.
The tool takes at most 4 options. With more targets than that:
- print the **complete** discovered list as text first, one line per target, so
nothing is hidden, then
- offer the 4 most useful entries (aggregate first, then the ones matching the
operator's stated intent or the files currently uncommitted), and note that
"Other" accepts any target name from the printed list.
Never silently truncate. If the operator already named a target in their
request, skip the question and run it.
## Step 3: run
Run the selected targets in the order the operator listed them, one `make`
invocation per target, each as its own command so a failure is attributable:
```bash
make <target>
```
Do not add `-k`, do not reorder, do not substitute a faster equivalent. If a
target needs a long timeout (container builds, e2e), set it on the Bash call
rather than backgrounding blindly.
Stop after the first failing target unless the operator asked for all of them -
a later suite running against a broken build produces noise, not information.
## Step 4: report
Per target: pass/fail plus the failing test names and the exact error output,
quoted verbatim. Never paraphrase a failure. Then one line with the exact
command to reproduce the failure alone.
Do not fix what failed unless the operator asks - report first. If the operator
does ask for a fix, apply the `triage` skill to it.
+57
View File
@@ -0,0 +1,57 @@
---
name: triage
description: >
Triage a failing CI run, pipeline, or job set to a verified root cause and a
real fix. Trigger when a build, test, or deploy pipeline has one or more failed
jobs. Applies the dialectic skill to every failing job independently. Portable
across projects.
---
Drive a red pipeline to green by root cause, not by guesswork. Treat every
failing job as its own investigation, and do not declare a fix until that job's
root cause is proven.
## Procedure
1. **Enumerate every failing job.** Pull the run's job list and isolate each job
whose conclusion is failure, cancelled, or timed-out. A downstream aggregate
job that fails only because an upstream job did is not a separate root cause —
note it and move on.
2. **Cluster the failures by error message.** Group the failing jobs by their
actual failing line, not by job name. Jobs whose messages differ in anything
but the interpolated identifiers (host, app, port, path) belong in separate
clusters.
3. **Download artifacts per cluster.** For each cluster, fetch every artifact of
one representative job: reports, rescue diagnostics, inventories, container
logs. Pull a second member's artifacts only when the representative's
evidence does not explain the whole cluster. The job log usually names an
artifact path and nothing more; the assertion text, the server-side
exception and the state dumps are inside the artifact. Do this even when the
log looks conclusive, because a log that explains the symptom rarely
explains the cause.
4. **Dialectic per cluster.** For EACH cluster, invoke the `dialectic` skill:
form a thesis about the root cause from evidence (the job log, its artifacts,
the code at the run's commit, git history), attack it with independent
skeptics, and iterate to a ~99% thesis. The clusters are independent, so run
their investigations in parallel where the tooling allows.
5. **Distinguish shared vs hidden causes.** A cluster is a hypothesis, not a
proof: if the dialectic shows one cluster splitting into two causes, split it.
When a job hides a second failure behind the first, keep going until the job
is actually green, not just past the first error.
6. **Fix at the root.** Apply the real fix in the repository for each proven root
cause. Never mask a failure — no retry-until-pass, no disabling the check, no
soft-skip. If a failure is genuinely external (upstream outage, flaky infra),
confirm that with evidence and surface it honestly instead of fixing around
it.
7. **Follow the run.** While the run is still in progress, re-check it
periodically and triage each newly failed job as it appears. The run is done
only when it has finished and every failure has a verified fix.
## Rules
- Executed evidence beats reasoning: read the actual failing log line and the
artifact, not the job title.
- Classify each failure: a real defect (fix it), a flake or infra hiccup (prove
it is transient before dismissing), or already-fixed-by-a-later-commit (verify
the fix is present in the run's commit before claiming it).
- Report a per-job verdict: root cause, the evidence that proves it, and the fix.
+10 -10
View File
@@ -1,4 +1,4 @@
"""Validate the kvb-skills-tune reminder hook and its settings registration.""" """Validate the autotune reminder hook and its settings registration."""
from __future__ import annotations from __future__ import annotations
@@ -10,8 +10,8 @@ import unittest
from pathlib import Path from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1] REPO_ROOT = Path(__file__).resolve().parents[1]
SKILL = REPO_ROOT / "skills" / "kvb-skills-tune" / "SKILL.md" SKILL = REPO_ROOT / "skills" / "autotune" / "SKILL.md"
HOOK = REPO_ROOT / "skills" / "kvb-skills-tune" / "hooks" / "reminder.sh" HOOK = REPO_ROOT / "skills" / "autotune" / "hooks" / "reminder.sh"
PATCHER = REPO_ROOT / "scripts" / "enable-plugins.js" PATCHER = REPO_ROOT / "scripts" / "enable-plugins.js"
@@ -33,7 +33,7 @@ def _run_hook(stamp: Path, interval: str = "3600") -> subprocess.CompletedProces
class TestAutotuneSkill(unittest.TestCase): class TestAutotuneSkill(unittest.TestCase):
def test_skill_is_trigger_only(self): def test_skill_is_trigger_only(self):
text = SKILL.read_text(encoding="utf-8") text = SKILL.read_text(encoding="utf-8")
self.assertIn("name: kvb-skills-tune", text) self.assertIn("name: autotune", text)
self.assertIn("multiSelect: false", text) self.assertIn("multiSelect: false", text)
@@ -51,7 +51,7 @@ class TestSkillPersistence(unittest.TestCase):
self.assertIn(target, text) self.assertIn(target, text)
def test_autoskill_routes_to_the_same_persistence(self): def test_autoskill_routes_to_the_same_persistence(self):
text = (REPO_ROOT / "skills" / "kvb-skills-auto" / "SKILL.md").read_text( text = (REPO_ROOT / "skills" / "autoskill" / "SKILL.md").read_text(
encoding="utf-8" encoding="utf-8"
) )
for target in ("Persistence", "~/.claude/skills/", "~/.agents/skills/"): for target in ("Persistence", "~/.claude/skills/", "~/.agents/skills/"):
@@ -66,7 +66,7 @@ class TestAutotuneHook(unittest.TestCase):
def test_first_run_hints_and_stamps(self): def test_first_run_hints_and_stamps(self):
result = _run_hook(self.stamp) result = _run_hook(self.stamp)
self.assertIn("/kvb-skills-tune", result.stdout) self.assertIn("/autotune", result.stdout)
self.assertTrue(self.stamp.is_file()) self.assertTrue(self.stamp.is_file())
def test_second_run_is_silent_within_interval(self): def test_second_run_is_silent_within_interval(self):
@@ -75,12 +75,12 @@ class TestAutotuneHook(unittest.TestCase):
def test_hint_returns_after_the_interval(self): def test_hint_returns_after_the_interval(self):
_run_hook(self.stamp) _run_hook(self.stamp)
self.assertIn("/kvb-skills-tune", _run_hook(self.stamp, interval="0").stdout) self.assertIn("/autotune", _run_hook(self.stamp, interval="0").stdout)
def test_corrupt_stamp_does_not_crash(self): def test_corrupt_stamp_does_not_crash(self):
self.stamp.parent.mkdir(parents=True, exist_ok=True) self.stamp.parent.mkdir(parents=True, exist_ok=True)
self.stamp.write_text("not-a-timestamp\n", encoding="utf-8") self.stamp.write_text("not-a-timestamp\n", encoding="utf-8")
self.assertIn("/kvb-skills-tune", _run_hook(self.stamp).stdout) self.assertIn("/autotune", _run_hook(self.stamp).stdout)
@unittest.skipUnless(shutil.which("node"), "node not installed") @unittest.skipUnless(shutil.which("node"), "node not installed")
@@ -100,7 +100,7 @@ class TestSettingsRegistration(unittest.TestCase):
self._patch() self._patch()
data = self._patch() data = self._patch()
entries = json.dumps(data["hooks"]["UserPromptSubmit"]) entries = json.dumps(data["hooks"]["UserPromptSubmit"])
self.assertEqual(entries.count("kvb-skills-tune/hooks/reminder.sh"), 1) self.assertEqual(entries.count("autotune/hooks/reminder.sh"), 1)
def test_existing_hooks_preserved(self): def test_existing_hooks_preserved(self):
self.settings.write_text( self.settings.write_text(
@@ -116,7 +116,7 @@ class TestSettingsRegistration(unittest.TestCase):
encoding="utf-8", encoding="utf-8",
) )
data = self._patch() data = self._patch()
self.assertEqual(len(data["hooks"]["UserPromptSubmit"]), 4) self.assertEqual(len(data["hooks"]["UserPromptSubmit"]), 3)
if __name__ == "__main__": if __name__ == "__main__":
-110
View File
@@ -1,110 +0,0 @@
"""Exercise ci_failures clustering and find_fix's ref search."""
from __future__ import annotations
import importlib.util
import shutil
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
CI_FAILURES = REPO_ROOT / "skills" / "kvb-ci-artifacts" / "scripts" / "ci_failures.py"
FIND_FIX = REPO_ROOT / "skills" / "kvb-git-merge-fix" / "scripts" / "find_fix.py"
def _load(path: Path):
spec = importlib.util.spec_from_file_location(path.stem, path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
class TestCiFailures(unittest.TestCase):
def setUp(self):
self.ci = _load(CI_FAILURES)
def test_jobs_differing_only_in_identifiers_share_a_cluster(self):
one = "2026-10-30T10:00:00.1Z setup ok\n##[error]Timeout on host 10.0.0.12 after 300s\n"
two = "\x1b[31m2026-10-30T11:00:00.1Z ##[error]Timeout on host 10.0.0.47 after 299s\x1b[0m\n"
other = "PLAY RECAP failed=0\nfatal: unable to reach registry\n"
groups = self.ci.cluster([("a", one), ("b", two), ("c", other)])
self.assertEqual(list(groups.values()), [["a", "b"], ["c"]])
def test_command_echo_and_retries_are_skipped_and_task_named(self):
text = (
'##[group]Run make deploy || echo "failed"\n'
"make deploy\n"
"##[endgroup]\n"
"FAILED - RETRYING: wait (3 retries left)\n"
"TASK [web-app-y : Probe] ****\n"
"Traceback (most recent call last): recovered on retry\n"
"TASK [web-app-x : Wait for health] ****\n"
'fatal: [localhost]: FAILED! => {"msg": "timeout 120"}\n'
"##[error]Process completed with exit code 2.\n"
)
self.assertEqual(
self.ci.signature(text),
'TASK [web-app-x : Wait for health] **** fatal: [localhost]: FAILED! => {"msg": "timeout <n>"}',
)
def test_lint_finding_beats_make_error_and_later_steps(self):
text = (
"Ran 41 tests in 1.0s\n"
"OK\n"
"UP017 [*] Use `datetime.UTC` alias\n"
" --> skills/kvb-org-organize/scripts/council.py:92:40\n"
"Found 1 error.\n"
"make: *** [Makefile:15: test] Error 1\n"
"##[error]Process completed with exit code 2.\n"
"Post job cleanup.\n"
"SHA256 digest of uploaded artifact is 0123456789abcdef\n"
)
self.assertEqual(self.ci.signature(text), "UP<n> [*] Use `datetime.UTC` alias")
def test_generic_exit_code_line_is_not_the_signature(self):
text = "Error: disk full\n##[error]Process completed with exit code 1.\n"
self.assertEqual(self.ci.signature(text), "Error: disk full")
@unittest.skipUnless(shutil.which("git"), "git not installed")
class TestFindFix(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.addCleanup(self.tmp.cleanup)
self.repo = Path(self.tmp.name)
self.env = {
"PATH": "/usr/bin:/bin",
"HOME": str(self.repo),
"GIT_AUTHOR_NAME": "t",
"GIT_AUTHOR_EMAIL": "t@t",
"GIT_COMMITTER_NAME": "t",
"GIT_COMMITTER_EMAIL": "t@t",
}
self._git("init", "-q", "-b", "main")
self._git("commit", "-q", "--allow-empty", "-m", "fix: probe in head")
self._git("switch", "-q", "-c", "side")
self._git("commit", "-q", "--allow-empty", "-m", "fix: probe on side")
self._git("switch", "-q", "main")
def _git(self, *args: str) -> None:
subprocess.run(["git", *args], check=True, capture_output=True, env=self.env, cwd=self.repo)
def test_only_commits_outside_head_are_candidates(self):
result = subprocess.run(
[sys.executable, str(FIND_FIX), "--grep", "probe"],
capture_output=True,
text=True,
check=True,
env=self.env,
cwd=self.repo,
)
self.assertIn("hits=2 in_head=1 candidates=1", result.stdout)
self.assertIn("fix: probe on side", result.stdout)
self.assertNotIn("probe in head", result.stdout)
if __name__ == "__main__":
unittest.main()
-88
View File
@@ -1,88 +0,0 @@
"""Exercise the kvb-org-organize council script: election, staleness, release, gate."""
from __future__ import annotations
import json
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
COUNCIL = REPO_ROOT / "skills" / "kvb-org-organize" / "scripts" / "council.py"
class TestCouncil(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.addCleanup(self.tmp.cleanup)
self.home = Path(self.tmp.name) / "home"
self.root = Path(self.tmp.name) / "council"
self.sessions = self.home / ".claude" / "sessions"
self.sessions.mkdir(parents=True)
self._live("s1", "One")
self._live("s2", "Two")
def _live(self, sid: str, name: str) -> None:
(self.sessions / f"{sid}.json").write_text(
json.dumps({"sessionId": sid, "name": name}), encoding="utf-8"
)
def _run(self, sid: str, *args: str, stdin: str = "") -> subprocess.CompletedProcess:
env = {
"PATH": "/usr/bin:/bin",
"HOME": str(self.home),
"AGENTS_COUNCIL_ROOT": str(self.root),
"CLAUDE_CODE_SESSION_ID": sid,
}
return subprocess.run(
[sys.executable, str(COUNCIL), *args],
capture_output=True,
text=True,
env=env,
input=stdin,
check=False,
)
def test_first_claim_wins_and_second_is_refused(self):
self.assertEqual(self._run("s1", "claim", "work/x", "--work", "w").returncode, 0)
second = self._run("s2", "claim", "work/x", "--work", "w")
self.assertEqual(second.returncode, 3)
self.assertIn('"session": "s1"', second.stdout)
def test_dead_holder_is_stale_and_can_be_taken_over(self):
self._run("s1", "claim", "work/x", "--work", "w")
(self.sessions / "s1.json").unlink()
self.assertEqual(self._run("s2", "claim", "work/x", "--work", "w").returncode, 4)
self.assertEqual(self._run("s2", "takeover", "work/x", "--work", "w").returncode, 0)
self.assertIn('"session": "s2"', (self.root / "work/x/claim.json").read_text())
def test_takeover_of_a_live_holder_is_refused(self):
self._run("s1", "claim", "work/x", "--work", "w")
self.assertEqual(self._run("s2", "takeover", "work/x", "--work", "w").returncode, 3)
def test_only_the_holder_releases(self):
self._run("s1", "claim", "work/x", "--work", "w")
self.assertEqual(self._run("s2", "release", "work/x").returncode, 3)
self.assertEqual(self._run("s1", "release", "work/x").returncode, 0)
self.assertFalse((self.root / "work/x").exists())
def test_gate_blocks_unclaimed_edits_in_a_git_worktree_only(self):
repo = Path(self.tmp.name) / "repo"
(repo / ".git").mkdir(parents=True)
inside = json.dumps({"session_id": "s1", "tool_input": {"file_path": str(repo / "f")}})
outside = json.dumps(
{"session_id": "s1", "tool_input": {"file_path": str(self.home / "f")}}
)
self.assertEqual(self._run("s1", "gate", stdin=inside).returncode, 2)
self.assertEqual(self._run("s1", "gate", stdin=outside).returncode, 0)
self._run("s1", "claim", "work/x", "--work", "w")
self.assertEqual(self._run("s1", "gate", stdin=inside).returncode, 0)
def test_invalid_office_is_a_usage_error(self):
self.assertEqual(self._run("s1", "claim", "../x", "--work", "w").returncode, 2)
if __name__ == "__main__":
unittest.main()
+9 -84
View File
@@ -10,8 +10,8 @@ import unittest
from pathlib import Path from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1] REPO_ROOT = Path(__file__).resolve().parents[1]
SKILL = REPO_ROOT / "skills" / "kvb-skills-dream" / "SKILL.md" SKILL = REPO_ROOT / "skills" / "dream" / "SKILL.md"
HOOK = REPO_ROOT / "skills" / "kvb-skills-dream" / "hooks" / "reminder.sh" HOOK = REPO_ROOT / "skills" / "dream" / "hooks" / "reminder.sh"
PATCHER = REPO_ROOT / "scripts" / "enable-plugins.js" PATCHER = REPO_ROOT / "scripts" / "enable-plugins.js"
@@ -38,7 +38,7 @@ def _run_hook(
class TestDreamSkill(unittest.TestCase): class TestDreamSkill(unittest.TestCase):
def test_skill_is_trigger_only(self): def test_skill_is_trigger_only(self):
text = SKILL.read_text(encoding="utf-8") text = SKILL.read_text(encoding="utf-8")
self.assertIn("name: kvb-skills-dream", text) self.assertIn("name: dream", text)
self.assertIn("multiSelect: false", text) self.assertIn("multiSelect: false", text)
@@ -51,7 +51,7 @@ class TestDreamHook(unittest.TestCase):
def test_first_run_hints_and_stamps(self): def test_first_run_hints_and_stamps(self):
result = _run_hook(self.cwd, self.stamp) result = _run_hook(self.cwd, self.stamp)
self.assertIn("/kvb-skills-dream", result.stdout) self.assertIn("/dream", result.stdout)
self.assertTrue(self.stamp.is_file()) self.assertTrue(self.stamp.is_file())
def test_second_run_is_silent_within_interval(self): def test_second_run_is_silent_within_interval(self):
@@ -60,12 +60,12 @@ class TestDreamHook(unittest.TestCase):
def test_hint_returns_after_the_interval(self): def test_hint_returns_after_the_interval(self):
_run_hook(self.cwd, self.stamp) _run_hook(self.cwd, self.stamp)
self.assertIn("/kvb-skills-dream", _run_hook(self.cwd, self.stamp, interval="0").stdout) self.assertIn("/dream", _run_hook(self.cwd, self.stamp, interval="0").stdout)
def test_corrupt_stamp_does_not_crash(self): def test_corrupt_stamp_does_not_crash(self):
self.stamp.parent.mkdir(parents=True, exist_ok=True) self.stamp.parent.mkdir(parents=True, exist_ok=True)
self.stamp.write_text("not-a-timestamp\n", encoding="utf-8") self.stamp.write_text("not-a-timestamp\n", encoding="utf-8")
self.assertIn("/kvb-skills-dream", _run_hook(self.cwd, self.stamp).stdout) self.assertIn("/dream", _run_hook(self.cwd, self.stamp).stdout)
def test_outside_a_repository_it_stays_silent(self): def test_outside_a_repository_it_stays_silent(self):
self.assertEqual(_run_hook(self.cwd).stdout, "") self.assertEqual(_run_hook(self.cwd).stdout, "")
@@ -78,34 +78,10 @@ class TestDreamHook(unittest.TestCase):
subprocess.run( subprocess.run(
["git", "init", "-q", str(repo)], check=True, capture_output=True ["git", "init", "-q", str(repo)], check=True, capture_output=True
) )
self.assertIn("/kvb-skills-dream", _run_hook(repo).stdout) self.assertIn("/dream", _run_hook(repo).stdout)
self.assertTrue((repo / ".git" / ".dream-stamp").is_file()) self.assertTrue((repo / ".git" / ".dream-stamp").is_file())
self.assertEqual(_run_hook(repo).stdout, "") self.assertEqual(_run_hook(repo).stdout, "")
@unittest.skipUnless(shutil.which("git"), "git not installed")
def test_linked_worktrees_share_the_stamp(self):
repo = self.cwd / "main"
tree = self.cwd / "tree"
env = {
"PATH": "/usr/bin:/bin",
"HOME": str(self.cwd),
"GIT_AUTHOR_NAME": "t",
"GIT_AUTHOR_EMAIL": "t@t",
"GIT_COMMITTER_NAME": "t",
"GIT_COMMITTER_EMAIL": "t@t",
}
repo.mkdir()
for args in (
["init", "-q"],
["commit", "-q", "--allow-empty", "-m", "init"],
["worktree", "add", "-q", str(tree)],
):
subprocess.run(
["git", *args], check=True, capture_output=True, env=env, cwd=str(repo)
)
self.assertIn("/kvb-skills-dream", _run_hook(repo).stdout)
self.assertEqual(_run_hook(tree).stdout, "")
@unittest.skipUnless(shutil.which("node"), "node not installed") @unittest.skipUnless(shutil.which("node"), "node not installed")
class TestSettingsRegistration(unittest.TestCase): class TestSettingsRegistration(unittest.TestCase):
@@ -124,59 +100,8 @@ class TestSettingsRegistration(unittest.TestCase):
self._patch() self._patch()
data = self._patch() data = self._patch()
entries = json.dumps(data["hooks"]["UserPromptSubmit"]) entries = json.dumps(data["hooks"]["UserPromptSubmit"])
self.assertEqual(entries.count("kvb-skills-dream/hooks/reminder.sh"), 1) self.assertEqual(entries.count("dream/hooks/reminder.sh"), 1)
self.assertEqual(entries.count("kvb-skills-tune/hooks/reminder.sh"), 1) self.assertEqual(entries.count("autotune/hooks/reminder.sh"), 1)
gate = json.dumps(data["hooks"]["PreToolUse"])
self.assertEqual(gate.count("kvb-org-organize/scripts/council.py"), 1)
self.assertEqual(gate.count("kvb-org-organize/scripts/watch.py"), 1)
self.assertEqual(entries.count("kvb-org-organize/scripts/watch.py"), 1)
post = json.dumps(data["hooks"]["PostToolUse"])
self.assertEqual(post.count("kvb-org-organize/scripts/watch.py"), 1)
self.assertEqual(gate.count("kvb-org-perform/scripts/log.py"), 1)
self.assertEqual(post.count("kvb-org-perform/scripts/log.py"), 1)
def test_hooks_of_renamed_skills_are_repointed(self):
base = Path(self.tmp.name) / "skills"
def hook(command):
return {"hooks": [{"type": "command", "command": command}]}
self.settings.write_text(
json.dumps(
{
"hooks": {
"UserPromptSubmit": [
hook(f'bash "{base}/autotune/hooks/reminder.sh"'),
hook(f'bash "{base}/dream/hooks/reminder.sh"'),
hook(f'python3 "{base}/democracy/scripts/watch.py" watch'),
hook("echo keep"),
],
"PreToolUse": [
hook(f'python3 "{base}/democracy/scripts/council.py" gate'),
hook(f'python3 "{base}/democracy/scripts/watch.py" block'),
hook(f'python3 "{base}/subagent-model/scripts/log.py" spawn'),
],
"PostToolUse": [
hook(f'python3 "{base}/democracy/scripts/watch.py" watch'),
hook(f'python3 "{base}/subagent-model/scripts/log.py" persist'),
],
}
}
),
encoding="utf-8",
)
data = self._patch()
text = json.dumps(data["hooks"])
for old in ("/autotune/", "/dream/", "/democracy/", "/subagent-model/"):
self.assertNotIn(f"skills{old}", text)
self.assertIn("echo keep", text)
entries = json.dumps(data["hooks"]["UserPromptSubmit"])
self.assertEqual(entries.count("kvb-skills-dream/hooks/reminder.sh"), 1)
self.assertEqual(entries.count("kvb-skills-tune/hooks/reminder.sh"), 1)
self.assertEqual(entries.count("kvb-org-organize/scripts/watch.py"), 1)
gate = json.dumps(data["hooks"]["PreToolUse"])
self.assertEqual(gate.count("kvb-org-organize/scripts/council.py"), 1)
self.assertEqual(gate.count("kvb-org-perform/scripts/log.py"), 1)
if __name__ == "__main__": if __name__ == "__main__":
-12
View File
@@ -12,8 +12,6 @@ LOCK_FILE = REPO_ROOT / "skills-lock.json"
HASH_RE = re.compile(r"^[0-9a-f]{64}$") HASH_RE = re.compile(r"^[0-9a-f]{64}$")
REQUIRED_FIELDS = ("source", "sourceType", "skillPath", "computedHash") REQUIRED_FIELDS = ("source", "sourceType", "skillPath", "computedHash")
DESCRIPTION_WORDS = 40
LISTING_WORDS = 1300
class TestSkillsLock(unittest.TestCase): class TestSkillsLock(unittest.TestCase):
@@ -63,16 +61,6 @@ class TestSkillsLock(unittest.TestCase):
"description:", frontmatter, "frontmatter missing description" "description:", frontmatter, "frontmatter missing description"
) )
def test_descriptions_fit_the_listing_budget(self):
total = 0
for skill_md in sorted((REPO_ROOT / "skills").glob("*/SKILL.md")):
frontmatter = skill_md.read_text(encoding="utf-8").split("---", 2)[1]
words = len(frontmatter.split("description:", 1)[1].split()) - 1
total += words
with self.subTest(skill=skill_md.parent.name):
self.assertLessEqual(words, DESCRIPTION_WORDS)
self.assertLessEqual(total, LISTING_WORDS)
def test_first_party_skills_do_not_shadow_locked_skills(self): def test_first_party_skills_do_not_shadow_locked_skills(self):
locked = set(self.lock["skills"].keys()) locked = set(self.lock["skills"].keys())
for skill_dir in sorted((REPO_ROOT / "skills").glob("*/")): for skill_dir in sorted((REPO_ROOT / "skills").glob("*/")):
-67
View File
@@ -1,67 +0,0 @@
"""Pin every first-party skill name to kvb-<category>-<name> and the Agent Skills name rules."""
from __future__ import annotations
import re
import unittest
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
SKILLS = REPO_ROOT / "skills"
README = REPO_ROOT / "README.md"
CATEGORIES = ("think", "create", "code", "ci", "git", "check", "org", "skills")
UNCATEGORIZED = frozenset({"kvb-cleanup", "kvb-help"})
SPEC = re.compile(r"^[a-z0-9]+(-[a-z0-9]+)*$")
SCHEMA = re.compile(rf"^kvb-(?:{'|'.join(CATEGORIES)})-[a-z0-9]+(?:-[a-z0-9]+)*$")
NAME_LINE = re.compile(r"^name:\s*(\S+)\s*$", re.MULTILINE)
ORGANIGRAM_EDGE = re.compile(r"^\s*(\w+) --> \w+\[(kvb-[a-z0-9-]+)\]\s*$", re.MULTILINE)
class TestSkillNames(unittest.TestCase):
def setUp(self):
self.names = sorted(p.name for p in SKILLS.iterdir() if p.is_dir())
def test_name_field_matches_the_directory(self):
for name in self.names:
with self.subTest(skill=name):
text = (SKILLS / name / "SKILL.md").read_text(encoding="utf-8")
match = NAME_LINE.search(text)
self.assertIsNotNone(match, "SKILL.md has no name: line")
self.assertEqual(match.group(1), name)
def test_names_follow_the_agent_skills_rules(self):
for name in self.names:
with self.subTest(skill=name):
self.assertRegex(name, SPEC)
self.assertLessEqual(len(name), 64)
self.assertNotRegex(name, "anthropic|claude")
def test_names_use_a_listed_category(self):
for name in self.names:
if name in UNCATEGORIZED:
continue
with self.subTest(skill=name):
self.assertRegex(name, SCHEMA)
def test_every_category_and_exception_is_in_use(self):
for category in CATEGORIES:
with self.subTest(category=category):
self.assertTrue(
any(n.startswith(f"kvb-{category}-") for n in self.names),
f"category {category} has no skill",
)
self.assertLessEqual(UNCATEGORIZED, set(self.names))
def test_readme_organigram_places_every_skill_under_its_category(self):
text = README.read_text(encoding="utf-8")
section = text.split("## Organigram", 1)[1].split("\n## ", 1)[0]
edges = ORGANIGRAM_EDGE.findall(section)
self.assertEqual(sorted(name for _, name in edges), self.names)
for parent, name in edges:
with self.subTest(skill=name):
expected = "none" if name in UNCATEGORIZED else name.split("-")[1]
self.assertEqual(parent, expected)
if __name__ == "__main__":
unittest.main()
+6 -8
View File
@@ -1,4 +1,4 @@
"""The self-improving skills must share the objective held by kvb-skills-tune.""" """The self-improving skills must share the optimize objective."""
from __future__ import annotations from __future__ import annotations
@@ -7,13 +7,13 @@ from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1] REPO_ROOT = Path(__file__).resolve().parents[1]
SKILLS = REPO_ROOT / "skills" SKILLS = REPO_ROOT / "skills"
ROUTERS = ("kvb-skills-auto", "kvb-check-confidence") ROUTERS = ("autotune", "autoskill", "confidence")
class TestOptimizeObjective(unittest.TestCase): class TestOptimizeObjective(unittest.TestCase):
def test_autotune_names_the_three_axes(self): def test_skill_names_the_three_axes(self):
text = (SKILLS / "kvb-skills-tune" / "SKILL.md").read_text(encoding="utf-8") text = (SKILLS / "optimize" / "SKILL.md").read_text(encoding="utf-8")
self.assertIn("## Objective", text) self.assertIn("name: optimize", text)
for axis in ("**Tokens**", "**Time**", "**Quality**"): for axis in ("**Tokens**", "**Time**", "**Quality**"):
self.assertIn(axis, text) self.assertIn(axis, text)
@@ -21,9 +21,7 @@ class TestOptimizeObjective(unittest.TestCase):
for skill in ROUTERS: for skill in ROUTERS:
text = (SKILLS / skill / "SKILL.md").read_text(encoding="utf-8") text = (SKILLS / skill / "SKILL.md").read_text(encoding="utf-8")
self.assertIn( self.assertIn(
"`kvb-skills-tune` skill's **Objective**", "`optimize` skill", text, f"{skill} does not route to optimize"
text,
f"{skill} does not route to the kvb-skills-tune objective",
) )
-251
View File
@@ -1,251 +0,0 @@
"""Exercise the kvb-org-rasci register: fields, roles, priority order, claims, artifacts, tools."""
from __future__ import annotations
import datetime
import json
import subprocess
import sys
import tempfile
import time
import unittest
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
RASCI = REPO_ROOT / "skills" / "kvb-org-rasci" / "scripts" / "rasci.py"
COUNCIL = REPO_ROOT / "skills" / "kvb-org-organize" / "scripts" / "council.py"
HEADER = "| Prio | WSJF | Task | OKR | Status | Worker | Artifacts | Start | End | R | A | S | C | I |"
def local(iso: str) -> str:
return datetime.datetime.fromisoformat(iso).astimezone().strftime("%m-%d %H:%M")
class TestRasci(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.addCleanup(self.tmp.cleanup)
self.home = Path(self.tmp.name) / "home"
self.root = Path(self.tmp.name) / "council"
self.sessions = self.home / ".claude" / "sessions"
self.sessions.mkdir(parents=True)
for sid, name in (("s1", "One"), ("s2", "Two")):
(self.sessions / f"{sid}.json").write_text(
json.dumps({"sessionId": sid, "name": name}), encoding="utf-8"
)
def _run(self, *args: str, script: Path = RASCI, sid: str = "s1") -> subprocess.CompletedProcess:
env = {
"PATH": "/usr/bin:/bin",
"HOME": str(self.home),
"AGENTS_COUNCIL_ROOT": str(self.root),
"CLAUDE_CODE_SESSION_ID": sid,
}
return subprocess.run(
[sys.executable, str(script), *args],
capture_output=True,
text=True,
env=env,
cwd=self.tmp.name,
check=False,
)
def _out(self, *args: str) -> list[str]:
result = self._run(*args)
self.assertEqual(result.returncode, 0, result.stderr)
return result.stdout.splitlines()
def _claim(self, office: str, session: str, name: str, work: str) -> None:
path = self.root / office
path.mkdir(parents=True)
claim = {
"office": office,
"session": session,
"runtime": "claude-code",
"name": name,
"since": "2026-11-01T00:00:00+00:00",
"beat": time.time(),
"work": work,
}
(path / "claim.json").write_text(json.dumps(claim), encoding="utf-8")
def test_matrix_orders_by_priority_and_lists_roles(self):
start = "2026-11-01T10:00:00+00:00"
self._run("set", "work/a", "--title", "Alpha", "--objective", "Ship A", "--prio", "2",
"--status", "running", "--worker", "Skills", "--start", start)
self._run("role", "work/a", "Skills", "ar")
self._run("role", "work/a", "Operator", "ic")
self._run("role", "work/a", "Pkgmgr", "C")
self._run("set", "work/b", "--title", "Beta", "--prio", "1", "--worker", "subagent Explore (haiku)")
self._run("role", "work/b", "Pkgmgr", "R")
lines = self._out("show")
self.assertEqual(lines[0], HEADER)
self.assertEqual(
lines[2], "| 1 | - | work/b: Beta | - | queued | subagent Explore (haiku) | - | - | - | Pkgmgr | | | | |"
)
self.assertEqual(
lines[3],
f"| 2 | - | work/a: Alpha | Ship A | running | Skills | - | {local(start)} | - "
"| Skills | Skills | | Operator, Pkgmgr | Operator |",
)
self.assertEqual(
lines[4], "gaps: work/b has 0 A; work/b has no key result; work/a has no key result"
)
def test_key_results_gate_done_and_fill_the_okr_column(self):
self._run("set", "work/a", "--objective", "Ship A", "--status", "running")
self._run("role", "work/a", "Skills", "RA")
self._run("kr", "work/a", "add", "tests green")
self._run("kr", "work/a", "add", "released")
self._run("kr", "work/a", "met", "1")
lines = self._out("show")
self.assertIn("| Ship A (1/2 KR) | running |", lines[2])
self.assertEqual(len(lines), 3)
self.assertEqual(self._run("set", "work/a", "--status", "done").returncode, 2)
self.assertIn("key result 2 [ ]: released", self._out("get", "work/a"))
self._run("kr", "work/a", "met", "2")
self.assertEqual(self._run("set", "work/a", "--status", "done").returncode, 0)
self.assertEqual(self._out("show"), ["no tasks"])
def test_status_sets_start_and_end(self):
self._run("set", "work/a", "--status", "running")
self._run("set", "work/a", "--status", "done", "--description", "long text")
lines = self._out("get", "work/a")
self.assertIn("status: done", lines)
self.assertIn("description: long text", lines)
self.assertTrue(any(line.startswith("start: 20") for line in lines))
self.assertTrue(any(line.startswith("end: 20") for line in lines))
self._run("set", "work/a", "--status", "running")
self.assertFalse(any(line.startswith("end:") for line in self._out("get", "work/a")))
def test_done_tasks_show_only_with_all(self):
self._run("set", "work/a", "--status", "done", "--end", "2026-11-01T12:00:00+00:00")
self._run("role", "work/a", "Skills", "RA")
self.assertEqual(self._out("show"), ["no tasks"])
self.assertIn("| done | - |", self._out("show", "--all")[2])
def test_artifacts_and_tools(self):
self._run("set", "work/a", "--workdir", "/w/a", "--branch", "feat/a", "--status", "done",
"--artifact", "image:core:dev", "--artifact", "volume:db",
"--tool", "council.py", "--tool", "make test")
self._run("set", "work/b", "--worker", "Two", "--tool", "make test", "--artifact", "dir:/tmp/b")
self._run("set", "work/a", "--drop-artifact", "volume:db")
self._run("set", "work/b", "--branch", "")
rows = [line.split("\t") for line in self._out("artifacts")]
self.assertEqual(
[row[:4] for row in rows],
[
["done", "work/a", "-", "workdir:/w/a"],
["done", "work/a", "-", "branch:feat/a"],
["done", "work/a", "-", "image:core:dev"],
["queued", "work/b", "Two", "dir:/tmp/b"],
],
)
self.assertEqual(rows[0][4], "-")
self.assertRegex(rows[2][4], r"^\d\d-\d\d \d\d:\d\d$")
self.assertEqual([r.split("\t")[1] for r in self._out("artifacts", "--ended")], ["work/a"] * 3)
self.assertEqual(len(self._out("artifacts", "--task", "work/b")), 1)
self.assertIn("| queued | Two | 1 |", self._out("show")[2])
self.assertTrue(any(line.startswith("artifact: image:core:dev (since ") for line in self._out("get", "work/a")))
self.assertEqual(
self._out("tools"), ["2\tmake test\twork/a work/b", "1\tcouncil.py\twork/a"]
)
self.assertIn("tool: council.py", self._out("get", "work/a"))
def test_claims_join_the_matrix_with_holder_as_ra(self):
self._claim("work/live", "s1", "One", "Gamma")
self._claim("cluster/dead", "s9", "Gone", "Delta")
self._claim("chair", "s1", "One", "allocator")
self._run("set", "work/live", "--prio", "1", "--status", "running")
self._run("role", "work/live", "Two", "S")
lines = self._out("show")
self.assertTrue(lines[2].startswith("| 1 | - | work/live | - | running | One |"))
self.assertTrue(lines[2].endswith("| One | One | Two | | |"))
self.assertTrue(lines[3].startswith("| 2 | - | cluster/dead | - | stale | Gone |"))
self.assertFalse(any("chair" in line for line in lines))
def test_council_claim_takeover_and_release_write_through(self):
self.assertEqual(self._run("claim", "work/x", "--work", "Do X", script=COUNCIL).returncode, 0)
lines = self._out("get", "work/x")
self.assertIn("status: running", lines)
self.assertIn("worker: One", lines)
self.assertIn("description: Do X", lines)
self.assertIn(f"workdir: {self.tmp.name}", lines)
self.assertIn("roles: One=RA", lines)
(self.sessions / "s1.json").unlink()
self.assertEqual(self._run("takeover", "work/x", "--work", "Do X", script=COUNCIL, sid="s2").returncode, 0)
lines = self._out("get", "work/x")
self.assertIn("worker: Two", lines)
self.assertIn("roles: Two=RA", lines)
self._run("kr", "work/x", "add", "X shipped")
self.assertEqual(self._run("release", "work/x", script=COUNCIL, sid="s2").returncode, 0)
lines = self._out("get", "work/x")
self.assertIn("status: stopped", lines)
self.assertTrue(any(line.startswith("end: 20") for line in lines))
def test_rm_and_clear_a_party(self):
self._run("set", "work/a", "--title", "Alpha")
self._run("role", "work/a", "Skills", "RA")
self._run("role", "work/a", "Pkgmgr", "C")
self._run("role", "work/a", "Pkgmgr", "-")
self.assertTrue(self._out("show")[2].endswith("| Skills | Skills | | | |"))
self.assertEqual(self._out("rm", "work/a"), ["removed work/a"])
self.assertEqual(self._out("show"), ["no tasks"])
def test_invalid_input_exits_2(self):
for args in (
("role", "work/a", "Skills", "RR"),
("role", "work/a", "Skills", "Q"),
("set", "Bad Task"),
("set", "work/a", "--prio", "0"),
("set", "work/a", "--status", "finished"),
("set", "work/a", "--start", "yesterday"),
("get", "work/none"),
("kr", "work/a", "met", "1"),
("set", "work/a", "--artifact", "no-kind"),
("estimate", "work/a", "One", "--value", "4", "--complexity", "2"),
("estimate", "work/a", "One", "--value", "8"),
):
with self.subTest(args=args):
self.assertEqual(self._run(*args).returncode, 2)
self.assertFalse((self.root / "rasci.json").exists())
def test_pipe_in_a_title_does_not_break_the_table(self):
self._run("set", "work/a", "--title", "a | b")
self._run("role", "work/a", "Skills", "RA")
self.assertTrue(self._out("show")[2].startswith("| 1 | - | work/a: a \\| b | - | queued |"))
def test_wsjf_ranks_by_median_estimates(self):
self._run("set", "work/a", "--objective", "Ship A")
self._run("kr", "work/a", "add", "released")
self._run("estimate", "work/a", "One", "--value", "8", "--complexity", "2")
self._run("estimate", "work/a", "Two", "--value", "13", "--complexity", "3")
self._run("estimate", "work/b", "One", "--value", "5", "--complexity", "5")
self._run("set", "work/c", "--title", "Gamma")
lines = self._out("wsjf")
self.assertEqual(
lines[0], "| Rank | Task | OKR | Business value | Complexity | WSJF | Estimators | Status |"
)
self.assertEqual(lines[2], "| 1 | work/a | Ship A (0/1 KR) | 10.5 | 2.5 | 4.2 | One, Two | queued |")
self.assertEqual(lines[3], "| 2 | work/b | - | 5 | 5 | 1 | One | queued |")
self.assertEqual(lines[4], "| 3 | work/c | - | - | - | - | - | queued |")
self.assertEqual(lines[5], "unestimated: work/c")
self.assertTrue(self._out("show")[2].startswith("| 1 | 4.2 | work/a |"))
self.assertIn("wsjf: 4.2", self._out("get", "work/a"))
self._run("set", "work/c", "--prio", "1")
self.assertTrue(self._out("show")[2].startswith("| 1 | - | work/c: Gamma |"))
self._run("estimate", "work/b", "One", "--drop")
self.assertFalse(any(line.startswith("estimate ") for line in self._out("get", "work/b")))
class TestOrgSkillsUseTheRegister(unittest.TestCase):
def test_every_org_skill_names_the_register(self):
for skill in sorted((REPO_ROOT / "skills").glob("kvb-org-*/SKILL.md")):
if skill.parent.name == "kvb-org-rasci":
continue
with self.subTest(skill=skill.parent.name):
self.assertIn("`kvb-org-rasci`", skill.read_text(encoding="utf-8"))
if __name__ == "__main__":
unittest.main()
+3 -3
View File
@@ -1,4 +1,4 @@
"""Validate the kvb-skills-shortcuts skill table: rows present, sorted, ``soc``-prefixed, """Validate the shortcuts skill table: rows present, sorted, ``soc``-prefixed,
no two adjacent consonants.""" no two adjacent consonants."""
from __future__ import annotations from __future__ import annotations
@@ -8,7 +8,7 @@ import unittest
from pathlib import Path from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1] REPO_ROOT = Path(__file__).resolve().parents[1]
SKILL = REPO_ROOT / "skills" / "kvb-skills-shortcuts" / "SKILL.md" SKILL = REPO_ROOT / "skills" / "shortcuts" / "SKILL.md"
ROW_RE = re.compile(r"^\|\s*`(?P<shortcut>[^`]+)`\s*\|") ROW_RE = re.compile(r"^\|\s*`(?P<shortcut>[^`]+)`\s*\|")
VOWELS = frozenset("aeiou") VOWELS = frozenset("aeiou")
@@ -37,7 +37,7 @@ class TestShortcuts(unittest.TestCase):
] ]
def test_shortcuts_present(self): def test_shortcuts_present(self):
self.assertTrue(self.shortcuts, "no shortcut rows found in kvb-skills-shortcuts skill") self.assertTrue(self.shortcuts, "no shortcut rows found in shortcuts skill")
def test_shortcuts_sorted_ascending(self): def test_shortcuts_sorted_ascending(self):
self.assertEqual( self.assertEqual(
-107
View File
@@ -1,107 +0,0 @@
"""Exercise the kvb-org-perform log: spawn hook, feedback, sandbox persist, report."""
from __future__ import annotations
import json
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
LOG_PY = REPO_ROOT / "skills" / "kvb-org-perform" / "scripts" / "log.py"
class TestSubagentModelLog(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.addCleanup(self.tmp.cleanup)
self.base = Path(self.tmp.name)
self.log = self.base / "state" / "subagent-model.jsonl"
def _run(self, *args: str, stdin: str = "", log: Path | None = None) -> subprocess.CompletedProcess:
env = {"SUBAGENT_MODEL_LOG": str(log or self.log), "CLAUDE_CODE_SESSION_ID": "s1"}
return subprocess.run(
[sys.executable, str(LOG_PY), *args], capture_output=True, text=True, env=env, input=stdin, check=False
)
def _spawn(self, description: str, prompt: str, **tool: str) -> None:
payload = {"session_id": "s1", "tool_input": {"description": description, "prompt": prompt, **tool}}
self.assertEqual(self._run("spawn", stdin=json.dumps(payload)).returncode, 0)
def _feedback(self, description: str, completed: str = "yes", redo: str = "no", tests: str = "none") -> None:
result = self._run(
"feedback", "--description", description, "--completed", completed, "--tests", tests, "--redo", redo
)
self.assertEqual(result.returncode, 0, result.stderr)
def _lines(self) -> list[dict]:
return [json.loads(text) for text in self.log.read_text(encoding="utf-8").splitlines()]
def _report(self) -> dict[str, list[str]]:
rows = self._run("report").stdout.splitlines()[1:]
return {row.split("\t")[0]: row.split("\t") for row in rows}
def test_spawn_logs_class_model_and_effort_or_the_defaults(self):
self._spawn("find callers", "task-class: lookup\nFind every caller.", model="haiku", effort="low")
self._spawn("fix it", "Find the root cause.")
first, second = self._lines()
self.assertEqual((first["task_class"], first["model"], first["effort"]), ("lookup", "haiku", "low"))
self.assertEqual((second["task_class"], second["model"], second["effort"]), ("unclassified", "default", "default"))
def _pending_feedback(self) -> str:
blocked = self.base / "file"
blocked.write_text("", encoding="utf-8")
result = self._run(
"feedback", "--description", "d", "--completed", "yes", "--tests", "none", "--redo", "no",
log=blocked / "log.jsonl",
)
self.assertTrue(result.stdout.startswith("pending: "), result.stdout)
return result.stdout
def _persist(self, command: str, stdout: str, session: str = "s1") -> None:
payload = {"session_id": session, "tool_input": {"command": command}, "tool_response": {"stdout": stdout}}
self.assertEqual(self._run("persist", stdin=json.dumps(payload)).returncode, 0)
def test_sandboxed_feedback_is_printed_and_stored_by_the_persist_hook(self):
stdout = self._pending_feedback()
self._persist("python3 /x/skills/kvb-org-perform/scripts/log.py feedback --description 'd'", stdout)
self.assertEqual(self._lines()[0]["kind"], "feedback")
def test_persist_rejects_pending_lines_it_did_not_get_from_a_bare_log_call(self):
stdout = self._pending_feedback()
line = json.loads(stdout[len("pending: "):])
call = "/x/skills/kvb-org-perform/scripts/log.py feedback --description d"
for command, output, session in (
("cat ci.log", stdout, "s1"),
(f"{call}; cat ci.log", stdout, "s1"),
(f"{call} | tee /tmp/x", stdout, "s1"),
(f"echo $({call})", stdout, "s1"),
(call, stdout, "other-session"),
(call, stdout + stdout, "s1"),
(call, "pending: " + json.dumps({**line, "extra": 1}), "s1"),
(call, "pending: " + json.dumps({**line, "completed": "yes"}), "s1"),
("/x/skills/kvb-org-perform/scripts/log.py followup --description d", stdout, "s1"),
):
self._persist(command, output, session)
self.assertFalse(self.log.exists())
def test_report_qualifies_a_row_only_at_ninety_percent_over_twenty_rated_runs(self):
for i in range(20):
self._spawn(f"q{i}", "task-class: lookup", model="haiku", effort="low")
self._feedback(f"q{i}", redo="yes" if i < 2 else "no")
for i in range(19):
self._spawn(f"r{i}", "task-class: mechanical-edit", model="haiku", effort="low")
self._feedback(f"r{i}")
self._run("followup", "--description", "q5", "--text", "that list missed a caller")
report = self._report()
self.assertEqual(report["lookup"][3:], ["20", "20", "17", "85%", "no"])
self.assertEqual(report["mechanical-edit"][3:], ["19", "19", "19", "100%", "no"])
self._spawn("r19", "task-class: mechanical-edit", model="haiku", effort="low")
self._feedback("r19")
self.assertEqual(self._report()["mechanical-edit"][-1], "yes")
if __name__ == "__main__":
unittest.main()
-46
View File
@@ -1,46 +0,0 @@
"""update.sh must reach every locked source and install for Claude Code only."""
from __future__ import annotations
import json
import shutil
import subprocess
import tempfile
import unittest
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
UPDATE = REPO_ROOT / "scripts" / "update.sh"
@unittest.skipUnless(shutil.which("jq") and shutil.which("bash"), "jq or bash missing")
class TestUpdate(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.addCleanup(self.tmp.cleanup)
self.bin = Path(self.tmp.name)
self.log = self.bin / "calls.log"
npx = self.bin / "npx"
npx.write_text(f'#!/bin/sh\ncat >/dev/null\necho "$@" >> "{self.log}"\n', encoding="utf-8")
npx.chmod(0o755)
for tool in ("jq", "sort"):
(self.bin / tool).symlink_to(shutil.which(tool))
def test_every_source_is_updated_for_claude_code_only(self):
lock = json.loads((REPO_ROOT / "skills-lock.json").read_text(encoding="utf-8"))
sources = sorted({entry["source"] for entry in lock["skills"].values()})
subprocess.run(
["bash", str(UPDATE)],
check=True,
capture_output=True,
env={"PATH": f"{self.bin}:/usr/bin:/bin"},
stdin=subprocess.DEVNULL,
)
calls = self.log.read_text(encoding="utf-8").splitlines()
self.assertEqual([c.split()[-1] for c in calls], sources)
for call in calls:
self.assertIn("--agent claude-code", call)
if __name__ == "__main__":
unittest.main()
-202
View File
@@ -1,202 +0,0 @@
"""Exercise the kvb-org-organize resource watch: stages, projection, block, admit, alerts, caches."""
from __future__ import annotations
import importlib.util
import json
import os
import subprocess
import sys
import tempfile
import time
import unittest
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
WATCH = REPO_ROOT / "skills" / "kvb-org-organize" / "scripts" / "watch.py"
LIMITS = {"warn": 40, "stop": 15, "critical": 12}
def _load():
spec = importlib.util.spec_from_file_location("watch", WATCH)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
class TestStage(unittest.TestCase):
def setUp(self):
self.watch = _load()
self.cfg = {"thresholds_gb": LIMITS, "projection_minutes": 30}
def test_thresholds(self):
now = time.time()
for free, stage in ((50, "ok"), (30, "warn"), (14, "stop"), (5, "critical")):
self.assertEqual(self.watch.stage_for(free, [], self.cfg, now), stage)
def test_cpu_and_ram_percentages(self):
self.assertEqual(self.watch.cpu_busy_percent((100, 40), (200, 60)), 80.0)
meminfo = "MemTotal: 1000 kB\nMemFree: 100 kB\nMemAvailable: 250 kB\n"
self.assertEqual(self.watch.mem_used_percent(meminfo), 75.0)
def test_fast_fall_escalates_one_stage(self):
now = time.time()
self.assertEqual(self.watch.stage_for(32, [[now - 600, 48]], self.cfg, now), "stop")
self.assertEqual(self.watch.stage_for(32, [[now - 600, 32.1]], self.cfg, now), "warn")
class TestWatchCommands(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.addCleanup(self.tmp.cleanup)
self.base = Path(self.tmp.name)
self.root = self.base / "council"
self.config = self.base / "config.json"
self.cache = self.base / "cache"
self.cache.mkdir()
self.cfg = {
"disk": str(self.base),
"thresholds_gb": LIMITS,
"projection_minutes": 30,
"interval_seconds": 0,
"cache_interval_seconds": 86400,
"heavy_commands": ["^make compose-deploy", "^make swarm-"],
"never_block": ["^make swarm-down"],
"cleanup": [],
"worktree_cleanup": [],
"worktrees_command": "true",
"worktree_marker": "Makefile",
"busy_processes": ["sleep"],
"cache_dirs": [str(self.cache)],
"cache_budget_percent": 3,
"slot_need_gb": {"swarm": 30, "test": 0.001},
"notify": ["true"],
"load_limits": {"cpu_percent": 101, "mem_percent": 101, "sample_seconds": 0.1, "kinds": ["test"]},
}
self._write_config()
def _write_config(self):
self.config.write_text(json.dumps(self.cfg), encoding="utf-8")
def _run(self, *args: str, stdin: str = "") -> subprocess.CompletedProcess:
env = {
"PATH": str(self.base),
"HOME": str(self.base),
"AGENTS_COUNCIL_ROOT": str(self.root),
"AGENTS_COUNCIL_CONFIG": str(self.config),
}
return subprocess.run(
[sys.executable, str(WATCH), *args],
capture_output=True, text=True, env=env, input=stdin, check=False,
)
def _stage(self, stage: str) -> None:
self.root.mkdir(exist_ok=True)
(self.root / "watch.json").write_text(json.dumps({"stage": stage}), encoding="utf-8")
def _bash(self, command: str) -> str:
return json.dumps({"tool_input": {"command": command}})
def test_block_only_heavy_jobs_at_stop_and_never_teardown(self):
self._stage("stop")
self.assertEqual(self._run("block", stdin=self._bash("make compose-deploy apps=x")).returncode, 2)
self.assertEqual(self._run("block", stdin=self._bash("make swarm-down")).returncode, 0)
self.assertEqual(self._run("block", stdin=self._bash("ls")).returncode, 0)
self._stage("warn")
self.assertEqual(self._run("block", stdin=self._bash("make compose-deploy")).returncode, 0)
def test_block_ignores_heavy_words_inside_quotes_and_heredocs(self):
self.cfg["heavy_commands"] = [r"\bmake\s+(-C\s+\S+\s+)?(compose-deploy|test|swarm-\S+)(\s|$)"]
self._write_config()
self._stage("stop")
message = "$(cat <<'EOF'\nfix: run \"make test\" and make swarm-zombie\nEOF\n)"
for command in (
'git commit --no-verify -m "fix: passes the vars to make swarm-zombie app=x"',
"grep -n 'make test' Makefile",
f'git commit --no-verify -m "{message}"',
):
self.assertEqual(self._run("block", stdin=self._bash(command)).returncode, 0, command)
for command in (
"make -C x swarm-zombie app=y",
"env FOO=1 make swarm-zombie",
'make compose-deploy apps="web-app-x"',
):
self.assertEqual(self._run("block", stdin=self._bash(command)).returncode, 2, command)
def test_admit_keeps_the_stop_floor(self):
self.cfg["thresholds_gb"] = {"warn": 0, "stop": 0, "critical": 0}
self._write_config()
self.assertEqual(self._run("admit", "--need-gb", "0.001").returncode, 0)
self.assertEqual(self._run("admit", "--need-gb", "1e9").returncode, 3)
def test_admit_for_a_load_kind_also_needs_cpu_and_ram_below_the_limits(self):
self.cfg["thresholds_gb"] = {"warn": 0, "stop": 0, "critical": 0}
self._write_config()
self.assertEqual(self._run("admit", "--kind", "test").returncode, 0)
self.cfg["load_limits"]["cpu_percent"] = 0
self._write_config()
denied = self._run("admit", "--kind", "test")
self.assertEqual(denied.returncode, 3)
self.assertIn("cpu", denied.stdout)
self.assertEqual(self._run("admit", "--need-gb", "0.001").returncode, 0)
def test_warn_alert_reaches_each_session_once(self):
self.cfg["thresholds_gb"] = {"warn": 1e9, "stop": -1, "critical": -2}
self._write_config()
def hook(session: str) -> str:
payload = {"session_id": session, "hook_event_name": "UserPromptSubmit"}
return self._run("watch", stdin=json.dumps(payload)).stdout
self.assertIn("RESOURCE WARN", hook("s1"))
self.assertEqual(hook("s1"), "")
self.assertIn("RESOURCE WARN", hook("s2"))
def test_cache_trim_evicts_least_recently_used_first(self):
old, new = self.cache / "old", self.cache / "new"
old.write_bytes(b"x" * 4096)
new.write_bytes(b"y" * 4096)
os.utime(old, (1_000_000, 1_000_000))
os.utime(new, (2_000_000, 2_000_000))
st = os.statvfs(self.base)
self.cfg["cache_budget_percent"] = (4096 + 100) / (st.f_blocks * st.f_frsize) * 100
self._write_config()
self.assertEqual(self._run("cleanup", "--caches").returncode, 0)
self.assertFalse(old.exists())
self.assertTrue(new.exists())
def test_full_cleanup_reaches_only_marked_running_worktrees(self):
marked, foreign = self.base / "marked", self.base / "foreign"
for tree in (marked, foreign):
tree.mkdir()
(marked / "Makefile").write_text("", encoding="utf-8")
self.cfg["worktrees_command"] = f"echo {marked}; echo {foreign}; echo {marked}"
self.cfg["worktree_cleanup"] = ["echo x >> {worktree}/cleaned"]
self._write_config()
self.assertEqual(self._run("cleanup", "--full").returncode, 0)
self.assertEqual((marked / "cleaned").read_text(encoding="utf-8"), "x\n")
self.assertFalse((foreign / "cleaned").exists())
def test_full_cleanup_skips_a_worktree_where_a_busy_process_runs(self):
idle, gated = self.base / "idle", self.base / "gated"
for tree in (idle, gated):
(tree / "sub").mkdir(parents=True)
(tree / "Makefile").write_text("", encoding="utf-8")
gate = subprocess.Popen(["sleep", "60"], cwd=gated / "sub")
self.addCleanup(gate.wait)
self.addCleanup(gate.kill)
self.cfg["worktrees_command"] = f"echo {idle}; echo {gated}"
self.cfg["worktree_cleanup"] = ["echo x >> {worktree}/cleaned"]
self._write_config()
self.assertEqual(self._run("cleanup", "--full").returncode, 0)
self.assertTrue((idle / "cleaned").exists())
self.assertFalse((gated / "cleaned").exists())
def test_without_config_every_command_is_a_no_op(self):
self.config.unlink()
result = self._run("status")
self.assertEqual((result.returncode, result.stdout), (0, ""))
if __name__ == "__main__":
unittest.main()