@polymorphism-tech/morph-spec 9.7.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 (211) hide show
  1. package/CLAUDE.md +11 -11
  2. package/README.md +4 -4
  3. package/bin/morph-spec.js +41 -44
  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 +100 -102
  18. package/framework/evals/review-checklist.md +46 -54
  19. package/framework/hooks/README.md +4 -4
  20. package/framework/hooks/claude-code/__pycache__/statusline.cpython-313.pyc +0 -0
  21. package/framework/hooks/claude-code/core/README.md +1 -1
  22. package/framework/hooks/claude-code/core/denylist.json +4 -4
  23. package/framework/hooks/claude-code/core/gate-guard.js +9 -0
  24. package/framework/hooks/claude-code/core/masked-exit.js +49 -0
  25. package/framework/hooks/claude-code/core/state-sync.js +4 -1
  26. package/framework/hooks/claude-code/core/telemetry-log.js +1 -2
  27. package/framework/hooks/claude-code/core/test-budget.js +10 -70
  28. package/framework/hooks/claude-code/core/validate-completion.js +11 -24
  29. package/framework/hooks/claude-code/core/worktree-context.js +5 -22
  30. package/framework/hooks/claude-code/core/worktree-create.js +6 -45
  31. package/framework/hooks/claude-code/core/worktree-env-inject.js +2 -10
  32. package/framework/hooks/shared/e2e-check.js +0 -0
  33. package/framework/hooks/shared/gate-event.js +2 -2
  34. package/framework/hooks/shared/graph-refresh.js +20 -3
  35. package/framework/hooks/shared/heavy-command.js +32 -1
  36. package/framework/hooks/shared/heavy-config.js +1 -1
  37. package/framework/hooks/shared/heavy-slots.js +41 -4
  38. package/framework/hooks/shared/next-step.js +63 -24
  39. package/framework/hooks/shared/phase-utils.js +6 -9
  40. package/framework/hooks/shared/report-progress.js +86 -0
  41. package/framework/hooks/shared/state-reader.js +3 -2
  42. package/framework/hooks/statusline.js +332 -0
  43. package/framework/phases.json +32 -10
  44. package/framework/rules/ai-agents-standards.md +9 -1
  45. package/framework/rules/csharp-standards.md +4 -0
  46. package/framework/rules/ef-standards.md +20 -0
  47. package/framework/rules/frontend-standards.md +1 -1
  48. package/framework/rules/implement-report.md +25 -0
  49. package/framework/rules/infrastructure-standards.md +2 -0
  50. package/framework/rules/nextjs-standards.md +7 -0
  51. package/framework/rules/plan-grammar.md +53 -0
  52. package/framework/rules/ui-design.md +27 -0
  53. package/framework/schemas/config.schema.json +12 -19
  54. package/framework/schemas/feature.schema.json +7 -1
  55. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  56. package/framework/skills/core/create-umbraco-lp/references/umbraco-lp-patterns.md +1 -1
  57. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +2 -2
  58. package/framework/skills/core/morph-apply/SKILL.md +38 -166
  59. package/framework/skills/core/morph-apply/references/debugging.md +8 -0
  60. package/framework/skills/core/morph-apply/references/hotfix.md +42 -0
  61. package/framework/skills/core/morph-apply/references/machine.md +29 -0
  62. package/framework/skills/core/{morph-media/SKILL.md → morph-apply/references/media.md} +11 -74
  63. package/framework/skills/core/morph-apply/references/review.md +65 -0
  64. package/framework/skills/core/morph-plan/SKILL.md +42 -108
  65. package/framework/skills/core/morph-plan/references/critique.md +40 -0
  66. package/framework/skills/core/morph-plan/references/interview.md +25 -0
  67. package/framework/skills/core/morph-uiux/SKILL.md +47 -64
  68. package/framework/skills/core/morph-uiux/surfaces.json +32 -0
  69. package/framework/standards/STANDARDS.json +32 -5
  70. package/framework/standards/STANDARDS.meta.json +5 -1
  71. package/framework/standards/ai-agents/agent-session.md +2 -3
  72. package/framework/standards/ai-agents/context-providers.md +2 -2
  73. package/framework/standards/ai-agents/durable-workflows-hitl.md +2 -2
  74. package/framework/standards/ai-agents/providers/model-registry.md +2 -2
  75. package/framework/standards/ai-agents/rag-custom-pgvector.md +6 -6
  76. package/framework/standards/ai-agents/setup.md +1 -1
  77. package/framework/standards/ai-agents/tool-adapters.md +2 -2
  78. package/framework/standards/architecture/stack-baseline.md +47 -0
  79. package/framework/standards/backend/integrations/neon-auth/neon-auth.md +2 -4
  80. package/framework/standards/frontend/design-system/ai-image-generation.md +51 -12
  81. package/framework/standards/frontend/design-system/ai-video-generation.md +50 -4
  82. package/framework/standards/frontend/design-system/art-direction.md +2 -2
  83. package/framework/standards/frontend/scroll-driven/frame-scrub.md +12 -2
  84. package/framework/standards/frontend/scroll-driven/scroll-components.md +76 -4
  85. package/framework/templates/.idea/morph-templates.xml +92 -0
  86. package/framework/templates/.vscode/morph-templates.code-snippets +186 -0
  87. package/framework/templates/IDE-SNIPPETS.md +262 -262
  88. package/framework/templates/REGISTRY.json +1 -2
  89. package/framework/templates/code/dotnet/abstractions/api-endpoint.cs +7 -7
  90. package/framework/templates/code/dotnet/feature/request.cs.hbs +4 -4
  91. package/framework/templates/code/dotnet/feature/response.cs.hbs +4 -4
  92. package/framework/templates/code/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -91
  93. package/framework/templates/context/ARCHITECTURE.md +1 -1
  94. package/framework/templates/docs/plan.md +12 -12
  95. package/framework/templates/docs/ui-mockups.md +17 -105
  96. package/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -26
  97. package/framework/templates/frontend/nextjs/tsconfig.json.hbs +26 -26
  98. package/framework/templates/infrastructure/docker/Dockerfile.template +58 -58
  99. package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
  100. package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
  101. package/framework/templates/infrastructure/github/actions/docker-build-push/action.yml.hbs +45 -45
  102. package/framework/templates/infrastructure/github/actions/health-check/action.yml.hbs +27 -27
  103. package/framework/templates/infrastructure/github/workflows/docker-build-push.yml.hbs +59 -59
  104. package/framework/templates/infrastructure/github/workflows/dotnet-build.yml.hbs +39 -39
  105. package/framework/templates/state.template.json +17 -17
  106. package/framework/templates/ui/design-system.css +226 -226
  107. package/package.json +1 -1
  108. package/src/commands/e2e/e2e.js +88 -23
  109. package/src/commands/graph/diff.js +74 -0
  110. package/src/commands/graph/status.js +1 -1
  111. package/src/commands/observability/cost.js +2 -4
  112. package/src/commands/observability/telemetry.js +1 -1
  113. package/src/commands/project/doctor.js +23 -39
  114. package/src/commands/project/fleet.js +5 -6
  115. package/src/commands/project/init.js +63 -36
  116. package/src/commands/project/stack-scan.js +3 -42
  117. package/src/commands/project/stack-set.js +26 -0
  118. package/src/commands/project/status.js +12 -13
  119. package/src/commands/project/update.js +39 -3
  120. package/src/commands/project/worktree.js +27 -16
  121. package/src/commands/state/advance.js +17 -17
  122. package/src/commands/state/approve.js +0 -6
  123. package/src/commands/state/archive.js +27 -1
  124. package/src/commands/state/auto-approval.js +1 -1
  125. package/src/commands/state/create.js +0 -5
  126. package/src/commands/state/finish.js +43 -1
  127. package/src/commands/state/gate-check.js +37 -30
  128. package/src/commands/state/gate-decision.js +3 -2
  129. package/src/commands/state/next.js +1 -0
  130. package/src/commands/state/recap.js +67 -0
  131. package/src/commands/state/report-check.js +39 -146
  132. package/src/commands/validation/plan-check.js +16 -11
  133. package/src/commands/validation/validate-feature.js +3 -3
  134. package/src/commands/validation/verify.js +6 -6
  135. package/src/core/paths/output-schema.js +3 -3
  136. package/src/core/state/state-manager.js +2 -4
  137. package/src/dashboard/app/components/CommandPalette.jsx +2 -5
  138. package/src/dashboard/app/components/GatePauses.jsx +116 -0
  139. package/src/dashboard/app/components/SessionMap.jsx +77 -0
  140. package/src/dashboard/app/main.jsx +13 -13
  141. package/src/dashboard/app/pages/Activity.jsx +4 -0
  142. package/src/dashboard/app/pages/AgentMap.jsx +1 -2
  143. package/src/dashboard/app/pages/FeatureDetail.jsx +129 -186
  144. package/src/dashboard/app/pages/Features.jsx +7 -7
  145. package/src/dashboard/app/pages/PhasePipeline.jsx +1 -1
  146. package/src/dashboard/app/pages/Portfolio.jsx +40 -5
  147. package/src/dashboard/scanner/activity-scanner.js +29 -0
  148. package/src/dashboard/scanner/agents-scanner.js +11 -14
  149. package/src/dashboard/scanner/features-scanner.js +45 -24
  150. package/src/dashboard/scanner/project-api.js +11 -2
  151. package/src/dashboard/scanner/skills-scanner.js +11 -1
  152. package/src/dashboard/vite.config.js +24 -24
  153. package/src/lib/e2e/applicability.js +3 -1
  154. package/src/lib/git/tree-state.js +1 -16
  155. package/src/lib/graph/diff-tree.js +127 -0
  156. package/src/lib/graph/graphify-runtime.js +12 -5
  157. package/src/lib/heavy/gate.js +22 -1
  158. package/src/lib/installers/memory-installer.js +2 -2
  159. package/src/lib/installers/rules-installer.js +57 -0
  160. package/src/lib/installers/stack-assets.js +17 -0
  161. package/src/lib/installers/stack-sync.js +219 -0
  162. package/src/lib/plan/checks.js +55 -110
  163. package/src/lib/plan/critique.js +28 -0
  164. package/src/lib/plan/media.js +42 -0
  165. package/src/lib/plan/plan-html.js +125 -0
  166. package/src/lib/plan/plan-parser.js +34 -45
  167. package/src/lib/recap/piper.js +36 -0
  168. package/src/lib/recap/recap.js +232 -0
  169. package/src/lib/review-reports.js +33 -7
  170. package/src/lib/risk-detector.js +90 -11
  171. package/src/lib/specs/screen-checklist.js +33 -48
  172. package/src/lib/stack/dotnet-build-ignores.js +8 -9
  173. package/src/lib/tasks/test-nodes.js +24 -6
  174. package/src/lib/tasks/test-runner.js +40 -1
  175. package/src/lib/telemetry/metrics.js +27 -29
  176. package/src/lib/validators/reuse/reuse-validator.js +1 -9
  177. package/src/lib/validators/verify-runner.js +50 -19
  178. package/src/lib/worktree/fleet-scanner.js +7 -20
  179. package/src/lib/worktree/worktree-audit.js +0 -37
  180. package/src/scripts/setup-infra.js +15 -50
  181. package/src/utils/agents-installer.js +27 -201
  182. package/src/utils/hooks-installer.js +11 -95
  183. package/framework/hooks/claude-code/core/warn-bash-source-write.js +0 -23
  184. package/framework/hooks/claude-code/statusline.py +0 -699
  185. package/framework/hooks/claude-code/statusline.sh +0 -11
  186. package/framework/hooks/shared/agent-reap.js +0 -74
  187. package/framework/hooks/shared/slice-progress.js +0 -110
  188. package/framework/skills/core/morph-hotfix/SKILL.md +0 -133
  189. package/framework/skills/core/morph-machine/SKILL.md +0 -46
  190. package/framework/skills/core/morph-review/SKILL.md +0 -129
  191. package/framework/skills/core/morph-standards/SKILL.md +0 -52
  192. package/framework/standards/PROVENANCE.json +0 -119
  193. package/src/commands/project/worktree-reap.js +0 -34
  194. package/src/commands/state/handoff.js +0 -65
  195. package/src/commands/validation/mutate.js +0 -263
  196. package/src/commands/validation/scout-check.js +0 -28
  197. package/src/lib/generators/handoff-generator.js +0 -172
  198. package/src/lib/mutation/adapters.js +0 -79
  199. package/src/lib/mutation/commands.js +0 -61
  200. package/src/lib/mutation/dotnet-introspection.js +0 -281
  201. package/src/lib/mutation/manifest.js +0 -144
  202. package/src/lib/mutation/plan.js +0 -44
  203. package/src/lib/mutation/report.js +0 -68
  204. package/src/lib/mutation/runner.js +0 -301
  205. package/src/lib/mutation/schemata.js +0 -60
  206. package/src/lib/mutation/snapshot.js +0 -65
  207. package/src/lib/mutation/subset.js +0 -109
  208. package/src/lib/mutation/trx.js +0 -50
  209. package/src/lib/scout/scout-check.js +0 -293
  210. package/src/lib/validators/traceability-check.js +0 -103
  211. package/src/lib/worktree/agent-reaper.js +0 -247
@@ -18,12 +18,12 @@ import {
18
18
  import { listClaims } from '../../../framework/hooks/shared/claim-registry.js';
19
19
  import { buildPrIndex } from '../git/pr-index.js';
20
20
  import { pendingPrFeatures } from '../git/pending-prs.js';
21
- import { sliceProgress, sliceProgressFrom } from '../../../framework/hooks/shared/slice-progress.js';
21
+ import { implementProgress, implementProgressFrom } from '../../../framework/hooks/shared/report-progress.js';
22
22
 
23
23
  const counts = (progress) => (progress ? { done: progress.done, total: progress.total } : null);
24
24
 
25
- // A root-resident feature's reports live on `morph/{f}`; the root tree only has the pre-work copy.
26
- function sliceCounts(featureDir, { root, feature, featureBranch } = {}) {
25
+ // A root-resident feature's report lives on `morph/{f}`; the root tree only has the pre-work copy.
26
+ function scenarioCounts(featureDir, { root, feature, featureBranch } = {}) {
27
27
  if (featureBranch && root && feature) {
28
28
  const show = (rel) => {
29
29
  try {
@@ -32,21 +32,10 @@ function sliceCounts(featureDir, { root, feature, featureBranch } = {}) {
32
32
  return null;
33
33
  }
34
34
  };
35
- const onBranch = sliceProgressFrom(show('2-plan/checks.json'), (id) => show(`3-implement/reports/${id}.md`));
35
+ const onBranch = implementProgressFrom(show('2-plan/checks.json'), show('3-implement/report.md'), (id) => show(`3-implement/reports/${id}.md`));
36
36
  if (onBranch) return counts(onBranch);
37
37
  }
38
- return counts(sliceProgress(featureDir));
39
- }
40
-
41
- function handoffInfo(featureDir) {
42
- try {
43
- const path = join(featureDir, 'handoff.md');
44
- if (!existsSync(path)) return { exists: false, generatedAt: null };
45
- const generatedAt = readFileSync(path, 'utf8').match(/gerado em (\d{4}-\d{2}-\d{2}T[\d:.]+Z)/)?.[1] || null;
46
- return { exists: true, generatedAt };
47
- } catch {
48
- return { exists: false, generatedAt: null };
49
- }
38
+ return counts(implementProgress(featureDir));
50
39
  }
51
40
 
52
41
  function lastCommitRelative(root, branch) {
@@ -132,11 +121,10 @@ function buildEntry(feature, { location, path, branch, root, allocations, blockS
132
121
  location,
133
122
  path,
134
123
  branch,
135
- slices: sliceCounts(featureDir, { root, feature, featureBranch }),
124
+ scenarios: scenarioCounts(featureDir, { root, feature, featureBranch }),
136
125
  env: alloc && Number.isInteger(alloc.base)
137
126
  ? { base: alloc.base, range: [alloc.base, alloc.base + blockSize - 1] }
138
127
  : null,
139
- handoff: handoffInfo(featureDir),
140
128
  lastCommit: location === 'worktree' && branch ? lastCommitRelative(root, branch) : null,
141
129
  dirty: location === 'worktree' ? dirtyCount(path) : null,
142
130
  remote: buildRemote(feature, branch, heads, prs),
@@ -221,9 +209,8 @@ export function scanFleet(cwd = process.cwd()) {
221
209
  location: 'pr-pending',
222
210
  path: null,
223
211
  branch: p.branch,
224
- slices: null,
212
+ scenarios: null,
225
213
  env: null,
226
- handoff: { exists: false, generatedAt: null },
227
214
  lastCommit: null,
228
215
  dirty: null,
229
216
  remote: { branch: p.branch, pushed: true, sha: null, pr: { number: p.number, state: p.state } },
@@ -13,40 +13,8 @@ import { buildPrIndex } from '../git/pr-index.js';
13
13
  import { classifyBranch, commitsAhead, resolveBaseRef } from '../git/branch-classifier.js';
14
14
  import { auditDockerStacks } from './docker-audit.js';
15
15
  import { auditFinishBackups } from './finish-backup.js';
16
- import { planAgentReap } from './agent-reaper.js';
17
16
 
18
17
  const norm = (p) => resolve(p).replace(/\\/g, '/');
19
- const AGENT_TASK = /^agent-[0-9a-f]+$/;
20
-
21
- const AGENT_FIX = {
22
- 'stopped-clean': 'morph-spec worktree reap (ou `morph-spec doctor --worktrees --prune`)',
23
- 'orphan-branch': 'morph-spec worktree reap (ou `morph-spec doctor --worktrees --prune`)',
24
- 'directory-missing': 'morph-spec doctor --worktrees --prune',
25
- locked: 'a árvore está com `git worktree lock`; `git worktree unlock <path>` libera, se o lock já não vale',
26
- unknown: 'não foi possível provar que é seguro remover — deixe como está',
27
- };
28
-
29
- function agentLeftoverIssue(item) {
30
- const where = item.path ?? item.branch;
31
- const reapable = item.state === 'reapable';
32
- const fix = AGENT_FIX[item.reason]
33
- ?? (item.reason === 'exclusive-commits'
34
- ? `integre a branch (\`git merge --no-ff ${item.branch}\` no orquestrador) antes de remover — ela é o único exemplar`
35
- : item.reason === 'dirty'
36
- ? `confira o trabalho na árvore (commite ou descarte) e rode \`morph-spec worktree remove --target "${item.path}"\``
37
- : `o harness não registrou o fim do agente; se ele já terminou, \`morph-spec worktree remove --target "${item.path}"\``);
38
- return {
39
- type: 'agent-worktree-leftover',
40
- severity: 'info',
41
- ...(item.path ? { path: item.path } : {}),
42
- branch: item.branch,
43
- reapable,
44
- detail: reapable
45
- ? `${where} é sobra de sub-agent que já parou, sem trabalho pendente`
46
- : `${where} é sobra de sub-agent com trabalho a preservar (${item.reason}${item.detail ? `: ${item.detail}` : ''})`,
47
- fix,
48
- };
49
- }
50
18
 
51
19
  function knownFeatures(root) {
52
20
  const names = new Set();
@@ -197,7 +165,6 @@ export function auditWorktrees(cwd = process.cwd(), { exec } = {}) {
197
165
  }
198
166
 
199
167
  if (task) {
200
- if (AGENT_TASK.test(task)) continue; // reported once, by the agent plan below
201
168
  const work = unintegratedWork(root, entry.branch, prIndex);
202
169
  if (work.unintegrated) {
203
170
  issues.push({
@@ -262,10 +229,6 @@ export function auditWorktrees(cwd = process.cwd(), { exec } = {}) {
262
229
  }
263
230
  }
264
231
 
265
- for (const item of planAgentReap(root, { cwd }).items) {
266
- if (item.state === 'reapable' || item.state === 'preserved') issues.push(agentLeftoverIssue(item));
267
- }
268
-
269
232
  for (const d of dirsOnDisk(root)) {
270
233
  if (registeredPaths.has(d.path)) continue;
271
234
  issues.push({
@@ -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`, 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();