@gobing-ai/spur 0.3.78 → 0.3.81
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 +29 -18
- package/config/config.global.yaml +10 -11
- package/config/pipeline-budgets.json +34 -2
- package/config/plugin-scripts.json +25 -0
- package/config/rules/boundary/config-loading-ownership.yaml +0 -3
- package/config/rules/boundary/dao-boundary.yaml +4 -17
- package/config/rules/boundary/planning-folder-hardcode.yaml +0 -1
- package/config/rules/boundary/sp-no-vendor-refs.yaml +3 -2
- package/config/rules/boundary/sp-runtime-path.yaml +3 -14
- package/config/rules/quality/coverage-gate.yaml +3 -14
- package/config/rules/quality/tsdoc-exports.yaml +4 -7
- package/config/rules/strict/http-boundaries.yaml +5 -8
- package/config/rules/strict/runtime-boundaries.yaml +1 -5
- package/config/rules/structure/protected-files.yaml +9 -3
- package/config/rules/structure/test-focus-skip.yaml +0 -2
- package/config/rules/structure/test-location.yaml +0 -5
- package/config/rules/surface/check-cli-surface.yaml +3 -2
- package/config/rules/typescript/bun-tooling.yaml +5 -7
- package/config/rules/typescript/guarded-happy-dom-register.yaml +0 -2
- package/config/rules/typescript/happy-dom-teardown.yaml +0 -2
- package/config/rules/typescript/no-biome-suppressions.yaml +0 -2
- package/config/rules/typescript/no-debugger.yaml +0 -2
- package/config/rules/typescript/no-eslint-suppressions.yaml +0 -4
- package/config/rules/typescript/no-leaky-module-mocks.yaml +6 -13
- package/config/rules/typescript/no-module-scope-import-calls.yaml +0 -2
- package/config/rules/typescript/no-syscall-emulation-in-boundary-mock.yaml +0 -3
- package/config/rules/typescript/no-unmocked-module-eval-side-effects.yaml +0 -3
- package/config/rules/typescript/output-boundaries.yaml +0 -3
- package/config/rules/typescript/prefer-accessible-role-for-button-queries.yaml +0 -3
- package/config/rules/ui/ui-import-boundary.yaml +1 -5
- package/config/templates/AGENTS.md +26 -23
- package/config/templates/docs/00_ADR.md +13 -23
- package/config/templates/docs/01_PRD.md +5 -2
- package/config/templates/docs/02_ROADMAP.md +9 -13
- package/config/templates/docs/03_ARCHITECTURE.md +2 -2
- package/config/templates/docs/04_DESIGN.md +12 -31
- package/config/templates/docs/05_FEATURES.md +6 -18
- package/config/templates/docs/99_PROJECT_CONSTITUTION.md +162 -394
- package/config/transition-shims.json +7 -7
- package/config/workflows/basic.yaml +4 -0
- package/config/workflows/docs-pipeline.yaml +13 -14
- package/config/workflows/feature-dev.yaml +20 -65
- package/config/workflows/history-anatomy.yaml +22 -1
- package/config/workflows/idea-pipeline.yaml +53 -97
- package/config/workflows/pr-review.yaml +21 -33
- package/config/workflows/task-pipeline.yaml +87 -330
- package/config/workflows/wayfinder-resolution.yaml +12 -26
- package/config/workflows/wrapup-pipeline.yaml +48 -189
- package/package.json +9 -9
- package/plugins/sp/README.md +22 -8
- package/plugins/sp/agents/expert-spur.md +41 -19
- package/plugins/sp/agents/super-reviewer.md +43 -8
- package/plugins/sp/lib/idea-handoff.generated.d.mts +17 -0
- package/plugins/sp/lib/idea-handoff.generated.mjs +1301 -0
- package/plugins/sp/plugin.json +1 -1
- package/plugins/sp/scripts/feature-dev-precheck.mjs +146 -0
- package/plugins/sp/scripts/feature-dev-precheck.ts +238 -0
- package/plugins/sp/scripts/idea-handoff.mjs +27 -0
- package/plugins/sp/scripts/idea-handoff.ts +44 -0
- package/plugins/sp/scripts/quality-gate.mjs +165 -0
- package/plugins/sp/scripts/quality-gate.ts +217 -0
- package/plugins/sp/scripts/verify-answer-lint.ts +21 -3
- package/plugins/sp/scripts/workflow-step-profile.mjs +319 -0
- package/plugins/sp/scripts/workflow-step-profile.ts +456 -0
- package/plugins/sp/scripts/wrapup-steps.mjs +350 -0
- package/plugins/sp/scripts/wrapup-steps.ts +466 -0
- package/plugins/sp/skills/conflict-finding/SKILL.md +6 -0
- package/plugins/sp/skills/daily-summary/SKILL.md +1 -1
- package/plugins/sp/skills/doc-evolve/SKILL.md +26 -40
- package/plugins/sp/skills/doc-evolve/references/operations.md +17 -30
- package/plugins/sp/skills/parallel-execution/references/dispatch-surface.md +1 -1
- package/plugins/sp/skills/spec-decomposition/references/decomposition.md +29 -0
- package/plugins/sp/skills/spur-cli/references/agent.md +56 -14
- package/plugins/sp/skills/spur-cli/references/message.md +30 -3
- package/plugins/sp/skills/spur-cli/references/projects.md +45 -1
- package/plugins/sp/skills/spur-cli/references/self.md +5 -4
- package/plugins/sp/skills/spur-cli/references/serve.md +5 -4
- package/plugins/sp/skills/spur-cli/references/tasks/verbs.md +17 -1
- package/plugins/sp/skills/spur-cli/references/tasks.md +32 -2
- package/plugins/sp/skills/spur-cli/references/team.md +21 -1
- package/plugins/sp/skills/spur-cli/references/workflows/operations.md +6 -3
- package/plugins/sp/skills/spur-cli/references/workflows/workflow-fit-and-tuning.md +57 -18
- package/plugins/sp/skills/spur-composer/SKILL.md +145 -0
- package/plugins/sp/skills/spur-dev/references/ac-style-guide.md +14 -0
- package/plugins/sp/skills/spur-dev/references/cross-cutting.md +3 -3
- package/plugins/sp/skills/spur-dev/references/done-housekeeping.md +12 -0
- package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +46 -4
- package/plugins/sp/skills/spur-dev/references/planning-workflow.md +24 -0
- package/plugins/sp/skills/spur-doctor/SKILL.md +138 -0
- package/plugins/sp/skills/taste-refactoring-api/README.md +43 -0
- package/plugins/sp/skills/taste-refactoring-api/SKILL.md +334 -0
- package/plugins/sp/skills/taste-refactoring-api/checklists/daily-api-review.md +71 -0
- package/plugins/sp/skills/taste-refactoring-api/examples/refactor-example.md +72 -0
- package/plugins/sp/skills/taste-refactoring-api/examples/review-template.md +93 -0
- package/plugins/sp/skills/taste-refactoring-api/references/api-refactoring-playbook.md +253 -0
- package/plugins/sp/skills/taste-refactoring-api/references/protocol-modes.md +79 -0
- package/plugins/sp/skills/taste-refactoring-api/references/research-basis.md +58 -0
- package/plugins/sp/skills/taste-refactoring-architect/README.md +26 -0
- package/plugins/sp/skills/taste-refactoring-architect/SKILL.md +471 -0
- package/plugins/sp/skills/taste-refactoring-architect/checklists/daily-architecture-review.md +48 -0
- package/plugins/sp/skills/taste-refactoring-architect/examples/refactor-example.md +55 -0
- package/plugins/sp/skills/taste-refactoring-architect/examples/review-template.md +51 -0
- package/plugins/sp/skills/taste-refactoring-architect/references/architecture-refactoring-playbook.md +173 -0
- package/plugins/sp/skills/taste-refactoring-architect/references/research-basis.md +28 -0
- package/plugins/sp/skills/taste-refactoring-tests/README.md +28 -0
- package/plugins/sp/skills/taste-refactoring-tests/SKILL.md +482 -0
- package/plugins/sp/skills/taste-refactoring-tests/checklists/daily-test-review.md +39 -0
- package/plugins/sp/skills/taste-refactoring-tests/examples/refactor-example.md +85 -0
- package/plugins/sp/skills/taste-refactoring-tests/examples/review-template.md +59 -0
- package/plugins/sp/skills/taste-refactoring-tests/references/research-basis.md +47 -0
- package/plugins/sp/skills/taste-refactoring-tests/references/test-refactoring-playbook.md +222 -0
- package/plugins/sp/skills/taste-refactoring-ui/README.md +12 -0
- package/plugins/sp/skills/taste-refactoring-ui/SKILL.md +290 -0
- package/plugins/sp/skills/taste-refactoring-ui/checklists/daily-ui-review.md +72 -0
- package/plugins/sp/skills/taste-refactoring-ui/examples/review-template.md +51 -0
- package/plugins/sp/skills/taste-refactoring-ui/references/refactoring-ui-playbook.md +170 -0
- package/plugins/sp/skills/wayfinder/SKILL.md +2 -2
- package/plugins/sp/skills/wayfinder/references/pipeline-resolution.md +30 -0
- package/schemas/spur-config.schema.json +49 -0
- package/spur.js +46936 -44198
- package/web/_astro/{BoardApp.CHQ1lycZ.js → BoardApp.B1U26g3I.js} +97 -95
- package/web/_astro/BoardApp.Csgyg-lS.js +1 -0
- package/web/_astro/{TaskDetail.GKfQJ60c.js → TaskDetail.DwPqpq7v.js} +1 -1
- package/web/_astro/{arc.DWEtA3Tx.js → arc.CweZEjN2.js} +1 -1
- package/web/_astro/{architectureDiagram-3BPJPVTR.DB42oWmP.js → architectureDiagram-3BPJPVTR.D89pbDuv.js} +1 -1
- package/web/_astro/{blockDiagram-GPEHLZMM.rhv-zNQV.js → blockDiagram-GPEHLZMM.BOuTeEpX.js} +1 -1
- package/web/_astro/{c4Diagram-AAUBKEIU.Ci4-4VvY.js → c4Diagram-AAUBKEIU.CASbkWZF.js} +1 -1
- package/web/_astro/channel.Cx6sXxhq.js +1 -0
- package/web/_astro/{chunk-2J33WTMH.Cc9veUgf.js → chunk-2J33WTMH.BKQYtOvY.js} +1 -1
- package/web/_astro/{chunk-4BX2VUAB.Bec9c4eI.js → chunk-4BX2VUAB.9sHLdMtG.js} +1 -1
- package/web/_astro/{chunk-55IACEB6.DoV8S1iB.js → chunk-55IACEB6.wOLXWlPs.js} +1 -1
- package/web/_astro/{chunk-727SXJPM.DwR-Qlyj.js → chunk-727SXJPM.DovFbwg3.js} +1 -1
- package/web/_astro/{chunk-AQP2D5EJ.ND_a81WY.js → chunk-AQP2D5EJ.B1Weod1X.js} +1 -1
- package/web/_astro/{chunk-FMBD7UC4.Wv_jwG48.js → chunk-FMBD7UC4.TEMS04st.js} +1 -1
- package/web/_astro/{chunk-ND2GUHAM.CXKXCMmp.js → chunk-ND2GUHAM.Cp8VT1wQ.js} +1 -1
- package/web/_astro/{chunk-QZHKN3VN.nkaoNYQq.js → chunk-QZHKN3VN.BzATdEcP.js} +1 -1
- package/web/_astro/{classDiagram-4FO5ZUOK.cMQcVlQu.js → classDiagram-4FO5ZUOK.C9BOCfAO.js} +1 -1
- package/web/_astro/{classDiagram-v2-Q7XG4LA2.cMQcVlQu.js → classDiagram-v2-Q7XG4LA2.C9BOCfAO.js} +1 -1
- package/web/_astro/{cose-bilkent-S5V4N54A.OaDJ7Mr2.js → cose-bilkent-S5V4N54A.DUnr4UAw.js} +1 -1
- package/web/_astro/{cynefin-OW5HDTMX.Chi8IphF.js → cynefin-OW5HDTMX.rYq5uM3D.js} +1 -1
- package/web/_astro/{cytoscape.esm.DzSz-X2X.js → cytoscape.esm.BB4DxJjf.js} +1 -1
- package/web/_astro/{dagre-BM42HDAG.CzK2t_Fp.js → dagre-BM42HDAG.CWeNKe3I.js} +1 -1
- package/web/_astro/{diagram-2AECGRRQ.DRvxlVS7.js → diagram-2AECGRRQ.DCkfls10.js} +1 -1
- package/web/_astro/{diagram-5GNKFQAL.CnYvNdwA.js → diagram-5GNKFQAL.D5U4JCka.js} +1 -1
- package/web/_astro/{diagram-KO2AKTUF.CpLpMw5R.js → diagram-KO2AKTUF.BZJgqaqG.js} +1 -1
- package/web/_astro/{diagram-LMA3HP47.JTb78qUA.js → diagram-LMA3HP47.DoMeHvPR.js} +1 -1
- package/web/_astro/{diagram-OG6HWLK6.Bk-1jDIb.js → diagram-OG6HWLK6.B50qwwWX.js} +1 -1
- package/web/_astro/{erDiagram-TEJ5UH35.D8hN9GZq.js → erDiagram-TEJ5UH35.DdGPG6LK.js} +1 -1
- package/web/_astro/{flowDiagram-I6XJVG4X.-6zQr6m5.js → flowDiagram-I6XJVG4X.QP2MJ12u.js} +1 -1
- package/web/_astro/{ganttDiagram-6RSMTGT7.DboLQ9ca.js → ganttDiagram-6RSMTGT7.BI6LgKSy.js} +1 -1
- package/web/_astro/{gitGraphDiagram-PVQCEYII.4tYvJKGR.js → gitGraphDiagram-PVQCEYII.npPZiC2G.js} +1 -1
- package/web/_astro/index.DayyIngm.css +1 -0
- package/web/_astro/{infoDiagram-5YYISTIA.Bd9rXpsB.js → infoDiagram-5YYISTIA.DCJCBVbp.js} +1 -1
- package/web/_astro/{ishikawaDiagram-YF4QCWOH.CvMoaf67.js → ishikawaDiagram-YF4QCWOH.BMLV-3I1.js} +1 -1
- package/web/_astro/{journeyDiagram-JHISSGLW.Ccy1CA7y.js → journeyDiagram-JHISSGLW.LE58crde.js} +1 -1
- package/web/_astro/{kanban-definition-UN3LZRKU.0MaMqHNS.js → kanban-definition-UN3LZRKU.BPbz8rH9.js} +1 -1
- package/web/_astro/{linear.CHXgcIbN.js → linear.DhZaBtYh.js} +1 -1
- package/web/_astro/{mermaid.core.Ca-kcelG.js → mermaid.core.BD5-jXum.js} +6 -6
- package/web/_astro/{mindmap-definition-RKZ34NQL.BUIDlHa0.js → mindmap-definition-RKZ34NQL.MTJyrQ65.js} +1 -1
- package/web/_astro/ordinal.BYWQX77i.js +1 -0
- package/web/_astro/{pieDiagram-4H26LBE5.2dX3CU1s.js → pieDiagram-4H26LBE5.BrDhDvIS.js} +1 -1
- package/web/_astro/{quadrantDiagram-W4KKPZXB.B3LBlRiv.js → quadrantDiagram-W4KKPZXB.71d73_5N.js} +1 -1
- package/web/_astro/{requirementDiagram-4Y6WPE33.X12I2uNx.js → requirementDiagram-4Y6WPE33.Bga6UF-z.js} +1 -1
- package/web/_astro/{sankeyDiagram-5OEKKPKP.BXohIHqx.js → sankeyDiagram-5OEKKPKP.BnHs4K82.js} +1 -1
- package/web/_astro/{sequenceDiagram-3UESZ5HK.C37ZIUzg.js → sequenceDiagram-3UESZ5HK.DsfY2gnj.js} +1 -1
- package/web/_astro/{stateDiagram-AJRCARHV.BRgz317z.js → stateDiagram-AJRCARHV.DvsTSc9a.js} +1 -1
- package/web/_astro/{stateDiagram-v2-BHNVJYJU.7VYSXN9-.js → stateDiagram-v2-BHNVJYJU.DxzzmHUR.js} +1 -1
- package/web/_astro/{timeline-definition-PNZ67QCA.BVNz_HiN.js → timeline-definition-PNZ67QCA.4ZuQmOTt.js} +1 -1
- package/web/_astro/{vennDiagram-CIIHVFJN.CHVDkPX4.js → vennDiagram-CIIHVFJN.Ck5Q86SG.js} +1 -1
- package/web/_astro/{wardleyDiagram-YWT4CUSO.EQQ_qT9v.js → wardleyDiagram-YWT4CUSO.BK7k2hXr.js} +1 -1
- package/web/_astro/{xychartDiagram-2RQKCTM6.DrAT9WoP.js → xychartDiagram-2RQKCTM6.DfCrgauK.js} +1 -1
- package/web/index.html +2 -2
- package/web/_astro/BoardApp.DV9kx0wo.js +0 -1
- package/web/_astro/channel.BAI6xLeV.js +0 -1
- package/web/_astro/index.Dcr_8fiK.css +0 -1
- package/web/_astro/ordinal.DBvzRdQf.js +0 -1
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* wrapup-steps — deterministic wrap-up capture, metrics and feature sync behind the
|
|
4
|
+
* wrapup-pipeline wrappers (task 0824, feature I21, governance §1.2 composition budgets).
|
|
5
|
+
*
|
|
6
|
+
* Reproduces the former wrapup-pipeline `task-resolve:onEnter:0`, `metrics-record:onEnter:0`
|
|
7
|
+
* and `feature-transition:onEnter:0` shell programs one-for-one so the workflow stays inside
|
|
8
|
+
* the shell-program caps while writing the same `.spur/run` artifacts:
|
|
9
|
+
* - `<runId>-wrapup-tasks.json` normalized, deduplicated WBS capture (resolve)
|
|
10
|
+
* - `<runId>-wrapup-resolve.status` `PASS`/`FAIL` (resolve)
|
|
11
|
+
* - `<runId>-route-reason.txt` route reason (written by the workflow route writer)
|
|
12
|
+
* - `.spur/memory/wrapup-metrics.jsonl` one row per task (metrics)
|
|
13
|
+
* - `<runId>-wrapup-metrics.status` `PASS`/`FAIL` (metrics)
|
|
14
|
+
* - `<runId>-wrapup-sync.status` `PASS`/`FAIL` (feature-transition)
|
|
15
|
+
*
|
|
16
|
+
* Environment comes from the workflow vars: `__runId`, `tasks`, `feature`, `featureGateCmd`
|
|
17
|
+
* and `spurBin` (split on whitespace into a command plus prefix args, so
|
|
18
|
+
* `bun apps/cli/src/index.ts` works).
|
|
19
|
+
*
|
|
20
|
+
* Truthfulness contract (0770 + 0783): wrap-up never mutates task status; a lookup failure is
|
|
21
|
+
* recorded as FAIL, never silently omitted as success; metrics rows are serialized as JSON
|
|
22
|
+
* (never interpolated printf); and the required sync succeeds only for a valid matching
|
|
23
|
+
* unblocked proposal whose target status is freshly observed — a gate PASS can never convert
|
|
24
|
+
* a failed sync into success. The process always exits 0 after resolve/metrics/
|
|
25
|
+
* feature-transition; the verdict lives in the status file (an empty `feature` is a
|
|
26
|
+
* mis-invocation and exits 1).
|
|
27
|
+
*
|
|
28
|
+
* Node-builtin imports only; pure helpers are exported for unit testing (ADR-065).
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { spawnSync } from 'node:child_process';
|
|
32
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
33
|
+
import { join } from 'node:path';
|
|
34
|
+
|
|
35
|
+
export interface WrapupStepsEnv {
|
|
36
|
+
__runId?: string;
|
|
37
|
+
tasks?: string;
|
|
38
|
+
feature?: string;
|
|
39
|
+
featureGateCmd?: string;
|
|
40
|
+
spurBin?: string;
|
|
41
|
+
[key: string]: string | undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface WrapupStepsOptions {
|
|
45
|
+
/** Base directory for `.spur/run` / `.spur/memory`; defaults to the process cwd. */
|
|
46
|
+
cwd?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** jq `//` chain: first value that is neither null, undefined nor false; otherwise the fallback. */
|
|
50
|
+
export function jqPick(...values: unknown[]): unknown {
|
|
51
|
+
for (const value of values) {
|
|
52
|
+
if (value !== null && value !== undefined && value !== false) return value;
|
|
53
|
+
}
|
|
54
|
+
return values[values.length - 1];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** `jq -r` text rendering of a JSON value (strings raw, everything else compact JSON). */
|
|
58
|
+
function jqText(value: unknown): string {
|
|
59
|
+
return typeof value === 'string' ? value : JSON.stringify(value);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Canonical four-digit WBS string: whitespace is rejected, not trimmed (0783 R1). */
|
|
63
|
+
export const WBS_PATTERN = /^[0-9]{4}$/;
|
|
64
|
+
|
|
65
|
+
/** `spurBin` splits on whitespace into a command plus prefix args (so `bun x.ts` works). */
|
|
66
|
+
export function spurCommand(spurBin: string | undefined): { cmd: string; prefix: string[] } {
|
|
67
|
+
const parts = (spurBin ?? 'spur')
|
|
68
|
+
.trim()
|
|
69
|
+
.split(/\s+/)
|
|
70
|
+
.filter((p) => p.length > 0);
|
|
71
|
+
return { cmd: parts[0] ?? 'spur', prefix: parts.slice(1) };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function spur(
|
|
75
|
+
env: WrapupStepsEnv,
|
|
76
|
+
args: string[],
|
|
77
|
+
options: { cwd?: string; stderr?: 'inherit' } = {},
|
|
78
|
+
): { status: number; stdout: string } {
|
|
79
|
+
const { cmd, prefix } = spurCommand(env.spurBin);
|
|
80
|
+
const result = spawnSync(cmd, [...prefix, ...args], {
|
|
81
|
+
cwd: options.cwd,
|
|
82
|
+
encoding: 'utf8',
|
|
83
|
+
...(options.stderr === 'inherit' ? { stdio: ['ignore', 'pipe', 'inherit'] as const } : {}),
|
|
84
|
+
});
|
|
85
|
+
if (result.error !== undefined) return { status: result.status ?? 1, stdout: '' };
|
|
86
|
+
return { status: result.status ?? 1, stdout: result.stdout ?? '' };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** jq status-chain semantics for `task show` output; null/missing status means the lookup failed. */
|
|
90
|
+
export function taskStatusOf(taskJson: string): { resolved: unknown; present: boolean } {
|
|
91
|
+
let parsed: unknown;
|
|
92
|
+
try {
|
|
93
|
+
parsed = JSON.parse(taskJson);
|
|
94
|
+
} catch {
|
|
95
|
+
return { resolved: null, present: false };
|
|
96
|
+
}
|
|
97
|
+
if (parsed === null || typeof parsed !== 'object') return { resolved: null, present: false };
|
|
98
|
+
const frontmatter = (parsed as Record<string, unknown>).frontmatter;
|
|
99
|
+
const fmStatus =
|
|
100
|
+
frontmatter !== null && typeof frontmatter === 'object'
|
|
101
|
+
? (frontmatter as Record<string, unknown>).status
|
|
102
|
+
: undefined;
|
|
103
|
+
const status = (parsed as Record<string, unknown>).status;
|
|
104
|
+
// jq: `.frontmatter.status // .status` is null only when frontmatter.status is null/absent
|
|
105
|
+
// AND .status is null; a trailing false is kept (false != null in jq).
|
|
106
|
+
if (fmStatus === null || fmStatus === undefined) {
|
|
107
|
+
return { resolved: status ?? null, present: status !== undefined && status !== null };
|
|
108
|
+
}
|
|
109
|
+
return { resolved: fmStatus, present: true };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** One truthy status display: `unresolved` when the lookup left nothing to show. */
|
|
113
|
+
function statusDisplay(taskJson: string): string {
|
|
114
|
+
const { resolved } = taskStatusOf(taskJson);
|
|
115
|
+
if (resolved === null || resolved === undefined || resolved === false) return 'unresolved';
|
|
116
|
+
return jqText(resolved);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export interface ResolveResult {
|
|
120
|
+
status: 'PASS' | 'FAIL';
|
|
121
|
+
statusFile: string;
|
|
122
|
+
tasksFile: string;
|
|
123
|
+
/** Exit code the workflow wrapper observes (only an empty __runId is a hard failure). */
|
|
124
|
+
exitCode: number;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** `resolve` — parse and validate vars.tasks exactly once, then resolve every member. */
|
|
128
|
+
export function resolveTasks(env: WrapupStepsEnv, options: WrapupStepsOptions = {}): ResolveResult {
|
|
129
|
+
const cwd = options.cwd;
|
|
130
|
+
const runId = env.__runId ?? '';
|
|
131
|
+
if (runId.length === 0) {
|
|
132
|
+
process.stderr.write('task-resolve: __runId is empty — refusing the legacy fixed-path fallback\n');
|
|
133
|
+
return { status: 'FAIL', statusFile: '', tasksFile: '', exitCode: 1 };
|
|
134
|
+
}
|
|
135
|
+
mkdirSync(cwd ? join(cwd, '.spur', 'run') : join('.spur', 'run'), { recursive: true });
|
|
136
|
+
const relTasksFile = join('.spur', 'run', `${runId}-wrapup-tasks.json`);
|
|
137
|
+
const relReasonFile = join('.spur', 'run', `${runId}-route-reason.txt`);
|
|
138
|
+
const relStatusFile = join('.spur', 'run', `${runId}-wrapup-resolve.status`);
|
|
139
|
+
const abs = (p: string): string => (cwd ? join(cwd, p) : p);
|
|
140
|
+
|
|
141
|
+
const writeFail = (reason: string): ResolveResult => {
|
|
142
|
+
writeFileSync(abs(relReasonFile), reason);
|
|
143
|
+
writeFileSync(abs(relStatusFile), 'FAIL\n');
|
|
144
|
+
return { status: 'FAIL', statusFile: relStatusFile, tasksFile: relTasksFile, exitCode: 0 };
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
let parsedTasks: unknown;
|
|
148
|
+
try {
|
|
149
|
+
parsedTasks = JSON.parse(env.tasks ?? '');
|
|
150
|
+
} catch {
|
|
151
|
+
parsedTasks = undefined;
|
|
152
|
+
}
|
|
153
|
+
const validArray =
|
|
154
|
+
Array.isArray(parsedTasks) && parsedTasks.every((w) => typeof w === 'string' && WBS_PATTERN.test(w));
|
|
155
|
+
if (!validArray) {
|
|
156
|
+
process.stderr.write(
|
|
157
|
+
'task-resolve: tasks must be a JSON array of canonical four-digit WBS strings (whitespace is rejected, not trimmed)\n',
|
|
158
|
+
);
|
|
159
|
+
return writeFail('failed:tasks is not a JSON array of canonical four-digit WBS strings');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Dedupe in first-seen order (never sorted).
|
|
163
|
+
const deduped: string[] = [];
|
|
164
|
+
for (const wbs of parsedTasks as string[]) {
|
|
165
|
+
if (!deduped.includes(wbs)) deduped.push(wbs);
|
|
166
|
+
}
|
|
167
|
+
writeFileSync(abs(relTasksFile), `${JSON.stringify(deduped)}\n`);
|
|
168
|
+
|
|
169
|
+
let unresolved = false;
|
|
170
|
+
for (const wbs of deduped) {
|
|
171
|
+
const shown = spur(env, ['task', 'show', wbs, '--json'], { cwd });
|
|
172
|
+
const status = shown.status === 0 ? statusDisplay(shown.stdout) : 'unresolved';
|
|
173
|
+
if (status !== 'done' && status !== 'cancelled') {
|
|
174
|
+
process.stderr.write(
|
|
175
|
+
`task-resolve: task ${wbs} did not resolve to a completed status (status=${status})\n`,
|
|
176
|
+
);
|
|
177
|
+
unresolved = true;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if (unresolved) {
|
|
181
|
+
return writeFail(`failed:unresolved or non-completed task (see ${relTasksFile})`);
|
|
182
|
+
}
|
|
183
|
+
writeFileSync(abs(relStatusFile), 'PASS\n');
|
|
184
|
+
return { status: 'PASS', statusFile: relStatusFile, tasksFile: relTasksFile, exitCode: 0 };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export interface MetricsResult {
|
|
188
|
+
status: 'PASS' | 'FAIL';
|
|
189
|
+
statusFile: string;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** `metrics` — append one JSONL row per captured task; a missing row is never silently absorbed. */
|
|
193
|
+
export function runMetrics(env: WrapupStepsEnv, options: WrapupStepsOptions = {}): MetricsResult {
|
|
194
|
+
const cwd = options.cwd;
|
|
195
|
+
const runId = env.__runId ?? '';
|
|
196
|
+
mkdirSync(cwd ? join(cwd, '.spur', 'run') : join('.spur', 'run'), { recursive: true });
|
|
197
|
+
mkdirSync(cwd ? join(cwd, '.spur', 'memory') : join('.spur', 'memory'), { recursive: true });
|
|
198
|
+
const relStatusFile = join('.spur', 'run', `${runId}-wrapup-metrics.status`);
|
|
199
|
+
const relTasksFile = join('.spur', 'run', `${runId}-wrapup-tasks.json`);
|
|
200
|
+
const relMetricsFile = join('.spur', 'memory', 'wrapup-metrics.jsonl');
|
|
201
|
+
const abs = (p: string): string => (cwd ? join(cwd, p) : p);
|
|
202
|
+
|
|
203
|
+
const fail = (): MetricsResult => {
|
|
204
|
+
writeFileSync(abs(relStatusFile), 'FAIL\n');
|
|
205
|
+
return { status: 'FAIL', statusFile: relStatusFile };
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
let captured: unknown;
|
|
209
|
+
try {
|
|
210
|
+
captured = JSON.parse(readFileSync(abs(relTasksFile), 'utf8'));
|
|
211
|
+
} catch {
|
|
212
|
+
captured = undefined;
|
|
213
|
+
}
|
|
214
|
+
const validCapture = Array.isArray(captured) && captured.every((w) => typeof w === 'string' && WBS_PATTERN.test(w));
|
|
215
|
+
if (!validCapture) {
|
|
216
|
+
process.stderr.write(
|
|
217
|
+
'metrics-record: run-scoped task capture missing, corrupted or non-canonical — refusing to record metrics\n',
|
|
218
|
+
);
|
|
219
|
+
return fail();
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
let metricsRc = 0;
|
|
223
|
+
for (const wbs of captured as string[]) {
|
|
224
|
+
const shown = spur(env, ['task', 'show', wbs, '--json'], { cwd });
|
|
225
|
+
const lookup = shown.status === 0 ? taskStatusOf(shown.stdout) : { resolved: null, present: false };
|
|
226
|
+
if (!lookup.present) {
|
|
227
|
+
process.stderr.write(
|
|
228
|
+
`metrics-record: task ${wbs} lookup failed or was malformed — recording FAIL instead of silently omitting its metrics row\n`,
|
|
229
|
+
);
|
|
230
|
+
metricsRc = 1;
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
const parsed = JSON.parse(shown.stdout) as Record<string, unknown>;
|
|
234
|
+
const frontmatter =
|
|
235
|
+
parsed.frontmatter !== null && typeof parsed.frontmatter === 'object'
|
|
236
|
+
? (parsed.frontmatter as Record<string, unknown>)
|
|
237
|
+
: {};
|
|
238
|
+
const featureId = String(jqPick(frontmatter.feature_id, parsed.feature_id, ''));
|
|
239
|
+
const status = String(jqPick(frontmatter.status, parsed.status, 'unknown'));
|
|
240
|
+
|
|
241
|
+
// jq `//` semantics: null and false count as missing; an empty string result stays UNKNOWN.
|
|
242
|
+
let verdict = 'UNKNOWN';
|
|
243
|
+
const verdictPath = join('.spur', 'run', `${wbs}-verdict.json`);
|
|
244
|
+
if (existsSync(abs(verdictPath))) {
|
|
245
|
+
try {
|
|
246
|
+
const raw = jqPick(JSON.parse(readFileSync(abs(verdictPath), 'utf8')).verdict, 'UNKNOWN');
|
|
247
|
+
const text = raw === 'UNKNOWN' ? 'UNKNOWN' : jqText(raw);
|
|
248
|
+
if (text.length > 0) verdict = text;
|
|
249
|
+
} catch {
|
|
250
|
+
// unreadable verdict file keeps UNKNOWN telemetry
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const timestamp = new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
|
|
255
|
+
const row = { wbs, feature_id: featureId, status, verdict, timestamp };
|
|
256
|
+
try {
|
|
257
|
+
appendFileSync(abs(relMetricsFile), `${JSON.stringify(row)}\n`);
|
|
258
|
+
} catch {
|
|
259
|
+
process.stderr.write(
|
|
260
|
+
`metrics-record: metrics append failed for task ${wbs} — recording FAIL instead of claiming the row landed\n`,
|
|
261
|
+
);
|
|
262
|
+
metricsRc = 1;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
const status: 'PASS' | 'FAIL' = metricsRc === 0 ? 'PASS' : 'FAIL';
|
|
266
|
+
writeFileSync(abs(relStatusFile), `${status}\n`);
|
|
267
|
+
return { status, statusFile: relStatusFile };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export interface FeatureTransitionResult {
|
|
271
|
+
status: 'PASS' | 'FAIL';
|
|
272
|
+
statusFile: string;
|
|
273
|
+
/** Only an empty vars.feature is a hard failure (mis-invocation, not a blocked sync). */
|
|
274
|
+
exitCode: number;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** Classify one sync result per 0783 R4; returns the blocking reason or '' for a verified sync. */
|
|
278
|
+
export function classifySync(
|
|
279
|
+
syncOutput: string,
|
|
280
|
+
syncRc: number,
|
|
281
|
+
feature: string,
|
|
282
|
+
observed: string,
|
|
283
|
+
): { reason: string; applied: string; syncOk: boolean } {
|
|
284
|
+
if (syncRc !== 0) {
|
|
285
|
+
return { reason: `sync exited nonzero (rc=${syncRc})`, applied: 'unreadable', syncOk: false };
|
|
286
|
+
}
|
|
287
|
+
let parsed: unknown;
|
|
288
|
+
try {
|
|
289
|
+
parsed = JSON.parse(syncOutput);
|
|
290
|
+
} catch {
|
|
291
|
+
parsed = undefined;
|
|
292
|
+
}
|
|
293
|
+
const obj = parsed !== null && typeof parsed === 'object' ? (parsed as Record<string, unknown>) : undefined;
|
|
294
|
+
const proposal =
|
|
295
|
+
obj?.proposal !== null && typeof obj?.proposal === 'object'
|
|
296
|
+
? (obj.proposal as Record<string, unknown>)
|
|
297
|
+
: undefined;
|
|
298
|
+
const shapeOk =
|
|
299
|
+
obj !== undefined &&
|
|
300
|
+
proposal !== undefined &&
|
|
301
|
+
typeof proposal.featureId === 'string' &&
|
|
302
|
+
typeof proposal.from === 'string' &&
|
|
303
|
+
typeof proposal.to === 'string' &&
|
|
304
|
+
typeof obj.applied === 'boolean';
|
|
305
|
+
if (!shapeOk) {
|
|
306
|
+
return { reason: 'malformed or unreadable sync result', applied: 'unreadable', syncOk: false };
|
|
307
|
+
}
|
|
308
|
+
const applied = obj.applied === true ? 'true' : 'false';
|
|
309
|
+
const pFrom = String(jqPick(proposal.from, ''));
|
|
310
|
+
const pTo = String(jqPick(proposal.to, ''));
|
|
311
|
+
if (String(jqPick(proposal.featureId, '')) !== feature) {
|
|
312
|
+
return { reason: `sync proposal does not match feature ${feature}`, applied, syncOk: false };
|
|
313
|
+
}
|
|
314
|
+
if (proposal.gateBlocked === true) {
|
|
315
|
+
return {
|
|
316
|
+
reason: 'sync proposal is gate-blocked — a blocked sync is not a no-change success',
|
|
317
|
+
applied,
|
|
318
|
+
syncOk: false,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
if (proposal.requiresConfirm === true) {
|
|
322
|
+
return { reason: 'sync proposal requires operator confirmation', applied, syncOk: false };
|
|
323
|
+
}
|
|
324
|
+
if (applied === 'true' && observed !== pTo) {
|
|
325
|
+
return {
|
|
326
|
+
reason: `applied sync did not land on the proposal target (observed=${observed}, to=${pTo})`,
|
|
327
|
+
applied,
|
|
328
|
+
syncOk: false,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
if (applied === 'false' && (pFrom !== pTo || observed !== pTo)) {
|
|
332
|
+
return {
|
|
333
|
+
reason: `sync applied nothing without a from==to observed no-op (from=${pFrom}, to=${pTo}, observed=${observed})`,
|
|
334
|
+
applied,
|
|
335
|
+
syncOk: false,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
return { reason: '', applied, syncOk: true };
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** `feature-transition` — required bounded sync, observation and the affected-feature gate. */
|
|
342
|
+
export function runFeatureTransition(env: WrapupStepsEnv, options: WrapupStepsOptions = {}): FeatureTransitionResult {
|
|
343
|
+
const cwd = options.cwd;
|
|
344
|
+
const runId = env.__runId ?? '';
|
|
345
|
+
const feature = env.feature ?? '';
|
|
346
|
+
mkdirSync(cwd ? join(cwd, '.spur', 'run') : join('.spur', 'run'), { recursive: true });
|
|
347
|
+
const relStatusFile = join('.spur', 'run', `${runId}-wrapup-sync.status`);
|
|
348
|
+
const abs = (p: string): string => (cwd ? join(cwd, p) : p);
|
|
349
|
+
if (feature.length === 0) {
|
|
350
|
+
process.stderr.write(
|
|
351
|
+
'feature-transition: vars.feature is empty — refusing no-op feature sync (mis-invocation, not a blocked sync)\n',
|
|
352
|
+
);
|
|
353
|
+
return { status: 'FAIL', statusFile: relStatusFile, exitCode: 1 };
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Three branches, first that exists relative to the cwd; current arguments including --spur-bin.
|
|
357
|
+
let syncOutput = '';
|
|
358
|
+
let syncRc = 1;
|
|
359
|
+
const boundedTs = join('plugins', 'sp', 'scripts', 'feature-sync-bounded.ts');
|
|
360
|
+
const boundedArgs = [feature, '--spur-bin', env.spurBin ?? 'spur', '--json'];
|
|
361
|
+
if (existsSync(cwd ? join(cwd, boundedTs) : boundedTs)) {
|
|
362
|
+
const result = spawnSync('bun', [boundedTs, ...boundedArgs], { cwd, encoding: 'utf8' });
|
|
363
|
+
syncOutput = result.stdout ?? '';
|
|
364
|
+
syncRc = result.status ?? 1;
|
|
365
|
+
if (result.stderr !== null && result.stderr.length > 0) process.stderr.write(result.stderr);
|
|
366
|
+
} else {
|
|
367
|
+
const probe = spawnSync('superskill', ['script', 'path', 'sp', 'feature-sync-bounded.mjs'], {
|
|
368
|
+
cwd,
|
|
369
|
+
encoding: 'utf8',
|
|
370
|
+
});
|
|
371
|
+
const twin = probe.status === 0 ? (probe.stdout ?? '').trim() : '';
|
|
372
|
+
if (twin.length > 0 && existsSync(twin)) {
|
|
373
|
+
const result = spawnSync('node', [twin, ...boundedArgs], { cwd, encoding: 'utf8' });
|
|
374
|
+
syncOutput = result.stdout ?? '';
|
|
375
|
+
syncRc = result.status ?? 1;
|
|
376
|
+
if (result.stderr !== null && result.stderr.length > 0) process.stderr.write(result.stderr);
|
|
377
|
+
} else {
|
|
378
|
+
// Last branch: stderr streams through (visible), stdout is the JSON payload.
|
|
379
|
+
const result = spur(env, ['feature', 'sync', feature, '--json'], { cwd, stderr: 'inherit' });
|
|
380
|
+
syncOutput = result.stdout;
|
|
381
|
+
syncRc = result.status;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
process.stdout.write(`${syncOutput}\n`);
|
|
385
|
+
|
|
386
|
+
const shown = spur(env, ['feature', 'show', feature, '--json'], { cwd });
|
|
387
|
+
let observed = '';
|
|
388
|
+
if (shown.status === 0) {
|
|
389
|
+
try {
|
|
390
|
+
const parsed = JSON.parse(shown.stdout) as Record<string, unknown>;
|
|
391
|
+
const frontmatter =
|
|
392
|
+
parsed.frontmatter !== null && typeof parsed.frontmatter === 'object'
|
|
393
|
+
? (parsed.frontmatter as Record<string, unknown>)
|
|
394
|
+
: {};
|
|
395
|
+
const picked = jqPick(parsed.status, frontmatter.status, '');
|
|
396
|
+
observed = picked === '' ? '' : jqText(picked);
|
|
397
|
+
} catch {
|
|
398
|
+
observed = '';
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
if (observed.length === 0) observed = 'unreadable';
|
|
402
|
+
|
|
403
|
+
const classified = classifySync(syncOutput, syncRc, feature, observed);
|
|
404
|
+
const { reason, applied } = classified;
|
|
405
|
+
const syncOk = classified.syncOk;
|
|
406
|
+
|
|
407
|
+
let gate = 'skipped';
|
|
408
|
+
if (applied === 'true' || syncRc !== 0) {
|
|
409
|
+
process.stdout.write(
|
|
410
|
+
`feature-transition: sync applied or failed after a possible partial transition for ${feature} — running feature gate: ${env.featureGateCmd ?? ''}\n`,
|
|
411
|
+
);
|
|
412
|
+
const gateResult = spawnSync('sh', ['-c', env.featureGateCmd ?? ''], { cwd, stdio: 'inherit' });
|
|
413
|
+
if ((gateResult.status ?? 1) === 0) {
|
|
414
|
+
gate = 'PASS';
|
|
415
|
+
process.stdout.write(`feature-transition: feature gate PASS for feature ${feature}\n`);
|
|
416
|
+
} else {
|
|
417
|
+
gate = 'FAIL';
|
|
418
|
+
process.stderr.write(
|
|
419
|
+
`feature-transition: feature gate FAIL for feature ${feature} — inspect findings before reporting the transition complete\n`,
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
} else {
|
|
423
|
+
process.stdout.write(
|
|
424
|
+
`feature-transition: sync did not apply a transition (rc=${syncRc}, applied=${applied}) — feature gate skipped\n`,
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
let syncStatus: 'PASS' | 'FAIL';
|
|
429
|
+
if (!syncOk || gate === 'FAIL') {
|
|
430
|
+
syncStatus = 'FAIL';
|
|
431
|
+
process.stderr.write(
|
|
432
|
+
`feature-transition: required synchronization failed for ${feature} — ${reason}; gate=${gate}\n`,
|
|
433
|
+
);
|
|
434
|
+
} else if (applied === 'false') {
|
|
435
|
+
syncStatus = 'PASS';
|
|
436
|
+
process.stdout.write(
|
|
437
|
+
`feature-transition: feature sync verified for ${feature} (from==to observed at ${observed}, gate=${gate}) — explicit no-change\n`,
|
|
438
|
+
);
|
|
439
|
+
} else {
|
|
440
|
+
syncStatus = 'PASS';
|
|
441
|
+
process.stdout.write(
|
|
442
|
+
`feature-transition: feature sync verified for ${feature} (applied, observed=${observed}, gate=${gate})\n`,
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
writeFileSync(abs(relStatusFile), `${syncStatus}\n`);
|
|
446
|
+
return { status: syncStatus, statusFile: relStatusFile, exitCode: 0 };
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
export const WRAPUP_STEPS_USAGE =
|
|
450
|
+
'usage: wrapup-steps.ts <resolve|metrics|feature-transition> (env: __runId, tasks, feature, featureGateCmd, spurBin)';
|
|
451
|
+
|
|
452
|
+
export function main(argv: string[], env: WrapupStepsEnv = process.env, options: WrapupStepsOptions = {}): number {
|
|
453
|
+
const sub = argv[0];
|
|
454
|
+
if (sub === 'resolve') return resolveTasks(env, options).exitCode;
|
|
455
|
+
if (sub === 'metrics') {
|
|
456
|
+
runMetrics(env, options);
|
|
457
|
+
return 0;
|
|
458
|
+
}
|
|
459
|
+
if (sub === 'feature-transition') return runFeatureTransition(env, options).exitCode;
|
|
460
|
+
process.stderr.write(`${WRAPUP_STEPS_USAGE}\n`);
|
|
461
|
+
return 2;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if (import.meta.main) {
|
|
465
|
+
process.exit(main(process.argv.slice(2)));
|
|
466
|
+
}
|
|
@@ -94,6 +94,12 @@ Resolve `<scope>` and `--pillar`; confirm the repository root; establish **audit
|
|
|
94
94
|
numbered-document mutation of any kind. With `--resolve`, no write happens until a repair set is
|
|
95
95
|
presented, explicitly confirmed, and freshness-revalidated.
|
|
96
96
|
|
|
97
|
+
Validate every enum flag against the domain declared by the command surface
|
|
98
|
+
(`plugins/sp/commands/dev-find-conflict.md`): `--pillar` ∈ `source|tasks|features|authority|all`,
|
|
99
|
+
`--mode` ∈ `adaptive|full`, `--agent` ∈ `inline|auto|name`. An out-of-domain value **refuses** the
|
|
100
|
+
audit before any discovery work — report the received value, the flag's valid domain, and stop;
|
|
101
|
+
never silently coerce or ignore it. Only `<scope>` is free-form and exempt from this check.
|
|
102
|
+
|
|
97
103
|
### Step 2 — Discover local authority
|
|
98
104
|
|
|
99
105
|
Read entry/process rules (`AGENTS.md`, `docs/99_PROJECT_CONSTITUTION.md`) before interpreting any
|
|
@@ -166,6 +166,6 @@ Read the skill file and follow the workflow manually.
|
|
|
166
166
|
## Additional Resources
|
|
167
167
|
|
|
168
168
|
- **Script source:** [scripts/daily-summary/daily-summary.ts](../../scripts/daily-summary/daily-summary.ts) — CLI implementation
|
|
169
|
-
- **Tests:** [tests/daily-summary.test.ts](tests/daily-summary.test.ts) — unit coverage for parsing, date ranges, markdown output
|
|
169
|
+
- **Tests:** [tests/daily-summary/daily-summary.test.ts](../../tests/daily-summary/daily-summary.test.ts) — unit coverage for parsing, date ranges, markdown output
|
|
170
170
|
- **Related skills:** `sp:dev-handover` (blocker handoff), `sp:dev-changelog` (commit-based changelog), `sp:spur-cli` (task management)
|
|
171
171
|
- **Upstream CLI:** [ccusage](https://github.com/ryoppippi/ccusage) — AI agent token usage reporter
|
|
@@ -4,7 +4,7 @@ description: "Evolve docs/00-05 + AGENTS.md per docs/99_PROJECT_CONSTITUTION.md:
|
|
|
4
4
|
license: Apache-2.0
|
|
5
5
|
metadata:
|
|
6
6
|
author: spur
|
|
7
|
-
version: "1.
|
|
7
|
+
version: "1.1"
|
|
8
8
|
platforms: "claude-code,codex,openclaw,opencode,antigravity"
|
|
9
9
|
interactions:
|
|
10
10
|
- reviewer
|
|
@@ -30,17 +30,17 @@ this skill applies it.
|
|
|
30
30
|
|
|
31
31
|
**Read `docs/99_PROJECT_CONSTITUTION.md` first.** It is the single source of truth for *how* these
|
|
32
32
|
files are maintained (authority §2, doc map §4.1, frontmatter contracts §4.3, sync triggers §5,
|
|
33
|
-
per-file edit rules §6, the audit §7,
|
|
33
|
+
per-file edit rules §6, the audit §7, lesson routing §8). This skill is a runbook for executing §5/§7/§8;
|
|
34
34
|
when the two disagree, the constitution wins and this skill is the bug.
|
|
35
35
|
|
|
36
36
|
## Operations
|
|
37
37
|
|
|
38
38
|
| Operation | What it does | Constitution authority | Deterministic helper |
|
|
39
39
|
| --------- | ------------ | ---------------------- | -------------------- |
|
|
40
|
-
| **drift-audit** | Reality (code/shipped) vs. what a key file says, and cross-doc contradictions | §7
|
|
41
|
-
| **sync-check** | Did a change touch the docs its trigger obligates in the same commit? | §5 (triggers
|
|
40
|
+
| **drift-audit** | Reality (code/shipped) vs. what a key file says, and cross-doc contradictions | §7 | `rg` the real CLI/config surface; diff vs `04`/`AGENTS.md`/`00` |
|
|
41
|
+
| **sync-check** | Did a change touch the docs its trigger obligates in the same commit? | §5 (applicable triggers) | git diff of code/config vs. the matching doc edit |
|
|
42
42
|
| **contract-verify** | Each doc's frontmatter matches its §4.1 row; `updated_at` is plausible | §4.3 | parse frontmatter; compare `owns`/`authority` vs §4.1; `git log` recency |
|
|
43
|
-
| **lesson-append** |
|
|
43
|
+
| **lesson-append** | Record a useful lesson in existing project context; deduplicate; propose governance changes separately | §8 | format-check the line; `rg` for an equivalent before adding |
|
|
44
44
|
|
|
45
45
|
No thin `dev-docs` command wrapper exists (`dev-operations.md §7`). Invoke this skill directly for
|
|
46
46
|
an audit or a lesson, or reach it via `/sp:dev-plan`'s docs step and `/sp:spur-init`'s `customize`.
|
|
@@ -84,48 +84,40 @@ stubs filled or deliberately documented; `bun run lint` passes where applicable.
|
|
|
84
84
|
Walk the §7 checklist. Each item pairs a detection command with the doc it validates:
|
|
85
85
|
|
|
86
86
|
```bash
|
|
87
|
-
# Real CLI surface vs.
|
|
87
|
+
# Real CLI surface vs. owning non-UI contracts
|
|
88
88
|
rg -n "\.command\('" apps/cli/src/commands/ # the true verb list
|
|
89
|
-
rg -n '^#### `spur ' docs/
|
|
89
|
+
rg -n '^#### `spur ' docs/design/ # documented commands
|
|
90
90
|
# → diff the two sets; a verb in code but not in 04 is T3 drift.
|
|
91
91
|
|
|
92
92
|
# 05 status rows vs. reality
|
|
93
|
-
|
|
93
|
+
cat docs/features/INDEX.md # generated feature states
|
|
94
94
|
# → spot-check each ✅/🔶 against code; confirm no ⏳ quietly shipped.
|
|
95
95
|
|
|
96
96
|
# 02 phase bullets name real things (no dead names)
|
|
97
97
|
# 03 module descriptions vs. the real tree
|
|
98
|
-
|
|
98
|
+
rg --files apps packages # real modules
|
|
99
99
|
# Frontmatter contracts (see contract-verify) + updated_at recency
|
|
100
100
|
git log -1 --format='%ci' -- docs/04_DESIGN.md # last touch vs. recent surface changes
|
|
101
101
|
```
|
|
102
102
|
|
|
103
|
-
**Repair protocol (§7
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
103
|
+
**Repair protocol (§7):** fix the authoritative statement first, then affected detail/index/entry
|
|
104
|
+
files. Preserve ADR numbers, original titles/dates and decision history; editorial condensation
|
|
105
|
+
follows §6.1, while actual reversals need a superseding decision. Record findings in a task/report.
|
|
106
|
+
Routine lessons go to existing learning/context storage (§8); no automatic constitution edits.
|
|
107
107
|
|
|
108
108
|
Output a **drift report**: per finding, `{ doc, what code says, what the doc says, authority, repair
|
|
109
109
|
}`. A clean report lists the checks run and that each returned no delta.
|
|
110
110
|
|
|
111
111
|
## sync-check (§5)
|
|
112
112
|
|
|
113
|
-
|
|
113
|
+
Read the live constitution §5; it owns the trigger table. Classify each changed fact, identify
|
|
114
|
+
its owner, and inspect the diff to confirm required synchronization. Update an index or AGENTS.md
|
|
115
|
+
only when its own facts changed. A satellite edit with an unchanged index pointer is synchronized.
|
|
114
116
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
| makes a new cross-cutting decision (or reverses one) | **T1** | `00` first (dated), then `03`, `01` if scope shifts |
|
|
120
|
-
| would contradict an existing ADR | **T2** | **stop** — add the superseding ADR entry first |
|
|
121
|
-
| completes/reorders a phase | **T5** | `02` (the real shipped name) |
|
|
122
|
-
| adds/cuts/defers scope | **T6** | `01` + placement in `02` |
|
|
123
|
-
| changes the doc map or process | **T7** | this file → re-sync `AGENTS.md` (§4.4) → siblings |
|
|
124
|
-
| plans a multi-wave batch | **T8** | schedule "doc sync" as an explicit work item |
|
|
125
|
-
|
|
126
|
-
Detection is a diff read: list the changed code/config paths, map each to its trigger, then confirm
|
|
127
|
-
the obligated doc was edited in the same change. A surface change with no `04` edit is the canonical
|
|
128
|
-
miss (the one this whole §5 table exists to prevent).
|
|
117
|
+
T1 applies only to real architectural choices passing §6.1. Feature approvals, task completion,
|
|
118
|
+
bugfixes restoring an existing contract and verification receipts do not justify ADR entries.
|
|
119
|
+
T7 applies only to operator-authorized governance corrections under §6.8; a routine doc edit or
|
|
120
|
+
lesson does not justify touching the constitution. Portable changes include in-scope init templates.
|
|
129
121
|
|
|
130
122
|
## contract-verify (§4.3)
|
|
131
123
|
|
|
@@ -140,19 +132,13 @@ stale given recent commits?"
|
|
|
140
132
|
|
|
141
133
|
## lesson-append (§8)
|
|
142
134
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
- [YYYY-MM-DD] <project>: <lesson — what went wrong / what to do instead>
|
|
147
|
-
```
|
|
135
|
+
Read constitution §8 for the existing project's destination. Record a useful, deduplicated
|
|
136
|
+
lesson in existing learning/context storage or a dated report, with evidence. Do not append
|
|
137
|
+
lessons to the constitution. Do not create a second learning ledger when one already exists.
|
|
148
138
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
promoted into a §6 rule (or §5 trigger) and removed from §8. Lessons are the inbox; §5/§6 are the
|
|
153
|
-
law.
|
|
154
|
-
- Lessons carry project provenance (this file is byte-identical across projects except §8 + the §3
|
|
155
|
-
tool column) — a lesson from one project is a warning, not yet a law, for the others.
|
|
139
|
+
A recurring lesson may justify proposing a document-governance correction; apply it only within
|
|
140
|
+
operator-authorized §6.8 scope. Task receipts remain in task records. Fresh project templates
|
|
141
|
+
contain neither inherited lessons nor fabricated decisions/status claims.
|
|
156
142
|
|
|
157
143
|
## What this skill is NOT
|
|
158
144
|
|