[US-449] docs: every invocation names pair-cli, and the staleness gate can see when it does not - #473
[US-449] docs: every invocation names pair-cli, and the staleness gate can see when it does not#473rucka wants to merge 21 commits into
pair-cli, and the staleness gate can see when it does not#473Conversation
Verdict
PR: #473 · Author: Gianluca Carucci · Reviewer: independent reviewer agent · Date: 2026-08-30 · Story: US-449 · Type: docs Classification matrix — per dimension
Tier = max(assessed) = yellow, unchanged from refinement. AssessmentsSecurity — Input validationVerdict: green — no input path touched; the only logic change is a docs-lint regex over repo-local files. Security — Output handlingVerdict: green — changed strings are CLI help/error text, no encoding surface. Security — Authentication / AuthorizationVerdict: green — not applicable, no auth path in the diff. Security — Introduced vulnerabilitiesVerdict: green — 0 introduced, 0 pre-existing in scope. The regex is anchored and bounded ( CostVerdict: Architecture (Coupling)Verdict: not assessed — until #263. Bug fix — Red test before fixVerdict: green — the latent fence-bleed bug carried in T-2 has a dedicated reproduction ( DetailsFindings by severityCritical (must fix before merge)
Major (must fix before merge)
Minor (must fix before merge — same bar as Major, just lower impact)
Questions (informational, never blocking)
Positive feedback
Functionality & requirements (AC coverage)
Testing & quality gates
Adoption compliance
Tech debt
Non-actionable (recorded, not blocking on this PR)
Documentation
Performance & deployment
|
… ones Review round 1 on PR #473. Seven findings, all fixed in place. Swept (the sweep's own reach, not the file list): - shipped KB dataset (24 spans / 7 files) + the mirrors regenerated by the real `pair-cli update`, never hand-edited — an installed KB was telling consumers to run commands no install creates - qa/release-validation: the three expected-output assertions an operator compares against the real error message, plus the Scope/MT-* lines - scripts/workflows/release: hints, comments, and TWO EXECUTABLE lines in smoke-test-kb-package.sh — detection probed `node_modules/.bin/pair` (aborts on a repo where pair-cli IS installed) and it ran `kb package --output`, which the CLI answers `error: unknown option '--output'` Gates that did not exist: - commands/index.test.ts asserts every registry `usage`/`examples` names pair-cli — the surface where #451's `pair run` slipped past every green check - the npx clause was a package SLOT that swallowed the binary, so no npx-prefixed invocation could ever match; the runner is now a prefix of the captured binary ADL: one measured number (29 pages), dataset+release-scripts moved into the stated scope, the two behavior changes named for review. Refs #449
pair-cli, and the staleness gate can see when it does not
Escalation — round 14 (13 review rounds, working log:
|
| round | commit | findings addressed |
|---|---|---|
| 1 | 6a58aa61 |
canonical rename sweep (qa/release-validation, knowledge-hub dataset, smoke-test hint, npx runner regex, command usage/examples, ADL) |
| 2 | 7f8939f3 |
pre-push-gate printed remedy + docstring, 29-span sweep, registry gate reads bin from manifest, doubled-space docstring |
| 3 | 294d586a |
commander .name()/.usage() wired to manifest, npm-artifact smoke-test hard-fail, code-span tokenization, wrong-binary message wording, .gitignore + adoption docs sweep, pair update regex fix |
| 4 | 1bcd1d7a |
frontmatter description backticks (3 pages), ADL rejected-alternative note |
| 5 | a15b802b |
flag-aware COMMAND_TOKEN, smoke-test exit-code-after-set-e fix, migrations sweep follow-on |
| 6 | 810e1bfb |
smoke-tests README sweep, runner-group widening (npx/pnpm dlx/pnpm exec/yarn dlx/bare pnpm) |
| 7 | 44c0d551 |
RUNNER_FLAG package-argument consumption, docstring/ADL criterion correction, exclusion glob broadened |
| 8 | 4435e922 |
doubled-delimiter docstring correction, derived BINARY alternation, ADL "single place" claim made true, PR body AC3 scope correction |
| 9 | c6a85844 |
CommonMark closer-rule code-span regex (fixes real corpus lines skill-management.mdx:211,219), ADL decision-1/exclusion corrections |
| 10 | bff4e614 |
dropped a fence assertion that pinned nothing (measured), off-by-one comment fix |
| 11 | 7c4e120f |
cell/prose fixture description precision (verbatim vs excerpt) |
| 12 | da9a5b1c |
"restore" wording replaced with the measured red-suite consequence (both fixtures go red, not silent collapse) |
| 13 | b1cef967 |
"restore to its corpus line" narrowed to fit only cell (the edit, not the noun) — prose has no corpus line |
Quality gates have stayed green every round (pnpm quality-gate, pnpm smoke-tests, website e2e, docs-staleness-check.test.ts, docs:staleness). Nothing escalated or deferred in rounds 1-13.
Still-open actionable findings (round 14)
- [Questions]
apps/website/lib/docs-staleness-check.test.ts:589— Cosmetic ragged wrap left by the round-13 edit, with no failure case. Line 589 (// exists and it simply turns red. A delimiter run that is balanced but whose) measures 79 chars while the rest of the same comment block measures 94-100 (579=102, 580=97, 581=97, 582=98, 583=97, 584=98, 585=100, 586=94, 587=99, 588=96, 589=79, 590=99). The short line sits mid-paragraph, reading as if a sentence ended there when it does not. Raised as a Question, not a defect: it cannot break the gate —prettierdoes not reflow//comments and the file passes verbatim (prettier --config tools/prettier-config/.prettierrc.json --checkreports clean despite printWidth 100; line 579 is pre-existing and already exceeds it at 102). Recommendation (optional, human's call): re-flow lines 587-590 to fill to ~98 chars uniformly. No functional change either way.
Convention for continuing this cycle
Further rework or re-review on this story — including manual out-of-band rounds — should be appended to the working log .pair/working/reviews/449.md, not posted as standalone PR comments. The next orchestrated run on this story continues the same cycle; its eventual convergence will synthesize ONE final remediation comment and minimize this one plus the first-review comment.
Note: the working log is an untracked file living only in the persistent authoring worktree ../pair-worktrees/449. That worktree must be preserved until merge — if it is pruned/recreated, the audit trail of all 13 rounds is lost (this comment and the first-review comment would still remain on the PR, and the first-review signal still prevents a duplicate first review on the next run, but the round-by-round evidence would not).
Not merged. No human decision has been made on the open Question above.
Bound re-review to immutable deltas and require a complete finite-state inventory before review or remediation.
Require real boundary evidence for externally-defined state and repair claims.\n\nRefs: #416
b1cef96 to
2c76abf
Compare
… ones Review round 1 on PR #473. Seven findings, all fixed in place. Swept (the sweep's own reach, not the file list): - shipped KB dataset (24 spans / 7 files) + the mirrors regenerated by the real `pair-cli update`, never hand-edited — an installed KB was telling consumers to run commands no install creates - qa/release-validation: the three expected-output assertions an operator compares against the real error message, plus the Scope/MT-* lines - scripts/workflows/release: hints, comments, and TWO EXECUTABLE lines in smoke-test-kb-package.sh — detection probed `node_modules/.bin/pair` (aborts on a repo where pair-cli IS installed) and it ran `kb package --output`, which the CLI answers `error: unknown option '--output'` Gates that did not exist: - commands/index.test.ts asserts every registry `usage`/`examples` names pair-cli — the surface where #451's `pair run` slipped past every green check - the npx clause was a package SLOT that swallowed the binary, so no npx-prefixed invocation could ever match; the runner is now a prefix of the captured binary ADL: one measured number (29 pages), dataset+release-scripts moved into the stated scope, the two behavior changes named for review. Refs #449
Questions are, by the review template's own definition, questions for the human: items the reviewer marks "No change requested". The floor defaulted to none, so they entered the set the fixer must resolve, and each answer enlarged the diff and created fresh surface for the next review. Measured on PR #477: the PR reached a zero-actionable APPROVED twice; both times the following round implemented the Questions and the re-review found new Minor findings inside the code that round had just added (three, then two). Work now completes when Critical, Major and Minor are closed. An explicit severityFloor still wins. The default is applied softly - a template without Minor, or without a usable ranking, falls back to no floor instead of throwing, because a default must not break a run that never asked for it. - ADL 2026-09-03-the-review-severity-floor-defaults-to-minor.md - three tests: Questions-only converges and is carried, a Minor still blocks, a vocabulary without Minor falls back Refs: #219 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… yaml-parsed allow-list (#477) The check-only pre-push gate was the sole enforcement point for formatting, and `--no-verify` skips it. CI now runs `format:check` in a dedicated workflow, and the workflow's own shape is guarded by a composition check. The guard parses `.github/workflows/format.yml` with yaml@2.8.2 and expresses every rule as an allow-list over the parsed document, per ADL 2026-09-01; the hand-rolled line reader it replaces is retired. Nothing is ever rewritten in CI — write-mode formatters are banned by the same guard. Also carries a path-scoped `.gitleaks.toml` allowlist for a synthetic 40-hex SHA fixture in scripts/smoke-tests, which gitleaks matched as a generic-api-key at historical commit 5e99d53. Closes #413 🤖 Generated with [Claude Code](https://claude.com/claude-code)
The published bin is `pair-cli` (ADL 2026-08-25); every documented and printed invocation said `pair`, which no npm install ever creates — a copy-paste that fails with `command not found`. - 310 invocations across 28 published docs pages (not the 2 the refinement enumerated: commands.mdx alone carried 81) - all 11 command `metadata.ts` usage/examples (the ten refined + `run`) - printed hints, usage and error strings in `apps/pair-cli/src`, the `scaffold-kb` templates it generates, and the `pair-cli update` regenerate hints in `packages/knowledge-hub`'s mirror tools - `DEVELOPMENT.md`'s command table and mirror-guard prose Prose mentions of "pair" as the product name, the PM-tool mapping diagrams, and `run_pair` in the smoke suite are left untouched. Refs: #449
`INVOCATION_PREFIX` matched the literal `pair-cli`, so the gate that
exists to catch drift in documented commands was structurally blind to
the wrong binary. It now captures the binary and judges it.
- bare `pair <cmd>` is its own error, reported once (never also as an
unknown command)
- separator is ONE space, not `\s+`: an aligned column is a diagram
(`pair Linear`), never an invocation
- span rule allows only horizontal whitespace after the backtick — a
CLOSING fence ends with one, and `\s*` reached across the newline
into the next paragraph ("pair creates Markdown files" read as a
command). Latent while only `pair-cli` matched; 8 failures once bare
`pair` counted.
Tests written first (RED verified) for the new error, the shell-prompt
and once-only cases, and for the diagram/prose/fence-reach
non-regressions; npx, version-string and flag cases unchanged.
Refs: #449
The 2026-08-25 ADL scoped the rename from a two-file grep; turning the gate on found 310 invocations across 28 pages, plus the diagram and closing-fence false positives the rule had to answer. Recorded as an ADL rather than left in a commit message. - ADL 2026-08-30: binary-capturing rule, one-space separator, no fence-reach, and the sweep's real perimeter (incl. what stays bare) - infrastructure.md: the canonical name is now ENFORCED by the docs:staleness gate, cross-referencing both ADLs - .pair/llms.txt: index the new decision (generator conformance) Refs: #449
… ones Review round 1 on PR #473. Seven findings, all fixed in place. Swept (the sweep's own reach, not the file list): - shipped KB dataset (24 spans / 7 files) + the mirrors regenerated by the real `pair-cli update`, never hand-edited — an installed KB was telling consumers to run commands no install creates - qa/release-validation: the three expected-output assertions an operator compares against the real error message, plus the Scope/MT-* lines - scripts/workflows/release: hints, comments, and TWO EXECUTABLE lines in smoke-test-kb-package.sh — detection probed `node_modules/.bin/pair` (aborts on a repo where pair-cli IS installed) and it ran `kb package --output`, which the CLI answers `error: unknown option '--output'` Gates that did not exist: - commands/index.test.ts asserts every registry `usage`/`examples` names pair-cli — the surface where #451's `pair run` slipped past every green check - the npx clause was a package SLOT that swallowed the binary, so no npx-prefixed invocation could ever match; the runner is now a prefix of the captured binary ADL: one measured number (29 pages), dataset+release-scripts moved into the stated scope, the two behavior changes named for review. Refs #449
The three places ADL 2026-07-31 requires to agree had drifted to two: the two documents said `pair-cli update`, PRE_PUSH_REMEDY still printed `pair update` — the copy-pasted one, and the one that does not exist. Pinned by test now. Comment boundary declared instead of accidental: 29 spans over 16 files swept (content-ops, knowledge-hub tools + conformance, dev-tools, docs-staleness-check itself), the deliberate wrong-form examples left alone, both stated in the ADL scope clause. Registry gate reads the sole `bin` key from package.json instead of a literal, so renaming the published name turns it red (72 offenders) instead of green. Separator docstring now covers TAB, pinned. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UQJzGMhRqBRRboxMrRqFPP
…d nothing Review round 3. - cli.ts: program name = the sole `bin` key, not `pkg.name` — `--help` opened `Usage: @pair/pair-cli install [options]`, a copy-paste that answers `command not found`. `metadata.usage` was DEAD (never passed to commander); now rendered via `cmd.usage()`, so the registry gate and the reader assert the same string. Tests read the RENDERED help. - smoke-test-npm-artifact.sh: `.bin/pair` → `.bin/pair-cli` (npm links only the declared bin key). Every branch took its fallback: Test A ran `npx`, Test B and the whole standardized suite were SKIPPED, exit 0. Warnings → hard failure. - docs-staleness-check.ts: span rule tokenizes real code spans instead of matching after ANY backtick — a CLOSING inline span + prose was read as an invocation. Wrong-binary message no longer recommends a command that does not exist. - `.gitignore` + 2 live adoption docs swept; pre-push negative pin uses a lookbehind so offset 0 is covered. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UQJzGMhRqBRRboxMrRqFPP
The rename put `pair-cli run` / `pair-cli update` inside YAML frontmatter `description`, which page.tsx renders raw in <DocsDescription> and in metadata.description: the /docs/integrations/pi subtitle and its <meta name="description"> (the Google/social snippet) both carried two visible backticks. Verified against the real build output before/after. Same defect on migrations/v0.4-to-v0.5 (pre-existing, line already in this diff) — same contract, swept with it. No gate added: frontmatter prose is outside the staleness rule's positional reach by design. ADL: names the CLI registry gate (reads the sole `bin` key) as what keeps the website's PUBLISHED_BIN literal safe — the asymmetry is deliberate.
…ics were unreachable `pair --version` was invisible: the command token was `[A-Za-z][\w.-]*`, so a leading `-` failed the WHOLE prefix and the line was never seen as an invocation. 9 pages carry that exact form correctly spelled — the slip is one edit away. Token is flag-aware now; only the binary half runs on a flag. Both release wrappers echoed the suite exit code AFTER a bare call under `set -e`, so the line only ever printed on success. `|| RET=$?` keeps the diagnostic reachable on the one path it exists for. Refs #449
… a binary no install creates `npx` was the only runner the invocation prefix knew, so every pnpm/yarn form the docs actually publish sat outside the rule: `pnpm dlx pair install` -> NO MATCH, `pnpm pair install` -> NO MATCH. Drop the `-cli` on workflows.mdx:269 or team-setup.mdx:48 and the gate returned [] — verified on the real pages, old rule PASS / new rule FAIL with 3 issues. Bare `pnpm` takes no flag run on purpose: `pnpm --filter @pair/pair-cli build` would otherwise report `build` as a nonexistent command on three correct pages. Both halves pinned. scripts/smoke-tests/README.md still told a contributor to run `pair install`, `pair package`, `pair kb-validate` and four more. The ADL excluded the whole directory on a `run_pair`-is-a-shell-function rationale that does not reach a README describing what each scenario verifies; exclusion narrowed to `**.sh`. Refs #449
…e flag `npx --package @foomakers/pair-cli pair-cli install` is correct and is the canonical npx idiom when a bin differs from its package name. The flag run ate `--package` alone, `@foomakers/` became the scope, the first `pair-cli` the binary, and the REAL binary token its command: `"pair-cli pair-cli" … is not a command` — CI red on a correct page, no edit clears it but deleting a correct line. Other direction: no `=value` form existed, so `npx --package=@foomakers/pair-cli pair install` returned [] — drift, green, behind a LISTED runner. Same for `-p` and for `pnpm dlx --package`. RUNNER_FLAG consumes `--package`/`-p` with its argument and accepts `--flag=value`. Bare `pnpm` still takes no flag run — its `--filter` is exactly such a flag and is deliberately unlisted, which is now the criterion the ADL states instead of "flags cannot take a package argument", which the code contradicted. ADL exclusion glob `scripts/smoke-tests/**.sh` → `**/*.sh`: `**` adjacent to `.sh` degrades to `*` in one segment, so the recorded rule left `scenarios/*.sh` and `lib/*.sh` — where `run_pair` lives — reading as in scope. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UQJzGMhRqBRRboxMrRqFPP
…ten twice
The `CODE_SPAN` docstring claimed a doubled delimiter (``pair install``) is
invisible to the gate and prescribed "balance the delimiter run". False: the
attempt at the outer backtick fails, the scan retries one char on and pairs the
INNER delimiters. Real `checkDocsCommands` on `Run ``pair install`` now.` →
`write "pair-cli install"`. A writer trusting the comment to quote a wrong form
deliberately got an unexplained red plus a remedy that adds nothing. Paragraph
corrected, both directions pinned (RED: a lookbehind/lookahead CODE_SPAN — the
shape the comment described — fails the 2 new cases).
`INVOCATION_PREFIX` hardcoded `(pair-cli|pair)` beside `PUBLISHED_BIN`, so the
name lived in two places while comment + ADL called the constant the single one.
With `PUBLISHED_BIN='pairx'`: literal form → `pairx install` returns [] (blind to
the new drift) and `pair-cli install` reported wrong against `pairx`. Built from
the constant now → `pairx install` clean, `pair install` → write "pairx install".
ADL: the "single place" claim made true (alternation derived), doubled-span
coverage recorded, and AC3 scoped — only the `pair kb validate` FORM was already
zero; the 67 bare-`pair` lines on reference/cli/{examples,workflows}.mdx are
renamed BY this branch.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UQJzGMhRqBRRboxMrRqFPP
`CODE_SPAN` paired single backticks, so a doubled span consumed ONE of its two closing backticks; the leftover flipped span parity and every later inline invocation on that line went unseen — silent, no error. Live, not latent: `reference/skill-management.mdx:211,219` already carry inline doubled spans (they quote a backticked literal), and appending an inline `pair install` to :219 shipped green on the real gate. Balancing the run alone is not enough — the reason to double a delimiter is backticked CONTENT — so `CODE_SPAN` implements CommonMark's closer rule: opening run of N, closed by the first run of exactly N, content free to hold other runs and forbidden to start/end with a backtick (so a fence still yields no span). Docstring + ADL decision 3 corrected: they asserted the opposite. Also: ADL decision 1's rename evidence labels the two `[]` (blind vs correct) and adds the discriminating `pair-cli install` -> silent case; the wrong-form exclusion is stated per FILE, not per line — its `:499,510-511` was off by four from the round it was written and never covered the spans later rounds added. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UQJzGMhRqBRRboxMrRqFPP
… count off by one "A fence yields no span" was asserted through checkDocsCommands, which cannot observe it: a fenced LINE is scanned by the fence pass anyway and errors dedup by binary+command, so the two passes reading the same line collapse to one error either way. The fixture's only invocation was the CORRECT `pair-cli install`, so it returned [] from the fence pass alone. Measured: the whole suite stays green under a content class stripped of the non-backtick anchors (`[^\n]+?`) — 106/106 — so the assertion would not have caught the degradation it was written to guard. The fence's observable pin is the closing-fence case, which DOES go red (7 failures) under the pre-tokenizer "after a backtick" shape. Assertion dropped, comment says which pin is real; docstring/ADL "the fence is pinned" narrowed to what it can actually change. The corpus carries THREE doubled spans across TWO lines, not two: skill-management.mdx:211 holds two of them, :219 one. Docstring, ADL decision 3 and the test comment all said "both". Corrected in all three. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UQJzGMhRqBRRboxMrRqFPP
…s to Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UQJzGMhRqBRRboxMrRqFPP
… fixtures go red The comment said dropping the appended invocation "collapses the case into the corpus-verbatim assertion below". Measured: `cell` and `prose` both yield 0 errors when restored, and the loop asserts toHaveLength(1) — the suite fails at :594 in both cases, loudly. The collapse claim is true only of `cell`, and only as duplication (its restored line IS the :599 fixture, byte-identical); `prose` has no counterpart there. Comment-only; no behavior change.
… edit, not the noun `prose` is an EXCERPT of skill-management.mdx:219 (the corpus clause runs on `; only triple-backtick/tilde *blocks* are excluded.`), so it has no corpus line to be restored to; the operative edit for both fixtures is dropping the appended `Run \`pair install\` to apply.`. Comment-only, no behaviour delta. Measured: cell 1 err / restored 0; prose 1 err / appendix dropped 0; both mutations -> suite 1 failed at :598 `expected [] to have a length of 1`.
Regenerated with pair update (offline, local dataset) to resolve the .pair/llms.txt rebase conflict. Refs: #449 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FfhvsS5rippi6aUbbGrf6F
2c76abf to
5139cb3
Compare
7b55900 to
adb9627
Compare
PR Information
PR Title: [US-449] docs: every invocation names
pair-cli, and the staleness gate can see when it does notStory/Epic: #449
Type: Documentation (with four tested behavior changes — see Breaking / Behavior Changes)
Assignee: rucka
Labels: documentation, user story, tech-debt, risk:yellow
Summary
What Changed
The published binary is
pair-cli. The docs, the CLI's own printed help, the shipped KB dataset, the release scripts, the manual-test procedures, the pre-push gate's printed remedy,.gitignore, the live adoption docs and the source comments describing all of the above saidpair. Three things happened here:INVOCATION_PREFIXnow captures the binary (pair-cli|pair) instead of pinning the literal, so a barepair <cmd>is a wrong-binary error reported once, and the alternation is BUILT fromPUBLISHED_BIN(const BINARY = `(${PUBLISHED_BIN}|pair)`) — which is what makes it genuinely the single place the canonical name is written in the website package, rather than one of two.Usage:line names the binary, and itsusagemetadata is finally rendered.program.name()waspkg.name, sopair-cli install --helpopened withUsage: @pair/pair-cli install [options]— the first line printed, and a copy-paste that answerscommand not found: @pair/pair-cli. It now comes from the solebinkey of the manifest.metadata.usagewas dead (never passed to commander at all): it is wired intocmd.usage(), so the elevenusage:strings the registry gate checks are the line the reader sees.Why This Change
A reader copy-pastes
pair installfrom the docs and the shell answerscommand not found. Nothing in CI could see it, so it spread:run/metadata.tsarrived from #451, after this story was refined, carryingusage: 'pair run [options]', and every green check stayed green.Story Context
User Story: as a reader of pair's documentation and printed help, I want every invocation to name the binary that actually exists, so that what I copy-paste runs.
Acceptance Criteria: AC1–AC7 on the issue. AC5 (
customization/organization.mdx) was already satisfied before this story started — verified zero repo-wide, and the file is untouched here (no numstat row). AC3 is two halves and only one was: thepair kb validateFORM was already zero repo-wide (git show origin/main:…/examples.mdx | grep -c 'pair kb validate'→ 0), but the AC's binary requirement was not —reference/cli/{examples,workflows}.mdxcarry 67 renamed lines here (43 + 24 pergit diff origin/main...HEAD --numstat), e.g.pair install --list-targets→pair-cli install --list-targets. AC3 is satisfied BY this PR; read those two files. AC3's sibling form needed a code fix too: see Behavior Changes.Changes Made
Implementation Details
apps/website/lib/docs-staleness-check.ts) — binary captured and judged; separator is ONE literal space (an aligned whitespace column is a diagram, not an invocation — the four PM-tool pages map their hierarchy aspair Linear), which excludes a TAB for the same reason; the runner (npx/pnpm dlx/pnpm exec/yarn dlx, plus a barepnpmthat takes no flag run) is a prefix of the captured binary, not a slot that swallows it, and its flag run consumes--package/-ptogether with the package name that follows them and accepts the--flag=valuespelling; the command token is flag-aware, becausepair --versionis an invocation too; the span rule tokenizes real code spans and anchors the prefix at the span's content, so neither a closing fence nor a closing inline span can make the text after it an invocation; and the wrong-binary message only prescribeswrite "pair-cli <cmd>"when that command exists.apps/pair-cli/src/cli.ts) —program.name()=publishedBinName(pkg), the solebinkey;usageArguments()strips the<bin> <command>head commander prints itself sometadata.usagerenders verbatim. Pinned against the renderedhelpInformation, not the registry field.apps/pair-cli/src/commands/index.test.ts) — every registryusageand everyexamplesentry must start with the published binary, read from the solebinkey ofapps/pair-cli/package.jsonrather than compared against a literal, so abinrename turns the gate red instead of leaving it green over 11 usage strings naming a binary no install creates.usageis held to the full<bin> <command>prefix, which is what keepscli.ts's fallback branch dead.metadata.ts· printed hints/usage/errors inapps/pair-cli/srcand in thescaffold-kbtemplates generated into a consumer's repo · thepair-cli updateregenerate hints inpackages/knowledge-hub's mirror tools · the shipped KB dataset (24 spans / 7 files) ·qa/release-validation/*.md·scripts/workflows/release/**·DEVELOPMENT.md·PRE_PUSH_REMEDY·.gitignoreand the two live adoption documents · source comments and test names, repo-wide (29 spans / 16 files).Files Changed
149 files, +1573 / −743.
apps/website/content/docs(29 pages) +apps/website/lib·apps/pair-cli/src(cli.ts, metadata, printed strings, scaffold templates, the registry gate) ·packages/knowledge-hub/dataset+ its regenerated twins under root.pair/knowledge/**,.claude/skills/**,.claude/workflows/**·packages/content-ops·packages/dev-tools·qa/release-validation·scripts/workflows/release·DEVELOPMENT.md·.gitignore· 2 adoption docs · one ADL.The dataset was edited at its canonical source only. The twins are the OUTPUT of a real
pair-cli updaterun; the three mirror guards (mirror-guard,skill-md-mirror,workflow-mirror) assert that insidepnpm quality-gate.Services to Release
@foomakers/pair-cli: theUsage:line, help, examples and error strings now namepair-cli. No flag, command, or exit-code change — the strings a user reads, not the interface they call.@pair/knowledge-hub(KB dataset): the installed.pair/knowledge/**a consumer gets no longer instructs them to run a binary no install creates.Testing
Test Coverage
docs-staleness-check.test.ts81 → 106 (every published runner form —npx, the realnpx --no @foomakers/pair-cli <cmd>,pnpm dlx, barepnpm— the--package/-pand--flag=valueforms in both directions; plus the non-invocations that must stay unflagged,pnpm --filter <pkg> <script>andpnpm add -D @foomakers/pair-cli; a flag as the command token; the TAB-separator exclusion; closing fence AND closing inline span; the DOUBLED-backtick span in both directions, a doubled span whose CONTENT holds backticks (both shapes the corpus actually carries) and the line-mate invocation each of them used to blind, and the unbackticked prose that is the actual exemption path; the wrong-binary message text, both directions).cli.test.ts+commands/index.test.ts34 → 40 (the renderedUsage:line top-level and per-command,publishedBinName,usageArguments, the registry gate bound to the publishedbin).pre-push-gate-composition.test.ts+1.packages/knowledge-hub/src/tools/{mirror-guard,skill-md-mirror}.test.tspin the printed regenerate hint, whose text changed. Assertion text only, no behavior change — flagged because a modified existing test is something a reviewer must be told, not discover.smoke-test-kb-package.shagainst a fixture repo,smoke-test-npm-artifact.shagainst a fixture npm tarball. Evidence below.Test Results
Tier:
Pre-merge tiering: disabled⇒ the full adopted suite IS the CI-parity set.dup:checkprints 18 pre-existing clones and exits 0 — none in a file this PR touches.Testing Strategy
Every gate here was verified RED before being trusted, and the two ungated scripts were executed:
run/metadata.tstousage: 'pair run [options]'expected [ 'run.usage: pair run [options]' ] to deeply equal []binpair-cli→paircinapps/pair-cli/package.json.name(chalk.blue(pkg.name))and drop thecmd.usage()callexpected 'Usage: @pair/pair-cli [options] [comm…' to be 'Usage: pair-cli [options] [command]'+ the same forinstall --helpINVOCATION_PREFIXPUBLISHED_BIN = 'pairx'in the real module and runcheckDocsCommandson three fenced linespairx install→[]— the newly-correct form not recognised as an invocation at all (silent blindness) — whilepair-cli installis reported wrong againstpairx. With it BUILT from the constant:pairx install→[]correctly recognised,pair install→write "pairx install". Editing only the constant, which the ADL instructs, is now sufficientCODE_SPANto the single-backtick pairing (/`([^`\n]+)`/g)Seepair-cli installthen `pair update` next.→[](the control with single backticks is flagged),Notefooand then `pair update` next.→[]— ANY doubled span, even one naming no binary, consumed one of its two closing backticks and flipped span parity for the rest of the linepair installinvocation toreference/skill-management.mdx:219PASS — 44 skills, 11 commands in sync— the wrong binary ships green. NEW rule:FAIL — 1 issue→reference/skill-management.mdx tells the reader to run "pair install" … write "pair-cli install". Page reverted ⇒PASS. Same result on:211; the identical text on line 10 of the same file was flagged either way, which is what isolates the doubled span as the causereference/cli/workflows.mdx:269topnpm dlx pair install,:272topnpm dlx pair kb validate,tutorials/team-setup.mdx:48to`pnpm pair install`PASS — 44 skills, 11 commands in sync(green on the exact drift this story exists to catch). NEW rule:FAIL — 3 issues— the two wrong-binary lines plus"pair kb" … is not one of its commands. Docs reverted ⇒ PASS again, with no false positive on the corpus's 39pnpm --filter/pnpm add/npm install -glinesreference/cli/workflows.mdx:269withnpx --package @foomakers/pair-cli pair-cli install(correct) +npx --package=@foomakers/pair-cli pair install(drift)FAIL — 1 issue—… run "pair-cli pair-cli", which is not a command: red on the CORRECT line, silent on the drifted one. NEW rule:FAIL — 1 issue—… run "pair install" … write "pair-cli install": the correct line clean, the drift caught. Page reverted ⇒PASSRUNNER_FLAGto a lone-flag runexpected [ Array(1) ] to deeply equal [](the correct--packageidiom flagged),… to deeply equal [ Array(1) ](-pgiving the wrong message),expected [] to deeply equal [ Array(1) ](the=valuedrift returning[])COMMAND_TOKENto[A-Za-z][\w.-]*pair --version— the line 9 pages carry — is not seen as an invocation at all; the flag cases fail`config.json` pair skills resolve state.is read as an invocation ofskills— 1 error where 0 is correctexpected '…tells the reader to run "pair init"…' to contain '"init" is not one of its commands'pair updateinPRE_PUSH_REMEDYexpected 'Formatting is checked…' to contain 'pair-cli update'node apps/pair-cli/dist/cli.js {--help, install --help, kb-cache --help}Usage: pair-cli [options] [command]·Usage: pair-cli install [target] [options]·Usage: pair-cli kb-cache [list|prune] [options]— the last two aremetadata.usageverbatimsmoke-test-npm-artifact.shagainst a fixture@foomakers/pair-clitarball whosebin.pair-cliexecs the built CLIcompleted successfully, exit 0. AFTER: 1240-line run — Test A + Test B execute the installed binary andrun-all.sh --binary …/.bin/pair-cli --ci --offline-onlyruns the full suitebin.pairinsteadError: the installed artifact linked no executable at …/.bin/pair-cliCODE_SPANcontent class stripped of its non-backtick anchors (([^\n]+?))CODE_SPANreverted to the pre-tokenizer "after a backtick" shape (/(?<=`)\s*()([^\n]*)/g)does not let a closing fence reach into the next paragraph— that case IS the fence's discriminating pin, and it is what the docstring now namesBreaking Changes
No API, CLI-interface or data change. Three behavior changes, all deliberate, all named here because the rest of the diff is a rename:
The staleness rule itself — a bare
pair <cmd>in a docs page now failsdocs:staleness. Fully tested. It also fixes a latent bug class in the same rule: the span rule used to match after any backtick, so a closing fence bled into the paragraph below ("pair creates Markdown files" → an invocation ofcreates, 8 real failures the moment barepaircounted) and a closing inline span bled into the prose after it on the same line (`config.json` pair skills …→ an invocation ofskills, which would turn a correct docs edit red with advice that corrupts the sentence, sincepairis the product name on ~10 pages). Tokenizing real spans removes that class. A third member of it: a doubled span left a stray backtick that blinded every later inline invocation on the same line — on lines the corpus already ships. All three are pinned.scripts/workflows/release/smoke-test-kb-package.sh— two executable lines, not strings:command -v pairandnode_modules/.bin/pair— a binary no install creates. On a repo wherepair-cliis npm-installed,origin/main's script prints❌ Error: pair CLI not foundand exits before Test 1; this branch prints✓ Found npm-installed pair-cli.$PAIR_CMD kb package --output …. The real CLI:kb package --output out.zip→error: unknown option '--output';package --output out.zip→ parses and proceeds.packageis a top-level command;kbis not a command group. This is AC3's ownpair kb validatedefect class — missed at refinement because the AC was checked by grepping one command name instead of the form.scripts/workflows/release/smoke-test-npm-artifact.sh— the same defect, in the release workflow, plus a warning that hid it.PAIR_BIN/INSTALLED_BINpointed atnode_modules/.bin/pair; the package declares one bin key,pair-cli, so every branch took its fallback — Test A rannpx pair-cli(resolving outside the artifact under test), Test B printedWarning: Test B skipped, the standardized suite printedWarning: Installed binary not found … Skipping suite., and the script exited 0. A release gate green having exercised nothing. Paths fixed, thenpxfallback removed, and the two warnings are now one hardexit 1: a smoke test that cannot find the binary it just installed has not passed, it has not run.Both release wrappers' exit-code diagnostic is now reachable. Each echoed the suite's exit code after a bare call under
set -e, so the line printed only when the suite had already passed — never on the one path a diagnostic exists for.|| RET=$?keeps it.Neither release script has an automated gate (neither is in
scripts/smoke-tests/**, and CI runssmoke-test-npm-artifact.shonly inside the release workflow), so both were verified by executing them — see the Testing Strategy table.Documentation
2026-08-30-docs-staleness-invocation-rule-and-repo-wide-pair-cli-sweep.md— the gate's reach defines the sweep, the single-space/TAB trade, the npx prefix, the span-tokenizing rule, the printedUsage:line, the comment boundary, and the full in/out scope list.tech/infrastructure.mdstates the canonical name is enforced;.pair/llms.txtindexes the ADL.PRE_PUSH_REMEDYis in scope as the PRINTED remedy. ADL2026-07-31-pre-push-gate-is-check-onlyrequires the two-step remedy to agree in three places —DEVELOPMENT.md, itsdevelopment-setup.mdxtwin, and this string. It is the only one a developer copy-pastes out of a terminal, so it is renamed here and pinned by test..gitignoreare in scope; append-only history is not..gitignore's "Generated bypair-cli update" header is a developer-facing instruction, and the two adoption documents state current facts..pair/adoption/decision-log/**,.pair/adoption/tech/adr/**and the CHANGELOGs keep theirpairspellings — that is the record of what was once true.descriptionis a plain string:app/docs/[[...slug]]/page.tsxrenders it raw in<DocsDescription>(the subtitle under the title) and hands the same string tometadata.description(the<meta name="description">a search/social preview shows), so backticks there would be two visible characters in both. Verified in the prerendered HTML of the three pages whose description names a command. Frontmatter prose is also outside the staleness rule's positional reach by design, so nothing is lost by writing it bare.PUBLISHED_BINin the website package is a literal while the CLI reads its manifest — recorded as a rejected alternative in the ADL: havingapps/websitereadapps/pair-cli/package.jsonwould couple a separate deployable for one string, and the CLI registry gate (which reads the solebinkey) is what makes abinrename impossible to land with the docs gate silently still prescribing the old name. It is one literal: the captured alternation is derived from it, so acting on that record is a one-line edit. The legacypairstays a literal beside it because it is the known-WRONG binary, not a second name for the published one.pairis not read as a miss):scripts/smoke-tests/**/*.sh(every.shunder it,scenarios/andlib/included) including their comments and log lines (run_pairis a shell function, not the binary — its README is in scope: it describes what each scenario verifies in the reader's own terms, so itspair installwas a copy-paste that fails) ·.github/workflows/*.ymlstep names · the product name in prose ("pair installs bridge files") · and fixtures or comments that deliberately quote the WRONG form as the defect being illustrated —docs-staleness-check.ts, the wrong-binary cases indocs-staleness-check.test.ts,commands/index.test.ts's quote of the shippedusage: 'pair run [options]', and ADL 2026-07-31's Context. Renaming those erases the evidence the gates exist for.Reviewer Guide
Why the diff is 149 files and not the ~15 the story enumerated
The gate defines the reach, and the DoD requires
docs:stalenessgreen. Measured, not estimated: run this branch's rule overorigin/main's docs tree (all 84 pages) ⇒ 78 wrong-binary errors across 28 pages. The sweep changed 29 — those 28 pluscustomization/adopt.mdx, whose single occurrence sits in a fenced tree-diagram comment, outside the positional rule, renamed for consistency and named as such in the ADL rather than hidden in the count. The remaining files are the ungated surfaces a human executes (release scripts, QA procedures,scripts/smoke-tests/README.md), the shipped dataset, the CLI's printed help, the pre-push remedy, and the source comments describing the same invocations.Review focus
apps/website/lib/docs-staleness-check.ts— the only non-mechanical docs logic. The docstring carries each rule's reason, including two knowing trades: neitherpair-cli <cmd>(two spaces) norpair-cli\t<cmd>(tab) is flagged — no such form exists in the docs today — in exchange for immunity to the alignment diagrams; andnpx/pnpm dlx/pnpm exec/yarn dlxconsume a flag run while a barepnpmdoes not, becausepnpm --filter <pkg>puts a package name in flag-argument position and this repo's package is literally calledpair-cli(pnpm --filter @pair/pair-cli buildwould be reported as the nonexistent commandbuildon three correct pages). The rule that makes that asymmetry checkable rather than folkloric: a flag whose ARGUMENT is a package name is consumed WITH its argument and must be listed inRUNNER_FLAG(--package/-pis), and a runner with an unlisted one gets no flag run at all — which is barepnpm/--filter. Both trades say what to narrow instead of re-widening, and every case is pinned.CODE_SPANimplements CommonMark's own closer rule — an opening RUN of N backticks closed by the first run of exactly N, content free to hold runs of any other length and forbidden to begin or end with one (so a fence still yields no span). That is not pedantry: pairing single backticks read a doubled delimiter's own content fine, but consumed only ONE of its two closing backticks, and the leftover flipped span parity for the REST of the line — every later inline invocation on it went unseen, silently. The corpus already carries three inline doubled spans over two lines (reference/skill-management.mdx:211, which holds two, and:219, which holds one — each quoting a backticked literal), so it was live, not latent. Doubling is still not an exemption; the way to quote a wrong form on a docs page is unbackticked prose, outside this positional rule by construction. One property the suite deliberately does not assert: "a fence yields no span" is unobservable throughcheckDocsCommands— a fenced line is scanned by the fence pass regardless and errors dedup by binary+command, so both passes reading the same line collapse to one error. The fence is pinned through what it can change (the fence pass's own cases, and the closing fence that must not reach the paragraph below). The two fixtures that reproduce the corpus's doubled spans are:211/:219plus the inline invocation an author would add on the same line — that line-mate IS the case, so dropping the appended invocation from either fixture leaves a line with no invocation and the suite goes red on the spot (measured, both). Dropping it fromcelladditionally leaves:211byte-for-byte, i.e. duplicates the corpus-verbatim assertion beside it;proseis only the span-bearing excerpt of:219(the corpus clause runs on), so there is no restored-corpus reading of it at all. The test comment states exactly that.apps/pair-cli/src/cli.ts— the program name and the newly renderedcmd.usage(). The tests assert the rendered help, not the registry field, because a gate on an unrendered field is exactly what shipped before.apps/pair-cli/src/commands/index.test.ts— the registry gate reads the publishedbinfrom the manifest; a secondbinkey fails the test too.scripts/workflows/release/smoke-test-{kb-package,npm-artifact}.sh— the executable lines, and the warning→failure change in the second.git grep -nE '$?pair [a-z][a-z-]*'minus the declared exclusions returns only the deliberate wrong-form fixtures and.pair/llms.txt`'s generated index entry for an excluded record's title.Testing the changes