@sporhq/spor 0.2.7 → 0.2.9

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.
@@ -56,3 +56,35 @@ export SPOR_DISTILL_CMD="$HOME/repos/spor/scripts/distill-gemini.sh"
56
56
 
57
57
  The journal records the backend per call (`journal/llm-calls/`), so distill
58
58
  quality stays observable across backends through the same eval loop.
59
+
60
+ ## Capture-nudge backend
61
+
62
+ The post-tool capture nudge (a Write/Edit of substantial prose to a `.md`
63
+ outside the graph runs a classifier and, if it finds capturable facts, injects
64
+ a capture-or-dismiss nudge) uses the **same backend contract** as the
65
+ distiller — prompt on stdin, response on stdout — but a separate variable so
66
+ you can point the two at different models:
67
+
68
+ - `SPOR_NUDGE_CMD` — custom classifier command (defaults to `claude -p --model
69
+ haiku`, same as the distiller). Because the contract is identical,
70
+ `scripts/distill-gemini.sh` doubles as a nudge backend (it keys off
71
+ `SPOR_DISTILL_MODEL`, default `gemini-3.5-flash`):
72
+
73
+ ```sh
74
+ export SPOR_NUDGE_CMD="$HOME/repos/spor/scripts/distill-gemini.sh"
75
+ ```
76
+
77
+ This is the recommended latency fix: the nudge runs **synchronously** in the
78
+ tool loop, and Gemini Flash returns in ~2–7s versus ~17s for a `claude -p`
79
+ cold boot, with no quality regression (dec-cc-nudge-flash-latency).
80
+ - `SPOR_NUDGE=0` — disable the nudge entirely.
81
+ - `SPOR_NUDGE_MAX` — per-session ceiling on classifier calls (default 20),
82
+ bounding spend/latency in a docs-heavy session where many `.md` files each
83
+ classify to nothing. (A separate cap stops after 3 *fired* nudges.)
84
+ - `SPOR_NUDGE_TIMEOUT` — milliseconds before a hung classifier is killed
85
+ (default 30000), so a wedged backend can't block the tool loop.
86
+
87
+ (Legacy `SUBSTRATE_*` spellings are still read for all four.) The same
88
+ `SPOR_DISTILLING=1` recursion guard is exported around the call, and the
89
+ journal records each nudge call under `journal/llm-calls/` (source `nudge`)
90
+ through the same eval loop as the distiller.
@@ -27,11 +27,15 @@ is just a manifest over `bin/spor-hook`.
27
27
  # Distiller backend — Codex hosts usually won't have the claude CLI.
28
28
  # Contract: prompt on stdin, response on stdout.
29
29
  export SPOR_DISTILL_CMD='codex exec -'
30
+ # Capture-nudge backend — same contract, but it runs synchronously in the
31
+ # tool loop, so prefer a fast model. SPOR_NUDGE=0 disables it.
32
+ export SPOR_NUDGE_CMD='codex exec -'
30
33
  ```
31
34
 
32
- (Legacy `SUBSTRATE_*` names are still read.) Without `SPOR_DISTILL_CMD`
33
- the distiller defaults to `claude -p --model haiku`, which is fine if the
34
- claude CLI is installed.
35
+ (Legacy `SUBSTRATE_*` names are still read.) Without `SPOR_DISTILL_CMD` /
36
+ `SPOR_NUDGE_CMD` both default to `claude -p --model haiku`, which is fine if
37
+ the claude CLI is installed. See [adapters/README.md](../README.md) for the
38
+ nudge bounds (`SPOR_NUDGE_MAX`, `SPOR_NUDGE_TIMEOUT`).
35
39
 
36
40
  ## Event mapping
37
41
 
@@ -20,8 +20,9 @@ sed "s|__SPOR_ROOT__|$SPOR_ROOT|g" \
20
20
  (Per-repo installs go in `.github/hooks/spor.json` — that is also the
21
21
  only location the Copilot cloud coding agent loads.) Environment is the
22
22
  usual set: `SPOR_SERVER`, `SPOR_TOKEN`, and `SPOR_DISTILL_CMD`
23
- (e.g. `copilot -p "$(cat)"`) if the claude CLI isn't installed; legacy
24
- `SUBSTRATE_*` names are still read.
23
+ (e.g. `copilot -p "$(cat)"`) if the claude CLI isn't installed — set
24
+ `SPOR_NUDGE_CMD` to the same value for the capture nudge, or `SPOR_NUDGE=0`
25
+ to skip it; legacy `SUBSTRATE_*` names are still read.
25
26
 
26
27
  ## Event mapping
27
28
 
@@ -17,8 +17,9 @@ sed "s|__SPOR_ROOT__|$SPOR_ROOT|g" \
17
17
  (Or merge into an existing `~/.cursor/hooks.json` / project
18
18
  `.cursor/hooks.json`.) Environment is the usual set: `SPOR_SERVER`,
19
19
  `SPOR_TOKEN`, and `SPOR_DISTILL_CMD` if the claude CLI isn't installed
20
- (`cursor-agent -p` works: prompt on stdin, response on stdout); legacy
21
- `SUBSTRATE_*` names are still read.
20
+ (`cursor-agent -p` works: prompt on stdin, response on stdout) — set
21
+ `SPOR_NUDGE_CMD` to the same value for the capture nudge, or `SPOR_NUDGE=0`
22
+ to skip it; legacy `SUBSTRATE_*` names are still read.
22
23
 
23
24
  ## Event mapping
24
25
 
@@ -31,6 +31,9 @@ export SPOR_SERVER=https://spor.example.com # remote mode
31
31
  export SPOR_TOKEN=spor_pat_...
32
32
  # Distiller backend (prompt on stdin, response on stdout):
33
33
  export SPOR_DISTILL_CMD='gemini --model gemini-2.5-flash'
34
+ # Capture-nudge backend (same contract; runs synchronously, so pick a fast
35
+ # model). SPOR_NUDGE=0 disables it; see adapters/README.md for the bounds.
36
+ export SPOR_NUDGE_CMD='gemini --model gemini-2.5-flash'
34
37
  ```
