@gobing-ai/spur 0.3.73 → 0.3.74
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 +15 -4
- package/config/pipeline-budgets.json +4 -4
- package/config/rules/strict/runtime-boundaries.yaml +1 -1
- package/config/templates/AGENTS.md +5 -0
- package/config/templates/docs/99_PROJECT_CONSTITUTION.md +2 -0
- package/config/workflows/basic.yaml +5 -2
- package/config/workflows/docs-pipeline.yaml +92 -55
- package/config/workflows/feature-dev.yaml +43 -20
- package/config/workflows/feature-lifecycle.yaml +1 -0
- package/config/workflows/history-anatomy.yaml +8 -4
- package/config/workflows/idea-pipeline.yaml +66 -33
- package/config/workflows/pr-review.yaml +31 -8
- package/config/workflows/task-lifecycle.yaml +1 -0
- package/config/workflows/task-pipeline.yaml +929 -905
- package/config/workflows/wayfinder-resolution.yaml +124 -90
- package/config/workflows/wrapup-pipeline.yaml +208 -83
- package/package.json +1 -1
- package/plugins/sp/plugin.json +1 -1
- package/plugins/sp/scripts/history-anatomy-cache.mjs +7 -1
- package/plugins/sp/scripts/history-anatomy-cache.ts +11 -2
- package/plugins/sp/scripts/pr-reviewing.mjs +5 -1
- package/plugins/sp/scripts/pr-reviewing.ts +7 -1
- package/plugins/sp/scripts/transition-shim-check.ts +6 -6
- package/plugins/sp/skills/pr-reviewing/SKILL.md +4 -1
- package/plugins/sp/skills/spur-cli/references/self.md +17 -3
- package/plugins/sp/skills/spur-cli/references/tasks.md +1 -1
- package/plugins/sp/skills/spur-cli/references/workflows/workflow-fit-and-tuning.md +5 -5
- package/plugins/sp/skills/spur-cli/references/workflows.md +4 -4
- package/plugins/sp/skills/spur-dev/SKILL.md +17 -0
- package/plugins/sp/skills/spur-dev/references/execution-batch.md +3 -3
- package/plugins/sp/skills/spur-dev/references/gate-checklists.md +1 -2
- package/plugins/sp/skills/wayfinder/SKILL.md +1 -1
- package/spur.js +1125 -784
- package/web/_astro/{BoardApp.BYCNkMOn.js → BoardApp.C60RJZRj.js} +78 -77
- package/web/_astro/BoardApp.ymfj1EHA.js +1 -0
- package/web/_astro/{TaskDetail.CgUreSP2.js → TaskDetail.Guk8VuNu.js} +1 -1
- package/web/_astro/{arc.BySSh34M.js → arc.CAZtlWJM.js} +1 -1
- package/web/_astro/{architectureDiagram-3BPJPVTR.DM46TS_h.js → architectureDiagram-3BPJPVTR.9-XbF_Tz.js} +1 -1
- package/web/_astro/{blockDiagram-GPEHLZMM.tZhvNUHA.js → blockDiagram-GPEHLZMM.DC4WLx3L.js} +1 -1
- package/web/_astro/{c4Diagram-AAUBKEIU.PT4Or4Nf.js → c4Diagram-AAUBKEIU.JgjUQMgG.js} +1 -1
- package/web/_astro/channel.Bxxe2Byc.js +1 -0
- package/web/_astro/{chunk-2J33WTMH.J9r0_Bbe.js → chunk-2J33WTMH.D_JFiXa-.js} +1 -1
- package/web/_astro/{chunk-4BX2VUAB.hzyeIvhR.js → chunk-4BX2VUAB.Bk_RxoeT.js} +1 -1
- package/web/_astro/{chunk-55IACEB6.B0rO7qVh.js → chunk-55IACEB6.a_Rj2VxL.js} +1 -1
- package/web/_astro/{chunk-727SXJPM.wE_Uk5D4.js → chunk-727SXJPM.C2NoS1U6.js} +1 -1
- package/web/_astro/{chunk-AQP2D5EJ.DqEEjQw7.js → chunk-AQP2D5EJ.D1Xn6CNa.js} +1 -1
- package/web/_astro/{chunk-FMBD7UC4.CDoD9sBX.js → chunk-FMBD7UC4.BhJrSBsX.js} +1 -1
- package/web/_astro/{chunk-ND2GUHAM.CtX5nF9P.js → chunk-ND2GUHAM.ByVGduYQ.js} +1 -1
- package/web/_astro/{chunk-QZHKN3VN.CK_EwfaT.js → chunk-QZHKN3VN.B47Paunq.js} +1 -1
- package/web/_astro/{classDiagram-4FO5ZUOK.DLt5a8Lh.js → classDiagram-4FO5ZUOK.BERMSD5C.js} +1 -1
- package/web/_astro/{classDiagram-v2-Q7XG4LA2.DLt5a8Lh.js → classDiagram-v2-Q7XG4LA2.BERMSD5C.js} +1 -1
- package/web/_astro/{cose-bilkent-S5V4N54A.CMCWP49h.js → cose-bilkent-S5V4N54A.O_rHGlhU.js} +1 -1
- package/web/_astro/{cynefin-OW5HDTMX.HyXw_vdS.js → cynefin-OW5HDTMX.qCW2GdNp.js} +1 -1
- package/web/_astro/{dagre-BM42HDAG.BTuAzh01.js → dagre-BM42HDAG.C5Y4lH_u.js} +1 -1
- package/web/_astro/{diagram-2AECGRRQ.D9dr9wfT.js → diagram-2AECGRRQ.DxZmRXxF.js} +1 -1
- package/web/_astro/{diagram-5GNKFQAL.C4Rot0hj.js → diagram-5GNKFQAL.qSxZeYSK.js} +1 -1
- package/web/_astro/{diagram-KO2AKTUF.B_TK5uWC.js → diagram-KO2AKTUF.-7vl3bXs.js} +1 -1
- package/web/_astro/{diagram-LMA3HP47.JkXKK7CO.js → diagram-LMA3HP47.BHVV-UH3.js} +1 -1
- package/web/_astro/{diagram-OG6HWLK6.BzMN8Bd6.js → diagram-OG6HWLK6.DHAuW9sK.js} +1 -1
- package/web/_astro/{erDiagram-TEJ5UH35.DVZaWGUd.js → erDiagram-TEJ5UH35.DMRLQwPN.js} +1 -1
- package/web/_astro/{flowDiagram-I6XJVG4X.rjEiWUfR.js → flowDiagram-I6XJVG4X.B3EpPp_8.js} +1 -1
- package/web/_astro/{ganttDiagram-6RSMTGT7.C_EgAarK.js → ganttDiagram-6RSMTGT7.BgUrExXM.js} +1 -1
- package/web/_astro/{gitGraphDiagram-PVQCEYII.B-QQSDsK.js → gitGraphDiagram-PVQCEYII.CU-9yVN4.js} +1 -1
- package/web/_astro/{index.B5MTfe7k.css → index.BhvM3djt.css} +1 -1
- package/web/_astro/{infoDiagram-5YYISTIA.DlWesz7T.js → infoDiagram-5YYISTIA.CvVTCRLe.js} +1 -1
- package/web/_astro/{ishikawaDiagram-YF4QCWOH.BUMZOawi.js → ishikawaDiagram-YF4QCWOH.BUzOfKPf.js} +1 -1
- package/web/_astro/{journeyDiagram-JHISSGLW.CWfkxfjY.js → journeyDiagram-JHISSGLW.BBTb7ziz.js} +1 -1
- package/web/_astro/{kanban-definition-UN3LZRKU.B-YpMwXf.js → kanban-definition-UN3LZRKU.CTQQr70i.js} +1 -1
- package/web/_astro/{linear.D7uqzENp.js → linear.CLKlJPiS.js} +1 -1
- package/web/_astro/{mermaid.core.CxrNppBD.js → mermaid.core.BA4wFhLP.js} +4 -4
- package/web/_astro/{mindmap-definition-RKZ34NQL.B4Qe7cM2.js → mindmap-definition-RKZ34NQL.t1vG2l5e.js} +1 -1
- package/web/_astro/{pieDiagram-4H26LBE5.Ds-5j2ro.js → pieDiagram-4H26LBE5.C8zJhyTu.js} +1 -1
- package/web/_astro/{quadrantDiagram-W4KKPZXB.tBd38uNC.js → quadrantDiagram-W4KKPZXB.DqYYvb7T.js} +1 -1
- package/web/_astro/{requirementDiagram-4Y6WPE33.sFENkWl3.js → requirementDiagram-4Y6WPE33.lzfDZ9nI.js} +1 -1
- package/web/_astro/{sankeyDiagram-5OEKKPKP.BeB-Hk7C.js → sankeyDiagram-5OEKKPKP.lm6N5ORM.js} +1 -1
- package/web/_astro/{sequenceDiagram-3UESZ5HK.DnTeaSpx.js → sequenceDiagram-3UESZ5HK.E6_IV4Dj.js} +1 -1
- package/web/_astro/{stateDiagram-AJRCARHV.B-8Jt5EJ.js → stateDiagram-AJRCARHV.Bnv6Ok2p.js} +1 -1
- package/web/_astro/{stateDiagram-v2-BHNVJYJU.Br7xoqMW.js → stateDiagram-v2-BHNVJYJU.mJHH_Ng_.js} +1 -1
- package/web/_astro/{timeline-definition-PNZ67QCA.C-3WdOyi.js → timeline-definition-PNZ67QCA.BhRgqSLa.js} +1 -1
- package/web/_astro/{vennDiagram-CIIHVFJN.DCIs7Lc6.js → vennDiagram-CIIHVFJN.pjb5DMC9.js} +1 -1
- package/web/_astro/{wardleyDiagram-YWT4CUSO.rGAL-bbz.js → wardleyDiagram-YWT4CUSO.CW20KUng.js} +1 -1
- package/web/_astro/{xychartDiagram-2RQKCTM6.hkfQKiRl.js → xychartDiagram-2RQKCTM6.vX9_yuMl.js} +1 -1
- package/web/index.html +2 -2
- package/config/corpus-baseline.json +0 -1805
- package/config/workflow-composition-baseline.json +0 -615
- package/web/_astro/BoardApp.E12MFjOS.js +0 -1
- package/web/_astro/channel.5cYKr5cs.js +0 -1
|
@@ -17,7 +17,10 @@
|
|
|
17
17
|
# -> feature-transition (conditional: if vars.feature set)
|
|
18
18
|
# -> branch-cleanup (conditional: if vars.merge=true)
|
|
19
19
|
# -> done
|
|
20
|
-
#
|
|
20
|
+
# task-resolve with a VALIDATED empty list short-circuits to `skipped`;
|
|
21
|
+
# invalid wrap input, an unresolved/non-completed task, a failed metrics
|
|
22
|
+
# capture, a failed required sync, or a failed feature gate routes to
|
|
23
|
+
# `failed` (0770) instead of being absorbed by an always-defensive skip.
|
|
21
24
|
#
|
|
22
25
|
# Vars (passed as a JSON object via `--vars`):
|
|
23
26
|
# tasks — JSON array of WBS strings, passed as a JSON-encoded STRING value
|
|
@@ -39,21 +42,33 @@
|
|
|
39
42
|
# a
|
|
40
43
|
# single model query that both repairs doc drift (sp:doc-evolve) and captures
|
|
41
44
|
# working learnings (task 0607 R2 — one query answers both; count 2 -> 1).
|
|
42
|
-
# - feature-transition is a soft shell (always exit 0)
|
|
43
|
-
#
|
|
45
|
+
# - feature-transition is a soft shell (always exit 0); a failed required sync
|
|
46
|
+
# or feature gate records FAIL and routes to `failed` instead of claiming
|
|
47
|
+
# success (0770).
|
|
48
|
+
# - 0770 truthful outcomes: vars.tasks is parsed and validated ONCE at
|
|
49
|
+
# task-resolve (JSON array of non-empty WBS strings, every member resolved
|
|
50
|
+
# via `task show` to a completed status); siblings consume the normalized
|
|
51
|
+
# run-scoped artifact and never re-parse raw input (malformed input can no
|
|
52
|
+
# longer be silently re-interpreted as an empty list). All captures are
|
|
53
|
+
# run-scoped under .spur/run/<runId>-wrapup-*; a lookup failure is
|
|
54
|
+
# recorded, never silently omitted as success.
|
|
44
55
|
# - branch-cleanup HITL is exhaustive (yes/no/cancel → done; missing answer
|
|
45
|
-
# defense).
|
|
56
|
+
# defense) and consent-only: it records the decision, performs no git op.
|
|
46
57
|
# - Wrap-up never mutates task status (consumes completed work only).
|
|
47
58
|
|
|
48
59
|
"$schema": "@gobing-ai/spur/schemas/state-machine-workflow.schema.json"
|
|
49
60
|
kind: state-machine
|
|
50
61
|
name: wrapup-pipeline
|
|
62
|
+
version: "1"
|
|
51
63
|
description: "Post-execution wrap-up: doc-sync (doc drift + learning capture), metrics, feature-transition, branch-cleanup"
|
|
52
64
|
iterationBound: 10
|
|
53
65
|
initialState: start
|
|
54
66
|
terminalStates:
|
|
55
67
|
- done
|
|
56
68
|
- skipped
|
|
69
|
+
- failed
|
|
70
|
+
failureStates:
|
|
71
|
+
- failed
|
|
57
72
|
vars:
|
|
58
73
|
tasks: "[]"
|
|
59
74
|
feature: ""
|
|
@@ -64,22 +79,11 @@ vars:
|
|
|
64
79
|
__runId: ""
|
|
65
80
|
mode: ""
|
|
66
81
|
stepTimeoutMs: "1800000"
|
|
67
|
-
#
|
|
68
|
-
#
|
|
69
|
-
# only
|
|
70
|
-
# sweep that observes feature-level findings — so a transition that arms one
|
|
71
|
-
# would go green. This gate is therefore the CORPUS SWEEP ALONE (~29 s), not
|
|
72
|
-
# `spur-check-new`: every task in the feature already paid a full `spur-check`
|
|
73
|
-
# in its own pipeline, and the states that run before this hop (`doc-sync`,
|
|
74
|
-
# `metrics-record`) write only markdown and task sections, which Biome skips
|
|
75
|
-
# (`ignoreUnknown: true`). Re-running the ~105 s per-task gate here
|
|
76
|
-
# re-verifies
|
|
77
|
-
# unchanged code and measures nothing new. Soft by design: the gate reports
|
|
78
|
-
# PASS/FAIL and lets the operator decide; it never hard-fails the wrap-up
|
|
79
|
-
# shell.
|
|
82
|
+
# Recheck the affected feature after sync (D61 / ADR-108), not the corpus
|
|
83
|
+
# or full code suite. Explicit caller overrides remain supported.
|
|
80
84
|
# TRUSTED CONFIG ONLY — this string is executed via `sh -c` (same surface as
|
|
81
85
|
# task-pipeline's qualityGateCmd). Never interpolate untrusted input into it.
|
|
82
|
-
featureGateCmd:
|
|
86
|
+
featureGateCmd: '$spurBin feature check "$feature"'
|
|
83
87
|
__hitlAnswer: ""
|
|
84
88
|
|
|
85
89
|
states:
|
|
@@ -94,10 +98,17 @@ states:
|
|
|
94
98
|
|
|
95
99
|
- id: task-resolve
|
|
96
100
|
description: >
|
|
97
|
-
Validate the task list and evaluate closed proportional routing (0758
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
+
Validate the task list ONCE and evaluate closed proportional routing (0758
|
|
102
|
+
R1-R4). Validation (0770): vars.tasks must be a JSON array of non-empty
|
|
103
|
+
WBS strings, deduplicated in first-seen order, and every member must
|
|
104
|
+
resolve via `spur task show` to a completed status (done/cancelled).
|
|
105
|
+
Invalid input or an unresolved/non-completed task records FAIL (reason
|
|
106
|
+
into the run-scoped reason file) and routes to `failed`; siblings consume
|
|
107
|
+
the normalized run-scoped artifact .spur/run/<runId>-wrapup-tasks.json
|
|
108
|
+
and never re-parse raw input. A validated empty list -> skipped;
|
|
109
|
+
tasks > 0 && mode == fast -> fast-path (metrics-record, bypassing
|
|
110
|
+
doc-sync); missing/unknown/conflict -> doc-sync (safety-path). Writes
|
|
111
|
+
bounded machine-readable reason to reasonFile.
|
|
101
112
|
onEnter:
|
|
102
113
|
- kind: note
|
|
103
114
|
options:
|
|
@@ -130,6 +141,49 @@ states:
|
|
|
130
141
|
printf '%s %s\n' "$RUN_ID" "$(cat "$REASON_FILE")"
|
|
131
142
|
>> .spur/memory/wrapup-routes.log &&
|
|
132
143
|
exit 0
|
|
144
|
+
# 0770 truthful input: parse and validate vars.tasks exactly once, here.
|
|
145
|
+
# Malformed JSON, a non-array, a non-string or empty entry, an unresolved
|
|
146
|
+
# task, or a task not at a completed status records FAIL (routed to
|
|
147
|
+
# `failed` by the first declared edge) and is never re-interpreted as an
|
|
148
|
+
# empty list by a later sibling guard. The normalized, deduplicated list
|
|
149
|
+
# is persisted run-scoped; metrics/doc siblings only ever read it.
|
|
150
|
+
- kind: shell
|
|
151
|
+
options:
|
|
152
|
+
command: >-
|
|
153
|
+
mkdir -p .spur/run &&
|
|
154
|
+
RUN_ID="$__runId" &&
|
|
155
|
+
if [ -z "$RUN_ID" ]; then
|
|
156
|
+
echo "task-resolve: __runId is empty — refusing the legacy fixed-path fallback" >&2;
|
|
157
|
+
exit 1;
|
|
158
|
+
fi &&
|
|
159
|
+
REASON_FILE=".spur/run/$RUN_ID-route-reason.txt" &&
|
|
160
|
+
STATUS_FILE=".spur/run/$RUN_ID-wrapup-resolve.status" &&
|
|
161
|
+
TASKS_FILE=".spur/run/$RUN_ID-wrapup-tasks.json" &&
|
|
162
|
+
if ! printf '%s' "$tasks" | jq -e 'type == "array" and all(.[]; type == "string" and length > 0)' > /dev/null 2>&1; then
|
|
163
|
+
echo "task-resolve: tasks must be a JSON array of non-empty WBS strings" >&2;
|
|
164
|
+
echo "failed:tasks is not a JSON array of non-empty WBS strings" > "$REASON_FILE";
|
|
165
|
+
printf 'FAIL\n' > "$STATUS_FILE";
|
|
166
|
+
else
|
|
167
|
+
printf '%s' "$tasks" | jq -c 'reduce .[] as $w ([]; if any(.[]; . == $w) then . else . + [$w] end)' > "$TASKS_FILE";
|
|
168
|
+
RESOLVE_RC=0;
|
|
169
|
+
for wbs in $(jq -r '.[]' "$TASKS_FILE"); do
|
|
170
|
+
TASK_JSON=$($spurBin task show "$wbs" --json 2>/dev/null || true);
|
|
171
|
+
TASK_STATUS=$(printf '%s' "$TASK_JSON" | jq -r '.frontmatter.status // .status // empty' 2>/dev/null || true);
|
|
172
|
+
if [ "$TASK_STATUS" != "done" ] && [ "$TASK_STATUS" != "cancelled" ]; then
|
|
173
|
+
STATUS_DISPLAY="$TASK_STATUS";
|
|
174
|
+
if [ -z "$STATUS_DISPLAY" ]; then STATUS_DISPLAY=unresolved; fi;
|
|
175
|
+
echo "task-resolve: task $wbs did not resolve to a completed status (status=$STATUS_DISPLAY)" >&2;
|
|
176
|
+
RESOLVE_RC=1;
|
|
177
|
+
fi;
|
|
178
|
+
done;
|
|
179
|
+
if [ "$RESOLVE_RC" -eq 0 ]; then
|
|
180
|
+
printf 'PASS\n' > "$STATUS_FILE";
|
|
181
|
+
else
|
|
182
|
+
echo "failed:unresolved or non-completed task (see $TASKS_FILE)" > "$REASON_FILE";
|
|
183
|
+
printf 'FAIL\n' > "$STATUS_FILE";
|
|
184
|
+
fi;
|
|
185
|
+
fi;
|
|
186
|
+
exit 0
|
|
133
187
|
|
|
134
188
|
- id: doc-sync
|
|
135
189
|
description: >
|
|
@@ -150,59 +204,72 @@ states:
|
|
|
150
204
|
docs/00_ADR.md, docs/03_ARCHITECTURE.md, docs/04_DESIGN.md, docs/design/* per constitution edit
|
|
151
205
|
rules; do not write task/feature corpus. THEN extract working learnings from tasks ${vars.tasks} —
|
|
152
206
|
conventions, errors fixed, patterns, gotchas, grouped by date and task WBS — as raw markdown
|
|
153
|
-
(no fences) and END your final message with that markdown (it is captured to
|
|
207
|
+
(no fences) and END your final message with that markdown (it is captured to the
|
|
208
|
+
run-scoped .spur/run/${vars.__runId}-wrapup-learnings.md).
|
|
154
209
|
# Declared Layer-1 role (0538 R2): routing reason beside the agent:
|
|
155
210
|
# pin.
|
|
156
211
|
role: coder
|
|
157
|
-
answerFile: .spur/run
|
|
158
|
-
expectFile: .spur/run
|
|
212
|
+
answerFile: .spur/run/${vars.__runId}-wrapup-learnings.md
|
|
213
|
+
expectFile: .spur/run/${vars.__runId}-wrapup-learnings.md
|
|
159
214
|
timeoutMs: ${vars.stepTimeoutMs}
|
|
160
215
|
- kind: shell
|
|
161
216
|
options:
|
|
162
217
|
command: >-
|
|
163
|
-
mkdir -p .spur/memory &&
|
|
164
|
-
|
|
165
|
-
|
|
218
|
+
mkdir -p .spur/run .spur/memory &&
|
|
219
|
+
LEARNINGS_FILE=".spur/run/$__runId-wrapup-learnings.md" &&
|
|
220
|
+
if test -s "$LEARNINGS_FILE"; then
|
|
221
|
+
cat "$LEARNINGS_FILE" >> .spur/memory/learnings.md &&
|
|
166
222
|
printf '\n' >> .spur/memory/learnings.md;
|
|
167
223
|
else
|
|
168
|
-
echo "wrapup: learning capture missing or empty at $PWD
|
|
224
|
+
echo "wrapup: learning capture missing or empty at $PWD/$LEARNINGS_FILE - append skipped" >&2;
|
|
169
225
|
fi &&
|
|
170
226
|
exit 0
|
|
171
227
|
|
|
172
228
|
- id: metrics-record
|
|
173
229
|
description: >
|
|
174
230
|
Append one JSONL row per task to .spur/memory/wrapup-metrics.jsonl deterministically.
|
|
231
|
+
Reads ONLY the normalized run-scoped task list written by task-resolve (0770):
|
|
232
|
+
a lookup failure for any member records FAIL and routes to `failed` — a
|
|
233
|
+
missing metrics row is never silently absorbed as success.
|
|
175
234
|
onEnter:
|
|
176
235
|
- kind: shell
|
|
177
236
|
options:
|
|
178
237
|
command: >-
|
|
179
|
-
mkdir -p .spur/memory &&
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
fi;
|
|
192
|
-
ts=$(date -u +"%Y-%m-%dT%H:%M:%SZ");
|
|
193
|
-
printf '{"wbs":"%s","feature_id":"%s","status":"%s","verdict":"%s","timestamp":"%s"}\n' "$wbs" "$feat" "$stat" "$verdict" "$ts" >> .spur/memory/wrapup-metrics.jsonl;
|
|
238
|
+
mkdir -p .spur/run .spur/memory &&
|
|
239
|
+
STATUS_FILE=".spur/run/$__runId-wrapup-metrics.status" &&
|
|
240
|
+
TASKS_FILE=".spur/run/$__runId-wrapup-tasks.json" &&
|
|
241
|
+
METRICS_RC=0 &&
|
|
242
|
+
for wbs in $(jq -r '.[]' "$TASKS_FILE" 2>/dev/null); do
|
|
243
|
+
task_json=$($spurBin task show "$wbs" --json 2>/dev/null || true);
|
|
244
|
+
if [ -n "$task_json" ]; then
|
|
245
|
+
feat=$(printf '%s' "$task_json" | jq -r '.frontmatter.feature_id // .feature_id // empty' 2>/dev/null || true);
|
|
246
|
+
stat=$(printf '%s' "$task_json" | jq -r '.frontmatter.status // .status // "unknown"' 2>/dev/null || true);
|
|
247
|
+
verdict="UNKNOWN";
|
|
248
|
+
if [ -f ".spur/run/$wbs-verdict.json" ]; then
|
|
249
|
+
v=$(jq -r '.verdict // "UNKNOWN"' ".spur/run/$wbs-verdict.json" 2>/dev/null || true);
|
|
250
|
+
if [ -n "$v" ]; then verdict="$v"; fi;
|
|
194
251
|
fi;
|
|
252
|
+
ts=$(date -u +"%Y-%m-%dT%H:%M:%SZ");
|
|
253
|
+
printf '{"wbs":"%s","feature_id":"%s","status":"%s","verdict":"%s","timestamp":"%s"}\n' "$wbs" "$feat" "$stat" "$verdict" "$ts" >> .spur/memory/wrapup-metrics.jsonl;
|
|
254
|
+
else
|
|
255
|
+
echo "metrics-record: task $wbs did not resolve — recording FAIL instead of silently omitting its metrics row" >&2;
|
|
256
|
+
METRICS_RC=1;
|
|
195
257
|
fi;
|
|
196
|
-
done
|
|
258
|
+
done;
|
|
259
|
+
if [ "$METRICS_RC" -eq 0 ]; then printf 'PASS\n' > "$STATUS_FILE"; else printf 'FAIL\n' > "$STATUS_FILE"; fi;
|
|
197
260
|
exit 0
|
|
198
261
|
|
|
199
262
|
- id: feature-transition
|
|
200
263
|
description: >
|
|
201
264
|
If vars.feature is set, sync feature status via bounded feature-sync-bounded
|
|
202
|
-
(0411). Soft shell —
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
265
|
+
(0411). Soft shell — the run is never aborted after learnings/metrics
|
|
266
|
+
landed; instead (0770) a failed required sync or a failed feature gate
|
|
267
|
+
records FAIL in the run-scoped sync status and routes to `failed`, and a
|
|
268
|
+
successful sync that applied no transition is reported as an explicit
|
|
269
|
+
no-change. Empty vars.feature is NOT a blocked sync but a mis-invocation
|
|
270
|
+
(the engine only enters this state when vars.feature is set) and fails
|
|
271
|
+
loud (dogfood 2026-08-15, feature I3: a silent exit 0 made the wrap look
|
|
272
|
+
complete with no transition).
|
|
206
273
|
onEnter:
|
|
207
274
|
# Genuinely soft: the terminating `exit 0` must be reached on BOTH
|
|
208
275
|
# branches, so
|
|
@@ -217,11 +284,12 @@ states:
|
|
|
217
284
|
# path (`spur init` does not scaffold `plugins/sp/`), so seeded projects
|
|
218
285
|
# fall
|
|
219
286
|
# back to the plain `spur feature sync` verb.
|
|
220
|
-
# R1 (0625): a feature transition that changed state may have
|
|
221
|
-
#
|
|
222
|
-
#
|
|
223
|
-
# may follow a partial multi-hop
|
|
224
|
-
# by design: the operator
|
|
287
|
+
# R1 (0625): a feature transition that changed state may have left
|
|
288
|
+
# cross-task fallout the per-task fast gates never see. Run the
|
|
289
|
+
# affected-feature gate (featureGateCmd) after an
|
|
290
|
+
# applied sync OR a non-zero exit that may follow a partial multi-hop
|
|
291
|
+
# transition, then report PASS/FAIL. Soft by design: the operator
|
|
292
|
+
# decides, and wrap-up never hard-fails here.
|
|
225
293
|
# NOTE: do not add
|
|
226
294
|
# `#` comments INSIDE the folded shell string — YAML `>-` folds them onto
|
|
227
295
|
# the following statement line and comments it out.
|
|
@@ -241,24 +309,41 @@ states:
|
|
|
241
309
|
fi);
|
|
242
310
|
SYNC_RC=$?;
|
|
243
311
|
printf '%s\n' "$SYNC_OUTPUT";
|
|
244
|
-
|
|
312
|
+
if printf '%s' "$SYNC_OUTPUT" | jq -e 'has("applied")' > /dev/null 2>&1; then
|
|
313
|
+
APPLIED=$(printf '%s' "$SYNC_OUTPUT" | jq -r '.applied // false' 2>/dev/null || echo false);
|
|
314
|
+
else
|
|
315
|
+
APPLIED="invalid";
|
|
316
|
+
fi;
|
|
317
|
+
GATE="skipped";
|
|
245
318
|
if [ "$APPLIED" = "true" ] || [ "$SYNC_RC" -ne 0 ]; then
|
|
246
|
-
echo "feature-transition: sync applied or failed after a possible partial transition for $feature — running
|
|
319
|
+
echo "feature-transition: sync applied or failed after a possible partial transition for $feature — running feature gate: $featureGateCmd";
|
|
247
320
|
if sh -c "$featureGateCmd"; then
|
|
248
|
-
|
|
321
|
+
GATE="PASS";
|
|
322
|
+
echo "feature-transition: feature gate PASS for feature $feature";
|
|
249
323
|
else
|
|
250
|
-
|
|
324
|
+
GATE="FAIL";
|
|
325
|
+
echo "feature-transition: feature gate FAIL for feature $feature — inspect findings before reporting the transition complete" >&2;
|
|
251
326
|
fi;
|
|
252
327
|
else
|
|
253
|
-
echo "feature-transition: sync did not apply a transition (rc=$SYNC_RC, applied=$APPLIED) —
|
|
328
|
+
echo "feature-transition: sync did not apply a transition (rc=$SYNC_RC, applied=$APPLIED) — feature gate skipped (explicit no-change)";
|
|
329
|
+
fi;
|
|
330
|
+
SYNC_STATUS="PASS";
|
|
331
|
+
if [ "$SYNC_RC" -ne 0 ] || [ "$GATE" = "FAIL" ] || [ "$APPLIED" = "invalid" ]; then SYNC_STATUS="FAIL"; fi;
|
|
332
|
+
if [ "$SYNC_STATUS" = "PASS" ]; then
|
|
333
|
+
printf 'PASS\n' > ".spur/run/$__runId-wrapup-sync.status";
|
|
334
|
+
else
|
|
335
|
+
printf 'FAIL\n' > ".spur/run/$__runId-wrapup-sync.status";
|
|
254
336
|
fi;
|
|
255
337
|
exit 0
|
|
256
338
|
|
|
257
339
|
- id: branch-cleanup
|
|
258
340
|
description: >
|
|
259
|
-
Irreversible HITL gate. If vars.merge=true,
|
|
260
|
-
|
|
261
|
-
|
|
341
|
+
Irreversible-operation HITL gate, consent-only (0770). If vars.merge=true,
|
|
342
|
+
branch cleanup (merge or delete) is a candidate — but this gate records
|
|
343
|
+
the operator decision and performs NO git operation itself; an operator
|
|
344
|
+
or a separate approved surface executes any actual merge/delete. This
|
|
345
|
+
gate ALWAYS pauses — even under --auto — because branch operations are
|
|
346
|
+
irreversible (Auto-Decision Principle #6).
|
|
262
347
|
pause: true
|
|
263
348
|
onEnter:
|
|
264
349
|
- kind: hitl.confirm
|
|
@@ -267,12 +352,13 @@ states:
|
|
|
267
352
|
|
|
268
353
|
- id: done
|
|
269
354
|
description: >
|
|
270
|
-
Terminal — wrap-up complete.
|
|
271
|
-
metrics recorded, feature advanced
|
|
355
|
+
Terminal — wrap-up complete. Summary: tasks wrapped, learnings captured,
|
|
356
|
+
metrics recorded, feature advanced or explicitly no-change (if applicable),
|
|
357
|
+
branch-cleanup decision recorded (consent-only; no git operation performed).
|
|
272
358
|
onEnter:
|
|
273
359
|
- kind: note
|
|
274
360
|
options:
|
|
275
|
-
message: "Wrap-up pipeline complete for tasks: ${vars.tasks}. Learnings at .spur/memory/learnings.md, metrics at .spur/memory/wrapup-metrics.jsonl."
|
|
361
|
+
message: "Wrap-up pipeline complete for tasks: ${vars.tasks}. Learnings at .spur/memory/learnings.md, metrics at .spur/memory/wrapup-metrics.jsonl. Branch cleanup: consent recorded only (merge=${vars.merge}); no git operation performed by wrap-up."
|
|
276
362
|
# Checkpoint write: record session state for resume (Phase 4, task 0171
|
|
277
363
|
# R3)
|
|
278
364
|
- kind: shell
|
|
@@ -282,6 +368,13 @@ states:
|
|
|
282
368
|
- id: skipped
|
|
283
369
|
description: Terminal — wrap-up skipped (empty task list or operator abort).
|
|
284
370
|
|
|
371
|
+
- id: failed
|
|
372
|
+
description: >
|
|
373
|
+
Terminal — wrap-up failed (0770): invalid wrap input, an unresolved or
|
|
374
|
+
non-completed task, a failed metrics capture, a failed required sync, or
|
|
375
|
+
a failed feature gate. Already-written artifacts (learnings, metrics
|
|
376
|
+
rows, route reason) are preserved; nothing is rolled back.
|
|
377
|
+
|
|
285
378
|
transitions:
|
|
286
379
|
# ── start -> task-resolve ──
|
|
287
380
|
- from: start
|
|
@@ -290,7 +383,17 @@ transitions:
|
|
|
290
383
|
guard:
|
|
291
384
|
kind: always
|
|
292
385
|
|
|
293
|
-
# ── task-resolve: proportional route table (
|
|
386
|
+
# ── task-resolve: validation fail + proportional route table (0758 R1-R4, 0770) ──
|
|
387
|
+
- from: task-resolve
|
|
388
|
+
to: failed
|
|
389
|
+
description: >
|
|
390
|
+
Validation FAIL (0770): malformed wrap input, a non-string/empty entry, an
|
|
391
|
+
unresolved or non-completed task, or an empty __runId. Declared before the
|
|
392
|
+
route edges so invalid input can never be absorbed by the empty-list skip.
|
|
393
|
+
guard:
|
|
394
|
+
kind: shell
|
|
395
|
+
options:
|
|
396
|
+
command: 'test "$(cat .spur/run/$__runId-wrapup-resolve.status 2>/dev/null)" = FAIL'
|
|
294
397
|
- from: task-resolve
|
|
295
398
|
to: skipped
|
|
296
399
|
description: Task list is empty — skip wrap-up.
|
|
@@ -313,8 +416,10 @@ transitions:
|
|
|
313
416
|
options:
|
|
314
417
|
command: 'test "$(echo "$tasks" | jq length 2>/dev/null || echo 0)" -gt 0 && test "$mode" != fast'
|
|
315
418
|
- from: task-resolve
|
|
316
|
-
to:
|
|
317
|
-
description:
|
|
419
|
+
to: failed
|
|
420
|
+
description: >
|
|
421
|
+
Defense (0770) — no PASS/FAIL resolve status (shell crashed or never ran):
|
|
422
|
+
route to `failed` rather than claiming a skip.
|
|
318
423
|
guard:
|
|
319
424
|
kind: always
|
|
320
425
|
|
|
@@ -325,47 +430,67 @@ transitions:
|
|
|
325
430
|
guard:
|
|
326
431
|
kind: always
|
|
327
432
|
|
|
328
|
-
# ── metrics-record: conditional routing
|
|
329
|
-
# Declaration order matters:
|
|
330
|
-
# set),
|
|
331
|
-
#
|
|
433
|
+
# ── metrics-record: fail edge, then conditional routing (feature → merge → done) ──
|
|
434
|
+
# Declaration order matters: the FAIL edge is tried first (0770), then
|
|
435
|
+
# feature-transition (if feature is set), then branch-cleanup (if merge=true
|
|
436
|
+
# but no feature), then done (neither). Routing edges additionally require
|
|
437
|
+
# the metrics status PASS so a missing status can never claim success.
|
|
438
|
+
- from: metrics-record
|
|
439
|
+
to: failed
|
|
440
|
+
description: >
|
|
441
|
+
Metrics capture failed for at least one task — a missing metrics row is
|
|
442
|
+
recorded as failure, never silently omitted as success (0770).
|
|
443
|
+
guard:
|
|
444
|
+
kind: shell
|
|
445
|
+
options:
|
|
446
|
+
command: 'test "$(cat .spur/run/$__runId-wrapup-metrics.status 2>/dev/null)" = FAIL'
|
|
332
447
|
- from: metrics-record
|
|
333
448
|
to: feature-transition
|
|
334
|
-
description:
|
|
449
|
+
description: Metrics PASS and feature id is set — advance feature through legal lifecycle edges.
|
|
335
450
|
guard:
|
|
336
451
|
kind: shell
|
|
337
452
|
options:
|
|
338
|
-
command: 'test -n "$feature"'
|
|
453
|
+
command: 'test "$(cat .spur/run/$__runId-wrapup-metrics.status 2>/dev/null)" = PASS && test -n "$feature"'
|
|
339
454
|
- from: metrics-record
|
|
340
455
|
to: branch-cleanup
|
|
341
|
-
description:
|
|
456
|
+
description: Metrics PASS, no feature id, but merge is requested — go to branch cleanup.
|
|
342
457
|
guard:
|
|
343
458
|
kind: shell
|
|
344
459
|
options:
|
|
345
|
-
command: 'test -z "$feature" && test "$merge" = true'
|
|
460
|
+
command: 'test "$(cat .spur/run/$__runId-wrapup-metrics.status 2>/dev/null)" = PASS && test -z "$feature" && test "$merge" = true'
|
|
346
461
|
- from: metrics-record
|
|
347
462
|
to: done
|
|
348
|
-
description:
|
|
463
|
+
description: Metrics PASS, no feature id and no merge — wrap-up is done.
|
|
349
464
|
guard:
|
|
350
465
|
kind: shell
|
|
351
466
|
options:
|
|
352
|
-
command: 'test -z "$feature" && test "$merge" != true'
|
|
467
|
+
command: 'test "$(cat .spur/run/$__runId-wrapup-metrics.status 2>/dev/null)" = PASS && test -z "$feature" && test "$merge" != true'
|
|
353
468
|
|
|
354
|
-
# ── feature-transition:
|
|
469
|
+
# ── feature-transition: fail edge, then branch-cleanup (if merge) or done ──
|
|
470
|
+
- from: feature-transition
|
|
471
|
+
to: failed
|
|
472
|
+
description: >
|
|
473
|
+
Required synchronization failed (0770): non-zero sync exit, an invalid
|
|
474
|
+
sync result, or a failed feature gate. The failed status carries the
|
|
475
|
+
actual reason; already-written learnings/metrics stay on disk.
|
|
476
|
+
guard:
|
|
477
|
+
kind: shell
|
|
478
|
+
options:
|
|
479
|
+
command: 'test "$(cat .spur/run/$__runId-wrapup-sync.status 2>/dev/null)" = FAIL'
|
|
355
480
|
- from: feature-transition
|
|
356
481
|
to: branch-cleanup
|
|
357
|
-
description:
|
|
482
|
+
description: Sync PASS — merge is requested.
|
|
358
483
|
guard:
|
|
359
484
|
kind: shell
|
|
360
485
|
options:
|
|
361
|
-
command: 'test "$merge" = true'
|
|
486
|
+
command: 'test "$(cat .spur/run/$__runId-wrapup-sync.status 2>/dev/null)" = PASS && test "$merge" = true'
|
|
362
487
|
- from: feature-transition
|
|
363
488
|
to: done
|
|
364
|
-
description:
|
|
489
|
+
description: Sync PASS — no merge requested.
|
|
365
490
|
guard:
|
|
366
491
|
kind: shell
|
|
367
492
|
options:
|
|
368
|
-
command: 'test "$merge" != true'
|
|
493
|
+
command: 'test "$(cat .spur/run/$__runId-wrapup-sync.status 2>/dev/null)" = PASS && test "$merge" != true'
|
|
369
494
|
|
|
370
495
|
# ── branch-cleanup: HITL exhaustive routing (yes / no / cancel → done) ──
|
|
371
496
|
# No irreversible git op is wired yet; confirmation is recorded, then wrap
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gobing-ai/spur",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.74",
|
|
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",
|
package/plugins/sp/plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sp",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.74",
|
|
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"]
|
|
@@ -198,6 +198,7 @@ function parseProvenance(reportMarkdown) {
|
|
|
198
198
|
contractDigest: String(obj.contractDigest ?? ""),
|
|
199
199
|
skillDigest: String(obj.skillDigest ?? ""),
|
|
200
200
|
workflowDigest: String(obj.workflowDigest ?? ""),
|
|
201
|
+
helperDigest: String(obj.helperDigest ?? ""),
|
|
201
202
|
coverage: coverage.map((c) => ({
|
|
202
203
|
source: String(c.source ?? ""),
|
|
203
204
|
status: String(c.status ?? ""),
|
|
@@ -244,6 +245,8 @@ function decideCache(cached, current, opts) {
|
|
|
244
245
|
reasons.push("logic-changed:skill");
|
|
245
246
|
if (cached.workflowDigest !== current.workflowDigest)
|
|
246
247
|
reasons.push("logic-changed:workflow");
|
|
248
|
+
if (cached.helperDigest !== current.helperDigest)
|
|
249
|
+
reasons.push("logic-changed:helper");
|
|
247
250
|
const currentSources = new Set(current.coverage.map((c) => c.source));
|
|
248
251
|
if (cached.coverage.some((c) => !currentSources.has(c.source)))
|
|
249
252
|
reasons.push("coverage-degraded");
|
|
@@ -473,6 +476,7 @@ function buildProvenance(opts) {
|
|
|
473
476
|
contractDigest: logicDigest(opts.contractFile),
|
|
474
477
|
skillDigest: logicDigest(opts.skillDir),
|
|
475
478
|
workflowDigest: logicDigest(opts.workflowFile),
|
|
479
|
+
helperDigest: logicDigest(opts.helperFile),
|
|
476
480
|
coverage,
|
|
477
481
|
runId: opts.runId,
|
|
478
482
|
currentArtifactPath: opts.artifact,
|
|
@@ -503,6 +507,7 @@ var YAML_KEYS = [
|
|
|
503
507
|
"contractDigest",
|
|
504
508
|
"skillDigest",
|
|
505
509
|
"workflowDigest",
|
|
510
|
+
"helperDigest",
|
|
506
511
|
"runId",
|
|
507
512
|
"currentArtifactPath",
|
|
508
513
|
"baselineArtifactPath",
|
|
@@ -595,7 +600,7 @@ function diffPorcelain(before, now, expects) {
|
|
|
595
600
|
return [...porcelainPaths(now)].filter((p) => !beforePaths.has(p) && !expects.has(p)).sort();
|
|
596
601
|
}
|
|
597
602
|
var VALID_COMMANDS = "digest, check, paths, assert-clean, probe, stamp, refresh, publish";
|
|
598
|
-
var PROBE_USAGE = "<script> probe --artifact <a.json> --target <report.md> [--baseline <b.json>] [--mode daily|ad-hoc] " + "[--date <YYYY-MM-DD>] [--recompute true] [--out <prov.json>] [--skill-dir <d>] [--contract <f>] [--workflow <f>]";
|
|
603
|
+
var PROBE_USAGE = "<script> probe --artifact <a.json> --target <report.md> [--baseline <b.json>] [--mode daily|ad-hoc] " + "[--date <YYYY-MM-DD>] [--recompute true] [--out <prov.json>] [--skill-dir <d>] [--contract <f>] [--workflow <f>] [--helper <f>]";
|
|
599
604
|
function parseFlags(args) {
|
|
600
605
|
const out = {};
|
|
601
606
|
for (let i = 0;i < args.length; i++) {
|
|
@@ -729,6 +734,7 @@ ${result.problems.map((p) => `- ${p}
|
|
|
729
734
|
skillDir: f["skill-dir"],
|
|
730
735
|
contractFile: f.contract,
|
|
731
736
|
workflowFile: f.workflow,
|
|
737
|
+
helperFile: f.helper,
|
|
732
738
|
contractVersion: f["contract-version"],
|
|
733
739
|
runId: f["run-id"],
|
|
734
740
|
spurVersion: f["spur-version"]
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
*
|
|
5
5
|
* ADR-079 makes cache validity a *derived* fact, not a stored claim: a cached report is reusable
|
|
6
6
|
* only for its model-authored half, and only when a freshly derived semantic digest of the analyze
|
|
7
|
-
* artifact plus the contract/skill/workflow logic digests all match what the cache
|
|
7
|
+
* artifact plus the contract/skill/workflow/helper logic digests all match what the cache
|
|
8
|
+
* recorded (helper digest: task 0771 — the deterministic half is part of cache identity).
|
|
8
9
|
*
|
|
9
10
|
* This script performs deterministic file, hash, and schema work only — no finding, remediation,
|
|
10
11
|
* severity, or ranking logic (that is judgment, owned by the sp:history-anatomy skill). Jobs:
|
|
@@ -68,6 +69,8 @@ export interface CacheProvenance {
|
|
|
68
69
|
contractDigest: string;
|
|
69
70
|
skillDigest: string;
|
|
70
71
|
workflowDigest: string;
|
|
72
|
+
/** Digest of the executing helper twin itself (0771): a changed deterministic half invalidates. */
|
|
73
|
+
helperDigest: string;
|
|
71
74
|
coverage: Array<{ source: string; status: string; lastImportedAt: string | null }>;
|
|
72
75
|
// 0660 R7 audit fields. Recorded in the published frontmatter for provenance; deliberately
|
|
73
76
|
// NOT part of the invalidation matrix — a changed run id or executor is not stale evidence.
|
|
@@ -243,6 +246,7 @@ export function parseProvenance(reportMarkdown: string): CacheProvenance | null
|
|
|
243
246
|
contractDigest: String(obj.contractDigest ?? ''),
|
|
244
247
|
skillDigest: String(obj.skillDigest ?? ''),
|
|
245
248
|
workflowDigest: String(obj.workflowDigest ?? ''),
|
|
249
|
+
helperDigest: String(obj.helperDigest ?? ''),
|
|
246
250
|
coverage: coverage.map((c) => ({
|
|
247
251
|
source: String(c.source ?? ''),
|
|
248
252
|
status: String(c.status ?? ''),
|
|
@@ -300,6 +304,7 @@ export function decideCache(
|
|
|
300
304
|
if (cached.contractDigest !== current.contractDigest) reasons.push('logic-changed:contract');
|
|
301
305
|
if (cached.skillDigest !== current.skillDigest) reasons.push('logic-changed:skill');
|
|
302
306
|
if (cached.workflowDigest !== current.workflowDigest) reasons.push('logic-changed:workflow');
|
|
307
|
+
if (cached.helperDigest !== current.helperDigest) reasons.push('logic-changed:helper');
|
|
303
308
|
|
|
304
309
|
// Coverage cannot degrade: the cache must not claim broader coverage than the current
|
|
305
310
|
// analyze covers. If the cached report covered a source the current analyze no longer does,
|
|
@@ -601,6 +606,7 @@ export interface ProbeOptions {
|
|
|
601
606
|
skillDir?: string;
|
|
602
607
|
contractFile?: string;
|
|
603
608
|
workflowFile?: string;
|
|
609
|
+
helperFile?: string;
|
|
604
610
|
contractVersion?: string;
|
|
605
611
|
runId?: string;
|
|
606
612
|
spurVersion?: string;
|
|
@@ -660,6 +666,7 @@ export function buildProvenance(opts: ProbeOptions): CacheProvenance {
|
|
|
660
666
|
contractDigest: logicDigest(opts.contractFile),
|
|
661
667
|
skillDigest: logicDigest(opts.skillDir),
|
|
662
668
|
workflowDigest: logicDigest(opts.workflowFile),
|
|
669
|
+
helperDigest: logicDigest(opts.helperFile),
|
|
663
670
|
coverage,
|
|
664
671
|
runId: opts.runId,
|
|
665
672
|
currentArtifactPath: opts.artifact,
|
|
@@ -697,6 +704,7 @@ const YAML_KEYS: Array<keyof CacheProvenance> = [
|
|
|
697
704
|
'contractDigest',
|
|
698
705
|
'skillDigest',
|
|
699
706
|
'workflowDigest',
|
|
707
|
+
'helperDigest',
|
|
700
708
|
'runId',
|
|
701
709
|
'currentArtifactPath',
|
|
702
710
|
'baselineArtifactPath',
|
|
@@ -823,7 +831,7 @@ export function diffPorcelain(before: string, now: string, expects: Set<string>)
|
|
|
823
831
|
const VALID_COMMANDS = 'digest, check, paths, assert-clean, probe, stamp, refresh, publish';
|
|
824
832
|
const PROBE_USAGE =
|
|
825
833
|
'<script> probe --artifact <a.json> --target <report.md> [--baseline <b.json>] [--mode daily|ad-hoc] ' +
|
|
826
|
-
'[--date <YYYY-MM-DD>] [--recompute true] [--out <prov.json>] [--skill-dir <d>] [--contract <f>] [--workflow <f>]';
|
|
834
|
+
'[--date <YYYY-MM-DD>] [--recompute true] [--out <prov.json>] [--skill-dir <d>] [--contract <f>] [--workflow <f>] [--helper <f>]';
|
|
827
835
|
|
|
828
836
|
/** `--key value` / `--flag` → record. Bare flags become `"true"` so `--recompute` needs no value. */
|
|
829
837
|
function parseFlags(args: string[]): Record<string, string | undefined> {
|
|
@@ -956,6 +964,7 @@ export function runCacheCli(argv: string[]): CacheCliResult {
|
|
|
956
964
|
skillDir: f['skill-dir'],
|
|
957
965
|
contractFile: f.contract,
|
|
958
966
|
workflowFile: f.workflow,
|
|
967
|
+
helperFile: f.helper,
|
|
959
968
|
contractVersion: f['contract-version'],
|
|
960
969
|
runId: f['run-id'],
|
|
961
970
|
spurVersion: f['spur-version'],
|
|
@@ -325,7 +325,11 @@ function hasPendingRequest(comments, head, login) {
|
|
|
325
325
|
}
|
|
326
326
|
function requireExpectedHead(args, pr) {
|
|
327
327
|
const expected = args.flags.get("--head");
|
|
328
|
-
if (expected
|
|
328
|
+
if (!expected) {
|
|
329
|
+
writeStatus(args, "FAIL");
|
|
330
|
+
fail(args, "--head <sha> is required and must match the reviewed request HEAD", 2);
|
|
331
|
+
}
|
|
332
|
+
if (expected !== pr.headRefOid) {
|
|
329
333
|
writeStatus(args, "FAIL");
|
|
330
334
|
fail(args, `PR HEAD moved from ${expected.slice(0, 7)} to ${pr.headRefOid.slice(0, 7)} \u2014 request a new review`, 2);
|
|
331
335
|
}
|
|
@@ -519,8 +519,14 @@ function hasPendingRequest(comments: readonly GhIssueComment[], head: string, lo
|
|
|
519
519
|
}
|
|
520
520
|
|
|
521
521
|
function requireExpectedHead(args: ParsedArgs, pr: GhPr): void {
|
|
522
|
+
// 0771: head pinning only protects when the flag actually carries a SHA — an empty or
|
|
523
|
+
// missing --head must fail loud instead of silently reviewing whatever HEAD is current.
|
|
522
524
|
const expected = args.flags.get('--head');
|
|
523
|
-
if (expected
|
|
525
|
+
if (!expected) {
|
|
526
|
+
writeStatus(args, 'FAIL');
|
|
527
|
+
fail(args, '--head <sha> is required and must match the reviewed request HEAD', 2);
|
|
528
|
+
}
|
|
529
|
+
if (expected !== pr.headRefOid) {
|
|
524
530
|
writeStatus(args, 'FAIL');
|
|
525
531
|
fail(
|
|
526
532
|
args,
|