Skip to content

Commit fc37493

Browse files
committed
fix: bug with old validate command, change to doctor
1 parent 79ccd4d commit fc37493

17 files changed

Lines changed: 471 additions & 27 deletions

.archcore/.sync-state.json

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3171,6 +3171,36 @@
31713171
"source": "plugin/codex-local-plugin-testing.guide.md",
31723172
"target": "plugin/bundled-cli-launcher.adr.md",
31733173
"type": "related"
3174+
},
3175+
{
3176+
"source": "plugin/cli-integration-tests.rule.md",
3177+
"target": "plugin/hooks-validation-system.spec.md",
3178+
"type": "implements"
3179+
},
3180+
{
3181+
"source": "plugin/cli-integration-tests.rule.md",
3182+
"target": "plugin/mcp-only-operations.rule.md",
3183+
"type": "related"
3184+
},
3185+
{
3186+
"source": "plugin/cli-integration-tests.rule.md",
3187+
"target": "plugin/plugin-testing.guide.md",
3188+
"type": "related"
3189+
},
3190+
{
3191+
"source": "plugin/cli-integration-tests.rule.md",
3192+
"target": "plugin/component-registry.doc.md",
3193+
"type": "related"
3194+
},
3195+
{
3196+
"source": "plugin/cli-integration-tests.rule.md",
3197+
"target": "plugin/plugin-architecture.spec.md",
3198+
"type": "related"
3199+
},
3200+
{
3201+
"source": "plugin/cli-integration-tests.rule.md",
3202+
"target": "plugin/bundled-cli-launcher.adr.md",
3203+
"type": "related"
31743204
}
31753205
]
31763206
}

.archcore/plugin/actualize-system.adr.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ tags:
1010

1111
## Context
1212

13-
The Archcore Claude Plugin has a comprehensive validation system (PreToolUse blocking, PostToolUse validation via `archcore validate`) that ensures **structural integrity** of `.archcore/` documents. The `/archcore:review` skill provides on-demand health checks for coverage gaps, relation health, and status issues.
13+
The Archcore Claude Plugin has a comprehensive validation system (PreToolUse blocking, PostToolUse validation via `archcore doctor`) that ensures **structural integrity** of `.archcore/` documents. The `/archcore:review` skill provides on-demand health checks for coverage gaps, relation health, and status issues.
1414

1515
However, no mechanism detects when documentation **content becomes stale**. Three types of staleness go undetected:
1616

.archcore/plugin/actualize-system.spec.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ Define the contract for the Actualize system — a 3-layer documentation freshne
1616

1717
This specification covers: the SessionStart staleness check (Layer 1), the PostToolUse cascade detection (Layer 2), and the `/archcore:actualize` intent skill (Layer 3). It defines their triggers, detection logic, output formats, and interaction with existing hooks and MCP tools.
1818

19-
It does not cover: structural validation (`archcore validate`), the `/archcore:review` skill (coverage/relation health), or the archcore-auditor agent (read-only background audits). Those are complementary but separate.
19+
It does not cover: structural validation (`archcore doctor`), the `/archcore:review` skill (coverage/relation health), or the archcore-auditor agent (read-only background audits). Those are complementary but separate.
2020

2121
## Authority
2222

@@ -350,7 +350,7 @@ Requirements:
350350
## Invariants
351351

352352
- SessionStart always loads context even if staleness check fails or is skipped.
353-
- PostToolUse validation (`archcore validate`) runs independently of cascade detection — both fire, neither depends on the other.
353+
- PostToolUse validation (`archcore doctor`) runs independently of cascade detection — both fire, neither depends on the other.
354354
- The actualize skill reads documents via MCP tools, never via direct file reads for `.archcore/` content.
355355
- Cascade detection never fires on `create_document` — only `update_document`.
356356
- No layer ever modifies documents autonomously — Layer 3 requires user confirmation.

.archcore/plugin/claude-plugin.prd.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ One subagent that covers all documentation scenarios:
8282

