@polymorphism-tech/morph-spec 4.8.16 → 4.8.18
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 +9 -1
- 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/global-install.js +24 -12
- package/src/scripts/setup-infra.js +22 -1
- package/src/utils/banner.js +51 -0
- package/src/utils/hooks-installer.js +11 -5
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
> Spec-driven development framework for multi-stack projects. Turns feature requests into implementation-ready code through structured, AI-orchestrated phases.
|
|
4
4
|
|
|
5
5
|
**Package:** `@polymorphism-tech/morph-spec`
|
|
6
|
-
**Version:** 4.8.
|
|
6
|
+
**Version:** 4.8.18
|
|
7
7
|
**Requires:** Node.js 18+, Claude Code
|
|
8
8
|
|
|
9
9
|
---
|
|
@@ -376,4 +376,4 @@ Code generated by morph-spec (contracts, templates, implementation output) belon
|
|
|
376
376
|
|
|
377
377
|
---
|
|
378
378
|
|
|
379
|
-
*morph-spec v4.8.
|
|
379
|
+
*morph-spec v4.8.18 by [Polymorphism Tech](https://polymorphism.tech)*
|
package/bin/morph-spec.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
1
|
+
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { program } from 'commander';
|
|
4
4
|
import chalk from 'chalk';
|
|
@@ -15,6 +15,7 @@ import { doctorCommand } from '../src/commands/project/doctor.js';
|
|
|
15
15
|
import { tutorialCommand } from '../src/commands/project/tutorial.js';
|
|
16
16
|
|
|
17
17
|
import { statusCommand } from '../src/commands/project/status.js';
|
|
18
|
+
import { monitorCommand } from '../src/commands/project/monitor.js';
|
|
18
19
|
import { checkpointSaveCommand, checkpointRestoreCommand, checkpointListCommand } from '../src/commands/project/checkpoint.js';
|
|
19
20
|
|
|
20
21
|
// State commands
|
|
@@ -111,6 +112,13 @@ program
|
|
|
111
112
|
.option('-v, --verbose', 'Show detailed output')
|
|
112
113
|
.action(statusCommand);
|
|
113
114
|
|
|
115
|
+
program
|
|
116
|
+
.command('monitor [feature]')
|
|
117
|
+
.description('Live TUI dashboard — hooks, agents, skills, rules in real time')
|
|
118
|
+
.option('--compact', 'Minimal single-view output')
|
|
119
|
+
.option('--mode <mode>', 'Start view: overview|hooks|agents|skills|rules', 'overview')
|
|
120
|
+
.action(monitorCommand);
|
|
121
|
+
|
|
114
122
|
// State management commands
|
|
115
123
|
program
|
|
116
124
|
.command('state <action> [args...]')
|
package/claude-plugin.json
CHANGED
package/docs/CHEATSHEET.md
CHANGED
package/docs/QUICKSTART.md
CHANGED
|
@@ -15,7 +15,8 @@ framework/hooks/
|
|
|
15
15
|
│ │ ├── protect-spec-files.js # Block edits to approved spec artifacts
|
|
16
16
|
│ │ └── enforce-phase-writes.js # Enforce writes to correct phase dir
|
|
17
17
|
│ ├── post-tool-use/
|
|
18
|
-
│ │
|
|
18
|
+
│ │ ├── dispatch.js # Dispatch on CLI commands (auto-checkpoint)
|
|
19
|
+
│ │ └── context-refresh.js # Auto-refresh context on structural git commits
|
|
19
20
|
│ ├── stop/
|
|
20
21
|
│ │ └── validate-completion.js # Advisory: warn about incomplete work
|
|
21
22
|
│ ├── pre-compact/
|
|
@@ -50,6 +51,7 @@ framework/hooks/
|
|
|
50
51
|
| **PreToolUse** (Write\|Edit) | enforce-phase-writes.js | Block | Ensures writes go to current phase directory |
|
|
51
52
|
| **PreToolUse** (Bash) | _(prompt-type inline guard)_ | Block | Blocks `rm -rf .morph/` and direct state edits via Claude's reasoning |
|
|
52
53
|
| **PostToolUse** (Bash) | dispatch.js | Dispatch | Triggers checkpoints on task completion |
|
|
54
|
+
| **PostToolUse** (Bash) | context-refresh.js | Inject context | Detects `git commit` with structural changes → instructs Claude to update `.morph/context/README.md` and `.morph/config/config.json` |
|
|
53
55
|
| **PostToolUseFailure** | handle-tool-failure.js | Logging | Appends structured JSON to .morph/logs/tool-failures.log |
|
|
54
56
|
| **Stop** | validate-completion.js | Advisory | Warns about incomplete tasks/missing outputs/pending gates |
|
|
55
57
|
| **PreCompact** | save-morph-context.js | Snapshot | Saves state to .morph/memory/ before compaction |
|
|
@@ -199,4 +201,4 @@ morph-spec doctor --reset
|
|
|
199
201
|
|
|
200
202
|
---
|
|
201
203
|
|
|
202
|
-
*MORPH-SPEC by Polymorphism Tech — Hooks Architecture v2.
|
|
204
|
+
*MORPH-SPEC by Polymorphism Tech — Hooks Architecture v2.7*
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
import { stateExists, getActiveFeature, getPendingGates } from '../../shared/state-reader.js';
|
|
14
14
|
import { injectContext, pass } from '../../shared/hook-response.js';
|
|
15
|
+
import { logHookActivity } from '../../shared/activity-logger.js';
|
|
15
16
|
|
|
16
17
|
try {
|
|
17
18
|
if (!stateExists()) pass();
|
|
@@ -44,6 +45,7 @@ try {
|
|
|
44
45
|
` morph-spec approve ${name} ${gate}`
|
|
45
46
|
);
|
|
46
47
|
|
|
48
|
+
logHookActivity('approval-reminder', 'Notification', `pending(${pendingRelevant.join(',')})`);
|
|
47
49
|
injectContext(
|
|
48
50
|
`MORPH-SPEC: Feature '${name}' has pending approval(s) that may be blocking progress:\n` +
|
|
49
51
|
reminders.join('\n')
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* PostToolUse Hook: Context Auto-Refresh
|
|
5
|
+
*
|
|
6
|
+
* Event: PostToolUse | Matcher: Bash
|
|
7
|
+
*
|
|
8
|
+
* Detects `git commit` commands that touch structural files (package.json,
|
|
9
|
+
* *.csproj, next.config.*, etc.) and injects instructions for Claude to
|
|
10
|
+
* update .morph/context/README.md and .morph/config/config.json.
|
|
11
|
+
*
|
|
12
|
+
* Only fires when:
|
|
13
|
+
* 1. .morph/state.json exists (morph-spec project)
|
|
14
|
+
* 2. Command contains `git commit`
|
|
15
|
+
* 3. .morph/context/README.md is initialized (not a setup-infra placeholder)
|
|
16
|
+
* 4. Structural files changed in the commit (compared to HEAD~1)
|
|
17
|
+
*
|
|
18
|
+
* Fail-open: exits 0 on any error.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { execSync } from 'child_process';
|
|
22
|
+
import { readFileSync, existsSync } from 'fs';
|
|
23
|
+
import { join, basename } from 'path';
|
|
24
|
+
import { readStdin } from '../../shared/stdin-reader.js';
|
|
25
|
+
import { stateExists } from '../../shared/state-reader.js';
|
|
26
|
+
import { pass, injectContext } from '../../shared/hook-response.js';
|
|
27
|
+
import { logHookActivity } from '../../shared/activity-logger.js';
|
|
28
|
+
|
|
29
|
+
const STRUCTURAL_PATTERNS = [
|
|
30
|
+
/^package\.json$/,
|
|
31
|
+
/\.csproj$/,
|
|
32
|
+
/\.sln$/,
|
|
33
|
+
/^next\.config\./,
|
|
34
|
+
/^tailwind\.config\./,
|
|
35
|
+
/^tsconfig\.json$/,
|
|
36
|
+
/^jsconfig\.json$/,
|
|
37
|
+
/^docker-compose/,
|
|
38
|
+
/^Dockerfile/,
|
|
39
|
+
/^\.env\.example$/,
|
|
40
|
+
/^go\.mod$/,
|
|
41
|
+
/^requirements\.txt$/,
|
|
42
|
+
/^pyproject\.toml$/,
|
|
43
|
+
/^Gemfile$/,
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
if (!stateExists()) pass();
|
|
48
|
+
|
|
49
|
+
const payload = await readStdin();
|
|
50
|
+
if (!payload) pass();
|
|
51
|
+
|
|
52
|
+
const command = payload?.tool_input?.command || '';
|
|
53
|
+
if (!/\bgit\s+commit\b/.test(command)) pass();
|
|
54
|
+
|
|
55
|
+
if (!isContextInitialized()) pass();
|
|
56
|
+
|
|
57
|
+
let changedFiles = [];
|
|
58
|
+
try {
|
|
59
|
+
const output = execSync('git diff HEAD~1 --name-only', {
|
|
60
|
+
encoding: 'utf-8',
|
|
61
|
+
stdio: 'pipe',
|
|
62
|
+
cwd: process.cwd(),
|
|
63
|
+
timeout: 10000,
|
|
64
|
+
});
|
|
65
|
+
changedFiles = output.trim().split('\n').filter(Boolean);
|
|
66
|
+
} catch {
|
|
67
|
+
// First commit (no HEAD~1) or git error — pass silently
|
|
68
|
+
pass();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const structuralFiles = changedFiles.filter(file => {
|
|
72
|
+
const name = basename(file);
|
|
73
|
+
return STRUCTURAL_PATTERNS.some(pattern => pattern.test(name));
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
if (structuralFiles.length === 0) pass();
|
|
77
|
+
|
|
78
|
+
const fileList = structuralFiles.join(', ');
|
|
79
|
+
logHookActivity('context-refresh', 'PostToolUse', `structural_change(${structuralFiles.length})`);
|
|
80
|
+
injectContext(
|
|
81
|
+
`📋 [morph-spec] Mudanças estruturais detectadas no commit: ${fileList}\n\n` +
|
|
82
|
+
`Atualizar contexto do projeto automaticamente:\n` +
|
|
83
|
+
`1. \`git show HEAD --stat\` — ver escopo completo da mudança\n` +
|
|
84
|
+
`2. Ler \`.morph/context/README.md\` → atualizar seções afetadas\n` +
|
|
85
|
+
` (Tech Stack, Architecture, Key Integrations, Agent Notes)\n` +
|
|
86
|
+
`3. Ler \`.morph/config/config.json\` → atualizar campos relevantes\n` +
|
|
87
|
+
` (stack, architecture, integrations, paths)\n` +
|
|
88
|
+
`4. Escrever os arquivos atualizados com as mudanças detectadas`
|
|
89
|
+
);
|
|
90
|
+
} catch {
|
|
91
|
+
// Fail-open
|
|
92
|
+
process.exit(0);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Check if .morph/context/README.md is initialized (not a setup-infra placeholder).
|
|
97
|
+
* Returns false if the file is missing or contains the placeholder sentinel.
|
|
98
|
+
* @returns {boolean}
|
|
99
|
+
*/
|
|
100
|
+
function isContextInitialized() {
|
|
101
|
+
try {
|
|
102
|
+
const contextPath = join(process.cwd(), '.morph/context/README.md');
|
|
103
|
+
if (!existsSync(contextPath)) return false;
|
|
104
|
+
const content = readFileSync(contextPath, 'utf-8');
|
|
105
|
+
return !content.includes('Run /morph-init to generate');
|
|
106
|
+
} catch {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -19,6 +19,7 @@ import { resolve } from 'path';
|
|
|
19
19
|
import { readStdin } from '../../shared/stdin-reader.js';
|
|
20
20
|
import { stateExists, getFeature } from '../../shared/state-reader.js';
|
|
21
21
|
import { pass, injectContext } from '../../shared/hook-response.js';
|
|
22
|
+
import { logHookActivity } from '../../shared/activity-logger.js';
|
|
22
23
|
|
|
23
24
|
try {
|
|
24
25
|
if (!stateExists()) pass();
|
|
@@ -42,6 +43,7 @@ function dispatch(command) {
|
|
|
42
43
|
const [, featureName, taskId] = taskDoneMatch;
|
|
43
44
|
|
|
44
45
|
// Check if checkpoint should run (every 3 tasks)
|
|
46
|
+
let dispatchResult = 'task_done';
|
|
45
47
|
try {
|
|
46
48
|
const feature = getFeature(featureName);
|
|
47
49
|
if (feature?.tasks) {
|
|
@@ -49,12 +51,14 @@ function dispatch(command) {
|
|
|
49
51
|
if (completed > 0 && completed % 3 === 0) {
|
|
50
52
|
const checkpointNum = Math.floor(completed / 3);
|
|
51
53
|
run(`morph-spec checkpoint-save ${featureName} --note "Auto-checkpoint #${checkpointNum} at task ${completed}"`);
|
|
54
|
+
dispatchResult = 'checkpoint_saved';
|
|
52
55
|
}
|
|
53
56
|
}
|
|
54
57
|
} catch {
|
|
55
58
|
// Non-blocking
|
|
56
59
|
}
|
|
57
60
|
|
|
61
|
+
logHookActivity('dispatch.js', 'PostToolUse', dispatchResult);
|
|
58
62
|
pass();
|
|
59
63
|
}
|
|
60
64
|
|
|
@@ -63,6 +67,7 @@ function dispatch(command) {
|
|
|
63
67
|
if (phaseAdvanceMatch) {
|
|
64
68
|
const [, featureName] = phaseAdvanceMatch;
|
|
65
69
|
evaluatePhaseChain(featureName);
|
|
70
|
+
logHookActivity('dispatch.js', 'PostToolUse', 'phase_chain');
|
|
66
71
|
pass();
|
|
67
72
|
}
|
|
68
73
|
|
|
@@ -15,6 +15,7 @@ import { appendFileSync, mkdirSync } from 'fs';
|
|
|
15
15
|
import { join } from 'path';
|
|
16
16
|
import { readStdin } from '../../shared/stdin-reader.js';
|
|
17
17
|
import { stateExists } from '../../shared/state-reader.js';
|
|
18
|
+
import { logHookActivity } from '../../shared/activity-logger.js';
|
|
18
19
|
|
|
19
20
|
try {
|
|
20
21
|
// Only log in morph-spec projects
|
|
@@ -35,6 +36,7 @@ try {
|
|
|
35
36
|
};
|
|
36
37
|
|
|
37
38
|
appendFileSync(join(logDir, 'tool-failures.log'), JSON.stringify(entry) + '\n');
|
|
39
|
+
logHookActivity('handle-tool-failure', 'PostToolUseFailure', `logged(${entry.tool})`);
|
|
38
40
|
} catch {
|
|
39
41
|
// Fail-open: logging is non-critical
|
|
40
42
|
}
|
|
@@ -21,6 +21,7 @@ import { join } from 'path';
|
|
|
21
21
|
import { readStdin } from '../../shared/stdin-reader.js';
|
|
22
22
|
import { stateExists } from '../../shared/state-reader.js';
|
|
23
23
|
import { injectContext, pass } from '../../shared/hook-response.js';
|
|
24
|
+
import { logHookActivity } from '../../shared/activity-logger.js';
|
|
24
25
|
|
|
25
26
|
// Standard IDs referenced in remediation messages
|
|
26
27
|
const STANDARD_REFS = {
|
|
@@ -65,9 +66,13 @@ try {
|
|
|
65
66
|
if (!taskHistory) pass();
|
|
66
67
|
|
|
67
68
|
// Only act on failed validations (passed = no feedback needed)
|
|
68
|
-
if (taskHistory.status === 'passed')
|
|
69
|
+
if (taskHistory.status === 'passed') {
|
|
70
|
+
logHookActivity('validator-feedback', 'PostToolUse', 'passed');
|
|
71
|
+
pass();
|
|
72
|
+
}
|
|
69
73
|
|
|
70
74
|
// Build remediation context based on validation issues
|
|
75
|
+
logHookActivity('validator-feedback', 'PostToolUse', `remediation_injected(attempt_${taskHistory.attempt || 1})`);
|
|
71
76
|
buildRemediationContext(featureName, taskId, taskHistory);
|
|
72
77
|
} catch {
|
|
73
78
|
// Fail-open
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
derivePhaseForFeature, getPendingGates, getOutputs,
|
|
23
23
|
} from '../../shared/state-reader.js';
|
|
24
24
|
import { injectContext, pass } from '../../shared/hook-response.js';
|
|
25
|
+
import { logHookActivity } from '../../shared/activity-logger.js';
|
|
25
26
|
|
|
26
27
|
const DECISIONS_MAX_CHARS = 1500;
|
|
27
28
|
const MAX_PENDING_TASKS = 8;
|
|
@@ -173,6 +174,7 @@ try {
|
|
|
173
174
|
lines.push('');
|
|
174
175
|
lines.push('=== END MORPH CONTEXT ===');
|
|
175
176
|
|
|
177
|
+
logHookActivity('save-morph-context', 'PreCompact', 'context_saved');
|
|
176
178
|
injectContext(lines.join('\n'));
|
|
177
179
|
} catch {
|
|
178
180
|
process.exit(0);
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
PHASE_DIRS
|
|
29
29
|
} from '../../shared/phase-utils.js';
|
|
30
30
|
import { block, pass } from '../../shared/hook-response.js';
|
|
31
|
+
import { logHookActivity } from '../../shared/activity-logger.js';
|
|
31
32
|
|
|
32
33
|
try {
|
|
33
34
|
// Amend mode: bypass phase write enforcement for legitimate corrections
|
|
@@ -65,9 +66,13 @@ try {
|
|
|
65
66
|
if (targetDir !== allowedDir) {
|
|
66
67
|
// Allow editing already-existing files from previous phases
|
|
67
68
|
const absolutePath = resolve(process.cwd(), filePath);
|
|
68
|
-
if (existsSync(absolutePath))
|
|
69
|
+
if (existsSync(absolutePath)) {
|
|
70
|
+
logHookActivity('enforce-phase-writes', 'PreToolUse', 'ok');
|
|
71
|
+
pass(); // editing existing file — allow
|
|
72
|
+
}
|
|
69
73
|
|
|
70
74
|
const phaseLabel = phase.toUpperCase();
|
|
75
|
+
logHookActivity('enforce-phase-writes', 'PreToolUse', 'blocked');
|
|
71
76
|
block(
|
|
72
77
|
`MORPH-SPEC: Writing to '${targetDir}/' is not allowed during ${phaseLabel} phase.\n` +
|
|
73
78
|
`Current phase '${phase}' only permits writes to '${allowedDir}/'.\n\n` +
|
|
@@ -76,6 +81,7 @@ try {
|
|
|
76
81
|
);
|
|
77
82
|
}
|
|
78
83
|
|
|
84
|
+
logHookActivity('enforce-phase-writes', 'PreToolUse', 'ok');
|
|
79
85
|
pass();
|
|
80
86
|
} catch {
|
|
81
87
|
// Fail-open
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
PROTECTED_SPEC_FILES
|
|
23
23
|
} from '../../shared/phase-utils.js';
|
|
24
24
|
import { block, pass } from '../../shared/hook-response.js';
|
|
25
|
+
import { logHookActivity } from '../../shared/activity-logger.js';
|
|
25
26
|
|
|
26
27
|
try {
|
|
27
28
|
// Amend mode: bypass spec protection for legitimate in-implementation corrections
|
|
@@ -52,6 +53,7 @@ try {
|
|
|
52
53
|
|
|
53
54
|
// Check if the gate has been approved
|
|
54
55
|
if (isGateApproved(featureName, requiredGate)) {
|
|
56
|
+
logHookActivity('protect-spec-files', 'PreToolUse', 'blocked');
|
|
55
57
|
block(
|
|
56
58
|
`MORPH-SPEC: '${filename}' is locked — the '${requiredGate}' gate has been approved.\n` +
|
|
57
59
|
`Editing approved specs breaks the spec contract.\n\n` +
|
|
@@ -63,6 +65,7 @@ try {
|
|
|
63
65
|
}
|
|
64
66
|
|
|
65
67
|
// Gate not yet approved — allow editing
|
|
68
|
+
logHookActivity('protect-spec-files', 'PreToolUse', 'ok');
|
|
66
69
|
pass();
|
|
67
70
|
} catch {
|
|
68
71
|
// Fail-open
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import { loadState, getActiveFeature, getPendingGates, getMissingOutputs, derivePhaseForFeature } from '../../shared/state-reader.js';
|
|
15
15
|
import { stateExists } from '../../shared/state-reader.js';
|
|
16
16
|
import { injectContext, pass } from '../../shared/hook-response.js';
|
|
17
|
+
import { resetActivity, logHookActivity } from '../../shared/activity-logger.js';
|
|
17
18
|
import { readFileSync, existsSync } from 'fs';
|
|
18
19
|
import { join } from 'path';
|
|
19
20
|
|
|
@@ -49,6 +50,13 @@ try {
|
|
|
49
50
|
if (!state?.features || Object.keys(state.features).length === 0) pass();
|
|
50
51
|
|
|
51
52
|
const active = getActiveFeature();
|
|
53
|
+
|
|
54
|
+
// Reset activity log for new session
|
|
55
|
+
const activeFeatureName = active?.name || '';
|
|
56
|
+
const activePhase = active ? derivePhaseForFeature(active.name) : '';
|
|
57
|
+
resetActivity(new Date().toISOString(), activeFeatureName, activePhase);
|
|
58
|
+
logHookActivity('inject-morph-context', 'SessionStart', 'ok');
|
|
59
|
+
|
|
52
60
|
const lines = ['MORPH-SPEC Status:'];
|
|
53
61
|
|
|
54
62
|
if (active) {
|
|
@@ -136,6 +144,17 @@ try {
|
|
|
136
144
|
lines.push('');
|
|
137
145
|
lines.push('Key commands: morph-spec status <feature> | morph-spec phase advance <feature> | morph-spec approve <feature> <gate>');
|
|
138
146
|
|
|
147
|
+
// System Map — compact summary for sessions without a live monitor terminal
|
|
148
|
+
if (active) {
|
|
149
|
+
const { name } = active;
|
|
150
|
+
const sysPhase = derivePhaseForFeature(name);
|
|
151
|
+
lines.push('');
|
|
152
|
+
lines.push('── MORPH SYSTEM MAP ─────────────────────────────────────────────');
|
|
153
|
+
lines.push(`🪝 Hooks: 10 registrados | 📏 Rules: em .claude/rules/ | 🎯 Skills: em .claude/skills/`);
|
|
154
|
+
lines.push(` Execute \`morph-spec monitor\` em terminal separado para live view`);
|
|
155
|
+
lines.push('─────────────────────────────────────────────────────────────────');
|
|
156
|
+
}
|
|
157
|
+
|
|
139
158
|
injectContext(lines.join('\n'));
|
|
140
159
|
} catch {
|
|
141
160
|
// Fail-open
|
|
@@ -202,6 +202,53 @@ def get_all_active_features(cwd, entries):
|
|
|
202
202
|
return []
|
|
203
203
|
|
|
204
204
|
|
|
205
|
+
# ── Activity log helpers ──────────────────────────────────────────────────────
|
|
206
|
+
|
|
207
|
+
def get_activity_info(cwd):
|
|
208
|
+
"""Read .morph/logs/activity.json and return compact summary dict.
|
|
209
|
+
Returns None if file is missing or unreadable.
|
|
210
|
+
"""
|
|
211
|
+
activity_path = Path(cwd) / '.morph' / 'logs' / 'activity.json'
|
|
212
|
+
if not activity_path.exists():
|
|
213
|
+
return None
|
|
214
|
+
try:
|
|
215
|
+
data = json.loads(activity_path.read_text(encoding='utf-8'))
|
|
216
|
+
hooks = data.get('hooks') or []
|
|
217
|
+
skills = data.get('skills') or []
|
|
218
|
+
|
|
219
|
+
last_hook = None
|
|
220
|
+
last_hook_age = None
|
|
221
|
+
if hooks:
|
|
222
|
+
last = hooks[-1]
|
|
223
|
+
last_hook = last.get('name', '')
|
|
224
|
+
# Try to compute age from timestamp (HH:MM:SS)
|
|
225
|
+
try:
|
|
226
|
+
ts = last.get('ts', '')
|
|
227
|
+
if ts and len(ts) == 8:
|
|
228
|
+
now = datetime.now()
|
|
229
|
+
hook_t = datetime.strptime(ts, '%H:%M:%S').replace(
|
|
230
|
+
year=now.year, month=now.month, day=now.day
|
|
231
|
+
)
|
|
232
|
+
diff_s = (now - hook_t).total_seconds()
|
|
233
|
+
if diff_s < 0:
|
|
234
|
+
diff_s = 0
|
|
235
|
+
if diff_s < 60:
|
|
236
|
+
last_hook_age = f"{int(diff_s)}s"
|
|
237
|
+
else:
|
|
238
|
+
last_hook_age = f"{int(diff_s // 60)}m"
|
|
239
|
+
except Exception:
|
|
240
|
+
pass
|
|
241
|
+
|
|
242
|
+
return {
|
|
243
|
+
'hook_count': len(hooks),
|
|
244
|
+
'skill_count': len(skills),
|
|
245
|
+
'last_hook': last_hook,
|
|
246
|
+
'last_hook_age': last_hook_age,
|
|
247
|
+
}
|
|
248
|
+
except Exception:
|
|
249
|
+
return None
|
|
250
|
+
|
|
251
|
+
|
|
205
252
|
# ── Git helpers ───────────────────────────────────────────────────────────────
|
|
206
253
|
|
|
207
254
|
def _run_git(args, cwd):
|
|
@@ -489,6 +536,20 @@ def main():
|
|
|
489
536
|
|
|
490
537
|
print(' | '.join(parts))
|
|
491
538
|
|
|
539
|
+
# ── Activity info line (hooks + last event) ──────────────────────────────
|
|
540
|
+
if features: # only show when a feature is active
|
|
541
|
+
activity = get_activity_info(cwd)
|
|
542
|
+
if activity and (activity['hook_count'] > 0 or activity['skill_count'] > 0):
|
|
543
|
+
act_parts = []
|
|
544
|
+
if activity['hook_count'] > 0:
|
|
545
|
+
hook_label = activity['last_hook'] or '?'
|
|
546
|
+
age_str = f"({activity['last_hook_age']})" if activity['last_hook_age'] else ''
|
|
547
|
+
act_parts.append(f"{BLUE}🪝 {hook_label} {age_str}{R}".strip())
|
|
548
|
+
if activity['skill_count'] > 0:
|
|
549
|
+
act_parts.append(f"{YELLOW}🎯 {activity['skill_count']} skill(s){R}")
|
|
550
|
+
if act_parts:
|
|
551
|
+
print(f" {GRAY}└{R} " + f" {GRAY}|{R} ".join(act_parts))
|
|
552
|
+
|
|
492
553
|
# ── Session info line (always shown) ─────────────────────────────────────
|
|
493
554
|
parts2 = []
|
|
494
555
|
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
stateExists, loadState, getActiveFeature, getMissingOutputs, derivePhaseForFeature,
|
|
25
25
|
} from '../../shared/state-reader.js';
|
|
26
26
|
import { injectContext, pass } from '../../shared/hook-response.js';
|
|
27
|
+
import { logHookActivity } from '../../shared/activity-logger.js';
|
|
27
28
|
|
|
28
29
|
// Phases where compaction is strongly recommended before starting
|
|
29
30
|
const COMPACT_TRIGGER_PHASES = new Set(['implement']);
|
|
@@ -163,6 +164,7 @@ try {
|
|
|
163
164
|
`Status: morph-spec status ${name}`,
|
|
164
165
|
].join('\n');
|
|
165
166
|
|
|
167
|
+
logHookActivity('validate-completion', 'Stop', `warnings(${warnings.length})`);
|
|
166
168
|
injectContext(message);
|
|
167
169
|
} catch {
|
|
168
170
|
process.exit(0);
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
import { execSync } from 'child_process';
|
|
19
19
|
import { readStdin } from '../../../shared/stdin-reader.js';
|
|
20
20
|
import { loadState, stateExists, getActiveFeature } from '../../../shared/state-reader.js';
|
|
21
|
+
import { logHookActivity } from '../../../shared/activity-logger.js';
|
|
21
22
|
|
|
22
23
|
try {
|
|
23
24
|
// Read hook payload (TeammateIdle provides teammate info)
|
|
@@ -66,6 +67,7 @@ try {
|
|
|
66
67
|
|
|
67
68
|
if (exitCode !== 0) {
|
|
68
69
|
// Exit 2: send feedback to teammate to continue working
|
|
70
|
+
logHookActivity('teammate-idle', 'TeammateIdle', 'validation_failed');
|
|
69
71
|
const feedback = [
|
|
70
72
|
'MORPH-SPEC validation found issues — please fix before going idle:',
|
|
71
73
|
'',
|
|
@@ -80,6 +82,7 @@ try {
|
|
|
80
82
|
}
|
|
81
83
|
|
|
82
84
|
// Exit 0: validation passed, teammate may become idle
|
|
85
|
+
logHookActivity('teammate-idle', 'TeammateIdle', 'passed');
|
|
83
86
|
process.exit(0);
|
|
84
87
|
} catch {
|
|
85
88
|
// Fail-open: never block a teammate due to hook errors
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
import { readStdin } from '../../shared/stdin-reader.js';
|
|
18
18
|
import { stateExists, loadState, getActiveFeature, getFeature, getPendingGates } from '../../shared/state-reader.js';
|
|
19
19
|
import { injectContext, pass } from '../../shared/hook-response.js';
|
|
20
|
+
import { logHookActivity } from '../../shared/activity-logger.js';
|
|
20
21
|
|
|
21
22
|
try {
|
|
22
23
|
if (!stateExists()) pass();
|
|
@@ -82,8 +83,12 @@ try {
|
|
|
82
83
|
}
|
|
83
84
|
}
|
|
84
85
|
|
|
85
|
-
if (context.length === 0)
|
|
86
|
+
if (context.length === 0) {
|
|
87
|
+
logHookActivity('enrich-prompt', 'UserPromptSubmit', 'ok');
|
|
88
|
+
pass();
|
|
89
|
+
}
|
|
86
90
|
|
|
91
|
+
logHookActivity('enrich-prompt', 'UserPromptSubmit', 'context_injected');
|
|
87
92
|
injectContext(context.join('\n'));
|
|
88
93
|
} catch {
|
|
89
94
|
// Fail-open
|
|
@@ -19,6 +19,7 @@ import { openSync, writeSync, closeSync } from 'fs';
|
|
|
19
19
|
import { writeFile, mkdir } from 'fs/promises';
|
|
20
20
|
import { homedir } from 'os';
|
|
21
21
|
import { join } from 'path';
|
|
22
|
+
import { logHookActivity } from '../../shared/activity-logger.js';
|
|
22
23
|
|
|
23
24
|
try {
|
|
24
25
|
const payload = await readStdin();
|
|
@@ -53,6 +54,7 @@ try {
|
|
|
53
54
|
closeSync(tty);
|
|
54
55
|
} catch { /* terminal may not support /dev/tty */ }
|
|
55
56
|
|
|
57
|
+
logHookActivity('set-terminal-title', 'UserPromptSubmit', 'ok');
|
|
56
58
|
} catch { /* fail-open */ }
|
|
57
59
|
|
|
58
60
|
process.exit(0);
|