@gobing-ai/spur 0.3.71 → 0.3.73

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 (81) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/config/config.example.yaml +33 -23
  3. package/config/corpus-baseline.json +168 -0
  4. package/config/pipeline-budgets.json +7 -3
  5. package/config/plugin-scripts.json +4 -0
  6. package/config/proportional-route-table.ts +155 -0
  7. package/config/rules/structure/protected-files.yaml +3 -0
  8. package/config/task-pipeline-proportional-migration-plan.md +79 -0
  9. package/config/workflow-composition-baseline.json +124 -85
  10. package/config/workflows/docs-pipeline.yaml +11 -1
  11. package/config/workflows/feature-dev.yaml +27 -20
  12. package/config/workflows/task-lifecycle.yaml +27 -15
  13. package/config/workflows/task-pipeline.yaml +72 -11
  14. package/config/workflows/wrapup-pipeline.yaml +100 -42
  15. package/package.json +9 -9
  16. package/plugins/sp/plugin.json +1 -1
  17. package/plugins/sp/scripts/daily-summary/daily-summary.mjs +119 -0
  18. package/plugins/sp/scripts/daily-summary/daily-summary.ts +202 -0
  19. package/plugins/sp/scripts/inline-pipeline-parity-check.ts +185 -0
  20. package/plugins/sp/scripts/task-evidence-precheck.ts +1 -1
  21. package/plugins/sp/scripts/verify-answer-lint.ts +4 -0
  22. package/plugins/sp/skills/spur-cli/references/workflows/authoring-workflows.md +23 -0
  23. package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +20 -0
  24. package/schemas/spur-config.schema.json +35 -0
  25. package/schemas/state-machine-workflow.schema.json +3 -1
  26. package/schemas/transition-flow-workflow.schema.json +3 -1
  27. package/spur.js +12391 -8659
  28. package/web/_astro/BoardApp.BYCNkMOn.js +185 -0
  29. package/web/_astro/BoardApp.E12MFjOS.js +1 -0
  30. package/web/_astro/{TaskDetail.BvkKvo57.js → TaskDetail.CgUreSP2.js} +1 -1
  31. package/web/_astro/{arc.DuEIzPMi.js → arc.BySSh34M.js} +1 -1
  32. package/web/_astro/{architectureDiagram-3BPJPVTR.D0dUxCjA.js → architectureDiagram-3BPJPVTR.DM46TS_h.js} +1 -1
  33. package/web/_astro/{blockDiagram-GPEHLZMM.Cv60iOpM.js → blockDiagram-GPEHLZMM.tZhvNUHA.js} +1 -1
  34. package/web/_astro/{c4Diagram-AAUBKEIU.Ddx7WGhb.js → c4Diagram-AAUBKEIU.PT4Or4Nf.js} +1 -1
  35. package/web/_astro/channel.5cYKr5cs.js +1 -0
  36. package/web/_astro/{chunk-2J33WTMH.BGKzU_1t.js → chunk-2J33WTMH.J9r0_Bbe.js} +1 -1
  37. package/web/_astro/{chunk-4BX2VUAB.FlApjIIH.js → chunk-4BX2VUAB.hzyeIvhR.js} +1 -1
  38. package/web/_astro/{chunk-55IACEB6.CDfmvDeW.js → chunk-55IACEB6.B0rO7qVh.js} +1 -1
  39. package/web/_astro/{chunk-727SXJPM.T9bc-xir.js → chunk-727SXJPM.wE_Uk5D4.js} +1 -1
  40. package/web/_astro/{chunk-AQP2D5EJ.BiTc-MQo.js → chunk-AQP2D5EJ.DqEEjQw7.js} +1 -1
  41. package/web/_astro/{chunk-FMBD7UC4.he4KrHni.js → chunk-FMBD7UC4.CDoD9sBX.js} +1 -1
  42. package/web/_astro/{chunk-ND2GUHAM.D05XuUuJ.js → chunk-ND2GUHAM.CtX5nF9P.js} +1 -1
  43. package/web/_astro/{chunk-QZHKN3VN.CA2NThIE.js → chunk-QZHKN3VN.CK_EwfaT.js} +1 -1
  44. package/web/_astro/{classDiagram-4FO5ZUOK.Couj-zYZ.js → classDiagram-4FO5ZUOK.DLt5a8Lh.js} +1 -1
  45. package/web/_astro/{classDiagram-v2-Q7XG4LA2.Couj-zYZ.js → classDiagram-v2-Q7XG4LA2.DLt5a8Lh.js} +1 -1
  46. package/web/_astro/{cose-bilkent-S5V4N54A.B8YYW7NG.js → cose-bilkent-S5V4N54A.CMCWP49h.js} +1 -1
  47. package/web/_astro/{cynefin-OW5HDTMX.BExFdiin.js → cynefin-OW5HDTMX.HyXw_vdS.js} +1 -1
  48. package/web/_astro/{dagre-BM42HDAG.BybKbz3q.js → dagre-BM42HDAG.BTuAzh01.js} +1 -1
  49. package/web/_astro/{diagram-2AECGRRQ.UyRTSl9n.js → diagram-2AECGRRQ.D9dr9wfT.js} +1 -1
  50. package/web/_astro/{diagram-5GNKFQAL.BrxCucBf.js → diagram-5GNKFQAL.C4Rot0hj.js} +1 -1
  51. package/web/_astro/{diagram-KO2AKTUF.CdE5oy5J.js → diagram-KO2AKTUF.B_TK5uWC.js} +1 -1
  52. package/web/_astro/{diagram-LMA3HP47.BJLgdosK.js → diagram-LMA3HP47.JkXKK7CO.js} +1 -1
  53. package/web/_astro/{diagram-OG6HWLK6.CUynieTU.js → diagram-OG6HWLK6.BzMN8Bd6.js} +1 -1
  54. package/web/_astro/{erDiagram-TEJ5UH35.C0vS6DJv.js → erDiagram-TEJ5UH35.DVZaWGUd.js} +1 -1
  55. package/web/_astro/{flowDiagram-I6XJVG4X.T3QLi_en.js → flowDiagram-I6XJVG4X.rjEiWUfR.js} +1 -1
  56. package/web/_astro/{ganttDiagram-6RSMTGT7.BNsk3w9Z.js → ganttDiagram-6RSMTGT7.C_EgAarK.js} +1 -1
  57. package/web/_astro/{gitGraphDiagram-PVQCEYII.Mwe2I4V6.js → gitGraphDiagram-PVQCEYII.B-QQSDsK.js} +1 -1
  58. package/web/_astro/index.B5MTfe7k.css +1 -0
  59. package/web/_astro/{infoDiagram-5YYISTIA.BlcjLmtc.js → infoDiagram-5YYISTIA.DlWesz7T.js} +1 -1
  60. package/web/_astro/{ishikawaDiagram-YF4QCWOH.js8qeS0h.js → ishikawaDiagram-YF4QCWOH.BUMZOawi.js} +1 -1
  61. package/web/_astro/{journeyDiagram-JHISSGLW.CM6UK0a4.js → journeyDiagram-JHISSGLW.CWfkxfjY.js} +1 -1
  62. package/web/_astro/{kanban-definition-UN3LZRKU.a6ihOzMd.js → kanban-definition-UN3LZRKU.B-YpMwXf.js} +1 -1
  63. package/web/_astro/{linear.CsIB2jFu.js → linear.D7uqzENp.js} +1 -1
  64. package/web/_astro/{mermaid.core.Br2Fo22q.js → mermaid.core.CxrNppBD.js} +4 -4
  65. package/web/_astro/{mindmap-definition-RKZ34NQL.29inC1Mk.js → mindmap-definition-RKZ34NQL.B4Qe7cM2.js} +1 -1
  66. package/web/_astro/{pieDiagram-4H26LBE5.C9CxG_Kf.js → pieDiagram-4H26LBE5.Ds-5j2ro.js} +1 -1
  67. package/web/_astro/{quadrantDiagram-W4KKPZXB.6qo9MOJM.js → quadrantDiagram-W4KKPZXB.tBd38uNC.js} +1 -1
  68. package/web/_astro/{requirementDiagram-4Y6WPE33.DN07zrP5.js → requirementDiagram-4Y6WPE33.sFENkWl3.js} +1 -1
  69. package/web/_astro/{sankeyDiagram-5OEKKPKP.BSw5o173.js → sankeyDiagram-5OEKKPKP.BeB-Hk7C.js} +1 -1
  70. package/web/_astro/{sequenceDiagram-3UESZ5HK.LJPzySKw.js → sequenceDiagram-3UESZ5HK.DnTeaSpx.js} +1 -1
  71. package/web/_astro/{stateDiagram-AJRCARHV.ClNjEiKV.js → stateDiagram-AJRCARHV.B-8Jt5EJ.js} +1 -1
  72. package/web/_astro/{stateDiagram-v2-BHNVJYJU.c6Z-_WfX.js → stateDiagram-v2-BHNVJYJU.Br7xoqMW.js} +1 -1
  73. package/web/_astro/{timeline-definition-PNZ67QCA.CIMR-87j.js → timeline-definition-PNZ67QCA.C-3WdOyi.js} +1 -1
  74. package/web/_astro/{vennDiagram-CIIHVFJN.BnRSRI9I.js → vennDiagram-CIIHVFJN.DCIs7Lc6.js} +1 -1
  75. package/web/_astro/{wardleyDiagram-YWT4CUSO.uN08C3gv.js → wardleyDiagram-YWT4CUSO.rGAL-bbz.js} +1 -1
  76. package/web/_astro/{xychartDiagram-2RQKCTM6.BjbEvfuq.js → xychartDiagram-2RQKCTM6.hkfQKiRl.js} +1 -1
  77. package/web/index.html +2 -2
  78. package/web/_astro/BoardApp.BnjsI80-.js +0 -178
  79. package/web/_astro/BoardApp.SJcrHBZp.js +0 -1
  80. package/web/_astro/channel.tWfETQvX.js +0 -1
  81. package/web/_astro/index.9npdrEIr.css +0 -1
