@gobing-ai/spur 0.3.70 → 0.3.72

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 (74) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/config/workflow-composition-baseline.json +7 -7
  3. package/config/workflows/idea-pipeline.yaml +4 -3
  4. package/config/workflows/task-pipeline.yaml +31 -11
  5. package/config/workflows/wrapup-pipeline.yaml +10 -4
  6. package/package.json +9 -9
  7. package/plugins/sp/commands/dev-fixall.md +4 -3
  8. package/plugins/sp/plugin.json +1 -1
  9. package/plugins/sp/scripts/daily-summary/daily-summary.mjs +121 -0
  10. package/plugins/sp/scripts/daily-summary/daily-summary.ts +204 -0
  11. package/plugins/sp/scripts/verify-answer-lint.ts +12 -3
  12. package/plugins/sp/skills/code-implementation/SKILL.md +1 -1
  13. package/plugins/sp/skills/spec-decomposition/SKILL.md +1 -1
  14. package/plugins/sp/skills/spur-cli/references/history.md +7 -6
  15. package/plugins/sp/skills/spur-dev/references/dev-operations.md +2 -2
  16. package/plugins/sp/skills/spur-dev/references/execution-workflow.md +8 -1
  17. package/plugins/sp/skills/spur-dev/references/gate-checklists.md +1 -1
  18. package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +25 -0
  19. package/schemas/spur-config.schema.json +35 -0
  20. package/spur.js +3117 -1485
  21. package/web/_astro/BoardApp.BYCNkMOn.js +185 -0
  22. package/web/_astro/BoardApp.E12MFjOS.js +1 -0
  23. package/web/_astro/{TaskDetail.DKzpkDj5.js → TaskDetail.CgUreSP2.js} +1 -1
  24. package/web/_astro/{arc.Bsa0gprH.js → arc.BySSh34M.js} +1 -1
  25. package/web/_astro/{architectureDiagram-3BPJPVTR.LOUZCDBC.js → architectureDiagram-3BPJPVTR.DM46TS_h.js} +1 -1
  26. package/web/_astro/{blockDiagram-GPEHLZMM.CGee4isG.js → blockDiagram-GPEHLZMM.tZhvNUHA.js} +1 -1
  27. package/web/_astro/{c4Diagram-AAUBKEIU.B2CrQJ_O.js → c4Diagram-AAUBKEIU.PT4Or4Nf.js} +1 -1
  28. package/web/_astro/channel.5cYKr5cs.js +1 -0
  29. package/web/_astro/{chunk-2J33WTMH.DJvf8RiP.js → chunk-2J33WTMH.J9r0_Bbe.js} +1 -1
  30. package/web/_astro/{chunk-4BX2VUAB.B_l35O-Y.js → chunk-4BX2VUAB.hzyeIvhR.js} +1 -1
  31. package/web/_astro/{chunk-55IACEB6.DqbxMswU.js → chunk-55IACEB6.B0rO7qVh.js} +1 -1
  32. package/web/_astro/{chunk-727SXJPM.D_7ZGNjE.js → chunk-727SXJPM.wE_Uk5D4.js} +1 -1
  33. package/web/_astro/{chunk-AQP2D5EJ.Cq6yZ0s8.js → chunk-AQP2D5EJ.DqEEjQw7.js} +1 -1
  34. package/web/_astro/{chunk-FMBD7UC4.BWrlUPYi.js → chunk-FMBD7UC4.CDoD9sBX.js} +1 -1
  35. package/web/_astro/{chunk-ND2GUHAM.BPLaXFZH.js → chunk-ND2GUHAM.CtX5nF9P.js} +1 -1
  36. package/web/_astro/{chunk-QZHKN3VN.BtmAEAwZ.js → chunk-QZHKN3VN.CK_EwfaT.js} +1 -1
  37. package/web/_astro/{classDiagram-4FO5ZUOK.CWycT7Ia.js → classDiagram-4FO5ZUOK.DLt5a8Lh.js} +1 -1
  38. package/web/_astro/{classDiagram-v2-Q7XG4LA2.CWycT7Ia.js → classDiagram-v2-Q7XG4LA2.DLt5a8Lh.js} +1 -1
  39. package/web/_astro/{cose-bilkent-S5V4N54A.DyxjSWon.js → cose-bilkent-S5V4N54A.CMCWP49h.js} +1 -1
  40. package/web/_astro/{cynefin-OW5HDTMX.DnNfLBpX.js → cynefin-OW5HDTMX.HyXw_vdS.js} +1 -1
  41. package/web/_astro/{dagre-BM42HDAG.BFAvnjKD.js → dagre-BM42HDAG.BTuAzh01.js} +1 -1
  42. package/web/_astro/{diagram-2AECGRRQ.BAoNCFeB.js → diagram-2AECGRRQ.D9dr9wfT.js} +1 -1
  43. package/web/_astro/{diagram-5GNKFQAL.DiiKe7BP.js → diagram-5GNKFQAL.C4Rot0hj.js} +1 -1
  44. package/web/_astro/{diagram-KO2AKTUF.t_uOQWVP.js → diagram-KO2AKTUF.B_TK5uWC.js} +1 -1
  45. package/web/_astro/{diagram-LMA3HP47.SB9997cP.js → diagram-LMA3HP47.JkXKK7CO.js} +1 -1
  46. package/web/_astro/{diagram-OG6HWLK6.Bgsxgf5G.js → diagram-OG6HWLK6.BzMN8Bd6.js} +1 -1
  47. package/web/_astro/{erDiagram-TEJ5UH35.DO9OAOAc.js → erDiagram-TEJ5UH35.DVZaWGUd.js} +1 -1
  48. package/web/_astro/{flowDiagram-I6XJVG4X.CIb41ZyL.js → flowDiagram-I6XJVG4X.rjEiWUfR.js} +1 -1
  49. package/web/_astro/{ganttDiagram-6RSMTGT7.Dc8Lk0fV.js → ganttDiagram-6RSMTGT7.C_EgAarK.js} +1 -1
  50. package/web/_astro/{gitGraphDiagram-PVQCEYII.CpPmMWrl.js → gitGraphDiagram-PVQCEYII.B-QQSDsK.js} +1 -1
  51. package/web/_astro/index.B5MTfe7k.css +1 -0
  52. package/web/_astro/{infoDiagram-5YYISTIA.DxBXfQSf.js → infoDiagram-5YYISTIA.DlWesz7T.js} +1 -1
  53. package/web/_astro/{ishikawaDiagram-YF4QCWOH.gA6OKtXq.js → ishikawaDiagram-YF4QCWOH.BUMZOawi.js} +1 -1
  54. package/web/_astro/{journeyDiagram-JHISSGLW.CMEyLMmm.js → journeyDiagram-JHISSGLW.CWfkxfjY.js} +1 -1
  55. package/web/_astro/{kanban-definition-UN3LZRKU.CKXlUNCr.js → kanban-definition-UN3LZRKU.B-YpMwXf.js} +1 -1
  56. package/web/_astro/{linear.CpTGdVx3.js → linear.D7uqzENp.js} +1 -1
  57. package/web/_astro/{mermaid.core.c8SSCsE5.js → mermaid.core.CxrNppBD.js} +4 -4
  58. package/web/_astro/{mindmap-definition-RKZ34NQL.BH59HDw6.js → mindmap-definition-RKZ34NQL.B4Qe7cM2.js} +1 -1
  59. package/web/_astro/{pieDiagram-4H26LBE5.DmgFXkZR.js → pieDiagram-4H26LBE5.Ds-5j2ro.js} +1 -1
  60. package/web/_astro/{quadrantDiagram-W4KKPZXB.DyfB7N4J.js → quadrantDiagram-W4KKPZXB.tBd38uNC.js} +1 -1
  61. package/web/_astro/{requirementDiagram-4Y6WPE33.DosszoFo.js → requirementDiagram-4Y6WPE33.sFENkWl3.js} +1 -1
  62. package/web/_astro/{sankeyDiagram-5OEKKPKP.pBkHhvVN.js → sankeyDiagram-5OEKKPKP.BeB-Hk7C.js} +1 -1
  63. package/web/_astro/{sequenceDiagram-3UESZ5HK.BE8tmkNR.js → sequenceDiagram-3UESZ5HK.DnTeaSpx.js} +1 -1
  64. package/web/_astro/{stateDiagram-AJRCARHV.C4n7vDiG.js → stateDiagram-AJRCARHV.B-8Jt5EJ.js} +1 -1
  65. package/web/_astro/{stateDiagram-v2-BHNVJYJU.BpgwYlRy.js → stateDiagram-v2-BHNVJYJU.Br7xoqMW.js} +1 -1
  66. package/web/_astro/{timeline-definition-PNZ67QCA.DvcVKc-W.js → timeline-definition-PNZ67QCA.C-3WdOyi.js} +1 -1
  67. package/web/_astro/{vennDiagram-CIIHVFJN.BLOk-UOl.js → vennDiagram-CIIHVFJN.DCIs7Lc6.js} +1 -1
  68. package/web/_astro/{wardleyDiagram-YWT4CUSO.CIzB7M2o.js → wardleyDiagram-YWT4CUSO.rGAL-bbz.js} +1 -1
  69. package/web/_astro/{xychartDiagram-2RQKCTM6.CGC_lZ19.js → xychartDiagram-2RQKCTM6.hkfQKiRl.js} +1 -1
  70. package/web/index.html +2 -2
  71. package/web/_astro/BoardApp.DHj-03Dp.js +0 -1
  72. package/web/_astro/BoardApp.DOadeEJV.js +0 -178
  73. package/web/_astro/channel.BGL7KSHC.js +0 -1
  74. package/web/_astro/index.C-t8kB0T.css +0 -1
