Global working rules, software standards, operating modes, role agents and gate scripts under one namespace: /adyusuf:<name>. Installed from the…

One script installs the adyusuf plugin, or updates it. It asks GitHub for the newest commit of prod; if an older version is installed it asks "Remove the old one and install the new one? [y/N]" and, on yes, replaces it while your settings and the marketplace link carry on unchanged. Without the plugin it links the marketplace and installs from scratch. It needs python3, git and claude on PATH; read the file before you run it (about 270 lines, standard library only).
macOS / Linux (Terminal):
d=$(mktemp -d) && curl -fsSL https://raw.githubusercontent.com/adyusuf/claude-code-standards/prod/scripts/plugin-setup.py -o "$d/plugin-setup.py" && python3 "$d/plugin-setup.py"
Windows (PowerShell):
$d = New-Item -ItemType Directory -Path (Join-Path $env:TEMP ([guid]::NewGuid()))
Invoke-WebRequest https://raw.githubusercontent.com/adyusuf/claude-code-standards/prod/scripts/plugin-setup.py -OutFile "$d\plugin-setup.py"
python "$d\plugin-setup.py"
Add --check to only report (exit 1 when work is due), --yes to answer the question for yourself, --ref <branch> to link another branch on a first install. Afterwards open a new Claude Code session (or /reload-plugins). Windows also needs a python3 on PATH for the plugin's own hooks: SETUP.md §1.
"claude is not on PATH"? The script looks in the installers' usual folders and in the copy the Claude desktop app keeps for its Code tab, whichever way the app was installed (Windows: %APPDATA%\Claude, the Microsoft Store package's %LOCALAPPDATA%\Claude-Data\claude_…, %LOCALAPPDATA%\Claude-3p; macOS: ~/Library/Application Support/Claude or Claude-3p; then claude-code/<version>/…), and uses what it finds for that run — only a program that answers --version as Claude Code counts, so the desktop app's own claude.exe launcher on PATH is skipped. If you have only the desktop app and it finds nothing, do the same two steps inside the app's Code tab: /plugin marketplace add adyusuf/claude-code-standards#prod, then /plugin install adyusuf@adyusuf-marketplace. Or pass the program yourself: --claude <full path> (PowerShell: Get-Command claude; macOS/Linux: command -v claude). What the script does, step by step: Level 3.
AI changed our lives — at least the lives of those of us who write software.
The coding world will not be what it was. But the real question is not whether the tools got better. It is this: in a world this vast, how do we get the most out of it without losing our way in it?
My answer turned out to be boring, and it is this repository. Not a prompt collection and not a framework — a rule set, a gate, and an audit protocol that together do the things I always knew I should do and mostly could not:
Agentic coding, not just an AI coding tool — that was a choice, and here is the reasoning: a tool that answers questions cannot run a gate, and rules nobody executes are documentation. The discipline only becomes real when something acts on it per change. That said, the lowest mode (A) uses no agents at all, so a plain coding assistant gets the same rules and the same gate; the agents are how it scales, not how it works.
There is one thing behind all of it. An agent will happily apply every practice on that list. An agent will just as happily tell you that it did. Everything here is built for that gap: rules loaded before the work starts, gates that fail loudly when a step did not run, and reports that are not accepted without evidence.
Concretely, this is what that gap looks like. One project reported 95.0% test coverage — in CI, on a dashboard, undisputed. 85% of the denominator was EF Core migrations: generated code that executes by itself whenever the tests run, so it counts as covered without anyone ever asserting anything about it. The honest figure was 83.0%. And because that single number averaged four codebases into one, it was also hiding a web client at ~14%, where 107 of 136 files had no test touching them at all. Nobody lied. The number was just never asked the right question, and nothing was gating on the answer. → Case 01
This is my live configuration, running as-is. It is not a sample, a write-up, or a tidied copy of something I keep privately: it is installed as a Claude Code plugin from this repository's prod branch, so every session I open — across nine repositories, .NET and TypeScript, web and mobile — loads exactly the files you are reading. When a rule here is wrong, it is wrong in my own working day first. That is also why it changes so often: prod is the live branch, work happens on dev, and promoting between them is a decision I have to make about my own tooling.
Not for you if you want a prompt pack. There are no clever prompts here. There are rules, gates, measurements, and the record of what each one cost.
These paste straight into your own CLAUDE.md. They are stack-agnostic, they do not depend on anything else in this repository, and every one of them exists because work was reported as finished when it was not.
1. Report the truth. If tests are red, say so with the output; name any step
that was skipped. Never report completion on the basis of "it probably
works".
2. Before saying "done", re-read the ORIGINAL request — not a summarised memory
of it — and check it item by item. Anything knowingly deferred is listed in
the final report, never skipped silently.
3. A gate that did not run did not pass. A missing tool is reported as SKIPPED
and the result is INCOMPLETE, never green. The exit code is the gate.
4. A new rule is never left verbal. The moment a permanent decision is made,
write it to the rule file in the same turn.
5. Line coverage is measured per codebase and never averaged. Only GENERATED
code may leave the denominator, and the exclusion list carries a reason per
entry.
In this repository these are rules #15, #24, #19 + #25, #14 and #29 — the numbering is kept stable so the decision log can be read alongside them.
Why these five and not the other twenty-eight: each one closes a gap that an agent cannot be trusted to close on its own. An agent summarising your request back to itself will drop an item; an agent whose linter was not installed will still say the lint passed; an agent told a rule in conversation will forget it by the next session. Rules 1-4 are about the report being true, and rule 5 is the measurement most often false in a way nobody notices.
Full set: CLAUDE.md (34 rules) · why each exists: docs/decision-log.md.
The list above is the promise. This is the mechanism, in the same order.
1. The practices become rules, not intentions. 34 invariant rules load into every session; 24 standards documents carry the detail. The agent does not get to decide whether backward compatibility matters this time.
2. Where a rule is not enough, a gate has an exit code. A shared gate (scripts/gate-core.sh) runs the same step set in every repository: formatter, typecheck, build, unit tests, coverage per codebase, secret scan, dependency CVE, SAST, backward-compatibility scan, guidance-file budget, and a missing-e2e-spec check. One step cannot be waived at all — coverage — because a threshold with an exception is a suggestion.
3. The cost is cut where the measurement says it is, not where it feels like it is. A subagent's tokens run roughly 4x an orchestrator turn, and the fixed prompt prefix is re-read on every single request — across the records here that is ~11.1 billion tokens. So the default mode uses almost no agents, the expensive modes are opt-in, and the audit protocol buys its rigour from the shape of the output rather than from another agent: a role returns the command that produced its finding, and re-running that command costs about nothing.
4. Scales up to agent teams when you want them. Five operating modes, A to E: no agents, three agents, nine roles, fourteen roles, fan-out. The mode is one file in the project; choosing it is the approval, so there is no per-call friction. Nothing else in the rule set changes with the mode.
The roles are subagents, not personas in a prompt. Each one is spawned by the orchestrator with its own context window, its own tool allow-list and its own model — and that is exactly why the bill behaves the way it does: a subagent starts by reading the whole fixed prefix again, which is what makes its tokens run about 4x an orchestrator turn. So the model is assigned per role by what the role actually has to do, not uniformly:
| Role | Model | Why that tier | Tools it may use |
|---|---|---|---|
architect | opus | designs a change across many files; a wrong plan costs more than the model does | read-only + shell |
developer | opus | writes product code against a fixed contract | read/write + shell |
qa | opus | the first review pass — correctness, security, backward compatibility | read-only + shell |
security | opus | OWASP, secret leakage, authorization, gate integrity | read-only + shell |
data | opus | schema, migrations, data loss risk — the least reversible work there is | read-only + shell |
analyst | sonnet | reads a lot, returns little; must also return the command that produced the finding | read-only + shell |
test-writer | sonnet | tests for a stated behaviour, scope already decided | read/write + shell |
e2e-writer | sonnet | Playwright/Maestro specs against the test environment only | read/write + shell |
devops | sonnet | CI, deploy, gate and backup configuration — its output still goes through qa | read/write + shell |
coverage-auditor | sonnet | measures the per-codebase threshold; writes no tests | read-only + shell |
observability | sonnet | logging, metrics, tracing, alerting gaps | read-only + shell |
designer | sonnet | flow, states, accessibility, empty and error states | read-only |
product-manager | sonnet | scope and acceptance criteria; output goes to human approval | read-only |
doc-writer | haiku | writes a decided rule into the right file | read/write, no shell |
⚠️ The orchestrator has no model: field — it is whatever /model selected for the session, and it is the most expensive seat at the table, because its context is re-read on every single request. Across the records in this repository that is where the money went: 73% of total spend was orchestrator cache reads and 7.7% was agent turns. Choosing cheaper agents while leaving the orchestrator unexamined optimises the small half.
Every model here is a Claude model on purpose, and the reason is written down rather than assumed: a subagent's model: field only accepts Anthropic models, so a third-party comparison would answer a question this system cannot act on.
5. Speed comes from where the checks sit, not from skipping them. Merging to dev is deliberately fast — build and unit tests only. The heavy gates run where code leaves for the outside world. The formatter runs once at the end of a task list, not per commit. E2E belongs to the pre-production gate alone, not to every branch.
6. The auditors assume the agents are wrong. Every role whose output someone relies on closes its report with an evidence block: the command it ran, each requested item mapped to file:line, and what it could not verify. A report with no evidence is not accepted. What cannot be verified does not count as fine. If something is missing, the work goes back to the producer — it is not escalated to the human as a question.
flowchart LR
R["Rules<br/>34 invariants + 24 standards"] --> G["Gates<br/>one shared step set<br/>per promotion"]
G --> A["Audit<br/>evidence block<br/>at every handoff"]
A --> M["Measurement<br/>ledger + decision log"]
M -->|"a rule that cost more<br/>than it returned is retired"| R
The live configuration, not a showcase written for display — most rules exist because something broke first.
| What | Where | Count |
|---|---|---|
| Working rules (index loaded every session) | CLAUDE.md | 34 rules |
| Engineering standards | standards/ | 24 documents + 7 templates |
| Agent roles (each with explicit scope and prohibitions) | agents/ | 14 roles |
| Operating modes (agent use + review + approval policy) | modes/ | 5 modes |
| Gate, measurement and install scripts | scripts/ | 30 scripts + 715 tests |
Slash commands, skills and hooks — one plugin, /adyusuf:<name> | .claude-plugin/, commands/, skills/, hooks/ | 9 |
| Decision log (rationale and measurement per rule) | docs/ | — |
pie showData title Repository composition (files)
"standards/ (24 docs + 7 templates)" : 32
"agents/ (14 roles)" : 14
"modes/ (5 modes + selection guide)" : 13
"scripts/ (gates, hooks, measurement, tests)" : 36
"docs/ (decision log + case studies + method)" : 9
"commands/ + skills/" : 9
"root (CLAUDE.md, settings, license)" : 5
There are numbers behind these rules, not opinions. The decision log records how each rule was discovered, which measurement produced it, and the text of rules that have since been retired.
Test coverage: what was reported vs. what was true
| Reported (raw) | ████████████████████ | 95.0% |
| Honest (generated code excluded) | █████████████████░░░ | 83.0% |
| Same project, web codebase | ███░░░░░░░░░░░░░░░░░ | ~14% |
EF Core migrations made up 85% of the denominator. A single averaged number hid the fact that 107 of 136 web files had no test touching them at all.
Where the cost actually was
| Orchestrator cache reads | ███████████████░░░░░ | 73% of total |
| Agent turns | ██░░░░░░░░░░░░░░░░░░ | 7.7% of total |
Every agent call required approval, and the stated reason was cost. The measurement showed the approval friction was guarding 7.7% of the bill while slowing down every turn.
Estimate vs. measurement for one agent role (qa, opus)
| Estimated (before) | ████████████████████ | $8 – $20 / turn |
| Measured (3 records, script) | ████░░░░░░░░░░░░░░░░ | $4.10 / turn |
The estimate was 2–5x too high. The table was corrected; the estimate was not defended.
One working day, nine repositories, one shared gate. Every number below was read from the run that produced it — each codebase measured separately, never averaged (#29).
Coverage: from "accepted as a gap" to a number
Five repositories listed coverage in their gate's accepted-gaps line. Their gates printed GREEN while nothing measured coverage at all.
| Codebases with a measured number, before | ░░░░░░░░░░░░░░░░░░░░ | 4 |
| After | ████████████████████ | 20 |
| Of those, at or above the 80% threshold | █████████░░░░░░░░░░░ | 9 |
The rule-#16 setup document (fail-closed)
The gate's document step only fired when a stack sat at the repository root, so it looked at three repositories and failed all three. Fixing the detection made it look at nine.
| Repositories the step examined, before | ██████░░░░░░░░░░░░░░ | 3 of 9 |
| Passing, before | ░░░░░░░░░░░░░░░░░░░░ | 0 of 3 |
| Examined, after | ████████████████████ | 9 of 9 |
| Passing, after | ████████████████████ | 9 of 9 |
One web codebase, taken over the line
| Before — 141 tests | ██████████████░░░░░░ | 57.2% |
| After — 221 tests | ████████████████████ | 81.4% |
Eighty tests, and not one of them written for the percentage: a cancelled prompt must not be read as a quota of zero, a failed delete must not navigate away as if it had succeeded, a missing invitation token must render no form at all. The repository's other codebase was already at 87.7%, so both are now above the threshold and coverage stopped being its promotion blocker.
Security findings, cleared
| High/critical dependency advisories | ████████████████████ 26 → ░░░░░░░░░░░░░░░░░░░░ 0 | four repositories |
| Secret-scan findings, read | ████████████████████ 17 | across two repositories |
| Of those, actual credentials | ░░░░░░░░░░░░░░░░░░░░ 0 | expired tokens, a model id, an interface name, a placeholder that says "enter a secret here" |
The scans had been failing for a while and nobody had read them. Reading them mattered more than fixing them: a wrong escalation ("rotate the payment key") was corrected by looking at the value the rule actually matched.
Four bugs in the gate itself
The gate is the thing that is supposed to catch everything else, so its own defects are the most expensive kind. All four were found by reading failures instead of trusting them, all four are covered by a test that fails without its fix, and all four were propagated to nine repositories.
| Bug | What it did |
|---|---|
.slnx and nested .csproj invisible | Skipped an entire .NET backend — 673 tests, a red build, two high-severity advisories — and printed GATE GREEN |
coverage waivable via accepted gaps | Turned rule #29's "no exceptions" into a suggestion in five repositories |
jest handed vitest's --run | Reported a passing suite of 10 tests as failing |
| Node detected only at the root | Skipped the setup-document check in any repository whose JS lives one level down |
⚠️ The first one is the one to remember. A gate that fails is annoying; a gate that reports success for work it never looked at is worse than no gate, because it ends an argument that should have continued.
What turned out not to be broken
Three failures that looked like product bugs were not: a frontend build and its 812 tests failing on an incomplete node_modules; a mobile tier reporting 0% coverage because all 57 suites died on a missing module and no test ran at all; an API-contract check failing without the environment variable its own documentation prescribes. Each coverage script now checks its install first and names which problem it is, because an environment problem that reads as a coverage problem sends the next person hunting in the wrong file.
flowchart TD
subgraph before["Before"]
B1["5 repos: coverage accepted as a gap<br/>nothing measured"]
B2["3 of 9 repos: setup document checked"]
B3["4 latent gate bugs<br/>one skipped a whole tier"]
B4["26 high/critical advisories"]
B5["17 secret findings, unread"]
end
subgraph after["After"]
A1["20 codebases measured<br/>9 over the threshold"]
A2["9 of 9 repos: checked and passing"]
A3["4 fixed + 12 tests<br/>propagated to 9 repos"]
A4["0 high/critical"]
A5["17 read · 0 credentials<br/>triaged by value, with evidence"]
end
B1 --> A1
B2 --> A2
B3 --> A3
B4 --> A4
B5 --> A5
Each one ends with an Honest limits section: which numbers were measured, which are estimates, and what is still open.
Cost-table calibration — how much of the cost model is measured rather than estimated. This is the honest state, not a target:
| Role class | Status |
|---|---|
qa (opus, auditing) | ████████████████████ measured — 3 records, ~$4.10/turn |
| Turn average across 33 sessions | ████████████████████ measured — ~$6.9/turn |
architect / developer (writing roles) | ░░░░░░░░░░░░░░░░░░░░ estimated only |
sonnet roles (analyst, test-writer, devops, …) | ░░░░░░░░░░░░░░░░░░░░ estimated only |
doc-writer (haiku) | ░░░░░░░░░░░░░░░░░░░░ estimated only |
| Team modes (X / Y / Z) | ░░░░░░░░░░░░░░░░░░░░ never measured |
Thresholds have deliberately not been adjusted on this data: all three calibration records came from the same kind of work, and a real end-to-end feature turn has not been measured yet.
flowchart LR
subgraph P1["Phase 1 · Foundations"]
A1[Standards extracted<br/>from one project]
A2[Agent bans and<br/>autonomy limits]
end
subgraph P2["Phase 2 · Measurement"]
B1[Cost measured<br/>across 33 sessions]
B2[Operating modes<br/>A-E introduced]
B3[Audit protocol:<br/>evidence blocks]
end
subgraph P3["Phase 3 · Gates"]
C1[Coverage honesty<br/>+ 80% gate]
C2[E2E run cycle<br/>and classification]
C3[E2E moved to<br/>the pre-prod gate]
end
A1 --> A2 --> B1 --> B2 --> B3 --> C1 --> C2 --> C3
dev is fast; heavy gates run on promotion — the quality gate belongs where code leaves for the outside world (#25)LIKE / ToLower.Contains is banned; one central normalizer (#13)Take the smallest amount that is useful to you. Each level works on its own, and nothing later is required to make an earlier one pay off.
To work on this repository itself (hooks, tests, the gate), or to see
hooks/register.tsx 198 lines1import type { EngineInterface, Hook, Register } from 'claude-code'
2
3import type { AgentRow, Promotion, Settings, Stage } from '../types'
4import {
5 agentsBody, agentsSummary, FOLDED_AT_START, promotionBody, promotionSummary, section, settingsBody, settingsSummary,
6} from './boxes'
7import type { SectionId } from './boxes'
8import {
9 countOf, DISCOVER_PY, IS_FILE_PY, overrideNote, parseDiscovery, plannedStages, refSet, scriptDir, waivedLines, waivedRange,
10} from './choose'
11
12/** One pane, three bordered boxes in it (Settings, Agents, Promotion), each folded by its header. */
13const BOXES = 'adyusuf-boxes'
14const TITLE = 'adyusuf'
15
16// The pane's values live in `$.state`, declared in ../types; a value never written reads as undefined, so each read names its initial one.
17const settings = { plugin: 'adyusuf', key: 'settings' } as const
18const settingsError = { plugin: 'adyusuf', key: 'settingsError' } as const
19const settingsProblems = { plugin: 'adyusuf', key: 'settingsProblems' } as const
20const promotion = { plugin: 'adyusuf', key: 'promotion' } as const
21const folded = { plugin: 'adyusuf', key: 'folded' } as const
22
23const PLUGIN = 'adyusuf'
24const MARKETPLACE = 'adyusuf-marketplace'
25const PLUGIN_ID = `${PLUGIN}@${MARKETPLACE}`
26
27type Run = (argv: readonly string[]) => Promise<{ exitCode: number; stdout: string; stderr: string }>
28
29/** The configuration script of the INSTALLED adyusuf plugin ('' when it has none yet); the choice is choose.ts's scriptDir. */
30async function scriptPath(run: Run): Promise<string> {
31 const found = await run(['python3', '-c', DISCOVER_PY, MARKETPLACE, PLUGIN])
32 const { record, listing } = parseDiscovery(found.stdout)
33 const dir = scriptDir(record, PLUGIN_ID, listing)
34 if (!dir) return ''
35 const script = `${dir}/scripts/adyusuf_config.py`
36 const has = await run(['python3', '-c', IS_FILE_PY, script])
37 return has.exitCode === 0 ? script : ''
38}
39
40/** This session's id ('' when the engine will not say): the boxes change THIS session's layer only, never the saved defaults. */
41async function sessionId($: EngineInterface): Promise<string> {
42 try {
43 return (await $.session.id()) || ''
44 } catch {
45 return ''
46 }
47}
48
49async function refreshSettings($: EngineInterface) {
50 const run: Run = argv => $.process.run(argv)
51 const script = await scriptPath(run)
52 if (!script) {
53 await $.state.set(settingsError, 'The adyusuf plugin with /adyusuf:config is not installed yet: /plugin update adyusuf')
54 return
55 }
56 const id = await sessionId($)
57 const scope = id ? ['--session', id] : []
58 const shown = await run(['python3', script, 'show', '--json', ...scope])
59 const agents = await run(['python3', script, 'agents', '--json', '--all', ...scope])
60 const schema = await run(['python3', script, 'schema'])
61 if (shown.exitCode !== 0 || agents.exitCode !== 0 || schema.exitCode !== 0) {
62 await $.state.set(settingsError, (shown.stderr || agents.stderr || schema.stderr || 'adyusuf_config.py failed').slice(0, 300))
63 return
64 }
65 try {
66 const data = JSON.parse(shown.stdout) as { values: Settings['values']; sources: Settings['sources']; problems?: string[] }
67 const next: Settings = {
68 schema: JSON.parse(schema.stdout),
69 values: data.values,
70 sources: data.sources,
71 agents: JSON.parse(agents.stdout) as AgentRow[],
72 }
73 await $.state.set(settings, next)
74 await $.state.set(settingsError, '')
75 await $.state.set(settingsProblems, data.problems ?? [])
76 } catch (error) {
77 await $.state.set(settingsError, `Unreadable answer from adyusuf_config.py: ${String(error).slice(0, 200)}`)
78 }
79}
80
81async function refreshPromotion($: EngineInterface) {
82 const git = (args: string[]) => $.process.run(['git', ...args])
83 const refs = await git(['for-each-ref', '--format=%(refname:short)', 'refs/heads', 'refs/remotes/origin'])
84 if (refs.exitCode !== 0) {
85 await $.state.set(promotion, ({ stages: [], error: 'Not inside a git repository' }) as Promotion)
86 return
87 }
88 const stages: Stage[] = []
89 for (const stage of plannedStages(refSet(refs.stdout))) {
90 const tip = await git(['rev-parse', '--short', `origin/${stage.branch}`])
91 const ahead = stage.local ? await git(['rev-list', '--count', `origin/${stage.branch}..${stage.branch}`]) : null
92 const range = waivedRange(stage)
93 const log = range ? await git(['log', '--format=@@%h%n%B', range]) : null
94 stages.push({
95 branch: stage.branch,
96 origin: tip.stdout.trim(),
97 unpushed: ahead ? countOf(ahead.stdout) : 0,
98 waived: log && log.exitCode === 0 ? waivedLines(log.stdout) : [],
99 })
100 }
101 await $.state.set(promotion, ({ stages }) as Promotion)
102}
103
104
105async function setSetting($: EngineInterface, key: string, value: string) {
106 const run: Run = argv => $.process.run(argv)
107 const script = await scriptPath(run)
108 if (!script) {
109 await $.ui.toast('The adyusuf plugin is not installed yet: /plugin install adyusuf@adyusuf-marketplace')
110 return
111 }
112 const id = await sessionId($)
113 if (!id) {
114 await $.ui.toast('Could not tell which session this is, so nothing was changed (the saved defaults stay as they are)')
115 return
116 }
117 const done = await run(['python3', script, 'set', '--session', id, key, value])
118 if (done.exitCode !== 0) await $.ui.toast(done.stderr.trim().slice(0, 200) || 'The change was refused')
119 await refreshSettings($)
120 const { value: now = null } = await $.state.get(settings)
121 if (done.exitCode === 0 && now) {
122 const note = overrideNote(key, value, now.values[key], now.sources[key])
123 if (note) await $.ui.toast(note)
124 }
125}
126
127/** A failing read must never stop the session from starting; the Settings box says what failed instead of waiting forever. */
128async function safely($: EngineInterface, work: Promise<void>) {
129 try {
130 await work
131 } catch (error) {
132 await $.state.set(settingsError, `Could not read: ${String(error).slice(0, 200)}`)
133 }
134}
135
136/** Folds or unfolds one box of the pane; called from a press, never while drawing. */
137async function flip($: EngineInterface, id: SectionId) {
138 const { value: now = FOLDED_AT_START } = await $.state.get(folded)
139 await $.state.set(folded, { ...now, [id]: !now[id] })
140}
141
142// Clicks run one after another: two settings changed quickly must not race each other's read-modify-write.
143let queue: Promise<void> = Promise.resolve()
144function click($: EngineInterface, key: string, value: string) {
145 queue = queue.then(() => setSetting($, key, value)).catch(() => undefined)
146}
147
148const onStart: Hook<'session.start'> = async ($, e, next) => {
149 await $.command.register({ name: 'adyusuf:panels', description: 'Open the adyusuf settings, agents and promotion boxes' })
150 void $.ui.open({ id: BOXES, title: TITLE })
151 await safely($, refreshSettings($))
152 await safely($, refreshPromotion($))
153 return next(e)
154}
155
156const onPanels: Hook<'command.run'> = async $ => {
157 await $.ui.open({ id: BOXES, title: TITLE })
158 await safely($, refreshSettings($))
159 await safely($, refreshPromotion($))
160 return { text: 'adyusuf boxes opened.' }
161}
162
163// The boxes follow the work: a turn may have merged, pushed or changed a setting through /adyusuf:config.
164const onTurnComplete: Hook<'turn.complete'> = async ($, e, next) => {
165 await safely($, refreshSettings($))
166 await safely($, refreshPromotion($))
167 return next(e)
168}
169
170const onRender: Hook<'ui.render'> = async ($, e) => {
171 const ui = { ...$.ui.resolve(e), h }
172 const { Box } = ui
173 const { value: data = null } = await $.state.get(settings)
174 const { value: problem = '' } = await $.state.get(settingsError)
175 const { value: problems = [] } = await $.state.get(settingsProblems)
176 const { value: stages = null } = await $.state.get(promotion)
177 const { value: fold = FOLDED_AT_START } = await $.state.get(folded)
178 const press = (key: string, value: string) => click($, key, value)
179 const toggle = (id: SectionId) => () => void flip($, id)
180 return (
181 <Box flexDirection="column">
182 {section(ui, 'settings', 'Settings', settingsSummary(data), fold.settings === true, toggle('settings'),
183 settingsBody(ui, data, problem, problems, press))}
184 {section(ui, 'agents', 'Agents', agentsSummary(data), fold.agents === true, toggle('agents'),
185 agentsBody(ui, data, press))}
186 {section(ui, 'promotion', 'Promotion', promotionSummary(stages), fold.promotion === true, toggle('promotion'),
187 promotionBody(ui, stages))}
188 </Box>
189 )
190}
191
192export const register: Register = on => {
193 on('session.start', onStart)
194 on('command.run', { command: 'adyusuf:panels' }, onPanels)
195 on('turn.complete', onTurnComplete)
196 on('ui.render', { component: 'Pane', requestId: BOXES }, onRender)
197}
198hooks/boxes.ts 144 lines1// What the boxes of the adyusuf pane show, kept apart from the engine so it can be tested without one: no import from
2// 'claude-code', no JSX. The element factory `h` and the elements themselves (Box, Text, Button) are handed in as
3// `ui`: each surface has its own elements, and a test hands in a recording factory instead of the engine's. A choice among
4// several values is a radio list (one Button per option, `(•)` on the chosen one), not a dropdown (user decision 24/10/2026).
5import { enabledFor, settingGroups } from './choose'
6
7export type Factory = (type: unknown, props: Record<string, unknown> | null, ...children: unknown[]) => unknown
8export type Ui = { h: Factory; Box: unknown; Text: unknown; Button: unknown }
9export type Press = (key: string, value: string) => void
10
11type Spec = { key: string; type: 'bool' | 'enum'; label: string; group: string; options?: string[] }
12type AgentRow = { role: string; defined: string; runs_on: string; active: boolean; in_mode: boolean; enabled: string; model: string }
13type SettingsData = { schema: Spec[]; values: Record<string, boolean | string>; sources: Record<string, string>; agents: AgentRow[] }
14type Stage = { branch: string; origin: string; unpushed: number; waived: string[] }
15type PromotionData = { stages: Stage[]; error?: string }
16
17export const SECTIONS = ['settings', 'agents', 'promotion'] as const
18export type SectionId = (typeof SECTIONS)[number]
19
20/** Sections that start folded, so the pane opens as a short card and not as one long list. */
21export const FOLDED_AT_START: Record<string, boolean> = { agents: true, promotion: true }
22
23const AGENT_MODELS = ['default', 'opus', 'sonnet', 'haiku', 'fable']
24
25type Option = { value: string; label: string }
26
27/** A radio list: the label, then one pressable line per option, the chosen one marked `(•)`. `wrap` lays the options in a row. */
28export function radio(ui: Ui, key: string, label: string, options: Option[], value: string, press: Press, wrap = false) {
29 const { h } = ui
30 return h(
31 ui.Box,
32 { flexDirection: 'column' },
33 h(ui.Text, {}, label),
34 h(ui.Box, wrap ? { flexDirection: 'row', flexWrap: 'wrap', columnGap: 2, paddingLeft: 2 } : { flexDirection: 'column', paddingLeft: 2 },
35 options.map(option =>
36 h(ui.Button, { key: `${key}=${option.value}`, plain: true, onPress: () => press(key, option.value) },
37 `${option.value === value ? '(•)' : '( )'} ${option.label}`))),
38 )
39}
40
41/** One bordered box with a header that folds it; the summary stays visible while it is folded. */
42export function section(ui: Ui, id: SectionId, title: string, summary: string, folded: boolean, toggle: () => void, body: unknown) {
43 const { h } = ui
44 return h(
45 ui.Box,
46 { flexDirection: 'column', borderStyle: 'round', paddingX: 1, marginBottom: 1 },
47 h(ui.Button, { key: `fold-${id}`, plain: true, onPress: toggle },
48 folded ? '▸ ' : '▾ ',
49 h(ui.Text, { bold: true }, title),
50 summary ? h(ui.Text, { dimColor: true }, ` ${summary}`) : ''),
51 folded ? null : body,
52 )
53}
54
55export function settingsSummary(data: SettingsData | null): string {
56 if (!data) return ''
57 const changed = Object.values(data.sources).filter(source => source === 'session').length
58 return changed > 0 ? `${changed} changed in this session` : 'this session only'
59}
60
61export function agentsSummary(data: SettingsData | null): string {
62 if (!data) return ''
63 const mode = String(data.values.mode)
64 return mode === 'A' ? 'mode A: no agents' : `mode ${mode}: ${data.agents.filter(agent => agent.active).length} of ${data.agents.length} active`
65}
66
67export function promotionSummary(data: PromotionData | null): string {
68 if (!data || data.error) return ''
69 return data.stages.map(stage => `${stage.branch} ${stage.origin}`).join(' · ')
70}
71
72export function settingsBody(ui: Ui, data: SettingsData | null, problem: string, problems: string[], press: Press) {
73 const { h } = ui
74 if (!data) return h(ui.Text, { dimColor: true }, problem || 'Reading the configuration…')
75 const { groups, shown } = settingGroups(data.schema)
76 return h(
77 ui.Box,
78 { flexDirection: 'column' },
79 problem !== '' ? h(ui.Text, { color: 'red' }, problem) : null,
80 problems.map(line => h(ui.Text, { color: 'yellow' }, `! ${line}`)),
81 groups.map(group =>
82 h(ui.Box, { flexDirection: 'column', marginBottom: 1 },
83 h(ui.Text, { bold: true, dimColor: true }, group),
84 shown.filter(spec => spec.group === group).map(spec => {
85 const value = data.values[spec.key]
86 const from = data.sources[spec.key]
87 const tag = from && from !== 'default' ? ` (${from})` : ''
88 if (spec.type === 'bool') {
89 return h(ui.Button, { key: spec.key, onPress: () => void press(spec.key, value === true ? 'no' : 'yes') },
90 value === true ? '[x] ' : '[ ] ', spec.label, tag)
91 }
92 return radio(ui, spec.key, `${spec.label}${tag}:`, (spec.options ?? []).map(option => ({ value: option, label: option })), String(value), press)
93 })),
94 ),
95 h(ui.Text, { dimColor: true }, 'Changes apply to this session only; a new session starts from your saved defaults (/adyusuf:config saves them).'),
96 )
97}
98
99export function agentsBody(ui: Ui, data: SettingsData | null, press: Press) {
100 const { h } = ui
101 if (!data) return h(ui.Text, { dimColor: true }, 'Reading the configuration…')
102 const mode = String(data.values.mode)
103 const model = String(data.values.mode_model)
104 return h(
105 ui.Box,
106 { flexDirection: 'column' },
107 h(ui.Text, { dimColor: true }, `Opus agents run on ${model === 'keep' ? 'their defined model' : model} unless one is set below`),
108 h(ui.Box, { flexDirection: 'column', marginTop: 1 },
109 data.agents.map(agent =>
110 h(ui.Box, { flexDirection: 'column', marginBottom: 1 },
111 h(ui.Button, {
112 key: `on-${agent.role}`,
113 onPress: () => {
114 if (mode === 'A') return
115 press(`agents.${agent.role}.enabled`, enabledFor(!agent.active, agent.in_mode))
116 },
117 }, `${agent.active ? '[x] ' : '[ ] '}${agent.role}${!agent.in_mode && agent.active ? ' (added)' : ''}${agent.in_mode && !agent.active && mode !== 'A' ? ' (off)' : ''}`),
118 agent.active
119 ? radio(ui, `agents.${agent.role}.model`, ` model (defined ${agent.defined}):`,
120 AGENT_MODELS.map(option => ({ value: option, label: option === 'default' ? `default (${agent.runs_on})` : option })),
121 agent.model, press, true)
122 : null)),
123 ),
124 h(ui.Text, { dimColor: true }, "A box ticked beyond the mode's set adds that agent; cleared, it removes one."),
125 )
126}
127
128export function promotionBody(ui: Ui, data: PromotionData | null) {
129 const { h } = ui
130 if (!data) return h(ui.Text, { dimColor: true }, 'Reading the branches…')
131 if (data.error) return h(ui.Text, { dimColor: true }, data.error)
132 return h(
133 ui.Box,
134 { flexDirection: 'column' },
135 data.stages.map(stage =>
136 h(ui.Box, { flexDirection: 'column', marginBottom: 1 },
137 h(ui.Text, { bold: true }, `${stage.branch.padEnd(5)} origin ${stage.origin}${stage.unpushed > 0 ? ` ${stage.unpushed} not pushed` : ' pushed'}`),
138 stage.waived.length === 0
139 ? h(ui.Text, { dimColor: true }, 'no waived gate on the way in')
140 : stage.waived.map(line => h(ui.Text, { color: 'yellow' }, `waived ${line}`)))),
141 data.stages.length === 0 ? h(ui.Text, { dimColor: true }, 'No dev, test or prod branch here.') : null,
142 )
143}
144hooks/choose.ts 143 lines1// Pure decisions of the boxes, kept apart from the drawing so they can be tested without an engine.
2
3/**
4 * The folder the plugin manager says `id` is installed in, from the text of installed_plugins.json ('' when it
5 * names none). Not the newest folder in the cache: a replaced version is marked and then becomes the newest.
6 */
7export function installPathOf(text: string, id: string): string {
8 try {
9 const entry = (JSON.parse(text) as { plugins?: Record<string, unknown> }).plugins?.[id]
10 const entries = Array.isArray(entry) ? entry : entry ? [entry] : []
11 for (const one of entries) {
12 const path = (one as { installPath?: unknown }).installPath
13 if (typeof path === 'string' && path !== '') return path.replace(/\/$/, '')
14 }
15 } catch {
16 // an unreadable record names nothing
17 }
18 return ''
19}
20
21/** Whether the value the script now reports is the one that was asked for (yes/no settings are booleans). */
22export function isWanted(value: unknown, wanted: string): boolean {
23 if (typeof value === 'boolean') return value === (wanted === 'yes')
24 return String(value) === wanted
25}
26
27/** What to write into `agents.<role>.enabled` so that the agent ends up `want` (on/off), given the mode's own set. */
28export function enabledFor(want: boolean, inMode: boolean): 'auto' | 'on' | 'off' {
29 if (want === inMode) return 'auto'
30 return want ? 'on' : 'off'
31}
32
33/** `<short sha> <gates> (neden: ...)` for every `Gate-Waived:` line of a `git log --format=@@%h%n%B` output. */
34export function waivedLines(log: string): string[] {
35 const out: string[] = []
36 let sha = ''
37 for (const line of log.split('\n')) {
38 if (line.startsWith('@@')) sha = line.slice(2)
39 else if (line.startsWith('Gate-Waived:')) out.push(`${sha} ${line.slice('Gate-Waived:'.length).trim()}`)
40 }
41 return out
42}
43
44/** The message for a click that changed nothing because a higher layer wins ('' when the click took effect). */
45export function overrideNote(key: string, wanted: string, value: unknown, source: string | undefined): string {
46 if (isWanted(value, wanted)) return ''
47 const from = source && source !== 'default' ? `the ${source} layer` : 'another layer'
48 return `${key} stays ${String(value)}: ${from} sets it, and it wins over the user's file`
49}
50
51/**
52 * The newest installed copy of the plugin that is not marked as replaced, from the lines `live <dir>` / `orphaned <dir>`
53 * that parseDiscovery builds, newest first ('' when there is none). Only used when the plugin manager's
54 * record names no folder.
55 */
56export function newestLiveDir(output: string): string {
57 for (const line of output.split('\n')) {
58 const [state, ...rest] = line.trim().split(/\s+/)
59 if (state === 'live' && rest.length > 0) return rest.join(' ').replace(/\/$/, '')
60 }
61 return ''
62}
63
64/**
65 * Python run as `python3 -c <this> <marketplace> <plugin>`: prints the plugin manager's record and the plugin's cache
66 * folders (newest first, each `live` or `orphaned`) as one JSON object. The panel needs no shell for this: the engine's
67 * process runner could not find `sh` on Windows (24/10/2026), and `python3` is needed for the configuration script anyway.
68 */
69export const DISCOVER_PY = [
70 'import json, os, sys',
71 "base = os.path.join(os.path.expanduser('~'), '.claude', 'plugins')",
72 'try:',
73 " record = open(os.path.join(base, 'installed_plugins.json'), encoding='utf-8').read()",
74 'except OSError:',
75 " record = ''",
76 "root = os.path.join(base, 'cache', sys.argv[1], sys.argv[2])",
77 'dirs = [os.path.join(root, d) for d in os.listdir(root)] if os.path.isdir(root) else []',
78 'dirs = sorted((d for d in dirs if os.path.isdir(d)), key=os.path.getmtime, reverse=True)',
79 "rows = [['orphaned' if os.path.exists(os.path.join(d, '.orphaned_at')) else 'live', d.replace(os.sep, '/')] for d in dirs]",
80 "sys.stdout.write(json.dumps({'record': record, 'cache': rows}))",
81].join('\n')
82
83/** Python run as `python3 -c <this> <path>`: exit 0 when `path` is a file, 1 when it is not. */
84export const IS_FILE_PY = 'import os, sys; sys.exit(0 if os.path.isfile(sys.argv[1]) else 1)'
85
86/**
87 * What DISCOVER_PY printed, as the record text and the `live <dir>` / `orphaned <dir>` lines newestLiveDir reads
88 * (both empty when the output is not what the script prints).
89 */
90export function parseDiscovery(output: string): { record: string; listing: string } {
91 try {
92 const data = JSON.parse(output) as { record?: unknown; cache?: unknown }
93 const rows = Array.isArray(data.cache) ? data.cache : []
94 const lines = rows
95 .filter((row): row is [string, string] => Array.isArray(row) && typeof row[0] === 'string' && typeof row[1] === 'string')
96 .map(([state, dir]) => `${state} ${dir}`)
97 return { record: typeof data.record === 'string' ? data.record : '', listing: lines.join('\n') }
98 } catch {
99 return { record: '', listing: '' }
100 }
101}
102
103/** The folder that holds the configuration script: the recorded one, else the newest live one in the cache. */
104export function scriptDir(recordText: string, id: string, cacheListing: string): string {
105 return installPathOf(recordText, id) || newestLiveDir(cacheListing)
106}
107
108/** The sections of the Settings box: the groups in schema order, and the settings that are shown (agents have their own box). */
109export function settingGroups<T extends { key: string; group: string }>(schema: T[]): { groups: string[]; shown: T[] } {
110 const shown = schema.filter(spec => !spec.key.startsWith('agents.'))
111 const groups: string[] = []
112 for (const spec of shown) if (!groups.includes(spec.group)) groups.push(spec.group)
113 return { groups, shown }
114}
115
116export type PlannedStage = { branch: string; previous: string; local: boolean }
117
118/** The promotion stages to read, in order: a stage exists when `origin/<branch>` does; `previous` is the stage before it. */
119export function plannedStages(have: Set<string>, order: readonly string[] = ['dev', 'test', 'prod']): PlannedStage[] {
120 const out: PlannedStage[] = []
121 order.forEach((branch, index) => {
122 if (!have.has(`origin/${branch}`)) return
123 const previous = index > 0 && have.has(`origin/${order[index - 1]}`) ? order[index - 1] : ''
124 out.push({ branch, previous, local: have.has(branch) })
125 })
126 return out
127}
128
129/** The git range of commits that reached `branch`'s origin from its previous stage's origin ('' for the first stage). */
130export function waivedRange(stage: PlannedStage): string {
131 return stage.previous ? `origin/${stage.branch}..origin/${stage.previous}` : ''
132}
133
134/** The number in git's `--count` output (0 when it is not one). */
135export function countOf(text: string): number {
136 return Number(text.trim()) || 0
137}
138
139/** The refs git printed, one per line, as a set. */
140export function refSet(text: string): Set<string> {
141 return new Set(text.split('\n').map(line => line.trim()).filter(Boolean))
142}
143types/index.d.ts 37 lines1export type Spec = { key: string; type: 'bool' | 'enum'; label: string; group: string; options?: string[] }
2export type AgentRow = {
3 role: string
4 defined: string
5 runs_on: string
6 active: boolean
7 in_mode: boolean
8 /** agents.<role>.enabled: auto, on or off */
9 enabled: string
10 /** agents.<role>.model: default or a model name */
11 model: string
12}
13export type Settings = {
14 schema: Spec[]
15 values: Record<string, boolean | string>
16 sources: Record<string, string>
17 agents: AgentRow[]
18}
19export type Stage = { branch: string; origin: string; unpushed: number; waived: string[] }
20export type Promotion = { stages: Stage[]; error?: string }
21
22declare module 'claude-code' {
23 interface PluginState {
24 'adyusuf': {
25 /** The effective configuration, read through the plugin's own script; null until the first read. */
26 settings: Settings | null
27 /** Why the settings could not be read (script missing, bad JSON); empty when they could. */
28 settingsError: string
29 /** Problems the script reports about the configuration files (an unreadable or invalid layer); empty when none. */
30 settingsProblems: string[]
31 promotion: Promotion | null
32 /** Which of the pane's boxes are folded, by id (settings, agents, promotion); the pane starts with agents and promotion folded. */
33 folded: Record<string, boolean>
34 }
35 }
36}
37