Commit Graph

9 Commits

Author SHA1 Message Date
bvandeusen b0a0bf8abd fix(plugin): justify the one shellcheck finding (SC1007, false positive)
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 16s
CI & Build / TypeScript typecheck (push) Successful in 35s
CI & Build / integration (push) Successful in 43s
CI & Build / Python tests (push) Successful in 1m6s
CI & Build / Build & push image (push) Successful in 21s
First CI run with shellcheck (run 3029) flagged exactly one thing:

  scribe_session_context.sh:44
  SC1007 Remove space after = if trying to assign a value
  here=$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)

`CDPATH= cd` is the deliberate POSIX idiom for running a single command
with CDPATH empty — it stops `cd` resolving through the operator's CDPATH
and echoing the resolved path into our stdout, which for a hook whose
stdout IS its protocol would be a real bug. shellcheck cannot distinguish
that from a typo'd `CDPATH=cd`, so this is a false positive.

Scoped `# shellcheck disable=SC1007` with the reason above it, matching
how CI-runner's own scripts/install-common.sh handles SC2086. One
line-scoped disable, no file-level or blanket suppression — a lint you
silence broadly stops being a lint.

Everything else in that run passed, including the parts that could only
run once jq was installed: all four hooks exit 0 and stay silent
unconfigured and against a refused connection, with the session-context
hook correctly still emitting its static floor.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UaYUaouG9jjhATyuxCKrQs
2026-07-28 22:43:30 -04:00
bvandeusen c569cdd0eb fix(plugin): every hook's credential + URL encoding was broken
Three defects, each of which independently made a hook a no-op, and all
three failing silently — which is why the whole dynamic side of the plugin
looked "shipped" while doing nothing.

1. Wrong env var case. Claude Code exports userConfig to hooks as
   CLAUDE_PLUGIN_OPTION_<KEY> with the key UPPERCASED. All four hooks read
   CLAUDE_PLUGIN_OPTION_api_endpoint / _api_token, so both values were
   always empty. That killed the SessionStart dynamic tier, process sync,
   prompt auto-inject, and the write-path prior-art trigger at once.

2. jq -rR is line-oriented. `@uri` under -R encodes input LINE BY LINE, so
   a multi-line payload came back as several encoded lines joined by raw
   newlines — an invalid URL, curl fails, hook exits 0 in silence. Now
   -sRr. This one hid behind (1): auto-inject only ever worked for
   single-line prompts, and prior-art (which posts code, always
   multi-line) could never have worked at all.

3. cut -c1-1200 caps each LINE, not the payload, so the prior-art code
   budget wasn't a budget. Now head -c 1200.

Also widens the SessionStart warning: "neither URL nor token arrived" used
to be treated as a benign unconfigured install and stayed quiet. That is
exactly the state defect (1) produced, so the one install state that most
needed a signal was the only one that emitted none. It now says so, and
names the two other features it silently disables.

Verified against the live instance: dynamic rules + project context load,
auto-inject surfaces #2192 on a multi-line prompt, and the write-path
trigger returns the [here] place-arm hit on a multi-line edit with session
dedup suppressing the repeat.