@@ -7,7 +7,7 @@
7
7
  "plugins": [
8
8
  {
9
9
  "name": "sp",
10
- "version": "0.3.70",
10
+ "version": "0.3.72",
11
11
  "source": "./plugins/sp"
12
12
  }
13
13
  ]
@@ -117,7 +117,7 @@
117
117
  },
118
118
  "discovery:onEnter:0": {
119
119
  "kind": "agent.run",
120
- "invocation": "Run sp:brainstorm for the idea: ${vars.idea}. The skill owns the approach-generation, design summary, and `needs_design` signal criteria; emit .spur/run/${vars.__runId}-idea-needs-design.json ({\"needs_design\": true|false}) and the design summary per the skill's `Design Approval Gate` and `The needs_design signal` sections. Also emit the idea-evaluation report to .spur/run/${vars.__runId}-idea-eval-report.md per plugins/sp/skills/spur-dev/references/idea-evaluation.md (urgency/necessity 0–5, premises, pros/cons, alternatives, enhanced idea, recommendation). At the END of the report, append a provenance footer block of the exact form: `---\\nrun_id: ${vars.__runId}\\ngenerated_at: <RFC3339 timestamp>\\n---` (omit the footer only if run_id is empty)."
120
+ "invocation": "Run sp:brainstorm for the idea: ${vars.idea}. The skill owns the approach-generation, design summary, and `needs_design` signal criteria; emit .spur/run/${vars.__runId}-idea-needs-design.json ({\"needs_design\": true|false}) and the design summary per the skill's `Design Approval Gate` and `The needs_design signal` sections. Also emit the idea-evaluation report to .spur/run/${vars.__runId}-idea-eval-report.md per the `sp:spur-dev` skill's `idea-evaluation` reference (urgency/necessity 0–5, premises, pros/cons, alternatives, enhanced idea, recommendation). At the END of the report, append a provenance footer block of the exact form: `---\\nrun_id: ${vars.__runId}\\ngenerated_at: <RFC3339 timestamp>\\n---` (omit the footer only if run_id is empty)."
121
121
  },
122
122
  "idea-eval:onEnter:0": {
123
123
  "kind": "hitl.confirm"
@@ -177,7 +177,7 @@
177
177
  },
