docs+feat: the line between a hook and the CLI, and the last plugin script crossing it - #778
Merged
Merged
Conversation
blafourcade
enabled auto-merge (squash)
September 6, 2026 02:41
…hat name files The telemetry pivot moved 4,355 lines of skill-owned scripts into the CLI on an argument nobody wrote down. What is left outside is one skill script and the bundled hooks, and the reason the hooks are not next is a measurement, not taste: on one machine, 12 runs each, `hooks/journal.cjs` starts in a median 27 ms and `cli/dist/cli.js` in 180 ms, and `PostToolUse` fires on every tool call a session makes. A thousand tool calls is 153 seconds a person waits through. So the line is not "plugin or CLI". It is what the code answers to: observing runs on a tool event thousands of times a session and must not be felt, so it is plain Node with no install behind it; answering runs once because a person or a skill asked, so latency is nothing and it belongs in `aidd`. The section states both consequences already paid for — a capability that answers moves even when a plugin is what asks for it, and a skill needing an absent CLI must say so out loud — and names the guard that already pins the second. It also states the cost rather than arguing it away: a plugin that promised "no npm install, no CLI, no account" now needs `node` to measure and `aidd` to answer. The doc named `hooks/update_memory.cjs`. That file has always been `.js`, in the tree and in `hooks.json`, so a reader following the architecture doc to the context plugin's session hook found nothing. `comments-name-files-that-exist.test.js` existed to catch exactly that and could not see it: it scanned `cli/src`, `cli/tests`, `plugins` and `scripts` for `.ts`, `.cjs` and `.js`, and no markdown at all. It now scans `docs/` markdown as well — extended rather than paired with a second test, since a durable doc naming a file makes the same promise a comment does. Markdown anywhere else stays out, measured rather than assumed: scanning every `.md` in the tree produced 17 findings and not one was a fault — a skill's own asset and a `cli/tests/fixtures` template name illustrative paths on purpose. Of the three findings in `docs/`, one is this bug and two name `INSTALL.md`, a seam artefact one plugin writes into a reader's project for another to read back, now listed in `NOT_A_REPOSITORY_FILE` for what it is. Red before the fix on the one line it names, green after. 371 repository script tests pass, the same count as before: the guard was extended, not duplicated. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp AIDD-Session-Id: 2c21d903-3a7e-47ac-83f8-d8b7ae3aa579
`plugins/aidd-context/skills/11-explore/scripts/list-rules.mjs` was the only script left inside a skill, 198 lines, and its own header said it was "synced from plugins/aidd-dev/scripts/list-rules.mjs. Keep in sync when the source changes" — a directory that no longer exists. A hand-maintained copy whose source is gone, and whose header is false. It had drifted where it mattered. It carried its own table of four tool directories and their extensions and stated "Codex CLI: rules not supported, skipped". `plugin-content-translator.ts` routes a plugin's `rules/` into every tool whose capability accepts them, and all five do, so a Codex project asking what rules it had was answered "none" — silently, and wrongly. `05-rule-generate`'s own `tool-paths.md` said the same of Codex and OpenCode; both rows are corrected here. `RulesCapability.installedLocation()` is what removes the table rather than moving it. It asks `buildInstallPath` with a sentinel name and reads its answer back, so the directory and the extension come from the installer itself: claude .claude/rules/ .md codex .codex/rules/ .md copilot .github/instructions/ .instructions.md cursor .cursor/rules/ .mdc opencode .opencode/rules/ .md Written where the knowledge already is: three of those five are a path template, one goes through `toMdc`, and Copilot's is a delegated handler. A caller splitting a path string apart would have been a second copy, free to disagree the day a tool moves. `registry-conformance` pins all five as a table, so a sixth tool, or a moved directory, is read by whoever changes it. The frontmatter parser goes too: `domain/formats/markdown.ts` already had `parseFrontmatter`, and the script carried a fourth hand-rolled one. `hasRules` was private to `plugin-content-translator.ts` and is now exported from `contracts.ts` beside the type it guards, generic so that translator keeps the narrowing it had. `--json` prints the array the script printed, field for field, so the skill reading it did not change what it reads. The default output is for a person, and a project with no rule says so rather than printing nothing: an empty answer and a command that never ran look identical on a terminal, and only one is a fact. The skill now runs `aidd ai rules --json` and, finding no command, stops and says the CLI is required — never an empty inventory, which is the rule `docs/ARCHITECTURE.md` states for a capability that answers. Bundle 596.6 -> 599.0 KB, budget 598 -> 601, measured and recorded beside the three raises before it. Mutations run, three killed: `installedLocation` answering the source path instead of the installed one (4 red), the scan dropping its extension filter (1), and a name trimmed at the last dot rather than by the whole extension (1). 3,488 CLI tests, 371 repository script tests, typecheck, biome `ci`, knip, jscpd, layering, 0 broken links in 798 files. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp AIDD-Session-Id: 2c21d903-3a7e-47ac-83f8-d8b7ae3aa579
Adding the use case to `deps.ts` and the guard to `plugin-content-translator.ts` left both import blocks out of the order `assist/source/organizeImports` fixes to, which `pnpm lint` fails on and `biome ci` alone does not report. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp AIDD-Session-Id: 2c21d903-3a7e-47ac-83f8-d8b7ae3aa579
Extending `comments-name-files-that-exist.test.js` to `docs/` markdown appended a second `find` to the same command string, separated by `;`. `execSync` runs through `cmd.exe` on Windows, where `;` separates nothing: the second `find` and its arguments were handed to the first as arguments, and the walk came back with a list that named files the repository does not hold. Green on macOS, red on the Windows job — which is exactly what that job is there for, and the reason the split is now two `execSync` calls with no shell operator between them rather than a quoting fix that would have worked on one platform. Line endings are split on either form while the calls are being touched: the same output arrives with `\r\n` on Windows, and a trailing carriage return would have made every path miss. 371 repository script tests pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp AIDD-Session-Id: 2c21d903-3a7e-47ac-83f8-d8b7ae3aa579
blafourcade
force-pushed
the
docs/the-line-between-a-hook-and-the-cli
branch
from
September 6, 2026 02:49
73d878f to
344c5cf
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
The telemetry pivot moved 4,355 lines of skill-owned scripts into the CLI. What is left outside is one skill script and the bundled hooks — and the argument that decides which is which was never written down. Nothing stops the next change from moving a hook into the CLI because "everything is in the CLI now".
The measurement that decides it
One machine, 12 runs each, median:
plugins/aidd-telemetry/hooks/journal.cjscli/dist/cli.js --version6.7×, on an event (
PostToolUse) that fires on every tool call a session makes. A thousand tool calls is 153 seconds a person waits through. The hooks are not a leftover; they are where they are for a reason with a number attached.The line
Not "plugin or CLI" — what the code answers to:
hooks/, no install, no dependencyaiddCLITwo consequences, both already paid for, both stated in the new section: a capability that answers belongs in the CLI even when a plugin is what asks for it; and a skill that needs an absent CLI must say so out loud, which
scripts/__tests__/telemetry-cli-required.test.jsalready pins word for word across every such skill.The cost is stated rather than argued away: a plugin that promised "no npm install, no CLI, no account" now needs
nodeto measure andaiddto answer.The bug the guard could not see
docs/ARCHITECTURE.mdnamedhooks/update_memory.cjs. That file has always been.js— in the tree and in the plugin's ownhooks.json— so a reader following the architecture doc to the context plugin's session hook found nothing.comments-name-files-that-exist.test.jsexists to catch exactly this. It scannedcli/src,cli/tests,pluginsandscriptsfor.ts,.cjsand.js, and no markdown at all. It now scansdocs/markdown too — the existing guard extended, not a second test beside it, because a durable doc naming a file makes the same promise a comment does.Markdown anywhere else stays out, and that is measured rather than assumed:
.mdin the treedocs/**/*.mdThe 17 are a skill's own assets and
cli/tests/fixturestemplates naming illustrative paths on purpose. Of the 3, one is this bug; the other two nameINSTALL.md, a seam artefact one plugin writes into a reader's own project for another to read back, now listed inNOT_A_REPOSITORY_FILEfor what it is.Verification
Red before the fix, on the one line it names:
Green after. 371 repository script tests pass — the same count as before, because the guard was extended rather than duplicated.
check-markdown-links.js: 0 broken in 798 files.🤖 Generated with Claude Code
https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
Second commit:
aidd ai rules, and the drift it uncoveredplugins/aidd-context/skills/11-explore/scripts/list-rules.mjswas the only script left inside a skill — 198 lines — and its own header read:plugins/aidd-dev/scripts/no longer exists. A hand-maintained copy whose source is gone, and whose header is false.It had drifted where it mattered
The script carried its own table of four tool directories and stated "Codex CLI: rules not supported, skipped".
plugin-content-translator.ts:237routes a plugin'srules/into every tool whose capability accepts them, and all five accept them. A Codex project asking what rules it had was answered none — silently, and wrongly.plugins/aidd-context/skills/05-rule-generate/references/tool-paths.mdsaid the same of Codex and OpenCode, and told a generator to put the convention inAGENTS.mdinstead. Both rows are corrected here.What removes the table instead of moving it
RulesCapability.installedLocation()asksbuildInstallPathwith a sentinel name and reads the answer back, so the directory and extension come from the installer:.claude/rules/.md.codex/rules/.md.github/instructions/.instructions.md.cursor/rules/.mdc.opencode/rules/.mdThree of those are a path template, Cursor's goes through
toMdc, Copilot's is a delegated handler — so a caller parsing a path string back apart would have been a second copy, free to disagree the day a tool moves.registry-conformancepins all five, so a sixth tool or a moved directory is read by whoever changes it.The hand-rolled frontmatter parser goes too:
domain/formats/markdown.tsalready exportedparseFrontmatter.hasRuleswas private toplugin-content-translator.tsand is now exported fromcontracts.tsbeside the type it guards, generic so that translator keeps the narrowing it had.Contract kept
--jsonprints the array the script printed, field for field, so the skill reading it did not change what it reads. The default output is for a person, and a project with no rule says so rather than printing nothing — an empty answer and a command that never ran look identical on a terminal, and only one of them is a fact about the project.The skill runs
aidd ai rules --jsonand, finding no command, stops and says the CLI is required. Never an empty inventory — which is the rule the first commit writes down.Tests
Fourteen new cases, each written before the code. Mutations run, three killed:
installedLocationanswers the source path, not the installed oneAn e2e case runs the built binary against a temp project holding a Claude rule, a Codex rule and a
README.mdsitting beside a Cursor rule — because the skill invokes the binary, and a use case passing in isolation says nothing about whether the subcommand is reachable.vitest3,488 passed / 312 files. 371 repository script tests. Typecheck, biomeci(2 pre-existing warnings), knip, jscpd, layering clean, 0 broken links in 798 files. Bundle 596.6 → 599.0 KB, budget 598 → 601, measured and recorded beside the three raises before it.