@polymorphism-tech/morph-spec 4.10.5 → 4.10.7
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 +3 -0
- package/README.md +3 -3
- package/bin/morph-spec.js +90 -1
- package/bin/task-manager.js +59 -6
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/agents/backend/hangfire-orchestrator.md +1 -1
- package/framework/agents/backend/ms-agent-expert.md +2 -1
- package/framework/agents/integrations/asaas-financial.md +1 -1
- package/framework/agents/integrations/clerk-auth.md +1 -2
- package/framework/agents/integrations/hangfire-integration.md +1 -1
- package/framework/agents/integrations/resend-email.md +1 -1
- package/framework/agents.json +345 -708
- package/framework/hooks/claude-code/post-tool-use/dispatch.js +10 -5
- package/framework/skills/README.md +0 -1
- package/framework/skills/level-1-workflows/morph-phase-clarify/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 +1 -1
- package/framework/skills/level-1-workflows/morph-phase-implement/SKILL.md +42 -13
- package/framework/skills/level-1-workflows/morph-phase-plan/SKILL.md +1 -1
- package/framework/skills/level-1-workflows/morph-phase-setup/SKILL.md +16 -12
- package/framework/skills/level-1-workflows/morph-phase-tasks/SKILL.md +1 -1
- package/framework/skills/level-1-workflows/morph-phase-uiux/SKILL.md +1 -1
- package/framework/skills/level-1-workflows/morph-scope-escalation/SKILL.md +1 -1
- package/package.json +1 -1
- package/src/commands/agents/agent-metrics.js +174 -0
- package/src/commands/agents/audit-dispatch.js +82 -0
- package/src/commands/agents/detect-agents.js +275 -0
- package/src/commands/agents/dispatch-agents.js +41 -135
- package/src/commands/agents/index.js +9 -0
- package/src/commands/agents/manage-agents.js +352 -0
- package/src/commands/project/doctor.js +61 -2
- package/src/commands/project/init.js +10 -2
- package/src/commands/project/update.js +12 -25
- package/src/commands/state/advance-phase.js +11 -3
- package/src/core/state/state-manager.js +81 -0
- package/src/lib/agents/agents-manager.js +349 -0
- package/src/lib/agents/metrics-calculator.js +125 -0
- package/src/lib/agents/output-collector.js +75 -0
- package/src/lib/generators/context-generator.js +8 -7
- package/src/lib/tasks/task-parser.js +94 -3
- package/src/lib/validators/checkpoint-persistence.js +39 -0
- package/src/lib/validators/content/content-validator.js +9 -1
- package/src/lib/validators/spec-validator.js +2 -2
- package/src/lib/validators/validation-runner.js +20 -0
- package/src/scripts/setup-infra.js +1 -17
- package/src/utils/agent-id-mapper.js +11 -0
- package/src/utils/agents-installer.js +40 -8
- package/src/utils/claude-settings-manager.js +1 -1
- package/src/utils/hooks-installer.js +0 -245
- package/src/utils/tier-helpers.js +24 -0
- package/framework/agents/architecture/po-pm-advisor.md +0 -203
- package/framework/hooks/claude-code/user-prompt/set-terminal-title.js +0 -68
|
@@ -1,13 +1,104 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Task Parser — Shared utilities for reading and
|
|
2
|
+
* Task Parser — Shared utilities for reading, parsing, and grouping tasks.
|
|
3
3
|
*
|
|
4
|
-
* Extracted from bin/task-manager.cjs
|
|
5
|
-
*
|
|
4
|
+
* Extracted from bin/task-manager.cjs and dispatch-agents.js so all
|
|
5
|
+
* consumers share the same parsing logic without duplication.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { readFile } from 'fs/promises';
|
|
9
|
+
import { readFileSync, existsSync } from 'fs';
|
|
9
10
|
import { join } from 'path';
|
|
10
11
|
|
|
12
|
+
// ============================================================================
|
|
13
|
+
// TASK_CATEGORY_TO_GROUP — maps task categories to parallel groups
|
|
14
|
+
// ============================================================================
|
|
15
|
+
|
|
16
|
+
const TASK_CATEGORY_TO_GROUP = {
|
|
17
|
+
'domain': 'backend',
|
|
18
|
+
'domain-bc': 'backend',
|
|
19
|
+
'infrastructure': 'backend',
|
|
20
|
+
'application': 'backend',
|
|
21
|
+
'presentation': 'frontend',
|
|
22
|
+
'tests': 'tests',
|
|
23
|
+
'infra': 'infra',
|
|
24
|
+
'docs': 'docs',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// ============================================================================
|
|
28
|
+
// parseTaskList — structured task parsing with error reporting
|
|
29
|
+
// ============================================================================
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Parse tasks.md content into structured task objects with error reporting.
|
|
33
|
+
*
|
|
34
|
+
* @param {string} content - Raw tasks.md content
|
|
35
|
+
* @returns {{ tasks: ParsedTask[], errors: TaskParseError[], groups: Object<string, string[]> }}
|
|
36
|
+
*/
|
|
37
|
+
export function parseTaskList(content) {
|
|
38
|
+
const tasks = [];
|
|
39
|
+
const errors = [];
|
|
40
|
+
const groups = {};
|
|
41
|
+
|
|
42
|
+
const taskHeadingRe = /^###\s+(T\d{3,})\s*[—–:\-]\s*(.+)$/gm;
|
|
43
|
+
const groupRe = /(?:\*\*Group\*\*\s*:\s*`?([a-z-]+)`?|- \*\*Group:\*\*\s*`?([a-z-]+)`?)/i;
|
|
44
|
+
const categoryRe = /(?:\*\*category\*\*\s*:\s*`?([a-z-]+)`?|category:\s*([a-z-]+))/i;
|
|
45
|
+
|
|
46
|
+
let headingMatch;
|
|
47
|
+
const taskPositions = [];
|
|
48
|
+
|
|
49
|
+
while ((headingMatch = taskHeadingRe.exec(content)) !== null) {
|
|
50
|
+
taskPositions.push({ id: headingMatch[1], title: headingMatch[2].trim(), pos: headingMatch.index });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
for (let i = 0; i < taskPositions.length; i++) {
|
|
54
|
+
const { id, title, pos } = taskPositions[i];
|
|
55
|
+
const end = i + 1 < taskPositions.length ? taskPositions[i + 1].pos : content.length;
|
|
56
|
+
const block = content.slice(pos, end);
|
|
57
|
+
|
|
58
|
+
// Try group field first (v4), fall back to category field (v3)
|
|
59
|
+
const grpMatch = groupRe.exec(block);
|
|
60
|
+
const catMatch = categoryRe.exec(block);
|
|
61
|
+
let group;
|
|
62
|
+
if (grpMatch) {
|
|
63
|
+
group = (grpMatch[1] || grpMatch[2]).toLowerCase();
|
|
64
|
+
} else if (catMatch) {
|
|
65
|
+
const category = (catMatch[1] || catMatch[2]).toLowerCase();
|
|
66
|
+
group = TASK_CATEGORY_TO_GROUP[category] || 'backend';
|
|
67
|
+
} else {
|
|
68
|
+
group = 'backend';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
tasks.push({ id, title, group });
|
|
72
|
+
|
|
73
|
+
if (!groups[group]) groups[group] = [];
|
|
74
|
+
groups[group].push(id);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return { tasks, errors, groups };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ============================================================================
|
|
81
|
+
// parseAndGroupTasks — drop-in replacement for dispatch-agents.js
|
|
82
|
+
// ============================================================================
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Parse tasks.md and return a map of { parallelGroup -> taskId[] }.
|
|
86
|
+
* Gracefully returns {} if the file doesn't exist or is unparseable.
|
|
87
|
+
*
|
|
88
|
+
* @param {string} tasksPath - Absolute path to tasks.md
|
|
89
|
+
* @returns {Object<string, string[]>}
|
|
90
|
+
*/
|
|
91
|
+
export function parseAndGroupTasks(tasksPath) {
|
|
92
|
+
if (!existsSync(tasksPath)) return {};
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
const content = readFileSync(tasksPath, 'utf8');
|
|
96
|
+
return parseTaskList(content).groups;
|
|
97
|
+
} catch {
|
|
98
|
+
return {};
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
11
102
|
// ============================================================================
|
|
12
103
|
// parseTasksMd
|
|
13
104
|
// ============================================================================
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checkpoint Validation Persistence
|
|
3
|
+
*
|
|
4
|
+
* Persists checkpoint validation results to disk for traceability.
|
|
5
|
+
* Each checkpoint writes a JSON file to:
|
|
6
|
+
* .morph/features/{feature}/5-implement/validations/checkpoint-{N}.json
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { mkdirSync, writeFileSync } from 'fs';
|
|
10
|
+
import { join } from 'path';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Persist a checkpoint validation result to disk.
|
|
14
|
+
*
|
|
15
|
+
* @param {string} projectPath - Project root
|
|
16
|
+
* @param {string} featureName - Feature name
|
|
17
|
+
* @param {{ checkpointNum: number, timestamp: string, passed: boolean, validators: Object, summary: string, feature?: string, phase?: string }} result - Validation result
|
|
18
|
+
* @returns {string} Path to the saved JSON file
|
|
19
|
+
*/
|
|
20
|
+
export function persistCheckpointValidation(projectPath, featureName, result) {
|
|
21
|
+
const dir = join(projectPath, `.morph/features/${featureName}/5-implement/validations`);
|
|
22
|
+
mkdirSync(dir, { recursive: true });
|
|
23
|
+
|
|
24
|
+
const filename = `checkpoint-${result.checkpointNum}.json`;
|
|
25
|
+
const filePath = join(dir, filename);
|
|
26
|
+
|
|
27
|
+
const output = {
|
|
28
|
+
checkpointNum: result.checkpointNum,
|
|
29
|
+
timestamp: result.timestamp || new Date().toISOString(),
|
|
30
|
+
passed: result.passed,
|
|
31
|
+
validators: result.validators || {},
|
|
32
|
+
summary: result.summary || '',
|
|
33
|
+
feature: result.feature || featureName,
|
|
34
|
+
phase: result.phase || 'implement',
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
writeFileSync(filePath, JSON.stringify(output, null, 2), 'utf8');
|
|
38
|
+
return filePath;
|
|
39
|
+
}
|
|
@@ -41,6 +41,14 @@ export function validateSpecContent(specPath) {
|
|
|
41
41
|
const errors = [];
|
|
42
42
|
const warnings = [];
|
|
43
43
|
|
|
44
|
+
// Missing sections are warnings, not errors — not every spec needs all sections
|
|
45
|
+
// (e.g., frontend-only features may not have Data Model, CLI tools may not have Technical Architecture)
|
|
46
|
+
if (missing.length > 0) {
|
|
47
|
+
missing.forEach(section => {
|
|
48
|
+
warnings.push(`Missing recommended section: ${section}`);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
44
52
|
// Check minimum content length
|
|
45
53
|
if (content.length < 500) {
|
|
46
54
|
warnings.push('Spec seems very short (< 500 characters). Consider adding more detail.');
|
|
@@ -70,7 +78,7 @@ export function validateSpecContent(specPath) {
|
|
|
70
78
|
}
|
|
71
79
|
|
|
72
80
|
return {
|
|
73
|
-
valid:
|
|
81
|
+
valid: errors.length === 0,
|
|
74
82
|
missing,
|
|
75
83
|
errors,
|
|
76
84
|
warnings,
|
|
@@ -75,9 +75,9 @@ function validateSpecMd(content) {
|
|
|
75
75
|
for (const section of requiredSections) {
|
|
76
76
|
if (!section.pattern.test(content)) {
|
|
77
77
|
issues.push({
|
|
78
|
-
level: '
|
|
78
|
+
level: 'warning',
|
|
79
79
|
type: 'spec',
|
|
80
|
-
message: `Missing
|
|
80
|
+
message: `Missing recommended section: "${section.name}"`,
|
|
81
81
|
solution: `Add ## ${section.name} section to spec.md`
|
|
82
82
|
});
|
|
83
83
|
}
|
|
@@ -20,6 +20,15 @@ const DOTNET_ONLY_VALIDATORS = new Set([
|
|
|
20
20
|
'blazor-state', 'contract-compliance'
|
|
21
21
|
]);
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Heavy validators skipped in 'light' mode (checkpoint-only).
|
|
25
|
+
* Light mode runs only spec, content, and stack-specific lightweight validators.
|
|
26
|
+
*/
|
|
27
|
+
const HEAVY_VALIDATORS = new Set([
|
|
28
|
+
'architecture', 'packages', 'contract-compliance',
|
|
29
|
+
'blazor-concurrency', 'css', 'design-system'
|
|
30
|
+
]);
|
|
31
|
+
|
|
23
32
|
/**
|
|
24
33
|
* Load agent → validators map from agents.json (data-driven)
|
|
25
34
|
* @param {string} projectPath - Root path of the project
|
|
@@ -74,6 +83,17 @@ export async function runValidation(projectPath, featureName, options = {}) {
|
|
|
74
83
|
// Determine which validators to run
|
|
75
84
|
const validatorIds = options.validators || detectValidators(featureName, projectPath);
|
|
76
85
|
|
|
86
|
+
// Filter by mode: 'light' skips heavy validators, 'full' (default) runs all
|
|
87
|
+
const mode = options.mode || 'full';
|
|
88
|
+
if (mode === 'light') {
|
|
89
|
+
const beforeCount = validatorIds.length;
|
|
90
|
+
for (let i = validatorIds.length - 1; i >= 0; i--) {
|
|
91
|
+
if (HEAVY_VALIDATORS.has(validatorIds[i])) {
|
|
92
|
+
validatorIds.splice(i, 1);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
77
97
|
// Merge constraint-enabled validators
|
|
78
98
|
validatorIds.push(...constraints.enabledValidators);
|
|
79
99
|
|
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
import { parseStacks, getProjectTags, shouldInstall } from '../lib/stack-filter.js';
|
|
28
28
|
import { injectMorphImport } from '../utils/claude-md-injector.js';
|
|
29
29
|
import { saveProjectMorphVersion, getInstalledCLIVersion } from '../utils/version-checker.js';
|
|
30
|
-
import { installClaudeHooks, installGlobalStatusline
|
|
30
|
+
import { installClaudeHooks, installGlobalStatusline } from '../utils/claude-settings-manager.js';
|
|
31
31
|
import { installSkills } from '../utils/skills-installer.js';
|
|
32
32
|
import { installAgents, installDomainAgents } from '../utils/agents-installer.js';
|
|
33
33
|
|
|
@@ -238,22 +238,6 @@ export async function setupInfra(targetPath, { _exec = execSync } = {}) {
|
|
|
238
238
|
log('Step 16: Installing Claude Code hooks...');
|
|
239
239
|
await installClaudeHooks(targetPath);
|
|
240
240
|
|
|
241
|
-
// --- 16a. installShellIntegration (non-critical) ---
|
|
242
|
-
log('Step 16a: Installing shell integration...');
|
|
243
|
-
try {
|
|
244
|
-
await installShellIntegration();
|
|
245
|
-
} catch {
|
|
246
|
-
// Non-critical — shell profile may not be writable
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
// --- 16b. installVSCodeTerminalSettings (non-critical) ---
|
|
250
|
-
log('Step 16b: Configuring VS Code terminal settings...');
|
|
251
|
-
try {
|
|
252
|
-
await installVSCodeTerminalSettings();
|
|
253
|
-
} catch {
|
|
254
|
-
// Non-critical — VS Code settings may not be present
|
|
255
|
-
}
|
|
256
|
-
|
|
257
241
|
// --- 17. saveProjectMorphVersion ---
|
|
258
242
|
log('Step 17: Saving version info...');
|
|
259
243
|
const cliVersion = getInstalledCLIVersion();
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Map an agents.json ID to the Claude Code subagent_type.
|
|
3
|
+
*
|
|
4
|
+
* @param {string} id - Agent ID from agents.json (e.g., "nextjs-expert")
|
|
5
|
+
* @param {number} [tier] - Agent tier (1-4). Defaults to non-validator.
|
|
6
|
+
* @returns {string} subagent_type matching .claude/agents/ filename stem
|
|
7
|
+
*/
|
|
8
|
+
export function agentIdToSubagentType(id, tier) {
|
|
9
|
+
const prefix = tier === 4 ? 'morph-validator-' : 'morph-';
|
|
10
|
+
return `${prefix}${id}`;
|
|
11
|
+
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* maxTurns, skills, memory) followed by the agent's spawn prompt as the body.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, statSync, unlinkSync } from 'node:fs';
|
|
11
|
+
import { readFileSync, writeFileSync, appendFileSync, mkdirSync, existsSync, readdirSync, statSync, unlinkSync } from 'node:fs';
|
|
12
12
|
import { join } from 'path';
|
|
13
13
|
import { parseStacks, shouldInstall } from '../lib/stack-filter.js';
|
|
14
14
|
|
|
@@ -88,6 +88,13 @@ export async function installAgents(projectDir, frameworkDir = 'framework', opti
|
|
|
88
88
|
if (allPossible.has(file) && !installed.has(file)) {
|
|
89
89
|
unlinkSync(join(targetDir, file));
|
|
90
90
|
}
|
|
91
|
+
// Remove morph-domain-{name}.md when morph-{name}.md was installed from agents.json
|
|
92
|
+
if (file.startsWith('morph-domain-')) {
|
|
93
|
+
const suffix = file.slice('morph-domain-'.length);
|
|
94
|
+
if (installed.has(`morph-${suffix}`)) {
|
|
95
|
+
unlinkSync(join(targetDir, file));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
91
98
|
}
|
|
92
99
|
|
|
93
100
|
return { tier1, tier2, tier3, tier4 };
|
|
@@ -170,6 +177,19 @@ function buildDescription(agent) {
|
|
|
170
177
|
return base.length > 200 ? base.slice(0, 197) + '...' : base;
|
|
171
178
|
}
|
|
172
179
|
|
|
180
|
+
/**
|
|
181
|
+
* Serialize an AgentStandard to a readable markdown line.
|
|
182
|
+
*
|
|
183
|
+
* @param {string|{id: string, scope?: string, priority?: string}} standard
|
|
184
|
+
* @returns {string} e.g., "- core-coding (all, required)" or "- core-coding"
|
|
185
|
+
*/
|
|
186
|
+
export function formatStandardRef(standard) {
|
|
187
|
+
if (typeof standard === 'string') return `- ${standard}`;
|
|
188
|
+
const id = standard.id || String(standard);
|
|
189
|
+
const parts = [standard.scope, standard.priority].filter(Boolean);
|
|
190
|
+
return parts.length > 0 ? `- ${id} (${parts.join(', ')})` : `- ${id}`;
|
|
191
|
+
}
|
|
192
|
+
|
|
173
193
|
function buildBody(agent) {
|
|
174
194
|
const lines = [];
|
|
175
195
|
if (agent.teammate?.spawn_prompt) {
|
|
@@ -181,7 +201,7 @@ function buildBody(agent) {
|
|
|
181
201
|
}
|
|
182
202
|
}
|
|
183
203
|
if (agent.standards?.length) {
|
|
184
|
-
lines.push(`\n## Standards to Reference\n${agent.standards.map(
|
|
204
|
+
lines.push(`\n## Standards to Reference\n${agent.standards.map(formatStandardRef).join('\n')}`);
|
|
185
205
|
}
|
|
186
206
|
return lines.join('\n');
|
|
187
207
|
}
|
|
@@ -270,11 +290,19 @@ export async function installDomainAgents(projectDir, frameworkDir = 'framework'
|
|
|
270
290
|
'',
|
|
271
291
|
].join('\n');
|
|
272
292
|
|
|
273
|
-
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
293
|
+
// If agents.json already generated morph-{name}.md, append domain content there
|
|
294
|
+
const agentsJsonFile = `morph-${name}.md`;
|
|
295
|
+
const agentsJsonPath = join(targetDir, agentsJsonFile);
|
|
296
|
+
if (existsSync(agentsJsonPath)) {
|
|
297
|
+
appendFileSync(agentsJsonPath, `\n\n---\n\n## Domain Knowledge\n\n${body.trimStart()}`, 'utf-8');
|
|
298
|
+
specialists++;
|
|
299
|
+
} else {
|
|
300
|
+
const filename = `morph-domain-${name}.md`;
|
|
301
|
+
const targetPath = join(targetDir, filename);
|
|
302
|
+
writeFileSync(targetPath, `---\n${frontmatter}---\n\n${body.trimStart()}`, 'utf-8');
|
|
303
|
+
installed.add(filename);
|
|
304
|
+
specialists++;
|
|
305
|
+
}
|
|
278
306
|
}
|
|
279
307
|
|
|
280
308
|
// Cleanup orphan morph-domain-* files
|
|
@@ -333,7 +361,7 @@ export function checkMissingAgents(projectDir, frameworkDir, projectTags = []) {
|
|
|
333
361
|
}
|
|
334
362
|
}
|
|
335
363
|
|
|
336
|
-
// Check domain agents
|
|
364
|
+
// Check domain agents (skip if merged into agents.json entry)
|
|
337
365
|
const domainsDir = join(frameworkDir, 'agents');
|
|
338
366
|
if (existsSync(domainsDir)) {
|
|
339
367
|
for (const filePath of collectSkillFiles(domainsDir)) {
|
|
@@ -342,6 +370,10 @@ export function checkMissingAgents(projectDir, frameworkDir, projectTags = []) {
|
|
|
342
370
|
if (!name) continue;
|
|
343
371
|
if (!shouldInstall(stacks, projectTags)) continue;
|
|
344
372
|
|
|
373
|
+
// If agents.json has morph-{name}.md, domain content is merged there
|
|
374
|
+
const agentsJsonFile = `morph-${name}.md`;
|
|
375
|
+
if (installedFiles.has(agentsJsonFile)) continue;
|
|
376
|
+
|
|
345
377
|
expected++;
|
|
346
378
|
const filename = `morph-domain-${name}.md`;
|
|
347
379
|
if (!installedFiles.has(filename)) {
|
|
@@ -17,7 +17,7 @@ const MORPH_MANAGED_KEY = '_morph_managed';
|
|
|
17
17
|
// Re-export from hooks-installer (v2 registry-based)
|
|
18
18
|
// ============================================================================
|
|
19
19
|
|
|
20
|
-
export { installClaudeHooks, installGlobalStatusline,
|
|
20
|
+
export { installClaudeHooks, installGlobalStatusline, getHooksVersion } from './hooks-installer.js';
|
|
21
21
|
export { installAgents } from './agents-installer.js';
|
|
22
22
|
|
|
23
23
|
// ============================================================================
|
|
@@ -79,10 +79,6 @@ const MORPH_HOOKS = [
|
|
|
79
79
|
type: 'command',
|
|
80
80
|
command: 'node framework/hooks/claude-code/user-prompt/enrich-prompt.js'
|
|
81
81
|
},
|
|
82
|
-
{
|
|
83
|
-
type: 'command',
|
|
84
|
-
command: 'node framework/hooks/claude-code/user-prompt/set-terminal-title.js'
|
|
85
|
-
}
|
|
86
82
|
]
|
|
87
83
|
},
|
|
88
84
|
|
|
@@ -416,248 +412,7 @@ export async function installGlobalStatusline(hooksSourceDir, globalClaudeDirOve
|
|
|
416
412
|
return { installed: true, globalDir };
|
|
417
413
|
}
|
|
418
414
|
|
|
419
|
-
/**
|
|
420
|
-
* Install VS Code terminal settings to enable OSC title sequences.
|
|
421
|
-
*
|
|
422
|
-
* Sets `terminal.integrated.tabs.title` to include `${sequence}` so VS Code
|
|
423
|
-
* renders OSC 0 escape sequences written by the set-terminal-title hook.
|
|
424
|
-
*
|
|
425
|
-
* Idempotent — only writes if the setting is missing or doesn't already
|
|
426
|
-
* include `${sequence}`.
|
|
427
|
-
*
|
|
428
|
-
* @param {string} [homeDirOverride] - Override home directory (for testing)
|
|
429
|
-
* @returns {Promise<{ updated: boolean, path: string|null, alreadySet: boolean }>}
|
|
430
|
-
*/
|
|
431
|
-
export async function installVSCodeTerminalSettings(homeDirOverride = null) {
|
|
432
|
-
const home = homeDirOverride || homedir();
|
|
433
|
-
|
|
434
|
-
let settingsPath;
|
|
435
|
-
if (process.platform === 'win32') {
|
|
436
|
-
settingsPath = join(home, 'AppData', 'Roaming', 'Code', 'User', 'settings.json');
|
|
437
|
-
} else if (process.platform === 'darwin') {
|
|
438
|
-
settingsPath = join(home, 'Library', 'Application Support', 'Code', 'User', 'settings.json');
|
|
439
|
-
} else {
|
|
440
|
-
settingsPath = join(home, '.config', 'Code', 'User', 'settings.json');
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
if (!existsSync(settingsPath)) {
|
|
444
|
-
return { updated: false, path: settingsPath, alreadySet: false };
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
let settings;
|
|
448
|
-
try {
|
|
449
|
-
settings = JSON.parse(await readFile(settingsPath, 'utf-8'));
|
|
450
|
-
} catch {
|
|
451
|
-
settings = {};
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
const KEY = 'terminal.integrated.tabs.title';
|
|
455
|
-
const current = settings[KEY] || '';
|
|
456
|
-
if (current.includes('${sequence}')) {
|
|
457
|
-
return { updated: false, path: settingsPath, alreadySet: true };
|
|
458
|
-
}
|
|
459
415
|
|
|
460
|
-
settings[KEY] = '${sequence}${separator}${local}';
|
|
461
|
-
await writeFile(settingsPath, JSON.stringify(settings, null, 4) + '\n', 'utf-8');
|
|
462
|
-
return { updated: true, path: settingsPath, alreadySet: false };
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
/**
|
|
466
|
-
* Install shell integration for Claude Code terminal title.
|
|
467
|
-
*
|
|
468
|
-
* Writes a wrapper function to the user's shell profile that:
|
|
469
|
-
* 1. Starts a background job watching ~/.claude/terminal_title
|
|
470
|
-
* 2. Updates the terminal tab title in real time via Console.Title (PS) / OSC 0 (bash)
|
|
471
|
-
* 3. Resets the title when the `claude` command exits
|
|
472
|
-
*
|
|
473
|
-
* On Windows: installs to PowerShell 5.1 and/or PowerShell 7 profiles.
|
|
474
|
-
* On Linux/macOS: installs to ~/.bashrc and/or ~/.zshrc (if they exist).
|
|
475
|
-
*
|
|
476
|
-
* Idempotent — skips if the integration block is already present.
|
|
477
|
-
*
|
|
478
|
-
* @param {string} [homeDirOverride] - Override home directory (for testing)
|
|
479
|
-
* @returns {Promise<{ installed: string[], skipped: string[], errors: string[] }>}
|
|
480
|
-
*/
|
|
481
|
-
export async function installShellIntegration(homeDirOverride = null) {
|
|
482
|
-
const home = homeDirOverride || homedir();
|
|
483
|
-
const installed = [];
|
|
484
|
-
const skipped = [];
|
|
485
|
-
const errors = [];
|
|
486
|
-
|
|
487
|
-
if (process.platform === 'win32') {
|
|
488
|
-
const profiles = [
|
|
489
|
-
join(home, 'Documents', 'WindowsPowerShell', 'Microsoft.PowerShell_profile.ps1'),
|
|
490
|
-
join(home, 'Documents', 'PowerShell', 'Microsoft.PowerShell_profile.ps1'),
|
|
491
|
-
];
|
|
492
|
-
for (const profilePath of profiles) {
|
|
493
|
-
try {
|
|
494
|
-
const result = await _appendShellBlock(profilePath, _buildPowerShellBlock(), _psAlreadyInstalled);
|
|
495
|
-
if (result === 'installed') installed.push(profilePath);
|
|
496
|
-
else skipped.push(profilePath);
|
|
497
|
-
} catch (err) {
|
|
498
|
-
errors.push(profilePath + ': ' + err.message);
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
} else {
|
|
502
|
-
const bashBlock = _buildBashBlock();
|
|
503
|
-
for (const profilePath of [join(home, '.bashrc'), join(home, '.zshrc')]) {
|
|
504
|
-
if (!existsSync(profilePath)) continue;
|
|
505
|
-
try {
|
|
506
|
-
const result = await _appendShellBlock(profilePath, bashBlock, _bashAlreadyInstalled);
|
|
507
|
-
if (result === 'installed') installed.push(profilePath);
|
|
508
|
-
else skipped.push(profilePath);
|
|
509
|
-
} catch (err) {
|
|
510
|
-
errors.push(profilePath + ': ' + err.message);
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
return { installed, skipped, errors };
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
/** @returns {string} PowerShell integration block */
|
|
519
|
-
function _buildPowerShellBlock() {
|
|
520
|
-
return [
|
|
521
|
-
'',
|
|
522
|
-
'# === Claude Code terminal title integration (morph-spec) ===',
|
|
523
|
-
'# Per-session title file prevents cross-session title contamination.',
|
|
524
|
-
'$script:_claudeExe = Get-Command claude -CommandType Application -ErrorAction SilentlyContinue |',
|
|
525
|
-
' Select-Object -First 1 -ExpandProperty Source',
|
|
526
|
-
'',
|
|
527
|
-
'function _Start-ClaudeTitleWatcher {',
|
|
528
|
-
' param([string]$TitleFile)',
|
|
529
|
-
' $rs = [runspacefactory]::CreateRunspace()',
|
|
530
|
-
' $rs.Open()',
|
|
531
|
-
' $ps = [powershell]::Create()',
|
|
532
|
-
' $ps.Runspace = $rs',
|
|
533
|
-
' [void]$ps.AddScript({',
|
|
534
|
-
' param($f)',
|
|
535
|
-
' $last = ""',
|
|
536
|
-
' while ($true) {',
|
|
537
|
-
' if (Test-Path $f) {',
|
|
538
|
-
' try {',
|
|
539
|
-
' $t = (Get-Content $f -Raw -ErrorAction Stop).Trim()',
|
|
540
|
-
' if ($t -and $t -ne $last) {',
|
|
541
|
-
' [Console]::Title = $t',
|
|
542
|
-
' $last = $t',
|
|
543
|
-
' }',
|
|
544
|
-
' } catch {}',
|
|
545
|
-
' }',
|
|
546
|
-
' Start-Sleep -Milliseconds 500',
|
|
547
|
-
' }',
|
|
548
|
-
' }).AddArgument($TitleFile)',
|
|
549
|
-
' $ps.BeginInvoke() | Out-Null',
|
|
550
|
-
' return $ps',
|
|
551
|
-
'}',
|
|
552
|
-
'',
|
|
553
|
-
'function Invoke-Claude {',
|
|
554
|
-
' [CmdletBinding()]',
|
|
555
|
-
' param([Parameter(ValueFromRemainingArguments=$true)][string[]]$Arguments)',
|
|
556
|
-
' if (-not $script:_claudeExe) { Write-Error "claude executable not found in PATH"; return }',
|
|
557
|
-
' # Per-session file: each Invoke-Claude gets its own title file so multiple',
|
|
558
|
-
' # open terminals don\'t overwrite each other\'s titles.',
|
|
559
|
-
' $sessionTitleFile = Join-Path $HOME ".claude\\terminal_title_$(New-Guid)"',
|
|
560
|
-
' $env:MORPH_TERMINAL_TITLE_FILE = $sessionTitleFile',
|
|
561
|
-
' $watcher = _Start-ClaudeTitleWatcher -TitleFile $sessionTitleFile',
|
|
562
|
-
' try {',
|
|
563
|
-
' & $script:_claudeExe @Arguments',
|
|
564
|
-
' } finally {',
|
|
565
|
-
' try { $watcher.Stop(); $watcher.Dispose() } catch {}',
|
|
566
|
-
' $env:MORPH_TERMINAL_TITLE_FILE = $null',
|
|
567
|
-
' Remove-Item $sessionTitleFile -ErrorAction SilentlyContinue',
|
|
568
|
-
' [Console]::Title = "PowerShell"',
|
|
569
|
-
' }',
|
|
570
|
-
'}',
|
|
571
|
-
'Set-Alias -Name claude -Value Invoke-Claude -Force',
|
|
572
|
-
'# === End Claude Code terminal title integration (morph-spec) ===',
|
|
573
|
-
].join('\n');
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
/** @returns {string} Bash/Zsh integration block */
|
|
577
|
-
function _buildBashBlock() {
|
|
578
|
-
return [
|
|
579
|
-
'',
|
|
580
|
-
'# === Claude Code terminal title integration (morph-spec) ===',
|
|
581
|
-
'# Per-session title file prevents cross-session title contamination.',
|
|
582
|
-
'_claude_title_watcher() {',
|
|
583
|
-
' local title_file="$1"',
|
|
584
|
-
' local last_title=""',
|
|
585
|
-
' while true; do',
|
|
586
|
-
' if [ -f "$title_file" ]; then',
|
|
587
|
-
' local current',
|
|
588
|
-
' current=$(cat "$title_file" 2>/dev/null)',
|
|
589
|
-
' if [ -n "$current" ] && [ "$current" != "$last_title" ]; then',
|
|
590
|
-
" printf '\\e]0;%s\\a' \"$current\"",
|
|
591
|
-
' last_title="$current"',
|
|
592
|
-
' fi',
|
|
593
|
-
' fi',
|
|
594
|
-
' sleep 0.5',
|
|
595
|
-
' done',
|
|
596
|
-
'}',
|
|
597
|
-
'',
|
|
598
|
-
'claude() {',
|
|
599
|
-
' # Per-session file: each invocation gets its own title file so multiple',
|
|
600
|
-
' # open terminals don\'t overwrite each other\'s titles.',
|
|
601
|
-
' local SESSION_TITLE_FILE',
|
|
602
|
-
' SESSION_TITLE_FILE="$HOME/.claude/terminal_title_$$_$RANDOM"',
|
|
603
|
-
' export MORPH_TERMINAL_TITLE_FILE="$SESSION_TITLE_FILE"',
|
|
604
|
-
' _claude_title_watcher "$SESSION_TITLE_FILE" &',
|
|
605
|
-
' local WATCHER_PID=$!',
|
|
606
|
-
' command claude "$@"',
|
|
607
|
-
' local EXIT_CODE=$?',
|
|
608
|
-
' kill "$WATCHER_PID" 2>/dev/null',
|
|
609
|
-
' wait "$WATCHER_PID" 2>/dev/null',
|
|
610
|
-
' unset MORPH_TERMINAL_TITLE_FILE',
|
|
611
|
-
' rm -f "$SESSION_TITLE_FILE"',
|
|
612
|
-
" printf '\\e]0;claude\\a'",
|
|
613
|
-
' return $EXIT_CODE',
|
|
614
|
-
'}',
|
|
615
|
-
'# === End Claude Code terminal title integration (morph-spec) ===',
|
|
616
|
-
].join('\n');
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
/** Check if PowerShell profile already has the integration */
|
|
620
|
-
function _psAlreadyInstalled(content) {
|
|
621
|
-
return content.includes('Invoke-Claude') || content.includes('_Start-ClaudeTitleWatcher');
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
/** Check if bash/zsh profile already has the integration */
|
|
625
|
-
function _bashAlreadyInstalled(content) {
|
|
626
|
-
return content.includes('_claude_title_watcher') || content.includes('command claude "$@"');
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
/**
|
|
630
|
-
* Append a shell block to a profile file if not already present.
|
|
631
|
-
* Creates the file (and parent dirs) if it doesn't exist.
|
|
632
|
-
* @param {string} profilePath
|
|
633
|
-
* @param {string} block
|
|
634
|
-
* @param {(content: string) => boolean} isInstalled
|
|
635
|
-
* @returns {Promise<'installed'|'skipped'>}
|
|
636
|
-
*/
|
|
637
|
-
async function _appendShellBlock(profilePath, block, isInstalled) {
|
|
638
|
-
let existing = '';
|
|
639
|
-
let isUtf16le = false;
|
|
640
|
-
|
|
641
|
-
if (existsSync(profilePath)) {
|
|
642
|
-
const raw = await readFile(profilePath);
|
|
643
|
-
// Detect UTF-16LE: starts with FF FE BOM or has null bytes in the first bytes
|
|
644
|
-
isUtf16le = (raw[0] === 0xFF && raw[1] === 0xFE) ||
|
|
645
|
-
(raw.length >= 4 && raw[1] === 0 && raw[3] === 0);
|
|
646
|
-
existing = isUtf16le ? raw.toString('utf16le').replace(/^\uFEFF/, '') : raw.toString('utf-8');
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
if (isInstalled(existing)) return 'skipped';
|
|
650
|
-
|
|
651
|
-
const dir = dirname(profilePath);
|
|
652
|
-
if (!existsSync(dir)) {
|
|
653
|
-
await mkdir(dir, { recursive: true });
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
// Always write as UTF-8; trim trailing whitespace from UTF-16LE-decoded content
|
|
657
|
-
const base = isUtf16le ? existing.trimEnd() + '\n' : existing;
|
|
658
|
-
await writeFile(profilePath, base + block + '\n', 'utf-8');
|
|
659
|
-
return 'installed';
|
|
660
|
-
}
|
|
661
416
|
|
|
662
417
|
/**
|
|
663
418
|
* Remove old v1 agent-teams/dispatch.js hook entries.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tier-to-role helpers for agents.json v4.0
|
|
3
|
+
*
|
|
4
|
+
* Derives role strings from tier numbers, eliminating the need
|
|
5
|
+
* for an explicit `role` field on each agent.
|
|
6
|
+
*
|
|
7
|
+
* @module tier-helpers
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const TIER_ROLES = {
|
|
11
|
+
1: 'orchestrator',
|
|
12
|
+
2: 'leader',
|
|
13
|
+
3: 'specialist',
|
|
14
|
+
4: 'validator',
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Derive role string from agent tier number.
|
|
19
|
+
* @param {number} tier - Agent tier (1-4)
|
|
20
|
+
* @returns {string} Role name
|
|
21
|
+
*/
|
|
22
|
+
export function tierToRole(tier) {
|
|
23
|
+
return TIER_ROLES[tier] || 'specialist';
|
|
24
|
+
}
|