178
178
  "decompose:onEnter:1": {
179
179
  "kind": "agent.run",
180
- "invocation": "Run sp:spec-decomposition for feature ${vars.featureId}. Read the brainstorm artifact, feature AC, and design doc. SIZING FIRST, before any JSON: apply the skill's `Default to NOT decomposing` rubric to the whole unit of work — if it scores 0-2 the correct output is a ONE-entry batch, not many. Scenario count is not task count: merge scenarios that one task delivers (same file surface, same subsystem, or unreadable apart in review), and list every scenario a task covers in its background. Merging never costs AC coverage — one task may carry several scenarios. Do not emit one entry per scenario or per requirement by reflex. Then produce a task-batch JSON array at .spur/run/${vars.__runId}-idea-task-batch.json, validated against apps/cli/schemas/task-batch.schema.json. Schema-permitted fields per entry: `name`, `background`, `requirements`, `feature_id`, `parent_wbs`, `priority`, `tags`, `template` — schema validation rejects anything else. Acceptance Criteria, Design, and Plan sections are filled in by the per-task refine step after batch-create, NOT at decompose time. Validate locally against the schema before emitting. Also emit the private task-order sidecar at .spur/run/${vars.__runId}-idea-task-order.json: a JSON array (one entry per batch item) of `{ name: <exact batch item name>, depends_on_names: [<batch item names>] }` declaring ordering/dependencies between the batch items; use `[]` when no ordering exists. Every `name` and every dependency must match exactly one batch item `name` — it is private workflow data, not part of task-batch.schema.json."
180
+ "invocation": "Run sp:spec-decomposition for feature ${vars.featureId}. Read the brainstorm artifact, feature AC, and design doc. SIZING FIRST, before any JSON: apply the skill's `Default to NOT decomposing` rubric to the whole unit of work — if it scores 0-2 the correct output is a ONE-entry batch, not many. Scenario count is not task count: merge scenarios that one task delivers (same file surface, same subsystem, or unreadable apart in review), and list every scenario a task covers in its background. Merging never costs AC coverage — one task may carry several scenarios. Do not emit one entry per scenario or per requirement by reflex. Then produce a task-batch JSON array at .spur/run/${vars.__runId}-idea-task-batch.json, validated against task-batch.schema.json. Schema-permitted fields per entry: `name`, `background`, `requirements`, `feature_id`, `parent_wbs`, `priority`, `tags`, `template` — schema validation rejects anything else. Acceptance Criteria, Design, and Plan sections are filled in by the per-task refine step after batch-create, NOT at decompose time. Validate locally against the schema before emitting. Also emit the private task-order sidecar at .spur/run/${vars.__runId}-idea-task-order.json: a JSON array (one entry per batch item) of `{ name: <exact batch item name>, depends_on_names: [<batch item names>] }` declaring ordering/dependencies between the batch items; use `[]` when no ordering exists. Every `name` and every dependency must match exactly one batch item `name` — it is private workflow data, not part of task-batch.schema.json."
181
181
  },
182
182
  "decompose:onEnter:2": {
183
183
  "kind": "shell",
@@ -230,11 +230,11 @@
230
230
  },
231
231
  "precheck:onEnter:3": {
232
232
  "kind": "shell",
233
- "invocation": "SIZE_FILE=\".spur/run/$wbs-precheck-size.status\" && mkdir -p .spur/run && if [ -f plugins/sp/scripts/task-size-precheck.ts ]; then bun plugins/sp/scripts/task-size-precheck.ts \"$wbs\" --spur-bin \"$spurBin\" --max-reqs \"$maxImplementReqs\" --max-plan-items \"$maxImplementPlanItems\"; else\n echo \"task-size-precheck failed closed — checker script\" >&2 &&\n echo \"plugins/sp/scripts/task-size-precheck.ts absent.\" >&2 &&\n echo \"FAIL\" > \"$SIZE_FILE\";\nfi && exit 0"
233
+ "invocation": "SIZE_FILE=\".spur/run/$wbs-precheck-size.status\" && mkdir -p .spur/run && SIZE_SCRIPT=\"plugins/sp/scripts/task-size-precheck.ts\"; [ -f \"$SIZE_SCRIPT\" ] || SIZE_SCRIPT=\"$(superskill script path sp task-size-precheck.ts 2>/dev/null)\"; if [ -n \"$SIZE_SCRIPT\" ] && [ -f \"$SIZE_SCRIPT\" ]; then bun \"$SIZE_SCRIPT\" \"$wbs\" --spur-bin \"$spurBin\" --max-reqs \"$maxImplementReqs\" --max-plan-items \"$maxImplementPlanItems\"; else\n echo \"task-size-precheck failed closed — checker not found in\" >&2 &&\n echo \"plugins/sp/scripts/ nor staged; run 'superskill install sp'.\" >&2 &&\n echo \"FAIL\" > \"$SIZE_FILE\";\nfi && exit 0"
234
234
  },
235
235
  "precheck:onEnter:4": {
236
236
  "kind": "shell",
237
- "invocation": "EVID_FILE=\".spur/run/$wbs-precheck-evidence.status\" && mkdir -p .spur/run && if [ -f plugins/sp/scripts/task-evidence-precheck.ts ]; then bun plugins/sp/scripts/task-evidence-precheck.ts \"$wbs\" --spur-bin \"$spurBin\"; else\n echo \"task-evidence-precheck failed closed —\" >&2 &&\n echo \"plugins/sp/scripts/task-evidence-precheck.ts absent.\" >&2 &&\n echo \"FAIL\" > \"$EVID_FILE\";\nfi && exit 0"
237
+ "invocation": "EVID_FILE=\".spur/run/$wbs-precheck-evidence.status\" && mkdir -p .spur/run && EVID_SCRIPT=\"plugins/sp/scripts/task-evidence-precheck.ts\"; [ -f \"$EVID_SCRIPT\" ] || EVID_SCRIPT=\"$(superskill script path sp task-evidence-precheck.ts 2>/dev/null)\"; if [ -n \"$EVID_SCRIPT\" ] && [ -f \"$EVID_SCRIPT\" ]; then bun \"$EVID_SCRIPT\" \"$wbs\" --spur-bin \"$spurBin\"; else\n echo \"task-evidence-precheck failed closed — checker not found\" >&2 &&\n echo \"in plugins/sp/scripts/ nor staged; run 'superskill install sp'.\" >&2 &&\n echo \"FAIL\" > \"$EVID_FILE\";\nfi && exit 0"
238
238
  },
239
239
  "implement:onEnter:0": {
240
240
  "kind": "agent.run",
@@ -299,7 +299,7 @@
299
299
  },
