@polymorphism-tech/morph-spec 4.21.0 → 5.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +57 -71
- package/README.md +2 -2
- package/bin/morph-spec.js +224 -491
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +55 -38
- package/framework/MORPH.md +244 -0
- package/framework/agents.json +127 -1183
- package/framework/commands/morph-apply.md +56 -240
- package/framework/commands/morph-archive.md +48 -85
- package/framework/commands/morph-preflight.md +7 -7
- package/framework/commands/morph-proposal.md +35 -191
- package/framework/commands/morph-status.md +19 -50
- package/framework/commands/morph-troubleshoot.md +2 -2
- package/framework/evals/rubrics/README.md +44 -0
- package/framework/evals/rubrics/architecture.md +46 -0
- package/framework/evals/rubrics/code-quality.md +50 -0
- package/framework/evals/rubrics/contracts.md +47 -0
- package/framework/evals/rubrics/test-coverage.md +50 -0
- package/framework/evals/scenarios/dotnet-crud.yaml +42 -0
- package/framework/evals/scenarios/nextjs-auth.yaml +38 -0
- package/framework/evals/suites/standard.yaml +5 -0
- package/framework/evals/trace-schema.md +67 -0
- package/framework/hooks/claude-code/core/README.md +3 -0
- package/framework/hooks/claude-code/core/denylist.json +14 -0
- package/framework/hooks/claude-code/{pre-tool-use → core}/enforce-phase-writes.js +5 -5
- package/framework/hooks/claude-code/core/post-edit-typecheck.js +69 -0
- package/framework/hooks/claude-code/core/pre-bash-denylist.js +29 -0
- package/framework/hooks/claude-code/{pre-tool-use → core}/protect-spec-files.js +4 -6
- package/framework/hooks/claude-code/{stop → core}/validate-completion.js +13 -82
- package/framework/hooks/shared/phase-utils.js +20 -28
- package/framework/hooks/shared/state-reader.js +4 -5
- package/framework/{skills/level-0-meta/mcp-registry.json → mcp/registry.json} +254 -210
- package/framework/phases.json +40 -114
- package/framework/rubrics/eval-rubric.json +66 -0
- package/framework/skills/README.md +39 -64
- package/framework/skills/core/README.md +3 -0
- package/framework/skills/core/morph-brainstorming/SKILL.md +102 -0
- package/framework/skills/core/morph-eval/SKILL.md +57 -0
- package/framework/skills/core/morph-implement/SKILL.md +83 -0
- package/framework/skills/core/morph-plan/SKILL.md +64 -0
- package/framework/skills/core/morph-review/SKILL.md +62 -0
- package/framework/skills/core/morph-stack-scan/SKILL.md +109 -0
- package/framework/skills/core/morph-standards/SKILL.md +49 -0
- package/framework/standards/STANDARDS.json +32 -0
- package/framework/standards/frontend/design-system/ai-image-generation.md +244 -0
- package/framework/standards/integration/mcp/mcp-tools.md +48 -0
- package/framework/templates/CLAUDE.md.core.guide.md +29 -0
- package/framework/templates/CLAUDE.md.core.template +22 -0
- package/framework/templates/REGISTRY.json +4 -4
- package/framework/workflows/configs/fast-track.json +5 -10
- package/framework/workflows/configs/full.json +4 -8
- package/framework/workflows/configs/quick.json +32 -0
- package/framework/workflows/configs/standard.json +57 -64
- package/package.json +7 -6
- package/src/commands/mcp/mcp-setup.js +58 -31
- package/src/commands/project/doctor.js +69 -70
- package/src/commands/project/init.js +26 -5
- package/src/commands/project/status.js +18 -19
- package/src/commands/state/approve.js +1 -1
- package/src/commands/validation/validate-feature.js +18 -112
- package/src/core/paths/output-schema.js +133 -151
- package/src/core/state/phase-state-machine.js +10 -24
- package/src/core/state/state-manager.js +17 -17
- package/src/core/workflows/workflow-detector.js +214 -31
- package/src/core/workflows/workflow-router.js +204 -0
- package/src/dashboard/app/pages/AgentMap.jsx +32 -33
- package/src/dashboard/app/pages/LiveState.jsx +1 -1
- package/src/dashboard/app/pages/PhasePipeline.jsx +2 -4
- package/src/dashboard/app/pages/SkillsLibrary.jsx +2 -3
- package/src/dashboard/app/pages/Workflows.jsx +2 -2
- package/src/dashboard/scanner/agents-scanner.js +30 -35
- package/src/lib/agents/metrics-aggregator.js +1 -1
- package/src/lib/agents/output-collector.js +7 -8
- package/src/lib/context/context-builder.js +289 -0
- package/src/lib/detectors/claude-config-detector.js +19 -3
- package/src/lib/generators/context-generator.js +20 -42
- package/src/lib/generators/index.js +1 -0
- package/src/lib/generators/mandate-generator.js +512 -0
- package/src/lib/generators/metadata-extractor.js +1 -1
- package/src/lib/generators/recap-generator.js +3 -4
- package/src/lib/installers/mcp-installer.js +300 -92
- package/src/lib/phase-chain/phase-validator.js +36 -40
- package/src/lib/stack/stack-profile.js +74 -91
- package/src/lib/tasks/task-parser.js +1 -1
- package/src/lib/tasks/wave-resolver.js +353 -0
- package/src/lib/validators/checkpoint-persistence.js +2 -2
- package/src/lib/validators/content/cross-phase-validator.js +281 -0
- package/src/lib/validators/content/index.js +1 -0
- package/src/lib/validators/shared/emit-validator-dispatch.js +6 -56
- package/src/scripts/global-install.js +2 -27
- package/src/utils/agents-installer.js +224 -447
- package/src/utils/claude-settings-manager.js +57 -15
- package/src/utils/file-copier.js +1 -0
- package/src/utils/hooks-installer.js +15 -117
- package/src/utils/skills-installer.js +12 -18
- package/bin/detect-agents.js +0 -226
- package/bin/task-manager.js +0 -1036
- package/framework/agents/README.md +0 -13
- package/framework/agents/ai-agents/ai-system-architect.md +0 -194
- package/framework/agents/ai-agents/vector-search-expert.md +0 -247
- package/framework/agents/architecture/architecture-expert.md +0 -98
- package/framework/agents/architecture/morph-spec-architect.md +0 -66
- package/framework/agents/architecture/packages-validator.md +0 -89
- package/framework/agents/architecture/security-expert.md +0 -79
- package/framework/agents/architecture/seo-growth-hacker.md +0 -168
- package/framework/agents/architecture/standards-architect.md +0 -153
- package/framework/agents/backend/dotnet-senior.md +0 -163
- package/framework/agents/backend/ef-modeler.md +0 -119
- package/framework/agents/backend/hangfire-orchestrator.md +0 -133
- package/framework/agents/backend/ms-agent-expert.md +0 -55
- package/framework/agents/backend/neon-expert.md +0 -379
- package/framework/agents/frontend/css-specialist.md +0 -89
- package/framework/agents/frontend/design-system-validator.md +0 -100
- package/framework/agents/frontend/gsap-animator.md +0 -131
- package/framework/agents/frontend/nextjs-component-validator.md +0 -97
- package/framework/agents/frontend/nextjs-expert.md +0 -201
- package/framework/agents/frontend/ui-designer.md +0 -259
- package/framework/agents/infrastructure/infra-architect.md +0 -215
- package/framework/agents/integrations/asaas-financial.md +0 -138
- package/framework/agents/integrations/neon-auth.md +0 -151
- package/framework/agents/integrations/resend-email.md +0 -127
- package/framework/hooks/claude-code/post-tool-use/agent-complete.js +0 -122
- package/framework/hooks/claude-code/post-tool-use/dispatch.js +0 -363
- package/framework/hooks/claude-code/post-tool-use/handle-tool-failure.js +0 -78
- package/framework/hooks/claude-code/post-tool-use/validator-feedback.js +0 -194
- package/framework/hooks/claude-code/pre-compact/save-morph-context.js +0 -199
- package/framework/hooks/claude-code/pre-tool-use/task-tracking-guard.js +0 -55
- package/framework/hooks/claude-code/session-start/inject-morph-context.js +0 -323
- package/framework/hooks/claude-code/session-start/post-compact-restore.js +0 -41
- package/framework/hooks/claude-code/session-start/resume-context.js +0 -77
- package/framework/hooks/claude-code/teammate-idle/teammate-idle.js +0 -90
- package/framework/hooks/claude-code/user-prompt/enrich-prompt.js +0 -114
- package/framework/hooks/shared/compact-restore.js +0 -123
- package/framework/hooks/shared/dispatch-helpers.js +0 -147
- package/framework/hooks/shared/skill-reminder-helpers.js +0 -79
- package/framework/hooks/shared/stale-task-reset.js +0 -57
- package/framework/hooks/shared/worktree-helpers.js +0 -75
- package/framework/skills/level-0-meta/README.md +0 -7
- package/framework/skills/level-0-meta/morph-agent-teams/SKILL.md +0 -472
- package/framework/skills/level-0-meta/morph-brainstorming/SKILL.md +0 -174
- package/framework/skills/level-0-meta/morph-brainstorming/references/proposal-example.md +0 -138
- package/framework/skills/level-0-meta/morph-checklist/SKILL.md +0 -145
- package/framework/skills/level-0-meta/morph-code-review/SKILL.md +0 -250
- package/framework/skills/level-0-meta/morph-code-review/references/review-example.md +0 -164
- package/framework/skills/level-0-meta/morph-code-review/references/review-guidelines.md +0 -100
- package/framework/skills/level-0-meta/morph-code-review/scripts/scan-csharp.mjs +0 -151
- package/framework/skills/level-0-meta/morph-code-review-nextjs/SKILL.md +0 -165
- package/framework/skills/level-0-meta/morph-code-review-nextjs/references/review-example-nextjs.md +0 -254
- package/framework/skills/level-0-meta/morph-code-review-nextjs/scripts/scan-nextjs.mjs +0 -189
- package/framework/skills/level-0-meta/morph-frontend-review/SKILL.md +0 -344
- package/framework/skills/level-0-meta/morph-frontend-review/scripts/scan-accessibility.mjs +0 -376
- package/framework/skills/level-0-meta/morph-init/SKILL.md +0 -537
- package/framework/skills/level-0-meta/morph-parallel-dispatch/SKILL.md +0 -225
- package/framework/skills/level-0-meta/morph-post-implementation/SKILL.md +0 -382
- package/framework/skills/level-0-meta/morph-post-implementation/scripts/detect-dev-server.mjs +0 -153
- package/framework/skills/level-0-meta/morph-post-implementation/scripts/detect-stack.mjs +0 -234
- package/framework/skills/level-0-meta/morph-simulation-checklist/SKILL.md +0 -106
- package/framework/skills/level-0-meta/morph-tool-usage-guide/SKILL.md +0 -243
- package/framework/skills/level-0-meta/morph-tool-usage-guide/references/tools-per-phase.md +0 -169
- package/framework/skills/level-0-meta/morph-verification-before-completion/SKILL.md +0 -148
- package/framework/skills/level-0-meta/morph-verification-before-completion/scripts/check-phase-outputs.mjs +0 -110
- package/framework/skills/level-1-workflows/README.md +0 -7
- package/framework/skills/level-1-workflows/morph-phase-codebase-analysis/SKILL.md +0 -269
- package/framework/skills/level-1-workflows/morph-phase-design/SKILL.md +0 -453
- package/framework/skills/level-1-workflows/morph-phase-design/references/architecture-analysis-guide.md +0 -44
- package/framework/skills/level-1-workflows/morph-phase-design/references/spec-authoring-guide.md +0 -55
- package/framework/skills/level-1-workflows/morph-phase-design/references/spec-example.md +0 -253
- package/framework/skills/level-1-workflows/morph-phase-implement/.morph/logs/activity.json +0 -38
- package/framework/skills/level-1-workflows/morph-phase-implement/SKILL.md +0 -493
- package/framework/skills/level-1-workflows/morph-phase-implement/prompts/code-quality-reviewer-prompt.md +0 -50
- package/framework/skills/level-1-workflows/morph-phase-implement/prompts/implementer-prompt.md +0 -46
- package/framework/skills/level-1-workflows/morph-phase-implement/prompts/spec-reviewer-prompt.md +0 -47
- package/framework/skills/level-1-workflows/morph-phase-implement/references/recap-example.md +0 -132
- package/framework/skills/level-1-workflows/morph-phase-implement/references/vsa-implementation-guide.md +0 -92
- package/framework/skills/level-1-workflows/morph-phase-plan/SKILL.md +0 -257
- package/framework/skills/level-1-workflows/morph-phase-tasks/.morph/logs/activity.json +0 -14
- package/framework/skills/level-1-workflows/morph-phase-tasks/SKILL.md +0 -291
- package/framework/skills/level-1-workflows/morph-phase-tasks/references/task-planning-patterns.md +0 -143
- package/framework/skills/level-1-workflows/morph-phase-tasks/references/tasks-example.md +0 -404
- package/framework/skills/level-1-workflows/morph-phase-tasks/scripts/validate-tasks.mjs +0 -112
- package/framework/skills/level-1-workflows/morph-phase-uiux/SKILL.md +0 -788
- package/framework/skills/level-1-workflows/morph-scope-escalation/SKILL.md +0 -147
- package/framework/squad-templates/backend-only.json +0 -47
- package/framework/squad-templates/frontend-only.json +0 -56
- package/framework/squad-templates/full-stack.json +0 -74
- package/src/commands/agents/agent-metrics.js +0 -174
- package/src/commands/agents/audit-dispatch.js +0 -82
- package/src/commands/agents/detect-agents.js +0 -275
- package/src/commands/agents/dispatch-agents.js +0 -461
- package/src/commands/agents/index.js +0 -13
- package/src/commands/agents/manage-agents.js +0 -352
- package/src/commands/agents/project-metrics.js +0 -74
- package/src/commands/phase/phase-reset.js +0 -74
- package/src/commands/scope/escalate.js +0 -217
- package/src/commands/state/advance-phase.js +0 -600
- package/src/commands/state/phase-runner.js +0 -215
- package/src/commands/state/state.js +0 -444
- package/src/commands/task/expand.js +0 -100
- package/src/commands/tasks/index.js +0 -4
- package/src/commands/tasks/task.js +0 -105
- package/src/commands/templates/index.js +0 -5
- package/src/commands/templates/template-render.js +0 -93
- package/src/commands/trust/trust.js +0 -118
- package/src/lib/agents/agents-manager.js +0 -349
- package/src/lib/build/build-runner.js +0 -99
- package/src/lib/checkpoints/checkpoint-hooks.js +0 -111
- package/src/lib/contracts/sprint-contract-generator.js +0 -137
- package/src/lib/scope/impact-analyzer.js +0 -106
- package/src/lib/trust/trust-manager.js +0 -157
- package/src/utils/tier-helpers.js +0 -24
- /package/framework/hooks/claude-code/{post-tool-use → core}/loop-detect.js +0 -0
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Implementation Mandate Generator
|
|
3
|
+
*
|
|
4
|
+
* Synthesizes all spec phase outputs (spec.md, contracts, plan.md, tasks.md,
|
|
5
|
+
* decisions.md) into a single task-structured implementation mandate.
|
|
6
|
+
*
|
|
7
|
+
* The mandate is the primary context artifact for the implement phase —
|
|
8
|
+
* each task section is self-contained with acceptance criteria, contracts,
|
|
9
|
+
* file paths, and architectural constraints.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { readFileSync, existsSync } from 'fs';
|
|
13
|
+
import { join } from 'path';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* v5: parseTaskFromMarkdown lived in sprint-contract-generator. Inlined here.
|
|
17
|
+
*/
|
|
18
|
+
function parseTaskFromMarkdown(tasksContent, taskId) {
|
|
19
|
+
const headingRe = new RegExp(`^### ${taskId}\\b[^\\n]*\\n([\\s\\S]*?)(?=^### |\\Z)`, 'm');
|
|
20
|
+
const match = tasksContent.match(headingRe);
|
|
21
|
+
if (!match) return null;
|
|
22
|
+
const block = match[1];
|
|
23
|
+
const frRefs = [...block.matchAll(/\bFR-?\d+\b/g)].map(m => m[0]);
|
|
24
|
+
return { id: taskId, frs: [...new Set(frRefs)], block };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Extract all FR sections from spec.md.
|
|
29
|
+
*
|
|
30
|
+
* @param {string} specContent - Full spec.md content
|
|
31
|
+
* @returns {Map<string, { title: string, content: string, criteria: string[] }>}
|
|
32
|
+
*/
|
|
33
|
+
export function parseFRSections(specContent) {
|
|
34
|
+
const sections = new Map();
|
|
35
|
+
const frPattern = /^##\s+(FR\d+)[:\s—–-]+(.+?)$/gm;
|
|
36
|
+
let match;
|
|
37
|
+
const matches = [];
|
|
38
|
+
|
|
39
|
+
while ((match = frPattern.exec(specContent)) !== null) {
|
|
40
|
+
matches.push({
|
|
41
|
+
id: match[1],
|
|
42
|
+
title: match[2].trim(),
|
|
43
|
+
start: match.index,
|
|
44
|
+
headerEnd: match.index + match[0].length,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
for (let i = 0; i < matches.length; i++) {
|
|
49
|
+
const current = matches[i];
|
|
50
|
+
const nextStart = i + 1 < matches.length ? matches[i + 1].start : specContent.length;
|
|
51
|
+
const body = specContent.slice(current.headerEnd, nextStart).trim();
|
|
52
|
+
|
|
53
|
+
// Extract bullet points as acceptance criteria
|
|
54
|
+
const criteria = [];
|
|
55
|
+
const bulletPattern = /^[\s]*[-*]\s+(.+)/gm;
|
|
56
|
+
let bulletMatch;
|
|
57
|
+
while ((bulletMatch = bulletPattern.exec(body)) !== null) {
|
|
58
|
+
criteria.push(bulletMatch[1].trim());
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
sections.set(current.id, {
|
|
62
|
+
title: current.title,
|
|
63
|
+
content: body,
|
|
64
|
+
criteria,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return sections;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Extract contract types/interfaces from contracts file content.
|
|
73
|
+
* Matches C# records, classes, interfaces and TypeScript types/interfaces.
|
|
74
|
+
*
|
|
75
|
+
* @param {string} contractsContent - Full contracts.cs or contracts.ts content
|
|
76
|
+
* @returns {Map<string, string>} Map of type name → definition
|
|
77
|
+
*/
|
|
78
|
+
export function parseContracts(contractsContent) {
|
|
79
|
+
const types = new Map();
|
|
80
|
+
|
|
81
|
+
// C# sealed records: sealed record Foo(...)
|
|
82
|
+
const csRecordPattern = /^\s*((?:public\s+)?sealed\s+record\s+\w+\([^)]*\)\s*;)/gm;
|
|
83
|
+
let match;
|
|
84
|
+
while ((match = csRecordPattern.exec(contractsContent)) !== null) {
|
|
85
|
+
const nameMatch = match[1].match(/record\s+(\w+)/);
|
|
86
|
+
if (nameMatch) {
|
|
87
|
+
types.set(nameMatch[1], match[1].trim());
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// C# interfaces: public interface IFoo { ... }
|
|
92
|
+
const csInterfacePattern = /^\s*((?:public\s+)?interface\s+\w+\s*\{[^}]*\})/gms;
|
|
93
|
+
while ((match = csInterfacePattern.exec(contractsContent)) !== null) {
|
|
94
|
+
const nameMatch = match[1].match(/interface\s+(\w+)/);
|
|
95
|
+
if (nameMatch) {
|
|
96
|
+
types.set(nameMatch[1], match[1].trim());
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// TypeScript types/interfaces
|
|
101
|
+
const tsTypePattern = /^\s*(export\s+(?:type|interface)\s+\w+[\s\S]*?(?:\};?|=[^;]+;))/gm;
|
|
102
|
+
while ((match = tsTypePattern.exec(contractsContent)) !== null) {
|
|
103
|
+
const nameMatch = match[1].match(/(?:type|interface)\s+(\w+)/);
|
|
104
|
+
if (nameMatch) {
|
|
105
|
+
types.set(nameMatch[1], match[1].trim());
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return types;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Extract file paths per task from plan.md.
|
|
114
|
+
* Looks for task references followed by file paths.
|
|
115
|
+
*
|
|
116
|
+
* @param {string} planContent - Full plan.md content
|
|
117
|
+
* @param {string} taskId - Task ID (e.g., 'T001')
|
|
118
|
+
* @returns {string[]} File paths
|
|
119
|
+
*/
|
|
120
|
+
export function extractTaskFilePaths(planContent, taskId) {
|
|
121
|
+
const paths = [];
|
|
122
|
+
|
|
123
|
+
// Look for task section in plan
|
|
124
|
+
const taskSectionPattern = new RegExp(
|
|
125
|
+
`(?:^|\\n).*${taskId}[^\\n]*\\n([\\s\\S]*?)(?=\\n(?:#+\\s|$)|$)`,
|
|
126
|
+
'i'
|
|
127
|
+
);
|
|
128
|
+
const sectionMatch = planContent.match(taskSectionPattern);
|
|
129
|
+
if (!sectionMatch) return paths;
|
|
130
|
+
|
|
131
|
+
const section = sectionMatch[1];
|
|
132
|
+
// Extract file paths (common patterns)
|
|
133
|
+
const filePatterns = [
|
|
134
|
+
/`([^`]+\.\w{1,6})`/g, // backtick-wrapped paths
|
|
135
|
+
/(?:^|\s)([\w/.]+\.(?:cs|ts|tsx|js|jsx|json|md|yaml|yml|csproj))/gm, // bare paths
|
|
136
|
+
];
|
|
137
|
+
|
|
138
|
+
for (const pattern of filePatterns) {
|
|
139
|
+
let fileMatch;
|
|
140
|
+
while ((fileMatch = pattern.exec(section)) !== null) {
|
|
141
|
+
const filePath = fileMatch[1].trim();
|
|
142
|
+
if (filePath.length > 3 && !paths.includes(filePath)) {
|
|
143
|
+
paths.push(filePath);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return paths;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Extract architectural constraints from decisions.md.
|
|
153
|
+
*
|
|
154
|
+
* @param {string} decisionsContent - Full decisions.md content
|
|
155
|
+
* @returns {string[]} One-line constraint summaries
|
|
156
|
+
*/
|
|
157
|
+
export function parseDecisions(decisionsContent) {
|
|
158
|
+
const constraints = [];
|
|
159
|
+
const adrPattern = /^###?\s+(ADR-\d+)[:\s—–-]+(.+?)$/gm;
|
|
160
|
+
let match;
|
|
161
|
+
|
|
162
|
+
while ((match = adrPattern.exec(decisionsContent)) !== null) {
|
|
163
|
+
constraints.push(`${match[1]}: ${match[2].trim()}`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return constraints;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Extract all task IDs from tasks.md in order.
|
|
171
|
+
*
|
|
172
|
+
* @param {string} tasksContent - Full tasks.md content
|
|
173
|
+
* @returns {string[]} Task IDs in order (e.g., ['T001', 'T002', ...])
|
|
174
|
+
*/
|
|
175
|
+
export function extractTaskIds(tasksContent) {
|
|
176
|
+
const ids = [];
|
|
177
|
+
const pattern = /^##\s+(T\d+)[:\s]/gm;
|
|
178
|
+
let match;
|
|
179
|
+
while ((match = pattern.exec(tasksContent)) !== null) {
|
|
180
|
+
if (!ids.includes(match[1])) {
|
|
181
|
+
ids.push(match[1]);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return ids;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Find contract types relevant to an FR or task.
|
|
189
|
+
* Matches by: type name in content, or FR title keywords in type name.
|
|
190
|
+
*
|
|
191
|
+
* @param {string} searchText - FR content + task title/description
|
|
192
|
+
* @param {Map<string, string>} contracts - Parsed contracts
|
|
193
|
+
* @returns {string[]} Matching contract definitions
|
|
194
|
+
*/
|
|
195
|
+
function findRelevantContracts(searchText, contracts) {
|
|
196
|
+
const relevant = [];
|
|
197
|
+
const searchLower = searchText.toLowerCase();
|
|
198
|
+
|
|
199
|
+
for (const [typeName, definition] of contracts) {
|
|
200
|
+
// Direct match: type name appears in the search text
|
|
201
|
+
if (searchText.includes(typeName)) {
|
|
202
|
+
relevant.push(definition);
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
// Keyword match: split type name into words (e.g., UserRegisterRequest → user, register, request)
|
|
206
|
+
// and check if the significant words appear in the search text
|
|
207
|
+
const typeWords = typeName
|
|
208
|
+
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
|
209
|
+
.toLowerCase()
|
|
210
|
+
.split(/\s+/)
|
|
211
|
+
.filter(w => w.length > 3 && !['request', 'response', 'result'].includes(w));
|
|
212
|
+
if (typeWords.length > 0 && typeWords.every(w => searchLower.includes(w))) {
|
|
213
|
+
relevant.push(definition);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return relevant;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Generate an implementation mandate from spec phase outputs.
|
|
221
|
+
*
|
|
222
|
+
* @param {string} projectPath - Project root
|
|
223
|
+
* @param {string} featureName - Feature name
|
|
224
|
+
* @returns {{ mandate: string, stats: { frCount: number, taskCount: number, totalChars: number } }|null}
|
|
225
|
+
*/
|
|
226
|
+
export function generateMandate(projectPath, featureName) {
|
|
227
|
+
const featureDir = join(projectPath, '.morph/features', featureName);
|
|
228
|
+
|
|
229
|
+
// Read required files
|
|
230
|
+
const tasksPath = join(featureDir, '2-plan/tasks.md');
|
|
231
|
+
const specPath = join(featureDir, '2-plan/spec.md');
|
|
232
|
+
|
|
233
|
+
if (!existsSync(tasksPath) || !existsSync(specPath)) return null;
|
|
234
|
+
|
|
235
|
+
const tasksContent = readFileSync(tasksPath, 'utf-8');
|
|
236
|
+
const specContent = readFileSync(specPath, 'utf-8');
|
|
237
|
+
|
|
238
|
+
// Read optional files
|
|
239
|
+
const contractsPaths = [
|
|
240
|
+
join(featureDir, '2-plan/contracts.cs'),
|
|
241
|
+
join(featureDir, '2-plan/contracts.ts'),
|
|
242
|
+
];
|
|
243
|
+
let contractsContent = '';
|
|
244
|
+
for (const cp of contractsPaths) {
|
|
245
|
+
if (existsSync(cp)) {
|
|
246
|
+
contractsContent += readFileSync(cp, 'utf-8') + '\n';
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const planPath = join(featureDir, '2-plan/plan.md');
|
|
251
|
+
const planContent = existsSync(planPath) ? readFileSync(planPath, 'utf-8') : '';
|
|
252
|
+
|
|
253
|
+
const decisionsPath = join(featureDir, '2-plan/decisions.md');
|
|
254
|
+
const decisionsContent = existsSync(decisionsPath) ? readFileSync(decisionsPath, 'utf-8') : '';
|
|
255
|
+
|
|
256
|
+
const proposalPath = join(featureDir, '0-proposal/proposal.md');
|
|
257
|
+
const proposalContent = existsSync(proposalPath) ? readFileSync(proposalPath, 'utf-8') : '';
|
|
258
|
+
|
|
259
|
+
// Read project config
|
|
260
|
+
let stack = '';
|
|
261
|
+
let architecture = '';
|
|
262
|
+
let workflow = '';
|
|
263
|
+
try {
|
|
264
|
+
const configPath = join(projectPath, '.morph/config/config.json');
|
|
265
|
+
if (existsSync(configPath)) {
|
|
266
|
+
const config = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
267
|
+
stack = config?.project?.stack || '';
|
|
268
|
+
architecture = config?.architecture?.style || '';
|
|
269
|
+
workflow = config?.workflow || '';
|
|
270
|
+
}
|
|
271
|
+
} catch { /* ignore */ }
|
|
272
|
+
|
|
273
|
+
// Parse all inputs
|
|
274
|
+
const frSections = parseFRSections(specContent);
|
|
275
|
+
const contracts = parseContracts(contractsContent);
|
|
276
|
+
const decisions = parseDecisions(decisionsContent);
|
|
277
|
+
const taskIds = extractTaskIds(tasksContent);
|
|
278
|
+
|
|
279
|
+
// Build mandate
|
|
280
|
+
const lines = [];
|
|
281
|
+
|
|
282
|
+
// Header
|
|
283
|
+
lines.push(`# Implementation Mandate: ${featureName}`);
|
|
284
|
+
lines.push('');
|
|
285
|
+
lines.push(`**Generated:** ${new Date().toISOString().split('T')[0]} | **FRs:** ${frSections.size} | **Tasks:** ${taskIds.length}`);
|
|
286
|
+
if (stack || architecture) {
|
|
287
|
+
lines.push(`**Stack:** ${stack || 'unspecified'} | **Architecture:** ${architecture || 'unspecified'}`);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Overview from proposal
|
|
291
|
+
if (proposalContent) {
|
|
292
|
+
lines.push('');
|
|
293
|
+
lines.push('## Overview');
|
|
294
|
+
// Extract first paragraph after the title
|
|
295
|
+
const paragraphs = proposalContent
|
|
296
|
+
.split(/\n\n+/)
|
|
297
|
+
.filter(p => p.trim() && !p.trim().startsWith('#'))
|
|
298
|
+
.slice(0, 1);
|
|
299
|
+
if (paragraphs.length > 0) {
|
|
300
|
+
lines.push(paragraphs[0].trim());
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Architectural constraints
|
|
305
|
+
if (decisions.length > 0) {
|
|
306
|
+
lines.push('');
|
|
307
|
+
lines.push('## Architectural Constraints');
|
|
308
|
+
for (const d of decisions) {
|
|
309
|
+
lines.push(`- ${d}`);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Task sections
|
|
314
|
+
for (const taskId of taskIds) {
|
|
315
|
+
const task = parseTaskFromMarkdown(tasksContent, taskId);
|
|
316
|
+
if (!task) continue;
|
|
317
|
+
|
|
318
|
+
lines.push('');
|
|
319
|
+
lines.push('---');
|
|
320
|
+
lines.push('');
|
|
321
|
+
lines.push(`## ${taskId}: ${task.title}`);
|
|
322
|
+
|
|
323
|
+
// Metadata line
|
|
324
|
+
const metaParts = [];
|
|
325
|
+
if (task.frs.length > 0) metaParts.push(`**FRs:** ${task.frs.join(', ')}`);
|
|
326
|
+
const filePaths = planContent ? extractTaskFilePaths(planContent, taskId) : [];
|
|
327
|
+
if (filePaths.length > 0) metaParts.push(`**Files:** ${filePaths.join(', ')}`);
|
|
328
|
+
if (metaParts.length > 0) lines.push(metaParts.join(' | '));
|
|
329
|
+
|
|
330
|
+
// Acceptance criteria from FR sections
|
|
331
|
+
const allCriteria = [];
|
|
332
|
+
const allFRContracts = [];
|
|
333
|
+
for (const frId of task.frs) {
|
|
334
|
+
const fr = frSections.get(frId);
|
|
335
|
+
if (fr) {
|
|
336
|
+
allCriteria.push(...fr.criteria);
|
|
337
|
+
// Search for contracts in FR content + task title for broader matching
|
|
338
|
+
const searchText = `${fr.title} ${fr.content} ${task.title} ${task.description}`;
|
|
339
|
+
allFRContracts.push(...findRelevantContracts(searchText, contracts));
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (allCriteria.length > 0) {
|
|
344
|
+
lines.push('');
|
|
345
|
+
lines.push('### Acceptance Criteria');
|
|
346
|
+
for (const c of allCriteria) {
|
|
347
|
+
lines.push(`- [ ] ${c}`);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Contracts
|
|
352
|
+
const uniqueContracts = [...new Set(allFRContracts)];
|
|
353
|
+
if (uniqueContracts.length > 0) {
|
|
354
|
+
lines.push('');
|
|
355
|
+
lines.push('### Contracts');
|
|
356
|
+
lines.push('```');
|
|
357
|
+
lines.push(uniqueContracts.join('\n'));
|
|
358
|
+
lines.push('```');
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Task description as notes
|
|
362
|
+
if (task.description) {
|
|
363
|
+
lines.push('');
|
|
364
|
+
lines.push('### Notes');
|
|
365
|
+
lines.push(task.description);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Verification summary
|
|
370
|
+
lines.push('');
|
|
371
|
+
lines.push('---');
|
|
372
|
+
lines.push('');
|
|
373
|
+
lines.push('## Verification');
|
|
374
|
+
lines.push('```bash');
|
|
375
|
+
if (stack.includes('dotnet')) {
|
|
376
|
+
lines.push('dotnet build');
|
|
377
|
+
lines.push('dotnet test');
|
|
378
|
+
} else {
|
|
379
|
+
lines.push('npm run build');
|
|
380
|
+
lines.push('npm test');
|
|
381
|
+
}
|
|
382
|
+
lines.push('```');
|
|
383
|
+
|
|
384
|
+
const mandate = lines.join('\n');
|
|
385
|
+
|
|
386
|
+
return {
|
|
387
|
+
mandate,
|
|
388
|
+
stats: {
|
|
389
|
+
frCount: frSections.size,
|
|
390
|
+
taskCount: taskIds.length,
|
|
391
|
+
totalChars: mandate.length,
|
|
392
|
+
},
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Extract a single task's section from a generated mandate.
|
|
398
|
+
*
|
|
399
|
+
* @param {string} mandateContent - Full mandate.md content
|
|
400
|
+
* @param {string} taskId - Task ID (e.g., 'T003')
|
|
401
|
+
* @returns {string|null} Task section text or null if not found
|
|
402
|
+
*/
|
|
403
|
+
export function extractTaskSection(mandateContent, taskId) {
|
|
404
|
+
// Find the start of this task's section
|
|
405
|
+
const headerPattern = new RegExp(`^## ${taskId}:`, 'm');
|
|
406
|
+
const headerMatch = headerPattern.exec(mandateContent);
|
|
407
|
+
if (!headerMatch) return null;
|
|
408
|
+
|
|
409
|
+
const start = headerMatch.index;
|
|
410
|
+
|
|
411
|
+
// Find the end: next task header, Verification section, or end of content
|
|
412
|
+
const rest = mandateContent.slice(start + 1);
|
|
413
|
+
const endPatterns = [
|
|
414
|
+
/\n## T\d+:/,
|
|
415
|
+
/\n## Verification/,
|
|
416
|
+
];
|
|
417
|
+
|
|
418
|
+
let end = mandateContent.length;
|
|
419
|
+
for (const pattern of endPatterns) {
|
|
420
|
+
const endMatch = pattern.exec(rest);
|
|
421
|
+
if (endMatch) {
|
|
422
|
+
const candidate = start + 1 + endMatch.index;
|
|
423
|
+
if (candidate < end) end = candidate;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Extract and trim, removing trailing --- separators
|
|
428
|
+
let section = mandateContent.slice(start, end).trim();
|
|
429
|
+
section = section.replace(/\n---\s*$/, '').trim();
|
|
430
|
+
|
|
431
|
+
return section;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Extract multiple task sections (for agent dispatch).
|
|
436
|
+
*
|
|
437
|
+
* @param {string} mandateContent - Full mandate.md content
|
|
438
|
+
* @param {string[]} taskIds - Task IDs (e.g., ['T001', 'T002', 'T003'])
|
|
439
|
+
* @returns {string} Combined task sections
|
|
440
|
+
*/
|
|
441
|
+
export function extractTaskSections(mandateContent, taskIds) {
|
|
442
|
+
const sections = [];
|
|
443
|
+
for (const taskId of taskIds) {
|
|
444
|
+
const section = extractTaskSection(mandateContent, taskId);
|
|
445
|
+
if (section) sections.push(section);
|
|
446
|
+
}
|
|
447
|
+
return sections.join('\n\n---\n\n');
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Generate a mandate overview (for session start injection).
|
|
452
|
+
* Shows FR list with task mapping and completion status.
|
|
453
|
+
*
|
|
454
|
+
* @param {string} mandateContent - Full mandate.md content
|
|
455
|
+
* @param {Object} taskStatus - Map of taskId → status ('completed'|'in_progress'|'pending')
|
|
456
|
+
* @returns {string} Overview text
|
|
457
|
+
*/
|
|
458
|
+
export function generateMandateOverview(mandateContent, taskStatus = {}) {
|
|
459
|
+
const lines = [];
|
|
460
|
+
|
|
461
|
+
// Extract task-to-FR mapping from mandate
|
|
462
|
+
const taskFRMap = new Map();
|
|
463
|
+
const frTaskMap = new Map();
|
|
464
|
+
|
|
465
|
+
const taskPattern = /^## (T\d+): .+$/gm;
|
|
466
|
+
const frLinePattern = /^\*\*FRs:\*\*\s+(.+?)(?:\s*\||$)/gm;
|
|
467
|
+
|
|
468
|
+
let match;
|
|
469
|
+
let lastTaskId = null;
|
|
470
|
+
|
|
471
|
+
while ((match = taskPattern.exec(mandateContent)) !== null) {
|
|
472
|
+
lastTaskId = match[1];
|
|
473
|
+
// Look for FRs line right after task header
|
|
474
|
+
const afterHeader = mandateContent.slice(match.index + match[0].length, match.index + match[0].length + 200);
|
|
475
|
+
const frMatch = afterHeader.match(/\*\*FRs:\*\*\s+([^\n|]+)/);
|
|
476
|
+
if (frMatch) {
|
|
477
|
+
const frs = frMatch[1].split(',').map(f => f.trim()).filter(Boolean);
|
|
478
|
+
taskFRMap.set(lastTaskId, frs);
|
|
479
|
+
for (const fr of frs) {
|
|
480
|
+
if (!frTaskMap.has(fr)) frTaskMap.set(fr, []);
|
|
481
|
+
frTaskMap.get(fr).push(lastTaskId);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Build overview
|
|
487
|
+
lines.push('Implementation Mandate Overview:');
|
|
488
|
+
|
|
489
|
+
for (const [frId, taskIds] of frTaskMap) {
|
|
490
|
+
const taskStatuses = taskIds.map(tid => {
|
|
491
|
+
const status = taskStatus[tid] || 'pending';
|
|
492
|
+
if (status === 'completed') return `${tid} ✓`;
|
|
493
|
+
if (status === 'in_progress') return `${tid} →`;
|
|
494
|
+
return tid;
|
|
495
|
+
});
|
|
496
|
+
lines.push(` ${frId} → ${taskStatuses.join(', ')}`);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Next task
|
|
500
|
+
const allTaskIds = [...taskFRMap.keys()];
|
|
501
|
+
const nextTask = allTaskIds.find(tid => {
|
|
502
|
+
const status = taskStatus[tid];
|
|
503
|
+
return status === 'in_progress' || !status || status === 'pending';
|
|
504
|
+
});
|
|
505
|
+
if (nextTask) {
|
|
506
|
+
const status = taskStatus[nextTask];
|
|
507
|
+
const verb = status === 'in_progress' ? 'In progress' : 'Next';
|
|
508
|
+
lines.push(` ${verb}: ${nextTask}`);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
return lines.join('\n');
|
|
512
|
+
}
|
|
@@ -308,7 +308,7 @@ export function extractTasksMetadata(tasksPath) {
|
|
|
308
308
|
byStatus: {
|
|
309
309
|
pending: regularTasks.filter(t => t.status === 'pending').length,
|
|
310
310
|
inProgress: regularTasks.filter(t => t.status === 'in_progress').length,
|
|
311
|
-
|
|
311
|
+
completed: regularTasks.filter(t => t.status === 'completed').length,
|
|
312
312
|
blocked: regularTasks.filter(t => t.status === 'blocked').length
|
|
313
313
|
},
|
|
314
314
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Auto-generates recap.md from real project data: state.json, contract compliance,
|
|
5
5
|
* validation results, and decisions. Replaces manual template filling.
|
|
6
6
|
*
|
|
7
|
-
* MORPH-SPEC
|
|
7
|
+
* MORPH-SPEC 5.0 - Recap Generator
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
@@ -98,10 +98,9 @@ function getTasksSummary(feature) {
|
|
|
98
98
|
};
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
// v3: feature.tasks is a counter object {total, completed, inProgress, pending}
|
|
102
|
-
// Use feature.taskList for individual items if available
|
|
101
|
+
// v3-v4 legacy: feature.tasks is a counter object {total, completed, inProgress, pending}
|
|
103
102
|
const counters = feature.tasks || {};
|
|
104
|
-
const items =
|
|
103
|
+
const items = [];
|
|
105
104
|
const total = counters.total || items.length || 0;
|
|
106
105
|
const completedCount = counters.completed ?? items.filter(t => t.status === 'completed').length;
|
|
107
106
|
const pendingCount = counters.pending ?? items.filter(t => t.status === 'pending').length;
|