@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.
- package/README.md +2 -2
- package/bin/morph-spec.js +22 -1
- package/bin/task-manager.cjs +120 -16
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/agents.json +1854 -1815
- package/framework/hooks/claude-code/pre-compact/save-morph-context.js +141 -23
- package/framework/hooks/claude-code/statusline.py +304 -280
- package/framework/hooks/claude-code/statusline.sh +6 -2
- package/framework/hooks/claude-code/stop/validate-completion.js +70 -23
- package/framework/hooks/dev/guard-version-numbers.js +1 -1
- package/framework/skills/level-0-meta/morph-init/SKILL.md +44 -6
- package/framework/skills/level-0-meta/tool-usage-guide/SKILL.md +67 -16
- package/framework/skills/level-1-workflows/phase-clarify/SKILL.md +1 -1
- package/framework/skills/level-1-workflows/phase-codebase-analysis/SKILL.md +77 -7
- package/framework/skills/level-1-workflows/phase-design/SKILL.md +114 -50
- package/framework/skills/level-1-workflows/phase-implement/SKILL.md +139 -1
- package/framework/skills/level-1-workflows/phase-setup/SKILL.md +29 -6
- package/framework/skills/level-1-workflows/phase-tasks/SKILL.md +4 -3
- package/framework/skills/level-1-workflows/phase-uiux/SKILL.md +1 -1
- package/framework/standards/STANDARDS.json +944 -933
- package/framework/standards/architecture/vertical-slice/vertical-slice.md +429 -0
- package/framework/templates/REGISTRY.json +1909 -1888
- package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +282 -0
- package/package.json +1 -1
- package/src/commands/agents/dispatch-agents.js +430 -0
- package/src/commands/agents/index.js +2 -1
- package/src/commands/project/doctor.js +137 -2
- package/src/commands/state/state.js +20 -4
- package/src/commands/templates/generate-contracts.js +445 -0
- package/src/commands/templates/index.js +1 -0
- 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:
|
|
4
|
+
* PreCompact Hook: Inject MORPH Context into Compact Summary
|
|
5
5
|
*
|
|
6
6
|
* Event: PreCompact
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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 {
|
|
17
|
-
|
|
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:
|
|
29
|
-
event:
|
|
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:
|
|
38
|
-
status:
|
|
65
|
+
phase: feature.phase || derivePhaseForFeature(name),
|
|
66
|
+
status: feature.status,
|
|
39
67
|
workflow: feature.workflow,
|
|
40
|
-
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
writeFileSync(snapshotPath, JSON.stringify(snapshot, null, 2), 'utf-8');
|
|
173
|
+
lines.push('');
|
|
174
|
+
lines.push('=== END MORPH CONTEXT ===');
|
|
56
175
|
|
|
57
|
-
|
|
176
|
+
injectContext(lines.join('\n'));
|
|
58
177
|
} catch {
|
|
59
|
-
// Fail-open
|
|
60
178
|
process.exit(0);
|
|
61
179
|
}
|