@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/config/config.example.yaml +33 -23
- package/config/corpus-baseline.json +168 -0
- package/config/pipeline-budgets.json +7 -3
- package/config/plugin-scripts.json +4 -0
- package/config/proportional-route-table.ts +155 -0
- package/config/rules/structure/protected-files.yaml +3 -0
- package/config/task-pipeline-proportional-migration-plan.md +79 -0
- package/config/workflow-composition-baseline.json +124 -85
- package/config/workflows/docs-pipeline.yaml +11 -1
- package/config/workflows/feature-dev.yaml +27 -20
- package/config/workflows/task-lifecycle.yaml +27 -15
- package/config/workflows/task-pipeline.yaml +72 -11
- package/config/workflows/wrapup-pipeline.yaml +100 -42
- package/package.json +9 -9
- package/plugins/sp/plugin.json +1 -1
- package/plugins/sp/scripts/daily-summary/daily-summary.mjs +119 -0
- package/plugins/sp/scripts/daily-summary/daily-summary.ts +202 -0
- package/plugins/sp/scripts/inline-pipeline-parity-check.ts +185 -0
- package/plugins/sp/scripts/task-evidence-precheck.ts +1 -1
- package/plugins/sp/scripts/verify-answer-lint.ts +4 -0
- package/plugins/sp/skills/spur-cli/references/workflows/authoring-workflows.md +23 -0
- package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +20 -0
- package/schemas/spur-config.schema.json +35 -0
- package/schemas/state-machine-workflow.schema.json +3 -1
- package/schemas/transition-flow-workflow.schema.json +3 -1
- package/spur.js +12391 -8659
- package/web/_astro/BoardApp.BYCNkMOn.js +185 -0
- package/web/_astro/BoardApp.E12MFjOS.js +1 -0
- package/web/_astro/{TaskDetail.BvkKvo57.js → TaskDetail.CgUreSP2.js} +1 -1
- package/web/_astro/{arc.DuEIzPMi.js → arc.BySSh34M.js} +1 -1
- package/web/_astro/{architectureDiagram-3BPJPVTR.D0dUxCjA.js → architectureDiagram-3BPJPVTR.DM46TS_h.js} +1 -1
- package/web/_astro/{blockDiagram-GPEHLZMM.Cv60iOpM.js → blockDiagram-GPEHLZMM.tZhvNUHA.js} +1 -1
- package/web/_astro/{c4Diagram-AAUBKEIU.Ddx7WGhb.js → c4Diagram-AAUBKEIU.PT4Or4Nf.js} +1 -1
- package/web/_astro/channel.5cYKr5cs.js +1 -0
- package/web/_astro/{chunk-2J33WTMH.BGKzU_1t.js → chunk-2J33WTMH.J9r0_Bbe.js} +1 -1
- package/web/_astro/{chunk-4BX2VUAB.FlApjIIH.js → chunk-4BX2VUAB.hzyeIvhR.js} +1 -1
- package/web/_astro/{chunk-55IACEB6.CDfmvDeW.js → chunk-55IACEB6.B0rO7qVh.js} +1 -1
- package/web/_astro/{chunk-727SXJPM.T9bc-xir.js → chunk-727SXJPM.wE_Uk5D4.js} +1 -1
- package/web/_astro/{chunk-AQP2D5EJ.BiTc-MQo.js → chunk-AQP2D5EJ.DqEEjQw7.js} +1 -1
- package/web/_astro/{chunk-FMBD7UC4.he4KrHni.js → chunk-FMBD7UC4.CDoD9sBX.js} +1 -1
- package/web/_astro/{chunk-ND2GUHAM.D05XuUuJ.js → chunk-ND2GUHAM.CtX5nF9P.js} +1 -1
- package/web/_astro/{chunk-QZHKN3VN.CA2NThIE.js → chunk-QZHKN3VN.CK_EwfaT.js} +1 -1
- package/web/_astro/{classDiagram-4FO5ZUOK.Couj-zYZ.js → classDiagram-4FO5ZUOK.DLt5a8Lh.js} +1 -1
- package/web/_astro/{classDiagram-v2-Q7XG4LA2.Couj-zYZ.js → classDiagram-v2-Q7XG4LA2.DLt5a8Lh.js} +1 -1
- package/web/_astro/{cose-bilkent-S5V4N54A.B8YYW7NG.js → cose-bilkent-S5V4N54A.CMCWP49h.js} +1 -1
- package/web/_astro/{cynefin-OW5HDTMX.BExFdiin.js → cynefin-OW5HDTMX.HyXw_vdS.js} +1 -1
- package/web/_astro/{dagre-BM42HDAG.BybKbz3q.js → dagre-BM42HDAG.BTuAzh01.js} +1 -1
- package/web/_astro/{diagram-2AECGRRQ.UyRTSl9n.js → diagram-2AECGRRQ.D9dr9wfT.js} +1 -1
- package/web/_astro/{diagram-5GNKFQAL.BrxCucBf.js → diagram-5GNKFQAL.C4Rot0hj.js} +1 -1
- package/web/_astro/{diagram-KO2AKTUF.CdE5oy5J.js → diagram-KO2AKTUF.B_TK5uWC.js} +1 -1
- package/web/_astro/{diagram-LMA3HP47.BJLgdosK.js → diagram-LMA3HP47.JkXKK7CO.js} +1 -1
- package/web/_astro/{diagram-OG6HWLK6.CUynieTU.js → diagram-OG6HWLK6.BzMN8Bd6.js} +1 -1
- package/web/_astro/{erDiagram-TEJ5UH35.C0vS6DJv.js → erDiagram-TEJ5UH35.DVZaWGUd.js} +1 -1
- package/web/_astro/{flowDiagram-I6XJVG4X.T3QLi_en.js → flowDiagram-I6XJVG4X.rjEiWUfR.js} +1 -1
- package/web/_astro/{ganttDiagram-6RSMTGT7.BNsk3w9Z.js → ganttDiagram-6RSMTGT7.C_EgAarK.js} +1 -1
- package/web/_astro/{gitGraphDiagram-PVQCEYII.Mwe2I4V6.js → gitGraphDiagram-PVQCEYII.B-QQSDsK.js} +1 -1
- package/web/_astro/index.B5MTfe7k.css +1 -0
- package/web/_astro/{infoDiagram-5YYISTIA.BlcjLmtc.js → infoDiagram-5YYISTIA.DlWesz7T.js} +1 -1
- package/web/_astro/{ishikawaDiagram-YF4QCWOH.js8qeS0h.js → ishikawaDiagram-YF4QCWOH.BUMZOawi.js} +1 -1
- package/web/_astro/{journeyDiagram-JHISSGLW.CM6UK0a4.js → journeyDiagram-JHISSGLW.CWfkxfjY.js} +1 -1
- package/web/_astro/{kanban-definition-UN3LZRKU.a6ihOzMd.js → kanban-definition-UN3LZRKU.B-YpMwXf.js} +1 -1
- package/web/_astro/{linear.CsIB2jFu.js → linear.D7uqzENp.js} +1 -1
- package/web/_astro/{mermaid.core.Br2Fo22q.js → mermaid.core.CxrNppBD.js} +4 -4
- package/web/_astro/{mindmap-definition-RKZ34NQL.29inC1Mk.js → mindmap-definition-RKZ34NQL.B4Qe7cM2.js} +1 -1
- package/web/_astro/{pieDiagram-4H26LBE5.C9CxG_Kf.js → pieDiagram-4H26LBE5.Ds-5j2ro.js} +1 -1
- package/web/_astro/{quadrantDiagram-W4KKPZXB.6qo9MOJM.js → quadrantDiagram-W4KKPZXB.tBd38uNC.js} +1 -1
- package/web/_astro/{requirementDiagram-4Y6WPE33.DN07zrP5.js → requirementDiagram-4Y6WPE33.sFENkWl3.js} +1 -1
- package/web/_astro/{sankeyDiagram-5OEKKPKP.BSw5o173.js → sankeyDiagram-5OEKKPKP.BeB-Hk7C.js} +1 -1
- package/web/_astro/{sequenceDiagram-3UESZ5HK.LJPzySKw.js → sequenceDiagram-3UESZ5HK.DnTeaSpx.js} +1 -1
- package/web/_astro/{stateDiagram-AJRCARHV.ClNjEiKV.js → stateDiagram-AJRCARHV.B-8Jt5EJ.js} +1 -1
- package/web/_astro/{stateDiagram-v2-BHNVJYJU.c6Z-_WfX.js → stateDiagram-v2-BHNVJYJU.Br7xoqMW.js} +1 -1
- package/web/_astro/{timeline-definition-PNZ67QCA.CIMR-87j.js → timeline-definition-PNZ67QCA.C-3WdOyi.js} +1 -1
- package/web/_astro/{vennDiagram-CIIHVFJN.BnRSRI9I.js → vennDiagram-CIIHVFJN.DCIs7Lc6.js} +1 -1
- package/web/_astro/{wardleyDiagram-YWT4CUSO.uN08C3gv.js → wardleyDiagram-YWT4CUSO.rGAL-bbz.js} +1 -1
- package/web/_astro/{xychartDiagram-2RQKCTM6.BjbEvfuq.js → xychartDiagram-2RQKCTM6.hkfQKiRl.js} +1 -1
- package/web/index.html +2 -2
- package/web/_astro/BoardApp.BnjsI80-.js +0 -178
- package/web/_astro/BoardApp.SJcrHBZp.js +0 -1
- package/web/_astro/channel.tWfETQvX.js +0 -1
- 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
|
-
#
|
|
345
|
-
|
|
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
|
|
592
|
-
# value (prose asserting proof validity is insufficient).
|
|
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
|
|
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
|
|
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
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
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
|
-
#
|
|
18
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
30
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
|
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
|
|
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
|
|
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
|
|
82
|
-
|
|
83
|
-
|
|
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:
|
|
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
|
|
162
|
-
#
|
|
163
|
-
#
|
|
164
|
-
#
|
|
165
|
-
#
|
|
166
|
-
#
|
|
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
|
|
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:
|
|
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:
|
|
247
|
-
description: Task list
|
|
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)" -
|
|
300
|
+
command: 'test "$(echo "$tasks" | jq length 2>/dev/null || echo 0)" -eq 0'
|
|
252
301
|
- from: task-resolve
|
|
253
|
-
to:
|
|
254
|
-
description:
|
|
302
|
+
to: metrics-record
|
|
303
|
+
description: Proportional fast path — complete 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)" -
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
57
|
-
"@gobing-ai/ts-ai-runner": "^0.4.
|
|
58
|
-
"@gobing-ai/ts-dual-workflow-engine": "^0.4.
|
|
59
|
-
"@gobing-ai/ts-infra": "^0.4.
|
|
60
|
-
"@gobing-ai/ts-llm-jsonl-importer": "^0.4.
|
|
61
|
-
"@gobing-ai/ts-rule-engine": "^0.4.
|
|
62
|
-
"@gobing-ai/ts-runtime": "^0.4.
|
|
63
|
-
"@gobing-ai/ts-utils": "^0.4.
|
|
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",
|
package/plugins/sp/plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sp",
|
|
3
|
-
"version": "0.3.
|
|
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,
|