@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.
Files changed (177) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/config/config.example.yaml +29 -18
  3. package/config/config.global.yaml +10 -11
  4. package/config/pipeline-budgets.json +34 -2
  5. package/config/plugin-scripts.json +25 -0
  6. package/config/rules/boundary/config-loading-ownership.yaml +0 -3
  7. package/config/rules/boundary/dao-boundary.yaml +4 -17
  8. package/config/rules/boundary/planning-folder-hardcode.yaml +0 -1
  9. package/config/rules/boundary/sp-no-vendor-refs.yaml +3 -2
  10. package/config/rules/boundary/sp-runtime-path.yaml +3 -14
  11. package/config/rules/quality/coverage-gate.yaml +3 -14
  12. package/config/rules/quality/tsdoc-exports.yaml +4 -7
  13. package/config/rules/strict/http-boundaries.yaml +5 -8
  14. package/config/rules/strict/runtime-boundaries.yaml +1 -5
  15. package/config/rules/structure/protected-files.yaml +9 -3
  16. package/config/rules/structure/test-focus-skip.yaml +0 -2
  17. package/config/rules/structure/test-location.yaml +0 -5
  18. package/config/rules/surface/check-cli-surface.yaml +3 -2
  19. package/config/rules/typescript/bun-tooling.yaml +5 -7
  20. package/config/rules/typescript/guarded-happy-dom-register.yaml +0 -2
  21. package/config/rules/typescript/happy-dom-teardown.yaml +0 -2
  22. package/config/rules/typescript/no-biome-suppressions.yaml +0 -2
  23. package/config/rules/typescript/no-debugger.yaml +0 -2
  24. package/config/rules/typescript/no-eslint-suppressions.yaml +0 -4
  25. package/config/rules/typescript/no-leaky-module-mocks.yaml +6 -13
  26. package/config/rules/typescript/no-module-scope-import-calls.yaml +0 -2
  27. package/config/rules/typescript/no-syscall-emulation-in-boundary-mock.yaml +0 -3
  28. package/config/rules/typescript/no-unmocked-module-eval-side-effects.yaml +0 -3
  29. package/config/rules/typescript/output-boundaries.yaml +0 -3
  30. package/config/rules/typescript/prefer-accessible-role-for-button-queries.yaml +0 -3
  31. package/config/rules/ui/ui-import-boundary.yaml +1 -5
  32. package/config/templates/AGENTS.md +26 -23
  33. package/config/templates/docs/00_ADR.md +13 -23
  34. package/config/templates/docs/01_PRD.md +5 -2
  35. package/config/templates/docs/02_ROADMAP.md +9 -13
  36. package/config/templates/docs/03_ARCHITECTURE.md +2 -2
  37. package/config/templates/docs/04_DESIGN.md +12 -31
  38. package/config/templates/docs/05_FEATURES.md +6 -18
  39. package/config/templates/docs/99_PROJECT_CONSTITUTION.md +162 -394
  40. package/config/transition-shims.json +7 -7
  41. package/config/workflows/basic.yaml +4 -0
  42. package/config/workflows/docs-pipeline.yaml +13 -14
  43. package/config/workflows/feature-dev.yaml +20 -65
  44. package/config/workflows/history-anatomy.yaml +22 -1
  45. package/config/workflows/idea-pipeline.yaml +53 -97
  46. package/config/workflows/pr-review.yaml +21 -33
  47. package/config/workflows/task-pipeline.yaml +87 -330
  48. package/config/workflows/wayfinder-resolution.yaml +12 -26
  49. package/config/workflows/wrapup-pipeline.yaml +48 -189
  50. package/package.json +9 -9
  51. package/plugins/sp/README.md +22 -8
  52. package/plugins/sp/agents/expert-spur.md +41 -19
  53. package/plugins/sp/agents/super-reviewer.md +43 -8
  54. package/plugins/sp/lib/idea-handoff.generated.d.mts +17 -0
  55. package/plugins/sp/lib/idea-handoff.generated.mjs +1301 -0
  56. package/plugins/sp/plugin.json +1 -1
  57. package/plugins/sp/scripts/feature-dev-precheck.mjs +146 -0
  58. package/plugins/sp/scripts/feature-dev-precheck.ts +238 -0
  59. package/plugins/sp/scripts/idea-handoff.mjs +27 -0
  60. package/plugins/sp/scripts/idea-handoff.ts +44 -0
  61. package/plugins/sp/scripts/quality-gate.mjs +165 -0
  62. package/plugins/sp/scripts/quality-gate.ts +217 -0
  63. package/plugins/sp/scripts/verify-answer-lint.ts +21 -3
  64. package/plugins/sp/scripts/workflow-step-profile.mjs +319 -0
  65. package/plugins/sp/scripts/workflow-step-profile.ts +456 -0
  66. package/plugins/sp/scripts/wrapup-steps.mjs +350 -0
  67. package/plugins/sp/scripts/wrapup-steps.ts +466 -0
  68. package/plugins/sp/skills/conflict-finding/SKILL.md +6 -0
  69. package/plugins/sp/skills/daily-summary/SKILL.md +1 -1
  70. package/plugins/sp/skills/doc-evolve/SKILL.md +26 -40
  71. package/plugins/sp/skills/doc-evolve/references/operations.md +17 -30
  72. package/plugins/sp/skills/parallel-execution/references/dispatch-surface.md +1 -1
  73. package/plugins/sp/skills/spec-decomposition/references/decomposition.md +29 -0
  74. package/plugins/sp/skills/spur-cli/references/agent.md +56 -14
  75. package/plugins/sp/skills/spur-cli/references/message.md +30 -3
  76. package/plugins/sp/skills/spur-cli/references/projects.md +45 -1
  77. package/plugins/sp/skills/spur-cli/references/self.md +5 -4
  78. package/plugins/sp/skills/spur-cli/references/serve.md +5 -4
  79. package/plugins/sp/skills/spur-cli/references/tasks/verbs.md +17 -1
  80. package/plugins/sp/skills/spur-cli/references/tasks.md +32 -2
  81. package/plugins/sp/skills/spur-cli/references/team.md +21 -1
  82. package/plugins/sp/skills/spur-cli/references/workflows/operations.md +6 -3
  83. package/plugins/sp/skills/spur-cli/references/workflows/workflow-fit-and-tuning.md +57 -18
  84. package/plugins/sp/skills/spur-composer/SKILL.md +145 -0
  85. package/plugins/sp/skills/spur-dev/references/ac-style-guide.md +14 -0
  86. package/plugins/sp/skills/spur-dev/references/cross-cutting.md +3 -3
  87. package/plugins/sp/skills/spur-dev/references/done-housekeeping.md +12 -0
  88. package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +46 -4
  89. package/plugins/sp/skills/spur-dev/references/planning-workflow.md +24 -0
  90. package/plugins/sp/skills/spur-doctor/SKILL.md +138 -0
  91. package/plugins/sp/skills/taste-refactoring-api/README.md +43 -0
  92. package/plugins/sp/skills/taste-refactoring-api/SKILL.md +334 -0
  93. package/plugins/sp/skills/taste-refactoring-api/checklists/daily-api-review.md +71 -0
  94. package/plugins/sp/skills/taste-refactoring-api/examples/refactor-example.md +72 -0
  95. package/plugins/sp/skills/taste-refactoring-api/examples/review-template.md +93 -0
  96. package/plugins/sp/skills/taste-refactoring-api/references/api-refactoring-playbook.md +253 -0
  97. package/plugins/sp/skills/taste-refactoring-api/references/protocol-modes.md +79 -0
  98. package/plugins/sp/skills/taste-refactoring-api/references/research-basis.md +58 -0
  99. package/plugins/sp/skills/taste-refactoring-architect/README.md +26 -0
  100. package/plugins/sp/skills/taste-refactoring-architect/SKILL.md +471 -0
  101. package/plugins/sp/skills/taste-refactoring-architect/checklists/daily-architecture-review.md +48 -0
  102. package/plugins/sp/skills/taste-refactoring-architect/examples/refactor-example.md +55 -0
  103. package/plugins/sp/skills/taste-refactoring-architect/examples/review-template.md +51 -0
  104. package/plugins/sp/skills/taste-refactoring-architect/references/architecture-refactoring-playbook.md +173 -0
  105. package/plugins/sp/skills/taste-refactoring-architect/references/research-basis.md +28 -0
  106. package/plugins/sp/skills/taste-refactoring-tests/README.md +28 -0
  107. package/plugins/sp/skills/taste-refactoring-tests/SKILL.md +482 -0
  108. package/plugins/sp/skills/taste-refactoring-tests/checklists/daily-test-review.md +39 -0
  109. package/plugins/sp/skills/taste-refactoring-tests/examples/refactor-example.md +85 -0
  110. package/plugins/sp/skills/taste-refactoring-tests/examples/review-template.md +59 -0
  111. package/plugins/sp/skills/taste-refactoring-tests/references/research-basis.md +47 -0
  112. package/plugins/sp/skills/taste-refactoring-tests/references/test-refactoring-playbook.md +222 -0
  113. package/plugins/sp/skills/taste-refactoring-ui/README.md +12 -0
  114. package/plugins/sp/skills/taste-refactoring-ui/SKILL.md +290 -0
  115. package/plugins/sp/skills/taste-refactoring-ui/checklists/daily-ui-review.md +72 -0
  116. package/plugins/sp/skills/taste-refactoring-ui/examples/review-template.md +51 -0
  117. package/plugins/sp/skills/taste-refactoring-ui/references/refactoring-ui-playbook.md +170 -0
  118. package/plugins/sp/skills/wayfinder/SKILL.md +2 -2
  119. package/plugins/sp/skills/wayfinder/references/pipeline-resolution.md +30 -0
  120. package/schemas/spur-config.schema.json +49 -0
  121. package/spur.js +46936 -44198
  122. package/web/_astro/{BoardApp.CHQ1lycZ.js → BoardApp.B1U26g3I.js} +97 -95
  123. package/web/_astro/BoardApp.Csgyg-lS.js +1 -0
  124. package/web/_astro/{TaskDetail.GKfQJ60c.js → TaskDetail.DwPqpq7v.js} +1 -1
  125. package/web/_astro/{arc.DWEtA3Tx.js → arc.CweZEjN2.js} +1 -1
  126. package/web/_astro/{architectureDiagram-3BPJPVTR.DB42oWmP.js → architectureDiagram-3BPJPVTR.D89pbDuv.js} +1 -1
  127. package/web/_astro/{blockDiagram-GPEHLZMM.rhv-zNQV.js → blockDiagram-GPEHLZMM.BOuTeEpX.js} +1 -1
  128. package/web/_astro/{c4Diagram-AAUBKEIU.Ci4-4VvY.js → c4Diagram-AAUBKEIU.CASbkWZF.js} +1 -1
  129. package/web/_astro/channel.Cx6sXxhq.js +1 -0
  130. package/web/_astro/{chunk-2J33WTMH.Cc9veUgf.js → chunk-2J33WTMH.BKQYtOvY.js} +1 -1
  131. package/web/_astro/{chunk-4BX2VUAB.Bec9c4eI.js → chunk-4BX2VUAB.9sHLdMtG.js} +1 -1
  132. package/web/_astro/{chunk-55IACEB6.DoV8S1iB.js → chunk-55IACEB6.wOLXWlPs.js} +1 -1
  133. package/web/_astro/{chunk-727SXJPM.DwR-Qlyj.js → chunk-727SXJPM.DovFbwg3.js} +1 -1
  134. package/web/_astro/{chunk-AQP2D5EJ.ND_a81WY.js → chunk-AQP2D5EJ.B1Weod1X.js} +1 -1
  135. package/web/_astro/{chunk-FMBD7UC4.Wv_jwG48.js → chunk-FMBD7UC4.TEMS04st.js} +1 -1
  136. package/web/_astro/{chunk-ND2GUHAM.CXKXCMmp.js → chunk-ND2GUHAM.Cp8VT1wQ.js} +1 -1
  137. package/web/_astro/{chunk-QZHKN3VN.nkaoNYQq.js → chunk-QZHKN3VN.BzATdEcP.js} +1 -1
  138. package/web/_astro/{classDiagram-4FO5ZUOK.cMQcVlQu.js → classDiagram-4FO5ZUOK.C9BOCfAO.js} +1 -1
  139. package/web/_astro/{classDiagram-v2-Q7XG4LA2.cMQcVlQu.js → classDiagram-v2-Q7XG4LA2.C9BOCfAO.js} +1 -1
  140. package/web/_astro/{cose-bilkent-S5V4N54A.OaDJ7Mr2.js → cose-bilkent-S5V4N54A.DUnr4UAw.js} +1 -1
  141. package/web/_astro/{cynefin-OW5HDTMX.Chi8IphF.js → cynefin-OW5HDTMX.rYq5uM3D.js} +1 -1
  142. package/web/_astro/{cytoscape.esm.DzSz-X2X.js → cytoscape.esm.BB4DxJjf.js} +1 -1
  143. package/web/_astro/{dagre-BM42HDAG.CzK2t_Fp.js → dagre-BM42HDAG.CWeNKe3I.js} +1 -1
  144. package/web/_astro/{diagram-2AECGRRQ.DRvxlVS7.js → diagram-2AECGRRQ.DCkfls10.js} +1 -1
  145. package/web/_astro/{diagram-5GNKFQAL.CnYvNdwA.js → diagram-5GNKFQAL.D5U4JCka.js} +1 -1
  146. package/web/_astro/{diagram-KO2AKTUF.CpLpMw5R.js → diagram-KO2AKTUF.BZJgqaqG.js} +1 -1
  147. package/web/_astro/{diagram-LMA3HP47.JTb78qUA.js → diagram-LMA3HP47.DoMeHvPR.js} +1 -1
  148. package/web/_astro/{diagram-OG6HWLK6.Bk-1jDIb.js → diagram-OG6HWLK6.B50qwwWX.js} +1 -1
  149. package/web/_astro/{erDiagram-TEJ5UH35.D8hN9GZq.js → erDiagram-TEJ5UH35.DdGPG6LK.js} +1 -1
  150. package/web/_astro/{flowDiagram-I6XJVG4X.-6zQr6m5.js → flowDiagram-I6XJVG4X.QP2MJ12u.js} +1 -1
  151. package/web/_astro/{ganttDiagram-6RSMTGT7.DboLQ9ca.js → ganttDiagram-6RSMTGT7.BI6LgKSy.js} +1 -1
  152. package/web/_astro/{gitGraphDiagram-PVQCEYII.4tYvJKGR.js → gitGraphDiagram-PVQCEYII.npPZiC2G.js} +1 -1
  153. package/web/_astro/index.DayyIngm.css +1 -0
  154. package/web/_astro/{infoDiagram-5YYISTIA.Bd9rXpsB.js → infoDiagram-5YYISTIA.DCJCBVbp.js} +1 -1
  155. package/web/_astro/{ishikawaDiagram-YF4QCWOH.CvMoaf67.js → ishikawaDiagram-YF4QCWOH.BMLV-3I1.js} +1 -1
  156. package/web/_astro/{journeyDiagram-JHISSGLW.Ccy1CA7y.js → journeyDiagram-JHISSGLW.LE58crde.js} +1 -1
  157. package/web/_astro/{kanban-definition-UN3LZRKU.0MaMqHNS.js → kanban-definition-UN3LZRKU.BPbz8rH9.js} +1 -1
  158. package/web/_astro/{linear.CHXgcIbN.js → linear.DhZaBtYh.js} +1 -1
  159. package/web/_astro/{mermaid.core.Ca-kcelG.js → mermaid.core.BD5-jXum.js} +6 -6
  160. package/web/_astro/{mindmap-definition-RKZ34NQL.BUIDlHa0.js → mindmap-definition-RKZ34NQL.MTJyrQ65.js} +1 -1
  161. package/web/_astro/ordinal.BYWQX77i.js +1 -0
  162. package/web/_astro/{pieDiagram-4H26LBE5.2dX3CU1s.js → pieDiagram-4H26LBE5.BrDhDvIS.js} +1 -1
  163. package/web/_astro/{quadrantDiagram-W4KKPZXB.B3LBlRiv.js → quadrantDiagram-W4KKPZXB.71d73_5N.js} +1 -1
  164. package/web/_astro/{requirementDiagram-4Y6WPE33.X12I2uNx.js → requirementDiagram-4Y6WPE33.Bga6UF-z.js} +1 -1
  165. package/web/_astro/{sankeyDiagram-5OEKKPKP.BXohIHqx.js → sankeyDiagram-5OEKKPKP.BnHs4K82.js} +1 -1
  166. package/web/_astro/{sequenceDiagram-3UESZ5HK.C37ZIUzg.js → sequenceDiagram-3UESZ5HK.DsfY2gnj.js} +1 -1
  167. package/web/_astro/{stateDiagram-AJRCARHV.BRgz317z.js → stateDiagram-AJRCARHV.DvsTSc9a.js} +1 -1
  168. package/web/_astro/{stateDiagram-v2-BHNVJYJU.7VYSXN9-.js → stateDiagram-v2-BHNVJYJU.DxzzmHUR.js} +1 -1
  169. package/web/_astro/{timeline-definition-PNZ67QCA.BVNz_HiN.js → timeline-definition-PNZ67QCA.4ZuQmOTt.js} +1 -1
  170. package/web/_astro/{vennDiagram-CIIHVFJN.CHVDkPX4.js → vennDiagram-CIIHVFJN.Ck5Q86SG.js} +1 -1
  171. package/web/_astro/{wardleyDiagram-YWT4CUSO.EQQ_qT9v.js → wardleyDiagram-YWT4CUSO.BK7k2hXr.js} +1 -1
  172. package/web/_astro/{xychartDiagram-2RQKCTM6.DrAT9WoP.js → xychartDiagram-2RQKCTM6.DfCrgauK.js} +1 -1
  173. package/web/index.html +2 -2
  174. package/web/_astro/BoardApp.DV9kx0wo.js +0 -1
  175. package/web/_astro/channel.BAI6xLeV.js +0 -1
  176. package/web/_astro/index.Dcr_8fiK.css +0 -1
  177. 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.0"
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, lessons §8). This skill is a runbook for executing §5/§7/§8;
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 (the 8-item checklist) | `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 (triggers T1–T8) | git diff of code/config vs. the matching doc edit |
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** | Append a dated lesson; dedup; promote recurring ones to a rule | §8 | format-check the line; `rg` for an equivalent before adding |
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. what 04 / AGENTS.md / 00 claim
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/04_DESIGN.md # documented commands
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
- rg -n '✅|🔶|⏳|💤' docs/05_FEATURES.md # claimed states
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
- fd -t d -d 2 . apps packages # real modules
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, always this order):** fix the **authoritative** doc first (append-only files
104
- by dated amendment, never rewrite), then the derived docs that restate/sequence it, then
105
- `AGENTS.md`, then **flag what drifted and why** in the commit/task (a silent fix hides the systemic
106
- cause). Anything systemic becomes a §8 lesson — or, if it recurs, a new §6 rule.
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
- Given a change (a diff, or a just-finished task), check the same-commit obligations:
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
- | If the change… | Trigger | …the same commit must touch |
116
- | -------------- | ------- | --------------------------- |
117
- | adds/changes a command, flag, config key, env var, schema, DTO | **T3** | `04_DESIGN` + the `AGENTS.md` surface block |
118
- | ships a feature or changes its state | **T4** | its `05` row (+ a `01` scope row if new surface) |
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
- Append a lesson to the right per-file section of §8:
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
- - **Low threshold** — when in doubt, append. **Check for an equivalent first** (`rg` the section);
150
- bump its date instead of duplicating.
151
- - **Promotion is the only sanctioned deletion:** a lesson that recurs or hardens into practice is
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