@polymorphism-tech/morph-spec 4.18.0 → 4.20.0

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 (86) hide show
  1. package/README.md +2 -2
  2. package/bin/morph-spec.js +11 -0
  3. package/bin/task-manager.js +47 -10
  4. package/claude-plugin.json +1 -1
  5. package/docs/CHEATSHEET.md +1 -1
  6. package/docs/QUICKSTART.md +1 -1
  7. package/framework/agents/architecture/architecture-expert.md +98 -0
  8. package/framework/agents/architecture/morph-spec-architect.md +66 -0
  9. package/framework/agents/architecture/packages-validator.md +89 -0
  10. package/framework/agents/architecture/security-expert.md +79 -0
  11. package/framework/agents/backend/ms-agent-expert.md +1 -1
  12. package/framework/agents/frontend/design-system-validator.md +100 -0
  13. package/framework/agents/frontend/nextjs-component-validator.md +97 -0
  14. package/framework/agents/frontend/{ui-ux-designer.md → ui-designer.md} +31 -0
  15. package/framework/agents.json +1 -1
  16. package/framework/hooks/claude-code/post-tool-use/dispatch.js +56 -0
  17. package/framework/hooks/claude-code/post-tool-use/handle-tool-failure.js +78 -44
  18. package/framework/hooks/claude-code/post-tool-use/loop-detect.js +103 -0
  19. package/framework/hooks/claude-code/post-tool-use/validator-feedback.js +60 -11
  20. package/framework/hooks/claude-code/session-start/inject-morph-context.js +21 -2
  21. package/framework/hooks/claude-code/stop/validate-completion.js +48 -1
  22. package/framework/hooks/shared/activity-logger.js +56 -1
  23. package/framework/hooks/shared/build-check.js +79 -0
  24. package/framework/hooks/shared/payload-utils.js +39 -0
  25. package/framework/hooks/shared/session-snapshot.js +105 -0
  26. package/framework/phases.json +1 -1
  27. package/framework/rubrics/backend.json +36 -0
  28. package/framework/rubrics/default.json +36 -0
  29. package/framework/rubrics/frontend.json +36 -0
  30. package/framework/skills/level-0-meta/mcp-registry.json +34 -1
  31. package/framework/skills/level-0-meta/morph-frontend-review/SKILL.md +1 -1
  32. package/framework/skills/level-1-workflows/morph-phase-codebase-analysis/SKILL.md +1 -1
  33. package/framework/skills/level-1-workflows/morph-phase-design/SKILL.md +11 -1
  34. package/framework/skills/level-1-workflows/morph-phase-implement/SKILL.md +12 -1
  35. package/framework/skills/level-1-workflows/morph-phase-plan/SKILL.md +1 -1
  36. package/framework/skills/level-1-workflows/morph-phase-tasks/SKILL.md +1 -1
  37. package/framework/skills/level-1-workflows/morph-phase-uiux/SKILL.md +119 -11
  38. package/framework/skills/level-1-workflows/morph-scope-escalation/SKILL.md +1 -1
  39. package/framework/standards/ai-agents/production.md +5 -2
  40. package/framework/standards/ai-agents/setup.md +13 -14
  41. package/framework/standards/ai-agents/workflows.md +8 -6
  42. package/framework/standards/backend/database/vector-search-rag.md +1 -1
  43. package/framework/templates/code/dotnet/jobs/agent.cs +0 -1
  44. package/package.json +1 -1
  45. package/src/commands/dashboard/dashboard.js +53 -0
  46. package/src/commands/mcp/mcp-setup.js +21 -0
  47. package/src/commands/project/init.js +11 -0
  48. package/src/dashboard/app/App.jsx +31 -0
  49. package/src/dashboard/app/components/DetailPanel.jsx +11 -0
  50. package/src/dashboard/app/components/FlowGraph.jsx +17 -0
  51. package/src/dashboard/app/components/SearchFilter.jsx +22 -0
  52. package/src/dashboard/app/components/Sidebar.jsx +45 -0
  53. package/src/dashboard/app/hooks/useDetailPanel.js +15 -0
  54. package/src/dashboard/app/main.jsx +13 -0
  55. package/src/dashboard/app/pages/AgentMap.jsx +371 -0
  56. package/src/dashboard/app/pages/HookInspector.jsx +418 -0
  57. package/src/dashboard/app/pages/LiveState.jsx +239 -0
  58. package/src/dashboard/app/pages/PhasePipeline.jsx +410 -0
  59. package/src/dashboard/app/pages/RulesMap.jsx +273 -0
  60. package/src/dashboard/app/pages/SkillsLibrary.jsx +367 -0
  61. package/src/dashboard/app/pages/Standards.jsx +332 -0
  62. package/src/dashboard/app/pages/Workflows.jsx +346 -0
  63. package/src/dashboard/app/styles/index.css +47 -0
  64. package/src/dashboard/index.html +12 -0
  65. package/src/dashboard/package.json +24 -0
  66. package/src/dashboard/scanner/agents-scanner.js +35 -0
  67. package/src/dashboard/scanner/hooks-scanner.js +41 -0
  68. package/src/dashboard/scanner/index.js +21 -0
  69. package/src/dashboard/scanner/phases-scanner.js +15 -0
  70. package/src/dashboard/scanner/rubrics-scanner.js +12 -0
  71. package/src/dashboard/scanner/rules-scanner.js +37 -0
  72. package/src/dashboard/scanner/skills-scanner.js +43 -0
  73. package/src/dashboard/scanner/squads-scanner.js +13 -0
  74. package/src/dashboard/scanner/standards-scanner.js +26 -0
  75. package/src/dashboard/scanner/state-scanner.js +8 -0
  76. package/src/dashboard/scanner/workflows-scanner.js +18 -0
  77. package/src/dashboard/vite-plugin-morph-data.js +17 -0
  78. package/src/dashboard/vite.config.js +24 -0
  79. package/src/lib/checkpoints/checkpoint-hooks.js +2 -1
  80. package/src/lib/contracts/sprint-contract-generator.js +137 -0
  81. package/src/lib/generators/recap-generator.js +15 -0
  82. package/src/lib/installers/mcp-installer.js +25 -0
  83. package/src/lib/validators/score-aggregator.js +244 -0
  84. package/src/lib/validators/validation-runner.js +44 -0
  85. package/src/scripts/setup-infra.js +8 -0
  86. package/src/utils/hooks-installer.js +12 -1
@@ -0,0 +1,97 @@
1
+ ---
2
+ name: nextjs-component-validator
3
+ description: Next.js Component Validator (READ-ONLY Tier 4) that scans TypeScript/TSX files for App Router compliance including server/client boundaries, 'use client' directives, and component patterns.
4
+ allowed-tools: Read, Glob, Grep
5
+ stacks:
6
+ - nextjs
7
+ color: gray
8
+ effort: medium
9
+ ---
10
+
11
+ # Next.js Component Validator
12
+
13
+ READ-ONLY scanner for App Router compliance in Next.js projects.
14
+
15
+ ## Responsibilities
16
+
17
+ 1. **Scan all .tsx/.ts files** under `src/` for App Router violations
18
+ 2. **Validate server/client boundaries** — correct `'use client'` placement
19
+ 3. **Check component patterns** — data fetching, metadata, layouts
20
+ 4. **Report findings** with severity and location
21
+
22
+ ---
23
+
24
+ ## Evaluation Posture
25
+
26
+ You are an ADVERSARIAL evaluator. Your default assumption is that App Router violations exist.
27
+
28
+ - **Assume violations exist** until proven otherwise — your job is to FIND boundary errors and pattern misuse, not confirm compliance
29
+ - **Challenge surface appearances** — components that render correctly may have wrong `'use client'` placement, missing Suspense boundaries, or server/client leaks
30
+ - **A clean report is suspicious** — if you found nothing, dig deeper before concluding
31
+ - **Prioritize by severity** — server/client boundary violations before performance optimizations
32
+ - **Be specific** — every finding needs file path, line number, the violation type, and concrete fix
33
+
34
+ ---
35
+
36
+ ## Scan Categories
37
+
38
+ ### Server/Client Boundaries
39
+
40
+ | Issue | Signal | Severity |
41
+ |-------|--------|----------|
42
+ | Missing `'use client'` | Component uses hooks (`useState`, `useEffect`) without directive | Error |
43
+ | Unnecessary `'use client'` | Server component marked as client without reason | Warning |
44
+ | Client importing server | Client component importing server-only module | Error |
45
+ | Hooks in server component | React hooks in file without `'use client'` | Error |
46
+
47
+ ### Data Fetching
48
+
49
+ | Issue | Signal | Severity |
50
+ |-------|--------|----------|
51
+ | `useEffect` for data | Using `useEffect` + `fetch` instead of server component | Warning |
52
+ | Missing `Suspense` | Async server component without Suspense boundary | Warning |
53
+ | Client-side fetch in page | `fetch` in client component that could be server | Info |
54
+
55
+ ### Routing & Metadata
56
+
57
+ | Issue | Signal | Severity |
58
+ |-------|--------|----------|
59
+ | Missing `metadata` export | Page without `export const metadata` or `generateMetadata` | Warning |
60
+ | Wrong file convention | `page.tsx` missing in route folder | Error |
61
+ | Layout without children | `layout.tsx` not rendering `{children}` | Error |
62
+
63
+ ### Performance
64
+
65
+ | Issue | Signal | Severity |
66
+ |-------|--------|----------|
67
+ | Large client bundle | `'use client'` at page level (should be leaf components) | Warning |
68
+ | Missing `loading.tsx` | Route without loading state | Info |
69
+ | Unoptimized images | `<img>` instead of `next/image` | Warning |
70
+ | Missing `dynamic` import | Heavy component not lazy-loaded | Info |
71
+
72
+ ---
73
+
74
+ ## Scan Procedure
75
+
76
+ 1. **Glob** all `.tsx`, `.ts` files under `src/`
77
+ 2. **Grep** for `'use client'` directives and React hooks
78
+ 3. **Read** files with hooks to verify `'use client'` presence
79
+ 4. **Validate** page/layout conventions in `app/` directory
80
+ 5. **Report** violations with file, line, severity, fix suggestion
81
+
82
+ ---
83
+
84
+ ## Checklist
85
+
86
+ - [ ] All components with hooks have `'use client'`
87
+ - [ ] Server components fetch data directly (no `useEffect`)
88
+ - [ ] Pages export `metadata` or `generateMetadata`
89
+ - [ ] Layouts render `{children}`
90
+ - [ ] Images use `next/image`
91
+ - [ ] Heavy components use `dynamic()` import
92
+ - [ ] `Suspense` boundaries around async components
93
+ - [ ] `'use client'` at leaf level, not page level
94
+
95
+ ---
96
+
97
+ *MORPH-SPEC by Polymorphism Tech*
@@ -55,6 +55,31 @@ Linear (linear.app), Vercel (vercel.com), Stripe (stripe.com), Dribbble dashboar
55
55
 
56
56
  ---
57
57
 
58
+ ### Stitch MCP (Optional)
59
+
60
+ When Stitch MCP is available, use AI-generated screen designs instead of manual wireframes:
61
+
62
+ | Tool | Purpose |
63
+ |------|---------|
64
+ | `generate_screen_from_text` | Generate screen design from text prompt |
65
+ | `fetch_screen_image` | Export screen as image for review |
66
+ | `fetch_screen_code` | Export design.md with tokens |
67
+ | `list_projects` / `list_screens` | Navigate existing projects |
68
+
69
+ **Companion skills** (auto-installed with Stitch MCP):
70
+ - `stitch-design` — entry point for design generation via Claude Code
71
+ - `stitch-loop` — multi-page website generation
72
+ - `design-md` — generate design.md from existing designs
73
+ - `prompt-enhance` — improve design prompts automatically
74
+ - `react-components` — convert Stitch HTML to React components
75
+ - `shadcn-ui` — guide conversion to ShadCN components (recommended for Next.js projects)
76
+
77
+ **Key:** Stitch MCP transmits real HTML/CSS code (not screenshots), producing dramatically better results.
78
+
79
+ **Rule:** Stitch design.md is NOT a separate deliverable. Extract tokens into `design-system.md`.
80
+
81
+ ---
82
+
58
83
  ## Design Brief Protocol (MANDATORY)
59
84
 
60
85
  > **This protocol runs BEFORE any wireframe, mockup, or visual decision.**
@@ -74,6 +99,11 @@ Read: .morph/context/design-system.md
74
99
  - Fill in the missing sections using the Design Thinking questions (Step 1 below)
75
100
  - This file is the persistent record — update it, don't just keep it in memory
76
101
 
102
+ **If Stitch MCP is available:**
103
+ - Check `list_projects` — if a project exists for this feature, its design.md can seed the design system
104
+ - Stitch tokens map INTO `design-system.md` — never a separate file
105
+ - Validate Stitch-exported colors against WCAG AA before adopting
106
+
77
107
  **When taking any design decision not covered by the file:**
78
108
  1. Add the token/decision to `design-system.md` in the correct section
79
109
  2. Register in the Changelog: `- YYYY-MM-DD [ui-ux-designer] Description`
@@ -208,6 +238,7 @@ Per flow: Trigger, steps, happy path, edge cases, ASCII flowchart diagram.
208
238
  - [ ] `design-system.md` read (project-level at `.morph/context/`) OR created with real tokens
209
239
  - [ ] Design Brief declared before any visual output (direction, palette, motion)
210
240
  - [ ] All color values in deliverables use CSS tokens (`--color-accent: #FF4D1C`) not generic names
241
+ - [ ] If Stitch was used: tokens extracted and mapped to `design-system.md` (no separate design.md output)
211
242
  - [ ] `mockups.md`: Wireframes for ALL screens + states (loading, error, empty) + responsiveness
