@gobing-ai/spur 0.3.91 → 0.3.93
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/pipeline-budgets.json +2 -9
- package/config/plugin-scripts.json +27 -1
- package/config/templates/AGENTS.md +4 -0
- package/config/templates/docs/02_ROADMAP.md +2 -0
- package/config/templates/docs/03_ARCHITECTURE.md +3 -1
- package/config/templates/docs/04_DESIGN.md +2 -0
- package/config/templates/docs/99_PROJECT_CONSTITUTION.md +10 -2
- package/config/workflow-candidates.json +72 -1
- package/config/workflows/feature-lifecycle.yaml +14 -5
- package/config/workflows/feature-verification.yaml +36 -27
- package/config/workflows/history-anatomy.yaml +16 -25
- package/config/workflows/idea-pipeline.yaml +76 -23
- package/config/workflows/pr-review.yaml +8 -0
- package/config/workflows/task-pipeline.yaml +362 -40
- package/config/workflows/wayfinder-resolution.yaml +5 -0
- package/config/workflows/wrapup-pipeline.yaml +102 -19
- package/package.json +9 -9
- package/plugins/sp/README.md +7 -2
- package/plugins/sp/agents/super-planner.md +14 -5
- package/plugins/sp/commands/dev-dogfood.md +4 -4
- package/plugins/sp/commands/dev-fixall.md +8 -5
- package/plugins/sp/commands/dev-run.md +8 -2
- package/plugins/sp/commands/dev-runall.md +14 -8
- package/plugins/sp/commands/dev-verify.md +9 -0
- package/plugins/sp/commands/dev-verifyall.md +5 -0
- package/plugins/sp/lib/idea-handoff.generated.mjs +306 -301
- package/plugins/sp/lib/inline-run.generated.d.mts +18 -0
- package/plugins/sp/lib/inline-run.generated.mjs +1468 -0
- package/plugins/sp/plugin.json +1 -1
- package/plugins/sp/references/environment-lens.md +1 -1
- package/plugins/sp/scripts/dogfood-testing/validate-report.mjs +136 -0
- package/plugins/sp/scripts/dogfood-testing/validate-report.ts +196 -2
- package/plugins/sp/scripts/feature-verification-steps.mjs +174 -0
- package/plugins/sp/scripts/feature-verification-steps.ts +275 -0
- package/plugins/sp/scripts/history-anatomy-cache.mjs +104 -4
- package/plugins/sp/scripts/history-anatomy-cache.ts +137 -13
- package/plugins/sp/scripts/inline-pipeline-parity-check.ts +2 -1
- package/plugins/sp/scripts/inline-run-setup.mjs +421 -0
- package/plugins/sp/scripts/inline-run-setup.ts +325 -78
- package/plugins/sp/scripts/quality-gate.mjs +248 -6
- package/plugins/sp/scripts/quality-gate.ts +410 -9
- package/plugins/sp/scripts/record-feature-sync.mjs +63 -0
- package/plugins/sp/scripts/record-feature-sync.ts +84 -0
- package/plugins/sp/scripts/residual-scan.mjs +484 -0
- package/plugins/sp/scripts/residual-scan.ts +640 -0
- package/plugins/sp/scripts/task-diffstat.mjs +156 -0
- package/plugins/sp/scripts/task-diffstat.ts +229 -0
- package/plugins/sp/scripts/task-evidence-precheck.ts +8 -3
- package/plugins/sp/scripts/task-size-precheck.ts +8 -3
- package/plugins/sp/scripts/wrapup-drift-probe.mjs +181 -0
- package/plugins/sp/scripts/wrapup-drift-probe.ts +258 -0
- package/plugins/sp/scripts/wrapup-steps.mjs +60 -1
- package/plugins/sp/scripts/wrapup-steps.ts +89 -4
- package/plugins/sp/skills/brainstorm/SKILL.md +2 -0
- package/plugins/sp/skills/brainstorm/references/workflows.md +17 -2
- package/plugins/sp/skills/branch-workflow/SKILL.md +1 -0
- package/plugins/sp/skills/branch-workflow/references/worktree-patterns.md +2 -0
- package/plugins/sp/skills/code-implementation/SKILL.md +17 -0
- package/plugins/sp/skills/code-verification/SKILL.md +21 -0
- package/plugins/sp/skills/code-verification/references/secu-review.md +3 -2
- package/plugins/sp/skills/code-verification/references/verdict-schema.md +1 -0
- package/plugins/sp/skills/dogfood-testing/SKILL.md +5 -3
- package/plugins/sp/skills/dogfood-testing/references/monitor-ledger.md +63 -26
- package/plugins/sp/skills/dogfood-testing/references/report-template.md +33 -10
- package/plugins/sp/skills/history-anatomy/references/modes.md +5 -3
- package/plugins/sp/skills/next-feature/references/ranking-rubric.md +1 -1
- package/plugins/sp/skills/next-router/references/routing-table.md +7 -0
- package/plugins/sp/skills/parallel-execution/references/dispatch-surface.md +1 -1
- package/plugins/sp/skills/session-review/SKILL.md +12 -2
- package/plugins/sp/skills/spur-check/SKILL.md +112 -0
- package/plugins/sp/skills/spur-cli/references/features.md +1 -1
- package/plugins/sp/skills/spur-cli/references/projects.md +3 -1
- package/plugins/sp/skills/spur-cli/references/workflows.md +39 -19
- package/plugins/sp/skills/spur-dev/SKILL.md +13 -5
- package/plugins/sp/skills/spur-dev/references/cross-cutting.md +18 -3
- package/plugins/sp/skills/spur-dev/references/dev-operations.md +2 -2
- package/plugins/sp/skills/spur-dev/references/document-authoring.md +85 -0
- package/plugins/sp/skills/spur-dev/references/execution-batch.md +286 -58
- package/plugins/sp/skills/spur-dev/references/execution-workflow.md +1 -1
- package/plugins/sp/skills/spur-dev/references/flag-glossary.md +27 -4
- package/plugins/sp/skills/spur-dev/references/gate-checklists.md +3 -2
- package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +61 -16
- package/plugins/sp/skills/spur-dev/references/planning-workflow.md +10 -5
- package/plugins/sp/skills/spur-dev/templates/design.md +31 -0
- package/plugins/sp/skills/spur-dev/templates/plan.md +32 -0
- package/plugins/sp/skills/spur-doctor/SKILL.md +60 -14
- package/schemas/state-machine-workflow.schema.json +4 -0
- package/spur.js +22501 -19644
- package/web/_astro/BoardApp.CerSBgis.js +192 -0
- package/web/_astro/BoardApp.eoTz0pZs.js +1 -0
- package/web/_astro/{TaskDetail.CXGltuT_.js → TaskDetail.DCqiC-OZ.js} +1 -1
- package/web/_astro/arc.CPwg6Rw0.js +1 -0
- package/web/_astro/{architectureDiagram-3BPJPVTR.DJ8DHkWE.js → architectureDiagram-3BPJPVTR.DM_vp_hO.js} +1 -1
- package/web/_astro/{blockDiagram-GPEHLZMM.D8DHK3Jl.js → blockDiagram-GPEHLZMM.DXVIiv0p.js} +1 -1
- package/web/_astro/{c4Diagram-AAUBKEIU.BugQbX9u.js → c4Diagram-AAUBKEIU.BbF_zCxW.js} +1 -1
- package/web/_astro/channel.MYZLKNwy.js +1 -0
- package/web/_astro/{chunk-2J33WTMH.Mv26KlVn.js → chunk-2J33WTMH.CAgQHpPC.js} +1 -1
- package/web/_astro/{chunk-4BX2VUAB.CD51JoT_.js → chunk-4BX2VUAB.BN-5tpw4.js} +1 -1
- package/web/_astro/{chunk-55IACEB6.D_PFaIEe.js → chunk-55IACEB6.CnPkEEr0.js} +1 -1
- package/web/_astro/{chunk-727SXJPM.DemMW1ao.js → chunk-727SXJPM.BQzQeMVm.js} +4 -4
- package/web/_astro/{chunk-AQP2D5EJ.Iu2V5-ex.js → chunk-AQP2D5EJ.B6xNyDnL.js} +1 -1
- package/web/_astro/{chunk-FMBD7UC4.MsNgSP-E.js → chunk-FMBD7UC4.C7f9Ih78.js} +1 -1
- package/web/_astro/{chunk-ND2GUHAM.DfbRaAlm.js → chunk-ND2GUHAM.CNV1dFXT.js} +1 -1
- package/web/_astro/{chunk-QZHKN3VN.BbJAQ4h-.js → chunk-QZHKN3VN.Cudn2TkJ.js} +1 -1
- package/web/_astro/{classDiagram-4FO5ZUOK.CPurtiC2.js → classDiagram-4FO5ZUOK.D1NwP50q.js} +1 -1
- package/web/_astro/{classDiagram-v2-Q7XG4LA2.CPurtiC2.js → classDiagram-v2-Q7XG4LA2.D1NwP50q.js} +1 -1
- package/web/_astro/{cose-bilkent-S5V4N54A.CKKdx1bM.js → cose-bilkent-S5V4N54A.B1wSL-Xb.js} +1 -1
- package/web/_astro/{cynefin-OW5HDTMX.CoKMTg-R.js → cynefin-OW5HDTMX.BmK52w8G.js} +1 -1
- package/web/_astro/{dagre-BM42HDAG.D4h4_k56.js → dagre-BM42HDAG.Bfy5CTDT.js} +2 -2
- package/web/_astro/diagram-2AECGRRQ.DhNnvUvX.js +43 -0
- package/web/_astro/diagram-5GNKFQAL.lTX5KwnS.js +10 -0
- package/web/_astro/{diagram-KO2AKTUF.BFoCkiCr.js → diagram-KO2AKTUF.CW_vMJ4z.js} +3 -3
- package/web/_astro/{diagram-LMA3HP47.exHn9OVx.js → diagram-LMA3HP47.B_8ZGF67.js} +1 -1
- package/web/_astro/{diagram-OG6HWLK6.CeqO34nN.js → diagram-OG6HWLK6.BppnHsdS.js} +1 -1
- package/web/_astro/{erDiagram-TEJ5UH35.D_v7HqxR.js → erDiagram-TEJ5UH35.BEuHXcjJ.js} +5 -5
- package/web/_astro/{flowDiagram-I6XJVG4X.EUmrpbwh.js → flowDiagram-I6XJVG4X.CH-UlnGr.js} +4 -4
- package/web/_astro/{ganttDiagram-6RSMTGT7.BOCF5lII.js → ganttDiagram-6RSMTGT7.BO81S85v.js} +1 -1
- package/web/_astro/{gitGraphDiagram-PVQCEYII.Di7otYZD.js → gitGraphDiagram-PVQCEYII.XnPxPPZN.js} +1 -1
- package/web/_astro/index.Hjbr15fG.css +1 -0
- package/web/_astro/{infoDiagram-5YYISTIA.TcBkCAJk.js → infoDiagram-5YYISTIA.JyjYRu_T.js} +1 -1
- package/web/_astro/{ishikawaDiagram-YF4QCWOH.D-2y4M0c.js → ishikawaDiagram-YF4QCWOH.BBRBF-Fo.js} +5 -5
- package/web/_astro/{journeyDiagram-JHISSGLW.DPbJI_n2.js → journeyDiagram-JHISSGLW.C_iymSyp.js} +1 -1
- package/web/_astro/{kanban-definition-UN3LZRKU.EFxhQ9Fj.js → kanban-definition-UN3LZRKU.DdfW-Oqt.js} +7 -7
- package/web/_astro/{linear.DSAsQLzs.js → linear.C2_IkbZT.js} +1 -1
- package/web/_astro/mermaid.core.GAOYeSR0.js +303 -0
- package/web/_astro/{mindmap-definition-RKZ34NQL.CJY1N_7V.js → mindmap-definition-RKZ34NQL.DAZIxQSK.js} +2 -2
- package/web/_astro/{pieDiagram-4H26LBE5.567ZNoL2.js → pieDiagram-4H26LBE5.CN8sIhKM.js} +3 -3
- package/web/_astro/{quadrantDiagram-W4KKPZXB.mqfz9-MY.js → quadrantDiagram-W4KKPZXB.3dGcX5GP.js} +1 -1
- package/web/_astro/{requirementDiagram-4Y6WPE33.Bv1Gv9In.js → requirementDiagram-4Y6WPE33.BV2y4dd6.js} +3 -3
- package/web/_astro/{sankeyDiagram-5OEKKPKP.B6Gs4X4r.js → sankeyDiagram-5OEKKPKP.Cqo15Tvo.js} +4 -4
- package/web/_astro/{sequenceDiagram-3UESZ5HK.BhYj4v-m.js → sequenceDiagram-3UESZ5HK.CROCPMJB.js} +1 -1
- package/web/_astro/{stateDiagram-AJRCARHV.BPbBnkpw.js → stateDiagram-AJRCARHV.RfXZrkFE.js} +1 -1
- package/web/_astro/{stateDiagram-v2-BHNVJYJU.C4squMNK.js → stateDiagram-v2-BHNVJYJU.CPXmbBs9.js} +1 -1
- package/web/_astro/{timeline-definition-PNZ67QCA.C_SwIHgl.js → timeline-definition-PNZ67QCA.DdgKTiO8.js} +3 -3
- package/web/_astro/{vennDiagram-CIIHVFJN.Bz4NZGpQ.js → vennDiagram-CIIHVFJN.CPNVSHF1.js} +5 -5
- package/web/_astro/{wardleyDiagram-YWT4CUSO.CozMVZ3i.js → wardleyDiagram-YWT4CUSO.CQhA0Jyr.js} +3 -3
- package/web/_astro/{xychartDiagram-2RQKCTM6.BwMGBwjB.js → xychartDiagram-2RQKCTM6.n61BWyy4.js} +1 -1
- package/web/index.html +2 -2
- package/config/workflows/decision-routing-example.yaml +0 -134
- package/web/_astro/BoardApp.BEDWpzsr.js +0 -188
- package/web/_astro/BoardApp.DQG2xfEz.js +0 -1
- package/web/_astro/arc.C0rrflm_.js +0 -1
- package/web/_astro/channel.SRrg1P-w.js +0 -1
- package/web/_astro/diagram-2AECGRRQ.DJ0h9zgw.js +0 -43
- package/web/_astro/diagram-5GNKFQAL.DvPk1jYd.js +0 -10
- package/web/_astro/index.Bx6GY4RH.css +0 -1
- package/web/_astro/mermaid.core.kAZjgJHG.js +0 -301
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* wrapup-drift-probe — deterministic doc-ownership drift probe behind the wrapup-pipeline
|
|
4
|
+
* task-resolve onEnter (task 0944, feature D64, ADR-115 composition budgets).
|
|
5
|
+
*
|
|
6
|
+
* Reads the validated wrapup capture `.spur/run/<runId>-wrapup-tasks.json` (never raw
|
|
7
|
+
* vars.tasks — 0783 contract), runs `spur task show <wbs> --json` per member, collects the
|
|
8
|
+
* changed paths from each task's `### Solution` file:line map, and writes:
|
|
9
|
+
* - `<runId>-drift-probe.json` `{ clean: boolean, reasons: string[], paths: string[] }`
|
|
10
|
+
* - `<runId>-mode.txt` projected wrapup mode: `fast` when clean, empty otherwise
|
|
11
|
+
*
|
|
12
|
+
* The probe runs ONLY when the caller left `mode` empty; a caller-set mode is projected
|
|
13
|
+
* verbatim by the workflow wrapper and this script is never invoked (0944 R3).
|
|
14
|
+
*
|
|
15
|
+
* Dirty (clean=false, mode stays empty → the safety route) whenever ANY changed path
|
|
16
|
+
* matches a doc-owned surface (AGENTS.md doc map + docs/99_PROJECT_CONSTITUTION.md
|
|
17
|
+
* ownership table), or a task's Solution section is empty or unparseable (fail safe —
|
|
18
|
+
* a wrapup without a readable change map must still reach doc-sync). Paths under the
|
|
19
|
+
* task/feature corpus are never treated as drift.
|
|
20
|
+
*
|
|
21
|
+
* Node-builtin imports only; the spur lookup is a spawnable `SpurRunner` so tests can
|
|
22
|
+
* fake it; `main(argv, env, options)` is the same injection point the workflow wrapper
|
|
23
|
+
* and its node twin exercise. Always exits 0 after probing; only an empty `__runId`
|
|
24
|
+
* (mis-invocation) exits 1.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { spawnSync } from 'node:child_process';
|
|
28
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
29
|
+
import { join } from 'node:path';
|
|
30
|
+
import { getEnvVars } from '../lib/env';
|
|
31
|
+
|
|
32
|
+
export interface DriftProbeEnv {
|
|
33
|
+
__runId?: string;
|
|
34
|
+
spurBin?: string;
|
|
35
|
+
[key: string]: string | undefined;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface DriftProbeOptions {
|
|
39
|
+
/** Base directory for `.spur/run`; defaults to the process cwd. */
|
|
40
|
+
cwd?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Minimal spawnable spur surface — tests fake this to return canned `task show` output. */
|
|
44
|
+
export interface SpurShowResult {
|
|
45
|
+
status: number;
|
|
46
|
+
stdout: string;
|
|
47
|
+
}
|
|
48
|
+
export type SpurRunner = (args: string[]) => SpurShowResult;
|
|
49
|
+
|
|
50
|
+
export interface DriftProbeResult {
|
|
51
|
+
clean: boolean;
|
|
52
|
+
reasons: string[];
|
|
53
|
+
paths: string[];
|
|
54
|
+
probeFile: string;
|
|
55
|
+
modeFile: string;
|
|
56
|
+
/** Exit code the workflow wrapper observes (only an empty __runId is a hard failure). */
|
|
57
|
+
exitCode: number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Doc-owned surfaces (0944 R2): a changed path under any of these means doc-sync must
|
|
62
|
+
* still run. Mirrors the AGENTS.md documentation map and the
|
|
63
|
+
* docs/99_PROJECT_CONSTITUTION.md ownership table: contracts / cli commands / config /
|
|
64
|
+
* migrations / workflow YAML / plugin commands, skills and hooks / root manifest / the
|
|
65
|
+
* authoritative docs (00_ADR, 03_ARCHITECTURE, 04_DESIGN, docs/design). docs/tasks* and
|
|
66
|
+
* docs/features/* (the corpus) are intentionally absent — they are never drift.
|
|
67
|
+
*
|
|
68
|
+
* WHY join('config', 'workflows'): rule sp-runtime-path forbids the literal
|
|
69
|
+
* config/<dir> in runtime source; this constant is a drift TARGET (Solution
|
|
70
|
+
* change-map paths against the build-time SSOT), not a runtime config read.
|
|
71
|
+
*/
|
|
72
|
+
const WORKFLOWS_GLOB = `${join('config', 'workflows')}/**`;
|
|
73
|
+
export const DOC_OWNED_SURFACES = [
|
|
74
|
+
'packages/contracts/**',
|
|
75
|
+
'apps/cli/src/commands/**',
|
|
76
|
+
'packages/config/src/**',
|
|
77
|
+
'drizzle/*.sql',
|
|
78
|
+
WORKFLOWS_GLOB,
|
|
79
|
+
'plugins/sp/commands/**',
|
|
80
|
+
'plugins/sp/skills/**',
|
|
81
|
+
'plugins/sp/hooks/**',
|
|
82
|
+
'package.json',
|
|
83
|
+
'docs/00_ADR.md',
|
|
84
|
+
'docs/03_ARCHITECTURE.md',
|
|
85
|
+
'docs/04_DESIGN.md',
|
|
86
|
+
'docs/design/**',
|
|
87
|
+
] as const;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Top-level workspace entries from the AGENTS.md stack layout. A changed path whose first
|
|
91
|
+
* segment is none of these is a new top-level workspace directory — doc-owned by default.
|
|
92
|
+
* Single-segment paths (root files) are not directories and are exempt.
|
|
93
|
+
*/
|
|
94
|
+
const KNOWN_TOP_LEVEL = new Set([
|
|
95
|
+
'apps',
|
|
96
|
+
'packages',
|
|
97
|
+
'plugins',
|
|
98
|
+
'config',
|
|
99
|
+
'docs',
|
|
100
|
+
'drizzle',
|
|
101
|
+
'scripts',
|
|
102
|
+
'vendors',
|
|
103
|
+
'package.json',
|
|
104
|
+
'bun.lock',
|
|
105
|
+
'bunfig.toml',
|
|
106
|
+
]);
|
|
107
|
+
|
|
108
|
+
/** Corpus paths (task/feature records) are never drift — excluded before surface matching. */
|
|
109
|
+
const CORPUS_PREFIXES = ['docs/tasks', 'docs/features/'];
|
|
110
|
+
|
|
111
|
+
/** `### Solution` section of a task record (tolerates `##`–`####` heading depth). */
|
|
112
|
+
export function solutionSectionOf(content: string): string | null {
|
|
113
|
+
const heading = /^#{2,4}\s+Solution\s*$/m.exec(content);
|
|
114
|
+
if (!heading) return null;
|
|
115
|
+
const rest = content.slice(heading.index + heading[0].length);
|
|
116
|
+
const next = /^#{2,4}\s+\S/m.exec(rest);
|
|
117
|
+
return next ? rest.slice(0, next.index) : rest;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const CHANGE_ENTRY = /`([^`\r\n]+):(\d+)(?:-\d+)?`/g;
|
|
121
|
+
|
|
122
|
+
/** Backticked `path:line` (or `path:line-range`) tokens from a Solution section. */
|
|
123
|
+
export function changedPathsOf(section: string | null): string[] {
|
|
124
|
+
if (section === null) return [];
|
|
125
|
+
const paths: string[] = [];
|
|
126
|
+
for (const match of section.matchAll(CHANGE_ENTRY)) {
|
|
127
|
+
const path = match[1];
|
|
128
|
+
// Path-like: no whitespace and at least one separator or dotted suffix, so prose
|
|
129
|
+
// backticks never masquerade as change-map entries.
|
|
130
|
+
if (/\s/.test(path)) continue;
|
|
131
|
+
if (!path.includes('/') && !path.includes('.')) continue;
|
|
132
|
+
paths.push(path);
|
|
133
|
+
}
|
|
134
|
+
return paths;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function surfaceRegex(glob: string): RegExp {
|
|
138
|
+
const escaped = glob
|
|
139
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
140
|
+
.replaceAll('**', '\u0000')
|
|
141
|
+
.replaceAll('*', '[^/]*')
|
|
142
|
+
.replaceAll('\u0000', '.*');
|
|
143
|
+
return new RegExp(`^${escaped}$`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const SURFACE_MATCHERS = DOC_OWNED_SURFACES.map((glob) => ({ glob, regex: surfaceRegex(glob) }));
|
|
147
|
+
|
|
148
|
+
/** 0944 R2 surface match: doc-owned glob, corpus exclusion, new top-level directory. */
|
|
149
|
+
export function driftReasonForPath(path: string): string | null {
|
|
150
|
+
if (CORPUS_PREFIXES.some((prefix) => path.startsWith(prefix))) return null;
|
|
151
|
+
if (path.includes('/') && !KNOWN_TOP_LEVEL.has(path.split('/')[0])) {
|
|
152
|
+
return 'new top-level workspace directory';
|
|
153
|
+
}
|
|
154
|
+
const match = SURFACE_MATCHERS.find((entry) => entry.regex.test(path));
|
|
155
|
+
return match ? `matches doc-owned surface ${match.glob}` : null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function defaultSpurRunner(env: DriftProbeEnv, cwd?: string): SpurRunner {
|
|
159
|
+
const parts = (env.spurBin ?? 'spur').split(/\s+/).filter((part) => part.length > 0);
|
|
160
|
+
return (args: string[]): SpurShowResult => {
|
|
161
|
+
const run = spawnSync(parts[0], [...parts.slice(1), ...args], {
|
|
162
|
+
cwd,
|
|
163
|
+
encoding: 'utf8',
|
|
164
|
+
env: getEnvVars(),
|
|
165
|
+
});
|
|
166
|
+
return { status: run.status ?? 1, stdout: run.stdout ?? '' };
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* `probe` — classify the wrapup as clean (pure application-code change, no doc-owned
|
|
172
|
+
* surface touched) or dirty, and project the wrapup mode. Fail safe: every lookup or
|
|
173
|
+
* parse problem is a dirty probe, never a silent clean.
|
|
174
|
+
*/
|
|
175
|
+
export function runDriftProbe(
|
|
176
|
+
env: DriftProbeEnv,
|
|
177
|
+
options: DriftProbeOptions = {},
|
|
178
|
+
spur = defaultSpurRunner(env, options.cwd),
|
|
179
|
+
): DriftProbeResult {
|
|
180
|
+
const cwd = options.cwd;
|
|
181
|
+
const runId = env.__runId ?? '';
|
|
182
|
+
if (runId.length === 0) {
|
|
183
|
+
process.stderr.write('wrapup-drift-probe: __runId is empty — refusing the legacy fixed-path fallback\n');
|
|
184
|
+
return { clean: false, reasons: [], paths: [], probeFile: '', modeFile: '', exitCode: 1 };
|
|
185
|
+
}
|
|
186
|
+
const relProbeFile = join('.spur', 'run', `${runId}-drift-probe.json`);
|
|
187
|
+
const relModeFile = join('.spur', 'run', `${runId}-mode.txt`);
|
|
188
|
+
const relTasksFile = join('.spur', 'run', `${runId}-wrapup-tasks.json`);
|
|
189
|
+
const abs = (p: string): string => (cwd ? join(cwd, p) : p);
|
|
190
|
+
mkdirSync(abs(join('.spur', 'run')), { recursive: true });
|
|
191
|
+
// Write the dirty projection first so a crash mid-probe fails safe (mode stays empty).
|
|
192
|
+
writeFileSync(abs(relModeFile), '\n');
|
|
193
|
+
|
|
194
|
+
const finish = (clean: boolean, reasons: string[], paths: string[]): DriftProbeResult => {
|
|
195
|
+
writeFileSync(abs(relProbeFile), `${JSON.stringify({ clean, reasons, paths })}\n`);
|
|
196
|
+
writeFileSync(abs(relModeFile), clean ? 'fast\n' : '\n');
|
|
197
|
+
return { clean, reasons, paths, probeFile: relProbeFile, modeFile: relModeFile, exitCode: 0 };
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
let tasks: unknown;
|
|
201
|
+
try {
|
|
202
|
+
tasks = JSON.parse(readFileSync(abs(relTasksFile), 'utf8'));
|
|
203
|
+
} catch {
|
|
204
|
+
tasks = undefined;
|
|
205
|
+
}
|
|
206
|
+
if (!Array.isArray(tasks) || !tasks.every((wbs) => typeof wbs === 'string')) {
|
|
207
|
+
process.stderr.write(
|
|
208
|
+
'wrapup-drift-probe: normalized task capture is missing or corrupted — failing safe (dirty)\n',
|
|
209
|
+
);
|
|
210
|
+
return finish(false, ['normalized task capture missing or corrupted'], []);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const reasons: string[] = [];
|
|
214
|
+
const allPaths = new Set<string>();
|
|
215
|
+
for (const wbs of tasks as string[]) {
|
|
216
|
+
const shown = spur(['task', 'show', wbs, '--json']);
|
|
217
|
+
if (shown.status !== 0) {
|
|
218
|
+
reasons.push(`${wbs}: task show failed (status=${shown.status})`);
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
let content: unknown;
|
|
222
|
+
try {
|
|
223
|
+
content = JSON.parse(shown.stdout).content;
|
|
224
|
+
} catch {
|
|
225
|
+
content = undefined;
|
|
226
|
+
}
|
|
227
|
+
if (typeof content !== 'string') {
|
|
228
|
+
reasons.push(`${wbs}: task show output unparseable`);
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
const changedPaths = changedPathsOf(solutionSectionOf(content));
|
|
232
|
+
if (changedPaths.length === 0) {
|
|
233
|
+
reasons.push(`${wbs}: Solution empty or unparseable`);
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
for (const path of changedPaths) {
|
|
237
|
+
if (CORPUS_PREFIXES.some((prefix) => path.startsWith(prefix))) continue;
|
|
238
|
+
allPaths.add(path);
|
|
239
|
+
const drift = driftReasonForPath(path);
|
|
240
|
+
if (drift) reasons.push(`${wbs}: ${path} ${drift}`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return finish(reasons.length === 0, reasons, [...allPaths].sort());
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export const WRAPUP_DRIFT_PROBE_USAGE = 'usage: wrapup-drift-probe (env: __runId, spurBin)';
|
|
247
|
+
|
|
248
|
+
export function main(argv: string[], env: DriftProbeEnv = getEnvVars(), options: DriftProbeOptions = {}): number {
|
|
249
|
+
if (argv.length > 0) {
|
|
250
|
+
process.stderr.write(`${WRAPUP_DRIFT_PROBE_USAGE}\n`);
|
|
251
|
+
return 2;
|
|
252
|
+
}
|
|
253
|
+
return runDriftProbe(env, options).exitCode;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (import.meta.main) {
|
|
257
|
+
process.exit(main(process.argv.slice(2)));
|
|
258
|
+
}
|
|
@@ -116,6 +116,62 @@ function resolveTasks(env, options = {}) {
|
|
|
116
116
|
`);
|
|
117
117
|
return { status: "PASS", statusFile: relStatusFile, tasksFile: relTasksFile, exitCode: 0 };
|
|
118
118
|
}
|
|
119
|
+
var ROUTE_REASON_TABLE = {
|
|
120
|
+
fast: "fast:evidence complete+consistent",
|
|
121
|
+
"": "safety:missing evidence (mode empty)",
|
|
122
|
+
unknown: "safety:unknown evidence quality",
|
|
123
|
+
conflict: "safety:conflicting evidence",
|
|
124
|
+
safety: "safety:operator-forced doc-sync"
|
|
125
|
+
};
|
|
126
|
+
function writeRouteReason(env, options = {}) {
|
|
127
|
+
const cwd = options.cwd;
|
|
128
|
+
const runId = env.__runId ?? "";
|
|
129
|
+
if (runId.length === 0) {
|
|
130
|
+
process.stderr.write(`task-resolve: __runId is empty \u2014 refusing to write a route reason
|
|
131
|
+
`);
|
|
132
|
+
return { reason: "", reasonFile: "", exitCode: 1 };
|
|
133
|
+
}
|
|
134
|
+
mkdirSync(cwd ? join(cwd, ".spur", "run") : join(".spur", "run"), { recursive: true });
|
|
135
|
+
mkdirSync(cwd ? join(cwd, ".spur", "memory") : join(".spur", "memory"), { recursive: true });
|
|
136
|
+
const relReasonFile = join(".spur", "run", `${runId}-route-reason.txt`);
|
|
137
|
+
const abs = (p) => cwd ? join(cwd, p) : p;
|
|
138
|
+
const statusFile = join(".spur", "run", `${runId}-wrapup-resolve.status`);
|
|
139
|
+
if (readFileSyncSafe(abs(statusFile))?.trim() === "FAIL") {
|
|
140
|
+
return { reason: "", reasonFile: relReasonFile, exitCode: 0 };
|
|
141
|
+
}
|
|
142
|
+
let taskCount = -1;
|
|
143
|
+
try {
|
|
144
|
+
const parsed = JSON.parse(readFileSync(abs(join(".spur", "run", `${runId}-wrapup-tasks.json`)), "utf8"));
|
|
145
|
+
if (Array.isArray(parsed))
|
|
146
|
+
taskCount = parsed.length;
|
|
147
|
+
} catch {}
|
|
148
|
+
let probeClean = false;
|
|
149
|
+
try {
|
|
150
|
+
const probe = JSON.parse(readFileSync(abs(join(".spur", "run", `${runId}-drift-probe.json`)), "utf8"));
|
|
151
|
+
probeClean = probe?.clean === true;
|
|
152
|
+
} catch {}
|
|
153
|
+
const mode = env.mode ?? "";
|
|
154
|
+
let reason;
|
|
155
|
+
if (taskCount === 0) {
|
|
156
|
+
reason = "skipped:empty task list";
|
|
157
|
+
} else if (mode === "fast" && probeClean) {
|
|
158
|
+
reason = "fast:drift-probe-clean";
|
|
159
|
+
} else {
|
|
160
|
+
reason = ROUTE_REASON_TABLE[mode] ?? `safety:unrecognized evidence (mode=${mode})`;
|
|
161
|
+
}
|
|
162
|
+
writeFileSync(abs(relReasonFile), `${reason}
|
|
163
|
+
`);
|
|
164
|
+
appendFileSync(abs(join(".spur", "memory", "wrapup-routes.log")), `${runId} ${reason}
|
|
165
|
+
`);
|
|
166
|
+
return { reason, reasonFile: relReasonFile, exitCode: 0 };
|
|
167
|
+
}
|
|
168
|
+
function readFileSyncSafe(path) {
|
|
169
|
+
try {
|
|
170
|
+
return readFileSync(path, "utf8");
|
|
171
|
+
} catch {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
119
175
|
function runMetrics(env, options = {}) {
|
|
120
176
|
const cwd = options.cwd;
|
|
121
177
|
const runId = env.__runId ?? "";
|
|
@@ -325,11 +381,13 @@ function runFeatureTransition(env, options = {}) {
|
|
|
325
381
|
`);
|
|
326
382
|
return { status: syncStatus, statusFile: relStatusFile, exitCode: 0 };
|
|
327
383
|
}
|
|
328
|
-
var WRAPUP_STEPS_USAGE = "usage: wrapup-steps.ts <resolve|metrics|feature-transition> (env: __runId, tasks, feature, featureGateCmd, spurBin)";
|
|
384
|
+
var WRAPUP_STEPS_USAGE = "usage: wrapup-steps.ts <resolve|route-reason|metrics|feature-transition> (env: __runId, tasks, mode, feature, featureGateCmd, spurBin)";
|
|
329
385
|
function main(argv, env = getEnvVars(), options = {}) {
|
|
330
386
|
const sub = argv[0];
|
|
331
387
|
if (sub === "resolve")
|
|
332
388
|
return resolveTasks(env, options).exitCode;
|
|
389
|
+
if (sub === "route-reason")
|
|
390
|
+
return writeRouteReason(env, options).exitCode;
|
|
333
391
|
if (sub === "metrics") {
|
|
334
392
|
runMetrics(env, options);
|
|
335
393
|
return 0;
|
|
@@ -344,6 +402,7 @@ function main(argv, env = getEnvVars(), options = {}) {
|
|
|
344
402
|
process.exit(main(process.argv.slice(2)));
|
|
345
403
|
}
|
|
346
404
|
export {
|
|
405
|
+
writeRouteReason,
|
|
347
406
|
taskStatusOf,
|
|
348
407
|
spurCommand,
|
|
349
408
|
runMetrics,
|
|
@@ -3,12 +3,13 @@
|
|
|
3
3
|
* wrapup-steps — deterministic wrap-up capture, metrics and feature sync behind the
|
|
4
4
|
* wrapup-pipeline wrappers (task 0824, feature I21, governance §1.2 composition budgets).
|
|
5
5
|
*
|
|
6
|
-
* Reproduces the former wrapup-pipeline `task-resolve:onEnter:0`, `
|
|
7
|
-
*
|
|
6
|
+
* Reproduces the former wrapup-pipeline `task-resolve:onEnter:0`, `task-resolve:onEnter:1`
|
|
7
|
+
* (route-reason writer, moved here in 0944), `metrics-record:onEnter:0` and
|
|
8
|
+
* `feature-transition:onEnter:0` shell programs one-for-one so the workflow stays inside
|
|
8
9
|
* the shell-program caps while writing the same `.spur/run` artifacts:
|
|
9
10
|
* - `<runId>-wrapup-tasks.json` normalized, deduplicated WBS capture (resolve)
|
|
10
11
|
* - `<runId>-wrapup-resolve.status` `PASS`/`FAIL` (resolve)
|
|
11
|
-
* - `<runId>-route-reason.txt` route reason (
|
|
12
|
+
* - `<runId>-route-reason.txt` route reason (route-reason subcommand)
|
|
12
13
|
* - `.spur/memory/wrapup-metrics.jsonl` one row per task (metrics)
|
|
13
14
|
* - `<runId>-wrapup-metrics.status` `PASS`/`FAIL` (metrics)
|
|
14
15
|
* - `<runId>-wrapup-sync.status` `PASS`/`FAIL` (feature-transition)
|
|
@@ -185,6 +186,89 @@ export function resolveTasks(env: WrapupStepsEnv, options: WrapupStepsOptions =
|
|
|
185
186
|
return { status: 'PASS', statusFile: relStatusFile, tasksFile: relTasksFile, exitCode: 0 };
|
|
186
187
|
}
|
|
187
188
|
|
|
189
|
+
/**
|
|
190
|
+
* 0944 R3 route-reason map — mirrors the former inline jq object one-for-one, plus the
|
|
191
|
+
* `safety` entry (operator-forced doc-sync). `fast:drift-probe-clean` is NOT in the map:
|
|
192
|
+
* it is only claimable when the drift probe itself classified the wrapup clean.
|
|
193
|
+
*/
|
|
194
|
+
const ROUTE_REASON_TABLE: Record<string, string> = {
|
|
195
|
+
fast: 'fast:evidence complete+consistent',
|
|
196
|
+
'': 'safety:missing evidence (mode empty)',
|
|
197
|
+
unknown: 'safety:unknown evidence quality',
|
|
198
|
+
conflict: 'safety:conflicting evidence',
|
|
199
|
+
safety: 'safety:operator-forced doc-sync',
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
export interface RouteReasonResult {
|
|
203
|
+
reason: string;
|
|
204
|
+
reasonFile: string;
|
|
205
|
+
exitCode: number;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* `route-reason` — derive the task-resolve route reason from the validated capture, the
|
|
210
|
+
* projected mode and the drift probe verdict (0944). A resolve FAIL keeps its own reason
|
|
211
|
+
* (nothing is written, exit 0). A missing or corrupted capture never yields a `skipped`
|
|
212
|
+
* or `fast:drift-probe-clean` claim. The log line is run-attributed (0770).
|
|
213
|
+
*/
|
|
214
|
+
export function writeRouteReason(env: WrapupStepsEnv, options: WrapupStepsOptions = {}): RouteReasonResult {
|
|
215
|
+
const cwd = options.cwd;
|
|
216
|
+
const runId = env.__runId ?? '';
|
|
217
|
+
if (runId.length === 0) {
|
|
218
|
+
process.stderr.write('task-resolve: __runId is empty — refusing to write a route reason\n');
|
|
219
|
+
return { reason: '', reasonFile: '', exitCode: 1 };
|
|
220
|
+
}
|
|
221
|
+
mkdirSync(cwd ? join(cwd, '.spur', 'run') : join('.spur', 'run'), { recursive: true });
|
|
222
|
+
mkdirSync(cwd ? join(cwd, '.spur', 'memory') : join('.spur', 'memory'), { recursive: true });
|
|
223
|
+
const relReasonFile = join('.spur', 'run', `${runId}-route-reason.txt`);
|
|
224
|
+
const abs = (p: string): string => (cwd ? join(cwd, p) : p);
|
|
225
|
+
|
|
226
|
+
const statusFile = join('.spur', 'run', `${runId}-wrapup-resolve.status`);
|
|
227
|
+
if (readFileSyncSafe(abs(statusFile))?.trim() === 'FAIL') {
|
|
228
|
+
// The failed reason stands; the failed edge already owns the run.
|
|
229
|
+
return { reason: '', reasonFile: relReasonFile, exitCode: 0 };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// A missing/corrupted capture stays at -1: never 0, so no skipped claim can be invented.
|
|
233
|
+
let taskCount = -1;
|
|
234
|
+
try {
|
|
235
|
+
const parsed: unknown = JSON.parse(
|
|
236
|
+
readFileSync(abs(join('.spur', 'run', `${runId}-wrapup-tasks.json`)), 'utf8'),
|
|
237
|
+
);
|
|
238
|
+
if (Array.isArray(parsed)) taskCount = parsed.length;
|
|
239
|
+
} catch {
|
|
240
|
+
// treated as uncountable below
|
|
241
|
+
}
|
|
242
|
+
let probeClean = false;
|
|
243
|
+
try {
|
|
244
|
+
const probe: unknown = JSON.parse(readFileSync(abs(join('.spur', 'run', `${runId}-drift-probe.json`)), 'utf8'));
|
|
245
|
+
probeClean = (probe as { clean?: unknown } | null)?.clean === true;
|
|
246
|
+
} catch {
|
|
247
|
+
// no probe verdict — the map decides
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const mode = env.mode ?? '';
|
|
251
|
+
let reason: string;
|
|
252
|
+
if (taskCount === 0) {
|
|
253
|
+
reason = 'skipped:empty task list';
|
|
254
|
+
} else if (mode === 'fast' && probeClean) {
|
|
255
|
+
reason = 'fast:drift-probe-clean';
|
|
256
|
+
} else {
|
|
257
|
+
reason = ROUTE_REASON_TABLE[mode] ?? `safety:unrecognized evidence (mode=${mode})`;
|
|
258
|
+
}
|
|
259
|
+
writeFileSync(abs(relReasonFile), `${reason}\n`);
|
|
260
|
+
appendFileSync(abs(join('.spur', 'memory', 'wrapup-routes.log')), `${runId} ${reason}\n`);
|
|
261
|
+
return { reason, reasonFile: relReasonFile, exitCode: 0 };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function readFileSyncSafe(path: string): string | null {
|
|
265
|
+
try {
|
|
266
|
+
return readFileSync(path, 'utf8');
|
|
267
|
+
} catch {
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
188
272
|
export interface MetricsResult {
|
|
189
273
|
status: 'PASS' | 'FAIL';
|
|
190
274
|
statusFile: string;
|
|
@@ -448,11 +532,12 @@ export function runFeatureTransition(env: WrapupStepsEnv, options: WrapupStepsOp
|
|
|
448
532
|
}
|
|
449
533
|
|
|
450
534
|
export const WRAPUP_STEPS_USAGE =
|
|
451
|
-
'usage: wrapup-steps.ts <resolve|metrics|feature-transition> (env: __runId, tasks, feature, featureGateCmd, spurBin)';
|
|
535
|
+
'usage: wrapup-steps.ts <resolve|route-reason|metrics|feature-transition> (env: __runId, tasks, mode, feature, featureGateCmd, spurBin)';
|
|
452
536
|
|
|
453
537
|
export function main(argv: string[], env: WrapupStepsEnv = getEnvVars(), options: WrapupStepsOptions = {}): number {
|
|
454
538
|
const sub = argv[0];
|
|
455
539
|
if (sub === 'resolve') return resolveTasks(env, options).exitCode;
|
|
540
|
+
if (sub === 'route-reason') return writeRouteReason(env, options).exitCode;
|
|
456
541
|
if (sub === 'metrics') {
|
|
457
542
|
runMetrics(env, options);
|
|
458
543
|
return 0;
|
|
@@ -137,6 +137,8 @@ template, source-citation format) is needed only inside each phase, not at the p
|
|
|
137
137
|
2. IDEATE → Generate 2-3 approaches with trade-offs (delegate research inline; escalate via spur agent run on a trigger)
|
|
138
138
|
3. OUTPUT → Structured markdown (Overview → Approaches → Recommendations → Next Steps),
|
|
139
139
|
delivered incrementally; saved to docs/plans/YYYY-MM-DD-<topic>-brainstorm.md
|
|
140
|
+
with the spur-dev plan frontmatter (kind: plan, tags: [brainstorm, …]) plus
|
|
141
|
+
needs_design/run_id; keeps its own sections and the required ## Design Summary
|
|
140
142
|
```
|
|
141
143
|
|
|
142
144
|
## Design Approval Gate
|
|
@@ -158,10 +158,25 @@ DO NOT implement research directly. Delegate to specialized skills:
|
|
|
158
158
|
|
|
159
159
|
### Output Template
|
|
160
160
|
|
|
161
|
+
Frontmatter follows the spur-dev
|
|
162
|
+
[document-authoring vocabulary](../../spur-dev/references/document-authoring.md#frontmatter-vocabulary);
|
|
163
|
+
`needs_design` and `run_id` are brainstorm keys kept beside it. The body keeps this brainstorm shape,
|
|
164
|
+
not the plan template's sections.
|
|
165
|
+
|
|
161
166
|
```markdown
|
|
162
|
-
|
|
167
|
+
---
|
|
168
|
+
kind: plan
|
|
169
|
+
title: "Brainstorm: [Topic]"
|
|
170
|
+
status: proposed
|
|
171
|
+
created_at: YYYY-MM-DD
|
|
172
|
+
updated_at: YYYY-MM-DD
|
|
173
|
+
related: [] # owning feature/task ids when known
|
|
174
|
+
tags: [brainstorm] # then feature ids, then ≤2 area tags
|
|
175
|
+
needs_design: true
|
|
176
|
+
run_id: <run id, omit when none>
|
|
177
|
+
---
|
|
163
178
|
|
|
164
|
-
|
|
179
|
+
# Brainstorm: [Topic]
|
|
165
180
|
|
|
166
181
|
## Overview
|
|
167
182
|
|
|
@@ -81,6 +81,7 @@ git push
|
|
|
81
81
|
```bash
|
|
82
82
|
git branch -d feature/<slug> # Delete merged branch
|
|
83
83
|
git worktree remove ../<project>-feature-<slug> # Remove worktree if used
|
|
84
|
+
spur projects remove ../<project>-feature-<slug> 2>/dev/null || true # Deregister from projects.json if registered
|
|
84
85
|
git worktree prune # Clean up stale worktree references
|
|
85
86
|
```
|
|
86
87
|
|
|
@@ -46,6 +46,7 @@ Shows all worktrees with their branches and paths.
|
|
|
46
46
|
|
|
47
47
|
```bash
|
|
48
48
|
git worktree remove ../project-hotfix
|
|
49
|
+
spur projects remove ../project-hotfix 2>/dev/null || true
|
|
49
50
|
```
|
|
50
51
|
|
|
51
52
|
### Prune (clean up stale references)
|
|
@@ -83,6 +84,7 @@ After removing a worktree, run `git worktree prune` and `git gc` to reclaim spac
|
|
|
83
84
|
|
|
84
85
|
```bash
|
|
85
86
|
git worktree remove ../old-worktree
|
|
87
|
+
spur projects remove ../old-worktree 2>/dev/null || true
|
|
86
88
|
git worktree prune
|
|
87
89
|
git gc --aggressive
|
|
88
90
|
```
|
|
@@ -52,6 +52,23 @@ When this skill is entered via `/sp:dev-run --mode implement <wbs>` (the form
|
|
|
52
52
|
The structural guard is the slash form itself (`--mode implement`). Prose in the workflow YAML
|
|
53
53
|
`agent.run` `input` is the wrong place for this rule; it belongs here and in `dev-run.md`.
|
|
54
54
|
|
|
55
|
+
## Escalation contract (task 0933, implement step only)
|
|
56
|
+
|
|
57
|
+
When the pipeline runs implement headless, you have a bounded channel to surface a question
|
|
58
|
+
instead of guessing. The rules, in decision order:
|
|
59
|
+
|
|
60
|
+
- **Decide without asking** when the answer follows from the task's frozen Design /
|
|
61
|
+
Requirements / Q&A, the project and global instructions, or the codebase itself. Ambiguity
|
|
62
|
+
you can resolve from evidence is not a question.
|
|
63
|
+
- **Escalate only when a requirement or design ambiguity would change scope, correctness, or
|
|
64
|
+
authorization.** Style preferences, implementation details, and curiosity are not escalations.
|
|
65
|
+
- **To escalate:** write ONE concise question — with options and a recommendation — to the
|
|
66
|
+
escalation file, then exit 0 **without further edits**. The pipeline pauses the run and an
|
|
67
|
+
operator answers; the transcript of prior Q/A pairs lives at the `--escalation-file` path
|
|
68
|
+
(the ask loop is bounded, default 2 — a third pause fails the task).
|
|
69
|
+
- **Read `--escalation-file` if it exists and treat its answers as binding.** They are the
|
|
70
|
+
operator's recorded decisions for this pass, not suggestions.
|
|
71
|
+
|
|
55
72
|
## One WBS per implement pass (task 0487 R1)
|
|
56
73
|
|
|
57
74
|
The target WBS is the **only** task you implement. Sibling tasks in the corpus are context you do
|
|
@@ -259,13 +259,34 @@ the deterministic Testing writer `spur task record` (section authorship never ha
|
|
|
259
259
|
|
|
260
260
|
```bash
|
|
261
261
|
# write .spur/run/<wbs>-verdict.json (shape in references/verdict-schema.md), then:
|
|
262
|
+
# F96 residual sweep (observe-only): scan + fold BEFORE record, under every --fix mode.
|
|
263
|
+
RESIDUAL=$(superskill script path sp residual-scan.mjs)
|
|
264
|
+
node "$RESIDUAL" scan <wbs>
|
|
265
|
+
node "$RESIDUAL" fold <wbs>
|
|
262
266
|
spur task record <wbs> --verdict-file .spur/run/<wbs>-verdict.json # renders ## Testing
|
|
263
267
|
```
|
|
264
268
|
|
|
269
|
+
> **Residual fold (F96).** `scan` reads `.spur/run/` artifacts and writes `residuals.json` +
|
|
270
|
+
> `residual-report.md`; `fold` rewrites the **just-written** verdict artifact's `residual-sweep`
|
|
271
|
+
> check in place (PASS → PARTIAL when a blocking residual exists), so `record` transcribes the
|
|
272
|
+
> downgraded verdict — a manual verify cannot certify what the pipeline would reject. Resolve the
|
|
273
|
+
> script via `superskill script path sp residual-scan.mjs`; shipped surfaces never reference
|
|
274
|
+
> `plugins/sp/scripts/` directly (script-contract-check rule 4). When the task reaches `done`
|
|
275
|
+
> through `--next`, run `residual-scan settle` (links follow-up tasks; best-effort).
|
|
276
|
+
|
|
265
277
|
> **Corrections: the answer file is the source of truth.** `spur task record` re-transcribes
|
|
266
278
|
> `## Testing` from the verdict artifact — direct `--section Testing` writes are futile. Fix
|
|
267
279
|
> `.spur/run/<wbs>-verify-answer.txt` → `spur task verdict <wbs> --from-answer <file>` → re-record.
|
|
268
280
|
|
|
281
|
+
> **Scenario-key carry-forward (standalone `--force` re-verifies).** When you author a fresh
|
|
282
|
+
> verdict artifact for a task whose `## Testing` already carries feature scenario-title rows
|
|
283
|
+
> (`Scenario: <title>` / `AC-N` keys with MET status), **copy those rows into the new artifact**
|
|
284
|
+
> keyed the same way — record re-transcribes `## Testing` wholesale, so a fresh artifact keyed by
|
|
285
|
+
> bare `R1`-style ids drops the scenario keys `spur feature check` needs for satisfaction
|
|
286
|
+
> (`L4.scenario-unverified` regresses; 0921/D63). Since 0936, `record` warns on stderr (exit 0)
|
|
287
|
+
> for each dropped MET-matched scenario key and when the new rows match no feature scenario —
|
|
288
|
+
> treat those warnings as a re-key instruction, not noise.
|
|
289
|
+
|
|
269
290
|
> **Do not write `## Review` directly, ever.** The `## Review` section is owned by the
|
|
270
291
|
> `review` coordinator (`/sp:dev-review` → `sp:super-reviewer`), which merges
|
|
271
292
|
> `functional-review` + `code-verification` review mode + `code-improvement` fragments. The
|
|
@@ -82,8 +82,9 @@ verdict artifact so quality failures are not lost behind a requirements-only PAS
|
|
|
82
82
|
|
|
83
83
|
Before declaring a task `done`, run this lightweight checklist. It catches the most common oversights that survive the formal pipeline gates:
|
|
84
84
|
|
|
85
|
-
- [ ]
|
|
86
|
-
|
|
85
|
+
- [ ] Check receipt `.spur/run/<wbs>-check-receipt.json` reports `reuse: true` for the current
|
|
86
|
+
digest via `quality-gate.ts status`; run `bun run spur-check` only when it reports stale or
|
|
87
|
+
missing (0940).
|
|
87
88
|
- [ ] No `TODO` or `FIXME` without a linked task WBS.
|
|
88
89
|
- [ ] `git status` shows only intentional changes (no debug artifacts, no temp files).
|
|
89
90
|
- [ ] No `console.log` / `console.error` in production code (use the project logger).
|
|
@@ -136,6 +136,7 @@ Wave C verification can emit the following additive `checks[]` rows:
|
|
|
136
136
|
| `evidence-rule-pass` | All behavior-bearing AC rows had executable evidence or were explicitly non-behavioral. |
|
|
137
137
|
| `evidence-rule-failed` | One or more MET behavior-bearing AC rows lacked `test` / `command` evidence and were downgraded to PARTIAL. |
|
|
138
138
|
| `cli-golden-path-present` | CLI-surface tasks supplied, or failed to supply, one golden-path command evidence row. |
|
|
139
|
+
| `residual-sweep` | Post-verdict residual scan (F96): `fail` when blocking leftovers exist (P1–P3 findings, added diff markers, unchecked boxes); evidence lists blocking/deferrable/advisory/housekeeping counts plus blocking and deferrable item ids. A `fail` downgrades an otherwise-PASS verdict to PARTIAL via the fold step. |
|
|
139
140
|
|
|
140
141
|
## How the gate reads it
|
|
141
142
|
|
|
@@ -194,8 +194,9 @@ On **every** step resolve:
|
|
|
194
194
|
|
|
195
195
|
The final report MUST include a `### 3. Monitor Ledger` section containing those rows (cardinality:
|
|
196
196
|
row count == the declared executed steps). Cardinality, full methodology, column contract,
|
|
197
|
-
token/
|
|
198
|
-
|
|
197
|
+
token/context-reuse estimation, multi-source Cost honesty, the evidence-based reuse and
|
|
198
|
+
pipeline-provenance observation rules (supported 0912 baseline findings, owner handoffs,
|
|
199
|
+
INSUFFICIENT_EVIDENCE limits — task 0913), and the **cache-conservation discipline** live in
|
|
199
200
|
**[monitor-ledger.md](references/monitor-ledger.md)** — apply conservation while monitoring; low
|
|
200
201
|
cache% is usually the driver re-fetching data it already holds.
|
|
201
202
|
|
|
@@ -532,7 +533,8 @@ Do NOT:
|
|
|
532
533
|
- [references/report-template.md](references/report-template.md) — report section contract,
|
|
533
534
|
mandatory footer, task-sink L3 rule.
|
|
534
535
|
- [references/monitor-ledger.md](references/monitor-ledger.md) — live-ledger column contract,
|
|
535
|
-
token/
|
|
536
|
+
token/context-reuse estimation, evidence-based reuse observation rule, pipeline-run provenance
|
|
537
|
+
observation rules.
|
|
536
538
|
|
|
537
539
|
## Platform Notes
|
|
538
540
|
|