@polymorphism-tech/morph-spec 4.8.6 → 4.8.8

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 (33) hide show
  1. package/README.md +2 -2
  2. package/bin/morph-spec.js +22 -1
  3. package/bin/task-manager.cjs +120 -16
  4. package/claude-plugin.json +1 -1
  5. package/docs/CHEATSHEET.md +1 -1
  6. package/docs/QUICKSTART.md +1 -1
  7. package/framework/agents.json +1854 -1815
  8. package/framework/hooks/claude-code/pre-compact/save-morph-context.js +141 -23
  9. package/framework/hooks/claude-code/statusline.py +304 -280
  10. package/framework/hooks/claude-code/statusline.sh +6 -2
  11. package/framework/hooks/claude-code/stop/validate-completion.js +70 -23
  12. package/framework/hooks/dev/guard-version-numbers.js +1 -1
  13. package/framework/skills/level-0-meta/morph-init/SKILL.md +44 -6
  14. package/framework/skills/level-0-meta/tool-usage-guide/SKILL.md +67 -16
  15. package/framework/skills/level-1-workflows/phase-clarify/SKILL.md +1 -1
  16. package/framework/skills/level-1-workflows/phase-codebase-analysis/SKILL.md +77 -7
  17. package/framework/skills/level-1-workflows/phase-design/SKILL.md +114 -50
  18. package/framework/skills/level-1-workflows/phase-implement/SKILL.md +139 -1
  19. package/framework/skills/level-1-workflows/phase-setup/SKILL.md +29 -6
  20. package/framework/skills/level-1-workflows/phase-tasks/SKILL.md +4 -3
  21. package/framework/skills/level-1-workflows/phase-uiux/SKILL.md +1 -1
  22. package/framework/standards/STANDARDS.json +944 -933
  23. package/framework/standards/architecture/vertical-slice/vertical-slice.md +429 -0
  24. package/framework/templates/REGISTRY.json +1909 -1888
  25. package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +282 -0
  26. package/package.json +1 -1
  27. package/src/commands/agents/dispatch-agents.js +430 -0
  28. package/src/commands/agents/index.js +2 -1
  29. package/src/commands/project/doctor.js +137 -2
  30. package/src/commands/state/state.js +20 -4
  31. package/src/commands/templates/generate-contracts.js +445 -0
  32. package/src/commands/templates/index.js +1 -0
  33. package/src/lib/validators/validation-runner.js +19 -7
@@ -1,20 +1,47 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * PreCompact Hook: Save Context Before Compaction
4
+ * PreCompact Hook: Inject MORPH Context into Compact Summary
5
5
  *
6
6
  * Event: PreCompact
7
7
  *
8
- * Snapshots current morph-spec state to .morph/memory/ before context is lost
9
- * to compaction. The SessionStart hook can use this to restore awareness.
8
+ * Injects a rich morph-spec state snapshot as additionalContext so that
9
+ * Claude's compacted summary includes feature state, task progress, outputs,
10
+ * and next steps. Ensures the session resumes with full morph awareness
11
+ * without having to re-read all the spec files.
12
+ *
13
+ * Also saves snapshot to .morph/memory/ for SessionStart to reference.
10
14
  *
11
15
  * Fail-open: exits 0 on any error.
12
16
  */
13
17
 
14
- import { writeFileSync, mkdirSync, existsSync } from 'fs';
18
+ import { writeFileSync, mkdirSync, existsSync, readFileSync } from 'fs';
15
19
  import { join } from 'path';
