@polymorphism-tech/morph-spec 4.2.0 → 4.3.1
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/CLAUDE.md +108 -946
- package/bin/morph-spec.js +284 -9
- package/bin/task-manager.cjs +102 -14
- package/bin/validate.js +4 -4
- package/docs/{v3.0 → next-generation}/AGENTS.md +1 -1
- package/docs/next-generation/CONTEXT-OPTIMIZATION.md +267 -0
- package/docs/next-generation/EXECUTION-FLOW.md +274 -0
- package/docs/next-generation/META-PROMPTS.md +235 -0
- package/docs/next-generation/MIGRATION-GUIDE.md +253 -0
- package/docs/next-generation/THREAD-MANAGEMENT.md +240 -0
- package/package.json +5 -5
- package/src/commands/agents/agents-fuse.js +97 -0
- package/src/commands/agents/micro-agent.js +112 -0
- package/src/commands/agents/spawn-team.js +69 -4
- package/src/commands/agents/squad-template.js +146 -0
- package/src/commands/analytics/analytics.js +176 -0
- package/src/commands/context/context-prime.js +63 -0
- package/src/commands/context/core-four.js +54 -0
- package/src/commands/mcp/mcp.js +102 -0
- package/src/commands/project/detect-agents.js +32 -2
- package/src/commands/project/detect.js +11 -1
- package/src/commands/project/doctor.js +573 -356
- package/src/commands/project/init.js +9 -2
- package/src/commands/project/update.js +13 -3
- package/src/commands/state/advance-phase.js +448 -416
- package/src/commands/state/state.js +14 -12
- package/src/commands/tasks/task.js +1 -1
- package/src/commands/templates/template-render.js +80 -1
- package/src/commands/threads/thread-template.js +103 -0
- package/src/commands/threads/threads.js +261 -0
- package/src/commands/trust/trust.js +205 -0
- package/src/{orchestrator.js → core/orchestrator.js} +8 -8
- package/src/core/state/state-manager.js +37 -17
- package/src/core/workflows/workflow-detector.js +114 -3
- package/src/lib/agents/micro-agent-factory.js +161 -0
- package/src/lib/analytics/analytics-engine.js +345 -0
- package/src/lib/checkpoints/checkpoint-hooks.js +298 -258
- package/src/lib/context/context-bundler.js +240 -0
- package/src/lib/context/context-optimizer.js +212 -0
- package/src/lib/context/context-tracker.js +273 -0
- package/src/lib/context/core-four-tracker.js +201 -0
- package/src/lib/context/mcp-optimizer.js +200 -0
- package/src/lib/detectors/index.js +1 -1
- package/src/lib/detectors/standards-generator.js +77 -17
- package/src/lib/detectors/structure-detector.js +67 -39
- package/src/lib/execution/fusion-executor.js +304 -0
- package/src/lib/execution/parallel-executor.js +270 -0
- package/src/lib/generators/context-generator.js +3 -3
- package/src/lib/generators/recap-generator.js +32 -12
- package/src/lib/hooks/hook-executor.js +169 -0
- package/src/lib/hooks/stop-hook-executor.js +286 -0
- package/src/lib/hops/hop-composer.js +221 -0
- package/src/lib/threads/thread-coordinator.js +238 -0
- package/src/lib/threads/thread-manager.js +317 -0
- package/src/lib/tracking/artifact-trail.js +202 -0
- package/src/lib/trust/trust-manager.js +269 -0
- package/src/lib/validators/design-system/design-system-validator.js +2 -2
- package/src/lib/validators/validation-runner.js +14 -30
- package/src/utils/hooks-installer.js +69 -0
- package/stacks/blazor-azure/.morph/config/agents.json +72 -3
- package/stacks/nextjs-supabase/.morph/config/agents.json +3 -3
- package/docs/llm-interaction-config.md +0 -735
- package/docs/v3.0/EXECUTION-FLOW.md +0 -1304
- package/src/commands/utils/migrate-state.js +0 -158
- package/src/commands/utils/upgrade.js +0 -346
- package/src/lib/validators/architecture-validator.js +0 -60
- package/src/lib/validators/content-validator.js +0 -164
- package/src/lib/validators/package-validator.js +0 -61
- package/src/lib/validators/ui-contrast-validator.js +0 -44
- package/stacks/blazor-azure/.claude/commands/morph-apply.md +0 -221
- package/stacks/blazor-azure/.claude/commands/morph-archive.md +0 -79
- package/stacks/blazor-azure/.claude/commands/morph-deploy.md +0 -529
- package/stacks/blazor-azure/.claude/commands/morph-infra.md +0 -209
- package/stacks/blazor-azure/.claude/commands/morph-preflight.md +0 -227
- package/stacks/blazor-azure/.claude/commands/morph-proposal.md +0 -122
- package/stacks/blazor-azure/.claude/commands/morph-status.md +0 -86
- package/stacks/blazor-azure/.claude/commands/morph-troubleshoot.md +0 -122
- package/stacks/blazor-azure/.claude/skills/level-0-meta/README.md +0 -7
- package/stacks/blazor-azure/.claude/skills/level-0-meta/code-review.md +0 -226
- package/stacks/blazor-azure/.claude/skills/level-0-meta/morph-checklist.md +0 -117
- package/stacks/blazor-azure/.claude/skills/level-0-meta/simulation-checklist.md +0 -77
- package/stacks/blazor-azure/.claude/skills/level-1-workflows/README.md +0 -7
- package/stacks/blazor-azure/.claude/skills/level-1-workflows/morph-replicate.md +0 -213
- package/stacks/blazor-azure/.claude/skills/level-1-workflows/phase-clarify.md +0 -131
- package/stacks/blazor-azure/.claude/skills/level-1-workflows/phase-design.md +0 -213
- package/stacks/blazor-azure/.claude/skills/level-1-workflows/phase-setup.md +0 -106
- package/stacks/blazor-azure/.claude/skills/level-1-workflows/phase-tasks.md +0 -164
- package/stacks/blazor-azure/.claude/skills/level-1-workflows/phase-uiux.md +0 -169
- package/stacks/blazor-azure/.claude/skills/level-2-domains/README.md +0 -14
- package/stacks/blazor-azure/.claude/skills/level-2-domains/ai-agents/ai-system-architect.md +0 -192
- package/stacks/blazor-azure/.claude/skills/level-2-domains/architecture/po-pm-advisor.md +0 -197
- package/stacks/blazor-azure/.claude/skills/level-2-domains/architecture/prompt-engineer.md +0 -189
- package/stacks/blazor-azure/.claude/skills/level-2-domains/architecture/seo-growth-hacker.md +0 -320
- package/stacks/blazor-azure/.claude/skills/level-2-domains/architecture/standards-architect.md +0 -156
- package/stacks/blazor-azure/.claude/skills/level-2-domains/backend/api-designer.md +0 -59
- package/stacks/blazor-azure/.claude/skills/level-2-domains/backend/dotnet-senior.md +0 -77
- package/stacks/blazor-azure/.claude/skills/level-2-domains/backend/ef-modeler.md +0 -58
- package/stacks/blazor-azure/.claude/skills/level-2-domains/backend/hangfire-orchestrator.md +0 -126
- package/stacks/blazor-azure/.claude/skills/level-2-domains/backend/ms-agent-expert.md +0 -45
- package/stacks/blazor-azure/.claude/skills/level-2-domains/frontend/blazor-builder.md +0 -210
- package/stacks/blazor-azure/.claude/skills/level-2-domains/frontend/nextjs-expert.md +0 -154
- package/stacks/blazor-azure/.claude/skills/level-2-domains/frontend/ui-ux-designer.md +0 -191
- package/stacks/blazor-azure/.claude/skills/level-2-domains/infrastructure/azure-architect.md +0 -142
- package/stacks/blazor-azure/.claude/skills/level-2-domains/infrastructure/azure-deploy-specialist.md +0 -699
- package/stacks/blazor-azure/.claude/skills/level-2-domains/infrastructure/bicep-architect.md +0 -126
- package/stacks/blazor-azure/.claude/skills/level-2-domains/infrastructure/container-specialist.md +0 -131
- package/stacks/blazor-azure/.claude/skills/level-2-domains/infrastructure/devops-engineer.md +0 -119
- package/stacks/blazor-azure/.claude/skills/level-2-domains/integrations/asaas-financial.md +0 -130
- package/stacks/blazor-azure/.claude/skills/level-2-domains/integrations/azure-identity.md +0 -142
- package/stacks/blazor-azure/.claude/skills/level-2-domains/integrations/clerk-auth.md +0 -108
- package/stacks/blazor-azure/.claude/skills/level-2-domains/integrations/hangfire-orchestrator.md +0 -64
- package/stacks/blazor-azure/.claude/skills/level-2-domains/integrations/resend-email.md +0 -119
- package/stacks/blazor-azure/.claude/skills/level-2-domains/quality/code-analyzer.md +0 -235
- package/stacks/blazor-azure/.claude/skills/level-2-domains/quality/testing-specialist.md +0 -126
- package/stacks/blazor-azure/.claude/skills/level-3-technologies/README.md +0 -7
- package/stacks/blazor-azure/.claude/skills/level-4-patterns/README.md +0 -7
- package/stacks/blazor-azure/.morph/archive/.gitkeep +0 -25
- package/stacks/blazor-azure/.morph/features/.gitkeep +0 -25
- package/stacks/blazor-azure/.morph/schemas/agent.schema.json +0 -296
- package/stacks/blazor-azure/.morph/schemas/tasks.schema.json +0 -220
- package/stacks/blazor-azure/.morph/specs/.gitkeep +0 -20
- package/stacks/blazor-azure/.morph/test-infra/example.bicep +0 -59
- package/stacks/nextjs-supabase/.claude/commands/morph-apply.md +0 -221
- package/stacks/nextjs-supabase/.claude/commands/morph-archive.md +0 -79
- package/stacks/nextjs-supabase/.claude/commands/morph-deploy.md +0 -529
- package/stacks/nextjs-supabase/.claude/commands/morph-infra.md +0 -209
- package/stacks/nextjs-supabase/.claude/commands/morph-preflight.md +0 -227
- package/stacks/nextjs-supabase/.claude/commands/morph-proposal.md +0 -122
- package/stacks/nextjs-supabase/.claude/commands/morph-status.md +0 -86
- package/stacks/nextjs-supabase/.claude/commands/morph-troubleshoot.md +0 -122
- package/stacks/nextjs-supabase/.claude/settings.local.json +0 -6
- package/stacks/nextjs-supabase/.claude/skills/level-2-domains/backend/dotnet-supabase.md +0 -244
- package/stacks/nextjs-supabase/.claude/skills/level-2-domains/frontend/nextjs-supabase.md +0 -335
- package/stacks/nextjs-supabase/.claude/skills/level-2-domains/infrastructure/easypanel-deployer.md +0 -189
- package/stacks/nextjs-supabase/.claude/skills/level-2-domains/integrations/supabase-expert.md +0 -50
- /package/docs/{v3.0 → next-generation}/ANALYSIS.md +0 -0
- /package/docs/{v3.0 → next-generation}/ARCHITECTURE.md +0 -0
- /package/docs/{v3.0 → next-generation}/FEATURES.md +0 -0
- /package/docs/{v3.0 → next-generation}/README.md +0 -0
- /package/docs/{v3.0 → next-generation}/ROADMAP.md +0 -0
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stop Hook Executor — L-Thread validation loops
|
|
3
|
+
*
|
|
4
|
+
* Manages stop hooks for Long-Running threads.
|
|
5
|
+
* Executes hook scripts, captures output, provides structured feedback.
|
|
6
|
+
*
|
|
7
|
+
* On failure: writes feedback to .morph/stop-hook-feedback/{threadId}.json
|
|
8
|
+
* Agent reads feedback before retrying.
|
|
9
|
+
*
|
|
10
|
+
* Max retries: 5. Interval: configurable (default: 30 min).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
14
|
+
import { join } from 'path';
|
|
15
|
+
import { execSync } from 'child_process';
|
|
16
|
+
import { recordEvent } from '../analytics/analytics-engine.js';
|
|
17
|
+
|
|
18
|
+
const FEEDBACK_DIR = join(process.cwd(), '.morph/stop-hook-feedback');
|
|
19
|
+
const MAX_RETRIES = 5;
|
|
20
|
+
const DEFAULT_INTERVAL_MINUTES = 30;
|
|
21
|
+
|
|
22
|
+
// Hook registry — hooks registered for each thread
|
|
23
|
+
const hookRegistry = new Map(); // threadId → [{ hookPath, config }]
|
|
24
|
+
|
|
25
|
+
// ============================================================================
|
|
26
|
+
// Hook Registration
|
|
27
|
+
// ============================================================================
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Register a stop hook for a thread
|
|
31
|
+
* @param {string} threadId - Thread ID
|
|
32
|
+
* @param {string} hookPath - Path to hook script
|
|
33
|
+
* @param {Object} [config]
|
|
34
|
+
* @param {string[]} [config.triggers] - When to run: ['interval', 'on-stop', 'on-error']
|
|
35
|
+
* @param {number} [config.intervalMinutes] - Interval for periodic execution
|
|
36
|
+
* @param {boolean} [config.blocking] - Whether failure blocks thread progress
|
|
37
|
+
*/
|
|
38
|
+
export function registerStopHook(threadId, hookPath, config = {}) {
|
|
39
|
+
if (!hookRegistry.has(threadId)) {
|
|
40
|
+
hookRegistry.set(threadId, []);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
hookRegistry.get(threadId).push({
|
|
44
|
+
hookPath,
|
|
45
|
+
config: {
|
|
46
|
+
triggers: config.triggers || ['on-stop'],
|
|
47
|
+
intervalMinutes: config.intervalMinutes || DEFAULT_INTERVAL_MINUTES,
|
|
48
|
+
blocking: config.blocking !== false,
|
|
49
|
+
...config
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Get registered hooks for a thread
|
|
56
|
+
* @param {string} threadId
|
|
57
|
+
* @returns {Array}
|
|
58
|
+
*/
|
|
59
|
+
export function getThreadHooks(threadId) {
|
|
60
|
+
return hookRegistry.get(threadId) || [];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ============================================================================
|
|
64
|
+
// Hook Execution
|
|
65
|
+
// ============================================================================
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Execute all stop hooks for a thread
|
|
69
|
+
* @param {string} threadId - Thread ID
|
|
70
|
+
* @param {string} hookType - Trigger type ('interval' | 'on-stop' | 'on-error')
|
|
71
|
+
* @param {Object} [context] - Additional context passed to hooks
|
|
72
|
+
* @returns {Promise<Object>} { passed, feedback, results }
|
|
73
|
+
*/
|
|
74
|
+
export async function executeStopHooks(threadId, hookType = 'on-stop', context = {}) {
|
|
75
|
+
const hooks = hookRegistry.get(threadId) || loadDefaultHooks(threadId);
|
|
76
|
+
const results = [];
|
|
77
|
+
let allPassed = true;
|
|
78
|
+
|
|
79
|
+
for (const hook of hooks) {
|
|
80
|
+
if (!hook.config.triggers.includes(hookType) && !hook.config.triggers.includes('all')) {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const result = await executeHook(hook.hookPath, { threadId, hookType, ...context });
|
|
85
|
+
results.push(result);
|
|
86
|
+
|
|
87
|
+
if (!result.passed && hook.config.blocking) {
|
|
88
|
+
allPassed = false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const passed = allPassed;
|
|
93
|
+
const feedback = buildFeedback(results, passed);
|
|
94
|
+
|
|
95
|
+
if (!passed) {
|
|
96
|
+
saveFeedback(threadId, feedback);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
recordEvent({
|
|
100
|
+
type: passed ? 'stop_hook_passed' : 'stop_hook_failed',
|
|
101
|
+
data: { threadId, hookType, resultsCount: results.length, passed }
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
return { passed, feedback, results };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Execute a single hook script
|
|
109
|
+
* @param {string} hookPath - Path to hook script
|
|
110
|
+
* @param {Object} context - Context passed as environment variables
|
|
111
|
+
* @returns {Promise<Object>} { hookPath, passed, output, error }
|
|
112
|
+
*/
|
|
113
|
+
async function executeHook(hookPath, context) {
|
|
114
|
+
const fullPath = existsSync(hookPath) ? hookPath : join(process.cwd(), hookPath);
|
|
115
|
+
|
|
116
|
+
if (!existsSync(fullPath)) {
|
|
117
|
+
return {
|
|
118
|
+
hookPath,
|
|
119
|
+
passed: true, // Missing hooks don't block
|
|
120
|
+
output: null,
|
|
121
|
+
error: `Hook not found: ${hookPath} (skipped)`,
|
|
122
|
+
skipped: true
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
const env = {
|
|
128
|
+
...process.env,
|
|
129
|
+
MORPH_THREAD_ID: context.threadId || '',
|
|
130
|
+
MORPH_HOOK_TYPE: context.hookType || '',
|
|
131
|
+
MORPH_FEATURE: context.feature || ''
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const output = execSync(`node "${fullPath}"`, {
|
|
135
|
+
encoding: 'utf8',
|
|
136
|
+
stdio: 'pipe',
|
|
137
|
+
timeout: 120000, // 2 minute timeout per hook
|
|
138
|
+
env
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// Parse JSON output from hook
|
|
142
|
+
let parsed;
|
|
143
|
+
try {
|
|
144
|
+
parsed = JSON.parse(output);
|
|
145
|
+
} catch {
|
|
146
|
+
// Non-JSON output means pass (for legacy hooks)
|
|
147
|
+
parsed = { passed: true, output };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
hookPath,
|
|
152
|
+
passed: parsed.passed !== false,
|
|
153
|
+
output: parsed,
|
|
154
|
+
error: parsed.error || null
|
|
155
|
+
};
|
|
156
|
+
} catch (err) {
|
|
157
|
+
return {
|
|
158
|
+
hookPath,
|
|
159
|
+
passed: false,
|
|
160
|
+
output: null,
|
|
161
|
+
error: err.message || 'Hook execution failed'
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ============================================================================
|
|
167
|
+
// Feedback Management
|
|
168
|
+
// ============================================================================
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Build structured feedback from hook results
|
|
172
|
+
* @param {Array} results - Hook execution results
|
|
173
|
+
* @param {boolean} passed - Overall pass/fail
|
|
174
|
+
* @returns {Object} Structured feedback
|
|
175
|
+
*/
|
|
176
|
+
function buildFeedback(results, passed) {
|
|
177
|
+
const failures = results.filter(r => !r.passed && !r.skipped);
|
|
178
|
+
const issues = failures.flatMap(r => {
|
|
179
|
+
const output = r.output;
|
|
180
|
+
if (output?.issues) return output.issues;
|
|
181
|
+
if (output?.errors) return output.errors;
|
|
182
|
+
if (r.error) return [{ message: r.error, severity: 'error' }];
|
|
183
|
+
return [];
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
passed,
|
|
188
|
+
timestamp: new Date().toISOString(),
|
|
189
|
+
hooksRun: results.length,
|
|
190
|
+
hooksFailed: failures.length,
|
|
191
|
+
issues,
|
|
192
|
+
suggestions: failures.flatMap(r => r.output?.suggestions || []),
|
|
193
|
+
nextAction: passed
|
|
194
|
+
? 'continue'
|
|
195
|
+
: failures.length > 0 ? 'fix-and-retry' : 'investigate'
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Save feedback to file for agent to read
|
|
201
|
+
* @param {string} threadId - Thread ID
|
|
202
|
+
* @param {Object} feedback - Feedback object
|
|
203
|
+
* @returns {string} Path to feedback file
|
|
204
|
+
*/
|
|
205
|
+
export function saveFeedback(threadId, feedback) {
|
|
206
|
+
if (!existsSync(FEEDBACK_DIR)) {
|
|
207
|
+
mkdirSync(FEEDBACK_DIR, { recursive: true });
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const filePath = join(FEEDBACK_DIR, `${threadId}.json`);
|
|
211
|
+
writeFileSync(filePath, JSON.stringify(feedback, null, 2), 'utf8');
|
|
212
|
+
return filePath;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Load feedback for a thread (agent reads this after failure)
|
|
217
|
+
* @param {string} threadId - Thread ID
|
|
218
|
+
* @returns {Object|null} Feedback object or null
|
|
219
|
+
*/
|
|
220
|
+
export function loadFeedback(threadId) {
|
|
221
|
+
const filePath = join(FEEDBACK_DIR, `${threadId}.json`);
|
|
222
|
+
if (!existsSync(filePath)) return null;
|
|
223
|
+
try {
|
|
224
|
+
return JSON.parse(readFileSync(filePath, 'utf8'));
|
|
225
|
+
} catch {
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Track retry count for a thread
|
|
232
|
+
* @param {string} threadId
|
|
233
|
+
* @returns {number} Current retry count
|
|
234
|
+
*/
|
|
235
|
+
export function getRetryCount(threadId) {
|
|
236
|
+
const feedback = loadFeedback(threadId);
|
|
237
|
+
return feedback?.retryCount || 0;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Increment retry count
|
|
242
|
+
* @param {string} threadId
|
|
243
|
+
* @returns {number} New retry count
|
|
244
|
+
*/
|
|
245
|
+
export function incrementRetryCount(threadId) {
|
|
246
|
+
const feedback = loadFeedback(threadId) || {};
|
|
247
|
+
feedback.retryCount = (feedback.retryCount || 0) + 1;
|
|
248
|
+
saveFeedback(threadId, feedback);
|
|
249
|
+
return feedback.retryCount;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Check if max retries exceeded
|
|
254
|
+
* @param {string} threadId
|
|
255
|
+
* @param {number} [maxRetries=5]
|
|
256
|
+
* @returns {boolean}
|
|
257
|
+
*/
|
|
258
|
+
export function isMaxRetriesExceeded(threadId, maxRetries = MAX_RETRIES) {
|
|
259
|
+
return getRetryCount(threadId) >= maxRetries;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// ============================================================================
|
|
263
|
+
// Helpers
|
|
264
|
+
// ============================================================================
|
|
265
|
+
|
|
266
|
+
function loadDefaultHooks(threadId) {
|
|
267
|
+
// Load hooks from llm-interaction.json if no hooks registered
|
|
268
|
+
const configPath = join(process.cwd(), '.morph/config/llm-interaction.json');
|
|
269
|
+
if (!existsSync(configPath)) return [];
|
|
270
|
+
|
|
271
|
+
try {
|
|
272
|
+
const config = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
273
|
+
const hookNames = config.stopHooks?.hooks || [];
|
|
274
|
+
|
|
275
|
+
return hookNames.map(name => ({
|
|
276
|
+
hookPath: `framework/hooks/agent-stop/${name}.js`,
|
|
277
|
+
config: {
|
|
278
|
+
triggers: ['on-stop', 'interval'],
|
|
279
|
+
intervalMinutes: config.stopHooks?.interval || DEFAULT_INTERVAL_MINUTES,
|
|
280
|
+
blocking: true
|
|
281
|
+
}
|
|
282
|
+
}));
|
|
283
|
+
} catch {
|
|
284
|
+
return [];
|
|
285
|
+
}
|
|
286
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HOP Composer — Higher-Order Prompt rendering and nesting
|
|
3
|
+
*
|
|
4
|
+
* Compiles Handlebars templates for agent prompts (HOPs).
|
|
5
|
+
* Supports:
|
|
6
|
+
* - renderHOP(templatePath, variables) → compiled prompt string
|
|
7
|
+
* - composeHOPs(hopChain[], variables) → nested HOPs (outer wraps inner)
|
|
8
|
+
* - listAvailableHOPs() → scan meta-prompts/ and return HOPDescriptor[]
|
|
9
|
+
*
|
|
10
|
+
* Standard HOP variables auto-injected:
|
|
11
|
+
* FEATURE_NAME, AGENT_ID, MISSION, SPEC_SUMMARY, STANDARDS, TASKS,
|
|
12
|
+
* DELIVERABLES, CONSTRAINTS, DATE, TIMESTAMP
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { readFileSync, existsSync, readdirSync, statSync } from 'fs';
|
|
16
|
+
import { join, relative, extname, basename } from 'path';
|
|
17
|
+
|
|
18
|
+
const META_PROMPTS_DIR = join(process.cwd(), 'framework/templates/meta-prompts');
|
|
19
|
+
const REGISTRY_PATH = join(process.cwd(), 'framework/templates/REGISTRY.json');
|
|
20
|
+
|
|
21
|
+
// ============================================================================
|
|
22
|
+
// Handlebars-Compatible Mini-Compiler
|
|
23
|
+
// ============================================================================
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Compile a Handlebars template with variables (subset of Handlebars features)
|
|
27
|
+
* Supports: {{variable}}, {{#if condition}}...{{/if}}, {{#each array}}...{{/each}}
|
|
28
|
+
* Also supports built-in helpers: {{pascalCase}}, {{camelCase}}, {{snakeCase}}
|
|
29
|
+
*
|
|
30
|
+
* @param {string} template - Handlebars template string
|
|
31
|
+
* @param {Object} variables - Variables to inject
|
|
32
|
+
* @returns {string} Compiled template
|
|
33
|
+
*/
|
|
34
|
+
export function compileTemplate(template, variables) {
|
|
35
|
+
const vars = {
|
|
36
|
+
DATE: new Date().toISOString().split('T')[0],
|
|
37
|
+
TIMESTAMP: new Date().toISOString(),
|
|
38
|
+
...injectCasedHelpers(variables),
|
|
39
|
+
...variables
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
let result = template;
|
|
43
|
+
|
|
44
|
+
// 1. Process {{#if VAR}}...{{/if}} blocks
|
|
45
|
+
result = result.replace(/\{\{#if\s+(\w+)\}\}([\s\S]*?)\{\{\/if\}\}/g, (_, key, content) => {
|
|
46
|
+
return vars[key] ? content : '';
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// 2. Process {{#each ARRAY}}...{{/each}} blocks
|
|
50
|
+
result = result.replace(/\{\{#each\s+(\w+)\}\}([\s\S]*?)\{\{\/each\}\}/g, (_, key, content) => {
|
|
51
|
+
const arr = vars[key];
|
|
52
|
+
if (!Array.isArray(arr)) return '';
|
|
53
|
+
return arr.map(item => {
|
|
54
|
+
const itemVars = typeof item === 'object' ? { ...vars, ...item, this: item } : { ...vars, this: item };
|
|
55
|
+
return compileTemplate(content, itemVars);
|
|
56
|
+
}).join('');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// 3. Process helper calls: {{pascalCase VARIABLE}}, {{camelCase VARIABLE}}, etc.
|
|
60
|
+
result = result.replace(/\{\{(pascalCase|camelCase|snakeCase|titleCase|pluralize)\s+(\w+)\}\}/g, (_, helper, key) => {
|
|
61
|
+
const value = String(vars[key] || '');
|
|
62
|
+
return applyHelper(helper, value);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// 4. Process simple {{VARIABLE}} substitutions
|
|
66
|
+
result = result.replace(/\{\{(\w+)\}\}/g, (_, key) => {
|
|
67
|
+
return vars[key] !== undefined ? String(vars[key]) : `{{${key}}}`;
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function injectCasedHelpers(variables) {
|
|
74
|
+
const extras = {};
|
|
75
|
+
for (const [key, value] of Object.entries(variables)) {
|
|
76
|
+
if (typeof value === 'string') {
|
|
77
|
+
extras[`${key}_PASCAL`] = applyHelper('pascalCase', value);
|
|
78
|
+
extras[`${key}_CAMEL`] = applyHelper('camelCase', value);
|
|
79
|
+
extras[`${key}_SNAKE`] = applyHelper('snakeCase', value);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return extras;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function applyHelper(helper, value) {
|
|
86
|
+
const words = value.replace(/[-_\s]+/g, ' ').trim().split(' ');
|
|
87
|
+
switch (helper) {
|
|
88
|
+
case 'pascalCase':
|
|
89
|
+
return words.map(w => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join('');
|
|
90
|
+
case 'camelCase':
|
|
91
|
+
return words[0].toLowerCase() + words.slice(1).map(w => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join('');
|
|
92
|
+
case 'snakeCase':
|
|
93
|
+
return value.replace(/[-\s]+/g, '_').replace(/([a-z])([A-Z])/g, '$1_$2').toLowerCase();
|
|
94
|
+
case 'titleCase':
|
|
95
|
+
return words.map(w => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join(' ');
|
|
96
|
+
case 'pluralize':
|
|
97
|
+
const last = value.slice(-1);
|
|
98
|
+
if (last === 'y') return value.slice(0, -1) + 'ies';
|
|
99
|
+
if (last === 's') return value;
|
|
100
|
+
return value + 's';
|
|
101
|
+
default:
|
|
102
|
+
return value;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ============================================================================
|
|
107
|
+
// HOP Rendering
|
|
108
|
+
// ============================================================================
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Render a HOP template with variables
|
|
112
|
+
* @param {string} templatePath - Path to template (relative to meta-prompts/ or absolute)
|
|
113
|
+
* @param {Object} variables - Variables to inject
|
|
114
|
+
* @returns {string} Rendered prompt string
|
|
115
|
+
*/
|
|
116
|
+
export function renderHOP(templatePath, variables = {}) {
|
|
117
|
+
// Resolve template path
|
|
118
|
+
const paths = [
|
|
119
|
+
templatePath,
|
|
120
|
+
join(META_PROMPTS_DIR, templatePath),
|
|
121
|
+
join(META_PROMPTS_DIR, templatePath + '.md'),
|
|
122
|
+
join(process.cwd(), templatePath)
|
|
123
|
+
];
|
|
124
|
+
|
|
125
|
+
let resolved = null;
|
|
126
|
+
for (const p of paths) {
|
|
127
|
+
if (existsSync(p)) {
|
|
128
|
+
resolved = p;
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (!resolved) {
|
|
134
|
+
throw new Error(`HOP template not found: ${templatePath}\nSearched: ${paths.join(', ')}`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const template = readFileSync(resolved, 'utf8');
|
|
138
|
+
return compileTemplate(template, variables);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Compose nested HOPs (outer wraps inner, variables bubble down)
|
|
143
|
+
* @param {Array} hopChain - Array of { templatePath, variables } objects (outer first)
|
|
144
|
+
* @param {Object} [sharedVariables] - Variables shared across all HOPs
|
|
145
|
+
* @returns {string} Composed prompt string
|
|
146
|
+
*/
|
|
147
|
+
export function composeHOPs(hopChain, sharedVariables = {}) {
|
|
148
|
+
if (!hopChain || hopChain.length === 0) {
|
|
149
|
+
throw new Error('hopChain must be a non-empty array');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (hopChain.length === 1) {
|
|
153
|
+
return renderHOP(hopChain[0].templatePath, { ...sharedVariables, ...hopChain[0].variables });
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Render inner HOPs first, then wrap with outer
|
|
157
|
+
let innerContent = '';
|
|
158
|
+
for (let i = hopChain.length - 1; i >= 1; i--) {
|
|
159
|
+
const hop = hopChain[i];
|
|
160
|
+
const vars = { ...sharedVariables, ...hop.variables, INNER_PROMPT: innerContent };
|
|
161
|
+
innerContent = renderHOP(hop.templatePath, vars);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Render outermost HOP with inner content
|
|
165
|
+
const outerHop = hopChain[0];
|
|
166
|
+
return renderHOP(outerHop.templatePath, {
|
|
167
|
+
...sharedVariables,
|
|
168
|
+
...outerHop.variables,
|
|
169
|
+
INNER_PROMPT: innerContent
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ============================================================================
|
|
174
|
+
// HOP Discovery
|
|
175
|
+
// ============================================================================
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* List all available HOPs from the meta-prompts directory
|
|
179
|
+
* @returns {Array} Array of HOPDescriptor objects
|
|
180
|
+
*/
|
|
181
|
+
export function listAvailableHOPs() {
|
|
182
|
+
if (!existsSync(META_PROMPTS_DIR)) return [];
|
|
183
|
+
|
|
184
|
+
const hops = [];
|
|
185
|
+
scanDir(META_PROMPTS_DIR, META_PROMPTS_DIR, hops);
|
|
186
|
+
return hops;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function scanDir(dir, baseDir, hops) {
|
|
190
|
+
if (!existsSync(dir)) return;
|
|
191
|
+
|
|
192
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
193
|
+
const fullPath = join(dir, entry.name);
|
|
194
|
+
if (entry.isDirectory()) {
|
|
195
|
+
scanDir(fullPath, baseDir, hops);
|
|
196
|
+
} else if (entry.name.endsWith('.md') || entry.name.endsWith('.hbs')) {
|
|
197
|
+
const relPath = relative(baseDir, fullPath);
|
|
198
|
+
const content = readFileSync(fullPath, 'utf8');
|
|
199
|
+
const firstLine = content.split('\n')[0] || '';
|
|
200
|
+
const description = firstLine.startsWith('#')
|
|
201
|
+
? firstLine.replace(/^#+\s*/, '')
|
|
202
|
+
: 'Higher-Order Prompt template';
|
|
203
|
+
|
|
204
|
+
// Extract variables from template
|
|
205
|
+
const variables = [...new Set(
|
|
206
|
+
(content.match(/\{\{(\w+)\}\}/g) || [])
|
|
207
|
+
.map(m => m.replace(/\{\{|\}\}/g, ''))
|
|
208
|
+
.filter(v => !['if', 'each', 'this', 'else', 'pascalCase', 'camelCase', 'snakeCase', 'titleCase', 'pluralize'].includes(v))
|
|
209
|
+
)];
|
|
210
|
+
|
|
211
|
+
hops.push({
|
|
212
|
+
id: relPath.replace(/\\/g, '/').replace(/\.(md|hbs)$/, ''),
|
|
213
|
+
path: fullPath,
|
|
214
|
+
relativePath: relPath.replace(/\\/g, '/'),
|
|
215
|
+
description,
|
|
216
|
+
variables,
|
|
217
|
+
category: relPath.split(/[\\/]/)[0]
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|