Skip to content

docs+feat: the line between a hook and the CLI, and the last plugin script crossing it - #778

Merged
blafourcade merged 4 commits into
nextfrom
docs/the-line-between-a-hook-and-the-cli
Sep 6, 2026
Merged

docs+feat: the line between a hook and the CLI, and the last plugin script crossing it#778
blafourcade merged 4 commits into
nextfrom
docs/the-line-between-a-hook-and-the-cli

Conversation

@blafourcade

@blafourcade blafourcade commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

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:

Start
plugins/aidd-telemetry/hooks/journal.cjs 27 ms
cli/dist/cli.js --version 180 ms

6.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:

Triggered by Latency Runs as
Observing a tool event, thousands of times a session must not be felt plain Node in hooks/, no install, no dependency
Answering a person or a skill, once irrelevant the aidd CLI

Two 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.js already 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 node to measure and aidd to answer.

The bug the guard could not see

docs/ARCHITECTURE.md named hooks/update_memory.cjs. That file has always been .js — in the tree and in the plugin's own 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 exists to catch exactly this. It scanned cli/src, cli/tests, plugins and scripts for .ts, .cjs and .js, and no markdown at all. It now scans docs/ 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:

Scope scanned Findings Real faults
every .md in the tree 17 0
docs/**/*.md 3 1

The 17 are a skill's own assets and cli/tests/fixtures templates naming illustrative paths on purpose. Of the 3, one is this bug; the other two name INSTALL.md, a seam artefact one plugin writes into a reader's own project for another to read back, now listed in NOT_A_REPOSITORY_FILE for what it is.

Verification

Red before the fix, on the one line it names:

docs/ARCHITECTURE.md names `hooks/update_memory.cjs`, which no tracked file matches

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 uncovered

plugins/aidd-context/skills/11-explore/scripts/list-rules.mjs was the only script left inside a skill — 198 lines — and its own header read:

// NOTE: synced from plugins/aidd-dev/scripts/list-rules.mjs. Keep in sync when the source changes.

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:237 routes a plugin's rules/ 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.md said the same of Codex and OpenCode, and told a generator to put the convention in AGENTS.md instead. Both rows are corrected here.

What removes the table instead of moving it

RulesCapability.installedLocation() asks buildInstallPath with a sentinel name and reads the answer back, so the directory and extension come from the installer:

Tool Directory Extension
claude .claude/rules/ .md
codex .codex/rules/ .md
copilot .github/instructions/ .instructions.md
cursor .cursor/rules/ .mdc
opencode .opencode/rules/ .md

Three 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-conformance pins 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.ts already exported parseFrontmatter. 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.

Contract kept

--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 of them is a fact about the project.

The skill runs aidd ai rules --json and, 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:

Mutation Red
installedLocation answers the source path, not the installed one 4
the scan drops its extension filter 1
a name trimmed at the last dot, not by the whole extension 1

An e2e case runs the built binary against a temp project holding a Claude rule, a Codex rule and a README.md sitting 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.

vitest 3,488 passed / 312 files. 371 repository script tests. Typecheck, biome ci (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.

@blafourcade
blafourcade requested a review from a team as a code owner September 5, 2026 12:32
@blafourcade blafourcade changed the title docs: state the line between a hook and the CLI, and guard the docs that name files docs+feat: the line between a hook and the CLI, and the last plugin script crossing it Sep 5, 2026
@blafourcade
blafourcade enabled auto-merge (squash) September 6, 2026 02:41
reference-week and others added 4 commits September 6, 2026 04:46
…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
blafourcade force-pushed the docs/the-line-between-a-hook-and-the-cli branch from 73d878f to 344c5cf Compare September 6, 2026 02:49
@blafourcade
blafourcade merged commit eb073b7 into next Sep 6, 2026
17 checks passed
@blafourcade
blafourcade deleted the docs/the-line-between-a-hook-and-the-cli branch September 6, 2026 06:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant