@polymorphism-tech/morph-spec 7.1.1 → 7.3.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/README.md +2 -2
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +4 -2
- package/framework/MORPH.md +3 -3
- package/framework/commands/morph-apply.md +1 -1
- package/framework/commands/morph-proposal.md +1 -1
- package/framework/hooks/README.md +1 -1
- package/framework/hooks/claude-code/core/protect-spec-files.js +193 -72
- package/framework/hooks/claude-code/core/validate-completion.js +136 -136
- package/framework/hooks/shared/phase-utils.js +10 -0
- package/framework/phases.json +1 -1
- package/framework/rules/morph-workflow.md +16 -9
- package/framework/skills/README.md +1 -1
- package/framework/skills/core/morph-apply/SKILL.md +1 -0
- package/framework/skills/core/morph-brainstorming/SKILL.md +1 -0
- package/framework/skills/core/morph-eval/SKILL.md +1 -0
- package/framework/skills/core/morph-implement/SKILL.md +7 -0
- package/framework/skills/core/morph-plan/SKILL.md +1 -0
- package/framework/skills/core/morph-proposal/SKILL.md +1 -0
- package/framework/skills/core/morph-review/SKILL.md +1 -0
- package/framework/skills/core/morph-stack-scan/SKILL.md +1 -0
- package/framework/skills/core/morph-standards/SKILL.md +1 -0
- package/framework/templates/README.md +708 -709
- package/framework/templates/REGISTRY.json +1 -20
- package/framework/templates/context/CONTEXT-FEATURE.md +276 -276
- package/framework/templates/docs/proposal.md +171 -171
- package/framework/templates/feature/recap.md +145 -145
- package/package.json +1 -1
- package/src/commands/project/doctor.js +941 -946
- package/src/commands/project/status.js +373 -376
- package/src/commands/validation/validate-feature.js +315 -287
- package/src/core/paths/output-schema.js +18 -1
- package/src/core/state/state-manager.js +900 -934
- package/src/dashboard/app/App.jsx +29 -31
- package/src/dashboard/app/components/Sidebar.jsx +44 -45
- package/src/dashboard/scanner/index.js +20 -21
- package/src/lib/context/context-builder.js +3 -3
- package/src/lib/generators/context-generator.js +2 -3
- package/src/lib/generators/mandate-generator.js +48 -92
- package/src/lib/generators/metadata-extractor.js +385 -386
- package/src/lib/generators/recap-generator.js +285 -285
- package/src/lib/validators/content/content-validator.js +89 -286
- package/src/lib/validators/content/index.js +0 -1
- package/framework/templates/feature/tasks.md +0 -199
- package/framework/workflows/README.md +0 -1041
- package/framework/workflows/configs/fast-track.json +0 -46
- package/framework/workflows/configs/full.json +0 -64
- package/framework/workflows/configs/quick.json +0 -32
- package/framework/workflows/configs/standard.json +0 -57
- package/framework/workflows/docs/STORY-DRIVEN-DEVELOPMENT.md +0 -392
- package/framework/workflows/docs/design-impl.md +0 -37
- package/framework/workflows/docs/enforcement-pipeline.md +0 -668
- package/framework/workflows/docs/fast-track.md +0 -29
- package/framework/workflows/docs/full-morph.md +0 -69
- package/framework/workflows/docs/standard.md +0 -49
- package/framework/workflows/docs/ui-refresh.md +0 -39
- package/src/commands/state/index.js +0 -7
- package/src/core/workflows/workflow-detector.js +0 -586
- package/src/core/workflows/workflow-router.js +0 -204
- package/src/dashboard/app/pages/Workflows.jsx +0 -346
- package/src/dashboard/scanner/workflows-scanner.js +0 -18
- package/src/lib/phase-chain/phase-validator.js +0 -359
- package/src/lib/validators/content/cross-phase-validator.js +0 -281
- 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
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
<
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
<Route path="/" element={<
|
|
19
|
-
<Route path="/
|
|
20
|
-
<Route path="/
|
|
21
|
-
<Route path="/
|
|
22
|
-
<Route path="/
|
|
23
|
-
<Route path="/
|
|
24
|
-
<Route path="/
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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: '/
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
</
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
<span
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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 {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
|
148
|
-
*
|
|
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] -
|
|
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.
|
|
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.
|
|
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
|
|
5
|
-
*
|
|
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
|
-
*
|
|
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
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
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
|
|
73
|
-
*
|
|
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 -
|
|
88
|
+
* @param {string} contractsContent - Content to scan for contract definitions
|
|
76
89
|
* @returns {Map<string, string>} Map of type name → definition
|
|
77
90
|
*/
|
|
78
|
-
|
|
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
|
-
|
|
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.
|
|
144
|
+
* Extract all task IDs from parsed tasks.json data, in declared order.
|
|
171
145
|
*
|
|
172
|
-
* @param {
|
|
173
|
-
* @returns {string[]} Task IDs in order (e.g., ['
|
|
146
|
+
* @param {object} tasksData - Parsed tasks.json structure
|
|
147
|
+
* @returns {string[]} Task IDs in order (e.g., ['T1', 'T2', ...])
|
|
174
148
|
*/
|
|
175
|
-
|
|
176
|
-
|
|
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.
|
|
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
|
-
|
|
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(
|
|
232
|
+
const contracts = parseContracts(specContent);
|
|
276
233
|
const decisions = parseDecisions(decisionsContent);
|
|
277
|
-
const taskIds = extractTaskIds(
|
|
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 =
|
|
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
|
-
|
|
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
|