@polymorphism-tech/morph-spec 6.0.0 → 7.1.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 +367 -366
- package/docs/CHEATSHEET.md +3 -3
- package/docs/QUICKSTART.md +4 -4
- package/framework/CLAUDE.md +13 -0
- package/framework/MORPH.md +57 -36
- package/framework/agents.json +132 -127
- package/framework/commands/morph-apply.md +4 -21
- package/framework/commands/morph-archive.md +1 -1
- package/framework/commands/morph-proposal.md +2 -0
- package/framework/hooks/claude-code/core/enforce-phase-writes.js +1 -1
- package/framework/hooks/claude-code/core/protect-spec-files.js +1 -1
- package/framework/hooks/claude-code/core/state-sync.js +212 -0
- package/framework/hooks/shared/phase-utils.js +5 -4
- package/framework/hooks/shared/state-reader.js +1 -1
- package/framework/phases.json +1 -1
- package/framework/rubrics/backend.json +41 -36
- package/framework/rules/ai-agents-standards.md +45 -15
- package/framework/rules/morph-workflow.md +15 -34
- package/framework/rules/neon-standards.md +25 -26
- package/framework/schemas/tasks.schema.json +35 -0
- package/framework/skills/core/morph-apply/SKILL.md +154 -0
- package/framework/skills/core/morph-brainstorming/SKILL.md +53 -30
- package/framework/skills/core/morph-implement/SKILL.md +25 -14
- package/framework/skills/core/morph-plan/SKILL.md +91 -23
- package/framework/skills/core/morph-proposal/SKILL.md +137 -0
- package/framework/skills/core/morph-review/SKILL.md +43 -7
- package/framework/skills/core/morph-stack-scan/SKILL.md +40 -1
- package/framework/standards/STANDARDS.json +1729 -1134
- package/framework/standards/ai-agents/agent-session.md +160 -0
- package/framework/standards/ai-agents/batching.md +193 -0
- package/framework/standards/ai-agents/context-providers.md +188 -0
- package/framework/standards/ai-agents/mcp-server.md +250 -0
- package/framework/standards/ai-agents/mcp-tools.md +197 -0
- package/framework/standards/ai-agents/middleware-patterns.md +207 -0
- package/framework/standards/ai-agents/modalities-image-gen.md +173 -0
- package/framework/standards/ai-agents/modalities-speech-to-text.md +171 -0
- package/framework/standards/ai-agents/modalities-text-to-speech.md +181 -0
- package/framework/standards/ai-agents/modalities-vision.md +201 -0
- package/framework/standards/ai-agents/multi-agent-patterns.md +155 -0
- package/framework/standards/ai-agents/production.md +114 -258
- package/framework/standards/ai-agents/prompts-in-db.md +214 -0
- package/framework/standards/ai-agents/providers/model-registry.md +158 -0
- package/framework/standards/ai-agents/rag-custom-pgvector.md +243 -0
- package/framework/standards/ai-agents/service-tier-flex.md +135 -0
- package/framework/standards/ai-agents/setup.md +142 -316
- package/framework/standards/ai-agents/structured-output.md +249 -0
- package/framework/standards/ai-agents/sweet-spot.md +109 -0
- package/framework/standards/ai-agents/workflows.md +114 -284
- package/framework/standards/architecture/vertical-slice/vertical-slice.md +433 -429
- package/framework/standards/backend/api/validation.md +92 -88
- package/framework/standards/backend/database/postgresql/database.md +336 -332
- package/framework/standards/backend/database/vector-search-rag.md +599 -541
- package/framework/standards/backend/dotnet/fluent-validation-vsa.md +4 -0
- package/framework/standards/backend/dotnet/result-pattern.md +4 -0
- package/framework/standards/backend/dotnet/scalar-ui.md +176 -172
- package/framework/standards/backend/dotnet/scrutor.md +4 -0
- package/framework/standards/backend/dotnet/vsa-handler-patterns.md +278 -274
- package/framework/standards/backend/integrations/asaas/asaas-api.md +260 -216
- package/framework/standards/backend/integrations/hangfire/hangfire-jobs.md +354 -350
- package/framework/standards/backend/integrations/neon-auth/neon-auth.md +30 -19
- package/framework/standards/backend/integrations/resend/resend-email.md +400 -385
- package/framework/standards/data/nosql/cache/redis.md +106 -97
- package/framework/standards/data/nosql/cosmos-db.md +127 -118
- package/framework/standards/data/vector-search/rag-chunking.md +113 -104
- package/framework/standards/frontend/design-system/ai-image-generation.md +4 -0
- package/framework/standards/frontend/design-system/animations.md +164 -160
- package/framework/standards/frontend/design-system/naming.md +68 -64
- package/framework/standards/frontend/gsap/gsap-core.md +14 -8
- package/framework/standards/frontend/gsap/gsap-plugins.md +26 -21
- package/framework/standards/frontend/gsap/gsap-scrolltrigger.md +9 -7
- package/framework/standards/frontend/nextjs/app-router.md +127 -123
- package/framework/standards/frontend/nextjs/components.md +136 -132
- package/framework/standards/frontend/nextjs/data-fetching.md +130 -126
- package/framework/standards/frontend/nextjs/forms.md +134 -128
- package/framework/standards/frontend/nextjs/motion-patterns.md +328 -322
- package/framework/standards/frontend/nextjs/naming-conventions.md +71 -67
- package/framework/standards/frontend/nextjs/nextjs-patterns.md +229 -225
- package/framework/standards/frontend/nextjs/project-structure.md +106 -102
- package/framework/standards/frontend/nextjs/state-management.md +76 -72
- package/framework/standards/frontend/nextjs/testing.md +115 -111
- package/framework/standards/infrastructure/docker/easypanel-deploy.md +196 -192
- package/framework/standards/infrastructure/neon/neon-pgrag.md +6 -2
- package/framework/standards/infrastructure/neon/neon-pgvector.md +4 -0
- package/framework/standards/infrastructure/neon/neon-setup.md +5 -1
- package/framework/standards/integration/mcp/mcp-tools.md +471 -462
- package/framework/templates/REGISTRY.json +1745 -1239
- package/framework/templates/code/dotnet/ai-agents/AgentBootstrap.cs.template +39 -0
- package/framework/templates/code/dotnet/ai-agents/EmbeddingService.cs.template +68 -0
- package/framework/templates/code/dotnet/ai-agents/ModelRegistry.cs.template +117 -0
- package/framework/templates/code/dotnet/ai-agents/PromptRepository.cs.template +95 -0
- package/framework/templates/code/dotnet/ai-agents/ResilientToolMiddleware.cs.template +61 -0
- package/framework/templates/code/dotnet/ai-agents/VectorSearchTool.cs.template +107 -0
- package/framework/templates/code/dotnet/ai-agents/model-registry.json.template +58 -0
- package/framework/templates/code/sql/ai-agents/agent-prompts-migration.sql.template +58 -0
- package/framework/templates/code/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -0
- package/framework/templates/docs/onboarding.md +4 -4
- package/framework/templates/tasks.template.json +17 -0
- package/package.json +89 -83
- package/src/commands/project/init.js +16 -0
- package/src/commands/validation/validate-feature.js +1 -1
- package/src/core/paths/output-schema.js +6 -6
- package/src/core/state/state-manager.js +1 -1
- package/src/core/templates/template-registry.js +289 -256
- package/src/lib/detectors/design-system-detector.js +1 -1
- package/src/lib/generators/context-generator.js +516 -518
- package/src/lib/generators/scaffold-generator.js +23 -26
- package/src/lib/installers/memory-installer.js +138 -0
- package/src/lib/phase-chain/phase-validator.js +4 -4
- package/src/lib/risk-detector.js +80 -0
- package/src/lib/tasks/tasks-json-parser.js +185 -0
- package/src/scripts/setup-infra.js +25 -0
- package/src/utils/hooks-installer.js +59 -10
- package/framework/standards/infrastructure/neon/neon-semantic-kernel.md +0 -310
- package/framework/templates/integrations/semantic-kernel-neon.cs +0 -125
|
@@ -9,14 +9,8 @@
|
|
|
9
9
|
|
|
10
10
|
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync } from 'fs';
|
|
11
11
|
import { join, dirname, basename } from 'path';
|
|
12
|
-
import { fileURLToPath } from 'url';
|
|
13
12
|
import { renderTemplate } from '../../core/templates/template-renderer.js';
|
|
14
|
-
|
|
15
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
16
|
-
const __dirname = dirname(__filename);
|
|
17
|
-
|
|
18
|
-
/** Framework templates root (relative to this file) */
|
|
19
|
-
const FRAMEWORK_TEMPLATES_DIR = join(__dirname, '../../../framework/templates');
|
|
13
|
+
import { resolveFrameworkTemplatePath } from '../../core/templates/template-registry.js';
|
|
20
14
|
|
|
21
15
|
/**
|
|
22
16
|
* Convert kebab-case to PascalCase.
|
|
@@ -67,19 +61,23 @@ export function normalizeNamespaceToKebab(ns) {
|
|
|
67
61
|
// .NET VSA Scaffold
|
|
68
62
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
69
63
|
|
|
70
|
-
|
|
64
|
+
// Each entry pairs a REGISTRY.json template `id` with the generator-specific
|
|
65
|
+
// output shaping (class-name suffix, target directory). REGISTRY owns the
|
|
66
|
+
// template path; the generator owns where the rendered file lands.
|
|
67
|
+
|
|
68
|
+
/** .NET slice-level files (endpoint/handler/request/response/validator). */
|
|
71
69
|
const DOTNET_SLICE_TEMPLATES = [
|
|
72
|
-
{
|
|
73
|
-
{
|
|
74
|
-
{
|
|
75
|
-
{
|
|
76
|
-
{
|
|
70
|
+
{ templateId: 'dotnet-feature-endpoint', suffix: 'Endpoint' },
|
|
71
|
+
{ templateId: 'dotnet-feature-handler', suffix: 'Handler' },
|
|
72
|
+
{ templateId: 'dotnet-feature-request', suffix: 'Request' },
|
|
73
|
+
{ templateId: 'dotnet-feature-response', suffix: 'Response' },
|
|
74
|
+
{ templateId: 'dotnet-feature-validator', suffix: 'Validator' },
|
|
77
75
|
];
|
|
78
76
|
|
|
79
|
-
/**
|
|
77
|
+
/** .NET feature-level files (entity, errors). */
|
|
80
78
|
const DOTNET_FEATURE_TEMPLATES = [
|
|
81
|
-
{
|
|
82
|
-
{
|
|
79
|
+
{ templateId: 'dotnet-feature-entity', suffix: '', dir: 'entities' },
|
|
80
|
+
{ templateId: 'dotnet-feature-errors', suffix: 'Errors', dir: 'feature' },
|
|
83
81
|
];
|
|
84
82
|
|
|
85
83
|
/**
|
|
@@ -98,7 +96,6 @@ export function scaffoldDotnetSlice(options) {
|
|
|
98
96
|
|
|
99
97
|
const pascalFeature = toPascalCase(feature);
|
|
100
98
|
const pascalSlice = toPascalCase(sliceName);
|
|
101
|
-
const templateDir = join(FRAMEWORK_TEMPLATES_DIR, 'code/dotnet/feature');
|
|
102
99
|
|
|
103
100
|
const variables = {
|
|
104
101
|
PROJECT_NAME: normalizeNamespaceToKebab(namespace),
|
|
@@ -112,7 +109,7 @@ export function scaffoldDotnetSlice(options) {
|
|
|
112
109
|
|
|
113
110
|
// 1. Render slice-level templates
|
|
114
111
|
for (const tmpl of DOTNET_SLICE_TEMPLATES) {
|
|
115
|
-
const templatePath =
|
|
112
|
+
const templatePath = resolveFrameworkTemplatePath(tmpl.templateId);
|
|
116
113
|
const outputPath = join(
|
|
117
114
|
outputDir,
|
|
118
115
|
'Features',
|
|
@@ -133,7 +130,7 @@ export function scaffoldDotnetSlice(options) {
|
|
|
133
130
|
|
|
134
131
|
// 2. Render feature-level templates (skip if already exist)
|
|
135
132
|
for (const tmpl of DOTNET_FEATURE_TEMPLATES) {
|
|
136
|
-
const templatePath =
|
|
133
|
+
const templatePath = resolveFrameworkTemplatePath(tmpl.templateId);
|
|
137
134
|
let outputPath;
|
|
138
135
|
|
|
139
136
|
if (tmpl.dir === 'entities') {
|
|
@@ -164,22 +161,23 @@ export function scaffoldDotnetSlice(options) {
|
|
|
164
161
|
// Next.js Scaffold
|
|
165
162
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
166
163
|
|
|
167
|
-
|
|
164
|
+
// Each entry pairs a REGISTRY.json template `id` with where the rendered file
|
|
165
|
+
// lands. REGISTRY owns the template path; the generator owns the output path.
|
|
168
166
|
const NEXTJS_TEMPLATES = [
|
|
169
167
|
{
|
|
170
|
-
|
|
168
|
+
templateId: 'nextjs-page',
|
|
171
169
|
outputFn: (kebab) => join('app', '(dashboard)', kebab, 'page.tsx'),
|
|
172
170
|
},
|
|
173
171
|
{
|
|
174
|
-
|
|
172
|
+
templateId: 'nextjs-client-component',
|
|
175
173
|
outputFn: (kebab) => join('features', kebab, 'components', `${kebab}-list.tsx`),
|
|
176
174
|
},
|
|
177
175
|
{
|
|
178
|
-
|
|
176
|
+
templateId: 'nextjs-feature-form',
|
|
179
177
|
outputFn: (kebab) => join('features', kebab, 'components', `create-${kebab}-form.tsx`),
|
|
180
178
|
},
|
|
181
179
|
{
|
|
182
|
-
|
|
180
|
+
templateId: 'nextjs-use-feature',
|
|
183
181
|
outputFn: (kebab) => join('features', kebab, 'hooks', `use-${kebab}s.ts`),
|
|
184
182
|
},
|
|
185
183
|
];
|
|
@@ -197,7 +195,6 @@ export function scaffoldNextjsFeature(options) {
|
|
|
197
195
|
const { featureName, outputDir = process.cwd(), dryRun = false } = options;
|
|
198
196
|
|
|
199
197
|
const kebab = featureName.toLowerCase();
|
|
200
|
-
const templateDir = join(FRAMEWORK_TEMPLATES_DIR, 'frontend/nextjs');
|
|
201
198
|
|
|
202
199
|
// Next.js templates use camelCase variable name `featureName`
|
|
203
200
|
const variables = {
|
|
@@ -207,7 +204,7 @@ export function scaffoldNextjsFeature(options) {
|
|
|
207
204
|
const files = [];
|
|
208
205
|
|
|
209
206
|
for (const tmpl of NEXTJS_TEMPLATES) {
|
|
210
|
-
const templatePath =
|
|
207
|
+
const templatePath = resolveFrameworkTemplatePath(tmpl.templateId);
|
|
211
208
|
const relativePath = tmpl.outputFn(kebab);
|
|
212
209
|
const outputPath = join(outputDir, relativePath);
|
|
213
210
|
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory Installer — populates Claude Code persistent memory during morph-init.
|
|
3
|
+
*
|
|
4
|
+
* Writes two memory files (project + reference) plus an entry in MEMORY.md
|
|
5
|
+
* so future Claude Code sessions enter pre-contextualized about the
|
|
6
|
+
* morph-spec setup of this project.
|
|
7
|
+
*
|
|
8
|
+
* Memory location: ~/.claude/projects/<encoded-project-path>/memory/
|
|
9
|
+
*
|
|
10
|
+
* The path encoding mirrors Claude Code's own scheme: replace slashes and
|
|
11
|
+
* colons with dashes. Tests can override via MORPH_MEMORY_ROOT env var.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import fs from 'fs-extra';
|
|
15
|
+
import { join } from 'path';
|
|
16
|
+
import { homedir } from 'os';
|
|
17
|
+
|
|
18
|
+
function getMemoryRoot(projectPath) {
|
|
19
|
+
if (process.env.MORPH_MEMORY_ROOT) return process.env.MORPH_MEMORY_ROOT;
|
|
20
|
+
|
|
21
|
+
const encoded = projectPath.replace(/[:\\/]/g, '-').replace(/^-+/, '');
|
|
22
|
+
return join(homedir(), '.claude', 'projects', encoded, 'memory');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function projectMemoryContent(info) {
|
|
26
|
+
return `---
|
|
27
|
+
name: project-morph-spec
|
|
28
|
+
description: morph-spec project configuration, paths, slash commands, and trust mode
|
|
29
|
+
metadata:
|
|
30
|
+
type: project
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
# ${info.projectName} — morph-spec project
|
|
34
|
+
|
|
35
|
+
**Framework version:** ${info.frameworkVersion}
|
|
36
|
+
**Stack:** ${info.stack ?? 'default (nextjs + dotnet + VSA + MAF + Neon)'}
|
|
37
|
+
**Trust mode:** auto (gates auto-pass without risk signals)
|
|
38
|
+
|
|
39
|
+
## Key paths
|
|
40
|
+
|
|
41
|
+
- \`.morph/framework/MORPH.md\` — NLH charter (read once per session)
|
|
42
|
+
- \`.morph/framework/agents.json\` — 7 domain personas
|
|
43
|
+
- \`.morph/framework/standards/STANDARDS.json\` — standards index (load on demand)
|
|
44
|
+
- \`.morph/framework/schemas/tasks.schema.json\` — JSON Schema for tasks.json
|
|
45
|
+
- \`.morph/features/{feature}/\` — feature artifacts: 0-proposal / 1-design / 2-plan / 3-implement / 4-review
|
|
46
|
+
- \`.morph/context/\` — long-lived architectural context (design-system, decisions)
|
|
47
|
+
- \`.morph/state.json\` — feature state (managed by state-sync hook; Edit fallback allowed)
|
|
48
|
+
|
|
49
|
+
## Slash commands
|
|
50
|
+
|
|
51
|
+
| Command | Skill invoked |
|
|
52
|
+
|---------|---------------|
|
|
53
|
+
| \`/morph-proposal {feature}\` | \`Skill(morph-proposal)\` — brainstorming → Gate 1 → plan → Gate 2 |
|
|
54
|
+
| \`/morph-apply {feature}\` | \`Skill(morph-apply)\` — DAG-adaptive implement loop → review → Gate 3 |
|
|
55
|
+
| \`/morph-status [feature]\` | inspect state.json |
|
|
56
|
+
| \`/morph-archive {feature}\` | move completed feature to .morph/archive/ |
|
|
57
|
+
| \`/morph-preflight\` | pre-deploy validation |
|
|
58
|
+
| \`/morph-troubleshoot [erro]\` | diagnose .NET/Next.js errors |
|
|
59
|
+
|
|
60
|
+
## Core skills
|
|
61
|
+
|
|
62
|
+
\`morph-proposal\`, \`morph-apply\`, \`morph-brainstorming\`, \`morph-plan\`, \`morph-implement\`, \`morph-eval\`, \`morph-review\`, \`morph-stack-scan\`, \`morph-standards\`
|
|
63
|
+
|
|
64
|
+
**Why:** This project is managed by morph-spec — spec-driven autonomous development with trust-aware gates.
|
|
65
|
+
|
|
66
|
+
**How to apply:**
|
|
67
|
+
- When user mentions a feature, check \`.morph/features/\` first.
|
|
68
|
+
- When user types \`/morph-proposal\`, invoke \`Skill(morph-proposal)\` directly.
|
|
69
|
+
- In \`trust: "auto"\`, gates auto-pass; only AskUserQuestion on risk signals.
|
|
70
|
+
- Prefer morph-* skills over superpowers:* equivalents in this project.
|
|
71
|
+
`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const REFERENCE_MEMORY_CONTENT = `---
|
|
75
|
+
name: reference-morph-context
|
|
76
|
+
description: where to find long-lived morph-spec project context and schema files
|
|
77
|
+
metadata:
|
|
78
|
+
type: reference
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
# morph-spec context locations
|
|
82
|
+
|
|
83
|
+
- \`.morph/context/README.md\` — generated architecture overview
|
|
84
|
+
- \`.morph/context/design-system.md\` — design tokens and components (source of truth)
|
|
85
|
+
- \`.morph/framework/MORPH.md\` — NLH charter, Trust Mode rules, gates, skills table
|
|
86
|
+
- \`.morph/framework/standards/STANDARDS.json\` — index; load specific standard via \`path\` field
|
|
87
|
+
- \`.morph/framework/schemas/tasks.schema.json\` — tasks.json schema reference
|
|
88
|
+
- \`.morph/framework/phases.json\` — phase folder mapping and skill associations
|
|
89
|
+
- \`.morph/framework/agents.json\` — domain persona definitions (dotnet-senior, ef-modeler, etc.)
|
|
90
|
+
- \`.morph/framework/rubrics/\` — eval rubrics for morph-eval scoring
|
|
91
|
+
`;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Populate Claude Code persistent memory for a morph-spec project.
|
|
95
|
+
*
|
|
96
|
+
* Idempotent: re-running overwrites the memory files with current info
|
|
97
|
+
* but appends MEMORY.md index entries only if missing.
|
|
98
|
+
*
|
|
99
|
+
* @param {string} projectPath - Absolute path of the project root
|
|
100
|
+
* @param {object} info - Project facts to embed in the memory
|
|
101
|
+
* @param {string} info.projectName
|
|
102
|
+
* @param {string} info.frameworkVersion
|
|
103
|
+
* @param {string} [info.stack]
|
|
104
|
+
* @returns {Promise<{ projectMemoryPath: string, referenceMemoryPath: string, indexPath: string }>}
|
|
105
|
+
*/
|
|
106
|
+
export async function installProjectMemory(projectPath, info) {
|
|
107
|
+
const memoryRoot = getMemoryRoot(projectPath);
|
|
108
|
+
await fs.ensureDir(memoryRoot);
|
|
109
|
+
|
|
110
|
+
const projectMemoryPath = join(memoryRoot, 'project_morph_spec.md');
|
|
111
|
+
await fs.writeFile(projectMemoryPath, projectMemoryContent(info), 'utf8');
|
|
112
|
+
|
|
113
|
+
const referenceMemoryPath = join(memoryRoot, 'reference_morph_context.md');
|
|
114
|
+
await fs.writeFile(referenceMemoryPath, REFERENCE_MEMORY_CONTENT, 'utf8');
|
|
115
|
+
|
|
116
|
+
const indexPath = join(memoryRoot, 'MEMORY.md');
|
|
117
|
+
let indexContent = '';
|
|
118
|
+
if (await fs.pathExists(indexPath)) {
|
|
119
|
+
indexContent = await fs.readFile(indexPath, 'utf8');
|
|
120
|
+
} else {
|
|
121
|
+
indexContent = '# Memory Index\n';
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const projectLine = '- [morph-spec project](project_morph_spec.md) — stack, trust mode, slash commands, key paths';
|
|
125
|
+
const refLine = '- [morph-spec context](reference_morph_context.md) — where to find long-lived context';
|
|
126
|
+
|
|
127
|
+
if (!indexContent.includes('project_morph_spec.md')) {
|
|
128
|
+
if (!indexContent.endsWith('\n')) indexContent += '\n';
|
|
129
|
+
indexContent += projectLine + '\n';
|
|
130
|
+
}
|
|
131
|
+
if (!indexContent.includes('reference_morph_context.md')) {
|
|
132
|
+
indexContent += refLine + '\n';
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
await fs.writeFile(indexPath, indexContent, 'utf8');
|
|
136
|
+
|
|
137
|
+
return { projectMemoryPath, referenceMemoryPath, indexPath };
|
|
138
|
+
}
|
|
@@ -66,10 +66,10 @@ const OUTPUT_PHASE_MAP = {
|
|
|
66
66
|
'spec.md': '2-plan',
|
|
67
67
|
'mandate.md': '2-plan',
|
|
68
68
|
'decisions.md': '2-plan',
|
|
69
|
-
'design-system.md': '1-
|
|
70
|
-
'mockups.md': '1-
|
|
71
|
-
'components.md': '1-
|
|
72
|
-
'flows.md': '1-
|
|
69
|
+
'design-system.md': '1-design',
|
|
70
|
+
'mockups.md': '1-design',
|
|
71
|
+
'components.md': '1-design',
|
|
72
|
+
'flows.md': '1-design',
|
|
73
73
|
'tasks.md': '2-plan',
|
|
74
74
|
'recap.md': '3-implement',
|
|
75
75
|
'review-report.md': '4-review'
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Risk Detector — decides whether a gate should pause execution.
|
|
3
|
+
*
|
|
4
|
+
* In `trust: "auto"` mode (default), gates auto-pass UNLESS one of these
|
|
5
|
+
* risk signals triggers a forced pause. In `trust: "manual"` mode, gates
|
|
6
|
+
* always pause via AskUserQuestion.
|
|
7
|
+
*
|
|
8
|
+
* Signals checked (in order):
|
|
9
|
+
* - designSystemDivergence: boolean — conflicting design sources detected
|
|
10
|
+
* - standardsViolations: string[] — violations of mandate.md proibições
|
|
11
|
+
* - evalScore: number — morph-eval score (pause if < 8)
|
|
12
|
+
* - taskCount: number — total tasks (pause if > 20)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const SCOPE_TASK_THRESHOLD = 20;
|
|
16
|
+
const MIN_EVAL_SCORE = 8;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {object} config - .morph/config/config.json contents (uses `trust`)
|
|
20
|
+
* @param {object} signals
|
|
21
|
+
* @returns {{ pause: boolean, reason: string }}
|
|
22
|
+
*/
|
|
23
|
+
export function shouldPauseGate(config, signals) {
|
|
24
|
+
const trust = config?.trust ?? 'auto';
|
|
25
|
+
|
|
26
|
+
if (trust === 'manual') {
|
|
27
|
+
return { pause: true, reason: 'trust mode is manual' };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (signals.designSystemDivergence) {
|
|
31
|
+
return { pause: true, reason: 'design-system divergence detected' };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (Array.isArray(signals.standardsViolations) && signals.standardsViolations.length > 0) {
|
|
35
|
+
return {
|
|
36
|
+
pause: true,
|
|
37
|
+
reason: `standards violations detected: ${signals.standardsViolations.join(', ')}`,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (typeof signals.evalScore === 'number' && signals.evalScore < MIN_EVAL_SCORE) {
|
|
42
|
+
return {
|
|
43
|
+
pause: true,
|
|
44
|
+
reason: `eval score ${signals.evalScore} below threshold ${MIN_EVAL_SCORE}`,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (typeof signals.taskCount === 'number' && signals.taskCount > SCOPE_TASK_THRESHOLD) {
|
|
49
|
+
return {
|
|
50
|
+
pause: true,
|
|
51
|
+
reason: `scope of ${signals.taskCount} tasks exceeds threshold ${SCOPE_TASK_THRESHOLD}`,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return { pause: false, reason: '' };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Human-readable list of risk signals present (for logging/Gate summary).
|
|
60
|
+
* @returns {string[]}
|
|
61
|
+
*/
|
|
62
|
+
export function getRiskSignals(signals) {
|
|
63
|
+
const out = [];
|
|
64
|
+
if (signals.designSystemDivergence) out.push('design-system divergence');
|
|
65
|
+
if (Array.isArray(signals.standardsViolations) && signals.standardsViolations.length > 0) {
|
|
66
|
+
out.push(`standards violations: ${signals.standardsViolations.join(', ')}`);
|
|
67
|
+
}
|
|
68
|
+
if (typeof signals.evalScore === 'number' && signals.evalScore < MIN_EVAL_SCORE) {
|
|
69
|
+
out.push(`eval score ${signals.evalScore}/10`);
|
|
70
|
+
}
|
|
71
|
+
if (typeof signals.taskCount === 'number' && signals.taskCount > SCOPE_TASK_THRESHOLD) {
|
|
72
|
+
out.push(`${signals.taskCount} tasks (over threshold)`);
|
|
73
|
+
}
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export const RISK_CONSTANTS = {
|
|
78
|
+
SCOPE_TASK_THRESHOLD,
|
|
79
|
+
MIN_EVAL_SCORE,
|
|
80
|
+
};
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tasks-json-parser.js — read, validate, query, mutate tasks.json
|
|
3
|
+
*
|
|
4
|
+
* Replaces the prior Markdown-based tasks.md parser for new features.
|
|
5
|
+
* Legacy features that still use tasks.md are handled by task-parser.js.
|
|
6
|
+
*
|
|
7
|
+
* Uses a minimal hand-rolled JSON Schema validator (no AJV dep) — sufficient
|
|
8
|
+
* for the controlled schema in framework/schemas/tasks.schema.json.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import fs from 'fs-extra';
|
|
12
|
+
import { readFileSync, existsSync } from 'fs';
|
|
13
|
+
import { join, dirname } from 'path';
|
|
14
|
+
import { fileURLToPath } from 'url';
|
|
15
|
+
|
|
16
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
const SCHEMA_PATH = join(__dirname, '..', '..', '..', 'framework', 'schemas', 'tasks.schema.json');
|
|
18
|
+
|
|
19
|
+
let cachedSchema = null;
|
|
20
|
+
export function getSchema() {
|
|
21
|
+
if (cachedSchema) return cachedSchema;
|
|
22
|
+
cachedSchema = JSON.parse(readFileSync(SCHEMA_PATH, 'utf8'));
|
|
23
|
+
return cachedSchema;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const VALID_STATUS = ['pending', 'in_progress', 'done', 'skipped', 'blocked'];
|
|
27
|
+
const VALID_EFFORT = ['S', 'M', 'L', 'XL'];
|
|
28
|
+
const VALID_GROUP = ['backend', 'frontend', 'tests', 'infra', 'docs'];
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Validate a tasks.json structure.
|
|
32
|
+
* @returns {{ valid: boolean, errors: string[] }}
|
|
33
|
+
*/
|
|
34
|
+
export function validateTasksJson(data) {
|
|
35
|
+
const errors = [];
|
|
36
|
+
if (!data || typeof data !== 'object') {
|
|
37
|
+
errors.push('tasks.json must be an object');
|
|
38
|
+
return { valid: false, errors };
|
|
39
|
+
}
|
|
40
|
+
if (data.version !== '1.0') errors.push('version must be "1.0"');
|
|
41
|
+
if (typeof data.feature !== 'string' || !data.feature) {
|
|
42
|
+
errors.push('feature must be a non-empty string');
|
|
43
|
+
}
|
|
44
|
+
if (!Array.isArray(data.tasks)) {
|
|
45
|
+
errors.push('tasks must be an array');
|
|
46
|
+
return { valid: false, errors };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const ids = new Set();
|
|
50
|
+
for (const [i, task] of data.tasks.entries()) {
|
|
51
|
+
const ctx = `tasks[${i}]`;
|
|
52
|
+
if (!task.id || !/^T\d+$/.test(task.id)) {
|
|
53
|
+
errors.push(`${ctx}.id must match /^T\\d+$/`);
|
|
54
|
+
} else if (ids.has(task.id)) {
|
|
55
|
+
errors.push(`${ctx}.id duplicate: ${task.id}`);
|
|
56
|
+
}
|
|
57
|
+
ids.add(task.id);
|
|
58
|
+
|
|
59
|
+
if (typeof task.title !== 'string' || !task.title) errors.push(`${ctx}.title required`);
|
|
60
|
+
if (typeof task.description !== 'string' || !task.description) errors.push(`${ctx}.description required`);
|
|
61
|
+
if (!Array.isArray(task.dependencies)) {
|
|
62
|
+
errors.push(`${ctx}.dependencies must be array`);
|
|
63
|
+
} else {
|
|
64
|
+
for (const dep of task.dependencies) {
|
|
65
|
+
if (typeof dep !== 'string' || !/^T\d+$/.test(dep)) {
|
|
66
|
+
errors.push(`${ctx}.dependencies[${dep}] must match /^T\\d+$/`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (!VALID_EFFORT.includes(task.effort)) {
|
|
71
|
+
errors.push(`${ctx}.effort must be one of ${VALID_EFFORT.join(', ')}`);
|
|
72
|
+
}
|
|
73
|
+
if (typeof task.doneCriteria !== 'string' || !task.doneCriteria) {
|
|
74
|
+
errors.push(`${ctx}.doneCriteria required`);
|
|
75
|
+
}
|
|
76
|
+
if (!VALID_STATUS.includes(task.status)) {
|
|
77
|
+
errors.push(`${ctx}.status must be one of ${VALID_STATUS.join(', ')}`);
|
|
78
|
+
}
|
|
79
|
+
if (task.group !== undefined && !VALID_GROUP.includes(task.group)) {
|
|
80
|
+
errors.push(`${ctx}.group must be one of ${VALID_GROUP.join(', ')}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return { valid: errors.length === 0, errors };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Read and parse tasks.json from disk. Throws on validation errors.
|
|
89
|
+
* @param {string} filePath - Absolute path to tasks.json
|
|
90
|
+
* @returns {Promise<object>}
|
|
91
|
+
*/
|
|
92
|
+
export async function parseTasksJson(filePath) {
|
|
93
|
+
const data = await fs.readJson(filePath);
|
|
94
|
+
const { valid, errors } = validateTasksJson(data);
|
|
95
|
+
if (!valid) {
|
|
96
|
+
throw new Error(`Invalid tasks.json:\n ${errors.join('\n ')}`);
|
|
97
|
+
}
|
|
98
|
+
return data;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Synchronous variant — used by hooks.
|
|
103
|
+
*/
|
|
104
|
+
export function parseTasksJsonSync(filePath) {
|
|
105
|
+
if (!existsSync(filePath)) {
|
|
106
|
+
throw new Error(`tasks.json not found: ${filePath}`);
|
|
107
|
+
}
|
|
108
|
+
const data = JSON.parse(readFileSync(filePath, 'utf8'));
|
|
109
|
+
const { valid, errors } = validateTasksJson(data);
|
|
110
|
+
if (!valid) {
|
|
111
|
+
throw new Error(`Invalid tasks.json:\n ${errors.join('\n ')}`);
|
|
112
|
+
}
|
|
113
|
+
return data;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* @returns {object[]} tasks with status === 'pending'
|
|
118
|
+
*/
|
|
119
|
+
export function getPendingTasks(data) {
|
|
120
|
+
return data.tasks.filter(t => t.status === 'pending');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Tasks ready to run: pending AND all dependencies are 'done'.
|
|
125
|
+
*/
|
|
126
|
+
export function getReadyTasks(data) {
|
|
127
|
+
const doneIds = new Set(data.tasks.filter(t => t.status === 'done').map(t => t.id));
|
|
128
|
+
return data.tasks.filter(t =>
|
|
129
|
+
t.status === 'pending' && t.dependencies.every(dep => doneIds.has(dep))
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Mutate `data` to set task status. Returns `data` for chaining.
|
|
135
|
+
*/
|
|
136
|
+
export function updateTaskStatus(data, taskId, newStatus) {
|
|
137
|
+
if (!VALID_STATUS.includes(newStatus)) {
|
|
138
|
+
throw new Error(`Invalid status: ${newStatus}`);
|
|
139
|
+
}
|
|
140
|
+
const task = data.tasks.find(t => t.id === taskId);
|
|
141
|
+
if (!task) throw new Error(`Task not found: ${taskId}`);
|
|
142
|
+
task.status = newStatus;
|
|
143
|
+
return data;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Write tasks.json to disk. Validates before writing.
|
|
148
|
+
*/
|
|
149
|
+
export async function writeTasksJson(filePath, data) {
|
|
150
|
+
const { valid, errors } = validateTasksJson(data);
|
|
151
|
+
if (!valid) {
|
|
152
|
+
throw new Error(`Cannot write invalid tasks.json:\n ${errors.join('\n ')}`);
|
|
153
|
+
}
|
|
154
|
+
await fs.writeJson(filePath, data, { spaces: 2 });
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Group tasks by `group` field, falling back to 'backend' when absent.
|
|
159
|
+
* Returns `{ [group]: taskId[] }` shape matching the legacy parseAndGroupTasks output.
|
|
160
|
+
*/
|
|
161
|
+
export function groupTasks(data) {
|
|
162
|
+
const groups = {};
|
|
163
|
+
for (const task of data.tasks) {
|
|
164
|
+
const group = task.group || 'backend';
|
|
165
|
+
if (!groups[group]) groups[group] = [];
|
|
166
|
+
groups[group].push(task.id);
|
|
167
|
+
}
|
|
168
|
+
return groups;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Convert tasks.json to the legacy taskList shape used in state.json.
|
|
173
|
+
* Lets the rest of the codebase keep using feature.taskList while migration
|
|
174
|
+
* is in progress.
|
|
175
|
+
*/
|
|
176
|
+
export function toTaskList(data) {
|
|
177
|
+
return data.tasks.map(t => ({
|
|
178
|
+
id: t.id,
|
|
179
|
+
title: t.title,
|
|
180
|
+
status: t.status === 'done' ? 'completed' : t.status,
|
|
181
|
+
dependencies: [...t.dependencies],
|
|
182
|
+
files: t.outputs || [],
|
|
183
|
+
checkpoint: null,
|
|
184
|
+
}));
|
|
185
|
+
}
|
|
@@ -185,6 +185,7 @@ export async function setupInfra(targetPath, { _exec = execSync, reinstall = fal
|
|
|
185
185
|
const config = {
|
|
186
186
|
framework: 'global',
|
|
187
187
|
frameworkVersion: getInstalledCLIVersion(),
|
|
188
|
+
trust: 'auto',
|
|
188
189
|
project: {
|
|
189
190
|
name: dirName
|
|
190
191
|
}
|
|
@@ -243,6 +244,30 @@ export async function setupInfra(targetPath, { _exec = execSync, reinstall = fal
|
|
|
243
244
|
await copyFile(agentsSrc, agentsDest);
|
|
244
245
|
}
|
|
245
246
|
|
|
247
|
+
// --- 8b. Copy framework/MORPH.md → .morph/framework/MORPH.md ---
|
|
248
|
+
log('Step 8b: Copying MORPH.md (NLH charter)...');
|
|
249
|
+
const morphMdSrc = join(FRAMEWORK_DIR, 'MORPH.md');
|
|
250
|
+
const morphMdDest = join(frameworkDestDir, 'MORPH.md');
|
|
251
|
+
if (await pathExists(morphMdSrc)) {
|
|
252
|
+
await copyFile(morphMdSrc, morphMdDest);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// --- 8c. Copy framework/phases.json → .morph/framework/phases.json ---
|
|
256
|
+
log('Step 8c: Copying phases.json...');
|
|
257
|
+
const phasesSrc = join(FRAMEWORK_DIR, 'phases.json');
|
|
258
|
+
const phasesDest = join(frameworkDestDir, 'phases.json');
|
|
259
|
+
if (await pathExists(phasesSrc)) {
|
|
260
|
+
await copyFile(phasesSrc, phasesDest);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// --- 8d. Copy framework/schemas → .morph/framework/schemas (JSON schemas) ---
|
|
264
|
+
log('Step 8d: Copying schemas...');
|
|
265
|
+
const schemasSrc = join(FRAMEWORK_DIR, 'schemas');
|
|
266
|
+
const schemasDest = join(frameworkDestDir, 'schemas');
|
|
267
|
+
if (await pathExists(schemasSrc)) {
|
|
268
|
+
await copyDirectory(schemasSrc, schemasDest);
|
|
269
|
+
}
|
|
270
|
+
|
|
246
271
|
// --- 9. Copy framework/commands → .claude/commands ---
|
|
247
272
|
log('Step 9: Setting up Claude Code integration...');
|
|
248
273
|
const claudeDest = join(targetPath, '.claude');
|
|
@@ -22,13 +22,13 @@ const OLD_DISPATCH_COMMAND = 'node framework/hooks/agent-teams/dispatch.js';
|
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
24
|
* Native permissions.deny entries managed by MORPH-SPEC.
|
|
25
|
-
*
|
|
26
|
-
*
|
|
25
|
+
*
|
|
26
|
+
* state.json is NOT in deny because the agent edits it directly as a
|
|
27
|
+
* fallback to the state-sync hook (per v7 hybrid state model). The
|
|
28
|
+
* framework/ tree remains read-only — only the installer touches it.
|
|
27
29
|
*/
|
|
28
|
-
const
|
|
29
|
-
//
|
|
30
|
-
'Write(.morph/state.json)',
|
|
31
|
-
'Edit(.morph/state.json)',
|
|
30
|
+
const MORPH_DENY = [
|
|
31
|
+
// Framework files — installer-managed, immutable at runtime
|
|
32
32
|
'Write(.morph/framework/**)',
|
|
33
33
|
'Edit(.morph/framework/**)',
|
|
34
34
|
// .claude/CLAUDE.md (managed copy — source is framework/CLAUDE.md)
|
|
@@ -45,6 +45,30 @@ const MORPH_PERMISSIONS = [
|
|
|
45
45
|
'Edit(.claude/skills/**)',
|
|
46
46
|
];
|
|
47
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Native permissions.allow entries managed by MORPH-SPEC.
|
|
50
|
+
*
|
|
51
|
+
* Pre-authorizes operations the agent legitimately performs during the
|
|
52
|
+
* morph-spec flow, so the user does not get permission prompts on every
|
|
53
|
+
* proposal.md / spec.md / tasks.json write.
|
|
54
|
+
*/
|
|
55
|
+
const MORPH_ALLOW = [
|
|
56
|
+
// Feature artifacts — the agent's working directory during proposal/plan/implement/review
|
|
57
|
+
'Write(.morph/features/**)',
|
|
58
|
+
'Edit(.morph/features/**)',
|
|
59
|
+
// state.json — fallback path when state-sync hook can't cover a case
|
|
60
|
+
'Edit(.morph/state.json)',
|
|
61
|
+
// Long-lived architectural context — agent may reconcile during brainstorming
|
|
62
|
+
'Write(.morph/context/**)',
|
|
63
|
+
'Edit(.morph/context/**)',
|
|
64
|
+
// Folder creation under .morph
|
|
65
|
+
'Bash(mkdir:.morph/**)',
|
|
66
|
+
'Bash(ls:.morph/**)',
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
// Backwards-compat alias used by older tests
|
|
70
|
+
const MORPH_PERMISSIONS = MORPH_DENY;
|
|
71
|
+
|
|
48
72
|
/**
|
|
49
73
|
* All MORPH-SPEC hook definitions.
|
|
50
74
|
* Each entry maps to a Claude Code hook event.
|
|
@@ -88,6 +112,17 @@ const MORPH_HOOKS = [
|
|
|
88
112
|
}]
|
|
89
113
|
},
|
|
90
114
|
|
|
115
|
+
// === PostToolUse: Write|Edit|AskUserQuestion — state-sync (autonomous state.json updates) ===
|
|
116
|
+
{
|
|
117
|
+
event: 'PostToolUse',
|
|
118
|
+
matcher: 'Write|Edit|AskUserQuestion',
|
|
119
|
+
hooks: [{
|
|
120
|
+
type: 'command',
|
|
121
|
+
command: 'node framework/hooks/claude-code/core/state-sync.js',
|
|
122
|
+
timeout: 3000
|
|
123
|
+
}]
|
|
124
|
+
},
|
|
125
|
+
|
|
91
126
|
// === PostToolUse: All — loop detection ===
|
|
92
127
|
{
|
|
93
128
|
event: 'PostToolUse',
|
|
@@ -179,17 +214,31 @@ export async function installClaudeHooks(targetPath) {
|
|
|
179
214
|
installed++;
|
|
180
215
|
}
|
|
181
216
|
|
|
182
|
-
// Install native permissions
|
|
217
|
+
// Install native permissions (idempotent — preserve user-added entries)
|
|
183
218
|
settings.permissions = settings.permissions || {};
|
|
184
219
|
settings.permissions.deny = settings.permissions.deny || [];
|
|
185
|
-
|
|
220
|
+
settings.permissions.allow = settings.permissions.allow || [];
|
|
221
|
+
|
|
222
|
+
// Stale-entry cleanup: remove old morph-managed deny entries (e.g. state.json
|
|
223
|
+
// was previously in deny — v7 moves it to allow)
|
|
224
|
+
const STALE_DENY = [
|
|
225
|
+
'Write(.morph/state.json)',
|
|
226
|
+
'Edit(.morph/state.json)',
|
|
227
|
+
];
|
|
186
228
|
settings.permissions.deny = settings.permissions.deny.filter(
|
|
187
|
-
p => !
|
|
229
|
+
p => !MORPH_DENY.includes(p) && !STALE_DENY.includes(p)
|
|
188
230
|
);
|
|
189
|
-
for (const perm of
|
|
231
|
+
for (const perm of MORPH_DENY) {
|
|
190
232
|
settings.permissions.deny.push(perm);
|
|
191
233
|
}
|
|
192
234
|
|
|
235
|
+
settings.permissions.allow = settings.permissions.allow.filter(
|
|
236
|
+
p => !MORPH_ALLOW.includes(p)
|
|
237
|
+
);
|
|
238
|
+
for (const perm of MORPH_ALLOW) {
|
|
239
|
+
settings.permissions.allow.push(perm);
|
|
240
|
+
}
|
|
241
|
+
|
|
193
242
|
// JSON Schema for IDE auto-complete and validation
|
|
194
243
|
settings['$schema'] = 'https://json.schemastore.org/claude-code-settings.json';
|
|
195
244
|
|