@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.
Files changed (137) hide show
  1. package/CLAUDE.md +21 -0
  2. package/README.md +2 -2
  3. package/bin/morph-spec.js +15 -56
  4. package/bin/task-manager.js +115 -14
  5. package/bin/validate.js +67 -33
  6. package/claude-plugin.json +1 -1
  7. package/docs/CHEATSHEET.md +201 -203
  8. package/docs/QUICKSTART.md +2 -2
  9. package/framework/CLAUDE.md +21 -0
  10. package/framework/agents.json +698 -176
  11. package/framework/hooks/claude-code/post-tool-use/context-refresh.js +1 -1
  12. package/framework/hooks/claude-code/post-tool-use/dispatch.js +2 -2
  13. package/framework/hooks/claude-code/post-tool-use/skill-reminder.js +155 -0
  14. package/framework/hooks/claude-code/pre-tool-use/protect-spec-files.js +1 -1
  15. package/framework/hooks/claude-code/session-start/inject-morph-context.js +71 -2
  16. package/framework/hooks/claude-code/statusline.py +76 -30
  17. package/framework/hooks/claude-code/user-prompt/set-terminal-title.js +14 -6
  18. package/framework/hooks/shared/activity-logger.js +0 -24
  19. package/framework/hooks/shared/phase-utils.js +3 -0
  20. package/framework/hooks/shared/skill-reminder-helpers.js +79 -0
  21. package/framework/hooks/shared/stale-task-reset.js +57 -0
  22. package/framework/hooks/shared/state-reader.js +2 -2
  23. package/framework/hooks/shared/worktree-helpers.js +53 -0
  24. package/framework/phases.json +40 -8
  25. package/framework/skills/level-0-meta/brainstorming/SKILL.md +1 -1
  26. package/framework/skills/level-0-meta/code-review/SKILL.md +1 -1
  27. package/framework/skills/level-0-meta/code-review-nextjs/SKILL.md +163 -163
  28. package/framework/skills/level-0-meta/frontend-review/SKILL.md +5 -5
  29. package/framework/skills/level-0-meta/morph-checklist/SKILL.md +2 -2
  30. package/framework/skills/level-0-meta/morph-init/SKILL.md +5 -5
  31. package/framework/skills/level-0-meta/morph-replicate/SKILL.md +4 -4
  32. package/framework/skills/level-0-meta/morph-replicate/references/blazor-html-mapping.md +1 -1
  33. package/framework/skills/level-0-meta/post-implementation/SKILL.md +59 -12
  34. package/framework/skills/level-0-meta/simulation-checklist/SKILL.md +1 -1
  35. package/framework/skills/level-0-meta/terminal-title/SKILL.md +1 -1
  36. package/framework/skills/level-0-meta/tool-usage-guide/SKILL.md +1 -1
  37. package/framework/skills/level-0-meta/tool-usage-guide/references/tools-per-phase.md +6 -5
  38. package/framework/skills/level-0-meta/verification-before-completion/SKILL.md +1 -1
  39. package/framework/skills/level-1-workflows/phase-clarify/SKILL.md +215 -189
  40. package/framework/skills/level-1-workflows/phase-codebase-analysis/SKILL.md +251 -251
  41. package/framework/skills/level-1-workflows/phase-design/SKILL.md +382 -365
  42. package/framework/skills/level-1-workflows/phase-implement/SKILL.md +492 -450
  43. package/framework/skills/level-1-workflows/phase-setup/SKILL.md +194 -190
  44. package/framework/skills/level-1-workflows/phase-tasks/SKILL.md +270 -270
  45. package/framework/skills/level-1-workflows/phase-uiux/SKILL.md +285 -285
  46. package/framework/standards/STANDARDS.json +640 -88
  47. package/framework/standards/infrastructure/vercel/vercel-database.md +106 -0
  48. package/framework/templates/REGISTRY.json +1825 -1909
  49. package/framework/templates/context/CONTEXT-FEATURE.md +276 -276
  50. package/framework/templates/docs/onboarding.md +1 -5
  51. package/package.json +2 -6
  52. package/src/commands/agents/dispatch-agents.js +55 -4
  53. package/src/commands/project/doctor.js +16 -47
  54. package/src/commands/project/init.js +1 -1
  55. package/src/commands/project/status.js +2 -2
  56. package/src/commands/project/update.js +381 -365
  57. package/src/commands/project/worktree.js +154 -0
  58. package/src/commands/state/advance-phase.js +120 -30
  59. package/src/commands/state/approve.js +2 -2
  60. package/src/commands/state/index.js +7 -8
  61. package/src/commands/state/phase-runner.js +1 -1
  62. package/src/commands/state/state.js +61 -6
  63. package/src/commands/tasks/task.js +78 -99
  64. package/src/commands/templates/template-render.js +93 -173
  65. package/src/commands/trust/trust.js +26 -21
  66. package/src/core/paths/output-schema.js +15 -0
  67. package/src/core/state/state-manager.js +28 -54
  68. package/src/core/workflows/workflow-detector.js +9 -87
  69. package/src/lib/phase-chain/phase-validator.js +330 -0
  70. package/src/lib/stack/stack-profile.js +88 -0
  71. package/src/lib/tasks/task-classifier.js +16 -0
  72. package/src/lib/tasks/test-runner.js +77 -0
  73. package/src/lib/trust/trust-manager.js +32 -144
  74. package/src/lib/validators/spec-validator.js +58 -4
  75. package/src/lib/validators/validation-runner.js +23 -11
  76. package/src/scripts/setup-infra.js +240 -224
  77. package/src/utils/agents-installer.js +2 -2
  78. package/src/utils/banner.js +1 -1
  79. package/src/utils/claude-settings-manager.js +1 -1
  80. package/src/utils/file-copier.js +1 -0
  81. package/src/utils/hooks-installer.js +258 -8
  82. package/framework/hooks/dev/check-sync-health.js +0 -117
  83. package/framework/hooks/dev/guard-version-numbers.js +0 -57
  84. package/framework/hooks/dev/sync-standards-registry.js +0 -60
  85. package/framework/hooks/dev/sync-template-registry.js +0 -60
  86. package/framework/hooks/dev/validate-skill-format.js +0 -70
  87. package/framework/hooks/dev/validate-standard-format.js +0 -73
  88. package/framework/templates/meta-prompts/hops/hop-retry.md +0 -78
  89. package/framework/templates/meta-prompts/hops/hop-validation.md +0 -97
  90. package/framework/templates/meta-prompts/hops/hop-wrapper.md +0 -36
  91. package/framework/workflows/configs/design-impl.json +0 -49
  92. package/framework/workflows/configs/express.json +0 -45
  93. package/framework/workflows/configs/fast-track.json +0 -42
  94. package/framework/workflows/configs/full-morph.json +0 -79
  95. package/framework/workflows/configs/fusion.json +0 -39
  96. package/framework/workflows/configs/long-running.json +0 -33
  97. package/framework/workflows/configs/spec-only.json +0 -43
  98. package/framework/workflows/configs/ui-refresh.json +0 -49
  99. package/framework/workflows/configs/zero-touch.json +0 -82
  100. package/src/commands/project/monitor.js +0 -295
  101. package/src/commands/project/tutorial.js +0 -115
  102. package/src/commands/state/validate-phase.js +0 -238
  103. package/src/commands/templates/generate-contracts.js +0 -445
  104. package/src/core/orchestrator.js +0 -171
  105. package/src/core/registry/command-registry.js +0 -28
  106. package/src/core/registry/index.js +0 -8
  107. package/src/core/registry/validator-registry.js +0 -204
  108. package/src/core/templates/template-validator.js +0 -296
  109. package/src/generator/config-generator.js +0 -206
  110. package/src/generator/templates/config.json.template +0 -40
  111. package/src/generator/templates/project.md.template +0 -67
  112. package/src/lib/agents/micro-agent-factory.js +0 -161
  113. package/src/lib/analysis/complexity-analyzer.js +0 -441
  114. package/src/lib/analysis/index.js +0 -7
  115. package/src/lib/analytics/analytics-engine.js +0 -345
  116. package/src/lib/checkpoints/checkpoint-hooks.js +0 -298
  117. package/src/lib/checkpoints/index.js +0 -7
  118. package/src/lib/context/context-bundler.js +0 -241
  119. package/src/lib/context/context-optimizer.js +0 -212
  120. package/src/lib/context/context-tracker.js +0 -273
  121. package/src/lib/context/core-four-tracker.js +0 -201
  122. package/src/lib/context/mcp-optimizer.js +0 -200
  123. package/src/lib/execution/fusion-executor.js +0 -304
  124. package/src/lib/execution/parallel-executor.js +0 -270
  125. package/src/lib/hooks/stop-hook-executor.js +0 -286
  126. package/src/lib/hops/hop-composer.js +0 -221
  127. package/src/lib/phase-chain/eligibility-checker.js +0 -243
  128. package/src/lib/threads/thread-coordinator.js +0 -238
  129. package/src/lib/threads/thread-manager.js +0 -317
  130. package/src/lib/tracking/artifact-trail.js +0 -202
  131. package/src/scanner/project-scanner.js +0 -242
  132. package/src/ui/diff-display.js +0 -91
  133. package/src/ui/interactive-wizard.js +0 -96
  134. package/src/ui/user-review.js +0 -211
  135. package/src/ui/wizard-questions.js +0 -188
  136. package/src/utils/color-utils.js +0 -70
  137. 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.8.0';
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 delete .morph/ (e.g. contains "rm" with "-r" or "-rf" and ".morph")? If yes, BLOCK.
108
- 2. Does the command edit state.json directly via sed, jq, awk, echo, cat, or printf with a shell redirect (>)? If yes, BLOCK.
109
- 3. Does the command write to .morph/framework/ via a shell redirect (>)? If yes, BLOCK.
110
- If any condition is true, respond: {"ok": false, "reason": "MORPH-SPEC: <brief explanation and suggested alternative using morph-spec CLI>"}
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
- }