@polymorphism-tech/morph-spec 7.2.0 → 7.4.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 (74) hide show
  1. package/CLAUDE.md +1 -1
  2. package/README.md +2 -2
  3. package/bin/morph-spec.js +4 -2
  4. package/claude-plugin.json +1 -1
  5. package/docs/CHEATSHEET.md +1 -1
  6. package/docs/QUICKSTART.md +1 -1
  7. package/framework/CLAUDE.md +2 -2
  8. package/framework/MORPH.md +29 -12
  9. package/framework/agents.json +132 -132
  10. package/framework/commands/morph-apply.md +1 -1
  11. package/framework/commands/morph-proposal.md +1 -1
  12. package/framework/evals/rubrics/README.md +9 -8
  13. package/framework/hooks/README.md +3 -1
  14. package/framework/hooks/claude-code/core/gate-guard.js +160 -0
  15. package/framework/hooks/claude-code/core/state-sync.js +15 -3
  16. package/framework/hooks/claude-code/core/validate-completion.js +241 -136
  17. package/framework/hooks/shared/state-reader.js +236 -221
  18. package/framework/phases.json +1 -1
  19. package/framework/skills/README.md +2 -2
  20. package/framework/skills/core/morph-apply/SKILL.md +63 -21
  21. package/framework/skills/core/morph-brainstorming/SKILL.md +5 -1
  22. package/framework/skills/core/morph-eval/SKILL.md +4 -5
  23. package/framework/skills/core/morph-implement/SKILL.md +47 -19
  24. package/framework/skills/core/morph-plan/SKILL.md +8 -2
  25. package/framework/skills/core/morph-proposal/SKILL.md +1 -1
  26. package/framework/skills/core/morph-review/SKILL.md +13 -6
  27. package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
  28. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  29. package/framework/standards/ai-agents/setup.md +258 -244
  30. package/framework/templates/README.md +708 -709
  31. package/framework/templates/REGISTRY.json +1 -20
  32. package/framework/templates/context/CONTEXT-FEATURE.md +276 -276
  33. package/framework/templates/docs/proposal.md +171 -171
  34. package/framework/templates/feature/recap.md +145 -145
  35. package/package.json +1 -1
  36. package/src/commands/project/doctor.js +941 -946
  37. package/src/commands/project/init.js +517 -515
  38. package/src/commands/project/status.js +373 -376
  39. package/src/commands/state/approve.js +251 -213
  40. package/src/commands/validation/validate-feature.js +356 -287
  41. package/src/core/state/state-manager.js +910 -934
  42. package/src/dashboard/app/App.jsx +29 -31
  43. package/src/dashboard/app/components/Sidebar.jsx +44 -45
  44. package/src/dashboard/scanner/index.js +20 -21
  45. package/src/lib/context/context-builder.js +3 -3
  46. package/src/lib/generators/context-generator.js +2 -3
  47. package/src/lib/generators/mandate-generator.js +48 -92
  48. package/src/lib/generators/metadata-extractor.js +385 -386
  49. package/src/lib/generators/recap-generator.js +285 -285
  50. package/src/lib/risk-detector.js +2 -2
  51. package/src/lib/validators/content/content-validator.js +89 -286
  52. package/src/lib/validators/content/index.js +0 -1
  53. package/src/utils/hooks-installer.js +527 -516
  54. package/framework/templates/feature/tasks.md +0 -199
  55. package/framework/workflows/README.md +0 -1041
  56. package/framework/workflows/configs/fast-track.json +0 -46
  57. package/framework/workflows/configs/full.json +0 -64
  58. package/framework/workflows/configs/quick.json +0 -32
  59. package/framework/workflows/configs/standard.json +0 -57
  60. package/framework/workflows/docs/STORY-DRIVEN-DEVELOPMENT.md +0 -392
  61. package/framework/workflows/docs/design-impl.md +0 -37
  62. package/framework/workflows/docs/enforcement-pipeline.md +0 -668
  63. package/framework/workflows/docs/fast-track.md +0 -29
  64. package/framework/workflows/docs/full-morph.md +0 -69
  65. package/framework/workflows/docs/standard.md +0 -49
  66. package/framework/workflows/docs/ui-refresh.md +0 -39
  67. package/src/commands/state/index.js +0 -7
  68. package/src/core/workflows/workflow-detector.js +0 -586
  69. package/src/core/workflows/workflow-router.js +0 -204
  70. package/src/dashboard/app/pages/Workflows.jsx +0 -346
  71. package/src/dashboard/scanner/workflows-scanner.js +0 -18
  72. package/src/lib/phase-chain/phase-validator.js +0 -359
  73. package/src/lib/validators/content/cross-phase-validator.js +0 -281
  74. package/src/lib/validators/spec-validator.js +0 -312
