@polymorphism-tech/morph-spec 7.9.4 → 8.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 (212) hide show
  1. package/CLAUDE.md +2 -2
  2. package/README.md +31 -2
  3. package/bin/morph-spec.js +50 -2
  4. package/bin/validate.js +379 -379
  5. package/claude-plugin.json +1 -1
  6. package/docs/CHEATSHEET.md +38 -1
  7. package/docs/QUICKSTART.md +60 -1
  8. package/framework/CLAUDE.md +14 -3
  9. package/framework/MORPH.md +41 -5
  10. package/framework/agents.json +22 -3
  11. package/framework/commands/commit.md +166 -166
  12. package/framework/commands/morph-hotfix.md +39 -0
  13. package/framework/commands/morph-infra.md +215 -215
  14. package/framework/commands/morph-preflight.md +205 -205
  15. package/framework/commands/morph-troubleshoot.md +124 -124
  16. package/framework/hooks/README.md +25 -11
  17. package/framework/hooks/claude-code/core/gate-guard.js +4 -2
  18. package/framework/hooks/claude-code/core/loop-detect.js +2 -1
  19. package/framework/hooks/claude-code/core/post-edit-typecheck.js +14 -5
  20. package/framework/hooks/claude-code/core/tasks-json-guard.js +4 -2
  21. package/framework/hooks/claude-code/core/validate-completion.js +1 -1
  22. package/framework/hooks/claude-code/core/worktree-context.js +160 -0
  23. package/framework/hooks/claude-code/statusline.py +679 -679
  24. package/framework/hooks/claude-code/statusline.sh +11 -11
  25. package/framework/hooks/git/commit-msg/conventional-commits.sh +33 -33
  26. package/framework/hooks/git/pre-commit/agents.sh +25 -25
  27. package/framework/hooks/git/pre-commit/orchestrator.sh +64 -64
  28. package/framework/hooks/git/pre-commit/specs.sh +50 -50
  29. package/framework/hooks/git/pre-push/run-tests.sh +44 -44
  30. package/framework/hooks/shared/activity-logger.js +160 -160
  31. package/framework/hooks/shared/build-check.js +2 -2
  32. package/framework/hooks/shared/hook-response.js +17 -5
  33. package/framework/hooks/shared/payload-utils.js +42 -0
  34. package/framework/hooks/shared/state-reader.js +22 -2
  35. package/framework/hooks/shared/stdin-reader.js +26 -26
  36. package/framework/rubrics/backend.json +41 -41
  37. package/framework/rules/ai-agents-standards.md +45 -45
  38. package/framework/rules/dotnet-templates.md +86 -86
  39. package/framework/rules/frontend-standards.md +13 -13
  40. package/framework/rules/infrastructure-standards.md +27 -27
  41. package/framework/rules/neon-standards.md +25 -25
  42. package/framework/rules/nextjs-standards.md +20 -20
  43. package/framework/rules/nextjs-templates.md +40 -40
  44. package/framework/schemas/feature.schema.json +95 -1
  45. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  46. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  47. package/framework/skills/core/morph-apply/SKILL.md +49 -27
  48. package/framework/skills/core/morph-brainstorming/SKILL.md +9 -1
  49. package/framework/skills/core/morph-eval/SKILL.md +6 -1
  50. package/framework/skills/core/morph-hotfix/SKILL.md +134 -0
  51. package/framework/skills/core/morph-implement/SKILL.md +17 -22
  52. package/framework/skills/core/morph-plan/SKILL.md +5 -1
  53. package/framework/skills/core/morph-proposal/SKILL.md +24 -1
  54. package/framework/skills/core/morph-review/SKILL.md +13 -16
  55. package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
  56. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  57. package/framework/standards/ai-agents/agent-session.md +160 -160
  58. package/framework/standards/ai-agents/batching.md +193 -193
  59. package/framework/standards/ai-agents/context-providers.md +188 -188
  60. package/framework/standards/ai-agents/mcp-server.md +250 -250
  61. package/framework/standards/ai-agents/mcp-tools.md +197 -197
  62. package/framework/standards/ai-agents/middleware-patterns.md +207 -207
  63. package/framework/standards/ai-agents/modalities-image-gen.md +173 -173
  64. package/framework/standards/ai-agents/modalities-speech-to-text.md +171 -171
  65. package/framework/standards/ai-agents/modalities-text-to-speech.md +181 -181
  66. package/framework/standards/ai-agents/modalities-vision.md +201 -201
  67. package/framework/standards/ai-agents/multi-agent-patterns.md +155 -155
  68. package/framework/standards/ai-agents/production.md +275 -275
  69. package/framework/standards/ai-agents/prompts-in-db.md +214 -214
  70. package/framework/standards/ai-agents/providers/model-registry.md +158 -158
  71. package/framework/standards/ai-agents/service-tier-flex.md +135 -135
  72. package/framework/standards/ai-agents/structured-output.md +249 -249
  73. package/framework/standards/ai-agents/sweet-spot.md +109 -109
  74. package/framework/templates/.idea/morph-templates.xml +92 -0
  75. package/framework/templates/.vscode/morph-templates.code-snippets +186 -0
  76. package/framework/templates/IDE-SNIPPETS.md +262 -262
  77. package/framework/templates/REGISTRY.json +0 -62
  78. package/framework/templates/code/dotnet/abstractions/api-endpoint.cs +7 -7
  79. package/framework/templates/code/dotnet/ai-agents/AgentBootstrap.cs.template +39 -39
  80. package/framework/templates/code/dotnet/ai-agents/EmbeddingService.cs.template +68 -68
  81. package/framework/templates/code/dotnet/ai-agents/ModelRegistry.cs.template +117 -117
  82. package/framework/templates/code/dotnet/ai-agents/PromptRepository.cs.template +95 -95
  83. package/framework/templates/code/dotnet/ai-agents/ResilientToolMiddleware.cs.template +61 -61
  84. package/framework/templates/code/dotnet/ai-agents/VectorSearchTool.cs.template +107 -107
  85. package/framework/templates/code/dotnet/ai-agents/model-registry.json.template +58 -58
  86. package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +281 -281
  87. package/framework/templates/code/dotnet/database/migration.cs +83 -83
  88. package/framework/templates/code/dotnet/feature/endpoint.cs.hbs +21 -21
  89. package/framework/templates/code/dotnet/feature/request.cs.hbs +4 -4
  90. package/framework/templates/code/dotnet/feature/response.cs.hbs +4 -4
  91. package/framework/templates/code/dotnet/feature/validator.cs.hbs +15 -15
  92. package/framework/templates/code/sql/ai-agents/agent-prompts-migration.sql.template +58 -58
  93. package/framework/templates/code/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -91
  94. package/framework/templates/code/typescript/contracts.ts +167 -167
  95. package/framework/templates/docs/clarifications.md +253 -253
  96. package/framework/templates/docs/schema-analysis.md +119 -119
  97. package/framework/templates/docs/ui-components.md +52 -52
  98. package/framework/templates/docs/ui-design-system.md +76 -76
  99. package/framework/templates/docs/ui-flows.md +167 -167
  100. package/framework/templates/docs/ui-mockups.md +98 -98
  101. package/framework/templates/feature/decisions.md +122 -122
  102. package/framework/templates/frontend/nextjs/Dockerfile.nextjs.hbs +43 -43
  103. package/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -26
  104. package/framework/templates/frontend/nextjs/env.mjs.hbs +32 -32
  105. package/framework/templates/frontend/nextjs/feature-form.tsx.hbs +56 -56
  106. package/framework/templates/frontend/nextjs/page.tsx.hbs +22 -22
  107. package/framework/templates/frontend/nextjs/tsconfig.json.hbs +26 -26
  108. package/framework/templates/frontend/nextjs/use-feature.ts.hbs +54 -54
  109. package/framework/templates/infrastructure/docker/Dockerfile.template +58 -58
  110. package/framework/templates/infrastructure/docker/docker-compose.template.yml +50 -50
  111. package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
  112. package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
  113. package/framework/templates/infrastructure/github/README.md +56 -56
  114. package/framework/templates/infrastructure/github/actions/docker-build-push/action.yml.hbs +45 -45
  115. package/framework/templates/infrastructure/github/actions/health-check/action.yml.hbs +27 -27
  116. package/framework/templates/infrastructure/github/workflows/docker-build-push.yml.hbs +59 -59
  117. package/framework/templates/infrastructure/github/workflows/dotnet-build.yml.hbs +39 -39
  118. package/framework/templates/integrations/asaas-client.cs +387 -387
  119. package/framework/templates/integrations/asaas-webhook.cs +351 -351
  120. package/framework/templates/meta-prompts/fusion/fusion-agent.md +76 -76
  121. package/framework/templates/meta-prompts/fusion/fusion-aggregator.md +100 -100
  122. package/framework/templates/meta-prompts/validators/pre-commit-validator.md +95 -95
  123. package/framework/templates/project/validate.js +120 -120
  124. package/framework/templates/state.template.json +17 -17
  125. package/framework/templates/ui/design-system.css +226 -226
  126. package/package.json +1 -1
  127. package/src/commands/dashboard/dashboard.js +53 -53
  128. package/src/commands/index.js +13 -13
  129. package/src/commands/mcp/mcp-setup.js +519 -519
  130. package/src/commands/project/checkpoint.js +209 -209
  131. package/src/commands/project/doctor.js +4 -2
  132. package/src/commands/project/init.js +46 -2
  133. package/src/commands/project/update.js +5 -3
  134. package/src/commands/project/worktree.js +1005 -46
  135. package/src/commands/state/advance.js +17 -2
  136. package/src/commands/state/approve.js +18 -1
  137. package/src/commands/state/create.js +158 -14
  138. package/src/commands/state/retype.js +126 -0
  139. package/src/commands/state/score.js +4 -0
  140. package/src/commands/tasks/dag.js +32 -0
  141. package/src/commands/validation/index.js +1 -0
  142. package/src/commands/validation/validate-feature.js +33 -5
  143. package/src/commands/validation/verify.js +190 -0
  144. package/src/core/state/phase-state-machine.js +197 -197
  145. package/src/core/state/state-manager.js +35 -10
  146. package/src/core/templates/template-renderer.js +477 -477
  147. package/src/core/work-types.js +79 -0
  148. package/src/dashboard/app/components/DetailPanel.jsx +11 -11
  149. package/src/dashboard/app/components/FlowGraph.jsx +17 -17
  150. package/src/dashboard/app/components/SearchFilter.jsx +22 -22
  151. package/src/dashboard/app/hooks/useDetailPanel.js +15 -15
  152. package/src/dashboard/app/main.jsx +13 -13
  153. package/src/dashboard/app/pages/AgentMap.jsx +370 -370
  154. package/src/dashboard/app/pages/HookInspector.jsx +418 -418
  155. package/src/dashboard/app/pages/LiveState.jsx +11 -1
  156. package/src/dashboard/app/pages/PhasePipeline.jsx +408 -408
  157. package/src/dashboard/app/pages/RulesMap.jsx +273 -273
  158. package/src/dashboard/app/pages/SkillsLibrary.jsx +366 -366
  159. package/src/dashboard/app/pages/Standards.jsx +332 -332
  160. package/src/dashboard/app/styles/index.css +47 -47
  161. package/src/dashboard/index.html +12 -12
  162. package/src/dashboard/package.json +24 -24
  163. package/src/dashboard/scanner/hooks-scanner.js +41 -41
  164. package/src/dashboard/scanner/phases-scanner.js +15 -15
  165. package/src/dashboard/scanner/rubrics-scanner.js +12 -12
  166. package/src/dashboard/scanner/rules-scanner.js +37 -37
  167. package/src/dashboard/scanner/skills-scanner.js +43 -43
  168. package/src/dashboard/scanner/squads-scanner.js +13 -13
  169. package/src/dashboard/scanner/standards-scanner.js +26 -26
  170. package/src/dashboard/scanner/state-scanner.js +42 -0
  171. package/src/dashboard/vite-plugin-morph-data.js +17 -17
  172. package/src/dashboard/vite.config.js +24 -24
  173. package/src/lib/agents/metrics-aggregator.js +56 -56
  174. package/src/lib/agents/metrics-calculator.js +125 -125
  175. package/src/lib/agents/output-collector.js +73 -73
  176. package/src/lib/detectors/claude-config-detector.js +152 -152
  177. package/src/lib/detectors/design-system-detector.js +231 -231
  178. package/src/lib/detectors/index.js +217 -217
  179. package/src/lib/generators/architecture-scanner.js +639 -639
  180. package/src/lib/generators/design-system-generator.js +508 -508
  181. package/src/lib/generators/index.js +13 -13
  182. package/src/lib/generators/settings-generator.js +209 -209
  183. package/src/lib/hooks/hook-executor.js +167 -167
  184. package/src/lib/installers/mcp-installer.js +719 -719
  185. package/src/lib/installers/memory-installer.js +1 -1
  186. package/src/lib/risk-detector.js +16 -0
  187. package/src/lib/tasks/dispatch-planner.js +122 -4
  188. package/src/lib/tasks/mermaid-renderer.js +132 -0
  189. package/src/lib/tasks/task-classifier.js +30 -30
  190. package/src/lib/tasks/task-parser.js +209 -209
  191. package/src/lib/tasks/test-runner.js +27 -6
  192. package/src/lib/validators/architecture/architecture-validator.js +259 -259
  193. package/src/lib/validators/checkpoint-persistence.js +39 -39
  194. package/src/lib/validators/contracts/contract-compliance-validator.js +274 -274
  195. package/src/lib/validators/css/css-validator.js +340 -340
  196. package/src/lib/validators/packages/package-validator.js +347 -347
  197. package/src/lib/validators/shared/emit-validator-dispatch.js +14 -14
  198. package/src/lib/validators/shared/issue-counter.js +14 -14
  199. package/src/lib/validators/validation-runner.js +349 -349
  200. package/src/lib/validators/verify-runner.js +322 -0
  201. package/src/lib/work-type-detector.js +143 -0
  202. package/src/scripts/setup-infra.js +32 -5
  203. package/src/utils/agent-id-mapper.js +11 -11
  204. package/src/utils/agents-installer.js +10 -6
  205. package/src/utils/claude-settings-manager.js +300 -300
  206. package/src/utils/file-copier.js +43 -0
  207. package/src/utils/hooks-installer.js +43 -34
  208. package/src/utils/skills-installer.js +174 -174
  209. package/framework/hooks/claude-code/core/subagent-report-guard.js +0 -95
  210. package/framework/templates/meta-prompts/parallel-workers/parallel-coordinator.md +0 -113
  211. package/framework/templates/meta-prompts/parallel-workers/parallel-worker.md +0 -80
  212. package/src/lib/tasks/wave-resolver.js +0 -353
