@polymorphism-tech/morph-spec 4.10.6 → 4.10.8

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 (149) hide show
  1. package/CLAUDE.md +3 -0
  2. package/LICENSE +71 -71
  3. package/README.md +379 -379
  4. package/bin/morph-spec.js +89 -0
  5. package/bin/task-manager.js +59 -6
  6. package/claude-plugin.json +14 -14
  7. package/docs/CHEATSHEET.md +1 -1
  8. package/docs/QUICKSTART.md +206 -206
  9. package/framework/agents/README.md +14 -14
  10. package/framework/agents/architecture/standards-architect.md +159 -159
  11. package/framework/agents/frontend/blazor-builder.md +13 -0
  12. package/framework/agents/frontend/css-specialist.md +87 -0
  13. package/framework/agents/frontend/nextjs-expert.md +13 -0
  14. package/framework/agents/frontend/nextjs-ui-specialist.md +218 -0
  15. package/framework/agents/frontend/ui-ux-designer.md +80 -10
  16. package/framework/agents/infrastructure/azure-architect.md +149 -149
  17. package/framework/agents/infrastructure/infra-architect.md +48 -48
  18. package/framework/agents.json +2268 -2567
  19. package/framework/hooks/README.md +204 -204
  20. package/framework/hooks/claude-code/post-tool-use/dispatch.js +10 -5
  21. package/framework/hooks/claude-code/session-start/inject-morph-context.js +10 -24
  22. package/framework/hooks/shared/phase-utils.js +139 -139
  23. package/framework/hooks/shared/worktree-helpers.js +30 -8
  24. package/framework/rules/csharp-standards.md +13 -13
  25. package/framework/rules/frontend-standards.md +13 -13
  26. package/framework/rules/infrastructure-standards.md +16 -16
  27. package/framework/rules/testing-standards.md +14 -14
  28. package/framework/skills/README.md +0 -1
  29. package/framework/skills/level-0-meta/morph-brainstorming/SKILL.md +12 -0
  30. package/framework/skills/level-0-meta/morph-code-review-nextjs/references/review-example-nextjs.md +254 -254
  31. package/framework/skills/level-0-meta/morph-init/SKILL.md +88 -3
  32. package/framework/skills/level-1-workflows/morph-phase-clarify/SKILL.md +247 -247
  33. package/framework/skills/level-1-workflows/morph-phase-codebase-analysis/SKILL.md +270 -270
  34. package/framework/skills/level-1-workflows/morph-phase-design/SKILL.md +515 -499
  35. package/framework/skills/level-1-workflows/morph-phase-implement/SKILL.md +508 -471
  36. package/framework/skills/level-1-workflows/morph-phase-plan/SKILL.md +12 -1
  37. package/framework/skills/level-1-workflows/morph-phase-setup/SKILL.md +272 -258
  38. package/framework/skills/level-1-workflows/morph-phase-tasks/SKILL.md +317 -311
  39. package/framework/skills/level-1-workflows/morph-phase-uiux/SKILL.md +424 -323
  40. package/framework/skills/level-1-workflows/morph-scope-escalation/SKILL.md +1 -1
  41. package/framework/standards/STANDARDS.json +2040 -2040
  42. package/framework/standards/frontend/nextjs/motion-patterns.md +321 -0
  43. package/framework/standards/frontend/nextjs/nextjs-patterns.md +17 -17
  44. package/framework/templates/README.md +814 -814
  45. package/framework/templates/code/dotnet/contracts/Commands.cs +74 -74
  46. package/framework/templates/code/dotnet/contracts/Entities.cs +25 -25
  47. package/framework/templates/code/dotnet/contracts/Queries.cs +74 -74
  48. package/framework/templates/code/dotnet/contracts/README.md +74 -74
  49. package/framework/templates/code/dotnet/contracts/api-contracts.cs +173 -173
  50. package/framework/templates/code/dotnet/frontend/component.razor +239 -239
  51. package/framework/templates/code/dotnet/jobs/agent.cs +163 -163
  52. package/framework/templates/code/dotnet/jobs/job.cs +171 -171
  53. package/framework/templates/code/dotnet/test.cs +239 -239
  54. package/framework/templates/code/sql/rls-policy.sql +57 -57
  55. package/framework/templates/code/sql/supabase-migration.sql +100 -100
  56. package/framework/templates/code/sql/supabase-migration.template.sql +113 -113
  57. package/framework/templates/code/typescript/contracts.ts +168 -168
  58. package/framework/templates/context/CONTEXT.md +181 -181
  59. package/framework/templates/context/design-system.md +211 -0
  60. package/framework/templates/docs/proposal.md +182 -182
  61. package/framework/templates/docs/user-stories.md +34 -34
  62. package/framework/templates/examples/design-system-examples.md +357 -357
  63. package/framework/templates/examples/spec-examples.md +90 -90
  64. package/framework/templates/feature/decisions.md +187 -187
  65. package/framework/templates/feature/recap.md +146 -146
  66. package/framework/templates/feature/tasks.md +199 -199
  67. package/framework/templates/infrastructure/azure/README.md +286 -286
  68. package/framework/templates/infrastructure/azure/deploy-checklist.md +426 -426
  69. package/framework/templates/infrastructure/azure/parameters.dev.json +29 -29
  70. package/framework/templates/infrastructure/azure/parameters.prod.json +29 -29
  71. package/framework/templates/infrastructure/azure/parameters.staging.json +29 -29
  72. package/framework/templates/infrastructure/docker/docker-compose.template.yml +67 -67
  73. package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
  74. package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
  75. package/framework/templates/infrastructure/docker/easypanel.template.json +54 -54
  76. package/package.json +82 -82
  77. package/src/commands/agents/agent-metrics.js +174 -0
  78. package/src/commands/agents/audit-dispatch.js +82 -0
  79. package/src/commands/agents/detect-agents.js +275 -0
  80. package/src/commands/agents/dispatch-agents.js +41 -135
  81. package/src/commands/agents/index.js +13 -4
  82. package/src/commands/agents/manage-agents.js +352 -0
  83. package/src/commands/project/doctor.js +59 -0
  84. package/src/commands/project/install-plugin-cmd.js +28 -28
  85. package/src/commands/project/setup-infra-cmd.js +12 -12
  86. package/src/commands/project/update.js +1 -17
  87. package/src/commands/project/worktree.js +58 -2
  88. package/src/commands/state/advance-phase.js +11 -3
  89. package/src/commands/tasks/index.js +4 -4
  90. package/src/commands/templates/index.js +5 -5
  91. package/src/commands/utils/index.js +3 -3
  92. package/src/commands/validation/index.js +4 -4
  93. package/src/core/state/phase-state-machine.js +217 -217
  94. package/src/core/state/state-manager.js +81 -0
  95. package/src/lib/agents/agents-manager.js +349 -0
  96. package/src/lib/agents/metrics-calculator.js +125 -0
  97. package/src/lib/agents/output-collector.js +75 -0
  98. package/src/lib/generators/context-generator.js +8 -7
  99. package/src/lib/generators/design-system-generator.js +298 -298
  100. package/src/lib/generators/index.js +10 -10
  101. package/src/lib/generators/metadata-extractor.js +387 -387
  102. package/src/lib/stack-filter.js +65 -65
  103. package/src/lib/standards/index.js +7 -7
  104. package/src/lib/standards/standards-context-injector.js +302 -302
  105. package/src/lib/tasks/task-parser.js +94 -3
  106. package/src/lib/validators/architecture/architecture-validator.js +387 -387
  107. package/src/lib/validators/architecture/index.js +7 -7
  108. package/src/lib/validators/blazor/blazor-concurrency-analyzer.js +316 -316
  109. package/src/lib/validators/blazor/blazor-state-validator.js +279 -279
  110. package/src/lib/validators/blazor/blazor-validator.js +369 -369
  111. package/src/lib/validators/blazor/index.js +9 -9
  112. package/src/lib/validators/checkpoint-persistence.js +39 -0
  113. package/src/lib/validators/content/content-validator.js +287 -279
  114. package/src/lib/validators/content/index.js +7 -7
  115. package/src/lib/validators/contracts/index.js +7 -7
  116. package/src/lib/validators/css/css-validator.js +352 -352
  117. package/src/lib/validators/css/index.js +7 -7
  118. package/src/lib/validators/design-system/design-system-validator.js +231 -231
  119. package/src/lib/validators/design-system/index.js +7 -7
  120. package/src/lib/validators/nextjs/index.js +6 -6
  121. package/src/lib/validators/nextjs/next-component-validator.js +183 -183
  122. package/src/lib/validators/packages/index.js +7 -7
  123. package/src/lib/validators/packages/package-validator.js +360 -360
  124. package/src/lib/validators/shared/index.js +12 -12
  125. package/src/lib/validators/shared/issue-counter.js +18 -18
  126. package/src/lib/validators/shared/result-formatter.js +124 -124
  127. package/src/lib/validators/spec-validator.js +2 -2
  128. package/src/lib/validators/ui/index.js +7 -7
  129. package/src/lib/validators/ui/ui-contrast-validator.js +422 -422
  130. package/src/lib/validators/validation-runner.js +20 -0
  131. package/src/scripts/global-install.js +58 -58
  132. package/src/scripts/install-plugin.js +126 -126
  133. package/src/scripts/setup-infra.js +1 -17
  134. package/src/utils/agent-id-mapper.js +11 -0
  135. package/src/utils/agents-installer.js +40 -8
  136. package/src/utils/claude-md-injector.js +90 -90
  137. package/src/utils/claude-settings-manager.js +1 -1
  138. package/src/utils/file-copier.js +200 -196
  139. package/src/utils/hooks-installer.js +556 -739
  140. package/src/utils/logger.js +32 -32
  141. package/src/utils/skills-installer.js +178 -178
  142. package/src/utils/tier-helpers.js +24 -0
  143. package/src/utils/version-checker.js +175 -175
  144. package/framework/agents/architecture/po-pm-advisor.md +0 -203
  145. package/framework/hooks/claude-code/user-prompt/set-terminal-title.js +0 -68
  146. package/framework/skills/level-1-workflows/morph-phase-implement/.morph/logs/activity.json +0 -38
  147. package/framework/skills/level-1-workflows/morph-phase-tasks/.morph/logs/activity.json +0 -14
  148. package/framework/templates/.idea/morph-templates.xml +0 -92
  149. package/framework/templates/.vscode/morph-templates.code-snippets +0 -186
@@ -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
 
@@ -1,58 +1,58 @@
1
- // src/scripts/global-install.js
2
- import { join, dirname } from 'path';
3
- import { fileURLToPath } from 'url';
4
- import { homedir } from 'os';
5
- import { mkdir, copyFile } from 'fs/promises';
6
- import { existsSync } from 'fs';
7
- import { installGlobalStatusline } from '../utils/hooks-installer.js';
8
-
9
- const __dirname = dirname(fileURLToPath(import.meta.url));
10
- const FRAMEWORK_DIR = join(__dirname, '..', '..', 'framework');
11
-
12
- /**
13
- * Install morph-init skill to a Claude global skills directory.
14
- * @param {string} claudeDir - target ~/.claude dir (defaults to real ~/.claude)
15
- */
16
- export async function installGlobalSkill(claudeDir = join(homedir(), '.claude')) {
17
- const src = join(FRAMEWORK_DIR, 'skills', 'level-0-meta', 'morph-init', 'SKILL.md');
18
- const destDir = join(claudeDir, 'skills', 'morph-init');
19
- const dest = join(destDir, 'SKILL.md');
20
-
21
- if (!existsSync(src)) {
22
- console.warn(`morph-init skill not found at ${src} — skipping global install`);
23
- return;
24
- }
25
-
26
- await mkdir(destDir, { recursive: true });
27
- await copyFile(src, dest);
28
- }
29
-
30
- // Run as postinstall script
31
- if (process.argv[1] === fileURLToPath(import.meta.url)) {
32
- const HOOKS_SRC = join(FRAMEWORK_DIR, 'hooks', 'claude-code');
33
- const { readFileSync } = await import('fs');
34
- const pkg = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8'));
35
-
36
- const [skillResult, statuslineResult] = await Promise.allSettled([
37
- installGlobalSkill(),
38
- installGlobalStatusline(HOOKS_SRC),
39
- ]);
40
-
41
- const results = [
42
- {
43
- ok: skillResult.status === 'fulfilled',
44
- label: 'morph-init skill',
45
- path: '~/.claude/skills/',
46
- error: skillResult.reason?.message,
47
- },
48
- {
49
- ok: statuslineResult.status === 'fulfilled',
50
- label: 'statusline',
51
- path: '~/.claude/',
52
- error: statuslineResult.reason?.message,
53
- },
54
- ];
55
-
56
- const { printInstallBanner } = await import('../utils/banner.js');
57
- printInstallBanner({ version: pkg.version, nodeVersion: process.version, results });
58
- }
1
+ // src/scripts/global-install.js
2
+ import { join, dirname } from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ import { homedir } from 'os';
5
+ import { mkdir, copyFile } from 'fs/promises';
6
+ import { existsSync } from 'fs';
7
+ import { installGlobalStatusline } from '../utils/hooks-installer.js';
8
+
9
+ const __dirname = dirname(fileURLToPath(import.meta.url));
10
+ const FRAMEWORK_DIR = join(__dirname, '..', '..', 'framework');
11
+
12
+ /**
13
+ * Install morph-init skill to a Claude global skills directory.
14
+ * @param {string} claudeDir - target ~/.claude dir (defaults to real ~/.claude)
15
+ */
16
+ export async function installGlobalSkill(claudeDir = join(homedir(), '.claude')) {
17
+ const src = join(FRAMEWORK_DIR, 'skills', 'level-0-meta', 'morph-init', 'SKILL.md');
18
+ const destDir = join(claudeDir, 'skills', 'morph-init');
19
+ const dest = join(destDir, 'SKILL.md');
20
+
21
+ if (!existsSync(src)) {
22
+ console.warn(`morph-init skill not found at ${src} — skipping global install`);
23
+ return;
24
+ }
25
+
26
+ await mkdir(destDir, { recursive: true });
27
+ await copyFile(src, dest);
28
+ }
29
+
30
+ // Run as postinstall script
31
+ if (process.argv[1] === fileURLToPath(import.meta.url)) {
32
+ const HOOKS_SRC = join(FRAMEWORK_DIR, 'hooks', 'claude-code');
33
+ const { readFileSync } = await import('fs');
34
+ const pkg = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8'));
35
+
36
+ const [skillResult, statuslineResult] = await Promise.allSettled([
37
+ installGlobalSkill(),
38
+ installGlobalStatusline(HOOKS_SRC),
39
+ ]);
40
+
41
+ const results = [
42
+ {
43
+ ok: skillResult.status === 'fulfilled',
44
+ label: 'morph-init skill',
45
+ path: '~/.claude/skills/',
46
+ error: skillResult.reason?.message,
47
+ },
48
+ {
49
+ ok: statuslineResult.status === 'fulfilled',
50
+ label: 'statusline',
51
+ path: '~/.claude/',
52
+ error: statuslineResult.reason?.message,
53
+ },
54
+ ];
55
+
56
+ const { printInstallBanner } = await import('../utils/banner.js');
57
+ printInstallBanner({ version: pkg.version, nodeVersion: process.version, results });
58
+ }
@@ -1,126 +1,126 @@
1
- // src/scripts/install-plugin.js
2
- import { join } from 'path';
3
- import { homedir } from 'os';
4
- import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'fs';
5
- import { mkdir, writeFile } from 'fs/promises';
6
- import https from 'node:https';
7
-
8
- const PLUGINS_DIR = join(homedir(), '.claude', 'plugins');
9
- const CACHE_DIR = join(PLUGINS_DIR, 'cache', 'claude-plugins-official');
10
- const INSTALLED_FILE = join(PLUGINS_DIR, 'installed_plugins.json');
11
- const GH_REPO = 'anthropics/claude-plugins-official';
12
-
13
- /** Fetch JSON from GitHub API (no auth — public repo) */
14
- async function fetchGitHub(path) {
15
- return new Promise((resolve, reject) => {
16
- const opts = {
17
- hostname: 'api.github.com',
18
- path,
19
- headers: { 'User-Agent': 'morph-spec', 'Accept': 'application/vnd.github.v3+json' }
20
- };
21
- https.get(opts, (res) => {
22
- let data = '';
23
- res.on('data', c => data += c);
24
- res.on('end', () => {
25
- try { resolve(JSON.parse(data)); }
26
- catch (e) { reject(new Error(`GitHub API parse error: ${data.slice(0, 200)}`)); }
27
- });
28
- }).on('error', reject);
29
- });
30
- }
31
-
32
- /** Download raw file content from a URL, following one redirect */
33
- async function downloadFile(rawUrl) {
34
- return new Promise((resolve, reject) => {
35
- const doGet = (url) => {
36
- const parsed = new URL(url);
37
- https.get({
38
- hostname: parsed.hostname,
39
- path: parsed.pathname + parsed.search,
40
- headers: { 'User-Agent': 'morph-spec' }
41
- }, (res) => {
42
- if (res.statusCode === 301 || res.statusCode === 302) {
43
- return doGet(res.headers.location);
44
- }
45
- const chunks = [];
46
- res.on('data', c => chunks.push(c));
47
- res.on('end', () => resolve(Buffer.concat(chunks)));
48
- }).on('error', reject);
49
- };
50
- doGet(rawUrl);
51
- });
52
- }
53
-
54
- /** Read installed_plugins.json safely */
55
- export function readInstalledPlugins() {
56
- if (!existsSync(INSTALLED_FILE)) return { version: 2, plugins: {} };
57
- try { return JSON.parse(readFileSync(INSTALLED_FILE, 'utf8')); }
58
- catch { return { version: 2, plugins: {} }; }
59
- }
60
-
61
- /** Check if plugin key exists in the plugins map */
62
- export function isPluginInstalled(plugins, name) {
63
- return `${name}@claude-plugins-official` in plugins;
64
- }
65
-
66
- /** Build an installed_plugins.json entry */
67
- export function buildInstallEntry(pluginName, installPath, gitCommitSha, version) {
68
- const now = new Date().toISOString();
69
- return { scope: 'user', installPath, version, installedAt: now, lastUpdated: now, gitCommitSha };
70
- }
71
-
72
- /** Resolve latest commit SHA for a plugin path in the repo */
73
- export async function resolvePluginVersion(pluginName) {
74
- const commits = await fetchGitHub(`/repos/${GH_REPO}/commits?path=${pluginName}&per_page=1`);
75
- if (!Array.isArray(commits) || commits.length === 0) {
76
- throw new Error(`Could not resolve version for plugin: ${pluginName}`);
77
- }
78
- const sha = commits[0].sha;
79
- return { sha, shortSha: sha.slice(0, 12) };
80
- }
81
-
82
- /** Download all files for a plugin from the repo tree */
83
- async function downloadPluginFiles(pluginName, sha, destDir) {
84
- const tree = await fetchGitHub(`/repos/${GH_REPO}/git/trees/${sha}?recursive=1`);
85
- if (!tree.tree) throw new Error('GitHub tree API returned unexpected shape');
86
-
87
- const pluginFiles = tree.tree.filter(f => f.path.startsWith(`${pluginName}/`) && f.type === 'blob');
88
- if (pluginFiles.length === 0) throw new Error(`Plugin "${pluginName}" not found in repository`);
89
-
90
- for (const file of pluginFiles) {
91
- const relativePath = file.path.slice(pluginName.length + 1);
92
- const destPath = join(destDir, relativePath);
93
- await mkdir(join(destPath, '..'), { recursive: true });
94
- const content = await downloadFile(
95
- `https://raw.githubusercontent.com/${GH_REPO}/${sha}/${file.path}`
96
- );
97
- await writeFile(destPath, content);
98
- }
99
- }
100
-
101
- /**
102
- * Install a Claude Code plugin from GitHub.
103
- * @param {string} pluginName - e.g. "superpowers"
104
- * @returns {{ installed: boolean, version: string, alreadyInstalled: boolean }}
105
- */
106
- export async function installPlugin(pluginName) {
107
- const data = readInstalledPlugins();
108
- if (isPluginInstalled(data.plugins, pluginName)) {
109
- const existing = data.plugins[`${pluginName}@claude-plugins-official`];
110
- const version = Array.isArray(existing) ? existing[0]?.version : 'unknown';
111
- return { installed: true, alreadyInstalled: true, version };
112
- }
113
-
114
- const { sha, shortSha } = await resolvePluginVersion(pluginName);
115
- const installPath = join(CACHE_DIR, pluginName, shortSha);
116
-
117
- await mkdir(installPath, { recursive: true });
118
- await downloadPluginFiles(pluginName, sha, installPath);
119
-
120
- const entry = buildInstallEntry(pluginName, installPath, sha, shortSha);
121
- data.plugins[`${pluginName}@claude-plugins-official`] = [entry];
122
- mkdirSync(PLUGINS_DIR, { recursive: true });
123
- writeFileSync(INSTALLED_FILE, JSON.stringify(data, null, 2));
124
-
125
- return { installed: true, alreadyInstalled: false, version: shortSha };
126
- }
1
+ // src/scripts/install-plugin.js
2
+ import { join } from 'path';
3
+ import { homedir } from 'os';
4
+ import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'fs';
5
+ import { mkdir, writeFile } from 'fs/promises';
6
+ import https from 'node:https';
7
+
8
+ const PLUGINS_DIR = join(homedir(), '.claude', 'plugins');
9
+ const CACHE_DIR = join(PLUGINS_DIR, 'cache', 'claude-plugins-official');
10
+ const INSTALLED_FILE = join(PLUGINS_DIR, 'installed_plugins.json');
11
+ const GH_REPO = 'anthropics/claude-plugins-official';
12
+
13
+ /** Fetch JSON from GitHub API (no auth — public repo) */
14
+ async function fetchGitHub(path) {
15
+ return new Promise((resolve, reject) => {
16
+ const opts = {
17
+ hostname: 'api.github.com',
18
+ path,
19
+ headers: { 'User-Agent': 'morph-spec', 'Accept': 'application/vnd.github.v3+json' }
20
+ };
21
+ https.get(opts, (res) => {
22
+ let data = '';
23
+ res.on('data', c => data += c);
24
+ res.on('end', () => {
25
+ try { resolve(JSON.parse(data)); }
26
+ catch (e) { reject(new Error(`GitHub API parse error: ${data.slice(0, 200)}`)); }
27
+ });
28
+ }).on('error', reject);
29
+ });
30
+ }
31
+
32
+ /** Download raw file content from a URL, following one redirect */
33
+ async function downloadFile(rawUrl) {
34
+ return new Promise((resolve, reject) => {
35
+ const doGet = (url) => {
36
+ const parsed = new URL(url);
37
+ https.get({
38
+ hostname: parsed.hostname,
39
+ path: parsed.pathname + parsed.search,
40
+ headers: { 'User-Agent': 'morph-spec' }
41
+ }, (res) => {
42
+ if (res.statusCode === 301 || res.statusCode === 302) {
43
+ return doGet(res.headers.location);
44
+ }
45
+ const chunks = [];
46
+ res.on('data', c => chunks.push(c));
47
+ res.on('end', () => resolve(Buffer.concat(chunks)));
48
+ }).on('error', reject);
49
+ };
50
+ doGet(rawUrl);
51
+ });
52
+ }
53
+
54
+ /** Read installed_plugins.json safely */
55
+ export function readInstalledPlugins() {
56
+ if (!existsSync(INSTALLED_FILE)) return { version: 2, plugins: {} };
57
+ try { return JSON.parse(readFileSync(INSTALLED_FILE, 'utf8')); }
58
+ catch { return { version: 2, plugins: {} }; }
59
+ }
60
+
61
+ /** Check if plugin key exists in the plugins map */
62
+ export function isPluginInstalled(plugins, name) {
63
+ return `${name}@claude-plugins-official` in plugins;
64
+ }
65
+
66
+ /** Build an installed_plugins.json entry */
67
+ export function buildInstallEntry(pluginName, installPath, gitCommitSha, version) {
68
+ const now = new Date().toISOString();
69
+ return { scope: 'user', installPath, version, installedAt: now, lastUpdated: now, gitCommitSha };
70
+ }
71
+
72
+ /** Resolve latest commit SHA for a plugin path in the repo */
73
+ export async function resolvePluginVersion(pluginName) {
74
+ const commits = await fetchGitHub(`/repos/${GH_REPO}/commits?path=${pluginName}&per_page=1`);
75
+ if (!Array.isArray(commits) || commits.length === 0) {
76
+ throw new Error(`Could not resolve version for plugin: ${pluginName}`);
77
+ }
78
+ const sha = commits[0].sha;
79
+ return { sha, shortSha: sha.slice(0, 12) };
80
+ }
81
+
82
+ /** Download all files for a plugin from the repo tree */
83
+ async function downloadPluginFiles(pluginName, sha, destDir) {
84
+ const tree = await fetchGitHub(`/repos/${GH_REPO}/git/trees/${sha}?recursive=1`);
85
+ if (!tree.tree) throw new Error('GitHub tree API returned unexpected shape');
86
+
87
+ const pluginFiles = tree.tree.filter(f => f.path.startsWith(`${pluginName}/`) && f.type === 'blob');
88
+ if (pluginFiles.length === 0) throw new Error(`Plugin "${pluginName}" not found in repository`);
89
+
90
+ for (const file of pluginFiles) {
91
+ const relativePath = file.path.slice(pluginName.length + 1);
92
+ const destPath = join(destDir, relativePath);
93
+ await mkdir(join(destPath, '..'), { recursive: true });
94
+ const content = await downloadFile(
95
+ `https://raw.githubusercontent.com/${GH_REPO}/${sha}/${file.path}`
96
+ );
97
+ await writeFile(destPath, content);
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Install a Claude Code plugin from GitHub.
103
+ * @param {string} pluginName - e.g. "superpowers"
104
+ * @returns {{ installed: boolean, version: string, alreadyInstalled: boolean }}
105
+ */
106
+ export async function installPlugin(pluginName) {
107
+ const data = readInstalledPlugins();
108
+ if (isPluginInstalled(data.plugins, pluginName)) {
109
+ const existing = data.plugins[`${pluginName}@claude-plugins-official`];
110
+ const version = Array.isArray(existing) ? existing[0]?.version : 'unknown';
111
+ return { installed: true, alreadyInstalled: true, version };
112
+ }
113
+
114
+ const { sha, shortSha } = await resolvePluginVersion(pluginName);
115
+ const installPath = join(CACHE_DIR, pluginName, shortSha);
116
+
117
+ await mkdir(installPath, { recursive: true });
118
+ await downloadPluginFiles(pluginName, sha, installPath);
119
+
120
+ const entry = buildInstallEntry(pluginName, installPath, sha, shortSha);
121
+ data.plugins[`${pluginName}@claude-plugins-official`] = [entry];
122
+ mkdirSync(PLUGINS_DIR, { recursive: true });
123
+ writeFileSync(INSTALLED_FILE, JSON.stringify(data, null, 2));
124
+
125
+ return { installed: true, alreadyInstalled: false, version: shortSha };
126
+ }
@@ -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, installVSCodeTerminalSettings, installShellIntegration } from '../utils/claude-settings-manager.js';
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(s => `- ${s}`).join('\n')}`);
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
- const filename = `morph-domain-${name}.md`;
274
- const targetPath = join(targetDir, filename);
275
- writeFileSync(targetPath, `---\n${frontmatter}---\n\n${body.trimStart()}`, 'utf-8');
276
- installed.add(filename);
277
- specialists++;
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)) {