@polymorphism-tech/morph-spec 7.9.3 → 8.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 (213) hide show
  1. package/CLAUDE.md +2 -2
  2. package/README.md +31 -2
  3. package/bin/morph-spec.js +50 -2
  4. package/bin/validate.js +379 -379
  5. package/claude-plugin.json +1 -1
  6. package/docs/CHEATSHEET.md +38 -1
  7. package/docs/QUICKSTART.md +60 -1
  8. package/framework/CLAUDE.md +13 -2
  9. package/framework/MORPH.md +38 -2
  10. package/framework/agents.json +22 -3
  11. package/framework/commands/commit.md +166 -166
  12. package/framework/commands/morph-hotfix.md +39 -0
  13. package/framework/commands/morph-infra.md +215 -215
  14. package/framework/commands/morph-preflight.md +205 -205
  15. package/framework/commands/morph-troubleshoot.md +124 -124
  16. package/framework/hooks/README.md +25 -10
  17. package/framework/hooks/claude-code/core/gate-guard.js +4 -2
  18. package/framework/hooks/claude-code/core/loop-detect.js +2 -1
  19. package/framework/hooks/claude-code/core/post-edit-typecheck.js +14 -5
  20. package/framework/hooks/claude-code/core/tasks-json-guard.js +4 -2
  21. package/framework/hooks/claude-code/core/validate-completion.js +1 -1
  22. package/framework/hooks/claude-code/core/worktree-context.js +160 -0
  23. package/framework/hooks/claude-code/statusline.py +679 -679
  24. package/framework/hooks/claude-code/statusline.sh +11 -11
  25. package/framework/hooks/git/commit-msg/conventional-commits.sh +33 -33
  26. package/framework/hooks/git/pre-commit/agents.sh +25 -25
  27. package/framework/hooks/git/pre-commit/orchestrator.sh +64 -64
  28. package/framework/hooks/git/pre-commit/specs.sh +50 -50
  29. package/framework/hooks/git/pre-push/run-tests.sh +44 -44
  30. package/framework/hooks/shared/activity-logger.js +160 -160
  31. package/framework/hooks/shared/build-check.js +2 -2
  32. package/framework/hooks/shared/hook-response.js +17 -5
  33. package/framework/hooks/shared/payload-utils.js +42 -0
  34. package/framework/hooks/shared/state-reader.js +22 -2
  35. package/framework/hooks/shared/stdin-reader.js +26 -26
  36. package/framework/rubrics/backend.json +41 -41
  37. package/framework/rules/ai-agents-standards.md +45 -45
  38. package/framework/rules/dotnet-templates.md +86 -86
  39. package/framework/rules/frontend-standards.md +13 -13
  40. package/framework/rules/infrastructure-standards.md +27 -27
  41. package/framework/rules/neon-standards.md +25 -25
  42. package/framework/rules/nextjs-standards.md +20 -20
  43. package/framework/rules/nextjs-templates.md +40 -40
  44. package/framework/schemas/feature.schema.json +95 -1
  45. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  46. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  47. package/framework/skills/core/morph-apply/SKILL.md +39 -8
  48. package/framework/skills/core/morph-brainstorming/SKILL.md +9 -1
  49. package/framework/skills/core/morph-eval/SKILL.md +6 -1
  50. package/framework/skills/core/morph-hotfix/SKILL.md +136 -0
  51. package/framework/skills/core/morph-implement/SKILL.md +8 -4
  52. package/framework/skills/core/morph-plan/SKILL.md +5 -1
  53. package/framework/skills/core/morph-proposal/SKILL.md +24 -1
  54. package/framework/skills/core/morph-review/SKILL.md +3 -2
  55. package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
  56. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  57. package/framework/standards/STANDARDS.json +7 -2
  58. package/framework/standards/ai-agents/agent-session.md +160 -160
  59. package/framework/standards/ai-agents/batching.md +193 -193
  60. package/framework/standards/ai-agents/context-providers.md +188 -188
  61. package/framework/standards/ai-agents/mcp-server.md +250 -250
  62. package/framework/standards/ai-agents/mcp-tools.md +197 -197
  63. package/framework/standards/ai-agents/middleware-patterns.md +207 -207
  64. package/framework/standards/ai-agents/modalities-image-gen.md +173 -173
  65. package/framework/standards/ai-agents/modalities-speech-to-text.md +171 -171
  66. package/framework/standards/ai-agents/modalities-text-to-speech.md +181 -181
  67. package/framework/standards/ai-agents/modalities-vision.md +201 -201
  68. package/framework/standards/ai-agents/multi-agent-patterns.md +155 -155
  69. package/framework/standards/ai-agents/production.md +275 -275
  70. package/framework/standards/ai-agents/prompts-in-db.md +214 -214
  71. package/framework/standards/ai-agents/providers/model-registry.md +158 -158
  72. package/framework/standards/ai-agents/service-tier-flex.md +135 -135
  73. package/framework/standards/ai-agents/structured-output.md +249 -249
  74. package/framework/standards/ai-agents/sweet-spot.md +109 -109
  75. package/framework/standards/backend/integrations/neon-auth/neon-auth.md +45 -3
  76. package/framework/templates/.idea/morph-templates.xml +92 -0
  77. package/framework/templates/.vscode/morph-templates.code-snippets +186 -0
  78. package/framework/templates/IDE-SNIPPETS.md +262 -262
  79. package/framework/templates/REGISTRY.json +0 -62
  80. package/framework/templates/code/dotnet/abstractions/api-endpoint.cs +7 -7
  81. package/framework/templates/code/dotnet/ai-agents/AgentBootstrap.cs.template +39 -39
  82. package/framework/templates/code/dotnet/ai-agents/EmbeddingService.cs.template +68 -68
  83. package/framework/templates/code/dotnet/ai-agents/ModelRegistry.cs.template +117 -117
  84. package/framework/templates/code/dotnet/ai-agents/PromptRepository.cs.template +95 -95
  85. package/framework/templates/code/dotnet/ai-agents/ResilientToolMiddleware.cs.template +61 -61
  86. package/framework/templates/code/dotnet/ai-agents/VectorSearchTool.cs.template +107 -107
  87. package/framework/templates/code/dotnet/ai-agents/model-registry.json.template +58 -58
  88. package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +281 -281
  89. package/framework/templates/code/dotnet/database/migration.cs +83 -83
  90. package/framework/templates/code/dotnet/feature/endpoint.cs.hbs +21 -21
  91. package/framework/templates/code/dotnet/feature/request.cs.hbs +4 -4
  92. package/framework/templates/code/dotnet/feature/response.cs.hbs +4 -4
  93. package/framework/templates/code/dotnet/feature/validator.cs.hbs +15 -15
  94. package/framework/templates/code/sql/ai-agents/agent-prompts-migration.sql.template +58 -58
  95. package/framework/templates/code/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -91
  96. package/framework/templates/code/typescript/contracts.ts +167 -167
  97. package/framework/templates/docs/clarifications.md +253 -253
  98. package/framework/templates/docs/schema-analysis.md +119 -119
  99. package/framework/templates/docs/ui-components.md +52 -52
  100. package/framework/templates/docs/ui-design-system.md +76 -76
  101. package/framework/templates/docs/ui-flows.md +167 -167
  102. package/framework/templates/docs/ui-mockups.md +98 -98
  103. package/framework/templates/feature/decisions.md +122 -122
  104. package/framework/templates/frontend/nextjs/Dockerfile.nextjs.hbs +43 -43
  105. package/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -26
  106. package/framework/templates/frontend/nextjs/env.mjs.hbs +32 -32
  107. package/framework/templates/frontend/nextjs/feature-form.tsx.hbs +56 -56
  108. package/framework/templates/frontend/nextjs/page.tsx.hbs +22 -22
  109. package/framework/templates/frontend/nextjs/tsconfig.json.hbs +26 -26
  110. package/framework/templates/frontend/nextjs/use-feature.ts.hbs +54 -54
  111. package/framework/templates/infrastructure/docker/Dockerfile.template +58 -58
  112. package/framework/templates/infrastructure/docker/docker-compose.template.yml +50 -50
  113. package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
  114. package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
  115. package/framework/templates/infrastructure/github/README.md +56 -56
  116. package/framework/templates/infrastructure/github/actions/docker-build-push/action.yml.hbs +45 -45
  117. package/framework/templates/infrastructure/github/actions/health-check/action.yml.hbs +27 -27
  118. package/framework/templates/infrastructure/github/workflows/docker-build-push.yml.hbs +59 -59
  119. package/framework/templates/infrastructure/github/workflows/dotnet-build.yml.hbs +39 -39
  120. package/framework/templates/integrations/asaas-client.cs +387 -387
  121. package/framework/templates/integrations/asaas-webhook.cs +351 -351
  122. package/framework/templates/meta-prompts/fusion/fusion-agent.md +76 -76
  123. package/framework/templates/meta-prompts/fusion/fusion-aggregator.md +100 -100
  124. package/framework/templates/meta-prompts/validators/pre-commit-validator.md +95 -95
  125. package/framework/templates/project/validate.js +120 -120
  126. package/framework/templates/state.template.json +17 -17
  127. package/framework/templates/ui/design-system.css +226 -226
  128. package/package.json +1 -1
  129. package/src/commands/dashboard/dashboard.js +53 -53
  130. package/src/commands/index.js +13 -13
  131. package/src/commands/mcp/mcp-setup.js +519 -519
  132. package/src/commands/project/checkpoint.js +209 -209
  133. package/src/commands/project/doctor.js +4 -2
  134. package/src/commands/project/init.js +46 -2
  135. package/src/commands/project/update.js +5 -3
  136. package/src/commands/project/worktree.js +1005 -46
  137. package/src/commands/state/advance.js +17 -2
  138. package/src/commands/state/approve.js +18 -1
  139. package/src/commands/state/create.js +158 -14
  140. package/src/commands/state/retype.js +126 -0
  141. package/src/commands/state/score.js +4 -0
  142. package/src/commands/tasks/dag.js +32 -0
  143. package/src/commands/validation/index.js +1 -0
  144. package/src/commands/validation/validate-feature.js +33 -5
  145. package/src/commands/validation/verify.js +190 -0
  146. package/src/core/state/phase-state-machine.js +197 -197
  147. package/src/core/state/state-manager.js +35 -10
  148. package/src/core/templates/template-renderer.js +477 -477
  149. package/src/core/work-types.js +79 -0
  150. package/src/dashboard/app/components/DetailPanel.jsx +11 -11
  151. package/src/dashboard/app/components/FlowGraph.jsx +17 -17
  152. package/src/dashboard/app/components/SearchFilter.jsx +22 -22
  153. package/src/dashboard/app/hooks/useDetailPanel.js +15 -15
  154. package/src/dashboard/app/main.jsx +13 -13
  155. package/src/dashboard/app/pages/AgentMap.jsx +370 -370
  156. package/src/dashboard/app/pages/HookInspector.jsx +418 -418
  157. package/src/dashboard/app/pages/LiveState.jsx +11 -1
  158. package/src/dashboard/app/pages/PhasePipeline.jsx +408 -408
  159. package/src/dashboard/app/pages/RulesMap.jsx +273 -273
  160. package/src/dashboard/app/pages/SkillsLibrary.jsx +366 -366
  161. package/src/dashboard/app/pages/Standards.jsx +332 -332
  162. package/src/dashboard/app/styles/index.css +47 -47
  163. package/src/dashboard/index.html +12 -12
  164. package/src/dashboard/package.json +24 -24
  165. package/src/dashboard/scanner/hooks-scanner.js +41 -41
  166. package/src/dashboard/scanner/phases-scanner.js +15 -15
  167. package/src/dashboard/scanner/rubrics-scanner.js +12 -12
  168. package/src/dashboard/scanner/rules-scanner.js +37 -37
  169. package/src/dashboard/scanner/skills-scanner.js +43 -43
  170. package/src/dashboard/scanner/squads-scanner.js +13 -13
  171. package/src/dashboard/scanner/standards-scanner.js +26 -26
  172. package/src/dashboard/scanner/state-scanner.js +42 -0
  173. package/src/dashboard/vite-plugin-morph-data.js +17 -17
  174. package/src/dashboard/vite.config.js +24 -24
  175. package/src/lib/agents/metrics-aggregator.js +56 -56
  176. package/src/lib/agents/metrics-calculator.js +125 -125
  177. package/src/lib/agents/output-collector.js +73 -73
  178. package/src/lib/detectors/claude-config-detector.js +152 -152
  179. package/src/lib/detectors/design-system-detector.js +231 -231
  180. package/src/lib/detectors/index.js +217 -217
  181. package/src/lib/generators/architecture-scanner.js +639 -639
  182. package/src/lib/generators/design-system-generator.js +508 -508
  183. package/src/lib/generators/index.js +13 -13
  184. package/src/lib/generators/settings-generator.js +209 -209
  185. package/src/lib/hooks/hook-executor.js +167 -167
  186. package/src/lib/installers/mcp-installer.js +719 -719
  187. package/src/lib/installers/memory-installer.js +1 -1
  188. package/src/lib/risk-detector.js +16 -0
  189. package/src/lib/tasks/dispatch-planner.js +122 -4
  190. package/src/lib/tasks/mermaid-renderer.js +132 -0
  191. package/src/lib/tasks/task-classifier.js +30 -30
  192. package/src/lib/tasks/task-parser.js +209 -209
  193. package/src/lib/tasks/test-runner.js +27 -6
  194. package/src/lib/validators/architecture/architecture-validator.js +259 -259
  195. package/src/lib/validators/checkpoint-persistence.js +39 -39
  196. package/src/lib/validators/contracts/contract-compliance-validator.js +274 -274
  197. package/src/lib/validators/css/css-validator.js +340 -340
  198. package/src/lib/validators/packages/package-validator.js +347 -347
  199. package/src/lib/validators/shared/emit-validator-dispatch.js +14 -14
  200. package/src/lib/validators/shared/issue-counter.js +14 -14
  201. package/src/lib/validators/validation-runner.js +349 -349
  202. package/src/lib/validators/verify-runner.js +322 -0
  203. package/src/lib/work-type-detector.js +143 -0
  204. package/src/scripts/setup-infra.js +32 -5
  205. package/src/utils/agent-id-mapper.js +11 -11
  206. package/src/utils/agents-installer.js +10 -6
  207. package/src/utils/claude-settings-manager.js +300 -300
  208. package/src/utils/file-copier.js +43 -0
  209. package/src/utils/hooks-installer.js +41 -11
  210. package/src/utils/skills-installer.js +174 -174
  211. package/framework/templates/meta-prompts/parallel-workers/parallel-coordinator.md +0 -113
  212. package/framework/templates/meta-prompts/parallel-workers/parallel-worker.md +0 -80
  213. package/src/lib/tasks/wave-resolver.js +0 -353
@@ -39,7 +39,7 @@ metadata:
39
39
  ## Key paths
40
40
 
41
41
  - \`.morph/framework/MORPH.md\` — NLH charter (read once per session)
42
- - \`.morph/framework/agents.json\` — 7 domain personas
42
+ - \`.morph/framework/agents.json\` — 8 agents: 7 domain personas + 1 stage persona (\`scout\`, read-only)
43
43
  - \`.morph/framework/standards/STANDARDS.json\` — standards index (load on demand)
44
44
  - \`.morph/framework/schemas/tasks.schema.json\` — JSON Schema for tasks.json
45
45
  - \`.morph/features/{feature}/\` — feature artifacts: 0-proposal / 1-design / 2-plan / 3-implement / 4-review
@@ -15,6 +15,13 @@
15
15
  const SCOPE_TASK_THRESHOLD = 20;
16
16
  const MIN_EVAL_SCORE = 9;
17
17
 
18
+ // Work types whose ship gate (review) always pauses for a human, regardless
19
+ // of trust mode — a type policy, not a per-feature risk signal. Mirrors
20
+ // `forcedPauseGates` in src/core/work-types.js (hotfix.review); kept here too
21
+ // because risk-detector is the deterministic layer consumed independent of
22
+ // the work-type config object.
23
+ const FORCED_PAUSE_WORK_TYPES = ['hotfix'];
24
+
18
25
  /**
19
26
  * @param {object} config - .morph/config/config.json contents (uses `trust`)
20
27
  * @param {object} signals
@@ -27,6 +34,13 @@ export function shouldPauseGate(config, signals) {
27
34
  return { pause: true, reason: 'trust mode is manual' };
28
35
  }
29
36
 
37
+ if (FORCED_PAUSE_WORK_TYPES.includes(signals.workType)) {
38
+ return {
39
+ pause: true,
40
+ reason: 'hotfix: aprovação humana obrigatória no ship (política de tipo — ignora trust:auto)',
41
+ };
42
+ }
43
+
30
44
  if (signals.designSystemDivergence) {
31
45
  return { pause: true, reason: 'design-system divergence detected' };
32
46
  }
@@ -61,6 +75,7 @@ export function shouldPauseGate(config, signals) {
61
75
  */
62
76
  export function getRiskSignals(signals) {
63
77
  const out = [];
78
+ if (FORCED_PAUSE_WORK_TYPES.includes(signals.workType)) out.push('hotfix (gate humano obrigatório)');
64
79
  if (signals.designSystemDivergence) out.push('design-system divergence');
65
80
  if (Array.isArray(signals.standardsViolations) && signals.standardsViolations.length > 0) {
66
81
  out.push(`standards violations: ${signals.standardsViolations.join(', ')}`);
@@ -77,4 +92,5 @@ export function getRiskSignals(signals) {
77
92
  export const RISK_CONSTANTS = {
78
93
  SCOPE_TASK_THRESHOLD,
79
94
  MIN_EVAL_SCORE,
95
+ FORCED_PAUSE_WORK_TYPES,
80
96
  };
@@ -39,6 +39,108 @@ function groupOf(task) {
39
39
  return task.group || 'backend';
40
40
  }
41
41
 
42
+ /**
43
+ * Normalize a planned output path for collision comparison: forward slashes,
44
+ * collapsed repeated slashes, no leading `./` or `/`, no trailing slash,
45
+ * lowercased (Windows/macOS filesystems are case-insensitive). Returns '' for
46
+ * empty/nullish input. `..`/`.` segments are left as-is on purpose (resolving
47
+ * them would risk false-positives; two genuinely different relative spellings
48
+ * stay distinct).
49
+ */
50
+ export function normalizeOutputPath(p) {
51
+ return String(p ?? '')
52
+ .trim()
53
+ .replace(/\\/g, '/')
54
+ .replace(/\/{2,}/g, '/')
55
+ .replace(/^\.\//, '')
56
+ .replace(/^\/+/, '')
57
+ .replace(/\/+$/, '')
58
+ .toLowerCase();
59
+ }
60
+
61
+ /**
62
+ * Two normalized paths "overlap" if they are equal, or one is a directory
63
+ * ancestor of the other — boundary-aware so `src/a` contains `src/a/b` but
64
+ * NOT `src/ab`. Empty paths never overlap.
65
+ */
66
+ export function pathsOverlap(a, b) {
67
+ if (!a || !b) return false;
68
+ if (a === b) return true;
69
+ const [shorter, longer] = a.length <= b.length ? [a, b] : [b, a];
70
+ return longer.startsWith(`${shorter}/`);
71
+ }
72
+
73
+ /**
74
+ * Decide whether a parallel batch's units would write to overlapping paths,
75
+ * and therefore whether each agent should run in its own git worktree
76
+ * (`isolation: "worktree"`, per morph-apply §3b-w) instead of the shared
77
+ * working tree.
78
+ *
79
+ * A *unit* is the set of task ids a single agent owns (one flow, or one batch
80
+ * root). Tasks WITHIN a unit run serially under that agent, so their outputs
81
+ * never race — only CROSS-unit overlaps matter. The recommendation is global
82
+ * per batch: any collision → isolate the whole batch. Units whose tasks
83
+ * declare no `outputs` are an *unknown footprint* — they do NOT force
84
+ * isolation (fail-open) but are flagged so the caller knows the dag can't
85
+ * prove the batch is collision-free.
86
+ *
87
+ * @param {Array<{ key: string, taskIds: string[] }>} units
88
+ * @param {Map<string, object>} tasksById
89
+ * @returns {{ recommended: 'none'|'worktree', collisions: Array<{path: string, units: string[], tasks: string[]}>, unknownFootprint: string[], reason: string }}
90
+ */
91
+ export function buildIsolation(units, tasksById) {
92
+ const unknownFootprint = [];
93
+ const unitOutputs = units.map(unit => {
94
+ const outs = [];
95
+ for (const id of unit.taskIds) {
96
+ const task = tasksById.get(id);
97
+ const declared = Array.isArray(task?.outputs) ? task.outputs : [];
98
+ let validForTask = 0;
99
+ for (const raw of declared) {
100
+ const path = normalizeOutputPath(raw);
101
+ if (path) { outs.push({ path, task: id }); validForTask += 1; }
102
+ }
103
+ // No usable output paths (none declared, or all normalize to '') → the
104
+ // dag can't prove this task's footprint. Flag it (fail-open).
105
+ if (validForTask === 0) unknownFootprint.push(id);
106
+ }
107
+ return { key: unit.key, outs };
108
+ });
109
+
110
+ const collisions = [];
111
+ for (let i = 0; i < unitOutputs.length; i++) {
112
+ for (let j = i + 1; j < unitOutputs.length; j++) {
113
+ for (const a of unitOutputs[i].outs) {
114
+ for (const b of unitOutputs[j].outs) {
115
+ if (pathsOverlap(a.path, b.path)) {
116
+ collisions.push({
117
+ path: a.path === b.path ? a.path : `${a.path} ∩ ${b.path}`,
118
+ units: [unitOutputs[i].key, unitOutputs[j].key],
119
+ tasks: [a.task, b.task],
120
+ });
121
+ }
122
+ }
123
+ }
124
+ }
125
+ }
126
+
127
+ const recommended = collisions.length > 0 ? 'worktree' : 'none';
128
+ let reason;
129
+ if (collisions.length > 0) {
130
+ reason =
131
+ `${collisions.length} colisão(ões) de outputs entre unidades paralelas — ` +
132
+ `isole cada agente num worktree (morph-apply §3b-w)`;
133
+ } else if (unknownFootprint.length > 0) {
134
+ reason =
135
+ `sem colisão detectada, mas ${unknownFootprint.length} task(s) paralela(s) sem ` +
136
+ `outputs declarados (footprint desconhecido) — o dag não garante ausência de colisão`;
137
+ } else {
138
+ reason = 'nenhuma colisão de outputs entre unidades paralelas — batch na working tree compartilhada';
139
+ }
140
+
141
+ return { recommended, collisions, unknownFootprint, reason };
142
+ }
143
+
42
144
  /**
43
145
  * Ids of tasks whose dependency chain crosses group boundaries — integration
44
146
  * tasks and everything downstream of them. These never belong to a flow: they
@@ -97,7 +199,8 @@ function buildStats(tasks, pending) {
97
199
  * flows: Array<{ group: string, taskIds: string[], weight: number }>,
98
200
  * batch: string[],
99
201
  * inline: string[],
100
- * sequentialRemainder: string[]
202
+ * sequentialRemainder: string[],
203
+ * isolation: { recommended: 'none'|'worktree', collisions: Array<{path: string, units: string[], tasks: string[]}>, unknownFootprint: string[], reason: string }
101
204
  * }}
102
205
  */
103
206
  export function planDispatch(data) {
@@ -105,12 +208,15 @@ export function planDispatch(data) {
105
208
  const pending = tasks.filter(t => t.status === 'pending');
106
209
  const stats = buildStats(tasks, pending);
107
210
  const empty = { flows: [], batch: [], inline: [] };
211
+ const tasksById = new Map(tasks.map(t => [t.id, t]));
212
+ // A batch with no parallel units can't have concurrent-write collisions.
213
+ const noIsolation = buildIsolation([], tasksById);
108
214
 
109
215
  if (pending.length === 0) {
110
216
  return {
111
217
  mode: 'sequential',
112
218
  reason: 'nenhuma task pendente — nada a despachar',
113
- stats, ...empty, sequentialRemainder: [],
219
+ stats, ...empty, sequentialRemainder: [], isolation: noIsolation,
114
220
  };
115
221
  }
116
222
 
@@ -119,7 +225,7 @@ export function planDispatch(data) {
119
225
  return {
120
226
  mode: 'sequential',
121
227
  reason: `${tasks.length} task(s) no total (<= 3) — overhead de Agent dispatch não compensa`,
122
- stats, ...empty, sequentialRemainder: pending.map(t => t.id),
228
+ stats, ...empty, sequentialRemainder: pending.map(t => t.id), isolation: noIsolation,
123
229
  };
124
230
  }
125
231
 
@@ -160,6 +266,12 @@ export function planDispatch(data) {
160
266
  (inline.length ? `; ${inline.length} task(s) leve(s) inline (sem dispatch próprio)` : '') +
161
267
  (sequentialRemainder.length ? `; integração cross-group sequencial no fim (${sequentialRemainder.join(', ')})` : ''),
162
268
  stats, flows, batch: [], inline, sequentialRemainder,
269
+ // Units = the dispatched flows (one agent each). Inline/overflow/cross-
270
+ // group remainder run in the sequential lane, never concurrently.
271
+ isolation: buildIsolation(
272
+ flows.map(f => ({ key: f.group, taskIds: f.taskIds })),
273
+ tasksById,
274
+ ),
163
275
  };
164
276
  }
165
277
 
@@ -184,6 +296,12 @@ export function planDispatch(data) {
184
296
  (inline.length ? `; raízes S inline — não ganham dispatch próprio (${inline.join(', ')})` : '') +
185
297
  `; restante sequencial`,
186
298
  stats, flows: [], batch, inline, sequentialRemainder,
299
+ // Units = each batch root (one agent each); inline S-roots and the
300
+ // sequential remainder don't run concurrently with the batch.
301
+ isolation: buildIsolation(
302
+ batch.map(id => ({ key: id, taskIds: [id] })),
303
+ tasksById,
304
+ ),
187
305
  };
188
306
  }
189
307
 
@@ -196,6 +314,6 @@ export function planDispatch(data) {
196
314
  return {
197
315
  mode: 'sequential',
198
316
  reason,
199
- stats, ...empty, sequentialRemainder: pending.map(t => t.id),
317
+ stats, ...empty, sequentialRemainder: pending.map(t => t.id), isolation: noIsolation,
200
318
  };
201
319
  }
@@ -0,0 +1,132 @@
1
+ /**
2
+ * mermaid-renderer.js — pure renderer: tasks.json DAG -> Mermaid flowchart TD.
3
+ *
4
+ * Backs `morph-spec dag <feature> --mermaid` (S8-T1), so the plan's DAG can be
5
+ * visualized at Gate 2 (IndyDevDan rec. 2: draw the workflow in Mermaid before
6
+ * automating it).
7
+ *
8
+ * STRICT Mermaid rules enforced here (see project CLAUDE.md — violating these
9
+ * breaks rendering in Obsidian):
10
+ * - every node label wrapped in double quotes, no exceptions
11
+ * - labels never start with a numeric-with-dot prefix ("1. ", "2. ...") —
12
+ * Obsidian's markdown-list parser chokes on it even inside quotes
13
+ * - `&` inside labels -> `e`
14
+ * - line breaks inside labels -> `<br/>`, never a literal `\n`
15
+ * - double quotes inside titles are stripped so they can't break out of the
16
+ * surrounding label quotes
17
+ *
18
+ * Pure module: no fs, no Date.now()/Math.random(). Tasks are sorted by
19
+ * numeric id so output is stable regardless of input order.
20
+ */
21
+
22
+ const MAX_TITLE_LENGTH = 40;
23
+
24
+ const VALID_STATUS_CLASSES = ['done', 'in_progress', 'blocked', 'skipped', 'pending'];
25
+
26
+ // Web-safe fill/stroke pairs, one per task status.
27
+ const CLASS_DEFS = [
28
+ 'classDef done fill:#c6f6d5,stroke:#2f855a,color:#22543d',
29
+ 'classDef in_progress fill:#bee3f8,stroke:#2b6cb0,color:#2a4365',
30
+ 'classDef blocked fill:#feb2b2,stroke:#c53030,color:#63171b',
31
+ 'classDef skipped fill:#e2e8f0,stroke:#718096,color:#2d3748',
32
+ 'classDef pending fill:#edf2f7,stroke:#a0aec0,color:#2d3748',
33
+ ];
34
+
35
+ function numericId(id) {
36
+ const match = /^T(\d+)$/.exec(id ?? '');
37
+ return match ? parseInt(match[1], 10) : Number.POSITIVE_INFINITY;
38
+ }
39
+
40
+ /**
41
+ * Sanitize arbitrary text for embedding inside a double-quoted Mermaid node
42
+ * label, per the project's strict Mermaid rules.
43
+ *
44
+ * - double quotes stripped (they would close the label's own quotes)
45
+ * - `&` -> `e`
46
+ * - truncated to MAX_TITLE_LENGTH chars + `…`
47
+ * - line breaks -> `<br/>`
48
+ *
49
+ * Ordering matters: truncation runs on the raw text BEFORE newlines expand to
50
+ * `<br/>`. Newline->`<br/>` is the only length-changing substitution (1 char ->
51
+ * 5); truncating after it could slice a tag mid-way ("...<b…"), so we truncate
52
+ * first — every emitted `<br/>` is then guaranteed whole. The char-for-char
53
+ * substitutions (`"`->`'`, `&`->`e`) don't move the truncation boundary.
54
+ */
55
+ export function sanitizeLabel(text) {
56
+ let out = String(text ?? '');
57
+ out = out.replace(/"/g, "'");
58
+ out = out.replace(/&/g, 'e');
59
+ if (out.length > MAX_TITLE_LENGTH) {
60
+ out = `${out.slice(0, MAX_TITLE_LENGTH)}…`;
61
+ }
62
+ out = out.replace(/\r\n|\r|\n/g, '<br/>');
63
+ return out;
64
+ }
65
+
66
+ /**
67
+ * Same fallback as groupOf() in dispatch-planner.js / groupTasks() in
68
+ * tasks-json-parser.js — tasks without a `group` field are treated as
69
+ * 'backend'.
70
+ */
71
+ function groupOf(task) {
72
+ return task.group || 'backend';
73
+ }
74
+
75
+ function statusClass(status) {
76
+ return VALID_STATUS_CLASSES.includes(status) ? status : 'pending';
77
+ }
78
+
79
+ /**
80
+ * Render a parsed tasks.json document as a Mermaid `flowchart TD`.
81
+ *
82
+ * @param {{ tasks?: Array<{ id: string, title: string, dependencies?: string[], effort?: string, group?: string, status?: string }> }} tasksData
83
+ * @returns {string}
84
+ */
85
+ export function renderMermaidFlowchart(tasksData) {
86
+ const tasks = Array.isArray(tasksData?.tasks) ? tasksData.tasks : [];
87
+
88
+ if (tasks.length === 0) {
89
+ return ['flowchart TD', ' empty["sem tasks"]'].join('\n');
90
+ }
91
+
92
+ const sorted = [...tasks].sort((a, b) => numericId(a.id) - numericId(b.id));
93
+ const knownIds = new Set(sorted.map(t => t.id));
94
+
95
+ // Group tasks by `group`, preserving the deterministic sorted-by-id order
96
+ // in which each group is first encountered.
97
+ const groups = new Map();
98
+ for (const task of sorted) {
99
+ const g = groupOf(task);
100
+ if (!groups.has(g)) groups.set(g, []);
101
+ groups.get(g).push(task);
102
+ }
103
+
104
+ const lines = ['flowchart TD'];
105
+
106
+ for (const [group, groupTasks] of groups) {
107
+ lines.push(` subgraph ${group}["${sanitizeLabel(group)}"]`);
108
+ for (const task of groupTasks) {
109
+ const label = `${task.id} · ${sanitizeLabel(task.title)} (${task.effort})`;
110
+ lines.push(` ${task.id}["${label}"]`);
111
+ }
112
+ lines.push(' end');
113
+ }
114
+
115
+ // Edges — one per dependency. Dependencies pointing at a non-existent id
116
+ // are silently skipped (never throw on a dangling reference).
117
+ for (const task of sorted) {
118
+ const deps = Array.isArray(task.dependencies) ? task.dependencies : [];
119
+ for (const dep of deps) {
120
+ if (!knownIds.has(dep)) continue;
121
+ lines.push(` ${dep} --> ${task.id}`);
122
+ }
123
+ }
124
+
125
+ lines.push(...CLASS_DEFS);
126
+
127
+ for (const task of sorted) {
128
+ lines.push(` class ${task.id} ${statusClass(task.status)}`);
129
+ }
130
+
131
+ return lines.join('\n');
132
+ }
@@ -1,30 +1,30 @@
1
- /**
2
- * Task classifier — identifies test/validation and infrastructure tasks by title.
3
- */
4
-
5
- const TEST_TASK_PATTERN = /\b(tests?|testing|specs?|coverage|e2e|unit|integration|integra[cç][aã]o|valida[cç][aã]o|cobertura|testes?)\b/i;
6
-
7
- /**
8
- * Returns true if the task title indicates a testing or validation task.
9
- *
10
- * @param {string} taskTitle
11
- * @returns {boolean}
12
- */
13
- export function isTestTask(taskTitle) {
14
- if (!taskTitle) return false;
15
- return TEST_TASK_PATTERN.test(taskTitle);
16
- }
17
-
18
- const INFRA_TASK_PATTERN = /\b(infrastructure|infra|cleanup|migration|migra[cç][aã]o|config|setup|ci\/cd|devops|deploy|docker|packages?|dependencies|tags)\b/i;
19
-
20
- /**
21
- * Returns true if the task title indicates an infrastructure/cleanup/migration task.
22
- * Used to skip contract validation and suppress breaking change warnings.
23
- *
24
- * @param {string} taskTitle
25
- * @returns {boolean}
26
- */
27
- export function isInfrastructureTask(taskTitle) {
28
- if (!taskTitle) return false;
29
- return INFRA_TASK_PATTERN.test(taskTitle);
30
- }
1
+ /**
2
+ * Task classifier — identifies test/validation and infrastructure tasks by title.
3
+ */
4
+
5
+ const TEST_TASK_PATTERN = /\b(tests?|testing|specs?|coverage|e2e|unit|integration|integra[cç][aã]o|valida[cç][aã]o|cobertura|testes?)\b/i;
6
+
7
+ /**
8
+ * Returns true if the task title indicates a testing or validation task.
9
+ *
10
+ * @param {string} taskTitle
11
+ * @returns {boolean}
12
+ */
13
+ export function isTestTask(taskTitle) {
14
+ if (!taskTitle) return false;
15
+ return TEST_TASK_PATTERN.test(taskTitle);
16
+ }
17
+
18
+ const INFRA_TASK_PATTERN = /\b(infrastructure|infra|cleanup|migration|migra[cç][aã]o|config|setup|ci\/cd|devops|deploy|docker|packages?|dependencies|tags)\b/i;
19
+
20
+ /**
21
+ * Returns true if the task title indicates an infrastructure/cleanup/migration task.
22
+ * Used to skip contract validation and suppress breaking change warnings.
23
+ *
24
+ * @param {string} taskTitle
25
+ * @returns {boolean}
26
+ */
27
+ export function isInfrastructureTask(taskTitle) {
28
+ if (!taskTitle) return false;
29
+ return INFRA_TASK_PATTERN.test(taskTitle);
30
+ }