@gobing-ai/spur 0.3.84 → 0.3.86

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.
Files changed (109) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/config/config.global.yaml +11 -6
  3. package/config/pipeline-budgets.json +0 -25
  4. package/config/plugin-scripts.json +0 -5
  5. package/config/rules/boundary/env-var-hygiene.yaml +41 -0
  6. package/config/transition-shims.json +1 -1
  7. package/config/workflow-candidates.json +41 -0
  8. package/config/workflows/feature-lifecycle.yaml +4 -2
  9. package/config/workflows/feature-verification.yaml +70 -0
  10. package/config/workflows/idea-pipeline.yaml +25 -13
  11. package/config/workflows/task-pipeline.yaml +43 -12
  12. package/config/workflows/wrapup-pipeline.yaml +71 -8
  13. package/package.json +9 -9
  14. package/plugins/sp/README.md +0 -3
  15. package/plugins/sp/agents/super-planner.md +2 -1
  16. package/plugins/sp/hooks/agent-hint.ts +5 -4
  17. package/plugins/sp/hooks/careful-guard.ts +3 -1
  18. package/plugins/sp/hooks/context-post-tool.ts +2 -1
  19. package/plugins/sp/hooks/context-session-start.ts +4 -3
  20. package/plugins/sp/hooks/context-session-stop.ts +2 -1
  21. package/plugins/sp/hooks/pi/guard-extension.ts +4 -3
  22. package/plugins/sp/hooks/task-write-guard.ts +4 -3
  23. package/plugins/sp/lib/idea-handoff.generated.mjs +260 -260
  24. package/plugins/sp/plugin.json +1 -1
  25. package/plugins/sp/scripts/daily-summary/daily-summary.mjs +13 -4
  26. package/plugins/sp/scripts/daily-summary/daily-summary.ts +6 -4
  27. package/plugins/sp/scripts/feature-sync-bounded.mjs +10 -2
  28. package/plugins/sp/scripts/feature-sync-bounded.ts +2 -1
  29. package/plugins/sp/scripts/idea-handoff.mjs +6 -1
  30. package/plugins/sp/scripts/idea-handoff.ts +4 -1
  31. package/plugins/sp/scripts/inline-pipeline-parity-check.ts +1 -1
  32. package/plugins/sp/scripts/inline-run-setup.ts +185 -2
  33. package/plugins/sp/scripts/pr-reviewing.mjs +8 -1
  34. package/plugins/sp/scripts/pr-reviewing.ts +2 -1
  35. package/plugins/sp/scripts/quality-gate.mjs +8 -1
  36. package/plugins/sp/scripts/quality-gate.ts +2 -1
  37. package/plugins/sp/scripts/surface-drift-inventory.ts +2 -5
  38. package/plugins/sp/scripts/task-evidence-precheck.ts +2 -1
  39. package/plugins/sp/scripts/task-size-precheck.ts +6 -4
  40. package/plugins/sp/scripts/verify-answer-lint.ts +2 -1
  41. package/plugins/sp/scripts/workflow-step-profile.mjs +10 -2
  42. package/plugins/sp/scripts/workflow-step-profile.ts +2 -1
  43. package/plugins/sp/scripts/wrapup-steps.mjs +8 -1
  44. package/plugins/sp/scripts/wrapup-steps.ts +2 -1
  45. package/plugins/sp/skills/spur-cli/references/workflows.md +8 -7
  46. package/plugins/sp/skills/spur-dev/references/cross-cutting.md +4 -5
  47. package/plugins/sp/skills/spur-dev/references/done-housekeeping.md +1 -1
  48. package/plugins/sp/skills/spur-dev/references/gate-checklists.md +2 -2
  49. package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +59 -1
  50. package/schemas/state-machine-workflow.schema.json +5 -0
  51. package/schemas/transition-flow-workflow.schema.json +5 -0
  52. package/spur.js +21393 -20831
  53. package/web/_astro/BoardApp.CJiqp5pS.js +1 -0
  54. package/web/_astro/{BoardApp.D4mBUGLT.js → BoardApp.yBBcFXWP.js} +60 -60
  55. package/web/_astro/{TaskDetail.DhCWaOPR.js → TaskDetail.DqFJbRFc.js} +1 -1
  56. package/web/_astro/{arc.D3X0nfq-.js → arc.DL-BpHoi.js} +1 -1
  57. package/web/_astro/{architectureDiagram-3BPJPVTR.BQDoiGme.js → architectureDiagram-3BPJPVTR.9IdQYyDq.js} +1 -1
  58. package/web/_astro/{blockDiagram-GPEHLZMM.BjHekBbM.js → blockDiagram-GPEHLZMM.BKsFCqTl.js} +1 -1
  59. package/web/_astro/{c4Diagram-AAUBKEIU.BvHt171l.js → c4Diagram-AAUBKEIU.DhwI0dh1.js} +1 -1
  60. package/web/_astro/channel.CX5453qQ.js +1 -0
  61. package/web/_astro/{chunk-2J33WTMH.CwxGXlDR.js → chunk-2J33WTMH.B3QVmQ9S.js} +1 -1
  62. package/web/_astro/{chunk-4BX2VUAB.Dia1YtwU.js → chunk-4BX2VUAB.DBSuqs9F.js} +1 -1
  63. package/web/_astro/{chunk-55IACEB6.Oz9epCXa.js → chunk-55IACEB6.BSYTWAYD.js} +1 -1
  64. package/web/_astro/{chunk-727SXJPM.DKFYJjYJ.js → chunk-727SXJPM.cWVuxXfS.js} +1 -1
  65. package/web/_astro/{chunk-AQP2D5EJ.AD7w5oVz.js → chunk-AQP2D5EJ.DpU_Ob3d.js} +1 -1
  66. package/web/_astro/{chunk-FMBD7UC4.D0OqTj52.js → chunk-FMBD7UC4.BykFkyji.js} +1 -1
  67. package/web/_astro/{chunk-ND2GUHAM.CU5ppR7j.js → chunk-ND2GUHAM.DwgHlMdY.js} +1 -1
  68. package/web/_astro/{chunk-QZHKN3VN.Bu1rFztw.js → chunk-QZHKN3VN.CusXUGWM.js} +1 -1
  69. package/web/_astro/{classDiagram-4FO5ZUOK.C0Pgm5yE.js → classDiagram-4FO5ZUOK.fx0ObzkN.js} +1 -1
  70. package/web/_astro/{classDiagram-v2-Q7XG4LA2.C0Pgm5yE.js → classDiagram-v2-Q7XG4LA2.fx0ObzkN.js} +1 -1
  71. package/web/_astro/{cose-bilkent-S5V4N54A.C0ecLb1e.js → cose-bilkent-S5V4N54A.Z4HgOlsd.js} +1 -1
  72. package/web/_astro/{cynefin-OW5HDTMX.CpStVmhO.js → cynefin-OW5HDTMX.B5dIZHJu.js} +1 -1
  73. package/web/_astro/{dagre-BM42HDAG.BPWN3W9f.js → dagre-BM42HDAG.DT70Q_Yw.js} +1 -1
  74. package/web/_astro/{diagram-2AECGRRQ.DnXbEot4.js → diagram-2AECGRRQ.DqHA3XBF.js} +1 -1
  75. package/web/_astro/{diagram-5GNKFQAL.BYJsgUiI.js → diagram-5GNKFQAL.BmCem957.js} +1 -1
  76. package/web/_astro/{diagram-KO2AKTUF.CU4Bqprx.js → diagram-KO2AKTUF.sn0-hrE0.js} +1 -1
  77. package/web/_astro/{diagram-LMA3HP47.BQLOANIu.js → diagram-LMA3HP47.BSHe9tVc.js} +1 -1
  78. package/web/_astro/{diagram-OG6HWLK6.Cn1UVhqj.js → diagram-OG6HWLK6.DHIc-86k.js} +1 -1
  79. package/web/_astro/{erDiagram-TEJ5UH35.C8FdZyvA.js → erDiagram-TEJ5UH35.Bxayrs7v.js} +1 -1
  80. package/web/_astro/{flowDiagram-I6XJVG4X.QSZCR8Uo.js → flowDiagram-I6XJVG4X.BkzoE_5I.js} +1 -1
  81. package/web/_astro/{ganttDiagram-6RSMTGT7.O1Py31MY.js → ganttDiagram-6RSMTGT7.okT6CvTo.js} +1 -1
  82. package/web/_astro/{gitGraphDiagram-PVQCEYII.DoeJRBfY.js → gitGraphDiagram-PVQCEYII.CJuYbhC7.js} +1 -1
  83. package/web/_astro/{index.TPF1FerP.css → index.CcU5weKX.css} +1 -1
  84. package/web/_astro/{infoDiagram-5YYISTIA.9npLJecW.js → infoDiagram-5YYISTIA.RqLy7nBo.js} +1 -1
  85. package/web/_astro/{ishikawaDiagram-YF4QCWOH.DXanvOr3.js → ishikawaDiagram-YF4QCWOH.BwIcoagw.js} +1 -1
  86. package/web/_astro/{journeyDiagram-JHISSGLW.DxbVjxAv.js → journeyDiagram-JHISSGLW.UB1VbWtH.js} +1 -1
  87. package/web/_astro/{kanban-definition-UN3LZRKU.BDk6_iq0.js → kanban-definition-UN3LZRKU.AaxMKpTk.js} +1 -1
  88. package/web/_astro/{linear.C9cCDjT3.js → linear.Nv_xOUjP.js} +1 -1
  89. package/web/_astro/{mermaid.core.CpHRe5nH.js → mermaid.core.Bc4LqQgX.js} +4 -4
  90. package/web/_astro/{mindmap-definition-RKZ34NQL.D-6C85bn.js → mindmap-definition-RKZ34NQL.oKUvU_qi.js} +1 -1
  91. package/web/_astro/{pieDiagram-4H26LBE5.B3Oxodty.js → pieDiagram-4H26LBE5.DQk0oo03.js} +1 -1
  92. package/web/_astro/{quadrantDiagram-W4KKPZXB.QZ_9Hshu.js → quadrantDiagram-W4KKPZXB.BdDjESDa.js} +1 -1
  93. package/web/_astro/{requirementDiagram-4Y6WPE33.BimvuK53.js → requirementDiagram-4Y6WPE33.C2u9hUeH.js} +1 -1
  94. package/web/_astro/{sankeyDiagram-5OEKKPKP.D5Kkpx2C.js → sankeyDiagram-5OEKKPKP.CDEoiJST.js} +1 -1
  95. package/web/_astro/{sequenceDiagram-3UESZ5HK.B4YR6fAE.js → sequenceDiagram-3UESZ5HK.D_hT_GAT.js} +1 -1
  96. package/web/_astro/{stateDiagram-AJRCARHV.Bt2wkZok.js → stateDiagram-AJRCARHV.DI8RYG0b.js} +1 -1
  97. package/web/_astro/{stateDiagram-v2-BHNVJYJU.YZ4X7dfQ.js → stateDiagram-v2-BHNVJYJU.Bkxz4DnP.js} +1 -1
  98. package/web/_astro/{timeline-definition-PNZ67QCA.DmiTBL8w.js → timeline-definition-PNZ67QCA.DSY-kH3-.js} +1 -1
  99. package/web/_astro/{vennDiagram-CIIHVFJN.KNJV40mf.js → vennDiagram-CIIHVFJN.CpaDtuGr.js} +1 -1
  100. package/web/_astro/{wardleyDiagram-YWT4CUSO.BVAaKgWH.js → wardleyDiagram-YWT4CUSO.DujQWvo8.js} +1 -1
  101. package/web/_astro/{xychartDiagram-2RQKCTM6.D5Jx7zB2.js → xychartDiagram-2RQKCTM6.DcM5Y4b9.js} +1 -1
  102. package/web/index.html +2 -2
  103. package/config/workflows/basic.yaml +0 -146
  104. package/config/workflows/docs-pipeline.yaml +0 -350
  105. package/config/workflows/feature-dev.yaml +0 -288
  106. package/plugins/sp/scripts/feature-dev-precheck.mjs +0 -146
  107. package/plugins/sp/scripts/feature-dev-precheck.ts +0 -238
  108. package/web/_astro/BoardApp.DFPYpI5O.js +0 -1
  109. package/web/_astro/channel.BlQwxyoI.js +0 -1