8383
- **PreToolUse (Write|Edit) — block** (`check-archcore-write`): If the target file matches `.archcore/**/*.md`, block the operation and return a message redirecting to the appropriate MCP tool.
8484
- **PreToolUse (Write|Edit) — inject** (`check-code-alignment`): If the target file is outside `.archcore/` and inside a configured source root, scan `.archcore/` for documents referencing the path and inject top-3 (by specificity → type priority) as `additionalContext`. Non-blocking by design.
85-
- **PostToolUse (MCP mutations) — validate** (`validate-archcore`): After `create_document` / `update_document` / `remove_document` / `add_relation` / `remove_relation`, run `archcore validate` and report issues.
85+
- **PostToolUse (MCP mutations) — validate** (`validate-archcore`): After `create_document` / `update_document` / `remove_document` / `add_relation` / `remove_relation`, run `archcore doctor` and report issues.
8686
- **PostToolUse (`update_document`) — cascade** (`check-cascade`): After document updates, list documents that reference the updated one via `implements` / `depends_on` / `extends` so the agent can review them for drift.
8787
- **SessionStart**: Loads project context at session start (document index + tags + relation count).
8888

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
---
2+
title: "CLI Integration Changes Require Strict Tests"
3+
status: accepted
4+
tags:
5+
- "hooks"
6+
- "plugin"
7+
- "rule"
8+
- "testing"
9+
- "validation"
10+
---
11+
12+
## Rule
13+
14+
Any change to plugin code that invokes the bundled `archcore` CLI — directly or through `bin/archcore` — MUST be accompanied by tests that assert the exact subcommand and arguments invoked. A passing test that did not verify *which* CLI subcommand ran does not satisfy this rule.
15+
16+
In particular:
17+
18+
1. Every shell-out from a `bin/*` script to the launcher (`"$LAUNCHER"` / `"$SCRIPT_DIR/archcore"`) MUST be covered by a unit test that asserts the invoked subcommand via the `MOCK_ARCHCORE_LOG` mechanism (see `mock_archcore_logging` in `test/helpers/common.bash`).
19+
2. Every `args` array in `.mcp.json` and `.codex.mcp.json`, and every subcommand in any new `hooks/*.json`-referenced script, MUST be covered by the allowlist guard in `test/structure/cli-contract.bats`.
20+
3. Every prescriptive `` `archcore <subcmd>` `` reference in `README.md` MUST be guarded by `test/structure/readme-cli-references.bats`.
21+
4. Skill or agent prose that instructs the agent to run `archcore <subcmd>` as a shell command MUST be reviewed against the canonical CLI surface and either pinned by an additional structure test or rewritten to delegate through the launcher (preferred).
22+
23+
A change is "covered" only when the test would fail if the code regressed to a phantom subcommand.
24+
25+
## Rationale
26+
27+
A real bug shipped because no test caught it: `bin/validate-archcore` invoked `archcore validate`, which is not a CLI subcommand (the canonical surface is `config | doctor | help | hooks | init | mcp | status | update`). The launcher returned exit 1 on every PostToolUse mutation, but the hook wraps the call in `|| true` and uses `timeout 2`, so production silently logged nothing while the test suite reported green — `mock_archcore` returned canned output regardless of the subcommand.
28+
29+
This is a structural class of failure, not a one-off:
30+
31+
- Hook scripts run with short timeouts and `|| true` error suppression. A wrong subcommand fails silently in production.
32+
- A test that asserts only `assert_success` is satisfied by the silent failure.
33+
- README and design docs that reference `archcore validate` look correct because the *string* is plausible and the CLI never prevented anyone from typing it.
34+
35+
Locking the contract at the test layer closes this gap so it cannot return through inattention or a CLI version bump.
36+
37+
The why-now: the bug was caught manually in Codex CLI, where hook output is more visible. We do not want to depend on accidental visibility for a contract this important.
38+
39+
## Examples
40+
41+
### Good
42+
43+
```bash
44+
# Unit test (test/unit/validate-archcore.bats):
45+
@test "validate-archcore calls archcore doctor (not validate)" {
46+
export MOCK_ARCHCORE_LOG="$BATS_TEST_TMPDIR/archcore.log"
47+
mock_archcore_logging "All checks passed ✓"
48+
run_with_fixture validate-archcore claude-code/mcp-create.json
49+
assert_success
50+
grep -qx 'doctor' "$MOCK_ARCHCORE_LOG" \
51+
|| fail "expected 'doctor', got: $(cat "$MOCK_ARCHCORE_LOG")"
52+
! grep -qx 'validate' "$MOCK_ARCHCORE_LOG" \
53+
|| fail "phantom subcommand 'validate' was invoked"
54+
}
55+
```
56+
57+
```bash
58+
# Structure test (test/structure/cli-contract.bats):
59+
ARCHCORE_SUBCOMMANDS="config doctor help hooks init mcp status update"
60+
61+
@test "bin/validate-archcore invokes only allowlisted subcommands" {
62+
local sub
63+
for sub in $(grep -oE '"\$LAUNCHER"[[:space:]]+[a-z][a-z0-9-]*' \
64+
"$PLUGIN_ROOT/bin/validate-archcore" \
65+
| sed -E 's/^"\$LAUNCHER"[[:space:]]+//'); do
66+
case " $ARCHCORE_SUBCOMMANDS " in
67+
*" $sub "*) ;;
68+
*) fail "phantom subcommand '$sub'" ;;
69+
esac
70+
done
71+
}
72+
```
73+
74+
### Bad
75+
76+
```bash
77+
# Mock that swallows any input — phantom subcommand passes silently.
78+
mock_archcore "All checks passed ✓"
79+
run_with_fixture validate-archcore claude-code/mcp-create.json
80+
assert_success # <-- meaningless; even `archcore unicorn` would pass
81+
```
82+
83+
```bash
84+
# Asserting only on the script's stdout. The launcher returns 1, the
85+
# hook swallows the error, the script prints nothing. Test passes.
86+
run_with_fixture validate-archcore claude-code/mcp-create.json
87+
assert_success
88+
assert_output ""
89+
```
90+
91+
```markdown
92+
<!-- README.md prose without a guarding test -->
93+
- **Validation** — runs `archcore some-future-name` after every document mutation
94+
```
95+
96+
## Enforcement
97+
98+
The rule is enforced by these tests, which already ship in the plugin:
99+
100+
- **`test/structure/cli-contract.bats`** — the allowlist guard. Scans `bin/*` scripts, `.mcp.json`, `.codex.mcp.json`, and `hooks/*.json`-referenced scripts; fails if any subcommand passed to the launcher is not in the canonical surface. Also ships a sentinel that fails on any executable reference to the historical phantoms `archcore validate` and `archcore sync`.
101+
- **`test/structure/readme-cli-references.bats`** — every code-quoted `` `archcore <subcmd>` `` in `README.md` must be allowlisted.
102+
- **`test/unit/validate-archcore.bats`** and **`test/unit/session-start.bats`** — invocation-log assertions using `MOCK_ARCHCORE_LOG`. Pattern is documented in `plugin-testing.guide.md` step 7.
103+
- **Live cross-check**`cli-contract.bats` parses `archcore --help` when the launcher resolves the binary and fails when the hardcoded allowlist drifts from the live surface.
104+
105+
When `bin/CLI_VERSION` bumps, the live cross-check signals which subcommands changed; the hardcoded allowlist must be updated in lockstep, and any new subcommand the plugin starts invoking gets its own invocation-log assertion before merge.
106+
107+
A change that does not satisfy this rule is rejected in code review. The rule applies to:
108+
109+
- Any script under `bin/` that calls `"$LAUNCHER"` or `archcore`
110+
- `.mcp.json` and `.codex.mcp.json` `args` arrays
111+
- Any new hook config (`hooks/*.json`) referencing a CLI-invoking script
112+
- README and other user-facing prescriptive docs naming `` `archcore <subcmd>` `` invocations
113+
- Skill or agent prompt text that instructs the agent to run an `archcore` shell command

