@polymorphism-tech/morph-spec 4.8.19 → 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 (214) hide show
  1. package/CLAUDE.md +21 -0
  2. package/README.md +2 -2
  3. package/bin/morph-spec.js +44 -55
  4. package/bin/task-manager.js +133 -20
  5. package/bin/validate.js +67 -33
  6. package/claude-plugin.json +1 -1
  7. package/docs/CHEATSHEET.md +201 -203
  8. package/docs/QUICKSTART.md +2 -2
  9. package/framework/CLAUDE.md +99 -77
  10. package/framework/agents.json +734 -182
  11. package/framework/commands/commit.md +166 -0
  12. package/framework/commands/morph-apply.md +13 -2
  13. package/framework/commands/morph-archive.md +8 -2
  14. package/framework/commands/morph-infra.md +6 -0
  15. package/framework/commands/morph-preflight.md +6 -0
  16. package/framework/commands/morph-proposal.md +56 -7
  17. package/framework/commands/morph-status.md +6 -0
  18. package/framework/commands/morph-troubleshoot.md +6 -0
  19. package/framework/hooks/claude-code/notification/approval-reminder.js +3 -2
  20. package/framework/hooks/claude-code/post-tool-use/context-refresh.js +1 -1
  21. package/framework/hooks/claude-code/post-tool-use/dispatch.js +155 -32
  22. package/framework/hooks/claude-code/post-tool-use/skill-reminder.js +78 -0
  23. package/framework/hooks/claude-code/post-tool-use/validator-feedback.js +8 -17
  24. package/framework/hooks/claude-code/pre-compact/save-morph-context.js +16 -3
  25. package/framework/hooks/claude-code/pre-tool-use/enforce-phase-writes.js +4 -3
  26. package/framework/hooks/claude-code/pre-tool-use/protect-spec-files.js +4 -3
  27. package/framework/hooks/claude-code/pre-tool-use/task-tracking-guard.js +60 -0
  28. package/framework/hooks/claude-code/session-start/inject-morph-context.js +124 -2
  29. package/framework/hooks/claude-code/session-start/post-compact-restore.js +41 -0
  30. package/framework/hooks/claude-code/statusline.py +76 -30
  31. package/framework/hooks/claude-code/stop/validate-completion.js +2 -15
  32. package/framework/hooks/claude-code/user-prompt/enrich-prompt.js +23 -5
  33. package/framework/hooks/claude-code/user-prompt/set-terminal-title.js +14 -6
  34. package/framework/hooks/shared/activity-logger.js +0 -24
  35. package/framework/hooks/shared/compact-restore.js +100 -0
  36. package/framework/hooks/shared/dispatch-helpers.js +116 -0
  37. package/framework/hooks/shared/phase-utils.js +12 -5
  38. package/framework/hooks/shared/skill-reminder-helpers.js +79 -0
  39. package/framework/hooks/shared/stale-task-reset.js +57 -0
  40. package/framework/hooks/shared/state-reader.js +29 -5
  41. package/framework/hooks/shared/worktree-helpers.js +53 -0
  42. package/framework/phases.json +69 -14
  43. package/framework/rules/morph-workflow.md +88 -86
  44. package/framework/skills/level-0-meta/mcp-registry.json +86 -51
  45. package/framework/skills/level-0-meta/{brainstorming → morph-brainstorming}/SKILL.md +14 -17
  46. package/framework/skills/level-0-meta/morph-checklist/SKILL.md +2 -2
  47. package/framework/skills/level-0-meta/{code-review → morph-code-review}/SKILL.md +2 -2
  48. package/framework/skills/level-0-meta/{code-review-nextjs → morph-code-review-nextjs}/SKILL.md +163 -163
  49. package/framework/skills/level-0-meta/{frontend-review → morph-frontend-review}/SKILL.md +9 -9
  50. package/framework/skills/level-0-meta/morph-init/SKILL.md +77 -12
  51. package/framework/skills/level-0-meta/{post-implementation → morph-post-implementation}/SKILL.md +62 -15
  52. package/framework/skills/level-0-meta/morph-replicate/SKILL.md +5 -5
  53. package/framework/skills/level-0-meta/morph-replicate/references/blazor-html-mapping.md +1 -1
  54. package/framework/skills/level-0-meta/{simulation-checklist → morph-simulation-checklist}/SKILL.md +1 -1
  55. package/framework/skills/level-0-meta/{terminal-title → morph-terminal-title}/SKILL.md +2 -2
  56. package/framework/skills/level-0-meta/{tool-usage-guide → morph-tool-usage-guide}/SKILL.md +3 -4
  57. package/framework/skills/level-0-meta/{tool-usage-guide → morph-tool-usage-guide}/references/tools-per-phase.md +7 -7
  58. package/framework/skills/level-0-meta/{verification-before-completion → morph-verification-before-completion}/SKILL.md +2 -2
  59. package/framework/skills/level-0-meta/{verification-before-completion → morph-verification-before-completion}/scripts/check-phase-outputs.mjs +2 -2
  60. package/framework/skills/level-1-workflows/morph-phase-clarify/SKILL.md +238 -0
  61. package/framework/skills/level-1-workflows/{phase-codebase-analysis → morph-phase-codebase-analysis}/SKILL.md +3 -3
  62. package/framework/skills/level-1-workflows/morph-phase-design/SKILL.md +507 -0
  63. package/framework/skills/level-1-workflows/{phase-implement → morph-phase-implement}/SKILL.md +168 -27
  64. package/framework/skills/level-1-workflows/morph-phase-implement/prompts/code-quality-reviewer-prompt.md +50 -0
  65. package/framework/skills/level-1-workflows/morph-phase-implement/prompts/implementer-prompt.md +45 -0
  66. package/framework/skills/level-1-workflows/morph-phase-implement/prompts/spec-reviewer-prompt.md +47 -0
  67. package/framework/skills/level-1-workflows/morph-phase-plan/SKILL.md +254 -0
  68. package/framework/skills/level-1-workflows/{phase-setup → morph-phase-setup}/SKILL.md +50 -3
  69. package/framework/skills/level-1-workflows/{phase-tasks → morph-phase-tasks}/SKILL.md +48 -11
  70. package/framework/skills/level-1-workflows/{phase-tasks → morph-phase-tasks}/scripts/validate-tasks.mjs +3 -3
  71. package/framework/skills/level-1-workflows/{phase-uiux → morph-phase-uiux}/SKILL.md +46 -11
  72. package/framework/skills/level-1-workflows/morph-scope-escalation/SKILL.md +97 -0
  73. package/framework/standards/STANDARDS.json +640 -88
  74. package/framework/standards/infrastructure/vercel/vercel-database.md +106 -0
  75. package/framework/standards/integration/mcp/mcp-tools.md +25 -7
  76. package/framework/templates/REGISTRY.json +1825 -1909
  77. package/framework/templates/context/CONTEXT-FEATURE.md +276 -276
  78. package/framework/templates/docs/onboarding.md +3 -7
  79. package/package.json +2 -7
  80. package/src/commands/agents/dispatch-agents.js +104 -6
  81. package/src/commands/mcp/mcp-setup.js +39 -2
  82. package/src/commands/phase/phase-reset.js +74 -0
  83. package/src/commands/project/doctor.js +34 -51
  84. package/src/commands/project/init.js +1 -1
  85. package/src/commands/project/status.js +2 -2
  86. package/src/commands/project/update.js +381 -365
  87. package/src/commands/project/worktree.js +154 -0
  88. package/src/commands/scope/escalate.js +215 -0
  89. package/src/commands/state/advance-phase.js +132 -68
  90. package/src/commands/state/approve.js +2 -2
  91. package/src/commands/state/index.js +7 -8
  92. package/src/commands/state/phase-runner.js +1 -1
  93. package/src/commands/state/state.js +61 -6
  94. package/src/commands/task/expand.js +100 -0
  95. package/src/commands/tasks/task.js +78 -99
  96. package/src/commands/templates/template-render.js +93 -173
  97. package/src/commands/trust/trust.js +26 -21
  98. package/src/core/paths/output-schema.js +19 -3
  99. package/src/core/state/phase-state-machine.js +7 -4
  100. package/src/core/state/state-manager.js +32 -57
  101. package/src/core/workflows/workflow-detector.js +9 -87
  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 +336 -0
  108. package/src/lib/scope/impact-analyzer.js +106 -0
  109. package/src/lib/stack/stack-profile.js +88 -0
  110. package/src/lib/tasks/task-classifier.js +16 -0
  111. package/src/lib/tasks/task-parser.js +1 -1
  112. package/src/lib/tasks/test-runner.js +77 -0
  113. package/src/lib/trust/trust-manager.js +32 -144
  114. package/src/lib/validators/shared/emit-validator-dispatch.js +64 -0
  115. package/src/lib/validators/spec-validator.js +58 -4
  116. package/src/lib/validators/validation-runner.js +23 -11
  117. package/src/scripts/setup-infra.js +255 -224
  118. package/src/utils/agents-installer.js +34 -14
  119. package/src/utils/banner.js +1 -1
  120. package/src/utils/claude-settings-manager.js +1 -1
  121. package/src/utils/file-copier.js +1 -1
  122. package/src/utils/hooks-installer.js +272 -8
  123. package/framework/hooks/dev/check-sync-health.js +0 -117
  124. package/framework/hooks/dev/guard-version-numbers.js +0 -57
  125. package/framework/hooks/dev/sync-standards-registry.js +0 -60
  126. package/framework/hooks/dev/sync-template-registry.js +0 -60
  127. package/framework/hooks/dev/validate-skill-format.js +0 -70
  128. package/framework/hooks/dev/validate-standard-format.js +0 -73
  129. package/framework/skills/level-1-workflows/phase-clarify/SKILL.md +0 -190
  130. package/framework/skills/level-1-workflows/phase-design/SKILL.md +0 -366
  131. package/framework/templates/meta-prompts/hops/hop-retry.md +0 -78
  132. package/framework/templates/meta-prompts/hops/hop-validation.md +0 -97
  133. package/framework/templates/meta-prompts/hops/hop-wrapper.md +0 -36
  134. package/framework/workflows/configs/design-impl.json +0 -49
  135. package/framework/workflows/configs/express.json +0 -45
  136. package/framework/workflows/configs/fast-track.json +0 -42
  137. package/framework/workflows/configs/full-morph.json +0 -79
  138. package/framework/workflows/configs/fusion.json +0 -39
  139. package/framework/workflows/configs/long-running.json +0 -33
  140. package/framework/workflows/configs/spec-only.json +0 -43
  141. package/framework/workflows/configs/ui-refresh.json +0 -49
  142. package/framework/workflows/configs/zero-touch.json +0 -82
  143. package/src/commands/project/index.js +0 -8
  144. package/src/commands/project/monitor.js +0 -295
  145. package/src/commands/project/tutorial.js +0 -115
  146. package/src/commands/state/validate-phase.js +0 -238
  147. package/src/commands/templates/generate-contracts.js +0 -445
  148. package/src/core/index.js +0 -10
  149. package/src/core/orchestrator.js +0 -171
  150. package/src/core/registry/command-registry.js +0 -28
  151. package/src/core/registry/index.js +0 -8
  152. package/src/core/registry/validator-registry.js +0 -204
  153. package/src/core/state/index.js +0 -8
  154. package/src/core/templates/index.js +0 -9
  155. package/src/core/templates/template-data-sources.js +0 -325
  156. package/src/core/templates/template-validator.js +0 -296
  157. package/src/core/workflows/index.js +0 -7
  158. package/src/generator/config-generator.js +0 -206
  159. package/src/generator/templates/config.json.template +0 -40
  160. package/src/generator/templates/project.md.template +0 -67
  161. package/src/lib/agents/micro-agent-factory.js +0 -161
  162. package/src/lib/analysis/complexity-analyzer.js +0 -441
  163. package/src/lib/analysis/index.js +0 -7
  164. package/src/lib/analytics/analytics-engine.js +0 -345
  165. package/src/lib/checkpoints/checkpoint-hooks.js +0 -298
  166. package/src/lib/checkpoints/index.js +0 -7
  167. package/src/lib/context/context-bundler.js +0 -241
  168. package/src/lib/context/context-optimizer.js +0 -212
  169. package/src/lib/context/context-tracker.js +0 -273
  170. package/src/lib/context/core-four-tracker.js +0 -201
  171. package/src/lib/context/mcp-optimizer.js +0 -200
  172. package/src/lib/detectors/config-detector.js +0 -223
  173. package/src/lib/detectors/standards-generator.js +0 -335
  174. package/src/lib/detectors/structure-detector.js +0 -275
  175. package/src/lib/execution/fusion-executor.js +0 -304
  176. package/src/lib/execution/parallel-executor.js +0 -270
  177. package/src/lib/hooks/stop-hook-executor.js +0 -286
  178. package/src/lib/hops/hop-composer.js +0 -221
  179. package/src/lib/monitor/agent-resolver.js +0 -144
  180. package/src/lib/monitor/renderer.js +0 -230
  181. package/src/lib/orchestration/index.js +0 -7
  182. package/src/lib/orchestration/team-orchestrator.js +0 -404
  183. package/src/lib/phase-chain/eligibility-checker.js +0 -243
  184. package/src/lib/threads/thread-coordinator.js +0 -238
  185. package/src/lib/threads/thread-manager.js +0 -317
  186. package/src/lib/tracking/artifact-trail.js +0 -202
  187. package/src/sanitizer/context-sanitizer.js +0 -221
  188. package/src/sanitizer/patterns.js +0 -163
  189. package/src/scanner/project-scanner.js +0 -242
  190. package/src/ui/diff-display.js +0 -91
  191. package/src/ui/interactive-wizard.js +0 -96
  192. package/src/ui/user-review.js +0 -211
  193. package/src/ui/wizard-questions.js +0 -188
  194. package/src/utils/color-utils.js +0 -70
  195. package/src/utils/process-handler.js +0 -97
  196. package/src/writer/file-writer.js +0 -86
  197. /package/framework/skills/level-0-meta/{brainstorming → morph-brainstorming}/references/proposal-example.md +0 -0
  198. /package/framework/skills/level-0-meta/{code-review → morph-code-review}/references/review-example.md +0 -0
  199. /package/framework/skills/level-0-meta/{code-review → morph-code-review}/references/review-guidelines.md +0 -0
  200. /package/framework/skills/level-0-meta/{code-review → morph-code-review}/scripts/scan-csharp.mjs +0 -0
  201. /package/framework/skills/level-0-meta/{code-review-nextjs → morph-code-review-nextjs}/references/review-example-nextjs.md +0 -0
  202. /package/framework/skills/level-0-meta/{code-review-nextjs → morph-code-review-nextjs}/scripts/scan-nextjs.mjs +0 -0
  203. /package/framework/skills/level-0-meta/{frontend-review → morph-frontend-review}/scripts/scan-accessibility.mjs +0 -0
  204. /package/framework/skills/level-0-meta/{post-implementation → morph-post-implementation}/scripts/detect-dev-server.mjs +0 -0
  205. /package/framework/skills/level-0-meta/{post-implementation → morph-post-implementation}/scripts/detect-stack.mjs +0 -0
  206. /package/framework/skills/level-0-meta/{terminal-title → morph-terminal-title}/scripts/set_title.sh +0 -0
  207. /package/framework/skills/level-1-workflows/{phase-clarify → morph-phase-clarify}/references/clarifications-example.md +0 -0
  208. /package/framework/skills/level-1-workflows/{phase-design → morph-phase-design}/references/architecture-analysis-guide.md +0 -0
  209. /package/framework/skills/level-1-workflows/{phase-design → morph-phase-design}/references/spec-authoring-guide.md +0 -0
  210. /package/framework/skills/level-1-workflows/{phase-design → morph-phase-design}/references/spec-example.md +0 -0
  211. /package/framework/skills/level-1-workflows/{phase-implement → morph-phase-implement}/references/recap-example.md +0 -0
  212. /package/framework/skills/level-1-workflows/{phase-implement → morph-phase-implement}/references/vsa-implementation-guide.md +0 -0
  213. /package/framework/skills/level-1-workflows/{phase-tasks → morph-phase-tasks}/references/task-planning-patterns.md +0 -0
  214. /package/framework/skills/level-1-workflows/{phase-tasks → morph-phase-tasks}/references/tasks-example.md +0 -0