212
243
  - [ ] `components.md`: shadcn components mapped with props/events/states
213
244
  - [ ] `flows.md`: Happy paths + edge cases + ASCII diagrams
@@ -261,7 +261,7 @@
261
261
  "role": "UI/UX Squad Leader",
262
262
  "icon": "🎨",
263
263
  "triggers": "UI/UX design, wireframes, user flows, design system, shadcn/ui component selection",
264
- "spawn_prompt": "You are the UI/UX Squad Leader. Create wireframes, design system, and user flows during the UI/UX phase.\n\nWorkflow:\n1. Read `.morph/context/design-system.md` FIRST — use only defined tokens\n2. Read spec.md for UI requirements\n3. Generate deliverables: design-system.md, mockups.md, components.md, flows.md\n4. Coordinate squad: nextjs-expert, css-specialist, seo-growth-hacker\n\nWork is UI/UX phase ONLY — no implementation code. Refer to the Domain Knowledge section below for design brief protocol, shadcn/ui reference, and accessibility rules."
264
+ "spawn_prompt": "You are the UI/UX Squad Leader. Create wireframes, design system, and user flows during the UI/UX phase.\n\nWorkflow:\n1. Read `.morph/context/design-system.md` FIRST — use only defined tokens\n2. Read spec.md for UI requirements\n3. If Stitch MCP available, offer AI-generated screen designs. Use companion skills (stitch-design, design-md) for workflow. Extract Stitch design.md tokens into design-system.md — no separate output.\n4. Generate deliverables: design-system.md, mockups.md, components.md, flows.md\n5. Coordinate squad: nextjs-expert, css-specialist, seo-growth-hacker\n\nWork is UI/UX phase ONLY — no implementation code. Refer to the Domain Knowledge section below for design brief protocol, shadcn/ui reference, and accessibility rules."
265
265
  }
266
266
  },