300
300
  "verify:onEnter:2": {
301
301
  "kind": "shell",
302
- "invocation": "bun plugins/sp/scripts/verify-answer-lint.ts \"$wbs\" --answer \".spur/run/$wbs-verify-answer.txt\" --spur-bin \"$spurBin\""
302
+ "invocation": "LINT_SCRIPT=\"plugins/sp/scripts/verify-answer-lint.ts\"; [ -f \"$LINT_SCRIPT\" ] || LINT_SCRIPT=\"$(superskill script path sp verify-answer-lint.ts 2>/dev/null)\"; if [ -z \"$LINT_SCRIPT\" ] || [ ! -f \"$LINT_SCRIPT\" ]; then\n echo \"verify-answer-lint: checker not found in plugins/sp/scripts/ nor staged — run 'superskill install sp'\" >&2;\n exit 1;\nfi; bun \"$LINT_SCRIPT\" \"$wbs\" --answer \".spur/run/$wbs-verify-answer.txt\" --spur-bin \"$spurBin\""
303
303
  },
304
304
  "verify:onEnter:3": {
305
305
  "kind": "shell",
@@ -318,7 +318,7 @@
318
318
  },
319
319
  "record:onEnter:2": {
320
320
  "kind": "shell",
321
- "invocation": "FID=$($spurBin task show $wbs --json 2>/dev/null | jq -r \".feature_id // .frontmatter.feature_id // empty\" 2>/dev/null); if [ -n \"$FID\" ]; then\n if [ -f plugins/sp/scripts/feature-sync-bounded.ts ]; then\n bun plugins/sp/scripts/feature-sync-bounded.ts \"$FID\" --spur-bin \"$spurBin\" --json;\n else\n $spurBin feature sync \"$FID\" --json;\n fi;\nelse\n echo \"Orphan task $wbs — no feature_id linked; proposal: consider linking to a parent feature.\" >> \".spur/run/$wbs-report.txt\";\nfi; exit 0"
321
+ "invocation": "FID=$($spurBin task show $wbs --json 2>/dev/null | jq -r \".feature_id // .frontmatter.feature_id // empty\" 2>/dev/null); if [ -n \"$FID\" ]; then\n if [ -f plugins/sp/scripts/feature-sync-bounded.ts ]; then\n bun plugins/sp/scripts/feature-sync-bounded.ts \"$FID\" --spur-bin \"$spurBin\" --json;\n elif SYNC_MJS=\"$(superskill script path sp feature-sync-bounded.mjs 2>/dev/null)\" && [ -f \"$SYNC_MJS\" ]; then\n node \"$SYNC_MJS\" \"$FID\" --spur-bin \"$spurBin\" --json;\n else\n $spurBin feature sync \"$FID\" --json;\n fi;\nelse\n echo \"Orphan task $wbs — no feature_id linked; proposal: consider linking to a parent feature.\" >> \".spur/run/$wbs-report.txt\";\nfi; exit 0"
322
322
  },
323
323
  "done:onEnter:0": {
324
324
  "kind": "shell",
@@ -367,7 +367,7 @@
367
367
  },
368
368
  "feature-transition:onEnter:0": {
369
369
  "kind": "shell",
370
- "invocation": "if [ -z \"$feature\" ]; then\n echo \"feature-transition: vars.feature is empty — refusing no-op feature sync (mis-invocation, not a blocked sync)\" >&2;\n exit 1;\nfi; SYNC_OUTPUT=$(if [ -f plugins/sp/scripts/feature-sync-bounded.ts ]; then\n bun plugins/sp/scripts/feature-sync-bounded.ts \"$feature\" --spur-bin \"$spurBin\" --json;\nelse\n $spurBin feature sync \"$feature\" --json;\nfi); SYNC_RC=$?; printf '%s\\n' \"$SYNC_OUTPUT\"; APPLIED=$(printf '%s' \"$SYNC_OUTPUT\" | jq -r '.applied // false' 2>/dev/null || echo false); if [ \"$APPLIED\" = \"true\" ] || [ \"$SYNC_RC\" -ne 0 ]; then\n echo \"feature-transition: sync applied or failed after a possible partial transition for $feature — running corpus-aware gate: $featureGateCmd\";\n if sh -c \"$featureGateCmd\"; then\n echo \"feature-transition: corpus-aware gate PASS for feature $feature\";\n else\n echo \"feature-transition: corpus-aware gate FAIL for feature $feature — inspect findings before reporting the transition complete\" >&2;\n fi;\nelse\n echo \"feature-transition: sync did not apply a transition (rc=$SYNC_RC, applied=$APPLIED) — corpus-aware gate skipped\";\nfi; exit 0"
370
+ "invocation": "if [ -z \"$feature\" ]; then\n echo \"feature-transition: vars.feature is empty — refusing no-op feature sync (mis-invocation, not a blocked sync)\" >&2;\n exit 1;\nfi; SYNC_OUTPUT=$(if [ -f plugins/sp/scripts/feature-sync-bounded.ts ]; then\n bun plugins/sp/scripts/feature-sync-bounded.ts \"$feature\" --spur-bin \"$spurBin\" --json;\nelif SYNC_MJS=\"$(superskill script path sp feature-sync-bounded.mjs 2>/dev/null)\" && [ -f \"$SYNC_MJS\" ]; then\n node \"$SYNC_MJS\" \"$feature\" --spur-bin \"$spurBin\" --json;\nelse\n $spurBin feature sync \"$feature\" --json;\nfi); SYNC_RC=$?; printf '%s\\n' \"$SYNC_OUTPUT\"; APPLIED=$(printf '%s' \"$SYNC_OUTPUT\" | jq -r '.applied // false' 2>/dev/null || echo false); if [ \"$APPLIED\" = \"true\" ] || [ \"$SYNC_RC\" -ne 0 ]; then\n echo \"feature-transition: sync applied or failed after a possible partial transition for $feature — running corpus-aware gate: $featureGateCmd\";\n if sh -c \"$featureGateCmd\"; then\n echo \"feature-transition: corpus-aware gate PASS for feature $feature\";\n else\n echo \"feature-transition: corpus-aware gate FAIL for feature $feature — inspect findings before reporting the transition complete\" >&2;\n fi;\nelse\n echo \"feature-transition: sync did not apply a transition (rc=$SYNC_RC, applied=$APPLIED) — corpus-aware gate skipped\";\nfi; exit 0"
371
371
  },
