@polymorphism-tech/morph-spec 4.9.0 → 4.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. package/README.md +2 -2
  2. package/bin/morph-spec.js +30 -0
  3. package/bin/task-manager.js +34 -22
  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 +35 -98
  8. package/framework/agents/backend/api-designer.md +3 -0
  9. package/framework/agents/backend/dotnet-senior.md +3 -0
  10. package/framework/agents/backend/ef-modeler.md +2 -0
  11. package/framework/agents/backend/hangfire-orchestrator.md +2 -0
  12. package/framework/agents/backend/ms-agent-expert.md +2 -0
  13. package/framework/agents/frontend/blazor-builder.md +2 -0
  14. package/framework/agents/frontend/nextjs-expert.md +2 -0
  15. package/framework/agents/infrastructure/azure-architect.md +2 -0
  16. package/framework/agents/infrastructure/azure-deploy-specialist.md +2 -0
  17. package/framework/agents/infrastructure/bicep-architect.md +2 -0
  18. package/framework/agents/infrastructure/container-specialist.md +2 -0
  19. package/framework/agents/infrastructure/devops-engineer.md +3 -0
  20. package/framework/agents/infrastructure/infra-architect.md +3 -0
  21. package/framework/agents/integrations/asaas-financial.md +2 -0
  22. package/framework/agents/integrations/azure-identity.md +2 -0
  23. package/framework/agents/integrations/clerk-auth.md +3 -0
  24. package/framework/agents/integrations/hangfire-integration.md +2 -0
  25. package/framework/agents/integrations/resend-email.md +2 -0
  26. package/framework/agents.json +37 -7
  27. package/framework/commands/commit.md +166 -0
  28. package/framework/commands/morph-apply.md +156 -155
  29. package/framework/commands/morph-archive.md +33 -27
  30. package/framework/commands/morph-infra.md +83 -77
  31. package/framework/commands/morph-preflight.md +97 -55
  32. package/framework/commands/morph-proposal.md +131 -58
  33. package/framework/commands/morph-status.md +36 -30
  34. package/framework/commands/morph-troubleshoot.md +68 -59
  35. package/framework/hooks/claude-code/notification/approval-reminder.js +3 -2
  36. package/framework/hooks/claude-code/post-tool-use/dispatch.js +154 -31
  37. package/framework/hooks/claude-code/post-tool-use/skill-reminder.js +7 -84
  38. package/framework/hooks/claude-code/post-tool-use/validator-feedback.js +8 -17
  39. package/framework/hooks/claude-code/pre-compact/save-morph-context.js +16 -3
  40. package/framework/hooks/claude-code/pre-tool-use/enforce-phase-writes.js +4 -3
  41. package/framework/hooks/claude-code/pre-tool-use/protect-spec-files.js +3 -2
  42. package/framework/hooks/claude-code/pre-tool-use/task-tracking-guard.js +60 -0
  43. package/framework/hooks/claude-code/session-start/inject-morph-context.js +55 -2
  44. package/framework/hooks/claude-code/session-start/post-compact-restore.js +41 -0
  45. package/framework/hooks/claude-code/stop/validate-completion.js +2 -15
  46. package/framework/hooks/claude-code/user-prompt/enrich-prompt.js +23 -5
  47. package/framework/hooks/shared/compact-restore.js +100 -0
  48. package/framework/hooks/shared/dispatch-helpers.js +116 -0
  49. package/framework/hooks/shared/phase-utils.js +9 -5
  50. package/framework/hooks/shared/state-reader.js +27 -3
  51. package/framework/phases.json +30 -7
  52. package/framework/rules/csharp-standards.md +3 -0
  53. package/framework/rules/frontend-standards.md +2 -0
  54. package/framework/rules/infrastructure-standards.md +3 -0
  55. package/framework/rules/morph-workflow.md +143 -86
  56. package/framework/rules/nextjs-standards.md +2 -0
  57. package/framework/rules/testing-standards.md +3 -0
  58. package/framework/skills/level-0-meta/mcp-registry.json +86 -51
  59. package/framework/skills/level-0-meta/morph-brainstorming/SKILL.md +139 -0
  60. package/framework/skills/level-0-meta/morph-checklist/SKILL.md +42 -19
  61. package/framework/skills/level-0-meta/{code-review → morph-code-review}/SKILL.md +8 -5
  62. package/framework/skills/level-0-meta/{code-review-nextjs → morph-code-review-nextjs}/SKILL.md +8 -6
  63. package/framework/skills/level-0-meta/morph-frontend-review/SKILL.md +362 -0
  64. package/framework/skills/level-0-meta/morph-init/SKILL.md +114 -20
  65. package/framework/skills/level-0-meta/morph-post-implementation/SKILL.md +362 -0
  66. package/framework/skills/level-0-meta/morph-replicate/SKILL.md +95 -87
  67. package/framework/skills/level-0-meta/{simulation-checklist → morph-simulation-checklist}/SKILL.md +24 -0
  68. package/framework/skills/level-0-meta/{tool-usage-guide → morph-tool-usage-guide}/SKILL.md +43 -43
  69. package/framework/skills/level-0-meta/{tool-usage-guide → morph-tool-usage-guide}/references/tools-per-phase.md +1 -2
  70. package/framework/skills/level-0-meta/{verification-before-completion → morph-verification-before-completion}/SKILL.md +23 -12
  71. package/framework/skills/level-0-meta/{verification-before-completion → morph-verification-before-completion}/scripts/check-phase-outputs.mjs +2 -2
  72. package/framework/skills/level-1-workflows/morph-phase-clarify/SKILL.md +247 -0
  73. package/framework/skills/level-1-workflows/morph-phase-codebase-analysis/SKILL.md +270 -0
  74. package/framework/skills/level-1-workflows/morph-phase-design/SKILL.md +499 -0
  75. package/framework/skills/level-1-workflows/morph-phase-implement/.morph/logs/activity.json +38 -0
  76. package/framework/skills/level-1-workflows/morph-phase-implement/SKILL.md +472 -0
  77. package/framework/skills/level-1-workflows/morph-phase-implement/prompts/code-quality-reviewer-prompt.md +50 -0
  78. package/framework/skills/level-1-workflows/morph-phase-implement/prompts/implementer-prompt.md +45 -0
  79. package/framework/skills/level-1-workflows/morph-phase-implement/prompts/spec-reviewer-prompt.md +47 -0
  80. package/framework/skills/level-1-workflows/morph-phase-plan/SKILL.md +246 -0
  81. package/framework/skills/level-1-workflows/morph-phase-setup/SKILL.md +238 -0
  82. package/framework/skills/level-1-workflows/morph-phase-tasks/.morph/logs/activity.json +14 -0
  83. package/framework/skills/level-1-workflows/morph-phase-tasks/SKILL.md +312 -0
  84. package/framework/skills/level-1-workflows/{phase-tasks → morph-phase-tasks}/scripts/validate-tasks.mjs +3 -3
  85. package/framework/skills/level-1-workflows/morph-phase-uiux/SKILL.md +324 -0
  86. package/framework/skills/level-1-workflows/morph-scope-escalation/SKILL.md +146 -0
  87. package/framework/standards/integration/mcp/mcp-tools.md +25 -7
  88. package/framework/templates/docs/onboarding.md +2 -2
  89. package/package.json +3 -4
  90. package/src/commands/agents/dispatch-agents.js +50 -3
  91. package/src/commands/mcp/mcp-setup.js +39 -2
  92. package/src/commands/phase/phase-reset.js +74 -0
  93. package/src/commands/project/doctor.js +26 -7
  94. package/src/commands/project/update.js +4 -4
  95. package/src/commands/scope/escalate.js +215 -0
  96. package/src/commands/state/advance-phase.js +27 -53
  97. package/src/commands/state/state.js +1 -1
  98. package/src/commands/task/expand.js +100 -0
  99. package/src/core/paths/output-schema.js +4 -3
  100. package/src/core/state/phase-state-machine.js +7 -4
  101. package/src/core/state/state-manager.js +4 -3
  102. package/src/lib/detectors/claude-config-detector.js +93 -347
  103. package/src/lib/detectors/design-system-detector.js +189 -189
  104. package/src/lib/detectors/index.js +155 -57
  105. package/src/lib/generators/context-generator.js +2 -2
  106. package/src/lib/installers/mcp-installer.js +37 -5
  107. package/src/lib/phase-chain/phase-validator.js +22 -16
  108. package/src/lib/scope/impact-analyzer.js +106 -0
  109. package/src/lib/stack-filter.js +58 -0
  110. package/src/lib/tasks/task-parser.js +1 -1
  111. package/src/lib/validators/shared/emit-validator-dispatch.js +64 -0
  112. package/src/scripts/setup-infra.js +68 -18
  113. package/src/utils/agents-installer.js +51 -17
  114. package/src/utils/claude-md-injector.js +90 -0
  115. package/src/utils/file-copier.js +0 -1
  116. package/src/utils/hooks-installer.js +16 -5
  117. package/src/utils/skills-installer.js +67 -7
  118. package/CLAUDE.md +0 -98
  119. package/framework/memory/patterns-learned.md +0 -766
  120. package/framework/skills/level-0-meta/brainstorming/SKILL.md +0 -137
  121. package/framework/skills/level-0-meta/frontend-review/SKILL.md +0 -359
  122. package/framework/skills/level-0-meta/post-implementation/SKILL.md +0 -362
  123. package/framework/skills/level-0-meta/terminal-title/SKILL.md +0 -61
  124. package/framework/skills/level-0-meta/terminal-title/scripts/set_title.sh +0 -65
  125. package/framework/skills/level-1-workflows/phase-clarify/SKILL.md +0 -216
  126. package/framework/skills/level-1-workflows/phase-codebase-analysis/SKILL.md +0 -252
  127. package/framework/skills/level-1-workflows/phase-design/SKILL.md +0 -383
  128. package/framework/skills/level-1-workflows/phase-implement/SKILL.md +0 -492
  129. package/framework/skills/level-1-workflows/phase-setup/SKILL.md +0 -195
  130. package/framework/skills/level-1-workflows/phase-tasks/SKILL.md +0 -271
  131. package/framework/skills/level-1-workflows/phase-uiux/SKILL.md +0 -286
  132. package/src/commands/project/index.js +0 -8
  133. package/src/core/index.js +0 -10
  134. package/src/core/state/index.js +0 -8
  135. package/src/core/templates/index.js +0 -9
  136. package/src/core/templates/template-data-sources.js +0 -325
  137. package/src/core/workflows/index.js +0 -7
  138. package/src/lib/detectors/config-detector.js +0 -223
  139. package/src/lib/detectors/standards-generator.js +0 -335
  140. package/src/lib/detectors/structure-detector.js +0 -275
  141. package/src/lib/monitor/agent-resolver.js +0 -144
  142. package/src/lib/monitor/renderer.js +0 -230
  143. package/src/lib/orchestration/index.js +0 -7
  144. package/src/lib/orchestration/team-orchestrator.js +0 -404
  145. package/src/sanitizer/context-sanitizer.js +0 -221
  146. package/src/sanitizer/patterns.js +0 -163
  147. package/src/writer/file-writer.js +0 -86
  148. /package/framework/skills/level-0-meta/{brainstorming → morph-brainstorming}/references/proposal-example.md +0 -0
  149. /package/framework/skills/level-0-meta/{code-review → morph-code-review}/references/review-example.md +0 -0
  150. /package/framework/skills/level-0-meta/{code-review → morph-code-review}/references/review-guidelines.md +0 -0
  151. /package/framework/skills/level-0-meta/{code-review → morph-code-review}/scripts/scan-csharp.mjs +0 -0
  152. /package/framework/skills/level-0-meta/{code-review-nextjs → morph-code-review-nextjs}/references/review-example-nextjs.md +0 -0
  153. /package/framework/skills/level-0-meta/{code-review-nextjs → morph-code-review-nextjs}/scripts/scan-nextjs.mjs +0 -0
  154. /package/framework/skills/level-0-meta/{frontend-review → morph-frontend-review}/scripts/scan-accessibility.mjs +0 -0
  155. /package/framework/skills/level-0-meta/{post-implementation → morph-post-implementation}/scripts/detect-dev-server.mjs +0 -0
  156. /package/framework/skills/level-0-meta/{post-implementation → morph-post-implementation}/scripts/detect-stack.mjs +0 -0
  157. /package/framework/skills/level-1-workflows/{phase-clarify → morph-phase-clarify}/references/clarifications-example.md +0 -0
  158. /package/framework/skills/level-1-workflows/{phase-design → morph-phase-design}/references/architecture-analysis-guide.md +0 -0
  159. /package/framework/skills/level-1-workflows/{phase-design → morph-phase-design}/references/spec-authoring-guide.md +0 -0
  160. /package/framework/skills/level-1-workflows/{phase-design → morph-phase-design}/references/spec-example.md +0 -0
  161. /package/framework/skills/level-1-workflows/{phase-implement → morph-phase-implement}/references/recap-example.md +0 -0
  162. /package/framework/skills/level-1-workflows/{phase-implement → morph-phase-implement}/references/vsa-implementation-guide.md +0 -0
  163. /package/framework/skills/level-1-workflows/{phase-tasks → morph-phase-tasks}/references/task-planning-patterns.md +0 -0
  164. /package/framework/skills/level-1-workflows/{phase-tasks → morph-phase-tasks}/references/tasks-example.md +0 -0
