@polymorphism-tech/morph-spec 8.47.0 → 9.0.0

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 (222) hide show
  1. package/CLAUDE.md +48 -39
  2. package/README.md +3 -4
  3. package/bin/morph-spec.js +37 -51
  4. package/bin/validate.js +3 -111
  5. package/claude-plugin.json +1 -1
  6. package/docs/CHEATSHEET.md +1 -1
  7. package/docs/QUICKSTART.md +1 -1
  8. package/framework/CLAUDE.md +41 -101
  9. package/framework/MORPH.md +95 -252
  10. package/framework/agents.json +75 -18
  11. package/framework/commands/morph-apply.md +5 -28
  12. package/framework/commands/morph-archive.md +1 -1
  13. package/framework/commands/morph-hotfix.md +8 -8
  14. package/framework/commands/morph-infra.md +1 -1
  15. package/framework/commands/morph-preflight.md +2 -3
  16. package/framework/commands/morph-proposal.md +2 -50
  17. package/framework/commands/morph-status.md +3 -3
  18. package/framework/docs/CLI.md +585 -286
  19. package/framework/evals/review-checklist.md +82 -41
  20. package/framework/hooks/README.md +10 -11
  21. package/framework/hooks/claude-code/core/comment-guard.js +1 -1
  22. package/framework/hooks/claude-code/core/denylist.json +33 -3
  23. package/framework/hooks/claude-code/core/enforce-phase-writes.js +16 -20
  24. package/framework/hooks/claude-code/core/gate-guard.js +2 -11
  25. package/framework/hooks/claude-code/core/pre-bash-denylist.js +38 -0
  26. package/framework/hooks/claude-code/core/protect-spec-files.js +30 -242
  27. package/framework/hooks/claude-code/core/state-sync.js +7 -21
  28. package/framework/hooks/claude-code/core/telemetry-log.js +3 -3
  29. package/framework/hooks/claude-code/core/validate-completion.js +33 -97
  30. package/framework/hooks/claude-code/core/warn-bash-source-write.js +31 -0
  31. package/framework/hooks/claude-code/core/worktree-context.js +5 -4
  32. package/framework/hooks/claude-code/statusline.py +55 -41
  33. package/framework/hooks/package.json +3 -0
  34. package/framework/hooks/shared/activity-logger.js +1 -1
  35. package/framework/hooks/shared/build-check.js +10 -6
  36. package/framework/hooks/shared/command-tokens.js +169 -0
  37. package/framework/hooks/shared/comment-rules.js +1 -1
  38. package/framework/hooks/shared/comment-scanner.js +10 -23
  39. package/framework/hooks/shared/comment-stripper.js +1 -1
  40. package/framework/hooks/shared/e2e-check.js +64 -27
  41. package/framework/hooks/shared/errata.js +72 -0
  42. package/framework/hooks/shared/feature-locator.js +1 -1
  43. package/framework/hooks/shared/frontend-locations.js +6 -0
  44. package/framework/hooks/shared/gate-question.js +18 -0
  45. package/framework/hooks/shared/phase-utils.js +15 -32
  46. package/framework/hooks/shared/slice-progress.js +118 -0
  47. package/framework/hooks/shared/spawn-command.js +23 -0
  48. package/framework/hooks/shared/state-reader.js +61 -87
  49. package/framework/hooks/shared/state-writer.js +2 -3
  50. package/framework/mcp/registry.json +3 -4
  51. package/framework/phases.json +23 -39
  52. package/framework/schemas/config.schema.json +14 -9
  53. package/framework/schemas/feature.schema.json +15 -50
  54. package/framework/skills/README.md +3 -4
  55. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  56. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  57. package/framework/skills/core/morph-apply/SKILL.md +149 -222
  58. package/framework/skills/core/morph-hotfix/SKILL.md +33 -42
  59. package/framework/skills/core/morph-machine/SKILL.md +2 -2
  60. package/framework/skills/core/morph-media/SKILL.md +1 -1
  61. package/framework/skills/core/morph-plan/SKILL.md +137 -299
  62. package/framework/skills/core/morph-review/SKILL.md +111 -136
  63. package/framework/skills/core/morph-standards/SKILL.md +27 -39
  64. package/framework/skills/core/morph-uiux/SKILL.md +77 -152
  65. package/framework/standards/STANDARDS.json +86 -1218
  66. package/framework/standards/STANDARDS.meta.json +1305 -0
  67. package/framework/standards/ai-agents/evals-with-cache.md +1 -2
  68. package/framework/standards/ai-agents/setup.md +1 -1
  69. package/framework/standards/architecture/code-comments.md +6 -6
  70. package/framework/standards/integration/mcp/mcp-tools.md +7 -7
  71. package/framework/templates/README.md +2 -3
  72. package/framework/templates/REGISTRY.json +6 -170
  73. package/framework/templates/docs/plan.md +72 -0
  74. package/framework/templates/dotnet/ai-kit/README.md +1 -9
  75. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Morph.AiKit.Tests.csproj +0 -10
  76. package/framework/templates/meta-prompts/squad-leaders/backend-squad.md +1 -1
  77. package/framework/templates/meta-prompts/squad-leaders/frontend-squad.md +1 -1
  78. package/framework/templates/meta-prompts/squad-leaders/squad-leader.md +1 -1
  79. package/framework/templates/meta-prompts/validators/pre-commit-validator.md +3 -4
  80. package/package.json +1 -1
  81. package/src/commands/e2e/e2e.js +2 -2
  82. package/src/commands/observability/cost.js +18 -60
  83. package/src/commands/project/doctor-procs.js +1 -1
  84. package/src/commands/project/doctor.js +9 -9
  85. package/src/commands/project/fleet.js +4 -4
  86. package/src/commands/project/heavy.js +3 -3
  87. package/src/commands/project/init.js +4 -0
  88. package/src/commands/project/setup-infra-cmd.js +2 -1
  89. package/src/commands/project/stack-scan.js +58 -0
  90. package/src/commands/project/standards.js +79 -25
  91. package/src/commands/project/status.js +29 -69
  92. package/src/commands/project/update.js +17 -1
  93. package/src/commands/project/worktree.js +1 -1
  94. package/src/commands/state/advance.js +62 -46
  95. package/src/commands/state/approve.js +81 -141
  96. package/src/commands/state/archive.js +20 -84
  97. package/src/commands/state/auto-approval.js +68 -0
  98. package/src/commands/state/create.js +28 -1
  99. package/src/commands/state/delete.js +1 -1
  100. package/src/commands/state/finish.js +11 -23
  101. package/src/commands/state/gate-check.js +85 -84
  102. package/src/commands/state/gate-decision.js +6 -4
  103. package/src/commands/state/handoff.js +1 -1
  104. package/src/commands/state/list.js +9 -9
  105. package/src/commands/state/report-check.js +157 -311
  106. package/src/commands/state/retype.js +9 -3
  107. package/src/commands/validation/mutate.js +77 -14
  108. package/src/commands/validation/plan-check.js +81 -0
  109. package/src/commands/validation/validate-feature.js +60 -135
  110. package/src/commands/validation/verify.js +40 -72
  111. package/src/core/paths/output-schema.js +19 -66
  112. package/src/core/state/gate-order.js +5 -6
  113. package/src/core/state/lifecycle-audit.js +2 -2
  114. package/src/core/state/state-manager.js +17 -98
  115. package/src/core/templates/template-registry.js +2 -2
  116. package/src/core/templates/template-seed.js +22 -0
  117. package/src/core/work-types.js +10 -10
  118. package/src/dashboard/app/components/ActivityFeed.jsx +2 -3
  119. package/src/dashboard/app/components/CommandPalette.jsx +5 -5
  120. package/src/dashboard/app/components/EntityLink.jsx +1 -1
  121. package/src/dashboard/app/hooks/useMorphQuery.js +1 -1
  122. package/src/dashboard/app/lib/phases.js +2 -4
  123. package/src/dashboard/app/pages/Activity.jsx +1 -1
  124. package/src/dashboard/app/pages/FeatureDetail.jsx +139 -129
  125. package/src/dashboard/app/pages/Features.jsx +9 -9
  126. package/src/dashboard/app/pages/Guide.jsx +2 -2
  127. package/src/dashboard/app/pages/PhasePipeline.jsx +3 -4
  128. package/src/dashboard/app/pages/Standards.jsx +1 -1
  129. package/src/dashboard/scanner/activity-scanner.js +5 -15
  130. package/src/dashboard/scanner/agents-scanner.js +1 -1
  131. package/src/dashboard/scanner/docs-scanner.js +2 -2
  132. package/src/dashboard/scanner/features-scanner.js +95 -60
  133. package/src/dashboard/scanner/project-api.js +1 -1
  134. package/src/dashboard/scanner/standards-scanner.js +2 -1
  135. package/src/lib/ci/ci-verify.js +17 -1
  136. package/src/lib/ci/github-ci.js +1 -1
  137. package/src/lib/detectors/index.js +3 -8
  138. package/src/lib/generators/handoff-generator.js +34 -65
  139. package/src/lib/generators/settings-generator.js +11 -89
  140. package/src/lib/git/evidence-staleness.js +1 -1
  141. package/src/lib/git/tree-fingerprint.js +19 -7
  142. package/src/lib/git/tree-state.js +2 -2
  143. package/src/lib/heavy/cleanup.js +1 -5
  144. package/src/lib/installers/memory-installer.js +6 -8
  145. package/src/lib/mutation/commands.js +3 -2
  146. package/src/lib/{tasks/dotnet-test-introspection.js → mutation/dotnet-introspection.js} +17 -282
  147. package/src/lib/mutation/plan.js +14 -6
  148. package/src/lib/mutation/runner.js +1 -1
  149. package/src/lib/mutation/subset.js +46 -9
  150. package/src/lib/plan/artifact-vocabulary.js +60 -0
  151. package/src/lib/plan/checks.js +234 -0
  152. package/src/lib/plan/plan-parser.js +209 -0
  153. package/src/lib/plan/plan-template.js +18 -0
  154. package/src/lib/review-reports.js +222 -0
  155. package/src/lib/risk-detector.js +131 -140
  156. package/src/lib/specs/adr-index.js +50 -69
  157. package/src/lib/specs/design-seed.js +18 -0
  158. package/src/lib/specs/frontmatter.js +44 -0
  159. package/src/lib/specs/gfm-table.js +17 -0
  160. package/src/lib/specs/old-format.js +68 -0
  161. package/src/lib/specs/revocations.js +37 -95
  162. package/src/lib/specs/screen-checklist.js +18 -17
  163. package/src/lib/stack/dotnet-build-ignores.js +47 -0
  164. package/src/lib/stack/stack-scan.js +267 -0
  165. package/src/lib/standards/registry.js +43 -0
  166. package/src/lib/standards/sections.js +58 -0
  167. package/src/lib/tasks/eval-node.js +4 -12
  168. package/src/lib/tasks/test-nodes.js +1 -74
  169. package/src/lib/tasks/test-runner.js +75 -117
  170. package/src/lib/telemetry/metrics.js +8 -69
  171. package/src/lib/telemetry/telemetry-reader.js +5 -118
  172. package/src/lib/telemetry/verify-event.js +5 -7
  173. package/src/lib/validators/code-budget/code-budget-validator.js +1 -1
  174. package/src/lib/validators/contracts/contract-compliance-validator.js +2 -2
  175. package/src/lib/validators/reuse/reuse-validator.js +22 -45
  176. package/src/lib/validators/traceability-check.js +85 -129
  177. package/src/lib/validators/verify-runner.js +118 -235
  178. package/src/lib/worktree/docker-audit.js +1 -2
  179. package/src/lib/worktree/fleet-scanner.js +25 -38
  180. package/src/scripts/setup-infra.js +9 -0
  181. package/src/utils/agents-installer.js +76 -9
  182. package/src/utils/claude-settings-manager.js +0 -35
  183. package/src/utils/file-copier.js +27 -1
  184. package/src/utils/hooks-installer.js +16 -9
  185. package/src/utils/skills-installer.js +1 -2
  186. package/framework/evals/trace-schema.md +0 -67
  187. package/framework/hooks/claude-code/core/tasks-json-guard.js +0 -83
  188. package/framework/hooks/claude-code/core/trace-autogen.js +0 -277
  189. package/framework/hooks/git/pre-commit/specs.sh +0 -50
  190. package/framework/hooks/shared/tasks-validator.js +0 -111
  191. package/framework/rules/morph-task-tracking.md +0 -54
  192. package/framework/rules/morph-workflow.md +0 -244
  193. package/framework/schemas/tasks.schema.json +0 -79
  194. package/framework/skills/core/morph-brainstorming/SKILL.md +0 -161
  195. package/framework/skills/core/morph-implement/SKILL.md +0 -194
  196. package/framework/skills/core/morph-proposal/SKILL.md +0 -152
  197. package/framework/skills/core/morph-stack-scan/SKILL.md +0 -145
  198. package/framework/templates/docs/proposal.md +0 -192
  199. package/framework/templates/docs/spec.md +0 -57
  200. package/framework/templates/docs/ui-components.md +0 -52
  201. package/framework/templates/docs/ui-design-system.md +0 -76
  202. package/framework/templates/docs/ui-flows.md +0 -167
  203. package/framework/templates/examples/spec-examples.md +0 -91
  204. package/framework/templates/feature/decisions.md +0 -124
  205. package/framework/templates/feature/recap.md +0 -145
  206. package/framework/templates/tasks.template.json +0 -17
  207. package/src/commands/state/usecase.js +0 -143
  208. package/src/commands/tasks/dag.js +0 -181
  209. package/src/lib/agents/output-collector.js +0 -76
  210. package/src/lib/evaluator-report.js +0 -104
  211. package/src/lib/specs/acervo-format.js +0 -70
  212. package/src/lib/specs/acervo-lifecycle.js +0 -264
  213. package/src/lib/specs/done-criteria-check.js +0 -200
  214. package/src/lib/specs/usecase-check.js +0 -301
  215. package/src/lib/specs/usecase-promoter.js +0 -301
  216. package/src/lib/specs/usecase-registry.js +0 -237
  217. package/src/lib/tasks/dispatch-planner.js +0 -319
  218. package/src/lib/tasks/mermaid-renderer.js +0 -132
  219. package/src/lib/tasks/task-classifier.js +0 -30
  220. package/src/lib/tasks/task-parser.js +0 -209
  221. package/src/lib/tasks/tasks-json-parser.js +0 -296
  222. package/src/lib/tasks/test-scope.js +0 -597