@@ -13,10 +13,12 @@
13
13
  # pauses,
14
14
  # even under --auto (Iron Law #6: irreversible action -> surface to human).
15
15
  #
16
- # Shape: start -> task-resolve -> doc-sync -> metrics-record
16
+ # Shape: start -> task-resolve -> doc-sync -> learnings-append -> metrics-record
17
17
  # -> feature-transition (conditional: if vars.feature set)
18
18
  # -> branch-cleanup (conditional: if vars.merge=true)
19
19
  # -> done
20
+ # doc-sync routes a contract violation to `repair` (cheap, no
21
+ # re-dispatch — ADR-118 pilot) and an executor failure to `failed`;
20
22
  # task-resolve with a VALIDATED empty list short-circuits to `skipped`;
21
23
  # invalid wrap input, an unresolved/non-completed task, a failed metrics
22
24
  # capture, a failed required sync, or a failed feature gate routes to
@@ -31,7 +33,7 @@
31
33
  # merge — set --vars '{"merge":"true"}' to run branch cleanup (irreversible
32
34
  # HITL)
33
35
  # spurBin — PATH-independent spur invocation (overridden by CLI at run start)
34
- # agent — agent for agent.run steps (default: omp)
36
+ # agent — agent for agent.run steps (default: auto → `agent.default` in config)
35
37
  #