16
- import { stateExists, loadState, getActiveFeature } from '../../shared/state-reader.js';
17
- import { pass } from '../../shared/hook-response.js';
20
+ import {
21
+ stateExists, loadState, getActiveFeature,
22
+ derivePhaseForFeature, getPendingGates, getOutputs,
23
+ } from '../../shared/state-reader.js';
24
+ import { injectContext, pass } from '../../shared/hook-response.js';
25
+
26
+ const DECISIONS_MAX_CHARS = 1500;
27
+ const MAX_PENDING_TASKS = 8;
28
+
29
+ const PHASE_POSITIONS = {
30
+ proposal: 1, setup: 1,
31
+ uiux: 2, design: 2,
32
+ clarify: 3,
33
+ tasks: 4,
34
+ implement: 5, sync: 5,
35
+ };
36
+ const PIPELINE_TOTAL = 5;
37
+
38
+ const NEXT_STEP_HINT = {
39
+ proposal: (n) => `Continue spec pipeline: /morph-proposal ${n}`,
40
+ design: (n) => `Approve design gate: morph-spec approve ${n} design`,
41
+ uiux: (n) => `Approve UI gate: morph-spec approve ${n} uiux`,
42
+ tasks: (n) => `Approve tasks gate: morph-spec approve ${n} tasks`,
43
+ implement: (n) => `Continue implementation: morph-spec task next ${n}`,
44
+ };
18
45
 
