@polymorphism-tech/morph-spec 7.5.0 → 7.7.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 (61) hide show
  1. package/README.md +2 -2
  2. package/bin/morph-spec.js +18 -0
  3. package/claude-plugin.json +1 -1
  4. package/docs/CHEATSHEET.md +1 -1
  5. package/docs/QUICKSTART.md +1 -1
  6. package/framework/CLAUDE.md +12 -4
  7. package/framework/MORPH.md +18 -10
  8. package/framework/agents.json +13 -5
  9. package/framework/commands/morph-preflight.md +10 -5
  10. package/framework/hooks/README.md +8 -10
  11. package/framework/hooks/claude-code/core/gate-guard.js +36 -16
  12. package/framework/hooks/claude-code/core/protect-spec-files.js +20 -5
  13. package/framework/hooks/claude-code/core/tasks-json-guard.js +81 -0
  14. package/framework/hooks/shared/tasks-validator.js +68 -0
  15. package/framework/mcp/registry.json +5 -48
  16. package/framework/rubrics/backend.json +3 -3
  17. package/framework/rubrics/default.json +3 -3
  18. package/framework/rubrics/frontend.json +3 -3
  19. package/framework/rules/infrastructure-standards.md +4 -4
  20. package/framework/skills/core/create-umbraco-lp/SKILL.md +22 -2
  21. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +84 -5
  22. package/framework/skills/core/morph-apply/SKILL.md +53 -40
  23. package/framework/skills/core/morph-brainstorming/SKILL.md +7 -12
  24. package/framework/skills/core/morph-eval/SKILL.md +10 -8
  25. package/framework/skills/core/morph-implement/SKILL.md +27 -21
  26. package/framework/skills/core/morph-plan/SKILL.md +12 -21
  27. package/framework/skills/core/morph-proposal/SKILL.md +3 -8
  28. package/framework/skills/core/morph-review/SKILL.md +30 -14
  29. package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
  30. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  31. package/framework/standards/STANDARDS.json +80 -8
  32. package/framework/standards/ai-agents/durable-workflows-hitl.md +191 -0
  33. package/framework/standards/ai-agents/rag-custom-pgvector.md +246 -243
  34. package/framework/standards/ai-agents/setup.md +3 -0
  35. package/framework/standards/ai-agents/vector-data-extensions.md +170 -0
  36. package/framework/standards/ai-agents/workflows.md +190 -187
  37. package/framework/standards/infrastructure/docker/{easypanel-deploy.md → coolify-deploy.md} +49 -20
  38. package/framework/standards/integration/mcp/mcp-tools.md +38 -43
  39. package/framework/templates/README.md +7 -7
  40. package/framework/templates/REGISTRY.json +14 -14
  41. package/framework/templates/context/CONTEXT.md +3 -3
  42. package/framework/templates/docs/proposal.md +2 -2
  43. package/framework/templates/frontend/nextjs/Dockerfile.nextjs.hbs +1 -1
  44. package/framework/templates/infrastructure/docker/coolify.template.json +49 -0
  45. package/framework/templates/infrastructure/github/README.md +6 -6
  46. package/framework/templates/infrastructure/github/workflows/deploy-coolify.yml.hbs +33 -0
  47. package/package.json +1 -1
  48. package/src/commands/project/doctor.js +36 -1
  49. package/src/commands/state/approve.js +29 -4
  50. package/src/commands/state/score.js +88 -0
  51. package/src/commands/tasks/dag.js +68 -0
  52. package/src/dashboard/scanner/rubrics-scanner.js +1 -1
  53. package/src/lib/generators/recap-generator.js +1 -1
  54. package/src/lib/tasks/dispatch-planner.js +201 -0
  55. package/src/lib/validators/score-aggregator.js +3 -3
  56. package/src/scripts/setup-infra.js +42 -0
  57. package/src/utils/agents-installer.js +142 -10
  58. package/src/utils/hooks-installer.js +13 -5
  59. package/framework/rubrics/eval-rubric.json +0 -66
  60. package/framework/templates/infrastructure/docker/easypanel.template.json +0 -46
  61. package/framework/templates/infrastructure/github/workflows/deploy-easypanel.yml.hbs +0 -31
