Skip to content

fix(plan-tune): project-local question preferences never matched — hook used basename, writers use gstack-slug#2332

Open
jawadakram20 wants to merge 1 commit into
garrytan:mainfrom
jawadakram20:fix/preference-slug-mismatch
Open

fix(plan-tune): project-local question preferences never matched — hook used basename, writers use gstack-slug#2332
jawadakram20 wants to merge 1 commit into
garrytan:mainfrom
jawadakram20:fix/preference-slug-mismatch

Conversation

@jawadakram20

Copy link
Copy Markdown

The bug

slugFromCwd() in the AskUserQuestion PreToolUse hook returned path.basename(cwd). Every writer of project-local state uses the slug from bin/gstack-slug, which derives it from the git remote (owner-repo) and falls back to basename only when the repo has no remote:

plan-tune/SKILL.md:  _PROJ="${GSTACK_HOME:-$HOME/.gstack}/projects/${SLUG}"     # remote-derived
hook:                <stateRoot>/projects/<basename>/question-preferences.json  # basename

In any repo with a remote the two disagree, so the hook read a directory that does not exist.

Observed on a real machine

gstack-slug          → Shmulyitz-tristatelvs     (what /plan-tune writes)
basename             → tristatelvs               (what the hook read)

~/.gstack/projects/  → Shmulyitz-tristatelvs
                       Shmulyitz-Tristate-pim
                       Shmulyitz-tristate-vendure
                       millwoods-registry

Every bucket is remote-derived. No basename-named directory existed at all.

Impact

Project-local preferences were silently inert, and the documented project > global precedence (D8) collapsed to global-only. A user setting a per-project never-ask preference got no enforcement and no error — the hook just deferred every time.

Fix

Resolve the same slug gstack-slug does by reading its on-disk cache (<root>/slug-cache/<abs-path-with-slashes-as-underscores>), falling back to basename on a miss — mirroring gstack-slug's own no-remote fallback.

This is a single file read, so the hot-path constraint that motivated the basename shortcut still holds; we do not shell out to git. The cached value is re-sanitized to the [a-zA-Z0-9._-] invariant on read, since it becomes a path segment.

Why it survived

Every fixture in question-preference-hook.test.ts used a cwd whose basename happened to equal the slug, so the mismatch could not appear.

Two tests are added. The first fails on the old code — confirmed by stashing the fix:

expect(received).toBe(expected)
Expected: "deny"
Received: "defer"
(fail) slug resolution matches gstack-slug > uses the cached gstack-slug value, not the cwd basename

The second covers the no-remote basename fallback.

Verification

bun test test/question-preference-hook.test.ts test/memory-cache-injection.test.ts
 29 pass
 0 fail

Also confirmed end-to-end against a real slug cache: with a preference written only under Shmulyitz-tristatelvs, the hook now resolves that bucket from cwd=/Users/…/tristatelvs and enforces it.

…ok used basename, writers use gstack-slug

`slugFromCwd()` in the AskUserQuestion PreToolUse hook returned
`path.basename(cwd)`. Every writer of project-local state uses the slug from
`bin/gstack-slug`, which derives it from the git remote (owner-repo) and only
falls back to basename when the repo has NO remote configured:

    plan-tune/SKILL.md:  _PROJ="${GSTACK_HOME:-$HOME/.gstack}/projects/${SLUG}"
    hook:                <stateRoot>/projects/<basename>/question-preferences.json

In any repo with a remote the two disagree, so the hook read a directory that
does not exist. Observed on a real machine:

    gstack-slug   → Shmulyitz-tristatelvs      (what /plan-tune writes)
    basename      → tristatelvs                (what the hook read)
    ~/.gstack/projects/ → Shmulyitz-tristatelvs, Shmulyitz-Tristate-pim,
                          Shmulyitz-tristate-vendure, millwoods-registry

Every bucket is remote-derived; no basename-named directory existed at all.

Impact: project-local preferences were silently inert, and the documented
project > global precedence (D8) collapsed to global-only. A user setting a
per-project never-ask preference got no enforcement and no error.

Fix: resolve the same slug gstack-slug does by reading its on-disk cache
(`<root>/slug-cache/<abs-path-with-slashes-as-underscores>`), falling back to
basename on a cache miss — which mirrors gstack-slug's own no-remote
fallback. This is a single file read, so the hot-path constraint that
motivated the basename shortcut still holds; we do not shell out to git. The
cached value is re-sanitized to the [a-zA-Z0-9._-] invariant on read because
it becomes a path segment.

Why it survived: every fixture in question-preference-hook.test.ts used a cwd
whose basename happened to equal the slug, so the mismatch could not appear.
The two added tests pin the real contract — the first FAILS on the old code
(expected "deny", received "defer"), the second covers the no-remote fallback.

Verified: 29 pass / 0 fail across question-preference-hook and
memory-cache-injection. Also confirmed end-to-end against a real slug cache.
@trunk-io

trunk-io Bot commented Jul 23, 2026

Copy link
Copy Markdown

Merging to main in this repository is managed by Trunk.

  • To merge this pull request, check the box to the left or comment /trunk merge below.

After your PR is submitted to the merge queue, this comment will be automatically updated with its status. If the PR fails, failure details will also be posted here

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