@@ -49,6 +49,9 @@ failureStates:
49
49
  vars:
50
50
  wbs: "0000"
51
51
  profile: "standard"
52
+ mode: ""
53
+ __runId: ""
54
+ __definitionDigest: ""
52
55
  # PATH-independent spur invocation for shell guards/actions. The CLI overrides this
53
56
  # at run start (resolveSpurBin); the literal default is a safe fallback so direct/dry
54
57
  # runs and `workflow validate` resolve the template. (See AGENTS.md / ADR-026.)
@@ -248,6 +251,34 @@ states:
248
251
  echo "FAIL" > "$EVID_FILE";
249
252
  fi &&
250
253
  exit 0
254
+ # Proportional route table evaluation (0759 R1/R4). The reason artifact is RUN-scoped, not
255
+ # wbs-scoped: ADR-107 names `.spur/run/<runId>-route-reason.txt`, and a wbs-scoped path lets
256
+ # a re-run of the same task overwrite the earlier run's route claim, so the artifact could
257
+ # not attribute a route to the run that took it (0759 R5). `__runId` is injected by
258
+ # WorkflowAppService.run(); the wbs fallback keeps a driver-less invocation from writing to
259
+ # a bare "-route-reason.txt". The log line carries the run id for the same reason — an
260
+ # unattributed append is log scraping, which R5 explicitly rejects as evidence.
261
+ - kind: shell
262
+ options:
263
+ command: >-
264
+ mkdir -p .spur/run .spur/memory &&
265
+ RUN_ID="$__runId" &&
266
+ if [ -z "$RUN_ID" ]; then RUN_ID="pipeline-$wbs"; fi &&
267
+ REASON_FILE=".spur/run/$RUN_ID-route-reason.txt" &&
268
+ if [ "$mode" = "fast" ]; then
269
+ echo "fast:evidence complete+consistent" > "$REASON_FILE";
270
+ elif [ -z "$mode" ]; then
271
+ echo "safety:standard verification" > "$REASON_FILE";
272
+ elif [ "$mode" = "unknown" ]; then
273
+ echo "safety:unknown evidence quality" > "$REASON_FILE";
274
+ elif [ "$mode" = "conflict" ]; then
275
+ echo "safety:conflicting evidence" > "$REASON_FILE";
276
+ else
277
+ echo "safety:unrecognized evidence (mode=$mode)" > "$REASON_FILE";
278
+ fi &&
279
+ printf '%s %s %s\n' "$RUN_ID" "$wbs" "$(cat "$REASON_FILE")"
280
+ >> .spur/memory/task-pipeline-routes.log &&
281
+ exit 0
251
282
 
