@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.
Files changed (114) hide show
  1. package/README.md +367 -366
  2. package/docs/CHEATSHEET.md +3 -3
  3. package/docs/QUICKSTART.md +4 -4
  4. package/framework/CLAUDE.md +13 -0
  5. package/framework/MORPH.md +57 -36
  6. package/framework/agents.json +132 -127
  7. package/framework/commands/morph-apply.md +4 -21
  8. package/framework/commands/morph-archive.md +1 -1
  9. package/framework/commands/morph-proposal.md +2 -0
  10. package/framework/hooks/claude-code/core/enforce-phase-writes.js +1 -1
  11. package/framework/hooks/claude-code/core/protect-spec-files.js +1 -1
  12. package/framework/hooks/claude-code/core/state-sync.js +212 -0
  13. package/framework/hooks/shared/phase-utils.js +5 -4
  14. package/framework/hooks/shared/state-reader.js +1 -1
  15. package/framework/phases.json +1 -1
  16. package/framework/rubrics/backend.json +41 -36
  17. package/framework/rules/ai-agents-standards.md +45 -15
  18. package/framework/rules/morph-workflow.md +15 -34
  19. package/framework/rules/neon-standards.md +25 -26
  20. package/framework/schemas/tasks.schema.json +35 -0
  21. package/framework/skills/core/morph-apply/SKILL.md +154 -0
  22. package/framework/skills/core/morph-brainstorming/SKILL.md +53 -30
  23. package/framework/skills/core/morph-implement/SKILL.md +25 -14
  24. package/framework/skills/core/morph-plan/SKILL.md +91 -23
  25. package/framework/skills/core/morph-proposal/SKILL.md +137 -0
  26. package/framework/skills/core/morph-review/SKILL.md +43 -7
  27. package/framework/skills/core/morph-stack-scan/SKILL.md +40 -1
  28. package/framework/standards/STANDARDS.json +1729 -1134
  29. package/framework/standards/ai-agents/agent-session.md +160 -0
  30. package/framework/standards/ai-agents/batching.md +193 -0
  31. package/framework/standards/ai-agents/context-providers.md +188 -0
  32. package/framework/standards/ai-agents/mcp-server.md +250 -0
  33. package/framework/standards/ai-agents/mcp-tools.md +197 -0
  34. package/framework/standards/ai-agents/middleware-patterns.md +207 -0
  35. package/framework/standards/ai-agents/modalities-image-gen.md +173 -0
  36. package/framework/standards/ai-agents/modalities-speech-to-text.md +171 -0
  37. package/framework/standards/ai-agents/modalities-text-to-speech.md +181 -0
  38. package/framework/standards/ai-agents/modalities-vision.md +201 -0
  39. package/framework/standards/ai-agents/multi-agent-patterns.md +155 -0
  40. package/framework/standards/ai-agents/production.md +114 -258
  41. package/framework/standards/ai-agents/prompts-in-db.md +214 -0
  42. package/framework/standards/ai-agents/providers/model-registry.md +158 -0
  43. package/framework/standards/ai-agents/rag-custom-pgvector.md +243 -0
  44. package/framework/standards/ai-agents/service-tier-flex.md +135 -0
  45. package/framework/standards/ai-agents/setup.md +142 -316
  46. package/framework/standards/ai-agents/structured-output.md +249 -0
  47. package/framework/standards/ai-agents/sweet-spot.md +109 -0
  48. package/framework/standards/ai-agents/workflows.md +114 -284
  49. package/framework/standards/architecture/vertical-slice/vertical-slice.md +433 -429
  50. package/framework/standards/backend/api/validation.md +92 -88
  51. package/framework/standards/backend/database/postgresql/database.md +336 -332
  52. package/framework/standards/backend/database/vector-search-rag.md +599 -541
  53. package/framework/standards/backend/dotnet/fluent-validation-vsa.md +4 -0
  54. package/framework/standards/backend/dotnet/result-pattern.md +4 -0
  55. package/framework/standards/backend/dotnet/scalar-ui.md +176 -172
  56. package/framework/standards/backend/dotnet/scrutor.md +4 -0
  57. package/framework/standards/backend/dotnet/vsa-handler-patterns.md +278 -274
  58. package/framework/standards/backend/integrations/asaas/asaas-api.md +260 -216
  59. package/framework/standards/backend/integrations/hangfire/hangfire-jobs.md +354 -350
  60. package/framework/standards/backend/integrations/neon-auth/neon-auth.md +30 -19
  61. package/framework/standards/backend/integrations/resend/resend-email.md +400 -385
  62. package/framework/standards/data/nosql/cache/redis.md +106 -97
  63. package/framework/standards/data/nosql/cosmos-db.md +127 -118
  64. package/framework/standards/data/vector-search/rag-chunking.md +113 -104
  65. package/framework/standards/frontend/design-system/ai-image-generation.md +4 -0
  66. package/framework/standards/frontend/design-system/animations.md +164 -160
  67. package/framework/standards/frontend/design-system/naming.md +68 -64
  68. package/framework/standards/frontend/gsap/gsap-core.md +14 -8
  69. package/framework/standards/frontend/gsap/gsap-plugins.md +26 -21
  70. package/framework/standards/frontend/gsap/gsap-scrolltrigger.md +9 -7
  71. package/framework/standards/frontend/nextjs/app-router.md +127 -123
  72. package/framework/standards/frontend/nextjs/components.md +136 -132
  73. package/framework/standards/frontend/nextjs/data-fetching.md +130 -126
  74. package/framework/standards/frontend/nextjs/forms.md +134 -128
  75. package/framework/standards/frontend/nextjs/motion-patterns.md +328 -322
  76. package/framework/standards/frontend/nextjs/naming-conventions.md +71 -67
  77. package/framework/standards/frontend/nextjs/nextjs-patterns.md +229 -225
  78. package/framework/standards/frontend/nextjs/project-structure.md +106 -102
  79. package/framework/standards/frontend/nextjs/state-management.md +76 -72
  80. package/framework/standards/frontend/nextjs/testing.md +115 -111
  81. package/framework/standards/infrastructure/docker/easypanel-deploy.md +196 -192
  82. package/framework/standards/infrastructure/neon/neon-pgrag.md +6 -2
  83. package/framework/standards/infrastructure/neon/neon-pgvector.md +4 -0
  84. package/framework/standards/infrastructure/neon/neon-setup.md +5 -1
  85. package/framework/standards/integration/mcp/mcp-tools.md +471 -462
  86. package/framework/templates/REGISTRY.json +1745 -1239
  87. package/framework/templates/code/dotnet/ai-agents/AgentBootstrap.cs.template +39 -0
  88. package/framework/templates/code/dotnet/ai-agents/EmbeddingService.cs.template +68 -0
  89. package/framework/templates/code/dotnet/ai-agents/ModelRegistry.cs.template +117 -0
  90. package/framework/templates/code/dotnet/ai-agents/PromptRepository.cs.template +95 -0
  91. package/framework/templates/code/dotnet/ai-agents/ResilientToolMiddleware.cs.template +61 -0
  92. package/framework/templates/code/dotnet/ai-agents/VectorSearchTool.cs.template +107 -0
  93. package/framework/templates/code/dotnet/ai-agents/model-registry.json.template +58 -0
  94. package/framework/templates/code/sql/ai-agents/agent-prompts-migration.sql.template +58 -0
  95. package/framework/templates/code/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -0
  96. package/framework/templates/docs/onboarding.md +4 -4
  97. package/framework/templates/tasks.template.json +17 -0
  98. package/package.json +89 -83
  99. package/src/commands/project/init.js +16 -0
  100. package/src/commands/validation/validate-feature.js +1 -1
  101. package/src/core/paths/output-schema.js +6 -6
  102. package/src/core/state/state-manager.js +1 -1
  103. package/src/core/templates/template-registry.js +289 -256
  104. package/src/lib/detectors/design-system-detector.js +1 -1
  105. package/src/lib/generators/context-generator.js +516 -518
  106. package/src/lib/generators/scaffold-generator.js +23 -26
  107. package/src/lib/installers/memory-installer.js +138 -0
  108. package/src/lib/phase-chain/phase-validator.js +4 -4
  109. package/src/lib/risk-detector.js +80 -0
  110. package/src/lib/tasks/tasks-json-parser.js +185 -0
  111. package/src/scripts/setup-infra.js +25 -0
  112. package/src/utils/hooks-installer.js +59 -10
  113. package/framework/standards/infrastructure/neon/neon-semantic-kernel.md +0 -310
  114. 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