@@ -1,31 +1,29 @@
1
- import { Routes, Route, Navigate } from 'react-router-dom';
2
- import Sidebar from './components/Sidebar.jsx';
3
- import AgentMap from './pages/AgentMap.jsx';
4
- import PhasePipeline from './pages/PhasePipeline.jsx';
5
- import HookInspector from './pages/HookInspector.jsx';
6
- import SkillsLibrary from './pages/SkillsLibrary.jsx';
7
- import RulesMap from './pages/RulesMap.jsx';
8
- import Standards from './pages/Standards.jsx';
9
- import Workflows from './pages/Workflows.jsx';
10
- import LiveState from './pages/LiveState.jsx';
11
-
12
- export default function App() {
13
- return (
14
- <div className="flex h-screen bg-gray-950 text-gray-100 overflow-hidden">
15
- <Sidebar />
16
- <main className="flex-1 overflow-auto">
17
- <Routes>
18
- <Route path="/" element={<Navigate to="/agents" replace />} />
19
- <Route path="/agents" element={<AgentMap />} />
20
- <Route path="/phases" element={<PhasePipeline />} />
21
- <Route path="/hooks" element={<HookInspector />} />
22
- <Route path="/skills" element={<SkillsLibrary />} />
23
- <Route path="/rules" element={<RulesMap />} />
24
- <Route path="/standards" element={<Standards />} />
25
- <Route path="/workflows" element={<Workflows />} />
26
- <Route path="/state" element={<LiveState />} />
27
- </Routes>
28
- </main>
29
- </div>
30
- );
31
- }
1
+ import { Routes, Route, Navigate } from 'react-router-dom';
2
+ import Sidebar from './components/Sidebar.jsx';
3
+ import AgentMap from './pages/AgentMap.jsx';
4
+ import PhasePipeline from './pages/PhasePipeline.jsx';
5
+ import HookInspector from './pages/HookInspector.jsx';
6
+ import SkillsLibrary from './pages/SkillsLibrary.jsx';
7
+ import RulesMap from './pages/RulesMap.jsx';
8
+ import Standards from './pages/Standards.jsx';
9
+ import LiveState from './pages/LiveState.jsx';
10
+
11
+ export default function App() {
12
+ return (
13
+ <div className="flex h-screen bg-gray-950 text-gray-100 overflow-hidden">
14
+ <Sidebar />
15
+ <main className="flex-1 overflow-auto">
16
+ <Routes>
17
+ <Route path="/" element={<Navigate to="/agents" replace />} />
18
+ <Route path="/agents" element={<AgentMap />} />
19
+ <Route path="/phases" element={<PhasePipeline />} />
20
+ <Route path="/hooks" element={<HookInspector />} />
21
+ <Route path="/skills" element={<SkillsLibrary />} />
22
+ <Route path="/rules" element={<RulesMap />} />
23
+ <Route path="/standards" element={<Standards />} />
24
+ <Route path="/state" element={<LiveState />} />
25
+ </Routes>
26
+ </main>
27
+ </div>
28
+ );
29
+ }
@@ -1,45 +1,44 @@
1
- import { NavLink } from 'react-router-dom';
2
-
3
- const NAV_ITEMS = [
4
- { to: '/agents', label: 'Agent Map', icon: '🗺️' },
5
- { to: '/phases', label: 'Phases', icon: '⚡' },
6
- { to: '/hooks', label: 'Hooks', icon: '🔗' },
7
- { to: '/skills', label: 'Skills', icon: '🧩' },
8
- { to: '/rules', label: 'Rules', icon: '📏' },
9
- { to: '/standards', label: 'Standards', icon: '📋' },
10
- { to: '/workflows', label: 'Workflows', icon: '🔄' },
11
- { to: '/state', label: 'Live State', icon: '📊' },
12
- ];
13
-
14
- export default function Sidebar() {
15
- return (
16
- <nav className="w-52 flex-shrink-0 bg-gray-900 border-r border-gray-800 flex flex-col">
17
- <div className="px-4 py-4 border-b border-gray-800">
18
- <h1 className="text-sm font-semibold text-gray-300 tracking-wide uppercase">
19
- morph-spec
20
- </h1>
21
- <p className="text-xs text-gray-500 mt-0.5">Dashboard</p>
22
- </div>
23
- <ul className="flex-1 py-2">
24
- {NAV_ITEMS.map(({ to, label, icon }) => (
25
- <li key={to}>
26
- <NavLink
27
- to={to}
28
- className={({ isActive }) =>
29
- [
30
- 'flex items-center gap-2.5 px-4 py-2 text-sm transition-colors',
31
- isActive
32
- ? 'bg-blue-900/40 border-r-2 border-blue-500 text-blue-300'
33
- : 'text-gray-400 hover:bg-gray-800 hover:text-gray-200',
34
- ].join(' ')
35
- }
36
- >
37
- <span className="text-base leading-none">{icon}</span>
38
- <span>{label}</span>
39
- </NavLink>
40
- </li>
41
- ))}
42
- </ul>
43
- </nav>
44
- );
45
- }
1
+ import { NavLink } from 'react-router-dom';
2
+
3
+ const NAV_ITEMS = [
4
+ { to: '/agents', label: 'Agent Map', icon: '🗺️' },
5
+ { to: '/phases', label: 'Phases', icon: '⚡' },
6
+ { to: '/hooks', label: 'Hooks', icon: '🔗' },
7
+ { to: '/skills', label: 'Skills', icon: '🧩' },
8
+ { to: '/rules', label: 'Rules', icon: '📏' },
9
+ { to: '/standards', label: 'Standards', icon: '📋' },
10
+ { to: '/state', label: 'Live State', icon: '📊' },
11
+ ];
12
+
13
+ export default function Sidebar() {
14
+ return (
15
+ <nav className="w-52 flex-shrink-0 bg-gray-900 border-r border-gray-800 flex flex-col">
16
+ <div className="px-4 py-4 border-b border-gray-800">
17
+ <h1 className="text-sm font-semibold text-gray-300 tracking-wide uppercase">
18
+ morph-spec
19
+ </h1>
20
+ <p className="text-xs text-gray-500 mt-0.5">Dashboard</p>
21
+ </div>
22
+ <ul className="flex-1 py-2">
23
+ {NAV_ITEMS.map(({ to, label, icon }) => (
24
+ <li key={to}>
25
+ <NavLink
26
+ to={to}
27
+ className={({ isActive }) =>
28
+ [
29
+ 'flex items-center gap-2.5 px-4 py-2 text-sm transition-colors',
30
+ isActive
31
+ ? 'bg-blue-900/40 border-r-2 border-blue-500 text-blue-300'
32
+ : 'text-gray-400 hover:bg-gray-800 hover:text-gray-200',
33
+ ].join(' ')
34
+ }
35
+ >
36
+ <span className="text-base leading-none">{icon}</span>
37
+ <span>{label}</span>
38
+ </NavLink>
39
+ </li>
40
+ ))}
41
+ </ul>
42
+ </nav>
43
+ );
44
+ }
@@ -1,21 +1,20 @@
1
- import { scanAgents } from './agents-scanner.js';
2
- import { scanPhases } from './phases-scanner.js';
3
- import { scanHooks } from './hooks-scanner.js';
4
- import { scanSkills } from './skills-scanner.js';
5
- import { scanStandards } from './standards-scanner.js';
6
- import { scanWorkflows } from './workflows-scanner.js';
7
- import { scanRules } from './rules-scanner.js';
8
- import { scanSquads } from './squads-scanner.js';
9
- import { scanRubrics } from './rubrics-scanner.js';
10
- import { scanState } from './state-scanner.js';
11
-
12
- export function scanAll(frameworkDir, projectDir) {
13
- const agents = scanAgents(frameworkDir);
14
- return {
15
- agents, phases: scanPhases(frameworkDir), hooks: scanHooks(frameworkDir),
16
- skills: scanSkills(frameworkDir), standards: scanStandards(frameworkDir, agents.list),
17
- workflows: scanWorkflows(frameworkDir), rules: scanRules(frameworkDir),
18
- squads: scanSquads(frameworkDir), rubrics: scanRubrics(frameworkDir),
19
- liveState: scanState(projectDir || process.cwd()),
20
- };
21
- }
1
+ import { scanAgents } from './agents-scanner.js';
2
+ import { scanPhases } from './phases-scanner.js';
3
+ import { scanHooks } from './hooks-scanner.js';
4
+ import { scanSkills } from './skills-scanner.js';
5
+ import { scanStandards } from './standards-scanner.js';
6
+ import { scanRules } from './rules-scanner.js';
7
+ import { scanSquads } from './squads-scanner.js';
8
+ import { scanRubrics } from './rubrics-scanner.js';
9
+ import { scanState } from './state-scanner.js';
10
+
11
+ export function scanAll(frameworkDir, projectDir) {
12
+ const agents = scanAgents(frameworkDir);
13
+ return {
14
+ agents, phases: scanPhases(frameworkDir), hooks: scanHooks(frameworkDir),
15
+ skills: scanSkills(frameworkDir), standards: scanStandards(frameworkDir, agents.list),
16
+ rules: scanRules(frameworkDir),
17
+ squads: scanSquads(frameworkDir), rubrics: scanRubrics(frameworkDir),
18
+ liveState: scanState(projectDir || process.cwd()),
19
+ };
20
+ }
@@ -144,11 +144,11 @@ export function buildAgentDigests(agentId) {
144
144
  }