252
283
  - id: implement
253
284
  description: >
@@ -341,8 +372,11 @@ states:
341
372
  options:
342
373
  # 0710 R4: resolve the spec path, then extract `priority:` from the TASK FILE itself (not the
343
374
  # path listing); normalize to upper so requiresDistinctExecutor's exact 'P0'/'P1' match hits.
344
- # Missing spec/line -> empty var -> back-compat (no distinctness requirement).
345
- command: '$spurBin task path $wbs --json 2>/dev/null | jq -r ".path // .filePath // empty" > ".spur/run/$wbs-taskpath.txt" || true; sed -n "s/^priority:[[:space:]]*//p" "$(cat ".spur/run/$wbs-taskpath.txt" 2>/dev/null)" 2>/dev/null | head -1 | tr -d "[:space:]" | tr "[:lower:]" "[:upper:]" > ".spur/run/$wbs-priority.txt"; exit 0'
375
+ # 0751 R2: the task path is NOT optional - an unresolved lookup fails
376
+ # the step (no `|| true`, no forced `exit 0`, no stderr suppression)
377
+ # instead of degrading the proof to whole-tree-only. The priority read
378
+ # stays tolerant: a missing line is genuinely optional.
379
+ command: 'mkdir -p .spur/run; $spurBin task path $wbs --json | jq -r ".path // .filePath // empty" > ".spur/run/$wbs-taskpath.txt"; task_path="$(cat ".spur/run/$wbs-taskpath.txt")"; if [ -z "$task_path" ]; then echo "fail-closed proof chain (0751 R2): task path for $wbs did not resolve - the task spec cannot be folded into the proof digest" >&2; exit 1; fi; sed -n "s/^priority:[[:space:]]*//p" "$task_path" | head -1 | tr -d "[:space:]" | tr "[:lower:]" "[:upper:]" > ".spur/run/$wbs-priority.txt"'
346
380
  - kind: file.read.into-var
347
381
  options:
348
382
  path: .spur/run/${vars.wbs}-taskpath.txt
@@ -588,8 +622,15 @@ states:
588
622
  options:
589
623
  command: "$spurBin task verdict $wbs --from-answer .spur/run/$wbs-verify-answer.txt"
590
624
  # R3 (0703): write the required proof block into the verdict artifact — the digest, the
591
- # capture point, and the named per-stage results, each stage carrying the SAME digest
592
- # value (prose asserting proof validity is insufficient). Also keeps the flat
625
+ # certifying run id, the capture point, and the named per-stage results, each stage carrying
626
+ # the SAME digest value (prose asserting proof validity is insufficient). `runId` closes
627
+ # 0730 §B.2 (task 0757 R4): without it the verified-outcome fold has to accept ANY linked
628
+ # run as certifying, so a dry-run probe linked to the same wbs reads as proof of completion.
629
+ # `definitionDigest` closes 0759 R5: the record binds to the certifying run AND the exact
630
+ # workflow definition it executed — a stale-definition resume or a definition edited
631
+ # between run and record cannot certify silently. `__definitionDigest` is injected at run
632
+ # start (workflow-service.ts) and equals the digest stamped on the run row (task 0603).
633
+ # Also keeps the flat
593
634
  # `proof-input-digest` check row for consumers that read `checks[]`. Soft action + hard