372
372
  "branch-cleanup:onEnter:0": {
373
373
  "kind": "hitl.confirm"
@@ -99,13 +99,14 @@ states:
99
99
  boolean signal written to .spur/run/${vars.__runId}-idea-needs-design.json — this determines
100
100
  whether the system-design state runs. As its terminal artifact for the idea path,
101
101
  brainstorm also emits the idea-evaluation report to .spur/run/${vars.__runId}-idea-eval-report.md
102
- (template: plugins/sp/skills/spur-dev/references/idea-evaluation.md).
102
+ (template: the `sp:spur-dev` skill's `idea-evaluation` reference — named by skill, not by
103
+ repo path, because `spur init` never scaffolds `plugins/sp/` into a seeded project).
103
104
  expectFile fails a silent no-op discovery (no eval report).
104
105
  onEnter:
105
106
  - kind: agent.run
106
107
  options:
107
108
  agent: ${vars.planningAgent}
108
- input: "Run sp:brainstorm for the idea: ${vars.idea}. The skill owns the approach-generation, design summary, and `needs_design` signal criteria; emit .spur/run/${vars.__runId}-idea-needs-design.json ({\"needs_design\": true|false}) and the design summary per the skill's `Design Approval Gate` and `The needs_design signal` sections. Also emit the idea-evaluation report to .spur/run/${vars.__runId}-idea-eval-report.md per plugins/sp/skills/spur-dev/references/idea-evaluation.md (urgency/necessity 0–5, premises, pros/cons, alternatives, enhanced idea, recommendation). At the END of the report, append a provenance footer block of the exact form: `---\\nrun_id: ${vars.__runId}\\ngenerated_at: <RFC3339 timestamp>\\n---` (omit the footer only if run_id is empty)."
109
+ input: "Run sp:brainstorm for the idea: ${vars.idea}. The skill owns the approach-generation, design summary, and `needs_design` signal criteria; emit .spur/run/${vars.__runId}-idea-needs-design.json ({\"needs_design\": true|false}) and the design summary per the skill's `Design Approval Gate` and `The needs_design signal` sections. Also emit the idea-evaluation report to .spur/run/${vars.__runId}-idea-eval-report.md per the `sp:spur-dev` skill's `idea-evaluation` reference (urgency/necessity 0–5, premises, pros/cons, alternatives, enhanced idea, recommendation). At the END of the report, append a provenance footer block of the exact form: `---\\nrun_id: ${vars.__runId}\\ngenerated_at: <RFC3339 timestamp>\\n---` (omit the footer only if run_id is empty)."
109
110
  # Declared Layer-1 role (0538 R2): routing reason beside the agent: pin.
110
111
  role: planner
111
112
  expectFile: .spur/run/${vars.__runId}-idea-eval-report.md
@@ -298,7 +299,7 @@ states:
298
299
  - kind: agent.run
299
300
  options:
300
301
  agent: ${vars.planningAgent}
301
- input: "Run sp:spec-decomposition for feature ${vars.featureId}. Read the brainstorm artifact, feature AC, and design doc. SIZING FIRST, before any JSON: apply the skill's `Default to NOT decomposing` rubric to the whole unit of work — if it scores 0-2 the correct output is a ONE-entry batch, not many. Scenario count is not task count: merge scenarios that one task delivers (same file surface, same subsystem, or unreadable apart in review), and list every scenario a task covers in its background. Merging never costs AC coverage — one task may carry several scenarios. Do not emit one entry per scenario or per requirement by reflex. Then produce a task-batch JSON array at .spur/run/${vars.__runId}-idea-task-batch.json, validated against apps/cli/schemas/task-batch.schema.json. Schema-permitted fields per entry: `name`, `background`, `requirements`, `feature_id`, `parent_wbs`, `priority`, `tags`, `template` — schema validation rejects anything else. Acceptance Criteria, Design, and Plan sections are filled in by the per-task refine step after batch-create, NOT at decompose time. Validate locally against the schema before emitting. Also emit the private task-order sidecar at .spur/run/${vars.__runId}-idea-task-order.json: a JSON array (one entry per batch item) of `{ name: <exact batch item name>, depends_on_names: [<batch item names>] }` declaring ordering/dependencies between the batch items; use `[]` when no ordering exists. Every `name` and every dependency must match exactly one batch item `name` — it is private workflow data, not part of task-batch.schema.json."
302
+ input: "Run sp:spec-decomposition for feature ${vars.featureId}. Read the brainstorm artifact, feature AC, and design doc. SIZING FIRST, before any JSON: apply the skill's `Default to NOT decomposing` rubric to the whole unit of work — if it scores 0-2 the correct output is a ONE-entry batch, not many. Scenario count is not task count: merge scenarios that one task delivers (same file surface, same subsystem, or unreadable apart in review), and list every scenario a task covers in its background. Merging never costs AC coverage — one task may carry several scenarios. Do not emit one entry per scenario or per requirement by reflex. Then produce a task-batch JSON array at .spur/run/${vars.__runId}-idea-task-batch.json, validated against task-batch.schema.json. Schema-permitted fields per entry: `name`, `background`, `requirements`, `feature_id`, `parent_wbs`, `priority`, `tags`, `template` — schema validation rejects anything else. Acceptance Criteria, Design, and Plan sections are filled in by the per-task refine step after batch-create, NOT at decompose time. Validate locally against the schema before emitting. Also emit the private task-order sidecar at .spur/run/${vars.__runId}-idea-task-order.json: a JSON array (one entry per batch item) of `{ name: <exact batch item name>, depends_on_names: [<batch item names>] }` declaring ordering/dependencies between the batch items; use `[]` when no ordering exists. Every `name` and every dependency must match exactly one batch item `name` — it is private workflow data, not part of task-batch.schema.json."
302
303
  # Declared Layer-1 role (0538 R2): routing reason beside the agent: pin.
303
304
  role: planner
304
305
  expectFile: .spur/run/${vars.__runId}-idea-task-batch.json
@@ -19,7 +19,7 @@
19
19
  # agent / spurBin — executor + spur binary (CLI overrides spurBin)
20
20
  # stepTimeoutMs — agent.run budget for review/verify/test-fix (ms)
21
21
  # implementTimeoutMs — implement agent.run budget (ms)
22
- # qualityGateCmd — project gate (default: bun run autofix && bun run spur-check)
22
+ # qualityGateCmd — project gate (default: bun run spur-check)
23
23
  # qualityGateMaxFixAttempts — max /sp:dev-fixall hops after a red gate (default: 2)
24
24
  #
25
25
  # Seeded by `spur init`. agent.run inputs are pure slash commands (ADR-043).
@@ -104,7 +104,12 @@ vars:
104
104
  # and the fixall slash input all use this same var so the command stays single-sourced.
105
105
  # TRUSTED CONFIG ONLY — this string is executed via `sh -c` (see test/test-recheck). Never
106
106
  # interpolate untrusted operator/LLM input into qualityGateCmd (task 0436 SECUA residual).
107
- qualityGateCmd: "bun run format && bun run spur-check"
107
+ # NO `format` PREFIX: `implement` already ran `$formatCmd` on its way out, and `test`
108
+ # captures `proofDigest` (onEnter[3]) BEFORE this command runs (onEnter[4]) — a formatter
109
+ # inside the gate rewrites the very tree the digest just fingerprinted (ADR-071 proof
110
+ # window). It was a no-op only because the implement-stage format got there first; that is
111
+ # an accident, not an invariant. The gate observes, it does not mutate.
112
+ qualityGateCmd: "bun run spur-check"
108
113
  # Cheap red-detector run before the full gate on **recheck only**; empty ⇒ no probe
109
114
  # (full gate every recheck — the pre-0587 behavior). A project overriding qualityGateCmd
110
115
  # should override this too. TRUSTED CONFIG ONLY — executed via `sh -c` (same surface as
@@ -208,13 +213,16 @@ states:
208
213
  command: >-
209
214
  SIZE_FILE=".spur/run/$wbs-precheck-size.status" &&
210
215
  mkdir -p .spur/run &&
211
- if [ -f plugins/sp/scripts/task-size-precheck.ts ]; then
212
- bun plugins/sp/scripts/task-size-precheck.ts "$wbs"
216
+ SIZE_SCRIPT="plugins/sp/scripts/task-size-precheck.ts";
217
+ [ -f "$SIZE_SCRIPT" ] ||
218
+ SIZE_SCRIPT="$(superskill script path sp task-size-precheck.ts 2>/dev/null)";
219
+ if [ -n "$SIZE_SCRIPT" ] && [ -f "$SIZE_SCRIPT" ]; then
220
+ bun "$SIZE_SCRIPT" "$wbs"
213
221
  --spur-bin "$spurBin" --max-reqs "$maxImplementReqs"
214
222
  --max-plan-items "$maxImplementPlanItems";
215
223
  else
216
- echo "task-size-precheck failed closed — checker script" >&2 &&
217
- echo "plugins/sp/scripts/task-size-precheck.ts absent." >&2 &&
224
+ echo "task-size-precheck failed closed — checker not found in" >&2 &&
225
+ echo "plugins/sp/scripts/ nor staged; run 'superskill install sp'." >&2 &&
218
226
  echo "FAIL" > "$SIZE_FILE";
219
227
  fi &&
220
228
  exit 0
@@ -228,12 +236,15 @@ states:
228
236
  command: >-
229
237
  EVID_FILE=".spur/run/$wbs-precheck-evidence.status" &&
230
238
  mkdir -p .spur/run &&
231
- if [ -f plugins/sp/scripts/task-evidence-precheck.ts ]; then
232
- bun plugins/sp/scripts/task-evidence-precheck.ts "$wbs"
239
+ EVID_SCRIPT="plugins/sp/scripts/task-evidence-precheck.ts";
240
+ [ -f "$EVID_SCRIPT" ] ||
241
+ EVID_SCRIPT="$(superskill script path sp task-evidence-precheck.ts 2>/dev/null)";
242
+ if [ -n "$EVID_SCRIPT" ] && [ -f "$EVID_SCRIPT" ]; then
243
+ bun "$EVID_SCRIPT" "$wbs"
233
244
  --spur-bin "$spurBin";
234
245
  else
235
- echo "task-evidence-precheck failed closed —" >&2 &&
236
- echo "plugins/sp/scripts/task-evidence-precheck.ts absent." >&2 &&
246
+ echo "task-evidence-precheck failed closed — checker not found" >&2 &&
247
+ echo "in plugins/sp/scripts/ nor staged; run 'superskill install sp'." >&2 &&
237
248
  echo "FAIL" > "$EVID_FILE";
238
249
  fi &&
239
250
  exit 0
@@ -563,7 +574,14 @@ states:
563
574
  - kind: shell
564
575
  options:
565
576
  command: >-
566
- bun plugins/sp/scripts/verify-answer-lint.ts "$wbs"
577
+ LINT_SCRIPT="plugins/sp/scripts/verify-answer-lint.ts";
578
+ [ -f "$LINT_SCRIPT" ] ||
579
+ LINT_SCRIPT="$(superskill script path sp verify-answer-lint.ts 2>/dev/null)";
580
+ if [ -z "$LINT_SCRIPT" ] || [ ! -f "$LINT_SCRIPT" ]; then
581
+ echo "verify-answer-lint: checker not found in plugins/sp/scripts/ nor staged — run 'superskill install sp'" >&2;
582
+ exit 1;
583
+ fi;
584
+ bun "$LINT_SCRIPT" "$wbs"
567
585
  --answer ".spur/run/$wbs-verify-answer.txt"
568
586
  --spur-bin "$spurBin"
569
587
  - kind: shell
@@ -643,6 +661,8 @@ states:
643
661
  if [ -n "$FID" ]; then
644
662
  if [ -f plugins/sp/scripts/feature-sync-bounded.ts ]; then
645
663
  bun plugins/sp/scripts/feature-sync-bounded.ts "$FID" --spur-bin "$spurBin" --json;
664
+ elif SYNC_MJS="$(superskill script path sp feature-sync-bounded.mjs 2>/dev/null)" && [ -f "$SYNC_MJS" ]; then
665
+ node "$SYNC_MJS" "$FID" --spur-bin "$spurBin" --json;
646
666
  else
647
667
  $spurBin feature sync "$FID" --json;
648
668
  fi;
@@ -54,12 +54,16 @@ vars:
54
54
  # Corpus-aware quality gate for the feature-transition hop (R1, task 0625).
55
55
  # The per-task gate (`spur-check`) deliberately excludes corpus-check — the only
56
56
  # sweep that observes feature-level findings — so a transition that arms one
57
- # would go green. A feature transition happens once per feature, so the ~41 s
58
- # corpus sweep is paid once here. Soft by design: the gate reports PASS/FAIL
59
- # and lets the operator decide; it never hard-fails the wrap-up shell.
57
+ # would go green. This gate is therefore the CORPUS SWEEP ALONE (~29 s), not
58
+ # `spur-check-new`: every task in the feature already paid a full `spur-check`
59
+ # in its own pipeline, and the states that run before this hop (`doc-sync`,
60
+ # `metrics-record`) write only markdown and task sections, which Biome skips
61
+ # (`ignoreUnknown: true`). Re-running the ~105 s per-task gate here re-verifies
62
+ # unchanged code and measures nothing new. Soft by design: the gate reports
63
+ # PASS/FAIL and lets the operator decide; it never hard-fails the wrap-up shell.
60
64
  # TRUSTED CONFIG ONLY — this string is executed via `sh -c` (same surface as
61
65
  # task-pipeline's qualityGateCmd). Never interpolate untrusted input into it.
62
- featureGateCmd: "bun run spur-check-new"
66
+ featureGateCmd: "bun run corpus-check"
63
67
  __hitlAnswer: ""
64
68
 
65
69
  states:
@@ -178,6 +182,8 @@ states:
178
182
  fi;
179
183
  SYNC_OUTPUT=$(if [ -f plugins/sp/scripts/feature-sync-bounded.ts ]; then
180
184
  bun plugins/sp/scripts/feature-sync-bounded.ts "$feature" --spur-bin "$spurBin" --json;
185
+ elif SYNC_MJS="$(superskill script path sp feature-sync-bounded.mjs 2>/dev/null)" && [ -f "$SYNC_MJS" ]; then
186
+ node "$SYNC_MJS" "$feature" --spur-bin "$spurBin" --json;
181
187
  else
182
188
  $spurBin feature sync "$feature" --json;
183
189
  fi);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gobing-ai/spur",
3
- "version": "0.3.70",
3
+ "version": "0.3.72",
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.49",
57
- "@gobing-ai/ts-ai-runner": "^0.4.49",
58
- "@gobing-ai/ts-dual-workflow-engine": "^0.4.49",
59
- "@gobing-ai/ts-infra": "^0.4.49",
60
- "@gobing-ai/ts-llm-jsonl-importer": "^0.4.49",
61
- "@gobing-ai/ts-rule-engine": "^0.4.49",
62
- "@gobing-ai/ts-runtime": "^0.4.49",
63
- "@gobing-ai/ts-utils": "^0.4.49",
56
+ "@gobing-ai/ts-db": "^0.4.54",
57
+ "@gobing-ai/ts-ai-runner": "^0.4.54",
58
+ "@gobing-ai/ts-dual-workflow-engine": "^0.4.54",
59
+ "@gobing-ai/ts-infra": "^0.4.54",
60
+ "@gobing-ai/ts-llm-jsonl-importer": "^0.4.54",
61
+ "@gobing-ai/ts-rule-engine": "^0.4.54",
62
+ "@gobing-ai/ts-runtime": "^0.4.54",
63
+ "@gobing-ai/ts-utils": "^0.4.54",
64
64
  "@types/bun": "1.3.14",
65
65
  "@types/figlet": "^1.7.0",
66
66
  "@types/node-notifier": "8.0.5",
@@ -28,7 +28,8 @@ For shared semantics, see the [flag glossary](../skills/spur-dev/references/flag
28
28
  ## Implementation
29
29
 
30
30
  Under the pipeline, the `test-recheck` state runs the full gate immediately after this hop — that is
31
- the deciding run. Fixall bounds itself to **one confirming gate run** (R4, task 0483); use targeted
32
- probes (`bun test <file> --test-name-pattern <test>`) during fix loops, never re-run the full gate
33
- per fix. `qualityGateCmd` itself is unchanged so `test-recheck` still runs the full gate.
31
+ the deciding run. When `--gate-log` is set (the pipeline signal), fixall runs **no full gate at
32
+ all**: targeted probes (`bun test <file> --test-name-pattern <test>`) during fix loops, then one
33
+ `bun run lint` before returning. Invoked standalone, it keeps the single confirming run (R4, task
34
+ 0483). `qualityGateCmd` itself is unchanged so `test-recheck` still runs the full gate.
34
35
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sp",
3
- "version": "0.3.70",
3
+ "version": "0.3.72",
4
4
  "description": "Spur — a local-first harness engineering toolkit that wraps mainstream coding agents with constraint checking, workflow orchestration, and history analytics.",
5
5
  "extensions": {
6
6
  "pi": ["./hooks/pi/guard-extension.ts"]
@@ -149,6 +149,78 @@ async function getCcusageData(date) {
149
149
  return null;
150
150
  }
151
151
  }
152
+ async function getSpurHistoryHealth(date, dbPath = ".spur/spur.db") {
153
+ try {
154
+ const resolvedPath = resolve(process.cwd(), dbPath);
155
+ if (!existsSync(resolvedPath)) {
156
+ return null;
157
+ }
158
+ const { Database } = await import("bun:sqlite");
159
+ const db = new Database(resolvedPath, { readonly: true });
160
+ try {
161
+ const loopTable = db.query("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?").get("history_board_loop_findings");
162
+ let loops = [];
163
+ if (loopTable) {
164
+ const rows = db.query(`SELECT tool_name, args_digest, repeats, session_id, first_seq, last_seq, started_at
165
+ FROM history_board_loop_findings
166
+ WHERE started_at IS NULL OR started_at LIKE ?
167
+ ORDER BY repeats DESC
168
+ LIMIT 20`).all(`${date}%`);
169
+ loops = rows.map((r) => ({
170
+ toolName: r.tool_name,
171
+ argsDigest: r.args_digest || "repeated execution",
172
+ repeats: r.repeats,
173
+ sessionId: r.session_id,
174
+ fromSeq: r.first_seq,
175
+ toSeq: r.last_seq,
176
+ wastedTokens: r.repeats * 250
177
+ }));
178
+ }
179
+ let toolCalls = 0;
180
+ let toolErrors = 0;
181
+ const toolTable = db.query("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?").get("history_board_tool_5m");
182
+ if (toolTable) {
183
+ const stats = db.query(`SELECT SUM(calls) AS calls, SUM(errors) AS errors
184
+ FROM history_board_tool_5m
185
+ WHERE bucket_start LIKE ?`).get(`${date}%`);
186
+ toolCalls = stats?.calls ?? 0;
187
+ toolErrors = stats?.errors ?? 0;
188
+ }
189
+ const redundantCalls = loops.reduce((acc, l) => acc + Math.max(0, l.repeats - 1), 0);
190
+ const wastedTokens = loops.reduce((acc, l) => acc + l.wastedTokens, 0);
191
+ const errorRatePct = toolCalls > 0 ? toolErrors / toolCalls * 100 : 0;
192
+ const remediationProposals = [];
193
+ for (const lp of loops.slice(0, 5)) {
194
+ const cleanTool = lp.toolName.replace(/[^a-zA-Z0-9_-]/g, "_");
195
+ const key = `repetition:${cleanTool}:${lp.argsDigest.slice(0, 16)}`;
196
+ const title = `Break execution loop in ${lp.toolName} (${lp.repeats} repeats)`;
197
+ const body = `Finding: ${key}\\nObserved ${lp.repeats} redundant invocations in session ${lp.sessionId} (steps #${lp.fromSeq ?? 1}\u2192#${lp.toSeq ?? lp.repeats}). Wasted tokens: ~${lp.wastedTokens}.`;
198
+ const command = `spur task create --section "Fix ${cleanTool} repetition loop" --body "${body}"`;
199
+ remediationProposals.push({ key, title, command });
200
+ }
201
+ if (toolCalls > 0 && errorRatePct > 10) {
202
+ const key = "reliability:tooling:high-error-rate";
203
+ const title = `Investigate high tool error rate (${errorRatePct.toFixed(1)}%)`;
204
+ const body = `Finding: ${key}\\nObserved ${toolErrors} errors across ${toolCalls} tool calls (${errorRatePct.toFixed(1)}% error rate) on ${date}.`;
205
+ const command = `spur task create --section "Investigate tool error rate spikes" --body "${body}"`;
206
+ remediationProposals.push({ key, title, command });
207
+ }
208
+ return {
209
+ toolCalls,
210
+ toolErrors,
211
+ errorRatePct,
212
+ loops,
213
+ redundantCalls,
214
+ wastedTokens,
215
+ remediationProposals
216
+ };
217
+ } finally {
218
+ db.close();
219
+ }
220
+ } catch {
221
+ return null;
222
+ }
223
+ }
152
224
  async function getGitCommits(date) {
153
225
  try {
154
226
  const { start, end } = getDateRange(date);
@@ -333,6 +405,49 @@ function generateMarkdown(summary) {
333
405
  lines.push(pending);
334
406
  lines.push("");
335
407
  }
408
+ if (summary.historyHealth) {
409
+ const hh = summary.historyHealth;
410
+ lines.push("## Execution Loops & Health Findings");
411
+ lines.push("");
412
+ if (hh.loops.length === 0 && hh.toolCalls === 0) {
413
+ lines.push("- **Status:** \u2705 Clean \u2014 No execution loops or tool calls recorded for this date.");
414
+ lines.push("");
415
+ } else {
416
+ lines.push("| Metric | Value |");
417
+ lines.push("|--------|-------|");
418
+ lines.push(`| Tool Invocations | ${hh.toolCalls.toLocaleString()} |`);
419
+ lines.push(`| Tool Errors | ${hh.toolErrors.toLocaleString()} (${hh.errorRatePct.toFixed(1)}%) |`);
420
+ lines.push(`| Detected Loops (Repeats \u2265 3) | ${hh.loops.length} |`);
421
+ lines.push(`| Redundant Invocations | ${hh.redundantCalls.toLocaleString()} |`);
422
+ lines.push(`| Estimated Wasted Tokens | ${hh.wastedTokens.toLocaleString()} |`);
423
+ lines.push("");
424
+ if (hh.loops.length > 0) {
425
+ lines.push("### Detected Execution Loops");
426
+ lines.push("");
427
+ for (const lp of hh.loops.slice(0, 10)) {
428
+ const seqInfo = lp.fromSeq && lp.toSeq ? ` (steps #${lp.fromSeq} \u2192 #${lp.toSeq})` : "";
429
+ const argsHint = lp.argsDigest === "74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b" ? "empty/unrecorded arguments" : lp.argsDigest.length > 28 ? `${lp.argsDigest.slice(0, 24)}...` : lp.argsDigest;
430
+ lines.push(`- \`${lp.toolName || "unknown"}\` \xD7 **${lp.repeats} repeats** in session \`${lp.sessionId}\`${seqInfo}`);
431
+ lines.push(` - *Args hint:* \`${argsHint}\` (~${lp.wastedTokens.toLocaleString()} wasted tokens)`);
432
+ }
433
+ lines.push("");
434
+ }
435
+ if (hh.remediationProposals.length > 0) {
436
+ lines.push("### Auto-Healing Remediation Proposals");
437
+ lines.push("");
438
+ lines.push("To remediate root causes and prevent recurring token waste, execute:");
439
+ lines.push("");
440
+ lines.push("```bash");
441
+ for (const prop of hh.remediationProposals) {
442
+ lines.push(`# ${prop.title} [${prop.key}]`);
443
+ lines.push(prop.command);
444
+ lines.push("");
445
+ }
446
+ lines.push("```");
447
+ lines.push("");
448
+ }
449
+ }
450
+ }
336
451
  if (summary.historyReportPath) {
337
452
  lines.push("## History Report");
338
453
  lines.push("");
@@ -417,6 +532,11 @@ async function buildDailySummary(options) {
417
532
  if (gitActivity !== undefined) {
418
533
  result.gitActivity = gitActivity;
419
534
  }
535
+ const historyHealth = await getSpurHistoryHealth(options.date);
536
+ if (historyHealth && (historyHealth.loops.length > 0 || historyHealth.toolCalls > 0)) {
537
+ result.historyHealth = historyHealth;
538
+ platforms.push("Spur History");
539
+ }
420
540
  return result;
421
541
  }
422
542
  async function main() {
@@ -469,6 +589,7 @@ export {
469
589
  printUsage,
470
590
  parseArgs,
471
591
  main,
592
+ getSpurHistoryHealth,
472
593
  getGitCommits,
473
594
  getDateRange,
474
595
  getCcusageData,