145
145
 
146
146
  /**
147
- * Extract FR IDs from a task's title/body in tasks.md format.
148
- * Format: ### T001 — Title [FR001, FR003]
147
+ * Extract FR IDs referenced by a task. Reads `[FR001, FR003]` markers from the
148
+ * task title and any `FR...` references in the task description/body.
149
149
  *
150
150
  * @param {string} taskTitle - Task title that may contain [FR...] references
151
- * @param {string} [taskBlock] - Full task block for body references
151
+ * @param {string} [taskBlock] - Task description/body for further references
152
152
  * @returns {string[]}
153
153
  */
154
154
  export function extractFRIds(taskTitle, taskBlock) {
@@ -347,7 +347,7 @@ export async function generateFeatureContext(projectPath, featureName) {
347
347
  }
348
348
 
349
349
  try {
350
- const tasksPath = path.join(outputsDir, '2-plan', 'tasks.md');
350
+ const tasksPath = path.join(outputsDir, '2-plan', 'tasks.json');
351
351
  const tasksContent = await fs.readFile(tasksPath, 'utf8');
352
352
  tasks = JSON.parse(tasksContent).tasks || [];
353
353
  } catch (err) {
@@ -380,7 +380,6 @@ export async function generateFeatureContext(projectPath, featureName) {
380
380
  const data = {
381
381
  FEATURE_NAME: feature.name,
382
382
  PHASE: feature.phase,
383
- WORKFLOW_TYPE: feature.workflow || 'standard',
384
383
  STATUS: feature.status,
385
384
  COMPLEXITY: feature.complexity || 'medium',
386
385
  DESCRIPTION: feature.description || '',
@@ -460,7 +459,7 @@ export async function generateFeatureContext(projectPath, featureName) {
460
459
  PROPOSAL_PATH: getOutputPath(featureName, 'proposal'),
461
460
  SPEC_PATH: getOutputPath(featureName, 'spec'),
462
461
  CONTRACTS_PATH: getOutputPath(featureName, 'contracts'),
463
- TASKS_PATH: `.morph/features/${featureName}/2-plan/tasks.md`,
462
+ TASKS_PATH: `.morph/features/${featureName}/2-plan/tasks.json`,
464
463
  DECISIONS_PATH: getOutputPath(featureName, 'decisions'),
465
464
  RECAP_PATH: getOutputPath(featureName, 'recap'),
466
465
 
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * Implementation Mandate Generator
3
3
  *
4
- * Synthesizes all spec phase outputs (spec.md, contracts, plan.md, tasks.md,
5
- * decisions.md) into a single task-structured implementation mandate.
4
+ * Synthesizes the plan-phase outputs (spec.md, tasks.json, decisions.md) into
5
+ * a single task-structured implementation mandate. Contracts are read from
6
+ * spec.md (v5 keeps contracts inside the spec, not in separate files).
6
7
  *
7
8
  * The mandate is the primary context artifact for the implement phase —
8
9
  * each task section is self-contained with acceptance criteria, contracts,
@@ -11,17 +12,28 @@
11
12
 
12
13
  import { readFileSync, existsSync } from 'fs';
13
14
  import { join } from 'path';
15
+ import { validateTasksJson } from '../tasks/tasks-json-parser.js';
14
16
 
15
17
  /**
16
- * v5: parseTaskFromMarkdown lived in sprint-contract-generator. Inlined here.
18
+ * Resolve a single task object from parsed tasks.json data.
19
+ * FR references are extracted from the task `description` (v5 convention:
20
+ * tasks reference their FR(s) inline in the description).
21
+ *
22
+ * @param {object} tasksData - Parsed tasks.json structure
23
+ * @param {string} taskId - Task id (e.g. 'T1')
24
+ * @returns {{ id: string, title: string, description: string, frs: string[] }|null}
17
25
  */
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 };
26
+ function resolveTask(tasksData, taskId) {
27
+ const task = tasksData.tasks.find(t => t.id === taskId);
28
+ if (!task) return null;
29
+ const frRefs = [...(task.description || '').matchAll(/\bFR-?\d+\b/g)].map(m => m[0]);
30
+ return {
31
+ id: task.id,
32
+ title: task.title || '',
33
+ description: task.description || '',
34
+ frs: [...new Set(frRefs)],
35
+ files: Array.isArray(task.outputs) ? task.outputs : [],
36
+ };
25
37
  }
26
38
 
27
39
  /**
@@ -30,7 +42,7 @@ function parseTaskFromMarkdown(tasksContent, taskId) {
30
42
  * @param {string} specContent - Full spec.md content
31
43
  * @returns {Map<string, { title: string, content: string, criteria: string[] }>}
32
44
  */
33
- export function parseFRSections(specContent) {
45
+ function parseFRSections(specContent) {
34
46
  const sections = new Map();
35
47
  const frPattern = /^##\s+(FR\d+)[:\s—–-]+(.+?)$/gm;
36
48
  let match;
@@ -69,13 +81,14 @@ export function parseFRSections(specContent) {
69
81
  }
70
82
 
71
83
  /**
72
- * Extract contract types/interfaces from contracts file content.
73
- * Matches C# records, classes, interfaces and TypeScript types/interfaces.
84
+ * Extract contract types/interfaces from content. Matches C# records, classes,
85
+ * interfaces and TypeScript types/interfaces — v5 contracts are defined inside
86
+ * spec.md, so this scans the spec content.
74
87
  *
75
- * @param {string} contractsContent - Full contracts.cs or contracts.ts content
88
+ * @param {string} contractsContent - Content to scan for contract definitions
76
89
  * @returns {Map<string, string>} Map of type name → definition
77
90
  */
78
- export function parseContracts(contractsContent) {
91
+ function parseContracts(contractsContent) {
79
92
  const types = new Map();
80
93
 
81
94
  // C# sealed records: sealed record Foo(...)
@@ -109,52 +122,13 @@ export function parseContracts(contractsContent) {
109
122
  return types;
110
123
  }
111
124
 
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
125
  /**
152
126
  * Extract architectural constraints from decisions.md.
153
127
  *
154
128
  * @param {string} decisionsContent - Full decisions.md content
155
129
  * @returns {string[]} One-line constraint summaries
156
130
  */
157
- export function parseDecisions(decisionsContent) {
131
+ function parseDecisions(decisionsContent) {
158
132
  const constraints = [];
159
133
  const adrPattern = /^###?\s+(ADR-\d+)[:\s—–-]+(.+?)$/gm;
160
134
  let match;
@@ -167,21 +141,13 @@ export function parseDecisions(decisionsContent) {
167
141
  }
168
142
 
169
143
  /**
170
- * Extract all task IDs from tasks.md in order.
144
+ * Extract all task IDs from parsed tasks.json data, in declared order.
171
145
  *
172
- * @param {string} tasksContent - Full tasks.md content
173
- * @returns {string[]} Task IDs in order (e.g., ['T001', 'T002', ...])
146
+ * @param {object} tasksData - Parsed tasks.json structure
147
+ * @returns {string[]} Task IDs in order (e.g., ['T1', 'T2', ...])
174
148
  */
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;
149
+ function extractTaskIds(tasksData) {
150
+ return tasksData.tasks.map(t => t.id);
185
151
  }
186
152
 
187
153
  /**
@@ -227,29 +193,21 @@ export function generateMandate(projectPath, featureName) {
227
193
  const featureDir = join(projectPath, '.morph/features', featureName);
228
194
 
229
195
  // Read required files
230
- const tasksPath = join(featureDir, '2-plan/tasks.md');
196
+ const tasksPath = join(featureDir, '2-plan/tasks.json');
231
197
  const specPath = join(featureDir, '2-plan/spec.md');
232
198
 
233
199
  if (!existsSync(tasksPath) || !existsSync(specPath)) return null;
234
200
 
235
- const tasksContent = readFileSync(tasksPath, 'utf-8');
201
+ let tasksData;
202
+ try {
203
+ tasksData = JSON.parse(readFileSync(tasksPath, 'utf-8'));
204
+ } catch {
205
+ return null;
206
+ }
207
+ if (!validateTasksJson(tasksData).valid) return null;
236
208
  const specContent = readFileSync(specPath, 'utf-8');
237
209
 
238
210
  // 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
211
  const decisionsPath = join(featureDir, '2-plan/decisions.md');
254
212
  const decisionsContent = existsSync(decisionsPath) ? readFileSync(decisionsPath, 'utf-8') : '';
255
213
 
@@ -259,22 +217,21 @@ export function generateMandate(projectPath, featureName) {
259
217
  // Read project config
260
218
  let stack = '';
261
219
  let architecture = '';
262
- let workflow = '';
263
220
  try {
264
221
  const configPath = join(projectPath, '.morph/config/config.json');
265
222
  if (existsSync(configPath)) {
266
223
  const config = JSON.parse(readFileSync(configPath, 'utf-8'));
267
224
  stack = config?.project?.stack || '';
268
225
  architecture = config?.architecture?.style || '';
269
- workflow = config?.workflow || '';
270
226
  }
271
227
  } catch { /* ignore */ }
272
228
 
273
- // Parse all inputs
229
+ // Parse all inputs. v5: contracts live inside spec.md (no separate
230
+ // contracts.cs/.ts file) — parseContracts scans the spec content.
274
231
  const frSections = parseFRSections(specContent);
275
- const contracts = parseContracts(contractsContent);
232
+ const contracts = parseContracts(specContent);
276
233
  const decisions = parseDecisions(decisionsContent);
277
- const taskIds = extractTaskIds(tasksContent);
234
+ const taskIds = extractTaskIds(tasksData);
278
235
 
279
236
  // Build mandate
280
237
  const lines = [];
@@ -312,7 +269,7 @@ export function generateMandate(projectPath, featureName) {
312
269
 
313
270
  // Task sections
314
271
  for (const taskId of taskIds) {
315
- const task = parseTaskFromMarkdown(tasksContent, taskId);
272
+ const task = resolveTask(tasksData, taskId);
316
273
  if (!task) continue;
317
274
 
318
275
  lines.push('');
@@ -323,8 +280,7 @@ export function generateMandate(projectPath, featureName) {
323
280
  // Metadata line
324
281
  const metaParts = [];
325
282
  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(', ')}`);
283
+ if (task.files.length > 0) metaParts.push(`**Files:** ${task.files.join(', ')}`);
328
284
  if (metaParts.length > 0) lines.push(metaParts.join(' | '));
329
285
 
330
286
  // Acceptance criteria from FR sections