.archcore/plugin/component-registry.doc.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ See the Bundled CLI Launcher ADR for rationale.
139139
| `bin/session-start` | SessionStart | Sources the normalizer, detects missing `.archcore/` and emits init guidance (instructs the agent to call `mcp__archcore__init_project`), otherwise invokes the local launcher with `ARCHCORE_SKIP_DOWNLOAD=1` to run `archcore hooks <host> session-start`, then calls `bin/check-staleness`. Always exits 0. |
140140
| `bin/check-archcore-write` | PreToolUse | Blocks direct Write/Edit to `.archcore/**/*.md` with exit 2 + stderr message redirecting to MCP tools. Allows `.archcore/settings.json` and `.archcore/.sync-state.json`. Allows all paths outside `.archcore/`. |
141141
| `bin/check-code-alignment` | PreToolUse | Injects relevant `.archcore/` context for source-file Write/Edit. Greps `.archcore/**/*.md` for documents referencing the edit path (directory prefixes, longest-first). Ranks by specificity → type priority (rule > cpat > adr > spec > guide). Emits top-3 via `hookSpecificOutput.additionalContext` (Claude Code/Codex/Copilot) or `additional_context` (Cursor). Never blocks; always exits 0. Honors `ARCHCORE_DISABLE_INJECTION=1` escape hatch and `.archcore/settings.json → codeAlignment.sourceRoots` override. |
142-
| `bin/validate-archcore` | PostToolUse | Runs `archcore validate` via the launcher after MCP document operations (by tool_name prefix). The legacy Write/Edit branch in the script is retained as defensive code but is never reached from the current hooks config. Outputs JSON `hookSpecificOutput` when issues found, empty otherwise. Silently exits 0 if the launcher/CLI is unavailable. Always exits 0. |
142+
| `bin/validate-archcore` | PostToolUse | Runs `archcore doctor` via the launcher after MCP document operations (by tool_name prefix). The script's subcommand invocation is locked by `test/structure/cli-contract.bats` and a `MOCK_ARCHCORE_LOG`-backed assertion in `test/unit/validate-archcore.bats` against the canonical CLI surface (sourced from `bin/CLI_VERSION`). The legacy Write/Edit branch in the script is retained as defensive code but is never reached from the current hooks config. Outputs JSON `hookSpecificOutput` when issues found, empty otherwise. Silently exits 0 if the launcher/CLI is unavailable. Always exits 0. |
143143
| `bin/check-staleness` | SessionStart (called by `bin/session-start`) | Detects code-document drift via git: finds source files changed since the last `.archcore/` commit, cross-references with documents that mention affected directories. Rate-limited to once per 24h via a timestamp file (`$CLAUDE_PLUGIN_DATA/archcore/last-staleness`, with XDG/HOME fallbacks). Emits only when matching documents exist — no generic "N files changed" fallback. Outputs plain text warning (max 2KB) or empty. Always exits 0. |
144144
| `bin/check-cascade` | PostToolUse | After `update_document`, queries `.sync-state.json` relation graph for documents connected via `implements`, `depends_on`, or `extends` to the updated document. Outputs JSON `hookSpecificOutput` listing potentially stale dependents, or empty if no cascade. Always exits 0. |
145145
| `bin/check-precision` | PostToolUse | Phase 1 of the Precision Initiative. After `create_document` and `update_document`, reads the resulting file from disk and runs four checks: forbidden vagueness lexicon (hardcoded list mirroring `skills/_shared/precision-rules.md`), mandatory sections by type (adr/rule/spec/guide/rfc), frontmatter title+status presence, body length ≥200 chars. Emits soft warnings via `additionalContext`. Always exits 0 (never blocks). See `precision-over-coverage.adr.md`. |
@@ -149,7 +149,7 @@ See the Bundled CLI Launcher ADR for rationale.
149149
| Component | Location | Tests | Description |
150150
| --------------- | ---------------------------- | -------- | ------------------------------------------------------------------------------------------------------- |
151151
| Unit tests | `test/unit/` | 94+ | Test each bin script: stdin parsing, host detection, exit codes, output format, edge cases. Includes `launcher.bats` (CLI launcher resolution order), `check-staleness.bats` (24h rate limit, corrupt-stamp recovery), and `check-code-alignment.bats` (source-root filter, specificity ranking, top-3 cap, settings override, Cursor JSON shape, non-blocking safety). |
152-
| Structure tests | `test/structure/` | 50+ | Validate JSON configs, skill frontmatter, agent frontmatter, hook references, script permissions, rules. `hooks.bats` includes Phase 2.1 anti-regression invariants: no Write/Edit matcher on PostToolUse, no postToolUse event on Cursor, exact event-set invariants per host. `codex-plugin.bats` enforces Codex manifest, marketplace schema, hooks shape, MCP wiring, TOML agents, and parity between `commands/*.md` wrappers and `skills/<name>/SKILL.md`. |
152+
| Structure tests | `test/structure/` | 50+ | Validate JSON configs, skill frontmatter, agent frontmatter, hook references, script permissions, rules. `hooks.bats` includes Phase 2.1 anti-regression invariants: no Write/Edit matcher on PostToolUse, no postToolUse event on Cursor, exact event-set invariants per host. `codex-plugin.bats` enforces Codex manifest, marketplace schema, hooks shape, MCP wiring, TOML agents, and parity between `commands/*.md` wrappers and `skills/<name>/SKILL.md`. `cli-contract.bats` locks every `archcore <subcmd>` invocation in `bin/*` and MCP-launcher JSONs against an allowlist sourced from `bin/CLI_VERSION`; `readme-cli-references.bats` does the same for prescriptive README references. |
153153
| Fixtures | `test/fixtures/stdin/` | 12 files | Mock stdin JSON for Claude Code, Cursor, Copilot, Codex CLI, and malformed inputs |
154154
| Helpers | `test/helpers/` || common.bash (setup, mocks, timeout shim), bats-support, bats-assert (git submodules) |
155155
| Makefile | `Makefile` || Targets: `test`, `test-unit`, `test-structure`, `lint`, `check-json`, `check-perms`, `verify` |

0 commit comments

Comments
 (0)