@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.
- package/README.md +2 -2
- package/bin/morph-spec.js +11 -0
- package/bin/task-manager.js +47 -10
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/agents/architecture/architecture-expert.md +98 -0
- package/framework/agents/architecture/morph-spec-architect.md +66 -0
- package/framework/agents/architecture/packages-validator.md +89 -0
- package/framework/agents/architecture/security-expert.md +79 -0
- package/framework/agents/backend/ms-agent-expert.md +1 -1
- package/framework/agents/frontend/design-system-validator.md +100 -0
- package/framework/agents/frontend/nextjs-component-validator.md +97 -0
- package/framework/agents/frontend/{ui-ux-designer.md → ui-designer.md} +31 -0
- package/framework/agents.json +1 -1
- package/framework/hooks/claude-code/post-tool-use/dispatch.js +56 -0
- package/framework/hooks/claude-code/post-tool-use/handle-tool-failure.js +78 -44
- package/framework/hooks/claude-code/post-tool-use/loop-detect.js +103 -0
- package/framework/hooks/claude-code/post-tool-use/validator-feedback.js +60 -11
- package/framework/hooks/claude-code/session-start/inject-morph-context.js +21 -2
- package/framework/hooks/claude-code/stop/validate-completion.js +48 -1
- package/framework/hooks/shared/activity-logger.js +56 -1
- package/framework/hooks/shared/build-check.js +79 -0
- package/framework/hooks/shared/payload-utils.js +39 -0
- package/framework/hooks/shared/session-snapshot.js +105 -0
- package/framework/phases.json +1 -1
- package/framework/rubrics/backend.json +36 -0
- package/framework/rubrics/default.json +36 -0
- package/framework/rubrics/frontend.json +36 -0
- package/framework/skills/level-0-meta/mcp-registry.json +34 -1
- package/framework/skills/level-0-meta/morph-frontend-review/SKILL.md +1 -1
- package/framework/skills/level-1-workflows/morph-phase-codebase-analysis/SKILL.md +1 -1
- package/framework/skills/level-1-workflows/morph-phase-design/SKILL.md +11 -1
- package/framework/skills/level-1-workflows/morph-phase-implement/SKILL.md +12 -1
- package/framework/skills/level-1-workflows/morph-phase-plan/SKILL.md +1 -1
- package/framework/skills/level-1-workflows/morph-phase-tasks/SKILL.md +1 -1
- package/framework/skills/level-1-workflows/morph-phase-uiux/SKILL.md +119 -11
- package/framework/skills/level-1-workflows/morph-scope-escalation/SKILL.md +1 -1
- package/framework/standards/ai-agents/production.md +5 -2
- package/framework/standards/ai-agents/setup.md +13 -14
- package/framework/standards/ai-agents/workflows.md +8 -6
- package/framework/standards/backend/database/vector-search-rag.md +1 -1
- package/framework/templates/code/dotnet/jobs/agent.cs +0 -1
- package/package.json +1 -1
- package/src/commands/dashboard/dashboard.js +53 -0
- package/src/commands/mcp/mcp-setup.js +21 -0
- package/src/commands/project/init.js +11 -0
- package/src/dashboard/app/App.jsx +31 -0
- package/src/dashboard/app/components/DetailPanel.jsx +11 -0
- package/src/dashboard/app/components/FlowGraph.jsx +17 -0
- package/src/dashboard/app/components/SearchFilter.jsx +22 -0
- package/src/dashboard/app/components/Sidebar.jsx +45 -0
- package/src/dashboard/app/hooks/useDetailPanel.js +15 -0
- package/src/dashboard/app/main.jsx +13 -0
- package/src/dashboard/app/pages/AgentMap.jsx +371 -0
- package/src/dashboard/app/pages/HookInspector.jsx +418 -0
- package/src/dashboard/app/pages/LiveState.jsx +239 -0
- package/src/dashboard/app/pages/PhasePipeline.jsx +410 -0
- package/src/dashboard/app/pages/RulesMap.jsx +273 -0
- package/src/dashboard/app/pages/SkillsLibrary.jsx +367 -0
- package/src/dashboard/app/pages/Standards.jsx +332 -0
- package/src/dashboard/app/pages/Workflows.jsx +346 -0
- package/src/dashboard/app/styles/index.css +47 -0
- package/src/dashboard/index.html +12 -0
- package/src/dashboard/package.json +24 -0
- package/src/dashboard/scanner/agents-scanner.js +35 -0
- package/src/dashboard/scanner/hooks-scanner.js +41 -0
- package/src/dashboard/scanner/index.js +21 -0
- package/src/dashboard/scanner/phases-scanner.js +15 -0
- package/src/dashboard/scanner/rubrics-scanner.js +12 -0
- package/src/dashboard/scanner/rules-scanner.js +37 -0
- package/src/dashboard/scanner/skills-scanner.js +43 -0
- package/src/dashboard/scanner/squads-scanner.js +13 -0
- package/src/dashboard/scanner/standards-scanner.js +26 -0
- package/src/dashboard/scanner/state-scanner.js +8 -0
- package/src/dashboard/scanner/workflows-scanner.js +18 -0
- package/src/dashboard/vite-plugin-morph-data.js +17 -0
- package/src/dashboard/vite.config.js +24 -0
- package/src/lib/checkpoints/checkpoint-hooks.js +2 -1
- package/src/lib/contracts/sprint-contract-generator.js +137 -0
- package/src/lib/generators/recap-generator.js +15 -0
- package/src/lib/installers/mcp-installer.js +25 -0
- package/src/lib/validators/score-aggregator.js +244 -0
- package/src/lib/validators/validation-runner.js +44 -0
- package/src/scripts/setup-infra.js +8 -0
- 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
|
package/framework/agents.json
CHANGED
|
@@ -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\
|
|
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
|
-
*
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
|
99
|
-
|
|
100
|
-
lines.push(
|
|
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
|
-
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
//
|
|
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);
|