- /** Template IDs for .NET slice-level files */
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
- { id: 'endpoint', file: 'endpoint.cs.hbs', suffix: 'Endpoint' },
73
- { id: 'handler', file: 'handler.cs.hbs', suffix: 'Handler' },
74
- { id: 'request', file: 'request.cs.hbs', suffix: 'Request' },
75
- { id: 'response', file: 'response.cs.hbs', suffix: 'Response' },
76
- { id: 'validator', file: 'validator.cs.hbs', suffix: 'Validator' },
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
- /** Template IDs for .NET feature-level files */
77
+ /** .NET feature-level files (entity, errors). */
80
78
  const DOTNET_FEATURE_TEMPLATES = [
81
- { id: 'entity', file: 'entity.cs.hbs', suffix: '', dir: 'entities' },
82
- { id: 'errors', file: 'errors.cs.hbs', suffix: 'Errors', dir: 'feature' },
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 = join(templateDir, tmpl.file);
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 = join(templateDir, tmpl.file);
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
- /** Template mappings for Next.js feature */
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
- file: 'page.tsx.hbs',
168
+ templateId: 'nextjs-page',
171
169
  outputFn: (kebab) => join('app', '(dashboard)', kebab, 'page.tsx'),
172
170
  },
173
171
  {
174
- file: 'client-component.tsx.hbs',
172
+ templateId: 'nextjs-client-component',
175
173
  outputFn: (kebab) => join('features', kebab, 'components', `${kebab}-list.tsx`),
176
174
  },
177
175
  {
178
- file: 'feature-form.tsx.hbs',
176
+ templateId: 'nextjs-feature-form',
179
177
  outputFn: (kebab) => join('features', kebab, 'components', `create-${kebab}-form.tsx`),
180
178
  },
181
179
  {
182
- file: 'use-feature.ts.hbs',
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 = join(templateDir, tmpl.file);
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-ui',
70
- 'mockups.md': '1-ui',
71
- 'components.md': '1-ui',
72
- 'flows.md': '1-ui',
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
- * These replace the protect-readonly-files.js PreToolUse hook,
26
- * using Claude Code's built-in permissions system instead.
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 MORPH_PERMISSIONS = [
29
- // Core framework state and readonly files
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.deny entries (idempotent)
217
+ // Install native permissions (idempotent — preserve user-added entries)
183
218
  settings.permissions = settings.permissions || {};
184
219
  settings.permissions.deny = settings.permissions.deny || [];
185
- // Remove previously managed permissions to avoid duplicates on re-install
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 => !MORPH_PERMISSIONS.includes(p)
229
+ p => !MORPH_DENY.includes(p) && !STALE_DENY.includes(p)
188
230
  );
189
- for (const perm of MORPH_PERMISSIONS) {
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