19
46
  try {
20
47
  if (!stateExists()) pass();
@@ -23,39 +50,130 @@ try {
23
50
  if (!state) pass();
24
51
 
25
52
  const active = getActiveFeature();
53
+ const cwd = process.cwd();
26
54
 
55
+ // ── Disk snapshot (for SessionStart restore) ──────────────────────────────
27
56
  const snapshot = {
28
- timestamp: new Date().toISOString(),
29
- event: 'pre-compact',
57
+ timestamp: new Date().toISOString(),
58
+ event: 'pre-compact',
30
59
  activeFeature: active ? active.name : null,
31
- features: {}
60
+ features: {},
32
61
  };
33
62
 
34
- // Save minimal feature summaries
35
63
  for (const [name, feature] of Object.entries(state.features || {})) {
36
64
  snapshot.features[name] = {
37
- phase: feature.phase,
38
- status: feature.status,
65
+ phase: feature.phase || derivePhaseForFeature(name),
66
+ status: feature.status,
39
67
  workflow: feature.workflow,
40
- tasks: feature.tasks,
68
+ tasks: feature.tasks,
41
69
  approvalGates: Object.fromEntries(
42
70
  Object.entries(feature.approvalGates || {}).map(([gate, data]) => [gate, data.approved])
43
- )
71
+ ),
44
72
  };
45
73
  }
46
74
 
47
- // Save to .morph/memory/
48
- const memoryDir = join(process.cwd(), '.morph', 'memory');
49
- if (!existsSync(memoryDir)) {
50
- mkdirSync(memoryDir, { recursive: true });
75
+ const memoryDir = join(cwd, '.morph', 'memory');
76
+ if (!existsSync(memoryDir)) mkdirSync(memoryDir, { recursive: true });
77
+ const ts = new Date().toISOString().replace(/[:.]/g, '-');
78
+ writeFileSync(join(memoryDir, `pre-compact-${ts}.json`), JSON.stringify(snapshot, null, 2), 'utf-8');
79
+
80
+ // ── Nothing more to inject if no active feature ───────────────────────────
81
+ if (!active) pass();
82
+
83
+ const { name, feature } = active;
84
+ const phase = derivePhaseForFeature(name);
85
+ const pos = PHASE_POSITIONS[phase];
86
+ const phaseLabel = pos ? `${phase} (${pos}/${PIPELINE_TOTAL})` : phase;
87
+
88
+ const lines = [
89
+ '=== MORPH-SPEC COMPACT CONTEXT ===',
90
+ `Preserve this section verbatim in the compact summary.`,
91
+ '',
92
+ `Feature : ${name}`,
93
+ `Phase : ${phaseLabel}`,
94
+ `Status : ${feature.status} | Workflow: ${feature.workflow || 'auto'}`,
95
+ ];
96
+
97
+ // Task progress
98
+ const tasks = feature.tasks || {};
99
+ const done = tasks.completed || 0;
100
+ const total = tasks.total || 0;
101
+ if (total > 0) {
102
+ const pct = Math.round(done / total * 100);
103
+ lines.push(`Tasks : ${done}/${total} completed (${pct}%)`);
104
+ }
105
+
106
+ // In-progress + next pending tasks from taskList
107
+ const taskList = Array.isArray(feature.taskList) ? feature.taskList : [];
108
+ const inProg = taskList.filter(t => t.status === 'in_progress');
109
+ const pending = taskList.filter(t => t.status === 'pending').slice(0, MAX_PENDING_TASKS);
110
+
111
+ if (inProg.length > 0) {
112
+ lines.push('');
113
+ lines.push('In progress:');
114
+ for (const t of inProg) lines.push(` [${t.id}] ${t.title}`);
115
+ }
116
+ if (pending.length > 0) {
117
+ lines.push('');
118
+ lines.push('Next pending tasks:');
119
+ for (const t of pending) lines.push(` [${t.id}] ${t.title}`);
120
+ if (taskList.filter(t => t.status === 'pending').length > MAX_PENDING_TASKS) {
121
+ lines.push(` ... (use morph-spec task next ${name} for full list)`);
122
+ }
123
+ }
124
+
125
+ // Created outputs
126
+ const outputs = getOutputs(name) || {};
127
+ const created = Object.entries(outputs).filter(([, o]) => o.created);
128
+ const missing = Object.entries(outputs).filter(([, o]) => !o.created);
129
+ if (created.length > 0) {
130
+ lines.push('');
131
+ lines.push('Created outputs:');
132
+ for (const [type, o] of created) lines.push(` ✓ ${type}: ${o.path}`);
133
+ }
134
+ if (missing.length > 0) {
135
+ lines.push('Pending outputs:');
136
+ for (const [type, o] of missing) lines.push(` ✗ ${type}: ${o.path}`);
137
+ }
138
+
139
+ // Active agents
140
+ const agents = feature.activeAgents || [];
141
+ if (agents.length > 0) {
142
+ lines.push('');
143
+ lines.push(`Active agents: ${agents.join(', ')}`);
144
+ }
145
+
146
+ // Pending approval gates
147
+ const pendingGates = getPendingGates(name);
148
+ if (pendingGates.length > 0) {
149
+ lines.push(`Pending approvals: ${pendingGates.join(', ')}`);
150
+ }
151
+
152
+ // Key decisions (truncated to stay within context budget)
153
+ const decisionsPath = join(cwd, `.morph/features/${name}/1-design/decisions.md`);
154
+ if (existsSync(decisionsPath)) {
155
+ try {
156
+ const raw = readFileSync(decisionsPath, 'utf-8');
157
+ const content = raw.length > DECISIONS_MAX_CHARS
158
+ ? raw.slice(0, DECISIONS_MAX_CHARS) + '\n[... truncated — full file at decisions.md]'
159
+ : raw;
160
+ lines.push('');
161
+ lines.push('Key decisions (decisions.md):');
162
+ lines.push(content);
163
+ } catch { /* non-blocking */ }
164
+ }
165
+
166
+ // Next step
167
+ const hintFn = NEXT_STEP_HINT[phase];
168
+ if (hintFn) {
169
+ lines.push('');
170
+ lines.push(`Next step: ${hintFn(name)}`);
51
171
  }
52
172
 
53
- const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
54
- const snapshotPath = join(memoryDir, `pre-compact-${timestamp}.json`);
55
- writeFileSync(snapshotPath, JSON.stringify(snapshot, null, 2), 'utf-8');
173
+ lines.push('');
174
+ lines.push('=== END MORPH CONTEXT ===');
56
175
 
57
- pass();
176
+ injectContext(lines.join('\n'));
58
177
  } catch {
59
- // Fail-open
60
178
  process.exit(0);
61
179
  }