@polymorphism-tech/morph-spec 8.47.0 → 9.0.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 (222) hide show
  1. package/CLAUDE.md +48 -39
  2. package/README.md +3 -4
  3. package/bin/morph-spec.js +37 -51
  4. package/bin/validate.js +3 -111
  5. package/claude-plugin.json +1 -1
  6. package/docs/CHEATSHEET.md +1 -1
  7. package/docs/QUICKSTART.md +1 -1
  8. package/framework/CLAUDE.md +41 -101
  9. package/framework/MORPH.md +95 -252
  10. package/framework/agents.json +75 -18
  11. package/framework/commands/morph-apply.md +5 -28
  12. package/framework/commands/morph-archive.md +1 -1
  13. package/framework/commands/morph-hotfix.md +8 -8
  14. package/framework/commands/morph-infra.md +1 -1
  15. package/framework/commands/morph-preflight.md +2 -3
  16. package/framework/commands/morph-proposal.md +2 -50
  17. package/framework/commands/morph-status.md +3 -3
  18. package/framework/docs/CLI.md +585 -286
  19. package/framework/evals/review-checklist.md +82 -41
  20. package/framework/hooks/README.md +10 -11
  21. package/framework/hooks/claude-code/core/comment-guard.js +1 -1
  22. package/framework/hooks/claude-code/core/denylist.json +33 -3
  23. package/framework/hooks/claude-code/core/enforce-phase-writes.js +16 -20
  24. package/framework/hooks/claude-code/core/gate-guard.js +2 -11
  25. package/framework/hooks/claude-code/core/pre-bash-denylist.js +38 -0
  26. package/framework/hooks/claude-code/core/protect-spec-files.js +30 -242
  27. package/framework/hooks/claude-code/core/state-sync.js +7 -21
  28. package/framework/hooks/claude-code/core/telemetry-log.js +3 -3
  29. package/framework/hooks/claude-code/core/validate-completion.js +33 -97
  30. package/framework/hooks/claude-code/core/warn-bash-source-write.js +31 -0
  31. package/framework/hooks/claude-code/core/worktree-context.js +5 -4
  32. package/framework/hooks/claude-code/statusline.py +55 -41
  33. package/framework/hooks/package.json +3 -0
  34. package/framework/hooks/shared/activity-logger.js +1 -1
  35. package/framework/hooks/shared/build-check.js +10 -6
  36. package/framework/hooks/shared/command-tokens.js +169 -0
  37. package/framework/hooks/shared/comment-rules.js +1 -1
  38. package/framework/hooks/shared/comment-scanner.js +10 -23
  39. package/framework/hooks/shared/comment-stripper.js +1 -1
  40. package/framework/hooks/shared/e2e-check.js +64 -27
  41. package/framework/hooks/shared/errata.js +72 -0
  42. package/framework/hooks/shared/feature-locator.js +1 -1
  43. package/framework/hooks/shared/frontend-locations.js +6 -0
  44. package/framework/hooks/shared/gate-question.js +18 -0
  45. package/framework/hooks/shared/phase-utils.js +15 -32
  46. package/framework/hooks/shared/slice-progress.js +118 -0
  47. package/framework/hooks/shared/spawn-command.js +23 -0
  48. package/framework/hooks/shared/state-reader.js +61 -87
  49. package/framework/hooks/shared/state-writer.js +2 -3
  50. package/framework/mcp/registry.json +3 -4
  51. package/framework/phases.json +23 -39
  52. package/framework/schemas/config.schema.json +14 -9
  53. package/framework/schemas/feature.schema.json +15 -50
  54. package/framework/skills/README.md +3 -4
  55. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  56. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  57. package/framework/skills/core/morph-apply/SKILL.md +149 -222
  58. package/framework/skills/core/morph-hotfix/SKILL.md +33 -42
  59. package/framework/skills/core/morph-machine/SKILL.md +2 -2
  60. package/framework/skills/core/morph-media/SKILL.md +1 -1
  61. package/framework/skills/core/morph-plan/SKILL.md +137 -299
  62. package/framework/skills/core/morph-review/SKILL.md +111 -136
  63. package/framework/skills/core/morph-standards/SKILL.md +27 -39
  64. package/framework/skills/core/morph-uiux/SKILL.md +77 -152
  65. package/framework/standards/STANDARDS.json +86 -1218
  66. package/framework/standards/STANDARDS.meta.json +1305 -0
  67. package/framework/standards/ai-agents/evals-with-cache.md +1 -2
  68. package/framework/standards/ai-agents/setup.md +1 -1
  69. package/framework/standards/architecture/code-comments.md +6 -6
  70. package/framework/standards/integration/mcp/mcp-tools.md +7 -7
  71. package/framework/templates/README.md +2 -3
  72. package/framework/templates/REGISTRY.json +6 -170
  73. package/framework/templates/docs/plan.md +72 -0
  74. package/framework/templates/dotnet/ai-kit/README.md +1 -9
  75. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Morph.AiKit.Tests.csproj +0 -10
  76. package/framework/templates/meta-prompts/squad-leaders/backend-squad.md +1 -1
  77. package/framework/templates/meta-prompts/squad-leaders/frontend-squad.md +1 -1
  78. package/framework/templates/meta-prompts/squad-leaders/squad-leader.md +1 -1
  79. package/framework/templates/meta-prompts/validators/pre-commit-validator.md +3 -4
  80. package/package.json +1 -1
  81. package/src/commands/e2e/e2e.js +2 -2
  82. package/src/commands/observability/cost.js +18 -60
  83. package/src/commands/project/doctor-procs.js +1 -1
  84. package/src/commands/project/doctor.js +9 -9
  85. package/src/commands/project/fleet.js +4 -4
  86. package/src/commands/project/heavy.js +3 -3
  87. package/src/commands/project/init.js +4 -0
  88. package/src/commands/project/setup-infra-cmd.js +2 -1
  89. package/src/commands/project/stack-scan.js +58 -0
  90. package/src/commands/project/standards.js +79 -25
  91. package/src/commands/project/status.js +29 -69
  92. package/src/commands/project/update.js +17 -1
  93. package/src/commands/project/worktree.js +1 -1
  94. package/src/commands/state/advance.js +62 -46
  95. package/src/commands/state/approve.js +81 -141
  96. package/src/commands/state/archive.js +20 -84
  97. package/src/commands/state/auto-approval.js +68 -0
  98. package/src/commands/state/create.js +28 -1
  99. package/src/commands/state/delete.js +1 -1
  100. package/src/commands/state/finish.js +11 -23
  101. package/src/commands/state/gate-check.js +85 -84
  102. package/src/commands/state/gate-decision.js +6 -4
  103. package/src/commands/state/handoff.js +1 -1
  104. package/src/commands/state/list.js +9 -9
  105. package/src/commands/state/report-check.js +157 -311
  106. package/src/commands/state/retype.js +9 -3
  107. package/src/commands/validation/mutate.js +77 -14
  108. package/src/commands/validation/plan-check.js +81 -0
  109. package/src/commands/validation/validate-feature.js +60 -135
  110. package/src/commands/validation/verify.js +40 -72
  111. package/src/core/paths/output-schema.js +19 -66
  112. package/src/core/state/gate-order.js +5 -6
  113. package/src/core/state/lifecycle-audit.js +2 -2
  114. package/src/core/state/state-manager.js +17 -98
  115. package/src/core/templates/template-registry.js +2 -2
  116. package/src/core/templates/template-seed.js +22 -0
  117. package/src/core/work-types.js +10 -10
  118. package/src/dashboard/app/components/ActivityFeed.jsx +2 -3
  119. package/src/dashboard/app/components/CommandPalette.jsx +5 -5
  120. package/src/dashboard/app/components/EntityLink.jsx +1 -1
  121. package/src/dashboard/app/hooks/useMorphQuery.js +1 -1
  122. package/src/dashboard/app/lib/phases.js +2 -4
  123. package/src/dashboard/app/pages/Activity.jsx +1 -1
  124. package/src/dashboard/app/pages/FeatureDetail.jsx +139 -129
  125. package/src/dashboard/app/pages/Features.jsx +9 -9
  126. package/src/dashboard/app/pages/Guide.jsx +2 -2
  127. package/src/dashboard/app/pages/PhasePipeline.jsx +3 -4
  128. package/src/dashboard/app/pages/Standards.jsx +1 -1
  129. package/src/dashboard/scanner/activity-scanner.js +5 -15
  130. package/src/dashboard/scanner/agents-scanner.js +1 -1
  131. package/src/dashboard/scanner/docs-scanner.js +2 -2
  132. package/src/dashboard/scanner/features-scanner.js +95 -60
  133. package/src/dashboard/scanner/project-api.js +1 -1
  134. package/src/dashboard/scanner/standards-scanner.js +2 -1
  135. package/src/lib/ci/ci-verify.js +17 -1
  136. package/src/lib/ci/github-ci.js +1 -1
  137. package/src/lib/detectors/index.js +3 -8
  138. package/src/lib/generators/handoff-generator.js +34 -65
  139. package/src/lib/generators/settings-generator.js +11 -89
  140. package/src/lib/git/evidence-staleness.js +1 -1
  141. package/src/lib/git/tree-fingerprint.js +19 -7
  142. package/src/lib/git/tree-state.js +2 -2
  143. package/src/lib/heavy/cleanup.js +1 -5
  144. package/src/lib/installers/memory-installer.js +6 -8
  145. package/src/lib/mutation/commands.js +3 -2
  146. package/src/lib/{tasks/dotnet-test-introspection.js → mutation/dotnet-introspection.js} +17 -282
  147. package/src/lib/mutation/plan.js +14 -6
  148. package/src/lib/mutation/runner.js +1 -1
  149. package/src/lib/mutation/subset.js +46 -9
  150. package/src/lib/plan/artifact-vocabulary.js +60 -0
  151. package/src/lib/plan/checks.js +234 -0
  152. package/src/lib/plan/plan-parser.js +209 -0
  153. package/src/lib/plan/plan-template.js +18 -0
  154. package/src/lib/review-reports.js +222 -0
  155. package/src/lib/risk-detector.js +131 -140
  156. package/src/lib/specs/adr-index.js +50 -69
  157. package/src/lib/specs/design-seed.js +18 -0
  158. package/src/lib/specs/frontmatter.js +44 -0
  159. package/src/lib/specs/gfm-table.js +17 -0
  160. package/src/lib/specs/old-format.js +68 -0
  161. package/src/lib/specs/revocations.js +37 -95
  162. package/src/lib/specs/screen-checklist.js +18 -17
  163. package/src/lib/stack/dotnet-build-ignores.js +47 -0
  164. package/src/lib/stack/stack-scan.js +267 -0
  165. package/src/lib/standards/registry.js +43 -0
  166. package/src/lib/standards/sections.js +58 -0
  167. package/src/lib/tasks/eval-node.js +4 -12
  168. package/src/lib/tasks/test-nodes.js +1 -74
  169. package/src/lib/tasks/test-runner.js +75 -117
  170. package/src/lib/telemetry/metrics.js +8 -69
  171. package/src/lib/telemetry/telemetry-reader.js +5 -118
  172. package/src/lib/telemetry/verify-event.js +5 -7
  173. package/src/lib/validators/code-budget/code-budget-validator.js +1 -1
  174. package/src/lib/validators/contracts/contract-compliance-validator.js +2 -2
  175. package/src/lib/validators/reuse/reuse-validator.js +22 -45
  176. package/src/lib/validators/traceability-check.js +85 -129
  177. package/src/lib/validators/verify-runner.js +118 -235
  178. package/src/lib/worktree/docker-audit.js +1 -2
  179. package/src/lib/worktree/fleet-scanner.js +25 -38
  180. package/src/scripts/setup-infra.js +9 -0
  181. package/src/utils/agents-installer.js +76 -9
  182. package/src/utils/claude-settings-manager.js +0 -35
  183. package/src/utils/file-copier.js +27 -1
  184. package/src/utils/hooks-installer.js +16 -9
  185. package/src/utils/skills-installer.js +1 -2
  186. package/framework/evals/trace-schema.md +0 -67
  187. package/framework/hooks/claude-code/core/tasks-json-guard.js +0 -83
  188. package/framework/hooks/claude-code/core/trace-autogen.js +0 -277
  189. package/framework/hooks/git/pre-commit/specs.sh +0 -50
  190. package/framework/hooks/shared/tasks-validator.js +0 -111
  191. package/framework/rules/morph-task-tracking.md +0 -54
  192. package/framework/rules/morph-workflow.md +0 -244
  193. package/framework/schemas/tasks.schema.json +0 -79
  194. package/framework/skills/core/morph-brainstorming/SKILL.md +0 -161
  195. package/framework/skills/core/morph-implement/SKILL.md +0 -194
  196. package/framework/skills/core/morph-proposal/SKILL.md +0 -152
  197. package/framework/skills/core/morph-stack-scan/SKILL.md +0 -145
  198. package/framework/templates/docs/proposal.md +0 -192
  199. package/framework/templates/docs/spec.md +0 -57
  200. package/framework/templates/docs/ui-components.md +0 -52
  201. package/framework/templates/docs/ui-design-system.md +0 -76
  202. package/framework/templates/docs/ui-flows.md +0 -167
  203. package/framework/templates/examples/spec-examples.md +0 -91
  204. package/framework/templates/feature/decisions.md +0 -124
  205. package/framework/templates/feature/recap.md +0 -145
  206. package/framework/templates/tasks.template.json +0 -17
  207. package/src/commands/state/usecase.js +0 -143
  208. package/src/commands/tasks/dag.js +0 -181
  209. package/src/lib/agents/output-collector.js +0 -76
  210. package/src/lib/evaluator-report.js +0 -104
  211. package/src/lib/specs/acervo-format.js +0 -70
  212. package/src/lib/specs/acervo-lifecycle.js +0 -264
  213. package/src/lib/specs/done-criteria-check.js +0 -200
  214. package/src/lib/specs/usecase-check.js +0 -301
  215. package/src/lib/specs/usecase-promoter.js +0 -301
  216. package/src/lib/specs/usecase-registry.js +0 -237
  217. package/src/lib/tasks/dispatch-planner.js +0 -319
  218. package/src/lib/tasks/mermaid-renderer.js +0 -132
  219. package/src/lib/tasks/task-classifier.js +0 -30
  220. package/src/lib/tasks/task-parser.js +0 -209
  221. package/src/lib/tasks/tasks-json-parser.js +0 -296
  222. package/src/lib/tasks/test-scope.js +0 -597