@@ -1,25 +1,33 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * PostToolUse Hook: Improved Dispatch
4
+ * PostToolUse Hook: Dispatch + Validator Injection
5
5
  *
6
6
  * Event: PostToolUse | Matcher: Bash
7
7
  *
8
- * Replaces the old agent-teams/dispatch.js with:
9
- * - Reads state BEFORE dispatching to pass correct from/to phases
10
- * - Handles more CLI commands: approve, state init, checkpoint commands
11
- * - Triggers checkpoint validation on task completion when count % 3 === 0
8
+ * Handles:
9
+ * - morph-spec task done: auto-checkpoint every 3 tasks + validator dispatch injection
10
+ * - morph-spec phase advance: phase chain evaluation + validator dispatch injection
11
+ * - morph-spec approve: pass-through
12
+ *
13
+ * Validator dispatch: extracts VALIDATION DISPATCH JSON from tool output
14
+ * and injects it as additionalContext so Claude reliably dispatches validators
15
+ * as subagents via the Agent tool.
12
16
  *
13
17
  * Fail-open: exits 0 on any error.
14
18
  */
15
19
 
16
20
  import { execSync } from 'child_process';
17
21
  import { readFileSync, existsSync } from 'fs';
18
- import { resolve } from 'path';
22
+ import { resolve, join, dirname } from 'path';
23
+ import { fileURLToPath } from 'url';
24
+
25
+ const __dirname = dirname(fileURLToPath(import.meta.url));
19
26
  import { readStdin } from '../../shared/stdin-reader.js';
