@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.
Files changed (38) hide show
  1. package/README.md +2 -2
  2. package/bin/morph-spec.js +9 -1
  3. package/claude-plugin.json +1 -1
  4. package/docs/CHEATSHEET.md +1 -1
  5. package/docs/QUICKSTART.md +1 -1
  6. package/framework/hooks/README.md +4 -2
  7. package/framework/hooks/claude-code/notification/approval-reminder.js +2 -0
  8. package/framework/hooks/claude-code/post-tool-use/context-refresh.js +109 -0
  9. package/framework/hooks/claude-code/post-tool-use/dispatch.js +5 -0
  10. package/framework/hooks/claude-code/post-tool-use/handle-tool-failure.js +2 -0
  11. package/framework/hooks/claude-code/post-tool-use/validator-feedback.js +6 -1
  12. package/framework/hooks/claude-code/pre-compact/save-morph-context.js +2 -0
  13. package/framework/hooks/claude-code/pre-tool-use/enforce-phase-writes.js +7 -1
  14. package/framework/hooks/claude-code/pre-tool-use/protect-spec-files.js +3 -0
  15. package/framework/hooks/claude-code/session-start/inject-morph-context.js +19 -0
  16. package/framework/hooks/claude-code/statusline.py +61 -0
  17. package/framework/hooks/claude-code/stop/validate-completion.js +2 -0
  18. package/framework/hooks/claude-code/teammate-idle/teammate-idle.js +3 -0
  19. package/framework/hooks/claude-code/user-prompt/enrich-prompt.js +6 -1
  20. package/framework/hooks/claude-code/user-prompt/set-terminal-title.js +2 -0
  21. package/framework/hooks/shared/activity-logger.js +129 -0
  22. package/framework/skills/level-0-meta/morph-init/SKILL.md +6 -10
  23. package/framework/skills/level-1-workflows/phase-clarify/SKILL.md +1 -1
  24. package/framework/skills/level-1-workflows/phase-codebase-analysis/SKILL.md +1 -1
  25. package/framework/skills/level-1-workflows/phase-design/SKILL.md +1 -1
  26. package/framework/skills/level-1-workflows/phase-implement/SKILL.md +1 -1
  27. package/framework/skills/level-1-workflows/phase-setup/SKILL.md +1 -1
  28. package/framework/skills/level-1-workflows/phase-tasks/SKILL.md +1 -1
  29. package/framework/skills/level-1-workflows/phase-uiux/SKILL.md +1 -1
  30. package/package.json +1 -1
  31. package/src/commands/project/index.js +1 -0
  32. package/src/commands/project/monitor.js +295 -0
  33. package/src/lib/monitor/agent-resolver.js +144 -0
  34. package/src/lib/monitor/renderer.js +230 -0
  35. package/src/scripts/global-install.js +24 -12
  36. package/src/scripts/setup-infra.js +22 -1
  37. package/src/utils/banner.js +51 -0
  38. 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.16
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.16 by [Polymorphism Tech](https://polymorphism.tech)*
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...]')
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "morph-spec",
3
- "version": "4.8.16",
3
+ "version": "4.8.18",
4
4
  "displayName": "MORPH-SPEC Framework",
5
5
  "description": "Spec-driven development with 38 agents and 8-phase workflow for .NET/Blazor/Next.js/Azure",
6
6
  "publisher": "polymorphism-tech",
@@ -200,4 +200,4 @@ These files are never edited directly. Use CLI commands or `morph-spec update` i
200
200
 
201
201
  ---
202
202
 
203
- *morph-spec v4.8.16 by Polymorphism Tech*
203
+ *morph-spec v4.8.18 by Polymorphism Tech*
@@ -203,4 +203,4 @@ morph-spec doctor
203
203
 
204
204
  ---
205
205
 
206
- *morph-spec v4.8.16 by Polymorphism Tech*
206
+ *morph-spec v4.8.18 by Polymorphism Tech*
@@ -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
- │ │ └── dispatch.js # Dispatch on CLI commands (auto-checkpoint)
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.5*
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') pass();
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)) pass(); // editing existing file — allow
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) pass();
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);