@@ -6,7 +6,7 @@
6
6
  * operator loses track of "what is each session doing, what is waiting on me".
7
7
  * This scanner answers that from the primary root's vantage point: one entry
8
8
  * per active feature, worktree or root-resident, with the operational facts
9
- * (phase, pending gates, task counts, port block, handoff freshness, last
9
+ * (phase, pending gates, slice counts, port block, handoff freshness, last
10
10
  * commit, dirtiness) resolved per entry.
11
11
  *
12
12
  * Read-only and fail-open per FIELD: a corrupt feature.json or a git hiccup
@@ -24,52 +24,39 @@ import {
24
24
  getFeature,
25
25
  derivePhaseForFeature,
26
26
  derivePhaseFrom,
27
+ fileSetArtifactProbe,
27
28
  getPendingGates,
28
29
  pendingGatesFrom,
29
30
  } from '../../../framework/hooks/shared/state-reader.js';
30
31
  import { listClaims } from '../../../framework/hooks/shared/claim-registry.js';
31
32
  import { buildPrIndex } from '../git/pr-index.js';
32
33
  import { pendingPrFeatures } from '../git/pending-prs.js';
34
+ import { sliceProgress, sliceProgressFrom } from '../../../framework/hooks/shared/slice-progress.js';
33
35
 
34
- function countFrom(raw) {
35
- const data = JSON.parse(raw);
36
- if (!Array.isArray(data.tasks)) return null;
37
- return { done: data.tasks.filter((t) => t.status === 'done').length, total: data.tasks.length };
38
- }
36
+ const counts = (progress) => (progress ? { done: progress.done, total: progress.total } : null);
39
37
 
40
38
  /**
41
- * Task progress for one feature.
42
- *
43
- * DIVERGENCE from the bug report, which blamed "a cached state.json/feature.json
44
- * snapshot": this never read either — it always read `2-plan/tasks.json`
45
- * directly. The real mechanism is which COPY of that file is on disk. Since
46
- * v8.4 every feature is born on `morph/{f}` and its tasks.json is COMMITTED
47
- * there; the primary root's working tree only ever holds the copy of the
48
- * CHECKED-OUT branch. An operator sitting on the default therefore read the
49
- * pre-work copy — the reported `2/2` for a feature whose branch already had
50
- * T1..T3 all done.
39
+ * Slice progress for one feature, read from the copy that holds the work: a
40
+ * root-resident feature's plan checks and reports are committed on its own
41
+ * `morph/{f}` branch, while the root working tree only holds the checked-out
42
+ * branch's (pre-work) copy.
51
43
  *
52
44
  * @param {string} featureDir - the feature dir inside `path`
53
45
  * @param {{ root: string, feature: string, featureBranch: string|null }} ctx
54
46
  */
55
- function taskCounts(featureDir, { root, feature, featureBranch } = {}) {
47
+ function sliceCounts(featureDir, { root, feature, featureBranch } = {}) {
56
48
  if (featureBranch && root && feature) {
57
- try {
58
- const raw = execFileSync(
59
- 'git',
60
- ['show', `${featureBranch}:.morph/features/${feature}/2-plan/tasks.json`],
61
- { cwd: root, stdio: 'pipe' },
62
- ).toString();
63
- const counted = countFrom(raw);
64
- if (counted) return counted;
65
- } catch { /* no such branch/blob — fall through to the working tree */ }
66
- }
67
-
68
- try {
69
- return countFrom(readFileSync(join(featureDir, '2-plan', 'tasks.json'), 'utf8'));
70
- } catch {
71
- return null;
49
+ const show = (rel) => {
50
+ try {
51
+ return execFileSync('git', ['show', `${featureBranch}:.morph/features/${feature}/${rel}`], { cwd: root, stdio: 'pipe' }).toString();
52
+ } catch {
53
+ return null;
54
+ }
55
+ };
56
+ const onBranch = sliceProgressFrom(show('2-plan/checks.json'), (id) => show(`3-implement/reports/${id}.md`));
57
+ if (onBranch) return counts(onBranch);
72
58
  }
59
+ return counts(sliceProgress(featureDir));
73
60
  }
74
61
 
75
62
  function handoffInfo(featureDir) {
@@ -164,15 +151,15 @@ function buildRemote(feature, branch, heads, prs) {
164
151
  function buildEntry(feature, { location, path, branch, root, allocations, blockSize, heads, prs, claims }) {
165
152
  let workType = null;
166
153
  let status = 'unknown';
167
- let phase = 'proposal';
154
+ let phase = 'plan';
168
155
  let pendingGates = [];
169
156
 
170
- // Resolved once and shared with taskCounts — see branchToReadFrom.
157
+ // Resolved once and shared with sliceCounts — see branchToReadFrom.
171
158
  const featureBranch = branchToReadFrom(root, feature, location, branch);
172
159
 
173
160
  try {
174
161
  // B3b: phase and gates suffered the same branch-staleness B3 fixed for
175
- // tasks. They read `.morph/features/{f}/` through the working tree, so a
162
+ // progress. They read `.morph/features/{f}/` through the working tree, so a
176
163
  // root parked on the default reported the PRE-WORK phase for a feature
177
164
  // whose branch was already at implement — and re-asked for a gate the
178
165
  // feature branch had already approved.
@@ -180,7 +167,7 @@ function buildEntry(feature, { location, path, branch, root, allocations, blockS
180
167
  if (view) {
181
168
  workType = view.fj?.workType || null;
182
169
  status = view.fj?.status || 'unknown';
183
- phase = derivePhaseFrom(view.fj, (rel) => view.files.has(rel));
170
+ phase = derivePhaseFrom(view.fj, fileSetArtifactProbe(view.files));
184
171
  pendingGates = pendingGatesFrom(view.fj);
185
172
  } else {
186
173
  const fj = getFeature(feature, path);
@@ -202,7 +189,7 @@ function buildEntry(feature, { location, path, branch, root, allocations, blockS
202
189
  location,
203
190
  path,
204
191
  branch,
205
- tasks: taskCounts(featureDir, { root, feature, featureBranch }),
192
+ slices: sliceCounts(featureDir, { root, feature, featureBranch }),
206
193
  env: alloc && Number.isInteger(alloc.base)
207
194
  ? { base: alloc.base, range: [alloc.base, alloc.base + blockSize - 1] }
208
195
  : null,
@@ -316,7 +303,7 @@ export function scanFleet(cwd = process.cwd()) {
316
303
  location: 'pr-pending',
317
304
  path: null,
318
305
  branch: p.branch,
319
- tasks: null,
306
+ slices: null,
320
307
  env: null,
321
308
  handoff: { exists: false, generatedAt: null },
322
309
  lastCommit: null,
@@ -32,6 +32,7 @@ import { installClaudeHooks, installGlobalStatusline } from '../utils/claude-set
32
32
  import { installSkills } from '../utils/skills-installer.js';
33
33
  import { installAgents, installDomainAgents } from '../utils/agents-installer.js';
34
34
  import { resolvePrimaryRoot } from '../commands/project/worktree.js';
35
+ import { findOldFormatFeatures, oldFormatGuardLines } from '../lib/specs/old-format.js';
35
36
  import { reconcileLegacyTemplatesCopy } from '../lib/installers/templates-reconcile.js';
36
37
  import { realEffects } from '../lib/installers/effects.js';
37
38
 
@@ -241,6 +242,14 @@ export async function setupInfra(targetPath, {
241
242
  }
242
243
  }
243
244
 
245
+ // Same refusal as `update`, and neither `force` nor `reinstall` waives it:
246
+ // `init --force` and `setup-infra` overwrite `.morph/` without passing through it.
247
+ const oldFormat = findOldFormatFeatures(targetPath);
248
+ if (oldFormat.length) {
249
+ for (const line of oldFormatGuardLines(oldFormat, targetPath)) log(line);
250
+ return { refused: true, reason: 'old-format-features', features: oldFormat };
251
+ }
252
+
244
253
  // --- Reinstall mode: only re-install stack-aware assets ---
245
254
  if (reinstall) {
246
255
  log('setup-infra (reinstall): re-installing stack-aware assets...');
@@ -15,7 +15,7 @@
15
15
  *
16
16
  * In v5 the harness has 8 personas (no tiers) across two axes — domain
17
17
  * (dotnet-senior, ef-modeler, maf-expert, nextjs-expert, ui-designer,
18
- * infra-engineer, evaluator) and stage (scout, read-only diagnosis before
18
+ * infra-engineer, verifier, reviewer, security) and stage (scout, read-only diagnosis before
19
19
  * implementation). Each persona is a subagent the Specialist (Claude in
20
20
  * implementer role) can dispatch. The 3 roles (Orchestrator/Specialist/
21
21
  * Evaluator) are assumed by Claude itself per MORPH.md, not as separate agents.
@@ -294,7 +294,7 @@ export function loadKnownMcpIds(frameworkDir) {
294
294
  * with NO known stack tags (greenfield / install before config.json exists)
295
295
  * EVERY persona is generated — a project that dispatches `morph-{id}` must
296
296
  * find the subagent registered, and an install that only lands
297
- * morph-evaluator forces every domain dispatch into the general-purpose
297
+ * morph-verifier forces every domain dispatch into the general-purpose
298
298
  * fallback. Stack filtering only applies when the project's stack is KNOWN
299
299
  * (tags non-empty): the fallback stays reserved for stacks the project
300
300
  * genuinely does not have. Note this deliberately differs from rules/skills
@@ -431,7 +431,7 @@ function buildFrontmatter(agent, description, knownMcpIds, maxTurns) {
431
431
  // Canonical 1:1 naming: frontmatter name === filename === subagent_type.
432
432
  `name: morph-${agent.id}`,
433
433
  `description: ${description}`,
434
- // The evaluator is sonnet here; the dispatch routes it to opus when the risk
434
+ // The verifier is sonnet here; the dispatch routes it to opus when the risk
435
435
  // detector flags the diff. A persona without the field inherits the session model.
436
436
  `model: ${agent.model ?? 'inherit'}`,
437
437
  `tools: ${buildToolsLine(agent, knownMcpIds)}`,
@@ -453,6 +453,63 @@ function buildDescription(agent) {
453
453
  return desc.length > 200 ? desc.slice(0, 197) + '...' : desc;
454
454
  }
455
455
 
456
+ /**
457
+ * The fixed half of every implementer dispatch. The orchestrator paraphrases
458
+ * a prompt template in the field; the generated body is the only text a
459
+ * dispatch is guaranteed to carry, so the contract lives here and the
460
+ * dispatch carries only what changes per slice.
461
+ */
462
+ const NEON_CAUTION = 'NEON: o MCP `neon` inspeciona schema e ensaia migração numa branch descartável ' +
463
+ '(prepare_database_migration → complete_database_migration); ele também APAGA branch e projeto — ' +
464
+ 'nunca chame tool destrutiva sem confirmação humana explícita.';
465
+
466
+ const STANDARDS_HINT = 'Digest de cada um: `morph-spec standards --find <id>`; só a seção que importa: ' +
467
+ '`morph-spec standards --show <id>#<âncora>` (`--show <id>` lista as âncoras).';
468
+
469
+ export const DISPATCH_CONTRACT = [
470
+ '## Contrato de dispatch',
471
+ '',
472
+ 'O dispatch traz só o que muda por fatia: id e título, `cobre` com o texto de cada cenário, `arquivos`, `standards`, ' +
473
+ 'o caminho do relatório e, se houver, o worktree. O resto é este contrato.',
474
+ '',
475
+ '1. RELATÓRIO: GRAVE o arquivo de relatório que o dispatch nomeia (`.morph/features/{feature}/3-implement/reports/F{n}.md`) ' +
476
+ 'ANTES de encerrar — é o retorno autoritativo, não a sua mensagem final. `SendMessage` não é substituto do relatório e não deve ser chamado.',
477
+ '2. ESTADO: NÃO toque em `plan.md`, `checks.json`, `feature.json`, `state.json` nem no board de sessão — todo estado é gravado exclusivamente pelo orquestrador.',
478
+ '3. TESTE: a prova é do orquestrador (`morph-spec verify {feature}`, depois do seu retorno), não sua. Escreva os testes pelo orçamento: ' +
479
+ 'um por pós-condição, um de regressão por bug, variantes numa tabela (`[Theory]`/`it.each`). NÃO rode teste a cada marco nem faça ciclo red→green: ' +
480
+ 'escreva código e testes, compile, e rode SÓ o filtro dos seus testes UMA vez, em primeiro plano, antes do relatório; se falhar, corrija e rode só os que falharam. ' +
481
+ 'Grave a saída em arquivo (`… > .morph/logs/test-last.log 2>&1`, sem nada depois) e leia o arquivo — um pipe troca o exit do teste pelo do último comando. ' +
482
+ 'Em batch paralelo SEM isolamento, só compile. Com `isolation: "worktree"`, o mesmo vale no seu worktree, e o último ato é o commit na branch dele.',
483
+ '4. ESCOPO: uma fatia, ~4 arquivos (nunca mais de 8). Escopo maior falha e volta — fatie antes, não depois.',
484
+ '5. SOBREVIVÊNCIA: o limite de turnos corta sem aviso: commite `wip(F{n})` a cada marco (um arquivo ou uma fatia coerente escrita), não só ao fim — ' +
485
+ 'e só os seus arquivos (`git add <paths>`, nunca `-A` nem diretório). Sem commit, a retomada recomeça do zero.',
486
+ '6. PROCESSOS: NUNCA `taskkill /IM node.exe` (derruba os MCPs de todas as sessões). Encerre dev server pelo PID que você mesmo subiu. ' +
487
+ 'Build e suíte pesados rodam por `morph-spec heavy run -- <comando>`, em primeiro plano. Exit 3 é fila cheia depois da espera da própria fila: ' +
488
+ 'não repita nem espere em laço — diga no relatório o que não rodou.',
489
+ '7. REUSO: antes de criar tipo, componente, hook ou serviço NOVO, verifique se já existe: `morph-spec graph explain <nome>` e ' +
490
+ '`graph affected <conceito> --uses-only`; sem grafo (`no-graph`), caia para `grep -rn "<nome>"` e siga — a ausência de grafo nunca bloqueia. ' +
491
+ 'Se criar mesmo assim, diga por que em uma linha no relatório.',
492
+ '8. MUTAÇÃO: nunca altere, reverta, mova ou apague código de produção para ver um teste falhar (`sed`, `mv`, `git show HEAD:… >`, `git stash`, ' +
493
+ '`git checkout --`). A prova de que o teste morde é do verificador (Gate 3).',
494
+ '9. ESCRITA: código e teste do projeto só por Write/Edit, nunca pelo Bash — heredoc, redirecionamento, `sed -i` ou script python/node que grava (`cat > X.cs <<EOF`, `sed -i … X.cs`): ' +
495
+ 'os guards de escrita só enxergam Write/Edit.',
496
+ '',
497
+ 'Formato do relatório (o `report-check` lê estas marcas):',
498
+ '',
499
+ '```',
500
+ '# F1 — <título>',
501
+ 'O que foi feito, em 2-3 frases: o comportamento entregue e onde ele mora, sem repetir a lista de arquivos.',
502
+ '**Arquivos:** `src/a.cs`, `src/a.test.cs`',
503
+ '**Checks:**',
504
+ '- C1 — provado: `a.test.cs` › nome do teste (ou `arquivo:linha`)',
505
+ '- C2 — não provado: por quê',
506
+ '```',
507
+ '',
508
+ '`**Arquivos:**` lista TODO arquivo criado ou modificado, inclusive artefato auxiliar fora do plano (factory, seed, config gerada): ' +
509
+ 'arquivo em produção sem dono é falha de protocolo que o Gate 3 aponta. Todo cenário que a fatia `cobre` aparece em `**Checks:**`. ' +
510
+ 'Problemas e como foram resolvidos vão em prosa depois.',
511
+ ];
512
+
456
513
  function buildBody(agent, maxTurns) {
457
514
  const lines = [
458
515
  `# ${agent.title ?? agent.id}`,
@@ -461,12 +518,20 @@ function buildBody(agent, maxTurns) {
461
518
  '',
462
519
  ];
463
520
 
464
- if (agent.domains?.length) {
521
+ // The 5.3.0 shape (no effort, no surfaces, implementer) renders byte-for-byte as before.
522
+ const legacyShape = !agent.effort && !agent.surfaces && isImplementer(agent);
523
+
524
+ if (legacyShape && agent.domains?.length) {
465
525
  lines.push('## Domains', ...agent.domains.map(d => `- ${d}`), '');
466
526
  }
467
527
 
528
+ if (!legacyShape && agent.mcp?.includes('neon')) {
529
+ lines.push(NEON_CAUTION, '');
530
+ }
531
+
468
532
  if (agent.standards?.length) {
469
533
  lines.push('## Standards to consult on demand');
534
+ if (!legacyShape) lines.push(STANDARDS_HINT);
470
535
  for (const s of agent.standards) lines.push(`- ${s}`);
471
536
  lines.push('');
472
537
  }
@@ -485,7 +550,9 @@ function buildBody(agent, maxTurns) {
485
550
  }
486
551
  }
487
552
 
488
- if (isImplementer(agent)) {
553
+ if (agent.effort && isImplementer(agent)) {
554
+ lines.push(...DISPATCH_CONTRACT, '');
555
+ } else if (isImplementer(agent)) {
489
556
  lines.push(
490
557
  '## How you operate',
491
558
  '',
@@ -493,11 +560,11 @@ function buildBody(agent, maxTurns) {
493
560
  '',
494
561
  );
495
562
  } else {
496
- // A read-only persona has no task to do nor doneCriteria to report.
563
+ // A read-only persona has no slice to implement nor scenario to report.
497
564
  lines.push(
498
565
  '## Como você opera',
499
566
  '',
500
- 'Você é read-only: nunca escreva, edite nem reverta código, testes, `tasks.json` ou `feature.json` (nada de `git checkout --`). ' +
567
+ 'Você é read-only: nunca escreva, edite nem reverta código, testes, o plano ou `feature.json` (nada de `git checkout --`). ' +
501
568
  'O entregável é o arquivo de relatório que o dispatch nomeia; sem arquivo nomeado, é a sua mensagem final (no scout, o mapa).',
502
569
  '',
503
570
  );
@@ -505,7 +572,7 @@ function buildBody(agent, maxTurns) {
505
572
 
506
573
  lines.push(
507
574
  // `PARCIAL` on line 1 is what report-check reads; "no verdict" keeps an
508
- // evaluator partial from carrying a Verdict the gate would read as final.
575
+ // review partial from carrying a Verdict the gate would read as final.
509
576
  '## Turn budget',
510
577
  '',
511
578
  // Pasted into EVERY dispatch of every persona — kept to the minimum that
@@ -529,7 +596,7 @@ function buildBody(agent, maxTurns) {
529
596
  'Use Read/Grep/Glob, never grep/sed/cat/find via Bash. Check a library API in the `context7` MCP before guessing ' +
530
597
  '(load a deferred `mcp__` tool with ToolSearch). Write the tests the budget asks for, then run only your own ' +
531
598
  'filter ONCE before the report — no red→green cycles, no `morph-spec verify` (the orchestrator proves it). ' +
532
- 'Never alter, revert or move production code to watch a test fail: mutation proof belongs to the Gate 3 evaluator.',
599
+ 'Never alter, revert or move production code to watch a test fail: mutation proof belongs to the Gate 3 verifier.',
533
600
  );
534
601
  }
535
602
 
@@ -24,41 +24,6 @@ export { installAgents } from './agents-installer.js';
24
24
  // Extended settings management
25
25
  // ============================================================================
26
26
 
27
- /**
28
- * Install permission rules into .claude/settings.local.json
29
- * @param {string} targetPath - Project root directory
30
- * @param {Array<string>} permissions - Permission patterns to add
31
- * @returns {Promise<Object>} Summary of changes
32
- */
33
- export async function installPermissions(targetPath, permissions) {
34
- const settings = await loadSettings(targetPath);
35
-
36
- settings.permissions = settings.permissions || [];
37
-
38
- const added = [];
39
- for (const perm of permissions) {
40
- if (!settings.permissions.includes(perm)) {
41
- settings.permissions.push(perm);
42
- added.push(perm);
43
- }
44
- }
45
-
46
- // Track in _morph_managed
47
- settings[MORPH_MANAGED_KEY] = settings[MORPH_MANAGED_KEY] || {};
48
- settings[MORPH_MANAGED_KEY].permissions_added =
49
- settings[MORPH_MANAGED_KEY].permissions_added || [];
50
- for (const perm of added) {
51
- if (!settings[MORPH_MANAGED_KEY].permissions_added.includes(perm)) {
52
- settings[MORPH_MANAGED_KEY].permissions_added.push(perm);
53
- }
54
- }
55
- settings[MORPH_MANAGED_KEY].generated_at = new Date().toISOString();
56
-
57
- await saveSettings(targetPath, settings);
58
-
59
- return { added, total: settings.permissions.length };
60
- }
61
-
62
27
  /**
63
28
  * Install environment variables into .claude/settings.local.json
64
29
  * @param {string} targetPath - Project root directory
@@ -156,7 +156,6 @@ export const MORPH_GITIGNORE_ENTRIES = [
156
156
  '.morph/memory/',
157
157
  '.morph/plans/',
158
158
  '.morph/analytics/',
159
- '.morph/traces/',
160
159
  // Per-instance activity/session logs (activity-logger.js). Each worktree
161
160
  // writes its own — committing this guarantees a merge conflict between
162
161
  // feature branches.
@@ -240,6 +239,33 @@ export async function updateGitignore(projectPath, fx = realEffects()) {
240
239
  if (content !== original) fx.writeFile(gitignorePath, content);
241
240
  }
242
241
 
242
+ const DOTNET_BUILD_DIRS = ['bin', 'obj'];
243
+
244
+ const ignoredDir = (line) => line.trim()
245
+ .replace(/^\/|^\*\*\//, '')
246
+ .replace(/\/(\*\*)?$/, '')
247
+ .replace(/^\[(\w)(\w)\]/, (_, a, b) => (a.toLowerCase() === b.toLowerCase() ? a : `[${a}${b}]`))
248
+ .toLowerCase();
249
+
250
+ /**
251
+ * Appends `bin/` and `obj/` to the project `.gitignore` when no line already
252
+ * ignores them. Never rewrites an existing line.
253
+ *
254
+ * @returns {string[]} the entries added
255
+ */
256
+ export function ensureDotnetBuildIgnores(projectPath) {
257
+ const gitignorePath = join(projectPath, '.gitignore');
258
+ const content = fs.existsSync(gitignorePath) ? fs.readFileSync(gitignorePath, 'utf8') : '';
259
+ const present = new Set(content.split(/\r?\n/).filter((l) => !l.trim().startsWith('#')).map(ignoredDir));
260
+ const missing = DOTNET_BUILD_DIRS.filter((dir) => !present.has(dir)).map((dir) => `${dir}/`);
261
+ if (!missing.length) return [];
262
+ const eol = content.includes('\r\n') ? '\r\n' : '\n';
263
+ const lead = content && !content.endsWith('\n') ? eol : '';
264
+ const block = [...(content ? [''] : []), '# .NET build output', ...missing, ''].join(eol);
265
+ fs.writeFileSync(gitignorePath, content + lead + block);
266
+ return missing;
267
+ }
268
+
243
269
  /**
244
270
  * Install a MORPH-SPEC .gitattributes section pinning morph state files to LF.
245
271
  *
@@ -55,8 +55,11 @@ import { realEffects } from '../lib/installers/effects.js';
55
55
  *
56
56
  * 3.12.0 adds heavy-gate to the Bash PreToolUse chain and heavy-release to the
57
57
  * PostToolUse and PostToolUseFailure chains: the opt-in `heavy.gateBash`.
58
+ *
59
+ * 3.13.0 adds warn-bash-source-write to the Bash PreToolUse chain: a warning,
60
+ * never a block, when a command writes project source through a redirect.
58
61
  */
59
- const HOOKS_VERSION = '3.12.0';
62
+ const HOOKS_VERSION = '3.13.0';
60
63
 
61
64
  /** Marker for old dispatch.js (v1) */
62
65
  const OLD_DISPATCH_COMMAND = 'node framework/hooks/agent-teams/dispatch.js';
@@ -93,21 +96,24 @@ const MORPH_DENY = [
93
96
  *
94
97
  * Pre-authorizes operations the agent legitimately performs during the
95
98
  * morph-spec flow, so the user does not get permission prompts on every
96
- * proposal.md / spec.md / tasks.json write.
99
+ * plan.md / slice report write.
97
100
  *
98
101
  * `Edit(path)` ONLY, for the same reason as MORPH_DENY: the `Edit` rule already
99
102
  * covers Write, and a `Write(path)` rule is inert noise.
100
103
  */
101
104
  const MORPH_ALLOW = [
102
- // Feature artifacts — the agent's working directory during proposal/plan/implement/review
105
+ // Feature artifacts — the agent's working directory during plan/uiux/implement/review
103
106
  'Edit(.morph/features/**)',
104
107
  // state.json — fallback path when state-sync hook can't cover a case
105
108
  'Edit(.morph/state.json)',
106
- // Long-lived architectural context — agent may reconcile during brainstorming
109
+ // Long-lived architectural context — agent may reconcile during planning
107
110
  'Edit(.morph/context/**)',
108
111
  // Folder creation under .morph
109
112
  'Bash(mkdir:.morph/**)',
110
113
  'Bash(ls:.morph/**)',
114
+ // Gate 3 mutation round: restores bytes itself and has --restore; an auto-mode deny pauses the gate
115
+ 'Bash(morph-spec mutate:*)',
116
+ 'Bash(npx morph-spec mutate:*)',
111
117
  ];
112
118
 
113
119
  // Backwards-compat alias used by older tests
@@ -168,6 +174,8 @@ const MORPH_HOOKS = [
168
174
  // on someone having read MORPH.md. block-raw-worktree-add is its way-IN
169
175
  // counterpart (a raw `git worktree add` builds a tree with no junctions, no
170
176
  // ports, no identity) — static, so it costs nothing after the first two.
177
+ // warn-bash-source-write only adds context (source written by redirect skips
178
+ // the Write/Edit guards); after the guards, so a blocked command is not warned.
171
179
  // heavy-gate comes after every guard — it takes a slot, and a command a guard
172
180
  // blocks must not hold one — and before the rewriter.
173
181
  // worktree-env-inject is LAST: it rewrites launcher commands (updatedInput)
@@ -179,7 +187,7 @@ const MORPH_HOOKS = [
179
187
  hooks: [{
180
188
  type: 'command',
181
189
  command: 'node framework/hooks/claude-code/core/chain.js',
182
- args: ['pre-bash-denylist', 'block-destructive-worktree-removal', 'block-raw-worktree-add', 'heavy-gate', 'worktree-env-inject']
190
+ args: ['pre-bash-denylist', 'block-destructive-worktree-removal', 'block-raw-worktree-add', 'warn-bash-source-write', 'heavy-gate', 'worktree-env-inject']
183
191
  }]
184
192
  },
185
193
 
@@ -214,15 +222,14 @@ const MORPH_HOOKS = [
214
222
  }]
215
223
  },
216
224
 
217
- // === PostToolUse: Write|Edit — tasks.json schema guard, trace auto-generation,
218
- // comment-guard LAST: it blocks, and a block ends the chain. ===
225
+ // === PostToolUse: Write|Edit — comment-guard, solo: it blocks the edit that
226
+ // adds a forbidden comment. ===
219
227
  {
220
228
  event: 'PostToolUse',
221
229
  matcher: 'Write|Edit',
222
230
  hooks: [{
223
231
  type: 'command',
224
- command: 'node framework/hooks/claude-code/core/chain.js',
225
- args: ['tasks-json-guard', 'trace-autogen', 'comment-guard']
232
+ command: 'node framework/hooks/claude-code/core/comment-guard.js'
226
233
  }]
227
234
  },
228
235
 
@@ -3,8 +3,7 @@
3
3
  *
4
4
  * Copies framework skills to .claude/skills/ during morph-spec init.
5
5
  * Installs only the `core/` skill set — the canonical NLH skills referenced by
6
- * MORPH.md (morph-brainstorming, morph-stack-scan, morph-plan, morph-implement,
7
- * morph-review, morph-standards). The 3-tier system was collapsed
6
+ * MORPH.md (morph-plan, morph-uiux, morph-apply, morph-review, morph-standards…). The 3-tier system was collapsed
8
7
  * into a single source of truth in framework/skills/core/.
9
8
  *
10
9
  * Source skills are directory-based: framework/skills/core/{skill-name}/SKILL.md
@@ -1,67 +0,0 @@
1
- # Trace Schema — morph-spec NLH
2
-
3
- ## Local de armazenamento
4
-
5
- `.morph/traces/YYYY-MM-DD-{feature}-{phase}.json`
6
-
7
- Um arquivo por fase. Se a fase for executada em múltiplas sessões, o mesmo arquivo é atualizado incrementalmente.
8
-
9
- ## Formato JSON
10
-
11
- ```json
12
- {
13
- "schema_version": "1.0",
14
- "session_id": "YYYY-MM-DD-HH-MM-SS",
15
- "feature": "nome-da-feature",
16
- "phase": "implement | plan | review",
17
- "started_at": "ISO 8601",
18
- "completed_at": "ISO 8601",
19
- "tasks": [
20
- {
21
- "id": "T1",
22
- "title": "descrição da task",
23
- "status": "completed | failed | skipped",
24
- "standards_consulted": ["id-do-standard-1"],
25
- "agent_persona": "dotnet-senior | nextjs-expert | etc",
26
- "sub_agent_dispatched": false,
27
- "files_modified": ["path/relativo/arquivo.cs"],
28
- "duration_seconds": null
29
- }
30
- ],
31
- "phase_summary": {
32
- "tasks_completed": 3,
33
- "tasks_failed": 0,
34
- "standards_most_used": ["architecture-vertical-slice"],
35
- "sub_agents_dispatched": 0
36
- }
37
- }
38
- ```
39
-
40
- ## Como o trace é gerado (automático — não escreva à mão)
41
-
42
- O trace é **gerado pelo hook `trace-autogen`** (PostToolUse em Write|Edit) a cada
43
- write de `2-plan/tasks.json` — o último write da cerimônia de cada task. As skills
44
- (`morph-implement`, `morph-review`) **não escrevem o trace**.
45
-
46
- | Fonte | O que alimenta |
47
- |-------|----------------|
48
- | `tasks.json` | conjunto de tasks, `status` (done→completed, skipped→skipped, blocked→failed; pending/in_progress ficam fora), `outputs` → `files_modified` |
49
- | `recap.md` | `**Standard:**` → `standards_consulted`; `**Persona:**` → `agent_persona` + `sub_agent_dispatched` (`morph-{id}`/`general-purpose` = true) |
50
-
51
- `completed_at` e `phase_summary` são preenchidos quando nenhuma task resta
52
- `pending`/`in_progress`. O arquivo existente da mesma feature+fase é atualizado
53
- in place (`session_id`/`started_at` preservados entre sessões).
54
-
55
- **Se o trace parecer incompleto, corrija a FONTE** (um recap sem o bloco `## T{N}`), nunca o trace.
56
-
57
- ## Regras de geração
58
-
59
- - Se um campo não tiver valor disponível, o hook usa `null` — nunca omite o campo.
60
- - Estrutura completa mantida para compatibilidade com análise de meta-harness.
61
- - `session_id` usa o formato `YYYY-MM-DD-HH-MM-SS` da primeira geração.
62
- - `standards_consulted` contém os `id`s do STANDARDS.json, não os paths.
63
- - `files_modified` usa paths relativos à raiz do repositório (vem de `outputs`).
64
-
65
- ## Motivação
66
-
67
- Raw traces preservam o sinal completo de execução. Summaries comprimem informação e perdem sinais que o meta-harness usa para otimização. Regra: logar o trace completo — nunca substituir por resumo.
@@ -1,83 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * tasks-json-guard.js — PostToolUse hook (validate-and-warn, never block)
4
- *
5
- * Event: PostToolUse | Matcher: Write|Edit
6
- *
7
- * After any Write/Edit that touches a tasks.json under .morph/features/,
8
- * re-reads the file from disk and validates the schema (status enum, effort
9
- * enum). Invalid → injects a warning so the LLM fixes the file immediately;
10
- * valid → logs one activity line and stays silent.
11
- *
12
- * The production failure this guards: a leaf sub-agent wrote
13
- * status:"completed" (not in the enum — the schema accepts "done") into
14
- * tasks.json and nothing flagged it. The blocking pre-write validation for
15
- * post-Gate-2 edits lives in protect-spec-files.js (PreToolUse); this
16
- * PostToolUse pass also covers pre-gate authoring (morph-plan) and anything
17
- * that landed on disk regardless.
18
- *
19
- * Fail-open: exits 0 on any error.
20
- */
21
-
22
- import { readFileSync } from 'fs';
23
- import { isAbsolute, join } from 'path';
24
- import { stateExists } from '../../shared/state-reader.js';
25
- import { contextResult, runStandalone } from '../../shared/hook-response.js';
26
- import { logHookActivity } from '../../shared/activity-logger.js';
27
- import { getFilePath } from '../../shared/payload-utils.js';
28
- import { isFeaturePath, getBasename } from '../../shared/phase-utils.js';
29
- import { VALID_TASK_STATUS, validateTasksDoc } from '../../shared/tasks-validator.js';
30
-
31
- function projectDir() {
32
- return process.env.CLAUDE_PROJECT_DIR || process.cwd();
33
- }
34
-
35
- /**
36
- * @param {object} payload - parsed PostToolUse stdin payload (Write|Edit)
37
- * @returns {Promise<object|null>} a hookSpecificOutput warning when tasks.json
38
- * is unparseable or schema-invalid, or null (pass/silent).
39
- */
40
- export async function run(payload) {
41
- try {
42
- const cwd = projectDir();
43
- if (!stateExists(cwd)) return null;
44
-
45
- const filePath = getFilePath(payload);
46
- if (!filePath) return null;
47
- if (!isFeaturePath(filePath)) return null;
48
- if (getBasename(filePath) !== 'tasks.json') return null;
49
-
50
- const absPath = isAbsolute(filePath) ? filePath : join(cwd, filePath);
51
-
52
- let doc;
53
- try {
54
- doc = JSON.parse(readFileSync(absPath, 'utf-8'));
55
- } catch {
56
- logHookActivity('tasks-json-guard', 'PostToolUse', 'invalid', cwd);
57
- return contextResult(
58
- '⚠️ [morph-spec] tasks.json INVÁLIDO — o arquivo gravado não parseia como JSON. ' +
59
- 'Corrija o arquivo antes de continuar.',
60
- 'PostToolUse'
61
- );
62
- }
63
-
64
- const { valid, errors } = validateTasksDoc(doc);
65
- if (!valid) {
66
- logHookActivity('tasks-json-guard', 'PostToolUse', 'invalid', cwd);
67
- return contextResult(
68
- '⚠️ [morph-spec] tasks.json com schema inválido após o write:\n' +
69
- errors.map(e => ` • ${e}`).join('\n') +
70
- `\nStatus válidos: ${VALID_TASK_STATUS.join(', ')}. Corrija antes de marcar a task como done.`,
71
- 'PostToolUse'
72
- );
73
- }
74
-
75
- logHookActivity('tasks-json-guard', 'PostToolUse', 'ok', cwd);
76
- return null;
77
- } catch {
78
- // Fail-open
79
- return null;
80
- }
81
- }
82
-
83
- await runStandalone(run);