@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
@@ -7,7 +7,7 @@ import { readFileSync, existsSync, readdirSync, statSync } from 'fs';
7
7
  import { join, basename, isAbsolute } from 'path';
8
8
  import { derivePhaseFrom, diskArtifactProbe, PHASE_ARTIFACTS } from '../../../framework/hooks/shared/state-reader.js';
9
9
  import { GATE_PREREQ } from '../../core/state/gate-order.js';
10
- import { sliceProgress, checksPath } from '../../../framework/hooks/shared/slice-progress.js';
10
+ import { implementProgress, checksPath } from '../../../framework/hooks/shared/report-progress.js';
11
11
  import {
12
12
  reviewReportsFromDir, requiredReviewRoles, combinedVerdict, allBlockers, checksCoverage, reviewFileName, REVIEW_ROLES,
13
13
  } from '../../lib/review-reports.js';
@@ -110,26 +110,16 @@ function resolveLocation(projectDir, name, loc, resolveTree) {
110
110
  };
111
111
  }
112
112
 
113
- export function readSlices(dir) {
114
- const progress = sliceProgress(dir);
113
+ export function readProgress(dir) {
114
+ const progress = implementProgress(dir);
115
115
  if (!progress) return null;
116
- const extracted = new Map((readJson(checksPath(dir))?.slices || []).filter((x) => x && x.id).map((x) => [x.id, x]));
117
- const list = progress.slices.map((sl) => {
118
- const x = extracted.get(sl.id) || {};
119
- return {
120
- ...sl,
121
- persona: x.persona ?? null,
122
- files: Array.isArray(x.files) ? x.files : [],
123
- covers: Array.isArray(x.covers) ? x.covers : [],
124
- };
125
- });
126
116
  return {
127
- counts: { total: progress.total, done: progress.done, partial: progress.partial, blocked: progress.blocked, pending: progress.pending },
128
- list,
117
+ counts: { total: progress.total, done: progress.done, blocked: progress.blocked, pending: progress.pending },
118
+ byId: new Map(progress.checks.map((c) => [c.id, c])),
129
119
  };
130
120
  }
131
121
 
132
- const NO_SLICES = { total: 0, done: 0, partial: 0, blocked: 0, pending: 0 };
122
+ const NO_SCENARIOS = { total: 0, done: 0, blocked: 0, pending: 0 };
133
123
 
134
124
  function gatesOf(feature) {
135
125
  const skipped = new Set(feature.skippedPhases || []);
@@ -142,9 +132,11 @@ function gatesOf(feature) {
142
132
  approved,
143
133
  approvedBy: g.approvedBy ?? (g.auto ? 'auto' : null),
144
134
  at: g.approvedAt || g.timestamp || null,
145
- paused: !approved && decision?.pause === true,
135
+ // `paused` is "waiting for the human"; a session-owned pause is work in progress.
136
+ paused: !approved && decision?.pause === true && decision.resolver !== 'session',
146
137
  skipped: skipped.has(gate),
147
138
  reason: decision?.reason ?? null,
139
+ since: decision?.decidedAt ?? null,
148
140
  };
149
141
  });
150
142
  }
@@ -174,8 +166,39 @@ export function reviewRows(reports, workType) {
174
166
  });
175
167
  }
176
168
 
169
+ // The contract opens the evidence with "Cn:"; a later mention is context, not the target.
170
+ const LEADING_CHECKS_RE = /^\s*(C\d+(?:\s*(?:,|e|\/)\s*C\d+)*)\s*:/;
171
+ function citedChecks(b) {
172
+ const lead = String(b.evidence).match(LEADING_CHECKS_RE)?.[1];
173
+ return [...new Set((lead ?? `${b.where} ${b.evidence}`).match(/\bC\d+\b/g) ?? [])];
174
+ }
175
+
176
+ // A role re-run fewer times keeps its last round; verifier rows accumulate across rounds.
177
+ export function scoreboard(reports) {
178
+ const roles = Object.keys(REVIEW_ROLES).filter((role) => reports[role].status !== 'missing' && reports[role].rounds?.length);
179
+ const total = Math.max(0, ...roles.map((role) => reports[role].rounds.length));
180
+ const rows = new Map();
181
+ return Array.from({ length: total }, (_, i) => {
182
+ const byRole = {};
183
+ for (const role of roles) {
184
+ const all = reports[role].rounds;
185
+ const r = all[Math.min(i, all.length - 1)];
186
+ byRole[role] = {
187
+ verdict: r.verdict,
188
+ reviewedSha: r.reviewedSha,
189
+ carried: i >= all.length,
190
+ blockers: r.blockers.map((b) => ({ ...b, checks: citedChecks(b) })),
191
+ };
192
+ if (role === 'verifier' && i < all.length) {
193
+ for (const c of r.checks ?? []) rows.set(c.id, { ...c, round: i });
194
+ }
195
+ }
196
+ return { roles: byRole, checks: [...rows.values()] };
197
+ });
198
+ }
199
+
177
200
  /** `coverage` is `null` until a verifier report exists; only PROVADO covers. */
178
- export function readChecks(dir, verifier) {
201
+ export function readChecks(dir, verifier, progress = readProgress(dir)) {
179
202
  const checks = readJson(checksPath(dir))?.checks;
180
203
  if (!Array.isArray(checks)) return null;
181
204
  const list = checks.filter((c) => c && c.id);
@@ -192,6 +215,7 @@ export function readChecks(dir, verifier) {
192
215
  proof: c.proof ?? null,
193
216
  status: byId.get(c.id)?.status ?? null,
194
217
  evidence: byId.get(c.id)?.evidence ?? null,
218
+ implement: progress?.byId.get(c.id)?.status ?? 'pendente',
195
219
  })),
196
220
  };
197
221
  }
@@ -201,7 +225,7 @@ function summarize(name, resolved) {
201
225
  const feature = normalizeFeature(resolved.feature);
202
226
  const gates = gatesOf(feature);
203
227
  const reviewApproved = gates.find((g) => g.gate === 'review')?.approved === true;
204
- const slices = readSlices(dir);
228
+ const progress = readProgress(dir);
205
229
  const verdict = reviewVerdict(dir, feature.workType);
206
230
  const status = feature.status || (location === 'archive' ? 'completed' : 'draft');
207
231
  const finished = isFinished({ status, location });
@@ -218,9 +242,7 @@ function summarize(name, resolved) {
218
242
  inEvaluation: verdict !== null && !reviewApproved,
219
243
  gates,
220
244
  awaiting: !finished && gates.some((g) => g.paused),
221
- slices: slices?.counts ?? NO_SLICES,
222
- // Finished features leave slice titles out: archived plans would bloat every list reply.
223
- sliceIndex: finished || !slices ? [] : slices.list.map((sl) => ({ id: sl.id, title: sl.title, status: sl.status })),
245
+ scenarios: progress?.counts ?? NO_SCENARIOS,
224
246
  verification: feature.verification
225
247
  ? { status: feature.verification.status ?? null, ts: feature.verification.ts ?? null }
226
248
  : null,
@@ -304,16 +326,15 @@ export function readFeatureDetail(projectDir, name, { resolveTree = resolveFeatu
304
326
  if (!resolved) return null;
305
327
  const summary = summarize(name, resolved);
306
328
  const feature = normalizeFeature(resolved.feature);
307
- const slices = readSlices(resolved.dir);
308
329
  const reports = reviewReportsFromDir(resolved.dir);
309
330
  const verdict = reviewVerdict(resolved.dir, feature.workType, reports);
310
331
  const checks = readChecks(resolved.dir, reports.verifier);
311
332
  return {
312
333
  ...summary,
313
- slicesList: slices?.list ?? [],
314
334
  checksList: checks?.list ?? [],
315
335
  checksCoverage: checks?.coverage ?? null,
316
336
  reviews: reviewRows(reports, feature.workType),
337
+ rounds: scoreboard(reports),
317
338
  gateDecisions: feature.gateDecisions || {},
318
339
  verificationNodes: feature.verification?.nodes || null,
319
340
  featureVerification: feature.verifications?.feature || null,
@@ -6,10 +6,11 @@
6
6
  import { join } from 'path';
7
7
  import { existsSync, readdirSync } from 'fs';
8
8
  import { scanFeatures, readFeatureDetail, readFeatureFile } from './features-scanner.js';
9
- import { createEventStore, buildFeed, lastActivityByFeature, ALL_KINDS, DEFAULT_KINDS } from './activity-scanner.js';
9
+ import { createEventStore, buildFeed, lastActivityByFeature, sessionMap, ALL_KINDS, DEFAULT_KINDS } from './activity-scanner.js';
10
10
  import { containerDirs } from '../../../framework/hooks/shared/worktree-root.js';
11
11
  import { isFinished } from './feature-status.js';
12
12
  import { resolveFeatureTree } from '../../lib/worktree/feature-truth.js';
13
+ import { computeMetrics } from '../../lib/telemetry/metrics.js';
13
14
 
14
15
  const TREE_TTL_MS = 60_000;
15
16
 
@@ -76,6 +77,7 @@ export function createProjectApi(projectDir, deps = {}) {
76
77
 
77
78
  const features = () => (cache.features ||= scan(projectDir, { resolveTree: rememberedTree }));
78
79
  const events = () => store.events();
80
+ let gatesCache = { from: null, length: -1, gates: null };
79
81
  const known = (name) => features().features?.find((f) => f.name === name) || undefined;
80
82
 
81
83
  /** Any `event` but 'change' may move a feature to another tree. */
@@ -111,7 +113,14 @@ export function createProjectApi(projectDir, deps = {}) {
111
113
  const limit = Math.min(Number(u.searchParams.get('limit')) || 100, 500);
112
114
  const all = events();
113
115
  const feed = buildFeed(all, { feature, kinds, before: u.searchParams.get('before') || undefined, limit });
114
- return json(200, { ...feed, kinds, cost: feature ? store.cost(feature) : null });
116
+ return json(200, { ...feed, kinds, cost: feature ? store.cost(feature) : null, session: feature ? sessionMap(all, feature) : null });
117
+ }
118
+ if (parts[0] === 'gates' && parts.length === 1) {
119
+ const all = events();
120
+ if (gatesCache.from !== all || gatesCache.length !== all.length) {
121
+ gatesCache = { from: all, length: all.length, gates: computeMetrics(all.filter((e) => e?.event === 'Gate')).gates };
122
+ }
123
+ return json(200, { gates: gatesCache.gates });
115
124
  }
116
125
  return json(404, { error: 'rota desconhecida' });
117
126
  } catch (err) {
@@ -85,10 +85,20 @@ export function scanSkills(frameworkDir, phases = []) {
85
85
  stacks: typeof fm.stacks === 'string' ? fm.stacks.replace(/[[\]]/g, '').split(',').map((s) => s.trim()).filter(Boolean) : [],
86
86
  allowedTools: typeof fm['allowed-tools'] === 'string' ? fm['allowed-tools'].split(',').map((t) => t.trim()) : [],
87
87
  phase: phaseOf.get(skillDir.name) || null,
88
- dispatches: extractDispatchPrompts(content),
88
+ dispatches: skillDispatches(join(levelPath, skillDir.name), content),
89
89
  content,
90
90
  });
91
91
  }
92
92
  }
93
93
  return skills.sort((a, b) => a.id.localeCompare(b.id));
94
94
  }
95
+
96
+ function skillDispatches(skillDir, content) {
97
+ const refsDir = join(skillDir, 'references');
98
+ const refs = existsSync(refsDir)
99
+ ? readdirSync(refsDir).filter((f) => f.endsWith('.md')).sort().map((f) => readFileSync(join(refsDir, f), 'utf8'))
100
+ : [];
101
+ return [content, ...refs]
102
+ .flatMap((text) => extractDispatchPrompts(text))
103
+ .map((d, index) => ({ ...d, index }));
104
+ }
@@ -54,7 +54,7 @@ export function detectProject(targetPath, opts = {}) {
54
54
  if (depNames.includes('@neondatabase/serverless') || depNames.includes('postgres') || depNames.includes('pg')) {
55
55
  structure.integrations.push('neon');
56
56
  }
57
- if (depNames.includes('@neondatabase/auth')) {
57
+ if (depNames.includes('@neon/auth') || depNames.includes('@neondatabase/auth')) {
58
58
  structure.integrations.push('neon-auth');
59
59
  }
60
60
  if (depNames.includes('stripe')) {
@@ -9,6 +9,8 @@ export const E2E_NOT_APPLICABLE = 'not applicable: no `prova: e2e` scenario and
9
9
  const UI_FILE = /\.(tsx|jsx|vue|svelte|astro|css|scss|sass|less|html)$/i;
10
10
  const RUNTIME_FILE = /(^|\/)(dockerfile[^/]*|\.dockerignore|(docker-)?compose[^/]*\.ya?ml|playwright\.config\.[cm]?[jt]s)$/i;
11
11
  const E2E_DIR = /(^|\/)e2e\//i;
12
+ // The harness writes here itself (verify syncs .claude/rules/), so these never mean the app changed.
13
+ const HARNESS_DIR = /^\.(morph|claude)\//;
12
14
  const KNOWN_PACKAGE_DIRS = E2E_SPEC_LOCATIONS.filter((dir) => dir !== '.');
13
15
 
14
16
  function packageDirsOf(config) {
@@ -28,7 +30,7 @@ export function e2eApplicability(projectPath, featureName, config) {
28
30
  const { checks } = readPlanChecks(projectPath, featureName);
29
31
  if (checks?.checks?.some((c) => c.proof === 'e2e')) return { applicable: true, reason: 'prova: e2e' };
30
32
  const base = detectDefaultBranch(projectPath);
31
- const changed = base ? changedPathsAgainst(base, projectPath).filter((f) => !f.startsWith('.morph/')) : [];
33
+ const changed = base ? changedPathsAgainst(base, projectPath).filter((f) => !HARNESS_DIR.test(f)) : [];
32
34
  if (changed.length === 0) return { applicable: true, reason: 'diff unknown' };
33
35
  const composeFile = resolveComposeFile(projectPath, config);
34
36
  const packageDirs = packageDirsOf(config);
@@ -4,7 +4,6 @@ import { OUTSIDE_MORPH, treeFingerprint, git as gitBuffer } from './tree-fingerp
4
4
  import { detectDefaultBranch } from './default-branch.js';
5
5
  import { resolveBaseRef } from './branch-classifier.js';
6
6
 
7
- export const MUTATION_MARKER = 'MUTACAO';
8
7
  const MAX_LISTED = 50;
9
8
  const MAX_COMMITS = 10;
10
9
 
@@ -51,7 +50,7 @@ export function snapshotTree(cwd) {
51
50
  }
52
51
 
53
52
  /**
54
- * Compares content, not status: a mutation committed since shows up, and a file
53
+ * Compares content, not status: an edit committed since shows up, and a file
55
54
  * committed unchanged does not.
56
55
  * @returns {{ at: string, headChanged: boolean, changed: string[], changedCount: number }}
57
56
  */
@@ -104,19 +103,6 @@ function commitsOnTop(cwd) {
104
103
  .split('\n').filter(Boolean);
105
104
  }
106
105
 
107
- function markers(cwd) {
108
- try {
109
- const out = git(cwd, ['grep', '-n', '-I', '--untracked', '-F', '-e', MUTATION_MARKER, ...OUTSIDE_MORPH]);
110
- return out.split('\n').filter(Boolean).slice(0, MAX_LISTED).map((line) => {
111
- const [file, lineNo] = line.split(':');
112
- return { file, line: Number(lineNo) };
113
- });
114
- } catch (err) {
115
- if (err.status === 1) return [];
116
- throw err;
117
- }
118
- }
119
-
120
106
  /** @returns {object|null} null when git cannot answer: unknown, never a clean tree. */
121
107
  export function treeState(cwd, { snapshot = null } = {}) {
122
108
  try {
@@ -131,7 +117,6 @@ export function treeState(cwd, { snapshot = null } = {}) {
131
117
  dirtyCount: dirty.length,
132
118
  diffstat: head ? diffstat(cwd) : null,
133
119
  commits: head ? commitsOnTop(cwd) : null,
134
- markers: markers(cwd),
135
120
  sinceBeforeEval: snapshot ? compareToSnapshot(cwd, snapshot) : null,
136
121
  };
137
122
  } catch {
@@ -0,0 +1,127 @@
1
+ import { buildIndex, linksOf, resolveEdgeTarget, isTestPath } from './sanitize.js';
2
+
3
+ const CALL_RELATIONS = new Set(['calls', 'indirect_call', 'dispatches_to']);
4
+ const HUNK_RE = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/;
5
+
6
+ /** `git diff -U0` → per file: whether it is new, and the new-side line ranges the diff touched. */
7
+ export function parseDiff(text) {
8
+ const files = new Map();
9
+ let current = null;
10
+ let fromNull = false;
11
+ for (const line of String(text || '').split('\n')) {
12
+ if (line.startsWith('--- ')) { fromNull = line === '--- /dev/null'; continue; }
13
+ if (line.startsWith('+++ ')) {
14
+ const path = line.slice(4).replace(/^b\//, '').trim();
15
+ current = path === '/dev/null' ? null : { added: fromNull, ranges: [] };
16
+ if (current) files.set(path, current);
17
+ continue;
18
+ }
19
+ const hunk = current && HUNK_RE.exec(line);
20
+ if (!hunk) continue;
21
+ const start = Number(hunk[1]);
22
+ const count = hunk[2] === undefined ? 1 : Number(hunk[2]);
23
+ // A pure deletion has no new-side line: it touches the line it left behind.
24
+ current.ranges.push(count === 0 ? [Math.max(1, start), Math.max(1, start)] : [start, start + count - 1]);
25
+ }
26
+ return files;
27
+ }
28
+
29
+ const lineOf = (node) => Number(/^L(\d+)/.exec(node.source_location || '')?.[1]) || null;
30
+
31
+ /**
32
+ * The call tree of what changed: every graph symbol whose lines the diff touched, `+` when its file is
33
+ * new and `~` otherwise, linked by the calls between them, entry points (no changed caller) first.
34
+ */
35
+ export function buildDiffTree(graph, changes) {
36
+ const index = buildIndex(graph);
37
+ const perFile = new Map();
38
+ for (const node of graph.nodes ?? []) {
39
+ if (!node._callable || !changes.has(node.source_file) || lineOf(node) === null) continue;
40
+ if (!perFile.has(node.source_file)) perFile.set(node.source_file, []);
41
+ perFile.get(node.source_file).push(node);
42
+ }
43
+
44
+ const changed = new Map();
45
+ for (const [file, nodes] of perFile) {
46
+ const change = changes.get(file);
47
+ nodes.sort((a, b) => lineOf(a) - lineOf(b));
48
+ nodes.forEach((node, i) => {
49
+ const start = lineOf(node);
50
+ const end = i + 1 < nodes.length ? lineOf(nodes[i + 1]) - 1 : Infinity;
51
+ const touched = change.added || change.ranges.some(([a, b]) => a <= end && b >= start);
52
+ if (touched) {
53
+ changed.set(node.id, { id: node.id, label: node.label, file, line: start, mark: change.added ? '+' : '~', test: isTestPath(file) });
54
+ }
55
+ });
56
+ }
57
+
58
+ const callees = new Map([...changed.keys()].map((id) => [id, new Set()]));
59
+ const hasCaller = new Set();
60
+ for (const link of linksOf(index, graph)) {
61
+ if (!CALL_RELATIONS.has(link.relation) || !changed.has(link.source)) continue;
62
+ const target = resolveEdgeTarget(index, link).id;
63
+ if (!target || target === link.source || !changed.has(target)) continue;
64
+ callees.get(link.source).add(target);
65
+ hasCaller.add(target);
66
+ }
67
+
68
+ const byPlace = (a, b) => a.file.localeCompare(b.file) || a.line - b.line;
69
+ const shown = new Set();
70
+ const grow = (id, path) => {
71
+ const node = changed.get(id);
72
+ if (path.has(id)) return { ...node, cycle: true, children: [] };
73
+ if (shown.has(id)) return { ...node, repeat: true, children: [] };
74
+ shown.add(id);
75
+ const next = new Set(path).add(id);
76
+ const children = [...callees.get(id)].map((c) => changed.get(c)).sort(byPlace).map((c) => grow(c.id, next));
77
+ return { ...node, children };
78
+ };
79
+ const entries = [...changed.values()].filter((n) => !hasCaller.has(n.id)).sort(byPlace);
80
+ const roots = entries.map((n) => grow(n.id, new Set()));
81
+ // Only cycles left: every node has a changed caller, so start from what was not reached.
82
+ for (const n of [...changed.values()].sort(byPlace)) if (!shown.has(n.id)) roots.push(grow(n.id, new Set()));
83
+
84
+ const mapped = new Set([...changed.values()].map((n) => n.file));
85
+ const values = [...changed.values()];
86
+ return {
87
+ roots,
88
+ counts: {
89
+ added: values.filter((n) => n.mark === '+').length,
90
+ modified: values.filter((n) => n.mark === '~').length,
91
+ entries: entries.length,
92
+ files: changes.size,
93
+ },
94
+ unmapped: [...changes.keys()].filter((f) => !mapped.has(f)).sort(),
95
+ };
96
+ }
97
+
98
+ /**
99
+ * Symbols whose graph line does not declare them in the committed file: the commit stamp can match HEAD
100
+ * while the extraction read a dirty tree, and then every line in the tree is off.
101
+ */
102
+ export function declarationDrift(tree, readLines) {
103
+ const drift = [];
104
+ const files = new Map();
105
+ const walk = (node) => {
106
+ if (!node.repeat && !node.cycle) {
107
+ if (!files.has(node.file)) files.set(node.file, readLines(node.file));
108
+ const name = String(node.label).replace(/^\./, '').split('(')[0].trim();
109
+ if (!(files.get(node.file)?.[node.line - 1] ?? '').includes(name)) drift.push({ file: node.file, line: node.line, label: node.label });
110
+ }
111
+ node.children.forEach(walk);
112
+ };
113
+ tree.roots.forEach(walk);
114
+ return drift;
115
+ }
116
+
117
+ export function renderDiffTree(tree) {
118
+ const lines = [];
119
+ const walk = (node, depth) => {
120
+ const tag = node.cycle ? ' ↺ ciclo' : node.repeat ? ' ↑ já mostrado' : '';
121
+ const test = node.test ? ' [teste]' : '';
122
+ lines.push(`${' '.repeat(depth)}${node.mark} ${node.label} ${node.file}:${node.line}${test}${tag}`);
123
+ for (const c of node.children) walk(c, depth + 1);
124
+ };
125
+ for (const r of tree.roots) walk(r, 0);
126
+ return lines;
127
+ }
@@ -1,11 +1,13 @@
1
1
  // O binário é `graphify` (e `graphify-mcp`). NUNCA `python -m graphify.serve`, como manda o README oficial:
2
2
  // com `uv tool install` o pacote vive num venv isolado que o Python do sistema não enxerga.
3
3
  import { execFileSync } from 'node:child_process';
4
+ import { GRAPHIFY_PACKAGE, resolveGraphify } from '../../../framework/hooks/shared/graph-refresh.js';
5
+
6
+ export { GRAPHIFY_PACKAGE };
4
7
 
5
8
  // Pin exato: o graphify é pré-1.0 com release quase diária e semver não garante nada; subir é decisão
6
9
  // consciente, com o fixture de regressão regerado.
7
10
  export const GRAPHIFY_VERSION = '0.9.50';
8
- export const GRAPHIFY_PACKAGE = 'graphifyy';
9
11
  export const GRAPHIFY_BIN = 'graphify';
10
12
 
11
13
  const PREFERENCE = ['uv', 'pipx', 'python3', 'python'];
@@ -56,10 +58,15 @@ export function installArgs(kind, { withSql }) {
56
58
 
57
59
  /** Versão instalada, ou null se o binário não responde. */
58
60
  export function detectGraphifyVersion(deps = {}) {
59
- const run = deps._run ?? (() => execFileSync(GRAPHIFY_BIN, ['--version'], {
60
- encoding: 'utf-8',
61
- stdio: ['ignore', 'pipe', 'ignore'],
62
- }));
61
+ const run = deps._run ?? (() => {
62
+ const bin = resolveGraphify();
63
+ if (!bin) return '';
64
+ return execFileSync(bin.file, ['--version'], {
65
+ encoding: 'utf-8',
66
+ stdio: ['ignore', 'pipe', 'ignore'],
67
+ shell: bin.shell,
68
+ });
69
+ });
63
70
  try {
64
71
  const m = /(\d+\.\d+\.\d+)/.exec(run());
65
72
  return m ? m[1] : null;
@@ -29,7 +29,7 @@ metadata:
29
29
  ## Key paths
30
30
 
31
31
  - \`.morph/framework/MORPH.md\` — NLH charter (read once per session)
32
- - \`.morph/framework/agents.json\` — 8 agents: 7 domain personas + 1 stage persona (\`scout\`, read-only)
32
+ - \`.morph/framework/agents.json\` — the 3 read-only Gate 3 agents (\`verifier\`, \`reviewer\`, \`security\`)
33
33
  - \`.morph/framework/standards/STANDARDS.json\` — standards index (load on demand)
34
34
  - \`.morph/features/{feature}/\` — feature artifacts: 2-plan / 1-design / 3-implement / 4-review
35
35
  - \`.morph/context/\` — long-lived architectural context (design-system, decisions)
@@ -39,7 +39,7 @@ metadata:
39
39
 
40
40
  | Command | Skill invoked |
41
41
  |---------|---------------|
42
- | \`/morph-plan {feature}\` | \`Skill(morph-plan)\` — entrevista → plan.md → plan-check → gate do plano |
42
+ | \`/morph-plan {feature}\` | \`Skill(morph-plan)\` — tese → plan.md → plan-check → gate do plano |
43
43
  | \`/morph-apply {feature}\` | \`Skill(morph-apply)\` — fatias do plano → verify → review → Gate 3 |
44
44
  | \`/morph-status [feature]\` | inspect state.json |
45
45
  | \`/morph-archive {feature}\` | move completed feature to .morph/archive/ |
@@ -72,7 +72,7 @@ metadata:
72
72
  - \`.morph/framework/MORPH.md\` — NLH charter, Trust Mode rules, gates, skills table
73
73
  - \`.morph/framework/standards/STANDARDS.json\` — index; load specific standard via \`path\` field
74
74
  - \`.morph/framework/phases.json\` — phase folder mapping and skill associations
75
- - \`.morph/framework/agents.json\` — domain persona definitions (dotnet-senior, ef-modeler, etc.)
75
+ - \`.morph/framework/agents.json\` — Gate 3 agent definitions (verifier, reviewer, security)
76
76
  - \`.morph/framework/evals/review-checklist.md\` — what the Gate 3 verifier, reviewer and security check (verdict SHIP/FIX + evidence-typed blockers)
77
77
  `;
78
78
 
@@ -0,0 +1,57 @@
1
+ import { readFileSync, readdirSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { parseStacks, shouldInstall } from '../stack-filter.js';
4
+ import { realEffects } from './effects.js';
5
+
6
+ export const MAF_TAG = 'maf';
7
+ export const MAF_RULE = 'ai-agents-standards.md';
8
+ const MAF_PATHS = ['**/*.cs'];
9
+
10
+ /** The text a rule is installed with: in a MAF project the agents rule covers every `.cs`, not only `Agents/` folders. */
11
+ export function ruleContentFor(file, content, projectTags) {
12
+ if (file !== MAF_RULE || !projectTags.includes(MAF_TAG)) return content;
13
+ const fm = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
14
+ if (!fm) return content;
15
+ const eol = content.includes('\r\n') ? '\r\n' : '\n';
16
+ const list = MAF_PATHS.map((p) => ` - "${p}"`).join(eol);
17
+ const rewritten = fm[1].replace(
18
+ /^paths:[ \t]*(?:\r?\n[ \t]+-[^\r\n]*)*(\r?\n)?/m,
19
+ (_, tail) => `paths:${eol}${list}${tail ? eol : ''}`,
20
+ );
21
+ const start = fm[0].indexOf(fm[1]);
22
+ return content.slice(0, start) + rewritten + content.slice(start + fm[1].length);
23
+ }
24
+
25
+ export function installRuleFile(rulesSrc, rulesDest, file, projectTags, fx = realEffects()) {
26
+ const from = join(rulesSrc, file);
27
+ const to = join(rulesDest, file);
28
+ const content = readFileSync(from, 'utf-8');
29
+ const desired = ruleContentFor(file, content, projectTags);
30
+ if (desired === content) fx.copyFile(from, to);
31
+ else fx.writeFile(to, desired);
32
+ }
33
+
34
+ /** Also removes orphan rules in rulesDest that are no longer in the install set. */
35
+ export function installRulesForStack(rulesSrc, rulesDest, projectTags, fx = realEffects()) {
36
+ const sourceFiles = readdirSync(rulesSrc).filter(f => f.endsWith('.md'));
37
+ const installed = new Set();
38
+
39
+ for (const file of sourceFiles) {
40
+ const content = readFileSync(join(rulesSrc, file), 'utf-8');
41
+ if (shouldInstall(parseStacks(content), projectTags)) {
42
+ installRuleFile(rulesSrc, rulesDest, file, projectTags, fx);
43
+ installed.add(file);
44
+ }
45
+ }
46
+
47
+ // Calado em `dryRun`: `cleanFrameworkDirs()` já apagou `.claude/rules/` inteiro antes da execução real.
48
+ if (!fx.dryRun) {
49
+ for (const file of readdirSync(rulesDest).filter(f => f.endsWith('.md'))) {
50
+ if (!installed.has(file)) {
51
+ fx.remove(join(rulesDest, file));
52
+ }
53
+ }
54
+ }
55
+
56
+ return { installed: installed.size };
57
+ }
@@ -0,0 +1,17 @@
1
+ import { join } from 'path';
2
+ import { installRulesForStack } from './rules-installer.js';
3
+ import { installCommandsForStack } from '../../scripts/setup-infra.js';
4
+ import { installSkills } from '../../utils/skills-installer.js';
5
+ import { installAgents } from '../../utils/agents-installer.js';
6
+
7
+ const FRAMEWORK_SRC = join(import.meta.dirname, '..', '..', '..', 'framework');
8
+
9
+ /** Everything filtered by `stacks:` — rules, commands, skills, agents — for the project's tags. The one path init and stack-set share. */
10
+ export async function installStackAssets(targetPath, stackTags, { frameworkSrc = FRAMEWORK_SRC } = {}) {
11
+ if (stackTags.length === 0) return false;
12
+ installRulesForStack(join(frameworkSrc, 'rules'), join(targetPath, '.claude', 'rules'), stackTags);
13
+ installCommandsForStack(join(frameworkSrc, 'commands'), join(targetPath, '.claude', 'commands'), stackTags);
14
+ await installSkills(targetPath, { projectTags: stackTags });
15
+ await installAgents(targetPath, frameworkSrc, { projectTags: stackTags });
16
+ return true;
17
+ }