@polymorphism-tech/morph-spec 4.9.0 → 4.10.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 (124) 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 +99 -98
  8. package/framework/agents.json +37 -7
  9. package/framework/commands/commit.md +166 -0
  10. package/framework/commands/morph-apply.md +13 -2
  11. package/framework/commands/morph-archive.md +8 -2
  12. package/framework/commands/morph-infra.md +6 -0
  13. package/framework/commands/morph-preflight.md +6 -0
  14. package/framework/commands/morph-proposal.md +56 -7
  15. package/framework/commands/morph-status.md +6 -0
  16. package/framework/commands/morph-troubleshoot.md +6 -0
  17. package/framework/hooks/claude-code/notification/approval-reminder.js +3 -2
  18. package/framework/hooks/claude-code/post-tool-use/dispatch.js +154 -31
  19. package/framework/hooks/claude-code/post-tool-use/skill-reminder.js +7 -84
  20. package/framework/hooks/claude-code/post-tool-use/validator-feedback.js +8 -17
  21. package/framework/hooks/claude-code/pre-compact/save-morph-context.js +16 -3
  22. package/framework/hooks/claude-code/pre-tool-use/enforce-phase-writes.js +4 -3
  23. package/framework/hooks/claude-code/pre-tool-use/protect-spec-files.js +3 -2
  24. package/framework/hooks/claude-code/pre-tool-use/task-tracking-guard.js +60 -0
  25. package/framework/hooks/claude-code/session-start/inject-morph-context.js +55 -2
  26. package/framework/hooks/claude-code/session-start/post-compact-restore.js +41 -0
  27. package/framework/hooks/claude-code/stop/validate-completion.js +2 -15
  28. package/framework/hooks/claude-code/user-prompt/enrich-prompt.js +23 -5
  29. package/framework/hooks/shared/compact-restore.js +100 -0
  30. package/framework/hooks/shared/dispatch-helpers.js +116 -0
  31. package/framework/hooks/shared/phase-utils.js +9 -5
  32. package/framework/hooks/shared/state-reader.js +27 -3
  33. package/framework/phases.json +30 -7
  34. package/framework/rules/morph-workflow.md +88 -86
  35. package/framework/skills/level-0-meta/mcp-registry.json +86 -51
  36. package/framework/skills/level-0-meta/{brainstorming → morph-brainstorming}/SKILL.md +13 -16
  37. package/framework/skills/level-0-meta/{code-review → morph-code-review}/SKILL.md +1 -1
  38. package/framework/skills/level-0-meta/{code-review-nextjs → morph-code-review-nextjs}/SKILL.md +2 -2
  39. package/framework/skills/level-0-meta/{frontend-review → morph-frontend-review}/SKILL.md +5 -5
  40. package/framework/skills/level-0-meta/morph-init/SKILL.md +72 -7
  41. package/framework/skills/level-0-meta/{post-implementation → morph-post-implementation}/SKILL.md +9 -9
  42. package/framework/skills/level-0-meta/morph-replicate/SKILL.md +1 -1
  43. package/framework/skills/level-0-meta/{terminal-title → morph-terminal-title}/SKILL.md +1 -1
  44. package/framework/skills/level-0-meta/{tool-usage-guide → morph-tool-usage-guide}/SKILL.md +2 -3
  45. package/framework/skills/level-0-meta/{tool-usage-guide → morph-tool-usage-guide}/references/tools-per-phase.md +1 -2
  46. package/framework/skills/level-0-meta/{verification-before-completion → morph-verification-before-completion}/SKILL.md +1 -1
  47. package/framework/skills/level-0-meta/{verification-before-completion → morph-verification-before-completion}/scripts/check-phase-outputs.mjs +2 -2
  48. package/framework/skills/level-1-workflows/morph-phase-clarify/SKILL.md +238 -0
  49. package/framework/skills/level-1-workflows/{phase-codebase-analysis → morph-phase-codebase-analysis}/SKILL.md +251 -251
  50. package/framework/skills/level-1-workflows/morph-phase-design/SKILL.md +507 -0
  51. package/framework/skills/level-1-workflows/{phase-implement → morph-phase-implement}/SKILL.md +590 -491
  52. package/framework/skills/level-1-workflows/morph-phase-implement/prompts/code-quality-reviewer-prompt.md +50 -0
  53. package/framework/skills/level-1-workflows/morph-phase-implement/prompts/implementer-prompt.md +45 -0
  54. package/framework/skills/level-1-workflows/morph-phase-implement/prompts/spec-reviewer-prompt.md +47 -0
  55. package/framework/skills/level-1-workflows/morph-phase-plan/SKILL.md +254 -0
  56. package/framework/skills/level-1-workflows/{phase-setup → morph-phase-setup}/SKILL.md +237 -194
  57. package/framework/skills/level-1-workflows/{phase-tasks → morph-phase-tasks}/SKILL.md +307 -270
  58. package/framework/skills/level-1-workflows/{phase-tasks → morph-phase-tasks}/scripts/validate-tasks.mjs +3 -3
  59. package/framework/skills/level-1-workflows/{phase-uiux → morph-phase-uiux}/SKILL.md +320 -285
  60. package/framework/skills/level-1-workflows/morph-scope-escalation/SKILL.md +97 -0
  61. package/framework/standards/integration/mcp/mcp-tools.md +25 -7
  62. package/framework/templates/docs/onboarding.md +2 -2
  63. package/package.json +1 -2
  64. package/src/commands/agents/dispatch-agents.js +50 -3
  65. package/src/commands/mcp/mcp-setup.js +39 -2
  66. package/src/commands/phase/phase-reset.js +74 -0
  67. package/src/commands/project/doctor.js +19 -5
  68. package/src/commands/scope/escalate.js +215 -0
  69. package/src/commands/state/advance-phase.js +27 -53
  70. package/src/commands/state/state.js +1 -1
  71. package/src/commands/task/expand.js +100 -0
  72. package/src/core/paths/output-schema.js +4 -3
  73. package/src/core/state/phase-state-machine.js +7 -4
  74. package/src/core/state/state-manager.js +4 -3
  75. package/src/lib/detectors/claude-config-detector.js +93 -347
  76. package/src/lib/detectors/design-system-detector.js +189 -189
  77. package/src/lib/detectors/index.js +155 -57
  78. package/src/lib/generators/context-generator.js +2 -2
  79. package/src/lib/installers/mcp-installer.js +37 -5
  80. package/src/lib/phase-chain/phase-validator.js +22 -16
  81. package/src/lib/scope/impact-analyzer.js +106 -0
  82. package/src/lib/tasks/task-parser.js +1 -1
  83. package/src/lib/validators/shared/emit-validator-dispatch.js +64 -0
  84. package/src/scripts/setup-infra.js +15 -0
  85. package/src/utils/agents-installer.js +32 -12
  86. package/src/utils/file-copier.js +0 -1
  87. package/src/utils/hooks-installer.js +15 -1
  88. package/framework/skills/level-1-workflows/phase-clarify/SKILL.md +0 -216
  89. package/framework/skills/level-1-workflows/phase-design/SKILL.md +0 -383
  90. package/src/commands/project/index.js +0 -8
  91. package/src/core/index.js +0 -10
  92. package/src/core/state/index.js +0 -8
  93. package/src/core/templates/index.js +0 -9
  94. package/src/core/templates/template-data-sources.js +0 -325
  95. package/src/core/workflows/index.js +0 -7
  96. package/src/lib/detectors/config-detector.js +0 -223
  97. package/src/lib/detectors/standards-generator.js +0 -335
  98. package/src/lib/detectors/structure-detector.js +0 -275
  99. package/src/lib/monitor/agent-resolver.js +0 -144
  100. package/src/lib/monitor/renderer.js +0 -230
  101. package/src/lib/orchestration/index.js +0 -7
  102. package/src/lib/orchestration/team-orchestrator.js +0 -404
  103. package/src/sanitizer/context-sanitizer.js +0 -221
  104. package/src/sanitizer/patterns.js +0 -163
  105. package/src/writer/file-writer.js +0 -86
  106. /package/framework/skills/level-0-meta/{brainstorming → morph-brainstorming}/references/proposal-example.md +0 -0
  107. /package/framework/skills/level-0-meta/{code-review → morph-code-review}/references/review-example.md +0 -0
  108. /package/framework/skills/level-0-meta/{code-review → morph-code-review}/references/review-guidelines.md +0 -0
  109. /package/framework/skills/level-0-meta/{code-review → morph-code-review}/scripts/scan-csharp.mjs +0 -0
  110. /package/framework/skills/level-0-meta/{code-review-nextjs → morph-code-review-nextjs}/references/review-example-nextjs.md +0 -0
  111. /package/framework/skills/level-0-meta/{code-review-nextjs → morph-code-review-nextjs}/scripts/scan-nextjs.mjs +0 -0
  112. /package/framework/skills/level-0-meta/{frontend-review → morph-frontend-review}/scripts/scan-accessibility.mjs +0 -0
  113. /package/framework/skills/level-0-meta/{post-implementation → morph-post-implementation}/scripts/detect-dev-server.mjs +0 -0
  114. /package/framework/skills/level-0-meta/{post-implementation → morph-post-implementation}/scripts/detect-stack.mjs +0 -0
  115. /package/framework/skills/level-0-meta/{simulation-checklist → morph-simulation-checklist}/SKILL.md +0 -0
  116. /package/framework/skills/level-0-meta/{terminal-title → morph-terminal-title}/scripts/set_title.sh +0 -0
  117. /package/framework/skills/level-1-workflows/{phase-clarify → morph-phase-clarify}/references/clarifications-example.md +0 -0
  118. /package/framework/skills/level-1-workflows/{phase-design → morph-phase-design}/references/architecture-analysis-guide.md +0 -0
  119. /package/framework/skills/level-1-workflows/{phase-design → morph-phase-design}/references/spec-authoring-guide.md +0 -0
  120. /package/framework/skills/level-1-workflows/{phase-design → morph-phase-design}/references/spec-example.md +0 -0
  121. /package/framework/skills/level-1-workflows/{phase-implement → morph-phase-implement}/references/recap-example.md +0 -0
  122. /package/framework/skills/level-1-workflows/{phase-implement → morph-phase-implement}/references/vsa-implementation-guide.md +0 -0
  123. /package/framework/skills/level-1-workflows/{phase-tasks → morph-phase-tasks}/references/task-planning-patterns.md +0 -0
  124. /package/framework/skills/level-1-workflows/{phase-tasks → morph-phase-tasks}/references/tasks-example.md +0 -0