35
38
 
36
39
  (Legacy `SUBSTRATE_*` names are still read.)
@@ -24,6 +24,9 @@ export SPOR_TOKEN=spor_pat_...
24
24
  # Distiller backend (prompt on stdin -> response on stdout); without the
25
25
  # claude CLI installed, route through opencode itself:
26
26
  export SPOR_DISTILL_CMD='opencode run "$(cat)"'
27
+ # Capture-nudge backend — same contract, runs synchronously, so prefer a fast
28
+ # model. SPOR_NUDGE=0 disables it; see adapters/README.md for the bounds.
29
+ export SPOR_NUDGE_CMD='opencode run "$(cat)"'
27
30
  ```
28
31
 
29
32
  ## Behavior mapping
package/bin/spor-hook CHANGED
@@ -3,5 +3,29 @@
3
3
  # kept so existing hooks configs and adapter manifests that exec this path
4
4
  # keep working unchanged. Windows hosts invoke spor-hook.js via node
5
5
  # (or spor-hook.cmd) instead. Fail-open: no node, no output, exit 0.
6
- command -v node >/dev/null 2>&1 || exit 0
7
- exec node "$(dirname "$0")/spor-hook.js" "$@"
6
+ if command -v node >/dev/null 2>&1; then
7
+ exec node "$(dirname "$0")/spor-hook.js" "$@"
8
+ fi
9
+ # No Node interpreter — the hook can do nothing (the client is zero-dep *Node*).
10
+ # Stay fail-open (exit 0, no output, no latency on the working path above), but
11
+ # drop a ONE-TIME greppable breadcrumb so the silence is diagnosable instead of
12
+ # invisible (issue-spor-onboarding-no-node-silent-fail-open). Best-effort: every
13
+ # step is guarded and we exit 0 regardless, so this can never break fail-open.
14
+ # Resolve the graph home the way lib/shell/home.js does: $SPOR_HOME, then the
15
+ # legacy $SUBSTRATE_HOME, else ~/.spor (or ~/.substrate if that legacy dir
16
+ # exists). The breadcrumb is written at most once (guarded by its own marker).
17
+ _spor_home="${SPOR_HOME:-${SUBSTRATE_HOME:-}}"
18
+ if [ -z "$_spor_home" ]; then
19
+ if [ -d "$HOME/.spor" ]; then _spor_home="$HOME/.spor"
20
+ elif [ -d "$HOME/.substrate" ]; then _spor_home="$HOME/.substrate"
21
+ else _spor_home="$HOME/.spor"
22
+ fi
23
+ fi
24
+ _spor_warn="$_spor_home/journal/no-node.warn"
25
+ if [ -n "$HOME" ] && [ ! -f "$_spor_warn" ]; then
26
+ if mkdir -p "$_spor_home/journal" 2>/dev/null; then
27
+ printf '%s spor-hook: node not found on PATH — every hook is a silent no-op. Spor needs a Node interpreter (see package.json engines; `spor status` reports the floor). Install Node, then restart your session.\n' \
28
+ "$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || echo now)" >"$_spor_warn" 2>/dev/null || true
29
+ fi
30
+ fi
31
+ exit 0
package/bin/spor-hook.js CHANGED
@@ -15,6 +15,7 @@
15
15
  // spor-hook post-tool [--host ...]
16
16
  // spor-hook distill [--host ...] [--debounce SECONDS]
17
17
  // spor-hook agents-md [--cwd DIR] # AGENTS.md floor; no stdin
18
+ // spor-hook doctor [--cwd DIR] # client health report; no stdin
18
19
  //
19
20
  // Fail-open contract (dec-cc-fail-open-hooks): any failure exits 0 with no
20
21
  // output — a Spor problem never costs the user their session.
@@ -114,6 +115,23 @@ async function main() {
114
115
  return;
115
116
  }
116
117
 
118
+ // `spor-hook doctor` (task-cc-client-hook-operability-diagnostics piece 3):
119
+ // an operator-run diagnostic, not a host hook — it takes no stdin and prints a
120
+ // human-readable health report. It runs even when the plugin is disabled for
121
+ // the repo (a disabled plugin is exactly what you'd want doctor to tell you).
122
+ // Write the report with fs.writeSync(1) so the full body flushes before the
123
+ // crash handler's process.exit(0) can truncate a piped stdout.
124
+ if (event === "doctor") {
125
+ const { doctor } = require("../scripts/engines/doctor");
126
+ let dCwd = process.cwd();
127
+ const ci = args.indexOf("--cwd");
128
+ if (ci >= 0 && args[ci + 1]) dCwd = args[ci + 1];
129
+ u.useConfig({ cwd: dCwd });
130
+ const report = await doctor();
131
+ fs.writeSync(1, report);
132
+ return;
133
+ }
134
+
117
135
  if (!(event in ENGINES)) {
118
136
  process.stderr.write(`spor-hook: unknown event '${event}'\n`);
119
137
  return;