267
267
  "ef-modeler": {
@@ -69,6 +69,62 @@ async function dispatch(command, payload) {
69
69
  // Non-blocking
70
70
  }
71
71
 
72
+ // Evaluator dispatch: if sprint contract exists, instruct Claude to dispatch evaluator
73
+ try {
74
+ const taskId = taskDoneParsed.taskId;
75
+ if (taskId && !command.includes('--skip-eval')) {
76
+ const contractPath = join(process.cwd(), '.morph', 'features', featureName, '5-implement', 'contracts', `${taskId}-contract.yaml`);
77
+ if (existsSync(contractPath)) {
78
+ const contractContent = readFileSync(contractPath, 'utf-8');
79
+ let changedFiles = '';
80
+ try {
81
+ changedFiles = execSync('git diff --name-only HEAD~1 2>/dev/null | head -10', {
82
+ cwd: process.cwd(), encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe']
83
+ }).trim();
84
+ } catch { /* no git */ }
85
+
86
+ // Include score context from latest validation if available
87
+ let scoreContext = '';
88
+ try {
89
+ const evalFeature = getFeature(featureName);
90
+ const taskScores = evalFeature?.validationHistory?.[taskId]?.scores;
91
+ if (taskScores) {
92
+ scoreContext = `\n## Previous Validation Score: ${taskScores.score}/10\n` +
93
+ Object.entries(taskScores.breakdown || {})
94
+ .map(([cat, d]) => ` ${cat}: ${Math.round(d.score * 10) / 10}/10 (weight ${Math.round(d.weight * 100)}%)`)
95
+ .join('\n') + '\n\nFocus your evaluation on categories with the lowest scores.\n';
96
+ }
97
+ } catch { /* non-blocking */ }
98
+
99
+ contextParts.push(
100
+ `🔍 EVALUATOR DISPATCH\n` +
101
+ `════════════════════════════════════════\n` +
102
+ `Sprint contract exists for ${taskId}. Dispatch an evaluator sub-agent:\n\n` +
103
+ `Use the Agent tool with:\n` +
104
+ ` subagent_type: "general-purpose"\n` +
105
+ ` model: "sonnet"\n` +
106
+ ` prompt: (see below)\n\n` +
107
+ `--- EVALUATOR PROMPT ---\n` +
108
+ `You are an ADVERSARIAL evaluator. Your default assumption is that defects exist.\n` +
109
+ `Do NOT confirm correctness — actively search for problems.\n\n` +
110
+ `## Contract\n${contractContent}\n\n` +
111
+ `## Changed Files\n${changedFiles || '(unable to determine)'}\n` +
112
+ `${scoreContext}\n` +
113
+ `## Instructions\n` +
114
+ `1. Read each changed file\n` +
115
+ `2. Run the verification commands from the contract\n` +
116
+ `3. Evaluate each definitionOfDone item: PASS or FAIL with evidence\n` +
117
+ `4. For each category, score 0-10 and explain deficiencies found\n` +
118
+ `5. A clean report is suspicious — dig deeper before declaring PASS\n` +
119
+ `6. Return a structured result with overall PASS/FAIL and score breakdown\n` +
120
+ `--- END EVALUATOR PROMPT ---`
121
+ );
122
+ }
123
+ }
124
+ } catch {
125
+ // Non-blocking — evaluator is optional
126
+ }
127
+
72
128
  // Always inject validator dispatch — deferred if --skip-validation
73
129
  const validatorText = await buildValidatorDispatchContext(featureName, payload);
74
130
  if (validatorText) {
@@ -1,44 +1,78 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * PostToolUseFailure Hook: Log Tool Failures
5
- *
6
- * Event: PostToolUseFailure
7
- *
8
- * Runs when any tool fails. Appends a structured JSON entry to
9
- * .morph/logs/tool-failures.log for debugging and pattern analysis.
10
- *
11
- * Fail-open: exits 0 on any error.
12
- */
13
-
14
- import { appendFileSync, mkdirSync } from 'fs';
15
- import { join } from 'path';
16
- import { readStdin } from '../../shared/stdin-reader.js';
17
- import { stateExists } from '../../shared/state-reader.js';
18
- import { logHookActivity } from '../../shared/activity-logger.js';
19
-
20
- try {
21
- // Only log in morph-spec projects
22
- if (!stateExists()) {
23
- process.exit(0);
24
- }
25
-
26
- const input = await readStdin();
27
-
28
- const logDir = join(process.cwd(), '.morph', 'logs');
29
- mkdirSync(logDir, { recursive: true });
30
-
31
- const entry = {
32
- timestamp: new Date().toISOString(),
33
- tool: input?.tool_name ?? 'unknown',
34
- error: input?.error ?? null,
35
- input: input?.tool_input ?? null,
36
- };
37
-
38
- appendFileSync(join(logDir, 'tool-failures.log'), JSON.stringify(entry) + '\n');
39
- logHookActivity('handle-tool-failure', 'PostToolUseFailure', `logged(${entry.tool})`);
40
- } catch {
41
- // Fail-open: logging is non-critical
42
- }
43
-
44
- process.exit(0);
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * PostToolUseFailure Hook: Log Tool Failures + Rollback Awareness
5
+ *
6
+ * Event: PostToolUseFailure
7
+ *
8
+ * Runs when any tool fails. Appends a structured JSON entry to
9
+ * .morph/logs/tool-failures.log for debugging and pattern analysis.
10
+ *
11
+ * If a critical failure occurs and there are unstaged changes,
12
+ * suggests git stash as a recovery option.
13
+ *
14
+ * Fail-open: exits 0 on any error.
15
+ */
16
+
17
+ import { appendFileSync, mkdirSync } from 'fs';
18
+ import { join } from 'path';
19
+ import { execSync } from 'child_process';
20
+ import { readStdin } from '../../shared/stdin-reader.js';
21
+ import { stateExists } from '../../shared/state-reader.js';
22
+ import { injectContext } from '../../shared/hook-response.js';
23
+ import { logHookActivity, logEvent } from '../../shared/activity-logger.js';
24
+
25
+ const CRITICAL_ERROR_PATTERNS = ['ENOENT', 'EPERM', 'EACCES', 'segfault', 'out of memory', 'ENOMEM'];
26
+
27
+ try {
28
+ // Only log in morph-spec projects
29
+ if (!stateExists()) {
30
+ process.exit(0);
31
+ }
32
+
33
+ const input = await readStdin();
34
+
35
+ const logDir = join(process.cwd(), '.morph', 'logs');
36
+ mkdirSync(logDir, { recursive: true });
37
+
38
+ const entry = {
39
+ timestamp: new Date().toISOString(),
40
+ tool: input?.tool_name ?? 'unknown',
41
+ error: input?.error ?? null,
42
+ input: input?.tool_input ?? null,
43
+ };
44
+
45
+ appendFileSync(join(logDir, 'tool-failures.log'), JSON.stringify(entry) + '\n');
46
+ logHookActivity('handle-tool-failure', 'PostToolUseFailure', `logged(${entry.tool})`);
47
+
48
+ // Rollback awareness: check for unstaged changes after critical failures
49
+ const errorStr = JSON.stringify(input?.error || '');
50
+ const isCritical = CRITICAL_ERROR_PATTERNS.some(p => errorStr.includes(p));
51
+
52
+ logEvent('tool_failure', { tool: entry.tool, critical: isCritical });
53
+
54
+ if (isCritical) {
55
+ try {
56
+ const gitStatus = execSync('git status --porcelain', {
57
+ cwd: process.cwd(),
58
+ encoding: 'utf-8',
59
+ timeout: 5000,
60
+ stdio: ['pipe', 'pipe', 'pipe']
61
+ }).trim();
62
+
63
+ if (gitStatus) {
64
+ injectContext(
65
+ `⚠️ [morph-spec] Critical tool failure (${entry.tool}). ` +
66
+ `Unstaged changes detected:\n${gitStatus.split('\n').slice(0, 5).join('\n')}\n\n` +
67
+ `Consider: git stash to save current state before retrying.`
68
+ );
69
+ }
70
+ } catch {
71
+ // git not available or not a git repo — ignore
72
+ }
73
+ }
74
+ } catch {
75
+ // Fail-open: logging is non-critical
76
+ }
77
+
78
+ process.exit(0);
@@ -0,0 +1,103 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * PostToolUse Hook: Loop Detection
5
+ *
6
+ * Event: PostToolUse | Matcher: none (all tools)
7
+ *
8
+ * Detects when an agent is stuck in a repetitive loop by tracking
9
+ * the last 15 actions. If the last 8 actions have <= 2 unique
10
+ * signatures, injects a warning with escape instructions.
11
+ *
12
+ * Cooldown: 5 minutes between warnings to prevent meta-loop.
13
+ *
14
+ * Fail-open: exits 0 on any error.
15
+ */
16
+
17
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
18
+ import { join } from 'path';
19
+ import { readStdin } from '../../shared/stdin-reader.js';
20
+ import { stateExists } from '../../shared/state-reader.js';
21
+ import { injectContext, pass } from '../../shared/hook-response.js';
22
+ import { getCommandSignature } from '../../shared/payload-utils.js';
23
+ import { logEvent } from '../../shared/activity-logger.js';
24
+
25
+ const MAX_BUFFER = 15;
26
+ const LOOP_WINDOW = 8;
27
+ const MAX_UNIQUE_SIGS = 2;
28
+ const COOLDOWN_MS = 5 * 60 * 1000; // 5 minutes
29
+
30
+ try {
31
+ if (!stateExists()) pass();
32
+
33
+ const payload = await readStdin();
34
+ if (!payload) pass();
35
+
36
+ const cwd = process.cwd();
37
+ const memoryDir = join(cwd, '.morph', 'memory');
38
+ const bufferPath = join(memoryDir, 'action-buffer.json');
39
+ const cooldownPath = join(memoryDir, 'loop-cooldown.json');
40
+
41
+ mkdirSync(memoryDir, { recursive: true });
42
+
43
+ // Read existing buffer
44
+ let buffer = [];
45
+ try {
46
+ if (existsSync(bufferPath)) {
47
+ buffer = JSON.parse(readFileSync(bufferPath, 'utf-8'));
48
+ if (!Array.isArray(buffer)) buffer = [];
49
+ }
50
+ } catch {
51
+ buffer = [];
52
+ }
53
+
54
+ // Append current action
55
+ const sig = getCommandSignature(payload);
56
+ buffer.push({ sig, ts: new Date().toISOString() });
57
+
58
+ // Trim to max size (FIFO)
59
+ if (buffer.length > MAX_BUFFER) {
60
+ buffer = buffer.slice(-MAX_BUFFER);
61
+ }
62
+
63
+ // Save updated buffer
64
+ writeFileSync(bufferPath, JSON.stringify(buffer), 'utf-8');
65
+
66
+ // Check for loop (need at least LOOP_WINDOW actions)
67
+ if (buffer.length < LOOP_WINDOW) pass();
68
+
69
+ const recent = buffer.slice(-LOOP_WINDOW);
70
+ const uniqueSigs = new Set(recent.map(a => a.sig));
71
+
72
+ if (uniqueSigs.size > MAX_UNIQUE_SIGS) pass();
73
+
74
+ // Check cooldown
75
+ try {
76
+ if (existsSync(cooldownPath)) {
77
+ const cooldown = JSON.parse(readFileSync(cooldownPath, 'utf-8'));
78
+ const elapsed = Date.now() - new Date(cooldown.lastDetected).getTime();
79
+ if (elapsed < COOLDOWN_MS) pass();
80
+ }
81
+ } catch {
82
+ // Ignore corrupt cooldown — proceed with detection
83
+ }
84
+
85
+ // Loop detected — set cooldown and warn
86
+ writeFileSync(cooldownPath, JSON.stringify({ lastDetected: new Date().toISOString() }), 'utf-8');
87
+
88
+ logEvent('loop_detected', { signatures: uniqueSigs.size }, process.cwd());
89
+
90
+ const sigList = [...uniqueSigs].join(', ');
91
+ injectContext(
92
+ `⚠️ [morph-spec] LOOP DETECTED — the last ${LOOP_WINDOW} actions repeat only ${uniqueSigs.size} pattern(s): ${sigList}\n\n` +
93
+ `This usually means you're stuck. Stop and try a different approach:\n` +
94
+ ` 1. Re-read the error message carefully — what is it actually saying?\n` +
95
+ ` 2. Try a completely different fix strategy\n` +
96
+ ` 3. If the test keeps failing, check if the test itself is wrong\n` +
97
+ ` 4. If blocked, ask the user for guidance\n\n` +
98
+ `Do NOT continue the same pattern.`
99
+ );
100
+ } catch {
101
+ // Fail-open
102
+ process.exit(0);
103
+ }
@@ -79,6 +79,7 @@ try {
79
79
  function buildRemediationContext(featureName, taskId, taskHistory) {
80
80
  const attempt = taskHistory.attempt || 1;
81
81
  const validators = taskHistory.validators || {};
82
+ const scores = taskHistory.scores || null;
82
83
 
83
84
  // Collect all issues across all validators
84
85
  const allIssues = [];
@@ -95,28 +96,60 @@ function buildRemediationContext(featureName, taskId, taskHistory) {
95
96
  const lines = [];
96
97
 
97
98
  if (taskHistory.status === 'blocked') {
98
- // Max attempts reached — escalation message
99
- lines.push(`⛔ ESCALATION REQUIRED — Task ${taskId} in feature '${featureName}'`);
100
- lines.push(` Validation failed ${attempt} times (max attempts reached).`);
99
+ // Max attempts reached OR score-based escalation
100
+ const scoreInfo = scores ? ` (score: ${scores.score}/10)` : '';
101
+ lines.push(`⛔ ESCALATION REQUIRED — Task ${taskId} in feature '${featureName}'${scoreInfo}`);
102
+ if (scores?.action === 'escalate') {
103
+ lines.push(` Score below retry threshold — immediate escalation.`);
104
+ } else {
105
+ lines.push(` Validation failed ${attempt} times (max attempts reached).`);
106
+ }
101
107
  lines.push(` Human review required before proceeding.`);
108
+ if (scores) {
109
+ lines.push(``);
110
+ lines.push(` Score breakdown:`);
111
+ appendScoreBreakdown(lines, scores);
112
+ }
102
113
  lines.push(` Last failures:`);
103
114
  for (const issue of allIssues.slice(0, 5)) {
104
115
  lines.push(` • [${issue.validator}] ${issue.message} ${issue.file ? `(${issue.file}${issue.line ? ':' + issue.line : ''})` : ''}`);
105
116
  }
106
117
  } else {
107
118
  // Active remediation — attempt N of 3
108
- lines.push(`🔄 REMEDIATION REQUIRED — Task ${taskId} (attempt ${attempt}/3)`);
119
+ const scoreInfo = scores ? ` | score: ${scores.score}/10` : '';
120
+ lines.push(`🔄 REMEDIATION REQUIRED — Task ${taskId} (attempt ${attempt}/3${scoreInfo})`);
109
121
  lines.push(` Validation failed. Fix the following issues and re-run:`);
110
122
  lines.push(` morph-spec task done ${featureName} ${taskId}`);
111
123
  lines.push(``);
112
124
 
113
- for (const issue of allIssues) {
114
- const refKey = getRefKey(issue.rule || issue.validator);
115
- const ref = refKey ? ` — Ref: ${STANDARD_REFS[refKey] || refKey}` : '';
116
- const location = issue.file
117
- ? ` (${issue.file}${issue.line ? ':' + issue.line : ''})`
118
- : '';
119
- lines.push(` • [${issue.validator}] ${issue.message}${location}${ref}`);
125
+ // Score-based prioritized feedback (when available)
126
+ if (scores?.breakdown) {
127
+ lines.push(` Priority fixes (by impact):`);
128
+ const sorted = Object.entries(scores.breakdown)
129
+ .filter(([, cat]) => (cat.issues?.length || 0) > 0)
130
+ .sort((a, b) => ((10 - b[1].score) * b[1].weight) - ((10 - a[1].score) * a[1].weight));
131
+
132
+ for (const [category, cat] of sorted) {
133
+ const topIssue = cat.issues[0];
134
+ const location = topIssue?.file ? ` (${topIssue.file}${topIssue.line ? ':' + topIssue.line : ''})` : '';
135
+ const msg = topIssue?.message || 'Issues detected';
136
+ lines.push(` ${Math.round(cat.score * 10) / 10}/10 [${category}] ${msg}${location}`);
137
+ if (cat.issues.length > 1) {
138
+ lines.push(` ... and ${cat.issues.length - 1} more issue(s)`);
139
+ }
140
+ }
141
+ lines.push(``);
142
+ appendScoreBreakdown(lines, scores);
143
+ } else {
144
+ // Fallback: original issue-by-issue format
145
+ for (const issue of allIssues) {
146
+ const refKey = getRefKey(issue.rule || issue.validator);
147
+ const ref = refKey ? ` — Ref: ${STANDARD_REFS[refKey] || refKey}` : '';
148
+ const location = issue.file
149
+ ? ` (${issue.file}${issue.line ? ':' + issue.line : ''})`
150
+ : '';
151
+ lines.push(` • [${issue.validator}] ${issue.message}${location}${ref}`);
152
+ }
120
153
  }
121
154
 
122
155
  if (attempt >= 2) {
@@ -128,6 +161,22 @@ function buildRemediationContext(featureName, taskId, taskHistory) {
128
161
  injectContext(lines.join('\n'));
129
162
  }
130
163
 
164
+ /**
165
+ * Append score breakdown lines to output.
166
+ * @param {string[]} lines - Output array
167
+ * @param {Object} scores - { score, action, breakdown }
168
+ */
169
+ function appendScoreBreakdown(lines, scores) {
170
+ if (!scores?.breakdown) return;
171
+ const approveThreshold = scores.approveThreshold ?? 8.0;
172
+ for (const [category, data] of Object.entries(scores.breakdown)) {
173
+ if (category === '_unmapped') continue; // skip internal unmapped category
174
+ const pct = Math.round(data.weight * 100);
175
+ const indicator = data.score >= approveThreshold ? '\u2713' : data.score >= 6 ? '\u2193' : '\u2193\u2193';
176
+ lines.push(` ${category}: ${Math.round(data.score * 10) / 10}/10 (weight ${pct}%) ${indicator}`);
177
+ }
178
+ }
179
+
131
180
  /**
132
181
  * Map a rule/validator name to a STANDARD_REFS key.
133
182
  * @param {string} ruleOrValidator
@@ -12,9 +12,10 @@
12
12
  */
13
13
 
14
14
  import { loadState, getActiveFeature, getPendingGates, getMissingOutputs, derivePhaseForFeature, getTaskStats } from '../../shared/state-reader.js';
15
+ import { loadSessionSnapshot, formatPreviousSession } from '../../shared/session-snapshot.js';
15
16
  import { stateExists } from '../../shared/state-reader.js';
16
17
  import { injectContext, pass } from '../../shared/hook-response.js';
17
- import { resetActivity, logHookActivity } from '../../shared/activity-logger.js';
18
+ import { resetActivity, logHookActivity, archiveSession, logEvent } from '../../shared/activity-logger.js';
18
19
  import { readFileSync, existsSync, writeFileSync, renameSync, unlinkSync, statSync } from 'fs';
19
20
  import { join } from 'path';
20
21
  import { resetStaleTasks } from '../../shared/stale-task-reset.js';
@@ -74,10 +75,14 @@ try {
74
75
 
75
76
  const active = getActiveFeature();
76
77
 
77
- // Reset activity log for new session
78
+ // Archive previous session log before resetting
78
79
  const activeFeatureName = active?.name || '';
79
80
  const activePhase = active ? derivePhaseForFeature(active.name) : '';
81
+ archiveSession();
82
+
83
+ // Reset activity log for new session
80
84
  resetActivity(new Date().toISOString(), activeFeatureName, activePhase);
85
+ logEvent('session_start', { feature: activeFeatureName, phase: activePhase });
81
86
  logHookActivity('inject-morph-context', 'SessionStart', 'ok');
82
87
 
83
88
  // ── Stale task cleanup ──────────────────────────────────────────────────────
@@ -201,6 +206,20 @@ try {
201
206
  // Fail-open — worktree context must never block the session
202
207
  }
203
208
  // ────────────────────────────────────────────────────────────────────────────
209
+
210
+ // ── Previous session context ──────────────────────────────────────────────
211
+ // Inject summary from previous session for "getting up to speed"
212
+ try {
213
+ const snapshot = loadSessionSnapshot(process.cwd());
214
+ const previousSessionText = formatPreviousSession(snapshot);
215
+ if (previousSessionText) {
216
+ lines.push('');
217
+ lines.push(previousSessionText);
218
+ }
219
+ } catch {
220
+ // Fail-open — previous session context must never block
221
+ }
222
+ // ────────────────────────────────────────────────────────────────────────
204
223
  } else {
205
224
  // Show summary of all features
206
225
  const featureNames = Object.keys(state.features);