Refs #2198, #2082

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UaYUaouG9jjhATyuxCKrQs
2026-07-28 18:00:11 -04:00
bvandeusen f6629d4bcf fix(plugin): keep always-on rules alive across compaction (0.1.10 → 0.1.11)
Always-on rules were on-demand, not always-present: Tier-1 static context only
tells the agent to call list_always_on_rules(), and Tier-2 dynamic fetch is dark
(token doesn't reach the hook subprocess). On compaction the fetched rules get
summarized away while the harness's own built-in git instruction ("branch first")
survives in the base prompt — so post-compact the generic git instinct wins and
rule #1 ("dev is home") is missed.

- scribe_static_context.md: new "Operator rules govern consequential actions"
  bullet — before any git branch/commit/push or hard-to-reverse action, loaded
  rules beat generic harness/default habits; re-pull rules if not loaded or
  summarized by a compaction. Tier 1 = always fires, keyless, re-fires on compact.
- scribe_session_context.sh: compaction banner now re-pulls list_always_on_rules(),
  not just enter_project().
- plugin.json: 0.1.10 → 0.1.11 so autoUpdate ships the plugin/ change (#1040).

Generic and instance-agnostic per rules #115/#119 — no operator-specific rule
text hardcoded. Refs issue #1197.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E4bNefPFAz7esmMZMZmkzL
2026-06-30 12:41:45 -04:00
bvandeusen d99c4e3c15 feat(plugin): compaction re-grounding in SessionStart hook (A7)
Deliver 'don't silently lose work at compaction' via the mechanism that
actually works. Verified contract: a PreCompact hook CANNOT make the model
flush to Scribe (host hooks can't trigger model tool calls, and can't know the
in-flight task ids), and its additionalContext only shapes the one-shot summary.
The correct tool is SessionStart scoped to source=compact, which fires AFTER
compaction and injects context the model reads.

Our SessionStart hook is matcher-less, so it already fires on compact — it just
said nothing compaction-specific. Now it reads the stdin event  and,
when source==compact, leads with a banner telling the model to reload the active
project + in-flight tasks from Scribe and reconcile half-remembered state.
Durable path = record-as-you-go (A4/B8) + this post-compaction reload.

Refs plan 812 (A7); supersedes the literal 'PreCompact hook' idea.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 16:49:10 -04:00
bvandeusen 700cfc664b feat(plugin): surface dynamic-tier failures in SessionStart hook (A2)
Fail-open but no longer silent. When the dynamic context fetch yields nothing,
append a short status line to the injected context so a session can tell
'couldn't load live context' apart from 'Scribe had nothing to say':
- endpoint+token present but fetch empty/failed -> 'instance unreachable / request failed'
- endpoint present but token absent -> fingerprints the known Claude Code
  userConfig export gap ('API token did not reach this hook')
A fully unconfigured install (no url AND no token) stays quiet — static-only is
the intended mode there. Static Tier 1 still always carries the mandate.

Refs plan 812 item A2.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 16:14:27 -04:00
bvandeusen d11eb9145b feat(plugin): two-tier SessionStart hook — static keyless floor + dynamic enrichment
The SessionStart push channel was single-tier: it curled /api/plugin/context
with a Bearer token and, on any failure (missing/unexported token, network
error, missing curl), injected nothing and exited 0 — silently. A known
upstream Claude Code gap (sensitive userConfig not reliably exported to hook
subprocesses) trips this routinely, so a fresh session gets no signal to reach
for Scribe and falls back to local file-memory (root cause of unlogged work on
remote/rc sessions).

Split into two tiers:
- Tier 1 (static, keyless, networkless, always fires): inject bundled
  scribe_static_context.md — the load-bearing behavioral mandate. Cannot be
  suppressed by the upstream key bug.
- Tier 2 (dynamic, best-effort, fails open): existing curl for live rules +
  active-project context, appended below the static block. Lights up as
  enrichment once the key reaches the hook.

Only jq is now required (JSON envelope); curl/token gate the dynamic tier only.
Bump plugin 0.1.5 -> 0.1.6 so clients pick up the change.

Refs milestone 55; task 809; decision note 810.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 15:01:35 -04:00
bvandeusen 8fe571e175 feat(plugin): resolve session project from git remote, not a pinned project_id
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 33s
CI & Build / Python tests (push) Successful in 51s
CI & Build / Build & push image (push) Successful in 1m0s
The SessionStart hook asked for a project_id via plugin userConfig, which pins
one install to a single project — wrong for an operator working across many
repos/projects. Resolve the active project server-side from the working repo's
git remote instead (a stable identifier, not a dir-name guess).

- repo_bindings table (migration 0064) + RepoBinding model: (user, repo_key) ->
  project, FKs CASCADE.
- services/repo_bindings: normalize_repo_key collapses ssh/https/scp/creds/port/
  .git to host/owner/repo; resolve/set/list/delete.
- GET /api/plugin/context takes ?repo=<remote>; unbound repo -> a "bind this
  repo" hint with a ready bind_repo() call. project_id kept as manual override.
- MCP tools: bind_repo / list_repo_bindings / unbind_repo.
- Hook sends ?repo=$(git remote get-url origin) URL-encoded; all project_id
  handling removed. plugin.json drops the project_id userConfig (0.1.2 -> 0.1.3).
- Tests: normalize equivalence classes + unbound-hint rendering.

Refs task 755 (Scribe-as-plugin push channel).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 01:33:58 -04:00
bvandeusen 651119cfb0 fix(plugin): read API token from CLAUDE_PLUGIN_OPTION env, not user_config placeholder
The SessionStart push-channel hook passed the key via
SCRIBE_TOKEN="${user_config.api_token}" in hooks.json, but api_token is
sensitive:true. Claude Code keeps sensitive userConfig in the keychain and
does not interpolate it into hook command strings (only into mcpServers
headers), so the hook received the literal placeholder, sent it as the Bearer
token, and the context endpoint 401'd -> fail-open -> no context injected.

Read the harness-exported CLAUDE_PLUGIN_OPTION_<key> env vars instead (SCRIBE_*
still override for the settings.json dogfooding path), and treat any unexpanded
${...} literal as unset so the hook fails open cleanly instead of 401-ing.
Bump 0.1.1 -> 0.1.2 so installs refresh the cache.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 00:43:40 -04:00
bvandeusen 9924f873b9 feat(plugin): ship the Scribe Claude Code plugin in-repo (marketplace + plugin)
Per operator: the plugin lives in the app repo so it ships and versions in
lockstep with the app and the /api/plugin/context contract it targets (same
co-location rationale as the former in-repo MCP). A git-cloned marketplace
supports relative plugin sources, so the FabledScribe repo IS the marketplace.

- .claude-plugin/marketplace.json — source ./plugin
- plugin/.claude-plugin/plugin.json — userConfig (base URL, api key, project id)
- plugin/.mcp.json — http scribe server, ${user_config.*} substitution
- plugin/hooks/ — SessionStart push-channel hook (fail-open)
- plugin/skills/using-scribe — bootstrap skill
- plugin/README.md — install via the FabledScribe repo marketplace

Phase 2 of plan #755. Install/userConfig-substitution test pending.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 23:04:09 -04:00