@@ -15,8 +15,11 @@ import { loadState, getActiveFeature, getPendingGates, getMissingOutputs, derive
15
15
  import { stateExists } from '../../shared/state-reader.js';
16
16
  import { injectContext, pass } from '../../shared/hook-response.js';
17
17
  import { resetActivity, logHookActivity } from '../../shared/activity-logger.js';
18
- import { readFileSync, existsSync } from 'fs';
18
+ import { readFileSync, existsSync, writeFileSync, renameSync, unlinkSync } from 'fs';
19
19
  import { join } from 'path';
20
+ import { resetStaleTasks } from '../../shared/stale-task-reset.js';
21
+ import { execSync as _execSync } from 'child_process';
22
+ import { parseWorktreeResult, buildWorktreeContextLine } from '../../shared/worktree-helpers.js';
20
23
 
21
24
  const DEFAULT_SPEC_MAX_CHARS = 3000;
22
25
 
@@ -43,6 +46,26 @@ function getProjectConfig() {
43
46
 
44
47
  const SPEC_MAX_CHARS = getSpecMaxChars();
45
48
 
49
+ /**
50
+ * Atomic state save for hook context.
51
+ * Same approach as state-manager.js saveState — write to tmp, then rename.
52
+ * Fail-open: any error is silently ignored.
53
+ * @param {Object} state - Full state object
54
+ * @param {string} cwd - Project root path
55
+ */
56
+ function saveStateSync(state, cwd) {
57
+ const statePath = join(cwd, '.morph/state.json');
58
+ const tmpPath = `${statePath}.tmp.hook.${process.pid}`;
59
+ try {
60
+ state.metadata = state.metadata || {};
61
+ state.metadata.lastUpdated = new Date().toISOString();
62
+ writeFileSync(tmpPath, JSON.stringify(state, null, 2), 'utf8');
63
+ renameSync(tmpPath, statePath);
64
+ } catch {
65
+ try { unlinkSync(tmpPath); } catch { /* ignore cleanup errors */ }
66
+ }
67
+ }
68
+
46
69
  try {
47
70
  if (!stateExists()) pass();
48
71
 
@@ -57,7 +80,19 @@ try {
57
80
  resetActivity(new Date().toISOString(), activeFeatureName, activePhase);
58
81
  logHookActivity('inject-morph-context', 'SessionStart', 'ok');
59
82
 
83
+ // ── Stale task cleanup ──────────────────────────────────────────────────────
84
+ // Reset any in_progress tasks older than 1 hour — they are orphans from an
85
+ // interrupted session and are no longer being actively implemented.
86
+ const staleResetLog = resetStaleTasks(state);
87
+ if (staleResetLog.length > 0) {
88
+ saveStateSync(state, process.cwd());
89
+ }
90
+ // ────────────────────────────────────────────────────────────────────────────
91
+
60
92
  const lines = ['MORPH-SPEC Status:'];
93
+ if (staleResetLog.length > 0) {
94
+ lines.push(`⚠️ ${staleResetLog.length} task(s) auto-reset in_progress → pending (orphaned from previous session): ${staleResetLog.join(', ')}`);
95
+ }
61
96
 
62
97
  if (active) {
63
98
  const { name, feature } = active;
@@ -129,6 +164,40 @@ try {
129
164
  // Non-blocking: skip spec injection on read error
130
165
  }
131
166
  }
167
+
168
+ // ── Worktree setup ──────────────────────────────────────────────────────────
169
+ // For the active feature, create or detect an existing git worktree.
170
+ // Trigger: uses getActiveFeature() (status-based) rather than checking
171
+ // activeAgents, because status is the canonical signal already used by the
172
+ // rest of this hook. activeAgents is populated later in the spec pipeline,
173
+ // so a status-based gate ensures the worktree is always created before work
174
+ // begins — even for features still in early phases.
175
+ // Fail-open: any error is silently ignored.
176
+ try {
177
+ let worktreeStdout = '';
178
+ try {
179
+ worktreeStdout = _execSync(`npx morph-spec worktree setup ${active.name}`, {
180
+ cwd: process.cwd(),
181
+ stdio: 'pipe',
182
+ timeout: 10000
183
+ }).toString();
184
+ } catch (execErr) {
185
+ // exit code 2 = already exists — stdout still has the JSON result
186
+ worktreeStdout = execErr.stdout?.toString() || '';
187
+ }
188
+ const worktreeResult = parseWorktreeResult(worktreeStdout);
189
+ if (worktreeResult) {
190
+ worktreeResult.feature = active.name; // enrich for context builder
191
+ const contextLine = buildWorktreeContextLine(worktreeResult);
192
+ if (contextLine) {
193
+ lines.push('');
194
+ lines.push(contextLine);
195
+ }
196
+ }
197
+ } catch {
198
+ // Fail-open — worktree setup must never block the session
199
+ }
200
+ // ────────────────────────────────────────────────────────────────────────────
132
201
  } else {
133
202
  // Show summary of all features
134
203
  const featureNames = Object.keys(state.features);
@@ -140,6 +209,59 @@ try {
140
209
  }
141
210
  }
142
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
+
143
265
  // Remind about key commands
144
266
  lines.push('');
145
267
  lines.push('Key commands: morph-spec status <feature> | morph-spec phase advance <feature> | morph-spec approve <feature> <gate>');
@@ -151,7 +273,7 @@ try {
151
273
  lines.push('');
152
274
  lines.push('── MORPH SYSTEM MAP ─────────────────────────────────────────────');
153
275
  lines.push(`🪝 Hooks: 10 registrados | 📏 Rules: em .claude/rules/ | 🎯 Skills: em .claude/skills/`);
154
- lines.push(` Execute \`morph-spec monitor\` em terminal separado para live view`);
276
+ lines.push(` Use \`morph-spec doctor\` para health check | \`morph-spec status ${name}\` para dashboard`);
155
277
  lines.push('─────────────────────────────────────────────────────────────────');
156
278
  }
157
279
 
@@ -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
+ }
@@ -151,7 +151,10 @@ def get_session_feature_names(features_dict, entries):
151
151
 
152
152
 
153
153
  def get_all_active_features(cwd, entries):
154
- """Return in_progress features that are active in the current session."""
154
+ """Return in_progress features that are active in the current session.
155
+
156
+ Caller is responsible for only calling this when inside a secondary worktree.
157
+ """
155
158
  state_path = Path(cwd) / '.morph' / 'state.json'
156
159
  if not state_path.exists():
157
160
  return []
@@ -162,11 +165,6 @@ def get_all_active_features(cwd, entries):
162
165
  # Filter to features belonging to this session (mentioned in transcript)
163
166
  session_names = get_session_feature_names(features, entries)
164
167
 
165
- # Auto-detect: if only one feature is in_progress, show it regardless of transcript
166
- in_progress = [n for n, f in features.items() if f.get('status') == 'in_progress']
167
- if len(in_progress) == 1:
168
- session_names.add(in_progress[0])
169
-
170
168
  if not session_names:
171
169
  return []
172
170
 
@@ -310,8 +308,14 @@ def get_git_info(cwd):
310
308
  return ""
311
309
 
312
310
 
313
- def get_worktree_info(cwd):
314
- """Detect if running in a git worktree (not the main worktree)."""
311
+ def get_worktree_data(cwd):
312
+ """Detect worktree status for cwd.
313
+
314
+ Returns (is_secondary: bool, display_str: str).
315
+ is_secondary is True only when cwd is a non-primary git worktree.
316
+ display_str is the formatted label (non-empty only when is_secondary).
317
+ Called once in main() to avoid duplicate git subprocess.
318
+ """
315
319
  try:
316
320
  out = _run_git(['worktree', 'list', '--porcelain'], cwd)
317
321
  entries, current = [], {}
@@ -329,10 +333,45 @@ def get_worktree_info(cwd):
329
333
  for entry in entries[1:]:
330
334
  if str(Path(entry.get('path', '')).resolve()) == cwd_r:
331
335
  branch = entry.get('branch', '').replace('refs/heads/', '')
332
- return f"{MAGENTA}worktree:{branch}{R}"
336
+ return True, f"{MAGENTA}worktree:{branch}{R}"
333
337
  except Exception:
334
338
  pass
335
- return ""
339
+ return False, ""
340
+
341
+
342
+ def get_recent_tool_calls(entries):
343
+ """Scan the last 50 transcript entries for the most recent Skill and Agent tool calls.
344
+
345
+ Returns (last_skill_name, last_agent_name) — either may be None.
346
+ Looks at assistant messages containing tool_use blocks.
347
+ """
348
+ last_skill = None
349
+ last_agent = None
350
+ recent = entries[-50:] if len(entries) > 50 else entries
351
+ for entry in reversed(recent):
352
+ if entry.get('isSidechain') or entry.get('isApiErrorMessage'):
353
+ continue
354
+ msg = entry.get('message') or {}
355
+ content = msg.get('content') or []
356
+ if not isinstance(content, list):
357
+ continue
358
+ # Within each entry, scan content from last to first to get most-recent call
359
+ for item in reversed(content):
360
+ if not isinstance(item, dict) or item.get('type') != 'tool_use':
361
+ continue
362
+ tool = item.get('name', '')
363
+ inp = item.get('input') or {}
364
+ if tool == 'Skill' and last_skill is None:
365
+ last_skill = inp.get('skill') or ''
366
+ elif tool == 'Agent' and last_agent is None:
367
+ agent_name = inp.get('subagent_type') or ''
368
+ if not agent_name:
369
+ desc = inp.get('description') or inp.get('prompt') or ''
370
+ agent_name = (desc[:22] + '…') if len(desc) > 25 else desc
371
+ last_agent = agent_name
372
+ if last_skill is not None and last_agent is not None:
373
+ break
374
+ return last_skill or None, last_agent or None
336
375
 
337
376
 
338
377
  # ── Transcript / JSONL helpers ────────────────────────────────────────────────
@@ -493,12 +532,18 @@ def main():
493
532
  cwd = data.get('cwd', os.getcwd())
494
533
  transcript_path = data.get('transcript_path')
495
534
 
496
- # Read JSONL transcript once — shared by session clock, block timer,
497
- # token metrics, and session name.
535
+ # Read JSONL transcript once — shared by session clock, token metrics,
536
+ # session name, and skill/agent detection.
498
537
  entries = read_transcript_jsonl(transcript_path) if transcript_path else []
499
538
 
539
+ # ── Worktree detection (single git call) ─────────────────────────────────
540
+ # Feature lines are only shown when inside a secondary git worktree.
541
+ # This prevents stale feature names showing up during unrelated work
542
+ # in the main worktree.
543
+ is_worktree, wt_display = get_worktree_data(cwd)
544
+
500
545
  # ── MORPH feature lines (one line per active feature) ────────────────────
501
- features = get_all_active_features(cwd, entries)
546
+ features = get_all_active_features(cwd, entries) if is_worktree else []
502
547
  for feat in features:
503
548
  # Feature name with visual prefix
504
549
  parts = [f"{CYAN}{BOLD}► {feat['name']}{R}"]
@@ -536,19 +581,14 @@ def main():
536
581
 
537
582
  print(' | '.join(parts))
538
583
 
539
- # ── Activity info line (hooks + last event) ──────────────────────────────
540
- if features: # only show when a feature is active
584
+ # ── Activity info line (hooks; only shown when a feature is active) ───────
585
+ if features:
541
586
  activity = get_activity_info(cwd)
542
- if activity and (activity['hook_count'] > 0 or activity['skill_count'] > 0):
543
- act_parts = []
544
- if activity['hook_count'] > 0:
545
- hook_label = activity['last_hook'] or '?'
546
- age_str = f"({activity['last_hook_age']})" if activity['last_hook_age'] else ''
547
- act_parts.append(f"{BLUE}🪝 {hook_label} {age_str}{R}".strip())
548
- if activity['skill_count'] > 0:
549
- act_parts.append(f"{YELLOW}🎯 {activity['skill_count']} skill(s){R}")
550
- if act_parts:
551
- print(f" {GRAY}└{R} " + f" {GRAY}|{R} ".join(act_parts))
587
+ if activity and activity['hook_count'] > 0:
588
+ hook_label = activity['last_hook'] or '?'
589
+ age_str = f"({activity['last_hook_age']})" if activity['last_hook_age'] else ''
590
+ hook_str = f"{BLUE}🪝 {hook_label} {age_str}{R}".strip()
591
+ print(f" {GRAY}└{R} {hook_str}")
552
592
 
553
593
  # ── Session info line (always shown) ─────────────────────────────────────
554
594
  parts2 = []
@@ -580,6 +620,14 @@ def main():
580
620
  }
581
621
  parts2.append(f"{YELLOW}{_perm_labels.get(perm, perm)}{R}")
582
622
 
623
+ # Last skill and agent invoked (parsed from transcript tool_use blocks)
624
+ if entries:
625
+ last_skill, last_agent = get_recent_tool_calls(entries)
626
+ if last_skill:
627
+ parts2.append(f"{YELLOW}🎯 {last_skill}{R}")
628
+ if last_agent:
629
+ parts2.append(f"{MAGENTA}⚡ {last_agent}{R}")
630
+
583
631
  # Session clock (elapsed time since session start, survives transcript transitions)
584
632
  session_start = get_session_start(cwd, transcript_path, entries)
585
633
  duration = get_session_duration(session_start)
@@ -611,16 +659,14 @@ def main():
611
659
  line += f" ({toks})"
612
660
  parts2.append(line + suffix)
613
661
 
614
-
615
662
  # Git info (branch + diff stats)
616
663
  git = get_git_info(cwd)
617
664
  if git:
618
665
  parts2.append(git)
619
666
 
620
- # Worktree info
621
- wt = get_worktree_info(cwd)
622
- if wt:
623
- parts2.append(wt)
667
+ # Worktree label (already computed above — reuse result)
668
+ if wt_display:
669
+ parts2.append(wt_display)
624
670
 
625
671
  if parts2:
626
672
  print(' | '.join(parts2))
@@ -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
  );