594
635
  # guard: a missing/malformed stamp fails the `verify → record` guard below, not this step.
595
636
  - kind: shell
@@ -597,8 +638,8 @@ states:
597
638
  command: >-
598
639
  V=".spur/run/$wbs-verdict.json";
599
640
  if [ -f "$V" ] && [ -n "$proofDigest" ]; then
600
- jq --arg d "$proofDigest" --arg g "$(cat .spur/run/$wbs-test-gate.status 2>/dev/null || echo UNKNOWN)"
601
- '. + {proof: {digest: $d, capturePoint: "quality-gate-entry", stages: {
641
+ jq --arg d "$proofDigest" --arg r "$__runId" --arg dd "$__definitionDigest" --arg g "$(cat .spur/run/$wbs-test-gate.status 2>/dev/null || echo UNKNOWN)"
642
+ '. + {proof: {digest: $d, runId: $r, definitionDigest: $dd, capturePoint: "quality-gate-entry", stages: {
602
643
  qualityGate: {status: $g, digest: $d},
603
644
  review: {status: "completed", digest: $d},
604
645
  verification: {status: .verdict, digest: $d}}}}
@@ -701,6 +742,10 @@ states:
701
742
  options:
702
743
  path: .spur/run/${vars.wbs}-verdict.json
703
744
  artifactKind: verify-verdict
745
+ # 0751 R4: bind the verdict to the run's captured proof digest. `record`
746
+ # re-captures `proofDigestNow` with expect=proofDigest, so the binding
747
+ # holds by construction here — making the option non-decorative.
748
+ proofBinding: current
704
749
  - kind: note
705
750
  options:
706
751
  message: "Pipeline complete for task ${vars.wbs} (done gate cleared)."
@@ -746,13 +791,20 @@ transitions:
746
791
  guard:
747
792
  kind: always
748
793
  # Soft probe branching (declaration order: PASS first, then FAIL, then defense).
794
+ - from: test
795
+ to: verify
796
+ description: Quality gate already green and mode is fast — proportional fast path bypasses review.
797
+ guard:
798
+ kind: shell
799
+ options:
800
+ command: 'test "$(cat .spur/run/$wbs-test-gate.status 2>/dev/null)" = PASS && test "$mode" = fast'
749
801
  - from: test
750
802
  to: review
751
- description: Quality gate already green one gate run only; skip fixall/recheck.
803
+ description: Quality gate already green and safety mode proceed to review.
752
804
  guard:
753
805
  kind: shell
754
806
  options:
755
- command: 'test "$(cat .spur/run/$wbs-test-gate.status 2>/dev/null)" = PASS'
807
+ command: 'test "$(cat .spur/run/$wbs-test-gate.status 2>/dev/null)" = PASS && test "$mode" != fast'
756
808
  - from: test
757
809
  to: test-fix
758
810
  description: Quality gate red — start bounded fixall loop.
@@ -772,13 +824,20 @@ transitions:
772
824
  guard:
773
825
  kind: always
774
826
  # Recheck branching (PASS first; under-max FAIL → fixall again; exhausted → failed).
827
+ - from: test-recheck
828
+ to: verify
829
+ description: Quality gate green after fixall and mode is fast — proportional fast path bypasses review.
830
+ guard:
831
+ kind: shell
832
+ options:
833
+ command: 'test "$(cat .spur/run/$wbs-test-gate.status 2>/dev/null)" = PASS && test "$mode" = fast'
775
834
  - from: test-recheck
776
835
  to: review
777
- description: Quality gate green after fixall — proceed to review.
836
+ description: Quality gate green after fixall and safety mode — proceed to review.
778
837
  guard:
779
838
  kind: shell
780
839
  options:
781
- command: 'test "$(cat .spur/run/$wbs-test-gate.status 2>/dev/null)" = PASS'
840
+ command: 'test "$(cat .spur/run/$wbs-test-gate.status 2>/dev/null)" = PASS && test "$mode" != fast'
782
841
  - from: test-recheck
783
842
  to: test-fix
784
843
  description: Still red and under qualityGateMaxFixAttempts — another fixall hop.
@@ -863,7 +922,9 @@ transitions:
863
922
  test "$(jq -r '.proof.digest // ""' "$V" 2>/dev/null)" = "$proofDigest" &&
864
923
  test "$(jq -r '.proof.stages.qualityGate.digest // ""' "$V" 2>/dev/null)" = "$proofDigest" &&
865
924
  test "$(jq -r '.proof.stages.review.digest // ""' "$V" 2>/dev/null)" = "$proofDigest" &&
866
- test "$(jq -r '.proof.stages.verification.digest // ""' "$V" 2>/dev/null)" = "$proofDigest"
925
+ test "$(jq -r '.proof.stages.verification.digest // ""' "$V" 2>/dev/null)" = "$proofDigest" &&
926
+ test "$(jq -r '.proof.runId // ""' "$V" 2>/dev/null)" = "$__runId" &&
927
+ test "$(jq -r '.proof.definitionDigest // ""' "$V" 2>/dev/null)" = "$__definitionDigest"
867
928
  - from: verify
