Skip to content

Add CI symbol guard for critical hook files#105

Merged
virtualian merged 1 commit intomainfrom
ci/hook-symbol-guard
Apr 13, 2026
Merged

Add CI symbol guard for critical hook files#105
virtualian merged 1 commit intomainfrom
ci/hook-symbol-guard

Conversation

@virtualian
Copy link
Copy Markdown
Owner

Summary

Adds .github/workflows/hook-symbol-guard.yml — a minimal CI job that fails PRs touching Releases/*/.claude/hooks/** if either of the following is true:

  1. bun build --target=bun fails on any of the critical hook files (RatingCapture.hook.ts, learning-readback.ts, paths.ts, LoadContext.hook.ts).
  2. Any of the named symbols below is missing from the file it's expected in.

Motivation

PR #101 squash-merged a four-commit change into main whose bundled commit ebc8130 silently deleted ~606 lines of unrelated functionality (CorrectionMode fast-path, BehavioralSignal capture, loadLatestSynthesis). PR #104 restored those files, but nothing structural prevents the same class of failure from happening again.

This workflow is the structural fix: a deletion of any guarded symbol — whether by a human, an agent, or an IDE refactor — fails the PR with a clear message before review.

Guarded symbols

File Symbols
RatingCapture.hook.ts CorrectionMode, BehavioralSignal, BEHAVIORAL_FEEDBACK_STATE
lib/learning-readback.ts loadLatestSynthesis, loadBehavioralTrends, loadLearningDigest, loadWisdomFrames, loadFailurePatterns, loadSignalTrends
lib/paths.ts getConfigDir, getPaiDir, configPath, codePath

13 symbol checks total.

How to remove a symbol legitimately

If a future PR needs to remove a guarded symbol on purpose, the contract is: update this workflow in the same PR to remove the symbol from the check list, and justify the removal in the PR description. The grep check is deliberately simple so this escape hatch is obvious.

Scope and cost

  • 1 file, +86 lines, no runtime dependency.
  • Path-scoped trigger — only fires on PRs touching the release hooks tree or this workflow itself, so unrelated PRs don't pay the CI cost.
  • No secrets required. No external network calls except actions/checkout@v4 and oven-sh/setup-bun@v2.
  • Total expected CI time: under 45 seconds.

Self-test evidence (from local working tree on fresh origin/main)

  • bun yaml parse on the workflow file: valid, 4 steps detected.
  • All 13 symbol checks pass on current origin/main.
  • bun build --target=bun clean on all 4 critical files.

Known limitations

  • Releases/v4.0.3+/ is hard-coded. When a future Releases/v4.0.4+/ tree appears, this workflow will need updating. That's an intentional simplification — much better to maintain a short explicit list than to ship a clever-but-brittle glob.
  • Symbol checks are grep-based, not AST-based. A symbol that survives only inside a comment would pass the check. Acceptable tradeoff for this use case: the intent is to catch accidental deletion, and an accidentally-commented-out critical symbol would also be caught by the bun build step when its call sites break.

Fail PRs that touch Releases/*/.claude/hooks/** and either fail
to bun-build the critical hook files or drop one of the named
symbols that a prior PR (#101 -> #104) silently deleted.

Guarded symbols:
- RatingCapture: CorrectionMode, BehavioralSignal,
  BEHAVIORAL_FEEDBACK_STATE
- learning-readback: loadLatestSynthesis, loadBehavioralTrends,
  loadLearningDigest, loadWisdomFrames, loadFailurePatterns,
  loadSignalTrends
- paths.ts: getConfigDir, getPaiDir, configPath, codePath

The grep-based check is deliberately simple: if a PR legitimately
needs to remove a guarded symbol, update this workflow in the same
PR and justify the removal in the description.

Build step additionally runs bun build --target=bun on each file to
catch module-resolution regressions (missing imports, renamed exports)
before they reach main.

Path scoping: triggers only on PRs touching the release hooks tree
or this workflow itself, so unrelated PRs don't pay the CI cost.
@virtualian virtualian merged commit 624e015 into main Apr 13, 2026
1 check passed
@virtualian virtualian deleted the ci/hook-symbol-guard branch April 13, 2026 15:13
virtualian added a commit that referenced this pull request Apr 13, 2026
Supersedes PR #103. Incorporates the 4 correction commits from
branch 100-claude-config-dir-pai-dir-separation (b3e4e49, 42f70b4,
39ea6b1, c7f304c) as a single focused refactor on top of the
CLAUDE_CONFIG_DIR + PAI_DIR split that landed via #101 and the
CorrectionMode + loadLatestSynthesis restore that landed via #104.

Path separation fixes
---------------------
- settings.json: revert 13 hook commands from CLAUDE_CONFIG_DIR back
  to PAI_DIR (the hook-root inversion originally introduced by ebc8130)
- paths.ts: getHooksDir() returns codePath('hooks') not configPath('hooks'),
  docstring rewrites for the two-root contract
- LoadContext.hook.ts: eliminate paiDir parameter chain across 5 helpers,
  route 5 raw join(paiDir, ...) calls through codePath(), rename
  misleading loadSettings param, extract sessionProgressPath
- UpdateCounts.ts: countHooks and refreshUsageCache now read MEMORY from
  the PAI root instead of the CONFIG root
- RelationshipMemory.hook.ts: ensureRelationshipDir fixed for the split
- LastResponseCache.hook.ts: mkdirSync before writeFileSync (silent
  ENOENT on fresh install)
- prd-utils.ts: correct replace() prefix from configPath to codePath
- VoiceCompletion.hook.ts: replace hardcoded ~/.claude/settings.json
  with getSettingsPath()
- VoiceNotification.ts, tab-setter.ts: drop dead configPath imports
- DocCrossRefIntegrity.ts, change-detection.ts, identity.ts,
  notifications.ts, SessionCleanup.hook.ts, KittyEnvPersist.hook.ts,
  WorkCompletionLearning.hook.ts: small codePath / configPath cleanups
- .pai-protected.json: correct stale comment

Deprecated alias deletion
-------------------------
- paths.ts: remove the paiPath() function entirely. The semantic flip
  in v4.0.3 (pre-split ~/.claude/x -> post-split ~/.pai/x) is exactly
  the class of change where a silent alias is a landmine, not a safety
  net. The compiler now catches any revenant caller. Zero code callers
  existed when this commit was written.
- SYSTEM_USER_EXTENDABILITY.md: update 4 code examples from paiPath()
  to codePath() so the documentation matches the public API.

MEMORY migration release note (manual upgrade path)
---------------------------------------------------
- Releases/v4.0.3+/README.md: add a "Breaking Changes" subsection
  explaining the two-root split and add the required MEMORY migration
  step (`mv ~/.claude/MEMORY ~/.pai/MEMORY`) to the "Upgrading from
  v4.0.x" recipe. An automated migrator is a follow-up - see the
  tracking issue.

Deferred (intentional)
----------------------
- statusLine.command still points at ${CLAUDE_CONFIG_DIR}/statusline-command.sh
  because the physical file currently lives at ~/.claude/statusline-command.sh.
  Relocating the script is a separate decision that does not belong in
  this refactor.

Verification
------------
- bun build --target=bun clean on all 17 touched hook TypeScript files
- settings.json parses as valid JSON
- env-var resolution tested under CLAUDE_CONFIG_DIR=/tmp/ctest PAI_DIR=/tmp/ptest:
  10/10 path primitives resolve to the expected roots
- settings.json hook-command audit: 0 references to CLAUDE_CONFIG_DIR/hooks,
  24 references to PAI_DIR/hooks
- MEMORY routing audit: 0 configPath('MEMORY'), 45 codePath('MEMORY'),
  0 raw ~/.claude/MEMORY references in hook sources
- CI symbol guard (merged in #105) passes all 13 required-symbol checks
  against this commit
virtualian added a commit that referenced this pull request Apr 13, 2026
)

Supersedes PR #103. Incorporates the 4 correction commits from
branch 100-claude-config-dir-pai-dir-separation (b3e4e49, 42f70b4,
39ea6b1, c7f304c) as a single focused refactor on top of the
CLAUDE_CONFIG_DIR + PAI_DIR split that landed via #101 and the
CorrectionMode + loadLatestSynthesis restore that landed via #104.

Path separation fixes
---------------------
- settings.json: revert 13 hook commands from CLAUDE_CONFIG_DIR back
  to PAI_DIR (the hook-root inversion originally introduced by ebc8130)
- paths.ts: getHooksDir() returns codePath('hooks') not configPath('hooks'),
  docstring rewrites for the two-root contract
- LoadContext.hook.ts: eliminate paiDir parameter chain across 5 helpers,
  route 5 raw join(paiDir, ...) calls through codePath(), rename
  misleading loadSettings param, extract sessionProgressPath
- UpdateCounts.ts: countHooks and refreshUsageCache now read MEMORY from
  the PAI root instead of the CONFIG root
- RelationshipMemory.hook.ts: ensureRelationshipDir fixed for the split
- LastResponseCache.hook.ts: mkdirSync before writeFileSync (silent
  ENOENT on fresh install)
- prd-utils.ts: correct replace() prefix from configPath to codePath
- VoiceCompletion.hook.ts: replace hardcoded ~/.claude/settings.json
  with getSettingsPath()
- VoiceNotification.ts, tab-setter.ts: drop dead configPath imports
- DocCrossRefIntegrity.ts, change-detection.ts, identity.ts,
  notifications.ts, SessionCleanup.hook.ts, KittyEnvPersist.hook.ts,
  WorkCompletionLearning.hook.ts: small codePath / configPath cleanups
- .pai-protected.json: correct stale comment

Deprecated alias deletion
-------------------------
- paths.ts: remove the paiPath() function entirely. The semantic flip
  in v4.0.3 (pre-split ~/.claude/x -> post-split ~/.pai/x) is exactly
  the class of change where a silent alias is a landmine, not a safety
  net. The compiler now catches any revenant caller. Zero code callers
  existed when this commit was written.
- SYSTEM_USER_EXTENDABILITY.md: update 4 code examples from paiPath()
  to codePath() so the documentation matches the public API.

MEMORY migration release note (manual upgrade path)
---------------------------------------------------
- Releases/v4.0.3+/README.md: add a "Breaking Changes" subsection
  explaining the two-root split and add the required MEMORY migration
  step (`mv ~/.claude/MEMORY ~/.pai/MEMORY`) to the "Upgrading from
  v4.0.x" recipe. An automated migrator is a follow-up - see the
  tracking issue.

Deferred (intentional)
----------------------
- statusLine.command still points at ${CLAUDE_CONFIG_DIR}/statusline-command.sh
  because the physical file currently lives at ~/.claude/statusline-command.sh.
  Relocating the script is a separate decision that does not belong in
  this refactor.

Verification
------------
- bun build --target=bun clean on all 17 touched hook TypeScript files
- settings.json parses as valid JSON
- env-var resolution tested under CLAUDE_CONFIG_DIR=/tmp/ctest PAI_DIR=/tmp/ptest:
  10/10 path primitives resolve to the expected roots
- settings.json hook-command audit: 0 references to CLAUDE_CONFIG_DIR/hooks,
  24 references to PAI_DIR/hooks
- MEMORY routing audit: 0 configPath('MEMORY'), 45 codePath('MEMORY'),
  0 raw ~/.claude/MEMORY references in hook sources
- CI symbol guard (merged in #105) passes all 13 required-symbol checks
  against this commit
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