@@ -1,277 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * trace-autogen.js — PostToolUse hook (Write|Edit)
4
- *
5
- * Auto-generates the execution trace
6
- * `.morph/traces/YYYY-MM-DD-{feature}-implement.json` from tasks.json +
7
- * recap.md whenever tasks.json is written during
8
- * the implement phase.
9
- *
10
- * This removes the manual trace-writing step from the skills (friction item
11
- * observed in MORPH_GHLBrain 2026-07-11: 5 writes per task). The tasks.json
12
- * status flip — the last write of every task's ceremony — now produces the
13
- * trace for free: deterministic, idempotent, always in sync with the real
14
- * on-disk state. The skills must NOT write traces by hand anymore.
15
- *
16
- * Sources:
17
- * - tasks.json → task set, status (done→completed, skipped→skipped,
18
- * blocked→failed; pending/in_progress stay out), outputs
19
- * - recap.md → per-task `**Standard:**` id and `**Persona:**` line
20
- *
21
- * The most recent existing `*-{feature}-implement.json` is updated in place
22
- * (session_id/started_at preserved); a new dated file is created otherwise.
23
- * `completed_at` + phase_summary are stamped when no task is left
24
- * pending/in_progress. Fail-open: any error exits 0 silently.
25
- *
26
- * This is also the vantage point for TaskStart/TaskEnd telemetry: a
27
- * pending→in_progress flip and a flip to done/skipped are the only two task
28
- * transitions the harness can observe (tasks.json is the single place task
29
- * status is written), so this hook diffs each task's previous status against
30
- * `.morph/logs/task-times.json` and appends the matching event via the shared
31
- * telemetry logger (see `updateTaskTimes` below). The same start/end pair
32
- * feeds `duration_seconds` in the trace entry.
33
- */
34
-
35
- import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync } from 'fs';
36
- import { join } from 'path';
37
- import { runStandalone } from '../../shared/hook-response.js';
38
- import { stateExists } from '../../shared/state-reader.js';
39
- import { getFilePath, resolveMorphRoot } from '../../shared/payload-utils.js';
40
- import { appendTelemetryEvent } from '../../shared/telemetry-logger.js';
41
-
42
- function projectDir() {
43
- return process.env.CLAUDE_PROJECT_DIR || process.cwd();
44
- }
45
-
46
- function extractFeature(filePath) {
47
- const normalized = String(filePath).replace(/\\/g, '/');
48
- const match = normalized.match(/\.morph\/features\/([^/]+)\/2-plan\/tasks\.json$/);
49
- return match ? match[1] : null;
50
- }
51
-
52
- /**
53
- * Parse recap.md task blocks (`## T{N} — ...`) into
54
- * { [id]: { standard, persona } }. Best-effort: missing fields stay null.
55
- */
56
- function parseRecap(raw) {
57
- const map = {};
58
- const blocks = String(raw).split(/^## (?=T\d+)/m).slice(1);
59
- for (const block of blocks) {
60
- const id = (block.match(/^(T\d+)/) ?? [])[1];
61
- if (!id) continue;
62
- const standard = (block.match(/\*\*Standard:\*\*\s*([^\s(]+)/) ?? [])[1] ?? null;
63
- const persona = (block.match(/\*\*Persona:\*\*\s*(.+)$/m) ?? [])[1]?.trim() ?? null;
64
- map[id] = { standard, persona };
65
- }
66
- return map;
67
- }
68
-
69
- /** session_id in the trace-schema format YYYY-MM-DD-HH-MM-SS. */
70
- function sessionId(d) {
71
- const p = (n) => String(n).padStart(2, '0');
72
- return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}-${p(d.getHours())}-${p(d.getMinutes())}-${p(d.getSeconds())}`;
73
- }
74
-
75
- /** tasks.json status → trace-schema status. Unsettled statuses are omitted. */
76
- const STATUS_MAP = { done: 'completed', skipped: 'skipped', blocked: 'failed' };
77
-
78
- const TASK_TIMES_PATH = join('.morph', 'logs', 'task-times.json');
79
-
80
- /**
81
- * Read `.morph/logs/task-times.json`: `{ [feature]: { [taskId]: { status,
82
- * start?, end? } } }`. Nested by feature — not flat by task id, as a literal
83
- * reading of "keyed by taskId" might suggest — because task ids (T1, T2…)
84
- * are only unique WITHIN a feature; a flat map would let two sibling
85
- * features in the same tree (see worktree-state-resolution.test.js for that
86
- * shape) clobber each other's start/end timestamps.
87
- * Fail-open: a missing/corrupt file reads as {}.
88
- */
89
- function readTaskTimes(root) {
90
- const p = join(root, TASK_TIMES_PATH);
91
- if (!existsSync(p)) return {};
92
- try {
93
- return JSON.parse(readFileSync(p, 'utf8'));
94
- } catch {
95
- return {};
96
- }
97
- }
98
-
99
- function writeTaskTimes(root, allTimes) {
100
- mkdirSync(join(root, '.morph', 'logs'), { recursive: true });
101
- writeFileSync(join(root, TASK_TIMES_PATH), JSON.stringify(allTimes, null, 2), 'utf8');
102
- }
103
-
104
- /**
105
- * Diff each task's PREVIOUS status (from `times`, this feature's slice of
106
- * task-times.json) against its status in the tasks.json that was just
107
- * written, and append the matching telemetry event via the shared logger —
108
- * same shape/mechanism as every other event in events.jsonl, just a
109
- * different `event` value.
110
- *
111
- * - pending → in_progress ⇒ TaskStart, stamp `start`
112
- * - anything → done/skipped ⇒ TaskEnd (carries the new status), stamp `end`
113
- * - first sight of a task id (no prior entry) only seeds the baseline,
114
- * silently — there is no prior status to diff, and emitting an event here
115
- * would fabricate a transition that never happened (e.g. a task whose
116
- * first-ever tasks.json write already shows it `done`, from historical
117
- * data predating this file).
118
- *
119
- * Mutates `times` in place and returns whether anything changed, so the
120
- * caller only rewrites task-times.json when needed and the entries built
121
- * right after this call see the freshly-stamped start/end for
122
- * `duration_seconds`.
123
- */
124
- function updateTaskTimes(times, tasks, feature, root, nowIso) {
125
- let changed = false;
126
- for (const t of tasks) {
127
- const prev = times[t.id];
128
- if (!prev) {
129
- times[t.id] = { status: t.status };
130
- changed = true;
131
- continue;
132
- }
133
- if (prev.status === t.status) continue;
134
-
135
- if (prev.status === 'pending' && t.status === 'in_progress') {
136
- appendTelemetryEvent({ event: 'TaskStart', feature, task: t.id, phase: 'implement' }, root);
137
- times[t.id] = { ...prev, status: t.status, start: nowIso };
138
- } else if (t.status === 'done' || t.status === 'skipped') {
139
- appendTelemetryEvent({ event: 'TaskEnd', feature, task: t.id, status: t.status, phase: 'implement' }, root);
140
- times[t.id] = { ...prev, status: t.status, end: nowIso };
141
- } else {
142
- times[t.id] = { ...prev, status: t.status };
143
- }
144
- changed = true;
145
- }
146
- return changed;
147
- }
148
-
149
- /** Seconds between recorded start/end for a task, or null when either is missing. */
150
- function taskDuration(times, taskId) {
151
- const rec = times[taskId];
152
- if (!rec?.start || !rec?.end) return null;
153
- const secs = (new Date(rec.end).getTime() - new Date(rec.start).getTime()) / 1000;
154
- return Number.isFinite(secs) ? Math.round(secs * 10) / 10 : null;
155
- }
156
-
157
- /**
158
- * @param {object} payload - parsed PostToolUse stdin payload (Write|Edit)
159
- * @returns {Promise<null>} always null — this hook is pure side-effect (trace
160
- * file + task-times.json), never prints/injects context.
161
- */
162
- export async function run(payload) {
163
- try {
164
- await generateTrace(payload);
165
- } catch {
166
- // Fail-open
167
- }
168
- return null;
169
- }
170
-
171
- async function generateTrace(payload) {
172
- if (!payload) return;
173
-
174
- const filePath = getFilePath(payload);
175
- const feature = filePath ? extractFeature(filePath) : null;
176
- if (!feature) return;
177
-
178
- // Read the sources — and write the trace — in the tree that owns the written
179
- // tasks.json. Resolving from the session instead made this hook INERT for
180
- // every worktree feature: the root's copy of the feature has no
181
- // `2-plan/tasks.json`, so the `existsSync` guard below returned silently and
182
- // no trace was ever produced for work done in a worktree.
183
- const cwd = resolveMorphRoot(filePath, projectDir());
184
- if (!stateExists(cwd)) return;
185
-
186
- const featureDir = join(cwd, '.morph', 'features', feature);
187
- const tasksPath = join(featureDir, '2-plan', 'tasks.json');
188
- if (!existsSync(tasksPath)) return;
189
- const tasksDoc = JSON.parse(readFileSync(tasksPath, 'utf8'));
190
- const tasks = Array.isArray(tasksDoc.tasks) ? tasksDoc.tasks : [];
191
-
192
- const now = new Date();
193
-
194
- // TaskStart/TaskEnd telemetry + duration_seconds source: diff each task's
195
- // previous status (persisted below) against the status just written.
196
- const allTimes = readTaskTimes(cwd);
197
- const times = allTimes[feature] ?? (allTimes[feature] = {});
198
- if (updateTaskTimes(times, tasks, feature, cwd, now.toISOString())) {
199
- writeTaskTimes(cwd, allTimes);
200
- }
201
-
202
- let recap = {};
203
- const recapPath = join(featureDir, '3-implement', 'recap.md');
204
- if (existsSync(recapPath)) {
205
- try { recap = parseRecap(readFileSync(recapPath, 'utf8')); } catch { recap = {}; }
206
- }
207
-
208
- const tracesDir = join(cwd, '.morph', 'traces');
209
- mkdirSync(tracesDir, { recursive: true });
210
-
211
- // Multi-session phases update the same file incrementally (trace-schema):
212
- // reuse the most recent trace for this feature+phase if one exists.
213
- const suffix = `-${feature}-implement.json`;
214
- const existing = readdirSync(tracesDir)
215
- .filter(f => /^\d{4}-\d{2}-\d{2}-/.test(f) && f.endsWith(suffix))
216
- .sort()
217
- .pop();
218
-
219
- let previous = null;
220
- if (existing) {
221
- try { previous = JSON.parse(readFileSync(join(tracesDir, existing), 'utf8')); } catch { previous = null; }
222
- }
223
-
224
- const fileName = existing ?? `${now.toISOString().slice(0, 10)}${suffix}`;
225
-
226
- const entries = tasks
227
- .filter(t => STATUS_MAP[t.status])
228
- .map(t => {
229
- const recapEntry = recap[t.id] ?? {};
230
- return {
231
- id: t.id,
232
- title: t.title ?? null,
233
- status: STATUS_MAP[t.status],
234
- standards_consulted: recapEntry.standard ? [recapEntry.standard] : [],
235
- agent_persona: recapEntry.persona ?? null,
236
- sub_agent_dispatched: recapEntry.persona
237
- ? /^morph-|general-purpose/.test(recapEntry.persona)
238
- : false,
239
- files_modified: Array.isArray(t.outputs) ? t.outputs.filter(o => typeof o === 'string') : [],
240
- duration_seconds: taskDuration(times, t.id),
241
- };
242
- });
243
-
244
- const allSettled = tasks.length > 0 &&
245
- tasks.every(t => t.status !== 'pending' && t.status !== 'in_progress');
246
-
247
- const standardCounts = {};
248
- for (const entry of entries) {
249
- for (const std of entry.standards_consulted) {
250
- standardCounts[std] = (standardCounts[std] ?? 0) + 1;
251
- }
252
- }
253
- const mostUsed = Object.entries(standardCounts)
254
- .sort((a, b) => b[1] - a[1])
255
- .slice(0, 3)
256
- .map(([std]) => std);
257
-
258
- const trace = {
259
- schema_version: '1.0',
260
- session_id: previous?.session_id ?? sessionId(now),
261
- feature,
262
- phase: 'implement',
263
- started_at: previous?.started_at ?? now.toISOString(),
264
- completed_at: allSettled ? (previous?.completed_at ?? now.toISOString()) : null,
265
- tasks: entries,
266
- phase_summary: {
267
- tasks_completed: entries.filter(e => e.status === 'completed').length,
268
- tasks_failed: entries.filter(e => e.status === 'failed').length,
269
- standards_most_used: mostUsed,
270
- sub_agents_dispatched: entries.filter(e => e.sub_agent_dispatched).length,
271
- },
272
- };
273
-
274
- writeFileSync(join(tracesDir, fileName), JSON.stringify(trace, null, 2), 'utf8');
275
- }
276
-
277
- await runStandalone(run);
@@ -1,50 +0,0 @@
1
- #!/bin/bash
2
- # Scope: universal | Validates spec.md required sections
3
- # MORPH-SPEC Pre-Commit Hook: Spec Validation
4
- # Validates that spec.md files have required sections
5
-
6
- echo "🔍 Validating spec files..."
7
-
8
- # Find modified spec.md files
9
- SPEC_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep 'spec\.md$')
10
-
11
- if [ -z "$SPEC_FILES" ]; then
12
- echo "✓ No spec files modified"
13
- exit 0
14
- fi
15
-
16
- HAS_ERRORS=false
17
-
18
- for spec_file in $SPEC_FILES; do
19
- echo "Checking: $spec_file"
20
-
21
- # Required sections
22
- REQUIRED_SECTIONS=(
23
- "## 📋 Metadata"
24
- "## 🎯 Overview"
25
- "## 🏗️ Technical Design"
26
- "## ✅ Acceptance Criteria"
27
- )
28
-
29
- for section in "${REQUIRED_SECTIONS[@]}"; do
30
- if ! grep -q "$section" "$spec_file"; then
31
- echo " ❌ Missing section: $section"
32
- HAS_ERRORS=true
33
- fi
34
- done
35
-
36
- # Check if has at least one user story or requirement
37
- if ! grep -qi "user story\|requirement\|acceptance criteria" "$spec_file"; then
38
- echo " ⚠️ Warning: No user stories or requirements found"
39
- fi
40
- done
41
-
42
- if [ "$HAS_ERRORS" = true ]; then
43
- echo ""
44
- echo "❌ COMMIT BLOCKED: spec.md files are incomplete"
45
- echo " Add missing sections before committing"
46
- exit 1
47
- fi
48
-
49
- echo "✓ All spec files are valid"
50
- exit 0
@@ -1,111 +0,0 @@
1
- /**
2
- * tasks-validator.js — minimal hook-side schema validation for tasks.json.
3
- *
4
- * Mirrors VALID_STATUS / VALID_EFFORT from src/lib/tasks/tasks-json-parser.js.
5
- * Hooks are installed standalone into target projects (.morph/framework/hooks/),
6
- * so they cannot import from src/ — the enum lives here, duplicated on purpose
7
- * and covered by tests on both sides.
8
- */
9
-
10
- export const VALID_TASK_STATUS = ['pending', 'in_progress', 'done', 'skipped', 'blocked'];
11
- export const VALID_TASK_EFFORT = ['S', 'M', 'L', 'XL'];
12
-
13
- /**
14
- * Validate the parts of a tasks.json document that hooks care about:
15
- * a tasks[] array whose entries carry a valid status (and effort, when set).
16
- * The production failure this guards: a leaf agent writing status "completed"
17
- * (not in the enum) without any validation.
18
- *
19
- * @param {object} doc - parsed tasks.json
20
- * @returns {{ valid: boolean, errors: string[] }}
21
- */
22
- /** Human-readable type name for error messages ('array', not 'object'). */
23
- function typeName(value) {
24
- return Array.isArray(value) ? 'array' : typeof value;
25
- }
26
-
27
- export function validateTasksDoc(doc) {
28
- if (!doc || typeof doc !== 'object' || !Array.isArray(doc.tasks)) {
29
- return { valid: false, errors: ['tasks.json must be an object with a tasks[] array'] };
30
- }
31
-
32
- const errors = [];
33
- for (const task of doc.tasks) {
34
- const id = task?.id ?? '?';
35
- if (!VALID_TASK_STATUS.includes(task?.status)) {
36
- errors.push(`task '${id}': invalid status '${task?.status}' (valid: ${VALID_TASK_STATUS.join(', ')})`);
37
- }
38
- if (task?.effort !== undefined && !VALID_TASK_EFFORT.includes(task.effort)) {
39
- errors.push(`task '${id}': invalid effort '${task.effort}' (valid: ${VALID_TASK_EFFORT.join(', ')})`);
40
- }
41
- // Mutable-field type checks with the exact field/value in the message —
42
- // a malformed notes/outputs used to surface as the misleading generic
43
- // "notes is empty" downstream (MORPH_GHLBrain 2026-07-11).
44
- if (task?.notes !== undefined && typeof task.notes !== 'string') {
45
- errors.push(`task '${id}': 'notes' must be a string (got ${typeName(task.notes)})`);
46
- }
47
- if (task?.outputs !== undefined) {
48
- if (!Array.isArray(task.outputs)) {
49
- errors.push(`task '${id}': 'outputs' must be an array of path strings (got ${typeName(task.outputs)})`);
50
- } else {
51
- task.outputs.forEach((entry, i) => {
52
- if (typeof entry !== 'string') {
53
- errors.push(`task '${id}': outputs[${i}] must be a string path (got ${typeName(entry)})`);
54
- }
55
- });
56
- }
57
- }
58
- }
59
- return { valid: errors.length === 0, errors };
60
- }
61
-
62
- /**
63
- * Planned-vs-real outputs divergence: tasks whose `outputs` changed relative
64
- * to the previous document but whose `notes` is still empty. The divergence
65
- * itself is legitimate (outputs is a mutable progress field) — shipping it
66
- * without an explanatory note is what gets flagged.
67
- *
68
- * @param {object} oldDoc - parsed previous tasks.json
69
- * @param {object} newDoc - parsed proposed tasks.json
70
- * @returns {string[]} human-readable violation messages (empty = OK)
71
- */
72
- /** Compact outputs rendering for messages, capped so huge arrays stay readable. */
73
- function fmtOutputs(outputs) {
74
- const s = JSON.stringify(outputs ?? []);
75
- return s.length > 120 ? s.slice(0, 117) + '...' : s;
76
- }
77
-
78
- export function outputsDivergenceViolations(oldDoc, newDoc) {
79
- const violations = [];
80
- const oldById = new Map((oldDoc?.tasks ?? []).map(t => [t.id, t]));
81
-
82
- for (const task of newDoc?.tasks ?? []) {
83
- const old = oldById.get(task.id);
84
- if (!old) continue;
85
- const outputsChanged =
86
- JSON.stringify(old.outputs ?? []) !== JSON.stringify(task.outputs ?? []);
87
- if (!outputsChanged) continue;
88
-
89
- // A filled string note explains the divergence — done.
90
- if (typeof task.notes === 'string' && task.notes.trim().length > 0) continue;
91
-
92
- // Root-cause specific messages: a malformed notes value is a DIFFERENT
93
- // failure than a missing one — claiming "notes is empty" when the agent
94
- // just wrote an array reads as a false positive and burns a retry cycle.
95
- if (task.notes !== undefined && typeof task.notes !== 'string') {
96
- violations.push(
97
- `task '${task.id}': outputs diverged from the plan and 'notes' is not a string ` +
98
- `(got ${typeName(task.notes)}) — write the explanation as a single string in 'notes'`
99
- );
100
- continue;
101
- }
102
-
103
- violations.push(
104
- `task '${task.id}': outputs diverged from the plan ` +
105
- `(${fmtOutputs(old.outputs)} → ${fmtOutputs(task.outputs)}) but 'notes' is empty — ` +
106
- `record why in 'notes' in the same edit. ` +
107
- `Deletions and annotations belong in 'notes', not in 'outputs' (outputs = produced paths only).`
108
- );
109
- }
110
- return violations;
111
- }
@@ -1,54 +0,0 @@
1
- ---
2
- stacks:
3
- - "*"
4
- ---
5
-
6
- # MORPH-SPEC Task Tracking
7
-
8
- > Progress lives in `tasks.json` — nowhere else. There is no CLI command to mark a task done and
9
- > no session-level checklist to keep in sync: this file IS the record of what happened.
10
-
11
- ## Persisting Progress to `tasks.json`
12
-
13
- To record task progress for the feature, write `.morph/features/{feature}/2-plan/tasks.json`,
14
- touching ONLY the mutable fields:
15
-
16
- - `status` — `pending` → `in_progress` → `done` (também: `skipped`, `blocked`)
17
- - `outputs` — the files the task produced
18
- - `notes` — free-text record (deviations, context, resolved blockers)
19
-
20
- > **O status terminal é `done`.** `completed` **não existe** no enum de
21
- > `.morph/framework/schemas/tasks.schema.json` — é vocabulário de outro sistema, e o hook
22
- > `protect-spec-files` rejeita a gravação.
23
-
24
- The plan contract — `title`, `description`, `dependencies`, `effort`, `doneCriteria`,
25
- `group`, `agent`, `standards` — is frozen after Gate 2. Any attempt to change it is
26
- blocked by the `protect-spec-files` hook. `agent` (persona id, no `morph-` prefix)
27
- and `standards` (paths pinned by morph-plan) are optional — legacy plans without
28
- them remain valid.
29
-
30
- Use the `Write` tool (whole-file rewrite) OR the `Edit` tool — `Edit` is allowed
31
- as long as it touches only `status`, `outputs`, and `notes`. State is then synced
32
- automatically by the `state-sync` PostToolUse hook; you do not call a CLI to mark
33
- output.
34
-
35
- Update each task as you go — `in_progress` when you pick it up, `done` when it is finished —
36
- rather than batching every write to the end of the phase. `morph-spec status`, `dag`, `verify`
37
- and the dashboard all read this file, so a stale `tasks.json` is a blind orchestrator.
38
-
39
- ## Deviation Tracking
40
-
41
- When completing a task differently than originally planned, record the reason in the
42
- task's `notes` field. `notes` is **uma única string** (o schema declara
43
- `"notes": { "type": "string" }`) — para registrar mais de um desvio, separe-os com
44
- `;` ou `\n` dentro da mesma string. Um array é **rejeitado** pelo validador, com a
45
- mensagem `'notes' must be a string`.
46
-
47
- ```json
48
- {
49
- "id": "T3",
50
- "status": "done",
51
- "outputs": ["Features/User/Update/Handler.cs"],
52
- "notes": "deviation: split validation into a separate validator for testability; blocker-resolved: EF migration conflict fixed by regenerating the snapshot"
53
- }
54
- ```