868
929
  to: test-fix
869
930
  description: >-
@@ -1,10 +1,16 @@
1
- # Wrap-up pipeline — post-execution wrap-up for one task or a batch (design §wrapup-pipeline).
1
+ # Wrap-up pipeline — post-execution wrap-up for one task or a batch (design
2
+ # §wrapup-pipeline).
2
3
  #
3
- # Orchestration is configuration (ADR-022 / §3.2): this is YAML over the existing
4
- # dual-workflow engine — zero new engine code. The pipeline NEVER mutates task status —
5
- # wrap-up consumes completed tasks and produces learning/metrics/doc artifacts only.
6
- # Feature transitions go through `spur feature update` so the feature-lifecycle guards
7
- # apply identically. Branch cleanup is an irreversible HITL gate — it always pauses,
4
+ # Orchestration is configuration (ADR-022 / §3.2): this is YAML over the
5
+ # existing
6
+ # dual-workflow engine zero new engine code. The pipeline NEVER mutates task
7
+ # status
8
+ # wrap-up consumes completed tasks and produces learning/metrics/doc artifacts
9
+ # only.
10
+ # Feature transitions go through `spur feature update` so the feature-lifecycle
11
+ # guards
12
+ # apply identically. Branch cleanup is an irreversible HITL gate — it always
13
+ # pauses,
8
14
  # even under --auto (Iron Law #6: irreversible action -> surface to human).
9
15
  #
10
16
  # Shape: start -> task-resolve -> doc-sync -> metrics-record
@@ -14,24 +20,29 @@
14
20
  # (task-resolve with empty list short-circuits to `skipped`).
15
21
  #
16
22
  # Vars (passed as a JSON object via `--vars`):
17
- # tasks — JSON array of WBS strings, passed as a JSON-encoded STRING value (the CLI
18
- # rejects non-string --vars values), e.g. --vars '{"tasks":"[\"0167\"]"}'
23
+ # tasks — JSON array of WBS strings, passed as a JSON-encoded STRING value
24
+ # (the CLI
25
+ # rejects non-string --vars values), e.g. --vars '{"tasks":"[\"0167\"]"}'
19
26
  # feature — feature id to advance through legal lifecycle edges (optional)
20
27
  # profile — set --vars '{"profile":"auto"}' to skip objective confirmations
21
- # merge — set --vars '{"merge":"true"}' to run branch cleanup (irreversible HITL)
28
+ # merge — set --vars '{"merge":"true"}' to run branch cleanup (irreversible
29
+ # HITL)
22
30
  # spurBin — PATH-independent spur invocation (overridden by CLI at run start)
23
31
  # agent — agent for agent.run steps (default: omp)
24
32
  #
25
33
  # Reliability (aligned with task-pipeline / ADR-043):
26
- # - Prefer pure slash commands when a command exists; free-form inputs remain only
34
+ # - Prefer pure slash commands when a command exists; free-form inputs remain
35
+ # only
27
36
  # where capture artifacts (answerFile) have no dedicated slash surface yet.
28
37
  # - doc-sync / metrics-record use expectFile + soft append (empty capture
29
- # does not abort wrap-up after prior steps already landed). The doc-sync hop is a
30
- # single model query that both repairs doc drift (sp:doc-evolve) and captures
38
+ # does not abort wrap-up after prior steps already landed). The doc-sync hop is
39
+ # a
40
+ # single model query that both repairs doc drift (sp:doc-evolve) and captures
31
41
  # working learnings (task 0607 R2 — one query answers both; count 2 -> 1).
32
- # - feature-transition is a soft shell (always exit 0) so a blocked feature sync
42
+ # - feature-transition is a soft shell (always exit 0) so a blocked feature sync
33
43
  # does not abort wrap-up after learnings/metrics already landed.
34
- # - branch-cleanup HITL is exhaustive (yes/no/cancel → done; missing answer defense).
44
+ # - branch-cleanup HITL is exhaustive (yes/no/cancel → done; missing answer
45
+ # defense).
35
46
  # - Wrap-up never mutates task status (consumes completed work only).
36
47
 
37
48
  "$schema": "@gobing-ai/spur/schemas/state-machine-workflow.schema.json"
@@ -50,17 +61,22 @@ vars:
50
61
  merge: "false"
51
62
  spurBin: "spur"
52
63
  agent: "auto"
64
+ __runId: ""
65
+ mode: ""
53
66
  stepTimeoutMs: "1800000"
54
67
  # Corpus-aware quality gate for the feature-transition hop (R1, task 0625).
55
- # The per-task gate (`spur-check`) deliberately excludes corpus-check — the only
68
+ # The per-task gate (`spur-check`) deliberately excludes corpus-check — the
69
+ # only
56
70
  # sweep that observes feature-level findings — so a transition that arms one
57
71
  # would go green. This gate is therefore the CORPUS SWEEP ALONE (~29 s), not
58
72
  # `spur-check-new`: every task in the feature already paid a full `spur-check`
59
73
  # in its own pipeline, and the states that run before this hop (`doc-sync`,
60
74
  # `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
75
+ # (`ignoreUnknown: true`). Re-running the ~105 s per-task gate here
76
+ # re-verifies
62
77
  # 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.