36
38
  # Reliability (aligned with task-pipeline / ADR-043):
37
39
  # - Prefer pure slash commands when a command exists; free-form inputs remain
@@ -72,7 +74,7 @@
72
74
  "$schema": "@gobing-ai/spur/schemas/state-machine-workflow.schema.json"
73
75
  kind: state-machine
74
76
  name: wrapup-pipeline
75
- version: "3"
77
+ version: "4"
76
78
  description: "Post-execution wrap-up: doc-sync (doc drift + learning capture), metrics, feature-transition, branch-cleanup"
77
79
  iterationBound: 10
78
80
  initialState: start
@@ -168,13 +170,16 @@ states:
168
170
  learnings (task 0607 R2: two adjacent agent.run hops merged, count 2 -> 1;
169
171
  no operator-decision pause or verdict boundary sits between them). No dedicated
170
172
  slash command yet — free-form skill pointer until a /sp:doc-evolve command
171
- exists (ADR-043 residual). Capture+shell decoupling preserved: expectFile gates
172
- a non-empty capture before the append; append is best-effort soft (empty capture
173
- skips without aborting wrap-up).
173
+ exists (ADR-043 residual). This is the expensive stage; a contract violation
174
+ (a clean exit that missed its declared answerFile/expectFile post-condition)
175
+ routes to the cheap `repair` edge instead of a full re-dispatch (ADR-118
176
+ pilot, 0871) — `onError: continue` lets the transition guards read the
177
+ agent's result instead of halting the run.
174
178
  onEnter:
175
179
  # (warn) free-form prompt: no /sp:doc-evolve command surface yet (ADR-043
176
180
  # residual, 0607 R2 one-query merge); capture is bounded by expectFile.
177
181
  - kind: agent.run
182
+ onError: continue
178
183
  options:
179
184
  agent: ${vars.agent}
180
185
  input: >-
@@ -192,6 +197,13 @@ states:
192
197
  answerFile: .spur/run/${vars.__runId}-wrapup-learnings.md
193
198
  expectFile: .spur/run/${vars.__runId}-wrapup-learnings.md
194
199
  timeoutMs: ${vars.stepTimeoutMs}
200
+
201
+ - id: learnings-append
202
+ description: >
203
+ Best-effort soft append of the captured learnings to .spur/memory/learnings.md
204
+ (0607 R2): an empty capture skips without aborting wrap-up. Runs only after
205
+ doc-sync's contract held; a contract violation never reaches this state.
206
+ onEnter:
195
207
  # (warn) learnings append is intentionally soft (empty capture skips
196
208
  # without aborting wrap-up, 0607 R2).
197
209
  - kind: shell
@@ -207,6 +219,21 @@ states:
207
219
  fi &&
208
220
  exit 0
209
221
 
222
+ - id: repair
223
+ description: >
224
+ Contract-violation repair (ADR-118 pilot, 0871): doc-sync's agent.run missed
225
+ its declared post-condition after a clean exit, so wrap-up does NOT re-dispatch
226
+ the expensive agent on this attempt. The miss is recorded to a run-scoped
227
+ repair status and wrap-up proceeds to metrics-record — the run log and action
228
+ trace already name the violated contract and the observed value.
229
+ onEnter:
230
+ - kind: shell
231
+ options:
232
+ command: >-
233
+ mkdir -p .spur/run &&
234
+ printf 'contract-violation: doc-sync learnings capture missed its declared contract — repair skipped re-dispatch (ADR-118 pilot)\n' > ".spur/run/$__runId-wrapup-repair.status" &&
235
+ exit 0
236
+
210
237
  - id: metrics-record
211
238
  description: >
212
239
  Append one JSONL row per task to .spur/memory/wrapup-metrics.jsonl deterministically.