20
- import { stateExists, getFeature } from '../../shared/state-reader.js';
27
+ import { stateExists, getFeature, getFeaturePhase } from '../../shared/state-reader.js';
21
28
  import { pass, injectContext } from '../../shared/hook-response.js';
22
29
  import { logHookActivity } from '../../shared/activity-logger.js';
30
+ import { extractValidationDispatch, formatValidatorInstruction, getToolOutput, parseTaskDoneCommand, parsePhaseAdvanceCommand } from '../../shared/dispatch-helpers.js';
23
31
 
24
32
  try {
25
33
  if (!stateExists()) pass();
@@ -30,24 +38,27 @@ try {
30
38
  const command = payload?.tool_input?.command || '';
31
39
  if (!command) pass();
32
40
 
33
- dispatch(command);
41
+ await dispatch(command, payload);
34
42
  } catch {
35
43
  // Fail-open
36
44
  process.exit(0);
37
45
  }
38
46
 
39
- function dispatch(command) {
47
+ async function dispatch(command, payload) {
40
48
  // morph-spec task done <feature> <taskId> [--skip-validation]
41
- const taskDoneMatch = command.match(/morph-spec\s+task\s+done\s+(\S+)\s+(\S+)/);
42
- if (taskDoneMatch) {
43
- const [, featureName, taskId] = taskDoneMatch;
49
+ const taskDoneParsed = parseTaskDoneCommand(command);
50
+ if (taskDoneParsed) {
51
+ const { featureName } = taskDoneParsed;
52
+ const skipValidation = command.includes('--skip-validation');
53
+
54
+ const contextParts = [];
44
55
 
45
56
  // Check if checkpoint should run (every 3 tasks)
46
57
  let dispatchResult = 'task_done';
47
58
  try {
48
59
  const feature = getFeature(featureName);
49
60
  if (feature?.tasks) {
50
- const completed = (feature.tasks.completed || 0) + 1; // +1 because task-done hasn't executed yet
61
+ const completed = (feature.tasks.completed || 0) + 1;
51
62
  if (completed > 0 && completed % 3 === 0) {
52
63
  const checkpointNum = Math.floor(completed / 3);
53
64
  run(`morph-spec checkpoint-save ${featureName} --note "Auto-checkpoint #${checkpointNum} at task ${completed}"`);
@@ -58,16 +69,45 @@ function dispatch(command) {
58
69
  // Non-blocking
59
70
  }
60
71
 
72
+ // Inject validator dispatch for task completion.
73
+ // Skip when --skip-validation: no validators needed, avoids subprocess fallback.
74
+ if (!skipValidation) {
75
+ const validatorText = await buildValidatorDispatchContext(featureName, payload);
76
+ if (validatorText) {
77
+ contextParts.push(validatorText);
78
+ dispatchResult = 'task_done_with_validators';
79
+ }
80
+ }
81
+
61
82
  logHookActivity('phase-dispatch', 'PostToolUse', dispatchResult);
83
+
84
+ if (contextParts.length > 0) {
85
+ injectContext(contextParts.join('\n\n'));
86
+ }
62
87
  pass();
63
88
  }
64
89
 
65
90
  // morph-spec phase advance <feature>
66
- const phaseAdvanceMatch = command.match(/morph-spec\s+phase\s+advance\s+(\S+)/);
67
- if (phaseAdvanceMatch) {
68
- const [, featureName] = phaseAdvanceMatch;
69
- evaluatePhaseChain(featureName);
70
- logHookActivity('phase-dispatch', 'PostToolUse', 'phase_chain');
91
+ const phaseAdvanceParsed = parsePhaseAdvanceCommand(command);
92
+ if (phaseAdvanceParsed) {
93
+ const { featureName } = phaseAdvanceParsed;
94
+
95
+ const contextParts = [];
96
+
97
+ // Phase chain evaluation
98
+ const chainText = buildPhaseChainContext(featureName);
99
+ if (chainText) contextParts.push(chainText);
100
+
101
+ // Validator dispatch injection
102
+ const validatorText = await buildValidatorDispatchContext(featureName, payload);
103
+ if (validatorText) contextParts.push(validatorText);
104
+
105
+ logHookActivity('phase-dispatch', 'PostToolUse',
106
+ validatorText ? 'phase_advance_with_validators' : 'phase_chain');
107
+
108
+ if (contextParts.length > 0) {
109
+ injectContext(contextParts.join('\n\n'));
110
+ }
71
111
  pass();
72
112
  }
73
113
 
@@ -83,45 +123,128 @@ function dispatch(command) {
83
123
  }
84
124
 
85
125
  /**
86
- * Evaluate whether the phase chain should auto-continue after a phase advance.
87
- * Injects a `morph-spec phase run` instruction when the workflow has phaseChain.enabled.
88
- * Silently passes when workflow doesn't support chaining or feature is not found.
126
+ * Build phase chain context text (returns null if not applicable).
127
+ * Replaces the old evaluatePhaseChain that called injectContext directly.
89
128
  *
90
129
  * @param {string} featureName
130
+ * @returns {string|null}
91
131
  */
92
- function evaluatePhaseChain(featureName) {
132
+ function buildPhaseChainContext(featureName) {
93
133
  try {
94
134
  const feature = getFeature(featureName);
95
- if (!feature) return;
135
+ if (!feature) return null;
96
136
 
97
137
  const workflowId = feature.workflow;
98
- if (!workflowId || workflowId === 'auto') return;
138
+ if (!workflowId || workflowId === 'auto') return null;
99
139
 
100
- // Load workflow config to check phaseChain.enabled
101
140
  const configPath = resolve(
102
141
  process.cwd(),
103
142
  `.morph/framework/workflows/configs/${workflowId}.json`
104
143
  );
105
- if (!existsSync(configPath)) return;
144
+ if (!existsSync(configPath)) return null;
106
145
 
107
146
  let workflowConfig;
108
147
  try {
109
148
  workflowConfig = JSON.parse(readFileSync(configPath, 'utf8'));
110
149
  } catch {
111
- return;
150
+ return null;
112
151
  }
113
152
 
114
- if (!workflowConfig?.phaseChain?.enabled) return;
153
+ if (!workflowConfig?.phaseChain?.enabled) return null;
115
154
 
116
- // Inject instruction to continue the phase chain
117
- injectContext(
155
+ return (
118
156
  `Phase advance complete. Continue the phase chain:\n` +
119
157
  ` morph-spec phase run ${featureName}\n\n` +
120
158
  `The phase runner will check eligibility and auto-advance if all gates pass. ` +
121
159
  `It will pause automatically on blocked tasks, low pass rate, or trust gates.`
122
160
  );
123
161
  } catch {
124
- // Non-blocking — fail silently
162
+ return null;
163
+ }
164
+ }
165
+
166
+ /**
167
+ * Extract validator dispatch from tool output and build injection context.
168
+ *
169
+ * Strategy:
170
+ * 1. Try to parse VALIDATION DISPATCH JSON from the tool_result (stdout of phase advance)
171
+ * 2. If not found, fallback to running dispatch-agents CLI in validate mode
172
+ *
173
+ * @param {string} featureName
174
+ * @param {object} payload - PostToolUse hook payload
175
+ * @returns {string|null} - Formatted injection text or null
176
+ */
177
+ async function buildValidatorDispatchContext(featureName, payload) {
178
+ try {
179
+ // Strategy 1: Extract from tool output
180
+ const toolOutput = getToolOutput(payload);
181
+ const dispatch = extractValidationDispatch(toolOutput);
182
+ if (dispatch) return formatValidatorInstruction(dispatch);
183
+
184
+ // Strategy 2: Direct import of buildDispatchConfig (no subprocess)
185
+ return await buildDispatchDirect(featureName);
186
+ } catch {
187
+ return null;
188
+ }
189
+ }
190
+
191
+ /**
192
+ * Fallback: build validator dispatch config via direct import (no subprocess).
193
+ *
194
+ * Attempts to import buildDispatchConfig from the installed package or local
195
+ * framework. Falls back to null on any error (fail-open).
196
+ *
197
+ * @param {string} featureName
198
+ * @returns {Promise<string|null>}
199
+ */
200
+ async function buildDispatchDirect(featureName) {
201
+ try {
202
+ const phase = getFeaturePhase(featureName);
203
+ if (!phase) return null;
204
+
205
+ // Try local project path first, then package path
206
+ let buildDispatchConfig;
207
+ const localPath = resolve(process.cwd(), 'src/commands/agents/dispatch-agents.js');
208
+ const pkgPath = resolve(process.cwd(), 'node_modules/@polymorphism-tech/morph-spec/src/commands/agents/dispatch-agents.js');
209
+
210
+ if (existsSync(localPath)) {
211
+ ({ buildDispatchConfig } = await import(`file://${localPath.replace(/\\/g, '/')}`));
212
+ } else if (existsSync(pkgPath)) {
213
+ ({ buildDispatchConfig } = await import(`file://${pkgPath.replace(/\\/g, '/')}`));
214
+ } else {
215
+ return null;
216
+ }
217
+
218
+ const config = await buildDispatchConfig(process.cwd(), featureName, phase, { mode: 'validate' });
219
+ const validators = config.agents?.filter(a => a.tier === 4);
220
+ if (!validators || validators.length === 0) return null;
221
+
222
+ const agentsJsonPath = resolve(process.cwd(), 'framework/agents.json');
223
+ const pkgAgentsPath = resolve(process.cwd(), 'node_modules/@polymorphism-tech/morph-spec/framework/agents.json');
224
+ const agentsPath = existsSync(agentsJsonPath) ? agentsJsonPath : pkgAgentsPath;
225
+
226
+ let allAgents = {};
227
+ if (existsSync(agentsPath)) {
228
+ allAgents = JSON.parse(readFileSync(agentsPath, 'utf8')).agents || {};
229
+ }
230
+
231
+ const dispatch = {
232
+ validators: validators.map(v => {
233
+ const agentData = allAgents[v.id];
234
+ return {
235
+ id: v.id,
236
+ title: v.title,
237
+ taskPrompt: v.taskPrompt,
238
+ severity: agentData?.hook_behavior?.severity || 'error',
239
+ blocksOnFail: agentData?.hook_behavior?.blocks_on_fail ?? true,
240
+ checks: agentData?.hook_behavior?.validates || [],
241
+ };
242
+ }),
243
+ };
244
+
245
+ return formatValidatorInstruction(dispatch);
246
+ } catch {
247
+ return null;
125
248
  }
126
249
  }
127
250
 
@@ -18,83 +18,13 @@
18
18
  import { readFileSync, existsSync } from 'fs';
19
19
  import { join } from 'path';
20
20
  import { readStdin } from '../../shared/stdin-reader.js';
21
- import { stateExists } from '../../shared/state-reader.js';
21
+ import { stateExists, loadState, derivePhaseForFeature } from '../../shared/state-reader.js';
22
22
  import { injectContext, pass } from '../../shared/hook-response.js';
23
23
  import { logHookActivity } from '../../shared/activity-logger.js';
24
+ import { parseTaskStartCommand, buildSkillReminderMessage } from '../../shared/skill-reminder-helpers.js';
24
25
 
25
- // ─────────────────────────────────────────────────────────────────────────────
26
- // Pure helpers (exported for testing)
27
- // ─────────────────────────────────────────────────────────────────────────────
28
-
29
- /**
30
- * Parse `morph-spec task start <feature> <taskId>` from a bash command string.
31
- * Returns { featureName, taskId } or null if the command doesn't match.
32
- *
33
- * @param {string} command
34
- * @returns {{ featureName: string, taskId: string } | null}
35
- */
36
- export function parseTaskStartCommand(command) {
37
- if (!command || typeof command !== 'string') return null;
38
- const match = command.match(/morph-spec\s+task\s+start\s+(\S+)\s+(\S+)/);
39
- if (!match) return null;
40
- return { featureName: match[1], taskId: match[2] };
41
- }
42
-
43
- /**
44
- * Build the mandatory skill reminder message for a task start event.
45
- * Returns null if no relevant skills exist for this phase.
46
- *
47
- * @param {string} featureName
48
- * @param {string} taskId
49
- * @param {string} phase
50
- * @param {Object|null} phasesData - Parsed phases.json content
51
- * @returns {string|null}
52
- */
53
- export function buildSkillReminderMessage(featureName, taskId, phase, phasesData) {
54
- try {
55
- const allSkills = phasesData?.phases?.[phase]?.requiredSkills;
56
- if (!allSkills || allSkills.length === 0) return null;
57
-
58
- const nowSkills = allSkills.filter(s => s.trigger === 'beforeEachTask');
59
- const laterSkills = allSkills.filter(s => s.trigger === 'beforeTaskDone');
60
- const bugSkills = allSkills.filter(s => s.trigger === 'onBugOrUnexpected');
61
-
62
- if (nowSkills.length === 0 && laterSkills.length === 0) return null;
63
-
64
- const lines = [];
65
- lines.push(`⚠️ MORPH-SPEC SKILL REMINDER — Task ${taskId} started (phase: ${phase})`);
66
- lines.push('');
67
-
68
- if (nowSkills.length > 0) {
69
- lines.push('INVOKE NOW before writing any code:');
70
- for (const s of nowSkills) {
71
- lines.push(` → Skill(${s.skill})`);
72
- }
73
- lines.push('');
74
- }
75
-
76
- if (laterSkills.length > 0) {
77
- lines.push('INVOKE BEFORE marking task done:');
78
- for (const s of laterSkills) {
79
- lines.push(` → Skill(${s.skill})`);
80
- }
81
- lines.push('');
82
- }
83
-
84
- if (bugSkills.length > 0) {
85
- lines.push('IF you encounter a bug or unexpected behavior:');
86
- for (const s of bugSkills) {
87
- lines.push(` → Skill(${s.skill})`);
88
- }
89
- lines.push('');
90
- }
91
-
92
- lines.push('These are MANDATORY. Use the Skill() tool. Do NOT skip.');
93
- return lines.join('\n');
94
- } catch {
95
- return null;
96
- }
97
- }
26
+ // Re-export for backwards compatibility (tests may import from this file)
27
+ export { parseTaskStartCommand, buildSkillReminderMessage };
98
28
 
99
29
  // ─────────────────────────────────────────────────────────────────────────────
100
30
  // Hook entry point
@@ -113,20 +43,13 @@ try {
113
43
  const { featureName, taskId } = parsed;
114
44
 
115
45
  // Load state to get current phase
116
- const statePath = join(process.cwd(), '.morph', 'state.json');
117
- if (!existsSync(statePath)) pass();
118
-
119
- let state;
120
- try {
121
- state = JSON.parse(readFileSync(statePath, 'utf8'));
122
- } catch {
123
- pass();
124
- }
46
+ const state = loadState();
47
+ if (!state) pass();
125
48
 
126
49
  const feature = state?.features?.[featureName];
127
50
  if (!feature) pass();
128
51
 
129
- const phase = feature.phase;
52
+ const phase = feature.phase || derivePhaseForFeature(featureName);
130
53
  if (!phase) pass();
131
54
 
132
55
  // Load phases.json (project-local first, fall back to package-level)
@@ -16,12 +16,11 @@
16
16
  * Fail-open: exits 0 on any error.
17
17
  */
18
18
 
19
- import { readFileSync, existsSync } from 'fs';
20
- import { join } from 'path';
21
19
  import { readStdin } from '../../shared/stdin-reader.js';
22
- import { stateExists } from '../../shared/state-reader.js';
20
+ import { stateExists, loadState } from '../../shared/state-reader.js';
23
21
  import { injectContext, pass } from '../../shared/hook-response.js';
24
22
  import { logHookActivity } from '../../shared/activity-logger.js';
23
+ import { parseTaskDoneCommand } from '../../shared/dispatch-helpers.js';
25
24
 
26
25
  // Standard IDs referenced in remediation messages
27
26
  const STANDARD_REFS = {
@@ -42,22 +41,14 @@ try {
42
41
  const command = payload?.tool_input?.command || '';
43
42
  if (!command) pass();
44
43
 
45
- // Match: morph-spec task done <feature> <taskId>
46
- const taskDoneMatch = command.match(/morph-spec\s+task\s+done\s+(\S+)\s+(\S+)/);
47
- if (!taskDoneMatch) pass();
44
+ const parsed = parseTaskDoneCommand(command);
45
+ if (!parsed) pass();
48
46
 
49
- const [, featureName, taskId] = taskDoneMatch;
47
+ const { featureName, taskId } = parsed;
50
48
 
51
49
  // Load state and find validationHistory for this task
52
- const statePath = join(process.cwd(), '.morph', 'state.json');
53
- if (!existsSync(statePath)) pass();
54
-
55
- let state;
56
- try {
57
- state = JSON.parse(readFileSync(statePath, 'utf8'));
58
- } catch {
59
- pass();
60
- }
50
+ const state = loadState();
51
+ if (!state) pass();
61
52
 
62
53
  const feature = state?.features?.[featureName];
63
54
  if (!feature) pass();
@@ -100,7 +91,7 @@ function buildRemediationContext(featureName, taskId, taskHistory) {
100
91
  }
101
92
  }
102
93
 
103
- if (allIssues.length === 0 && taskHistory.status !== 'blocked') pass();
94
+ if (allIssues.length === 0 && taskHistory.status !== 'blocked') return;
104
95
 
105
96
  const lines = [];
106
97
 
@@ -23,9 +23,7 @@ import {
23
23
  } from '../../shared/state-reader.js';
24
24
  import { injectContext, pass } from '../../shared/hook-response.js';
25
25
  import { logHookActivity } from '../../shared/activity-logger.js';
26
-
27
- const DECISIONS_MAX_CHARS = 1500;
28
- const MAX_PENDING_TASKS = 8;
26
+ import { buildRichContext, DECISIONS_MAX_CHARS, MAX_PENDING_TASKS } from '../../shared/compact-restore.js';
29
27
 
30
28
  const PHASE_POSITIONS = {
31
29
  proposal: 1, setup: 1,
@@ -73,6 +71,21 @@ try {
73
71
  };
74
72
  }
75
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}/1-design/decisions.md`);
81
+ if (existsSync(decisionsPath)) {
82
+ try { decisionsContent = readFileSync(decisionsPath, 'utf-8'); } catch { /* ignore */ }
83
+ }
84
+ snapshot.richContext = buildRichContext(activeFeature, activePhase, decisionsContent);
85
+ } catch { /* fail-open: richContext is optional */ }
86
+ }
87
+ // ─────────────────────────────────────────────────────────────────────────
88
+
76
89
  const memoryDir = join(cwd, '.morph', 'memory');
77
90
  if (!existsSync(memoryDir)) mkdirSync(memoryDir, { recursive: true });
78
91
  const ts = new Date().toISOString().replace(/[:.]/g, '-');
@@ -9,8 +9,8 @@
9
9
  * proposal → only 0-proposal/
10
10
  * design, clarify → only 1-design/
11
11
  * uiux → only 2-ui/
12
- * tasks → only 3-tasks/
13
- * implement → 4-implement/ + any source code (unrestricted)
12
+ * tasks → only 4-tasks/
13
+ * implement → 5-implement/ + any source code (unrestricted)
14
14
  *
15
15
  * Files outside .morph/features/ are always allowed.
16
16
  *
@@ -29,6 +29,7 @@ import {
29
29
  } from '../../shared/phase-utils.js';
30
30
  import { block, pass } from '../../shared/hook-response.js';
31
31
  import { logHookActivity } from '../../shared/activity-logger.js';
32
+ import { getFilePath } from '../../shared/payload-utils.js';
32
33
 
33
34
  try {
34
35
  // Amend mode: bypass phase write enforcement for legitimate corrections
@@ -42,7 +43,7 @@ try {
42
43
  const payload = await readStdin();
43
44
  if (!payload) pass();
44
45
 
45
- const filePath = payload?.tool_input?.file_path || payload?.tool_input?.path || '';
46
+ const filePath = getFilePath(payload);
46
47
  if (!filePath) pass();
47
48
 
48
49
  // Only check files inside .morph/features/
@@ -7,7 +7,7 @@
7
7
  *
8
8
  * Blocks Write/Edit to spec artifacts after their approval gate has passed:
9
9
  * - 1-design/spec.md, contracts.cs, etc. → blocked if 'design' gate approved
10
- * - 3-tasks/tasks.md → blocked if 'tasks' gate approved
10
+ * - 4-tasks/tasks.md → blocked if 'tasks' gate approved
11
11
  * - 2-ui/design-system.md, etc. → blocked if 'uiux' gate approved
12
12
  *
13
13
  * Fail-open: exits 0 on any error.
@@ -23,6 +23,7 @@ import {
23
23
  } from '../../shared/phase-utils.js';
24
24
  import { block, pass } from '../../shared/hook-response.js';
25
25
  import { logHookActivity } from '../../shared/activity-logger.js';
26
+ import { getFilePath } from '../../shared/payload-utils.js';
26
27
 
27
28
  try {
28
29
  // Amend mode: bypass spec protection for legitimate in-implementation corrections
@@ -36,7 +37,7 @@ try {
36
37
  const payload = await readStdin();
37
38
  if (!payload) pass();
38
39
 
39
- const filePath = payload?.tool_input?.file_path || payload?.tool_input?.path || '';
40
+ const filePath = getFilePath(payload);
40
41
  if (!filePath) pass();
41
42
 
42
43
  // Only check files inside .morph/features/
@@ -0,0 +1,60 @@
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
40
+ // Method 1: tasks.inProgress counter (v3 state format)
41
+ if (feature.tasks?.inProgress > 0) pass();
42
+
43
+ // Method 2: taskList array (v2 state format)
44
+ if (feature.taskList) {
45
+ const hasActive = feature.taskList.some(t => t.status === 'in_progress');
46
+ if (hasActive) pass();
47
+ }
48
+
49
+ // No task in_progress — inject advisory warning
50
+ approve(
51
+ `[morph-spec] WARNING: No active task tracking detected during implement phase.\n` +
52
+ `Before writing code, start a task:\n` +
53
+ ` npx morph-spec task next ${name} # see what's next\n` +
54
+ ` npx morph-spec task start ${name} <id> # mark it in progress\n` +
55
+ `Task tracking ensures validators run, checkpoints fire, and progress is recorded.`
56
+ );
57
+ } catch {
58
+ // Fail-open
59
+ process.exit(0);
60
+ }
@@ -54,11 +54,11 @@ const SPEC_MAX_CHARS = getSpecMaxChars();
54
54
  * @param {string} cwd - Project root path
55
55
  */
56
56
  function saveStateSync(state, cwd) {
57
+ const statePath = join(cwd, '.morph/state.json');
58
+ const tmpPath = `${statePath}.tmp.hook.${process.pid}`;
57
59
  try {
58
60
  state.metadata = state.metadata || {};
59
61
  state.metadata.lastUpdated = new Date().toISOString();
60
- const statePath = join(cwd, '.morph/state.json');
61
- const tmpPath = `${statePath}.tmp.hook.${process.pid}`;
62
62
  writeFileSync(tmpPath, JSON.stringify(state, null, 2), 'utf8');
63
63
  renameSync(tmpPath, statePath);
64
64
  } catch {
@@ -209,6 +209,59 @@ try {
209
209
  }
210
210
  }
211
211
 
212
+ // ── MCP status injection ────────────────────────────────────────────────────
213
+ // Cross-reference configured MCPs with phase recommendations from mcp-registry.json.
214
+ // Fail-open: all inside try/catch, no crash on missing files.
215
+ try {
216
+ const currentPhase = active ? derivePhaseForFeature(active.name) : '';
217
+ if (currentPhase) {
218
+ // Read configured MCPs from settings
219
+ const configuredMcps = new Set();
220
+ const settingsFiles = [
221
+ join(process.cwd(), '.claude', 'settings.local.json'),
222
+ join(process.cwd(), '.claude', 'settings.json'),
223
+ ];
224
+ for (const sf of settingsFiles) {
225
+ if (existsSync(sf)) {
226
+ try {
227
+ const s = JSON.parse(readFileSync(sf, 'utf8'));
228
+ for (const name of Object.keys(s.mcpServers || {})) {
229
+ configuredMcps.add(name.toLowerCase());
230
+ }
231
+ } catch { /* ignore */ }
232
+ }
233
+ }
234
+
235
+ // Read recommended MCPs from phases.json or mcp-registry.json
236
+ let recommended = [];
237
+ const registryPaths = [
238
+ join(process.cwd(), 'framework', 'skills', 'level-0-meta', 'mcp-registry.json'),
239
+ join(process.cwd(), '.morph', 'framework', 'skills', 'level-0-meta', 'mcp-registry.json'),
240
+ ];
241
+ for (const rp of registryPaths) {
242
+ if (existsSync(rp)) {
243
+ try {
244
+ const registry = JSON.parse(readFileSync(rp, 'utf8'));
245
+ recommended = registry.phaseMatrix?.[currentPhase] || [];
246
+ break;
247
+ } catch { /* ignore */ }
248
+ }
249
+ }
250
+
251
+ if (recommended.length > 0) {
252
+ const mcpStatus = recommended.map(name => {
253
+ const isConfigured = configuredMcps.has(name.toLowerCase());
254
+ return `${name} ${isConfigured ? '✓' : '✗ (recommended)'}`;
255
+ });
256
+ lines.push('');
257
+ lines.push(`MCPs: ${mcpStatus.join(', ')}`);
258
+ }
259
+ }
260
+ } catch {
261
+ // Fail-open — MCP status must never block the session
262
+ }
263
+ // ────────────────────────────────────────────────────────────────────────────
264
+
212
265
  // Remind about key commands
213
266
  lines.push('');
214
267
  lines.push('Key commands: morph-spec status <feature> | morph-spec phase advance <feature> | morph-spec approve <feature> <gate>');