@gobing-ai/spur 0.3.47 → 0.3.49
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 +94 -15
- package/config/transition-shims.json +33 -0
- package/config/workflows/basic.yaml +2 -0
- package/config/workflows/docs-pipeline.yaml +2 -0
- package/config/workflows/feature-dev.yaml +8 -0
- package/config/workflows/idea-pipeline.yaml +10 -0
- package/config/workflows/planning-pipeline.yaml +4 -0
- package/config/workflows/pr-review.yaml +338 -0
- package/config/workflows/task-pipeline.yaml +8 -0
- package/config/workflows/wayfinder-resolution.yaml +4 -0
- package/config/workflows/wrapup-pipeline.yaml +18 -1
- package/package.json +8 -8
- package/plugins/sp/README.md +9 -6
- package/plugins/sp/agents/expert-spur.md +1 -0
- package/plugins/sp/commands/dev-arch.md +2 -1
- package/plugins/sp/commands/dev-brainstorm.md +2 -1
- package/plugins/sp/commands/dev-changelog.md +1 -0
- package/plugins/sp/commands/dev-daily.md +1 -0
- package/plugins/sp/commands/dev-debug.md +2 -1
- package/plugins/sp/commands/dev-dogfood.md +2 -1
- package/plugins/sp/commands/{dev-featurechange.md → dev-feature-change.md} +8 -10
- package/plugins/sp/commands/dev-find-conflict.md +2 -1
- package/plugins/sp/commands/dev-find-issue.md +36 -43
- package/plugins/sp/commands/dev-find-next.md +5 -4
- package/plugins/sp/commands/dev-fixall.md +1 -0
- package/plugins/sp/commands/dev-gitmsg.md +1 -0
- package/plugins/sp/commands/dev-gtd.md +12 -12
- package/plugins/sp/commands/dev-handover.md +1 -0
- package/plugins/sp/commands/dev-history-load.md +63 -0
- package/plugins/sp/commands/dev-idea.md +1 -0
- package/plugins/sp/commands/dev-next.md +2 -1
- package/plugins/sp/commands/dev-parallel.md +2 -1
- package/plugins/sp/commands/dev-plan.md +2 -1
- package/plugins/sp/commands/dev-pr-review.md +39 -0
- package/plugins/sp/commands/dev-refine.md +5 -3
- package/plugins/sp/commands/dev-refineall.md +2 -1
- package/plugins/sp/commands/dev-refresh.md +2 -1
- package/plugins/sp/commands/dev-reverse.md +2 -1
- package/plugins/sp/commands/dev-review.md +2 -1
- package/plugins/sp/commands/dev-run.md +3 -2
- package/plugins/sp/commands/dev-runall.md +3 -2
- package/plugins/sp/commands/dev-simplify.md +2 -1
- package/plugins/sp/commands/dev-unit.md +2 -1
- package/plugins/sp/commands/dev-verify.md +2 -1
- package/plugins/sp/commands/dev-verifyall.md +2 -1
- package/plugins/sp/commands/dev-wrap.md +7 -5
- package/plugins/sp/commands/dev-wrapall.md +7 -5
- package/plugins/sp/commands/rule-add.md +1 -0
- package/plugins/sp/commands/rule-refine.md +1 -0
- package/plugins/sp/commands/rule-scan.md +1 -0
- package/plugins/sp/commands/spur-init.md +1 -0
- package/plugins/sp/commands/workflow-add.md +1 -0
- package/plugins/sp/commands/workflow-refine.md +1 -0
- package/plugins/sp/hooks/careful-guard.ts +5 -80
- package/plugins/sp/hooks/destructive-policy.ts +146 -0
- package/plugins/sp/hooks/pi/guard-extension.ts +33 -46
- package/plugins/sp/hooks/task-file-policy.ts +31 -0
- package/plugins/sp/hooks/task-write-guard.ts +4 -0
- package/plugins/sp/plugin.json +1 -1
- package/plugins/sp/references/roles.md +106 -0
- package/plugins/sp/scripts/feature-sync-bounded.ts +28 -2
- package/plugins/sp/scripts/history-load.ts +400 -0
- package/plugins/sp/scripts/pr-reviewing.ts +867 -0
- package/plugins/sp/scripts/stage-registry-adapter.ts +66 -31
- package/plugins/sp/scripts/surface-drift-inventory.ts +908 -0
- package/plugins/sp/scripts/task-size-precheck.ts +30 -4
- package/plugins/sp/scripts/transition-shim-check.ts +238 -0
- package/plugins/sp/scripts/validate-commands.ts +33 -2
- package/plugins/sp/scripts/validate-flag-contracts.ts +5 -2
- package/plugins/sp/skills/code-implementation/SKILL.md +9 -1
- package/plugins/sp/skills/code-verification/SKILL.md +29 -28
- package/plugins/sp/skills/issue-finding/SKILL.md +123 -141
- package/plugins/sp/skills/issue-finding/examples/expected-findings.json +1 -1
- package/plugins/sp/skills/issue-finding/references/session-formats.md +87 -90
- package/plugins/sp/skills/next-feature/SKILL.md +6 -6
- package/plugins/sp/skills/next-feature/references/handoff-routing.md +5 -5
- package/plugins/sp/skills/next-feature/references/signal-derivation.md +7 -2
- package/plugins/sp/skills/next-router/SKILL.md +1 -1
- package/plugins/sp/skills/parallel-execution/references/dispatch-surface.md +40 -2
- package/plugins/sp/skills/pr-reviewing/SKILL.md +285 -0
- package/plugins/sp/skills/spur-cli/SKILL.md +3 -0
- package/plugins/sp/skills/spur-cli/references/agent.md +12 -7
- package/plugins/sp/skills/spur-cli/references/features/hierarchy-mece.md +5 -5
- package/plugins/sp/skills/spur-cli/references/features.md +1 -1
- package/plugins/sp/skills/spur-cli/references/team.md +10 -3
- package/plugins/sp/skills/spur-dev/SKILL.md +2 -0
- package/plugins/sp/skills/spur-dev/references/ac-style-guide.md +17 -0
- package/plugins/sp/skills/spur-dev/references/cross-cutting.md +44 -23
- package/plugins/sp/skills/spur-dev/references/dev-operations.md +14 -11
- package/plugins/sp/skills/spur-dev/references/execution-workflow.md +14 -12
- package/plugins/sp/skills/spur-dev/references/flag-glossary.md +28 -7
- package/plugins/sp/skills/spur-dev/references/gate-checklists.md +2 -0
- package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +12 -2
- package/schemas/spur-config.schema.json +47 -3
- package/spur.js +12223 -7716
- package/web/_astro/BoardApp.8hiqShQn.js +1 -0
- package/web/_astro/{BoardApp.DKyrGxdo.js → BoardApp.BjQUNhuj.js} +74 -74
- package/web/_astro/{TaskDetail.6-27_LMa.js → TaskDetail.CVBuD6dF.js} +1 -1
- package/web/_astro/{arc.Df-9AQvS.js → arc.BMMjdODi.js} +1 -1
- package/web/_astro/{architectureDiagram-3BPJPVTR.VAI_-paS.js → architectureDiagram-3BPJPVTR.BU5ShzXf.js} +1 -1
- package/web/_astro/{blockDiagram-GPEHLZMM.DFpUY1ue.js → blockDiagram-GPEHLZMM.Bj1iEqPD.js} +1 -1
- package/web/_astro/{c4Diagram-AAUBKEIU.CF8doOpg.js → c4Diagram-AAUBKEIU.vX8wepCL.js} +1 -1
- package/web/_astro/channel.EwdSemIC.js +1 -0
- package/web/_astro/{chunk-2J33WTMH.BnjK3fjt.js → chunk-2J33WTMH.BKAipTym.js} +1 -1
- package/web/_astro/{chunk-4BX2VUAB.x6ZDnJKq.js → chunk-4BX2VUAB.B68XkPG7.js} +1 -1
- package/web/_astro/{chunk-55IACEB6.zY-0uu7w.js → chunk-55IACEB6.BmeDLcrc.js} +1 -1
- package/web/_astro/{chunk-727SXJPM.BZxKg_Vi.js → chunk-727SXJPM.PDuBA3Kw.js} +1 -1
- package/web/_astro/{chunk-AQP2D5EJ.Cpi9G9Td.js → chunk-AQP2D5EJ.C7A044za.js} +1 -1
- package/web/_astro/{chunk-FMBD7UC4.DWTB-Pif.js → chunk-FMBD7UC4.BtzKKFqR.js} +1 -1
- package/web/_astro/{chunk-ND2GUHAM.BPDQbiOG.js → chunk-ND2GUHAM.BJuDeeOy.js} +1 -1
- package/web/_astro/{chunk-QZHKN3VN.BRWIcuoM.js → chunk-QZHKN3VN.DSeMDgcQ.js} +1 -1
- package/web/_astro/{classDiagram-4FO5ZUOK.mGTCZsDO.js → classDiagram-4FO5ZUOK.D53Q4tCw.js} +1 -1
- package/web/_astro/{classDiagram-v2-Q7XG4LA2.mGTCZsDO.js → classDiagram-v2-Q7XG4LA2.D53Q4tCw.js} +1 -1
- package/web/_astro/{cose-bilkent-S5V4N54A.D1GEut-z.js → cose-bilkent-S5V4N54A.c712AFRH.js} +1 -1
- package/web/_astro/{dagre-BM42HDAG.BV0XG9Do.js → dagre-BM42HDAG.D-idisph.js} +1 -1
- package/web/_astro/{diagram-2AECGRRQ.DzpYxsjo.js → diagram-2AECGRRQ.DLgnsJCU.js} +1 -1
- package/web/_astro/{diagram-5GNKFQAL.Cm9YzJh4.js → diagram-5GNKFQAL.BiaxBVqx.js} +1 -1
- package/web/_astro/{diagram-KO2AKTUF.BjhottUj.js → diagram-KO2AKTUF.C8HX1vd8.js} +1 -1
- package/web/_astro/{diagram-LMA3HP47.BFsQW5kb.js → diagram-LMA3HP47.CfqDLLes.js} +1 -1
- package/web/_astro/{diagram-OG6HWLK6.8pdpzSWO.js → diagram-OG6HWLK6.15SDiEed.js} +1 -1
- package/web/_astro/{erDiagram-TEJ5UH35.Bd7KUJmJ.js → erDiagram-TEJ5UH35.DksYtOYM.js} +1 -1
- package/web/_astro/{flowDiagram-I6XJVG4X.7LWffkaE.js → flowDiagram-I6XJVG4X.DR_Au-HV.js} +1 -1
- package/web/_astro/{ganttDiagram-6RSMTGT7.BeDcO5tI.js → ganttDiagram-6RSMTGT7.CHhHrffI.js} +1 -1
- package/web/_astro/{gitGraphDiagram-PVQCEYII.Ca4n730A.js → gitGraphDiagram-PVQCEYII.B2Xehvam.js} +1 -1
- package/web/_astro/{index.Dbvuw6d4.css → index.DAxu50UF.css} +1 -1
- package/web/_astro/{infoDiagram-5YYISTIA.B0OakQYb.js → infoDiagram-5YYISTIA.C9c3CNNN.js} +1 -1
- package/web/_astro/{ishikawaDiagram-YF4QCWOH.DSmNQe-1.js → ishikawaDiagram-YF4QCWOH.BibUHkh8.js} +1 -1
- package/web/_astro/{journeyDiagram-JHISSGLW.Cy5ruEUu.js → journeyDiagram-JHISSGLW.BYoVHiyO.js} +1 -1
- package/web/_astro/{kanban-definition-UN3LZRKU.CUJXub0p.js → kanban-definition-UN3LZRKU.CM1K5wHE.js} +1 -1
- package/web/_astro/{linear.DC1jCCXn.js → linear.SPpjJUb-.js} +1 -1
- package/web/_astro/{mermaid.core.DxVP99Ab.js → mermaid.core.BAgx3nnb.js} +4 -4
- package/web/_astro/{mindmap-definition-RKZ34NQL.D0MaV6sJ.js → mindmap-definition-RKZ34NQL.D35oPG1R.js} +1 -1
- package/web/_astro/{pieDiagram-4H26LBE5.DCC6_q32.js → pieDiagram-4H26LBE5.DiWuRwk7.js} +1 -1
- package/web/_astro/{quadrantDiagram-W4KKPZXB.BeUOAM7C.js → quadrantDiagram-W4KKPZXB.B9PBzTWn.js} +1 -1
- package/web/_astro/{requirementDiagram-4Y6WPE33.Dbl4MASO.js → requirementDiagram-4Y6WPE33.CYuuamFN.js} +1 -1
- package/web/_astro/{sankeyDiagram-5OEKKPKP.HsLg0VS4.js → sankeyDiagram-5OEKKPKP.W24UhhtD.js} +1 -1
- package/web/_astro/{sequenceDiagram-3UESZ5HK.DT7DJTnZ.js → sequenceDiagram-3UESZ5HK.BpbNjA51.js} +1 -1
- package/web/_astro/{stateDiagram-AJRCARHV.d_ju1Vr1.js → stateDiagram-AJRCARHV.DqVsHudf.js} +1 -1
- package/web/_astro/{stateDiagram-v2-BHNVJYJU.DMCAjMJ4.js → stateDiagram-v2-BHNVJYJU.CzwHYX81.js} +1 -1
- package/web/_astro/{timeline-definition-PNZ67QCA.DNOHr62_.js → timeline-definition-PNZ67QCA.Bc3B6djw.js} +1 -1
- package/web/_astro/{vennDiagram-CIIHVFJN.B7dUy-1W.js → vennDiagram-CIIHVFJN.C-D5rh8O.js} +1 -1
- package/web/_astro/{wardley-L42UT6IY.DEqOXvBh.js → wardley-L42UT6IY.D7PdYCqn.js} +1 -1
- package/web/_astro/{wardleyDiagram-YWT4CUSO.BCRb2p6x.js → wardleyDiagram-YWT4CUSO.CwmJKXF3.js} +1 -1
- package/web/_astro/{xychartDiagram-2RQKCTM6.NxVQLdBh.js → xychartDiagram-2RQKCTM6.avDYnLsb.js} +1 -1
- package/web/index.html +2 -2
- package/web/_astro/BoardApp.Ce6zJYAH.js +0 -1
- package/web/_astro/channel.Uhm9O3UV.js +0 -1
|
@@ -26,6 +26,8 @@
|
|
|
26
26
|
import { execFileSync } from 'node:child_process';
|
|
27
27
|
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
28
28
|
import { join } from 'node:path';
|
|
29
|
+
import { fileURLToPath } from 'node:url';
|
|
30
|
+
import { STAGE_FLOOR_TIER, TIER_ORDER } from './stage-registry-adapter';
|
|
29
31
|
|
|
30
32
|
// ─── Regex (sync with packages/app/src/services/task-size-precheck.ts) ───────
|
|
31
33
|
|
|
@@ -44,8 +46,17 @@ const CHECKLIST_ITEM_RE = /^\s*-\s*\[[ xX]\]/m;
|
|
|
44
46
|
const LARGE_TASK_REQS = 5;
|
|
45
47
|
const LARGE_TASK_PLAN_ITEMS = 8;
|
|
46
48
|
|
|
47
|
-
/**
|
|
48
|
-
|
|
49
|
+
/**
|
|
50
|
+
* Capability tiers strong enough for a large task (R3, task 0487). The floor is
|
|
51
|
+
* the `review` stage's Layer-1 tier — `reviewer` per `references/roles.md`,
|
|
52
|
+
* read via the stage-registry adapter (0538 R4: no tier literal here; roles.md
|
|
53
|
+
* is the pointer). Tiers at or above the floor pass. An unreachable roles.md
|
|
54
|
+
* degrades to the pre-reconcile band — fail-closed for a safety gate.
|
|
55
|
+
*/
|
|
56
|
+
const CAPABLE_TIERS: ReadonlySet<string> = (() => {
|
|
57
|
+
const floor = STAGE_FLOOR_TIER.get('review') ?? 'capable-1';
|
|
58
|
+
return new Set(TIER_ORDER.slice(Math.max(0, TIER_ORDER.indexOf(floor))));
|
|
59
|
+
})();
|
|
49
60
|
|
|
50
61
|
// ─── CLI ─────────────────────────────────────────────────────────────────────
|
|
51
62
|
|
|
@@ -56,6 +67,21 @@ function usage(): never {
|
|
|
56
67
|
process.exit(1);
|
|
57
68
|
}
|
|
58
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Resolve the spur CLI command in a monorepo-safe way:
|
|
72
|
+
* --spur-bin > SPUR_BIN > monorepo-local CLI entry > PATH `spur`.
|
|
73
|
+
* The plugin's own CI always passes an explicit --spur-bin; this fallback chain
|
|
74
|
+
* keeps ad-hoc invocations from silently hitting a stale PATH install.
|
|
75
|
+
*/
|
|
76
|
+
function defaultSpurBin(): string {
|
|
77
|
+
if (process.env.SPUR_BIN) return process.env.SPUR_BIN;
|
|
78
|
+
// scripts/ -> plugins/sp/ -> <repo>/apps/cli/src/index.ts (fileURLToPath — raw pathname breaks
|
|
79
|
+
// on %-encoded paths, e.g. spaces in the checkout directory)
|
|
80
|
+
const local = fileURLToPath(new URL('../../../apps/cli/src/index.ts', import.meta.url));
|
|
81
|
+
if (existsSync(local)) return `bun ${local}`;
|
|
82
|
+
return 'spur';
|
|
83
|
+
}
|
|
84
|
+
|
|
59
85
|
function parseArgs(argv: string[]): {
|
|
60
86
|
wbs: string;
|
|
61
87
|
spurBin: string;
|
|
@@ -63,7 +89,7 @@ function parseArgs(argv: string[]): {
|
|
|
63
89
|
maxPlanItems: number;
|
|
64
90
|
executor: string;
|
|
65
91
|
} {
|
|
66
|
-
let spurBin =
|
|
92
|
+
let spurBin = defaultSpurBin();
|
|
67
93
|
let wbs = '';
|
|
68
94
|
let maxReqs = Number(process.env.MAX_IMPLEMENT_REQS) || 5;
|
|
69
95
|
let maxPlanItems = Number(process.env.MAX_IMPLEMENT_PLAN_ITEMS) || 8;
|
|
@@ -73,7 +99,7 @@ function parseArgs(argv: string[]): {
|
|
|
73
99
|
while (i < argv.length) {
|
|
74
100
|
const arg = argv[i];
|
|
75
101
|
if (arg === '--spur-bin') {
|
|
76
|
-
spurBin = argv[i + 1] ??
|
|
102
|
+
spurBin = argv[i + 1] ?? defaultSpurBin();
|
|
77
103
|
i += 2;
|
|
78
104
|
} else if (arg === '--max-reqs') {
|
|
79
105
|
maxReqs = Number(argv[i + 1]) || 5;
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* transition-shim-check — two-sided gate over `config/transition-shims.json`
|
|
4
|
+
* (task 0541, feature B2).
|
|
5
|
+
*
|
|
6
|
+
* A transition shim is a compatibility path that must survive the agent-role
|
|
7
|
+
* transition. Every shim carries a source marker comment `@transition-shim(<id>)`
|
|
8
|
+
* so it is a grep target and a review signal, and a manifest entry in
|
|
9
|
+
* `config/transition-shims.json` records who owns it and when it can be removed.
|
|
10
|
+
*
|
|
11
|
+
* The gate is deliberately two-sided, matching `config/corpus-baseline.json`
|
|
12
|
+
* semantics (corpus-check.ts): a marker with no manifest entry FAILS, and a
|
|
13
|
+
* manifest entry whose marker no longer appears in source FAILS. Without the
|
|
14
|
+
* second half the manifest would rot into a permanent suppression list — the
|
|
15
|
+
* exact invisible-debt pattern this gate exists to end.
|
|
16
|
+
*
|
|
17
|
+
* Marker convention (docs/04_DESIGN.md §2.5):
|
|
18
|
+
* // @transition-shim(<id>) — <one line on what this keeps working>
|
|
19
|
+
* <id> matches ^[a-z0-9][a-z0-9-]*$ (lowercase kebab).
|
|
20
|
+
* Markers live in production source roots (apps, packages, plugins, config,
|
|
21
|
+
* scripts, tooling). Docs and test fixtures are not scanned — prose examples
|
|
22
|
+
* and gate-fixture marker text must not trip the gate.
|
|
23
|
+
*
|
|
24
|
+
* The manifest is the removal worklist: emptying it is the definition of the
|
|
25
|
+
* transition being complete (R4).
|
|
26
|
+
*
|
|
27
|
+
* Ships with the plugin to arbitrary projects, so it stays node-builtin-only —
|
|
28
|
+
* no workspace imports.
|
|
29
|
+
*
|
|
30
|
+
* Usage:
|
|
31
|
+
* bun plugins/sp/scripts/transition-shim-check.ts
|
|
32
|
+
* [--manifest <path>] default: config/transition-shims.json
|
|
33
|
+
* [--roots <a,b,c>] default: apps,packages,plugins,config,scripts,tooling
|
|
34
|
+
*
|
|
35
|
+
* Exit code: 0 when every manifest entry is present in source and every source
|
|
36
|
+
* marker is registered; 1 on any violation (unregistered marker, stale entry,
|
|
37
|
+
* or incomplete entry). Violations are printed to stderr; a summary to stdout.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
41
|
+
import { join, relative, resolve } from 'node:path';
|
|
42
|
+
|
|
43
|
+
const MARKER_RE = /@transition-shim\(\s*([a-z0-9][a-z0-9-]*)\s*\)/g;
|
|
44
|
+
const ID_RE = /^[a-z0-9][a-z0-9-]*$/;
|
|
45
|
+
|
|
46
|
+
/** Directories never scanned for markers (build output, vendored, VCS internals).
|
|
47
|
+
* `tests`/`test` are excluded too: a test fixture that mentions a marker id is
|
|
48
|
+
* data exercising the gate, not a compatibility path — shims live in production
|
|
49
|
+
* source, and only there do they need tracking. */
|
|
50
|
+
const SKIP_DIRS = new Set([
|
|
51
|
+
'node_modules',
|
|
52
|
+
'.git',
|
|
53
|
+
'dist',
|
|
54
|
+
'coverage',
|
|
55
|
+
'.spur',
|
|
56
|
+
'build',
|
|
57
|
+
'.next',
|
|
58
|
+
'.venv',
|
|
59
|
+
'__pycache__',
|
|
60
|
+
'vendors',
|
|
61
|
+
'_legacy_reference',
|
|
62
|
+
'artifacts',
|
|
63
|
+
'tests',
|
|
64
|
+
'test',
|
|
65
|
+
]);
|
|
66
|
+
|
|
67
|
+
const DEFAULT_ROOTS = ['apps', 'packages', 'plugins', 'config', 'scripts', 'tooling'];
|
|
68
|
+
|
|
69
|
+
/** One manifest record — every field is required (R1). */
|
|
70
|
+
interface ManifestEntry {
|
|
71
|
+
id: string;
|
|
72
|
+
wbs: string;
|
|
73
|
+
file: string;
|
|
74
|
+
keepsWorking: string;
|
|
75
|
+
removalCondition: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
interface Violation {
|
|
79
|
+
kind: 'unregistered' | 'stale' | 'incomplete';
|
|
80
|
+
id: string;
|
|
81
|
+
message: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function parseArgs(argv: string[]): { manifest: string; roots: string[]; cwd: string } {
|
|
85
|
+
let manifest = 'config/transition-shims.json';
|
|
86
|
+
let roots = DEFAULT_ROOTS;
|
|
87
|
+
for (let i = 0; i < argv.length; i++) {
|
|
88
|
+
if (argv[i] === '--manifest') manifest = argv[++i] ?? manifest;
|
|
89
|
+
else if (argv[i] === '--roots') roots = (argv[++i] ?? '').split(',').filter(Boolean);
|
|
90
|
+
}
|
|
91
|
+
return { manifest, roots, cwd: process.cwd() };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Walk a root and return marker id -> repo-relative files. */
|
|
95
|
+
function scanMarkers(cwd: string, roots: string[]): Map<string, Set<string>> {
|
|
96
|
+
const found = new Map<string, Set<string>>();
|
|
97
|
+
const walk = (dir: string): void => {
|
|
98
|
+
let entries: string[];
|
|
99
|
+
try {
|
|
100
|
+
entries = readdirSync(dir);
|
|
101
|
+
} catch {
|
|
102
|
+
return; // unreadable or missing root — skip
|
|
103
|
+
}
|
|
104
|
+
for (const name of entries) {
|
|
105
|
+
if (SKIP_DIRS.has(name)) continue;
|
|
106
|
+
const abs = join(dir, name);
|
|
107
|
+
let st: ReturnType<typeof statSync>;
|
|
108
|
+
try {
|
|
109
|
+
st = statSync(abs);
|
|
110
|
+
} catch {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (st.isDirectory()) {
|
|
114
|
+
walk(abs);
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (!st.isFile()) continue;
|
|
118
|
+
let text: string;
|
|
119
|
+
try {
|
|
120
|
+
text = readFileSync(abs, 'utf8');
|
|
121
|
+
} catch {
|
|
122
|
+
continue; // binary or unreadable — markers are text only
|
|
123
|
+
}
|
|
124
|
+
for (const m of text.matchAll(MARKER_RE)) {
|
|
125
|
+
const id = m[1] as string;
|
|
126
|
+
const file = relative(cwd, abs);
|
|
127
|
+
const set = found.get(id) ?? new Set<string>();
|
|
128
|
+
set.add(file);
|
|
129
|
+
found.set(id, set);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
for (const root of roots) walk(resolve(cwd, root));
|
|
134
|
+
return found;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Load the manifest; a missing file degrades to no entries (corpus-check parity). */
|
|
138
|
+
function loadManifest(path: string): { entries: ManifestEntry[]; raw: string | null } {
|
|
139
|
+
if (!existsSync(path)) return { entries: [], raw: null };
|
|
140
|
+
const raw = readFileSync(path, 'utf8');
|
|
141
|
+
let parsed: unknown;
|
|
142
|
+
try {
|
|
143
|
+
parsed = JSON.parse(raw);
|
|
144
|
+
} catch (error) {
|
|
145
|
+
return { entries: [], raw: `malformed JSON at ${path}: ${String(error)}` };
|
|
146
|
+
}
|
|
147
|
+
const entries = (parsed as { entries?: unknown[] }).entries;
|
|
148
|
+
if (!Array.isArray(entries)) return { entries: [], raw: `${path}: missing "entries" array` };
|
|
149
|
+
return { entries: entries as ManifestEntry[], raw: null };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function validateEntry(e: ManifestEntry, violations: Violation[]): void {
|
|
153
|
+
const field = (key: keyof ManifestEntry, value: unknown, why: string): void => {
|
|
154
|
+
if (typeof value !== 'string' || value.trim() === '') {
|
|
155
|
+
violations.push({
|
|
156
|
+
kind: 'incomplete',
|
|
157
|
+
id: typeof e.id === 'string' ? e.id : '<missing>',
|
|
158
|
+
message: `manifest entry ${JSON.stringify(e.id ?? null)} is missing required field "${key}"${why}`,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
field('id', e.id, '');
|
|
163
|
+
field('wbs', e.wbs, ` in entry ${JSON.stringify(e.id ?? null)}`);
|
|
164
|
+
field('file', e.file, ` in entry ${JSON.stringify(e.id ?? null)}`);
|
|
165
|
+
field('keepsWorking', e.keepsWorking, ` in entry ${JSON.stringify(e.id ?? null)}`);
|
|
166
|
+
field('removalCondition', e.removalCondition, ` in entry ${JSON.stringify(e.id ?? null)}`);
|
|
167
|
+
if (typeof e.id === 'string' && e.id !== '' && !ID_RE.test(e.id)) {
|
|
168
|
+
violations.push({
|
|
169
|
+
kind: 'incomplete',
|
|
170
|
+
id: e.id,
|
|
171
|
+
message: `manifest entry id ${JSON.stringify(e.id)} is not lowercase-kebab (^[a-z0-9][a-z0-9-]*$)`,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function run(): number {
|
|
177
|
+
const { manifest, roots, cwd } = parseArgs(process.argv.slice(2));
|
|
178
|
+
const manifestPath = resolve(cwd, manifest);
|
|
179
|
+
const { entries, raw } = loadManifest(manifestPath);
|
|
180
|
+
const violations: Violation[] = [];
|
|
181
|
+
const incomplete = new Set<string>();
|
|
182
|
+
|
|
183
|
+
if (raw !== null) {
|
|
184
|
+
violations.push({ kind: 'incomplete', id: '<manifest>', message: raw });
|
|
185
|
+
// A manifest that cannot be parsed has no valid entries to compare against.
|
|
186
|
+
for (const v of violations) console.error(`transition-shim-check: FAIL - ${v.message}`);
|
|
187
|
+
console.error('transition-shim-check: FAIL - manifest unreadable; refusing to pass');
|
|
188
|
+
return 1;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
for (const e of entries) {
|
|
192
|
+
const before = violations.length;
|
|
193
|
+
validateEntry(e, violations);
|
|
194
|
+
if (violations.length > before) incomplete.add(String(e.id));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const markers = scanMarkers(cwd, roots);
|
|
198
|
+
const observedIds = new Set(markers.keys());
|
|
199
|
+
const manifestIds = new Set(entries.filter((e) => !incomplete.has(String(e.id))).map((e) => e.id));
|
|
200
|
+
|
|
201
|
+
for (const [id, files] of markers) {
|
|
202
|
+
if (manifestIds.has(id)) continue;
|
|
203
|
+
const file = [...files].sort().join(', ');
|
|
204
|
+
violations.push({
|
|
205
|
+
kind: 'unregistered',
|
|
206
|
+
id,
|
|
207
|
+
message: `new unregistered shim @transition-shim(${id}) in ${file} — add an entry to ${manifest} or remove the marker`,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
for (const e of entries) {
|
|
211
|
+
if (incomplete.has(e.id)) continue;
|
|
212
|
+
if (observedIds.has(e.id)) continue;
|
|
213
|
+
violations.push({
|
|
214
|
+
kind: 'stale',
|
|
215
|
+
id: e.id,
|
|
216
|
+
message: `stale manifest entry ${e.id} (wbs ${e.wbs}) — @transition-shim(${e.id}) no longer appears in source; remove the entry, its removal condition was: ${e.removalCondition}`,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const unregistered = violations.filter((v) => v.kind === 'unregistered');
|
|
221
|
+
const stale = violations.filter((v) => v.kind === 'stale');
|
|
222
|
+
const incompleteCount = violations.filter((v) => v.kind === 'incomplete').length;
|
|
223
|
+
const ok = violations.length === 0;
|
|
224
|
+
|
|
225
|
+
const noun = entries.length === 1 ? 'entry' : 'entries';
|
|
226
|
+
console.log(
|
|
227
|
+
`transition-shim-check: ${markers.size} marker(s) observed, ${entries.length} manifest ${noun} baselined, ` +
|
|
228
|
+
`${unregistered.length} new, ${stale.length} stale, ${incompleteCount} incomplete — ${ok ? 'PASS' : 'FAIL'}`,
|
|
229
|
+
);
|
|
230
|
+
if (ok) return 0;
|
|
231
|
+
|
|
232
|
+
for (const v of violations) {
|
|
233
|
+
console.error(`transition-shim-check: ${v.kind === 'incomplete' ? 'FAIL' : `FAIL (${v.kind})`} - ${v.message}`);
|
|
234
|
+
}
|
|
235
|
+
return 1;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
process.exit(run());
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Gates:
|
|
10
10
|
* (a) heading whitelist — H1 title + the per-contract ordered section headings
|
|
11
|
-
* (b) frontmatter schema — description, argument-hint, allowed-tools; dev-only extras
|
|
11
|
+
* (b) frontmatter schema — description, argument-hint, allowed-tools; dev-only extras;
|
|
12
|
+
* real-YAML re-parse so malformed blocks cannot ship an empty description
|
|
12
13
|
* (c) target resolution — sp:<skill> refs, workflow files, procedure anchors
|
|
13
14
|
* (d) allowed-tools coherence — Skill present iff body contains Skill() call
|
|
14
15
|
* (e) dev-command argument contract — syntax-only hint, Argument Flags table columns,
|
|
@@ -19,6 +20,7 @@
|
|
|
19
20
|
|
|
20
21
|
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
21
22
|
import { join, resolve } from 'node:path';
|
|
23
|
+
import { parse as parseYaml } from 'yaml';
|
|
22
24
|
|
|
23
25
|
// ─── Types ──────────────────────────────────────────────────────────────────
|
|
24
26
|
|
|
@@ -53,6 +55,8 @@ interface ParsedCommand {
|
|
|
53
55
|
readonly description: string | undefined;
|
|
54
56
|
readonly argumentHint: string | undefined;
|
|
55
57
|
readonly allowedTools: string[] | undefined;
|
|
58
|
+
/** Set when the frontmatter fails a real YAML parse or yields an empty description. */
|
|
59
|
+
readonly frontmatterYamlProblem: string | undefined;
|
|
56
60
|
readonly body: string;
|
|
57
61
|
}
|
|
58
62
|
|
|
@@ -68,6 +72,30 @@ function parseCommand(filePath: string, name: string): ParsedCommand {
|
|
|
68
72
|
const argumentHint = extractYamlField(fm, 'argument-hint');
|
|
69
73
|
const allowedTools = extractYamlList(fm, 'allowed-tools');
|
|
70
74
|
|
|
75
|
+
// The regex extractions above feed the per-field gates, but they cannot see
|
|
76
|
+
// malformed YAML: `description: >-` followed by unindented keys reads as a
|
|
77
|
+
// present description to the regex while a real YAML parser folds the next
|
|
78
|
+
// keys into the block scalar — the bug that shipped an empty description to
|
|
79
|
+
// superskill install for dev-feature-change. Gate (b) re-checks with a real
|
|
80
|
+
// parse so that class cannot regress.
|
|
81
|
+
let frontmatterYamlProblem: string | undefined;
|
|
82
|
+
if (fm.trim() !== '') {
|
|
83
|
+
try {
|
|
84
|
+
const parsed: unknown = parseYaml(fm);
|
|
85
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
86
|
+
frontmatterYamlProblem = 'frontmatter is not a YAML mapping';
|
|
87
|
+
} else {
|
|
88
|
+
const parsedDescription = (parsed as Record<string, unknown>).description;
|
|
89
|
+
if (typeof parsedDescription !== 'string' || parsedDescription.trim() === '') {
|
|
90
|
+
frontmatterYamlProblem = 'frontmatter description is empty after YAML parsing';
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
} catch (error) {
|
|
94
|
+
const detail = error instanceof Error ? error.message.split('\n')[0] : String(error);
|
|
95
|
+
frontmatterYamlProblem = `frontmatter is not valid YAML: ${detail}`;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
71
99
|
const lines = body.split('\n');
|
|
72
100
|
const title = lines[0]?.startsWith('# ') ? lines[0].slice(2).trim() : '';
|
|
73
101
|
|
|
@@ -90,7 +118,7 @@ function parseCommand(filePath: string, name: string): ParsedCommand {
|
|
|
90
118
|
}
|
|
91
119
|
}
|
|
92
120
|
|
|
93
|
-
return { name, title, headings, description, argumentHint, allowedTools, body };
|
|
121
|
+
return { name, title, headings, description, argumentHint, allowedTools, frontmatterYamlProblem, body };
|
|
94
122
|
}
|
|
95
123
|
|
|
96
124
|
/** Extract a plain YAML string field (handles quoted and unquoted). */
|
|
@@ -212,6 +240,9 @@ function checkHeadingWhitelist(cmd: ParsedCommand): readonly Violation[] {
|
|
|
212
240
|
|
|
213
241
|
function checkFrontmatterSchema(cmd: ParsedCommand): readonly Violation[] {
|
|
214
242
|
const violations: Violation[] = [];
|
|
243
|
+
if (cmd.frontmatterYamlProblem) {
|
|
244
|
+
violations.push({ command: cmd.name, gate: 'b', message: cmd.frontmatterYamlProblem });
|
|
245
|
+
}
|
|
215
246
|
if (!cmd.description) {
|
|
216
247
|
violations.push({ command: cmd.name, gate: 'b', message: 'missing frontmatter description' });
|
|
217
248
|
}
|
|
@@ -406,10 +406,13 @@ export function extractTriggerTable(crossCuttingRaw: string): string[] | null {
|
|
|
406
406
|
function adrAgentClaims(adrRaw: string): Map<string, SurfaceBehavior> | null {
|
|
407
407
|
if (adrRaw.includes('## ADR-047')) {
|
|
408
408
|
const out = new Map<string, SurfaceBehavior>();
|
|
409
|
+
// G5 amendment (feature G5 / task 0565): explicit inline is host-session-only — headless
|
|
410
|
+
// surfaces reject it with the stable special error; 0508 native-subagent eligibility
|
|
411
|
+
// applies to omitted --agent only, never explicit inline.
|
|
409
412
|
out.set('inline', {
|
|
410
|
-
surfaces: new Set(['inline'
|
|
413
|
+
surfaces: new Set(['inline']),
|
|
411
414
|
conditional: false,
|
|
412
|
-
defaultWhenOmitted:
|
|
415
|
+
defaultWhenOmitted: false,
|
|
413
416
|
});
|
|
414
417
|
out.set('auto', { surfaces: new Set(['subprocess']), conditional: false });
|
|
415
418
|
out.set('<name>', { surfaces: new Set(['inline', 'subprocess']), conditional: true });
|
|
@@ -89,6 +89,14 @@ only burns wall clock and context budget.
|
|
|
89
89
|
`bun test <file> --test-name-pattern "<test>"`, or `bunx tsc --noEmit` on a single package.
|
|
90
90
|
- **NEVER run** `bun run test`, `bun run spur-check`, `bun run check`, or any other full-suite /
|
|
91
91
|
project-gate command from inside implement. These belong to the pipeline's `test` hop.
|
|
92
|
+
- **Full-suite budget: at most 2 per task** (task 0436 R2) — counted across the whole task run
|
|
93
|
+
(implement probes + the pipeline `test` hop + verify/recheck), not per step. When a check fails,
|
|
94
|
+
run the narrow target (`bun test <file> --test-name-pattern <test>`) to green before any full
|
|
95
|
+
suite; reach for the second full run only when the narrow target cannot reproduce the failure.
|
|
96
|
+
- **Consolidate dogfood runs**: one combined real-data execution that exercises all scenarios,
|
|
97
|
+
not N near-identical `--dry-run`/real invocations of the same script. If you find yourself
|
|
98
|
+
rerunning the same dogfood command with one flag changed, stop and fold the variants into a
|
|
99
|
+
single run — repeated identical commands are loop-detector findings and pure cost.
|
|
92
100
|
- If a targeted probe reveals a failure you cannot fix within implement scope, note it in
|
|
93
101
|
`## Solution` and let the `test` hop's fixall handle it — do not pre-empt the gate.
|
|
94
102
|
|
|
@@ -146,7 +154,7 @@ reproduce → isolate → minimal fix → regression guard.
|
|
|
146
154
|
## Common Rationalizations
|
|
147
155
|
|
|
148
156
|
| Rationalization | Reality |
|
|
149
|
-
|
|
157
|
+
| --- | --- |
|
|
150
158
|
| "The spec is clear — I don't need to read the callers." | Code that looks orthogonal is how regressions ship (R5). Read the exports you touch and their immediate callers before writing. |
|
|
151
159
|
| "I'll add the tests in a follow-up." | Untested production code is unverified code. The task's test step is not optional; behavior ships with its test. |
|
|
152
160
|
| "This abstraction will be useful later." | Speculative abstraction is complexity without a caller (R2). Build for the requirement in front of you; add the seam when the second use arrives. |
|
|
@@ -239,6 +239,11 @@ printf '...' > /tmp/<wbs>-testing.md
|
|
|
239
239
|
spur task update <wbs> --section Testing --from-file /tmp/<wbs>-testing.md
|
|
240
240
|
```
|
|
241
241
|
|
|
242
|
+
> **Corrections: the answer file is the source of truth.** `spur task record` re-transcribes
|
|
243
|
+
> `## Testing` from the verdict artifact, overwriting `--section Testing` writes — direct section
|
|
244
|
+
> fixes are futile. Fix `.spur/run/<wbs>-verify-answer.txt` → `spur task verdict <wbs>
|
|
245
|
+
> --from-answer <file>` → re-record. `--section` is initial authorship only.
|
|
246
|
+
|
|
242
247
|
> **Do not write `## Review` directly in verify mode.** The `## Review` section is owned by the
|
|
243
248
|
> `review` step (`/sp:dev-review`), which dispatches `functional-review` + `code-verification`
|
|
244
249
|
> review mode + `code-improvement`. The `record` step backfills `## Review` from the verdict
|
|
@@ -283,9 +288,7 @@ Verdict: PASS
|
|
|
283
288
|
| P4 | — | — | No P1–P3 findings; verify verdict PASS |
|
|
284
289
|
```
|
|
285
290
|
|
|
286
|
-
The per-requirement traceability table MUST use `| Req | Status | Evidence |` (exactly this header, no `R#`/`R`/`Requirement` variant
|
|
287
|
-
|
|
288
|
-
**MUST NOT:** use `| R# | ... |` as the sole id header without `Status` in column 2.
|
|
291
|
+
The per-requirement traceability table MUST use `| Req | Status | Evidence |` (exactly this header, no `R#`/`R`/`Requirement` variant — `Status` in column 2 — and no extra columns between Req and Status). The Acceptance Criteria table MUST use `| AC | Status | Evidence Type | Evidence |`.
|
|
289
292
|
**MUST NOT:** place a `Severity` column between `Req` and `Status` in the authoring contract.
|
|
290
293
|
The parser is tolerant of these variants (defense-in-depth), but the authoring contract is
|
|
291
294
|
canonical.
|
|
@@ -348,8 +351,7 @@ mutation is discoverable from the tracked task file alone, without diffing untra
|
|
|
348
351
|
### Step 13 — Shippable readiness gate (feature-level)
|
|
349
352
|
|
|
350
353
|
Per-task PASS is **not** the same as “this feature is ready to ship.” After Steps 11–12, when the
|
|
351
|
-
gate is **active**, evaluate feature AC satisfaction via the existing CLI
|
|
352
|
-
framework).
|
|
354
|
+
gate is **active**, evaluate feature AC satisfaction via the existing CLI.
|
|
353
355
|
|
|
354
356
|
**When active**
|
|
355
357
|
|
|
@@ -371,16 +373,17 @@ framework).
|
|
|
371
373
|
`docs/.spur/run` or other nested `.spur` trees). Ephemeral scratch may use `/tmp` or
|
|
372
374
|
`/private/tmp`. Requirement / AC row `id`s in the verdict MUST match feature scenario titles
|
|
373
375
|
(or `AC-N` aliases) so satisfaction can mark MET.
|
|
374
|
-
2. Run:
|
|
376
|
+
2. Run (0568 R6 monorepo-safe: SPUR_BIN env > local CLI > PATH):
|
|
375
377
|
|
|
376
378
|
```bash
|
|
377
|
-
|
|
378
|
-
|
|
379
|
+
SPUR_BIN="${SPUR_BIN:-$([ -f apps/cli/src/index.ts ] && echo 'bun apps/cli/src/index.ts' || echo spur)}"
|
|
380
|
+
$SPUR_BIN feature check <featureId> --json
|
|
381
|
+
$SPUR_BIN task list --feature <featureId> --json
|
|
379
382
|
```
|
|
380
383
|
|
|
381
384
|
3. Classify **Shippable: PASS** only if **all** of:
|
|
382
385
|
- No finding whose code/message indicates **linked but unverified** scenarios
|
|
383
|
-
(`L4_SCENARIO_UNVERIFIED`
|
|
386
|
+
(`L4_SCENARIO_UNVERIFIED`).
|
|
384
387
|
- No **orphan / uncovered** feature scenarios (`L4_ORPHAN_SCENARIOS`,
|
|
385
388
|
`L4_UNCOVERED_FEATURE_SCENARIO` / no covering task).
|
|
386
389
|
- No **incomplete** linked tasks: every task with this `feature_id` is `done` or `cancelled`
|
|
@@ -424,16 +427,14 @@ Full flag matrix and ops notes: [spur-dev/references/dev-operations.md](../spur-
|
|
|
424
427
|
|
|
425
428
|
### Step 14 — Report
|
|
426
429
|
|
|
427
|
-
Show the per-task verdict,
|
|
428
|
-
**Shippable:** line from Step 13 when applicable. Under the pipeline the
|
|
429
|
-
|
|
430
|
+
Show the per-task verdict, per-requirement table, gate outcome (cleared/blocked), and the
|
|
431
|
+
**Shippable:** line from Step 13 when applicable. Under the pipeline the done-gate consumes the
|
|
432
|
+
verdict; for a direct `/sp:dev-verify` invocation the full report is the operator's summary.
|
|
430
433
|
|
|
431
|
-
**`--next` on
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
machine signal; the report line is the operator-visible summary - both must agree so a terminal-task
|
|
436
|
-
re-audit cannot be misread as a successful `testing -> done` transition.
|
|
434
|
+
**`--next` on a terminal task (no-op surfacing).** The transition cannot fire; the verify report
|
|
435
|
+
line MUST state the no-op itself (e.g. `--next: no-op - task already terminal (<status>)`). The CLI
|
|
436
|
+
print is the machine signal, the report line the operator summary — both must agree so a terminal
|
|
437
|
+
re-audit is never misread as a successful `testing -> done` (dev-verify.md `--next` chain).
|
|
437
438
|
|
|
438
439
|
---
|
|
439
440
|
|
|
@@ -507,18 +508,18 @@ Do **not** use this skill for:
|
|
|
507
508
|
and the per-requirement aggregation rule.
|
|
508
509
|
- [references/secu-review.md](references/secu-review.md) — the SECUA dimensions and finding-severity
|
|
509
510
|
rubric.
|
|
511
|
+
- `plugins/sp/references/roles.md` — the Layer-1 role→tier table; this skill's verify/review modes
|
|
512
|
+
run under the `reviewer` role (0538 R4: this file points at roles.md, it does not restate tiers).
|
|
510
513
|
- `.spur/workflows/task-pipeline.yaml` — the `verify → record` gate that consumes the verdict.
|
|
511
514
|
- **`sp:spur-dev`** — the execution-half umbrella that drives the pipeline this skill gates.
|
|
512
515
|
- [references/code-improvement.md](references/code-improvement.md) — architecture-improvement lens
|
|
513
516
|
for module depth, seam placement, locality, coupling, and testability.
|
|
514
|
-
- **`sp:functional-review`** —
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
blocker/major/minor/advisory). When review findings expose structural friction rather than a
|
|
521
|
-
localized defect, dispatch this skill; see [../code-improvement/SKILL.md](../code-improvement/SKILL.md).
|
|
517
|
+
- **`sp:functional-review`** — requirements-traceability peer review (R{n} → file:line,
|
|
518
|
+
per-requirement MET/PARTIAL/UNMET). Dispatch when the `review` dimension needs functional
|
|
519
|
+
traceability, not just SECUA: [../functional-review/SKILL.md](../functional-review/SKILL.md).
|
|
520
|
+
- **`sp:code-improvement`** — architectural-deepening peer review (5 signals, severity
|
|
521
|
+
blocker/major/minor/advisory). Dispatch for structural friction rather than localized
|
|
522
|
+
defects: [../code-improvement/SKILL.md](../code-improvement/SKILL.md).
|
|
522
523
|
|
|
523
524
|
---
|
|
524
525
|
|
|
@@ -531,5 +532,5 @@ directly: `Skill(skill="sp:code-verification", args="verify <wbs> --fix all")`.
|
|
|
531
532
|
|
|
532
533
|
### Codex / OpenClaw / OpenCode / Antigravity
|
|
533
534
|
|
|
534
|
-
Run `spur` CLI via Bash; parse `--json`. Invoke
|
|
535
|
-
|
|
535
|
+
Run `spur` CLI via Bash; parse `--json`. Invoke the skill directly — the skill is the SSOT; the
|
|
536
|
+
commands are thin wrappers.
|