@@ -354,10 +381,46 @@ transitions:
354
381
  guard:
355
382
  kind: always
356
383
 
357
- # ── linear body: doc-sync -> metrics-record ──
384
+ # ── doc-sync: contract-first routing (ADR-118 pilot, 0871) ──
385
+ # Declaration order matters: the contract-violation edge is tried first, then
386
+ # success (action-ok), then the executor-failure defense (always) — a failed
387
+ # agent.run reaches these guards because doc-sync's agent.run declares
388
+ # onError: continue.
358
389
  - from: doc-sync
390
+ to: repair
391
+ description: >
392
+ Contract violation (ADR-118): the agent exited 0 but missed its declared
393
+ post-condition. Route to the cheap repair edge — never re-dispatch the
394
+ full agent.run on this attempt.
395
+ trigger: contract-violation
396
+ guard:
397
+ kind: contract-violation
398
+ - from: doc-sync
399
+ to: learnings-append
400
+ description: Doc drift repair + learning capture succeeded — append learnings.
401
+ guard:
402
+ kind: action-ok
403
+ - from: doc-sync
404
+ to: failed
405
+ description: >
406
+ Executor failure (non-zero exit, signal, or dispatch error) — keep the
407
+ existing fail semantics; a contract violation never falls through here
408
+ because the contract-violation edge is declared first.
409
+ trigger: executor-failure
410
+ guard:
411
+ kind: always
412
+
413
+ # ── linear body: learnings-append -> metrics-record ──
414
+ - from: learnings-append
415
+ to: metrics-record
416
+ description: Learning capture appended (or soft-skipped) — record metrics.
417
+ guard:
418
+ kind: always
419
+
420
+ # ── repair: cheap path, no re-dispatch -> metrics-record ──
421
+ - from: repair
359
422
  to: metrics-record
360
- description: Doc drift repair + learning capture complete — record metrics.
423
+ description: Contract-violation repair recorded — proceed to metrics without re-dispatch.
361
424
  guard:
362
425
  kind: always
363
426
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gobing-ai/spur",
3
- "version": "0.3.84",
3
+ "version": "0.3.86",
4
4
  "description": "Spur CLI — local-first harness for mainstream coding agents: constraint checking, workflow orchestration, agent health, and history analytics. Bun-native; exposes the `spur` command.",
5
5
  "keywords": [
6
6
  "spur",
@@ -53,14 +53,14 @@
53
53
  },