@@ -40,21 +40,29 @@ try {
40
40
  const prefix = process.env.CLAUDE_TITLE_PREFIX ? `${process.env.CLAUDE_TITLE_PREFIX} ` : '';
41
41
  const finalTitle = `${prefix}${title}`;
42
42
 
43
- // Save to ~/.claude/terminal_title (for shell hook integration)
43
+ // Save to session-specific file (MORPH_TERMINAL_TITLE_FILE) or fallback to global
44
+ // MORPH_TERMINAL_TITLE_FILE is set by the shell wrapper (Invoke-Claude / claude())
45
+ // per-session so multiple terminal windows don't interfere with each other.
44
46
  try {
47
+ const titleFilePath = process.env.MORPH_TERMINAL_TITLE_FILE
48
+ || join(homedir(), '.claude', 'terminal_title');
45
49
  const claudeDir = join(homedir(), '.claude');
46
50
  await mkdir(claudeDir, { recursive: true });
47
- await writeFile(join(claudeDir, 'terminal_title'), finalTitle, 'utf-8');
51
+ await writeFile(titleFilePath, finalTitle, 'utf-8');
48
52
  } catch { /* non-critical */ }
49
53
 
50
- // Write ANSI escape to /dev/tty (bypasses stdout capture by Claude Code)
54
+ // Write ANSI escape to terminal device (bypasses stdout capture by Claude Code)
55
+ // Windows uses \\.\CONOUT$ as the equivalent of /dev/tty
56
+ let wrote = false;
51
57
  try {
52
- const tty = openSync('/dev/tty', 'w');
58
+ const devicePath = process.platform === 'win32' ? '\\\\.\\CONOUT$' : '/dev/tty';
59
+ const tty = openSync(devicePath, 'w');
53
60
  writeSync(tty, `\x1b]0;${finalTitle}\x07`);
54
61
  closeSync(tty);
55
- } catch { /* terminal may not support /dev/tty */ }
62
+ wrote = true;
63
+ } catch { /* terminal may not support direct device access */ }
56
64
 
