backthread 0.5.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,12 +2,11 @@
2
2
  "name": "backthread",
3
3
  "displayName": "Backthread",
4
4
  "description": "Backthread helps you understand your codebase while AI ships features. It captures the why behind every Claude Code session so you can ask \"how does X work?\" without digging through PRs.",
5
- "version": "0.5.1",
5
+ "version": "0.6.0",
6
6
  "author": {
7
7
  "name": "Backthread"
8
8
  },
9
9
  "homepage": "https://backthread.dev",
10
- "hooks": "./hooks/hooks.json",
11
10
  "mcpServers": {
12
11
  "backthread": {
13
12
  "command": "node",
@@ -0,0 +1,27 @@
1
+ ---
2
+ description: Ask how or why something in THIS repo works — get a short, grounded, cited answer synthesized from your Backthread "How it works" decision log (the captured "why" the code doesn't contain), instead of digging through PRs or guessing.
3
+ argument-hint: "<your question, e.g. how does auth work?>"
4
+ disable-model-invocation: true
5
+ ---
6
+
7
+ # /backthread:how — how does it work?
8
+
9
+ Answers your "how/why does X work?" question about this repository from your
10
+ Backthread decision log. The server retrieves the question-relevant captured
11
+ decisions and synthesizes ONE short, grounded answer — every claim cited, anything
12
+ inferred flagged, and a partial-coverage note when the log is thin. Read-only:
13
+ nothing leaves the machine but the question.
14
+
15
+ ## Grounded answer
16
+
17
+ !`BT="${CLAUDE_PLUGIN_ROOT}/dist-bundle/backthread.js"; if [ -f "$BT" ]; then node "$BT" how --cwd "$(pwd)" $ARGUMENTS; else npx backthread how --cwd "$(pwd)" $ARGUMENTS; fi`
18
+
19
+ ## Your task
20
+
21
+ Relay the grounded answer above to the user **verbatim** — it is already written
22
+ for them, with its inline [n] citations, its Sources list, and the diagram link.
23
+ Do not re-answer from your own knowledge, re-run the command, or call any other
24
+ tool. If the result says "not logged in", tell the user to run `backthread login`.
25
+ If it says no repo could be determined, tell them to run from the repo directory or
26
+ `backthread connect` it. If it leads with a partial-coverage caveat, keep that
27
+ caveat in what you surface — do not present a partial answer as complete.
@@ -7017,6 +7017,9 @@ function workerBaseUrl(env = process.env) {
7017
7017
  function buildInferDecisionsUrl(env = process.env) {
7018
7018
  return new URL("/infer-decisions", workerBaseUrl(env)).toString();
7019
7019
  }
7020
+ function buildGroundedAskUrl(env = process.env) {
7021
+ return new URL("/grounded-ask", workerBaseUrl(env)).toString();
7022
+ }
7020
7023
  var DEFAULT_FUNCTIONS_URL = "https://yempemohevgpctkpstuf.supabase.co/functions/v1";
7021
7024
  function functionsBaseUrl(env = process.env) {
7022
7025
  const override = env.BACKTHREAD_FUNCTIONS_URL;
@@ -7026,9 +7029,6 @@ function functionsBaseUrl(env = process.env) {
7026
7029
  function buildIngestDecisionsUrl(env = process.env) {
7027
7030
  return new URL(`${functionsBaseUrl(env).replace(/\/+$/, "")}/ingest-decisions`).toString();
7028
7031
  }
7029
- function buildReadDecisionsUrl(env = process.env) {
7030
- return new URL(`${functionsBaseUrl(env).replace(/\/+$/, "")}/read-decisions`).toString();
7031
- }
7032
7032
  function buildOnboardingStateUrl(env = process.env) {
7033
7033
  return new URL(`${functionsBaseUrl(env).replace(/\/+$/, "")}/onboarding-state`).toString();
7034
7034
  }
@@ -33696,6 +33696,8 @@ var StdioServerTransport = class {
33696
33696
  };
33697
33697
 
33698
33698
  // src/query.ts
33699
+ var DEFAULT_QUESTION = "How does this project work?";
33700
+ var GROUNDED_ASK_TIMEOUT_MS = 3e4;
33699
33701
  function parseSlug2(slug) {
33700
33702
  const parts = slug.trim().replace(/^\/+|\/+$/g, "").split("/").filter(Boolean);
33701
33703
  if (parts.length !== 2) return null;
@@ -33741,9 +33743,12 @@ async function queryDecisions(input, deps = {}) {
33741
33743
  };
33742
33744
  }
33743
33745
  const deepLink = buildRepoDeepLink(repo.owner, repo.name, env);
33746
+ const question = typeof input.question === "string" && input.question.trim().length > 0 ? input.question.trim() : DEFAULT_QUESTION;
33747
+ const ac = new AbortController();
33748
+ const timer = setTimeout(() => ac.abort(), GROUNDED_ASK_TIMEOUT_MS);
33744
33749
  let res;
33745
33750
  try {
33746
- res = await doFetch(buildReadDecisionsUrl(env), {
33751
+ res = await doFetch(buildGroundedAskUrl(env), {
33747
33752
  method: "POST",
33748
33753
  headers: {
33749
33754
  // Bearer device token — never logged.
@@ -33752,15 +33757,20 @@ async function queryDecisions(input, deps = {}) {
33752
33757
  ...versionHeaders()
33753
33758
  // x-backthread-version — server-side compat guard
33754
33759
  },
33755
- body: JSON.stringify({ repo: { owner: repo.owner, name: repo.name } })
33760
+ // The server accepts `repo` as an "owner/name" slug (it re-resolves + gates).
33761
+ body: JSON.stringify({ question, repo: `${repo.owner}/${repo.name}` }),
33762
+ signal: ac.signal
33756
33763
  });
33757
33764
  } catch (e) {
33765
+ const aborted2 = e.name === "AbortError";
33758
33766
  return {
33759
33767
  status: "read-failed",
33760
- detail: `read request failed: ${e.message}`,
33768
+ detail: aborted2 ? `grounded-ask timed out after ${GROUNDED_ASK_TIMEOUT_MS / 1e3}s \u2014 try again.` : `grounded-ask request failed: ${e.message}`,
33761
33769
  repo,
33762
33770
  deepLink
33763
33771
  };
33772
+ } finally {
33773
+ clearTimeout(timer);
33764
33774
  }
33765
33775
  let payload;
33766
33776
  try {
@@ -33768,60 +33778,53 @@ async function queryDecisions(input, deps = {}) {
33768
33778
  } catch {
33769
33779
  payload = null;
33770
33780
  }
33781
+ const rec = payload && typeof payload === "object" ? payload : {};
33771
33782
  if (!res.ok) {
33772
- const obj = payload && typeof payload === "object" ? payload : {};
33773
- const serverErr = typeof obj.message === "string" && obj.message.length > 0 ? obj.message : "error" in obj ? String(obj.error) : `HTTP ${res.status}`;
33783
+ const serverErr = typeof rec.message === "string" && rec.message.length > 0 ? rec.message : "error" in rec ? String(rec.error) : `HTTP ${res.status}`;
33774
33784
  return {
33775
33785
  status: "read-failed",
33776
- detail: `read rejected (${res.status}): ${serverErr}`,
33786
+ detail: `grounded-ask rejected (${res.status}): ${serverErr}`,
33787
+ repo,
33788
+ deepLink
33789
+ };
33790
+ }
33791
+ const answer = typeof rec.answer === "string" ? rec.answer : "";
33792
+ if (!answer) {
33793
+ return {
33794
+ status: "read-failed",
33795
+ detail: "grounded-ask returned no answer.",
33777
33796
  repo,
33778
33797
  deepLink
33779
33798
  };
33780
33799
  }
33781
- const rec = payload && typeof payload === "object" ? payload : {};
33782
- const flows = normalizeFlows(rec.flows);
33783
- const decisions = normalizeDecisions(rec.decisions);
33784
33800
  const upgrade = typeof rec.upgrade === "string" && rec.upgrade.length > 0 ? rec.upgrade : void 0;
33785
- const base = `${flows.length} flow(s), ${decisions.length} decision(s) for ${repo.owner}/${repo.name}.`;
33786
33801
  return {
33787
33802
  status: "ok",
33788
- detail: base,
33803
+ detail: `grounded answer (${typeof rec.coverage === "string" ? rec.coverage : "partial"} coverage)`,
33789
33804
  repo,
33790
- flows,
33791
- decisions,
33792
- deepLink,
33805
+ answer,
33806
+ coverage: typeof rec.coverage === "string" ? rec.coverage : void 0,
33807
+ citations: normalizeCitations(rec.citations),
33808
+ inferredSpans: Array.isArray(rec.inferredSpans) ? rec.inferredSpans.map(String) : [],
33809
+ // Prefer the server's deepLink; fall back to the locally-built one.
33810
+ deepLink: typeof rec.deepLink === "string" && rec.deepLink.length > 0 ? rec.deepLink : deepLink,
33793
33811
  ...upgrade ? { upgrade } : {}
33794
33812
  };
33795
33813
  } catch (e) {
33796
33814
  return { status: "error", detail: `query failed (swallowed): ${e.message}` };
33797
33815
  }
33798
33816
  }
33799
- function normalizeFlows(raw) {
33817
+ function normalizeCitations(raw) {
33800
33818
  if (!Array.isArray(raw)) return [];
33801
- return raw.map((f) => {
33802
- const r = f && typeof f === "object" ? f : {};
33819
+ return raw.map((c) => {
33820
+ const r = c && typeof c === "object" ? c : {};
33803
33821
  return {
33804
- id: String(r.id ?? ""),
33805
- name: String(r.name ?? ""),
33806
- lifecycle: String(r.lifecycle ?? ""),
33807
- salience: typeof r.salience === "number" ? r.salience : null,
33808
- canonicalFlowId: typeof r.canonicalFlowId === "string" ? r.canonicalFlowId : null
33809
- };
33810
- });
33811
- }
33812
- function normalizeDecisions(raw) {
33813
- if (!Array.isArray(raw)) return [];
33814
- return raw.map((d) => {
33815
- const r = d && typeof d === "object" ? d : {};
33816
- return {
33817
- id: String(r.id ?? ""),
33822
+ n: typeof r.n === "number" ? r.n : 0,
33823
+ decisionId: String(r.decisionId ?? ""),
33818
33824
  title: String(r.title ?? ""),
33819
- why: typeof r.why === "string" ? r.why : null,
33820
- significance: typeof r.significance === "number" ? r.significance : null,
33821
- domainRisk: typeof r.domainRisk === "string" ? r.domainRisk : null,
33822
- decidedAt: typeof r.decidedAt === "string" ? r.decidedAt : null,
33823
- flowIds: Array.isArray(r.flowIds) ? r.flowIds.map(String) : [],
33824
- moduleIds: Array.isArray(r.moduleIds) ? r.moduleIds.map(String) : []
33825
+ url: String(r.url ?? ""),
33826
+ moduleIds: Array.isArray(r.moduleIds) ? r.moduleIds.map(String) : [],
33827
+ decidedAt: typeof r.decidedAt === "string" ? r.decidedAt : null
33825
33828
  };
33826
33829
  });
33827
33830
  }
@@ -33857,7 +33860,7 @@ function isFailure(o) {
33857
33860
  async function handleQueryTool(args = {}, deps = {}) {
33858
33861
  const run = deps.queryDecisionsImpl ?? queryDecisions;
33859
33862
  try {
33860
- const outcome = await run({ repo: args.repo, cwd: args.cwd }, deps.queryDeps);
33863
+ const outcome = await run({ question: args.question, repo: args.repo, cwd: args.cwd }, deps.queryDeps);
33861
33864
  let text = formatQueryOutcome(outcome, args.question);
33862
33865
  const nudge = await (deps.upgradeNudgeImpl ?? maybeUpgradeNudge)(outcome.upgrade);
33863
33866
  if (nudge) text += `
@@ -33868,42 +33871,11 @@ ${nudge}`;
33868
33871
  return textResult(`query: error \u2014 ${e.message}`, true);
33869
33872
  }
33870
33873
  }
33871
- function formatQueryOutcome(outcome, question) {
33872
- if (outcome.status !== "ok") {
33874
+ function formatQueryOutcome(outcome, _question) {
33875
+ if (outcome.status !== "ok" || !outcome.answer) {
33873
33876
  return `query: ${outcome.status} \u2014 ${outcome.detail}`;
33874
33877
  }
33875
- const lines = [];
33876
- const q = question && question.trim().length > 0 ? ` for "${question.trim()}"` : "";
33877
- const repoSlug = outcome.repo ? `${outcome.repo.owner}/${outcome.repo.name}` : "this repo";
33878
- lines.push(`How ${repoSlug} works${q} \u2014 salience-ranked from the decision log:`);
33879
- lines.push("");
33880
- const flows = outcome.flows ?? [];
33881
- if (flows.length > 0) {
33882
- lines.push("Flows (most salient first):");
33883
- for (const f of flows) {
33884
- const sal = f.salience != null ? ` [salience ${f.salience}]` : "";
33885
- lines.push(` - ${f.name} (${f.lifecycle})${sal}`);
33886
- }
33887
- } else {
33888
- lines.push("Flows: none recorded yet.");
33889
- }
33890
- lines.push("");
33891
- const decisions = outcome.decisions ?? [];
33892
- if (decisions.length > 0) {
33893
- lines.push('Decisions (the "why", most significant first):');
33894
- for (const d of decisions) {
33895
- const risk = d.domainRisk ? ` {${d.domainRisk}-risk}` : "";
33896
- lines.push(` - ${d.title}${risk}`);
33897
- if (d.why) lines.push(` why: ${d.why}`);
33898
- }
33899
- } else {
33900
- lines.push("Decisions: none recorded yet.");
33901
- }
33902
- lines.push("");
33903
- if (outcome.deepLink) {
33904
- lines.push(`Open the "How it works" diagram: ${outcome.deepLink}`);
33905
- }
33906
- return lines.join("\n");
33878
+ return outcome.answer;
33907
33879
  }
33908
33880
  function buildMcpServer(deps = {}) {
33909
33881
  const server = new McpServer({
@@ -33928,10 +33900,10 @@ function buildMcpServer(deps = {}) {
33928
33900
  server.registerTool(
33929
33901
  "query",
33930
33902
  {
33931
- title: "How does X work?",
33932
- description: 'Answer "how does X work?" for the current repo: returns the salience-ranked Flows and Decisions (the "why" layer) from your Backthread decision log, plus a deep-link into the web-app "How it works" diagram. Read-only.',
33903
+ title: "How does it work? (grounded answer from this repo's decision log)",
33904
+ description: `Call this FIRST whenever the user asks how or why any part of THIS repository works, is built, or was decided \u2014 before reading source files or guessing. It returns a short, grounded, CITED answer synthesized from the repo's Backthread decision log (the captured "why" that the code itself does not contain): the real reasons, trade-offs, and superseded approaches behind the current design. Pass the user's question. The answer is ready to relay verbatim \u2014 it already cites its sources and flags anything inferred. Read-only; nothing leaves the machine but the question.`,
33933
33905
  inputSchema: {
33934
- question: external_exports.string().optional().describe('The "how does X work?" question (the agent narrates the answer against the returned log).'),
33906
+ question: external_exports.string().optional().describe(`The user's "how/why does X work?" question, in their words. Load-bearing: the server retrieves and synthesizes the answer against it.`),
33935
33907
  repo: external_exports.string().optional().describe('Optional repo override as "owner/name"; otherwise the configured repo or the cwd git remote.'),
33936
33908
  cwd: external_exports.string().optional().describe("The session's working directory (repo fallback).")
33937
33909
  }
@@ -33947,6 +33919,87 @@ async function startMcpServer(deps = {}) {
33947
33919
  return server;
33948
33920
  }
33949
33921
 
33922
+ // src/routingStats.ts
33923
+ import { join as join12 } from "node:path";
33924
+ import { readFile as readFile11, writeFile as writeFile9, mkdir as mkdir9, chmod as chmod8 } from "node:fs/promises";
33925
+ var STATS_FILE = "routing-stats.json";
33926
+ function statsPath(env) {
33927
+ return join12(configDir(env), STATS_FILE);
33928
+ }
33929
+ async function readRoutingStats(deps = {}) {
33930
+ const env = deps.env ?? process.env;
33931
+ const read = deps.readFileImpl ?? readFile11;
33932
+ try {
33933
+ const raw = await read(statsPath(env), "utf8");
33934
+ const obj = JSON.parse(raw);
33935
+ return {
33936
+ injected: typeof obj.injected === "number" && obj.injected >= 0 ? Math.floor(obj.injected) : 0,
33937
+ lastInjectedAt: typeof obj.lastInjectedAt === "string" ? obj.lastInjectedAt : void 0
33938
+ };
33939
+ } catch {
33940
+ return { injected: 0 };
33941
+ }
33942
+ }
33943
+ async function recordRoutingInjected(deps = {}) {
33944
+ const env = deps.env ?? process.env;
33945
+ const now = deps.now ?? (() => /* @__PURE__ */ new Date());
33946
+ const write = deps.writeFileImpl ?? writeFile9;
33947
+ const mkdirp = deps.mkdirImpl ?? mkdir9;
33948
+ const chmodp = deps.chmodImpl ?? chmod8;
33949
+ try {
33950
+ const prev = await readRoutingStats(deps);
33951
+ const next = { injected: prev.injected + 1, lastInjectedAt: now().toISOString() };
33952
+ await mkdirp(configDir(env), { recursive: true, mode: DIR_MODE });
33953
+ await write(statsPath(env), `${JSON.stringify(next, null, 2)}
33954
+ `, { mode: CONFIG_MODE });
33955
+ await chmodp(statsPath(env), CONFIG_MODE).catch(() => {
33956
+ });
33957
+ } catch {
33958
+ }
33959
+ }
33960
+
33961
+ // src/sessionStart.ts
33962
+ var ROUTING_CONTEXT = [
33963
+ 'This repository is set up with Backthread, which has captured the "why" behind its',
33964
+ "changes into a decision log. The backthread `query` MCP tool (and the",
33965
+ '`/backthread:how` slash command) answer "how does X work?" and "why was Y decided?"',
33966
+ "with a short, CITED answer synthesized from that log \u2014 the real reasons, trade-offs,",
33967
+ "and superseded approaches the source code itself does not contain.",
33968
+ "",
33969
+ "When the user asks how or why any part of THIS codebase works, is structured, or was",
33970
+ "decided, call the backthread `query` tool FIRST \u2014 before grepping or reading source",
33971
+ "files. Pass the user's question and relay the cited answer it returns; only read",
33972
+ "source afterwards if it didn't cover what you need."
33973
+ ].join("\n");
33974
+ function buildSessionStartOutput(isSetUp) {
33975
+ if (!isSetUp) return {};
33976
+ return {
33977
+ hookSpecificOutput: {
33978
+ hookEventName: "SessionStart",
33979
+ additionalContext: ROUTING_CONTEXT
33980
+ }
33981
+ };
33982
+ }
33983
+ async function runSessionStart(deps = {}) {
33984
+ const readConfig2 = deps.readConfig ?? readConfig;
33985
+ const record2 = deps.recordRoutingInjected ?? recordRoutingInjected;
33986
+ let isSetUp = false;
33987
+ try {
33988
+ const cfg = await readConfig2();
33989
+ isSetUp = !!cfg.device_token;
33990
+ } catch {
33991
+ isSetUp = false;
33992
+ }
33993
+ const output = buildSessionStartOutput(isSetUp);
33994
+ if (output.hookSpecificOutput) {
33995
+ try {
33996
+ await record2();
33997
+ } catch {
33998
+ }
33999
+ }
34000
+ return output;
34001
+ }
34002
+
33950
34003
  // src/bin/backthread.ts
33951
34004
  var USAGE = `backthread \u2014 capture the "why" of your AI-coded changes
33952
34005
 
@@ -33963,6 +34016,9 @@ Usage:
33963
34016
  (no browser needed \u2014 codes expire in ~10 minutes)
33964
34017
  backthread login --device Headless / SSH login (device-code flow \u2014 coming soon)
33965
34018
  backthread whoami Show the current device's config (token is never printed)
34019
+ backthread how <question> Ask how/why something in this repo works \u2014 prints a
34020
+ grounded, cited answer from your Backthread decision log
34021
+ (backs the /backthread:how slash command). [--cwd <path>]
33966
34022
  backthread capture Capture this session's decisions (run by the SessionEnd/Stop hook)
33967
34023
  backthread capture --from-hook
33968
34024
  Shared multi-agent hook entrypoint: read the hook payload off
@@ -33997,6 +34053,12 @@ function flagValue(rest, flag) {
33997
34053
  if (!value || value.startsWith("--")) return void 0;
33998
34054
  return value;
33999
34055
  }
34056
+ function stripFlag(rest, flag) {
34057
+ const i = rest.indexOf(flag);
34058
+ if (i === -1) return rest;
34059
+ const dropValue = rest[i + 1] !== void 0 && !rest[i + 1].startsWith("--");
34060
+ return [...rest.slice(0, i), ...rest.slice(i + (dropValue ? 2 : 1))];
34061
+ }
34000
34062
  async function runOnboarding(rest) {
34001
34063
  const claim = parseClaimFlag(rest);
34002
34064
  const result = await runStart({
@@ -34060,6 +34122,14 @@ async function main(argv, deps = {}) {
34060
34122
  }
34061
34123
  return 0;
34062
34124
  }
34125
+ case "session-start": {
34126
+ const ssAgent = parseAgent(flagValue(rest, "--agent"));
34127
+ setRequestAgent(ssAgent === "unknown" ? "claude-code" : ssAgent);
34128
+ await readRawHookInput().catch(() => "");
34129
+ const output = await runSessionStart();
34130
+ console.log(JSON.stringify(output));
34131
+ return 0;
34132
+ }
34063
34133
  case "mcp": {
34064
34134
  await startMcpServer();
34065
34135
  return null;
@@ -34083,6 +34153,15 @@ async function main(argv, deps = {}) {
34083
34153
  });
34084
34154
  return result.exitCode;
34085
34155
  }
34156
+ case "how":
34157
+ case "ask": {
34158
+ const query = deps.queryDecisionsImpl ?? queryDecisions;
34159
+ const cwd = flagValue(rest, "--cwd") ?? process.cwd();
34160
+ const question = stripFlag(rest, "--cwd").join(" ").trim();
34161
+ const outcome = await query({ question, cwd });
34162
+ console.log(formatQueryOutcome(outcome, question));
34163
+ return outcome.status === "ok" ? 0 : 1;
34164
+ }
34086
34165
  case void 0:
34087
34166
  return onboarding(rest);
34088
34167
  case "help":
@@ -34126,5 +34205,6 @@ if (isEntryPoint()) {
34126
34205
  }
34127
34206
  export {
34128
34207
  main,
34129
- runOnboarding
34208
+ runOnboarding,
34209
+ stripFlag
34130
34210
  };
package/hooks/hooks.json CHANGED
@@ -1,6 +1,17 @@
1
1
  {
2
2
  "$comment": "The SessionEnd capture hook, declared in the PLUGIN MANIFEST (referenced from .claude-plugin/plugin.json). When Backthread is installed as a Claude Code plugin, this registers the hook automatically AT USER/GLOBAL SCOPE — no mutation of the user's project .claude/settings.json, and it fires across EVERY repo AND git worktree (ARP-680: a per-project, gitignored .claude/settings.json hook is exactly what froze the dogfood log — worktree sessions never carried it; an installed plugin's hooks follow the user, not the cwd). The command invokes the plugin's BUNDLED, self-contained bin via ${CLAUDE_PLUGIN_ROOT} (ARP-474's dist-bundle/backthread.js — committed to git so the marketplace-cloned plugin has it; CC runs no build step on install), NOT `npx backthread`, which would resolve a possibly-stale or absent global / npm-linked install (the second half of the ARP-680 dogfood freeze). The detached worker re-spawns via process.execPath + process.argv[1], so pointing argv[1] at the bundle propagates the same self-contained bin through the WHOLE chain (hook → detached worker). It routes through the shared `--from-hook` entrypoint with `--detach`: it reads the SessionEnd payload off stdin, then re-spawns a DETACHED worker that does the slow LOCALLY-redacted redact→infer→persist round-trip and returns immediately — so a ≥30s inference can't be SIGTERM'd by CC's SessionEnd hook timeout (or reaped on session exit). Best-effort, never blocks/delays the session, always exits 0 (ARP-682). `--agent claude-code` selects the CC payload shape. Mirrored by the USER-SCOPE ~/.claude/settings.json fallback that `backthread install` writes for the bare-npx (non-plugin) path (ARP-503). We register ONLY SessionEnd (once per session) on purpose — `runCapture` also handles a Stop payload, but Stop fires on every turn-end, which would capture far too aggressively, so Stop is intentionally NOT registered here.",
3
+ "$comment_sessionstart": "The SessionStart AMBIENT-ROUTING hook (ARP-763). Injects a one-time instruction into the session context telling Claude Code to call the `query` MCP tool FIRST on how/why-does-X questions, before grepping — so a plain 'how does X work?' routes to a grounded, cited answer with no new user habit. SYNCHRONOUS, NOT --detach: CC reads this command's STDOUT for hookSpecificOutput.additionalContext, so the bundle must print it (a detached re-spawn would print an ack instead). It does only a FAST LOCAL config read (no network), gates on a present device token (only routes when `query` can actually answer), and always exits 0 with valid JSON. PLUGIN-ONLY: runs the shipped self-contained bundle (fast, offline); the bare-npx settings.json fallback deliberately does NOT register it, because a synchronous `npx backthread session-start` would block every session start on npm's resolve (the capture hook only gets away with @latest because it's --detach'ed). npx-fallback users keep the `query` tool's imperative description.",
3
4
  "hooks": {
5
+ "SessionStart": [
6
+ {
7
+ "hooks": [
8
+ {
9
+ "type": "command",
10
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/dist-bundle/backthread.js\" session-start --agent claude-code"
11
+ }
12
+ ]
13
+ }
14
+ ],
4
15
  "SessionEnd": [
5
16
  {
6
17
  "hooks": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "backthread",
3
- "version": "0.5.1",
3
+ "version": "0.6.0",
4
4
  "description": "Backthread helps you understand your codebase while AI ships features. The CLI captures the why behind every AI session and lets you ask how your codebase works, right from the terminal.",
5
5
  "license": "MIT",
6
6
  "author": "Backthread",