@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.
- package/CLAUDE.md +2 -2
- package/README.md +31 -2
- package/bin/morph-spec.js +50 -2
- package/bin/validate.js +379 -379
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +38 -1
- package/docs/QUICKSTART.md +60 -1
- package/framework/CLAUDE.md +13 -2
- package/framework/MORPH.md +38 -2
- package/framework/agents.json +22 -3
- package/framework/commands/commit.md +166 -166
- package/framework/commands/morph-hotfix.md +39 -0
- package/framework/commands/morph-infra.md +215 -215
- package/framework/commands/morph-preflight.md +205 -205
- package/framework/commands/morph-troubleshoot.md +124 -124
- package/framework/hooks/README.md +25 -10
- package/framework/hooks/claude-code/core/gate-guard.js +4 -2
- package/framework/hooks/claude-code/core/loop-detect.js +2 -1
- package/framework/hooks/claude-code/core/post-edit-typecheck.js +14 -5
- package/framework/hooks/claude-code/core/tasks-json-guard.js +4 -2
- package/framework/hooks/claude-code/core/validate-completion.js +1 -1
- package/framework/hooks/claude-code/core/worktree-context.js +160 -0
- package/framework/hooks/claude-code/statusline.py +679 -679
- package/framework/hooks/claude-code/statusline.sh +11 -11
- package/framework/hooks/git/commit-msg/conventional-commits.sh +33 -33
- package/framework/hooks/git/pre-commit/agents.sh +25 -25
- package/framework/hooks/git/pre-commit/orchestrator.sh +64 -64
- package/framework/hooks/git/pre-commit/specs.sh +50 -50
- package/framework/hooks/git/pre-push/run-tests.sh +44 -44
- package/framework/hooks/shared/activity-logger.js +160 -160
- package/framework/hooks/shared/build-check.js +2 -2
- package/framework/hooks/shared/hook-response.js +17 -5
- package/framework/hooks/shared/payload-utils.js +42 -0
- package/framework/hooks/shared/state-reader.js +22 -2
- package/framework/hooks/shared/stdin-reader.js +26 -26
- package/framework/rubrics/backend.json +41 -41
- package/framework/rules/ai-agents-standards.md +45 -45
- package/framework/rules/dotnet-templates.md +86 -86
- package/framework/rules/frontend-standards.md +13 -13
- package/framework/rules/infrastructure-standards.md +27 -27
- package/framework/rules/neon-standards.md +25 -25
- package/framework/rules/nextjs-standards.md +20 -20
- package/framework/rules/nextjs-templates.md +40 -40
- package/framework/schemas/feature.schema.json +95 -1
- package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
- package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
- package/framework/skills/core/morph-apply/SKILL.md +39 -8
- package/framework/skills/core/morph-brainstorming/SKILL.md +9 -1
- package/framework/skills/core/morph-eval/SKILL.md +6 -1
- package/framework/skills/core/morph-hotfix/SKILL.md +136 -0
- package/framework/skills/core/morph-implement/SKILL.md +8 -4
- package/framework/skills/core/morph-plan/SKILL.md +5 -1
- package/framework/skills/core/morph-proposal/SKILL.md +24 -1
- package/framework/skills/core/morph-review/SKILL.md +3 -2
- package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/framework/standards/ai-agents/agent-session.md +160 -160
- package/framework/standards/ai-agents/batching.md +193 -193
- package/framework/standards/ai-agents/context-providers.md +188 -188
- package/framework/standards/ai-agents/mcp-server.md +250 -250
- package/framework/standards/ai-agents/mcp-tools.md +197 -197
- package/framework/standards/ai-agents/middleware-patterns.md +207 -207
- package/framework/standards/ai-agents/modalities-image-gen.md +173 -173
- package/framework/standards/ai-agents/modalities-speech-to-text.md +171 -171
- package/framework/standards/ai-agents/modalities-text-to-speech.md +181 -181
- package/framework/standards/ai-agents/modalities-vision.md +201 -201
- package/framework/standards/ai-agents/multi-agent-patterns.md +155 -155
- package/framework/standards/ai-agents/production.md +275 -275
- package/framework/standards/ai-agents/prompts-in-db.md +214 -214
- package/framework/standards/ai-agents/providers/model-registry.md +158 -158
- package/framework/standards/ai-agents/service-tier-flex.md +135 -135
- package/framework/standards/ai-agents/structured-output.md +249 -249
- package/framework/standards/ai-agents/sweet-spot.md +109 -109
- package/framework/templates/.idea/morph-templates.xml +92 -0
- package/framework/templates/.vscode/morph-templates.code-snippets +186 -0
- package/framework/templates/IDE-SNIPPETS.md +262 -262
- package/framework/templates/REGISTRY.json +0 -62
- package/framework/templates/code/dotnet/abstractions/api-endpoint.cs +7 -7
- package/framework/templates/code/dotnet/ai-agents/AgentBootstrap.cs.template +39 -39
- package/framework/templates/code/dotnet/ai-agents/EmbeddingService.cs.template +68 -68
- package/framework/templates/code/dotnet/ai-agents/ModelRegistry.cs.template +117 -117
- package/framework/templates/code/dotnet/ai-agents/PromptRepository.cs.template +95 -95
- package/framework/templates/code/dotnet/ai-agents/ResilientToolMiddleware.cs.template +61 -61
- package/framework/templates/code/dotnet/ai-agents/VectorSearchTool.cs.template +107 -107
- package/framework/templates/code/dotnet/ai-agents/model-registry.json.template +58 -58
- package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +281 -281
- package/framework/templates/code/dotnet/database/migration.cs +83 -83
- package/framework/templates/code/dotnet/feature/endpoint.cs.hbs +21 -21
- package/framework/templates/code/dotnet/feature/request.cs.hbs +4 -4
- package/framework/templates/code/dotnet/feature/response.cs.hbs +4 -4
- package/framework/templates/code/dotnet/feature/validator.cs.hbs +15 -15
- package/framework/templates/code/sql/ai-agents/agent-prompts-migration.sql.template +58 -58
- package/framework/templates/code/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -91
- package/framework/templates/code/typescript/contracts.ts +167 -167
- package/framework/templates/docs/clarifications.md +253 -253
- package/framework/templates/docs/schema-analysis.md +119 -119
- package/framework/templates/docs/ui-components.md +52 -52
- package/framework/templates/docs/ui-design-system.md +76 -76
- package/framework/templates/docs/ui-flows.md +167 -167
- package/framework/templates/docs/ui-mockups.md +98 -98
- package/framework/templates/feature/decisions.md +122 -122
- package/framework/templates/frontend/nextjs/Dockerfile.nextjs.hbs +43 -43
- package/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -26
- package/framework/templates/frontend/nextjs/env.mjs.hbs +32 -32
- package/framework/templates/frontend/nextjs/feature-form.tsx.hbs +56 -56
- package/framework/templates/frontend/nextjs/page.tsx.hbs +22 -22
- package/framework/templates/frontend/nextjs/tsconfig.json.hbs +26 -26
- package/framework/templates/frontend/nextjs/use-feature.ts.hbs +54 -54
- package/framework/templates/infrastructure/docker/Dockerfile.template +58 -58
- package/framework/templates/infrastructure/docker/docker-compose.template.yml +50 -50
- package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
- package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
- package/framework/templates/infrastructure/github/README.md +56 -56
- package/framework/templates/infrastructure/github/actions/docker-build-push/action.yml.hbs +45 -45
- package/framework/templates/infrastructure/github/actions/health-check/action.yml.hbs +27 -27
- package/framework/templates/infrastructure/github/workflows/docker-build-push.yml.hbs +59 -59
- package/framework/templates/infrastructure/github/workflows/dotnet-build.yml.hbs +39 -39
- package/framework/templates/integrations/asaas-client.cs +387 -387
- package/framework/templates/integrations/asaas-webhook.cs +351 -351
- package/framework/templates/meta-prompts/fusion/fusion-agent.md +76 -76
- package/framework/templates/meta-prompts/fusion/fusion-aggregator.md +100 -100
- package/framework/templates/meta-prompts/validators/pre-commit-validator.md +95 -95
- package/framework/templates/project/validate.js +120 -120
- package/framework/templates/state.template.json +17 -17
- package/framework/templates/ui/design-system.css +226 -226
- package/package.json +1 -1
- package/src/commands/dashboard/dashboard.js +53 -53
- package/src/commands/index.js +13 -13
- package/src/commands/mcp/mcp-setup.js +519 -519
- package/src/commands/project/checkpoint.js +209 -209
- package/src/commands/project/doctor.js +4 -2
- package/src/commands/project/init.js +46 -2
- package/src/commands/project/update.js +5 -3
- package/src/commands/project/worktree.js +1005 -46
- package/src/commands/state/advance.js +17 -2
- package/src/commands/state/approve.js +18 -1
- package/src/commands/state/create.js +158 -14
- package/src/commands/state/retype.js +126 -0
- package/src/commands/state/score.js +4 -0
- package/src/commands/tasks/dag.js +32 -0
- package/src/commands/validation/index.js +1 -0
- package/src/commands/validation/validate-feature.js +33 -5
- package/src/commands/validation/verify.js +190 -0
- package/src/core/state/phase-state-machine.js +197 -197
- package/src/core/state/state-manager.js +35 -10
- package/src/core/templates/template-renderer.js +477 -477
- package/src/core/work-types.js +79 -0
- package/src/dashboard/app/components/DetailPanel.jsx +11 -11
- package/src/dashboard/app/components/FlowGraph.jsx +17 -17
- package/src/dashboard/app/components/SearchFilter.jsx +22 -22
- package/src/dashboard/app/hooks/useDetailPanel.js +15 -15
- package/src/dashboard/app/main.jsx +13 -13
- package/src/dashboard/app/pages/AgentMap.jsx +370 -370
- package/src/dashboard/app/pages/HookInspector.jsx +418 -418
- package/src/dashboard/app/pages/LiveState.jsx +11 -1
- package/src/dashboard/app/pages/PhasePipeline.jsx +408 -408
- package/src/dashboard/app/pages/RulesMap.jsx +273 -273
- package/src/dashboard/app/pages/SkillsLibrary.jsx +366 -366
- package/src/dashboard/app/pages/Standards.jsx +332 -332
- package/src/dashboard/app/styles/index.css +47 -47
- package/src/dashboard/index.html +12 -12
- package/src/dashboard/package.json +24 -24
- package/src/dashboard/scanner/hooks-scanner.js +41 -41
- package/src/dashboard/scanner/phases-scanner.js +15 -15
- package/src/dashboard/scanner/rubrics-scanner.js +12 -12
- package/src/dashboard/scanner/rules-scanner.js +37 -37
- package/src/dashboard/scanner/skills-scanner.js +43 -43
- package/src/dashboard/scanner/squads-scanner.js +13 -13
- package/src/dashboard/scanner/standards-scanner.js +26 -26
- package/src/dashboard/scanner/state-scanner.js +42 -0
- package/src/dashboard/vite-plugin-morph-data.js +17 -17
- package/src/dashboard/vite.config.js +24 -24
- package/src/lib/agents/metrics-aggregator.js +56 -56
- package/src/lib/agents/metrics-calculator.js +125 -125
- package/src/lib/agents/output-collector.js +73 -73
- package/src/lib/detectors/claude-config-detector.js +152 -152
- package/src/lib/detectors/design-system-detector.js +231 -231
- package/src/lib/detectors/index.js +217 -217
- package/src/lib/generators/architecture-scanner.js +639 -639
- package/src/lib/generators/design-system-generator.js +508 -508
- package/src/lib/generators/index.js +13 -13
- package/src/lib/generators/settings-generator.js +209 -209
- package/src/lib/hooks/hook-executor.js +167 -167
- package/src/lib/installers/mcp-installer.js +719 -719
- package/src/lib/installers/memory-installer.js +1 -1
- package/src/lib/risk-detector.js +16 -0
- package/src/lib/tasks/dispatch-planner.js +122 -4
- package/src/lib/tasks/mermaid-renderer.js +132 -0
- package/src/lib/tasks/task-classifier.js +30 -30
- package/src/lib/tasks/task-parser.js +209 -209
- package/src/lib/tasks/test-runner.js +27 -6
- package/src/lib/validators/architecture/architecture-validator.js +259 -259
- package/src/lib/validators/checkpoint-persistence.js +39 -39
- package/src/lib/validators/contracts/contract-compliance-validator.js +274 -274
- package/src/lib/validators/css/css-validator.js +340 -340
- package/src/lib/validators/packages/package-validator.js +347 -347
- package/src/lib/validators/shared/emit-validator-dispatch.js +14 -14
- package/src/lib/validators/shared/issue-counter.js +14 -14
- package/src/lib/validators/validation-runner.js +349 -349
- package/src/lib/validators/verify-runner.js +322 -0
- package/src/lib/work-type-detector.js +143 -0
- package/src/scripts/setup-infra.js +32 -5
- package/src/utils/agent-id-mapper.js +11 -11
- package/src/utils/agents-installer.js +10 -6
- package/src/utils/claude-settings-manager.js +300 -300
- package/src/utils/file-copier.js +43 -0
- package/src/utils/hooks-installer.js +41 -11
- package/src/utils/skills-installer.js +174 -174
- package/framework/templates/meta-prompts/parallel-workers/parallel-coordinator.md +0 -113
- package/framework/templates/meta-prompts/parallel-workers/parallel-worker.md +0 -80
- 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
|
package/src/lib/risk-detector.js
CHANGED
|
@@ -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
|
+
}
|