@polymorphism-tech/morph-spec 4.22.0 → 6.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 (211) hide show
  1. package/CLAUDE.md +57 -71
  2. package/README.md +2 -2
  3. package/bin/morph-spec.js +224 -491
  4. package/claude-plugin.json +1 -1
  5. package/docs/CHEATSHEET.md +1 -1
  6. package/docs/QUICKSTART.md +1 -1
  7. package/framework/CLAUDE.md +55 -38
  8. package/framework/MORPH.md +244 -0
  9. package/framework/agents.json +127 -1198
  10. package/framework/commands/morph-apply.md +56 -240
  11. package/framework/commands/morph-archive.md +48 -85
  12. package/framework/commands/morph-preflight.md +7 -7
  13. package/framework/commands/morph-proposal.md +35 -191
  14. package/framework/commands/morph-status.md +19 -50
  15. package/framework/commands/morph-troubleshoot.md +2 -2
  16. package/framework/evals/rubrics/README.md +44 -0
  17. package/framework/evals/rubrics/architecture.md +46 -0
  18. package/framework/evals/rubrics/code-quality.md +50 -0
  19. package/framework/evals/rubrics/contracts.md +47 -0
  20. package/framework/evals/rubrics/test-coverage.md +50 -0
  21. package/framework/evals/scenarios/dotnet-crud.yaml +42 -0
  22. package/framework/evals/scenarios/nextjs-auth.yaml +38 -0
  23. package/framework/evals/suites/standard.yaml +5 -0
  24. package/framework/evals/trace-schema.md +67 -0
  25. package/framework/hooks/claude-code/core/README.md +3 -0
  26. package/framework/hooks/claude-code/core/denylist.json +14 -0
  27. package/framework/hooks/claude-code/{pre-tool-use → core}/enforce-phase-writes.js +5 -5
  28. package/framework/hooks/claude-code/core/post-edit-typecheck.js +69 -0
  29. package/framework/hooks/claude-code/core/pre-bash-denylist.js +29 -0
  30. package/framework/hooks/claude-code/{pre-tool-use → core}/protect-spec-files.js +4 -6
  31. package/framework/hooks/claude-code/{stop → core}/validate-completion.js +13 -82
  32. package/framework/hooks/shared/phase-utils.js +20 -31
  33. package/framework/hooks/shared/state-reader.js +4 -5
  34. package/framework/{skills/level-0-meta/mcp-registry.json → mcp/registry.json} +254 -243
  35. package/framework/phases.json +41 -115
  36. package/framework/rubrics/eval-rubric.json +66 -0
  37. package/framework/skills/README.md +39 -64
  38. package/framework/skills/core/README.md +3 -0
  39. package/framework/skills/core/morph-brainstorming/SKILL.md +102 -0
  40. package/framework/skills/core/morph-eval/SKILL.md +57 -0
  41. package/framework/skills/core/morph-implement/SKILL.md +83 -0
  42. package/framework/skills/core/morph-plan/SKILL.md +64 -0
  43. package/framework/skills/core/morph-review/SKILL.md +62 -0
  44. package/framework/skills/core/morph-stack-scan/SKILL.md +109 -0
  45. package/framework/skills/core/morph-standards/SKILL.md +49 -0
  46. package/framework/templates/CLAUDE.md.core.guide.md +29 -0
  47. package/framework/templates/CLAUDE.md.core.template +22 -0
  48. package/framework/templates/REGISTRY.json +4 -4
  49. package/framework/workflows/configs/fast-track.json +5 -10
  50. package/framework/workflows/configs/full.json +4 -8
  51. package/framework/workflows/configs/quick.json +32 -0
  52. package/framework/workflows/configs/standard.json +57 -64
  53. package/package.json +7 -6
  54. package/src/commands/mcp/mcp-setup.js +58 -31
  55. package/src/commands/project/doctor.js +69 -70
  56. package/src/commands/project/init.js +26 -5
  57. package/src/commands/project/status.js +18 -19
  58. package/src/commands/state/approve.js +1 -1
  59. package/src/commands/validation/validate-feature.js +18 -112
  60. package/src/core/paths/output-schema.js +133 -152
  61. package/src/core/state/phase-state-machine.js +10 -24
  62. package/src/core/state/state-manager.js +17 -17
  63. package/src/core/workflows/workflow-detector.js +214 -31
  64. package/src/core/workflows/workflow-router.js +204 -0
  65. package/src/dashboard/app/pages/AgentMap.jsx +32 -33
  66. package/src/dashboard/app/pages/LiveState.jsx +1 -1
  67. package/src/dashboard/app/pages/PhasePipeline.jsx +2 -4
  68. package/src/dashboard/app/pages/SkillsLibrary.jsx +2 -3
  69. package/src/dashboard/app/pages/Workflows.jsx +2 -2
  70. package/src/dashboard/scanner/agents-scanner.js +30 -35
  71. package/src/lib/agents/metrics-aggregator.js +1 -1
  72. package/src/lib/agents/output-collector.js +7 -9
  73. package/src/lib/context/context-builder.js +289 -0
  74. package/src/lib/detectors/claude-config-detector.js +19 -3
  75. package/src/lib/generators/context-generator.js +20 -42
  76. package/src/lib/generators/index.js +1 -0
  77. package/src/lib/generators/mandate-generator.js +512 -0
  78. package/src/lib/generators/metadata-extractor.js +1 -1
  79. package/src/lib/generators/recap-generator.js +3 -4
  80. package/src/lib/installers/mcp-installer.js +300 -92
  81. package/src/lib/phase-chain/phase-validator.js +36 -40
  82. package/src/lib/stack/stack-profile.js +74 -91
  83. package/src/lib/tasks/task-parser.js +1 -1
  84. package/src/lib/tasks/wave-resolver.js +353 -0
  85. package/src/lib/validators/checkpoint-persistence.js +2 -2
  86. package/src/lib/validators/content/cross-phase-validator.js +281 -0
  87. package/src/lib/validators/content/index.js +1 -0
  88. package/src/lib/validators/shared/emit-validator-dispatch.js +6 -56
  89. package/src/scripts/global-install.js +2 -27
  90. package/src/utils/agents-installer.js +224 -447
  91. package/src/utils/claude-settings-manager.js +57 -15
  92. package/src/utils/file-copier.js +1 -0
  93. package/src/utils/hooks-installer.js +15 -117
  94. package/src/utils/skills-installer.js +12 -18
  95. package/bin/detect-agents.js +0 -226
  96. package/bin/task-manager.js +0 -1036
  97. package/framework/agents/README.md +0 -13
  98. package/framework/agents/ai-agents/ai-system-architect.md +0 -194
  99. package/framework/agents/ai-agents/vector-search-expert.md +0 -247
  100. package/framework/agents/architecture/architecture-expert.md +0 -98
  101. package/framework/agents/architecture/morph-spec-architect.md +0 -66
  102. package/framework/agents/architecture/packages-validator.md +0 -89
  103. package/framework/agents/architecture/security-expert.md +0 -79
  104. package/framework/agents/architecture/seo-growth-hacker.md +0 -168
  105. package/framework/agents/architecture/standards-architect.md +0 -153
  106. package/framework/agents/backend/dotnet-senior.md +0 -163
  107. package/framework/agents/backend/ef-modeler.md +0 -119
  108. package/framework/agents/backend/hangfire-orchestrator.md +0 -133
  109. package/framework/agents/backend/ms-agent-expert.md +0 -55
  110. package/framework/agents/backend/neon-expert.md +0 -379
  111. package/framework/agents/frontend/css-specialist.md +0 -89
  112. package/framework/agents/frontend/design-system-validator.md +0 -100
  113. package/framework/agents/frontend/gsap-animator.md +0 -131
  114. package/framework/agents/frontend/nextjs-component-validator.md +0 -97
  115. package/framework/agents/frontend/nextjs-expert.md +0 -201
  116. package/framework/agents/frontend/ui-designer.md +0 -259
  117. package/framework/agents/infrastructure/infra-architect.md +0 -215
  118. package/framework/agents/integrations/asaas-financial.md +0 -138
  119. package/framework/agents/integrations/neon-auth.md +0 -151
  120. package/framework/agents/integrations/resend-email.md +0 -127
  121. package/framework/hooks/claude-code/post-tool-use/agent-complete.js +0 -122
  122. package/framework/hooks/claude-code/post-tool-use/dispatch.js +0 -363
  123. package/framework/hooks/claude-code/post-tool-use/handle-tool-failure.js +0 -78
  124. package/framework/hooks/claude-code/post-tool-use/validator-feedback.js +0 -194
  125. package/framework/hooks/claude-code/pre-compact/save-morph-context.js +0 -199
  126. package/framework/hooks/claude-code/pre-tool-use/task-tracking-guard.js +0 -55
  127. package/framework/hooks/claude-code/session-start/inject-morph-context.js +0 -323
  128. package/framework/hooks/claude-code/session-start/post-compact-restore.js +0 -41
  129. package/framework/hooks/claude-code/session-start/resume-context.js +0 -77
  130. package/framework/hooks/claude-code/teammate-idle/teammate-idle.js +0 -90
  131. package/framework/hooks/claude-code/user-prompt/enrich-prompt.js +0 -114
  132. package/framework/hooks/shared/compact-restore.js +0 -123
  133. package/framework/hooks/shared/dispatch-helpers.js +0 -147
  134. package/framework/hooks/shared/skill-reminder-helpers.js +0 -79
  135. package/framework/hooks/shared/stale-task-reset.js +0 -57
  136. package/framework/hooks/shared/worktree-helpers.js +0 -75
  137. package/framework/skills/level-0-meta/README.md +0 -7
  138. package/framework/skills/level-0-meta/morph-agent-teams/SKILL.md +0 -472
  139. package/framework/skills/level-0-meta/morph-brainstorming/SKILL.md +0 -174
  140. package/framework/skills/level-0-meta/morph-brainstorming/references/proposal-example.md +0 -138
  141. package/framework/skills/level-0-meta/morph-checklist/SKILL.md +0 -145
  142. package/framework/skills/level-0-meta/morph-code-review/SKILL.md +0 -250
  143. package/framework/skills/level-0-meta/morph-code-review/references/review-example.md +0 -164
  144. package/framework/skills/level-0-meta/morph-code-review/references/review-guidelines.md +0 -100
  145. package/framework/skills/level-0-meta/morph-code-review/scripts/scan-csharp.mjs +0 -151
  146. package/framework/skills/level-0-meta/morph-code-review-nextjs/SKILL.md +0 -165
  147. package/framework/skills/level-0-meta/morph-code-review-nextjs/references/review-example-nextjs.md +0 -254
  148. package/framework/skills/level-0-meta/morph-code-review-nextjs/scripts/scan-nextjs.mjs +0 -189
  149. package/framework/skills/level-0-meta/morph-frontend-review/SKILL.md +0 -344
  150. package/framework/skills/level-0-meta/morph-frontend-review/scripts/scan-accessibility.mjs +0 -376
  151. package/framework/skills/level-0-meta/morph-gemini-image/SKILL.md +0 -290
  152. package/framework/skills/level-0-meta/morph-init/SKILL.md +0 -537
  153. package/framework/skills/level-0-meta/morph-parallel-dispatch/SKILL.md +0 -225
  154. package/framework/skills/level-0-meta/morph-post-implementation/SKILL.md +0 -401
  155. package/framework/skills/level-0-meta/morph-post-implementation/scripts/detect-dev-server.mjs +0 -153
  156. package/framework/skills/level-0-meta/morph-post-implementation/scripts/detect-stack.mjs +0 -234
  157. package/framework/skills/level-0-meta/morph-simulation-checklist/SKILL.md +0 -106
  158. package/framework/skills/level-0-meta/morph-tool-usage-guide/SKILL.md +0 -243
  159. package/framework/skills/level-0-meta/morph-tool-usage-guide/references/tools-per-phase.md +0 -169
  160. package/framework/skills/level-0-meta/morph-verification-before-completion/SKILL.md +0 -148
  161. package/framework/skills/level-0-meta/morph-verification-before-completion/scripts/check-phase-outputs.mjs +0 -110
  162. package/framework/skills/level-1-workflows/README.md +0 -7
  163. package/framework/skills/level-1-workflows/morph-phase-codebase-analysis/SKILL.md +0 -269
  164. package/framework/skills/level-1-workflows/morph-phase-design/SKILL.md +0 -453
  165. package/framework/skills/level-1-workflows/morph-phase-design/references/architecture-analysis-guide.md +0 -44
  166. package/framework/skills/level-1-workflows/morph-phase-design/references/spec-authoring-guide.md +0 -55
  167. package/framework/skills/level-1-workflows/morph-phase-design/references/spec-example.md +0 -253
  168. package/framework/skills/level-1-workflows/morph-phase-implement/.morph/logs/activity.json +0 -38
  169. package/framework/skills/level-1-workflows/morph-phase-implement/SKILL.md +0 -634
  170. package/framework/skills/level-1-workflows/morph-phase-implement/prompts/code-quality-reviewer-prompt.md +0 -50
  171. package/framework/skills/level-1-workflows/morph-phase-implement/prompts/implementer-prompt.md +0 -54
  172. package/framework/skills/level-1-workflows/morph-phase-implement/prompts/spec-reviewer-prompt.md +0 -47
  173. package/framework/skills/level-1-workflows/morph-phase-implement/references/recap-example.md +0 -132
  174. package/framework/skills/level-1-workflows/morph-phase-implement/references/vsa-implementation-guide.md +0 -92
  175. package/framework/skills/level-1-workflows/morph-phase-plan/SKILL.md +0 -257
  176. package/framework/skills/level-1-workflows/morph-phase-tasks/.morph/logs/activity.json +0 -14
  177. package/framework/skills/level-1-workflows/morph-phase-tasks/SKILL.md +0 -291
  178. package/framework/skills/level-1-workflows/morph-phase-tasks/references/task-planning-patterns.md +0 -143
  179. package/framework/skills/level-1-workflows/morph-phase-tasks/references/tasks-example.md +0 -404
  180. package/framework/skills/level-1-workflows/morph-phase-tasks/scripts/validate-tasks.mjs +0 -112
  181. package/framework/skills/level-1-workflows/morph-phase-uiux/SKILL.md +0 -995
  182. package/framework/skills/level-1-workflows/morph-scope-escalation/SKILL.md +0 -147
  183. package/framework/squad-templates/backend-only.json +0 -47
  184. package/framework/squad-templates/frontend-only.json +0 -56
  185. package/framework/squad-templates/full-stack.json +0 -74
  186. package/src/commands/agents/agent-metrics.js +0 -174
  187. package/src/commands/agents/audit-dispatch.js +0 -82
  188. package/src/commands/agents/detect-agents.js +0 -275
  189. package/src/commands/agents/dispatch-agents.js +0 -461
  190. package/src/commands/agents/index.js +0 -13
  191. package/src/commands/agents/manage-agents.js +0 -352
  192. package/src/commands/agents/project-metrics.js +0 -74
  193. package/src/commands/phase/phase-reset.js +0 -74
  194. package/src/commands/scope/escalate.js +0 -217
  195. package/src/commands/state/advance-phase.js +0 -600
  196. package/src/commands/state/phase-runner.js +0 -215
  197. package/src/commands/state/state.js +0 -444
  198. package/src/commands/task/expand.js +0 -100
  199. package/src/commands/tasks/index.js +0 -4
  200. package/src/commands/tasks/task.js +0 -105
  201. package/src/commands/templates/index.js +0 -5
  202. package/src/commands/templates/template-render.js +0 -93
  203. package/src/commands/trust/trust.js +0 -118
  204. package/src/lib/agents/agents-manager.js +0 -349
  205. package/src/lib/build/build-runner.js +0 -99
  206. package/src/lib/checkpoints/checkpoint-hooks.js +0 -111
  207. package/src/lib/contracts/sprint-contract-generator.js +0 -137
  208. package/src/lib/scope/impact-analyzer.js +0 -106
  209. package/src/lib/trust/trust-manager.js +0 -157
  210. package/src/utils/tier-helpers.js +0 -24
  211. /package/framework/hooks/claude-code/{post-tool-use → core}/loop-detect.js +0 -0
