@polymorphism-tech/morph-spec 4.8.15 → 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 +9 -12
- 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/file-copier.js +1 -0
- 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.
|
|
@@ -250,7 +246,7 @@ For each detected integration with an available MCP:
|
|
|
250
246
|
|
|
251
247
|
Only offer Figma, Docker, Azure if explicitly detected in `.env.example`.
|
|
252
248
|
|
|
253
|
-
> **Format rule:** Always write MCP entries
|
|
249
|
+
> **Format rule:** Always write MCP entries with `"command": "cmd"` and `"/c"` as the **first element of `"args"`**:
|
|
254
250
|
> ```json
|
|
255
251
|
> "mcp-name": {
|
|
256
252
|
> "command": "cmd",
|
|
@@ -258,7 +254,8 @@ Only offer Figma, Docker, Azure if explicitly detected in `.env.example`.
|
|
|
258
254
|
> "env": { "KEY": "VALUE" }
|
|
259
255
|
> }
|
|
260
256
|
> ```
|
|
261
|
-
> Never use `"command": "
|
|
257
|
+
> **Never** use `"command": "cmd /c"` (space-separated string) — that is invalid and will fail.
|
|
258
|
+
> **Never** use `"command": "npx"` directly — it also fails on Windows without `cmd`.
|
|
262
259
|
|
|
263
260
|
---
|
|
264
261
|
|
|
@@ -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
|
+
}
|