@polymorphism-tech/morph-spec 9.8.0 → 10.0.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 (185) hide show
  1. package/CLAUDE.md +10 -10
  2. package/README.md +4 -4
  3. package/bin/morph-spec.js +40 -43
  4. package/claude-plugin.json +1 -1
  5. package/docs/CHEATSHEET.md +1 -1
  6. package/docs/QUICKSTART.md +1 -1
  7. package/framework/CLAUDE.md +24 -30
  8. package/framework/MORPH.md +47 -123
  9. package/framework/agents.json +6 -374
  10. package/framework/commands/morph-apply.md +1 -1
  11. package/framework/commands/morph-archive.md +4 -3
  12. package/framework/commands/morph-hotfix.md +8 -30
  13. package/framework/commands/morph-infra.md +2 -2
  14. package/framework/commands/morph-media.md +10 -9
  15. package/framework/commands/morph-review.md +15 -0
  16. package/framework/commands/morph-status.md +1 -1
  17. package/framework/docs/CLI.md +83 -94
  18. package/framework/evals/review-checklist.md +45 -53
  19. package/framework/hooks/README.md +4 -4
  20. package/framework/hooks/claude-code/core/README.md +1 -1
  21. package/framework/hooks/claude-code/core/denylist.json +4 -4
  22. package/framework/hooks/claude-code/core/gate-guard.js +9 -0
  23. package/framework/hooks/claude-code/core/masked-exit.js +49 -0
  24. package/framework/hooks/claude-code/core/state-sync.js +4 -1
  25. package/framework/hooks/claude-code/core/telemetry-log.js +1 -2
  26. package/framework/hooks/claude-code/core/test-budget.js +10 -70
  27. package/framework/hooks/claude-code/core/validate-completion.js +11 -24
  28. package/framework/hooks/claude-code/core/worktree-context.js +5 -22
  29. package/framework/hooks/claude-code/core/worktree-create.js +6 -45
  30. package/framework/hooks/claude-code/core/worktree-env-inject.js +2 -10
  31. package/framework/hooks/shared/e2e-check.js +0 -0
  32. package/framework/hooks/shared/gate-event.js +2 -2
  33. package/framework/hooks/shared/graph-refresh.js +20 -3
  34. package/framework/hooks/shared/heavy-command.js +32 -1
  35. package/framework/hooks/shared/heavy-config.js +1 -1
  36. package/framework/hooks/shared/next-step.js +63 -24
  37. package/framework/hooks/shared/phase-utils.js +6 -9
  38. package/framework/hooks/shared/report-progress.js +86 -0
  39. package/framework/hooks/shared/state-reader.js +3 -2
  40. package/framework/hooks/statusline.js +332 -0
  41. package/framework/phases.json +32 -10
  42. package/framework/rules/ai-agents-standards.md +9 -1
  43. package/framework/rules/csharp-standards.md +4 -0
  44. package/framework/rules/ef-standards.md +20 -0
  45. package/framework/rules/frontend-standards.md +1 -1
  46. package/framework/rules/implement-report.md +25 -0
  47. package/framework/rules/infrastructure-standards.md +2 -0
  48. package/framework/rules/nextjs-standards.md +7 -0
  49. package/framework/rules/plan-grammar.md +53 -0
  50. package/framework/rules/ui-design.md +27 -0
  51. package/framework/schemas/config.schema.json +11 -18
  52. package/framework/schemas/feature.schema.json +7 -1
  53. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  54. package/framework/skills/core/create-umbraco-lp/references/umbraco-lp-patterns.md +1 -1
  55. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +2 -2
  56. package/framework/skills/core/morph-apply/SKILL.md +38 -166
  57. package/framework/skills/core/morph-apply/references/debugging.md +8 -0
  58. package/framework/skills/core/morph-apply/references/hotfix.md +42 -0
  59. package/framework/skills/core/morph-apply/references/machine.md +29 -0
  60. package/framework/skills/core/{morph-media/SKILL.md → morph-apply/references/media.md} +11 -74
  61. package/framework/skills/core/morph-apply/references/review.md +65 -0
  62. package/framework/skills/core/morph-plan/SKILL.md +42 -108
  63. package/framework/skills/core/morph-plan/references/critique.md +40 -0
  64. package/framework/skills/core/morph-plan/references/interview.md +25 -0
  65. package/framework/skills/core/morph-uiux/SKILL.md +47 -64
  66. package/framework/skills/core/morph-uiux/surfaces.json +32 -0
  67. package/framework/standards/STANDARDS.json +32 -5
  68. package/framework/standards/STANDARDS.meta.json +5 -1
  69. package/framework/standards/ai-agents/agent-session.md +2 -3
  70. package/framework/standards/ai-agents/context-providers.md +2 -2
  71. package/framework/standards/ai-agents/durable-workflows-hitl.md +2 -2
  72. package/framework/standards/ai-agents/providers/model-registry.md +2 -2
  73. package/framework/standards/ai-agents/rag-custom-pgvector.md +6 -6
  74. package/framework/standards/ai-agents/setup.md +1 -1
  75. package/framework/standards/ai-agents/tool-adapters.md +2 -2
  76. package/framework/standards/architecture/stack-baseline.md +47 -0
  77. package/framework/standards/backend/integrations/neon-auth/neon-auth.md +2 -4
  78. package/framework/standards/frontend/design-system/ai-image-generation.md +51 -12
  79. package/framework/standards/frontend/design-system/ai-video-generation.md +50 -4
  80. package/framework/standards/frontend/design-system/art-direction.md +2 -2
  81. package/framework/standards/frontend/scroll-driven/frame-scrub.md +12 -2
  82. package/framework/standards/frontend/scroll-driven/scroll-components.md +76 -4
  83. package/framework/templates/REGISTRY.json +1 -2
  84. package/framework/templates/context/ARCHITECTURE.md +1 -1
  85. package/framework/templates/docs/plan.md +12 -12
  86. package/framework/templates/docs/ui-mockups.md +17 -105
  87. package/package.json +1 -1
  88. package/src/commands/e2e/e2e.js +88 -23
  89. package/src/commands/graph/diff.js +74 -0
  90. package/src/commands/graph/status.js +1 -1
  91. package/src/commands/observability/cost.js +2 -4
  92. package/src/commands/observability/telemetry.js +1 -1
  93. package/src/commands/project/doctor.js +23 -39
  94. package/src/commands/project/fleet.js +5 -6
  95. package/src/commands/project/init.js +63 -36
  96. package/src/commands/project/stack-scan.js +3 -42
  97. package/src/commands/project/stack-set.js +26 -0
  98. package/src/commands/project/status.js +12 -13
  99. package/src/commands/project/update.js +39 -3
  100. package/src/commands/project/worktree.js +3 -14
  101. package/src/commands/state/advance.js +17 -17
  102. package/src/commands/state/approve.js +0 -6
  103. package/src/commands/state/archive.js +27 -1
  104. package/src/commands/state/auto-approval.js +1 -1
  105. package/src/commands/state/create.js +0 -5
  106. package/src/commands/state/gate-check.js +37 -30
  107. package/src/commands/state/gate-decision.js +3 -2
  108. package/src/commands/state/next.js +1 -0
  109. package/src/commands/state/recap.js +67 -0
  110. package/src/commands/state/report-check.js +36 -178
  111. package/src/commands/validation/plan-check.js +16 -11
  112. package/src/commands/validation/validate-feature.js +3 -3
  113. package/src/commands/validation/verify.js +3 -6
  114. package/src/core/paths/output-schema.js +3 -3
  115. package/src/core/state/state-manager.js +2 -4
  116. package/src/dashboard/app/components/CommandPalette.jsx +2 -5
  117. package/src/dashboard/app/components/GatePauses.jsx +116 -0
  118. package/src/dashboard/app/components/SessionMap.jsx +77 -0
  119. package/src/dashboard/app/pages/Activity.jsx +4 -0
  120. package/src/dashboard/app/pages/AgentMap.jsx +1 -2
  121. package/src/dashboard/app/pages/FeatureDetail.jsx +129 -186
  122. package/src/dashboard/app/pages/Features.jsx +7 -7
  123. package/src/dashboard/app/pages/PhasePipeline.jsx +1 -1
  124. package/src/dashboard/app/pages/Portfolio.jsx +40 -5
  125. package/src/dashboard/scanner/activity-scanner.js +29 -0
  126. package/src/dashboard/scanner/agents-scanner.js +11 -14
  127. package/src/dashboard/scanner/features-scanner.js +45 -24
  128. package/src/dashboard/scanner/project-api.js +11 -2
  129. package/src/dashboard/scanner/skills-scanner.js +11 -1
  130. package/src/lib/e2e/applicability.js +3 -1
  131. package/src/lib/git/tree-state.js +1 -16
  132. package/src/lib/graph/diff-tree.js +127 -0
  133. package/src/lib/graph/graphify-runtime.js +12 -5
  134. package/src/lib/installers/memory-installer.js +2 -2
  135. package/src/lib/installers/rules-installer.js +57 -0
  136. package/src/lib/installers/stack-assets.js +17 -0
  137. package/src/lib/installers/stack-sync.js +219 -0
  138. package/src/lib/plan/checks.js +55 -110
  139. package/src/lib/plan/critique.js +28 -0
  140. package/src/lib/plan/media.js +42 -0
  141. package/src/lib/plan/plan-html.js +125 -0
  142. package/src/lib/plan/plan-parser.js +34 -45
  143. package/src/lib/recap/piper.js +36 -0
  144. package/src/lib/recap/recap.js +232 -0
  145. package/src/lib/review-reports.js +28 -4
  146. package/src/lib/risk-detector.js +90 -11
  147. package/src/lib/specs/screen-checklist.js +33 -48
  148. package/src/lib/stack/dotnet-build-ignores.js +8 -9
  149. package/src/lib/telemetry/metrics.js +27 -29
  150. package/src/lib/validators/reuse/reuse-validator.js +1 -9
  151. package/src/lib/validators/verify-runner.js +0 -7
  152. package/src/lib/worktree/fleet-scanner.js +7 -20
  153. package/src/lib/worktree/worktree-audit.js +0 -37
  154. package/src/scripts/setup-infra.js +15 -50
  155. package/src/utils/agents-installer.js +27 -201
  156. package/src/utils/hooks-installer.js +11 -95
  157. package/framework/hooks/claude-code/core/warn-bash-source-write.js +0 -23
  158. package/framework/hooks/claude-code/statusline.py +0 -699
  159. package/framework/hooks/claude-code/statusline.sh +0 -11
  160. package/framework/hooks/shared/agent-reap.js +0 -74
  161. package/framework/hooks/shared/slice-progress.js +0 -110
  162. package/framework/skills/core/morph-hotfix/SKILL.md +0 -133
  163. package/framework/skills/core/morph-machine/SKILL.md +0 -46
  164. package/framework/skills/core/morph-review/SKILL.md +0 -129
  165. package/framework/skills/core/morph-standards/SKILL.md +0 -52
  166. package/framework/standards/PROVENANCE.json +0 -119
  167. package/src/commands/project/worktree-reap.js +0 -34
  168. package/src/commands/state/handoff.js +0 -65
  169. package/src/commands/validation/mutate.js +0 -263
  170. package/src/commands/validation/scout-check.js +0 -28
  171. package/src/lib/generators/handoff-generator.js +0 -172
  172. package/src/lib/mutation/adapters.js +0 -79
  173. package/src/lib/mutation/commands.js +0 -61
  174. package/src/lib/mutation/dotnet-introspection.js +0 -281
  175. package/src/lib/mutation/manifest.js +0 -144
  176. package/src/lib/mutation/plan.js +0 -44
  177. package/src/lib/mutation/report.js +0 -68
  178. package/src/lib/mutation/runner.js +0 -301
  179. package/src/lib/mutation/schemata.js +0 -60
  180. package/src/lib/mutation/snapshot.js +0 -65
  181. package/src/lib/mutation/subset.js +0 -109
  182. package/src/lib/mutation/trx.js +0 -50
  183. package/src/lib/scout/scout-check.js +0 -293
  184. package/src/lib/validators/traceability-check.js +0 -103
  185. package/src/lib/worktree/agent-reaper.js +0 -247
@@ -18,10 +18,11 @@ import { injectMorphImport } from '../utils/claude-md-injector.js';
18
18
  import { saveProjectMorphVersion, getInstalledCLIVersion, getProjectMorphVersion } from '../utils/version-checker.js';
19
19
  import { installClaudeHooks, installGlobalStatusline } from '../utils/claude-settings-manager.js';
20
20
  import { installSkills } from '../utils/skills-installer.js';
21
- import { installAgents, installDomainAgents } from '../utils/agents-installer.js';
21
+ import { installAgents } from '../utils/agents-installer.js';
22
22
  import { resolvePrimaryRoot } from '../commands/project/worktree.js';
23
23
  import { reconcileLegacyTemplatesCopy } from '../lib/installers/templates-reconcile.js';
24
24
  import { realEffects } from '../lib/installers/effects.js';
25
+ import { installRulesForStack } from '../lib/installers/rules-installer.js';
25
26
 
26
27
  const FRAMEWORK_DIR = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'framework');
27
28
 
@@ -41,31 +42,7 @@ async function installRequiredPlugins(log, exec) {
41
42
  }
42
43
  }
43
44
 
44
- /** Also removes orphan rules in rulesDest that are no longer in the install set. */
45
- export function installRulesForStack(rulesSrc, rulesDest, projectTags, fx = realEffects()) {
46
- const sourceFiles = readdirSync(rulesSrc).filter(f => f.endsWith('.md'));
47
- const installed = new Set();
48
-
49
- for (const file of sourceFiles) {
50
- const content = readFileSync(join(rulesSrc, file), 'utf-8');
51
- const stacks = parseStacks(content);
52
- if (shouldInstall(stacks, projectTags)) {
53
- fx.copyFile(join(rulesSrc, file), join(rulesDest, file));
54
- installed.add(file);
55
- }
56
- }
57
-
58
- // Calado em `dryRun`: `cleanFrameworkDirs()` já apagou `.claude/rules/` inteiro antes da execução real.
59
- if (!fx.dryRun) {
60
- for (const file of readdirSync(rulesDest).filter(f => f.endsWith('.md'))) {
61
- if (!installed.has(file)) {
62
- fx.remove(join(rulesDest, file));
63
- }
64
- }
65
- }
66
-
67
- return { installed: installed.size };
68
- }
45
+ export { installRulesForStack };
69
46
 
70
47
  /** Clean-slate: removes only a framework command the stack filtered out, never one the user wrote. */
71
48
  export function installCommandsForStack(commandsSrc, commandsDest, projectTags, fx = realEffects()) {
@@ -118,10 +95,7 @@ async function installStackAwareAssets(targetPath, log) {
118
95
  log('Installing agents...');
119
96
  const agentCounts = await installAgents(targetPath, FRAMEWORK_DIR, { projectTags });
120
97
 
121
- log('Installing domain agents...');
122
- const domainCounts = await installDomainAgents(targetPath, FRAMEWORK_DIR, { projectTags });
123
-
124
- return { agentCounts, domainCounts, projectTags };
98
+ return { agentCounts, projectTags };
125
99
  }
126
100
 
127
101
  /**
@@ -138,9 +112,9 @@ export async function updateClaudeMdAgentsBlock(claudeMdPath, agents = [], optio
138
112
 
139
113
  const list = agents.length > 0
140
114
  ? agents.map(a => `- \`${a.name}\` — ${a.title}${a.domains?.length ? ` (${a.domains.join(', ')})` : ''}`).join('\n')
141
- : '_Nenhuma persona instalada — configure `project.stack` em `.morph/config/config.json` e rode `morph-spec setup-infra --reinstall`._';
115
+ : '_Nenhum agente instalado — rode `morph-spec setup-infra --reinstall`._';
142
116
 
143
- const block = `${START}\n**Personas instaladas neste projeto** (derivado de agents.json na última instalação):\n\n${list}\n${END}`;
117
+ const block = `${START}\n**Agentes do Gate 3** (derivado de agents.json na última instalação):\n\n${list}\n${END}`;
144
118
  const updated = content.replace(
145
119
  new RegExp(`${START}[\\s\\S]*?${END}`),
146
120
  block
@@ -178,16 +152,10 @@ export async function setupInfra(targetPath, {
178
152
 
179
153
  if (reinstall) {
180
154
  log('setup-infra (reinstall): re-installing stack-aware assets...');
181
- const { agentCounts, domainCounts } = await installStackAwareAssets(targetPath, log);
155
+ const { agentCounts } = await installStackAwareAssets(targetPath, log);
182
156
  await updateClaudeMdAgentsBlock(join(targetPath, '.claude', 'CLAUDE.md'), agentCounts?.agents ?? []);
183
157
  log('setup-infra (reinstall): complete.');
184
- return {
185
- agents: {
186
- tier1: agentCounts?.tier1 ?? 0,
187
- tier2: agentCounts?.tier2 ?? 0,
188
- specialists: domainCounts?.specialists ?? 0,
189
- },
190
- };
158
+ return { agents: { installed: agentCounts?.installed ?? 0 } };
191
159
  }
192
160
 
193
161
  await installRequiredPlugins(log, _exec);
@@ -316,7 +284,7 @@ export async function setupInfra(targetPath, {
316
284
  const claudeDest = join(targetPath, '.claude');
317
285
 
318
286
  log('Steps 10-13: Installing stack-aware assets...');
319
- const { agentCounts, domainCounts, projectTags } = await installStackAwareAssets(targetPath, log);
287
+ const { agentCounts, projectTags } = await installStackAwareAssets(targetPath, log);
320
288
 
321
289
  log('Step 14: Installing .claude/CLAUDE.md...');
322
290
  const runtimeClaudeMdDest = join(claudeDest, 'CLAUDE.md');
@@ -374,7 +342,9 @@ export async function setupInfra(targetPath, {
374
342
  log(` ✓ graphify ${installed} já instalado`);
375
343
  } else {
376
344
  const { kind } = detectInstaller();
377
- if (!kind) {
345
+ if (process.env.MORPH_NO_TOOL_INSTALL === '1') {
346
+ log(` ○ graphify não instalado (MORPH_NO_TOOL_INSTALL=1). Depois: uv tool install ${GRAPHIFY_PACKAGE}==${GRAPHIFY_VERSION}`);
347
+ } else if (!kind) {
378
348
  log(` ⚠ Sem uv/pipx/python — grafo indisponível. Depois: uv tool install ${GRAPHIFY_PACKAGE}==${GRAPHIFY_VERSION}`);
379
349
  } else {
380
350
  // Sem o extra [sql] os arquivos .sql são ignorados em silêncio.
@@ -382,7 +352,8 @@ export async function setupInfra(targetPath, {
382
352
  const { cmd, args } = installArgs(kind, { withSql });
383
353
  try {
384
354
  _exec(`${cmd} ${args.join(' ')}`, { stdio: 'ignore' });
385
- log(` ✓ graphify ${GRAPHIFY_VERSION}${withSql ? ' [sql]' : ''} via ${kind}`);
355
+ if (probe() === GRAPHIFY_VERSION) log(` ✓ graphify ${GRAPHIFY_VERSION}${withSql ? ' [sql]' : ''} via ${kind}`);
356
+ else log(` ⚠ ${kind} instalou o graphify, mas o binário não responde (nem no PATH nem no venv da ferramenta) — rode ${cmd} ${args.join(' ')} --force no seu terminal`);
386
357
  } catch {
387
358
  log(` ⚠ Falha ao instalar o graphify via ${kind} — manualmente: ${cmd} ${args.join(' ')}`);
388
359
  }
@@ -414,11 +385,5 @@ export async function setupInfra(targetPath, {
414
385
 
415
386
  log('setup-infra: complete.');
416
387
 
417
- return {
418
- agents: {
419
- tier1: agentCounts?.tier1 ?? 0,
420
- tier2: agentCounts?.tier2 ?? 0,
421
- specialists: domainCounts?.specialists ?? 0,
422
- },
423
- };
388
+ return { agents: { installed: agentCounts?.installed ?? 0 } };
424
389
  }
@@ -4,15 +4,8 @@ import { shouldInstall } from '../lib/stack-filter.js';
4
4
  import { realEffects } from '../lib/installers/effects.js';
5
5
  import { REVIEW_ROLES, VERDICT_TOKEN, REVIEWED_SHA_TOKEN, VERDICTS } from '../lib/review-reports.js';
6
6
 
7
- export const DEFAULT_AGENT_TOOLS = 'Read, Write, Edit, Bash, Glob, Grep';
7
+ export const DEFAULT_AGENT_TOOLS = 'Read, Glob, Grep, Bash';
8
8
 
9
- /** Derived from the effective tool scope (Write or Edit), so a persona made read-only stops being one. */
10
- export function isImplementer(agent) {
11
- const tools = String(agent?.tools ?? DEFAULT_AGENT_TOOLS).split(',').map((t) => t.trim());
12
- return tools.includes('Write') || tools.includes('Edit');
13
- }
14
-
15
- // The value hardcoded before the field existed: an agents.json without `maxTurns` renders byte-for-byte as before.
16
9
  export const DEFAULT_MAX_TURNS = 20;
17
10
 
18
11
  const CHECKPOINT_PERCENT = 70;
@@ -43,84 +36,12 @@ export function resolveMaxTurns(agent) {
43
36
  return agent.maxTurns ?? DEFAULT_MAX_TURNS;
44
37
  }
45
38
 
46
- export const VALID_EFFORTS = ['low', 'medium', 'high', 'xhigh', 'max'];
47
-
48
- // Absent: no `effort:` line, so the sub-agent inherits the session effort.
49
- export function effortProblem(value) {
50
- if (value === undefined) return null;
51
- return VALID_EFFORTS.includes(value) ? null : `esperado um de ${VALID_EFFORTS.join('|')}`;
52
- }
53
-
54
- export function effortError(id, value) {
55
- const problem = effortProblem(value);
56
- if (!problem) return null;
57
- return `agents.json: persona "${id}" tem effort inválido (${JSON.stringify(value)}) — ${problem}. ` +
58
- 'Remova o campo para herdar o effort da sessão.';
59
- }
60
-
61
- const SURFACE_ID_RE = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
62
-
63
- const SURFACE_KEYS = new Set(['when', 'standards']);
64
-
65
- // A standard both universal and in a surface is refused: the persona would keep
66
- // loading it on every surface without anyone noticing.
67
- export function surfacesProblem(surfaces, standards = []) {
68
- if (surfaces === undefined) return null;
69
- if (surfaces === null || typeof surfaces !== 'object' || Array.isArray(surfaces)) {
70
- return 'esperado objeto { "<superfície>": { when, standards } }';
71
- }
72
- const entries = Object.entries(surfaces);
73
- if (entries.length === 0) return 'esperado ao menos uma superfície';
74
-
75
- const universal = new Set(Array.isArray(standards) ? standards : []);
76
- for (const [name, def] of entries) {
77
- if (!SURFACE_ID_RE.test(name)) return `superfície "${name}" — o id é kebab-case (ex.: "admin-app")`;
78
- if (def === null || typeof def !== 'object' || Array.isArray(def)) {
79
- return `superfície "${name}" — esperado { when, standards }`;
80
- }
81
- const unknown = Object.keys(def).filter((k) => !SURFACE_KEYS.has(k));
82
- if (unknown.length > 0) return `superfície "${name}" — campo desconhecido: ${unknown.join(', ')}`;
83
- if (typeof def.when !== 'string' || def.when.trim() === '') {
84
- return `superfície "${name}" — "when" precisa dizer que telas são dessa superfície`;
85
- }
86
- const ids = def.standards;
87
- if (!Array.isArray(ids) || ids.length === 0 || ids.some((s) => typeof s !== 'string' || s.trim() === '')) {
88
- return `superfície "${name}" — "standards" precisa ser uma lista não vazia de ids`;
89
- }
90
- const repeated = ids.filter((s, i) => ids.indexOf(s) !== i);
91
- if (repeated.length > 0) return `superfície "${name}" repete ${[...new Set(repeated)].join(', ')}`;
92
- const overlap = ids.filter((s) => universal.has(s));
93
- if (overlap.length > 0) {
94
- return `superfície "${name}" repete standard da lista universal (${overlap.join(', ')}) — ` +
95
- 'um standard é universal OU de uma superfície';
96
- }
97
- }
98
- return null;
99
- }
100
-
101
- export function surfacesError(id, surfaces, standards) {
102
- const problem = surfacesProblem(surfaces, standards);
103
- if (!problem) return null;
104
- return `agents.json: persona "${id}" tem surfaces inválido — ${problem}. ` +
105
- 'Remova o campo para a persona ler só `standards`.';
106
- }
107
-
108
- /** @returns {string[]} unique ids of `standards` plus every surface's, universal first */
109
39
  export function personaStandardIds(persona) {
110
- const ids = [...(Array.isArray(persona?.standards) ? persona.standards : [])];
111
- const surfaces = persona?.surfaces;
112
- if (surfaces && typeof surfaces === 'object' && !Array.isArray(surfaces)) {
113
- for (const def of Object.values(surfaces)) {
114
- if (Array.isArray(def?.standards)) ids.push(...def.standards);
115
- }
116
- }
117
- return [...new Set(ids)];
40
+ return [...new Set(Array.isArray(persona?.standards) ? persona.standards : [])];
118
41
  }
119
42
 
120
43
  function personaError(agent) {
121
- return maxTurnsError(agent.id, agent.maxTurns)
122
- ?? surfacesError(agent.id, agent.surfaces, agent.standards)
123
- ?? effortError(agent.id, agent.effort);
44
+ return maxTurnsError(agent.id, agent.maxTurns);
124
45
  }
125
46
 
126
47
  // `tools:` is a strict allowlist: without Skill no skill runs, without ToolSearch no deferred MCP tool loads.
@@ -165,6 +86,8 @@ const MANAGED_HEADER =
165
86
  ' Do NOT edit by hand: changes are overwritten on `morph-spec update` / `setup-infra --reinstall`.\n' +
166
87
  ' To change this persona, edit agents.json and regenerate. Drift is flagged by `morph-spec doctor`. -->';
167
88
 
89
+ const MANAGED_HEADER_MARKER = MANAGED_HEADER.split('\n')[0];
90
+
168
91
  export async function installAgents(projectDir, frameworkDir = 'framework', options = {}) {
169
92
  const { projectTags = [], fx = realEffects() } = options;
170
93
 
@@ -213,24 +136,30 @@ export async function installAgents(projectDir, frameworkDir = 'framework', opti
213
136
  // Silent in dryRun: the real update's cleanFrameworkDirs() already emptied the dir.
214
137
  if (!fx.dryRun) {
215
138
  for (const file of readdirSync(targetDir)) {
216
- if (allPossible.has(file) && !installedFilenames.has(file)) {
217
- fx.remove(join(targetDir, file));
218
- }
139
+ if (installedFilenames.has(file)) continue;
140
+ const path = join(targetDir, file);
141
+ if (allPossible.has(file) || isManagedOrphan(file, path)) fx.remove(path);
219
142
  }
220
143
  }
221
144
 
222
145
  return { installed, agents: installedAgents };
223
146
  }
224
147
 
148
+ // Only the managed header proves the file is ours: a hand-written morph-*.md is never deleted.
149
+ function isManagedOrphan(file, path) {
150
+ if (!file.startsWith('morph-') || !file.endsWith('.md')) return false;
151
+ try {
152
+ return readFileSync(path, 'utf-8').includes(MANAGED_HEADER_MARKER);
153
+ } catch {
154
+ return false;
155
+ }
156
+ }
157
+
225
158
  // Deterministic: the doctor drift check compares files byte-for-byte against this output.
226
159
  export function renderAgentMd(agent, knownMcpIds = null) {
227
160
  const description = buildDescription(agent);
228
161
  // One value for the frontmatter and the body: the sub-agent never sees its own frontmatter.
229
162
  const maxTurns = resolveMaxTurns(agent);
230
- const surfaces = surfacesError(agent.id, agent.surfaces, agent.standards);
231
- if (surfaces) throw new Error(surfaces);
232
- const effort = effortError(agent.id, agent.effort);
233
- if (effort) throw new Error(effort);
234
163
  const frontmatter = buildFrontmatter(agent, description, knownMcpIds, maxTurns);
235
164
  const body = buildBody(agent, maxTurns);
236
165
  return `---\n${frontmatter}---\n\n${MANAGED_HEADER}\n\n${body}\n`;
@@ -251,10 +180,8 @@ function buildFrontmatter(agent, description, knownMcpIds, maxTurns) {
251
180
  `description: ${description}`,
252
181
  `model: ${agent.model ?? 'inherit'}`,
253
182
  `tools: ${buildToolsLine(agent, knownMcpIds)}`,
254
- // Position fixed so an agents.json without the field renders byte-for-byte as before.
255
183
  `maxTurns: ${maxTurns}`,
256
184
  ];
257
- if (agent.effort) lines.push(`effort: ${agent.effort}`);
258
185
  if (agent.color) lines.push(`color: ${agent.color}`);
259
186
  lines.push(`memory: project`, '');
260
187
  return lines.join('\n');
@@ -267,58 +194,9 @@ function buildDescription(agent) {
267
194
  return desc.length > 200 ? desc.slice(0, 197) + '...' : desc;
268
195
  }
269
196
 
270
- const NEON_CAUTION = 'NEON: o MCP `neon` inspeciona schema e ensaia migração numa branch descartável ' +
271
- '(prepare_database_migration → complete_database_migration); ele também APAGA branch e projeto — ' +
272
- 'nunca chame tool destrutiva sem confirmação humana explícita.';
273
-
274
197
  const STANDARDS_HINT = 'Digest de cada um: `morph-spec standards --find <id>`; só a seção que importa: ' +
275
198
  '`morph-spec standards --show <id>#<âncora>` (`--show <id>` lista as âncoras).';
276
199
 
277
- export const DISPATCH_CONTRACT = [
278
- '## Contrato de dispatch',
279
- '',
280
- 'O dispatch traz só o que muda por fatia: id e título, `cobre` com o texto de cada cenário, `arquivos`, `standards`, ' +
281
- 'o caminho do relatório e, se houver, o worktree. O resto é este contrato.',
282
- '',
283
- '1. RELATÓRIO: GRAVE o arquivo de relatório que o dispatch nomeia (`.morph/features/{feature}/3-implement/reports/F{n}.md`) ' +
284
- 'ANTES de encerrar — é o retorno autoritativo, não a sua mensagem final.',
285
- '2. ESTADO: NÃO toque em `plan.md`, `checks.json`, `feature.json` nem `state.json`: o estado é do orquestrador.',
286
- '3. TESTE: a prova é do orquestrador (`morph-spec verify {feature}`, depois do seu retorno), não sua. Escreva os testes pelo orçamento: ' +
287
- 'um por Cn `prova: teste`, na camada mais baixa que o prova (a de cima só para o que só existe lá), variantes em tabela (`[Theory]`/`it.each`); `leitura` sem teste. NÃO rode teste a cada marco nem faça ciclo red→green: ' +
288
- 'escreva código e testes, compile, e rode SÓ o filtro dos seus testes UMA vez, em primeiro plano, antes do relatório ' +
289
- '(`vitest related <arquivos da fatia> --run`; `dotnet test --filter "FullyQualifiedName~<Classe>" --no-build` após o build); ' +
290
- 'se falhar, corrija e rode só os que falharam. Teto: 3 rodadas (`verify --filter` conta, exit 3 não); ' +
291
- 'o hook nega a 4ª e o `verify` sem `--filter`: grave `PARCIAL` com o que ainda falha e devolva. ' +
292
- 'Grave a saída em arquivo (`… > .morph/logs/test-last.log 2>&1`, sem nada depois) e leia o arquivo — um pipe troca o exit do teste pelo do último comando. ' +
293
- 'Em batch paralelo SEM isolamento, só compile. Com `isolation: "worktree"`, o mesmo vale no seu worktree, e o último ato é o commit na branch dele.',
294
- '4. SOBREVIVÊNCIA: o limite de turnos corta sem aviso: commite `chore(F{n}): wip …` a cada marco (um arquivo ou uma fatia escrita), não só ao fim — ' +
295
- 'e só os seus arquivos (`git add <paths>`, nunca diretório). Sem commit, a retomada recomeça do zero.',
296
- '5. PROCESSOS: encerre dev server pelo PID que você mesmo subiu. Build e suíte pesados rodam por `morph-spec heavy run -- <comando>`, em primeiro plano; ' +
297
- 'exit 3 é fila cheia: não repita nem espere em laço, diga no relatório o que não rodou.',
298
- '6. REUSO: antes de criar tipo, componente, hook ou serviço NOVO, verifique se já existe: `morph-spec graph explain <nome>` e ' +
299
- '`graph affected <conceito> --uses-only`; sem grafo (`no-graph`), caia para `grep -rn "<nome>"` e siga — a ausência de grafo nunca bloqueia. ' +
300
- 'Se criar mesmo assim, diga por que em uma linha no relatório.',
301
- '7. MUTAÇÃO: nunca altere, reverta, mova ou apague código de produção para ver um teste falhar; a prova de que o teste morde é do verificador.',
302
- '8. ESCRITA: código e teste do projeto só por Write/Edit, nunca pelo Bash (heredoc, `sed -i`, script que grava): os guards só enxergam Write/Edit.',
303
- '9. BLOQUEIO: decisão que não é sua (requisito contra o plano ou um ADR, escolha deixada ao humano) → `BLOQUEADO` na 1ª linha, ' +
304
- 'com a pergunta e as opções. Dificuldade técnica não é bloqueio.',
305
- '',
306
- 'Formato do relatório (o `report-check` lê estas marcas):',
307
- '',
308
- '```',
309
- '# F1 — <título>',
310
- 'O que foi feito, em 2-3 frases: o comportamento entregue e onde ele mora, sem repetir a lista de arquivos.',
311
- '**Arquivos:** `src/a.cs`, `src/a.test.cs`',
312
- '**Checks:**',
313
- '- C1 — provado: `a.test.cs` › nome do teste (ou `arquivo:linha`)',
314
- '- C2 — não provado: por quê',
315
- '```',
316
- '',
317
- '`**Arquivos:**` lista TODO arquivo criado ou modificado, inclusive artefato auxiliar fora do plano (factory, seed, config gerada): ' +
318
- 'arquivo em produção sem dono é falha de protocolo que o Gate 3 aponta. Todo cenário que a fatia `cobre` aparece em `**Checks:**`. ' +
319
- 'Problemas e como foram resolvidos vão em prosa depois.',
320
- ];
321
-
322
200
  export const REVIEW_REPORT_CONTRACT = [
323
201
  '## Relatório do Gate 3',
324
202
  '',
@@ -346,57 +224,20 @@ function buildBody(agent, maxTurns) {
346
224
  '',
347
225
  ];
348
226
 
349
- // The shape without effort or surfaces renders byte-for-byte as the old fixture.
350
- const legacyShape = !agent.effort && !agent.surfaces && isImplementer(agent);
351
-
352
- if (legacyShape && agent.domains?.length) {
353
- lines.push('## Domains', ...agent.domains.map(d => `- ${d}`), '');
354
- }
355
-
356
- if (!legacyShape && agent.mcp?.includes('neon')) {
357
- lines.push(NEON_CAUTION, '');
358
- }
359
-
360
227
  if (agent.standards?.length) {
361
- lines.push('## Standards to consult on demand');
362
- if (!legacyShape) lines.push(STANDARDS_HINT);
228
+ lines.push('## Standards to consult on demand', STANDARDS_HINT);
363
229
  for (const s of agent.standards) lines.push(`- ${s}`);
364
230
  lines.push('');
365
231
  }
366
232
 
367
- if (agent.surfaces) {
368
- const read = agent.standards?.length ? 'Read the list above plus only' : 'Read only';
369
- lines.push(
370
- '## Standards by surface',
371
- '',
372
- 'A screen is ONE surface — the mockup checklist names it (`Superfície`). ' +
373
- `${read} the list of that screen's surface; never another surface's standards for that screen.`,
374
- '',
375
- );
376
- for (const [name, def] of Object.entries(agent.surfaces)) {
377
- lines.push(`### ${name}`, def.when, ...def.standards.map((s) => `- ${s}`), '');
378
- }
379
- }
380
-
381
- if (agent.effort && isImplementer(agent)) {
382
- lines.push(...DISPATCH_CONTRACT, '');
383
- } else if (isImplementer(agent)) {
384
- lines.push(
385
- '## How you operate',
386
- '',
387
- 'You are a domain persona dispatched per task. Read the spec/tasks/standards passed to you, do the work, and report each doneCriteria with its evidence. Refer to MORPH.md section 6 for the dispatch contract.',
388
- '',
389
- );
390
- } else {
391
- lines.push(
392
- '## Como você opera',
393
- '',
394
- 'Você é read-only: nunca escreva, edite nem reverta código, testes, o plano ou `feature.json` (nada de `git checkout --`). ' +
395
- 'O entregável é o arquivo de relatório que o dispatch nomeia; sem arquivo nomeado, é a sua mensagem final (no scout, o mapa).',
396
- '',
397
- );
398
- if (Object.hasOwn(REVIEW_ROLES, agent.id)) lines.push(...REVIEW_REPORT_CONTRACT, '');
399
- }
233
+ lines.push(
234
+ '## Como você opera',
235
+ '',
236
+ 'Você é read-only: nunca escreva, edite nem reverta código, testes, o plano ou `feature.json` (nada de `git checkout --`). ' +
237
+ 'O entregável é o arquivo de relatório que o dispatch nomeia; sem arquivo nomeado, é a sua mensagem final.',
238
+ '',
239
+ );
240
+ if (Object.hasOwn(REVIEW_ROLES, agent.id)) lines.push(...REVIEW_REPORT_CONTRACT, '');
400
241
 
401
242
  lines.push(
402
243
  // `PARCIAL` on line 1 is what report-check reads; "no verdict" keeps a partial
@@ -409,17 +250,6 @@ function buildBody(agent, maxTurns) {
409
250
  '`PARCIAL` on line 1, done, remaining, files, no verdict (final report only) — then continue or stop, your call; the final report replaces it.',
410
251
  );
411
252
 
412
- // The only text every implementer dispatch is guaranteed to carry.
413
- if (agent.effort && isImplementer(agent)) {
414
- lines.push(
415
- '',
416
- '## Working fast',
417
- '',
418
- 'Independent tool calls go in the SAME response: read every listed file at once, fire the searches together. ' +
419
- 'Check a library API in the `context7` MCP before guessing (load a deferred `mcp__` tool with ToolSearch).',
420
- );
421
- }
422
-
423
253
  return lines.join('\n');
424
254
  }
425
255
 
@@ -428,10 +258,6 @@ function checkpointTurn(maxTurns) {
428
258
  return Math.max(1, Math.floor((maxTurns * CHECKPOINT_PERCENT) / 100));
429
259
  }
430
260
 
431
- export async function installDomainAgents(projectDir, frameworkDir, options) {
432
- return { specialists: 0 };
433
- }
434
-
435
261
  export function checkMissingAgents(projectDir, frameworkDir, projectTags = []) {
436
262
  const targetDir = join(projectDir, '.claude', 'agents');
437
263
  const installedFiles = new Set();
@@ -1,11 +1,10 @@
1
1
  import { join, dirname } from 'path';
2
2
  import { readFile } from 'fs/promises';
3
- import { existsSync, chmodSync, readdirSync } from 'fs';
3
+ import { existsSync, readdirSync } from 'fs';
4
4
  import { homedir } from 'os';
5
- import { execSync } from 'child_process';
6
5
  import { realEffects } from '../lib/installers/effects.js';
7
6
 
8
- const HOOKS_VERSION = '3.14.0';
7
+ const HOOKS_VERSION = '3.17.0';
9
8
 
10
9
  const OLD_DISPATCH_COMMAND = 'node framework/hooks/agent-teams/dispatch.js';
11
10
 
@@ -25,9 +24,6 @@ const MORPH_ALLOW = [
25
24
  'Edit(.morph/context/**)',
26
25
  'Bash(mkdir:.morph/**)',
27
26
  'Bash(ls:.morph/**)',
28
- // An auto-mode deny here pauses the review mutation round.
29
- 'Bash(morph-spec mutate:*)',
30
- 'Bash(npx morph-spec mutate:*)',
31
27
  ];
32
28
 
33
29
  const MORPH_PERMISSIONS = MORPH_DENY;
@@ -64,7 +60,7 @@ const MORPH_HOOKS = [
64
60
  hooks: [{
65
61
  type: 'command',
66
62
  command: 'node framework/hooks/claude-code/core/chain.js',
67
- args: ['pre-bash-denylist', 'test-budget', 'conventional-commit', 'block-destructive-worktree-removal', 'block-raw-worktree-add', 'warn-bash-source-write', 'heavy-gate', 'worktree-env-inject']
63
+ args: ['pre-bash-denylist', 'test-budget', 'conventional-commit', 'block-destructive-worktree-removal', 'block-raw-worktree-add', 'heavy-gate', 'worktree-env-inject']
68
64
  }]
69
65
  },
70
66
 
@@ -117,7 +113,7 @@ const MORPH_HOOKS = [
117
113
  hooks: [{
118
114
  type: 'command',
119
115
  command: 'node framework/hooks/claude-code/core/chain.js',
120
- args: ['telemetry-log', 'loop-detect', 'graph-refresh-kick', 'heavy-release', 'test-budget'],
116
+ args: ['telemetry-log', 'loop-detect', 'graph-refresh-kick', 'heavy-release', 'masked-exit'],
121
117
  timeout: 10
122
118
  }]
123
119
  },
@@ -129,7 +125,7 @@ const MORPH_HOOKS = [
129
125
  hooks: [{
130
126
  type: 'command',
131
127
  command: 'node framework/hooks/claude-code/core/chain.js',
132
- args: ['telemetry-log', 'heavy-release', 'test-budget'],
128
+ args: ['telemetry-log', 'heavy-release'],
133
129
  timeout: 5,
134
130
  async: true
135
131
  }]
@@ -254,6 +250,8 @@ export async function installClaudeHooks(targetPath, { fx = realEffects() } = {}
254
250
  const STALE_ALLOW = [
255
251
  'Write(.morph/features/**)',
256
252
  'Write(.morph/context/**)',
253
+ 'Bash(morph-spec mutate:*)',
254
+ 'Bash(npx morph-spec mutate:*)',
257
255
  ];
258
256
  settings.permissions.allow = settings.permissions.allow.filter(
259
257
  p => !MORPH_ALLOW.includes(p) && !STALE_ALLOW.includes(p)
@@ -292,78 +290,6 @@ export async function installClaudeHooks(targetPath, { fx = realEffects() } = {}
292
290
  return { installed, updated: !alreadyCurrent };
293
291
  }
294
292
 
295
- function findPython() {
296
- if (process.platform !== 'win32') {
297
- for (const cmd of ['python3', 'python']) {
298
- try {
299
- const ver = execSync(`${cmd} --version`, { encoding: 'utf-8', timeout: 3000, stdio: ['pipe', 'pipe', 'pipe'] });
300
- if ((ver + '').includes('Python 3')) return cmd;
301
- } catch { /* not on PATH */ }
302
- }
303
- return null;
304
- }
305
- for (const cmd of ['python3', 'py', 'python']) {
306
- try {
307
- const ver = execSync(`${cmd} --version`, { encoding: 'utf-8', timeout: 3000, stdio: ['pipe', 'pipe', 'pipe'] });
308
- if ((ver + '').includes('Python 3')) return cmd;
309
- } catch { /* not on PATH */ }
310
- }
311
- const base = join(homedir(), 'AppData', 'Local', 'Programs', 'Python');
312
- if (existsSync(base)) {
313
- let dirs;
314
- try { dirs = readdirSync(base); } catch { dirs = []; }
315
- const py3 = dirs
316
- .filter(d => /^Python3\d*/i.test(d))
317
- .sort()
318
- .reverse();
319
- for (const dir of py3) {
320
- const exe = join(base, dir, 'python.exe');
321
- if (existsSync(exe)) return exe;
322
- }
323
- }
324
- return null;
325
- }
326
-
327
- function tryInstallPython() {
328
- const log = (msg) => process.stdout.write(` ${msg}\n`);
329
- log('⚠ Python 3 not found — attempting automatic installation...');
330
-
331
- try {
332
- if (process.platform === 'win32') {
333
- execSync('winget install -e --id Python.Python.3.12 --accept-source-agreements --accept-package-agreements', {
334
- stdio: 'inherit',
335
- timeout: 120_000,
336
- });
337
- } else if (process.platform === 'darwin') {
338
- execSync('brew install python3', { stdio: 'inherit', timeout: 120_000 });
339
- } else {
340
- try {
341
- execSync('sudo apt-get update -qq && sudo apt-get install -y python3', { stdio: 'inherit', timeout: 120_000 });
342
- } catch {
343
- execSync('sudo dnf install -y python3', { stdio: 'inherit', timeout: 120_000 });
344
- }
345
- }
346
- log('✓ Python 3 installed successfully.');
347
- return true;
348
- } catch (err) {
349
- log(`✗ Automatic Python installation failed: ${err.message}`);
350
- log(' Please install Python 3 manually: https://www.python.org/downloads/');
351
- return false;
352
- }
353
- }
354
-
355
- function resolvePythonForStatusline() {
356
- let python = findPython();
357
- if (python) return python;
358
-
359
- if (tryInstallPython()) {
360
- python = findPython();
361
- if (python) return python;
362
- }
363
-
364
- return 'python3';
365
- }
366
-
367
293
  /**
368
294
  * Writes OUTSIDE the project (`~/.claude/`), so `--dry-run` must name these effects separately.
369
295
  * @returns {Promise<{ installed: boolean, globalDir: string }>}
@@ -372,23 +298,13 @@ export async function installGlobalStatusline(hooksSourceDir, globalClaudeDirOve
372
298
  const globalDir = globalClaudeDirOverride || join(homedir(), '.claude');
373
299
  fx.ensureDir(globalDir);
374
300
 
375
- const scripts = ['statusline.sh', 'statusline.py'];
376
- for (const fname of scripts) {
377
- const src = join(hooksSourceDir, fname);
378
- if (existsSync(src)) {
379
- fx.copyFile(src, join(globalDir, fname));
380
- if (process.platform !== 'win32' && !fx.dryRun) {
381
- try { chmodSync(join(globalDir, fname), 0o755); } catch { /* non-critical */ }
382
- }
383
- }
301
+ fx.copyFile(join(hooksSourceDir, '..', 'statusline.js'), join(globalDir, 'statusline.js'));
302
+ for (const legacy of ['statusline.py', 'statusline.sh']) {
303
+ if (existsSync(join(globalDir, legacy))) fx.remove(join(globalDir, legacy));
384
304
  }
385
305
 
386
306
  const settingsPath = join(globalDir, 'settings.json');
387
-
388
- // Claude Code on Windows cannot execute .sh directly.
389
- const statuslineCommand = process.platform === 'win32'
390
- ? `"${resolvePythonForStatusline()}" "${join(globalDir, 'statusline.py').replace(/\\/g, '/')}"`
391
- : join(globalDir, 'statusline.sh').replace(/\\/g, '/');
307
+ const statuslineCommand = `node "${join(globalDir, 'statusline.js').replace(/\\/g, '/')}"`;
392
308
 
393
309
  // Without refreshInterval the status line only re-renders on events, freezing
394
310
  // the rate-limit countdown through a long turn. Shallow merge: only `statusLine` is ours.
@@ -1,23 +0,0 @@
1
- #!/usr/bin/env node
2
- // Warn only: a source file written by the shell skips every Write/Edit guard. The command still runs.
3
- import { contextResult, runStandalone } from '../../shared/hook-response.js';
4
- import { stateExists } from '../../shared/state-reader.js';
5
- import { writeTargets } from '../../shared/command-tokens.js';
6
- import { isProjectSource } from '../../shared/source-files.js';
7
-
8
- export async function run(payload) {
9
- try {
10
- if (!stateExists()) return null;
11
- const sources = writeTargets(payload?.tool_input?.command || '').filter(isProjectSource);
12
- if (!sources.length) return null;
13
- return contextResult(
14
- `⚠️ Código do projeto escrito pelo Bash (${sources.join(', ')}): os guards de escrita (fase, arquivo protegido, comment-guard) ` +
15
- 'só enxergam Write/Edit. Reescreva esse arquivo por Write/Edit.',
16
- 'PreToolUse',
17
- );
18
- } catch {
19
- return null;
20
- }
21
- }
22
-
23
- await runStandalone(run);