57
- logHookActivity('set-terminal-title', 'UserPromptSubmit', 'ok');
65
+ logHookActivity('set-terminal-title', 'UserPromptSubmit', wrote ? 'ok' : 'failed');
58
66
  } catch { /* fail-open */ }
59
67
 
60
68
  process.exit(0);
@@ -64,30 +64,6 @@ export function logHookActivity(name, event, result, projectPath) {
64
64
  }
65
65
  }
66
66
 
67
- /**
68
- * Log a skill invocation to the session activity log.
69
- *
70
- * @param {string} name - Skill name (e.g. 'brainstorming')
71
- * @param {string} [projectPath]
72
- */
73
- export function logSkillActivity(name, projectPath) {
74
- try {
75
- const activityPath = getActivityPath(projectPath);
76
- ensureLogsDir(projectPath);
77
-
78
- const now = new Date();
79
- const ts = now.toTimeString().slice(0, 8);
80
-
81
- const data = readRaw(activityPath) || { sessionId: '', feature: '', phase: '', hooks: [], skills: [] };
82
-
83
- data.skills = data.skills || [];
84
- data.skills.push({ name, ts });
85
-
86
- writeFileSync(activityPath, JSON.stringify(data, null, 2), 'utf-8');
87
- } catch {
88
- // Fail-silent
89
- }
90
- }
91
67
 
92
68
  /**
93
69
  * Read the current activity log.
@@ -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
+ }