78
+ # PASS/FAIL and lets the operator decide; it never hard-fails the wrap-up
79
+ # shell.
64
80
  # TRUSTED CONFIG ONLY — this string is executed via `sh -c` (same surface as
65
81
  # task-pipeline's qualityGateCmd). Never interpolate untrusted input into it.
66
82
  featureGateCmd: "bun run corpus-check"
@@ -78,13 +94,42 @@ states:
78
94
 
79
95
  - id: task-resolve
80
96
  description: >
81
- Validate the task list passed by the wrapper is non-empty. The command wrapper
82
- (dev-wrap/dev-wrapall) resolves the task list and passes it as vars.tasks;
83
- this state validates it before proceeding. Empty list routes to `skipped`.
97
+ Validate the task list and evaluate closed proportional routing (0758 R1-R4).
98
+ Empty list -> skipped; tasks > 0 && mode == fast -> fast-path (metrics-record,
99
+ bypassing doc-sync); missing/unknown/conflict -> doc-sync (safety-path).
100
+ Writes bounded machine-readable reason to reasonFile.
84
101
  onEnter:
85
102
  - kind: note
86
103
  options:
87
- message: "Resolving task list: ${vars.tasks}"
104
+ message: "Resolving task list: ${vars.tasks} (proportional mode: ${vars.mode})"
105
+ # Run-id confinement (0758 R3). The run-scoped reason file is the only route artifact: a
106
+ # second copy at the fixed path `.spur/run/wrapup-route-reason.txt` had no reader and was
107
+ # overwritten by whichever run finished last, so a route claim read from it belonged to no
108
+ # particular run. The log append carries the run id for the same reason — an unattributed
109
+ # line is log scraping, which R5 rejects as evidence.
110
+ - kind: shell
111
+ options:
112
+ command: >-
113
+ mkdir -p .spur/run .spur/memory &&
114
+ RUN_ID="$__runId" &&
115
+ if [ -z "$RUN_ID" ]; then RUN_ID="wrapup"; fi &&
116
+ REASON_FILE=".spur/run/$RUN_ID-route-reason.txt" &&
117
+ if [ "$(echo "$tasks" | jq length 2>/dev/null || echo 0)" -eq 0 ]; then
118
+ echo "skipped:empty task list" > "$REASON_FILE";
119
+ elif [ "$mode" = "fast" ]; then
120
+ echo "fast:evidence complete+consistent" > "$REASON_FILE";
121
+ elif [ -z "$mode" ]; then
122
+ echo "safety:missing evidence (mode empty)" > "$REASON_FILE";
123
+ elif [ "$mode" = "unknown" ]; then
124
+ echo "safety:unknown evidence quality" > "$REASON_FILE";
125
+ elif [ "$mode" = "conflict" ]; then
126
+ echo "safety:conflicting evidence" > "$REASON_FILE";
127
+ else
128
+ echo "safety:unrecognized evidence (mode=$mode)" > "$REASON_FILE";
129
+ fi &&
130
+ printf '%s %s\n' "$RUN_ID" "$(cat "$REASON_FILE")"
131
+ >> .spur/memory/wrapup-routes.log &&
132
+ exit 0
88
133
 
89
134
  - id: doc-sync
90
135
  description: >
@@ -106,7 +151,8 @@ states:
106
151
  rules; do not write task/feature corpus. THEN extract working learnings from tasks ${vars.tasks} —
107
152
  conventions, errors fixed, patterns, gotchas, grouped by date and task WBS — as raw markdown
108
153
  (no fences) and END your final message with that markdown (it is captured to .spur/run/wrapup-learnings.md).
109
- # Declared Layer-1 role (0538 R2): routing reason beside the agent: pin.
154
+ # Declared Layer-1 role (0538 R2): routing reason beside the agent:
155
+ # pin.
110
156
  role: coder
111
157
  answerFile: .spur/run/wrapup-learnings.md
112
158
  expectFile: .spur/run/wrapup-learnings.md
@@ -158,12 +204,18 @@ states:
158
204
  enters this state when vars.feature is set) and fails loud (dogfood 2026-08-15,
159
205
  feature I3: a silent exit 0 made the wrap look complete with no transition).
160
206
  onEnter:
161
- # Genuinely soft: the terminating `exit 0` must be reached on BOTH branches, so
162
- # the sync is chained with `;` — with `&&` a non-zero sync skipped `exit 0` and
163
- # aborted the run at this state, discarding the learnings/metrics that already
164
- # landed and never reaching branch-cleanup/done (the exact failure the state
165
- # description promises against). `feature-sync-bounded.ts` is a Spur-monorepo
166
- # path (`spur init` does not scaffold `plugins/sp/`), so seeded projects fall
207
+ # Genuinely soft: the terminating `exit 0` must be reached on BOTH
208
+ # branches, so
209
+ # the sync is chained with `;` with `&&` a non-zero sync skipped `exit
210
+ # 0` and
211
+ # aborted the run at this state, discarding the learnings/metrics that
212
+ # already
213
+ # landed and never reaching branch-cleanup/done (the exact failure the
214
+ # state
215
+ # description promises against). `feature-sync-bounded.ts` is a
216
+ # Spur-monorepo
217
+ # path (`spur init` does not scaffold `plugins/sp/`), so seeded projects
218
+ # fall
167
219
  # back to the plain `spur feature sync` verb.
