@gobing-ai/spur 0.3.99 → 0.4.1
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.global.yaml +17 -0
- package/config/decisions/gates.yaml +36 -0
- package/config/decisions/history-anatomy.yaml +35 -0
- package/config/decisions/idea-pipeline.yaml +38 -0
- package/config/decisions/task-pipeline.yaml +55 -0
- package/config/plugin-scripts.json +25 -0
- package/config/rules/boundary/sp-runtime-path.yaml +2 -0
- package/config/rules/docs/sourced-api-claims.yaml +60 -0
- package/config/rules/quality/coverage-gate.yaml +5 -3
- package/config/rules/recommended-pre-check.yaml +8 -3
- package/config/rules/strict/runtime-boundaries.yaml +11 -0
- package/config/rules/structure/protected-files.yaml +3 -0
- package/config/rules/structure/scratch-evidence-pointer.yaml +35 -0
- package/config/script-placement-baseline.json +2 -2
- package/config/workflows/feature-lifecycle.yaml +9 -2
- package/config/workflows/history-anatomy.yaml +22 -0
- package/config/workflows/idea-pipeline.yaml +75 -0
- package/config/workflows/task-pipeline.yaml +368 -49
- package/config/workflows/wrapup-pipeline.yaml +106 -13
- package/package.json +9 -9
- package/plugins/sp/README.md +25 -2
- package/plugins/sp/agents/super-planner.md +11 -0
- package/plugins/sp/agents/super-reviewer.md +39 -11
- package/plugins/sp/bunfig.toml +8 -0
- package/plugins/sp/commands/dev-brainstorm.md +29 -16
- package/plugins/sp/commands/dev-changelog.md +5 -4
- package/plugins/sp/commands/dev-find-way.md +80 -0
- package/plugins/sp/commands/dev-idea.md +6 -0
- package/plugins/sp/commands/dev-parallel.md +8 -2
- package/plugins/sp/commands/dev-plan.md +6 -0
- package/plugins/sp/commands/dev-review-session.md +14 -7
- package/plugins/sp/commands/dev-run.md +17 -4
- package/plugins/sp/commands/dev-runall.md +21 -10
- package/plugins/sp/hooks/agent-lifecycle.ts +155 -0
- package/plugins/sp/hooks/context-session-start.ts +1 -1
- package/plugins/sp/hooks/fleet-guest-stop.ts +180 -0
- package/plugins/sp/hooks/hooks.json +45 -1
- package/plugins/sp/hooks/pi/guard-extension.ts +21 -0
- package/plugins/sp/lib/history-anatomy.generated.mjs +5 -0
- package/plugins/sp/lib/idea-handoff.generated.mjs +72 -11
- package/plugins/sp/lib/inline-run-app.ts +28 -0
- package/plugins/sp/lib/inline-run.generated.d.mts +6 -0
- package/plugins/sp/lib/inline-run.generated.mjs +364 -46
- package/plugins/sp/lib/plan-projection.generated.d.mts +28 -0
- package/plugins/sp/lib/plan-projection.generated.mjs +255 -0
- package/plugins/sp/lib/quality-gate.generated.d.mts +47 -1
- package/plugins/sp/lib/quality-gate.generated.mjs +30489 -82
- package/plugins/sp/lib/residual-scan.generated.d.mts +19 -0
- package/plugins/sp/lib/residual-scan.generated.mjs +66 -0
- package/plugins/sp/lib/run-summary-core.ts +561 -0
- package/plugins/sp/lib/transcript.ts +275 -0
- package/plugins/sp/plugin.json +1 -1
- package/plugins/sp/references/roles.md +1 -1
- package/plugins/sp/scripts/batch-plan.mjs +335 -0
- package/plugins/sp/scripts/batch-plan.ts +100 -0
- package/plugins/sp/scripts/batch-preflight.mjs +51 -3
- package/plugins/sp/scripts/batch-preflight.ts +72 -1
- package/plugins/sp/scripts/commit-guard.mjs +216 -0
- package/plugins/sp/scripts/commit-guard.ts +249 -0
- package/plugins/sp/scripts/history-anatomy-cache.mjs +5 -0
- package/plugins/sp/scripts/inline-run-dispatch.mjs +130 -0
- package/plugins/sp/scripts/inline-run-dispatch.ts +135 -0
- package/plugins/sp/scripts/inline-run-setup.mjs +563 -91
- package/plugins/sp/scripts/inline-run-setup.ts +65 -97
- package/plugins/sp/scripts/persist-out-check.mjs +90 -41
- package/plugins/sp/scripts/persist-out-check.ts +112 -57
- package/plugins/sp/scripts/quality-gate.mjs +30407 -73
- package/plugins/sp/scripts/quality-gate.ts +21 -5
- package/plugins/sp/scripts/residual-scan.mjs +88 -38
- package/plugins/sp/scripts/residual-scan.ts +35 -38
- package/plugins/sp/scripts/run-summary.mjs +693 -0
- package/plugins/sp/scripts/run-summary.ts +105 -0
- package/plugins/sp/scripts/session-timeline.mjs +331 -0
- package/plugins/sp/scripts/session-timeline.ts +204 -0
- package/plugins/sp/scripts/wrapup-steps.mjs +150 -2
- package/plugins/sp/scripts/wrapup-steps.ts +203 -1
- package/plugins/sp/skills/brainstorm/SKILL.md +9 -47
- package/plugins/sp/skills/branch-workflow/SKILL.md +1 -0
- package/plugins/sp/skills/branch-workflow/references/worktree-patterns.md +1 -0
- package/plugins/sp/skills/code-implementation/SKILL.md +48 -1
- package/plugins/sp/skills/code-verification/SKILL.md +8 -4
- package/plugins/sp/skills/code-verification/references/secu-review.md +3 -0
- package/plugins/sp/skills/code-verification/references/verdict-schema.md +68 -0
- package/plugins/sp/skills/fleet-join/SKILL.md +63 -0
- package/plugins/sp/skills/session-review/SKILL.md +77 -24
- package/plugins/sp/skills/source-driven-development/SKILL.md +5 -0
- package/plugins/sp/skills/spec-decomposition/references/decomposition.md +5 -3
- package/plugins/sp/skills/spur-cli/SKILL.md +6 -2
- package/plugins/sp/skills/spur-cli/references/agent.md +124 -17
- package/plugins/sp/skills/spur-cli/references/decision.md +98 -0
- package/plugins/sp/skills/spur-cli/references/message.md +7 -7
- package/plugins/sp/skills/spur-cli/references/tasks/verbs.md +41 -3
- package/plugins/sp/skills/spur-cli/references/tasks.md +10 -7
- package/plugins/sp/skills/spur-cli/references/workflows/authoring-workflows.md +7 -0
- package/plugins/sp/skills/spur-cli/references/workflows.md +7 -2
- package/plugins/sp/skills/spur-composer/SKILL.md +28 -2
- package/plugins/sp/skills/spur-dev/SKILL.md +18 -14
- package/plugins/sp/skills/spur-dev/references/ac-style-guide.md +6 -3
- package/plugins/sp/skills/spur-dev/references/cross-cutting.md +49 -14
- package/plugins/sp/skills/spur-dev/references/dev-operations.md +51 -18
- package/plugins/sp/skills/spur-dev/references/execution-batch-continuation.md +68 -0
- package/plugins/sp/skills/spur-dev/references/execution-batch-report.md +194 -0
- package/plugins/sp/skills/spur-dev/references/execution-batch.md +113 -963
- package/plugins/sp/skills/spur-dev/references/execution-parallel-isolation.md +130 -0
- package/plugins/sp/skills/spur-dev/references/execution-workflow.md +20 -0
- package/plugins/sp/skills/spur-dev/references/execution-worktree-landing.md +471 -0
- package/plugins/sp/skills/spur-dev/references/execution-worktree-setup.md +379 -0
- package/plugins/sp/skills/spur-dev/references/flag-glossary.md +51 -9
- package/plugins/sp/skills/spur-dev/references/glossary.md +2 -2
- package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +361 -156
- package/plugins/sp/skills/spur-dev/references/planning-workflow.md +6 -0
- package/plugins/sp/skills/spur-dev/references/structured-trace-emission.md +178 -0
- package/plugins/sp/skills/wayfinder/SKILL.md +10 -14
- package/schemas/decision-catalog.schema.json +226 -0
- package/schemas/spur-config.schema.json +25 -0
- package/schemas/state-machine-workflow.schema.json +23 -0
- package/spur.js +28165 -19051
- package/web/_astro/{BoardApp.i4eh_fHA.js → BoardApp.D66k_s_T.js} +1 -1
- package/web/_astro/BoardApp.D6sjAJr_.js +324 -0
- package/web/_astro/{TaskDetail.C5Cu8LdB.js → TaskDetail.BynLnMNc.js} +1 -1
- package/web/_astro/{arc.Bu5XpBEz.js → arc._BUadrdH.js} +1 -1
- package/web/_astro/{architectureDiagram-3BPJPVTR.9vHQTvhF.js → architectureDiagram-3BPJPVTR.Dh7xS1Pp.js} +1 -1
- package/web/_astro/{blockDiagram-GPEHLZMM.Cn2Xl7D6.js → blockDiagram-GPEHLZMM.DFDVpfXm.js} +1 -1
- package/web/_astro/{c4Diagram-AAUBKEIU.7o0P_QDo.js → c4Diagram-AAUBKEIU.V7_3_glh.js} +1 -1
- package/web/_astro/channel.DZVfto-r.js +1 -0
- package/web/_astro/{chunk-2J33WTMH.C3zZFsNt.js → chunk-2J33WTMH.De1yx7Jz.js} +1 -1
- package/web/_astro/{chunk-4BX2VUAB.CNy_M7fO.js → chunk-4BX2VUAB.BDhHFuxx.js} +1 -1
- package/web/_astro/{chunk-55IACEB6.2TiPnLBL.js → chunk-55IACEB6.DDLkWnmC.js} +1 -1
- package/web/_astro/{chunk-727SXJPM.5CGSyKAp.js → chunk-727SXJPM.CwfiLnA7.js} +1 -1
- package/web/_astro/{chunk-AQP2D5EJ.w-ZIgfsK.js → chunk-AQP2D5EJ.CtGDmUnM.js} +1 -1
- package/web/_astro/{chunk-FMBD7UC4.BkcEyhKa.js → chunk-FMBD7UC4.BOLjpw73.js} +1 -1
- package/web/_astro/{chunk-ND2GUHAM.mxKKbBG4.js → chunk-ND2GUHAM.BDhQls6O.js} +1 -1
- package/web/_astro/{chunk-QZHKN3VN.cyA0z9aT.js → chunk-QZHKN3VN.76qBynDF.js} +1 -1
- package/web/_astro/{classDiagram-4FO5ZUOK.CqtUzwhX.js → classDiagram-4FO5ZUOK.N0mpPHT0.js} +1 -1
- package/web/_astro/{classDiagram-v2-Q7XG4LA2.CqtUzwhX.js → classDiagram-v2-Q7XG4LA2.N0mpPHT0.js} +1 -1
- package/web/_astro/{cose-bilkent-S5V4N54A.BExPTopI.js → cose-bilkent-S5V4N54A.CKknFfjB.js} +1 -1
- package/web/_astro/{cynefin-OW5HDTMX.Bq2i10Cn.js → cynefin-OW5HDTMX.Cw3ZWBAh.js} +1 -1
- package/web/_astro/{dagre-BM42HDAG.By_HsOuv.js → dagre-BM42HDAG.D3yIKCRD.js} +1 -1
- package/web/_astro/{diagram-2AECGRRQ.BtU0b7hz.js → diagram-2AECGRRQ.D51dO83Z.js} +1 -1
- package/web/_astro/{diagram-5GNKFQAL.D00ztMdT.js → diagram-5GNKFQAL.Bb-FViDj.js} +1 -1
- package/web/_astro/{diagram-KO2AKTUF.DMVJWaUN.js → diagram-KO2AKTUF.D3mN8S6x.js} +1 -1
- package/web/_astro/{diagram-LMA3HP47.Cxqrjg5-.js → diagram-LMA3HP47.dMMyPcbJ.js} +1 -1
- package/web/_astro/{diagram-OG6HWLK6.2lz_1B5h.js → diagram-OG6HWLK6.AzbvzY4C.js} +1 -1
- package/web/_astro/{erDiagram-TEJ5UH35.BVQEuJ3T.js → erDiagram-TEJ5UH35.BR5I4OZb.js} +1 -1
- package/web/_astro/{flowDiagram-I6XJVG4X.Qlyo90ky.js → flowDiagram-I6XJVG4X.DFqvhGkn.js} +1 -1
- package/web/_astro/{ganttDiagram-6RSMTGT7.DRwb8HHZ.js → ganttDiagram-6RSMTGT7.ByJbhH6J.js} +1 -1
- package/web/_astro/{gitGraphDiagram-PVQCEYII.YfoPthfS.js → gitGraphDiagram-PVQCEYII.CInHjQR3.js} +1 -1
- package/web/_astro/index.COh_kBxN.css +1 -0
- package/web/_astro/{infoDiagram-5YYISTIA.DvFREck1.js → infoDiagram-5YYISTIA.Cm0dGVjJ.js} +1 -1
- package/web/_astro/{ishikawaDiagram-YF4QCWOH.BzCj9Q0-.js → ishikawaDiagram-YF4QCWOH.C3nOP6Sj.js} +1 -1
- package/web/_astro/{journeyDiagram-JHISSGLW.BDMT_AFb.js → journeyDiagram-JHISSGLW.4IvLa2MW.js} +1 -1
- package/web/_astro/{kanban-definition-UN3LZRKU.D6Fh3gj9.js → kanban-definition-UN3LZRKU.DS87Cl-k.js} +1 -1
- package/web/_astro/{linear.Dxw00fGQ.js → linear.MzmRrNER.js} +1 -1
- package/web/_astro/{mermaid.core.CsoxUWa0.js → mermaid.core.fK-WNswS.js} +4 -4
- package/web/_astro/{mindmap-definition-RKZ34NQL.CN8oeHLu.js → mindmap-definition-RKZ34NQL.BqipOHhY.js} +1 -1
- package/web/_astro/{pieDiagram-4H26LBE5.C7ADqQ_7.js → pieDiagram-4H26LBE5.0nBiPqFG.js} +1 -1
- package/web/_astro/{quadrantDiagram-W4KKPZXB.Cw9lKyHP.js → quadrantDiagram-W4KKPZXB.Szj7Zpx5.js} +1 -1
- package/web/_astro/{requirementDiagram-4Y6WPE33.B1ni-z4q.js → requirementDiagram-4Y6WPE33.D4UR6TWo.js} +1 -1
- package/web/_astro/{sankeyDiagram-5OEKKPKP.BhexdjVr.js → sankeyDiagram-5OEKKPKP.DAE7BdOs.js} +1 -1
- package/web/_astro/{sequenceDiagram-3UESZ5HK.C3Rm7dpQ.js → sequenceDiagram-3UESZ5HK.BP9ZVq25.js} +1 -1
- package/web/_astro/{stateDiagram-AJRCARHV.CkuMt27u.js → stateDiagram-AJRCARHV.BQW7mIxk.js} +1 -1
- package/web/_astro/{stateDiagram-v2-BHNVJYJU.Dc_aazUA.js → stateDiagram-v2-BHNVJYJU.PbK4nDih.js} +1 -1
- package/web/_astro/{timeline-definition-PNZ67QCA.ja7sUFhH.js → timeline-definition-PNZ67QCA.Ci_GvUpo.js} +1 -1
- package/web/_astro/{vennDiagram-CIIHVFJN.BxIshX_K.js → vennDiagram-CIIHVFJN.DNw1Y5qx.js} +1 -1
- package/web/_astro/{wardleyDiagram-YWT4CUSO.DfHlC0sh.js → wardleyDiagram-YWT4CUSO.CwlkaaNe.js} +1 -1
- package/web/_astro/{xychartDiagram-2RQKCTM6.DSnhHwCh.js → xychartDiagram-2RQKCTM6.MI6ZuiH_.js} +1 -1
- package/web/board-runtime.json +1 -1
- package/web/index.html +2 -2
- package/web/_astro/BoardApp.BVp-LYi6.js +0 -322
- package/web/_astro/channel.6IFIKBhu.js +0 -1
- package/web/_astro/index.CbNz17Sx.css +0 -1
|
@@ -443,7 +443,137 @@ function runFeatureTransition(env, options = {}) {
|
|
|
443
443
|
`);
|
|
444
444
|
return { status: syncStatus, statusFile: relStatusFile, exitCode: 0 };
|
|
445
445
|
}
|
|
446
|
-
var
|
|
446
|
+
var DOC_SYNC_SNAPSHOT_SUFFIX = "wrapup-doc-sync-snapshot.txt";
|
|
447
|
+
var DOC_SYNC_SCOPE_STATUS_SUFFIX = "wrapup-doc-sync-scope.status";
|
|
448
|
+
var DOC_SYNC_SUPERSESSION_STATUS_SUFFIX = "wrapup-doc-supersession.status";
|
|
449
|
+
var SUPERSESSION_PIN = "repo-wide-tests/adr-supersession.test.ts";
|
|
450
|
+
var GIT_UNAVAILABLE_MARKER = "# git-unavailable:";
|
|
451
|
+
var SNAPSHOT_MISSING = "snapshot-missing";
|
|
452
|
+
function porcelainPath(line) {
|
|
453
|
+
const body = line.length >= 3 ? line.slice(3) : line;
|
|
454
|
+
const arrow = body.indexOf(" -> ");
|
|
455
|
+
const target = arrow >= 0 ? body.slice(arrow + 4) : body;
|
|
456
|
+
const trimmed = target.trim();
|
|
457
|
+
return trimmed.startsWith('"') && trimmed.endsWith('"') && trimmed.length >= 2 ? trimmed.slice(1, -1) : trimmed;
|
|
458
|
+
}
|
|
459
|
+
function isDocSyncAllowedPath(path, allowedExtra) {
|
|
460
|
+
if (allowedExtra.includes(path))
|
|
461
|
+
return true;
|
|
462
|
+
if (!path.startsWith("docs/"))
|
|
463
|
+
return false;
|
|
464
|
+
return !path.startsWith("docs/tasks") && !path.startsWith("docs/features");
|
|
465
|
+
}
|
|
466
|
+
function docSyncScopeViolations(snapshot, current, allowedExtra) {
|
|
467
|
+
const before = new Set(snapshot.split(`
|
|
468
|
+
`).filter((line) => line.trim() !== ""));
|
|
469
|
+
const paths = new Set;
|
|
470
|
+
for (const line of current.split(`
|
|
471
|
+
`)) {
|
|
472
|
+
if (line.trim() === "" || before.has(line))
|
|
473
|
+
continue;
|
|
474
|
+
const path = porcelainPath(line);
|
|
475
|
+
if (!isDocSyncAllowedPath(path, allowedExtra))
|
|
476
|
+
paths.add(path);
|
|
477
|
+
}
|
|
478
|
+
return [...paths].sort();
|
|
479
|
+
}
|
|
480
|
+
function gitPorcelain(cwd) {
|
|
481
|
+
const result = spawnSync("git", ["status", "--porcelain", "-uall"], {
|
|
482
|
+
cwd,
|
|
483
|
+
encoding: "utf8"
|
|
484
|
+
});
|
|
485
|
+
if (result.error !== undefined)
|
|
486
|
+
return { ok: false, listing: "", detail: String(result.error) };
|
|
487
|
+
if (result.status !== 0)
|
|
488
|
+
return { ok: false, listing: "", detail: (result.stderr ?? "").trim() };
|
|
489
|
+
return { ok: true, listing: result.stdout ?? "", detail: "" };
|
|
490
|
+
}
|
|
491
|
+
function wrapupRunPaths(env, cwd) {
|
|
492
|
+
const runId = env.__runId ?? "";
|
|
493
|
+
const rel = (suffix) => join(".spur", "run", `${runId}-${suffix}`);
|
|
494
|
+
return { rel, abs: (suffix) => cwd ? join(cwd, rel(suffix)) : rel(suffix) };
|
|
495
|
+
}
|
|
496
|
+
function docSyncAllowedExtra(env) {
|
|
497
|
+
const runId = env.__runId ?? "";
|
|
498
|
+
return [
|
|
499
|
+
join(".spur", "run", `${runId}-wrapup-learnings.md`),
|
|
500
|
+
join(".spur", "run", `${runId}-${DOC_SYNC_SNAPSHOT_SUFFIX}`),
|
|
501
|
+
join(".spur", "run", `${runId}-${DOC_SYNC_SCOPE_STATUS_SUFFIX}`)
|
|
502
|
+
];
|
|
503
|
+
}
|
|
504
|
+
function runDocSyncSnapshot(env, options = {}) {
|
|
505
|
+
const cwd = options.cwd;
|
|
506
|
+
const paths = wrapupRunPaths(env, cwd);
|
|
507
|
+
mkdirSync(cwd ? join(cwd, ".spur", "run") : join(".spur", "run"), { recursive: true });
|
|
508
|
+
writeFileSync(paths.abs(DOC_SYNC_SNAPSHOT_SUFFIX), "");
|
|
509
|
+
const listing = gitPorcelain(cwd);
|
|
510
|
+
if (!listing.ok) {
|
|
511
|
+
writeFileSync(paths.abs(DOC_SYNC_SNAPSHOT_SUFFIX), `${GIT_UNAVAILABLE_MARKER} ${listing.detail}
|
|
512
|
+
`);
|
|
513
|
+
process.stderr.write(`doc-sync-snapshot: git status unavailable \u2014 scope check skips (${listing.detail})
|
|
514
|
+
`);
|
|
515
|
+
return { statusFile: paths.rel(DOC_SYNC_SNAPSHOT_SUFFIX), captured: false, exitCode: 0 };
|
|
516
|
+
}
|
|
517
|
+
writeFileSync(paths.abs(DOC_SYNC_SNAPSHOT_SUFFIX), listing.listing);
|
|
518
|
+
return { statusFile: paths.rel(DOC_SYNC_SNAPSHOT_SUFFIX), captured: true, exitCode: 0 };
|
|
519
|
+
}
|
|
520
|
+
function runDocSyncScope(env, options = {}) {
|
|
521
|
+
const cwd = options.cwd;
|
|
522
|
+
const paths = wrapupRunPaths(env, cwd);
|
|
523
|
+
mkdirSync(cwd ? join(cwd, ".spur", "run") : join(".spur", "run"), { recursive: true });
|
|
524
|
+
const statusFile = paths.rel(DOC_SYNC_SCOPE_STATUS_SUFFIX);
|
|
525
|
+
const write = (status, detail) => {
|
|
526
|
+
writeFileSync(paths.abs(DOC_SYNC_SCOPE_STATUS_SUFFIX), `${detail}
|
|
527
|
+
`);
|
|
528
|
+
if (status !== "PASS")
|
|
529
|
+
process.stderr.write(`doc-sync-scope: ${detail}
|
|
530
|
+
`);
|
|
531
|
+
return { status, statusFile, violations: status === "PASS" ? [] : [detail], exitCode: 0 };
|
|
532
|
+
};
|
|
533
|
+
const snapshot = readFileSyncSafe(paths.abs(DOC_SYNC_SNAPSHOT_SUFFIX));
|
|
534
|
+
if (snapshot === null) {
|
|
535
|
+
return write("scope-violation", `scope-violation: ${SNAPSHOT_MISSING}`);
|
|
536
|
+
}
|
|
537
|
+
if (snapshot.startsWith(GIT_UNAVAILABLE_MARKER)) {
|
|
538
|
+
process.stderr.write(`doc-sync-scope: entry snapshot recorded no git listing \u2014 scope check skipped
|
|
539
|
+
`);
|
|
540
|
+
return write("PASS", "PASS");
|
|
541
|
+
}
|
|
542
|
+
const current = gitPorcelain(cwd);
|
|
543
|
+
if (!current.ok) {
|
|
544
|
+
return write("scope-violation", `scope-violation: git-unavailable (${current.detail})`);
|
|
545
|
+
}
|
|
546
|
+
const violations = docSyncScopeViolations(snapshot, current.listing, docSyncAllowedExtra(env));
|
|
547
|
+
if (violations.length === 0)
|
|
548
|
+
return write("PASS", "PASS");
|
|
549
|
+
return write("scope-violation", `scope-violation: ${violations.join(" ")}`);
|
|
550
|
+
}
|
|
551
|
+
function runDocSupersessionCheck(env, options = {}) {
|
|
552
|
+
const cwd = options.cwd;
|
|
553
|
+
const paths = wrapupRunPaths(env, cwd);
|
|
554
|
+
mkdirSync(cwd ? join(cwd, ".spur", "run") : join(".spur", "run"), { recursive: true });
|
|
555
|
+
const statusFile = paths.rel(DOC_SYNC_SUPERSESSION_STATUS_SUFFIX);
|
|
556
|
+
const pinPath = cwd ? join(cwd, SUPERSESSION_PIN) : SUPERSESSION_PIN;
|
|
557
|
+
if (!existsSync(pinPath)) {
|
|
558
|
+
writeFileSync(paths.abs(DOC_SYNC_SUPERSESSION_STATUS_SUFFIX), `PASS
|
|
559
|
+
`);
|
|
560
|
+
return { status: "PASS", statusFile, exitCode: 0 };
|
|
561
|
+
}
|
|
562
|
+
const pin = spawnSync("bun", ["test", SUPERSESSION_PIN], { cwd, encoding: "utf8" });
|
|
563
|
+
if (pin.status === 0) {
|
|
564
|
+
writeFileSync(paths.abs(DOC_SYNC_SUPERSESSION_STATUS_SUFFIX), `PASS
|
|
565
|
+
`);
|
|
566
|
+
return { status: "PASS", statusFile, exitCode: 0 };
|
|
567
|
+
}
|
|
568
|
+
writeFileSync(paths.abs(DOC_SYNC_SUPERSESSION_STATUS_SUFFIX), `supersession-pin-failed: ${SUPERSESSION_PIN}
|
|
569
|
+
`);
|
|
570
|
+
const detail = `${pin.stdout ?? ""}${pin.stderr ?? ""}`.trim();
|
|
571
|
+
process.stderr.write(`doc-supersession: ${SUPERSESSION_PIN} failed (exit ${String(pin.status ?? 1)}) \u2014 the doc-sync step introduced or kept superseded index content:
|
|
572
|
+
${detail.slice(-2000)}
|
|
573
|
+
`);
|
|
574
|
+
return { status: "supersession-pin-failed", statusFile, exitCode: 0 };
|
|
575
|
+
}
|
|
576
|
+
var WRAPUP_STEPS_USAGE = "usage: wrapup-steps.ts <resolve|route-reason|metrics|feature-transition|doc-sync-snapshot|doc-sync-scope|doc-supersession> (env: __runId, tasks, mode, feature, featureGateCmd, spurBin)";
|
|
447
577
|
function main(argv, env = getEnvVars(), options = {}) {
|
|
448
578
|
const sub = argv[0];
|
|
449
579
|
if (sub === "resolve")
|
|
@@ -456,6 +586,12 @@ function main(argv, env = getEnvVars(), options = {}) {
|
|
|
456
586
|
}
|
|
457
587
|
if (sub === "feature-transition")
|
|
458
588
|
return runFeatureTransition(env, options).exitCode;
|
|
589
|
+
if (sub === "doc-sync-snapshot")
|
|
590
|
+
return runDocSyncSnapshot(env, options).exitCode;
|
|
591
|
+
if (sub === "doc-sync-scope")
|
|
592
|
+
return runDocSyncScope(env, options).exitCode;
|
|
593
|
+
if (sub === "doc-supersession")
|
|
594
|
+
return runDocSupersessionCheck(env, options).exitCode;
|
|
459
595
|
process.stderr.write(`${WRAPUP_STEPS_USAGE}
|
|
460
596
|
`);
|
|
461
597
|
return 2;
|
|
@@ -469,11 +605,23 @@ export {
|
|
|
469
605
|
taskStatusOf,
|
|
470
606
|
runMetrics,
|
|
471
607
|
runFeatureTransition,
|
|
608
|
+
runDocSyncSnapshot,
|
|
609
|
+
runDocSyncScope,
|
|
610
|
+
runDocSupersessionCheck,
|
|
472
611
|
resolveTasks,
|
|
473
612
|
preflightFeature,
|
|
613
|
+
porcelainPath,
|
|
474
614
|
main,
|
|
475
615
|
jqPick,
|
|
616
|
+
isDocSyncAllowedPath,
|
|
617
|
+
docSyncScopeViolations,
|
|
476
618
|
classifySync,
|
|
477
619
|
WRAPUP_STEPS_USAGE,
|
|
478
|
-
WBS_PATTERN
|
|
620
|
+
WBS_PATTERN,
|
|
621
|
+
SUPERSESSION_PIN,
|
|
622
|
+
SNAPSHOT_MISSING,
|
|
623
|
+
GIT_UNAVAILABLE_MARKER,
|
|
624
|
+
DOC_SYNC_SUPERSESSION_STATUS_SUFFIX,
|
|
625
|
+
DOC_SYNC_SNAPSHOT_SUFFIX,
|
|
626
|
+
DOC_SYNC_SCOPE_STATUS_SUFFIX
|
|
479
627
|
};
|
|
@@ -14,6 +14,9 @@
|
|
|
14
14
|
* - `.spur/memory/wrapup-metrics.jsonl` one row per task (metrics)
|
|
15
15
|
* - `<runId>-wrapup-metrics.status` `PASS`/`FAIL` (metrics)
|
|
16
16
|
* - `<runId>-wrapup-sync.status` `PASS`/`FAIL` (feature-transition)
|
|
17
|
+
* - `<runId>-wrapup-doc-sync-snapshot.txt` working listing at doc-sync entry (doc-sync-snapshot)
|
|
18
|
+
* - `<runId>-wrapup-doc-sync-scope.status` `PASS`/`scope-violation: <paths>` (doc-sync-scope)
|
|
19
|
+
* - `<runId>-wrapup-doc-supersession.status` `PASS`/`supersession-pin-failed: <pin>` (doc-supersession)
|
|
17
20
|
*
|
|
18
21
|
* Environment comes from the workflow vars: `__runId`, `tasks`, `feature`, `featureGateCmd`
|
|
19
22
|
* and `spurBin` (split on whitespace into a command plus prefix args, so
|
|
@@ -668,8 +671,204 @@ export function runFeatureTransition(env: WrapupStepsEnv, options: WrapupStepsOp
|
|
|
668
671
|
return { status: syncStatus, statusFile: relStatusFile, exitCode: 0 };
|
|
669
672
|
}
|
|
670
673
|
|
|
674
|
+
/** Run-scoped snapshot of the working listing taken at doc-sync entry (1147 R1). */
|
|
675
|
+
export const DOC_SYNC_SNAPSHOT_SUFFIX = 'wrapup-doc-sync-snapshot.txt';
|
|
676
|
+
/** Run-scoped verdict of the post-doc-sync write-scope check (1147 R1). */
|
|
677
|
+
export const DOC_SYNC_SCOPE_STATUS_SUFFIX = 'wrapup-doc-sync-scope.status';
|
|
678
|
+
/** Run-scoped verdict of the post-doc-sync supersession-pin re-run (1147 R2). */
|
|
679
|
+
export const DOC_SYNC_SUPERSESSION_STATUS_SUFFIX = 'wrapup-doc-supersession.status';
|
|
680
|
+
/**
|
|
681
|
+
* The supersession pins doc-sync must not resurrect (1147 R2). Re-run right after doc-sync so a
|
|
682
|
+
* re-added delinked index row is attributed to doc-sync instead of to the later `doc-tripwire`
|
|
683
|
+
* backstop, which stays unchanged.
|
|
684
|
+
*/
|
|
685
|
+
export const SUPERSESSION_PIN = 'repo-wide-tests/adr-supersession.test.ts';
|
|
686
|
+
/** Snapshot marker for "git could not list the tree" — the check then skips instead of guessing. */
|
|
687
|
+
export const GIT_UNAVAILABLE_MARKER = '# git-unavailable:';
|
|
688
|
+
|
|
689
|
+
/** Snapshot marker the scope check reports when doc-sync's entry snapshot is absent (fail closed). */
|
|
690
|
+
export const SNAPSHOT_MISSING = 'snapshot-missing';
|
|
691
|
+
|
|
692
|
+
/**
|
|
693
|
+
* One `git status --porcelain` line → the repo-relative path it names. A rename/copy line
|
|
694
|
+
* (`R old -> new`) reports its target, and a quoted path is unquoted (1147 R1).
|
|
695
|
+
*/
|
|
696
|
+
export function porcelainPath(line: string): string {
|
|
697
|
+
const body = line.length >= 3 ? line.slice(3) : line;
|
|
698
|
+
const arrow = body.indexOf(' -> ');
|
|
699
|
+
const target = arrow >= 0 ? body.slice(arrow + 4) : body;
|
|
700
|
+
const trimmed = target.trim();
|
|
701
|
+
return trimmed.startsWith('"') && trimmed.endsWith('"') && trimmed.length >= 2 ? trimmed.slice(1, -1) : trimmed;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
/**
|
|
705
|
+
* doc-sync's declared write scope (1147 R1): `docs/**` except the task/feature corpus, the
|
|
706
|
+
* learnings capture the step declares, and the guard's own snapshot/status bookkeeping (never
|
|
707
|
+
* doc-sync edits, and present in the listing on a repeated check).
|
|
708
|
+
*/
|
|
709
|
+
export function isDocSyncAllowedPath(path: string, allowedExtra: readonly string[]): boolean {
|
|
710
|
+
if (allowedExtra.includes(path)) return true;
|
|
711
|
+
if (!path.startsWith('docs/')) return false;
|
|
712
|
+
return !path.startsWith('docs/tasks') && !path.startsWith('docs/features');
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/**
|
|
716
|
+
* Paths doc-sync newly dirtied: the current porcelain listing minus the entry snapshot's lines.
|
|
717
|
+
* A line present in both listings is pre-existing dirt (a sibling task's uncommitted work) and is
|
|
718
|
+
* never attributed to this pass (1147 R1).
|
|
719
|
+
*/
|
|
720
|
+
export function docSyncScopeViolations(snapshot: string, current: string, allowedExtra: readonly string[]): string[] {
|
|
721
|
+
const before = new Set(snapshot.split('\n').filter((line) => line.trim() !== ''));
|
|
722
|
+
const paths = new Set<string>();
|
|
723
|
+
for (const line of current.split('\n')) {
|
|
724
|
+
if (line.trim() === '' || before.has(line)) continue;
|
|
725
|
+
const path = porcelainPath(line);
|
|
726
|
+
if (!isDocSyncAllowedPath(path, allowedExtra)) paths.add(path);
|
|
727
|
+
}
|
|
728
|
+
return [...paths].sort();
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
/** `git status --porcelain -uall` — the listing both the snapshot and the scope check compare. */
|
|
732
|
+
function gitPorcelain(cwd?: string): { ok: boolean; listing: string; detail: string } {
|
|
733
|
+
const result = spawnSync('git', ['status', '--porcelain', '-uall'], {
|
|
734
|
+
cwd,
|
|
735
|
+
encoding: 'utf8',
|
|
736
|
+
});
|
|
737
|
+
if (result.error !== undefined) return { ok: false, listing: '', detail: String(result.error) };
|
|
738
|
+
if (result.status !== 0) return { ok: false, listing: '', detail: (result.stderr ?? '').trim() };
|
|
739
|
+
return { ok: true, listing: result.stdout ?? '', detail: '' };
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
interface WrapupRunPaths {
|
|
743
|
+
rel: (suffix: string) => string;
|
|
744
|
+
abs: (suffix: string) => string;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
function wrapupRunPaths(env: WrapupStepsEnv, cwd?: string): WrapupRunPaths {
|
|
748
|
+
const runId = env.__runId ?? '';
|
|
749
|
+
const rel = (suffix: string): string => join('.spur', 'run', `${runId}-${suffix}`);
|
|
750
|
+
return { rel, abs: (suffix: string) => (cwd ? join(cwd, rel(suffix)) : rel(suffix)) };
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
/** The four path shapes this check knows are not doc-sync edits (1147 R1). */
|
|
754
|
+
function docSyncAllowedExtra(env: WrapupStepsEnv): string[] {
|
|
755
|
+
const runId = env.__runId ?? '';
|
|
756
|
+
return [
|
|
757
|
+
join('.spur', 'run', `${runId}-wrapup-learnings.md`),
|
|
758
|
+
join('.spur', 'run', `${runId}-${DOC_SYNC_SNAPSHOT_SUFFIX}`),
|
|
759
|
+
join('.spur', 'run', `${runId}-${DOC_SYNC_SCOPE_STATUS_SUFFIX}`),
|
|
760
|
+
];
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
/** Outcome of `doc-sync-snapshot`: the recorded entry listing (or a git-unavailable marker). */
|
|
764
|
+
export interface DocSyncSnapshotResult {
|
|
765
|
+
statusFile: string;
|
|
766
|
+
/** True when the listing was captured; false when git could not list the tree. */
|
|
767
|
+
captured: boolean;
|
|
768
|
+
exitCode: number;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
/**
|
|
772
|
+
* `doc-sync-snapshot` — record the working listing at doc-sync entry (1147 R1), before the model
|
|
773
|
+
* query runs, so the post-doc-sync check can attribute only this step's writes. Never aborts: a
|
|
774
|
+
* git failure is recorded in the snapshot (the check then skips rather than guessing).
|
|
775
|
+
*/
|
|
776
|
+
export function runDocSyncSnapshot(env: WrapupStepsEnv, options: WrapupStepsOptions = {}): DocSyncSnapshotResult {
|
|
777
|
+
const cwd = options.cwd;
|
|
778
|
+
const paths = wrapupRunPaths(env, cwd);
|
|
779
|
+
mkdirSync(cwd ? join(cwd, '.spur', 'run') : join('.spur', 'run'), { recursive: true });
|
|
780
|
+
// Create the file BEFORE listing so it is present in both listings (never a false violation).
|
|
781
|
+
writeFileSync(paths.abs(DOC_SYNC_SNAPSHOT_SUFFIX), '');
|
|
782
|
+
const listing = gitPorcelain(cwd);
|
|
783
|
+
if (!listing.ok) {
|
|
784
|
+
writeFileSync(paths.abs(DOC_SYNC_SNAPSHOT_SUFFIX), `${GIT_UNAVAILABLE_MARKER} ${listing.detail}\n`);
|
|
785
|
+
process.stderr.write(`doc-sync-snapshot: git status unavailable — scope check skips (${listing.detail})\n`);
|
|
786
|
+
return { statusFile: paths.rel(DOC_SYNC_SNAPSHOT_SUFFIX), captured: false, exitCode: 0 };
|
|
787
|
+
}
|
|
788
|
+
writeFileSync(paths.abs(DOC_SYNC_SNAPSHOT_SUFFIX), listing.listing);
|
|
789
|
+
return { statusFile: paths.rel(DOC_SYNC_SNAPSHOT_SUFFIX), captured: true, exitCode: 0 };
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
/** Outcome of `doc-sync-scope`: the recorded verdict plus the offending paths it named. */
|
|
793
|
+
export interface DocSyncScopeResult {
|
|
794
|
+
status: 'PASS' | 'scope-violation';
|
|
795
|
+
statusFile: string;
|
|
796
|
+
violations: string[];
|
|
797
|
+
exitCode: number;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
/**
|
|
801
|
+
* `doc-sync-scope` — the deterministic write-scope guard (1147 R1): diff the working listing
|
|
802
|
+
* against the entry snapshot and fail the step on any path outside the declared scope. The
|
|
803
|
+
* violating edits are reported, never reverted (the operator decides); the verdict lives in the
|
|
804
|
+
* status file so the workflow guard routes on it.
|
|
805
|
+
*/
|
|
806
|
+
export function runDocSyncScope(env: WrapupStepsEnv, options: WrapupStepsOptions = {}): DocSyncScopeResult {
|
|
807
|
+
const cwd = options.cwd;
|
|
808
|
+
const paths = wrapupRunPaths(env, cwd);
|
|
809
|
+
mkdirSync(cwd ? join(cwd, '.spur', 'run') : join('.spur', 'run'), { recursive: true });
|
|
810
|
+
const statusFile = paths.rel(DOC_SYNC_SCOPE_STATUS_SUFFIX);
|
|
811
|
+
const write = (status: DocSyncScopeResult['status'], detail: string): DocSyncScopeResult => {
|
|
812
|
+
writeFileSync(paths.abs(DOC_SYNC_SCOPE_STATUS_SUFFIX), `${detail}\n`);
|
|
813
|
+
if (status !== 'PASS') process.stderr.write(`doc-sync-scope: ${detail}\n`);
|
|
814
|
+
return { status, statusFile, violations: status === 'PASS' ? [] : [detail], exitCode: 0 };
|
|
815
|
+
};
|
|
816
|
+
|
|
817
|
+
const snapshot = readFileSyncSafe(paths.abs(DOC_SYNC_SNAPSHOT_SUFFIX));
|
|
818
|
+
if (snapshot === null) {
|
|
819
|
+
return write('scope-violation', `scope-violation: ${SNAPSHOT_MISSING}`);
|
|
820
|
+
}
|
|
821
|
+
if (snapshot.startsWith(GIT_UNAVAILABLE_MARKER)) {
|
|
822
|
+
process.stderr.write('doc-sync-scope: entry snapshot recorded no git listing — scope check skipped\n');
|
|
823
|
+
return write('PASS', 'PASS');
|
|
824
|
+
}
|
|
825
|
+
const current = gitPorcelain(cwd);
|
|
826
|
+
if (!current.ok) {
|
|
827
|
+
return write('scope-violation', `scope-violation: git-unavailable (${current.detail})`);
|
|
828
|
+
}
|
|
829
|
+
const violations = docSyncScopeViolations(snapshot, current.listing, docSyncAllowedExtra(env));
|
|
830
|
+
if (violations.length === 0) return write('PASS', 'PASS');
|
|
831
|
+
return write('scope-violation', `scope-violation: ${violations.join(' ')}`);
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
/** Outcome of `doc-supersession`: the recorded verdict of the supersession-pin re-run. */
|
|
835
|
+
export interface DocSupersessionResult {
|
|
836
|
+
status: 'PASS' | 'supersession-pin-failed';
|
|
837
|
+
statusFile: string;
|
|
838
|
+
exitCode: number;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
/**
|
|
842
|
+
* `doc-supersession` — re-run the supersession pins right after doc-sync (1147 R2). A doc-sync
|
|
843
|
+
* that resurrects a deliberately delinked index row fails HERE, named with the pin, instead of at
|
|
844
|
+
* the later repo-wide `doc-tripwire` backstop (which is never weakened). A project without the pin
|
|
845
|
+
* skips: this check must stay inert outside the spur source repo.
|
|
846
|
+
*/
|
|
847
|
+
export function runDocSupersessionCheck(env: WrapupStepsEnv, options: WrapupStepsOptions = {}): DocSupersessionResult {
|
|
848
|
+
const cwd = options.cwd;
|
|
849
|
+
const paths = wrapupRunPaths(env, cwd);
|
|
850
|
+
mkdirSync(cwd ? join(cwd, '.spur', 'run') : join('.spur', 'run'), { recursive: true });
|
|
851
|
+
const statusFile = paths.rel(DOC_SYNC_SUPERSESSION_STATUS_SUFFIX);
|
|
852
|
+
const pinPath = cwd ? join(cwd, SUPERSESSION_PIN) : SUPERSESSION_PIN;
|
|
853
|
+
if (!existsSync(pinPath)) {
|
|
854
|
+
writeFileSync(paths.abs(DOC_SYNC_SUPERSESSION_STATUS_SUFFIX), 'PASS\n');
|
|
855
|
+
return { status: 'PASS', statusFile, exitCode: 0 };
|
|
856
|
+
}
|
|
857
|
+
const pin = spawnSync('bun', ['test', SUPERSESSION_PIN], { cwd, encoding: 'utf8' });
|
|
858
|
+
if (pin.status === 0) {
|
|
859
|
+
writeFileSync(paths.abs(DOC_SYNC_SUPERSESSION_STATUS_SUFFIX), 'PASS\n');
|
|
860
|
+
return { status: 'PASS', statusFile, exitCode: 0 };
|
|
861
|
+
}
|
|
862
|
+
writeFileSync(paths.abs(DOC_SYNC_SUPERSESSION_STATUS_SUFFIX), `supersession-pin-failed: ${SUPERSESSION_PIN}\n`);
|
|
863
|
+
const detail = `${pin.stdout ?? ''}${pin.stderr ?? ''}`.trim();
|
|
864
|
+
process.stderr.write(
|
|
865
|
+
`doc-supersession: ${SUPERSESSION_PIN} failed (exit ${String(pin.status ?? 1)}) — the doc-sync step introduced or kept superseded index content:\n${detail.slice(-2000)}\n`,
|
|
866
|
+
);
|
|
867
|
+
return { status: 'supersession-pin-failed', statusFile, exitCode: 0 };
|
|
868
|
+
}
|
|
869
|
+
|
|
671
870
|
export const WRAPUP_STEPS_USAGE =
|
|
672
|
-
'usage: wrapup-steps.ts <resolve|route-reason|metrics|feature-transition> (env: __runId, tasks, mode, feature, featureGateCmd, spurBin)';
|
|
871
|
+
'usage: wrapup-steps.ts <resolve|route-reason|metrics|feature-transition|doc-sync-snapshot|doc-sync-scope|doc-supersession> (env: __runId, tasks, mode, feature, featureGateCmd, spurBin)';
|
|
673
872
|
|
|
674
873
|
export function main(argv: string[], env: WrapupStepsEnv = getEnvVars(), options: WrapupStepsOptions = {}): number {
|
|
675
874
|
const sub = argv[0];
|
|
@@ -680,6 +879,9 @@ export function main(argv: string[], env: WrapupStepsEnv = getEnvVars(), options
|
|
|
680
879
|
return 0;
|
|
681
880
|
}
|
|
682
881
|
if (sub === 'feature-transition') return runFeatureTransition(env, options).exitCode;
|
|
882
|
+
if (sub === 'doc-sync-snapshot') return runDocSyncSnapshot(env, options).exitCode;
|
|
883
|
+
if (sub === 'doc-sync-scope') return runDocSyncScope(env, options).exitCode;
|
|
884
|
+
if (sub === 'doc-supersession') return runDocSupersessionCheck(env, options).exitCode;
|
|
683
885
|
process.stderr.write(`${WRAPUP_STEPS_USAGE}\n`);
|
|
684
886
|
return 2;
|
|
685
887
|
}
|
|
@@ -26,7 +26,6 @@ metadata:
|
|
|
26
26
|
see_also:
|
|
27
27
|
- sp:source-driven-development
|
|
28
28
|
- sp:spur-cli
|
|
29
|
-
- sp:wayfinder
|
|
30
29
|
---
|
|
31
30
|
|
|
32
31
|
# sp:brainstorm — Structured Ideation Workflow
|
|
@@ -35,7 +34,6 @@ Generate solution options with trade-offs, recommendations, and confidence scori
|
|
|
35
34
|
|
|
36
35
|
**Key distinction:**
|
|
37
36
|
- **`sp:brainstorm`** = Ideation: generate approaches with trade-offs
|
|
38
|
-
- **`sp:wayfinder`** = Wayfinding: chart a multi-session map when the destination itself is foggy
|
|
39
37
|
- **research** = verify and synthesize information (delegate inline by default — see [inline-default contract](../spur-dev/references/cross-cutting.md#inline-default-execution-surface))
|
|
40
38
|
- **`sp:spur-dev`** = Task creation: structured task breakdown (planning half)
|
|
41
39
|
- **`sp:source-driven-development`** = Verification: source-first claim validation
|
|
@@ -65,14 +63,13 @@ Activate sp:brainstorm when:
|
|
|
65
63
|
| "research approaches" | User wants evidence-backed options |
|
|
66
64
|
| "what are my options?" | User wants multiple solutions |
|
|
67
65
|
| "how should I approach X?" | User wants recommendation with reasoning |
|
|
68
|
-
| "wayfind" / "chart a course" | User needs a multi-session investigation map — escalate to `sp:wayfinder` (Phase 2) |
|
|
69
66
|
|
|
70
67
|
**NOT for:**
|
|
71
68
|
- Pure research (delegate to research skills inline by default; escalate via `spur agent run` only on a subprocess trigger)
|
|
72
69
|
- Task creation without ideation (use `sp:spur-dev` instead)
|
|
73
70
|
- Fact-checking or verification only (use `sp:source-driven-development` instead)
|
|
74
71
|
- Task file operations (use `sp:spur-cli` instead)
|
|
75
|
-
- Multi-session investigation when the destination itself is foggy (
|
|
72
|
+
- Multi-session investigation when the destination itself is foggy (recommend `/sp:dev-find-way`; see [Scope Check](#scope-check-end-of-phase-1))
|
|
76
73
|
|
|
77
74
|
## Core Principles
|
|
78
75
|
|
|
@@ -317,51 +314,16 @@ non-deterministic intent into a reliable sequence, not bare forwarders (ADR-016)
|
|
|
317
314
|
|
|
318
315
|
---
|
|
319
316
|
|
|
320
|
-
##
|
|
317
|
+
## Scope Check (end of Phase 1)
|
|
321
318
|
|
|
322
|
-
|
|
319
|
+
Before ideation, ask: **"Can this be spec'd in one session, or is the destination itself still foggy?"**
|
|
323
320
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
**Signals that wayfinding is needed:**
|
|
331
|
-
- The topic touches ≥3 subsystems or unknown boundaries
|
|
332
|
-
- Key decisions depend on research not yet done
|
|
333
|
-
- The operator can describe the goal but not the shape of the solution
|
|
334
|
-
- Multiple "it depends" answers in the discovery interview
|
|
335
|
-
- The operator uses fog language: "I'm not sure yet", "we need to explore", "it depends on what we find"
|
|
336
|
-
|
|
337
|
-
**Signals that standard ideation suffices:**
|
|
338
|
-
- The destination is clear; only the approach is in question
|
|
339
|
-
- All key decisions can be made from existing knowledge
|
|
340
|
-
- The operator can enumerate the constraints and trade-offs
|
|
341
|
-
|
|
342
|
-
### Escalation Path
|
|
343
|
-
|
|
344
|
-
When the scope check indicates a foggy destination, offer the escalation:
|
|
345
|
-
|
|
346
|
-
> *"This is a multi-session investigation. Want me to chart a wayfinder map so we can work through it one decision at a time?"*
|
|
347
|
-
|
|
348
|
-
The operator **confirms** before wayfinding begins — never silently escalate. A 30-minute quick-answer need might touch a big domain without requiring a multi-session map.
|
|
349
|
-
|
|
350
|
-
On confirmation, delegate to `sp:wayfinder` for the "Chart the map" mode. The resolved decision tree from Phase 1 seeds the map's **## Notes** and initial **## Not yet specified** sections.
|
|
351
|
-
|
|
352
|
-
### `--wayfind` Flag
|
|
353
|
-
|
|
354
|
-
When `/sp:dev-brainstorm` is invoked with `--wayfind`, the scope check is **skipped** — the operator has pre-approved the escalation. After the discovery interview, proceed directly to `sp:wayfinder` charting without the confirmation prompt.
|
|
355
|
-
|
|
356
|
-
Use `--wayfind` when:
|
|
357
|
-
- The operator already knows this is a multi-session investigation
|
|
358
|
-
- A previous session recommended wayfinding
|
|
359
|
-
- The topic is explicitly exploratory ("explore the solution space for X")
|
|
360
|
-
|
|
361
|
-
### Integration with the Design Approval Gate
|
|
362
|
-
|
|
363
|
-
A wayfinding escalation **replaces** the standard ideation output. The map feature (with its destination, notes, fog, and child tickets) is the artifact. The `needs_design` signal is not emitted — wayfinding defers design until the route to the destination is clear. When the last ticket resolves, the final session hands off to standard `sp:brainstorm` → `sp:spec-decomposition` with a now-clear destination.
|
|
321
|
+
Fog signals: the topic touches ≥3 subsystems or unknown boundaries; key decisions depend on research
|
|
322
|
+
not yet done; the operator can name the goal but not the solution's shape; repeated "it depends"
|
|
323
|
+
answers. When they dominate, **stop** — do not ideate toward a destination that isn't there. Report
|
|
324
|
+
the resolved decision tree and recommend `/sp:dev-find-way "<topic>"`, which owns multi-session
|
|
325
|
+
investigation maps. Brainstorm never charts a map itself.
|
|
364
326
|
|
|
365
327
|
---
|
|
366
328
|
|
|
367
|
-
**Remember:** Ideation ≠ Research. Generate approaches with trade-offs. Delegate verification to `sp:source-driven-development`. Delegate synthesis/research inline (escalate to `spur agent run` only on a subprocess trigger). Delegate task creation to `sp:spur-dev`. When the destination itself is foggy,
|
|
329
|
+
**Remember:** Ideation ≠ Research. Generate approaches with trade-offs. Delegate verification to `sp:source-driven-development`. Delegate synthesis/research inline (escalate to `spur agent run` only on a subprocess trigger). Delegate task creation to `sp:spur-dev`. When the destination itself is foggy, stop and recommend `/sp:dev-find-way` — never force a spec that isn't ready.
|
|
@@ -40,6 +40,7 @@ A git worktree gives you an isolated working directory for a branch without stas
|
|
|
40
40
|
```bash
|
|
41
41
|
git worktree add ../<project>-<branch> <branch>
|
|
42
42
|
cd ../<project>-<branch>
|
|
43
|
+
spur projects add . 2>/dev/null || true # Register in projects.json for the Board project switcher
|
|
43
44
|
```
|
|
44
45
|
|
|
45
46
|
**When to use worktrees:**
|
|
@@ -28,6 +28,7 @@ A git worktree gives you an isolated working directory linked to the same reposi
|
|
|
28
28
|
```bash
|
|
29
29
|
git worktree add ../project-hotfix hotfix/critical-fix
|
|
30
30
|
cd ../project-hotfix && bun install --frozen-lockfile
|
|
31
|
+
spur projects add . 2>/dev/null || true # list it in the Board project switcher
|
|
31
32
|
```
|
|
32
33
|
|
|
33
34
|
The worktree directory is created alongside the main project. Name it `<project>-<branch>` for clarity.
|
|
@@ -104,6 +104,13 @@ only burns wall clock and context budget.
|
|
|
104
104
|
|
|
105
105
|
- **Run only targeted probes** to validate your changes: `bun test <file>`,
|
|
106
106
|
`bun test <file> --test-name-pattern "<test>"`, or `bunx tsc --noEmit` on a single package.
|
|
107
|
+
- **Run Spur through this tree's own entry point, never a bare `spur` (task 1110 R2).** Use the
|
|
108
|
+
invocation the pipeline supplies (`${vars.spurBin}`, e.g. `bun apps/cli/src/index.ts`) from the
|
|
109
|
+
execution tree — a bare `spur` on `PATH` resolves to the published bundle in `~/.bun/bin` or
|
|
110
|
+
`~/node_modules`, so a guard, a `task check` or a `task record` silently runs a different build
|
|
111
|
+
against a different tree. Same rule for the working directory: each shell call starts fresh, so
|
|
112
|
+
re-select the execution tree inside the call (`cd -- "$TREE"` plus the identity check) instead of
|
|
113
|
+
relying on a `cd` a previous call left behind.
|
|
107
114
|
- **NEVER run** `bun run test`, `bun run spur-check`, `bun run check`, or any other full-suite /
|
|
108
115
|
project-gate command from inside implement. These belong to the pipeline's `test` hop.
|
|
109
116
|
- **Full-suite budget: at most 2 per task** (task 0436 R2) — counted across the whole task run
|
|
@@ -117,7 +124,35 @@ only burns wall clock and context budget.
|
|
|
117
124
|
- If a targeted probe reveals a failure you cannot fix within implement scope, note it in
|
|
118
125
|
`## Solution` and let the `test` hop's fixall handle it — do not pre-empt the gate.
|
|
119
126
|
|
|
120
|
-
|
|
127
|
+
### Public-surface probe (task 1135 R1/R6/R8)
|
|
128
|
+
|
|
129
|
+
Narrow unit tests prove the seam you refactored, not the surface a user calls. Before this stage
|
|
130
|
+
reports done it runs **one end-to-end probe per new or changed public surface** — a CLI verb/flag,
|
|
131
|
+
an HTTP route, or a plugin script entry:
|
|
132
|
+
|
|
133
|
+
- against a **throwaway project root** (a temp dir), never the repo corpus or the execution tree's
|
|
134
|
+
tracked files;
|
|
135
|
+
- bounded: 120 s per invocation, output capped at 8 KiB, no network beyond the CLI under test;
|
|
136
|
+
- recorded as acceptance evidence: the **exact command** and its **observed output**;
|
|
137
|
+
- a failing probe is evidence, never a reason to mutate the corpus.
|
|
138
|
+
|
|
139
|
+
A service-layer test suite does **not** satisfy this. The gap it closes is measured: 19 service tests
|
|
140
|
+
green while `task create --feature <verifying>` failed end-to-end (task 1135 Background). If the stage
|
|
141
|
+
changed no public surface, report the single line
|
|
142
|
+
`probe: not-applicable (no public surface changed)` instead.
|
|
143
|
+
|
|
144
|
+
**Changed-path static self-check (R8)** — over the changed paths only, before reporting done:
|
|
145
|
+
|
|
146
|
+
- `spur rule run` with the configured pre-check preset (catches a direct `process.env` read, the
|
|
147
|
+
`env-var-hygiene` rule, and its siblings);
|
|
148
|
+
- `bun run script-contract-check` when a plugin script was touched (a stale generated `.mjs` twin);
|
|
149
|
+
- the paired-script parity test when `package.json` changed (a half-updated `spur-check` /
|
|
150
|
+
`spur-check-new` chain).
|
|
151
|
+
|
|
152
|
+
The single full project check stays the pipeline `test` hop's job (see the section above); these are
|
|
153
|
+
changed-path checks, not that gate.
|
|
154
|
+
|
|
155
|
+
## Changed-path targeted checks (dependency-aware verification, task 0510 R3 / task 1142 R3)
|
|
121
156
|
|
|
122
157
|
"Run the narrow test first" needs a second half: *which* narrow tests, when a change to a shared
|
|
123
158
|
surface can break a downstream consumer. The matrix below is dependency-aware — domain → app →
|
|
@@ -126,6 +161,12 @@ implement. **It augments narrow behavior tests; it never authorizes `bun run spu
|
|
|
126
161
|
`bun run test`, or another full project check inside implement** — the pipeline's `test` hop owns
|
|
127
162
|
that single full gate.
|
|
128
163
|
|
|
164
|
+
**Worker iteration check order (task 1142 R3):**
|
|
165
|
+
1. The **changed-path matrix** below (narrow behavior tests + affected consumers);
|
|
166
|
+
2. the resolved `quality-gate` script in `light` mode (source-repo probe, else the installed twin) (biome on changed files, per-workspace typecheck, rule presets `rules:pre`/`rules:post`, related tests).
|
|
167
|
+
|
|
168
|
+
Neither step takes the host-wide full-gate lock. The full gate is named only for the pipeline's `test` and `test-recheck` hops.
|
|
169
|
+
|
|
129
170
|
| Changed surface | Required targeted tests | Required typechecks |
|
|
130
171
|
| --- | --- | --- |
|
|
131
172
|
| `packages/domain/src/**` public type/query | affected domain test; affected app service test; affected CLI command test | `@gobing-ai/spur-domain`, `@gobing-ai/spur-app`, `@gobing-ai/spur` |
|
|
@@ -143,6 +184,12 @@ that single full gate.
|
|
|
143
184
|
creep into every plugin edit.
|
|
144
185
|
- Run the applicable rows, then stop: the single full project check belongs to the pipeline's
|
|
145
186
|
`test` hop (`task-pipeline.yaml` `${vars.qualityGateCmd}`).
|
|
187
|
+
- **Mirrored surfaces: an edit lands on all of them.** When the change is one instance of a repeated
|
|
188
|
+
pattern — the same wiring rule on two runbooks, a signature in both a generator and its generated
|
|
189
|
+
`.d.mts`, a command chain in both `spur-check` and `spur-check-new` — grep for the siblings and
|
|
190
|
+
update each in this pass. Two H1-batch cycles (2026-10-08) were spent re-finding exactly this:
|
|
191
|
+
task 1129 fixed the guard wiring on one runbook and left its sibling, and task 1127 changed
|
|
192
|
+
`spur-check` while `spur-check-new`'s chain-parity assertion still expected the old chain.
|
|
146
193
|
|
|
147
194
|
## Behavior
|
|
148
195
|
|
|
@@ -165,7 +165,7 @@ classifies it as external and never raises `L4.stale-line-anchor` for it (R1). D
|
|
|
165
165
|
that lives in this repo — in-repo evidence MUST use the repo-relative backtick form
|
|
166
166
|
`` `path:line` `` / `` `path:start-end` ``, and citing it in the external form still reports (R2).
|
|
167
167
|
|
|
168
|
-
**Concrete anchors (0804 R9):**
|
|
168
|
+
**Concrete anchors (0804 R9):** existing `file:line`s only, never globs, always repo-root-relative — a basename is `L4.anchor-unresolved` at the `--as done` gate (`references/verdict-schema.md`).
|
|
169
169
|
|
|
170
170
|
### Step 5 — Acceptance Criteria guard
|
|
171
171
|
|
|
@@ -315,6 +315,11 @@ node "$RESIDUAL" fold <wbs>
|
|
|
315
315
|
> for each dropped MET-matched scenario key and when the new rows match no feature scenario —
|
|
316
316
|
> treat those warnings as a re-key instruction, not noise.
|
|
317
317
|
|
|
318
|
+
> **Keying rule for feature-linked tasks (1086/1089 re-audits).** Key AC rows by the exact feature
|
|
319
|
+
> scenario title (`R1 — …`): feature `Scenario:` titles are declared identities and satisfy L4
|
|
320
|
+
> scenario matching directly. Bare `AC1` fails `ac-identity` on Gherkin AC sections, and checklist
|
|
321
|
+
> labels truncate at the first colon (a `(req: …)` suffix), so the task's own AC title may not resolve.
|
|
322
|
+
|
|
318
323
|
> **Do not write `## Review` directly, ever.** The `## Review` section is owned by the
|
|
319
324
|
> `review` coordinator (`/sp:dev-review` → `sp:super-reviewer`), which merges
|
|
320
325
|
> `functional-review` + `code-verification` review mode + `code-improvement` fragments. The
|
|
@@ -357,9 +362,8 @@ Confidence: HIGH
|
|
|
357
362
|
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 |`.
|
|
358
363
|
The answer MUST carry exactly one `Confidence: HIGH|MEDIUM|LOW` line (task 1068): the verifier's stated confidence in the verdict — HIGH = fresh executable evidence + re-read anchors this run; MEDIUM = partial reliance on prior evidence, external/delegated outcomes, or an unstable tree; LOW = key evidence not re-executed. A missing or out-of-vocabulary line fails lint (`confidence-missing` / `confidence-value`) and blocks derivation; the value lands in the verdict artifact's `confidence` field and the recorded `## Testing` section.
|
|
359
364
|
The answer MUST cite a source for external API/library claims in Requirement/AC evidence cells: a cell asserting a scoped-package (`@scope/name`), `node_modules/`, semver-version (`x.y.z`), or backticked function-call (`` `name(args)` ``) claim is rejected with `evidence-citation` unless the SAME cell carries a citation — a repo `path:line(-end)` (backticked or bare), the external named-origin form (@origin `path` line N), or a URL. Cross-cell references ("receipt above") do not count; marker-free receipts ("bun test 398 pass / 0 fail") need no citation.
|
|
360
|
-
**MUST NOT:** place a `Severity` column between `Req` and `Status
|
|
361
|
-
|
|
362
|
-
canonical.
|
|
365
|
+
**MUST NOT:** place a `Severity` column between `Req` and `Status`. The parser tolerates variants
|
|
366
|
+
as defense-in-depth; this contract is canonical.
|
|
363
367
|
|
|
364
368
|
Under the pipeline, the verifier owns the answer file `.spur/run/<wbs>-verify-answer.txt` (0726
|
|
365
369
|
R3): `Verdict: PARTIAL` first, append one row at a time, replace the verdict line only when all
|
|
@@ -90,6 +90,9 @@ Before declaring a task `done`, run this lightweight checklist. It catches the m
|
|
|
90
90
|
- [ ] No `console.log` / `console.error` in production code (use the project logger).
|
|
91
91
|
- [ ] No `--no-verify`, `--force`, or new suppression comments added to bypass gates.
|
|
92
92
|
- [ ] Solution section contains `file:line` citations for every changed file.
|
|
93
|
+
- [ ] API/library claims in changed docs carry a source citation (URL or `path:line`) and an honest
|
|
94
|
+
confidence level (HIGH/MEDIUM/LOW) per source-driven-development; the deterministic advisory backstop is
|
|
95
|
+
the `cited-version-strings` / `cited-api-claims` rules (`.spur/rules/docs/sourced-api-claims.yaml`).
|
|
93
96
|
- [ ] Review section has P1–P4 findings table (even if all rows are empty — the table itself proves review happened).
|
|
94
97
|
|
|
95
98
|
This checklist runs as part of the verify step when the task reaches `testing` status. It does not replace the formal gates — it augments them with between-pipeline hygiene checks.
|