@polymorphism-tech/morph-spec 7.9.4 → 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 (211) 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/ai-agents/agent-session.md +160 -160
  58. package/framework/standards/ai-agents/batching.md +193 -193
  59. package/framework/standards/ai-agents/context-providers.md +188 -188
  60. package/framework/standards/ai-agents/mcp-server.md +250 -250
  61. package/framework/standards/ai-agents/mcp-tools.md +197 -197
  62. package/framework/standards/ai-agents/middleware-patterns.md +207 -207
  63. package/framework/standards/ai-agents/modalities-image-gen.md +173 -173
  64. package/framework/standards/ai-agents/modalities-speech-to-text.md +171 -171
  65. package/framework/standards/ai-agents/modalities-text-to-speech.md +181 -181
  66. package/framework/standards/ai-agents/modalities-vision.md +201 -201
  67. package/framework/standards/ai-agents/multi-agent-patterns.md +155 -155
  68. package/framework/standards/ai-agents/production.md +275 -275
  69. package/framework/standards/ai-agents/prompts-in-db.md +214 -214
  70. package/framework/standards/ai-agents/providers/model-registry.md +158 -158
  71. package/framework/standards/ai-agents/service-tier-flex.md +135 -135
  72. package/framework/standards/ai-agents/structured-output.md +249 -249
  73. package/framework/standards/ai-agents/sweet-spot.md +109 -109
  74. package/framework/templates/.idea/morph-templates.xml +92 -0
  75. package/framework/templates/.vscode/morph-templates.code-snippets +186 -0
  76. package/framework/templates/IDE-SNIPPETS.md +262 -262
  77. package/framework/templates/REGISTRY.json +0 -62
  78. package/framework/templates/code/dotnet/abstractions/api-endpoint.cs +7 -7
  79. package/framework/templates/code/dotnet/ai-agents/AgentBootstrap.cs.template +39 -39
  80. package/framework/templates/code/dotnet/ai-agents/EmbeddingService.cs.template +68 -68
  81. package/framework/templates/code/dotnet/ai-agents/ModelRegistry.cs.template +117 -117
  82. package/framework/templates/code/dotnet/ai-agents/PromptRepository.cs.template +95 -95
  83. package/framework/templates/code/dotnet/ai-agents/ResilientToolMiddleware.cs.template +61 -61
  84. package/framework/templates/code/dotnet/ai-agents/VectorSearchTool.cs.template +107 -107
  85. package/framework/templates/code/dotnet/ai-agents/model-registry.json.template +58 -58
  86. package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +281 -281
  87. package/framework/templates/code/dotnet/database/migration.cs +83 -83
  88. package/framework/templates/code/dotnet/feature/endpoint.cs.hbs +21 -21
  89. package/framework/templates/code/dotnet/feature/request.cs.hbs +4 -4
  90. package/framework/templates/code/dotnet/feature/response.cs.hbs +4 -4
  91. package/framework/templates/code/dotnet/feature/validator.cs.hbs +15 -15
  92. package/framework/templates/code/sql/ai-agents/agent-prompts-migration.sql.template +58 -58
  93. package/framework/templates/code/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -91
  94. package/framework/templates/code/typescript/contracts.ts +167 -167
  95. package/framework/templates/docs/clarifications.md +253 -253
  96. package/framework/templates/docs/schema-analysis.md +119 -119
  97. package/framework/templates/docs/ui-components.md +52 -52
  98. package/framework/templates/docs/ui-design-system.md +76 -76
  99. package/framework/templates/docs/ui-flows.md +167 -167
  100. package/framework/templates/docs/ui-mockups.md +98 -98
  101. package/framework/templates/feature/decisions.md +122 -122
  102. package/framework/templates/frontend/nextjs/Dockerfile.nextjs.hbs +43 -43
  103. package/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -26
  104. package/framework/templates/frontend/nextjs/env.mjs.hbs +32 -32
  105. package/framework/templates/frontend/nextjs/feature-form.tsx.hbs +56 -56
  106. package/framework/templates/frontend/nextjs/page.tsx.hbs +22 -22
  107. package/framework/templates/frontend/nextjs/tsconfig.json.hbs +26 -26
  108. package/framework/templates/frontend/nextjs/use-feature.ts.hbs +54 -54
  109. package/framework/templates/infrastructure/docker/Dockerfile.template +58 -58
  110. package/framework/templates/infrastructure/docker/docker-compose.template.yml +50 -50
  111. package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
  112. package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
  113. package/framework/templates/infrastructure/github/README.md +56 -56
  114. package/framework/templates/infrastructure/github/actions/docker-build-push/action.yml.hbs +45 -45
  115. package/framework/templates/infrastructure/github/actions/health-check/action.yml.hbs +27 -27
  116. package/framework/templates/infrastructure/github/workflows/docker-build-push.yml.hbs +59 -59
  117. package/framework/templates/infrastructure/github/workflows/dotnet-build.yml.hbs +39 -39
  118. package/framework/templates/integrations/asaas-client.cs +387 -387
  119. package/framework/templates/integrations/asaas-webhook.cs +351 -351
  120. package/framework/templates/meta-prompts/fusion/fusion-agent.md +76 -76
  121. package/framework/templates/meta-prompts/fusion/fusion-aggregator.md +100 -100
  122. package/framework/templates/meta-prompts/validators/pre-commit-validator.md +95 -95
  123. package/framework/templates/project/validate.js +120 -120
  124. package/framework/templates/state.template.json +17 -17
  125. package/framework/templates/ui/design-system.css +226 -226
  126. package/package.json +1 -1
  127. package/src/commands/dashboard/dashboard.js +53 -53
  128. package/src/commands/index.js +13 -13
  129. package/src/commands/mcp/mcp-setup.js +519 -519
  130. package/src/commands/project/checkpoint.js +209 -209
  131. package/src/commands/project/doctor.js +4 -2
  132. package/src/commands/project/init.js +46 -2
  133. package/src/commands/project/update.js +5 -3
  134. package/src/commands/project/worktree.js +1005 -46
  135. package/src/commands/state/advance.js +17 -2
  136. package/src/commands/state/approve.js +18 -1
  137. package/src/commands/state/create.js +158 -14
  138. package/src/commands/state/retype.js +126 -0
  139. package/src/commands/state/score.js +4 -0
  140. package/src/commands/tasks/dag.js +32 -0
  141. package/src/commands/validation/index.js +1 -0
  142. package/src/commands/validation/validate-feature.js +33 -5
  143. package/src/commands/validation/verify.js +190 -0
  144. package/src/core/state/phase-state-machine.js +197 -197
  145. package/src/core/state/state-manager.js +35 -10
  146. package/src/core/templates/template-renderer.js +477 -477
  147. package/src/core/work-types.js +79 -0
  148. package/src/dashboard/app/components/DetailPanel.jsx +11 -11
  149. package/src/dashboard/app/components/FlowGraph.jsx +17 -17
  150. package/src/dashboard/app/components/SearchFilter.jsx +22 -22
  151. package/src/dashboard/app/hooks/useDetailPanel.js +15 -15
  152. package/src/dashboard/app/main.jsx +13 -13
  153. package/src/dashboard/app/pages/AgentMap.jsx +370 -370
  154. package/src/dashboard/app/pages/HookInspector.jsx +418 -418
  155. package/src/dashboard/app/pages/LiveState.jsx +11 -1
  156. package/src/dashboard/app/pages/PhasePipeline.jsx +408 -408
  157. package/src/dashboard/app/pages/RulesMap.jsx +273 -273
  158. package/src/dashboard/app/pages/SkillsLibrary.jsx +366 -366
  159. package/src/dashboard/app/pages/Standards.jsx +332 -332
  160. package/src/dashboard/app/styles/index.css +47 -47
  161. package/src/dashboard/index.html +12 -12
  162. package/src/dashboard/package.json +24 -24
  163. package/src/dashboard/scanner/hooks-scanner.js +41 -41
  164. package/src/dashboard/scanner/phases-scanner.js +15 -15
  165. package/src/dashboard/scanner/rubrics-scanner.js +12 -12
  166. package/src/dashboard/scanner/rules-scanner.js +37 -37
  167. package/src/dashboard/scanner/skills-scanner.js +43 -43
  168. package/src/dashboard/scanner/squads-scanner.js +13 -13
  169. package/src/dashboard/scanner/standards-scanner.js +26 -26
  170. package/src/dashboard/scanner/state-scanner.js +42 -0
  171. package/src/dashboard/vite-plugin-morph-data.js +17 -17
  172. package/src/dashboard/vite.config.js +24 -24
  173. package/src/lib/agents/metrics-aggregator.js +56 -56
  174. package/src/lib/agents/metrics-calculator.js +125 -125
  175. package/src/lib/agents/output-collector.js +73 -73
  176. package/src/lib/detectors/claude-config-detector.js +152 -152
  177. package/src/lib/detectors/design-system-detector.js +231 -231
  178. package/src/lib/detectors/index.js +217 -217
  179. package/src/lib/generators/architecture-scanner.js +639 -639
  180. package/src/lib/generators/design-system-generator.js +508 -508
  181. package/src/lib/generators/index.js +13 -13
  182. package/src/lib/generators/settings-generator.js +209 -209
  183. package/src/lib/hooks/hook-executor.js +167 -167
  184. package/src/lib/installers/mcp-installer.js +719 -719
  185. package/src/lib/installers/memory-installer.js +1 -1
  186. package/src/lib/risk-detector.js +16 -0
  187. package/src/lib/tasks/dispatch-planner.js +122 -4
  188. package/src/lib/tasks/mermaid-renderer.js +132 -0
  189. package/src/lib/tasks/task-classifier.js +30 -30
  190. package/src/lib/tasks/task-parser.js +209 -209
  191. package/src/lib/tasks/test-runner.js +27 -6
  192. package/src/lib/validators/architecture/architecture-validator.js +259 -259
  193. package/src/lib/validators/checkpoint-persistence.js +39 -39
  194. package/src/lib/validators/contracts/contract-compliance-validator.js +274 -274
  195. package/src/lib/validators/css/css-validator.js +340 -340
  196. package/src/lib/validators/packages/package-validator.js +347 -347
  197. package/src/lib/validators/shared/emit-validator-dispatch.js +14 -14
  198. package/src/lib/validators/shared/issue-counter.js +14 -14
  199. package/src/lib/validators/validation-runner.js +349 -349
  200. package/src/lib/validators/verify-runner.js +322 -0
  201. package/src/lib/work-type-detector.js +143 -0
  202. package/src/scripts/setup-infra.js +32 -5
  203. package/src/utils/agent-id-mapper.js +11 -11
  204. package/src/utils/agents-installer.js +10 -6
  205. package/src/utils/claude-settings-manager.js +300 -300
  206. package/src/utils/file-copier.js +43 -0
  207. package/src/utils/hooks-installer.js +41 -11
  208. package/src/utils/skills-installer.js +174 -174
  209. package/framework/templates/meta-prompts/parallel-workers/parallel-coordinator.md +0 -113
  210. package/framework/templates/meta-prompts/parallel-workers/parallel-worker.md +0 -80
  211. 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
+ }