168
220
  # R1 (0625): a feature transition that changed state may have armed a
169
221
  # feature-level finding the per-task fast gate (spur-check) never sees.
@@ -221,7 +273,8 @@ states:
221
273
  - kind: note
222
274
  options:
223
275
  message: "Wrap-up pipeline complete for tasks: ${vars.tasks}. Learnings at .spur/memory/learnings.md, metrics at .spur/memory/wrapup-metrics.jsonl."
224
- # Checkpoint write: record session state for resume (Phase 4, task 0171 R3)
276
+ # Checkpoint write: record session state for resume (Phase 4, task 0171
277
+ # R3)
225
278
  - kind: shell
226
279
  options:
227
280
  command: 'mkdir -p .spur/memory/sessions && echo "checkpoint: wrapup-pipeline done tasks=$tasks ts=$(date -u +%Y-%m-%dT%H:%M:%SZ)" > .spur/memory/sessions/wrapup-checkpoint.md'
@@ -237,25 +290,28 @@ transitions:
237
290
  guard:
238
291
  kind: always
239
292
 
240
- # ── task-resolve: non-empty -> doc-sync, empty/unparseable -> skipped ──
241
- # `|| echo 0` is load-bearing: a malformed vars.tasks (or a box without `jq`) makes
242
- # `jq length` emit nothing, and bare `test "" -gt 0` exits 2 — so BOTH edges failed
243
- # and the run died with an opaque `no-passing-transition` instead of skipping. The
244
- # trailing `always` edge is the belt-and-braces defense for any other guard fault.
293
+ # ── task-resolve: proportional route table (task 0758 R1-R4) ──
245
294
  - from: task-resolve
246
- to: doc-sync
247
- description: Task list resolved and non-empty — begin doc-sync.
295
+ to: skipped
296
+ description: Task list is empty — skip wrap-up.
248
297
  guard:
249
298
  kind: shell
250
299
  options:
251
- command: 'test "$(echo "$tasks" | jq length 2>/dev/null || echo 0)" -gt 0'
300
+ command: 'test "$(echo "$tasks" | jq length 2>/dev/null || echo 0)" -eq 0'
252
301
  - from: task-resolve
253
- to: skipped
254
- description: Task list is empty skip wrap-up.
302
+ to: metrics-record
303
+ description: Proportional fast pathcomplete and consistent evidence bypasses doc-sync.
255
304
  guard:
256
305
  kind: shell
257
306
  options:
258
- command: 'test "$(echo "$tasks" | jq length 2>/dev/null || echo 0)" -eq 0'
307
+ command: 'test "$(echo "$tasks" | jq length 2>/dev/null || echo 0)" -gt 0 && test "$mode" = fast'
308
+ - from: task-resolve
309
+ to: doc-sync
310
+ description: Proportional safety path — missing/unknown/conflicting evidence routes to full doc-sync.
311
+ guard:
312
+ kind: shell
313
+ options:
314
+ command: 'test "$(echo "$tasks" | jq length 2>/dev/null || echo 0)" -gt 0 && test "$mode" != fast'
259
315
  - from: task-resolve
260
316
  to: skipped
261
317
  description: Defense — task list unparseable; skip rather than fail with no-passing-transition.
@@ -270,7 +326,8 @@ transitions:
270
326
  kind: always
271
327
 
272
328
  # ── metrics-record: conditional routing based on feature and merge ──
273
- # Declaration order matters: feature-transition is tried first (if feature is set),
329
+ # Declaration order matters: feature-transition is tried first (if feature is
330
+ # set),
274
331
  # then branch-cleanup (if merge=true but no feature), then done (neither).
275
332
  - from: metrics-record
276
333
  to: feature-transition
@@ -311,7 +368,8 @@ transitions:
311
368
  command: 'test "$merge" != true'
312
369
 
313
370
  # ── branch-cleanup: HITL exhaustive routing (yes / no / cancel → done) ──
314
- # No irreversible git op is wired yet; confirmation is recorded, then wrap completes.
371
+ # No irreversible git op is wired yet; confirmation is recorded, then wrap
372
+ # completes.
315
373
  - from: branch-cleanup
316
374
  to: done
317
375
  description: Branch cleanup HITL answered — wrap-up complete.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gobing-ai/spur",
3
- "version": "0.3.71",
3
+ "version": "0.3.73",
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.50",
57
- "@gobing-ai/ts-ai-runner": "^0.4.50",
58
- "@gobing-ai/ts-dual-workflow-engine": "^0.4.50",
59
- "@gobing-ai/ts-infra": "^0.4.50",
60
- "@gobing-ai/ts-llm-jsonl-importer": "^0.4.50",
61
- "@gobing-ai/ts-rule-engine": "^0.4.50",
62
- "@gobing-ai/ts-runtime": "^0.4.50",
63
- "@gobing-ai/ts-utils": "^0.4.50",
56
+ "@gobing-ai/ts-db": "^0.4.56",
57
+ "@gobing-ai/ts-ai-runner": "^0.4.56",
58
+ "@gobing-ai/ts-dual-workflow-engine": "^0.4.56",
59
+ "@gobing-ai/ts-infra": "^0.4.56",
60
+ "@gobing-ai/ts-llm-jsonl-importer": "^0.4.56",
61
+ "@gobing-ai/ts-rule-engine": "^0.4.56",
62
+ "@gobing-ai/ts-runtime": "^0.4.56",
63
+ "@gobing-ai/ts-utils": "^0.4.56",
64
64
  "@types/bun": "1.3.14",
