Skip to content

fix: anticipate.sh never finds its own modules on a real install (L4 ships inert) - #114

Open
ObiWanKenobi wants to merge 1 commit into
stuinfla:mainfrom
ObiWanKenobi:fix/anticipate-module-resolution
Open

fix: anticipate.sh never finds its own modules on a real install (L4 ships inert)#114
ObiWanKenobi wants to merge 1 commit into
stuinfla:mainfrom
ObiWanKenobi:fix/anticipate-module-resolution

Conversation

@ObiWanKenobi

Copy link
Copy Markdown

What

plugin/scripts/anticipate.sh derives its module directory by assertion:

CODE_ROOT=$(CDPATH='' cd -- "$SELF_DIR/../.." 2>/dev/null && pwd)
GOAL_MATCH="${RUVNET_GOAL_MATCH:-$CODE_ROOT/scripts/goal-match.mjs}"

../.. is correct only for the source layout <root>/plugin/scripts/. Both layouts a user actually receives flatten the plugin/ level, so it overshoots by one directory:

anticipate.sh at ../.. resolves to matcher found?
<plugin-cache>/ruvnet-brain/<ver>/scripts/ <plugin-cache>/ruvnet-brain
~/.cache/ruvnet-brain/versions/<gen>/scripts/ .../versions
<src>/plugin/scripts/ <src>

[ -f "$GOAL_MATCH" ] || exit 0 then fires and the entire L4 anticipatory surface is disabled. Measured on a real install — identical stdin and registry, only the directory differs:

console-runtime layout (keeps plugin/)   -> SPEAKS
installed plugin layout                  -> SILENT
installed plugin + explicit module paths -> SPEAKS

The script's own comment asserts that a dev checkout and the Spine "both keep scripts/ as a sibling of plugin/". On a real install, neither does.

Why it survived the test suite

tests/integration/anticipate.test.mjs is 678 lines and thorough, but every run injects the path:

...(matcher ? { RUVNET_GOAL_MATCH: matcher } : {}),

so the default resolution has no coverage. Even "exits 0 and stays quiet when the matcher module does not exist" passes an explicit path to a missing file. The only symptom is silence — which is also the correct behaviour on the overwhelming majority of prompts — so nothing ever looked wrong.

This is the fifth instance of the class already documented in tests/unit/installer-sibling-imports-packaged.test.mjs:

A capability that silently does not exist on real installs is worse than one that was never built, because the team believes it is running and stops looking.

Same shape, different file. That gate derives its list from bin/install.mjs's imports, so it does not cover anticipate.sh's.

The fix

Resolve by probing the filesystem rather than asserting a shape: try each candidate directory, take the first that actually holds the matcher. If none does, fall through to the historical path so existing diagnostics still name the expected location, and keep the no-node fast path intact. Silence on doubt, never a guess.

Test

Adds tests/integration/anticipate-module-resolution.test.mjs, which sets no module env vars — that omission is the point.

  • Fails on the shipped-layout case without this change; passes with it (verified both directions).
  • Re-asserts the silence contract on the same reachable modules, so a "make it speak" fix cannot become a nag.
  • Keeps the documented degradation: modules genuinely absent → exit 0, no output.
tests/integration/anticipate-module-resolution.test.mjs   5 passed
tests/integration/anticipate.test.mjs
tests/integration/anticipate-dial.test.mjs
tests/unit/goal-match.test.mjs
tests/unit/capability-registry.test.mjs                 159 passed

Full tests/unit shows 7 failures — all present on unmodified main (verified on a clean tree), all in session-start / brain-off / star-ask / hook-registry-lint, none touching this path. One is self-labelled expected-red per ADR-055 appendix B.

Not addressed here — packaging is the other half

This change alone does not make L4 speak on a real install. goal-match.mjs, capability-registry.mjs and advocacy-outcomes.mjs live only in the repo-root scripts/, which the plugin directory does not carry, so the resolver still finds nothing until they ship under plugin/.

I did not guess at that, because it interacts with how the plugin is assembled for the marketplace and with the root-scripts/ importers (console-engine.mjs, the test suites). Happy to follow up with whichever mechanism you prefer — moving them under plugin/scripts/, or a copy step during assembly.

Worth noting separately: goal-match.mjs itself is in good shape. Against an adversarial table of 20 negatives borrowing every goal's vocabulary it produced zero false alarms, and it recognised all 10 goals on corroborated prompts. The two-key INTENT+SUBJECT rule does what its header claims — this is purely a delivery problem, not a matcher problem.

…ped inert)

anticipate.sh derived its module directory by assertion:

    CODE_ROOT=$SELF_DIR/../..   ->   $CODE_ROOT/scripts/goal-match.mjs

That is correct only for the source layout <root>/plugin/scripts/. Both
layouts a user actually receives flatten the `plugin/` level -- the Claude
Code plugin directory and the Stable Spine's versions/<gen>/ tree both put
this script at <root>/scripts/ -- so `../..` overshoots by one directory,
goal-match.mjs is never found, and the

    [ -f "$GOAL_MATCH" ] || exit 0

guard disables the whole L4 anticipatory surface. Measured on a real
install: identical inputs, the source layout speaks and the shipped layout
is silent.

The reason this survived a 678-line integration suite is that every run in
tests/integration/anticipate.test.mjs injects the path:

    ...(matcher ? { RUVNET_GOAL_MATCH: matcher } : {}),

so the default resolution had no coverage. Even "exits 0 and stays quiet
when the matcher module does not exist" passes an explicit path to a
missing file. The only symptom is silence, which is also the correct
behaviour on most prompts, so nothing ever looked wrong.

This is the fifth instance of the class documented in
tests/unit/installer-sibling-imports-packaged.test.mjs: "A capability that
silently does not exist on real installs is worse than one that was never
built, because the team believes it is running and stops looking."

Fix: resolve by probing the filesystem instead of asserting a shape -- try
each candidate directory, take the first that actually holds the matcher.
If none does, fall through to the historical path so the existing
diagnostics still name the expected location, and keep the no-node fast
path. Silence on doubt, never a guess.

Adds tests/integration/anticipate-module-resolution.test.mjs, which sets NO
module env vars and asserts resolution in both layouts. It fails on the
shipped-layout case without this change and passes with it, and re-asserts
the silence contract so a "make it speak" fix cannot become a nag.

NOTE: packaging is the other half and is NOT addressed here. goal-match.mjs,
capability-registry.mjs and advocacy-outcomes.mjs live only in the repo-root
scripts/, which the plugin directory does not carry, so on a real install
this resolver still finds nothing until they ship under plugin/. Raised in
the PR rather than guessed at here.
@vercel

vercel Bot commented Aug 4, 2026

Copy link
Copy Markdown

@ObiWanKenobi is attempting to deploy a commit to the stuart kerr's projects Team on Vercel.

A member of the Team first needs to authorize it.

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