@@ -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>');
@@ -0,0 +1,41 @@
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
+ }
@@ -21,7 +21,7 @@
21
21
  import { readFileSync, writeFileSync, existsSync } from 'fs';
22
22
  import { join } from 'path';
23
23
  import {
24
- stateExists, loadState, getActiveFeature, getMissingOutputs, derivePhaseForFeature,
24
+ stateExists, getActiveFeature, getMostRecentFeature, getMissingOutputs, derivePhaseForFeature,
25
25
  } from '../../shared/state-reader.js';
26
26
  import { injectContext, pass } from '../../shared/hook-response.js';
27
27
  import { logHookActivity } from '../../shared/activity-logger.js';
@@ -40,20 +40,7 @@ try {
40
40
 
41
41
  // getActiveFeature() only returns in_progress/draft features.
42
42
  // Fall back to the most recently updated feature when all features are 'done'.
43
- let active = getActiveFeature();
44
- if (!active) {
45
- const state = loadState();
46
- if (state?.features) {
47
- let latestUpdate = '';
48
- for (const [name, feature] of Object.entries(state.features)) {
49
- const updated = feature.updatedAt || feature.createdAt || '';
50
- if (updated >= latestUpdate) {
51
- latestUpdate = updated;
52
- active = { name, feature };
53
- }
54
- }
55
- }
56
- }
43
+ let active = getActiveFeature() || getMostRecentFeature();
57
44
  if (!active) pass();
58
45
 
59
46
  const { name, feature } = active;
@@ -15,7 +15,7 @@
15
15
  */
16
16
 
17
17
  import { readStdin } from '../../shared/stdin-reader.js';
18
- import { stateExists, loadState, getActiveFeature, getFeature, getPendingGates } from '../../shared/state-reader.js';
18
+ import { stateExists, loadState, getActiveFeature, getFeature, getPendingGates, derivePhaseForFeature } from '../../shared/state-reader.js';
19
19
  import { injectContext, pass } from '../../shared/hook-response.js';
20
20
  import { logHookActivity } from '../../shared/activity-logger.js';
21
21
 
@@ -37,7 +37,8 @@ try {
37
37
  // Check if a feature name is mentioned
38
38
  for (const [featureName, feature] of Object.entries(state.features)) {
39
39
  if (promptLower.includes(featureName.toLowerCase())) {
40
- context.push(`[morph-spec] Feature '${featureName}': phase=${feature.phase}, status=${feature.status}`);
40
+ const featurePhase = feature.phase || derivePhaseForFeature(featureName);
41
+ context.push(`[morph-spec] Feature '${featureName}': phase=${featurePhase}, status=${feature.status}`);
41
42
  if (feature.tasks?.total > 0) {
42
43
  context.push(` Tasks: ${feature.tasks.completed || 0}/${feature.tasks.total} completed`);
43
44
  }
@@ -50,16 +51,33 @@ try {
50
51
  if (active) {
51
52
  const { name, feature } = active;
52
53
 
54
+ const activePhase = feature.phase || derivePhaseForFeature(name);
53
55
  const codeKeywords = ['implement', 'code', 'start coding', 'write the code', 'build it', 'let\'s build'];
54
56
  const wantsToCode = codeKeywords.some(kw => promptLower.includes(kw));
55
57
 
56
- if (wantsToCode && feature.phase !== 'implement' && feature.phase !== 'sync') {
58
+ if (wantsToCode && activePhase !== 'implement' && activePhase !== 'sync') {
57
59
  context.push(
58
- `[morph-spec] WARNING: Feature '${name}' is in '${feature.phase}' phase, not 'implement'.` +
60
+ `[morph-spec] WARNING: Feature '${name}' is in '${activePhase}' phase, not 'implement'.` +
59
61
  ` Complete the current phase first or advance: morph-spec phase advance ${name}`
60
62
  );
61
63
  }
62
64
 
65
+ // Coding intent during implement phase but no task in_progress
66
+ if (wantsToCode && activePhase === 'implement') {
67
+ let hasActiveTask = feature.tasks?.inProgress > 0;
68
+ if (!hasActiveTask && feature.taskList) {
69
+ hasActiveTask = feature.taskList.some(t => t.status === 'in_progress');
70
+ }
71
+ if (!hasActiveTask) {
72
+ context.push(
73
+ `[morph-spec] REMINDER: No task is currently in_progress for '${name}'.` +
74
+ ` Start a task before coding:\n` +
75
+ ` npx morph-spec task next ${name} # see what's next\n` +
76
+ ` npx morph-spec task start ${name} <id> # mark it in progress`
77
+ );
78
+ }
79
+ }
80
+
63
81
  // Check for approval intent
64
82
  const approvalKeywords = ['approve', 'approved', 'looks good', 'lgtm', 'ship it'];
65
83
  const wantsToApprove = approvalKeywords.some(kw => promptLower.includes(kw));
@@ -75,7 +93,7 @@ try {
75
93
 
76
94
  // Check for "next task" intent
77
95
  if (promptLower.includes('next task') || promptLower.includes('what\'s next')) {
78
- if (feature.phase === 'implement') {
96
+ if (activePhase === 'implement') {
79
97
  context.push(
80
98
  `[morph-spec] Use: morph-spec task next ${name}`
81
99
  );
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Shared helpers for post-compact context restoration.
3
+ *
4
+ * Used by:
5
+ * - pre-compact hook: buildRichContext() to enrich memory file
6
+ * - post-compact-restore hook: buildRestoreBlock() + findLatestMemoryFile() to inject context
7
+ *
8
+ * Pure functions — no I/O.
9
+ */
10
+
11
+ export const DECISIONS_MAX_CHARS = 1500;
12
+ export const MAX_PENDING_TASKS = 8;
13
+
14
+ /**
15
+ * Build the richContext object to embed in pre-compact memory file.
16
+ * @param {Object} feature - Feature state object (tasks, taskList)
17
+ * @param {string} phase - Current derived phase string
18
+ * @param {string} decisionsContent - Raw text of decisions.md (may be empty string)
19
+ * @returns {Object} richContext
20
+ */
21
+ export function buildRichContext(feature, phase, decisionsContent) {
22
+ const taskList = Array.isArray(feature.taskList) ? feature.taskList : [];
23
+
24
+ const inProgress = taskList
25
+ .filter(t => t.status === 'in_progress')
26
+ .map(t => t.id);
27
+
28
+ const nextPending = taskList
29
+ .filter(t => t.status === 'pending')
30
+ .slice(0, MAX_PENDING_TASKS)
31
+ .map(t => ({ id: t.id, title: t.title }));
32
+
33
+ const decisionsSnippet = decisionsContent.length > DECISIONS_MAX_CHARS
34
+ ? decisionsContent.slice(0, DECISIONS_MAX_CHARS)
35
+ : decisionsContent;
36
+
37
+ return {
38
+ phase,
39
+ tasks: feature.tasks || {},
40
+ decisionsSnippet,
41
+ inProgress,
42
+ nextPending,
43
+ };
44
+ }
45
+
46
+ /**
47
+ * Build the context block string to inject in session-start after compact.
48
+ * @param {Object} memorySnapshot - Parsed pre-compact-*.json file contents
49
+ * @returns {string|null} Block string, or null if no richContext present
50
+ */
51
+ export function buildRestoreBlock(memorySnapshot) {
52
+ const rc = memorySnapshot?.richContext;
53
+ if (!rc) return null;
54
+
55
+ const { timestamp, activeFeature } = memorySnapshot;
56
+ const { phase, tasks, decisionsSnippet, inProgress, nextPending } = rc;
57
+
58
+ const done = tasks?.completed ?? '?';
59
+ const total = tasks?.total ?? '?';
60
+
61
+ const lines = [
62
+ `\uD83D\uDD04 POST-COMPACT RESTORE \u2014 context from pre-compact snapshot (${timestamp})`,
63
+ `Active feature: ${activeFeature} | Phase: ${phase} | Tasks: ${done}/${total}`,
64
+ ];
65
+
66
+ if (decisionsSnippet) {
67
+ lines.push('');
68
+ lines.push('Key decisions (at time of compact):');
69
+ lines.push(decisionsSnippet);
70
+ }
71
+
72
+ if (inProgress.length > 0) {
73
+ lines.push('');
74
+ lines.push(`In progress at compact time: [${inProgress.join(', ')}]`);
75
+ }
76
+
77
+ if (nextPending.length > 0) {
78
+ lines.push('');
79
+ lines.push('Next pending at compact time:');
80
+ for (const t of nextPending) {
81
+ lines.push(` [${t.id}] ${t.title}`);
82
+ }
83
+ }
84
+
85
+ return lines.join('\n');
86
+ }
87
+
88
+ /**
89
+ * Find the most recent pre-compact memory file from a list of filenames.
90
+ * Files named pre-compact-{ISO-timestamp-sanitized}.json — sort descending picks latest.
91
+ * @param {string[]} filenames - Array of filenames in the memory directory
92
+ * @returns {string|null} Latest filename, or null if none found
93
+ */
94
+ export function findLatestMemoryFile(filenames) {
95
+ const matches = filenames
96
+ .filter(f => f.startsWith('pre-compact-') && f.endsWith('.json'))
97
+ .sort()
98
+ .reverse();
99
+ return matches.length > 0 ? matches[0] : null;
100
+ }