@polymorphism-tech/morph-spec 4.8.16 → 4.8.17
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 +8 -0
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/hooks/README.md +4 -2
- package/framework/hooks/claude-code/notification/approval-reminder.js +2 -0
- package/framework/hooks/claude-code/post-tool-use/context-refresh.js +109 -0
- package/framework/hooks/claude-code/post-tool-use/dispatch.js +5 -0
- package/framework/hooks/claude-code/post-tool-use/handle-tool-failure.js +2 -0
- package/framework/hooks/claude-code/post-tool-use/validator-feedback.js +6 -1
- package/framework/hooks/claude-code/pre-compact/save-morph-context.js +2 -0
- package/framework/hooks/claude-code/pre-tool-use/enforce-phase-writes.js +7 -1
- package/framework/hooks/claude-code/pre-tool-use/protect-spec-files.js +3 -0
- package/framework/hooks/claude-code/session-start/inject-morph-context.js +19 -0
- package/framework/hooks/claude-code/statusline.py +61 -0
- package/framework/hooks/claude-code/stop/validate-completion.js +2 -0
- package/framework/hooks/claude-code/teammate-idle/teammate-idle.js +3 -0
- package/framework/hooks/claude-code/user-prompt/enrich-prompt.js +6 -1
- package/framework/hooks/claude-code/user-prompt/set-terminal-title.js +2 -0
- package/framework/hooks/shared/activity-logger.js +129 -0
- package/framework/skills/level-0-meta/morph-init/SKILL.md +6 -10
- package/framework/skills/level-1-workflows/phase-clarify/SKILL.md +1 -1
- package/framework/skills/level-1-workflows/phase-codebase-analysis/SKILL.md +1 -1
- package/framework/skills/level-1-workflows/phase-design/SKILL.md +1 -1
- package/framework/skills/level-1-workflows/phase-implement/SKILL.md +1 -1
- package/framework/skills/level-1-workflows/phase-setup/SKILL.md +1 -1
- package/framework/skills/level-1-workflows/phase-tasks/SKILL.md +1 -1
- package/framework/skills/level-1-workflows/phase-uiux/SKILL.md +1 -1
- package/package.json +1 -1
- package/src/commands/project/index.js +1 -0
- package/src/commands/project/monitor.js +295 -0
- package/src/lib/monitor/agent-resolver.js +144 -0
- package/src/lib/monitor/renderer.js +230 -0
- package/src/scripts/setup-infra.js +22 -1
- package/src/utils/hooks-installer.js +11 -5
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Monitor Renderer — TUI rendering utilities for `morph-spec monitor`
|
|
3
|
+
*
|
|
4
|
+
* Uses chalk (already a project dependency) + native box-drawing characters.
|
|
5
|
+
* No additional dependencies required.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import chalk from 'chalk';
|
|
9
|
+
|
|
10
|
+
// Box-drawing characters
|
|
11
|
+
const BOX = {
|
|
12
|
+
tl: '┌', tr: '┐', bl: '└', br: '┘',
|
|
13
|
+
h: '─', v: '│',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const TIER_ICONS = {
|
|
17
|
+
1: '🎯',
|
|
18
|
+
2: '⚙️ ',
|
|
19
|
+
3: '🔧',
|
|
20
|
+
4: '✅',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const EVENT_COLORS = {
|
|
24
|
+
SessionStart: 'cyan',
|
|
25
|
+
UserPromptSubmit: 'yellow',
|
|
26
|
+
PreToolUse: 'magenta',
|
|
27
|
+
PostToolUse: 'blue',
|
|
28
|
+
PostToolUseFailure: 'red',
|
|
29
|
+
Stop: 'gray',
|
|
30
|
+
PreCompact: 'gray',
|
|
31
|
+
Notification: 'white',
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Render a bordered box with a title and lines of content.
|
|
36
|
+
*
|
|
37
|
+
* @param {string[]} lines - Content lines
|
|
38
|
+
* @param {string} color - chalk color name ('green'|'blue'|'yellow'|'magenta'|'cyan'|'red'|'white')
|
|
39
|
+
* @param {string} [title] - Optional title shown in top border
|
|
40
|
+
* @param {number} [width] - Box width (default: 70)
|
|
41
|
+
* @returns {string}
|
|
42
|
+
*/
|
|
43
|
+
export function renderBox(lines, color, title = '', width = 70) {
|
|
44
|
+
const paint = chalk[color] || chalk.white;
|
|
45
|
+
const innerWidth = width - 2;
|
|
46
|
+
|
|
47
|
+
// Top border with optional title
|
|
48
|
+
let topBorder;
|
|
49
|
+
if (title) {
|
|
50
|
+
const titleText = ` ${title} `;
|
|
51
|
+
const remaining = innerWidth - titleText.length;
|
|
52
|
+
const left = Math.max(0, Math.floor(remaining / 2));
|
|
53
|
+
const right = Math.max(0, remaining - left);
|
|
54
|
+
topBorder = paint(BOX.tl + BOX.h.repeat(left) + titleText + BOX.h.repeat(right) + BOX.tr);
|
|
55
|
+
} else {
|
|
56
|
+
topBorder = paint(BOX.tl + BOX.h.repeat(innerWidth) + BOX.tr);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const bottomBorder = paint(BOX.bl + BOX.h.repeat(innerWidth) + BOX.br);
|
|
60
|
+
|
|
61
|
+
const contentLines = lines.map(line => {
|
|
62
|
+
const truncated = line.length > innerWidth - 2
|
|
63
|
+
? line.slice(0, innerWidth - 5) + '...'
|
|
64
|
+
: line;
|
|
65
|
+
const padded = truncated.padEnd(innerWidth - 2);
|
|
66
|
+
return paint(BOX.v) + ' ' + padded + paint(BOX.v);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
return [topBorder, ...contentLines, bottomBorder].join('\n');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Render the chronological hook event feed.
|
|
74
|
+
*
|
|
75
|
+
* @param {Array<{name: string, event: string, result: string, ts: string}>} hooks
|
|
76
|
+
* @param {number} [maxItems] - Max events to show (most recent first)
|
|
77
|
+
* @returns {string}
|
|
78
|
+
*/
|
|
79
|
+
export function renderFeed(hooks, maxItems = 12) {
|
|
80
|
+
if (!hooks || hooks.length === 0) {
|
|
81
|
+
return renderBox([' No hook events yet this session.'], 'blue', '🪝 HOOK EVENTS');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const recent = [...hooks].reverse().slice(0, maxItems);
|
|
85
|
+
const lines = recent.map(h => {
|
|
86
|
+
const color = EVENT_COLORS[h.event] || 'white';
|
|
87
|
+
const eventLabel = chalk[color] ? chalk[color](h.event) : h.event;
|
|
88
|
+
return `${h.ts} ${chalk.bold(h.name)} ${eventLabel} → ${h.result}`;
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
return renderBox(lines, 'blue', '🪝 HOOK EVENTS');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Render the agents view, grouped by tier.
|
|
96
|
+
*
|
|
97
|
+
* @param {Object} agentsByTier - { tier1: [...], tier2: [...], tier3: [...], tier4: [...] }
|
|
98
|
+
* @param {string} phase
|
|
99
|
+
* @returns {string}
|
|
100
|
+
*/
|
|
101
|
+
export function renderAgents(agentsByTier, phase) {
|
|
102
|
+
const lines = [`Phase: ${chalk.bold(phase || 'unknown')}`];
|
|
103
|
+
lines.push('');
|
|
104
|
+
|
|
105
|
+
for (const [tierKey, agents] of Object.entries(agentsByTier)) {
|
|
106
|
+
const tierNum = parseInt(tierKey.replace('tier', ''));
|
|
107
|
+
const icon = TIER_ICONS[tierNum] || ' ';
|
|
108
|
+
if (!agents || agents.length === 0) continue;
|
|
109
|
+
const names = agents.map(a => a.name).join(' · ');
|
|
110
|
+
lines.push(`T${tierNum} ${icon} ${names}`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (lines.length <= 2) {
|
|
114
|
+
lines.push(' No active agents for this phase.');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const total = Object.values(agentsByTier).flat().length;
|
|
118
|
+
return renderBox(lines, 'magenta', `🤖 AGENTS ATIVOS (${total} de 39)`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Render the skills view.
|
|
123
|
+
*
|
|
124
|
+
* @param {Array<{name: string, ts: string}>} skills - Skills invoked this session
|
|
125
|
+
* @param {string[]} [available] - Available skill names
|
|
126
|
+
* @returns {string}
|
|
127
|
+
*/
|
|
128
|
+
export function renderSkills(skills, available = []) {
|
|
129
|
+
const lines = [];
|
|
130
|
+
|
|
131
|
+
if (skills && skills.length > 0) {
|
|
132
|
+
lines.push('Invocadas nesta sessão:');
|
|
133
|
+
for (const s of [...skills].reverse()) {
|
|
134
|
+
lines.push(` ${s.ts} 🎯 ${s.name}`);
|
|
135
|
+
}
|
|
136
|
+
} else {
|
|
137
|
+
lines.push(' Nenhuma skill invocada nesta sessão.');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (available.length > 0) {
|
|
141
|
+
lines.push('');
|
|
142
|
+
lines.push(`Disponíveis: ${available.slice(0, 8).join(', ')}${available.length > 8 ? ` (+${available.length - 8})` : ''}`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return renderBox(lines, 'yellow', '🎯 SKILLS');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Render the rules view.
|
|
150
|
+
*
|
|
151
|
+
* @param {Array<{name: string, glob: string}>} rules
|
|
152
|
+
* @returns {string}
|
|
153
|
+
*/
|
|
154
|
+
export function renderRules(rules) {
|
|
155
|
+
if (!rules || rules.length === 0) {
|
|
156
|
+
return renderBox([' No rules found.'], 'cyan', '📏 RULES ATIVAS');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const lines = rules.map(r => ` ${chalk.bold(r.name).padEnd(35)} ${chalk.gray(r.glob || '*')}`);
|
|
160
|
+
return renderBox(lines, 'cyan', `📏 RULES ATIVAS (${rules.length})`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Render the overview (compact summary of all views).
|
|
165
|
+
*
|
|
166
|
+
* @param {Object} data - { feature, phase, agentsByTier, hooks, skills, rules }
|
|
167
|
+
* @returns {string}
|
|
168
|
+
*/
|
|
169
|
+
export function renderOverview(data) {
|
|
170
|
+
const { feature, phase, agentsByTier, hooks, skills, rules } = data;
|
|
171
|
+
const totalAgents = Object.values(agentsByTier || {}).flat().length;
|
|
172
|
+
const lines = [];
|
|
173
|
+
|
|
174
|
+
lines.push(`Feature: ${chalk.bold.green(feature || '(none)')} | Phase: ${chalk.bold(phase || 'unknown')}`);
|
|
175
|
+
lines.push('');
|
|
176
|
+
lines.push(`🤖 Agents ativos: ${totalAgents} de 39`);
|
|
177
|
+
|
|
178
|
+
// Tier breakdown
|
|
179
|
+
for (const [tierKey, agents] of Object.entries(agentsByTier || {})) {
|
|
180
|
+
if (!agents || agents.length === 0) continue;
|
|
181
|
+
const tierNum = parseInt(tierKey.replace('tier', ''));
|
|
182
|
+
lines.push(` T${tierNum}: ${agents.map(a => a.name).join(', ')}`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
lines.push('');
|
|
186
|
+
lines.push(`🪝 Hooks disparados: ${hooks?.length || 0}`);
|
|
187
|
+
if (hooks && hooks.length > 0) {
|
|
188
|
+
const last = hooks[hooks.length - 1];
|
|
189
|
+
lines.push(` Último: ${last.name} (${last.ts})`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
lines.push('');
|
|
193
|
+
lines.push(`🎯 Skills invocadas: ${skills?.length || 0}`);
|
|
194
|
+
lines.push(`📏 Rules ativas: ${rules?.length || 0}`);
|
|
195
|
+
|
|
196
|
+
return renderBox(lines, 'green', '● MORPH-SPEC OVERVIEW');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Render the bottom status bar.
|
|
201
|
+
*
|
|
202
|
+
* @param {string} mode - Current view mode
|
|
203
|
+
* @param {string[]} modes - All available modes
|
|
204
|
+
* @returns {string}
|
|
205
|
+
*/
|
|
206
|
+
export function renderStatusBar(mode, modes) {
|
|
207
|
+
const modeList = modes.map(m =>
|
|
208
|
+
m === mode ? chalk.bold.white.bgBlue(` ${m} `) : chalk.gray(` ${m} `)
|
|
209
|
+
).join(' ');
|
|
210
|
+
|
|
211
|
+
return '\n' + modeList + chalk.gray(' shift+tab: cycle | q: quit');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Render the feature header (top of screen).
|
|
216
|
+
*
|
|
217
|
+
* @param {string} feature
|
|
218
|
+
* @param {string} phase
|
|
219
|
+
* @param {Object} tasks - { completed, total }
|
|
220
|
+
* @returns {string}
|
|
221
|
+
*/
|
|
222
|
+
export function renderHeader(feature, phase, tasks) {
|
|
223
|
+
const taskInfo = tasks && tasks.total > 0
|
|
224
|
+
? ` [${phase} ${tasks.completed}/${tasks.total}]`
|
|
225
|
+
: ` [${phase}]`;
|
|
226
|
+
const title = feature
|
|
227
|
+
? chalk.bold.green(`● Watching ${feature}${taskInfo}`)
|
|
228
|
+
: chalk.bold.yellow('● morph-spec monitor — no active feature');
|
|
229
|
+
return title + '\n';
|
|
230
|
+
}
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
import { join, dirname } from 'path';
|
|
15
15
|
import { fileURLToPath } from 'url';
|
|
16
|
+
import { execSync } from 'child_process';
|
|
16
17
|
import {
|
|
17
18
|
copyDirectory,
|
|
18
19
|
copyFile,
|
|
@@ -29,19 +30,39 @@ import { installAgents, installDomainAgents } from '../utils/agents-installer.js
|
|
|
29
30
|
|
|
30
31
|
const FRAMEWORK_DIR = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'framework');
|
|
31
32
|
|
|
33
|
+
const REQUIRED_PLUGINS = [
|
|
34
|
+
'superpowers@claude-plugins-official',
|
|
35
|
+
'context7@claude-plugins-official',
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
async function installRequiredPlugins(log, exec) {
|
|
39
|
+
log('Step 0: Installing required Claude Code plugins...');
|
|
40
|
+
for (const plugin of REQUIRED_PLUGINS) {
|
|
41
|
+
try {
|
|
42
|
+
exec(`claude plugin install ${plugin}`, { stdio: 'inherit' });
|
|
43
|
+
log(` ✓ ${plugin}`);
|
|
44
|
+
} catch {
|
|
45
|
+
log(` ⚠ Failed to install ${plugin} — install manually: claude plugin install ${plugin}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
32
50
|
/**
|
|
33
51
|
* Installs MORPH-SPEC infrastructure into the target project directory.
|
|
34
52
|
* Headless — no prompts, no spinner (suppressed when MORPH_QUIET=1), no stack detection.
|
|
35
53
|
*
|
|
36
54
|
* @param {string} targetPath - Absolute path to the target project directory
|
|
37
55
|
*/
|
|
38
|
-
export async function setupInfra(targetPath) {
|
|
56
|
+
export async function setupInfra(targetPath, { _exec = execSync } = {}) {
|
|
39
57
|
const quiet = process.env.MORPH_QUIET === '1';
|
|
40
58
|
|
|
41
59
|
function log(msg) {
|
|
42
60
|
if (!quiet) process.stdout.write(msg + '\n');
|
|
43
61
|
}
|
|
44
62
|
|
|
63
|
+
// --- 0. Install required Claude Code plugins ---
|
|
64
|
+
await installRequiredPlugins(log, _exec);
|
|
65
|
+
|
|
45
66
|
// --- 1. Copy CLAUDE.md (backup existing non-MORPH CLAUDE.md) ---
|
|
46
67
|
log('Step 1: Copying CLAUDE.md...');
|
|
47
68
|
const claudeMdSrc = join(FRAMEWORK_DIR, 'CLAUDE.md');
|
|
@@ -15,7 +15,7 @@ import { homedir } from 'os';
|
|
|
15
15
|
import { execSync } from 'child_process';
|
|
16
16
|
|
|
17
17
|
/** Current hooks schema version — bump when hook definitions change */
|
|
18
|
-
const HOOKS_VERSION = '2.
|
|
18
|
+
const HOOKS_VERSION = '2.8.0';
|
|
19
19
|
|
|
20
20
|
/** Marker for old dispatch.js (v1) */
|
|
21
21
|
const OLD_DISPATCH_COMMAND = 'node framework/hooks/agent-teams/dispatch.js';
|
|
@@ -116,10 +116,16 @@ Otherwise respond: {"ok": true}`
|
|
|
116
116
|
{
|
|
117
117
|
event: 'PostToolUse',
|
|
118
118
|
matcher: 'Bash',
|
|
119
|
-
hooks: [
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
119
|
+
hooks: [
|
|
120
|
+
{
|
|
121
|
+
type: 'command',
|
|
122
|
+
command: 'node framework/hooks/claude-code/post-tool-use/dispatch.js'
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
type: 'command',
|
|
126
|
+
command: 'node framework/hooks/claude-code/post-tool-use/context-refresh.js'
|
|
127
|
+
}
|
|
128
|
+
]
|
|
123
129
|
},
|
|
124
130
|
|
|
125
131
|
// === Stop ===
|