Skip to content

Scaffold the mod in the plugin with its dev and test loop #202

Closed

Description

@ToRvaLDz

Parent: 🗺 trailhead mod

Question

Scaffold the mod inside plugins/trailhead and the dev loop around it, with no user-visible behaviour yet:

  • the module wired per the packaging finding (hooks/hooks.json modules or the shape research settles), a register that loads and does nothing visible, typed against claude-code, its $.state contract in types/index.d.ts;
  • claude plugin validate plugins/trailhead clean, a first *.test.ts run by claude plugin test, wired into the repo's test command;
  • a documented local loop (claude --plugin-dir plugins/trailhead or CLAUDE_CODE_PLUGIN_DIRS) to try changes with hot reload;
  • the installer and Codex projection handle the module as research found (shipped for Claude, skipped for Codex), with their tests updated.
    Done when a session loads the plugin with the module and the command hooks both active, and Codex installs unchanged.

Blocked by

Activity

  1. self-assigned this
    on Oct 10, 2026
  2. ToRvaLDz commented on Oct 10, 2026

    @ToRvaLDz
    OwnerAuthor

    PLAN

    Target branch: feat/mod (the map's integration branch, created from main). Tested build: Claude Code 2.1.296.

    Steps

    1. Contract + RED test. Add plugins/trailhead/types/index.d.ts: export type WorkState = { map: number | null; ticket: number | null; title: string | null; step: string | null; auto: boolean; session: string | null; updatedAt: string | null } (the Where does the mod read the work state from? #200 shape) and declare module 'claude-code' { interface PluginState { trailhead: { workState: WorkState | null } } }. Name it in plugin.json as "types": "./types/index.d.ts". Add plugins/trailhead/tests/mod.test.ts (kit claude-code/testing): the plugin loads with no skipped hook, trailhead.workState reads null by default, and a session.start pass leaves no status/toast. Run claude plugin test plugins/trailhead: RED (no module yet).
    2. Module (GREEN). plugins/trailhead/hooks/mod/register.ts: export const register: Register = on => { ... } typed against claude-code, one pass-through hook (session.start -> next(e)) and the workState atom declared against the contract; nothing drawn, no status, no toast. Add "modules": ["./mod/register.ts"] to plugins/trailhead/hooks/hooks.json next to the unchanged hooks. claude plugin validate plugins/trailhead clean; claude plugin test GREEN. Type-check with tsc against the engine's laid types (a plugins/trailhead/tsconfig.json extending .claude-plugin/types/tsconfig.json, only if the engine does not supply one); gitignore plugins/trailhead/.claude-plugin/types/ (engine-generated at every --plugin-dir load).
    3. Repo test command. The repo has none today (each *.test.js runs standalone). Add scripts/test.js (outside the npm files) + "scripts": { "test": "node scripts/test.js" } in package.json: runs every *.test.js under bin/ and plugins/trailhead/hooks/ (skipping .claude/worktrees), then claude plugin validate plugins/trailhead and claude plugin test plugins/trailhead; when claude is not on PATH the two mod checks are reported skipped, not failed. Exit non-zero on any failure.
    4. Installer + Codex pinned by tests (no installer code change). Research Can the core plugin ship a mod alongside its command hooks? #199: neither channel reads the plugin's hooks.json; hook scripts are copied by allowlist. Add assertions: in bin/trailhead.test.js, a --codex install contains no register.ts anywhere under the codex dir and its hooks.json has no modules key; a --claude npm install copies no hooks/mod/ and writes no modules into settings.json (npm delivery of the mod is map fog item 14, out of this ticket). A source-side check that plugins/trailhead/hooks/hooks.json declares exactly one module whose file exists.
    5. Dev loop doc. plugins/trailhead/hooks/mod/README.md (contributor note, not user docs): claude --plugin-dir plugins/trailhead (or CLAUDE_CODE_PLUGIN_DIRS) with hot reload on save, claude plugin validate, claude plugin test, npm test, claude --debug for the hooks module trailhead@inline loaded line, the 2.1.287 minimum and that command hooks stay the floor. User-facing READMEs/docs are fog item 13.
    6. Done check. Headless claude -p --plugin-dir plugins/trailhead --debug-file <tmp> shows both hooks module trailhead@inline loaded and a trailhead command hook (trailhead-check-update.js) finishing; node bin/trailhead.js --codex --dir=<tmp> output unchanged vs main (same file set).

    Seams (TDD: seams)

    • Module load + $.state contract: tests/mod.test.ts via claude plugin test.
    • Installer/projection boundary: bin/trailhead.test.js assertions.

    Files touched

    plugins/trailhead/hooks/hooks.json, plugins/trailhead/hooks/mod/register.ts, plugins/trailhead/hooks/mod/README.md, plugins/trailhead/types/index.d.ts, plugins/trailhead/tests/mod.test.ts, plugins/trailhead/.claude-plugin/plugin.json, package.json, scripts/test.js, bin/trailhead.test.js, .gitignore.

    Verification criteria

    • claude plugin validate plugins/trailhead passes and lists the module's hooks.
    • claude plugin test plugins/trailhead passes; npm test passes end to end.
    • A headless session loads the module and runs the command hooks together (debug log).
    • Codex install file set identical to main; no modules, no .ts in the Codex tree.
    • No user-visible behaviour (no status, band, toast).
  3. ToRvaLDz commented on Oct 10, 2026

    @ToRvaLDz
    OwnerAuthor

    PLAN (revised after cross-AI review)

    Target branch: feat/mod (the map's integration branch, created from main). Tested build: Claude Code 2.1.296.

    Steps

    1. Contract + RED test. Add plugins/trailhead/types/index.d.ts: export type WorkState = { map: number | null; ticket: number | null; title: string | null; step: string | null; auto: boolean; session: string | null; updatedAt: string | null } (the Where does the mod read the work state from? #200 shape) and declare module 'claude-code' { interface PluginState { trailhead: { workState: WorkState | null } } }. Name it in plugin.json as "types": "./types/index.d.ts". Add plugins/trailhead/tests/mod.test.ts (kit claude-code/testing): the plugin loads with no skipped hook, (the state default is checked by validate, see Seams). Run claude plugin test plugins/trailhead: RED (no module yet).
    2. Module (GREEN). plugins/trailhead/hooks/mod/register.ts: export const register: Register = on => { ... } typed against claude-code, one pass-through hook (session.start -> next(e)) and the workState atom declared against the contract; nothing drawn, no status, no toast. Add "modules": ["./mod/register.ts"] to plugins/trailhead/hooks/hooks.json next to the unchanged hooks. claude plugin validate plugins/trailhead clean; claude plugin test GREEN. The types manifest field and the PluginState augmentation are confirmed on 2.1.296 by a scratch spike: claude plugin validate reports types ./types/index.d.ts declares state: trailhead.workState and state reads: trailhead.workState. Typing gate = import type { Register } from 'claude-code' + validate holding every $.state key to the contract. A full tsc pass is not a test gate: the repo has zero npm dependencies and the engine lays .claude-plugin/types/ only in an interactive --plugin-dir session (a headless -p run lays nothing, verified); the dev-loop note documents it as an optional step (npx -p typescript@5.9.3 tsc -p plugins/trailhead after an interactive load, using the engine-supplied tsconfig). Gitignore plugins/trailhead/.claude-plugin/types/ and any engine-written plugins/trailhead/tsconfig.json. The pass-through session.start hook is kept on purpose: it is the observable proof of load (debug log events: session.start, and the test's bottom hook is reached through it); it changes nothing.
    3. Repo test command. The repo has none today (each *.test.js runs standalone). Add scripts/test.js (outside the npm files) + "scripts": { "test": "node scripts/test.js" } in package.json: runs every *.test.js under bin/ and plugins/trailhead/hooks/ (skipping .claude/worktrees), then claude plugin validate plugins/trailhead and claude plugin test plugins/trailhead; when claude is not on PATH the run fails unless the explicit opt-out TRAILHEAD_SKIP_MOD_CHECKS=1 is set (then reported skipped). Exit non-zero on any failure. (CI runs no plugin tests today, per the codebase map; adding a CI job is out of this ticket.)
    4. Installer + Codex pinned by tests (no installer code change). Research Can the core plugin ship a mod alongside its command hooks? #199: neither channel reads the plugin's hooks.json; hook scripts are copied by allowlist. Add assertions: in bin/trailhead.test.js, a --codex install contains no register.ts anywhere under the codex dir and its hooks.json has no modules key; a --claude npm install copies no hooks/mod/ and writes no modules into settings.json (npm delivery of the mod is map fog item 14, out of this ticket). A source-side check that plugins/trailhead/hooks/hooks.json declares exactly one module whose file exists.
    5. Dev loop doc. plugins/trailhead/hooks/mod/README.md (contributor note, not user docs): claude --plugin-dir plugins/trailhead (or CLAUDE_CODE_PLUGIN_DIRS) with hot reload on save, claude plugin validate, claude plugin test, npm test, claude --debug for the hooks module trailhead@inline loaded line, the 2.1.287 minimum and that command hooks stay the floor. User-facing READMEs/docs are fog item 13.
    6. Done check. Headless claude -p --plugin-dir plugins/trailhead --debug-file <tmp> shows both hooks module trailhead@inline loaded and a trailhead command hook (trailhead-check-update.js) finishing; node bin/trailhead.js --codex --dir=<tmp> tree identical to main's: baseline from a temporary git worktree of main installed into its own temp dir, compared on relative path, file type (file/symlink), mode bits and content hash.

    Seams (TDD: seams)

    • Module load: tests/mod.test.ts via claude plugin test (the test $ has no state noun, verified, so the test raises session.start through $ and asserts the bottom hook receives the input unchanged and the result is echoed; verified in the spike that a module that fails to parse/load fails claude plugin test with hooks module did not load, so the test is a real load gate; the $.state contract is held by validate).
    • Installer/projection boundary: bin/trailhead.test.js assertions.

    Files touched

    plugins/trailhead/hooks/hooks.json, plugins/trailhead/hooks/mod/register.ts, plugins/trailhead/hooks/mod/README.md, plugins/trailhead/types/index.d.ts, plugins/trailhead/tests/mod.test.ts, plugins/trailhead/.claude-plugin/plugin.json, package.json, scripts/test.js, bin/trailhead.test.js, .gitignore.

    Verification criteria

    • claude plugin validate plugins/trailhead passes and reports ./mod/register.ts hooks: session.start and types ... declares state: trailhead.workState.
    • claude plugin test plugins/trailhead passes; npm test passes end to end.
    • A headless session loads the module and runs the command hooks together (debug log).
    • Codex install tree identical to main (paths + content hashes); no modules, no .ts in the Codex tree.
    • No user-visible behaviour (no status, band, toast).
  4. ToRvaLDz commented on Oct 10, 2026

    @ToRvaLDz
    OwnerAuthor

    PLAN-REVIEW

    Reviewer: codex (codex exec -s read-only), 2 rounds. Final round: no blocking concerns.

    Concern Round 1 tag Resolution
    npm test passes silently when claude is absent blocking The run fails without claude unless TRAILHEAD_SKIP_MOD_CHECKS=1 is set explicitly. A CI job is out of this ticket (CI runs no plugin tests today).
    tsc check not reproducible blocking Dropped as a gate: zero npm deps, and the engine lays .claude-plugin/types/ only in an interactive --plugin-dir session (headless lays nothing, verified). Typing gate = Register import + claude plugin validate holding $.state keys to the contract; tsc documented as an optional dev step.
    types field / PluginState augmentation unconfirmed blocking Confirmed on 2.1.296 by a scratch spike (validate reports the declared state and the module's reads).
    Pass-through session.start hook unnecessary non-blocking Kept on purpose: it is the observable load proof (debug log events: session.start; the test reaches its bottom hook through it). A module that fails to load fails claude plugin test (verified).
    Codex file-set comparison weak non-blocking Compare path, file type, mode bits and content hash against a baseline installed from a temporary worktree of main.
    validate criterion vs evidence non-blocking (round 2) Criterion aligned to the lines validate actually prints.
  5. ToRvaLDz commented on Oct 10, 2026

    @ToRvaLDz
    OwnerAuthor

    status: clean
    critical: 0 · warning: 2 · info: 4

    VERIFY

    Tests / plan criteria. npm test: 22/22 (20 *.test.js suites, claude plugin validate, claude plugin test); installer suite 709 assertions. claude plugin validate plugins/trailhead reports ./mod/register.ts hooks: session.start and types ./types/index.d.ts declares state: trailhead.workState. Headless claude -p --plugin-dir plugins/trailhead (2.1.296) logs hooks module trailhead@inline loaded (...); events: session.start and trailhead-check-update.js (plugin trailhead@inline) finished with status 0 in the same session. Codex install from this branch vs a temporary worktree of main: 120 files each, identical on path, type, mode and sha256 (only hooks.json embeds the absolute install dir; identical once normalised), no modules key, no .ts.

    Code review (ticket commits 8f90437, 22b1aae, 935f859, bd71a75, 96d3626, 21044bb, 78b5b61, 8dbd2af).

    Ledger:

    • W1 fixed (8dbd2af): the mod test cannot tell a passthrough hook from no hook; renamed to what it proves (the module loads, session.start unchanged). A module that fails to load fails it (verified).
    • W2 fixed (21044bb): npm test now fails when a test root yields no suites.
    • I1 fixed (78b5b61): dropped the vacuous "no modules in settings.json" assertion.
    • I2 fixed (78b5b61): pinned that hooks.json keeps its command hooks (PreToolUse, PostToolUse, SessionStart) beside the module.
    • I3 accepted: hooks/mod/, tests/, types/ ship in the npm tarball; harmless (installer copies by allowlist, verified) and needed for the later npm delivery (map fog 14).
    • I4 accepted: no CI runs npm test; CI was out of this ticket (CI runs no plugin tests today); belongs with the release work (map fog 13).

    Goal-backward verification: status: met.

    • Module wired via hooks/hooks.json modules: ["./mod/register.ts"], command hooks unchanged: met (22b1aae).
    • register loads, nothing visible (one session.start passthrough, no status/band/toast): met (22b1aae).
    • Typed against claude-code (Register), $.state contract in types/index.d.ts with the Where does the mod read the work state from? #200 shape, named in plugin.json types: met (8f90437).
    • validate clean, first *.test.ts under claude plugin test, wired into npm test (fails without claude unless TRAILHEAD_SKIP_MOD_CHECKS=1): met (8f90437, 935f859, 21044bb).
    • Documented local loop with hot reload: met (96d3626, plugins/trailhead/hooks/mod/README.md).
    • Installer and Codex projection: no code change, pinned by tests: met (bd71a75, 78b5b61).
    • Done: a session loads module + command hooks together; Codex installs unchanged: met (evidence above).

    Acceptance testing: not applicable (no user-facing change).

  6. ToRvaLDz commented on Oct 10, 2026

    @ToRvaLDz
    OwnerAuthor

    Resolution. The mod scaffold is in the core plugin on feat/mod, with no user-visible behaviour.

    • plugins/trailhead/hooks/hooks.json declares modules: ["./mod/register.ts"] beside the unchanged command hooks; hooks/mod/register.ts registers a session.start passthrough, typed against claude-code (8f90437, 22b1aae).
    • $.state contract trailhead.workState (the Where does the mod read the work state from? #200 shape) in plugins/trailhead/types/index.d.ts, named in plugin.json types (8f90437).
    • npm test (scripts/test.js, new): every *.test.js suite, then claude plugin validate and claude plugin test; fails without claude unless TRAILHEAD_SKIP_MOD_CHECKS=1, and fails when a test root yields no suites (935f859, 21044bb).
    • Installer and Codex projection unchanged, pinned by tests: no .ts/modules in a Codex install, no hooks/mod/ in a Claude npm install, command hooks kept beside the module (bd71a75, 78b5b61).
    • Dev loop documented in plugins/trailhead/hooks/mod/README.md (--plugin-dir hot reload, validate, test, debug log, optional tsc) (96d3626).

    Verified on Claude Code 2.1.296: one headless session loads the module and runs the command hooks; Codex install identical to main. Review ledger and goal check: VERIFY.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

trailhead:buildtype: construct — discuss/plan/execute/verifytrailhead:map-198Tickets of map #198 (trailhead mod)trailhead:ticketa trailhead ticket

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions