@polymorphism-tech/morph-spec 9.8.0 → 10.1.1

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 (202) hide show
  1. package/CLAUDE.md +12 -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 +28 -36
  6. package/docs/QUICKSTART.md +31 -191
  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-preflight.md +3 -3
  16. package/framework/commands/morph-review.md +15 -0
  17. package/framework/commands/morph-status.md +1 -1
  18. package/framework/docs/CLI.md +84 -95
  19. package/framework/evals/review-checklist.md +45 -53
  20. package/framework/hooks/README.md +4 -4
  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/next-step.js +63 -24
  38. package/framework/hooks/shared/phase-utils.js +6 -9
  39. package/framework/hooks/shared/report-progress.js +86 -0
  40. package/framework/hooks/shared/state-reader.js +3 -2
  41. package/framework/hooks/statusline.js +332 -0
  42. package/framework/mcp/registry.json +4 -4
  43. package/framework/phases.json +32 -10
  44. package/framework/rules/ai-agents-standards.md +10 -2
  45. package/framework/rules/csharp-standards.md +4 -0
  46. package/framework/rules/data-standards.md +1 -1
  47. package/framework/rules/ef-standards.md +20 -0
  48. package/framework/rules/frontend-standards.md +1 -1
  49. package/framework/rules/hangfire-standards.md +1 -1
  50. package/framework/rules/implement-report.md +25 -0
  51. package/framework/rules/infrastructure-standards.md +2 -0
  52. package/framework/rules/neon-auth-standards.md +1 -1
  53. package/framework/rules/neon-standards.md +3 -3
  54. package/framework/rules/nextjs-standards.md +7 -0
  55. package/framework/rules/plan-grammar.md +53 -0
  56. package/framework/rules/ui-design.md +27 -0
  57. package/framework/schemas/config.schema.json +11 -18
  58. package/framework/schemas/feature.schema.json +7 -1
  59. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  60. package/framework/skills/core/create-umbraco-lp/references/umbraco-lp-patterns.md +1 -1
  61. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +2 -2
  62. package/framework/skills/core/morph-apply/SKILL.md +39 -166
  63. package/framework/skills/core/morph-apply/references/debugging.md +8 -0
  64. package/framework/skills/core/morph-apply/references/hotfix.md +42 -0
  65. package/framework/skills/core/morph-apply/references/machine.md +29 -0
  66. package/framework/skills/core/{morph-media/SKILL.md → morph-apply/references/media.md} +11 -74
  67. package/framework/skills/core/morph-apply/references/review.md +65 -0
  68. package/framework/skills/core/morph-plan/SKILL.md +49 -109
  69. package/framework/skills/core/morph-plan/references/critique.md +43 -0
  70. package/framework/skills/core/morph-plan/references/interview.md +37 -0
  71. package/framework/skills/core/morph-uiux/SKILL.md +47 -64
  72. package/framework/skills/core/morph-uiux/surfaces.json +32 -0
  73. package/framework/standards/STANDARDS.json +51 -28
  74. package/framework/standards/STANDARDS.meta.json +16 -19
  75. package/framework/standards/ai-agents/agent-session.md +2 -3
  76. package/framework/standards/ai-agents/context-providers.md +2 -2
  77. package/framework/standards/ai-agents/durable-workflows-hitl.md +2 -2
  78. package/framework/standards/ai-agents/providers/model-registry.md +2 -2
  79. package/framework/standards/ai-agents/rag-custom-pgvector.md +7 -6
  80. package/framework/standards/ai-agents/setup.md +1 -1
  81. package/framework/standards/ai-agents/tool-adapters.md +2 -2
  82. package/framework/standards/architecture/stack-baseline.md +70 -0
  83. package/framework/standards/backend/database/postgresql/database.md +22 -1
  84. package/framework/standards/backend/dotnet/vsa-handler-patterns.md +33 -1
  85. package/framework/standards/backend/integrations/hangfire/hangfire-jobs.md +2 -1
  86. package/framework/standards/backend/integrations/neon-auth/neon-auth.md +12 -13
  87. package/framework/standards/frontend/design-system/ai-image-generation.md +51 -12
  88. package/framework/standards/frontend/design-system/ai-video-generation.md +50 -4
  89. package/framework/standards/frontend/design-system/art-direction.md +2 -2
  90. package/framework/standards/frontend/scroll-driven/frame-scrub.md +12 -2
  91. package/framework/standards/frontend/scroll-driven/scroll-components.md +76 -4
  92. package/framework/standards/infrastructure/docker/local-compose-isolation.md +2 -2
  93. package/framework/standards/infrastructure/neon/neon-pgvector.md +62 -20
  94. package/framework/standards/infrastructure/neon/neon-platform.md +61 -0
  95. package/framework/standards/infrastructure/neon/neon-setup.md +14 -13
  96. package/framework/standards/integration/mcp/mcp-tools.md +20 -20
  97. package/framework/templates/REGISTRY.json +1 -2
  98. package/framework/templates/context/ARCHITECTURE.md +1 -1
  99. package/framework/templates/docs/plan.md +15 -12
  100. package/framework/templates/docs/ui-mockups.md +17 -105
  101. package/framework/templates/infrastructure/docker/docker-compose.template.yml +1 -1
  102. package/package.json +1 -1
  103. package/src/commands/e2e/e2e.js +88 -23
  104. package/src/commands/graph/diff.js +74 -0
  105. package/src/commands/graph/status.js +1 -1
  106. package/src/commands/observability/cost.js +2 -4
  107. package/src/commands/observability/telemetry.js +1 -1
  108. package/src/commands/project/doctor.js +23 -39
  109. package/src/commands/project/fleet.js +5 -6
  110. package/src/commands/project/init.js +63 -36
  111. package/src/commands/project/stack-scan.js +3 -42
  112. package/src/commands/project/stack-set.js +26 -0
  113. package/src/commands/project/status.js +12 -13
  114. package/src/commands/project/update.js +39 -3
  115. package/src/commands/project/worktree.js +3 -14
  116. package/src/commands/state/advance.js +17 -17
  117. package/src/commands/state/approve.js +0 -6
  118. package/src/commands/state/archive.js +27 -1
  119. package/src/commands/state/auto-approval.js +2 -2
  120. package/src/commands/state/create.js +1 -6
  121. package/src/commands/state/gate-check.js +38 -31
  122. package/src/commands/state/gate-decision.js +3 -2
  123. package/src/commands/state/next.js +1 -0
  124. package/src/commands/state/recap.js +67 -0
  125. package/src/commands/state/report-check.js +36 -178
  126. package/src/commands/validation/plan-check.js +16 -11
  127. package/src/commands/validation/validate-feature.js +3 -3
  128. package/src/commands/validation/verify.js +3 -6
  129. package/src/core/paths/output-schema.js +3 -3
  130. package/src/core/state/state-manager.js +2 -4
  131. package/src/dashboard/app/components/CommandPalette.jsx +2 -5
  132. package/src/dashboard/app/components/GatePauses.jsx +116 -0
  133. package/src/dashboard/app/components/SessionMap.jsx +77 -0
  134. package/src/dashboard/app/pages/Activity.jsx +4 -0
  135. package/src/dashboard/app/pages/AgentMap.jsx +1 -2
  136. package/src/dashboard/app/pages/FeatureDetail.jsx +129 -186
  137. package/src/dashboard/app/pages/Features.jsx +7 -7
  138. package/src/dashboard/app/pages/PhasePipeline.jsx +1 -1
  139. package/src/dashboard/app/pages/Portfolio.jsx +40 -5
  140. package/src/dashboard/scanner/activity-scanner.js +29 -0
  141. package/src/dashboard/scanner/agents-scanner.js +11 -14
  142. package/src/dashboard/scanner/features-scanner.js +45 -24
  143. package/src/dashboard/scanner/project-api.js +11 -2
  144. package/src/dashboard/scanner/skills-scanner.js +11 -1
  145. package/src/lib/detectors/index.js +1 -1
  146. package/src/lib/e2e/applicability.js +3 -1
  147. package/src/lib/git/tree-state.js +1 -16
  148. package/src/lib/graph/diff-tree.js +127 -0
  149. package/src/lib/graph/graphify-runtime.js +12 -5
  150. package/src/lib/installers/memory-installer.js +3 -3
  151. package/src/lib/installers/rules-installer.js +57 -0
  152. package/src/lib/installers/stack-assets.js +17 -0
  153. package/src/lib/installers/stack-sync.js +219 -0
  154. package/src/lib/plan/checks.js +61 -109
  155. package/src/lib/plan/critique.js +28 -0
  156. package/src/lib/plan/media.js +42 -0
  157. package/src/lib/plan/plan-html.js +125 -0
  158. package/src/lib/plan/plan-parser.js +34 -45
  159. package/src/lib/recap/piper.js +36 -0
  160. package/src/lib/recap/recap.js +232 -0
  161. package/src/lib/review-reports.js +28 -4
  162. package/src/lib/risk-detector.js +90 -11
  163. package/src/lib/specs/screen-checklist.js +33 -48
  164. package/src/lib/stack/dotnet-build-ignores.js +8 -9
  165. package/src/lib/telemetry/metrics.js +27 -29
  166. package/src/lib/validators/reuse/reuse-validator.js +1 -9
  167. package/src/lib/validators/verify-runner.js +0 -7
  168. package/src/lib/worktree/fleet-scanner.js +7 -20
  169. package/src/lib/worktree/worktree-audit.js +0 -37
  170. package/src/scripts/setup-infra.js +15 -50
  171. package/src/utils/agents-installer.js +27 -201
  172. package/src/utils/hooks-installer.js +11 -95
  173. package/framework/hooks/claude-code/core/warn-bash-source-write.js +0 -23
  174. package/framework/hooks/claude-code/statusline.py +0 -699
  175. package/framework/hooks/claude-code/statusline.sh +0 -11
  176. package/framework/hooks/shared/agent-reap.js +0 -74
  177. package/framework/hooks/shared/slice-progress.js +0 -110
  178. package/framework/skills/core/morph-hotfix/SKILL.md +0 -133
  179. package/framework/skills/core/morph-machine/SKILL.md +0 -46
  180. package/framework/skills/core/morph-review/SKILL.md +0 -129
  181. package/framework/skills/core/morph-standards/SKILL.md +0 -52
  182. package/framework/standards/PROVENANCE.json +0 -119
  183. package/framework/standards/infrastructure/neon/neon-pgrag.md +0 -243
  184. package/src/commands/project/worktree-reap.js +0 -34
  185. package/src/commands/state/handoff.js +0 -65
  186. package/src/commands/validation/mutate.js +0 -263
  187. package/src/commands/validation/scout-check.js +0 -28
  188. package/src/lib/generators/handoff-generator.js +0 -172
  189. package/src/lib/mutation/adapters.js +0 -79
  190. package/src/lib/mutation/commands.js +0 -61
  191. package/src/lib/mutation/dotnet-introspection.js +0 -281
  192. package/src/lib/mutation/manifest.js +0 -144
  193. package/src/lib/mutation/plan.js +0 -44
  194. package/src/lib/mutation/report.js +0 -68
  195. package/src/lib/mutation/runner.js +0 -301
  196. package/src/lib/mutation/schemata.js +0 -60
  197. package/src/lib/mutation/snapshot.js +0 -65
  198. package/src/lib/mutation/subset.js +0 -109
  199. package/src/lib/mutation/trx.js +0 -50
  200. package/src/lib/scout/scout-check.js +0 -293
  201. package/src/lib/validators/traceability-check.js +0 -103
  202. package/src/lib/worktree/agent-reaper.js +0 -247
@@ -5,30 +5,19 @@
5
5
  import { createHash } from 'crypto';
6
6
  import { existsSync, readFileSync } from 'fs';
7
7
  import { dirname, join } from 'path';
8
- import { fileURLToPath } from 'url';
9
8
  import { getAbsoluteOutputPath } from '../../core/paths/output-schema.js';
10
9
  import { verifierScreenChecklist } from '../review-reports.js';
11
10
  import { LEGACY_MOCKUP_EXEMPTION, uiuxChecklistPause } from '../risk-detector.js';
12
11
 
13
- const PACKAGE_AGENTS = fileURLToPath(new URL('../../../framework/agents.json', import.meta.url));
14
-
15
12
  /** `match` roda sobre o rótulo normalizado; um rótulo que casa com vários itens responde a todos. */
16
13
  export const SCREEN_CHECKLIST_ITEMS = [
17
- { id: 'superficie', label: 'Superfície', match: /\bsuperficie\b/ },
18
- { id: 'volume', label: 'Volume esperado', match: /\bvolume\b/ },
19
- { id: 'paginacao', label: 'Paginação', match: /\bpaginacao\b/ },
20
- { id: 'ordenacao', label: 'Ordenação', match: /\bordenacao\b/ },
21
- { id: 'filtro', label: 'Filtro', match: /\bfiltros?\b/ },
22
- { id: 'vazio', label: 'Estado vazio', match: /\bvazio\b/ },
23
- { id: 'carregando', label: 'Estado carregando', match: /\bcarregando\b|\bcarregamento\b/ },
24
- { id: 'erro', label: 'Estado de erro', match: /\berro\b/ },
25
- { id: 'mobile', label: 'Mobile', match: /\bmobile\b/ },
26
- { id: 'abas', label: 'Abas', match: /\babas\b/ },
14
+ { id: 'servidor', label: 'Lista no servidor', match: /\bpaginacao\b|\bordenacao\b|\bservidor\b/ },
15
+ { id: 'estados', label: 'Estados vazio e carregando', match: /\bvazio\b|\bcarregando\b|\bcarregamento\b/ },
16
+ { id: 'erro', label: 'Erro com próxima ação', match: /\berro\b/ },
17
+ { id: 'acao', label: 'Uma ação primária', match: /\bprimaria\b/ },
18
+ { id: 'mobile', label: 'Celular', match: /\bmobile\b|\bcelular\b/ },
27
19
  ];