@@ -55,9 +55,24 @@ export async function advanceCommand(featureName, options = {}) {
55
55
 
56
56
  // 1) Approve the gate that completes the current phase.
57
57
  const gate = PHASE_GATE[phase];
58
+ const approvedBy = resolveApprovedBy(options);
59
+
60
+ // Hotfix backstop (defensive, mirrors approve.js): the phase==='review'
61
+ // early-return above already stops advance from ever reaching a
62
+ // gate==='review' approval, but guard the approval itself too so this
63
+ // stays true even if the early-return's shape changes later. Gate on the
64
+ // RESOLVED approvedBy — headless contexts have no TTY and resolve to
65
+ // "auto" regardless of --mode, so checking the raw --mode string alone
66
+ // would let them slip through (same bug the approve.js backstop had).
67
+ if (gate === 'review' && feature.workType === 'hotfix' && approvedBy === 'auto') {
68
+ console.error(chalk.red('\n❌ Hotfix exige aprovação humana no Gate 3 — trust:auto não se aplica.'));
69
+ console.error(chalk.gray(' Aprove com --approver <nome>, num terminal interativo, ou pela resposta humana "aprovado" (state-sync) ao review-report via AskUserQuestion.\n'));
70
+ process.exit(1);
71
+ }
72
+
58
73
  if (gate) {
59
74
  await setApprovalGate(featureName, gate, true, {
60
- approvedBy: resolveApprovedBy(options),
75
+ approvedBy,
61
76
  approvedAt: new Date().toISOString(),
62
77
  notes: options.notes,
63
78
  });
@@ -72,7 +87,7 @@ export async function advanceCommand(featureName, options = {}) {
72
87
  }
73
88
 
74
89
  console.log(chalk.green(`\n✅ Advanced "${featureName}" past the ${phase} phase.`));
75
- if (gate) console.log(chalk.gray(` Gate "${gate}" approved (${resolveApprovedBy(options)}).`));
90
+ if (gate) console.log(chalk.gray(` Gate "${gate}" approved (${approvedBy}).`));
76
91
  if (nextDir) console.log(chalk.gray(` Scaffolded ${nextDir}/ — ready for the next phase's first artifact.\n`));
77
92
  else console.log('');
78
93
  } catch (error) {
@@ -99,7 +99,8 @@ export async function approveCommand(featureName, gateName, options = {}) {
99
99
  return;
100
100
  }
101
101
 
102
- // Set approval.
102
+ // Resolve who approved BEFORE the hotfix backstop — the backstop gates on
103
+ // the resolved signature, not the raw --mode string (see below).
103
104
  //
104
105
  // approvedBy resolution (most explicit wins):
105
106
  // 1. --approver flag (CLI caller signalled who approved)
@@ -125,6 +126,22 @@ export async function approveCommand(featureName, gateName, options = {}) {
125
126
  approvedBy = 'auto';
126
127
  }
127
128
 
129
+ // Hotfix backstop: Gate 3 (review) requires a genuine HUMAN signature even
130
+ // in trust:auto — a work-type policy (forcedPauseGates in
131
+ // src/core/work-types.js, mirrored by risk-detector's
132
+ // FORCED_PAUSE_WORK_TYPES), enforced server-side rather than trusting the
133
+ // calling skill. It gates on the RESOLVED approvedBy, NOT the raw --mode
134
+ // string: every headless context (claude -p, CI, the Bash tool) runs
135
+ // without a TTY, so an omitted --mode — or any --mode ≠ 'auto' — still
136
+ // resolves to "auto" and would otherwise rubber-stamp the gate. A human
137
+ // approval is --approver <name> or an interactive TTY ($USER); anything
138
+ // resolving to "auto" is refused here.
139
+ if (gateName === 'review' && feature.workType === 'hotfix' && approvedBy === 'auto') {
140
+ console.error(chalk.red('\n❌ Hotfix exige aprovação humana no Gate 3 — trust:auto não se aplica.'));
141
+ console.error(chalk.gray(' Aprove com --approver <nome>, num terminal interativo, ou pela resposta humana "aprovado" (state-sync) ao review-report via AskUserQuestion.\n'));
142
+ process.exit(1);
143
+ }
144
+
128
145
  await setApprovalGate(featureName, gateName, true, {
129
146
  approvedBy,
130
147
  approvedAt: new Date().toISOString(),
@@ -1,22 +1,85 @@
1
1
  import chalk from 'chalk';
2
- import { mkdirSync } from 'fs';
2
+ import { mkdirSync, cpSync, readFileSync, writeFileSync } from 'fs';
3
3
  import { join } from 'path';
4
- import { createFeature } from '../../core/state/state-manager.js';
4
+ import { createFeature, setApprovalGate, getFeature } from '../../core/state/state-manager.js';
5
+ import { WORK_TYPES, DEFAULT_WORK_TYPE, POLICY_APPROVER_PREFIX, isValidWorkType, getWorkType } from '../../core/work-types.js';
6
+ import { detectWorkType } from '../../lib/work-type-detector.js';
7
+ import { runWorktreeSetup, materializeState } from '../project/worktree.js';
5
8
 
6
9
  /** Feature names are kebab-case: lowercase alphanumerics and hyphens. */
7
10
  const FEATURE_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
8
11
 
9
12
  const VALID_STATUSES = ['draft', 'in_progress', 'completed'];
13
+ const VALID_DETECT_SOURCES = ['deterministic', 'llm', 'user'];
10
14
 
11
15
  /**
12
- * `morph-spec create <feature>` — explicit feature birth: creates
13
- * `.morph/features/{feature}/0-proposal/` and the canonical feature.json
14
- * (shape documented in framework/schemas/feature.schema.json).
16
+ * Resolve the work type for a NEW feature from `--type`/`--request`, or fall
17
+ * back to the default. Never touches the filesystem/state — pure control
18
+ * flow so it can `process.exit` before any side effect happens.
19
+ *
20
+ * @param {{ type?: string, request?: string, detectSource?: string }} options
21
+ * @param {string} now - ISO timestamp, stamped into workTypeDetection.decidedAt
22
+ * @returns {{ workType: string, workTypeDetection?: object }}
23
+ */
24
+ function resolveWorkType(options, now) {
25
+ if (options.type !== undefined) {
26
+ if (!isValidWorkType(options.type)) {
27
+ console.error(chalk.red(`\n❌ Invalid --type: "${options.type}"`));
28
+ console.error(chalk.gray(`Valid types: ${Object.keys(WORK_TYPES).join(', ')}\n`));
29
+ process.exit(1);
30
+ }
31
+ if (options.detectSource !== undefined && !VALID_DETECT_SOURCES.includes(options.detectSource)) {
32
+ console.error(chalk.red(`\n❌ Invalid --detect-source: "${options.detectSource}"`));
33
+ console.error(chalk.gray(`Valid sources: ${VALID_DETECT_SOURCES.join(', ')}\n`));
34
+ process.exit(1);
35
+ }
36
+ return {
37
+ workType: options.type,
38
+ workTypeDetection: {
39
+ source: options.detectSource || 'user',
40
+ confidence: 'high',
41
+ decidedAt: now,
42
+ },
43
+ };
44
+ }
45
+
46
+ if (options.request !== undefined) {
47
+ const d = detectWorkType({ text: options.request });
48
+ if (d.confidence !== 'high') {
49
+ // Router contract: determinism first, ambiguity escalates to the
50
+ // LLM/HCOP caller (which re-runs `create --type <chosen>`). The
51
+ // feature must NOT be created on this path — always JSON, regardless
52
+ // of --json, since this is the machine-consumable half of the contract.
53
+ console.log(JSON.stringify({ needsDecision: true, candidates: d.candidates, matched: d.matched }));
54
+ process.exit(2);
55
+ }
56
+ return {
57
+ workType: d.workType,
58
+ workTypeDetection: {
59
+ source: 'deterministic',
60
+ confidence: 'high',
61
+ matched: d.matched,
62
+ decidedAt: now,
63
+ },
64
+ };
65
+ }
66
+
67
+ return { workType: DEFAULT_WORK_TYPE };
68
+ }
69
+
70
+ /**
71
+ * `morph-spec create <feature>` — explicit feature birth: classifies the
72
+ * work type (`--type`/`--request`), creates `.morph/features/{feature}/`
73
+ * scaffolded per the type's dimensioned pipeline, stamps policy-approved
74
+ * gates (`approvedBy: "policy:{type}"`) for the gates that type skips, and
75
+ * (with `--worktree`) births the feature already inside its own git
76
+ * worktree on branch `morph/{feature}`.
15
77
  *
16
78
  * Replaces the implicit birth via the state-sync hook on the first Write —
17
79
  * the skills call this at proposal start so the authoritative state exists
18
80
  * with the full canonical shape before any artifact lands. Idempotent: an
19
- * existing feature is reported and left untouched.
81
+ * existing feature is reported and left untouched — no re-stamping, no
82
+ * re-scaffolding, no worktree setup.
20
83
  */
21
84
  export async function createCommand(featureName, options = {}) {
22
85
  try {
@@ -32,24 +95,105 @@ export async function createCommand(featureName, options = {}) {
32
95
  process.exit(1);
33
96
  }
34
97
 
35
- const { feature, created } = await createFeature(featureName, { status: options.status });
98
+ const now = new Date().toISOString();
99
+ const { workType, workTypeDetection } = resolveWorkType(options, now);
100
+
101
+ const { feature, created } = await createFeature(featureName, {
102
+ status: options.status,
103
+ workType,
104
+ workTypeDetection,
105
+ });
36
106
 
37
- // The proposal folder is the first working directory of every feature.
38
107
  const featureDir = join(process.cwd(), '.morph', 'features', featureName);
39
- mkdirSync(join(featureDir, '0-proposal'), { recursive: true });
108
+ const wt = getWorkType(feature);
109
+
110
+ let worktreeResult = null;
111
+
112
+ if (created) {
113
+ // Scaffold the phase folders this work type starts with.
114
+ for (const folder of wt.scaffold) {
115
+ mkdirSync(join(featureDir, folder), { recursive: true });
116
+ }
117
+
118
+ // Stamp the gates this type skips by policy — NEVER "auto" (the
119
+ // gate-guard hook only special-cases approvedBy === "auto"; policy:{type}
120
+ // keeps the audit trail honest while staying silent to the guard).
121
+ for (const gate of wt.skippedGates) {
122
+ await setApprovalGate(featureName, gate, true, {
123
+ approvedBy: `${POLICY_APPROVER_PREFIX}${workType}`,
124
+ approvedAt: now,
125
+ });
126
+ }
127
+
128
+ if (options.worktree) {
129
+ worktreeResult = await runWorktreeSetup(featureName, {});
130
+ if (worktreeResult?.created && worktreeResult.path) {
131
+ try {
132
+ // feature.json in the root is NOT committed yet (create just
133
+ // wrote it), so the branch `runWorktreeSetup` checked out from
134
+ // HEAD does not have it — copy the freshly-scaffolded folder
135
+ // into the worktree and re-materialize its local state.json.
136
+ cpSync(featureDir, join(worktreeResult.path, '.morph', 'features', featureName), { recursive: true });
137
+ // The `.worktree` registration is ROOT-OWNED metadata (runWorktreeSetup
138
+ // stamped it onto the root's feature.json, which cpSync just copied in).
139
+ // Strip it from the worktree's branch-local copy so the branch commits a
140
+ // clean feature.json — otherwise it merges back into the root at
141
+ // `worktree finish` and the post-merge cleanup leaves the root dirty.
142
+ // verify.decidePersistence reads the registration from the ROOT's copy,
143
+ // never this branch-local one (see verify.js), so stripping is safe and
144
+ // restores the invariant that comment relies on.
145
+ try {
146
+ const wtFeatureJson = join(worktreeResult.path, '.morph', 'features', featureName, 'feature.json');
147
+ const fj = JSON.parse(readFileSync(wtFeatureJson, 'utf8'));
148
+ if (fj.worktree) {
149
+ delete fj.worktree;
150
+ writeFileSync(wtFeatureJson, JSON.stringify(fj, null, 2), 'utf8');
151
+ }
152
+ } catch { /* fail-open — root copy still carries the registration for verify */ }
153
+ // Re-materialize now that the feature.json exists in the worktree, and
154
+ // report THIS result — runWorktreeSetup's earlier materialize ran before
155
+ // the copy (no features yet), so its `materialized:false` was stale.
156
+ const remat = await materializeState(worktreeResult.path);
157
+ worktreeResult.materialized = remat.materialized;
158
+ } catch { /* fail-open — the feature already exists in root */ }
159
+ }
160
+ }
161
+ }
162
+
163
+ // `feature` is a snapshot from BEFORE the gate-stamping/worktree steps —
164
+ // setApprovalGate/runWorktreeSetup each do their own loadState()/saveState()
165
+ // round-trip on disk, so re-read the authoritative copy for reporting.
166
+ const finalFeature = created ? (getFeature(featureName) || feature) : feature;
40
167
 
41
168
  if (options.json) {
42
- console.log(JSON.stringify({ feature: featureName, created, ...feature }, null, 2));
169
+ const out = { feature: featureName, created, ...finalFeature };
170
+ if (worktreeResult) out.worktree = worktreeResult;
171
+ console.log(JSON.stringify(out, null, 2));
43
172
  return;
44
173
  }
45
174
 
46
175
  if (created) {
47
- console.log(chalk.green(`\n✅ Feature created: ${featureName}`));
48
- console.log(chalk.gray(` .morph/features/${featureName}/feature.json (canonical shape) + 0-proposal/`));
49
- console.log(chalk.gray(` status: ${feature.status} | createdAt: ${feature.createdAt}\n`));
176
+ console.log(chalk.green(`\n✅ Feature created: ${featureName} (type: ${finalFeature.workType})`));
177
+ console.log(chalk.gray(` .morph/features/${featureName}/feature.json (canonical shape)`));
178
+ console.log(chalk.gray(` status: ${finalFeature.status} | createdAt: ${finalFeature.createdAt}`));
179
+ console.log(chalk.gray(` scaffolded: ${wt.scaffold.join(', ')}`));
180
+ if (wt.skippedGates.length) {
181
+ console.log(chalk.yellow(` gates skipped by policy:${finalFeature.workType}: ${wt.skippedGates.join(', ')}`));
182
+ }
183
+ console.log(chalk.cyan(` next: ${wt.entrySkill}\n`));
184
+
185
+ if (worktreeResult) {
186
+ if (worktreeResult.created) {
187
+ console.log(chalk.green(` worktree: ${worktreeResult.path} (branch ${worktreeResult.branch})`));
188
+ console.log(chalk.gray(` ${worktreeResult.nextStep}\n`));
189
+ } else {
190
+ const why = worktreeResult.error || (worktreeResult.alreadyExists ? 'already exists' : 'unknown');
191
+ console.log(chalk.yellow(` ⚠️ worktree setup skipped (${why}) — feature stays in the root worktree\n`));
192
+ }
193
+ }
50
194
  } else {
51
195
  console.log(chalk.yellow(`\n⚠️ Feature "${featureName}" already exists — left untouched`));
52
- console.log(chalk.gray(` status: ${feature.status} | createdAt: ${feature.createdAt || 'unknown'}\n`));
196
+ console.log(chalk.gray(` status: ${finalFeature.status} | workType: ${finalFeature.workType || 'feature'} | createdAt: ${finalFeature.createdAt || 'unknown'}\n`));
53
197
  }
54
198
  } catch (error) {
55
199
  console.error(chalk.red(`\n❌ Error creating feature: ${error.message}\n`));
@@ -0,0 +1,126 @@
1
+ import chalk from 'chalk';
2
+ import { mkdirSync } from 'fs';
3
+ import { join } from 'path';
4
+ import { getFeature, setApprovalGate, updateFeature } from '../../core/state/state-manager.js';
5
+ import { WORK_TYPES, isValidWorkType, POLICY_APPROVER_PREFIX } from '../../core/work-types.js';
6
+
7
+ /**
8
+ * `morph-spec retype <feature> <newType>` — mid-flight work-type change with
9
+ * asymmetric gate rules that preserve GENUINE human/auto approvals and only
10
+ * ever undo policy stamps:
11
+ *
12
+ * - Blocked outright once Gate 3 (review) is approved — the feature already
13
+ * shipped its pipeline, retyping it retroactively would be revisionist.
14
+ * - Downgrade (fewer skipped gates → more skipped gates, e.g. feature/bug
15
+ * → chore/hotfix) is blocked once Gate 2 (plan) carries a genuine
16
+ * (non-policy) approval — the plan was already accepted under the old
17
+ * type's scrutiny; retyping around it would erase that judgment call.
18
+ * - Upgrade (more skipped gates → fewer, e.g. chore/hotfix → feature/bug)
19
+ * is always allowed before review: it only ever ADDS scrutiny.
20
+ *
21
+ * Applying a retype resets exactly the gates that were stamped
22
+ * `policy:{oldType}` and the NEW type no longer skips (so the human is asked
23
+ * again), stamps `policy:{newType}` on any of the new type's skippedGates
24
+ * that aren't already approved (a prior genuine approval is left standing —
25
+ * that's the "preserves human approvals" half of the contract), scaffolds
26
+ * any phase folders the new type needs that don't exist yet, and appends an
27
+ * audit entry to `workTypeHistory`.
28
+ */
29
+ export async function retypeCommand(featureName, newType, options = {}) {
30
+ try {
31
+ if (!isValidWorkType(newType)) {
32
+ console.error(chalk.red(`\n❌ Invalid work type: "${newType}"`));
33
+ console.error(chalk.gray(`Valid types: ${Object.keys(WORK_TYPES).join(', ')}\n`));
34
+ process.exit(1);
35
+ }
36
+
37
+ const feature = getFeature(featureName);
38
+ if (!feature) {
39
+ console.error(chalk.red(`\n❌ Feature not found: ${featureName}\n`));
40
+ process.exit(1);
41
+ }
42
+
43
+ const oldType = feature.workType || 'feature';
44
+
45
+ if (oldType === newType) {
46
+ console.log(chalk.yellow(`\n⚠️ Feature "${featureName}" already is type "${newType}" — no-op\n`));
47
+ return;
48
+ }
49
+
50
+ if (feature.approvalGates?.review?.approved === true) {
51
+ console.error(chalk.red(`\n❌ Feature "${featureName}" já passou pelo Gate 3 (review); retype bloqueado\n`));
52
+ process.exit(1);
53
+ }
54
+
55
+ const oldWorkType = WORK_TYPES[oldType] || WORK_TYPES.feature;
56
+ const newWorkType = WORK_TYPES[newType];
57
+ const isDowngrade = newWorkType.skippedGates.length > oldWorkType.skippedGates.length;
58
+
59
+ if (isDowngrade) {
60
+ const planGate = feature.approvalGates?.plan;
61
+ const genuinelyApproved = planGate?.approved === true
62
+ && !String(planGate.approvedBy || '').startsWith(POLICY_APPROVER_PREFIX);
63
+ if (genuinelyApproved) {
64
+ console.error(chalk.red(`\n❌ Gate 2 (plan) já aprovado por "${planGate.approvedBy}"; downgrade bloqueado — o plano já foi aceito\n`));
65
+ process.exit(1);
66
+ }
67
+ }
68
+
69
+ const now = new Date().toISOString();
70
+
71
+ // Reset gates that were stamped policy:{oldType} and the new type no
72
+ // longer skips — a genuine (non-policy) approval is never touched.
73
+ const resetGates = [];
74
+ for (const [gate, data] of Object.entries(feature.approvalGates || {})) {
75
+ const isPolicyStamped = String(data?.approvedBy || '').startsWith(POLICY_APPROVER_PREFIX);
76
+ if (isPolicyStamped && !newWorkType.skippedGates.includes(gate)) {
77
+ await setApprovalGate(featureName, gate, false, { approvedBy: null, approvedAt: null });
78
+ resetGates.push(gate);
79
+ }
80
+ }
81
+
82
+ // Stamp the new type's skipped gates — but only the ones not already
83
+ // approved (preserves a prior genuine approval untouched).
84
+ const stampedGates = [];
85
+ for (const gate of newWorkType.skippedGates) {
86
+ if (!feature.approvalGates?.[gate]?.approved) {
87
+ await setApprovalGate(featureName, gate, true, {
88
+ approvedBy: `${POLICY_APPROVER_PREFIX}${newType}`,
89
+ approvedAt: now,
90
+ });
91
+ stampedGates.push(gate);
92
+ }
93
+ }
94
+
95
+ const featureDir = join(process.cwd(), '.morph', 'features', featureName);
96
+ for (const folder of newWorkType.scaffold) {
97
+ mkdirSync(join(featureDir, folder), { recursive: true });
98
+ }
99
+
100
+ await updateFeature(featureName, 'workType', newType);
101
+ await updateFeature(featureName, 'workTypeHistory', [
102
+ ...(feature.workTypeHistory || []),
103
+ { from: oldType, to: newType, reason: options.reason || null, at: now },
104
+ ]);
105
+
106
+ const finalFeature = getFeature(featureName);
107
+
108
+ if (options.json) {
109
+ console.log(JSON.stringify({ feature: featureName, from: oldType, to: newType, resetGates, stampedGates, ...finalFeature }, null, 2));
110
+ return;
111
+ }
112
+
113
+ console.log(chalk.green(`\n✅ Feature retyped: ${featureName} (${oldType} → ${newType})`));
114
+ if (resetGates.length) {
115
+ console.log(chalk.yellow(` gates reset (were policy-stamped, no longer skipped): ${resetGates.join(', ')}`));
116
+ }
117
+ if (stampedGates.length) {
118
+ console.log(chalk.yellow(` gates stamped policy:${newType}: ${stampedGates.join(', ')}`));
119
+ }
120
+ console.log(chalk.gray(` scaffolded: ${newWorkType.scaffold.join(', ')}`));
121
+ console.log('');
122
+ } catch (error) {
123
+ console.error(chalk.red(`\n❌ Error retyping feature: ${error.message}\n`));
124
+ process.exit(1);
125
+ }
126
+ }
@@ -33,6 +33,10 @@ export async function scoreCommand(featureName, taskId, scoreArg, options = {})
33
33
  process.exit(1);
34
34
  }
35
35
 
36
+ if (feature.taskVerifications?.[taskId]?.status === 'fail') {
37
+ console.error(chalk.yellow(`⚠️ Último verify de ${taskId} está VERMELHO — score sendo gravado sobre código que não passou no verify determinístico`));
38
+ }
39
+
36
40
  let dimensions;
37
41
  if (options.dims) {
38
42
  const parts = String(options.dims).split(',').map(s => s.trim());
@@ -3,6 +3,7 @@ import { existsSync } from 'fs';
3
3
  import { join } from 'path';
4
4
  import { parseTasksJson } from '../../lib/tasks/tasks-json-parser.js';
5
5
  import { planDispatch } from '../../lib/tasks/dispatch-planner.js';
6
+ import { renderMermaidFlowchart } from '../../lib/tasks/mermaid-renderer.js';
6
7
 
7
8
  /**
8
9
  * `morph-spec dag <feature>` — analyze the tasks.json DAG and print the
@@ -10,6 +11,9 @@ import { planDispatch } from '../../lib/tasks/dispatch-planner.js';
10
11
  *
11
12
  * This is the command morph-apply §2 runs instead of re-deriving the rules
12
13
  * (and instead of the old inline JS snippets the LLM could not execute).
14
+ *
15
+ * `--mermaid` emits the DAG as a Mermaid `flowchart TD` instead (for
16
+ * visualizing the plan at Gate 2), bypassing the dispatch analysis entirely.
13
17
  */
14
18
  export async function dagCommand(featureName, options = {}) {
15
19
  try {
@@ -22,6 +26,14 @@ export async function dagCommand(featureName, options = {}) {
22
26
 
23
27
  // parseTasksJson validates the schema and throws with the error list.
24
28
  const data = await parseTasksJson(tasksPath);
29
+
30
+ // --mermaid short-circuits before the dispatch analysis: visualizing the
31
+ // plan's DAG doesn't need a sequential-vs-parallel decision.
32
+ if (options.mermaid) {
33
+ console.log(renderMermaidFlowchart(data));
34
+ return;
35
+ }
36
+
25
37
  const plan = planDispatch(data);
26
38
 
27
39
  if (options.json) {
@@ -60,6 +72,26 @@ export async function dagCommand(featureName, options = {}) {
60
72
  console.log(` ${chalk.gray('→')} Sequencial: ${plan.sequentialRemainder.join(', ')}`);
61
73
  }
62
74
 
75
+ // Isolamento — only meaningful when there IS a parallel batch. In a shared
76
+ // working tree two agents writing the same path corrupt each other; the
77
+ // dag recommends `isolation: "worktree"` so each runs in its own worktree
78
+ // (morph-apply §3b-w).
79
+ const iso = plan.isolation;
80
+ if (iso && (iso.recommended === 'worktree' || iso.unknownFootprint.length > 0)) {
81
+ console.log(chalk.bold('\nIsolamento:'));
82
+ if (iso.recommended === 'worktree') {
83
+ console.log(` ${chalk.yellow('⚠')} recomendado: ${chalk.bold('worktree')} — ${iso.reason}`);
84
+ for (const c of iso.collisions) {
85
+ console.log(chalk.gray(` colisão em "${c.path}": ${c.units.join(' ‖ ')} (${c.tasks.join(', ')})`));
86
+ }
87
+ } else {
88
+ console.log(` ${chalk.gray('○')} ${iso.reason}`);
89
+ }
90
+ if (iso.unknownFootprint.length > 0) {
91
+ console.log(chalk.gray(` footprint desconhecido (sem outputs): ${iso.unknownFootprint.join(', ')}`));
92
+ }
93
+ }
94
+
63
95
  console.log('\n' + '━'.repeat(60) + '\n');
64
96
  } catch (error) {
65
97
  console.error(chalk.red(`\n❌ Error analyzing DAG: ${error.message}\n`));
@@ -2,3 +2,4 @@
2
2
  * Validation Commands
3
3
  */
4
4
  export { validateFeatureCommand } from './validate-feature.js';
5
+ export { verifyCommand } from './verify.js';
@@ -132,6 +132,14 @@ const PHASE_OUTPUTS = {
132
132
  review: ['review-report.md'],
133
133
  };
134
134
 
135
+ // Work types whose plan phase (see src/core/work-types.js) skips proposal.md/
136
+ // spec.md/mandate.md — chore and hotfix stamp proposal+plan gates at birth
137
+ // (policy-approved, no human deliberation over those docs) and go straight to
138
+ // tasks.json. Validating the full plan doc set against a feature that was
139
+ // never asked to write those docs would be a false failure, not a real gap.
140
+ const LEAN_PLAN_WORK_TYPES = ['chore', 'hotfix'];
141
+ const LEAN_PLAN_OUTPUTS = ['tasks.json'];
142
+
135
143
  /**
136
144
  * Phase-to-folder mapping (v5 filesystem convention)
137
145
  */
@@ -295,11 +303,16 @@ function validateMarkdownSections(content, sections, result) {
295
303
  *
296
304
  * @param {string} featurePath Absolute path to the feature directory.
297
305
  * @param {string|null} phase Phase key, or null to validate everything.
306
+ * @param {string|undefined} workType Feature's workType (see src/core/work-types.js).
307
+ * When `phase === 'plan'` and workType is 'chore'/'hotfix', only tasks.json
308
+ * is required (see LEAN_PLAN_WORK_TYPES above).
298
309
  * @returns {{ outputFiles?: Array<{file:string, dir:string}>, error?: string }}
299
310
  */
300
- function resolveOutputFiles(featurePath, phase) {
311
+ function resolveOutputFiles(featurePath, phase, workType) {
301
312
  if (phase) {
302
- const files = PHASE_OUTPUTS[phase];
313
+ const files = (phase === 'plan' && LEAN_PLAN_WORK_TYPES.includes(workType))
314
+ ? LEAN_PLAN_OUTPUTS
315
+ : PHASE_OUTPUTS[phase];
303
316
  if (!files) return { error: `Unknown phase: ${phase}` };
304
317
  const phaseFolder = PHASE_FOLDERS[phase];
305
318
  const searchPath = phaseFolder ? path.join(featurePath, phaseFolder) : featurePath;
@@ -327,7 +340,7 @@ function resolveOutputFiles(featurePath, phase) {
327
340
  * Pure validation core — no console output, no process.exit.
328
341
  *
329
342
  * @param {string} featurePath Absolute path to the feature directory.
330
- * @param {{ phase?: string }} [options]
343
+ * @param {{ phase?: string, workType?: string }} [options]
331
344
  * @returns {{
332
345
  * feature: string, phase: string, valid: boolean, error?: string,
333
346
  * totalErrors: number, totalWarnings: number, totalFound: number,
@@ -336,9 +349,10 @@ function resolveOutputFiles(featurePath, phase) {
336
349
  */
337
350
  export function runFeatureValidation(featurePath, options = {}) {
338
351
  const phase = options.phase || null;
352
+ const workType = options.workType || null;
339
353
  const feature = path.basename(featurePath);
340
354
 
341
- const { outputFiles, error } = resolveOutputFiles(featurePath, phase);
355
+ const { outputFiles, error } = resolveOutputFiles(featurePath, phase, workType);
342
356
  if (error) {
343
357
  return {
344
358
  feature,
@@ -392,7 +406,21 @@ export async function validateFeatureCommand(featureName, options = {}) {
392
406
  process.exit(1);
393
407
  }
394
408
 
395
- const validation = runFeatureValidation(featurePath, { phase });
409
+ // workType is read straight off the feature's own feature.json (not via
410
+ // state-manager's getFeature/loadState) so a broken/missing root state.json
411
+ // elsewhere in the project can never make `validate` itself throw — it just
412
+ // falls back to the full (non-lean) output set, same as today.
413
+ let workType;
414
+ try {
415
+ const fjPath = path.join(featurePath, 'feature.json');
416
+ if (fs.existsSync(fjPath)) {
417
+ workType = JSON.parse(fs.readFileSync(fjPath, 'utf8')).workType;
418
+ }
419
+ } catch {
420
+ workType = undefined;
421
+ }
422
+
423
+ const validation = runFeatureValidation(featurePath, { phase, workType });
396
424
  const { results, totalErrors, totalWarnings, totalFound } = validation;
397
425
  const schemaWarnings = []; // v5: cross-validation removed (contracts in spec.md)
398
426