@gobing-ai/spur 0.3.50 → 0.3.52
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/corpus-baseline.json +1995 -1939
- package/config/rules/quality/coverage-gate.yaml +3 -2
- package/config/tasks/section-matrix.yaml +7 -2
- package/config/workflows/pr-review.yaml +13 -11
- package/config/workflows/task-lifecycle.yaml +7 -6
- package/config/workflows/task-pipeline.yaml +37 -16
- package/package.json +1 -1
- package/plugins/sp/agents/super-planner.md +3 -2
- package/plugins/sp/agents/super-reviewer.md +5 -0
- package/plugins/sp/commands/dev-review.md +1 -1
- package/plugins/sp/plugin.json +1 -1
- package/plugins/sp/scripts/pr-reviewing.ts +57 -5
- package/plugins/sp/scripts/stage-registry-adapter.ts +40 -25
- package/plugins/sp/skills/code-improvement/SKILL.md +5 -3
- package/plugins/sp/skills/code-verification/SKILL.md +28 -31
- package/plugins/sp/skills/code-verification/references/verdict-schema.md +47 -0
- package/plugins/sp/skills/functional-review/SKILL.md +16 -15
- package/plugins/sp/skills/pr-reviewing/SKILL.md +6 -3
- package/plugins/sp/skills/spec-decomposition/references/decomposition.md +13 -14
- package/plugins/sp/skills/spur-cli/references/tasks/l3-guard-cheatsheet.md +7 -3
- package/plugins/sp/skills/spur-cli/references/tasks/section-editing.md +22 -13
- package/plugins/sp/skills/spur-cli/references/tasks/verbs.md +22 -11
- package/plugins/sp/skills/spur-cli/references/tasks.md +16 -10
- package/plugins/sp/skills/spur-dev/SKILL.md +10 -5
- package/plugins/sp/skills/spur-dev/references/dev-operations.md +5 -5
- package/plugins/sp/skills/spur-dev/references/execution-workflow.md +11 -4
- package/plugins/sp/skills/spur-dev/references/gate-checklists.md +5 -3
- package/plugins/sp/skills/spur-dev/references/section-batching.md +29 -13
- package/spur.js +382 -231
|
@@ -2,8 +2,9 @@ $schema: "@gobing-ai/spur/schemas/rule-file.schema.json"
|
|
|
2
2
|
# Coverage gate — per-file line coverage meets threshold from Bun's lcov output.
|
|
3
3
|
# Absorbed from ts-libs/.spur/rules/quality/coverage-gate.yaml, re-scoped to
|
|
4
4
|
# Spur's monorepo layout:
|
|
5
|
-
# - lcovPath kept at .coverage/lcov.info (Spur's `bun run test`
|
|
6
|
-
#
|
|
5
|
+
# - lcovPath kept at .coverage/lcov.info (Spur's `bun run test:coverage` —
|
|
6
|
+
# what `bun run check` and the `*:full` chains run — writes coverage there
|
|
7
|
+
# via --coverage-dir=.coverage; plain `bun run test` skips coverage)
|
|
7
8
|
# - include expanded to apps/** + packages/** (Spur tests cover both;
|
|
8
9
|
# ts-libs only covers packages/**)
|
|
9
10
|
# - threshold kept at 90 matching bunfig.toml coverageThreshold
|
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
# Section-Status-Matrix: which sections are required/optional/forbidden per task status.
|
|
2
|
-
# Design §3.2 —
|
|
2
|
+
# Design §3.2 — the SOLE semantic authority for task section layout (F92 R1):
|
|
3
|
+
# * VALIDATION (`spur task check`, incl. target-aware `--as <status>` lifecycle guard)
|
|
4
|
+
# * CREATION (`spur task create` / `spur task batch-create` -> `buildTaskSkeleton`)
|
|
5
|
+
# There is NO hand-maintained creation or matrix fallback — packaged/compiled
|
|
6
|
+
# execution loads data copied/generated from THIS canonical file and fails loudly
|
|
7
|
+
# if no asset is reachable. Template files supply section BODY content + guidance
|
|
8
|
+
# only; they never own the heading list (which comes from this matrix).
|
|
3
9
|
#
|
|
4
10
|
# Each VARIANT (the unified template axis — TASK_VARIANTS) maps status →
|
|
5
11
|
# { required, optional, forbidden } section lists. Sections reference the canonical
|
|
6
12
|
# section vocabulary (§2.1). `gate: true` on a status makes L2 violations hard errors.
|
|
7
13
|
#
|
|
8
14
|
# The variant is selected by a task's `template:` frontmatter (default: `standard`).
|
|
9
|
-
# It drives BOTH validation (`spur task check`) AND creation (`buildTaskSkeleton`).
|
|
10
15
|
$schema: "@gobing-ai/spur/schemas/section-matrix.schema.json"
|
|
11
16
|
|
|
12
17
|
variants:
|
|
@@ -43,19 +43,21 @@ terminalStates:
|
|
|
43
43
|
failureStates:
|
|
44
44
|
- failed
|
|
45
45
|
vars:
|
|
46
|
-
mode:
|
|
47
|
-
baseBranch:
|
|
48
|
-
focus:
|
|
49
|
-
noWait:
|
|
50
|
-
waitTimeoutSec:
|
|
51
|
-
waitIntervalSec:
|
|
52
|
-
preReviewCmd:
|
|
53
|
-
__runId:
|
|
46
|
+
mode: "full"
|
|
47
|
+
baseBranch: ""
|
|
48
|
+
focus: ""
|
|
49
|
+
noWait: "false"
|
|
50
|
+
waitTimeoutSec: "600"
|
|
51
|
+
waitIntervalSec: "30"
|
|
52
|
+
preReviewCmd: ""
|
|
53
|
+
__runId: ""
|
|
54
54
|
|
|
55
55
|
states:
|
|
56
56
|
- id: preflight
|
|
57
57
|
description: >
|
|
58
|
-
Soft probe: git/gh/repo checks (detached HEAD, dirty tree, gh auth, GitHub remote
|
|
58
|
+
Soft probe: git/gh/repo checks (detached HEAD, dirty tree, gh auth, GitHub remote,
|
|
59
|
+
base-branch refusal — preflight refuses when the current branch IS the resolved base,
|
|
60
|
+
before any push can publish it).
|
|
59
61
|
Writes PASS|FAIL to .spur/run/${vars.__runId}-pr-preflight.status; always exit 0.
|
|
60
62
|
onEnter:
|
|
61
63
|
- kind: shell
|
|
@@ -64,7 +66,7 @@ states:
|
|
|
64
66
|
mkdir -p .spur/run &&
|
|
65
67
|
STATUS_FILE=".spur/run/$__runId-pr-preflight.status" &&
|
|
66
68
|
set +e &&
|
|
67
|
-
bun "$(superskill script path sp pr-reviewing.ts)" preflight --json > ".spur/run/$__runId-pr-context.json";
|
|
69
|
+
bun "$(superskill script path sp pr-reviewing.ts)" preflight --base "$baseBranch" --json > ".spur/run/$__runId-pr-context.json";
|
|
68
70
|
rc=$?; set -e &&
|
|
69
71
|
if [ "$rc" -eq 0 ]; then printf 'PASS\n' > "$STATUS_FILE"; else printf 'FAIL\n' > "$STATUS_FILE"; fi &&
|
|
70
72
|
exit 0
|
|
@@ -212,7 +214,7 @@ transitions:
|
|
|
212
214
|
command: 'test "$(cat .spur/run/$__runId-pr-preflight.status 2>/dev/null)" = PASS'
|
|
213
215
|
- from: preflight
|
|
214
216
|
to: failed
|
|
215
|
-
description: Preflight red (detached HEAD, dirty tree, gh auth, no GitHub remote) — stop before any publishing
|
|
217
|
+
description: Preflight red (detached HEAD, dirty tree, gh auth, no GitHub remote, or the current branch being the base branch) — stop before any publishing
|
|
216
218
|
guard:
|
|
217
219
|
kind: always
|
|
218
220
|
|
|
@@ -12,15 +12,16 @@ name: task-lifecycle
|
|
|
12
12
|
description: >
|
|
13
13
|
Task lifecycle FSM (design §2.3, §5.1). States are the canonical TaskStatus
|
|
14
14
|
vocabulary (DD-01); transitions encode the §2.3 graph; guards invoke
|
|
15
|
-
`${vars.spurBin} task check
|
|
15
|
+
`${vars.spurBin} task check --as <target>` at the wip→testing and testing→done
|
|
16
|
+
placements (F92 R3): each guard evaluates the task AS the transition target so
|
|
17
|
+
testing→done checks the `done` row instead of the current `testing` row.
|
|
16
18
|
`done` is re-enterable (reopen with a warning); `cancelled` is terminal.
|
|
17
|
-
Guard commands reference the check verb (0051)
|
|
18
|
-
today, behavioral wiring activates as the verb ships. Unconditional transitions
|
|
19
|
+
Guard commands reference the check verb (0051). Unconditional transitions
|
|
19
20
|
use the `always` guard (externally-driven via `requestTransition`, not
|
|
20
21
|
auto-advance).
|
|
21
22
|
|
|
22
23
|
Two-layer done gate (task 0292): this workflow's testing→done guard
|
|
23
|
-
(`spur task check --
|
|
24
|
+
(`spur task check --as done`, F92 R3 — target-aware) is the FIRST gate. The CLI verdict guard
|
|
24
25
|
(`packages/app/src/services/done-transition-guard.ts`) is the SECOND gate,
|
|
25
26
|
invoked by `apps/cli/src/commands/task.ts` for every `spur task update <wbs>
|
|
26
27
|
done` call — it reads `.spur/run/<wbs>-verdict.json` and blocks non-PASS
|
|
@@ -69,14 +70,14 @@ transitions:
|
|
|
69
70
|
guard:
|
|
70
71
|
kind: shell
|
|
71
72
|
options:
|
|
72
|
-
command: '$spurBin task check $wbs'
|
|
73
|
+
command: '$spurBin task check $wbs --as testing'
|
|
73
74
|
- from: testing
|
|
74
75
|
to: done
|
|
75
76
|
description: Testing passed — task complete
|
|
76
77
|
guard:
|
|
77
78
|
kind: shell
|
|
78
79
|
options:
|
|
79
|
-
command: '$spurBin task check $wbs --
|
|
80
|
+
command: '$spurBin task check $wbs --as done'
|
|
80
81
|
|
|
81
82
|
# Reopen: done → wip (warned, mandatory History entry)
|
|
82
83
|
- from: done
|
|
@@ -85,6 +85,12 @@ vars:
|
|
|
85
85
|
# TRUSTED CONFIG ONLY — this string is executed via `sh -c` (see test/test-recheck). Never
|
|
86
86
|
# interpolate untrusted operator/LLM input into qualityGateCmd (task 0436 SECUA residual).
|
|
87
87
|
qualityGateCmd: "bun run format && bun run spur-check"
|
|
88
|
+
# Cheap red-detector run before the full gate on **recheck only**; empty ⇒ no probe
|
|
89
|
+
# (full gate every recheck — the pre-0587 behavior). A project overriding qualityGateCmd
|
|
90
|
+
# should override this too. TRUSTED CONFIG ONLY — executed via `sh -c` (same surface as
|
|
91
|
+
# qualityGateCmd). Invariant: `review` is only ever entered through a full green
|
|
92
|
+
# qualityGateCmd — only the full gate writes PASS to <wbs>-test-gate.status.
|
|
93
|
+
gateProbeCmd: "bun run lint"
|
|
88
94
|
# Max /sp:dev-fixall attempts after a red quality-gate probe/recheck (bounded; no thrash).
|
|
89
95
|
# Attempt counter: .spur/run/<wbs>-test-fix-attempt. Default 2 = two fixall hops before failed.
|
|
90
96
|
qualityGateMaxFixAttempts: "2"
|
|
@@ -300,7 +306,7 @@ states:
|
|
|
300
306
|
# `${vars.qualityGateCmd}` at `test` is the gate that actually decides.
|
|
301
307
|
- kind: shell
|
|
302
308
|
options:
|
|
303
|
-
command:
|
|
309
|
+
command: "$formatCmd ; exit 0"
|
|
304
310
|
|
|
305
311
|
# ── test hop (quality gate + bounded auto-fix) ─────────────────────────────
|
|
306
312
|
# NOT /sp:dev-unit. That command (sp:code-testing) *extends/generates* tests toward
|
|
@@ -389,28 +395,43 @@ states:
|
|
|
389
395
|
or the pipeline `failed` state (FAIL and attempts exhausted) — never a
|
|
390
396
|
raw lifecycle abort that skips the terminal `failed` state.
|
|
391
397
|
onEnter:
|
|
398
|
+
# 0587 R3: probe-then-full recheck. A red gateProbeCmd records FAIL and skips the full
|
|
399
|
+
# gate (the measured waste is re-running a 110–140s gate to learn the tree is still red);
|
|
400
|
+
# a green probe (or empty gateProbeCmd) falls through to the full-gate loop unchanged.
|
|
401
|
+
# Only the full gate writes PASS, so the `test-recheck → review` guard (reads PASS)
|
|
402
|
+
# still means a full green qualityGateCmd ran — invariant preserved by construction.
|
|
392
403
|
- kind: shell
|
|
393
404
|
options:
|
|
394
405
|
command: >-
|
|
395
406
|
mkdir -p .spur/run &&
|
|
396
407
|
STATUS_FILE=".spur/run/$wbs-test-gate.status" &&
|
|
397
408
|
LOG_FILE=".spur/run/$wbs-test-gate.log" &&
|
|
409
|
+
FINDINGS_FILE=".spur/run/$wbs-test-gate.findings" &&
|
|
398
410
|
: > "$LOG_FILE" &&
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
411
|
+
probe_rc=0;
|
|
412
|
+
if [ -n "$gateProbeCmd" ]; then
|
|
413
|
+
sh -c "$gateProbeCmd" > "$LOG_FILE.probe" 2>&1; probe_rc=$?;
|
|
414
|
+
cat "$LOG_FILE.probe" >> "$LOG_FILE";
|
|
415
|
+
rm -f "$LOG_FILE.probe";
|
|
416
|
+
fi;
|
|
417
|
+
if [ "$probe_rc" -ne 0 ]; then
|
|
418
|
+
gate_rc=$probe_rc;
|
|
419
|
+
else
|
|
420
|
+
gate_attempt=1;
|
|
421
|
+
while [ "$gate_attempt" -le 5 ]; do
|
|
422
|
+
ATTEMPT_LOG="$LOG_FILE.attempt-$gate_attempt";
|
|
423
|
+
sh -c "$qualityGateCmd" > "$ATTEMPT_LOG" 2>&1; gate_rc=$?;
|
|
424
|
+
gate_locked=0;
|
|
425
|
+
grep -q 'SQLiteError: database is locked' "$ATTEMPT_LOG" && gate_locked=1;
|
|
426
|
+
cat "$ATTEMPT_LOG" >> "$LOG_FILE";
|
|
427
|
+
rm -f "$ATTEMPT_LOG";
|
|
428
|
+
if [ "$gate_rc" -eq 0 ] || [ "$gate_locked" -ne 1 ] || [ "$gate_attempt" -ge 5 ]; then break; fi;
|
|
429
|
+
printf 'quality gate: database is locked; retrying (%s/5) in 10s\n' "$gate_attempt" | tee -a "$LOG_FILE";
|
|
430
|
+
sleep 10;
|
|
431
|
+
gate_attempt=$((gate_attempt + 1));
|
|
432
|
+
done;
|
|
433
|
+
fi &&
|
|
412
434
|
cat "$LOG_FILE" &&
|
|
413
|
-
FINDINGS_FILE=".spur/run/$wbs-test-gate.findings" &&
|
|
414
435
|
set +e; grep -oE '[A-Za-z0-9_./-]+\.[A-Za-z]+:[0-9]+' "$LOG_FILE" | sort -u | head -20 | tr '\n' ' ' > "$FINDINGS_FILE"; set -e &&
|
|
415
436
|
if [ "$gate_rc" -eq 0 ]; then
|
|
416
437
|
printf 'PASS\n' > "$STATUS_FILE";
|
|
@@ -709,4 +730,4 @@ transitions:
|
|
|
709
730
|
guard:
|
|
710
731
|
kind: shell
|
|
711
732
|
options:
|
|
712
|
-
command:
|
|
733
|
+
command: "! $spurBin task check $wbs"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gobing-ai/spur",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.52",
|
|
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",
|
|
@@ -183,8 +183,9 @@ When you fan out or dispatch a subagent, apply the four disciplines the SSOT
|
|
|
183
183
|
- [ ] Never replace yourself as orchestrator when `--agent` is set - it pins the step executor, not you.
|
|
184
184
|
- [ ] Never auto-approve a HITL gate inside a task unless `--auto` was passed (it sets `profile=auto`).
|
|
185
185
|
- [ ] Never silent-pick multi-candidate router stops; surface HITL (batch `--auto` does not break ties).
|
|
186
|
-
- [ ] Never mutate the corpus - the
|
|
187
|
-
|
|
186
|
+
- [ ] Never mutate the corpus - the review coordinator writes `## Review`, the pipeline's `record`
|
|
187
|
+
step writes `## Testing` deterministically (bare-`## Review` fallback only — never an
|
|
188
|
+
overwrite of authored Review, F92 0593 R1); your sole output is the batch report (+ optional recovery dispatch of an existing
|
|
188
189
|
`/sp:dev-*` command).
|
|
189
190
|
- [ ] Never run tasks in parallel unless the operator requested parallel mode and the
|
|
190
191
|
`sp:parallel-execution` checks pass. If checks fail, serialize and report why.
|
|
@@ -38,6 +38,11 @@ You are a **thin delegator**. You do not own the review logic; the three skills
|
|
|
38
38
|
| SECUA quality | `sp:code-verification` (review mode) | Is the code correct/secure/efficient/usable? |
|
|
39
39
|
| Architectural depth | `sp:code-improvement` | Is the architecture deep / testable? |
|
|
40
40
|
|
|
41
|
+
**Section ownership (F92 0593 R1).** Component skills **return review fragments only** — never
|
|
42
|
+
write `## Review`. This coordinator is the single `## Review` writer in coordinated/pipeline mode
|
|
43
|
+
(combined fragment merge). `spur task record`'s bare-Review backfill is a standalone compatibility
|
|
44
|
+
fallback only and never overwrites authored Review.
|
|
45
|
+
|
|
41
46
|
Your job: establish scope, dispatch each requested dimension to its skill, collect findings, merge
|
|
42
47
|
them into a ranked report, and write the report to the task's `## Review` section (pipeline mode) or
|
|
43
48
|
emit it as advisory output (standalone mode).
|
|
@@ -27,6 +27,6 @@ For shared semantics, see the [flag glossary](../skills/spur-dev/references/flag
|
|
|
27
27
|
## Implementation
|
|
28
28
|
|
|
29
29
|
- Apply the [inline-default execution-surface contract](../skills/spur-dev/references/cross-cutting.md#inline-default-execution-surface).
|
|
30
|
-
- WBS mode (`<wbs>`): `Skill(skill="sp:functional-review", args="<wbs> $ARGUMENTS")` + `Skill(skill="sp:code-verification", args="review $ARGUMENTS")` + `Skill(skill="sp:code-improvement", args="<wbs> $ARGUMENTS")` (functional traceability + SECUA framework + architectural depth;
|
|
30
|
+
- WBS mode (`<wbs>`): `Skill(skill="sp:functional-review", args="<wbs> $ARGUMENTS")` + `Skill(skill="sp:code-verification", args="review $ARGUMENTS")` + `Skill(skill="sp:code-improvement", args="<wbs> $ARGUMENTS")` (functional traceability + SECUA framework + architectural depth; the three skills return review fragments and the coordinator (`sp:super-reviewer`) writes the combined `## Review` — F92 0593 R1)
|
|
31
31
|
- Path mode (`<path>`): `Skill(skill="sp:code-verification", args="review $ARGUMENTS")` + `Skill(skill="sp:code-improvement", args="<path> $ARGUMENTS")` (advisory SECUA quality + architectural depth; performs no task mutation)
|
|
32
32
|
- `--fix`: Deprecated (no-op + warning message; route remediation to `/sp:dev-verify --fix`). **`--next` removed** (feature H8, 2026-07-31) — it was a deprecated no-op; route progression to `/sp:dev-next`. **was: `--next` deprecated no-op.**
|
package/plugins/sp/plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sp",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.52",
|
|
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"]
|
|
@@ -25,7 +25,7 @@ import { join } from 'node:path';
|
|
|
25
25
|
|
|
26
26
|
// ─── Types ──────────────────────────────────────────────────────────────────
|
|
27
27
|
|
|
28
|
-
interface CmdResult {
|
|
28
|
+
export interface CmdResult {
|
|
29
29
|
code: number;
|
|
30
30
|
stdout: string;
|
|
31
31
|
stderr: string;
|
|
@@ -88,6 +88,12 @@ interface PreflightContext {
|
|
|
88
88
|
defaultBranch: string;
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
interface Upstream {
|
|
92
|
+
ref: string;
|
|
93
|
+
ahead: number;
|
|
94
|
+
behind: number;
|
|
95
|
+
}
|
|
96
|
+
|
|
91
97
|
interface Finding {
|
|
92
98
|
kind: 'review' | 'inline' | 'comment';
|
|
93
99
|
severity: string;
|
|
@@ -104,10 +110,14 @@ interface ParsedArgs {
|
|
|
104
110
|
booleans: Set<string>;
|
|
105
111
|
}
|
|
106
112
|
|
|
107
|
-
// ─── Process runner (git/gh
|
|
113
|
+
// ─── Process runner (single seam; every git/gh call routes through run) ─────
|
|
108
114
|
|
|
109
|
-
|
|
110
|
-
|
|
115
|
+
/** How an external command is executed. The only seam between this script and git/gh. */
|
|
116
|
+
export type CommandRunner = (cmd: readonly string[]) => CmdResult;
|
|
117
|
+
|
|
118
|
+
/** Real execution: resolve the binary from PATH and capture its output. The default runner. */
|
|
119
|
+
export const spawnRunner: CommandRunner = (cmd) => {
|
|
120
|
+
// Pass env explicitly so both Node and Bun resolve git/gh against the caller's PATH.
|
|
111
121
|
const proc = spawnSync(cmd[0] ?? '', [...cmd.slice(1)], { encoding: 'utf8', env: process.env });
|
|
112
122
|
return {
|
|
113
123
|
code: proc.status ?? 1,
|
|
@@ -115,6 +125,20 @@ function run(cmd: readonly string[]): CmdResult {
|
|
|
115
125
|
stderr: proc.stderr ?? '',
|
|
116
126
|
error: proc.error?.message,
|
|
117
127
|
};
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
let runner: CommandRunner = spawnRunner;
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Swap the git/gh runner. Tests inject an in-process stub so a suite of CLI cases costs no
|
|
134
|
+
* subprocess spawns; call with no argument to restore real execution.
|
|
135
|
+
*/
|
|
136
|
+
export function setCommandRunner(next?: CommandRunner): void {
|
|
137
|
+
runner = next ?? spawnRunner;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function run(cmd: readonly string[]): CmdResult {
|
|
141
|
+
return runner(cmd);
|
|
118
142
|
}
|
|
119
143
|
|
|
120
144
|
function runOk(cmd: readonly string[], what: string): string {
|
|
@@ -433,6 +457,22 @@ function preflightContext(): PreflightContext {
|
|
|
433
457
|
};
|
|
434
458
|
}
|
|
435
459
|
|
|
460
|
+
/** Upstream divergence, non-fatal: a missing upstream is a normal state, not an error. */
|
|
461
|
+
function resolveUpstream(): Upstream | null {
|
|
462
|
+
const refRes = run(['git', 'rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}']);
|
|
463
|
+
if (refRes.code !== 0) return null;
|
|
464
|
+
const ref = refRes.stdout.trim();
|
|
465
|
+
const count = (res: CmdResult): number => {
|
|
466
|
+
const n = Number(res.stdout.trim());
|
|
467
|
+
return res.code === 0 && Number.isFinite(n) ? n : 0;
|
|
468
|
+
};
|
|
469
|
+
return {
|
|
470
|
+
ref,
|
|
471
|
+
ahead: count(run(['git', 'rev-list', '--count', '@{u}..HEAD'])),
|
|
472
|
+
behind: count(run(['git', 'rev-list', '--count', 'HEAD..@{u}'])),
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
|
|
436
476
|
function viewPr(): GhPr | null {
|
|
437
477
|
const res = run([
|
|
438
478
|
'gh',
|
|
@@ -507,15 +547,27 @@ function cmdPreflight(args: ParsedArgs): void {
|
|
|
507
547
|
2,
|
|
508
548
|
);
|
|
509
549
|
}
|
|
550
|
+
const base = (args.flags.get('--base') ?? '').trim() || ctx.defaultBranch;
|
|
551
|
+
const upstream = resolveUpstream();
|
|
552
|
+
if (ctx.branch === base) {
|
|
553
|
+
writeStatus(args, 'FAIL');
|
|
554
|
+
fail(
|
|
555
|
+
args,
|
|
556
|
+
`current branch is the base branch (${base}) — a PR reviews a feature branch against it; ` +
|
|
557
|
+
'check out a feature branch (nothing on the base branch is reviewable)',
|
|
558
|
+
2,
|
|
559
|
+
);
|
|
560
|
+
}
|
|
510
561
|
writeStatus(args, 'PASS');
|
|
511
562
|
emit(
|
|
512
563
|
args,
|
|
513
|
-
{ ok: true, ...ctx },
|
|
564
|
+
{ ok: true, ...ctx, upstream },
|
|
514
565
|
[
|
|
515
566
|
`Repository: ${ctx.nameWithOwner}`,
|
|
516
567
|
`Branch: ${ctx.branch}`,
|
|
517
568
|
`HEAD: ${ctx.shortHead}`,
|
|
518
569
|
`Default: ${ctx.defaultBranch}`,
|
|
570
|
+
`Upstream: ${upstream ? `${upstream.ref} (ahead ${upstream.ahead}, behind ${upstream.behind})` : `none (publishing would create origin/${ctx.branch})`}`,
|
|
519
571
|
'Local: clean',
|
|
520
572
|
].join('\n'),
|
|
521
573
|
);
|
|
@@ -30,7 +30,7 @@ import { join } from 'node:path';
|
|
|
30
30
|
// ─── Inline type definitions (mirrors packages/domain/src/stage-registry/) ─
|
|
31
31
|
|
|
32
32
|
export type SchemaVersion = { major: number; minor: number };
|
|
33
|
-
export const CURRENT_SCHEMA_VERSION: SchemaVersion = { major: 1, minor:
|
|
33
|
+
export const CURRENT_SCHEMA_VERSION: SchemaVersion = { major: 1, minor: 3 };
|
|
34
34
|
export const AUTHORITY_LANES = ['registry', 'workflow', 'skill', 'cli', 'adapter'] as const;
|
|
35
35
|
export type AuthorityLane = (typeof AUTHORITY_LANES)[number];
|
|
36
36
|
// Mirrors packages/domain/src/stage-registry/schema.ts MUTATION_CLASSES. Pinned by
|
|
@@ -65,6 +65,8 @@ export type ContextLayerName = (typeof CONTEXT_LAYER_NAMES)[number];
|
|
|
65
65
|
export interface StageArtifact {
|
|
66
66
|
kind: string;
|
|
67
67
|
direction: ArtifactDirection;
|
|
68
|
+
/** Exact artifact identity (F92 0593 R2): task-section name or artifact file basename. */
|
|
69
|
+
identity?: string;
|
|
68
70
|
description?: string;
|
|
69
71
|
required?: boolean;
|
|
70
72
|
}
|
|
@@ -308,10 +310,7 @@ export const REGISTERED_STAGES: StageRecord[] = [
|
|
|
308
310
|
],
|
|
309
311
|
reasoning_skill: 'sp:spur-dev',
|
|
310
312
|
required_references: ['references/dev-operations.md', 'spur-dev/references/decision-brief.md'],
|
|
311
|
-
gates: [
|
|
312
|
-
{ name: 'refine-skip-gate', timing: 'pre', description: 'Skip sections that already meet L3' },
|
|
313
|
-
{ name: 'l4-advisory', timing: 'post', min_verdict: 'pass', description: 'L4 advisory surface' },
|
|
314
|
-
],
|
|
313
|
+
gates: [],
|
|
315
314
|
mutation_class: 'corpus',
|
|
316
315
|
retry: defaultRetry,
|
|
317
316
|
model_policy: policy('refine', ['capable-2']),
|
|
@@ -346,7 +345,7 @@ export const REGISTERED_STAGES: StageRecord[] = [
|
|
|
346
345
|
description: 'dev-run --mode implement: code edits in worktree',
|
|
347
346
|
artifacts: [
|
|
348
347
|
{ kind: 'worktree-diff', direction: 'output', required: true },
|
|
349
|
-
{ kind: 'task-section', direction: '
|
|
348
|
+
{ kind: 'task-section', direction: 'output', required: true, identity: 'Solution' },
|
|
350
349
|
],
|
|
351
350
|
reasoning_skill: 'sp:code-implementation',
|
|
352
351
|
gates: [],
|
|
@@ -370,7 +369,7 @@ export const REGISTERED_STAGES: StageRecord[] = [
|
|
|
370
369
|
{ kind: 'coverage-report', direction: 'output', required: false },
|
|
371
370
|
],
|
|
372
371
|
reasoning_skill: 'sp:code-testing',
|
|
373
|
-
gates: [
|
|
372
|
+
gates: [],
|
|
374
373
|
mutation_class: 'tests',
|
|
375
374
|
retry: defaultRetry,
|
|
376
375
|
model_policy: policy('test', ['capable-1']),
|
|
@@ -403,13 +402,13 @@ export const REGISTERED_STAGES: StageRecord[] = [
|
|
|
403
402
|
id: 'verify',
|
|
404
403
|
description: 'dev-verify: SECUA review + requirements traceability',
|
|
405
404
|
artifacts: [
|
|
406
|
-
{ kind: '
|
|
407
|
-
{ kind: '
|
|
405
|
+
{ kind: 'worktree-diff', direction: 'input', required: true },
|
|
406
|
+
{ kind: 'verdict-artifact', direction: 'output', required: true, identity: '<wbs>-verdict.json' },
|
|
408
407
|
],
|
|
409
408
|
reasoning_skill: 'sp:code-verification',
|
|
410
409
|
gates: [
|
|
411
410
|
{ name: 'verdict-artifact', timing: 'post', min_verdict: 'pass' },
|
|
412
|
-
{ name: 'strict-core', timing: 'post', description: 'L3 core findings must pass' },
|
|
411
|
+
{ name: 'strict-core', timing: 'post', min_verdict: 'pass', description: 'L3 core findings must pass' },
|
|
413
412
|
],
|
|
414
413
|
mutation_class: 'verdict',
|
|
415
414
|
retry: { max_attempts: 2, terminal_stop: 'escalate', timeout_seconds: 600 },
|
|
@@ -423,10 +422,7 @@ export const REGISTERED_STAGES: StageRecord[] = [
|
|
|
423
422
|
id: 'wrap',
|
|
424
423
|
aliases: ['dev-wrap'],
|
|
425
424
|
description: 'dev-wrap: learnings/doc-sync/feature transition',
|
|
426
|
-
artifacts: [
|
|
427
|
-
{ kind: 'learning-entry', direction: 'output' },
|
|
428
|
-
{ kind: 'task-section', direction: 'output', description: 'Testing/Review updated' },
|
|
429
|
-
],
|
|
425
|
+
artifacts: [{ kind: 'learning-entry', direction: 'output' }],
|
|
430
426
|
reasoning_skill: 'sp:spur-dev',
|
|
431
427
|
gates: [
|
|
432
428
|
{
|
|
@@ -447,9 +443,9 @@ export const REGISTERED_STAGES: StageRecord[] = [
|
|
|
447
443
|
schema_version: CURRENT_SCHEMA_VERSION,
|
|
448
444
|
id: 'review',
|
|
449
445
|
description: 'dev-review: multi-dimensional code review (functional/SECUA/architecture)',
|
|
450
|
-
artifacts: [{ kind: 'review-findings', direction: 'output', required: true }],
|
|
446
|
+
artifacts: [{ kind: 'review-findings', direction: 'output', required: true, identity: 'Review' }],
|
|
451
447
|
reasoning_skill: 'sp:code-verification',
|
|
452
|
-
gates: [
|
|
448
|
+
gates: [],
|
|
453
449
|
mutation_class: 'verdict',
|
|
454
450
|
retry: { max_attempts: 2, terminal_stop: 'block', timeout_seconds: 300 },
|
|
455
451
|
model_policy: policy('review', ['capable-1']),
|
|
@@ -457,6 +453,32 @@ export const REGISTERED_STAGES: StageRecord[] = [
|
|
|
457
453
|
observability: [event('stage-started'), event('findings-produced')],
|
|
458
454
|
execution: inlineInline(),
|
|
459
455
|
},
|
|
456
|
+
{
|
|
457
|
+
schema_version: CURRENT_SCHEMA_VERSION,
|
|
458
|
+
id: 'record',
|
|
459
|
+
aliases: ['dev-record'],
|
|
460
|
+
description: 'record: deterministic Testing write-back from the verdict artifact; bare-Review fallback only',
|
|
461
|
+
artifacts: [
|
|
462
|
+
{ kind: 'verdict-artifact', direction: 'input', required: true, identity: '<wbs>-verdict.json' },
|
|
463
|
+
{ kind: 'task-section', direction: 'output', required: true, identity: 'Testing' },
|
|
464
|
+
{
|
|
465
|
+
kind: 'task-section',
|
|
466
|
+
direction: 'output',
|
|
467
|
+
required: false,
|
|
468
|
+
identity: 'Review',
|
|
469
|
+
description:
|
|
470
|
+
'fallback-only: backfills Review only when the section is bare; never overwrites authored Review',
|
|
471
|
+
},
|
|
472
|
+
],
|
|
473
|
+
reasoning_skill: 'inline',
|
|
474
|
+
gates: [],
|
|
475
|
+
mutation_class: 'corpus',
|
|
476
|
+
retry: { max_attempts: 1, terminal_stop: 'block', timeout_seconds: 120 },
|
|
477
|
+
model_policy: policy('record'),
|
|
478
|
+
context_layers: [layer('task-state')],
|
|
479
|
+
observability: [event('stage-started')],
|
|
480
|
+
execution: inlineDeterministic('cli'),
|
|
481
|
+
},
|
|
460
482
|
{
|
|
461
483
|
schema_version: CURRENT_SCHEMA_VERSION,
|
|
462
484
|
id: 'dogfood',
|
|
@@ -467,15 +489,7 @@ export const REGISTERED_STAGES: StageRecord[] = [
|
|
|
467
489
|
],
|
|
468
490
|
reasoning_skill: 'sp:dogfood-testing',
|
|
469
491
|
required_references: ['references/monitor-ledger.md', 'references/report-template.md'],
|
|
470
|
-
gates: [
|
|
471
|
-
{ name: 'detect-pipeline-driving', timing: 'pre', description: 'Refuse dogfood when driving a pipeline' },
|
|
472
|
-
{
|
|
473
|
-
name: 'report-validate',
|
|
474
|
-
timing: 'post',
|
|
475
|
-
min_verdict: 'pass',
|
|
476
|
-
description: 'Report must pass schema validation',
|
|
477
|
-
},
|
|
478
|
-
],
|
|
492
|
+
gates: [],
|
|
479
493
|
mutation_class: 'driver',
|
|
480
494
|
retry: { max_attempts: 3, terminal_stop: 'block', timeout_seconds: 600 },
|
|
481
495
|
model_policy: policy('dogfood'),
|
|
@@ -568,6 +582,7 @@ export function listStages(): StageLookupEntry[] {
|
|
|
568
582
|
'quality-gate': '/sp:dev-fixall',
|
|
569
583
|
verify: '/sp:dev-verify',
|
|
570
584
|
wrap: '/sp:dev-wrap',
|
|
585
|
+
record: 'spur task record',
|
|
571
586
|
review: '/sp:dev-review',
|
|
572
587
|
dogfood: '/sp:dev-dogfood',
|
|
573
588
|
handover: 'inline (dev-handover)',
|
|
@@ -187,9 +187,11 @@ When invoked as the `--focus architecture` dimension of `/sp:dev-review`:
|
|
|
187
187
|
- The scope is the task's diff (pipeline) or the `path` arg (standalone).
|
|
188
188
|
- `blocker`/`major` candidates block the `approve(HITL)` gate alongside any SECUA blockers from
|
|
189
189
|
`sp:code-verification`.
|
|
190
|
-
- The candidate list is
|
|
191
|
-
`
|
|
192
|
-
|
|
190
|
+
- The candidate list is returned as a **review fragment**; the review coordinator
|
|
191
|
+
(`sp:super-reviewer` under `/sp:dev-review`) merges it into the combined `## Review` section —
|
|
192
|
+
it is never written by `record`, which backfills `## Review` only when the section is bare
|
|
193
|
+
(fallback-only, F92 0593 R1).
|
|
194
|
+
- This skill does **not** write to the task file directly — the coordinator (or the operator) does.
|
|
193
195
|
|
|
194
196
|
Standalone, the skill emits the candidate list as advisory output; the operator acts on it.
|
|
195
197
|
|