@polymorphism-tech/morph-spec 4.22.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 -1198
- 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 -31
- package/framework/hooks/shared/state-reader.js +4 -5
- package/framework/{skills/level-0-meta/mcp-registry.json → mcp/registry.json} +254 -243
- package/framework/phases.json +41 -115
- 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/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 -152
- 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 -9
- 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-gemini-image/SKILL.md +0 -290
- 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 -401
- 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 -634
- 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 -54
- 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 -995
- 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
|
@@ -1,349 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* agents-manager.js — Core logic for agent CRUD operations
|
|
3
|
-
*
|
|
4
|
-
* Pure functions that accept agentsData object. File I/O only in
|
|
5
|
-
* loadAgentsFile / saveAgentsFile. All other functions are testable
|
|
6
|
-
* with fixture data.
|
|
7
|
-
*
|
|
8
|
-
* @module agents-manager
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { existsSync, readFileSync, writeFileSync, renameSync } from 'node:fs';
|
|
12
|
-
import { mkdirSync } from 'node:fs';
|
|
13
|
-
import { dirname } from 'node:path';
|
|
14
|
-
|
|
15
|
-
// ── Constants ──────────────────────────────────────────────────────────────────
|
|
16
|
-
|
|
17
|
-
/** Known valid phases for active_phases validation */
|
|
18
|
-
export const KNOWN_PHASES = [
|
|
19
|
-
'proposal', 'uiux', 'design',
|
|
20
|
-
'plan', 'tasks', 'implement', 'sync',
|
|
21
|
-
];
|
|
22
|
-
|
|
23
|
-
/** Tier labels for display and validation */
|
|
24
|
-
export const TIER_LABELS = {
|
|
25
|
-
1: 'orchestrator',
|
|
26
|
-
2: 'leader',
|
|
27
|
-
3: 'specialist',
|
|
28
|
-
4: 'validator',
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
/** Tier metadata key names in agents.json */
|
|
32
|
-
export const TIER_KEYS = {
|
|
33
|
-
1: 'tier_1_orchestrators',
|
|
34
|
-
2: 'tier_2_leaders',
|
|
35
|
-
3: 'tier_3_specialists',
|
|
36
|
-
4: 'tier_4_validators',
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
/** Agent ID pattern: kebab-case starting with a letter */
|
|
40
|
-
export const AGENT_ID_PATTERN = /^[a-z][a-z0-9-]*$/;
|
|
41
|
-
|
|
42
|
-
// ── Validation ─────────────────────────────────────────────────────────────────
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Validate agent fields.
|
|
46
|
-
* @param {string} id
|
|
47
|
-
* @param {object} fields
|
|
48
|
-
* @param {string[]} existingIds - IDs already in agents.json (for uniqueness check)
|
|
49
|
-
* @param {string[]} [knownGroups] - Known group names (for warnings)
|
|
50
|
-
* @param {object} [allAgents] - Full agents map (for circular reports_to check)
|
|
51
|
-
* @returns {{ valid: boolean, errors: string[], warnings: string[] }}
|
|
52
|
-
*/
|
|
53
|
-
export function validateAgent(id, fields, existingIds = [], knownGroups = [], allAgents = {}) {
|
|
54
|
-
const errors = [];
|
|
55
|
-
const warnings = [];
|
|
56
|
-
|
|
57
|
-
// ID validation
|
|
58
|
-
if (!AGENT_ID_PATTERN.test(id)) {
|
|
59
|
-
errors.push('Agent ID must be kebab-case (lowercase letters, numbers, hyphens) and start with a letter');
|
|
60
|
-
}
|
|
61
|
-
if (existingIds.includes(id)) {
|
|
62
|
-
errors.push(`Agent ID '${id}' is not unique — already exists in agents.json`);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Tier
|
|
66
|
-
const tier = fields.tier;
|
|
67
|
-
if (tier == null || !Number.isInteger(tier) || tier < 1 || tier > 4) {
|
|
68
|
-
errors.push('Tier must be 1 (orchestrator), 2 (leader), 3 (specialist), or 4 (validator)');
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// Title
|
|
72
|
-
if (!fields.title || typeof fields.title !== 'string' || fields.title.trim() === '') {
|
|
73
|
-
errors.push('Title is required');
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// Group
|
|
77
|
-
if (!fields.group || typeof fields.group !== 'string' || fields.group.trim() === '') {
|
|
78
|
-
errors.push('Group is required');
|
|
79
|
-
} else if (knownGroups.length > 0 && !knownGroups.includes(fields.group)) {
|
|
80
|
-
warnings.push(`Unknown group '${fields.group}' — known groups: ${knownGroups.join(', ')}`);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// Active phases
|
|
84
|
-
if (fields.active_phases) {
|
|
85
|
-
const phases = fields.active_phases;
|
|
86
|
-
if (!Array.isArray(phases)) {
|
|
87
|
-
errors.push('active_phases must be an array');
|
|
88
|
-
} else if (phases.length === 1 && phases[0] === '*') {
|
|
89
|
-
// wildcard — valid
|
|
90
|
-
} else {
|
|
91
|
-
for (const p of phases) {
|
|
92
|
-
if (!KNOWN_PHASES.includes(p)) {
|
|
93
|
-
errors.push(`Invalid phase '${p}' — valid: ${KNOWN_PHASES.join(', ')}`);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// reports_to — circular check
|
|
100
|
-
if (fields.reports_to) {
|
|
101
|
-
if (fields.reports_to === id) {
|
|
102
|
-
errors.push('Agent cannot report to itself');
|
|
103
|
-
} else if (fields.reports_to !== 'user' && allAgents && Object.keys(allAgents).length > 0) {
|
|
104
|
-
// Walk the chain to detect cycles
|
|
105
|
-
const visited = new Set([id]);
|
|
106
|
-
let current = fields.reports_to;
|
|
107
|
-
while (current && current !== 'user') {
|
|
108
|
-
if (visited.has(current)) {
|
|
109
|
-
errors.push(`Circular reports_to chain detected: ${[...visited, current].join(' \u2192 ')}`);
|
|
110
|
-
break;
|
|
111
|
-
}
|
|
112
|
-
visited.add(current);
|
|
113
|
-
current = allAgents[current]?.reports_to;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// spawn_prompt required for tier >= 3
|
|
119
|
-
if (Number.isInteger(tier) && tier >= 3) {
|
|
120
|
-
const hasPrompt = fields.teammate?.spawn_prompt ||
|
|
121
|
-
(typeof fields.spawn_prompt === 'string' && fields.spawn_prompt.trim());
|
|
122
|
-
if (!hasPrompt) {
|
|
123
|
-
errors.push('spawn_prompt is required for tier 3+ agents');
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
return { valid: errors.length === 0, errors, warnings };
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// ── Metadata Sync ──────────────────────────────────────────────────────────────
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* Recalculate total_agents and tier counts.
|
|
134
|
-
* @param {object} agentsData - Full agents.json data
|
|
135
|
-
* @returns {object} Updated agentsData (mutated in place + returned)
|
|
136
|
-
*/
|
|
137
|
-
export function syncMetadata(agentsData) {
|
|
138
|
-
const agents = agentsData.agents || {};
|
|
139
|
-
const entries = Object.values(agents);
|
|
140
|
-
|
|
141
|
-
agentsData.total_agents = entries.length;
|
|
142
|
-
agentsData.tiers = {
|
|
143
|
-
tier_1_orchestrators: entries.filter(a => a.tier === 1).length,
|
|
144
|
-
tier_2_leaders: entries.filter(a => a.tier === 2).length,
|
|
145
|
-
tier_3_specialists: entries.filter(a => a.tier === 3).length,
|
|
146
|
-
tier_4_validators: entries.filter(a => a.tier === 4).length,
|
|
147
|
-
};
|
|
148
|
-
|
|
149
|
-
return agentsData;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// ── List + Show ────────────────────────────────────────────────────────────────
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* List agents with optional filters.
|
|
156
|
-
* @param {object} agentsData
|
|
157
|
-
* @param {{ group?: string, tier?: number }} [filters]
|
|
158
|
-
* @returns {{ agents: Array, groupCounts: Record<string, number>, total: number }}
|
|
159
|
-
*/
|
|
160
|
-
export function listAgents(agentsData, filters = {}) {
|
|
161
|
-
const agents = agentsData.agents || {};
|
|
162
|
-
let entries = Object.entries(agents).map(([id, data]) => ({ id, ...data }));
|
|
163
|
-
|
|
164
|
-
if (filters.group) {
|
|
165
|
-
entries = entries.filter(a => a.group === filters.group);
|
|
166
|
-
}
|
|
167
|
-
if (filters.tier != null) {
|
|
168
|
-
entries = entries.filter(a => a.tier === filters.tier);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
entries.sort((a, b) => a.tier - b.tier || a.id.localeCompare(b.id));
|
|
172
|
-
|
|
173
|
-
// groupCounts over ALL agents (not filtered)
|
|
174
|
-
const groupCounts = {};
|
|
175
|
-
for (const a of Object.values(agents)) {
|
|
176
|
-
groupCounts[a.group] = (groupCounts[a.group] || 0) + 1;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
return {
|
|
180
|
-
agents: entries.map(a => ({
|
|
181
|
-
id: a.id,
|
|
182
|
-
tier: a.tier,
|
|
183
|
-
title: a.title,
|
|
184
|
-
group: a.group,
|
|
185
|
-
phases: Array.isArray(a.active_phases) ? a.active_phases.join(',') : '*',
|
|
186
|
-
keywords: (a.keywords || []).join(', ') || '\u2014',
|
|
187
|
-
})),
|
|
188
|
-
groupCounts,
|
|
189
|
-
total: entries.length,
|
|
190
|
-
};
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
/**
|
|
194
|
-
* Show detailed info for a single agent.
|
|
195
|
-
* @param {object} agentsData
|
|
196
|
-
* @param {string} id
|
|
197
|
-
* @returns {object|null}
|
|
198
|
-
*/
|
|
199
|
-
export function showAgent(agentsData, id) {
|
|
200
|
-
return agentsData.agents?.[id] ?? null;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// ── Mutations ──────────────────────────────────────────────────────────────────
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* Add a new agent.
|
|
207
|
-
* @param {object} agentsData
|
|
208
|
-
* @param {string} id
|
|
209
|
-
* @param {object} fields
|
|
210
|
-
* @returns {{ success: boolean, data?: object, changes?: string[], errors?: string[], warnings?: string[] }}
|
|
211
|
-
*/
|
|
212
|
-
export function addAgent(agentsData, id, fields) {
|
|
213
|
-
const existingIds = Object.keys(agentsData.agents || {});
|
|
214
|
-
const knownGroups = [...new Set(Object.values(agentsData.agents || {}).map(a => a.group))];
|
|
215
|
-
const validation = validateAgent(id, fields, existingIds, knownGroups, agentsData.agents);
|
|
216
|
-
|
|
217
|
-
if (!validation.valid) {
|
|
218
|
-
return { success: false, errors: validation.errors, warnings: validation.warnings };
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
const entry = {
|
|
222
|
-
tier: fields.tier,
|
|
223
|
-
title: fields.title,
|
|
224
|
-
group: fields.group,
|
|
225
|
-
active_phases: fields.active_phases || ['implement'],
|
|
226
|
-
reports_to: fields.reports_to || 'user',
|
|
227
|
-
domains: fields.domains || [],
|
|
228
|
-
keywords: fields.keywords || [],
|
|
229
|
-
validators: fields.validators || [],
|
|
230
|
-
standards: fields.standards || [],
|
|
231
|
-
};
|
|
232
|
-
|
|
233
|
-
if (fields.stacks) entry.stacks = fields.stacks;
|
|
234
|
-
if (fields.teammate) entry.teammate = fields.teammate;
|
|
235
|
-
|
|
236
|
-
agentsData.agents[id] = entry;
|
|
237
|
-
syncMetadata(agentsData);
|
|
238
|
-
|
|
239
|
-
return {
|
|
240
|
-
success: true,
|
|
241
|
-
data: agentsData,
|
|
242
|
-
changes: [`Added agent '${id}' (tier ${fields.tier}, group '${fields.group}')`],
|
|
243
|
-
warnings: validation.warnings,
|
|
244
|
-
};
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
/**
|
|
248
|
-
* Edit existing agent fields.
|
|
249
|
-
* @param {object} agentsData
|
|
250
|
-
* @param {string} id
|
|
251
|
-
* @param {object} changes
|
|
252
|
-
* @returns {{ success: boolean, data?: object, changes?: string[], errors?: string[] }}
|
|
253
|
-
*/
|
|
254
|
-
export function editAgent(agentsData, id, changes) {
|
|
255
|
-
const agent = agentsData.agents?.[id];
|
|
256
|
-
if (!agent) {
|
|
257
|
-
return { success: false, errors: [`Agent '${id}' not found`] };
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
const changedFields = [];
|
|
261
|
-
|
|
262
|
-
// Handle spawn_prompt shortcut -> nested teammate
|
|
263
|
-
if (changes.spawn_prompt) {
|
|
264
|
-
if (!agent.teammate) {
|
|
265
|
-
agent.teammate = { role: 'specialist', icon: '\u{1F527}', spawn_prompt: changes.spawn_prompt };
|
|
266
|
-
} else {
|
|
267
|
-
agent.teammate.spawn_prompt = changes.spawn_prompt;
|
|
268
|
-
}
|
|
269
|
-
changedFields.push('teammate.spawn_prompt');
|
|
270
|
-
delete changes.spawn_prompt;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
// Apply direct field changes
|
|
274
|
-
for (const [key, value] of Object.entries(changes)) {
|
|
275
|
-
if (value !== undefined && agent[key] !== value) {
|
|
276
|
-
changedFields.push(key);
|
|
277
|
-
agent[key] = value;
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
if (changedFields.length === 0) {
|
|
282
|
-
return { success: true, data: agentsData, changes: ['No changes detected'] };
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
syncMetadata(agentsData);
|
|
286
|
-
return { success: true, data: agentsData, changes: changedFields.map(f => `${f} updated`) };
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
/**
|
|
290
|
-
* Remove an agent.
|
|
291
|
-
* @param {object} agentsData
|
|
292
|
-
* @param {string} id
|
|
293
|
-
* @returns {{ success: boolean, data?: object, dependents?: string[], errors?: string[] }}
|
|
294
|
-
*/
|
|
295
|
-
export function removeAgent(agentsData, id) {
|
|
296
|
-
if (!agentsData.agents?.[id]) {
|
|
297
|
-
return { success: false, errors: [`Agent '${id}' not found`] };
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
// Find dependents
|
|
301
|
-
const dependents = Object.entries(agentsData.agents)
|
|
302
|
-
.filter(([aid, a]) => aid !== id && a.reports_to === id)
|
|
303
|
-
.map(([aid]) => aid);
|
|
304
|
-
|
|
305
|
-
delete agentsData.agents[id];
|
|
306
|
-
syncMetadata(agentsData);
|
|
307
|
-
|
|
308
|
-
return { success: true, data: agentsData, dependents };
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
// ── File I/O ───────────────────────────────────────────────────────────────────
|
|
312
|
-
|
|
313
|
-
/**
|
|
314
|
-
* Load and parse agents.json.
|
|
315
|
-
* @param {string} filePath
|
|
316
|
-
* @returns {object}
|
|
317
|
-
* @throws {Error} If file not found or invalid JSON
|
|
318
|
-
*/
|
|
319
|
-
export function loadAgentsFile(filePath) {
|
|
320
|
-
if (!existsSync(filePath)) {
|
|
321
|
-
throw new Error(`agents.json not found at ${filePath}. Run 'morph-spec doctor' to diagnose.`);
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
try {
|
|
325
|
-
const content = readFileSync(filePath, 'utf8');
|
|
326
|
-
return JSON.parse(content);
|
|
327
|
-
} catch (err) {
|
|
328
|
-
if (err instanceof SyntaxError) {
|
|
329
|
-
throw new Error(`Failed to parse agents.json: ${err.message}. Run 'morph-spec doctor' to diagnose.`);
|
|
330
|
-
}
|
|
331
|
-
throw err;
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
/**
|
|
336
|
-
* Write agents.json atomically (write to .tmp, then rename).
|
|
337
|
-
* @param {string} filePath
|
|
338
|
-
* @param {object} agentsData
|
|
339
|
-
*/
|
|
340
|
-
export function saveAgentsFile(filePath, agentsData) {
|
|
341
|
-
const dir = dirname(filePath);
|
|
342
|
-
if (!existsSync(dir)) {
|
|
343
|
-
mkdirSync(dir, { recursive: true });
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
const tmpPath = `${filePath}.tmp.${process.pid}`;
|
|
347
|
-
writeFileSync(tmpPath, JSON.stringify(agentsData, null, 2) + '\n', 'utf8');
|
|
348
|
-
renameSync(tmpPath, filePath);
|
|
349
|
-
}
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Build Runner — Resolves and executes the project's build command.
|
|
3
|
-
*
|
|
4
|
-
* Resolution order for build command:
|
|
5
|
-
* 1. .morph/config/config.json → project.buildCommand
|
|
6
|
-
* 2. Stack profile → profile.buildCommand
|
|
7
|
-
* 3. package.json → scripts.build → "npm run build"
|
|
8
|
-
* 4. tsconfig.json at root → "npx tsc --noEmit"
|
|
9
|
-
* 5. null (no build command detected)
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { execSync } from 'child_process';
|
|
13
|
-
import { existsSync, readFileSync } from 'fs';
|
|
14
|
-
import { join } from 'path';
|
|
15
|
-
import { getStackProfile } from '../stack/stack-profile.js';
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Resolve the build command for a project.
|
|
19
|
-
*
|
|
20
|
-
* @param {string} [projectPath] - Project root directory (defaults to '.')
|
|
21
|
-
* @returns {string | null} The build command to run, or null if none found
|
|
22
|
-
*/
|
|
23
|
-
export function resolveBuildCommand(projectPath = '.') {
|
|
24
|
-
// 1. Explicit override from .morph/config/config.json
|
|
25
|
-
const configPath = join(projectPath, '.morph', 'config', 'config.json');
|
|
26
|
-
if (existsSync(configPath)) {
|
|
27
|
-
try {
|
|
28
|
-
const config = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
29
|
-
const cmd = config?.project?.buildCommand;
|
|
30
|
-
if (cmd && typeof cmd === 'string') {
|
|
31
|
-
return cmd;
|
|
32
|
-
}
|
|
33
|
-
} catch {
|
|
34
|
-
// Malformed config — fall through to detection
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// 2. Stack profile default (only when an actual stack is configured)
|
|
39
|
-
const profile = getStackProfile(projectPath);
|
|
40
|
-
if (profile.stack !== 'unknown' && profile.buildCommand) {
|
|
41
|
-
return profile.buildCommand;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// 3. package.json with a build script
|
|
45
|
-
const packageJsonPath = join(projectPath, 'package.json');
|
|
46
|
-
if (existsSync(packageJsonPath)) {
|
|
47
|
-
try {
|
|
48
|
-
const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
|
|
49
|
-
if (pkg?.scripts?.build) {
|
|
50
|
-
return 'npm run build';
|
|
51
|
-
}
|
|
52
|
-
} catch {
|
|
53
|
-
// Malformed package.json — fall through
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// 4. tsconfig.json at root → generic type-check
|
|
58
|
-
if (existsSync(join(projectPath, 'tsconfig.json'))) {
|
|
59
|
-
return 'npx tsc --noEmit';
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// 5. Nothing found
|
|
63
|
-
return null;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Run the project's build check.
|
|
68
|
-
*
|
|
69
|
-
* @param {string} [projectPath] - Project root directory (defaults to '.')
|
|
70
|
-
* @param {object} [opts] - Optional overrides for testing
|
|
71
|
-
* @param {Function} [opts._execSync] - Override execSync for unit testing
|
|
72
|
-
* @returns {{ passed?: boolean, output?: string, exitCode?: number, command?: string, skipped?: boolean, reason?: string }}
|
|
73
|
-
*/
|
|
74
|
-
export function runBuildCheck(projectPath = '.', opts = {}) {
|
|
75
|
-
const cmd = resolveBuildCommand(projectPath);
|
|
76
|
-
|
|
77
|
-
if (!cmd) {
|
|
78
|
-
return { skipped: true, reason: 'No buildCommand configured and no known project file detected' };
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const exec = opts._execSync ?? execSync;
|
|
82
|
-
|
|
83
|
-
try {
|
|
84
|
-
const output = exec(cmd, {
|
|
85
|
-
cwd: projectPath,
|
|
86
|
-
timeout: 120_000,
|
|
87
|
-
encoding: 'utf8',
|
|
88
|
-
stdio: 'pipe'
|
|
89
|
-
});
|
|
90
|
-
return { passed: true, output: output ?? '', exitCode: 0, command: cmd };
|
|
91
|
-
} catch (err) {
|
|
92
|
-
return {
|
|
93
|
-
passed: false,
|
|
94
|
-
output: (err.stdout || '') + (err.stderr || ''),
|
|
95
|
-
exitCode: err.status ?? 1,
|
|
96
|
-
command: cmd
|
|
97
|
-
};
|
|
98
|
-
}
|
|
99
|
-
}
|
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Checkpoint Hooks — Orchestrates validation, build, and test phases at checkpoints.
|
|
3
|
-
*
|
|
4
|
-
* This is the module that task-manager.js imports at:
|
|
5
|
-
* import('../src/lib/checkpoints/checkpoint-hooks.js')
|
|
6
|
-
*
|
|
7
|
-
* All three phases run independently (failures don't short-circuit) so the user
|
|
8
|
-
* gets a complete picture of all issues in a single checkpoint report.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import chalk from 'chalk';
|
|
12
|
-
import { runValidation } from '../validators/validation-runner.js';
|
|
13
|
-
import { runBuildCheck } from '../build/build-runner.js';
|
|
14
|
-
import { runTestSuite } from '../tasks/test-runner.js';
|
|
15
|
-
import { persistCheckpointValidation } from '../validators/checkpoint-persistence.js';
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Determine whether a checkpoint should run based on task count and frequency.
|
|
19
|
-
*
|
|
20
|
-
* @param {number} tasksCompleted - Number of tasks completed so far
|
|
21
|
-
* @param {number} frequency - Run every N tasks (default: 3)
|
|
22
|
-
* @returns {boolean}
|
|
23
|
-
*/
|
|
24
|
-
export function shouldRunCheckpoint(tasksCompleted, frequency = 3) {
|
|
25
|
-
return tasksCompleted > 0 && tasksCompleted % frequency === 0;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Run all checkpoint hooks: static validation, build check, and test suite.
|
|
30
|
-
*
|
|
31
|
-
* @param {string} featureName - The feature being worked on
|
|
32
|
-
* @param {number} checkpointNum - Checkpoint sequence number
|
|
33
|
-
* @param {object} [opts] - Options
|
|
34
|
-
* @param {string} [opts.projectPath] - Project root (defaults to cwd)
|
|
35
|
-
* @returns {Promise<{ passed: boolean, summary: { errors: number, warnings: number }, results: object }>}
|
|
36
|
-
*/
|
|
37
|
-
export async function runCheckpointHooks(featureName, checkpointNum, opts = {}) {
|
|
38
|
-
const projectPath = opts.projectPath ?? process.cwd();
|
|
39
|
-
const results = {};
|
|
40
|
-
let totalErrors = 0;
|
|
41
|
-
let totalWarnings = 0;
|
|
42
|
-
|
|
43
|
-
// Phase 1: Static validators
|
|
44
|
-
console.log(chalk.gray('\n Phase 1/3: Running static validators...'));
|
|
45
|
-
try {
|
|
46
|
-
const valResult = await runValidation(projectPath, featureName, { verbose: false, mode: 'full' });
|
|
47
|
-
results.validators = {
|
|
48
|
-
passed: valResult.passed,
|
|
49
|
-
errors: valResult.errors,
|
|
50
|
-
warnings: valResult.warnings,
|
|
51
|
-
details: valResult.results,
|
|
52
|
-
scores: valResult.scores || null
|
|
53
|
-
};
|
|
54
|
-
totalErrors += valResult.errors;
|
|
55
|
-
totalWarnings += valResult.warnings;
|
|
56
|
-
} catch (err) {
|
|
57
|
-
results.validators = { passed: false, errors: 1, warnings: 0, details: {}, error: err.message };
|
|
58
|
-
totalErrors += 1;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// Phase 2: Build check
|
|
62
|
-
console.log(chalk.gray(' Phase 2/3: Running build check...'));
|
|
63
|
-
const buildResult = runBuildCheck(projectPath);
|
|
64
|
-
results.build = buildResult;
|
|
65
|
-
if (buildResult.skipped) {
|
|
66
|
-
console.log(chalk.gray(` Skipped: ${buildResult.reason}`));
|
|
67
|
-
} else if (buildResult.passed) {
|
|
68
|
-
console.log(chalk.green(` ✓ Build passed (${buildResult.command})`));
|
|
69
|
-
} else {
|
|
70
|
-
console.log(chalk.red(` ✗ Build failed (${buildResult.command})`));
|
|
71
|
-
totalErrors += 1;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Phase 3: Test suite
|
|
75
|
-
console.log(chalk.gray(' Phase 3/3: Running test suite...'));
|
|
76
|
-
const testResult = runTestSuite(projectPath);
|
|
77
|
-
results.tests = testResult;
|
|
78
|
-
if (testResult.skipped) {
|
|
79
|
-
console.log(chalk.gray(` Skipped: ${testResult.reason}`));
|
|
80
|
-
} else if (testResult.passed) {
|
|
81
|
-
console.log(chalk.green(' ✓ Tests passed'));
|
|
82
|
-
} else {
|
|
83
|
-
console.log(chalk.red(' ✗ Tests failed'));
|
|
84
|
-
totalErrors += 1;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// Persist results
|
|
88
|
-
const summaryText = [
|
|
89
|
-
`Validators: ${results.validators?.errors || 0} errors, ${results.validators?.warnings || 0} warnings`,
|
|
90
|
-
buildResult.skipped ? 'Build: skipped' : `Build: ${buildResult.passed ? 'passed' : 'FAILED'}`,
|
|
91
|
-
testResult.skipped ? 'Tests: skipped' : `Tests: ${testResult.passed ? 'passed' : 'FAILED'}`
|
|
92
|
-
].join(' | ');
|
|
93
|
-
|
|
94
|
-
try {
|
|
95
|
-
persistCheckpointValidation(projectPath, featureName, {
|
|
96
|
-
checkpointNum,
|
|
97
|
-
timestamp: new Date().toISOString(),
|
|
98
|
-
passed: totalErrors === 0,
|
|
99
|
-
validators: results,
|
|
100
|
-
summary: summaryText
|
|
101
|
-
});
|
|
102
|
-
} catch {
|
|
103
|
-
// Non-blocking — persistence failure shouldn't break the checkpoint
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
return {
|
|
107
|
-
passed: totalErrors === 0,
|
|
108
|
-
summary: { errors: totalErrors, warnings: totalWarnings },
|
|
109
|
-
results
|
|
110
|
-
};
|
|
111
|
-
}
|
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Sprint Contract Generator
|
|
3
|
-
*
|
|
4
|
-
* Generates sprint contracts from tasks.md + spec.md for the evaluator.
|
|
5
|
-
* Contracts define objective "definition of done" criteria.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
|
|
9
|
-
import { join } from 'path';
|
|
10
|
-
import { getBuildCommand } from '../../../framework/hooks/shared/build-check.js';
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Parse a task entry from tasks.md markdown.
|
|
14
|
-
*
|
|
15
|
-
* Expected format: ## T001: Task Title [FR001, FR003]
|
|
16
|
-
*
|
|
17
|
-
* @param {string} content - tasks.md content
|
|
18
|
-
* @param {string} taskId - Task ID (e.g., 'T001')
|
|
19
|
-
* @returns {{ id: string, title: string, frs: string[], description: string }|null}
|
|
20
|
-
*/
|
|
21
|
-
export function parseTaskFromMarkdown(content, taskId) {
|
|
22
|
-
const pattern = new RegExp(
|
|
23
|
-
`##\\s+${taskId}:\\s+(.+?)(?:\\s+\\[([^\\]]+)\\])?\\s*\\n([\\s\\S]*?)(?=\\n##\\s|$)`,
|
|
24
|
-
'i'
|
|
25
|
-
);
|
|
26
|
-
const match = content.match(pattern);
|
|
27
|
-
if (!match) return null;
|
|
28
|
-
|
|
29
|
-
const title = match[1].trim();
|
|
30
|
-
const frString = match[2] || '';
|
|
31
|
-
const description = match[3].trim();
|
|
32
|
-
|
|
33
|
-
const frs = frString
|
|
34
|
-
? frString.split(',').map(fr => fr.trim()).filter(Boolean)
|
|
35
|
-
: [];
|
|
36
|
-
|
|
37
|
-
return { id: taskId, title, frs, description };
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Generate a sprint contract for a task.
|
|
42
|
-
*
|
|
43
|
-
* @param {string} projectPath - Project root
|
|
44
|
-
* @param {string} featureName - Feature name
|
|
45
|
-
* @param {string} taskId - Task ID
|
|
46
|
-
* @returns {Object|null} Contract object or null if unable to generate
|
|
47
|
-
*/
|
|
48
|
-
export function generateSprintContract(projectPath, featureName, taskId) {
|
|
49
|
-
const featureDir = join(projectPath, '.morph/features', featureName);
|
|
50
|
-
const tasksPath = join(featureDir, '4-tasks/tasks.md');
|
|
51
|
-
|
|
52
|
-
if (!existsSync(tasksPath)) return null;
|
|
53
|
-
|
|
54
|
-
const tasksContent = readFileSync(tasksPath, 'utf-8');
|
|
55
|
-
const task = parseTaskFromMarkdown(tasksContent, taskId);
|
|
56
|
-
if (!task) return null;
|
|
57
|
-
|
|
58
|
-
// Read spec for FR-based criteria
|
|
59
|
-
const specPath = join(featureDir, '2-design/spec.md');
|
|
60
|
-
let definitionOfDone = [];
|
|
61
|
-
if (task.frs.length > 0 && existsSync(specPath)) {
|
|
62
|
-
const specContent = readFileSync(specPath, 'utf-8');
|
|
63
|
-
for (const fr of task.frs) {
|
|
64
|
-
const frPattern = new RegExp(`##\\s+${fr}[:\\s]([\\s\\S]*?)(?=\\n##\\s|$)`, 'i');
|
|
65
|
-
const frMatch = specContent.match(frPattern);
|
|
66
|
-
if (frMatch) {
|
|
67
|
-
const bullets = frMatch[1].match(/^[\s]*[-*]\s+(.+)/gm);
|
|
68
|
-
if (bullets) {
|
|
69
|
-
definitionOfDone.push(...bullets.map(b => b.replace(/^[\s]*[-*]\s+/, '').trim()));
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
if (definitionOfDone.length === 0) {
|
|
76
|
-
definitionOfDone = [task.description || `Complete ${task.title}`];
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// Read stack for build commands
|
|
80
|
-
let stack = '';
|
|
81
|
-
try {
|
|
82
|
-
const configPath = join(projectPath, '.morph/config/config.json');
|
|
83
|
-
if (existsSync(configPath)) {
|
|
84
|
-
const config = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
85
|
-
stack = config?.project?.stack || '';
|
|
86
|
-
}
|
|
87
|
-
} catch { /* ignore */ }
|
|
88
|
-
|
|
89
|
-
const hasTsConfig = existsSync(join(projectPath, 'tsconfig.json'));
|
|
90
|
-
const buildCmd = getBuildCommand(stack, hasTsConfig);
|
|
91
|
-
|
|
92
|
-
const contract = {
|
|
93
|
-
task: taskId,
|
|
94
|
-
title: task.title,
|
|
95
|
-
functionalRequirements: task.frs,
|
|
96
|
-
definitionOfDone,
|
|
97
|
-
verification: {
|
|
98
|
-
build: buildCmd || 'echo "No build command configured"',
|
|
99
|
-
test: stack.includes('dotnet') ? `dotnet test --filter ${task.title.replace(/\s+/g, '')}` : 'npm test',
|
|
100
|
-
e2e: null
|
|
101
|
-
},
|
|
102
|
-
maxAttempts: 3,
|
|
103
|
-
generatedAt: new Date().toISOString()
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
// Write contract to disk
|
|
107
|
-
const contractsDir = join(featureDir, '5-implement/contracts');
|
|
108
|
-
mkdirSync(contractsDir, { recursive: true });
|
|
109
|
-
const yamlContent = contractToYaml(contract);
|
|
110
|
-
writeFileSync(join(contractsDir, `${taskId}-contract.yaml`), yamlContent, 'utf-8');
|
|
111
|
-
|
|
112
|
-
return contract;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Simple YAML serializer for contracts (no dependency needed).
|
|
117
|
-
*
|
|
118
|
-
* @param {Object} contract - Contract object
|
|
119
|
-
* @returns {string} YAML string
|
|
120
|
-
*/
|
|
121
|
-
function contractToYaml(contract) {
|
|
122
|
-
const lines = [
|
|
123
|
-
`task: ${contract.task}`,
|
|
124
|
-
`title: "${contract.title}"`,
|
|
125
|
-
`functionalRequirements:`,
|
|
126
|
-
...contract.functionalRequirements.map(fr => ` - ${fr}`),
|
|
127
|
-
`definitionOfDone:`,
|
|
128
|
-
...contract.definitionOfDone.map(d => ` - "${d}"`),
|
|
129
|
-
`verification:`,
|
|
130
|
-
` build: "${contract.verification.build}"`,
|
|
131
|
-
` test: "${contract.verification.test}"`,
|
|
132
|
-
` e2e: ${contract.verification.e2e ? `"${contract.verification.e2e}"` : 'null'}`,
|
|
133
|
-
`maxAttempts: ${contract.maxAttempts}`,
|
|
134
|
-
`generatedAt: "${contract.generatedAt}"`,
|
|
135
|
-
];
|
|
136
|
-
return lines.join('\n') + '\n';
|
|
137
|
-
}
|