@gobing-ai/spur 0.3.72 → 0.3.74
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/config/config.example.yaml +48 -27
- package/config/pipeline-budgets.json +9 -5
- package/config/plugin-scripts.json +4 -0
- package/config/proportional-route-table.ts +155 -0
- package/config/rules/strict/runtime-boundaries.yaml +1 -1
- package/config/rules/structure/protected-files.yaml +3 -0
- package/config/task-pipeline-proportional-migration-plan.md +79 -0
- package/config/templates/AGENTS.md +5 -0
- package/config/templates/docs/99_PROJECT_CONSTITUTION.md +2 -0
- package/config/workflows/basic.yaml +5 -2
- package/config/workflows/docs-pipeline.yaml +101 -54
- package/config/workflows/feature-dev.yaml +57 -27
- package/config/workflows/feature-lifecycle.yaml +1 -0
- package/config/workflows/history-anatomy.yaml +8 -4
- package/config/workflows/idea-pipeline.yaml +66 -33
- package/config/workflows/pr-review.yaml +31 -8
- package/config/workflows/task-lifecycle.yaml +28 -15
- package/config/workflows/task-pipeline.yaml +929 -844
- package/config/workflows/wayfinder-resolution.yaml +124 -90
- package/config/workflows/wrapup-pipeline.yaml +290 -107
- package/package.json +9 -9
- package/plugins/sp/plugin.json +1 -1
- package/plugins/sp/scripts/daily-summary/daily-summary.mjs +2 -4
- package/plugins/sp/scripts/daily-summary/daily-summary.ts +2 -4
- package/plugins/sp/scripts/history-anatomy-cache.mjs +7 -1
- package/plugins/sp/scripts/history-anatomy-cache.ts +11 -2
- package/plugins/sp/scripts/inline-pipeline-parity-check.ts +185 -0
- package/plugins/sp/scripts/pr-reviewing.mjs +5 -1
- package/plugins/sp/scripts/pr-reviewing.ts +7 -1
- package/plugins/sp/scripts/task-evidence-precheck.ts +1 -1
- package/plugins/sp/scripts/transition-shim-check.ts +6 -6
- package/plugins/sp/scripts/verify-answer-lint.ts +4 -0
- package/plugins/sp/skills/pr-reviewing/SKILL.md +4 -1
- package/plugins/sp/skills/spur-cli/references/self.md +17 -3
- package/plugins/sp/skills/spur-cli/references/tasks.md +1 -1
- package/plugins/sp/skills/spur-cli/references/workflows/authoring-workflows.md +23 -0
- package/plugins/sp/skills/spur-cli/references/workflows/workflow-fit-and-tuning.md +5 -5
- package/plugins/sp/skills/spur-cli/references/workflows.md +4 -4
- package/plugins/sp/skills/spur-dev/SKILL.md +17 -0
- package/plugins/sp/skills/spur-dev/references/execution-batch.md +3 -3
- package/plugins/sp/skills/spur-dev/references/gate-checklists.md +1 -2
- package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +20 -0
- package/plugins/sp/skills/wayfinder/SKILL.md +1 -1
- package/schemas/state-machine-workflow.schema.json +3 -1
- package/schemas/transition-flow-workflow.schema.json +3 -1
- package/spur.js +12639 -10106
- package/web/_astro/{BoardApp.BYCNkMOn.js → BoardApp.C60RJZRj.js} +78 -77
- package/web/_astro/BoardApp.ymfj1EHA.js +1 -0
- package/web/_astro/{TaskDetail.CgUreSP2.js → TaskDetail.Guk8VuNu.js} +1 -1
- package/web/_astro/{arc.BySSh34M.js → arc.CAZtlWJM.js} +1 -1
- package/web/_astro/{architectureDiagram-3BPJPVTR.DM46TS_h.js → architectureDiagram-3BPJPVTR.9-XbF_Tz.js} +1 -1
- package/web/_astro/{blockDiagram-GPEHLZMM.tZhvNUHA.js → blockDiagram-GPEHLZMM.DC4WLx3L.js} +1 -1
- package/web/_astro/{c4Diagram-AAUBKEIU.PT4Or4Nf.js → c4Diagram-AAUBKEIU.JgjUQMgG.js} +1 -1
- package/web/_astro/channel.Bxxe2Byc.js +1 -0
- package/web/_astro/{chunk-2J33WTMH.J9r0_Bbe.js → chunk-2J33WTMH.D_JFiXa-.js} +1 -1
- package/web/_astro/{chunk-4BX2VUAB.hzyeIvhR.js → chunk-4BX2VUAB.Bk_RxoeT.js} +1 -1
- package/web/_astro/{chunk-55IACEB6.B0rO7qVh.js → chunk-55IACEB6.a_Rj2VxL.js} +1 -1
- package/web/_astro/{chunk-727SXJPM.wE_Uk5D4.js → chunk-727SXJPM.C2NoS1U6.js} +1 -1
- package/web/_astro/{chunk-AQP2D5EJ.DqEEjQw7.js → chunk-AQP2D5EJ.D1Xn6CNa.js} +1 -1
- package/web/_astro/{chunk-FMBD7UC4.CDoD9sBX.js → chunk-FMBD7UC4.BhJrSBsX.js} +1 -1
- package/web/_astro/{chunk-ND2GUHAM.CtX5nF9P.js → chunk-ND2GUHAM.ByVGduYQ.js} +1 -1
- package/web/_astro/{chunk-QZHKN3VN.CK_EwfaT.js → chunk-QZHKN3VN.B47Paunq.js} +1 -1
- package/web/_astro/{classDiagram-4FO5ZUOK.DLt5a8Lh.js → classDiagram-4FO5ZUOK.BERMSD5C.js} +1 -1
- package/web/_astro/{classDiagram-v2-Q7XG4LA2.DLt5a8Lh.js → classDiagram-v2-Q7XG4LA2.BERMSD5C.js} +1 -1
- package/web/_astro/{cose-bilkent-S5V4N54A.CMCWP49h.js → cose-bilkent-S5V4N54A.O_rHGlhU.js} +1 -1
- package/web/_astro/{cynefin-OW5HDTMX.HyXw_vdS.js → cynefin-OW5HDTMX.qCW2GdNp.js} +1 -1
- package/web/_astro/{dagre-BM42HDAG.BTuAzh01.js → dagre-BM42HDAG.C5Y4lH_u.js} +1 -1
- package/web/_astro/{diagram-2AECGRRQ.D9dr9wfT.js → diagram-2AECGRRQ.DxZmRXxF.js} +1 -1
- package/web/_astro/{diagram-5GNKFQAL.C4Rot0hj.js → diagram-5GNKFQAL.qSxZeYSK.js} +1 -1
- package/web/_astro/{diagram-KO2AKTUF.B_TK5uWC.js → diagram-KO2AKTUF.-7vl3bXs.js} +1 -1
- package/web/_astro/{diagram-LMA3HP47.JkXKK7CO.js → diagram-LMA3HP47.BHVV-UH3.js} +1 -1
- package/web/_astro/{diagram-OG6HWLK6.BzMN8Bd6.js → diagram-OG6HWLK6.DHAuW9sK.js} +1 -1
- package/web/_astro/{erDiagram-TEJ5UH35.DVZaWGUd.js → erDiagram-TEJ5UH35.DMRLQwPN.js} +1 -1
- package/web/_astro/{flowDiagram-I6XJVG4X.rjEiWUfR.js → flowDiagram-I6XJVG4X.B3EpPp_8.js} +1 -1
- package/web/_astro/{ganttDiagram-6RSMTGT7.C_EgAarK.js → ganttDiagram-6RSMTGT7.BgUrExXM.js} +1 -1
- package/web/_astro/{gitGraphDiagram-PVQCEYII.B-QQSDsK.js → gitGraphDiagram-PVQCEYII.CU-9yVN4.js} +1 -1
- package/web/_astro/{index.B5MTfe7k.css → index.BhvM3djt.css} +1 -1
- package/web/_astro/{infoDiagram-5YYISTIA.DlWesz7T.js → infoDiagram-5YYISTIA.CvVTCRLe.js} +1 -1
- package/web/_astro/{ishikawaDiagram-YF4QCWOH.BUMZOawi.js → ishikawaDiagram-YF4QCWOH.BUzOfKPf.js} +1 -1
- package/web/_astro/{journeyDiagram-JHISSGLW.CWfkxfjY.js → journeyDiagram-JHISSGLW.BBTb7ziz.js} +1 -1
- package/web/_astro/{kanban-definition-UN3LZRKU.B-YpMwXf.js → kanban-definition-UN3LZRKU.CTQQr70i.js} +1 -1
- package/web/_astro/{linear.D7uqzENp.js → linear.CLKlJPiS.js} +1 -1
- package/web/_astro/{mermaid.core.CxrNppBD.js → mermaid.core.BA4wFhLP.js} +4 -4
- package/web/_astro/{mindmap-definition-RKZ34NQL.B4Qe7cM2.js → mindmap-definition-RKZ34NQL.t1vG2l5e.js} +1 -1
- package/web/_astro/{pieDiagram-4H26LBE5.Ds-5j2ro.js → pieDiagram-4H26LBE5.C8zJhyTu.js} +1 -1
- package/web/_astro/{quadrantDiagram-W4KKPZXB.tBd38uNC.js → quadrantDiagram-W4KKPZXB.DqYYvb7T.js} +1 -1
- package/web/_astro/{requirementDiagram-4Y6WPE33.sFENkWl3.js → requirementDiagram-4Y6WPE33.lzfDZ9nI.js} +1 -1
- package/web/_astro/{sankeyDiagram-5OEKKPKP.BeB-Hk7C.js → sankeyDiagram-5OEKKPKP.lm6N5ORM.js} +1 -1
- package/web/_astro/{sequenceDiagram-3UESZ5HK.DnTeaSpx.js → sequenceDiagram-3UESZ5HK.E6_IV4Dj.js} +1 -1
- package/web/_astro/{stateDiagram-AJRCARHV.B-8Jt5EJ.js → stateDiagram-AJRCARHV.Bnv6Ok2p.js} +1 -1
- package/web/_astro/{stateDiagram-v2-BHNVJYJU.Br7xoqMW.js → stateDiagram-v2-BHNVJYJU.mJHH_Ng_.js} +1 -1
- package/web/_astro/{timeline-definition-PNZ67QCA.C-3WdOyi.js → timeline-definition-PNZ67QCA.BhRgqSLa.js} +1 -1
- package/web/_astro/{vennDiagram-CIIHVFJN.DCIs7Lc6.js → vennDiagram-CIIHVFJN.pjb5DMC9.js} +1 -1
- package/web/_astro/{wardleyDiagram-YWT4CUSO.rGAL-bbz.js → wardleyDiagram-YWT4CUSO.CW20KUng.js} +1 -1
- package/web/_astro/{xychartDiagram-2RQKCTM6.hkfQKiRl.js → xychartDiagram-2RQKCTM6.vX9_yuMl.js} +1 -1
- package/web/index.html +2 -2
- package/config/corpus-baseline.json +0 -1637
- package/config/workflow-composition-baseline.json +0 -576
- package/web/_astro/BoardApp.E12MFjOS.js +0 -1
- package/web/_astro/channel.5cYKr5cs.js +0 -1
|
@@ -369,16 +369,14 @@ export async function getSpurHistoryHealth(
|
|
|
369
369
|
const cleanTool = lp.toolName.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
370
370
|
const key = `repetition:${cleanTool}:${lp.argsDigest.slice(0, 16)}`;
|
|
371
371
|
const title = `Break execution loop in ${lp.toolName} (${lp.repeats} repeats)`;
|
|
372
|
-
const
|
|
373
|
-
const command = `spur task create --section "Fix ${cleanTool} repetition loop" --body "${body}"`;
|
|
372
|
+
const command = `spur task create "<title>" --feature <id> && spur task update <wbs> --section Plan --from-file <path>`;
|
|
374
373
|
remediationProposals.push({ key, title, command });
|
|
375
374
|
}
|
|
376
375
|
|
|
377
376
|
if (toolCalls > 0 && errorRatePct > 10) {
|
|
378
377
|
const key = 'reliability:tooling:high-error-rate';
|
|
379
378
|
const title = `Investigate high tool error rate (${errorRatePct.toFixed(1)}%)`;
|
|
380
|
-
const
|
|
381
|
-
const command = `spur task create --section "Investigate tool error rate spikes" --body "${body}"`;
|
|
379
|
+
const command = `spur task create "<title>" --feature <id> && spur task update <wbs> --section Plan --from-file <path>`;
|
|
382
380
|
remediationProposals.push({ key, title, command });
|
|
383
381
|
}
|
|
384
382
|
|
|
@@ -198,6 +198,7 @@ function parseProvenance(reportMarkdown) {
|
|
|
198
198
|
contractDigest: String(obj.contractDigest ?? ""),
|
|
199
199
|
skillDigest: String(obj.skillDigest ?? ""),
|
|
200
200
|
workflowDigest: String(obj.workflowDigest ?? ""),
|
|
201
|
+
helperDigest: String(obj.helperDigest ?? ""),
|
|
201
202
|
coverage: coverage.map((c) => ({
|
|
202
203
|
source: String(c.source ?? ""),
|
|
203
204
|
status: String(c.status ?? ""),
|
|
@@ -244,6 +245,8 @@ function decideCache(cached, current, opts) {
|
|
|
244
245
|
reasons.push("logic-changed:skill");
|
|
245
246
|
if (cached.workflowDigest !== current.workflowDigest)
|
|
246
247
|
reasons.push("logic-changed:workflow");
|
|
248
|
+
if (cached.helperDigest !== current.helperDigest)
|
|
249
|
+
reasons.push("logic-changed:helper");
|
|
247
250
|
const currentSources = new Set(current.coverage.map((c) => c.source));
|
|
248
251
|
if (cached.coverage.some((c) => !currentSources.has(c.source)))
|
|
249
252
|
reasons.push("coverage-degraded");
|
|
@@ -473,6 +476,7 @@ function buildProvenance(opts) {
|
|
|
473
476
|
contractDigest: logicDigest(opts.contractFile),
|
|
474
477
|
skillDigest: logicDigest(opts.skillDir),
|
|
475
478
|
workflowDigest: logicDigest(opts.workflowFile),
|
|
479
|
+
helperDigest: logicDigest(opts.helperFile),
|
|
476
480
|
coverage,
|
|
477
481
|
runId: opts.runId,
|
|
478
482
|
currentArtifactPath: opts.artifact,
|
|
@@ -503,6 +507,7 @@ var YAML_KEYS = [
|
|
|
503
507
|
"contractDigest",
|
|
504
508
|
"skillDigest",
|
|
505
509
|
"workflowDigest",
|
|
510
|
+
"helperDigest",
|
|
506
511
|
"runId",
|
|
507
512
|
"currentArtifactPath",
|
|
508
513
|
"baselineArtifactPath",
|
|
@@ -595,7 +600,7 @@ function diffPorcelain(before, now, expects) {
|
|
|
595
600
|
return [...porcelainPaths(now)].filter((p) => !beforePaths.has(p) && !expects.has(p)).sort();
|
|
596
601
|
}
|
|
597
602
|
var VALID_COMMANDS = "digest, check, paths, assert-clean, probe, stamp, refresh, publish";
|
|
598
|
-
var PROBE_USAGE = "<script> probe --artifact <a.json> --target <report.md> [--baseline <b.json>] [--mode daily|ad-hoc] " + "[--date <YYYY-MM-DD>] [--recompute true] [--out <prov.json>] [--skill-dir <d>] [--contract <f>] [--workflow <f>]";
|
|
603
|
+
var PROBE_USAGE = "<script> probe --artifact <a.json> --target <report.md> [--baseline <b.json>] [--mode daily|ad-hoc] " + "[--date <YYYY-MM-DD>] [--recompute true] [--out <prov.json>] [--skill-dir <d>] [--contract <f>] [--workflow <f>] [--helper <f>]";
|
|
599
604
|
function parseFlags(args) {
|
|
600
605
|
const out = {};
|
|
601
606
|
for (let i = 0;i < args.length; i++) {
|
|
@@ -729,6 +734,7 @@ ${result.problems.map((p) => `- ${p}
|
|
|
729
734
|
skillDir: f["skill-dir"],
|
|
730
735
|
contractFile: f.contract,
|
|
731
736
|
workflowFile: f.workflow,
|
|
737
|
+
helperFile: f.helper,
|
|
732
738
|
contractVersion: f["contract-version"],
|
|
733
739
|
runId: f["run-id"],
|
|
734
740
|
spurVersion: f["spur-version"]
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
*
|
|
5
5
|
* ADR-079 makes cache validity a *derived* fact, not a stored claim: a cached report is reusable
|
|
6
6
|
* only for its model-authored half, and only when a freshly derived semantic digest of the analyze
|
|
7
|
-
* artifact plus the contract/skill/workflow logic digests all match what the cache
|
|
7
|
+
* artifact plus the contract/skill/workflow/helper logic digests all match what the cache
|
|
8
|
+
* recorded (helper digest: task 0771 — the deterministic half is part of cache identity).
|
|
8
9
|
*
|
|
9
10
|
* This script performs deterministic file, hash, and schema work only — no finding, remediation,
|
|
10
11
|
* severity, or ranking logic (that is judgment, owned by the sp:history-anatomy skill). Jobs:
|
|
@@ -68,6 +69,8 @@ export interface CacheProvenance {
|
|
|
68
69
|
contractDigest: string;
|
|
69
70
|
skillDigest: string;
|
|
70
71
|
workflowDigest: string;
|
|
72
|
+
/** Digest of the executing helper twin itself (0771): a changed deterministic half invalidates. */
|
|
73
|
+
helperDigest: string;
|
|
71
74
|
coverage: Array<{ source: string; status: string; lastImportedAt: string | null }>;
|
|
72
75
|
// 0660 R7 audit fields. Recorded in the published frontmatter for provenance; deliberately
|
|
73
76
|
// NOT part of the invalidation matrix — a changed run id or executor is not stale evidence.
|
|
@@ -243,6 +246,7 @@ export function parseProvenance(reportMarkdown: string): CacheProvenance | null
|
|
|
243
246
|
contractDigest: String(obj.contractDigest ?? ''),
|
|
244
247
|
skillDigest: String(obj.skillDigest ?? ''),
|
|
245
248
|
workflowDigest: String(obj.workflowDigest ?? ''),
|
|
249
|
+
helperDigest: String(obj.helperDigest ?? ''),
|
|
246
250
|
coverage: coverage.map((c) => ({
|
|
247
251
|
source: String(c.source ?? ''),
|
|
248
252
|
status: String(c.status ?? ''),
|
|
@@ -300,6 +304,7 @@ export function decideCache(
|
|
|
300
304
|
if (cached.contractDigest !== current.contractDigest) reasons.push('logic-changed:contract');
|
|
301
305
|
if (cached.skillDigest !== current.skillDigest) reasons.push('logic-changed:skill');
|
|
302
306
|
if (cached.workflowDigest !== current.workflowDigest) reasons.push('logic-changed:workflow');
|
|
307
|
+
if (cached.helperDigest !== current.helperDigest) reasons.push('logic-changed:helper');
|
|
303
308
|
|
|
304
309
|
// Coverage cannot degrade: the cache must not claim broader coverage than the current
|
|
305
310
|
// analyze covers. If the cached report covered a source the current analyze no longer does,
|
|
@@ -601,6 +606,7 @@ export interface ProbeOptions {
|
|
|
601
606
|
skillDir?: string;
|
|
602
607
|
contractFile?: string;
|
|
603
608
|
workflowFile?: string;
|
|
609
|
+
helperFile?: string;
|
|
604
610
|
contractVersion?: string;
|
|
605
611
|
runId?: string;
|
|
606
612
|
spurVersion?: string;
|
|
@@ -660,6 +666,7 @@ export function buildProvenance(opts: ProbeOptions): CacheProvenance {
|
|
|
660
666
|
contractDigest: logicDigest(opts.contractFile),
|
|
661
667
|
skillDigest: logicDigest(opts.skillDir),
|
|
662
668
|
workflowDigest: logicDigest(opts.workflowFile),
|
|
669
|
+
helperDigest: logicDigest(opts.helperFile),
|
|
663
670
|
coverage,
|
|
664
671
|
runId: opts.runId,
|
|
665
672
|
currentArtifactPath: opts.artifact,
|
|
@@ -697,6 +704,7 @@ const YAML_KEYS: Array<keyof CacheProvenance> = [
|
|
|
697
704
|
'contractDigest',
|
|
698
705
|
'skillDigest',
|
|
699
706
|
'workflowDigest',
|
|
707
|
+
'helperDigest',
|
|
700
708
|
'runId',
|
|
701
709
|
'currentArtifactPath',
|
|
702
710
|
'baselineArtifactPath',
|
|
@@ -823,7 +831,7 @@ export function diffPorcelain(before: string, now: string, expects: Set<string>)
|
|
|
823
831
|
const VALID_COMMANDS = 'digest, check, paths, assert-clean, probe, stamp, refresh, publish';
|
|
824
832
|
const PROBE_USAGE =
|
|
825
833
|
'<script> probe --artifact <a.json> --target <report.md> [--baseline <b.json>] [--mode daily|ad-hoc] ' +
|
|
826
|
-
'[--date <YYYY-MM-DD>] [--recompute true] [--out <prov.json>] [--skill-dir <d>] [--contract <f>] [--workflow <f>]';
|
|
834
|
+
'[--date <YYYY-MM-DD>] [--recompute true] [--out <prov.json>] [--skill-dir <d>] [--contract <f>] [--workflow <f>] [--helper <f>]';
|
|
827
835
|
|
|
828
836
|
/** `--key value` / `--flag` → record. Bare flags become `"true"` so `--recompute` needs no value. */
|
|
829
837
|
function parseFlags(args: string[]): Record<string, string | undefined> {
|
|
@@ -956,6 +964,7 @@ export function runCacheCli(argv: string[]): CacheCliResult {
|
|
|
956
964
|
skillDir: f['skill-dir'],
|
|
957
965
|
contractFile: f.contract,
|
|
958
966
|
workflowFile: f.workflow,
|
|
967
|
+
helperFile: f.helper,
|
|
959
968
|
contractVersion: f['contract-version'],
|
|
960
969
|
runId: f['run-id'],
|
|
961
970
|
spurVersion: f['spur-version'],
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* inline-pipeline-parity-check — two-sided gate between the inline pipeline
|
|
4
|
+
* driver's documented action/guard set and the resolved action/guard sets in
|
|
5
|
+
* `.spur/workflows/task-pipeline.yaml` and `.spur/workflows/idea-pipeline.yaml`
|
|
6
|
+
* (task 0755 R2/R3).
|
|
7
|
+
*
|
|
8
|
+
* The driver reference at `plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md`
|
|
9
|
+
* documents the set of action and guard kinds it implements. The two runtime
|
|
10
|
+
* pipelines are the only consumers the driver needs to keep in step with. The
|
|
11
|
+
* check is a symmetric set diff: an element present in one and absent in the
|
|
12
|
+
* other fails the check and names the element.
|
|
13
|
+
*
|
|
14
|
+
* The set is defined in {@link DOCUMENTED} below; the driver's markdown list is
|
|
15
|
+
* the human mirror. Update both when the driver adds or drops a kind.
|
|
16
|
+
*
|
|
17
|
+
* Usage:
|
|
18
|
+
* bun plugins/sp/scripts/inline-pipeline-parity-check.ts
|
|
19
|
+
* [--root <path>] default: repo root
|
|
20
|
+
*
|
|
21
|
+
* Exit code: 0 when the sets agree; 1 on any divergence. Violations are printed
|
|
22
|
+
* to stderr; a summary to stdout.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { readdirSync, readFileSync, statSync } from 'node:fs';
|
|
26
|
+
import { join, resolve } from 'node:path';
|
|
27
|
+
import { parse as parseYaml } from 'yaml';
|
|
28
|
+
|
|
29
|
+
/** Documented action and guard set. Must stay in lockstep with the
|
|
30
|
+
* "Supported action and guard set (0755 R2 parity contract)" section in
|
|
31
|
+
* `plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md`. The
|
|
32
|
+
* driver supports a kind if ANY workflow in `.spur/workflows/*.yaml` uses
|
|
33
|
+
* it (the driver applies to any selected pipeline per its reference doc). */
|
|
34
|
+
const DOCUMENTED = {
|
|
35
|
+
actions: new Set([
|
|
36
|
+
'shell',
|
|
37
|
+
'note',
|
|
38
|
+
'doctor.probe',
|
|
39
|
+
'file.read.into-var',
|
|
40
|
+
'hitl.confirm',
|
|
41
|
+
'agent.run',
|
|
42
|
+
'proof.fingerprint',
|
|
43
|
+
'run.artifact',
|
|
44
|
+
'command.gate',
|
|
45
|
+
]),
|
|
46
|
+
guards: new Set(['always', 'shell']),
|
|
47
|
+
} as const;
|
|
48
|
+
|
|
49
|
+
/** Directory of workflow definitions the driver is responsible for. */
|
|
50
|
+
const WORKFLOW_DIR = join('config', 'workflows');
|
|
51
|
+
|
|
52
|
+
/** Walk a state list and yield every `kind:` value found in `onEnter` action
|
|
53
|
+
* lists. Skips the top-level workflow `kind:` (e.g. `state-machine`). */
|
|
54
|
+
function collectActionKinds(states: unknown): Set<string> {
|
|
55
|
+
const out = new Set<string>();
|
|
56
|
+
if (!Array.isArray(states)) return out;
|
|
57
|
+
for (const state of states) {
|
|
58
|
+
if (typeof state !== 'object' || state === null) continue;
|
|
59
|
+
const onEnter = (state as { onEnter?: unknown }).onEnter;
|
|
60
|
+
if (!Array.isArray(onEnter)) continue;
|
|
61
|
+
for (const action of onEnter) {
|
|
62
|
+
if (typeof action !== 'object' || action === null) continue;
|
|
63
|
+
const kind = (action as { kind?: unknown }).kind;
|
|
64
|
+
if (typeof kind === 'string') out.add(kind);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Walk a transition list and yield every `guard.kind` value. */
|
|
71
|
+
function collectGuardKinds(transitions: unknown): Set<string> {
|
|
72
|
+
const out = new Set<string>();
|
|
73
|
+
if (!Array.isArray(transitions)) return out;
|
|
74
|
+
for (const transition of transitions) {
|
|
75
|
+
if (typeof transition !== 'object' || transition === null) continue;
|
|
76
|
+
const guard = (transition as { guard?: { kind?: unknown } }).guard;
|
|
77
|
+
const kind = guard?.kind;
|
|
78
|
+
if (typeof kind === 'string') out.add(kind);
|
|
79
|
+
}
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Symmetric set diff. Returns elements in `a` but not in `b`, and vice versa. */
|
|
84
|
+
function diff<T>(a: Set<T>, b: Set<T>): { onlyInA: T[]; onlyInB: T[] } {
|
|
85
|
+
const onlyInA: T[] = [];
|
|
86
|
+
const onlyInB: T[] = [];
|
|
87
|
+
for (const x of a) if (!b.has(x)) onlyInA.push(x);
|
|
88
|
+
for (const x of b) if (!a.has(x)) onlyInB.push(x);
|
|
89
|
+
return { onlyInA, onlyInB };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function parseArgs(argv: string[]): { root: string } {
|
|
93
|
+
let root = resolve('.');
|
|
94
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
95
|
+
const arg = argv[i];
|
|
96
|
+
if (arg === '--root' && i + 1 < argv.length) {
|
|
97
|
+
root = resolve(argv[++i] ?? '.');
|
|
98
|
+
} else if (arg === '--help' || arg === '-h') {
|
|
99
|
+
process.stdout.write('Usage: bun inline-pipeline-parity-check.ts [--root <path>]\n');
|
|
100
|
+
process.exit(0);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return { root };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function listWorkflowFiles(dir: string): string[] {
|
|
107
|
+
let entries: string[];
|
|
108
|
+
try {
|
|
109
|
+
entries = readdirSync(dir);
|
|
110
|
+
} catch {
|
|
111
|
+
return [];
|
|
112
|
+
}
|
|
113
|
+
return entries.filter((e) => e.endsWith('.yaml')).map((e) => join(dir, e));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function main(): Promise<number> {
|
|
117
|
+
const { root } = parseArgs(process.argv.slice(2));
|
|
118
|
+
const errors: string[] = [];
|
|
119
|
+
|
|
120
|
+
const workflowDir = join(root, WORKFLOW_DIR);
|
|
121
|
+
if (!statSync(workflowDir, { throwIfNoEntry: false })) {
|
|
122
|
+
process.stderr.write(`inline-pipeline-parity-check: workflow directory not found: ${workflowDir}\n`);
|
|
123
|
+
return 1;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const files = listWorkflowFiles(workflowDir);
|
|
127
|
+
if (files.length === 0) {
|
|
128
|
+
process.stderr.write(`inline-pipeline-parity-check: no .yaml workflows found in ${workflowDir}\n`);
|
|
129
|
+
return 1;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const unionActions = new Set<string>();
|
|
133
|
+
const unionGuards = new Set<string>();
|
|
134
|
+
const perFileKinds: { path: string; actions: Set<string>; guards: Set<string> }[] = [];
|
|
135
|
+
|
|
136
|
+
for (const path of files) {
|
|
137
|
+
let parsed: unknown;
|
|
138
|
+
try {
|
|
139
|
+
parsed = parseYaml(readFileSync(path, 'utf8'));
|
|
140
|
+
} catch (err) {
|
|
141
|
+
errors.push(`${path}: failed to parse (${err instanceof Error ? err.message : String(err)})`);
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (typeof parsed !== 'object' || parsed === null) {
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
const def = parsed as { states?: unknown; transitions?: unknown };
|
|
148
|
+
const actions = collectActionKinds(def.states);
|
|
149
|
+
const guards = collectGuardKinds(def.transitions);
|
|
150
|
+
perFileKinds.push({ path, actions, guards });
|
|
151
|
+
for (const a of actions) unionActions.add(a);
|
|
152
|
+
for (const g of guards) unionGuards.add(g);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const actionDiff = diff(unionActions, DOCUMENTED.actions);
|
|
156
|
+
const guardDiff = diff(unionGuards, DOCUMENTED.guards);
|
|
157
|
+
|
|
158
|
+
for (const x of actionDiff.onlyInA) {
|
|
159
|
+
const usedIn = perFileKinds.filter((f) => f.actions.has(x)).map((f) => f.path);
|
|
160
|
+
errors.push(`action kind "${x}" used in YAML (${usedIn.join(', ')}) but absent from inline-pipeline-driver.md`);
|
|
161
|
+
}
|
|
162
|
+
for (const x of actionDiff.onlyInB) {
|
|
163
|
+
errors.push(`action kind "${x}" documented in inline-pipeline-driver.md but never used in any workflow`);
|
|
164
|
+
}
|
|
165
|
+
for (const x of guardDiff.onlyInA) {
|
|
166
|
+
const usedIn = perFileKinds.filter((f) => f.guards.has(x)).map((f) => f.path);
|
|
167
|
+
errors.push(`guard kind "${x}" used in YAML (${usedIn.join(', ')}) but absent from inline-pipeline-driver.md`);
|
|
168
|
+
}
|
|
169
|
+
for (const x of guardDiff.onlyInB) {
|
|
170
|
+
errors.push(`guard kind "${x}" documented in inline-pipeline-driver.md but never used in any workflow`);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (errors.length > 0) {
|
|
174
|
+
process.stderr.write(`inline-pipeline-parity-check: ${errors.length} divergence(s)\n`);
|
|
175
|
+
for (const e of errors) process.stderr.write(` - ${e}\n`);
|
|
176
|
+
return 1;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
process.stdout.write(
|
|
180
|
+
`inline-pipeline-parity-check: ok (${unionActions.size} actions, ${unionGuards.size} guards agree across ${files.length} workflows)\n`,
|
|
181
|
+
);
|
|
182
|
+
return 0;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
process.exit(await main());
|
|
@@ -325,7 +325,11 @@ function hasPendingRequest(comments, head, login) {
|
|
|
325
325
|
}
|
|
326
326
|
function requireExpectedHead(args, pr) {
|
|
327
327
|
const expected = args.flags.get("--head");
|
|
328
|
-
if (expected
|
|
328
|
+
if (!expected) {
|
|
329
|
+
writeStatus(args, "FAIL");
|
|
330
|
+
fail(args, "--head <sha> is required and must match the reviewed request HEAD", 2);
|
|
331
|
+
}
|
|
332
|
+
if (expected !== pr.headRefOid) {
|
|
329
333
|
writeStatus(args, "FAIL");
|
|
330
334
|
fail(args, `PR HEAD moved from ${expected.slice(0, 7)} to ${pr.headRefOid.slice(0, 7)} \u2014 request a new review`, 2);
|
|
331
335
|
}
|
|
@@ -519,8 +519,14 @@ function hasPendingRequest(comments: readonly GhIssueComment[], head: string, lo
|
|
|
519
519
|
}
|
|
520
520
|
|
|
521
521
|
function requireExpectedHead(args: ParsedArgs, pr: GhPr): void {
|
|
522
|
+
// 0771: head pinning only protects when the flag actually carries a SHA — an empty or
|
|
523
|
+
// missing --head must fail loud instead of silently reviewing whatever HEAD is current.
|
|
522
524
|
const expected = args.flags.get('--head');
|
|
523
|
-
if (expected
|
|
525
|
+
if (!expected) {
|
|
526
|
+
writeStatus(args, 'FAIL');
|
|
527
|
+
fail(args, '--head <sha> is required and must match the reviewed request HEAD', 2);
|
|
528
|
+
}
|
|
529
|
+
if (expected !== pr.headRefOid) {
|
|
524
530
|
writeStatus(args, 'FAIL');
|
|
525
531
|
fail(
|
|
526
532
|
args,
|
|
@@ -145,7 +145,7 @@ function main(): void {
|
|
|
145
145
|
|
|
146
146
|
const dbPath = join(process.cwd(), '.spur', 'spur.db');
|
|
147
147
|
if (!existsSync(dbPath)) {
|
|
148
|
-
fail(wbs, [`spur database not found at ${dbPath} — run a real history import first`]);
|
|
148
|
+
fail(wbs, [`local spur database not found at ${dbPath} — run a real history import first`]);
|
|
149
149
|
}
|
|
150
150
|
|
|
151
151
|
let count: number;
|
|
@@ -8,11 +8,11 @@
|
|
|
8
8
|
* so it is a grep target and a review signal, and a manifest entry in
|
|
9
9
|
* `config/transition-shims.json` records who owns it and when it can be removed.
|
|
10
10
|
*
|
|
11
|
-
* The gate is deliberately two-sided
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
11
|
+
* The gate is deliberately two-sided: a marker with no manifest entry
|
|
12
|
+
* FAILS, and a manifest entry whose marker no longer appears in source
|
|
13
|
+
* FAILS. Without the second half the manifest would rot into a permanent
|
|
14
|
+
* suppression list — the exact invisible-debt pattern this gate exists to
|
|
15
|
+
* end.
|
|
16
16
|
*
|
|
17
17
|
* Marker convention (docs/04_DESIGN.md §2.5):
|
|
18
18
|
* // @transition-shim(<id>) — <one line on what this keeps working>
|
|
@@ -134,7 +134,7 @@ function scanMarkers(cwd: string, roots: string[]): Map<string, Set<string>> {
|
|
|
134
134
|
return found;
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
-
/** Load the manifest; a missing file degrades to no entries (
|
|
137
|
+
/** Load the manifest; a missing file degrades to no entries (fail-open). */
|
|
138
138
|
function loadManifest(path: string): { entries: ManifestEntry[]; raw: string | null } {
|
|
139
139
|
if (!existsSync(path)) return { entries: [], raw: null };
|
|
140
140
|
const raw = readFileSync(path, 'utf8');
|
|
@@ -269,6 +269,10 @@ function extractAcIdentities(taskContent: string, featureContent: string | null)
|
|
|
269
269
|
const leading = label.split(/\s+/)[0] ?? '';
|
|
270
270
|
if (leading && leading !== label) identities.add(leading);
|
|
271
271
|
}
|
|
272
|
+
for (const m of section.matchAll(/^[ \t]*Scenario:\s*(.+)\s*$/gm)) {
|
|
273
|
+
const title = (m[1] ?? '').trim();
|
|
274
|
+
if (title) identities.add(title);
|
|
275
|
+
}
|
|
272
276
|
if (featureContent !== null) {
|
|
273
277
|
for (const m of featureContent.matchAll(/^[ \t]*Scenario:\s*(.+)\s*$/gm)) {
|
|
274
278
|
const title = (m[1] ?? '').trim();
|
|
@@ -164,12 +164,15 @@ superskill script contract.
|
|
|
164
164
|
8. **Collect** — `<script> collect --since "$requestedAt" --head "$requestHead" --json`. Normalize
|
|
165
165
|
only the requested HEAD and request window; then invoke `status` with the same bounds for
|
|
166
166
|
PR/base/CI/local state. Never turn an absent, stale, or moved-HEAD result into `clean`.
|
|
167
|
+
`--head` is mandatory on wait/collect/status (0771): an empty or missing pin fails loud
|
|
168
|
+
instead of silently reviewing whatever HEAD is current.
|
|
167
169
|
|
|
168
170
|
## Fix mode
|
|
169
171
|
|
|
170
172
|
Only in `fix` mode:
|
|
171
173
|
|
|
172
|
-
1. **Collect before editing.** Invoke direct `collect --json
|
|
174
|
+
1. **Collect before editing.** Invoke direct `collect --json --head "$(git rev-parse HEAD)"` plus
|
|
175
|
+
`status --json --head "$(git rev-parse HEAD)"` and require a
|
|
173
176
|
completed result for the current pushed HEAD. If the result is pending, stale, or unavailable,
|
|
174
177
|
report it and stop without source edits. If it is explicitly clean, report no fixes and stop.
|
|
175
178
|
2. **Validate each finding.** Open the referenced code; trace callers/callees; verify the problem
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: spur-cli-self
|
|
3
|
-
description: "spur-cli noun reference for `spur self`: self-management verbs — scaffold (`init`), schema migrations (`migrate`), local web server (`serve`), and status overview (`status`). Each verb mounts the same command builder as its legacy top-level noun, which remains a hidden alias over the identical command."
|
|
3
|
+
description: "spur-cli noun reference for `spur self`: self-management verbs — scaffold (`init`), database maintenance (`maintain`), schema migrations (`migrate`), local web server (`serve`), and status overview (`status`). Each verb mounts the same command builder as its legacy top-level noun, which remains a hidden alias over the identical command."
|
|
4
4
|
see_also:
|
|
5
5
|
- spur-cli
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# spur self - self-management verbs
|
|
9
9
|
|
|
10
|
-
`spur self` hosts the
|
|
11
|
-
that also remains registered as a legacy top-level **hidden alias** (`spur init`, `spur migrate`,
|
|
10
|
+
`spur self` hosts the self-management verbs. Each verb is the canonical path for a command
|
|
11
|
+
that also remains registered as a legacy top-level **hidden alias** (`spur init`, `spur maintain`, `spur migrate`,
|
|
12
12
|
`spur serve`, `spur status`) so existing scripts, workflow YAML, and habits keep working unchanged.
|
|
13
13
|
Both paths share the same command builder: identical flags, output, and exit codes. The legacy
|
|
14
14
|
top-level forms are omitted from `spur --help`, leaving `self` as the visible surface.
|
|
@@ -18,6 +18,7 @@ top-level forms are omitted from `spur --help`, leaving `self` as the visible su
|
|
|
18
18
|
| Verb | Purpose | Key flags |
|
|
19
19
|
| ---- | ------- | --------- |
|
|
20
20
|
| `init` | Scaffold a new Spur project in the current directory | `--name <name>` `--force` `--minimal` `--json` |
|
|
21
|
+
| `maintain` | Run database maintenance: PRAGMA optimize, WAL truncation, optional VACUUM | `--vacuum` `--json` |
|
|
21
22
|
| `migrate` | Apply CLI-owned schema migrations | `--json` |
|
|
22
23
|
| `serve` | Start the Spur web server (local fallback) | `--port <n>` `--host <addr>` `--no-open` `--cwd <path>` `--json` |
|
|
23
24
|
| `status [path]` | Show project and git status for a Spur project | `--json` |
|
|
@@ -43,6 +44,19 @@ Materializes the `.spur/` directory tree with config, docs, rules, and workflow
|
|
|
43
44
|
(Phase 1.5 / 1.6) run immediately after this verb completes — see **[init.md](init.md)** for the
|
|
44
45
|
probe protocol and rule-glob adaptation procedure.
|
|
45
46
|
|
|
47
|
+
## `self maintain` - run database maintenance
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
spur self maintain # run PRAGMA optimize + wal_checkpoint(TRUNCATE)
|
|
51
|
+
spur self maintain --vacuum # deep maintenance: VACUUM + optimize + checkpoint
|
|
52
|
+
spur self maintain --json # machine-readable
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Executes SQLite database maintenance operations on `.spur/spur.db`:
|
|
56
|
+
- `PRAGMA optimize;` to refresh query planner statistics in `sqlite_stat1`.
|
|
57
|
+
- `PRAGMA wal_checkpoint(TRUNCATE);` to checkpoint committed WAL frames and truncate WAL to 0 bytes.
|
|
58
|
+
- `--vacuum` runs `VACUUM;` first for page compaction and B-tree defragmentation (requires 2x free disk space).
|
|
59
|
+
|
|
46
60
|
## `self migrate` - apply CLI-owned schema migrations
|
|
47
61
|
|
|
48
62
|
```bash
|
|
@@ -51,7 +51,7 @@ re-reading or re-tokenizing the task.
|
|
|
51
51
|
| `batch-create` | Create many tasks from a validated JSON array | `--file <path>` `--folder` `--json` |
|
|
52
52
|
| `record <wbs>` | Write `Testing` from a verify verdict (deterministic); bare-`## Review` fallback only; optional Solution + transition | `--verdict-file <path>` `--solution-from-diff` `--transition <status>` `--folder` `--json` |
|
|
53
53
|
| `verdict <wbs>` | Derive PASS/PARTIAL/FAIL/UNKNOWN from verify answer text → verdict JSON; see [answer-file shape](tasks/verbs.md#answer-file-shape-what---from-answer-parses) | `--from-answer <path>` `--folder` `--json` |
|
|
54
|
-
| `check [wbs]` | Four-layer validation;
|
|
54
|
+
| `check [wbs]` | Four-layer validation; `--fix` repairs structure; `--corpus` is the explicit unsuppressed audit | `--strict` `--as <status>` `--strict-core` `--fix` `--folder` `--corpus` `--since <ref>` `--json` |
|
|
55
55
|
| `resolve <file-path>` | Map a file path to its owning task WBS | `--strict` `--folder` `--json` |
|
|
56
56
|
| `path <wbs>` | Map a WBS to its absolute task file path (inverse of `resolve`) | `--folder` `--json` |
|
|
57
57
|
| `run-link <wbs>` | Record pipeline run provenance link for task | `--source <src>` `--run-id <id>` `--json` |
|
|
@@ -229,3 +229,26 @@ verifying shape, or stand in a `note` action until the path is proven, then swap
|
|
|
229
229
|
`agent.run` input referencing a command rather than a raw prompt, guards a single predicate
|
|
230
230
|
([workflow-fit-and-tuning.md](workflow-fit-and-tuning.md#3-node-simplicity-budget)).
|
|
231
231
|
- [ ] Validates clean AND dry-run reaches the expected terminal state.
|
|
232
|
+
|
|
233
|
+
## Optional version literal (task 0756)
|
|
234
|
+
|
|
235
|
+
Both dialects accept an optional root `version` field. The literal is **behavior-neutral** — it
|
|
236
|
+
exists as an identity tag, not a routing key. The contract:
|
|
237
|
+
|
|
238
|
+
- **Absent** → reported as `unversioned`. The default for all 11 shipped definitions.
|
|
239
|
+
- **Present, non-empty string** → reported as `explicit(<literal>)`. The literal is wrapped in
|
|
240
|
+
parentheses verbatim — no parsing, no ordering, no compatibility check.
|
|
241
|
+
- **Present, empty string (`version: ""`)** → **rejected** with a diagnostic naming the empty
|
|
242
|
+
value. The rejection lives in the resolve/preflight seam
|
|
243
|
+
(`packages/app/src/workflow/workflow-resolver.ts`), not in the dialect JSON schemas: those carry
|
|
244
|
+
`minLength: 1` for editors and Ajv consumers, but the load path validates against the engine's
|
|
245
|
+
Zod schema, which has no minimum. Move the check upstream once
|
|
246
|
+
`@gobing-ai/ts-dual-workflow-engine` ships `z.string().min(1)` on the root version.
|
|
247
|
+
|
|
248
|
+
The literal folds into the definition digest (`packages/app/src/workflow/composition-baseline.ts`),
|
|
249
|
+
so a version-only edit changes the digest with zero behavior change. `show` and `trace` do **not**
|
|
250
|
+
surface the literal by default — the digest stays the rendered run identity (D8 decision D5).
|
|
251
|
+
|
|
252
|
+
**No registry, no semver parser, no compatibility engine.** A future-major requirement needs
|
|
253
|
+
objective evidence: a consumer that branches on version, or a real drift incident the digest
|
|
254
|
+
diagnostic could not disambiguate. Neither exists today.
|
|
@@ -140,10 +140,10 @@ reports it. Do not invent a second threshold; author to the one that is frozen (
|
|
|
140
140
|
| Node count | Every node earns its transition round-trip | A node that always runs immediately after another, with no guard between them, is one node |
|
|
141
141
|
|
|
142
142
|
**When a node breaches the budget, do not reformat to dodge the measure.** Joining five lines with
|
|
143
|
-
`&&` moves the complexity, not the ownership. Pick
|
|
143
|
+
`&&` moves the complexity, not the ownership. Pick one of the four remaining owners from
|
|
144
144
|
`docs/design/workflow-shell-ownership.md`: public `spur` verb (consent-gated), application service,
|
|
145
|
-
least-privilege built-in action kind, workflow-relative external extension
|
|
146
|
-
stays-shell exception with
|
|
145
|
+
least-privilege built-in action kind, or workflow-relative external extension. (0775 retired the
|
|
146
|
+
recorded stays-shell exception along with the suppression snapshot.)
|
|
147
147
|
|
|
148
148
|
**Advisory posture is binding.** Composition findings never block a run, never change a `validate`
|
|
149
149
|
exit status, and are never a reason to hot-edit an executing pipeline. Surface them; fix on operator
|
|
@@ -193,8 +193,8 @@ Steps:
|
|
|
193
193
|
2. **Return each node's work to its owner** — judgment nodes to the command or skill they should
|
|
194
194
|
have been calling; a genuinely useful shell sequence to one script under its owning surface.
|
|
195
195
|
3. **Rewrite the entry surface as the procedure**, in the order the graph ran.
|
|
196
|
-
4. **Delete the YAML
|
|
197
|
-
|
|
196
|
+
4. **Delete the YAML** in the same change. (0775 retired the composition-baseline entries that
|
|
197
|
+
used to be deleted alongside it; the live definition is the only artifact left.)
|
|
198
198
|
5. **Record the demotion** and its trigger, so the next author does not re-promote it by reflex.
|
|
199
199
|
|
|
200
200
|
### optimize — refine an accepted workflow in place
|
|
@@ -231,10 +231,10 @@ agent prompts where a first-class surface would own it better?" — nothing more
|
|
|
231
231
|
raw prompt length only sets the reported severity (<200 low / ≤1000 medium / >1000 high).
|
|
232
232
|
|
|
233
233
|
**How to run it:** it is not a separate command — `spur workflow validate <file> --json` returns
|
|
234
|
-
`composition: {findings[]
|
|
235
|
-
with exit 0.
|
|
236
|
-
`
|
|
237
|
-
|
|
234
|
+
`composition: {findings[]}` on the valid path; human mode prints the advisory to stderr
|
|
235
|
+
with exit 0. Findings are derived from the workflow definition itself
|
|
236
|
+
(`extractResolvedWorkflowFacts`, guarded by `composition-baseline.test.ts`); 0775 retired the
|
|
237
|
+
suppression snapshot, so every finding is reported — none are suppressed.
|
|
238
238
|
|
|
239
239
|
**How to read it, and the fix path per defect class:**
|
|
240
240
|
|
|
@@ -153,6 +153,23 @@ CLI does.
|
|
|
153
153
|
|
|
154
154
|
## Gotchas
|
|
155
155
|
|
|
156
|
+
### Bounded execution and recovery (0777)
|
|
157
|
+
|
|
158
|
+
- During planning/refinement of corpus-scale work, record an execution budget, persisted-artifact
|
|
159
|
+
paths and requireDiff guidance in the task Design. For classification-only work, declare
|
|
160
|
+
`mutationPolicy: none`; its task/evidence change is the deliverable, not invented source edits.
|
|
161
|
+
Preserve partial artifacts at the budget boundary; resume only against the current task contract.
|
|
162
|
+
- Before accepting a child/watcher result, compare its run ID with the dispatched run ID and check
|
|
163
|
+
current trace state through Spur. If using a run log as evidence, require its mtime to be at least
|
|
164
|
+
the dispatch time. A mismatch or stale timestamp is not completion evidence. Do not scrape terminals.
|
|
165
|
+
Bound each watch invocation to 10 minutes or 20 polls, whichever comes first; persist the last
|
|
166
|
+
confirmed identity/state and report a checkpoint before continuing. A watcher timeout does not
|
|
167
|
+
cancel the owned run or authorize launching a replacement.
|
|
168
|
+
- Mark superseded scratch with `SUPERSEDED` and a pointer to the authoritative task Design. Never
|
|
169
|
+
let a scratch instruction override the live task, even when its old run is still readable.
|
|
170
|
+
- Checker-policy changes require one explicit unsuppressed audit (T10); ordinary corpus commit
|
|
171
|
+
prep checks affected documents and linked evidence (T11), not the corpus. No acceptance snapshots.
|
|
172
|
+
|
|
156
173
|
1. **Never skip a gate.** A clean `feature check` is the only proof the AC is valid; a
|
|
157
174
|
passing `batch-create` is the only proof the decomposition is well-formed. Skip either and
|
|
158
175
|
you ship corrupted corpus.
|
|
@@ -887,9 +887,9 @@ time. Before launching a full `spur-check-new`:
|
|
|
887
887
|
`console.*`), spawn processes via `NodeProcessExecutor` (no `Bun.spawnSync`), get a
|
|
888
888
|
`runtime-boundaries` fs-io exemption for sync reads (mirrors `task.ts`), and a non-command helper
|
|
889
889
|
must not live in `apps/cli/src/commands/` (the noun scan treats every file there as a noun).
|
|
890
|
-
3. **Doc/TSDoc edits shift `file:line` anchors** cited by other tasks —
|
|
891
|
-
as `L4.anchor-subject-mismatch
|
|
892
|
-
|
|
890
|
+
3. **Doc/TSDoc edits shift `file:line` anchors** cited by other tasks — the per-task gate surfaces
|
|
891
|
+
them as `L4.anchor-subject-mismatch` (0775: the corpus sweep retired; run `spur task check <wbs>`
|
|
892
|
+
on touched tasks). Repoint the shifted citations (via `spur task update --section`)
|
|
893
893
|
|
|
894
894
|
## AC traceability
|
|
895
895
|
|
|
@@ -170,7 +170,7 @@ transition to `done`.
|
|
|
170
170
|
> The verify skill writes this shape automatically — operators only need it when hand-authoring
|
|
171
171
|
> an answer file or debugging an UNKNOWN verdict.
|
|
172
172
|
|
|
173
|
-
> **
|
|
173
|
+
> **Finding-rule discipline (ADR-108).** When checker policy changes, run focused tests and one explicit unsuppressed `bun run corpus-check` audit; record/reconcile essential failures without waivers (T10). Ordinary corpus commit prep checks changed tasks/features and their required linked evidence, not the corpus (T11). The baseline and regenerators remain deleted; tests are not a substitute for the explicit policy-change audit.
|
|
174
174
|
>
|
|
175
175
|
> **Transition-shim discipline (`transition-shim-check`, task 0541 / ADR-058).** Wired as an early step of `spur-check` / `spur-check-new` (second, after `link-check` and before `lint`) so a shim violation fails in ~0.3 s rather than after the test run. Two-sided against `config/transition-shims.json`: an `@transition-shim(<id>)` marker with no manifest entry fails as a **new unregistered shim**, **and** a listed entry whose marker is gone from source fails as a **stale entry**. Emptying the manifest is the definition of the agent-role transition being complete. A removal condition must be objectively checkable against the repository. Shapes: `docs/04_DESIGN.md` §2.5.
|
|
176
176
|
|
|
@@ -188,4 +188,3 @@ transition to `done`.
|
|
|
188
188
|
> failure is not yours. This costs seconds and mutates nothing — do **not** `git stash` to get a
|
|
189
189
|
> clean baseline on a shared working tree. Treat any failure that does *not* name a port/listen/`ps`
|
|
190
190
|
> cause as yours until proven otherwise; the count is a moving baseline, not a constant.
|
|
191
|
-
|