28
20
 
29
- // A superfície decide quais standards a tela lê, não um comportamento que a tela entregue exiba.
30
- const NOT_VERIFIED_AT_REVIEW = new Set(['superficie']);
31
-
32
21
  // As exatas, e as que começam adiando a decisão ("a definir depois", "TBD.").
33
22
  const PLACEHOLDERS = new Set(['', '-', '--', '—', '–', '...', 'x', 'todo', 'definir', 'pendente']);
34
23
  const PLACEHOLDER_PREFIX_RE = /^(a definir|tbd|\?|…)/;
@@ -141,30 +130,9 @@ const isNotApplicable = (answer) => NOT_APPLICABLE_RE.test(normalizeLabel(answer
141
130
 
142
131
  const labelOf = (id) => SCREEN_CHECKLIST_ITEMS.find((item) => item.id === id)?.label ?? id;
143
132
 
144
- // O vocabulário válido de `Superfície`: as `surfaces` da `ui-designer`, da cópia instalada ou do pacote.
145
- // null quando a persona não declara o campo, e então o valor não é validado.
146
- export function knownSurfaces(cwd) {
147
- const surfaces = readAgentsJson(cwd)?.agents?.['ui-designer']?.surfaces;
148
- return surfaces && typeof surfaces === 'object' && !Array.isArray(surfaces) ? Object.keys(surfaces) : null;
149
- }
150
-
151
- /** The project's installed `agents.json`, else the package's; null when unreadable. */
152
- export function readAgentsJson(cwd) {
153
- for (const path of [join(cwd, '.morph', 'framework', 'agents.json'), PACKAGE_AGENTS]) {
154
- if (!existsSync(path)) continue;
155
- try { return JSON.parse(readFileSync(path, 'utf8')); } catch { return null; }
156
- }
157
- return null;
158
- }
159
-
160
- function namesSurface(answer, surfaces) {
161
- const norm = normalizeLabel(answer);
162
- return surfaces.some((key) => new RegExp(`(^|[^a-z0-9-])${key}([^a-z0-9-]|$)`).test(norm));
163
- }
164
-
165
133
  // Sem nenhuma `## Tela:`, só o formato antigo é legado; qualquer outro heading é "nenhuma tela
166
134
  // declarada", senão trocar o heading pularia o checklist.
167
- export function auditScreenChecklist(markdown, { surfaces = null } = {}) {
135
+ export function auditScreenChecklist(markdown) {
168
136
  const parsed = parseScreens(markdown);
169
137
  const problems = [];
170
138
  if (parsed.length === 0) {
@@ -185,18 +153,25 @@ export function auditScreenChecklist(markdown, { surfaces = null } = {}) {
185
153
  } else {
186
154
  if (missing.length) problems.push(`tela "${screen.name}": falta ${missing.map(labelOf).join(', ')}`);
187
155
  if (unanswered.length) problems.push(`tela "${screen.name}": sem resposta em ${unanswered.map(labelOf).join(', ')}`);
188
- const surface = screen.answers.get('superficie');
189
- if (surfaces?.length && surface !== undefined && !isPlaceholder(surface) && !isNotApplicable(surface)
190
- && !namesSurface(surface, surfaces)) {
191
- problems.push(`tela "${screen.name}": Superfície "${surface}" não é uma das superfícies da ui-designer (${surfaces.join(', ')})`);
192
- }
193
156
  }
194
- return { name: screen.name, hasChecklist: screen.hasChecklist, missing, unanswered, prototype: screen.prototype };
157
+ return { name: screen.name, hasChecklist: screen.hasChecklist, missing, unanswered, prototype: screen.prototype, states: statesOf(screen) };
195
158
  });
196
159
  return { legacy: false, screens, problems };
197
160
  }
198
161
 
199
- // Linha **Protótipo:** da tela + o HTML + os prints do laço de autocrítica ao lado dele.
162
+ /** The states the prototype must switch to, by the checklist items that apply to the screen. */
163
+ export const PROTOTYPE_STATES = { estados: ['vazio', 'carregando'], erro: ['erro'] };
164
+ export const STATE_PRINT_WIDTH = 390;
165
+
166
+ function statesOf(screen) {
167
+ return Object.entries(PROTOTYPE_STATES).flatMap(([id, states]) => {
168
+ const answer = screen.answers.get(id);
169
+ return answer && !isPlaceholder(answer) && !isNotApplicable(answer) ? states : [];
170
+ });
171
+ }
172
+
173
+ // Linha **Protótipo:** da tela + o HTML + os prints do laço de autocrítica ao lado dele; cada estado
174
+ // que o checklist diz que se aplica tem `data-estado` no HTML e o print de celular.
200
175
  export function auditPrototypes(screens, designDir) {
201
176
  const problems = [];
202
177
  for (const screen of screens) {
@@ -205,9 +180,19 @@ export function auditPrototypes(screens, designDir) {
205
180
  continue;
206
181
  }
207
182
  const base = screen.prototype.replace(/\.html$/i, '');
208
- const expected = [screen.prototype, ...PROTOTYPE_WIDTHS.map((width) => `${base}-${width}.png`)];
183
+ const states = screen.states ?? [];
184
+ const expected = [
185
+ screen.prototype,
186
+ ...PROTOTYPE_WIDTHS.map((width) => `${base}-${width}.png`),
187
+ ...states.map((state) => `${base}-${state}-${STATE_PRINT_WIDTH}.png`),
188
+ ];
209
189
  const missing = expected.filter((rel) => !existsSync(join(designDir, rel)));
210
190
  if (missing.length) problems.push(`tela "${screen.name}": falta ${missing.map((rel) => `1-design/${rel}`).join(', ')}`);
191
+ const html = existsSync(join(designDir, screen.prototype)) ? readFileSync(join(designDir, screen.prototype), 'utf8') : '';
192
+ const unswitchable = html ? states.filter((state) => !new RegExp(`data-estado\\s*=\\s*["']${state}["']`).test(html)) : [];
193
+ if (unswitchable.length) {
194
+ problems.push(`tela "${screen.name}": o protótipo não alterna para ${unswitchable.join(', ')} (falta \`data-estado="…"\` no seletor de estado)`);
195
+ }
211
196
  }
212
197
  return problems;
213
198
  }
@@ -216,7 +201,7 @@ export function auditPrototypes(screens, designDir) {
216
201
  export function collectScreenChecklist(cwd, featureName, gateName) {
217
202
  const mockups = readMockups(cwd, featureName);
218
203
  if (mockups === null) return { mockups: false, legacy: false, screens: [], problems: [] };
219
- const audit = auditScreenChecklist(mockups, { surfaces: knownSurfaces(cwd) });
204
+ const audit = auditScreenChecklist(mockups);
220
205
  const signal = { mockups: true, legacy: audit.legacy, screens: audit.screens.map((s) => s.name), problems: audit.problems };
221
206
  if (audit.legacy) signal.exempt = LEGACY_MOCKUP_EXEMPTION;
222
207
  if (gateName === 'uiux' && !audit.legacy) {
@@ -255,7 +240,7 @@ export function checklistCoverage(designMarkdown, reportSection) {
255
240
  for (const screen of parseScreens(designMarkdown)) {
256
241
  const done = covered.get(screenKey(screen.name)) ?? new Set();
257
242
  const owed = [...screen.answers]
258
- .filter(([id, answer]) => !NOT_VERIFIED_AT_REVIEW.has(id) && !isPlaceholder(answer) && !isNotApplicable(answer))
243
+ .filter(([, answer]) => !isPlaceholder(answer) && !isNotApplicable(answer))
259
244
  .map(([id]) => id)
260
245
  .filter((id) => !done.has(id));
261
246
  if (owed.length) gaps.push(`${screen.name}: ${owed.map(labelOf).join(', ')}`);
@@ -1,15 +1,14 @@
1
- import { readdirSync } from 'fs';
1
+ import { existsSync, readdirSync, readFileSync } from 'fs';
2
2
  import { join } from 'path';
3
3
  import { ensureDotnetBuildIgnores } from '../../utils/file-copier.js';
4
4
  import { getProjectTags } from '../stack-filter.js';
5
5
  import { readMorphConfig } from '../tasks/test-runner.js';
6
- import { readPlanChecks } from '../plan/checks.js';
6
+ import { getAbsoluteOutputPath } from '../../core/paths/output-schema.js';
7
7
  import { WORKTREE_DIR_NAMES } from '../worktree/dirs.js';
8
8
 
9
- export const DOTNET_IMPLEMENTERS = ['dotnet-senior', 'ef-modeler', 'maf-expert'];
10
-
11
9
  const SKIPPED_DIRS = new Set([...WORKTREE_DIR_NAMES, 'node_modules', 'bin', 'obj']);
12
10
  const DOTNET_PROJECT_FILE = /\.(?:csproj|slnx?)$/i;
11
+ const DOTNET_IN_PLAN = /\.(?:cs|csproj|slnx?)\b|\bdotnet\s+(?:test|build|new)\b/i;
13
12
  const MAX_DEPTH = 6;
14
13
 
15
14
  export function hasDotnetProjectFile(root, depth = 0) {
@@ -21,16 +20,16 @@ export function hasDotnetProjectFile(root, depth = 0) {
21
20
  && hasDotnetProjectFile(join(root, e.name), depth + 1));
22
21
  }
23
22
 
24
- function planHasDotnetSlice(tree, featureName) {
23
+ function planMentionsDotnet(tree, featureName) {
25
24
  if (!featureName) return false;
26
- const { checks } = readPlanChecks(tree, featureName);
27
- return (checks?.slices || []).some((slice) => DOTNET_IMPLEMENTERS.includes(String(slice.persona || '').replace(/^morph-/, '')));
25
+ const path = getAbsoluteOutputPath(tree, featureName, 'plan');
26
+ try { return existsSync(path) && DOTNET_IN_PLAN.test(readFileSync(path, 'utf8')); } catch { return false; }
28
27
  }
29
28
 
30
- // The plan's slices are checked because a greenfield tree has no stack or .csproj yet.
29
+ // The plan is read because a greenfield tree has no stack or .csproj yet.
31
30
  export function isDotnetProject(tree, { featureName, configRoot = tree } = {}) {
32
31
  if (getProjectTags(readMorphConfig(configRoot)).includes('dotnet')) return true;
33
- return planHasDotnetSlice(tree, featureName) || hasDotnetProjectFile(tree);
32
+ return planMentionsDotnet(tree, featureName) || hasDotnetProjectFile(tree);
34
33
  }
35
34
 
36
35
  /** Returns the `.gitignore` entries added; `[]` when not .NET or on any error. */
@@ -95,36 +95,16 @@ function addRun(slot, durationMs) {
95
95
  const REVIEW_NAME = /^(?:ver|rev|sec|g3|gate3|eval|review|reviewer|security)\d*-/;
96
96
  const isReviewAgent = (type) => REVIEW_AGENT_TYPES.has(type) || type === 'morph-evaluator' || REVIEW_NAME.test(String(type ?? ''));
97
97
 
98
- // Same split as the test-budget hook: review first, then an unfiltered verify, then the round count.
99
98
  // A run refused by the full heavy queue ran no test.
100
99
  function testOutsideVerify(bashEvents) {
101
- const out = {
102
- total: newTimeSlot(),
103
- subagentVerify: newTimeSlot(),
104
- implementerRound3Plus: newTimeSlot(),
105
- reviewTests: { ...newTimeSlot(), byPersona: newTimeSlot() },
106
- };
107
- const byAgent = new Map();
100
+ const out = { total: newTimeSlot(), reviewTests: { ...newTimeSlot(), byPersona: newTimeSlot() } };
108
101
  for (const e of bashEvents) {
109
102
  if (e.event === 'PostToolUseFailure' && e.exitCode === QUEUE_FULL_EXIT) continue;
110
- const verify = morphVerifyCall(e.command);
111
- if (!verify && !testRunKind(e.command)) continue;
103
+ if (!morphVerifyCall(e.command) && !testRunKind(e.command)) continue;
112
104
  addRun(out.total, e.durationMs);
113
- if (!e.agentId) continue;
114
- if (isReviewAgent(e.agentType)) {
115
- addRun(out.reviewTests, e.durationMs);
116
- if (REVIEW_AGENT_TYPES.has(e.agentType)) addRun(out.reviewTests.byPersona, e.durationMs);
117
- continue;
118
- }
119
- if (!byAgent.has(e.agentId)) byAgent.set(e.agentId, []);
120
- byAgent.get(e.agentId).push({ e, unfilteredVerify: Boolean(verify && !verify.filtered) });
121
- }
122
- for (const runs of byAgent.values()) {
123
- runs.sort((a, b) => String(a.e.ts ?? '').localeCompare(String(b.e.ts ?? '')));
124
- runs.forEach(({ e, unfilteredVerify }, round) => {
125
- if (unfilteredVerify) addRun(out.subagentVerify, e.durationMs);
126
- else if (round >= 2) addRun(out.implementerRound3Plus, e.durationMs);
127
- });
105
+ if (!e.agentId || !isReviewAgent(e.agentType)) continue;
106
+ addRun(out.reviewTests, e.durationMs);
107
+ if (REVIEW_AGENT_TYPES.has(e.agentType)) addRun(out.reviewTests.byPersona, e.durationMs);
128
108
  }
129
109
  return out;
130
110
  }
@@ -164,7 +144,7 @@ function reuseStateOf(e) {
164
144
  return { state: 'absent', reason: null };
165
145
  }
166
146
 
167
- const newGateSlot = () => ({ decisions: 0, paused: 0, autoPassed: 0, approvedAfterPause: 0, reworkedAfterPause: 0, unapproved: 0, bySignal: {} });
147
+ const newGateSlot = () => ({ decisions: 0, paused: 0, pausedForHuman: 0, pausedUnattributed: 0, autoPassed: 0, approvedAfterPause: 0, reworkedAfterPause: 0, unapproved: 0, bySignal: {} });
168
148
 
169
149
  // A pause stays open until the next approve or a decision that no longer pauses on the same signal:
170
150
  // re-running gate-check over the same unchanged pause is not rework.
@@ -173,7 +153,7 @@ function trackGate(gates, open, e) {
173
153
  const slot = gates[e.gate] ||= newGateSlot();
174
154
  const key = `${e.feature || ''}\u0000${e.gate}`;
175
155
  const pending = open.get(key);
176
- const bySignal = (signal) => slot.bySignal[signal || 'desconhecido'] ||= { paused: 0, approvedAfterPause: 0, reworkedAfterPause: 0 };
156
+ const bySignal = (signal) => slot.bySignal[signal || 'desconhecido'] ||= { paused: 0, pausedForHuman: 0, pausedUnattributed: 0, approvedAfterPause: 0, reworkedAfterPause: 0 };
177
157
  const credit = (field) => {
178
158
  slot[field] += 1;
179
159
  bySignal(pending.signal)[field] += 1;
@@ -189,12 +169,30 @@ function trackGate(gates, open, e) {
189
169
  return;
190
170
  }
191
171
  slot.decisions += 1;
192
- if (pending && e.pause && e.signal === pending.signal) return;
172
+ const resolver = e.resolver ?? 'human';
173
+ if (pending && e.pause && e.signal === pending.signal) {
174
+ // Same signal, new owner: the session ran out of attempts and the pause escalated to the human.
175
+ if (pending.resolver === 'session' && resolver === 'human') {
176
+ slot.pausedForHuman += 1;
177
+ bySignal(pending.signal).pausedForHuman += 1;
178
+ }
179
+ pending.resolver = resolver;
180
+ return;
181
+ }
193
182
  if (pending) credit(e.source === 'manual' && !e.pause ? 'approvedAfterPause' : 'reworkedAfterPause');
194
183
  if (e.pause) {
195
184
  slot.paused += 1;
196
185
  bySignal(e.signal).paused += 1;
197
- open.set(key, { signal: e.signal });
186
+ if (resolver === 'human') {
187
+ slot.pausedForHuman += 1;
188
+ bySignal(e.signal).pausedForHuman += 1;
189
+ }
190
+ // Written before the resolver existed: counted as human above, but nobody recorded who cleared it.
191
+ if (e.resolver === undefined) {
192
+ slot.pausedUnattributed += 1;
193
+ bySignal(e.signal).pausedUnattributed += 1;
194
+ }
195
+ open.set(key, { signal: e.signal, resolver });
198
196
  } else {
199
197
  slot.autoPassed += 1;
200
198
  open.delete(key);
@@ -123,16 +123,8 @@ export function readReuseVerdicts(projectPath, featureName) {
123
123
  return verdicts;
124
124
  }
125
125
 
126
- // União de duas fontes cegas em momentos diferentes: `arquivos:` das fatias sobrevivem
127
- // ao commit; `git status` pega o que o plano não previu.
128
- export function collectChangedFiles(projectPath, featureName) {
126
+ export function collectChangedFiles(projectPath) {
129
127
  const files = new Set();
130
- for (const slice of readPlanChecks(projectPath, featureName).checks?.slices ?? []) {
131
- for (const file of slice.files ?? []) {
132
- if (typeof file === 'string') files.add(file.replace(/\\/g, '/'));
133
- }
134
- }
135
-
136
128
  try {
137
129
  const out = execFileSync('git', ['status', '--porcelain', '--untracked-files=all'], {
138
130
  cwd: projectPath,
@@ -21,7 +21,6 @@ import {
21
21
  evalEmptyRunProblem, evalColdCacheWarning,
22
22
  } from '../tasks/eval-node.js';
23
23
  import { runValidation } from './validation-runner.js';
24
- import { checkOutputsTraceability } from './traceability-check.js';
25
24
  import { createHeavyGate } from '../heavy/gate.js';
26
25
 
27
26
  const DEFAULT_BUILD_TIMEOUT = 1_800_000;
@@ -70,7 +69,6 @@ async function runVerifyGated(projectPath, featureName, options, gate) {
70
69
  _runValidation = runValidation,
71
70
  _runE2eCheck = runE2eCheck,
72
71
  _e2eApplicability = e2eApplicability,
73
- _checkOutputsTraceability = checkOutputsTraceability,
74
72
  } = options;
75
73
 
76
74
  const stack = readStack(projectPath);
@@ -170,10 +168,6 @@ async function runVerifyGated(projectPath, featureName, options, gate) {
170
168
  appendTestsFeedback(feedback, nodes.tests);
171
169
  }
172
170
 
173
- // Outside `nodes` on purpose: status comes only from the nodes, so a traceability
174
- // warning never turns red.
175
- const traceability = _checkOutputsTraceability(projectPath, featureName);
176
-
177
171
  const baseline = recordBaseline ? recordBaselineFrom(projectPath, nodes.tests) : null;
178
172
  if (baseline && !baseline.recorded) {
179
173
  feedback.push(`[baseline] não gravada: ${baseline.reason}`);
@@ -193,7 +187,6 @@ async function runVerifyGated(projectPath, featureName, options, gate) {
193
187
  status,
194
188
  nodes,
195
189
  feedback,
196
- traceability,
197
190
  ...(baseline ? { baseline } : {}),
198
191
  };
199
192
  }
@@ -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
  }