Compare commits
51
Commits
c0f813b792
...
v0.1.0
No files matched your search
@@ -15,6 +15,7 @@ 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
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
## [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*
|
||||||
@@ -7,6 +7,135 @@ 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.
|
||||||
|
|
||||||
|
## 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 `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])
|
||||||
|
end
|
||||||
|
subgraph commit [Commit]
|
||||||
|
commit_next[commit-next]
|
||||||
|
commit_no_verify[commit-no-verify]
|
||||||
|
comments_clean[comments-clean]
|
||||||
|
advocate[advocate]
|
||||||
|
confidence[confidence]
|
||||||
|
break_risk[break-risk]
|
||||||
|
explain[explain]
|
||||||
|
end
|
||||||
|
subgraph coordination [Coordination]
|
||||||
|
democracy[democracy]
|
||||||
|
coordinate[coordinate]
|
||||||
|
funnel[funnel]
|
||||||
|
cleanup[cleanup]
|
||||||
|
end
|
||||||
|
subgraph ci [CI]
|
||||||
|
observe[observe]
|
||||||
|
triage[triage]
|
||||||
|
ci_artifacts[ci-artifacts]
|
||||||
|
tune[tune]
|
||||||
|
merge_fix[merge-fix]
|
||||||
|
end
|
||||||
|
subgraph reasoning [Reasoning]
|
||||||
|
active_listening[active-listening]
|
||||||
|
dialectic[dialectic]
|
||||||
|
criticize[criticize]
|
||||||
|
sceptic[sceptic]
|
||||||
|
auto_feature[auto-feature]
|
||||||
|
end
|
||||||
|
subgraph autonomy [Autonomy]
|
||||||
|
plan[plan]
|
||||||
|
robot[robot]
|
||||||
|
end
|
||||||
|
subgraph self_improvement [Self-improvement]
|
||||||
|
dream[dream]
|
||||||
|
autotune[autotune]
|
||||||
|
autoskill[autoskill]
|
||||||
|
essence[essence]
|
||||||
|
shortcuts[shortcuts]
|
||||||
|
end
|
||||||
|
subgraph quality [Quality]
|
||||||
|
test[test]
|
||||||
|
refactor[refactor]
|
||||||
|
no_defaults[no-defaults]
|
||||||
|
scope_guard[scope-guard]
|
||||||
|
end
|
||||||
|
subgraph standalone [Standalone]
|
||||||
|
changelog[changelog]
|
||||||
|
help_skills[help-skills]
|
||||||
|
status_matrix[status-matrix]
|
||||||
|
medusa[medusa]
|
||||||
|
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
|
||||||
|
|
||||||
|
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
|
||||||
|
funnel --> democracy
|
||||||
|
funnel --> dialectic
|
||||||
|
democracy --> merge_fix
|
||||||
|
|
||||||
|
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`).
|
||||||
@@ -26,7 +155,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.json`: the caveman and ponytail plugins are enabled so their modes auto-activate on session start, and the `autotune` reminder hook is registered so the agent points you at `/autotune` at most once an hour. Existing settings are preserved.
|
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 — `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). A PreToolUse gate from the `democracy` skill blocks edits to files in a git worktree until the session holds a claim (`skills/democracy/scripts/council.py`), and its resource watch (`skills/democracy/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. Existing settings are preserved.
|
||||||
|
|
||||||
Restart your agent afterwards so it loads the new skills.
|
Restart your agent afterwards so it loads the new skills.
|
||||||
|
|
||||||
|
|||||||
@@ -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, and register the autotune
|
// their SessionStart hooks auto-activate both modes, register the autotune
|
||||||
// reminder hook. Merges non-destructively: existing marketplaces, plugins and
|
// and dream reminder hooks, the democracy claim gate and the resource watch. Merges non-destructively: existing marketplaces,
|
||||||
// hooks are preserved, 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,6 +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 = ["autotune", "dream"];
|
||||||
|
|
||||||
let data = {};
|
let data = {};
|
||||||
if (fs.existsSync(settingsPath)) {
|
if (fs.existsSync(settingsPath)) {
|
||||||
@@ -39,15 +40,41 @@ for (const [name, entry] of Object.entries(MARKETPLACES)) {
|
|||||||
}
|
}
|
||||||
data.enabledPlugins = Object.assign(data.enabledPlugins || {}, PLUGINS);
|
data.enabledPlugins = Object.assign(data.enabledPlugins || {}, PLUGINS);
|
||||||
|
|
||||||
const reminder = path.join(path.dirname(settingsPath), "skills", "autotune", "hooks", "reminder.sh");
|
|
||||||
data.hooks = data.hooks || {};
|
data.hooks = data.hooks || {};
|
||||||
data.hooks.UserPromptSubmit = data.hooks.UserPromptSubmit || [];
|
data.hooks.UserPromptSubmit = data.hooks.UserPromptSubmit || [];
|
||||||
const registered = JSON.stringify(data.hooks.UserPromptSubmit).includes("autotune/hooks/reminder.sh");
|
const alreadyRegistered = JSON.stringify(data.hooks.UserPromptSubmit);
|
||||||
if (!registered) {
|
for (const skill of REMINDERS) {
|
||||||
|
if (alreadyRegistered.includes(`${skill}/hooks/reminder.sh`)) continue;
|
||||||
|
const reminder = path.join(path.dirname(settingsPath), "skills", skill, "hooks", "reminder.sh");
|
||||||
data.hooks.UserPromptSubmit.push({
|
data.hooks.UserPromptSubmit.push({
|
||||||
hooks: [{ type: "command", command: `bash ${JSON.stringify(reminder)}` }],
|
hooks: [{ type: "command", command: `bash ${JSON.stringify(reminder)}` }],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data.hooks.PreToolUse = data.hooks.PreToolUse || [];
|
||||||
|
if (!JSON.stringify(data.hooks.PreToolUse).includes("democracy/scripts/council.py")) {
|
||||||
|
const council = path.join(path.dirname(settingsPath), "skills", "democracy", "scripts", "council.py");
|
||||||
|
data.hooks.PreToolUse.push({
|
||||||
|
matcher: "Edit|Write|MultiEdit|NotebookEdit",
|
||||||
|
hooks: [{ type: "command", command: `python3 ${JSON.stringify(council)} gate` }],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const watch = path.join(path.dirname(settingsPath), "skills", "democracy", "scripts", "watch.py");
|
||||||
|
const WATCH_HOOKS = [
|
||||||
|
["UserPromptSubmit", null, "watch"],
|
||||||
|
["PostToolUse", "Bash", "watch"],
|
||||||
|
["PreToolUse", "Bash", "block"],
|
||||||
|
];
|
||||||
|
for (const [event, matcher, sub] of WATCH_HOOKS) {
|
||||||
|
data.hooks[event] = data.hooks[event] || [];
|
||||||
|
const registered = data.hooks[event].some((entry) =>
|
||||||
|
(entry.hooks || []).some((hook) =>
|
||||||
|
String(hook.command).includes("democracy/scripts/watch.py") && String(hook.command).endsWith(` ${sub}`)));
|
||||||
|
if (registered) continue;
|
||||||
|
const entry = { hooks: [{ type: "command", command: `python3 ${JSON.stringify(watch)} ${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 and the autotune reminder in ${settingsPath}`);
|
console.log(`skills: enabled caveman + ponytail plugins, the ${REMINDERS.join(" + ")} reminders, the democracy claim gate and resource watch in ${settingsPath}`);
|
||||||
+3
-2
@@ -23,10 +23,11 @@ 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
|
||||||
@@ -44,7 +45,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.json"
|
settings="${TARGET}/.claude/settings.local.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}"
|
||||||
|
|||||||
+19
-13
@@ -1,83 +1,89 @@
|
|||||||
{
|
{
|
||||||
"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": "c5527c994fbd4c22b36714e3b124a0f167a533d114ab164fb1d35e2123533917"
|
"computedHash": "2e122d819a812bef0e4958706f306e4346e78ecb5a4893997b99cbc19866f514"
|
||||||
},
|
},
|
||||||
"caveman": {
|
"caveman": {
|
||||||
"source": "JuliusBrussee/caveman",
|
"source": "JuliusBrussee/caveman",
|
||||||
"sourceType": "github",
|
"sourceType": "github",
|
||||||
"skillPath": "skills/caveman/SKILL.md",
|
"skillPath": "skills/caveman/SKILL.md",
|
||||||
"computedHash": "d18cdf73a5f5c496d681a43a6846336b110223bdffa6817b8992529c57f1a815"
|
"computedHash": "71b600c88c8d1d36f18c4875db23b53c5d6cb9ba3195e19451d6b1bb67db07c4"
|
||||||
},
|
},
|
||||||
"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": "790a4eeace0be35c6691faf923518ba5bd50f1f1305d1101d09dd4971be94e00"
|
"computedHash": "b416cf134b0f33ddadeca7977aebdbb2d3db79a166d0fce2e1aabeec98132766"
|
||||||
},
|
},
|
||||||
"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": "84517cd4cf7a49d8d2bc1baf00f61bc359306c6ad9389756b6937eff958bd374"
|
"computedHash": "9c8d35882a93e387a584cc4efaacb74433d379a2bd02e3e6f6ad4f7c30ff55ed"
|
||||||
},
|
},
|
||||||
"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": "dd85267e76baad76995157e7b9f762dfa557cd58951ee92af0c283f48aa26537"
|
"computedHash": "c7a73a8c6afb6f871690666449209106aba847b4a9b0698686648960ece80012"
|
||||||
},
|
},
|
||||||
"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": "fb7214a1c5793bae6ba8b1be4329e2e6f40dbec6dd911dfb335ad29f09c316a1"
|
"computedHash": "36a4007c5c0c6b90b5accfee1cd769a86e4963b651dece2ce5f17a223a0abb4b"
|
||||||
},
|
},
|
||||||
"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": "47ce2de3d6cb39a75047b5c962e4eb3da15594e7397c94103e9a104d42626553"
|
"computedHash": "cc9fcd21f2a30cff5596b3cbb197fe0f4b1b30b58084a864350853c217ad301a"
|
||||||
},
|
},
|
||||||
"ponytail": {
|
"ponytail": {
|
||||||
"source": "DietrichGebert/ponytail",
|
"source": "DietrichGebert/ponytail",
|
||||||
"sourceType": "github",
|
"sourceType": "github",
|
||||||
"skillPath": "skills/ponytail/SKILL.md",
|
"skillPath": "skills/ponytail/SKILL.md",
|
||||||
"computedHash": "210fa0831f649ce9f8a305f7b7c8cf2a0dd09ac4dd53067109941b96e7811d4e"
|
"computedHash": "061f887d9e3e735f85db3be3655a8936a00f24d5fb65ac4ab74a968a770fe342"
|
||||||
},
|
},
|
||||||
"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": "8b8d72e6ff70b65b6c9518d470fcf29dc696eea93d28f3efa10fdcc60cbd696d"
|
"computedHash": "1d718f893dabc01caad0dacef86444b5b4b3e1e59409527d66e8b5dd0cd85ae0"
|
||||||
},
|
},
|
||||||
"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": "0099b82174e3a4261828b4ec04d2c068b1b294fdab14f76701c2acd1f7c99539"
|
"computedHash": "f5d1d2ab891ff0a650a90b67235e487828e7d2a189de08a1ae073032de379407"
|
||||||
},
|
},
|
||||||
"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": "c45ecba7906960eea904218b623abf097ebff84ecbacca6d68da1d0687435f75"
|
"computedHash": "fca2297cf7b5fd26692f6bd94d398b522e19fa81ad6e2aaebf1933575f7a6803"
|
||||||
},
|
},
|
||||||
"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": "10e8382b1033c9a0fec0073de6d3de3846b4f5dca7d78b6df2d476a1f4cd43c6"
|
"computedHash": "4582025c3923072fb6c35b8cbd75adf4f7ce47df0eff43cc9a2a39808537fd61"
|
||||||
},
|
},
|
||||||
"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": "4646594fcd75455533f7ff1425f4f1c5ba0fa65f88241bf262b1c4fc275f049a"
|
"computedHash": "800dc0c86402978e12359f701b9e0c81ba4607a0cd0027f4596d0f835a67eadc"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,11 +1,10 @@
|
|||||||
---
|
---
|
||||||
name: active-listening
|
name: active-listening
|
||||||
description: >
|
description: >
|
||||||
Gather every requirement before acting. Trigger at the start of any task
|
Ask focused questions until every requirement is pinned down, then reflect
|
||||||
whose scope, constraints, or success criteria are not fully pinned down, and
|
the task back. Trigger at the start of any task with unclear scope,
|
||||||
whenever another skill needs an operator-only fact — ask focused questions
|
constraints or success criteria, and when another skill needs an
|
||||||
until nothing needed is still open, then reflect the understanding back.
|
operator-only fact.
|
||||||
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
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
---
|
||||||
|
name: advocate
|
||||||
|
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 /advocate
|
||||||
|
[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 `medusa`, engineering
|
||||||
|
principles to `sceptic`.
|
||||||
|
|
||||||
|
## 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).
|
||||||
|
- `/advocate <ref>` or `/advocate <a>..<b>`: that commit or range.
|
||||||
|
- `/advocate` 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 `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 `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.
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# Advocate: the checks
|
||||||
|
|
||||||
|
Apply each check to a target the `advocate` 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.
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
---
|
||||||
|
name: auto-feature
|
||||||
|
description: >
|
||||||
|
Derive missing work (sceptic pass for structural flaws, usability pass for
|
||||||
|
missing features) and write requirement documents after single-select
|
||||||
|
confirmation. Trigger on /auto-feature, "was fehlt noch", "welche features
|
||||||
|
brauchen wir", or when the operator asks 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 sceptic pass
|
||||||
|
|
||||||
|
Invoke the `sceptic` 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 `active-listening`: 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 `confidence` skill's numbers for the analysis: how sure the
|
||||||
|
gaps are real, and what the passes did not cover.
|
||||||
@@ -1,12 +1,16 @@
|
|||||||
---
|
---
|
||||||
name: autoskill
|
name: autoskill
|
||||||
description: >
|
description: >
|
||||||
Detect operator repetition and turn it into a skill. Trigger when the
|
Turn operator repetition into a skill. Trigger when the operator gives the
|
||||||
operator gives an instruction, correction, or prompt pattern for roughly
|
same instruction, correction or prompt pattern about the third time in a
|
||||||
the third time in a conversation (or says they keep repeating themselves),
|
conversation, or says they keep repeating themselves, and the pattern is
|
||||||
and the pattern is general enough to reuse. Portable across projects.
|
reusable.
|
||||||
---
|
---
|
||||||
|
|
||||||
|
Its objective is the `autotune` skill's **Objective** section: a skill is worth
|
||||||
|
creating only when it cuts tokens or time and raises quality; judge every
|
||||||
|
candidate against its 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
|
||||||
vary), do the following:
|
vary), do the following:
|
||||||
@@ -18,10 +22,12 @@ vary), do the following:
|
|||||||
3. On approval, write the skill:
|
3. On approval, write the skill:
|
||||||
- a conversation shortcut or expansion: add it to the `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.
|
||||||
- project-specific behavior: create it in the project's own skills
|
- anything else: persist it exactly as the `autotune` skill's
|
||||||
directory (e.g. `skills/<name>/SKILL.md` in the repository).
|
**Persistence** section specifies — locate the operator's skills
|
||||||
- portable behavior: create `skills/<name>/SKILL.md` in the operator's
|
repository, write `skills/<name>/SKILL.md` there (or in the current
|
||||||
skills repository.
|
project for project-specific behavior), then mirror the skill directory
|
||||||
|
into `~/.claude/skills/` and `~/.agents/skills/`, plus the project's
|
||||||
|
`.claude/skills/` and `.agents/skills/` when it is project-specific.
|
||||||
Follow the local naming conventions and keep the skill a thin,
|
Follow the local naming conventions and keep the skill a thin,
|
||||||
single-purpose instruction; route to an authoritative doc when one
|
single-purpose instruction; route to an authoritative doc when one
|
||||||
exists instead of duplicating it.
|
exists instead of duplicating it.
|
||||||
|
|||||||
+69
-13
@@ -1,18 +1,46 @@
|
|||||||
---
|
---
|
||||||
name: autotune
|
name: autotune
|
||||||
description: >
|
description: >
|
||||||
Generate new agent skills and optimize existing ones so the agent works more
|
Generate new skills or optimize existing ones (fewer tokens, fewer errors,
|
||||||
efficiently: fewer input and output tokens, fewer errors, more focus, and
|
persistent learnings), each proposal confirmed by single-select question.
|
||||||
learnings that persist across sessions. Every proposal is confirmed with the
|
Trigger ONLY on an explicit request: /autotune, "autotune", "tune the
|
||||||
operator through single-select (radio button) questions. Trigger ONLY on an
|
skills"; never on your own initiative.
|
||||||
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
|
||||||
|
|
||||||
|
The shared objective of the self-improving skills (autotune, autoskill,
|
||||||
|
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: `/autotune`, "autotune", "tune the
|
- Run **only** when the operator asks: `/autotune`, "autotune", "tune the
|
||||||
@@ -65,10 +93,35 @@ for a final `Write it` / `Revise it` / `Discard` confirmation before touching
|
|||||||
disk. Rewrites of an existing skill additionally show a before/after diff of
|
disk. Rewrites of an existing skill additionally show a before/after diff of
|
||||||
the changed lines in that confirmation.
|
the changed lines in that confirmation.
|
||||||
|
|
||||||
|
## Persistence
|
||||||
|
|
||||||
|
A skill that exists only under `.claude/skills` is lost on the next install, and
|
||||||
|
one that exists only in the repository is not live until the next restart — so
|
||||||
|
every skill is written to **both**, in this order:
|
||||||
|
|
||||||
|
1. **Locate the operator's skills repository.** Take the first hit: the
|
||||||
|
`SKILLS_REPO` environment variable; the current repository, when it holds
|
||||||
|
`skills-lock.json` and a `skills/` directory; `pkgmgr path skills`; a git
|
||||||
|
checkout named `skills` under the usual roots
|
||||||
|
(`~/Repositories/*/*/skills`, `~/git`, `~/src`, `~/Projects`) carrying
|
||||||
|
`skills-lock.json`. If none matches, ask the operator for the path with a
|
||||||
|
single-select question — never skip the repository and write only the live
|
||||||
|
copies.
|
||||||
|
2. **Write the source of truth**: `skills/<name>/SKILL.md` in that repository
|
||||||
|
(portable skills), or `skills/<name>/SKILL.md` in the current project
|
||||||
|
(project-specific ones). This is the copy that gets committed.
|
||||||
|
3. **Mirror it live**, so it works without a reinstall: copy the skill's whole
|
||||||
|
directory to `~/.claude/skills/<name>/` and `~/.agents/skills/<name>/`, and
|
||||||
|
for a project-specific skill additionally to that project's
|
||||||
|
`.claude/skills/<name>/` and `.agents/skills/<name>/`. Copy the directory,
|
||||||
|
not just `SKILL.md`, so hooks and references come along.
|
||||||
|
|
||||||
|
Report all written paths. Deleting or renaming a skill follows the same list in
|
||||||
|
reverse — remove the mirrors too, or the old name keeps firing.
|
||||||
|
|
||||||
## Step 3: write
|
## Step 3: write
|
||||||
|
|
||||||
- Portable skills go to `skills/<name>/SKILL.md` in the operator's skills
|
- Write the skill to every location the **Persistence** section lists.
|
||||||
repository; project-specific ones to that project's skills directory.
|
|
||||||
- Frontmatter carries `name` and a `description` that states what the skill
|
- Frontmatter carries `name` and a `description` that states what the skill
|
||||||
does **and** when to trigger it - the description is the only part loaded into
|
does **and** when to trigger it - the description is the only part loaded into
|
||||||
every session, so it decides whether the skill ever fires.
|
every session, so it decides whether the skill ever fires.
|
||||||
@@ -76,7 +129,11 @@ the changed lines in that confirmation.
|
|||||||
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.
|
cut every sentence that does not change an action. Route that compaction
|
||||||
|
through the `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`.
|
||||||
|
|
||||||
@@ -86,7 +143,6 @@ the changed lines in that confirmation.
|
|||||||
whole collection is not what autotune 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.
|
||||||
- A skill that saves tokens but loses correctness is a regression; validation,
|
- The **Objective** section decides what counts as an improvement: at least
|
||||||
error handling, and security steps are never the thing that gets trimmed.
|
one axis better, none damaged, and the claim named as observed friction rather
|
||||||
- Efficiency claims stay honest: name the observed friction the skill removes,
|
than a guessed percentage.
|
||||||
never a guessed percentage.
|
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
---
|
||||||
|
name: break-risk
|
||||||
|
description: >
|
||||||
|
Derive the break-risk percentage from measurement, not feel. Called by
|
||||||
|
`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.
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
# Break risk: the method in detail
|
||||||
|
|
||||||
|
Each step of the `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.
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
---
|
||||||
|
name: 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
|
||||||
|
/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, check the previous tag's CI run
|
||||||
|
(`gh run list --branch <tag> -L 1`). A failed run means that version was
|
||||||
|
never published: say so under *Upgrade notes* and carry its upgrade notes
|
||||||
|
forward to this release.
|
||||||
|
|
||||||
|
## 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).
|
||||||
|
|
||||||
|
Then stop. Never edit CHANGELOG.md or a version file, never run
|
||||||
|
`pkgmgr release`, never tag: the operator releases.
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
---
|
||||||
|
name: 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.
|
||||||
Executable
+149
@@ -0,0 +1,149 @@
|
|||||||
|
#!/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"Process completed with exit code|failed=0\b|FAILED - RETRYING")
|
||||||
|
ANNOTATION = re.compile(r"##\[error\]")
|
||||||
|
FATAL = re.compile(r"fatal: \[|FAILED!|Traceback|AssertionError|panic:")
|
||||||
|
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 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 signature(text: str) -> str:
|
||||||
|
lines = output_lines(text)
|
||||||
|
for pattern in (ANNOTATION, FATAL, SIGNAL):
|
||||||
|
for task, line in reversed(lines):
|
||||||
|
if pattern.search(line):
|
||||||
|
found = ANNOTATION.sub("", line).strip()
|
||||||
|
return normalise(f"{task} {found}" if pattern is FATAL 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())
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
---
|
||||||
|
name: cleanup
|
||||||
|
description: >
|
||||||
|
Free disk space agents filled: ask peer sessions what they still need,
|
||||||
|
inventory /tmp, Docker and agent dirs, and delete only what the operator
|
||||||
|
picks. Trigger on /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 `democracy` 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 `democracy` 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.
|
||||||
|
- **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. Attributable to a peer that has not answered: `keep (no answer from
|
||||||
|
<peer>)`. Silence is not consent.
|
||||||
|
3. Archive duty: `archive first`.
|
||||||
|
4. 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.
|
||||||
|
- 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.
|
||||||
|
- 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.
|
||||||
@@ -2,12 +2,9 @@
|
|||||||
name: comments-clean
|
name: comments-clean
|
||||||
description: >
|
description: >
|
||||||
Purge explanatory comments from staged and unstaged changes. Run
|
Purge explanatory comments from staged and unstaged changes. Run
|
||||||
AUTOMATICALLY, without being asked, every time the operator requests a
|
AUTOMATICALLY before every commit or stage request (commit, commit
|
||||||
commit or stage in any wording (commit, commit no-verify, stage next,
|
no-verify, stage next, socon, sobu, socobu, "commite ..."); also on
|
||||||
socon, sobu, socobu, "commite ..."), BEFORE executing that request; also
|
/comments-clean or comment complaints.
|
||||||
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
|
When the operator asks to commit or stage, run this audit first and only
|
||||||
@@ -23,9 +20,22 @@ added lines only) and delete every explanatory comment:
|
|||||||
2. Keep only: parameter/exit-code docs, nocheck/noqa/shellcheck directives,
|
2. Keep only: parameter/exit-code docs, nocheck/noqa/shellcheck directives,
|
||||||
and project-sanctioned exception markers that name a concrete trip-wire.
|
and project-sanctioned exception markers that name a concrete trip-wire.
|
||||||
A relabeled narration is not an exception; delete it instead.
|
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
|
3. Block-comment languages are not line-oriented. In Jinja (`{# … #}`), C-like
|
||||||
that were already staged.
|
(`/* … */`), HTML (`<!-- … -->`) and their kin, a closing delimiter alone on
|
||||||
4. Penalty: if this audit finds anything you wrote yourself, admit it in
|
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 `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
|
the report (file and count, no excuses), lower your confidence numbers
|
||||||
for the affected bundle, and write the incident to persistent agent
|
for the affected bundle, and write the incident to persistent agent
|
||||||
memory (extend the existing minimal-comments feedback entry with the
|
memory (extend the existing minimal-comments feedback entry with the
|
||||||
|
|||||||
+26
-10
@@ -1,20 +1,36 @@
|
|||||||
---
|
---
|
||||||
name: commit-next
|
name: commit-next
|
||||||
description: >
|
description: >
|
||||||
Commit the staged bundle with --no-verify, then stage the next logical
|
Check every branch for the staged fix, commit the staged bundle with
|
||||||
bundle and brief the operator on it. Trigger on /commit-next or when the
|
--no-verify, then stage and brief the next bundle. Trigger on /commit-next
|
||||||
operator asks to commit and stage the next bundle in one step. Portable
|
or when the operator asks to commit and stage the next bundle in one step.
|
||||||
across projects.
|
|
||||||
---
|
---
|
||||||
|
|
||||||
1. Run the commit-no-verify skill on the currently staged changes
|
1. `git fetch --all --prune --tags`, then hunt the staged fix across every
|
||||||
(comments-clean audit first, then `git commit --no-verify`, exact
|
ref with the 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 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
|
||||||
|
commit-no-verify skill on the currently staged changes (comments-clean
|
||||||
|
audit and advocate check first, then `git commit --no-verify`, exact
|
||||||
staging index only).
|
staging index only).
|
||||||
2. Group the remaining uncommitted changes into logical bundles (one
|
4. Group the remaining uncommitted changes into logical bundles (one
|
||||||
concern per bundle: same failure class, same mechanism, or same role)
|
concern per bundle: same failure class, same mechanism, or same role)
|
||||||
and stage the next one; never mix concerns to save a commit.
|
and stage the next one; never mix concerns to save a commit. Staging
|
||||||
3. For the newly staged bundle run the explain skill (what the changes do
|
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 explain skill (what the changes do
|
||||||
and how the pieces interact, file:line-anchored) followed by the
|
and how the pieces interact, file:line-anchored) followed by the
|
||||||
confidence skill (fix confidence, break risk, evidence, residuals).
|
confidence skill (fix confidence, break risk, evidence, residuals).
|
||||||
4. Close with what remains unstaged and wait for the operator's commit
|
6. Close with what remains unstaged and wait for the operator's commit
|
||||||
decision; never auto-commit the newly staged bundle.
|
decision; never auto-commit the newly staged bundle.
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
---
|
---
|
||||||
name: commit-no-verify
|
name: commit-no-verify
|
||||||
description: >
|
description: >
|
||||||
Commit the currently staged changes with --no-verify (skip the pre-commit
|
Commit the staged changes with --no-verify. Trigger on /commit-no-verify or
|
||||||
hooks). Trigger on /commit-no-verify or when the operator asks for a
|
any request for a no-verify commit; invoking it IS the per-commit
|
||||||
no-verify commit in any wording. Invoking this skill IS the explicit
|
authorization --no-verify requires, never carried over to later commits.
|
||||||
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.
|
1. Run the comments-clean 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
|
2. Commit exactly the staging index with `git commit --no-verify` and a
|
||||||
Conventional-Commits message derived from the staged diff; never add
|
Conventional-Commits message derived from the staged diff; never add
|
||||||
unstaged files, never push.
|
unstaged files, never push.
|
||||||
|
|||||||
@@ -1,18 +1,21 @@
|
|||||||
---
|
---
|
||||||
name: confidence
|
name: confidence
|
||||||
description: >
|
description: >
|
||||||
Report calibrated confidence unprompted. Trigger whenever you stage
|
Report calibrated fix confidence and break risk unprompted whenever you
|
||||||
changes, declare a fix or bundle ready, or summarize completed work the
|
stage changes, declare a fix or bundle ready, or summarize work the operator
|
||||||
operator may commit or deploy — the operator must never have to ask "how
|
may commit or deploy.
|
||||||
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 `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,
|
||||||
Two separate numbers, never one blended figure.
|
derived via the `break-risk` skill. Two separate numbers, never one
|
||||||
|
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).
|
||||||
@@ -21,9 +24,17 @@ confidence block:
|
|||||||
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
|
||||||
hidden layers may follow, and give both numbers.
|
hidden layers may follow, and give both numbers.
|
||||||
- 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
|
||||||
|
the work, handled as the `break-risk` skill's last section says, for both
|
||||||
|
numbers.
|
||||||
|
|
||||||
|
Objective: the `autotune` skill's **Objective** section. Two figures with their residuals replace a
|
||||||
|
hedging paragraph and save the operator a verification round trip.
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
---
|
||||||
|
name: coordinate
|
||||||
|
description: >
|
||||||
|
Coordinate a failure or fix with the agents on related branches through
|
||||||
|
merges: claim, ask, report. Trigger on /coordinate and when a fix touches
|
||||||
|
code other branches share.
|
||||||
|
---
|
||||||
|
|
||||||
|
Run the `democracy` skill's **Lineage** and **Peer protocol** sections for the
|
||||||
|
failure or fix at hand. They hold the whole procedure; this skill only keeps
|
||||||
|
`/coordinate` working.
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
---
|
||||||
|
name: criticize
|
||||||
|
description: >
|
||||||
|
Give constructive criticism of a problem, plan, design, decision, text or
|
||||||
|
code from diverse aspects, each point hardened by dialectic. Trigger on
|
||||||
|
/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 (`sceptic` 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 `active-listening` 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 `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 `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.
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
---
|
||||||
|
name: democracy
|
||||||
|
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 /democracy.
|
||||||
|
---
|
||||||
|
|
||||||
|
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 `triage`, a cost cluster from
|
||||||
|
`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**: 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, 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>`),
|
||||||
|
and never cancels a running grant to free capacity.
|
||||||
|
- 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.
|
||||||
|
- **Critical**: additionally a desktop alarm to the operator. A running stack
|
||||||
|
is torn down only with the operator's OK.
|
||||||
|
- 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; `coordinate`, `triage`,
|
||||||
|
`tune`, `funnel` and `cleanup` use it.
|
||||||
|
|
||||||
|
1. **Look**: `council.py list` and the refs (the 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.
|
||||||
|
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. Then release the claim.
|
||||||
|
|
||||||
|
- 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.
|
||||||
|
- 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 /democracy, one table of the current offices from `council.py list`, stale
|
||||||
|
claims marked:
|
||||||
|
|
||||||
|
| Office | Holder | Since | Work | Grants or queue |
|
||||||
Executable
+224
@@ -0,0 +1,224 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Claim, release and list democracy 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 json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
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"
|
||||||
|
|
||||||
|
|
||||||
|
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) -> None:
|
||||||
|
claim = {
|
||||||
|
"office": office,
|
||||||
|
"session": sid,
|
||||||
|
"runtime": runtime,
|
||||||
|
"name": live_sessions().get(sid),
|
||||||
|
"since": datetime.datetime.now(datetime.timezone.utc).isoformat(timespec="seconds"),
|
||||||
|
"beat": time.time(),
|
||||||
|
"work": work,
|
||||||
|
}
|
||||||
|
(path / CLAIM).write_text(json.dumps(claim), encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
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:
|
||||||
|
write_claim(path, args.office, sid, runtime, args.work)
|
||||||
|
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
|
||||||
|
write_claim(path, args.office, sid, runtime, args.work)
|
||||||
|
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
|
||||||
|
# Two successors racing between this check and the rename can both win;
|
||||||
|
# democracy 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)
|
||||||
|
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(
|
||||||
|
"democracy: 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 "
|
||||||
|
"democracy 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())
|
||||||
Executable
+359
@@ -0,0 +1,359 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Watch disk, Docker and caches for the democracy 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), cache_dirs, cache_budget_percent,
|
||||||
|
slot_need_gb {kind: GB}, notify (argv the alert text is appended to).
|
||||||
|
"""
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_block(cfg: dict) -> int:
|
||||||
|
payload = json.load(sys.stdin)
|
||||||
|
command = (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"democracy: 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 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"]
|
||||||
|
verdict = "grant" if left >= floor else "deny"
|
||||||
|
print(f"{verdict}: free {free:.1f} GB, need {need} GB, leaves {left:.1f} GB (floor {floor} GB)")
|
||||||
|
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 run_cleanup(cfg: dict) -> list[str]:
|
||||||
|
commands = list(cfg["cleanup"])
|
||||||
|
for tree in running_worktrees(cfg):
|
||||||
|
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())
|
||||||
@@ -1,11 +1,10 @@
|
|||||||
---
|
---
|
||||||
name: dialectic
|
name: dialectic
|
||||||
description: >
|
description: >
|
||||||
Reach a defensible conclusion through a dialectical process — thesis,
|
Reach a defensible conclusion by thesis, independent skeptic antithesis and
|
||||||
adversarial antithesis, synthesis — iterated until one thesis survives every
|
synthesis, iterated to ~99% confidence. Trigger for any consequential root
|
||||||
attack at ~99% confidence. Trigger for any consequential root-cause, design,
|
cause, design or decision where being wrong is expensive and the answer is
|
||||||
or decision where being wrong is expensive and the answer is not yet certain.
|
uncertain.
|
||||||
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
|
||||||
@@ -19,6 +18,10 @@ needs. A dialectic run on the wrong question wastes the whole loop. Return to
|
|||||||
`active-listening` 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 (robot, 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,
|
||||||
|
|||||||
@@ -0,0 +1,135 @@
|
|||||||
|
---
|
||||||
|
name: dream
|
||||||
|
description: >
|
||||||
|
Consolidate the last 24 hours of work in this repository: what held, what
|
||||||
|
broke, which hypotheses follow, into memory and improvement proposals.
|
||||||
|
Trigger on /dream or when the operator asks to reflect on or learn from
|
||||||
|
recent work.
|
||||||
|
---
|
||||||
|
|
||||||
|
Sleep on the work: replay what happened in this repository, separate what held
|
||||||
|
from what broke, and keep only the conclusions that survive an attack. A dream
|
||||||
|
that flatters the dreamer is worthless — an unverified success counts as
|
||||||
|
unknown, not as a win.
|
||||||
|
|
||||||
|
## Objective
|
||||||
|
|
||||||
|
The dream exists to raise the confidence of every future run in this repository
|
||||||
|
until the mistakes it found cannot happen again. Judge every finding, memory
|
||||||
|
entry, and proposal by one question: does it make the next run measurably less
|
||||||
|
likely to repeat this failure? What changes nothing about future behaviour is
|
||||||
|
not worth recording.
|
||||||
|
|
||||||
|
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
|
||||||
|
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` skill requires, and name what is still open.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
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
|
||||||
|
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.
|
||||||
|
|
||||||
|
Run only when the operator asks. The 12-hour reminder hook
|
||||||
|
(`hooks/reminder.sh`, stamped per repository in its git dir) is a hint for the
|
||||||
|
operator, not a trigger: print it and continue with their actual request.
|
||||||
|
|
||||||
|
## Step 1: recall
|
||||||
|
|
||||||
|
Gather the window's actual record, not your recollection of it:
|
||||||
|
|
||||||
|
- **History**: `git log --since` across all branches plus `--reflog`, with
|
||||||
|
`--stat`; the reflog carries the resets, rebases, and amends the log hides.
|
||||||
|
- **Working state**: `git status`, the staged and unstaged diff, stashes.
|
||||||
|
- **CI**: the runs created in the window, their conclusions, and the failing
|
||||||
|
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
|
||||||
|
comments on them.
|
||||||
|
- **Memory**: this project's memory directory and its `MEMORY.md` index — the
|
||||||
|
findings already recorded, so the dream extends them instead of repeating them.
|
||||||
|
- **Instructions**: `CLAUDE.md`, `AGENTS.md`, and the project's own skills, to
|
||||||
|
see which rule a mistake actually violated.
|
||||||
|
- **Sessions**: the readable session transcripts touched in the window, for the
|
||||||
|
operator corrections that never reached a commit.
|
||||||
|
|
||||||
|
## Step 2: score
|
||||||
|
|
||||||
|
Classify every action in the window, each with its evidence (`sha`, `file:line`,
|
||||||
|
run id):
|
||||||
|
|
||||||
|
- **Held** — shipped and still proven: green CI, a passing test that covers it,
|
||||||
|
no revert, no follow-up fix.
|
||||||
|
- **Broke** — reverted, amended, hot-fixed by a later commit, red CI, or
|
||||||
|
corrected by the operator. A failure that ended green is still a failure; count
|
||||||
|
the detour, not only the destination.
|
||||||
|
- **Unknown** — no verification exists either way. Unknown is a finding in its
|
||||||
|
own right, never a quiet pass.
|
||||||
|
|
||||||
|
## Step 3: hypothesize
|
||||||
|
|
||||||
|
Turn the scored record into falsifiable hypotheses, each written as *cause →
|
||||||
|
predicted consequence → what would disprove it*. Take a pattern (two or more
|
||||||
|
occurrences) or a single expensive failure; drop the one-off noise. A hypothesis
|
||||||
|
that nothing could disprove is not a hypothesis — cut it.
|
||||||
|
|
||||||
|
## Step 4: dialectic
|
||||||
|
|
||||||
|
Put every surviving hypothesis through the `dialectic` skill: thesis,
|
||||||
|
independent skeptics, synthesis, iterated to ~99%. What survives becomes a
|
||||||
|
finding. What gets refuted is recorded as discarded **with the refutation**, so
|
||||||
|
the next dream does not re-derive it.
|
||||||
|
|
||||||
|
## Step 5: write memory
|
||||||
|
|
||||||
|
Persist the findings in this project's memory directory: one file per fact, one
|
||||||
|
pointer line in `MEMORY.md`. Update the file that already covers a fact instead
|
||||||
|
of adding a duplicate, and delete the memories this run disproved. Absolute
|
||||||
|
dates, never "yesterday". Nothing that the repository already records — code
|
||||||
|
structure, git history, `CLAUDE.md` — belongs in memory.
|
||||||
|
|
||||||
|
## Step 6: offer the improvements
|
||||||
|
|
||||||
|
Two kinds come out of the findings:
|
||||||
|
|
||||||
|
- **Code** — the change to this repository's source, tests, config, or CI that
|
||||||
|
the finding calls for.
|
||||||
|
- **Agent instructions** — the missing or wrong rule in `CLAUDE.md`,
|
||||||
|
`AGENTS.md`, the project's own skills, or the contributor docs. Every mistake
|
||||||
|
the agent repeated in the window is a candidate: quote the line that failed to
|
||||||
|
prevent it, or name the rule that is absent, and propose the exact wording.
|
||||||
|
|
||||||
|
Ask for each one with `AskUserQuestion` and `multiSelect: false` so it renders
|
||||||
|
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
|
||||||
|
improvement. No repository file changes before an answer comes back.
|
||||||
|
|
||||||
|
## Step 7: autotune, then autoskill
|
||||||
|
|
||||||
|
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 `autoskill`
|
||||||
|
for the operator corrections the window revealed. Both keep their own
|
||||||
|
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
|
||||||
|
is twelve hours out: `date +%s > "$(git rev-parse --path-format=absolute --git-common-dir)/.dream-stamp"`.
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
- Evidence or unknown: a claim about what held needs a green signal, and its
|
||||||
|
absence is reported as unknown rather than assumed.
|
||||||
|
- Honest scoring: reverts, amends, and follow-up fixes are failures of the first
|
||||||
|
attempt even when the final state is green.
|
||||||
|
- Read-only until step 6: the dream inspects, and writes memory; repository code
|
||||||
|
changes only on an explicit radio answer.
|
||||||
|
- An empty window is a valid result — report "nothing in the last 24 hours in
|
||||||
|
this repository" and stop. Never invent activity to have something to dream
|
||||||
|
about.
|
||||||
|
- Prefer the structural fix over the remembered one: a test, check, hook, or
|
||||||
|
written rule that makes the error impossible beats a memory entry that only
|
||||||
|
reminds you of it.
|
||||||
|
- Report the whole ledger: held, broke, unknown, the surviving findings, the
|
||||||
|
refuted hypotheses with why they fell, and the confidence each closed error
|
||||||
|
class now carries into the next run.
|
||||||
Executable
+40
@@ -0,0 +1,40 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# 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
|
||||||
|
# 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
|
||||||
|
# to show the hint and keep going instead of running dream by itself.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
interval="${DREAM_INTERVAL_SECONDS:-43200}"
|
||||||
|
if [[ -n "${DREAM_STAMP:-}" ]]; then
|
||||||
|
stamp="${DREAM_STAMP}"
|
||||||
|
else
|
||||||
|
git_dir="$(git rev-parse --path-format=absolute --git-common-dir 2>/dev/null)" || exit 0
|
||||||
|
stamp="${git_dir}/.dream-stamp"
|
||||||
|
fi
|
||||||
|
|
||||||
|
now="$(date +%s)"
|
||||||
|
last=0
|
||||||
|
if [[ -f "${stamp}" ]]; then
|
||||||
|
read -r last <"${stamp}" || true
|
||||||
|
[[ "${last}" =~ ^[0-9]+$ ]] || last=0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ((now - last < interval)); then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$(dirname "${stamp}")"
|
||||||
|
printf '%s\n' "${now}" >"${stamp}"
|
||||||
|
|
||||||
|
cat <<'HINT'
|
||||||
|
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
|
||||||
|
request. Do NOT run dream, do not start replaying the history, do not ask
|
||||||
|
questions about it.
|
||||||
|
|
||||||
|
💤 /dream - consolidate the last 24h in this repository: what held, what
|
||||||
|
broke, which hypotheses follow, into memory and improvement proposals.
|
||||||
|
Runs only when you trigger it.
|
||||||
|
HINT
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
---
|
||||||
|
name: essence
|
||||||
|
description: >
|
||||||
|
Compact skills and memory to their essence after a health pass that finds
|
||||||
|
dead weight. Trigger on /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.
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
---
|
---
|
||||||
name: explain
|
name: explain
|
||||||
description: >
|
description: >
|
||||||
Explain code and its interactions. Trigger when the operator asks what a
|
Explain code and its interactions. Trigger when the operator asks what code
|
||||||
piece of code does, how a mechanism works, why components interact the way
|
does, how a mechanism works, why components interact, or invokes /explain on
|
||||||
they do, or invokes /explain on a file, function, role, or module.
|
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:
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
---
|
||||||
|
name: funnel
|
||||||
|
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 /funnel.
|
||||||
|
---
|
||||||
|
|
||||||
|
# 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 `confidence` skill, the break risk derived through the
|
||||||
|
`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. Name the goal, the exact base commit, the
|
||||||
|
scope that may change, the evidence expected back, a hard time cap (default
|
||||||
|
60 minutes) and what the agent must not do: no pushes, no gates or deploys
|
||||||
|
unless the resource chair of the `democracy` 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.
|
||||||
|
5. **Independent review.** Before anything is merged, run the `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 `democracy` claims and every claim
|
||||||
|
announced to other agents.
|
||||||
|
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
|
||||||
|
|
||||||
|
- Stop an agent at its time cap and ask for its findings; an uncapped brief can
|
||||||
|
run for hours and deliver nothing.
|
||||||
|
- 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.
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
---
|
---
|
||||||
name: help-skills
|
name: help-skills
|
||||||
description: >
|
description: >
|
||||||
List and explain every skill available in the current session. Trigger on
|
List and explain every skill in the session. Trigger on /help-skills,
|
||||||
/help-skills, "welche skills gibt es", "skill übersicht", or when the
|
"welche skills gibt es", "skill übersicht", or when the operator asks what
|
||||||
operator asks what skills exist or what a skill does. Portable across
|
skills exist or what one does.
|
||||||
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:
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
---
|
---
|
||||||
name: medusa
|
name: medusa
|
||||||
description: >
|
description: >
|
||||||
Run the MEDUSA AI security scanner (medusa-security on PyPI) against a
|
Run the MEDUSA security scanner (medusa-security, PyPI) on a repository,
|
||||||
repository, working tree, or secret stores. Use when the user asks for a
|
working tree or secret stores. Use for security scans, vetting a repo before
|
||||||
security scan, supply-chain vetting of a repo before cloning, leaked-secret
|
cloning, leaked-secret detection in chat or shell history, or /medusa.
|
||||||
detection in AI chat or shell history, or invokes /medusa. Defensive scanning
|
Defensive only.
|
||||||
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).
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
---
|
||||||
|
name: merge-fix
|
||||||
|
description: >
|
||||||
|
Hunt down the commits on any branch that carry a described fix and merge it
|
||||||
|
into the current branch. Trigger on /merge-fix <description> or when the
|
||||||
|
operator asks to bring over a fix from another branch.
|
||||||
|
---
|
||||||
|
|
||||||
|
The free text after `/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:
|
||||||
|
|
||||||
|
```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
|
||||||
|
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.
|
||||||
Executable
+67
@@ -0,0 +1,67 @@
|
|||||||
|
#!/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())
|
||||||
@@ -1,15 +1,10 @@
|
|||||||
---
|
---
|
||||||
name: no-defaults
|
name: no-defaults
|
||||||
description: >
|
description: >
|
||||||
Forbid default values for parameters, settings, and environment lookups in
|
Forbid default values for parameters, settings and environment lookups;
|
||||||
every language. Load and apply AUTOMATICALLY on every task that writes,
|
demand explicit values and fail loudly. Apply AUTOMATICALLY on every task
|
||||||
edits, generates, refactors, or reviews code, config, templates, schemas,
|
that writes, edits, generates, refactors or reviews code, config, templates,
|
||||||
or infrastructure — never wait to be asked. A default silently substitutes
|
schemas or infrastructure.
|
||||||
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
|
||||||
@@ -59,7 +54,7 @@ with `false`, `null`, or `""` is permitted, and with any other value is permitte
|
|||||||
only when justified in a comment.
|
only when justified in a comment.
|
||||||
|
|
||||||
| Language | Never | Instead |
|
| Language | Never | Instead |
|
||||||
|---|---|---|
|
| --- | --- | --- |
|
||||||
| Bash | `${VAR:-x}`, `${VAR:=x}`, `: "${VAR:=x}"` | `: "${VAR:?set VAR in <file>}"` |
|
| Bash | `${VAR:-x}`, `${VAR:=x}`, `: "${VAR:=x}"` | `: "${VAR:?set VAR in <file>}"` |
|
||||||
| Python | `def f(a=1)`, `d.get(k, x)`, `getattr(o, n, x)`, `os.environ.get(k, x)`, `argparse(default=)` | required args, `d[k]`, `os.environ[k]`, `add_argument(required=True)` |
|
| Python | `def f(a=1)`, `d.get(k, x)`, `getattr(o, n, x)`, `os.environ.get(k, x)`, `argparse(default=)` | required args, `d[k]`, `os.environ[k]`, `add_argument(required=True)` |
|
||||||
| Jinja / Ansible | `\| default(x)`, `lookup('env', k) \| default('')` | `\| mandatory`, `assert` on the var |
|
| Jinja / Ansible | `\| default(x)`, `lookup('env', k) \| default('')` | `\| mandatory`, `assert` on the var |
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
---
|
---
|
||||||
name: observe
|
name: observe
|
||||||
description: >
|
description: >
|
||||||
Watch a named target (a CI run, a pipeline, a build, a log) on a fixed
|
Watch a named target (CI run, pipeline, build, log) on a cadence and triage
|
||||||
cadence and apply the triage skill whenever new failures appear. Trigger on
|
new failures as they appear. Trigger on /observe <target> or when the
|
||||||
/observe <target> or when the operator asks to keep monitoring something and
|
operator asks to keep monitoring and fix regressions.
|
||||||
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
|
||||||
@@ -27,7 +26,8 @@ gave one): after each observation, schedule the next wake-up yourself and pass
|
|||||||
the same `/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). Enumerate the failures exactly as the `triage` skill's step 1
|
log's tail); for a GitHub Actions run, the ci-artifacts skill's
|
||||||
|
`ci_failures.py <run>`. Enumerate the failures exactly as the `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
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
---
|
---
|
||||||
name: plan
|
name: plan
|
||||||
description: >
|
description: >
|
||||||
Turn a request into an objective-oriented plan and hand it to the robot skill
|
Turn a request into an objective-oriented plan and hand it to the robot
|
||||||
for autonomous execution. Trigger on /plan or when the operator asks for a
|
skill. Trigger on /plan or when the operator asks for a plan, breakdown or
|
||||||
plan, a breakdown, or a strategy for a task before any work starts. Portable
|
strategy before work starts.
|
||||||
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
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
---
|
---
|
||||||
name: refactor
|
name: refactor
|
||||||
description: >
|
description: >
|
||||||
Refactor every file that is currently uncommitted in the git working tree -
|
Refactor every uncommitted file (staged, unstaged, untracked) up to the
|
||||||
staged, unstaged and untracked - up to the project's coding standards,
|
project's standards without changing behaviour. Trigger on /refactor or when
|
||||||
without changing behaviour. Trigger on /refactor or when the operator asks to
|
the operator asks to clean up, tidy or polish the current changes.
|
||||||
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
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
---
|
---
|
||||||
name: robot
|
name: robot
|
||||||
description: >
|
description: >
|
||||||
Work fully autonomously to a goal. Trigger when the operator wants hands-off,
|
Work fully autonomously to a goal: no check-ins, no stopping before the goal,
|
||||||
uninterrupted execution — the agent drives the task to completion without
|
only commands already cleared. Trigger when the operator wants hands-off,
|
||||||
check-ins, never stops before the goal is reached, and uses only commands it
|
uninterrupted execution.
|
||||||
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.
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
---
|
||||||
|
name: sceptic
|
||||||
|
description: >
|
||||||
|
Analyse a repository, path or diff with a hostile eye for violated
|
||||||
|
principles: SPOT, DRY, KISS, SRP, security at trust boundaries. Trigger on
|
||||||
|
/sceptic, "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 (`medusa`). 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 `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 `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 `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 `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.
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
---
|
||||||
|
name: 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.
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
---
|
---
|
||||||
name: shortcuts
|
name: shortcuts
|
||||||
description: >
|
description: >
|
||||||
Portable operator conversation shortcuts, reusable across every project.
|
Operator conversation shortcuts. Trigger when the message is exactly one of
|
||||||
Trigger when the user's message is exactly one of these tokens (optionally
|
these tokens, optionally with arguments: sobu, soco, socobu, socon, sona,
|
||||||
followed by arguments): sobu, soco, socobu, socon, sona, sote. Expand the
|
sote. Expand it and act on the expanded instruction.
|
||||||
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.
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
---
|
||||||
|
name: status-matrix
|
||||||
|
description: >
|
||||||
|
Print one matrix of everything in flight: tasks, workflows, background jobs,
|
||||||
|
with confidence and break risk per item. Trigger on /status-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 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 `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.
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
---
|
|
||||||
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.
|
|
||||||
+59
-6
@@ -1,10 +1,9 @@
|
|||||||
---
|
---
|
||||||
name: test
|
name: test
|
||||||
description: >
|
description: >
|
||||||
Discover the test targets of the project's Makefile, let the operator pick
|
Discover the Makefile's test targets, let the operator pick, run and report;
|
||||||
which ones to run, then run them and report. Trigger on /test or when the
|
fix mode loops until green. Trigger on /test, /test --fix, or when the
|
||||||
operator asks to run the tests, the test suite, or a specific test target
|
operator asks to run the tests, make them pass, or get the suite green.
|
||||||
without naming the exact command. Portable across projects.
|
|
||||||
---
|
---
|
||||||
|
|
||||||
The Makefile is the source of truth for how this project runs tests. Never
|
The Makefile is the source of truth for how this project runs tests. Never
|
||||||
@@ -73,5 +72,59 @@ 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
|
quoted verbatim. Never paraphrase a failure. Then one line with the exact
|
||||||
command to reproduce the failure alone.
|
command to reproduce the failure alone.
|
||||||
|
|
||||||
Do not fix what failed unless the operator asks - report first. If the operator
|
Do not fix what failed unless the operator asks - report first. A request to
|
||||||
does ask for a fix, apply the `triage` skill to it.
|
make the tests pass, fix them, or get the suite green (`/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
|
||||||
|
`triage` skill to reach a verified root cause; for a cause still unclear
|
||||||
|
after one look, apply `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 `no-defaults` and `comments-clean` 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 `confidence` skill. Do
|
||||||
|
not commit unless the operator asked for a commit.
|
||||||
+72
-18
@@ -1,10 +1,9 @@
|
|||||||
---
|
---
|
||||||
name: triage
|
name: triage
|
||||||
description: >
|
description: >
|
||||||
Triage a failing CI run, pipeline, or job set to a verified root cause and a
|
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
|
real fix, one cluster per worktree. Trigger when a build, test or deploy
|
||||||
jobs. Applies the dialectic skill to every failing job independently. Portable
|
pipeline has failed jobs.
|
||||||
across projects.
|
|
||||||
---
|
---
|
||||||
|
|
||||||
Drive a red pipeline to green by root cause, not by guesswork. Treat every
|
Drive a red pipeline to green by root cause, not by guesswork. Treat every
|
||||||
@@ -13,6 +12,23 @@ root cause is proven.
|
|||||||
|
|
||||||
## Procedure
|
## Procedure
|
||||||
|
|
||||||
|
For a GitHub Actions run, the 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
|
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
|
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 —
|
job that fails only because an upstream job did is not a separate root cause —
|
||||||
@@ -21,7 +37,14 @@ root cause is proven.
|
|||||||
actual failing line, not by job name. Jobs whose messages differ in anything
|
actual failing line, not by job name. Jobs whose messages differ in anything
|
||||||
but the interpolated identifiers (host, app, port, path) belong in separate
|
but the interpolated identifiers (host, app, port, path) belong in separate
|
||||||
clusters.
|
clusters.
|
||||||
3. **Download artifacts per cluster.** For each cluster, fetch every artifact of
|
3. **Coordinate every cluster.** Run the `democracy` 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.
|
||||||
|
4. **Download artifacts per cluster.** Use the `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
|
one representative job: reports, rescue diagnostics, inventories, container
|
||||||
logs. Pull a second member's artifacts only when the representative's
|
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
|
evidence does not explain the whole cluster. The job log usually names an
|
||||||
@@ -29,21 +52,39 @@ root cause is proven.
|
|||||||
exception and the state dumps are inside the artifact. Do this even when the
|
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
|
log looks conclusive, because a log that explains the symptom rarely
|
||||||
explains the cause.
|
explains the cause.
|
||||||
4. **Dialectic per cluster.** For EACH cluster, invoke the `dialectic` skill:
|
5. **One funnel per cluster.** Hand every cluster you claimed in step 3 to the
|
||||||
|
`funnel` 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 hard time cap.
|
||||||
|
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 `dialectic` skill:
|
||||||
form a thesis about the root cause from evidence (the job log, its artifacts,
|
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
|
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
|
skeptics, and iterate to a ~99% thesis.
|
||||||
their investigations in parallel where the tooling allows.
|
7. **Distinguish shared vs hidden causes.** A cluster is a hypothesis, not a
|
||||||
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.
|
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
|
When a job hides a second failure behind the first, keep going until the job
|
||||||
is actually green, not just past the first error.
|
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
|
8. **Fix at the root, through the funnel.** Apply the real fix for each proven
|
||||||
cause. Never mask a failure — no retry-until-pass, no disabling the check, no
|
root cause in the cluster's sidetree, with a test and a negative control as
|
||||||
soft-skip. If a failure is genuinely external (upstream outage, flaky infra),
|
the `funnel` skill demands. The fix enters the main line only at the funnel's
|
||||||
confirm that with evidence and surface it honestly instead of fixing around
|
bar (at least 90 % fix confidence, under 5 % break risk) after its
|
||||||
it.
|
independent review; below the bar the cluster ends as a proposal for the
|
||||||
7. **Follow the run.** While the run is still in progress, re-check it
|
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 `democracy` 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 `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
|
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.
|
only when it has finished and every failure has a verified fix.
|
||||||
|
|
||||||
@@ -51,7 +92,20 @@ root cause is proven.
|
|||||||
|
|
||||||
- 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.
|
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
|
- 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
|
it is transient before dismissing), or already-fixed-by-a-later-commit. For
|
||||||
the fix is present in the run's commit before claiming it).
|
that last class the comparison is `<run-sha>..HEAD`, never the run's commit
|
||||||
- Report a per-job verdict: root cause, the evidence that proves it, and the fix.
|
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.
|
||||||
@@ -0,0 +1,192 @@
|
|||||||
|
---
|
||||||
|
name: tune
|
||||||
|
description: >
|
||||||
|
Find and apply performance optimizations (CI, deploy, build, gate, hot
|
||||||
|
paths) without reducing logic, coverage or correctness. Trigger on /tune,
|
||||||
|
when a job dies on its time budget, when a pipeline or deploy grows slower,
|
||||||
|
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 `active-listening`
|
||||||
|
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,
|
||||||
|
resource use (CPU, memory, disk, I/O), and language or runtime choice for
|
||||||
|
hot paths.
|
||||||
|
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 `funnel` 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 (triage, observe) or an autonomous run invoked tune, 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 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 `democracy` 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 `democracy`
|
||||||
|
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.
|
||||||
|
- **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.
|
||||||
|
|
||||||
|
## 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 runtime
|
||||||
|
|
||||||
|
Look for language optimizations where they make sense, for example replacing a
|
||||||
|
Python hot path with Rust:
|
||||||
|
|
||||||
|
- **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.
|
||||||
|
- **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 change is applied only when the session's limits allow new
|
||||||
|
languages 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 `funnel` skill: one sidetree
|
||||||
|
per candidate, branched from an exact commit, with a hard 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 and deliver nothing; stop an agent at its cap and collect
|
||||||
|
its findings.
|
||||||
|
|
||||||
|
Before merging, run independent skeptics through the `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 `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.
|
||||||
+34
-3
@@ -37,6 +37,27 @@ class TestAutotuneSkill(unittest.TestCase):
|
|||||||
self.assertIn("multiSelect: false", text)
|
self.assertIn("multiSelect: false", text)
|
||||||
|
|
||||||
|
|
||||||
|
class TestSkillPersistence(unittest.TestCase):
|
||||||
|
"""Both skill writers must persist to the skills repository AND the live dirs."""
|
||||||
|
|
||||||
|
def test_autotune_names_every_target(self):
|
||||||
|
text = SKILL.read_text(encoding="utf-8")
|
||||||
|
for target in (
|
||||||
|
"SKILLS_REPO",
|
||||||
|
"skills/<name>/SKILL.md",
|
||||||
|
"~/.claude/skills/",
|
||||||
|
"~/.agents/skills/",
|
||||||
|
):
|
||||||
|
self.assertIn(target, text)
|
||||||
|
|
||||||
|
def test_autoskill_routes_to_the_same_persistence(self):
|
||||||
|
text = (REPO_ROOT / "skills" / "autoskill" / "SKILL.md").read_text(
|
||||||
|
encoding="utf-8"
|
||||||
|
)
|
||||||
|
for target in ("Persistence", "~/.claude/skills/", "~/.agents/skills/"):
|
||||||
|
self.assertIn(target, text)
|
||||||
|
|
||||||
|
|
||||||
class TestAutotuneHook(unittest.TestCase):
|
class TestAutotuneHook(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.tmp = tempfile.TemporaryDirectory()
|
self.tmp = tempfile.TemporaryDirectory()
|
||||||
@@ -70,7 +91,9 @@ class TestSettingsRegistration(unittest.TestCase):
|
|||||||
self.settings = Path(self.tmp.name) / "settings.json"
|
self.settings = Path(self.tmp.name) / "settings.json"
|
||||||
|
|
||||||
def _patch(self) -> dict:
|
def _patch(self) -> dict:
|
||||||
subprocess.run(["node", str(PATCHER), str(self.settings)], check=True, capture_output=True)
|
subprocess.run(
|
||||||
|
["node", str(PATCHER), str(self.settings)], check=True, capture_output=True
|
||||||
|
)
|
||||||
return json.loads(self.settings.read_text(encoding="utf-8"))
|
return json.loads(self.settings.read_text(encoding="utf-8"))
|
||||||
|
|
||||||
def test_hook_registered_once(self):
|
def test_hook_registered_once(self):
|
||||||
@@ -81,11 +104,19 @@ class TestSettingsRegistration(unittest.TestCase):
|
|||||||
|
|
||||||
def test_existing_hooks_preserved(self):
|
def test_existing_hooks_preserved(self):
|
||||||
self.settings.write_text(
|
self.settings.write_text(
|
||||||
json.dumps({"hooks": {"UserPromptSubmit": [{"hooks": [{"type": "command", "command": "true"}]}]}}),
|
json.dumps(
|
||||||
|
{
|
||||||
|
"hooks": {
|
||||||
|
"UserPromptSubmit": [
|
||||||
|
{"hooks": [{"type": "command", "command": "true"}]}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
),
|
||||||
encoding="utf-8",
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
data = self._patch()
|
data = self._patch()
|
||||||
self.assertEqual(len(data["hooks"]["UserPromptSubmit"]), 2)
|
self.assertEqual(len(data["hooks"]["UserPromptSubmit"]), 4)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
"""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" / "ci-artifacts" / "scripts" / "ci_failures.py"
|
||||||
|
FIND_FIX = REPO_ROOT / "skills" / "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_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()
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
"""Exercise the democracy 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" / "democracy" / "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()
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
"""Validate the dream reminder hook and its settings registration."""
|
||||||
|
|
||||||
|
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]
|
||||||
|
SKILL = REPO_ROOT / "skills" / "dream" / "SKILL.md"
|
||||||
|
HOOK = REPO_ROOT / "skills" / "dream" / "hooks" / "reminder.sh"
|
||||||
|
PATCHER = REPO_ROOT / "scripts" / "enable-plugins.js"
|
||||||
|
|
||||||
|
|
||||||
|
def _run_hook(
|
||||||
|
cwd: Path, stamp: Path | None = None, interval: str = "43200"
|
||||||
|
) -> subprocess.CompletedProcess:
|
||||||
|
env = {
|
||||||
|
"PATH": "/usr/bin:/bin",
|
||||||
|
"HOME": str(cwd),
|
||||||
|
"DREAM_INTERVAL_SECONDS": interval,
|
||||||
|
}
|
||||||
|
if stamp is not None:
|
||||||
|
env["DREAM_STAMP"] = str(stamp)
|
||||||
|
return subprocess.run(
|
||||||
|
["bash", str(HOOK)],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
check=True,
|
||||||
|
cwd=str(cwd),
|
||||||
|
env=env,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestDreamSkill(unittest.TestCase):
|
||||||
|
def test_skill_is_trigger_only(self):
|
||||||
|
text = SKILL.read_text(encoding="utf-8")
|
||||||
|
self.assertIn("name: dream", text)
|
||||||
|
self.assertIn("multiSelect: false", text)
|
||||||
|
|
||||||
|
|
||||||
|
class TestDreamHook(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.tmp = tempfile.TemporaryDirectory()
|
||||||
|
self.addCleanup(self.tmp.cleanup)
|
||||||
|
self.cwd = Path(self.tmp.name)
|
||||||
|
self.stamp = self.cwd / ".claude" / ".dream-stamp"
|
||||||
|
|
||||||
|
def test_first_run_hints_and_stamps(self):
|
||||||
|
result = _run_hook(self.cwd, self.stamp)
|
||||||
|
self.assertIn("/dream", result.stdout)
|
||||||
|
self.assertTrue(self.stamp.is_file())
|
||||||
|
|
||||||
|
def test_second_run_is_silent_within_interval(self):
|
||||||
|
_run_hook(self.cwd, self.stamp)
|
||||||
|
self.assertEqual(_run_hook(self.cwd, self.stamp).stdout, "")
|
||||||
|
|
||||||
|
def test_hint_returns_after_the_interval(self):
|
||||||
|
_run_hook(self.cwd, self.stamp)
|
||||||
|
self.assertIn("/dream", _run_hook(self.cwd, self.stamp, interval="0").stdout)
|
||||||
|
|
||||||
|
def test_corrupt_stamp_does_not_crash(self):
|
||||||
|
self.stamp.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
self.stamp.write_text("not-a-timestamp\n", encoding="utf-8")
|
||||||
|
self.assertIn("/dream", _run_hook(self.cwd, self.stamp).stdout)
|
||||||
|
|
||||||
|
def test_outside_a_repository_it_stays_silent(self):
|
||||||
|
self.assertEqual(_run_hook(self.cwd).stdout, "")
|
||||||
|
|
||||||
|
@unittest.skipUnless(shutil.which("git"), "git not installed")
|
||||||
|
def test_stamp_is_per_repository(self):
|
||||||
|
for name in ("one", "two"):
|
||||||
|
repo = self.cwd / name
|
||||||
|
repo.mkdir()
|
||||||
|
subprocess.run(
|
||||||
|
["git", "init", "-q", str(repo)], check=True, capture_output=True
|
||||||
|
)
|
||||||
|
self.assertIn("/dream", _run_hook(repo).stdout)
|
||||||
|
self.assertTrue((repo / ".git" / ".dream-stamp").is_file())
|
||||||
|
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("/dream", _run_hook(repo).stdout)
|
||||||
|
self.assertEqual(_run_hook(tree).stdout, "")
|
||||||
|
|
||||||
|
|
||||||
|
@unittest.skipUnless(shutil.which("node"), "node not installed")
|
||||||
|
class TestSettingsRegistration(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.tmp = tempfile.TemporaryDirectory()
|
||||||
|
self.addCleanup(self.tmp.cleanup)
|
||||||
|
self.settings = Path(self.tmp.name) / "settings.json"
|
||||||
|
|
||||||
|
def _patch(self) -> dict:
|
||||||
|
subprocess.run(
|
||||||
|
["node", str(PATCHER), str(self.settings)], check=True, capture_output=True
|
||||||
|
)
|
||||||
|
return json.loads(self.settings.read_text(encoding="utf-8"))
|
||||||
|
|
||||||
|
def test_hook_registered_once(self):
|
||||||
|
self._patch()
|
||||||
|
data = self._patch()
|
||||||
|
entries = json.dumps(data["hooks"]["UserPromptSubmit"])
|
||||||
|
self.assertEqual(entries.count("dream/hooks/reminder.sh"), 1)
|
||||||
|
self.assertEqual(entries.count("autotune/hooks/reminder.sh"), 1)
|
||||||
|
gate = json.dumps(data["hooks"]["PreToolUse"])
|
||||||
|
self.assertEqual(gate.count("democracy/scripts/council.py"), 1)
|
||||||
|
self.assertEqual(gate.count("democracy/scripts/watch.py"), 1)
|
||||||
|
self.assertEqual(entries.count("democracy/scripts/watch.py"), 1)
|
||||||
|
post = json.dumps(data["hooks"]["PostToolUse"])
|
||||||
|
self.assertEqual(post.count("democracy/scripts/watch.py"), 1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -12,6 +12,8 @@ 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):
|
||||||
@@ -61,6 +63,16 @@ 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("*/")):
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
"""The self-improving skills must share the objective held by autotune."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
SKILLS = REPO_ROOT / "skills"
|
||||||
|
ROUTERS = ("autoskill", "confidence")
|
||||||
|
|
||||||
|
|
||||||
|
class TestOptimizeObjective(unittest.TestCase):
|
||||||
|
def test_autotune_names_the_three_axes(self):
|
||||||
|
text = (SKILLS / "autotune" / "SKILL.md").read_text(encoding="utf-8")
|
||||||
|
self.assertIn("## Objective", text)
|
||||||
|
for axis in ("**Tokens**", "**Time**", "**Quality**"):
|
||||||
|
self.assertIn(axis, text)
|
||||||
|
|
||||||
|
def test_routers_point_at_it(self):
|
||||||
|
for skill in ROUTERS:
|
||||||
|
text = (SKILLS / skill / "SKILL.md").read_text(encoding="utf-8")
|
||||||
|
self.assertIn(
|
||||||
|
"`autotune` skill's **Objective**",
|
||||||
|
text,
|
||||||
|
f"{skill} does not route to the autotune objective",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
"""Exercise the democracy 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" / "democracy" / "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_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",
|
||||||
|
"cache_dirs": [str(self.cache)],
|
||||||
|
"cache_budget_percent": 3,
|
||||||
|
"slot_need_gb": {"swarm": 30},
|
||||||
|
"notify": ["true"],
|
||||||
|
}
|
||||||
|
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_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_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_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()
|
||||||
Reference in new issue
Block a user