65
65
  "@types/figlet": "^1.7.0",
66
66
  "@types/node-notifier": "8.0.5",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sp",
3
- "version": "0.3.71",
3
+ "version": "0.3.73",
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,76 @@ 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 command = `spur task create "<title>" --feature <id> && spur task update <wbs> --section Plan --from-file <path>`;
198
+ remediationProposals.push({ key, title, command });
199
+ }
200
+ if (toolCalls > 0 && errorRatePct > 10) {
201
+ const key = "reliability:tooling:high-error-rate";
202
+ const title = `Investigate high tool error rate (${errorRatePct.toFixed(1)}%)`;
203
+ const command = `spur task create "<title>" --feature <id> && spur task update <wbs> --section Plan --from-file <path>`;
204
+ remediationProposals.push({ key, title, command });
205
+ }
206
+ return {
207
+ toolCalls,
208
+ toolErrors,
209
+ errorRatePct,
210
+ loops,
211
+ redundantCalls,
212
+ wastedTokens,
213
+ remediationProposals
214
+ };
215
+ } finally {
216
+ db.close();
217
+ }
218
+ } catch {
219
+ return null;
220
+ }
221
+ }
152
222
  async function getGitCommits(date) {
153
223
  try {
154
224
  const { start, end } = getDateRange(date);
@@ -333,6 +403,49 @@ function generateMarkdown(summary) {
333
403
  lines.push(pending);
334
404
  lines.push("");
335
405
  }
406
+ if (summary.historyHealth) {
407
+ const hh = summary.historyHealth;
408
+ lines.push("## Execution Loops & Health Findings");
409
+ lines.push("");
410
+ if (hh.loops.length === 0 && hh.toolCalls === 0) {
411
+ lines.push("- **Status:** \u2705 Clean \u2014 No execution loops or tool calls recorded for this date.");
412
+ lines.push("");
413
+ } else {
414
+ lines.push("| Metric | Value |");
415
+ lines.push("|--------|-------|");
416
+ lines.push(`| Tool Invocations | ${hh.toolCalls.toLocaleString()} |`);
417
+ lines.push(`| Tool Errors | ${hh.toolErrors.toLocaleString()} (${hh.errorRatePct.toFixed(1)}%) |`);
418
+ lines.push(`| Detected Loops (Repeats \u2265 3) | ${hh.loops.length} |`);
419
+ lines.push(`| Redundant Invocations | ${hh.redundantCalls.toLocaleString()} |`);
420
+ lines.push(`| Estimated Wasted Tokens | ${hh.wastedTokens.toLocaleString()} |`);
421
+ lines.push("");
422
+ if (hh.loops.length > 0) {
423
+ lines.push("### Detected Execution Loops");
424
+ lines.push("");
425
+ for (const lp of hh.loops.slice(0, 10)) {
426
+ const seqInfo = lp.fromSeq && lp.toSeq ? ` (steps #${lp.fromSeq} \u2192 #${lp.toSeq})` : "";
427
+ const argsHint = lp.argsDigest === "74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b" ? "empty/unrecorded arguments" : lp.argsDigest.length > 28 ? `${lp.argsDigest.slice(0, 24)}...` : lp.argsDigest;
428
+ lines.push(`- \`${lp.toolName || "unknown"}\` \xD7 **${lp.repeats} repeats** in session \`${lp.sessionId}\`${seqInfo}`);
429
+ lines.push(` - *Args hint:* \`${argsHint}\` (~${lp.wastedTokens.toLocaleString()} wasted tokens)`);
430
+ }
431
+ lines.push("");
432
+ }
433
+ if (hh.remediationProposals.length > 0) {
434
+ lines.push("### Auto-Healing Remediation Proposals");
435
+ lines.push("");
436
+ lines.push("To remediate root causes and prevent recurring token waste, execute:");
437
+ lines.push("");
438
+ lines.push("```bash");
439
+ for (const prop of hh.remediationProposals) {
440
+ lines.push(`# ${prop.title} [${prop.key}]`);
441
+ lines.push(prop.command);
442
+ lines.push("");
443
+ }
444
+ lines.push("```");
445
+ lines.push("");
446
+ }
447
+ }
448
+ }
336
449
  if (summary.historyReportPath) {
337
450
  lines.push("## History Report");
338
451
  lines.push("");
@@ -417,6 +530,11 @@ async function buildDailySummary(options) {
417
530
  if (gitActivity !== undefined) {
418
531
  result.gitActivity = gitActivity;
419
532
  }
533
+ const historyHealth = await getSpurHistoryHealth(options.date);
534
+ if (historyHealth && (historyHealth.loops.length > 0 || historyHealth.toolCalls > 0)) {
535
+ result.historyHealth = historyHealth;
536
+ platforms.push("Spur History");
537
+ }
420
538
  return result;
421
539
  }
422
540
  async function main() {
@@ -469,6 +587,7 @@ export {
469
587
  printUsage,
470
588
  parseArgs,
471
589
  main,
590
+ getSpurHistoryHealth,
472
591
  getGitCommits,
473
592
  getDateRange,
474
593
  getCcusageData,