54
54
  "devDependencies": {
55
55
  "@commander-js/extra-typings": "^14.0.0",
56
- "@gobing-ai/ts-db": "^0.4.66",
57
- "@gobing-ai/ts-ai-runner": "^0.4.66",
58
- "@gobing-ai/ts-dual-workflow-engine": "^0.4.66",
59
- "@gobing-ai/ts-infra": "^0.4.66",
60
- "@gobing-ai/ts-llm-jsonl-importer": "^0.4.66",
61
- "@gobing-ai/ts-rule-engine": "^0.4.66",
62
- "@gobing-ai/ts-runtime": "^0.4.66",
63
- "@gobing-ai/ts-utils": "^0.4.66",
56
+ "@gobing-ai/ts-db": "^0.4.67",
57
+ "@gobing-ai/ts-ai-runner": "^0.4.67",
58
+ "@gobing-ai/ts-dual-workflow-engine": "^0.4.67",
59
+ "@gobing-ai/ts-infra": "^0.4.67",
60
+ "@gobing-ai/ts-llm-jsonl-importer": "^0.4.67",
61
+ "@gobing-ai/ts-rule-engine": "^0.4.67",
62
+ "@gobing-ai/ts-runtime": "^0.4.67",
63
+ "@gobing-ai/ts-utils": "^0.4.67",
64
64
  "@types/bun": "1.3.14",
65
65
  "@types/figlet": "^1.7.0",
66
66
  "@types/node-notifier": "8.0.5",
@@ -620,14 +620,11 @@ pipeline owns one lifecycle phase:
620
620
 
621
621
  | Workflow | Phase | Entry command |
622
622
  | --------------------------- | --------------------------------- | --------------------------------- |
623
- | `basic.yaml` | Generic implement/check/fix | direct `spur workflow run` |
624
623
  | `feature-lifecycle.yaml` | Feature status FSM | `spur feature update` |
625
624
  | `task-lifecycle.yaml` | Task status FSM | `spur task update` |
626
625
  | `task-pipeline.yaml` | Single-task execution | `/sp:dev-run` |
627
626
  | `idea-pipeline.yaml` | Idea/planning → feature + tasks | `/sp:dev-idea`, `/sp:dev-plan` |
628
- | `feature-dev.yaml` | Feature umbrella execution | `/sp:dev-runall --feature` |
629
627
  | `wrapup-pipeline.yaml` | Post-execution wrap-up | `/sp:dev-wrap`, `/sp:dev-wrapall` |
630
- | `docs-pipeline.yaml` | Docs-only task execution | `/sp:dev-run --mode implement` |
631
628
  | `wayfinder-resolution.yaml` | Wayfinder ticket resolution loop | `spur workflow run` (free-form) |
632
629
 
633
630
  ### Lifecycle operations
@@ -97,7 +97,8 @@ You own the spaces **between** task runs:
97
97
  You explicitly do **NOT** own step-level execution:
98
98
 
99
99
  - How an `agent.run` step (implement/test/review/verify) runs is `vars.agent`'s concern - default
100
- `omp`, pinned in `task-pipeline.yaml`. `--agent <value>` from the command flows into each
100
+ `auto` (`agent.default` in config resolves the executor), pinned in `task-pipeline.yaml`.
101
+ `--agent <value>` from the command flows into each
101
102
  per-task `vars.agent`; you forward it, you do not interpret it.
102
103
  - You never edit the pipeline YAML, never reach into a step, and never decide how a single
103
104
  `agent.run` stage executes. The per-task pipeline is invoked **verbatim**.
@@ -1,3 +1,4 @@
1
+ import { getEnvVars } from '@gobing-ai/ts-utils';
1
2
  /**
2
3
  * Shared host-agent identification helpers — the **only** host-agent detection path
3
4
  * in the sp plugin hooks.
@@ -22,11 +23,11 @@
22
23
  * 3. `TERM_PROGRAM` — terminal identifier (e.g. `claude`, `ghostty`)
23
24
  * 4. `SPUR_DEFAULT_AGENT` — configured default agent
24
25
  *
25
- * @param env - environment record (defaults to `process.env`)
26
+ * @param env - environment record (defaults to `getEnvVars()`)
26
27
  * @param fallback - value returned when no candidate matches (defaults to `undefined`)
27
28
  */
28
29
  export function resolveAgentHint(
29
- env: NodeJS.ProcessEnv = process.env,
30
+ env: NodeJS.ProcessEnv = getEnvVars(),
30
31
  fallback: string | undefined = undefined,
31
32
  ): string | undefined {
32
33
  const candidates = [env.SPUR_AGENT, env.CLAUDE_CODE_ENTRYPOINT, env.TERM_PROGRAM, env.SPUR_DEFAULT_AGENT];
@@ -45,11 +46,11 @@ export function resolveAgentHint(
45
46
  * 3. `OPENAI_MODEL` — OpenAI API model
46
47
  * 4. `CLAUDE_MODEL` — Claude Code model setting
47
48
  *
48
- * @param env - environment record (defaults to `process.env`)
49
+ * @param env - environment record (defaults to `getEnvVars()`)
49
50
  * @param fallback - value returned when no candidate matches (defaults to `undefined`)
50
51
  */
51
52
  export function resolveModelHint(
52
- env: NodeJS.ProcessEnv = process.env,
53
+ env: NodeJS.ProcessEnv = getEnvVars(),
53
54
  fallback: string | undefined = undefined,
54
55
  ): string | undefined {
55
56
  const candidates = [env.SPUR_MODEL, env.ANTHROPIC_MODEL, env.OPENAI_MODEL, env.CLAUDE_MODEL];
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env bun
2
+
2
3
  /**
3
4
  * careful-guard — PreToolUse guard for destructive shell commands (task 0215, R3).
4
5
  *
@@ -21,6 +22,7 @@
21
22
  * adapter. This file owns the Claude Code payload/decision shape only.
22
23
  */
23
24
 
25
+ import { getEnvVar } from '@gobing-ai/ts-utils';
24
26
  import { classifyCommand } from './destructive-policy';
25
27
 
26
28
  interface ToolPayload {
@@ -43,7 +45,7 @@ function preToolUseDecision(decision: Decision, reason?: string): never {
43
45
  }
44
46
 
45
47
  async function main(): Promise<void> {
46
- if (process.env.SPUR_CAREFUL === 'off') preToolUseDecision('allow');
48
+ if (getEnvVar('SPUR_CAREFUL') === 'off') preToolUseDecision('allow');
47
49
 
48
50
  const stdinText = await Bun.stdin.text();
49
51
  let payload: ToolPayload;
@@ -23,6 +23,7 @@
23
23
  import { execSync } from 'node:child_process';
24
24
  import { appendFileSync, existsSync, readFileSync, statSync, writeFileSync } from 'node:fs';
25
25
  import { join, resolve, sep } from 'node:path';
26
+ import { getEnvVar } from '@gobing-ai/ts-utils';
26
27
 
27
28
  /** Tools recorded by this hook (must match hooks.json PostToolUse matcher). */
28
29
  export const ALLOWED_TOOLS = new Set(['Bash', 'Grep', 'Glob', 'Read', 'Write', 'Edit']);
@@ -411,7 +412,7 @@ export function readContextFreshness(contextDir: string): string | null {
411
412
  // Entrypoint — thin wrapper; logic lives in {@link recordToolUseEvent} for unit coverage.
412
413
  if (import.meta.main) {
413
414
  void (async () => {
414
- const dir = join(process.env.CLAUDE_PROJECT_DIR ?? process.cwd(), '.spur', 'context');
415
+ const dir = join(getEnvVar('CLAUDE_PROJECT_DIR') ?? process.cwd(), '.spur', 'context');
415
416
  try {
416
417
  const stdinText = await Bun.stdin.text();
417
418
  const payload = JSON.parse(stdinText) as ToolPayload;
@@ -15,6 +15,7 @@
15
15
  import { execSync } from 'node:child_process';
16
16
  import { appendFileSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
17
17
  import { join } from 'node:path';
18
+ import { getEnvVar, getEnvVars } from '@gobing-ai/ts-utils';
18
19
  import { resolveAgentHint, resolveModelHint } from './agent-hint';
19
20
  import { checkContextFreshness } from './context-post-tool';
20
21
 
@@ -82,7 +83,7 @@ interface SessionFileBody {
82
83
  * a stale file is only reachable from inside a live agent run, where reusing it is the correct
83
84
  * answer anyway.
84
85
  */
85
- export function resolveActiveSession(dir: string, now: Date, env: NodeJS.ProcessEnv = process.env): string | null {
86
+ export function resolveActiveSession(dir: string, now: Date, env: NodeJS.ProcessEnv = getEnvVars()): string | null {
86
87
  let raw: string;
87
88
  try {
88
89
  raw = readFileSync(join(dir, '.session.json'), 'utf-8');
@@ -123,7 +124,7 @@ export function resolveActiveSession(dir: string, now: Date, env: NodeJS.Process
123
124
  */
124
125
  export function recordSessionStart(
125
126
  dir: string,
126
- env: NodeJS.ProcessEnv = process.env,
127
+ env: NodeJS.ProcessEnv = getEnvVars(),
127
128
  now: () => Date = () => new Date(),
128
129
  ): string | null {
129
130
  try {
@@ -195,7 +196,7 @@ export function recordSessionStart(
195
196
  // Entrypoint — kept minimal so unit coverage focuses on pure helpers above.
196
197
  if (import.meta.main) {
197
198
  try {
198
- recordSessionStart(join(process.env.CLAUDE_PROJECT_DIR ?? process.cwd(), '.spur', 'context'));
199
+ recordSessionStart(join(getEnvVar('CLAUDE_PROJECT_DIR') ?? process.cwd(), '.spur', 'context'));
199
200
  } catch {
200
201
  /* fail-open */
201
202
  }
@@ -17,6 +17,7 @@
17
17
 
18
18
  import { appendFileSync, existsSync, readFileSync, rmSync } from 'node:fs';
19
19
  import { join } from 'node:path';
20
+ import { getEnvVar } from '@gobing-ai/ts-utils';
20
21
 
21
22
  interface LedgerEvent {
22
23
  session: string;
@@ -53,7 +54,7 @@ function exitOk(): never {
53
54
  }
54
55
 
55
56
  async function main(): Promise<void> {
56
- const dir = join(process.env.CLAUDE_PROJECT_DIR ?? process.cwd(), '.spur', 'context');
57
+ const dir = join(getEnvVar('CLAUDE_PROJECT_DIR') ?? process.cwd(), '.spur', 'context');
57
58
 
58
59
  const sessionFile = join(dir, '.session.json');
59
60
  if (!existsSync(sessionFile)) exitOk();
@@ -23,6 +23,7 @@ import { appendFileSync, existsSync, mkdirSync, readFileSync, realpathSync, rmSy
23
23
  import { homedir } from 'node:os';
24
24
  import { isAbsolute, join, resolve } from 'node:path';
25
25
  import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
26
+ import { getEnvVar, getEnvVars } from '@gobing-ai/ts-utils';
26
27
  import { resolveAgentHint as resolveAgentHintShared, resolveModelHint as resolveModelHintShared } from '../agent-hint';
27
28
  import { classifyCommand } from '../destructive-policy';
28
29
  import { couldBeTaskFile } from '../task-file-policy';
@@ -81,7 +82,7 @@ function resolveSpurTaskOwnership(filePath: string): TaskOwnership {
81
82
  spawnSync(cmd, args, { cwd: process.cwd(), encoding: 'utf-8', timeout: 8000 });
82
83
 
83
84
  // 1. SPUR_BIN env override (may include args) or `spur` on PATH
84
- const envBin = process.env.SPUR_BIN || 'spur';
85
+ const envBin = getEnvVar('SPUR_BIN') || 'spur';
85
86
  const envParts = envBin.split(' ');
86
87
  let res = run(envParts[0] ?? 'spur', [...envParts.slice(1), 'task', 'resolve', filePath, '--strict', '--json']);
87
88
  // Only 0 (owned) / 1 (unowned) are valid spur exit codes; 127 (interpreter
@@ -203,8 +204,8 @@ function initSession(): void {
203
204
  const sessionId = generateSessionId();
204
205
  const session = {
205
206
  session_id: sessionId,
206
- agent: resolveAgentHintShared(process.env, 'pi'),
207
- model: resolveModelHintShared(process.env),
207
+ agent: resolveAgentHintShared(getEnvVars(), 'pi'),
208
+ model: resolveModelHintShared(getEnvVars()),
208
209
  started_at: new Date().toISOString(),
209
210
  };
210
211
  writeFileSync(sessionFilePath(), `${JSON.stringify(session, null, 2)}\n`);
@@ -20,6 +20,7 @@
20
20
  */
21
21
 
22
22
  import { spawnSync } from 'node:child_process';
23
+ import { getEnvVar } from '@gobing-ai/ts-utils';
23
24
  import { couldBeTaskFile } from './task-file-policy';
24
25
 
25
26
  interface ToolPayload {
@@ -40,7 +41,7 @@ function preToolUseDecision(decision: 'allow' | 'deny', reason?: string): never
40
41
 
41
42
  /** Resolve whether a path is owned by a Spur task via `spur task resolve --strict --json`. */
42
43
  function resolveSpurTaskOwnership(filePath: string, cwd: string): TaskOwnership {
43
- const spurBin = process.env.SPUR_BIN || 'spur';
44
+ const spurBin = getEnvVar('SPUR_BIN') || 'spur';
44
45
  const parts = spurBin.split(' ');
45
46
  const cmd = parts[0] ?? 'spur';
46
47
  const args = [...parts.slice(1), 'task', 'resolve', filePath, '--strict', '--json'];
@@ -54,7 +55,7 @@ function resolveSpurTaskOwnership(filePath: string, cwd: string): TaskOwnership
54
55
  }
55
56
 
56
57
  async function main(): Promise<void> {
57
- if (process.env.SPUR_WRITE_GUARD === 'off') preToolUseDecision('allow');
58
+ if (getEnvVar('SPUR_WRITE_GUARD') === 'off') preToolUseDecision('allow');
58
59
 
59
60
  const stdinText = await Bun.stdin.text();
60
61
  let payload: ToolPayload;
@@ -73,7 +74,7 @@ async function main(): Promise<void> {
73
74
  // cannot name a task file — that is every ordinary source edit.
74
75
  if (!couldBeTaskFile(filePath)) preToolUseDecision('allow');
75
76
 
76
- const ownership = resolveSpurTaskOwnership(filePath, process.env.CLAUDE_PROJECT_DIR ?? process.cwd());
77
+ const ownership = resolveSpurTaskOwnership(filePath, getEnvVar('CLAUDE_PROJECT_DIR') ?? process.cwd());
77
78
  if (ownership === 'owned') {
78
79
  preToolUseDecision(
79
80
  'deny',