@@ -1,199 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * PreCompact Hook: Inject MORPH Context into Compact Summary
5
- *
6
- * Event: PreCompact
7
- *
8
- * Injects a rich morph-spec state snapshot as additionalContext so that
9
- * Claude's compacted summary includes feature state, task progress, outputs,
10
- * and next steps. Ensures the session resumes with full morph awareness
11
- * without having to re-read all the spec files.
12
- *
13
- * Also saves snapshot to .morph/memory/ for SessionStart to reference.
14
- *
15
- * Fail-open: exits 0 on any error.
16
- */
17
-
18
- import { writeFileSync, mkdirSync, existsSync, readFileSync } from 'fs';
19
- import { join } from 'path';
20
- import {
21
- stateExists, loadState, getActiveFeature,
22
- derivePhaseForFeature, getPendingGates, getOutputs,
23
- } from '../../shared/state-reader.js';
24
- import { injectContext, pass } from '../../shared/hook-response.js';
25
- import { logHookActivity } from '../../shared/activity-logger.js';
26
- import { buildRichContext, DECISIONS_MAX_CHARS, MAX_PENDING_TASKS } from '../../shared/compact-restore.js';
27
-
28
- const PHASE_POSITIONS = {
29
- proposal: 1,
30
- uiux: 2, design: 2,
31
- plan: 3,
32
- tasks: 4,
33
- implement: 5, sync: 5,
34
- };
35
- const PIPELINE_TOTAL = 5;
36
-
37
- const NEXT_STEP_HINT = {
38
- proposal: (n) => `Continue spec pipeline: /morph-proposal ${n}`,
39
- design: (n) => `Approve design gate: morph-spec approve ${n} design`,
40
- uiux: (n) => `Approve UI gate: morph-spec approve ${n} uiux`,
41
- tasks: (n) => `Approve tasks gate: morph-spec approve ${n} tasks`,
42
- implement: (n) => `Continue implementation: morph-spec task start ${n} <next-task-id>`,
43
- };
44
-
45
- try {
46
- if (!stateExists()) pass();
47
-
48
- const state = loadState();
49
- if (!state) pass();
50
-
51
- const active = getActiveFeature();
52
- const cwd = process.cwd();
53
-
54
- // ── Disk snapshot (for SessionStart restore) ──────────────────────────────
55
- const snapshot = {
56
- timestamp: new Date().toISOString(),
57
- event: 'pre-compact',
58
- activeFeature: active ? active.name : null,
59
- features: {},
60
- };
61
-
62
- for (const [name, feature] of Object.entries(state.features || {})) {
63
- snapshot.features[name] = {
64
- phase: feature.phase || derivePhaseForFeature(name),
65
- status: feature.status,
66
- workflow: feature.workflow,
67
- tasks: feature.tasks,
68
- approvalGates: Object.fromEntries(
69
- Object.entries(feature.approvalGates || {}).map(([gate, data]) => [gate, data.approved])
70
- ),
71
- };
72
- }
73
-
74
- // ── Enrich snapshot with richContext (decisions + taskList) ───────────────
75
- if (active) {
76
- try {
77
- const { name: activeName, feature: activeFeature } = active;
78
- const activePhase = derivePhaseForFeature(activeName);
79
- let decisionsContent = '';
80
- const decisionsPath = join(cwd, `.morph/features/${activeName}/2-design/decisions.md`);
81
- if (existsSync(decisionsPath)) {
82
- try { decisionsContent = readFileSync(decisionsPath, 'utf-8'); } catch { /* ignore */ }
83
- }
84
- let specContent = '';
85
- const specPath2 = join(cwd, `.morph/features/${activeName}/2-design/spec.md`);
86
- if (existsSync(specPath2)) {
87
- try { specContent = readFileSync(specPath2, 'utf-8'); } catch { /* ignore */ }
88
- }
89
- snapshot.richContext = buildRichContext(activeFeature, activePhase, decisionsContent, specContent);
90
- } catch { /* fail-open: richContext is optional */ }
91
- }
92
- // ─────────────────────────────────────────────────────────────────────────
93
-
94
- const memoryDir = join(cwd, '.morph', 'memory');
95
- if (!existsSync(memoryDir)) mkdirSync(memoryDir, { recursive: true });
96
- const ts = new Date().toISOString().replace(/[:.]/g, '-');
97
- writeFileSync(join(memoryDir, `pre-compact-${ts}.json`), JSON.stringify(snapshot, null, 2), 'utf-8');
98
-
99
- // ── Nothing more to inject if no active feature ───────────────────────────
100
- if (!active) pass();
101
-
102
- const { name, feature } = active;
103
- const phase = derivePhaseForFeature(name);
104
- const pos = PHASE_POSITIONS[phase];
105
- const phaseLabel = pos ? `${phase} (${pos}/${PIPELINE_TOTAL})` : phase;
106
-
107
- const lines = [
108
- '=== MORPH-SPEC COMPACT CONTEXT ===',
109
- `Preserve this section verbatim in the compact summary.`,
110
- '',
111
- `Feature : ${name}`,
112
- `Phase : ${phaseLabel}`,
113
- `Status : ${feature.status} | Workflow: ${feature.workflow || 'auto'}`,
114
- ];
115
-
116
- // Task progress
117
- const tasks = feature.tasks || {};
118
- const done = tasks.completed || 0;
119
- const total = tasks.total || 0;
120
- if (total > 0) {
121
- const pct = Math.round(done / total * 100);
122
- lines.push(`Tasks : ${done}/${total} completed (${pct}%)`);
123
- }
124
-
125
- // In-progress + next pending tasks from taskList
126
- const taskList = Array.isArray(feature.taskList) ? feature.taskList : [];
127
- const inProg = taskList.filter(t => t.status === 'in_progress');
128
- const pending = taskList.filter(t => t.status === 'pending').slice(0, MAX_PENDING_TASKS);
129
-
130
- if (inProg.length > 0) {
131
- lines.push('');
132
- lines.push('In progress:');
133
- for (const t of inProg) lines.push(` [${t.id}] ${t.title}`);
134
- }
135
- if (pending.length > 0) {
136
- lines.push('');
137
- lines.push('Next pending tasks:');
138
- for (const t of pending) lines.push(` [${t.id}] ${t.title}`);
139
- if (taskList.filter(t => t.status === 'pending').length > MAX_PENDING_TASKS) {
140
- lines.push(` ... (see tasks.md for full list)`);
141
- }
142
- }
143
-
144
- // Created outputs
145
- const outputs = getOutputs(name) || {};
146
- const created = Object.entries(outputs).filter(([, o]) => o.created);
147
- const missing = Object.entries(outputs).filter(([, o]) => !o.created);
148
- if (created.length > 0) {
149
- lines.push('');
150
- lines.push('Created outputs:');
151
- for (const [type, o] of created) lines.push(` ✓ ${type}: ${o.path}`);
152
- }
153
- if (missing.length > 0) {
154
- lines.push('Pending outputs:');
155
- for (const [type, o] of missing) lines.push(` ✗ ${type}: ${o.path}`);
156
- }
157
-
158
- // Active agents
159
- const agents = feature.activeAgents || [];
160
- if (agents.length > 0) {
161
- lines.push('');
162
- lines.push(`Active agents: ${agents.join(', ')}`);
163
- }
164
-
165
- // Pending approval gates
166
- const pendingGates = getPendingGates(name);
167
- if (pendingGates.length > 0) {
168
- lines.push(`Pending approvals: ${pendingGates.join(', ')}`);
169
- }
170
-
171
- // Key decisions (truncated to stay within context budget)
172
- const decisionsPath = join(cwd, `.morph/features/${name}/2-design/decisions.md`);
173
- if (existsSync(decisionsPath)) {
174
- try {
175
- const raw = readFileSync(decisionsPath, 'utf-8');
176
- const content = raw.length > DECISIONS_MAX_CHARS
177
- ? raw.slice(0, DECISIONS_MAX_CHARS) + '\n[... truncated — full file at decisions.md]'
178
- : raw;
179
- lines.push('');
180
- lines.push('Key decisions (decisions.md):');
181
- lines.push(content);
182
- } catch { /* non-blocking */ }
183
- }
184
-
185
- // Next step
186
- const hintFn = NEXT_STEP_HINT[phase];
187
- if (hintFn) {
188
- lines.push('');
189
- lines.push(`Next step: ${hintFn(name)}`);
190
- }
191
-
192
- lines.push('');
193
- lines.push('=== END MORPH CONTEXT ===');
194
-
195
- logHookActivity('save-morph-context', 'PreCompact', 'context_saved');
196
- injectContext(lines.join('\n'));
197
- } catch {
198
- process.exit(0);
199
- }
@@ -1,55 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * PreToolUse Hook: Advisory Task Tracking Guard
5
- *
6
- * Event: PreToolUse | Matcher: Write|Edit
7
- *
8
- * During implement phase, injects a warning if no task is currently in_progress.
9
- * Advisory only (type: approve with context) — never blocks.
10
- *
11
- * Fail-open: exits 0 on any error.
12
- */
13
-
14
- import { readStdin } from '../../shared/stdin-reader.js';
15
- import { stateExists, loadState, getActiveFeature, derivePhaseForFeature } from '../../shared/state-reader.js';
16
- import { approve, pass } from '../../shared/hook-response.js';
17
- import { getFilePath } from '../../shared/payload-utils.js';
18
-
19
- try {
20
- if (!stateExists()) pass();
21
-
22
- const payload = await readStdin();
23
- if (!payload) pass();
24
-
25
- const filePath = getFilePath(payload);
26
- if (!filePath) pass();
27
-
28
- // Skip files inside .morph/ — internal framework writes don't need task tracking
29
- if (filePath.includes('.morph/') || filePath.includes('.morph\\')) pass();
30
-
31
- // Only check during implement phase
32
- const active = getActiveFeature();
33
- if (!active) pass();
34
-
35
- const { name, feature } = active;
36
- const phase = feature.phase || derivePhaseForFeature(name);
37
- if (phase !== 'implement') pass();
38
-
39
- // Check if any task is in_progress (read directly from taskList)
40
- if (feature.taskList?.some(t => t.status === 'in_progress')) pass();
41
- // Fallback: legacy counter check
42
- if (feature.tasks?.inProgress > 0) pass();
43
-
44
- // No task in_progress — inject advisory warning
45
- approve(
46
- `[morph-spec] WARNING: No active task tracking detected during implement phase.\n` +
47
- `Before writing code, start a task:\n` +
48
- ` morph-spec task start ${name} <id> # mark it in progress\n` +
49
- `Task tracking ensures validators run, checkpoints fire, and progress is recorded.\n` +
50
- `If a task is more complex than expected, use: morph-spec task expand ${name} <id> --into "T00Xa: Sub" "T00Xb: Sub"`
51
- );
52
- } catch {
53
- // Fail-open
54
- process.exit(0);
55
- }
@@ -1,323 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * SessionStart Hook: Inject MORPH-SPEC Context
5
- *
6
- * Event: SessionStart | Matcher: startup|resume|compact
7
- *
8
- * Reads state.json and injects a summary as additionalContext so Claude
9
- * knows the current morph-spec state at session start.
10
- *
11
- * Fail-open: exits 0 on any error.
12
- */
13
-
14
- import { loadState, getActiveFeature, getPendingGates, getMissingOutputs, derivePhaseForFeature, getTaskStats } from '../../shared/state-reader.js';
15
- import { loadSessionSnapshot, formatPreviousSession } from '../../shared/session-snapshot.js';
16
- import { stateExists } from '../../shared/state-reader.js';
17
- import { injectContext, pass } from '../../shared/hook-response.js';
18
- import { resetActivity, logHookActivity, archiveSession, logEvent } from '../../shared/activity-logger.js';
19
- import { readFileSync, existsSync, writeFileSync, renameSync, unlinkSync, statSync } from 'fs';
20
- import { join } from 'path';
21
- import { resetStaleTasks } from '../../shared/stale-task-reset.js';
22
- import { execSync as _execSync } from 'child_process';
23
- import { parseWorktreeResult, buildWorktreeContextLine, buildWorktreeResumeContext } from '../../shared/worktree-helpers.js';
24
-
25
- const DEFAULT_SPEC_MAX_CHARS = 6000;
26
-
27
- function getSpecMaxChars() {
28
- try {
29
- const configPath = join(process.cwd(), '.morph/config/config.json');
30
- if (!existsSync(configPath)) return DEFAULT_SPEC_MAX_CHARS;
31
- const cfg = JSON.parse(readFileSync(configPath, 'utf-8'));
32
- return cfg?.hooks?.specInjectionLimit ?? DEFAULT_SPEC_MAX_CHARS;
33
- } catch {
34
- return DEFAULT_SPEC_MAX_CHARS;
35
- }
36
- }
37
-
38
- function getProjectConfig() {
39
- try {
40
- const configPath = join(process.cwd(), '.morph/config/config.json');
41
- if (!existsSync(configPath)) return null;
42
- return JSON.parse(readFileSync(configPath, 'utf-8'));
43
- } catch {
44
- return null;
45
- }
46
- }
47
-
48
- const SPEC_MAX_CHARS = getSpecMaxChars();
49
-
50
- /**
51
- * Atomic state save for hook context.
52
- * Same approach as state-manager.js saveState — write to tmp, then rename.
53
- * Fail-open: any error is silently ignored.
54
- * @param {Object} state - Full state object
55
- * @param {string} cwd - Project root path
56
- */
57
- function saveStateSync(state, cwd) {
58
- const statePath = join(cwd, '.morph/state.json');
59
- const tmpPath = `${statePath}.tmp.hook.${process.pid}`;
60
- try {
61
- state.metadata = state.metadata || {};
62
- state.metadata.lastUpdated = new Date().toISOString();
63
- writeFileSync(tmpPath, JSON.stringify(state, null, 2), 'utf8');
64
- renameSync(tmpPath, statePath);
65
- } catch {
66
- try { unlinkSync(tmpPath); } catch { /* ignore cleanup errors */ }
67
- }
68
- }
69
-
70
- try {
71
- if (!stateExists()) pass();
72
-
73
- const state = loadState();
74
- if (!state?.features || Object.keys(state.features).length === 0) pass();
75
-
76
- const active = getActiveFeature();
77
-
78
- // Archive previous session log before resetting
79
- const activeFeatureName = active?.name || '';
80
- const activePhase = active ? derivePhaseForFeature(active.name) : '';
81
- archiveSession();
82
-
83
- // Reset activity log for new session
84
- resetActivity(new Date().toISOString(), activeFeatureName, activePhase);
85
- logEvent('session_start', { feature: activeFeatureName, phase: activePhase });
86
- logHookActivity('inject-morph-context', 'SessionStart', 'ok');
87
-
88
- // ── Stale task cleanup ──────────────────────────────────────────────────────
89
- // Reset any in_progress tasks older than 1 hour — they are orphans from an
90
- // interrupted session and are no longer being actively implemented.
91
- const staleResetLog = resetStaleTasks(state);
92
- if (staleResetLog.length > 0) {
93
- saveStateSync(state, process.cwd());
94
- }
95
- // ────────────────────────────────────────────────────────────────────────────
96
-
97
- const lines = ['MORPH-SPEC Status:'];
98
-
99
- // ── Context freshness check ─────────────────────────────────────────────
100
- // If .morph/context/README.md is missing or older than 24h, suggest refresh.
101
- try {
102
- const contextReadmePath = join(process.cwd(), '.morph/context/README.md');
103
- let contextStale = false;
104
- if (!existsSync(contextReadmePath)) {
105
- contextStale = true;
106
- } else {
107
- const stat = statSync(contextReadmePath);
108
- const ageMs = Date.now() - stat.mtimeMs;
109
- const TWENTY_FOUR_HOURS = 24 * 60 * 60 * 1000;
110
- contextStale = ageMs > TWENTY_FOUR_HOURS;
111
- }
112
- if (contextStale) {
113
- lines.push('');
114
- lines.push('⚠ Architecture context is stale or missing. Run: morph-spec context refresh');
115
- }
116
- } catch {
117
- // Fail-open — context freshness check must never block the session
118
- }
119
- // ────────────────────────────────────────────────────────────────────────
120
-
121
- if (staleResetLog.length > 0) {
122
- lines.push(`⚠️ ${staleResetLog.length} task(s) auto-reset in_progress → pending (orphaned from previous session): ${staleResetLog.join(', ')}`);
123
- }
124
-
125
- if (active) {
126
- const { name, feature } = active;
127
- const phase = derivePhaseForFeature(name);
128
- lines.push(`- Active feature: ${name} (phase: ${phase}, workflow: ${feature.workflow || 'auto'})`);
129
- lines.push(`- Status: ${feature.status}`);
130
-
131
- // Task progress (read directly from taskList)
132
- const stats = getTaskStats(feature);
133
- if (stats.total > 0) {
134
- lines.push(`- Tasks: ${stats.completed}/${stats.total} completed`);
135
- }
136
-
137
- // Pending approvals
138
- const pending = getPendingGates(name);
139
- if (pending.length > 0) {
140
- lines.push(`- Pending approvals: ${pending.join(', ')}`);
141
- }
142
-
143
- // Next required output
144
- const missing = getMissingOutputs(name);
145
- if (missing.length > 0) {
146
- const next = missing[0];
147
- lines.push(`- Next required output: ${next.type} → ${next.path}`);
148
- }
149
-
150
- // Checkpoints
151
- if (feature.checkpoints?.length > 0) {
152
- const lastCp = feature.checkpoints[feature.checkpoints.length - 1];
153
- lines.push(`- Last checkpoint: #${lastCp.checkpointNum} (${lastCp.passed ? 'passed' : 'failed'})`);
154
- }
155
-
156
- // Trust level visibility — warn when low trust may block approval gates
157
- const APPROVAL_GATE_PHASES = new Set(['proposal', 'design', 'tasks']);
158
- if (APPROVAL_GATE_PHASES.has(phase)) {
159
- const config = getProjectConfig();
160
- const trustLevel = config?.trust?.level || config?.trustLevel || 'low';
161
- const workflow = feature.workflow || config?.workflow;
162
- if (trustLevel !== 'high' && trustLevel !== 'maximum') {
163
- lines.push('');
164
- lines.push(`⚠ Trust level: ${trustLevel} — approval gates require manual approval`);
165
- lines.push(` To auto-approve: morph-spec trust set ${name} high`);
166
- }
167
- if (!workflow && config?.workflowDetection?.confidence === 0) {
168
- lines.push('');
169
- lines.push('ℹ Workflow not detected (confidence: 0). Set manually in .morph/config/config.json → workflow');
170
- }
171
- }
172
-
173
- // Active feature spec (truncated for context budget)
174
- const specPath = join(process.cwd(), `.morph/features/${name}/2-design/spec.md`);
175
- if (existsSync(specPath)) {
176
- try {
177
- const specContent = readFileSync(specPath, 'utf-8');
178
- const truncated = specContent.length > SPEC_MAX_CHARS
179
- ? specContent.slice(0, SPEC_MAX_CHARS) + `\n\n[... spec truncated — full file at .morph/features/${name}/2-design/spec.md]`
180
- : specContent;
181
- lines.push('');
182
- lines.push(`--- Active feature spec (${name}/2-design/spec.md) ---`);
183
- lines.push(truncated);
184
- lines.push('--- End spec ---');
185
- } catch {
186
- // Non-blocking: skip spec injection on read error
187
- }
188
- }
189
-
190
- // ── Worktree context injection ─────────────────────────────────────────────
191
- // For the active feature, detect existing worktree from state metadata and
192
- // inject MANDATORY instructions to work inside it.
193
- // The worktree is created by `morph-proposal` Step 0 or `morph-apply` Step 0.
194
- // This hook only injects the context — it does NOT create worktrees.
195
- // Fail-open: any error is silently ignored.
196
- try {
197
- const worktreeMeta = feature.worktree;
198
- if (worktreeMeta?.path) {
199
- const contextLine = buildWorktreeResumeContext(worktreeMeta, name);
200
- if (contextLine) {
201
- lines.push('');
202
- lines.push(contextLine);
203
- }
204
- }
205
- } catch {
206
- // Fail-open — worktree context must never block the session
207
- }
208
- // ────────────────────────────────────────────────────────────────────────────
209
-
210
- // ── Previous session context ──────────────────────────────────────────────
211
- // Inject summary from previous session for "getting up to speed"
212
- try {
213
- const snapshot = loadSessionSnapshot(process.cwd());
214
- const previousSessionText = formatPreviousSession(snapshot);
215
- if (previousSessionText) {
216
- lines.push('');
217
- lines.push(previousSessionText);
218
- }
219
- } catch {
220
- // Fail-open — previous session context must never block
221
- }
222
- // ────────────────────────────────────────────────────────────────────────
223
- } else {
224
- // Show summary of all features
225
- const featureNames = Object.keys(state.features);
226
- lines.push(`- Features: ${featureNames.length} (${featureNames.join(', ')})`);
227
-
228
- for (const [fname, feat] of Object.entries(state.features)) {
229
- const fPhase = derivePhaseForFeature(fname);
230
- lines.push(` - ${fname}: phase=${fPhase}, status=${feat.status}`);
231
- }
232
- }
233
-
234
- // ── MCP status injection ────────────────────────────────────────────────────
235
- // Cross-reference configured MCPs with phase recommendations from mcp-registry.json.
236
- // Fail-open: all inside try/catch, no crash on missing files.
237
- try {
238
- const currentPhase = active ? derivePhaseForFeature(active.name) : '';
239
- if (currentPhase) {
240
- // Read configured MCPs from settings
241
- const configuredMcps = new Set();
242
- const settingsFiles = [
243
- join(process.cwd(), '.claude', 'settings.local.json'),
244
- join(process.cwd(), '.claude', 'settings.json'),
245
- ];
246
- for (const sf of settingsFiles) {
247
- if (existsSync(sf)) {
248
- try {
249
- const s = JSON.parse(readFileSync(sf, 'utf8'));
250
- for (const name of Object.keys(s.mcpServers || {})) {
251
- configuredMcps.add(name.toLowerCase());
252
- }
253
- } catch { /* ignore */ }
254
- }
255
- }
256
-
257
- // Read recommended MCPs from phases.json or mcp-registry.json
258
- let recommended = [];
259
- const registryPaths = [
260
- join(process.cwd(), 'framework', 'skills', 'level-0-meta', 'mcp-registry.json'),
261
- join(process.cwd(), '.morph', 'framework', 'skills', 'level-0-meta', 'mcp-registry.json'),
262
- ];
263
- for (const rp of registryPaths) {
264
- if (existsSync(rp)) {
265
- try {
266
- const registry = JSON.parse(readFileSync(rp, 'utf8'));
267
- recommended = registry.phaseMatrix?.[currentPhase] || [];
268
- break;
269
- } catch { /* ignore */ }
270
- }
271
- }
272
-
273
- if (recommended.length > 0) {
274
- const mcpStatus = recommended.map(name => {
275
- const isConfigured = configuredMcps.has(name.toLowerCase());
276
- return `${name} ${isConfigured ? '✓' : '✗ (recommended)'}`;
277
- });
278
- lines.push('');
279
- lines.push(`MCPs: ${mcpStatus.join(', ')}`);
280
- }
281
- }
282
- } catch {
283
- // Fail-open — MCP status must never block the session
284
- }
285
- // ────────────────────────────────────────────────────────────────────────────
286
-
287
- // ── CLI availability detection ─────────────────────────────────────────────
288
- // Check which development CLIs are available for the current phase.
289
- // Uses execSync with short timeout — fail-open on any error.
290
- try {
291
- const currentPhaseForCli = active ? derivePhaseForFeature(active.name) : '';
292
- if (currentPhaseForCli) {
293
- const cliChecks = [
294
- { name: 'docker', cmd: 'docker --version', phases: ['design', 'implement'] },
295
- { name: 'neonctl', cmd: 'neonctl --version', phases: ['design', 'implement'] },
296
- { name: 'psql', cmd: 'psql --version', phases: ['design', 'implement'] },
297
- { name: 'az', cmd: 'az version --output none', phases: ['implement'] },
298
- { name: 'gh', cmd: 'gh --version', phases: ['design', 'tasks', 'implement'] },
299
- ];
300
-
301
- const relevant = cliChecks.filter(c => c.phases.includes(currentPhaseForCli));
302
- if (relevant.length > 0) {
303
- const cliStatus = relevant.map(c => {
304
- try {
305
- _execSync(c.cmd, { stdio: 'pipe', timeout: 5000 });
306
- return `${c.name} ✓`;
307
- } catch {
308
- return `${c.name} ✗`;
309
- }
310
- });
311
- lines.push(`CLIs: ${cliStatus.join(', ')}`);
312
- }
313
- }
314
- } catch {
315
- // Fail-open — CLI detection must never block the session
316
- }
317
- // ────────────────────────────────────────────────────────────────────────────
318
-
319
- injectContext(lines.join('\n'));
320
- } catch {
321
- // Fail-open
322
- process.exit(0);
323
- }
@@ -1,41 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * SessionStart Hook: Post-Compact Context Restore
5
- *
6
- * Event: SessionStart | Matcher: compact
7
- *
8
- * Fires ONLY when a session starts after context compaction.
9
- * Reads the latest pre-compact memory file and injects the richContext
10
- * block (decisions.md snippet + task list) so Claude resumes with full
11
- * morph awareness even if the compact summary was truncated.
12
- *
13
- * Fail-open: exits 0 on any error.
14
- */
15
-
16
- import { existsSync, readdirSync, readFileSync } from 'fs';
17
- import { join } from 'path';
18
- import { injectContext, pass } from '../../shared/hook-response.js';
19
- import { stateExists } from '../../shared/state-reader.js';
20
- import { buildRestoreBlock, findLatestMemoryFile } from '../../shared/compact-restore.js';
21
-
22
- try {
23
- if (!stateExists()) pass();
24
-
25
- const memoryDir = join(process.cwd(), '.morph', 'memory');
26
- if (!existsSync(memoryDir)) pass();
27
-
28
- const files = readdirSync(memoryDir);
29
- const latest = findLatestMemoryFile(files);
30
- if (!latest) pass();
31
-
32
- const raw = readFileSync(join(memoryDir, latest), 'utf-8');
33
- const snapshot = JSON.parse(raw);
34
-
35
- const block = buildRestoreBlock(snapshot);
36
- if (!block) pass();
37
-
38
- injectContext(block);
39
- } catch {
40
- process.exit(0);
41
- }