@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,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Activity Logger — Session-scoped hook event log
|
|
3
|
+
*
|
|
4
|
+
* Writes hook activity to .morph/logs/activity.json for live consumption
|
|
5
|
+
* by `morph-spec monitor`.
|
|
6
|
+
*
|
|
7
|
+
* Design constraints:
|
|
8
|
+
* - Synchronous only (hooks run in process, no async overhead)
|
|
9
|
+
* - Fail-silent (never throws — hooks must not be blocked by logger)
|
|
10
|
+
* - ≤5ms per call (no retries, no network)
|
|
11
|
+
* - Windows-safe paths via path.join
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
|
|
15
|
+
import { join } from 'path';
|
|
16
|
+
|
|
17
|
+
const LOGS_DIR = '.morph/logs';
|
|
18
|
+
const ACTIVITY_FILE = join(LOGS_DIR, 'activity.json');
|
|
19
|
+
|
|
20
|
+
function getActivityPath(projectPath) {
|
|
21
|
+
return join(projectPath || process.cwd(), ACTIVITY_FILE);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function ensureLogsDir(projectPath) {
|
|
25
|
+
const dir = join(projectPath || process.cwd(), LOGS_DIR);
|
|
26
|
+
if (!existsSync(dir)) {
|
|
27
|
+
mkdirSync(dir, { recursive: true });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function readRaw(activityPath) {
|
|
32
|
+
try {
|
|
33
|
+
if (!existsSync(activityPath)) return null;
|
|
34
|
+
return JSON.parse(readFileSync(activityPath, 'utf-8'));
|
|
35
|
+
} catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Log a hook firing event to the session activity log.
|
|
42
|
+
*
|
|
43
|
+
* @param {string} name - Hook name (e.g. 'dispatch.js', 'enrich-prompt')
|
|
44
|
+
* @param {string} event - Claude Code event (e.g. 'PostToolUse', 'UserPromptSubmit')
|
|
45
|
+
* @param {string} result - Outcome summary (e.g. 'ok', 'blocked', 'checkpoint_saved')
|
|
46
|
+
* @param {string} [projectPath] - Project root (defaults to cwd)
|
|
47
|
+
*/
|
|
48
|
+
export function logHookActivity(name, event, result, projectPath) {
|
|
49
|
+
try {
|
|
50
|
+
const activityPath = getActivityPath(projectPath);
|
|
51
|
+
ensureLogsDir(projectPath);
|
|
52
|
+
|
|
53
|
+
const now = new Date();
|
|
54
|
+
const ts = now.toTimeString().slice(0, 8); // HH:MM:SS
|
|
55
|
+
|
|
56
|
+
const data = readRaw(activityPath) || { sessionId: '', feature: '', phase: '', hooks: [], skills: [] };
|
|
57
|
+
|
|
58
|
+
data.hooks = data.hooks || [];
|
|
59
|
+
data.hooks.push({ name, event, result, ts });
|
|
60
|
+
|
|
61
|
+
writeFileSync(activityPath, JSON.stringify(data, null, 2), 'utf-8');
|
|
62
|
+
} catch {
|
|
63
|
+
// Fail-silent — never block hooks
|
|
64
|
+
}
|
|
65
|
+
}
|
|
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
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Read the current activity log.
|
|
94
|
+
*
|
|
95
|
+
* @param {string} [projectPath]
|
|
96
|
+
* @returns {{ sessionId: string, feature: string, phase: string, hooks: Array, skills: Array }|null}
|
|
97
|
+
*/
|
|
98
|
+
export function readActivity(projectPath) {
|
|
99
|
+
try {
|
|
100
|
+
return readRaw(getActivityPath(projectPath));
|
|
101
|
+
} catch {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Reset the activity log for a new session.
|
|
108
|
+
* Called from SessionStart hook to clear stale data.
|
|
109
|
+
*
|
|
110
|
+
* @param {string} sessionId - ISO timestamp of new session
|
|
111
|
+
* @param {string} feature - Active feature name (empty string if none)
|
|
112
|
+
* @param {string} phase - Current phase (empty string if none)
|
|
113
|
+
* @param {string} [projectPath]
|
|
114
|
+
*/
|
|
115
|
+
export function resetActivity(sessionId, feature, phase, projectPath) {
|
|
116
|
+
try {
|
|
117
|
+
ensureLogsDir(projectPath);
|
|
118
|
+
const data = {
|
|
119
|
+
sessionId: sessionId || new Date().toISOString(),
|
|
120
|
+
feature: feature || '',
|
|
121
|
+
phase: phase || '',
|
|
122
|
+
hooks: [],
|
|
123
|
+
skills: []
|
|
124
|
+
};
|
|
125
|
+
writeFileSync(getActivityPath(projectPath), JSON.stringify(data, null, 2), 'utf-8');
|
|
126
|
+
} catch {
|
|
127
|
+
// Fail-silent
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -51,8 +51,8 @@ Read `~/.claude/plugins/installed_plugins.json` and check for:
|
|
|
51
51
|
For each missing plugin, run:
|
|
52
52
|
|
|
53
53
|
```bash
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
claude plugin install superpowers@claude-plugins-official
|
|
55
|
+
claude plugin install context7@claude-plugins-official
|
|
56
56
|
```
|
|
57
57
|
|
|
58
58
|
**If the command succeeds:** `✓ {plugin} installed. Restart Claude Code after /morph-init completes.`
|
|
@@ -60,14 +60,10 @@ npx morph-spec install-plugin context7
|
|
|
60
60
|
**If the command fails:** Show this and **STOP** — do not continue:
|
|
61
61
|
|
|
62
62
|
```
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
│ 2. Browse → search "{plugin}" → Install │
|
|
68
|
-
│ 3. Restart Claude Code │
|
|
69
|
-
│ 4. Re-run /morph-init │
|
|
70
|
-
└─────────────────────────────────────────────────────────────┘
|
|
63
|
+
Plugin {plugin} could not be installed automatically.
|
|
64
|
+
Run manually and restart Claude Code:
|
|
65
|
+
claude plugin install {plugin}
|
|
66
|
+
Then re-run /morph-init.
|
|
71
67
|
```
|
|
72
68
|
|
|
73
69
|
Both `superpowers` and `context7` are required. Do not continue if either is missing.
|
|
@@ -4,7 +4,7 @@ description: MORPH-SPEC Phase 3 (Clarify). Reviews spec.md for ambiguities, gene
|
|
|
4
4
|
argument-hint: "[feature-name]"
|
|
5
5
|
user-invocable: false
|
|
6
6
|
allowed-tools: Read, Write, Edit, Bash, Glob, Grep
|
|
7
|
-
cliVersion: "4.8.
|
|
7
|
+
cliVersion: "4.8.17"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# MORPH Clarify - FASE 3
|
|
@@ -3,7 +3,7 @@ name: phase-codebase-analysis
|
|
|
3
3
|
description: MORPH-SPEC Design sub-phase that analyzes existing codebase and database schema, producing schema-analysis.md with real column names, types, relationships, and field mismatches. Use at the start of Design phase before generating contracts.cs to prevent incorrect field names or types.
|
|
4
4
|
user-invocable: false
|
|
5
5
|
allowed-tools: Read, Write, Edit, Bash, Glob, Grep
|
|
6
|
-
cliVersion: "4.8.
|
|
6
|
+
cliVersion: "4.8.17"
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# MORPH Codebase Analysis - Sub-fase de DESIGN
|
|
@@ -4,7 +4,7 @@ description: MORPH-SPEC Phase 2 (Design). Analyzes codebase/schema, then produce
|
|
|
4
4
|
argument-hint: "[feature-name]"
|
|
5
5
|
user-invocable: false
|
|
6
6
|
allowed-tools: Read, Write, Edit, Bash, Glob, Grep
|
|
7
|
-
cliVersion: "4.8.
|
|
7
|
+
cliVersion: "4.8.17"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# MORPH Design - FASE 2
|
|
@@ -4,7 +4,7 @@ description: MORPH-SPEC Phase 1 (Setup). Reads project context, detects tech sta
|
|
|
4
4
|
argument-hint: "[feature-name]"
|
|
5
5
|
user-invocable: false
|
|
6
6
|
allowed-tools: Read, Write, Edit, Bash, Glob, Grep
|
|
7
|
-
cliVersion: "4.8.
|
|
7
|
+
cliVersion: "4.8.17"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# MORPH Setup - FASE 1
|
|
@@ -4,7 +4,7 @@ description: MORPH-SPEC Phase 1.5 (UI/UX). Creates design-system.md, mockups.md,
|
|
|
4
4
|
argument-hint: "[feature-name]"
|
|
5
5
|
user-invocable: false
|
|
6
6
|
allowed-tools: Read, Write, Edit, Bash, Glob, Grep
|
|
7
|
-
cliVersion: "4.8.
|
|
7
|
+
cliVersion: "4.8.17"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# MORPH UI/UX Design - FASE 1.5
|
package/package.json
CHANGED
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* morph-spec monitor — Live TUI Dashboard
|
|
3
|
+
*
|
|
4
|
+
* Opens a live-updating terminal UI showing:
|
|
5
|
+
* - Active agents by tier
|
|
6
|
+
* - Hook events (chronological feed)
|
|
7
|
+
* - Skills invoked this session
|
|
8
|
+
* - Rules in effect
|
|
9
|
+
* - Overview summary
|
|
10
|
+
*
|
|
11
|
+
* Reads .morph/logs/activity.json (written by hooks via activity-logger.js).
|
|
12
|
+
* Uses fs.watch for live updates (no polling, no chokidar).
|
|
13
|
+
*
|
|
14
|
+
* Usage:
|
|
15
|
+
* morph-spec monitor [feature]
|
|
16
|
+
* morph-spec monitor --compact
|
|
17
|
+
* morph-spec monitor --mode hooks
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { watch, existsSync, readFileSync, readdirSync, statSync } from 'fs';
|
|
21
|
+
import { join, basename } from 'path';
|
|
22
|
+
import chalk from 'chalk';
|
|
23
|
+
|
|
24
|
+
import {
|
|
25
|
+
renderBox,
|
|
26
|
+
renderFeed,
|
|
27
|
+
renderAgents,
|
|
28
|
+
renderSkills,
|
|
29
|
+
renderRules,
|
|
30
|
+
renderOverview,
|
|
31
|
+
renderStatusBar,
|
|
32
|
+
renderHeader,
|
|
33
|
+
} from '../../lib/monitor/renderer.js';
|
|
34
|
+
|
|
35
|
+
import { getActiveAgents } from '../../lib/monitor/agent-resolver.js';
|
|
36
|
+
import { readActivity } from '../../../framework/hooks/shared/activity-logger.js';
|
|
37
|
+
import { loadState, getActiveFeature, derivePhaseForFeature } from '../../../framework/hooks/shared/state-reader.js';
|
|
38
|
+
|
|
39
|
+
const MODES = ['overview', 'hooks', 'agents', 'skills', 'rules'];
|
|
40
|
+
const ACTIVITY_FILE = '.morph/logs/activity.json';
|
|
41
|
+
const RULES_DIR_PROJECT = '.claude/rules';
|
|
42
|
+
const RULES_DIR_FRAMEWORK = '.morph/framework/rules';
|
|
43
|
+
|
|
44
|
+
// ─────────────────────────────────────────────────────────
|
|
45
|
+
// Rules loading
|
|
46
|
+
// ─────────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
function loadRules(cwd) {
|
|
49
|
+
const rules = [];
|
|
50
|
+
const dirs = [
|
|
51
|
+
join(cwd, RULES_DIR_PROJECT),
|
|
52
|
+
join(cwd, RULES_DIR_FRAMEWORK),
|
|
53
|
+
join(cwd, 'framework/rules'),
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
for (const dir of dirs) {
|
|
57
|
+
if (!existsSync(dir)) continue;
|
|
58
|
+
try {
|
|
59
|
+
const files = readdirSync(dir).filter(f => f.endsWith('.md'));
|
|
60
|
+
for (const file of files) {
|
|
61
|
+
// Parse front-matter paths glob if present
|
|
62
|
+
let glob = '*';
|
|
63
|
+
try {
|
|
64
|
+
const content = readFileSync(join(dir, file), 'utf-8');
|
|
65
|
+
const match = content.match(/paths:\s*\n((?:\s*-\s*.+\n?)+)/);
|
|
66
|
+
if (match) {
|
|
67
|
+
const paths = match[1].match(/- (.+)/g);
|
|
68
|
+
if (paths) glob = paths.map(p => p.replace('- ', '').trim()).join(', ');
|
|
69
|
+
}
|
|
70
|
+
} catch { /* skip */ }
|
|
71
|
+
rules.push({ name: basename(file, '.md'), glob });
|
|
72
|
+
}
|
|
73
|
+
} catch { /* skip */ }
|
|
74
|
+
}
|
|
75
|
+
return rules;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ─────────────────────────────────────────────────────────
|
|
79
|
+
// Available skills loading
|
|
80
|
+
// ─────────────────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
function loadAvailableSkills(cwd) {
|
|
83
|
+
const skillDirs = [
|
|
84
|
+
join(cwd, '.claude/skills'),
|
|
85
|
+
join(cwd, 'framework/skills'),
|
|
86
|
+
];
|
|
87
|
+
const names = [];
|
|
88
|
+
for (const dir of skillDirs) {
|
|
89
|
+
if (!existsSync(dir)) continue;
|
|
90
|
+
try {
|
|
91
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
92
|
+
for (const entry of entries) {
|
|
93
|
+
if (entry.isDirectory()) {
|
|
94
|
+
// Level-N-meta/skill-name or direct skill dirs
|
|
95
|
+
const sub = readdirSync(join(dir, entry.name), { withFileTypes: true });
|
|
96
|
+
for (const s of sub) {
|
|
97
|
+
if (s.isDirectory()) names.push(s.name);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
} catch { /* skip */ }
|
|
102
|
+
}
|
|
103
|
+
return [...new Set(names)];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ─────────────────────────────────────────────────────────
|
|
107
|
+
// Screen rendering
|
|
108
|
+
// ─────────────────────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
function render(state) {
|
|
111
|
+
const { feature, phase, tasks, agentsByTier, activity, rules, availableSkills, mode, compact } = state;
|
|
112
|
+
|
|
113
|
+
const hooks = activity?.hooks || [];
|
|
114
|
+
const skills = activity?.skills || [];
|
|
115
|
+
|
|
116
|
+
// Clear screen
|
|
117
|
+
process.stdout.write('\x1Bc');
|
|
118
|
+
|
|
119
|
+
// Header
|
|
120
|
+
process.stdout.write(renderHeader(feature, phase, tasks) + '\n');
|
|
121
|
+
|
|
122
|
+
if (compact) {
|
|
123
|
+
// Compact: just show overview
|
|
124
|
+
const overviewData = { feature, phase, agentsByTier, hooks, skills, rules };
|
|
125
|
+
process.stdout.write(renderOverview(overviewData) + '\n');
|
|
126
|
+
} else {
|
|
127
|
+
switch (mode) {
|
|
128
|
+
case 'hooks':
|
|
129
|
+
process.stdout.write(renderFeed(hooks) + '\n');
|
|
130
|
+
break;
|
|
131
|
+
|
|
132
|
+
case 'agents':
|
|
133
|
+
process.stdout.write(renderAgents(agentsByTier, phase) + '\n');
|
|
134
|
+
break;
|
|
135
|
+
|
|
136
|
+
case 'skills':
|
|
137
|
+
process.stdout.write(renderSkills(skills, availableSkills) + '\n');
|
|
138
|
+
break;
|
|
139
|
+
|
|
140
|
+
case 'rules':
|
|
141
|
+
process.stdout.write(renderRules(rules) + '\n');
|
|
142
|
+
break;
|
|
143
|
+
|
|
144
|
+
case 'overview':
|
|
145
|
+
default: {
|
|
146
|
+
const overviewData = { feature, phase, agentsByTier, hooks, skills, rules };
|
|
147
|
+
process.stdout.write(renderOverview(overviewData) + '\n');
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Status bar
|
|
154
|
+
process.stdout.write(renderStatusBar(mode, MODES) + '\n');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ─────────────────────────────────────────────────────────
|
|
158
|
+
// State loading
|
|
159
|
+
// ─────────────────────────────────────────────────────────
|
|
160
|
+
|
|
161
|
+
function loadMonitorState(cwd, featureArg) {
|
|
162
|
+
let feature = featureArg || '';
|
|
163
|
+
let phase = '';
|
|
164
|
+
let tasks = null;
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
const morphState = loadState(cwd);
|
|
168
|
+
if (!feature && morphState) {
|
|
169
|
+
const active = getActiveFeature(cwd);
|
|
170
|
+
if (active) {
|
|
171
|
+
feature = active.name;
|
|
172
|
+
tasks = active.feature.tasks || null;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (feature) {
|
|
176
|
+
phase = derivePhaseForFeature(feature, cwd);
|
|
177
|
+
}
|
|
178
|
+
} catch { /* fail-open */ }
|
|
179
|
+
|
|
180
|
+
const agentsByTier = getActiveAgents(cwd, phase);
|
|
181
|
+
const activity = readActivity(cwd);
|
|
182
|
+
const rules = loadRules(cwd);
|
|
183
|
+
const availableSkills = loadAvailableSkills(cwd);
|
|
184
|
+
|
|
185
|
+
return { feature, phase, tasks, agentsByTier, activity, rules, availableSkills };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ─────────────────────────────────────────────────────────
|
|
189
|
+
// Main command
|
|
190
|
+
// ─────────────────────────────────────────────────────────
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* @param {string|undefined} featureArg
|
|
194
|
+
* @param {{ compact: boolean, mode: string }} opts
|
|
195
|
+
*/
|
|
196
|
+
export function monitorCommand(featureArg, opts = {}) {
|
|
197
|
+
const cwd = process.cwd();
|
|
198
|
+
const compact = !!opts.compact;
|
|
199
|
+
const startMode = MODES.includes(opts.mode) ? opts.mode : 'overview';
|
|
200
|
+
|
|
201
|
+
let modeIndex = MODES.indexOf(startMode);
|
|
202
|
+
let currentMode = startMode;
|
|
203
|
+
|
|
204
|
+
// Initial render
|
|
205
|
+
let monitorState = loadMonitorState(cwd, featureArg);
|
|
206
|
+
render({ ...monitorState, mode: currentMode, compact });
|
|
207
|
+
|
|
208
|
+
// Watch activity.json for changes
|
|
209
|
+
const activityPath = join(cwd, ACTIVITY_FILE);
|
|
210
|
+
let watcher = null;
|
|
211
|
+
|
|
212
|
+
function startWatcher() {
|
|
213
|
+
if (!existsSync(activityPath)) {
|
|
214
|
+
// Retry watch setup when file is created
|
|
215
|
+
setTimeout(startWatcher, 2000);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
try {
|
|
220
|
+
watcher = watch(activityPath, { persistent: true }, () => {
|
|
221
|
+
try {
|
|
222
|
+
monitorState = loadMonitorState(cwd, featureArg);
|
|
223
|
+
render({ ...monitorState, mode: currentMode, compact });
|
|
224
|
+
} catch { /* fail-open */ }
|
|
225
|
+
});
|
|
226
|
+
} catch {
|
|
227
|
+
// fs.watch not supported — fall back to polling
|
|
228
|
+
setInterval(() => {
|
|
229
|
+
try {
|
|
230
|
+
monitorState = loadMonitorState(cwd, featureArg);
|
|
231
|
+
render({ ...monitorState, mode: currentMode, compact });
|
|
232
|
+
} catch { /* fail-open */ }
|
|
233
|
+
}, 2000);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
startWatcher();
|
|
238
|
+
|
|
239
|
+
// Keyboard input: raw mode
|
|
240
|
+
if (process.stdin.isTTY) {
|
|
241
|
+
process.stdin.setRawMode(true);
|
|
242
|
+
process.stdin.resume();
|
|
243
|
+
process.stdin.setEncoding('utf8');
|
|
244
|
+
|
|
245
|
+
process.stdin.on('data', (key) => {
|
|
246
|
+
// q or Ctrl-C → quit
|
|
247
|
+
if (key === 'q' || key === '\u0003') {
|
|
248
|
+
if (watcher) watcher.close();
|
|
249
|
+
process.stdin.setRawMode(false);
|
|
250
|
+
process.stdout.write('\n' + chalk.gray('morph-spec monitor closed.\n'));
|
|
251
|
+
process.exit(0);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// shift+tab → cycle modes (forward)
|
|
255
|
+
// \x1b[Z is the standard escape for shift+tab
|
|
256
|
+
if (key === '\x1b[Z' || key === '\t') {
|
|
257
|
+
modeIndex = (modeIndex + 1) % MODES.length;
|
|
258
|
+
currentMode = MODES[modeIndex];
|
|
259
|
+
render({ ...monitorState, mode: currentMode, compact });
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Arrow keys: left/right also cycle
|
|
263
|
+
if (key === '\x1b[C') { // right arrow
|
|
264
|
+
modeIndex = (modeIndex + 1) % MODES.length;
|
|
265
|
+
currentMode = MODES[modeIndex];
|
|
266
|
+
render({ ...monitorState, mode: currentMode, compact });
|
|
267
|
+
}
|
|
268
|
+
if (key === '\x1b[D') { // left arrow
|
|
269
|
+
modeIndex = (modeIndex - 1 + MODES.length) % MODES.length;
|
|
270
|
+
currentMode = MODES[modeIndex];
|
|
271
|
+
render({ ...monitorState, mode: currentMode, compact });
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// r → force refresh
|
|
275
|
+
if (key === 'r') {
|
|
276
|
+
monitorState = loadMonitorState(cwd, featureArg);
|
|
277
|
+
render({ ...monitorState, mode: currentMode, compact });
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
} else {
|
|
281
|
+
// Non-TTY: just render once and exit (useful for scripting/piping)
|
|
282
|
+
process.stdout.write('\n' + chalk.gray('(non-interactive mode — run in a real terminal for live updates)\n'));
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Handle process signals
|
|
286
|
+
process.on('SIGINT', () => {
|
|
287
|
+
if (watcher) watcher.close();
|
|
288
|
+
process.stdout.write('\n' + chalk.gray('morph-spec monitor closed.\n'));
|
|
289
|
+
process.exit(0);
|
|
290
|
+
});
|
|
291
|
+
process.on('SIGTERM', () => {
|
|
292
|
+
if (watcher) watcher.close();
|
|
293
|
+
process.exit(0);
|
|
294
|
+
});
|
|
295
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Resolver — Resolves active agents for a given phase
|
|
3
|
+
*
|
|
4
|
+
* Reads .morph/framework/agents.json (installed copy) or falls back to
|
|
5
|
+
* framework/agents.json (source). Filters agents by phase and VSA config.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readFileSync, existsSync } from 'fs';
|
|
9
|
+
import { join } from 'path';
|
|
10
|
+
|
|
11
|
+
const AGENTS_PATHS = [
|
|
12
|
+
'.morph/framework/agents.json',
|
|
13
|
+
'framework/agents.json',
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Load agents.json from the project or framework source.
|
|
18
|
+
*
|
|
19
|
+
* @param {string} [cwd]
|
|
20
|
+
* @returns {Object|null}
|
|
21
|
+
*/
|
|
22
|
+
export function loadAgentsJson(cwd = process.cwd()) {
|
|
23
|
+
for (const rel of AGENTS_PATHS) {
|
|
24
|
+
const full = join(cwd, rel);
|
|
25
|
+
if (existsSync(full)) {
|
|
26
|
+
try {
|
|
27
|
+
return JSON.parse(readFileSync(full, 'utf-8'));
|
|
28
|
+
} catch {
|
|
29
|
+
// Try next path
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Determine which agents are active for the given phase.
|
|
38
|
+
*
|
|
39
|
+
* Rules:
|
|
40
|
+
* - always_active: true → always included
|
|
41
|
+
* - Agents with phase-scoped keywords matching the current phase
|
|
42
|
+
* - VSA architecture: include vsa-architect, exclude domain-architect in phase-setup
|
|
43
|
+
* - Tier-4 validators are always included in implement phase
|
|
44
|
+
*
|
|
45
|
+
* @param {string} [cwd]
|
|
46
|
+
* @param {string} [phase]
|
|
47
|
+
* @returns {{ tier1: Agent[], tier2: Agent[], tier3: Agent[], tier4: Agent[] }}
|
|
48
|
+
*/
|
|
49
|
+
export function getActiveAgents(cwd = process.cwd(), phase = '') {
|
|
50
|
+
const data = loadAgentsJson(cwd);
|
|
51
|
+
if (!data?.agents) {
|
|
52
|
+
return { tier1: [], tier2: [], tier3: [], tier4: [] };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Detect VSA config
|
|
56
|
+
let isVsa = false;
|
|
57
|
+
try {
|
|
58
|
+
const configPath = join(cwd, '.morph/config/config.json');
|
|
59
|
+
if (existsSync(configPath)) {
|
|
60
|
+
const cfg = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
61
|
+
isVsa = cfg?.config?.architecture?.style === 'vertical-slice';
|
|
62
|
+
}
|
|
63
|
+
} catch {
|
|
64
|
+
// Non-blocking
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Phase → active domains heuristic
|
|
68
|
+
const PHASE_DOMAINS = {
|
|
69
|
+
proposal: ['architecture', 'developer-experience', 'ai-orchestration'],
|
|
70
|
+
setup: ['architecture', 'developer-experience', 'ai-orchestration'],
|
|
71
|
+
design: ['architecture', 'dotnet', 'frontend', 'ai-orchestration'],
|
|
72
|
+
uiux: ['frontend', 'ui', 'architecture'],
|
|
73
|
+
clarify: ['architecture', 'developer-experience'],
|
|
74
|
+
tasks: ['architecture', 'dotnet', 'frontend', 'testing'],
|
|
75
|
+
implement: ['dotnet', 'frontend', 'testing', 'devops', 'blazor'],
|
|
76
|
+
sync: ['architecture', 'developer-experience'],
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const activeDomains = new Set(PHASE_DOMAINS[phase] || Object.values(PHASE_DOMAINS).flat());
|
|
80
|
+
|
|
81
|
+
const result = { tier1: [], tier2: [], tier3: [], tier4: [] };
|
|
82
|
+
|
|
83
|
+
for (const [name, agent] of Object.entries(data.agents)) {
|
|
84
|
+
if (name.startsWith('_comment')) continue;
|
|
85
|
+
|
|
86
|
+
const tier = agent.tier;
|
|
87
|
+
if (!tier || tier < 1 || tier > 4) continue;
|
|
88
|
+
|
|
89
|
+
const tierKey = `tier${tier}`;
|
|
90
|
+
|
|
91
|
+
// Always-active agents
|
|
92
|
+
if (agent.always_active === true) {
|
|
93
|
+
// VSA: swap domain-architect for vsa-architect
|
|
94
|
+
if (name === 'domain-architect' && isVsa) continue;
|
|
95
|
+
result[tierKey].push({ name, ...agent });
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// VSA architect: only active when VSA config is set
|
|
100
|
+
if (name === 'vsa-architect') {
|
|
101
|
+
if (isVsa && (phase === 'setup' || phase === 'design')) {
|
|
102
|
+
result[tierKey].push({ name, ...agent });
|
|
103
|
+
}
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Tier-4 validators: active in implement phase
|
|
108
|
+
if (tier === 4) {
|
|
109
|
+
if (phase === 'implement' || phase === 'sync') {
|
|
110
|
+
result[tierKey].push({ name, ...agent });
|
|
111
|
+
}
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Domain-based activation
|
|
116
|
+
const agentDomains = agent.domains || [];
|
|
117
|
+
const isActive = agentDomains.some(d => activeDomains.has(d));
|
|
118
|
+
if (isActive) {
|
|
119
|
+
result[tierKey].push({ name, ...agent });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return result;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Get icon for an agent based on its teammate config or tier.
|
|
128
|
+
*
|
|
129
|
+
* @param {Object} agent
|
|
130
|
+
* @returns {string}
|
|
131
|
+
*/
|
|
132
|
+
export function getAgentIcon(agent) {
|
|
133
|
+
return agent.teammate?.icon || '';
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Count total active agents across all tiers.
|
|
138
|
+
*
|
|
139
|
+
* @param {{ tier1: any[], tier2: any[], tier3: any[], tier4: any[] }} agentsByTier
|
|
140
|
+
* @returns {number}
|
|
141
|
+
*/
|
|
142
|
+
export function countActiveAgents(agentsByTier) {
|
|
143
|
+
return Object.values(agentsByTier).flat().length;
|
|
144
|
+
}
|