@@ -4,6 +4,15 @@
4
4
  * Installs morph-spec domain personas as native Claude Code subagents
5
5
  * in .claude/agents/morph-{id}.md.
6
6
  *
7
+ * agents.json is the SINGLE SOURCE OF TRUTH (ADR-001, docs/specs/
8
+ * dispatchable-personas/decisions.md). The .md files are DERIVED artifacts:
9
+ * generated with a MANAGED header, regenerated on update/--reinstall, and
10
+ * drift-checked by `morph-spec doctor` (checkAgentsDrift). Never hand-edit
11
+ * them — that is exactly the v4 dual-source drift the v5 migration removed.
12
+ *
13
+ * Canonical naming: subagent_type = `morph-{id}` — the frontmatter `name`
14
+ * matches the filename 1:1, and the skills dispatch that exact type.
15
+ *
7
16
  * In v5 the harness has 7 domain personas (no tiers) — each persona is a
8
17
  * subagent the Specialist (Claude in implementer role) can dispatch when
9
18
  * working in that domain. The 3 roles (Orchestrator/Specialist/Evaluator)
@@ -14,6 +23,33 @@ import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, unlink
14
23
  import { join } from 'path';
15
24
  import { shouldInstall } from '../lib/stack-filter.js';
16
25
 
26
+ /** Default tool scope for domain personas (override per agent via agents.json `tools`). */
27
+ export const DEFAULT_AGENT_TOOLS = 'Read, Write, Edit, Bash, Glob, Grep';
28
+
29
+ /**
30
+ * Load the ids of MCP servers known to the framework registry
31
+ * (framework/mcp/registry.json). Used to validate agents.json `mcp` entries:
32
+ * only registry-known servers become tool grants (fail-open on any error).
33
+ *
34
+ * @param {string} frameworkDir
35
+ * @returns {Set<string>|null} null = no registry found (accept nothing)
36
+ */
37
+ export function loadKnownMcpIds(frameworkDir) {
38
+ try {
39
+ const registryPath = join(frameworkDir, 'mcp', 'registry.json');
40
+ if (!existsSync(registryPath)) return null;
41
+ const registry = JSON.parse(readFileSync(registryPath, 'utf-8'));
42
+ return new Set(Object.keys(registry.mcps ?? {}));
43
+ } catch {
44
+ return null;
45
+ }
46
+ }
47
+
48
+ const MANAGED_HEADER =
49
+ '<!-- MANAGED BY morph-spec — generated from .morph/framework/agents.json (single source of truth).\n' +
50
+ ' Do NOT edit by hand: changes are overwritten on `morph-spec update` / `setup-infra --reinstall`.\n' +
51
+ ' To change this persona, edit agents.json and regenerate. Drift is flagged by `morph-spec doctor`. -->';
52
+
17
53
  /**
18
54
  * Install morph-spec domain personas as native Claude Code subagents.
19
55
  *
@@ -37,6 +73,8 @@ export async function installAgents(projectDir, frameworkDir = 'framework', opti
37
73
  const targetDir = join(projectDir, '.claude', 'agents');
38
74
  mkdirSync(targetDir, { recursive: true });
39
75
 
76
+ const knownMcpIds = loadKnownMcpIds(frameworkDir);
77
+
40
78
  // Filter by stack
41
79
  const eligible = agents.filter(a => {
42
80
  const stack = a.stack ?? ['*'];
@@ -48,17 +86,20 @@ export async function installAgents(projectDir, frameworkDir = 'framework', opti
48
86
 
49
87
  let installed = 0;
50
88
  const installedFilenames = new Set();
89
+ const installedAgents = [];
51
90
 
52
91
  for (const agent of eligible) {
53
92
  const filename = `morph-${agent.id}.md`;
54
93
  const targetPath = join(targetDir, filename);
55
94
 
56
- const description = buildDescription(agent);
57
- const body = buildBody(agent);
58
- const frontmatter = buildFrontmatter(agent, description);
59
-
60
- writeFileSync(targetPath, `---\n${frontmatter}---\n\n${body}\n`, 'utf-8');
95
+ writeFileSync(targetPath, renderAgentMd(agent, knownMcpIds), 'utf-8');
61
96
  installedFilenames.add(filename);
97
+ installedAgents.push({
98
+ id: agent.id,
99
+ name: `morph-${agent.id}`,
100
+ title: agent.title ?? agent.id,
101
+ domains: agent.domains ?? [],
102
+ });
62
103
  installed++;
63
104
  }
64
105
 
@@ -69,15 +110,46 @@ export async function installAgents(projectDir, frameworkDir = 'framework', opti
69
110
  }
70
111
  }
71
112
 
72
- return { installed };
113
+ return { installed, agents: installedAgents };
73
114
  }
74
115
 
75
- function buildFrontmatter(agent, description) {
116
+ /**
117
+ * Render the full .md content for a persona — deterministic, so the same
118
+ * function serves the installer AND the doctor drift check (a hand-edited or
119
+ * stale file differs byte-for-byte from this output).
120
+ *
121
+ * @param {object} agent - agents.json entry with `id` merged in
122
+ * @param {Set<string>|null} [knownMcpIds] - registry-known MCP ids; agents.json
123
+ * `mcp` entries outside this set are ignored (null = ignore all)
124
+ * @returns {string} complete file content
125
+ */
126
+ export function renderAgentMd(agent, knownMcpIds = null) {
127
+ const description = buildDescription(agent);
128
+ const frontmatter = buildFrontmatter(agent, description, knownMcpIds);
129
+ const body = buildBody(agent);
130
+ return `---\n${frontmatter}---\n\n${MANAGED_HEADER}\n\n${body}\n`;
131
+ }
132
+
133
+ function buildToolsLine(agent, knownMcpIds) {
134
+ const base = agent.tools ?? DEFAULT_AGENT_TOOLS;
135
+ // Domain-relevant MCP servers (agents.json `mcp: [ids]`) become server-level
136
+ // grants (`mcp__{id}`). Only registry-known ids are emitted; in projects
137
+ // without that server configured the grant is inert.
138
+ const mcpGrants = (agent.mcp ?? [])
139
+ .filter(id => knownMcpIds?.has(id))
140
+ .map(id => `mcp__${id}`);
141
+ return [base, ...mcpGrants].join(', ');
142
+ }
143
+
144
+ function buildFrontmatter(agent, description, knownMcpIds) {
76
145
  const lines = [
77
- `name: ${agent.id}`,
146
+ // Canonical 1:1 naming: frontmatter name === filename === subagent_type.
147
+ `name: morph-${agent.id}`,
78
148
  `description: ${description}`,
79
- `model: sonnet`,
80
- `tools: Read, Write, Edit, Bash, Glob, Grep`,
149
+ // Domain implementers inherit the session model by default — a hardcoded
150
+ // downgrade here would silently lower quality. Override via agents.json.
151
+ `model: ${agent.model ?? 'inherit'}`,
152
+ `tools: ${buildToolsLine(agent, knownMcpIds)}`,
81
153
  `maxTurns: 20`,
82
154
  ];
83
155
  if (agent.color) lines.push(`color: ${agent.color}`);
@@ -171,6 +243,66 @@ export function checkMissingAgents(projectDir, frameworkDir, projectTags = []) {
171
243
  return { missing, installed: installedFiles.size, expected };
172
244
  }
173
245
 
246
+ /**
247
+ * Drift check between agents.json (source of truth) and the generated
248
+ * .claude/agents/morph-*.md files. Consumed by `morph-spec doctor`.
249
+ *
250
+ * - missing: eligible persona with no .md installed → run setup-infra --reinstall
251
+ * - stale: .md exists but differs from renderAgentMd() output (hand-edited
252
+ * or generated by an older CLI) → regenerate
253
+ * - orphans: morph-*.md files with no matching agents.json entry
254
+ *
255
+ * @param {string} projectDir
256
+ * @param {string} frameworkDir - directory containing agents.json
257
+ * @param {string[]} projectTags
258
+ * @returns {{ missing: string[], stale: string[], orphans: string[], expected: number }}
259
+ */
260
+ export function checkAgentsDrift(projectDir, frameworkDir, projectTags = []) {
261
+ const targetDir = join(projectDir, '.claude', 'agents');
262
+ const agentsJsonPath = join(frameworkDir, 'agents.json');
263
+ if (!existsSync(agentsJsonPath)) {
264
+ return { missing: [], stale: [], orphans: [], expected: 0 };
265
+ }
266
+
267
+ const agentsJson = JSON.parse(readFileSync(agentsJsonPath, 'utf-8'));
268
+ const agents = Object.entries(agentsJson.agents ?? {})
269
+ .filter(([key]) => !key.startsWith('_comment'))
270
+ .map(([id, data]) => ({ id, ...data }));
271
+
272
+ const knownMcpIds = loadKnownMcpIds(frameworkDir);
273
+ const knownFilenames = new Set(agents.map(a => `morph-${a.id}.md`));
274
+ const missing = [];
275
+ const stale = [];
276
+ let expected = 0;
277
+
278
+ for (const agent of agents) {
279
+ const stack = agent.stack ?? ['*'];
280
+ if (!shouldInstall(stack, projectTags)) continue;
281
+ expected++;
282
+ const filename = `morph-${agent.id}.md`;
283
+ const filePath = join(targetDir, filename);
284
+ if (!existsSync(filePath)) {
285
+ missing.push(filename);
286
+ continue;
287
+ }
288
+ const onDisk = readFileSync(filePath, 'utf-8').replace(/\r\n/g, '\n');
289
+ if (onDisk !== renderAgentMd(agent, knownMcpIds)) {
290
+ stale.push(filename);
291
+ }
292
+ }
293
+
294
+ const orphans = [];
295
+ if (existsSync(targetDir)) {
296
+ for (const file of readdirSync(targetDir)) {
297
+ if (file.startsWith('morph-') && file.endsWith('.md') && !knownFilenames.has(file)) {
298
+ orphans.push(file);
299
+ }
300
+ }
301
+ }
302
+
303
+ return { missing, stale, orphans, expected };
304
+ }
305
+
174
306
  /**
175
307
  * Compatibility export — kept so existing tests/CLI code that imports
176
308
  * formatStandardRef or parseSkillFile / collectSkillFiles still work,
@@ -102,14 +102,22 @@ const MORPH_HOOKS = [
102
102
  ]
103
103
  },
104
104
 
105
- // === PostToolUse: Write|Edit — typecheck (debounced, silent on success) ===
105
+ // === PostToolUse: Write|Edit — typecheck (debounced, silent on success) +
106
+ // tasks.json schema guard (validate-and-warn; status enum, effort enum) ===
106
107
  {
107
108
  event: 'PostToolUse',
108
109
  matcher: 'Write|Edit',
109
- hooks: [{
110
- type: 'command',
111
- command: 'node framework/hooks/claude-code/core/post-edit-typecheck.js'
112
- }]
110
+ hooks: [
111
+ {
112
+ type: 'command',
113
+ command: 'node framework/hooks/claude-code/core/post-edit-typecheck.js'
114
+ },
115
+ {
116
+ type: 'command',
117
+ command: 'node framework/hooks/claude-code/core/tasks-json-guard.js',
118
+ timeout: 3000
119
+ }
120
+ ]
113
121
  },
114
122
 
115
123
  // === PostToolUse: Write|Edit|AskUserQuestion — state-sync (autonomous state.json updates) ===
@@ -1,66 +0,0 @@
1
- {
2
- "id": "eval",
3
- "version": "1.0.0",
4
- "description": "Eval rubric for morph-eval system. Scores phase outputs across structural and semantic dimensions.",
5
- "categories": {
6
- "structural_compliance": {
7
- "weight": 0.25,
8
- "description": "Required sections, headers, formatting present in phase outputs",
9
- "dimensions": {
10
- "proposal": ["has_problem_statement", "has_proposed_solution", "min_length_300"],
11
- "spec": ["has_overview", "has_functional_requirements", "has_fr_sections", "has_data_model"],
12
- "tasks": ["has_task_headers", "has_fr_references", "no_duplicate_ids"],
13
- "contracts": ["has_type_definitions", "correct_naming_conventions"]
14
- }
15
- },
16
- "content_quality": {
17
- "weight": 0.30,
18
- "description": "LLM-as-judge: completeness, specificity, actionability of outputs",
19
- "dimensions": {
20
- "completeness": "Covers all aspects of the feature request",
21
- "specificity": "Requirements are concrete and testable, not vague",
22
- "actionability": "A developer could implement from this alone",
23
- "architecture_depth": "Component boundaries, data flow, error handling clear"
24
- }
25
- },
26
- "cross_phase_consistency": {
27
- "weight": 0.25,
28
- "description": "Alignment between spec, tasks, mandate, and contracts",
29
- "dimensions": {
30
- "fr_coverage": "Every spec FR referenced by at least one task",
31
- "mandate_integrity": "Mandate includes all tasks with acceptance criteria",
32
- "contract_alignment": "Spec Data Model entities appear in contracts",
33
- "dependency_ordering": "Task dependencies are topologically valid"
34
- }
35
- },
36
- "stack_appropriateness": {
37
- "weight": 0.20,
38
- "description": "Outputs match the target technology stack",
39
- "dimensions": {
40
- "correct_language": "Contracts use the right language for the stack (TS vs CS)",
41
- "framework_patterns": "Architecture follows stack conventions (App Router, VSA, etc.)",
42
- "tool_chain": "Build/test commands match the stack"
43
- }
44
- }
45
- },
46
- "thresholds": {
47
- "approve": 8.0,
48
- "retry": 6.0,
49
- "max_iterations": 3,
50
- "regression_tolerance": 0.5
51
- },
52
- "scoring": {
53
- "structural": {
54
- "method": "binary_checklist",
55
- "description": "Each dimension is pass/fail. Score = (passed / total) * 10"
56
- },
57
- "llm_judge": {
58
- "method": "evidence_based",
59
- "description": "LLM scores 1-10 per dimension with evidence quotes and gap descriptions"
60
- },
61
- "cross_phase": {
62
- "method": "ratio_based",
63
- "description": "FR coverage ratio, mandate inclusion ratio. Score = ratio * 10"
64
- }
65
- }
66
- }
@@ -1,46 +0,0 @@
1
- {
2
- "name": "{{kebabCase FEATURE_NAME}}",
3
- "services": [
4
- {
5
- "type": "app",
6
- "data": {
7
- "projectName": "{{kebabCase FEATURE_NAME}}",
8
- "serviceName": "{{kebabCase FEATURE_NAME}}-app",
9
- "source": {
10
- "type": "github",
11
- "owner": "{{GITHUB_OWNER}}",
12
- "repo": "{{GITHUB_REPO}}",
13
- "ref": "main",
14
- "autoDeploy": true
15
- },
16
- "build": {
17
- "type": "dockerfile",
18
- "dockerfile": "Dockerfile"
19
- },
20
- "deploy": {
21
- "replicas": 1,
22
- "command": null,
23
- "zeroDowntime": true
24
- },
25
- "env": [
26
- {
27
- "key": "NODE_ENV",
28
- "value": "production"
29
- },
30
- {
31
- "key": "DATABASE_URL",
32
- "value": "${DATABASE_URL}"
33
- }
34
- ],
35
- "domains": [
36
- {
37
- "host": "{{kebabCase FEATURE_NAME}}.easypanel.host",
38
- "port": 3000,
39
- "https": true
40
- }
41
- ],
42
- "mounts": []
43
- }
44
- }
45
- ]
46
- }
@@ -1,31 +0,0 @@
1
- name: Deploy to EasyPanel
2
-
3
- on:
4
- workflow_call:
5
- inputs:
6
- environment:
7
- required: true
8
- type: string
9
- image:
10
- required: true
11
- type: string
12
-
13
- jobs:
14
- deploy:
15
- runs-on: ubuntu-latest
16
- environment: ${{{{ inputs.environment }}}}
17
- steps:
18
- - name: Trigger EasyPanel Deploy
19
- run: |
20
- curl -X POST ${{{{ secrets.EASYPANEL_WEBHOOK_URL }}}} \
21
- -H "Authorization: Bearer ${{{{ secrets.EASYPANEL_API_TOKEN }}}}" \
22
- -H "Content-Type: application/json" \
23
- -d '{
24
- "image": "${{{{ inputs.image }}}}",
25
- "environment": "${{{{ inputs.environment }}}}"
26
- }'
27
-
28
- - name: Health Check
29
- run: |
30
- sleep 30 # Wait for deployment
31
- curl --retry 10 --retry-delay 5 --retry-connrefused ${{{{ secrets.APP_URL }}}}/health