@polymorphism-tech/morph-spec 4.8.19 → 4.9.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/CLAUDE.md +21 -0
- package/README.md +2 -2
- package/bin/morph-spec.js +15 -56
- package/bin/task-manager.js +115 -14
- package/bin/validate.js +67 -33
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +201 -203
- package/docs/QUICKSTART.md +2 -2
- package/framework/CLAUDE.md +21 -0
- package/framework/agents.json +698 -176
- package/framework/hooks/claude-code/post-tool-use/context-refresh.js +1 -1
- package/framework/hooks/claude-code/post-tool-use/dispatch.js +2 -2
- package/framework/hooks/claude-code/post-tool-use/skill-reminder.js +155 -0
- package/framework/hooks/claude-code/pre-tool-use/protect-spec-files.js +1 -1
- package/framework/hooks/claude-code/session-start/inject-morph-context.js +71 -2
- package/framework/hooks/claude-code/statusline.py +76 -30
- package/framework/hooks/claude-code/user-prompt/set-terminal-title.js +14 -6
- package/framework/hooks/shared/activity-logger.js +0 -24
- package/framework/hooks/shared/phase-utils.js +3 -0
- package/framework/hooks/shared/skill-reminder-helpers.js +79 -0
- package/framework/hooks/shared/stale-task-reset.js +57 -0
- package/framework/hooks/shared/state-reader.js +2 -2
- package/framework/hooks/shared/worktree-helpers.js +53 -0
- package/framework/phases.json +40 -8
- package/framework/skills/level-0-meta/brainstorming/SKILL.md +1 -1
- package/framework/skills/level-0-meta/code-review/SKILL.md +1 -1
- package/framework/skills/level-0-meta/code-review-nextjs/SKILL.md +163 -163
- package/framework/skills/level-0-meta/frontend-review/SKILL.md +5 -5
- package/framework/skills/level-0-meta/morph-checklist/SKILL.md +2 -2
- package/framework/skills/level-0-meta/morph-init/SKILL.md +5 -5
- package/framework/skills/level-0-meta/morph-replicate/SKILL.md +4 -4
- package/framework/skills/level-0-meta/morph-replicate/references/blazor-html-mapping.md +1 -1
- package/framework/skills/level-0-meta/post-implementation/SKILL.md +59 -12
- package/framework/skills/level-0-meta/simulation-checklist/SKILL.md +1 -1
- package/framework/skills/level-0-meta/terminal-title/SKILL.md +1 -1
- package/framework/skills/level-0-meta/tool-usage-guide/SKILL.md +1 -1
- package/framework/skills/level-0-meta/tool-usage-guide/references/tools-per-phase.md +6 -5
- package/framework/skills/level-0-meta/verification-before-completion/SKILL.md +1 -1
- package/framework/skills/level-1-workflows/phase-clarify/SKILL.md +215 -189
- package/framework/skills/level-1-workflows/phase-codebase-analysis/SKILL.md +251 -251
- package/framework/skills/level-1-workflows/phase-design/SKILL.md +382 -365
- package/framework/skills/level-1-workflows/phase-implement/SKILL.md +492 -450
- package/framework/skills/level-1-workflows/phase-setup/SKILL.md +194 -190
- package/framework/skills/level-1-workflows/phase-tasks/SKILL.md +270 -270
- package/framework/skills/level-1-workflows/phase-uiux/SKILL.md +285 -285
- package/framework/standards/STANDARDS.json +640 -88
- package/framework/standards/infrastructure/vercel/vercel-database.md +106 -0
- package/framework/templates/REGISTRY.json +1825 -1909
- package/framework/templates/context/CONTEXT-FEATURE.md +276 -276
- package/framework/templates/docs/onboarding.md +1 -5
- package/package.json +2 -6
- package/src/commands/agents/dispatch-agents.js +55 -4
- package/src/commands/project/doctor.js +16 -47
- package/src/commands/project/init.js +1 -1
- package/src/commands/project/status.js +2 -2
- package/src/commands/project/update.js +381 -365
- package/src/commands/project/worktree.js +154 -0
- package/src/commands/state/advance-phase.js +120 -30
- package/src/commands/state/approve.js +2 -2
- package/src/commands/state/index.js +7 -8
- package/src/commands/state/phase-runner.js +1 -1
- package/src/commands/state/state.js +61 -6
- package/src/commands/tasks/task.js +78 -99
- package/src/commands/templates/template-render.js +93 -173
- package/src/commands/trust/trust.js +26 -21
- package/src/core/paths/output-schema.js +15 -0
- package/src/core/state/state-manager.js +28 -54
- package/src/core/workflows/workflow-detector.js +9 -87
- package/src/lib/phase-chain/phase-validator.js +330 -0
- package/src/lib/stack/stack-profile.js +88 -0
- package/src/lib/tasks/task-classifier.js +16 -0
- package/src/lib/tasks/test-runner.js +77 -0
- package/src/lib/trust/trust-manager.js +32 -144
- package/src/lib/validators/spec-validator.js +58 -4
- package/src/lib/validators/validation-runner.js +23 -11
- package/src/scripts/setup-infra.js +240 -224
- package/src/utils/agents-installer.js +2 -2
- package/src/utils/banner.js +1 -1
- package/src/utils/claude-settings-manager.js +1 -1
- package/src/utils/file-copier.js +1 -0
- package/src/utils/hooks-installer.js +258 -8
- package/framework/hooks/dev/check-sync-health.js +0 -117
- package/framework/hooks/dev/guard-version-numbers.js +0 -57
- package/framework/hooks/dev/sync-standards-registry.js +0 -60
- package/framework/hooks/dev/sync-template-registry.js +0 -60
- package/framework/hooks/dev/validate-skill-format.js +0 -70
- package/framework/hooks/dev/validate-standard-format.js +0 -73
- package/framework/templates/meta-prompts/hops/hop-retry.md +0 -78
- package/framework/templates/meta-prompts/hops/hop-validation.md +0 -97
- package/framework/templates/meta-prompts/hops/hop-wrapper.md +0 -36
- package/framework/workflows/configs/design-impl.json +0 -49
- package/framework/workflows/configs/express.json +0 -45
- package/framework/workflows/configs/fast-track.json +0 -42
- package/framework/workflows/configs/full-morph.json +0 -79
- package/framework/workflows/configs/fusion.json +0 -39
- package/framework/workflows/configs/long-running.json +0 -33
- package/framework/workflows/configs/spec-only.json +0 -43
- package/framework/workflows/configs/ui-refresh.json +0 -49
- package/framework/workflows/configs/zero-touch.json +0 -82
- package/src/commands/project/monitor.js +0 -295
- package/src/commands/project/tutorial.js +0 -115
- package/src/commands/state/validate-phase.js +0 -238
- package/src/commands/templates/generate-contracts.js +0 -445
- package/src/core/orchestrator.js +0 -171
- package/src/core/registry/command-registry.js +0 -28
- package/src/core/registry/index.js +0 -8
- package/src/core/registry/validator-registry.js +0 -204
- package/src/core/templates/template-validator.js +0 -296
- package/src/generator/config-generator.js +0 -206
- package/src/generator/templates/config.json.template +0 -40
- package/src/generator/templates/project.md.template +0 -67
- package/src/lib/agents/micro-agent-factory.js +0 -161
- package/src/lib/analysis/complexity-analyzer.js +0 -441
- package/src/lib/analysis/index.js +0 -7
- package/src/lib/analytics/analytics-engine.js +0 -345
- package/src/lib/checkpoints/checkpoint-hooks.js +0 -298
- package/src/lib/checkpoints/index.js +0 -7
- package/src/lib/context/context-bundler.js +0 -241
- package/src/lib/context/context-optimizer.js +0 -212
- package/src/lib/context/context-tracker.js +0 -273
- package/src/lib/context/core-four-tracker.js +0 -201
- package/src/lib/context/mcp-optimizer.js +0 -200
- package/src/lib/execution/fusion-executor.js +0 -304
- package/src/lib/execution/parallel-executor.js +0 -270
- package/src/lib/hooks/stop-hook-executor.js +0 -286
- package/src/lib/hops/hop-composer.js +0 -221
- package/src/lib/phase-chain/eligibility-checker.js +0 -243
- package/src/lib/threads/thread-coordinator.js +0 -238
- package/src/lib/threads/thread-manager.js +0 -317
- package/src/lib/tracking/artifact-trail.js +0 -202
- package/src/scanner/project-scanner.js +0 -242
- package/src/ui/diff-display.js +0 -91
- package/src/ui/interactive-wizard.js +0 -96
- package/src/ui/user-review.js +0 -211
- package/src/ui/wizard-questions.js +0 -188
- package/src/utils/color-utils.js +0 -70
- package/src/utils/process-handler.js +0 -97
|
@@ -8,14 +8,14 @@
|
|
|
8
8
|
* Called by `morph-spec init` and `morph-spec update`.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { join } from 'path';
|
|
11
|
+
import { join, dirname } from 'path';
|
|
12
12
|
import { readFile, writeFile, mkdir, copyFile } from 'fs/promises';
|
|
13
13
|
import { existsSync, chmodSync, readdirSync } from 'fs';
|
|
14
14
|
import { homedir } from 'os';
|
|
15
15
|
import { execSync } from 'child_process';
|
|
16
16
|
|
|
17
17
|
/** Current hooks schema version — bump when hook definitions change */
|
|
18
|
-
const HOOKS_VERSION = '2.
|
|
18
|
+
const HOOKS_VERSION = '2.10.0';
|
|
19
19
|
|
|
20
20
|
/** Marker for old dispatch.js (v1) */
|
|
21
21
|
const OLD_DISPATCH_COMMAND = 'node framework/hooks/agent-teams/dispatch.js';
|
|
@@ -103,11 +103,11 @@ const MORPH_HOOKS = [
|
|
|
103
103
|
matcher: 'Bash',
|
|
104
104
|
hooks: [{
|
|
105
105
|
type: 'prompt',
|
|
106
|
-
prompt: `You are a guard for a morph-spec project. Read the bash command from the hook input and check:
|
|
107
|
-
1. Does the command
|
|
108
|
-
2. Does the command
|
|
109
|
-
3. Does the command
|
|
110
|
-
If
|
|
106
|
+
prompt: `You are a guard for a morph-spec project. Read the bash command from the hook input and check EXACTLY these 3 conditions. IMPORTANT: reading files (cat, head, tail, grep, less, bat) is NEVER blocked regardless of path.
|
|
107
|
+
1. Does the command DELETE .morph/ or .morph/state.json or .morph/framework/ using rm -r, rm -rf, or rmdir? BLOCK only these exact paths. NOTE: deleting .morph/features/ subdirectories is NOT blocked.
|
|
108
|
+
2. Does the command OVERWRITE .morph/state.json using a shell redirect (>) combined with sed, jq, awk, echo, cat, or printf? If yes, BLOCK.
|
|
109
|
+
3. Does the command WRITE TO .morph/framework/ via a shell redirect (>) or Node.js fs.writeFileSync/appendFileSync on a path under .morph/framework/? READING .morph/framework/ is always allowed. Writing to .morph/features/, .morph/config/, .morph/context/ is allowed.
|
|
110
|
+
If ANY of these 3 specific conditions matches, respond: {"ok": false, "reason": "MORPH-SPEC: [explain what was blocked]. To read agents/state use: morph-spec state get <feature> | morph-spec dispatch-agents <feature> <phase> --table | cat .morph/framework/agents.json (reading is always allowed). To update state use: morph-spec state set <feature> <key> <value>."}
|
|
111
111
|
Otherwise respond: {"ok": true}`
|
|
112
112
|
}]
|
|
113
113
|
},
|
|
@@ -124,6 +124,14 @@ Otherwise respond: {"ok": true}`
|
|
|
124
124
|
{
|
|
125
125
|
type: 'command',
|
|
126
126
|
command: 'node framework/hooks/claude-code/post-tool-use/context-refresh.js'
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
type: 'command',
|
|
130
|
+
command: 'node framework/hooks/claude-code/post-tool-use/skill-reminder.js'
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
type: 'command',
|
|
134
|
+
command: 'node framework/hooks/claude-code/post-tool-use/validator-feedback.js'
|
|
127
135
|
}
|
|
128
136
|
]
|
|
129
137
|
},
|
|
@@ -302,7 +310,6 @@ export async function installClaudeHooks(targetPath) {
|
|
|
302
310
|
return { installed, updated: !alreadyCurrent };
|
|
303
311
|
}
|
|
304
312
|
|
|
305
|
-
/**
|
|
306
313
|
/**
|
|
307
314
|
* Resolve Python executable suitable for the statusLine command.
|
|
308
315
|
*
|
|
@@ -398,6 +405,249 @@ export async function installGlobalStatusline(hooksSourceDir, globalClaudeDirOve
|
|
|
398
405
|
return { installed: true, globalDir };
|
|
399
406
|
}
|
|
400
407
|
|
|
408
|
+
/**
|
|
409
|
+
* Install VS Code terminal settings to enable OSC title sequences.
|
|
410
|
+
*
|
|
411
|
+
* Sets `terminal.integrated.tabs.title` to include `${sequence}` so VS Code
|
|
412
|
+
* renders OSC 0 escape sequences written by the set-terminal-title hook.
|
|
413
|
+
*
|
|
414
|
+
* Idempotent — only writes if the setting is missing or doesn't already
|
|
415
|
+
* include `${sequence}`.
|
|
416
|
+
*
|
|
417
|
+
* @param {string} [homeDirOverride] - Override home directory (for testing)
|
|
418
|
+
* @returns {Promise<{ updated: boolean, path: string|null, alreadySet: boolean }>}
|
|
419
|
+
*/
|
|
420
|
+
export async function installVSCodeTerminalSettings(homeDirOverride = null) {
|
|
421
|
+
const home = homeDirOverride || homedir();
|
|
422
|
+
|
|
423
|
+
let settingsPath;
|
|
424
|
+
if (process.platform === 'win32') {
|
|
425
|
+
settingsPath = join(home, 'AppData', 'Roaming', 'Code', 'User', 'settings.json');
|
|
426
|
+
} else if (process.platform === 'darwin') {
|
|
427
|
+
settingsPath = join(home, 'Library', 'Application Support', 'Code', 'User', 'settings.json');
|
|
428
|
+
} else {
|
|
429
|
+
settingsPath = join(home, '.config', 'Code', 'User', 'settings.json');
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (!existsSync(settingsPath)) {
|
|
433
|
+
return { updated: false, path: settingsPath, alreadySet: false };
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
let settings;
|
|
437
|
+
try {
|
|
438
|
+
settings = JSON.parse(await readFile(settingsPath, 'utf-8'));
|
|
439
|
+
} catch {
|
|
440
|
+
settings = {};
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const KEY = 'terminal.integrated.tabs.title';
|
|
444
|
+
const current = settings[KEY] || '';
|
|
445
|
+
if (current.includes('${sequence}')) {
|
|
446
|
+
return { updated: false, path: settingsPath, alreadySet: true };
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
settings[KEY] = '${sequence}${separator}${local}';
|
|
450
|
+
await writeFile(settingsPath, JSON.stringify(settings, null, 4) + '\n', 'utf-8');
|
|
451
|
+
return { updated: true, path: settingsPath, alreadySet: false };
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Install shell integration for Claude Code terminal title.
|
|
456
|
+
*
|
|
457
|
+
* Writes a wrapper function to the user's shell profile that:
|
|
458
|
+
* 1. Starts a background job watching ~/.claude/terminal_title
|
|
459
|
+
* 2. Updates the terminal tab title in real time via Console.Title (PS) / OSC 0 (bash)
|
|
460
|
+
* 3. Resets the title when the `claude` command exits
|
|
461
|
+
*
|
|
462
|
+
* On Windows: installs to PowerShell 5.1 and/or PowerShell 7 profiles.
|
|
463
|
+
* On Linux/macOS: installs to ~/.bashrc and/or ~/.zshrc (if they exist).
|
|
464
|
+
*
|
|
465
|
+
* Idempotent — skips if the integration block is already present.
|
|
466
|
+
*
|
|
467
|
+
* @param {string} [homeDirOverride] - Override home directory (for testing)
|
|
468
|
+
* @returns {Promise<{ installed: string[], skipped: string[], errors: string[] }>}
|
|
469
|
+
*/
|
|
470
|
+
export async function installShellIntegration(homeDirOverride = null) {
|
|
471
|
+
const home = homeDirOverride || homedir();
|
|
472
|
+
const installed = [];
|
|
473
|
+
const skipped = [];
|
|
474
|
+
const errors = [];
|
|
475
|
+
|
|
476
|
+
if (process.platform === 'win32') {
|
|
477
|
+
const profiles = [
|
|
478
|
+
join(home, 'Documents', 'WindowsPowerShell', 'Microsoft.PowerShell_profile.ps1'),
|
|
479
|
+
join(home, 'Documents', 'PowerShell', 'Microsoft.PowerShell_profile.ps1'),
|
|
480
|
+
];
|
|
481
|
+
for (const profilePath of profiles) {
|
|
482
|
+
try {
|
|
483
|
+
const result = await _appendShellBlock(profilePath, _buildPowerShellBlock(), _psAlreadyInstalled);
|
|
484
|
+
if (result === 'installed') installed.push(profilePath);
|
|
485
|
+
else skipped.push(profilePath);
|
|
486
|
+
} catch (err) {
|
|
487
|
+
errors.push(profilePath + ': ' + err.message);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
} else {
|
|
491
|
+
const bashBlock = _buildBashBlock();
|
|
492
|
+
for (const profilePath of [join(home, '.bashrc'), join(home, '.zshrc')]) {
|
|
493
|
+
if (!existsSync(profilePath)) continue;
|
|
494
|
+
try {
|
|
495
|
+
const result = await _appendShellBlock(profilePath, bashBlock, _bashAlreadyInstalled);
|
|
496
|
+
if (result === 'installed') installed.push(profilePath);
|
|
497
|
+
else skipped.push(profilePath);
|
|
498
|
+
} catch (err) {
|
|
499
|
+
errors.push(profilePath + ': ' + err.message);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
return { installed, skipped, errors };
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/** @returns {string} PowerShell integration block */
|
|
508
|
+
function _buildPowerShellBlock() {
|
|
509
|
+
return [
|
|
510
|
+
'',
|
|
511
|
+
'# === Claude Code terminal title integration (morph-spec) ===',
|
|
512
|
+
'# Per-session title file prevents cross-session title contamination.',
|
|
513
|
+
'$script:_claudeExe = Get-Command claude -CommandType Application -ErrorAction SilentlyContinue |',
|
|
514
|
+
' Select-Object -First 1 -ExpandProperty Source',
|
|
515
|
+
'',
|
|
516
|
+
'function _Start-ClaudeTitleWatcher {',
|
|
517
|
+
' param([string]$TitleFile)',
|
|
518
|
+
' $rs = [runspacefactory]::CreateRunspace()',
|
|
519
|
+
' $rs.Open()',
|
|
520
|
+
' $ps = [powershell]::Create()',
|
|
521
|
+
' $ps.Runspace = $rs',
|
|
522
|
+
' [void]$ps.AddScript({',
|
|
523
|
+
' param($f)',
|
|
524
|
+
' $last = ""',
|
|
525
|
+
' while ($true) {',
|
|
526
|
+
' if (Test-Path $f) {',
|
|
527
|
+
' try {',
|
|
528
|
+
' $t = (Get-Content $f -Raw -ErrorAction Stop).Trim()',
|
|
529
|
+
' if ($t -and $t -ne $last) {',
|
|
530
|
+
' [Console]::Title = $t',
|
|
531
|
+
' $last = $t',
|
|
532
|
+
' }',
|
|
533
|
+
' } catch {}',
|
|
534
|
+
' }',
|
|
535
|
+
' Start-Sleep -Milliseconds 500',
|
|
536
|
+
' }',
|
|
537
|
+
' }).AddArgument($TitleFile)',
|
|
538
|
+
' $ps.BeginInvoke() | Out-Null',
|
|
539
|
+
' return $ps',
|
|
540
|
+
'}',
|
|
541
|
+
'',
|
|
542
|
+
'function Invoke-Claude {',
|
|
543
|
+
' [CmdletBinding()]',
|
|
544
|
+
' param([Parameter(ValueFromRemainingArguments=$true)][string[]]$Arguments)',
|
|
545
|
+
' if (-not $script:_claudeExe) { Write-Error "claude executable not found in PATH"; return }',
|
|
546
|
+
' # Per-session file: each Invoke-Claude gets its own title file so multiple',
|
|
547
|
+
' # open terminals don\'t overwrite each other\'s titles.',
|
|
548
|
+
' $sessionTitleFile = Join-Path $HOME ".claude\\terminal_title_$(New-Guid)"',
|
|
549
|
+
' $env:MORPH_TERMINAL_TITLE_FILE = $sessionTitleFile',
|
|
550
|
+
' $watcher = _Start-ClaudeTitleWatcher -TitleFile $sessionTitleFile',
|
|
551
|
+
' try {',
|
|
552
|
+
' & $script:_claudeExe @Arguments',
|
|
553
|
+
' } finally {',
|
|
554
|
+
' try { $watcher.Stop(); $watcher.Dispose() } catch {}',
|
|
555
|
+
' $env:MORPH_TERMINAL_TITLE_FILE = $null',
|
|
556
|
+
' Remove-Item $sessionTitleFile -ErrorAction SilentlyContinue',
|
|
557
|
+
' [Console]::Title = "PowerShell"',
|
|
558
|
+
' }',
|
|
559
|
+
'}',
|
|
560
|
+
'Set-Alias -Name claude -Value Invoke-Claude -Force',
|
|
561
|
+
'# === End Claude Code terminal title integration (morph-spec) ===',
|
|
562
|
+
].join('\n');
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/** @returns {string} Bash/Zsh integration block */
|
|
566
|
+
function _buildBashBlock() {
|
|
567
|
+
return [
|
|
568
|
+
'',
|
|
569
|
+
'# === Claude Code terminal title integration (morph-spec) ===',
|
|
570
|
+
'# Per-session title file prevents cross-session title contamination.',
|
|
571
|
+
'_claude_title_watcher() {',
|
|
572
|
+
' local title_file="$1"',
|
|
573
|
+
' local last_title=""',
|
|
574
|
+
' while true; do',
|
|
575
|
+
' if [ -f "$title_file" ]; then',
|
|
576
|
+
' local current',
|
|
577
|
+
' current=$(cat "$title_file" 2>/dev/null)',
|
|
578
|
+
' if [ -n "$current" ] && [ "$current" != "$last_title" ]; then',
|
|
579
|
+
" printf '\\e]0;%s\\a' \"$current\"",
|
|
580
|
+
' last_title="$current"',
|
|
581
|
+
' fi',
|
|
582
|
+
' fi',
|
|
583
|
+
' sleep 0.5',
|
|
584
|
+
' done',
|
|
585
|
+
'}',
|
|
586
|
+
'',
|
|
587
|
+
'claude() {',
|
|
588
|
+
' # Per-session file: each invocation gets its own title file so multiple',
|
|
589
|
+
' # open terminals don\'t overwrite each other\'s titles.',
|
|
590
|
+
' local SESSION_TITLE_FILE',
|
|
591
|
+
' SESSION_TITLE_FILE="$HOME/.claude/terminal_title_$$_$RANDOM"',
|
|
592
|
+
' export MORPH_TERMINAL_TITLE_FILE="$SESSION_TITLE_FILE"',
|
|
593
|
+
' _claude_title_watcher "$SESSION_TITLE_FILE" &',
|
|
594
|
+
' local WATCHER_PID=$!',
|
|
595
|
+
' command claude "$@"',
|
|
596
|
+
' local EXIT_CODE=$?',
|
|
597
|
+
' kill "$WATCHER_PID" 2>/dev/null',
|
|
598
|
+
' wait "$WATCHER_PID" 2>/dev/null',
|
|
599
|
+
' unset MORPH_TERMINAL_TITLE_FILE',
|
|
600
|
+
' rm -f "$SESSION_TITLE_FILE"',
|
|
601
|
+
" printf '\\e]0;claude\\a'",
|
|
602
|
+
' return $EXIT_CODE',
|
|
603
|
+
'}',
|
|
604
|
+
'# === End Claude Code terminal title integration (morph-spec) ===',
|
|
605
|
+
].join('\n');
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/** Check if PowerShell profile already has the integration */
|
|
609
|
+
function _psAlreadyInstalled(content) {
|
|
610
|
+
return content.includes('Invoke-Claude') || content.includes('_Start-ClaudeTitleWatcher');
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/** Check if bash/zsh profile already has the integration */
|
|
614
|
+
function _bashAlreadyInstalled(content) {
|
|
615
|
+
return content.includes('_claude_title_watcher') || content.includes('command claude "$@"');
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Append a shell block to a profile file if not already present.
|
|
620
|
+
* Creates the file (and parent dirs) if it doesn't exist.
|
|
621
|
+
* @param {string} profilePath
|
|
622
|
+
* @param {string} block
|
|
623
|
+
* @param {(content: string) => boolean} isInstalled
|
|
624
|
+
* @returns {Promise<'installed'|'skipped'>}
|
|
625
|
+
*/
|
|
626
|
+
async function _appendShellBlock(profilePath, block, isInstalled) {
|
|
627
|
+
let existing = '';
|
|
628
|
+
let isUtf16le = false;
|
|
629
|
+
|
|
630
|
+
if (existsSync(profilePath)) {
|
|
631
|
+
const raw = await readFile(profilePath);
|
|
632
|
+
// Detect UTF-16LE: starts with FF FE BOM or has null bytes in the first bytes
|
|
633
|
+
isUtf16le = (raw[0] === 0xFF && raw[1] === 0xFE) ||
|
|
634
|
+
(raw.length >= 4 && raw[1] === 0 && raw[3] === 0);
|
|
635
|
+
existing = isUtf16le ? raw.toString('utf16le').replace(/^\uFEFF/, '') : raw.toString('utf-8');
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
if (isInstalled(existing)) return 'skipped';
|
|
639
|
+
|
|
640
|
+
const dir = dirname(profilePath);
|
|
641
|
+
if (!existsSync(dir)) {
|
|
642
|
+
await mkdir(dir, { recursive: true });
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// Always write as UTF-8; trim trailing whitespace from UTF-16LE-decoded content
|
|
646
|
+
const base = isUtf16le ? existing.trimEnd() + '\n' : existing;
|
|
647
|
+
await writeFile(profilePath, base + block + '\n', 'utf-8');
|
|
648
|
+
return 'installed';
|
|
649
|
+
}
|
|
650
|
+
|
|
401
651
|
/**
|
|
402
652
|
* Remove old v1 agent-teams/dispatch.js hook entries.
|
|
403
653
|
* @param {Object} settings
|
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Dev Stop Hook: Sync Health Check (Advisory)
|
|
5
|
-
*
|
|
6
|
-
* Event: Stop
|
|
7
|
-
* Scope: Framework codebase only (dev hook)
|
|
8
|
-
*
|
|
9
|
-
* When Claude stops, checks for out-of-sync conditions:
|
|
10
|
-
* 1. Templates not in REGISTRY.json
|
|
11
|
-
* 2. Standards not in STANDARDS.json
|
|
12
|
-
*
|
|
13
|
-
* Uses readdirSync (no git dependency, fast).
|
|
14
|
-
* Returns advisory context if out-of-sync items found.
|
|
15
|
-
*
|
|
16
|
-
* Fail-open: exits 0 on any error.
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
import { readdirSync, readFileSync, existsSync } from 'fs';
|
|
20
|
-
import { join } from 'path';
|
|
21
|
-
import { injectContext, pass } from '../shared/hook-response.js';
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Recursively collect .md files from a directory.
|
|
25
|
-
* @param {string} dir - Directory to scan
|
|
26
|
-
* @param {string} [base] - Base path for relative paths
|
|
27
|
-
* @returns {string[]} Relative paths
|
|
28
|
-
*/
|
|
29
|
-
function collectMdFiles(dir, base) {
|
|
30
|
-
base = base || dir;
|
|
31
|
-
const results = [];
|
|
32
|
-
if (!existsSync(dir)) return results;
|
|
33
|
-
|
|
34
|
-
try {
|
|
35
|
-
const entries = readdirSync(dir, { withFileTypes: true });
|
|
36
|
-
for (const entry of entries) {
|
|
37
|
-
const fullPath = join(dir, entry.name);
|
|
38
|
-
if (entry.isDirectory()) {
|
|
39
|
-
if (entry.name === 'node_modules') continue;
|
|
40
|
-
results.push(...collectMdFiles(fullPath, base));
|
|
41
|
-
} else if (entry.name.endsWith('.md') && entry.name !== 'README.md') {
|
|
42
|
-
const rel = fullPath.replace(base + '/', '').replace(base + '\\', '').replace(/\\/g, '/');
|
|
43
|
-
results.push(rel);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
} catch {
|
|
47
|
-
// Fail-open
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
return results;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
try {
|
|
54
|
-
// Prevent infinite loop
|
|
55
|
-
if (process.env.MORPH_STOP_HOOK_ACTIVE === '1') pass();
|
|
56
|
-
|
|
57
|
-
const warnings = [];
|
|
58
|
-
|
|
59
|
-
// Check templates vs REGISTRY.json
|
|
60
|
-
const registryPath = 'framework/templates/REGISTRY.json';
|
|
61
|
-
if (existsSync(registryPath)) {
|
|
62
|
-
try {
|
|
63
|
-
const registry = JSON.parse(readFileSync(registryPath, 'utf-8'));
|
|
64
|
-
const registeredPaths = new Set((registry.templates || []).map(t => t.path));
|
|
65
|
-
const templateFiles = collectMdFiles('framework/templates');
|
|
66
|
-
|
|
67
|
-
const unregistered = templateFiles.filter(f => !registeredPaths.has(f));
|
|
68
|
-
if (unregistered.length > 0) {
|
|
69
|
-
warnings.push(`Templates not in REGISTRY.json (${unregistered.length}):`);
|
|
70
|
-
for (const f of unregistered.slice(0, 5)) {
|
|
71
|
-
warnings.push(` - ${f}`);
|
|
72
|
-
}
|
|
73
|
-
if (unregistered.length > 5) {
|
|
74
|
-
warnings.push(` ... and ${unregistered.length - 5} more`);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
} catch {
|
|
78
|
-
// Skip on error
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Check standards vs STANDARDS.json
|
|
83
|
-
const standardsPath = 'framework/standards/STANDARDS.json';
|
|
84
|
-
if (existsSync(standardsPath)) {
|
|
85
|
-
try {
|
|
86
|
-
const registry = JSON.parse(readFileSync(standardsPath, 'utf-8'));
|
|
87
|
-
const registeredPaths = new Set((registry.standards || []).map(s => s.path));
|
|
88
|
-
const standardFiles = collectMdFiles('framework/standards');
|
|
89
|
-
|
|
90
|
-
const unregistered = standardFiles.filter(f => !registeredPaths.has(f));
|
|
91
|
-
if (unregistered.length > 0) {
|
|
92
|
-
warnings.push(`Standards not in STANDARDS.json (${unregistered.length}):`);
|
|
93
|
-
for (const f of unregistered.slice(0, 5)) {
|
|
94
|
-
warnings.push(` - ${f}`);
|
|
95
|
-
}
|
|
96
|
-
if (unregistered.length > 5) {
|
|
97
|
-
warnings.push(` ... and ${unregistered.length - 5} more`);
|
|
98
|
-
}
|
|
99
|
-
warnings.push(' Run: node scripts/generate-standards-registry.js');
|
|
100
|
-
}
|
|
101
|
-
} catch {
|
|
102
|
-
// Skip on error
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
if (warnings.length === 0) pass();
|
|
107
|
-
|
|
108
|
-
const message = [
|
|
109
|
-
'MORPH-SPEC Dev: Out-of-sync items detected:',
|
|
110
|
-
...warnings.map(w => ` ${w}`),
|
|
111
|
-
].join('\n');
|
|
112
|
-
|
|
113
|
-
injectContext(message);
|
|
114
|
-
} catch {
|
|
115
|
-
// Fail-open
|
|
116
|
-
process.exit(0);
|
|
117
|
-
}
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Dev PreToolUse Hook: Guard Version Numbers
|
|
5
|
-
*
|
|
6
|
-
* Event: PreToolUse | Matcher: Write|Edit
|
|
7
|
-
* Scope: Framework codebase only (dev hook)
|
|
8
|
-
*
|
|
9
|
-
* Blocks Write/Edit to files that contain hardcoded version patterns
|
|
10
|
-
* like "MORPH-SPEC v4.8.12". Version should only be in package.json.
|
|
11
|
-
*
|
|
12
|
-
* Checked extensions: .md, .cs, .css, .js (covers templates + source)
|
|
13
|
-
* Exceptions: CHANGELOG.md, node_modules/, test/, package.json, package-lock.json
|
|
14
|
-
*
|
|
15
|
-
* Fail-open: exits 0 on any error.
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import { readStdin } from '../shared/stdin-reader.js';
|
|
19
|
-
import { getFilePath, getContentToValidate } from '../shared/payload-utils.js';
|
|
20
|
-
import { block, pass } from '../shared/hook-response.js';
|
|
21
|
-
|
|
22
|
-
const VERSION_PATTERN = /MORPH-SPEC\s+v\d+\.\d+\.\d+/;
|
|
23
|
-
const CHECKED_EXTENSIONS = ['.md', '.cs', '.css', '.js', '.html', '.txt'];
|
|
24
|
-
|
|
25
|
-
try {
|
|
26
|
-
const payload = await readStdin();
|
|
27
|
-
if (!payload) pass();
|
|
28
|
-
|
|
29
|
-
const filePath = getFilePath(payload);
|
|
30
|
-
if (!filePath) pass();
|
|
31
|
-
|
|
32
|
-
// Only check relevant file types
|
|
33
|
-
if (!CHECKED_EXTENSIONS.some(ext => filePath.endsWith(ext))) pass();
|
|
34
|
-
|
|
35
|
-
// Exceptions
|
|
36
|
-
if (filePath.includes('node_modules/')) pass();
|
|
37
|
-
if (filePath.endsWith('CHANGELOG.md')) pass();
|
|
38
|
-
if (filePath.includes('test/')) pass();
|
|
39
|
-
if (filePath.endsWith('package.json')) pass();
|
|
40
|
-
if (filePath.endsWith('package-lock.json')) pass();
|
|
41
|
-
|
|
42
|
-
const content = getContentToValidate(payload);
|
|
43
|
-
if (!content) pass();
|
|
44
|
-
|
|
45
|
-
if (VERSION_PATTERN.test(content)) {
|
|
46
|
-
block(
|
|
47
|
-
`MORPH-SPEC: Hardcoded version number detected in '${filePath}'.\n` +
|
|
48
|
-
`Version should only be in package.json. Use 'MORPH-SPEC by Polymorphism Tech' without version.\n` +
|
|
49
|
-
`Pattern found: ${content.match(VERSION_PATTERN)?.[0]}`
|
|
50
|
-
);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
pass();
|
|
54
|
-
} catch {
|
|
55
|
-
// Fail-open
|
|
56
|
-
process.exit(0);
|
|
57
|
-
}
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Dev PostToolUse Hook: Standards Registry Sync Advisory
|
|
5
|
-
*
|
|
6
|
-
* Event: PostToolUse | Matcher: Write
|
|
7
|
-
* Scope: Framework codebase only (dev hook)
|
|
8
|
-
*
|
|
9
|
-
* After a Write to framework/standards/**/*.md, checks if the file
|
|
10
|
-
* is registered in framework/standards/STANDARDS.json.
|
|
11
|
-
* If not, injects an advisory to regenerate the registry.
|
|
12
|
-
*
|
|
13
|
-
* Fail-open: exits 0 on any error.
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import { readFileSync, existsSync } from 'fs';
|
|
17
|
-
import { readStdin } from '../shared/stdin-reader.js';
|
|
18
|
-
import { getFilePath } from '../shared/payload-utils.js';
|
|
19
|
-
import { approve, pass } from '../shared/hook-response.js';
|
|
20
|
-
|
|
21
|
-
try {
|
|
22
|
-
const payload = await readStdin();
|
|
23
|
-
if (!payload) pass();
|
|
24
|
-
|
|
25
|
-
const filePath = getFilePath(payload);
|
|
26
|
-
if (!filePath) pass();
|
|
27
|
-
|
|
28
|
-
// Only check framework/standards/**/*.md
|
|
29
|
-
if (!filePath.includes('framework/standards/')) pass();
|
|
30
|
-
if (!filePath.endsWith('.md')) pass();
|
|
31
|
-
|
|
32
|
-
// Skip meta files
|
|
33
|
-
const basename = filePath.split('/').pop();
|
|
34
|
-
if (basename === 'README.md') pass();
|
|
35
|
-
if (basename === 'STANDARDS.json') pass();
|
|
36
|
-
|
|
37
|
-
// Extract relative path from framework/standards/
|
|
38
|
-
const standardsIdx = filePath.indexOf('framework/standards/');
|
|
39
|
-
if (standardsIdx === -1) pass();
|
|
40
|
-
const relativePath = filePath.slice(standardsIdx + 'framework/standards/'.length);
|
|
41
|
-
|
|
42
|
-
// Read STANDARDS.json
|
|
43
|
-
const registryPath = 'framework/standards/STANDARDS.json';
|
|
44
|
-
if (!existsSync(registryPath)) pass();
|
|
45
|
-
|
|
46
|
-
const registry = JSON.parse(readFileSync(registryPath, 'utf-8'));
|
|
47
|
-
const registeredPaths = (registry.standards || []).map(s => s.path);
|
|
48
|
-
|
|
49
|
-
if (!registeredPaths.includes(relativePath)) {
|
|
50
|
-
approve(
|
|
51
|
-
`MORPH-SPEC: Standard '${relativePath}' is not in STANDARDS.json.\n` +
|
|
52
|
-
`Run: node scripts/generate-standards-registry.js`
|
|
53
|
-
);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
pass();
|
|
57
|
-
} catch {
|
|
58
|
-
// Fail-open
|
|
59
|
-
process.exit(0);
|
|
60
|
-
}
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Dev PostToolUse Hook: Template Registry Sync Advisory
|
|
5
|
-
*
|
|
6
|
-
* Event: PostToolUse | Matcher: Write
|
|
7
|
-
* Scope: Framework codebase only (dev hook)
|
|
8
|
-
*
|
|
9
|
-
* After a Write to framework/templates/**/*.md, checks if the file
|
|
10
|
-
* is registered in framework/templates/REGISTRY.json.
|
|
11
|
-
* If not, injects an advisory to add the entry.
|
|
12
|
-
*
|
|
13
|
-
* Fail-open: exits 0 on any error.
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import { readFileSync, existsSync } from 'fs';
|
|
17
|
-
import { readStdin } from '../shared/stdin-reader.js';
|
|
18
|
-
import { getFilePath } from '../shared/payload-utils.js';
|
|
19
|
-
import { approve, pass } from '../shared/hook-response.js';
|
|
20
|
-
|
|
21
|
-
try {
|
|
22
|
-
const payload = await readStdin();
|
|
23
|
-
if (!payload) pass();
|
|
24
|
-
|
|
25
|
-
const filePath = getFilePath(payload);
|
|
26
|
-
if (!filePath) pass();
|
|
27
|
-
|
|
28
|
-
// Only check framework/templates/**/*.md
|
|
29
|
-
if (!filePath.includes('framework/templates/')) pass();
|
|
30
|
-
if (!filePath.endsWith('.md')) pass();
|
|
31
|
-
|
|
32
|
-
// Skip meta files
|
|
33
|
-
const basename = filePath.split('/').pop();
|
|
34
|
-
if (basename === 'README.md') pass();
|
|
35
|
-
if (basename === 'REGISTRY.json') pass();
|
|
36
|
-
|
|
37
|
-
// Extract relative path from framework/templates/
|
|
38
|
-
const templateIdx = filePath.indexOf('framework/templates/');
|
|
39
|
-
if (templateIdx === -1) pass();
|
|
40
|
-
const relativePath = filePath.slice(templateIdx + 'framework/templates/'.length);
|
|
41
|
-
|
|
42
|
-
// Read REGISTRY.json
|
|
43
|
-
const registryPath = 'framework/templates/REGISTRY.json';
|
|
44
|
-
if (!existsSync(registryPath)) pass();
|
|
45
|
-
|
|
46
|
-
const registry = JSON.parse(readFileSync(registryPath, 'utf-8'));
|
|
47
|
-
const registeredPaths = (registry.templates || []).map(t => t.path);
|
|
48
|
-
|
|
49
|
-
if (!registeredPaths.includes(relativePath)) {
|
|
50
|
-
approve(
|
|
51
|
-
`MORPH-SPEC: Template '${relativePath}' is not in REGISTRY.json.\n` +
|
|
52
|
-
`Add an entry to framework/templates/REGISTRY.json with id, path, phase, and outputName fields.`
|
|
53
|
-
);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
pass();
|
|
57
|
-
} catch {
|
|
58
|
-
// Fail-open
|
|
59
|
-
process.exit(0);
|
|
60
|
-
}
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Dev PreToolUse Hook: Validate Skill Frontmatter Format
|
|
5
|
-
*
|
|
6
|
-
* Event: PreToolUse | Matcher: Write|Edit
|
|
7
|
-
* Scope: Framework codebase only (dev hook)
|
|
8
|
-
*
|
|
9
|
-
* Validates that SKILL.md files written to framework/skills/ have valid
|
|
10
|
-
* YAML frontmatter with required `name:` and `description:` fields.
|
|
11
|
-
*
|
|
12
|
-
* Only validates on Write (full content). Edit operations pass through.
|
|
13
|
-
* Simple regex parsing (no YAML library dependency).
|
|
14
|
-
*
|
|
15
|
-
* Fail-open: exits 0 on any error.
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import { readStdin } from '../shared/stdin-reader.js';
|
|
19
|
-
import { getFilePath, getContentToValidate, isWriteOperation } from '../shared/payload-utils.js';
|
|
20
|
-
import { block, pass } from '../shared/hook-response.js';
|
|
21
|
-
|
|
22
|
-
try {
|
|
23
|
-
const payload = await readStdin();
|
|
24
|
-
if (!payload) pass();
|
|
25
|
-
|
|
26
|
-
const filePath = getFilePath(payload);
|
|
27
|
-
if (!filePath) pass();
|
|
28
|
-
|
|
29
|
-
// Only check framework/skills/**/SKILL.md
|
|
30
|
-
if (!filePath.includes('framework/skills/')) pass();
|
|
31
|
-
if (!filePath.endsWith('SKILL.md')) pass();
|
|
32
|
-
|
|
33
|
-
// Only validate full Write operations (not Edit patches)
|
|
34
|
-
if (!isWriteOperation(payload)) pass();
|
|
35
|
-
|
|
36
|
-
const content = getContentToValidate(payload);
|
|
37
|
-
if (!content) pass();
|
|
38
|
-
|
|
39
|
-
// Check for YAML frontmatter delimiters
|
|
40
|
-
const frontmatterMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
41
|
-
if (!frontmatterMatch) {
|
|
42
|
-
block(
|
|
43
|
-
`MORPH-SPEC: SKILL.md at '${filePath}' is missing YAML frontmatter.\n` +
|
|
44
|
-
'All skill files must start with --- delimited YAML frontmatter containing name: and description: fields.'
|
|
45
|
-
);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const frontmatter = frontmatterMatch[1];
|
|
49
|
-
const missing = [];
|
|
50
|
-
|
|
51
|
-
if (!/^name:/m.test(frontmatter)) {
|
|
52
|
-
missing.push('name:');
|
|
53
|
-
}
|
|
54
|
-
if (!/^description:/m.test(frontmatter)) {
|
|
55
|
-
missing.push('description:');
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
if (missing.length > 0) {
|
|
59
|
-
block(
|
|
60
|
-
`MORPH-SPEC: SKILL.md at '${filePath}' is missing required frontmatter fields:\n` +
|
|
61
|
-
missing.map(m => ` - ${m}`).join('\n') + '\n\n' +
|
|
62
|
-
'All skill files must have name: and description: in their YAML frontmatter.'
|
|
63
|
-
);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
pass();
|
|
67
|
-
} catch {
|
|
68
|
-
// Fail-open
|
|
69
|
-
process.exit(0);
|
|
70
|
-
}
|