@polymorphism-tech/morph-spec 8.46.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 (304) hide show
  1. package/CLAUDE.md +53 -44
  2. package/README.md +3 -4
  3. package/bin/morph-spec.js +38 -55
  4. package/bin/validate.js +5 -134
  5. package/claude-plugin.json +1 -1
  6. package/docs/CHEATSHEET.md +2 -2
  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/create-umbraco-lp.md +2 -0
  12. package/framework/commands/migrate-lp-to-umbraco.md +2 -0
  13. package/framework/commands/morph-apply.md +5 -28
  14. package/framework/commands/morph-archive.md +2 -2
  15. package/framework/commands/morph-hotfix.md +8 -8
  16. package/framework/commands/morph-infra.md +17 -203
  17. package/framework/commands/morph-media.md +3 -0
  18. package/framework/commands/morph-preflight.md +7 -31
  19. package/framework/commands/morph-proposal.md +2 -50
  20. package/framework/commands/morph-status.md +3 -3
  21. package/framework/commands/morph-troubleshoot.md +2 -14
  22. package/framework/docs/CLI.md +585 -286
  23. package/framework/evals/review-checklist.md +82 -41
  24. package/framework/hooks/README.md +10 -11
  25. package/framework/hooks/claude-code/core/comment-guard.js +1 -1
  26. package/framework/hooks/claude-code/core/denylist.json +33 -3
  27. package/framework/hooks/claude-code/core/enforce-phase-writes.js +16 -20
  28. package/framework/hooks/claude-code/core/gate-guard.js +2 -11
  29. package/framework/hooks/claude-code/core/pre-bash-denylist.js +38 -0
  30. package/framework/hooks/claude-code/core/protect-spec-files.js +30 -242
  31. package/framework/hooks/claude-code/core/state-sync.js +17 -19
  32. package/framework/hooks/claude-code/core/telemetry-log.js +3 -3
  33. package/framework/hooks/claude-code/core/validate-completion.js +33 -97
  34. package/framework/hooks/claude-code/core/warn-bash-source-write.js +31 -0
  35. package/framework/hooks/claude-code/core/worktree-context.js +5 -4
  36. package/framework/hooks/claude-code/statusline.py +55 -41
  37. package/framework/hooks/package.json +3 -0
  38. package/framework/hooks/shared/activity-logger.js +1 -1
  39. package/framework/hooks/shared/build-check.js +10 -6
  40. package/framework/hooks/shared/command-tokens.js +169 -0
  41. package/framework/hooks/shared/comment-rules.js +1 -1
  42. package/framework/hooks/shared/comment-scanner.js +10 -23
  43. package/framework/hooks/shared/comment-stripper.js +1 -1
  44. package/framework/hooks/shared/e2e-check.js +67 -31
  45. package/framework/hooks/shared/errata.js +72 -0
  46. package/framework/hooks/shared/feature-locator.js +1 -1
  47. package/framework/hooks/shared/frontend-locations.js +6 -0
  48. package/framework/hooks/shared/gate-question.js +18 -0
  49. package/framework/hooks/shared/phase-utils.js +15 -32
  50. package/framework/hooks/shared/slice-progress.js +118 -0
  51. package/framework/hooks/shared/spawn-command.js +23 -0
  52. package/framework/hooks/shared/state-reader.js +61 -87
  53. package/framework/hooks/shared/state-writer.js +3 -3
  54. package/framework/mcp/registry.json +3 -4
  55. package/framework/phases.json +23 -39
  56. package/framework/rules/ai-agents-standards.md +18 -16
  57. package/framework/rules/asaas-standards.md +3 -1
  58. package/framework/rules/csharp-standards.md +10 -8
  59. package/framework/rules/data-standards.md +5 -5
  60. package/framework/rules/dotnet-templates.md +1 -1
  61. package/framework/rules/frontend-standards.md +4 -2
  62. package/framework/rules/gsap-standards.md +10 -5
  63. package/framework/rules/hangfire-standards.md +3 -1
  64. package/framework/rules/infrastructure-standards.md +4 -2
  65. package/framework/rules/neon-auth-standards.md +3 -1
  66. package/framework/rules/neon-standards.md +6 -4
  67. package/framework/rules/nextjs-standards.md +13 -11
  68. package/framework/rules/nextjs-templates.md +1 -1
  69. package/framework/rules/resend-standards.md +3 -1
  70. package/framework/schemas/config.schema.json +14 -9
  71. package/framework/schemas/feature.schema.json +15 -52
  72. package/framework/skills/README.md +4 -5
  73. package/framework/skills/core/create-umbraco-lp/SKILL.md +3 -3
  74. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +3 -3
  75. package/framework/skills/core/morph-apply/SKILL.md +150 -223
  76. package/framework/skills/core/morph-hotfix/SKILL.md +34 -43
  77. package/framework/skills/core/morph-machine/SKILL.md +4 -4
  78. package/framework/skills/core/morph-media/SKILL.md +3 -3
  79. package/framework/skills/core/morph-plan/SKILL.md +138 -300
  80. package/framework/skills/core/morph-review/SKILL.md +112 -137
  81. package/framework/skills/core/morph-standards/SKILL.md +29 -41
  82. package/framework/skills/core/morph-uiux/SKILL.md +78 -153
  83. package/framework/standards/STANDARDS.json +90 -1219
  84. package/framework/standards/STANDARDS.meta.json +1305 -0
  85. package/framework/standards/ai-agents/evals-with-cache.md +2 -3
  86. package/framework/standards/ai-agents/llm-runtime-defaults.md +1 -1
  87. package/framework/standards/ai-agents/modalities-speech-to-text.md +36 -26
  88. package/framework/standards/ai-agents/prompt-sources.md +6 -4
  89. package/framework/standards/ai-agents/providers/model-registry.md +1 -1
  90. package/framework/standards/ai-agents/rag-custom-pgvector.md +67 -39
  91. package/framework/standards/ai-agents/setup.md +2 -2
  92. package/framework/standards/ai-agents/structured-output.md +1 -1
  93. package/framework/standards/ai-agents/sweet-spot.md +1 -1
  94. package/framework/standards/architecture/code-comments.md +8 -8
  95. package/framework/standards/architecture/vertical-slice/vertical-slice.md +18 -9
  96. package/framework/standards/backend/api/validation.md +135 -47
  97. package/framework/standards/backend/database/postgresql/database.md +11 -7
  98. package/framework/standards/backend/database/vector-search-rag.md +207 -324
  99. package/framework/standards/backend/dotnet/fluent-validation-vsa.md +14 -8
  100. package/framework/standards/backend/dotnet/result-pattern.md +40 -6
  101. package/framework/standards/backend/dotnet/scalar-ui.md +13 -7
  102. package/framework/standards/backend/dotnet/scrutor.md +1 -1
  103. package/framework/standards/backend/dotnet/testing.md +1 -1
  104. package/framework/standards/backend/dotnet/vsa-handler-patterns.md +7 -7
  105. package/framework/standards/backend/integrations/asaas/asaas-api.md +309 -165
  106. package/framework/standards/backend/integrations/hangfire/hangfire-jobs.md +134 -154
  107. package/framework/standards/data/nosql/cache/redis.md +120 -44
  108. package/framework/standards/data/nosql/cosmos-db.md +114 -71
  109. package/framework/standards/frontend/design-system/animations.md +1 -1
  110. package/framework/standards/frontend/design-system/naming.md +1 -1
  111. package/framework/standards/frontend/nextjs/motion-patterns.md +45 -29
  112. package/framework/standards/frontend/nextjs/naming-conventions.md +1 -1
  113. package/framework/standards/infrastructure/docker/coolify-deploy.md +8 -6
  114. package/framework/standards/infrastructure/neon/neon-pgvector.md +120 -75
  115. package/framework/standards/infrastructure/neon/neon-setup.md +53 -31
  116. package/framework/standards/integration/mcp/mcp-tools.md +7 -7
  117. package/framework/templates/README.md +2 -3
  118. package/framework/templates/REGISTRY.json +6 -170
  119. package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +2 -2
  120. package/framework/templates/code/dotnet/extensions/database-extensions.cs +1 -1
  121. package/framework/templates/code/dotnet/extensions/result-extensions.cs +27 -0
  122. package/framework/templates/code/dotnet/feature/endpoint.cs.hbs +2 -2
  123. package/framework/templates/docs/plan.md +72 -0
  124. package/framework/templates/dotnet/ai-kit/README.md +1 -9
  125. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaDisclosureTests.cs +1 -1
  126. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Morph.AiKit.Tests.csproj +0 -10
  127. package/framework/templates/infrastructure/docker/coolify.template.json +3 -3
  128. package/framework/templates/infrastructure/github/README.md +2 -1
  129. package/framework/templates/infrastructure/github/workflows/deploy-coolify.yml.hbs +2 -1
  130. package/framework/templates/meta-prompts/squad-leaders/backend-squad.md +1 -1
  131. package/framework/templates/meta-prompts/squad-leaders/frontend-squad.md +1 -1
  132. package/framework/templates/meta-prompts/squad-leaders/squad-leader.md +1 -1
  133. package/framework/templates/meta-prompts/validators/pre-commit-validator.md +3 -4
  134. package/package.json +1 -1
  135. package/src/commands/e2e/e2e.js +2 -2
  136. package/src/commands/observability/cost.js +18 -60
  137. package/src/commands/observability/telemetry.js +3 -5
  138. package/src/commands/project/doctor-procs.js +1 -1
  139. package/src/commands/project/doctor.js +9 -9
  140. package/src/commands/project/fleet.js +4 -4
  141. package/src/commands/project/heavy.js +3 -3
  142. package/src/commands/project/init.js +6 -1
  143. package/src/commands/project/setup-infra-cmd.js +2 -1
  144. package/src/commands/project/stack-scan.js +58 -0
  145. package/src/commands/project/standards.js +79 -25
  146. package/src/commands/project/status.js +29 -69
  147. package/src/commands/project/update.js +33 -19
  148. package/src/commands/project/worktree.js +1 -1
  149. package/src/commands/state/advance.js +62 -46
  150. package/src/commands/state/approve.js +95 -138
  151. package/src/commands/state/archive.js +20 -84
  152. package/src/commands/state/auto-approval.js +68 -0
  153. package/src/commands/state/create.js +28 -1
  154. package/src/commands/state/delete.js +1 -1
  155. package/src/commands/state/finish.js +11 -23
  156. package/src/commands/state/gate-check.js +86 -95
  157. package/src/commands/state/gate-decision.js +6 -4
  158. package/src/commands/state/handoff.js +1 -1
  159. package/src/commands/state/list.js +9 -9
  160. package/src/commands/state/report-check.js +157 -311
  161. package/src/commands/state/retype.js +9 -3
  162. package/src/commands/validation/mutate.js +77 -14
  163. package/src/commands/validation/plan-check.js +81 -0
  164. package/src/commands/validation/validate-feature.js +60 -135
  165. package/src/commands/validation/verify.js +41 -82
  166. package/src/core/paths/output-schema.js +20 -67
  167. package/src/core/state/gate-order.js +5 -6
  168. package/src/core/state/lifecycle-audit.js +2 -2
  169. package/src/core/state/state-manager.js +17 -98
  170. package/src/core/templates/template-registry.js +2 -2
  171. package/src/core/templates/template-seed.js +22 -0
  172. package/src/core/work-types.js +10 -10
  173. package/src/dashboard/app/App.jsx +0 -2
  174. package/src/dashboard/app/components/ActivityFeed.jsx +2 -3
  175. package/src/dashboard/app/components/CommandPalette.jsx +7 -8
  176. package/src/dashboard/app/components/EntityLink.jsx +1 -2
  177. package/src/dashboard/app/components/Markdown.jsx +1 -1
  178. package/src/dashboard/app/components/Sidebar.jsx +1 -2
  179. package/src/dashboard/app/hooks/useMorphQuery.js +1 -1
  180. package/src/dashboard/app/lib/phases.js +2 -4
  181. package/src/dashboard/app/pages/Activity.jsx +1 -1
  182. package/src/dashboard/app/pages/FeatureDetail.jsx +139 -129
  183. package/src/dashboard/app/pages/Features.jsx +9 -9
  184. package/src/dashboard/app/pages/Guide.jsx +2 -2
  185. package/src/dashboard/app/pages/PhasePipeline.jsx +3 -4
  186. package/src/dashboard/app/pages/RulesMap.jsx +3 -3
  187. package/src/dashboard/app/pages/SkillsLibrary.jsx +3 -3
  188. package/src/dashboard/app/pages/Standards.jsx +1 -1
  189. package/src/dashboard/scanner/activity-scanner.js +6 -16
  190. package/src/dashboard/scanner/agents-scanner.js +1 -1
  191. package/src/dashboard/scanner/docs-scanner.js +5 -23
  192. package/src/dashboard/scanner/features-scanner.js +95 -60
  193. package/src/dashboard/scanner/index.js +0 -2
  194. package/src/dashboard/scanner/project-api.js +1 -1
  195. package/src/dashboard/scanner/rules-scanner.js +15 -10
  196. package/src/dashboard/scanner/skills-scanner.js +2 -3
  197. package/src/dashboard/scanner/standards-scanner.js +2 -1
  198. package/src/lib/ci/ci-verify.js +17 -1
  199. package/src/lib/ci/github-ci.js +1 -1
  200. package/src/lib/detectors/index.js +3 -8
  201. package/src/lib/generators/handoff-generator.js +34 -65
  202. package/src/lib/generators/settings-generator.js +11 -89
  203. package/src/lib/git/evidence-staleness.js +1 -1
  204. package/src/lib/git/tree-fingerprint.js +19 -7
  205. package/src/lib/git/tree-state.js +2 -2
  206. package/src/lib/heavy/cleanup.js +1 -5
  207. package/src/lib/installers/memory-installer.js +6 -8
  208. package/src/lib/mutation/commands.js +3 -2
  209. package/src/lib/{tasks/dotnet-test-introspection.js → mutation/dotnet-introspection.js} +17 -282
  210. package/src/lib/mutation/plan.js +14 -6
  211. package/src/lib/mutation/runner.js +1 -1
  212. package/src/lib/mutation/subset.js +46 -9
  213. package/src/lib/plan/artifact-vocabulary.js +60 -0
  214. package/src/lib/plan/checks.js +234 -0
  215. package/src/lib/plan/plan-parser.js +209 -0
  216. package/src/lib/plan/plan-template.js +18 -0
  217. package/src/lib/review-reports.js +222 -0
  218. package/src/lib/risk-detector.js +134 -135
  219. package/src/lib/specs/adr-index.js +50 -69
  220. package/src/lib/specs/design-seed.js +18 -0
  221. package/src/lib/specs/frontmatter.js +44 -0
  222. package/src/lib/specs/gfm-table.js +17 -0
  223. package/src/lib/specs/old-format.js +68 -0
  224. package/src/lib/specs/revocations.js +37 -95
  225. package/src/lib/specs/screen-checklist.js +18 -17
  226. package/src/lib/stack/dotnet-build-ignores.js +47 -0
  227. package/src/lib/stack/stack-profile.js +1 -1
  228. package/src/lib/stack/stack-scan.js +267 -0
  229. package/src/lib/standards/registry.js +43 -0
  230. package/src/lib/standards/sections.js +58 -0
  231. package/src/lib/tasks/eval-node.js +4 -12
  232. package/src/lib/tasks/test-nodes.js +1 -74
  233. package/src/lib/tasks/test-runner.js +75 -117
  234. package/src/lib/telemetry/metrics.js +8 -69
  235. package/src/lib/telemetry/telemetry-reader.js +5 -118
  236. package/src/lib/telemetry/verify-event.js +5 -7
  237. package/src/lib/validators/code-budget/code-budget-validator.js +1 -1
  238. package/src/lib/validators/contracts/contract-compliance-validator.js +2 -2
  239. package/src/lib/validators/reuse/reuse-validator.js +24 -48
  240. package/src/lib/validators/reuse/symbol-scanner.js +2 -4
  241. package/src/lib/validators/traceability-check.js +85 -129
  242. package/src/lib/validators/validation-runner.js +8 -88
  243. package/src/lib/validators/verify-runner.js +134 -301
  244. package/src/lib/worktree/docker-audit.js +1 -2
  245. package/src/lib/worktree/fleet-scanner.js +25 -38
  246. package/src/scripts/setup-infra.js +42 -22
  247. package/src/utils/agents-installer.js +92 -13
  248. package/src/utils/claude-settings-manager.js +0 -35
  249. package/src/utils/file-copier.js +34 -6
  250. package/src/utils/hooks-installer.js +16 -9
  251. package/src/utils/skills-installer.js +1 -2
  252. package/framework/commands/commit.md +0 -166
  253. package/framework/evals/trace-schema.md +0 -67
  254. package/framework/hooks/claude-code/core/tasks-json-guard.js +0 -83
  255. package/framework/hooks/claude-code/core/trace-autogen.js +0 -277
  256. package/framework/hooks/git/pre-commit/specs.sh +0 -50
  257. package/framework/hooks/shared/tasks-validator.js +0 -111
  258. package/framework/rubrics/backend.json +0 -41
  259. package/framework/rubrics/default.json +0 -36
  260. package/framework/rubrics/frontend.json +0 -36
  261. package/framework/rules/morph-task-tracking.md +0 -54
  262. package/framework/rules/morph-workflow.md +0 -244
  263. package/framework/schemas/tasks.schema.json +0 -79
  264. package/framework/skills/core/morph-brainstorming/SKILL.md +0 -161
  265. package/framework/skills/core/morph-implement/SKILL.md +0 -194
  266. package/framework/skills/core/morph-proposal/SKILL.md +0 -152
  267. package/framework/skills/core/morph-stack-scan/SKILL.md +0 -145
  268. package/framework/templates/docs/proposal.md +0 -192
  269. package/framework/templates/docs/spec.md +0 -57
  270. package/framework/templates/docs/ui-components.md +0 -52
  271. package/framework/templates/docs/ui-design-system.md +0 -76
  272. package/framework/templates/docs/ui-flows.md +0 -167
  273. package/framework/templates/examples/spec-examples.md +0 -91
  274. package/framework/templates/feature/decisions.md +0 -124
  275. package/framework/templates/feature/recap.md +0 -145
  276. package/framework/templates/project/validate.js +0 -97
  277. package/framework/templates/tasks.template.json +0 -17
  278. package/src/commands/state/usecase.js +0 -143
  279. package/src/commands/tasks/dag.js +0 -181
  280. package/src/dashboard/app/pages/Rubrics.jsx +0 -97
  281. package/src/dashboard/scanner/rubrics-scanner.js +0 -12
  282. package/src/lib/agents/output-collector.js +0 -76
  283. package/src/lib/evaluator-report.js +0 -91
  284. package/src/lib/specs/acervo-format.js +0 -70
  285. package/src/lib/specs/acervo-lifecycle.js +0 -264
  286. package/src/lib/specs/done-criteria-check.js +0 -200
  287. package/src/lib/specs/usecase-check.js +0 -301
  288. package/src/lib/specs/usecase-promoter.js +0 -301
  289. package/src/lib/specs/usecase-registry.js +0 -237
  290. package/src/lib/tasks/dispatch-planner.js +0 -319
  291. package/src/lib/tasks/mermaid-renderer.js +0 -132
  292. package/src/lib/tasks/task-classifier.js +0 -30
  293. package/src/lib/tasks/task-parser.js +0 -209
  294. package/src/lib/tasks/tasks-json-parser.js +0 -296
  295. package/src/lib/tasks/test-scope.js +0 -597
  296. package/src/lib/validators/architecture/architecture-validator.js +0 -270
  297. package/src/lib/validators/architecture/index.js +0 -7
  298. package/src/lib/validators/css/css-validator.js +0 -340
  299. package/src/lib/validators/css/index.js +0 -7
  300. package/src/lib/validators/design-system/design-system-validator.js +0 -237
  301. package/src/lib/validators/design-system/index.js +0 -7
  302. package/src/lib/validators/score-aggregator.js +0 -244
  303. package/src/lib/validators/ui/index.js +0 -7
  304. package/src/lib/validators/ui/ui-contrast-validator.js +0 -422
@@ -6,19 +6,9 @@
6
6
  * Event: PreToolUse | Matcher: Write|Edit
7
7
  *
8
8
  * Blocks Write/Edit to spec artifacts after their approval gate has passed:
9
- * - 2-plan/spec.md, etc. → blocked if 'plan' gate approved
10
- * - 1-design/design-system.md, etc. → blocked if 'uiux' gate approved
11
- *
12
- * Semantic exception for files in MUTABLE_SPEC_FIELDS (e.g. tasks.json):
13
- * the plan contract is frozen, but per-task progress fields (status, outputs,
14
- * notes) may still change during the implement phase. For those files BOTH a
15
- * full Write and a targeted Edit are allowed — but only when the semantic diff
16
- * touches solely the mutable fields. The Edit is simulated (old_string →
17
- * new_string applied to the current file) and diffed exactly like a Write, so a
18
- * one-field status flip via Edit is permitted while any change to the frozen
19
- * plan contract (titles, dependencies, doneCriteria, the task set) is blocked.
20
- * This is the sanctioned progress path — the agent never has to rewrite the
21
- * whole file nor reach for a Node script to record task status.
9
+ * - 2-plan/plan.md → after 'plan', only an append under `## Errata`
10
+ * - 2-plan/checks.json → never by hand: `morph-spec plan-check` is its only writer
11
+ * - 1-design/design-system.md, etc. → after 'uiux', only an errata append
22
12
  *
23
13
  * A gate freezes the APPROVED ARTIFACT, so a protected file that does not yet
24
14
  * exist on disk is a CREATION and passes — the criterion is the file's
@@ -33,152 +23,14 @@ import {
33
23
  isFeaturePath,
34
24
  extractFeatureName,
35
25
  getBasename,
36
- PROTECTED_SPEC_FILES,
37
- MUTABLE_SPEC_FIELDS
26
+ PROTECTED_SPEC_FILES
38
27
  } from '../../shared/phase-utils.js';
39
28
  import { blockResult, runStandalone } from '../../shared/hook-response.js';
40
29
  import { logHookActivity } from '../../shared/activity-logger.js';
41
30
  import { getFilePath, getToolName, resolveMorphRoot, resolveTargetPath } from '../../shared/payload-utils.js';
42
- import { validateTasksDoc, outputsDivergenceViolations } from '../../shared/tasks-validator.js';
43
-
44
- /**
45
- * Compare two tasks.json structures and return the list of plan-contract
46
- * violations — changes that touch anything other than the mutable fields.
47
- *
48
- * Frozen-by-default, not an enumerated frozen list: the loop below walks the
49
- * UNION of old/new task keys and flags any key not in `mutableFields`. A new
50
- * plan-contract field (e.g. `agent`/`standards`, stamped by morph-plan — R2
51
- * token-cut) needs NO entry added here to become protected; it is frozen the
52
- * moment it exists on a task, simply by never being added to
53
- * MUTABLE_SPEC_FIELDS (output-schema.js → `npm run generate` →
54
- * framework/hooks/shared/phase-utils.js). Only fields that must stay
55
- * EDITABLE post-Gate-2 (today: status, outputs, notes) belong in that list.
56
- *
57
- * @param {object} oldDoc - parsed previous file content
58
- * @param {object} newDoc - parsed proposed file content
59
- * @param {string[]} mutableFields - field names allowed to change
60
- * @returns {string[]} human-readable violation messages (empty = OK)
61
- */
62
- function diffPlanContract(oldDoc, newDoc, mutableFields) {
63
- const violations = [];
64
- const mutable = new Set(mutableFields);
65
-
66
- // Backfilling a previously-absent `version` (undefined → any value) is a
67
- // benign schema-tooling update, not a plan-contract change — it's what
68
- // lets legacy tasks.json adopt the field so `morph-spec dag` can analyze
69
- // it without a lenient fallback. An actual change between two SET
70
- // versions is still a real contract change and stays blocked.
71
- const versionBackfilled = oldDoc.version === undefined && newDoc.version !== undefined;
72
- if (!versionBackfilled && oldDoc.version !== newDoc.version) {
73
- violations.push(`'version' changed (${oldDoc.version} → ${newDoc.version})`);
74
- }
75
- if (oldDoc.feature !== newDoc.feature) {
76
- violations.push(`'feature' changed (${oldDoc.feature} → ${newDoc.feature})`);
77
- }
78
-
79
- const oldTasks = Array.isArray(oldDoc.tasks) ? oldDoc.tasks : [];
80
- const newTasks = Array.isArray(newDoc.tasks) ? newDoc.tasks : [];
81
-
82
- if (oldTasks.length !== newTasks.length) {
83
- violations.push(`task count changed (${oldTasks.length} → ${newTasks.length}) — tasks cannot be added or removed`);
84
- }
85
-
86
- const oldById = new Map(oldTasks.map(t => [t.id, t]));
87
- const newById = new Map(newTasks.map(t => [t.id, t]));
88
-
89
- // A task present before must still exist (no removals / id renames).
90
- for (const id of oldById.keys()) {
91
- if (!newById.has(id)) violations.push(`task '${id}' was removed`);
92
- }
93
- for (const id of newById.keys()) {
94
- if (!oldById.has(id)) violations.push(`task '${id}' was added`);
95
- }
96
-
97
- // For tasks present on both sides, only mutable fields may differ.
98
- for (const [id, newTask] of newById) {
99
- const oldTask = oldById.get(id);
100
- if (!oldTask) continue;
101
- const keys = new Set([...Object.keys(oldTask), ...Object.keys(newTask)]);
102
- for (const key of keys) {
103
- if (mutable.has(key)) continue;
104
- if (JSON.stringify(oldTask[key]) !== JSON.stringify(newTask[key])) {
105
- violations.push(`task '${id}': immutable field '${key}' changed`);
106
- }
107
- }
108
- }
109
-
110
- return violations;
111
- }
112
-
113
- /**
114
- * O heading que abre o bloco de errata, no fim de um arquivo congelado.
115
- *
116
- * Congelar o CONTRATO não é a mesma coisa que congelar um ERRO DE FATO. Caso
117
- * real: a spec afirmava, entre parênteses, que certos registros também eram
118
- * escritos por humanos; o código mostrava que os três escritores eram do
119
- * próprio motor. A afirmação era falsa sobre o código, não uma mudança de
120
- * escopo — mas a trava cega não distingue as duas coisas, então a mentira
121
- * ficava na spec e a correção morava em outro arquivo. Quem abrisse a spec
122
- * primeiro lia a versão errada.
123
- *
124
- * A errata resolve isso SEM abrir o contrato: o corpo continua byte-idêntico e
125
- * só se acrescenta texto ao fim, num bloco identificado. Escopo negociado ⇒
126
- * revogar o gate. Fato errado sobre o código ⇒ errata.
127
- */
128
- const ERRATA_HEADING = '## Errata';
129
-
130
- /** Índice onde começa o bloco de errata (-1 se não há). Só reconhece o heading em início de linha. */
131
- function errataIndex(text) {
132
- if (text.startsWith(ERRATA_HEADING)) return 0;
133
- const at = text.indexOf(`\n${ERRATA_HEADING}`);
134
- return at === -1 ? -1 : at + 1;
135
- }
136
-
137
- /**
138
- * Corpo congelado = tudo ANTES do bloco de errata (ou o texto inteiro, se não
139
- * há bloco), sem o branco final.
140
- *
141
- * O `trimEnd` não é frouxidão: separar o bloco por uma linha em branco é o
142
- * formato markdown natural, e sem isso a primeira errata de todas seria
143
- * recusada por "mudou o corpo" — quando o que mudou foi um `\n` de formatação.
144
- * A garantia que importa (nenhum caractere SIGNIFICATIVO do texto aprovado
145
- * muda) fica intacta, porque só espaço em branco terminal é ignorado.
146
- */
147
- function frozenBody(text) {
148
- const at = errataIndex(text);
149
- return (at === -1 ? text : text.slice(0, at)).trimEnd();
150
- }
31
+ import { ERRATA_HEADING, errataViolation } from '../../shared/errata.js';
151
32
 
152
- /**
153
- * A mudança proposta é uma errata legítima?
154
- *
155
- * Três condições, todas necessárias — frouxo em qualquer uma delas e a trava
156
- * cega vira uma trava que uma edição criativa atravessa:
157
- * 1. o corpo congelado permanece BYTE-IDÊNTICO;
158
- * 2. o resultado de fato tem um bloco `## Errata` (senão é edição do corpo);
159
- * 3. a errata só CRESCE — o que já estava escrito nela não é reescrito.
160
- *
161
- * A (3) existe porque uma errata reescrita perde a serventia: o valor dela é
162
- * ser trilha acumulada, não um segundo lugar editável.
163
- *
164
- * @returns {{ok: true} | {ok: false, why: string}}
165
- */
166
- function errataViolation(original, proposed) {
167
- if (frozenBody(proposed) !== frozenBody(original)) {
168
- return { ok: false, why: 'o corpo da spec mudou — errata só ACRESCENTA ao fim, nunca altera o texto aprovado' };
169
- }
170
- if (errataIndex(proposed) === -1) {
171
- return { ok: false, why: `o conteúdo novo precisa estar sob um bloco \`${ERRATA_HEADING}\` no fim do arquivo` };
172
- }
173
- const oldAt = errataIndex(original);
174
- if (oldAt !== -1) {
175
- const oldErrata = original.slice(oldAt);
176
- if (!proposed.slice(errataIndex(proposed)).startsWith(oldErrata)) {
177
- return { ok: false, why: 'a errata existente foi reescrita — ela é append-only, entradas anteriores não se apagam' };
178
- }
179
- }
180
- return { ok: true };
181
- }
33
+ const CHECKS_FILE = 'checks.json';
182
34
 
183
35
  /**
184
36
  * Texto proposto para um arquivo totalmente travado, a partir de Write ou Edit.
@@ -221,7 +73,7 @@ export async function run(payload) {
221
73
  // The gate that locks this file is recorded in the feature.json of the tree
222
74
  // that OWNS it. Asking the session's tree instead made this guard go DARK in
223
75
  // a secondary worktree: the root had no approved gate, so a post-Gate-2
224
- // spec.md rewrite sailed through. Same defect as enforce-phase-writes,
76
+ // plan.md rewrite sailed through. Same defect as enforce-phase-writes,
225
77
  // opposite direction — there it over-blocked, here it under-protected.
226
78
  const root = resolveMorphRoot(filePath, process.cwd());
227
79
  if (!stateExists(root)) return null;
@@ -235,6 +87,19 @@ export async function run(payload) {
235
87
  // Not a protected file
236
88
  if (!requiredGate) return null;
237
89
 
90
+ // Only plan-check writes checks.json; a hand-written one would carry
91
+ // checks the parser never judged. Decided before the creation carve-out:
92
+ // chore/hotfix are born with the plan gate stamped.
93
+ if (filename === CHECKS_FILE) {
94
+ logHookActivity('protect-spec-files', 'PreToolUse', 'blocked');
95
+ return blockResult(
96
+ `MORPH-SPEC: '${CHECKS_FILE}' is generated — never written by hand.\n` +
97
+ `Edit 2-plan/plan.md and run \`morph-spec plan-check ${featureName}\`: it parses the plan and writes ${CHECKS_FILE}.\n` +
98
+ `After the plan gate the plan is frozen: a wrong fact goes in \`${ERRATA_HEADING}\` (append-only, ${CHECKS_FILE} unchanged);\n` +
99
+ `a real scope change needs \`morph-spec unapprove ${featureName} plan\`, then plan-check and the gate again.`
100
+ );
101
+ }
102
+
238
103
  // Gate not yet approved — allow editing
239
104
  if (!isGateApproved(featureName, requiredGate, root)) {
240
105
  logHookActivity('protect-spec-files', 'PreToolUse', 'ok');
@@ -247,9 +112,9 @@ export async function run(payload) {
247
112
  // Um gate aprovado congela o ARTEFATO APROVADO. Se o arquivo ainda não
248
113
  // existe no disco, não há artefato aprovado a proteger: isto é a CRIAÇÃO
249
114
  // inicial, não a edição de uma spec fechada. O caso é real e não é raro —
250
- // `chore`/`hotfix` nascem com `proposal`+`plan` carimbados `policy:{tipo}`
115
+ // `chore`/`hotfix` nascem com `plan` carimbado `policy:{tipo}`
251
116
  // (create.js → work-types.js skippedGates), então o primeiro `Write` de
252
- // `2-plan/spec.md` já chega aqui com o gate "aprovado" sem que humano
117
+ // `2-plan/plan.md` já chega aqui com o gate "aprovado" sem que humano
253
118
  // nenhum tenha aprovado coisa alguma — e era barrado com uma mensagem
254
119
  // mandando revogar o gate de um arquivo inexistente.
255
120
  //
@@ -259,93 +124,16 @@ export async function run(payload) {
259
124
  // sobre o disco, a janela fecha sozinha no instante em que o arquivo
260
125
  // existe — os contra-canários de test/hooks/protect-spec-files.test.js
261
126
  // pinam exatamente isso.
262
- //
263
- // A decisão é explícita e vale para as DUAS metades do hook. Antes, o ramo
264
- // de campos mutáveis (tasks.json) já deixava a primeira criação passar, mas
265
- // por acidente — o ENOENT caía no catch fail-open da leitura — enquanto o
266
- // ramo travado bloqueava. Duas metades respondendo à mesma pergunta em
267
- // direções opostas; agora respondem aqui, uma vez só. O `result` distinto
268
- // no log é o que torna essa unificação observável.
127
+ // O `result` distinto no log é o que torna a criação observável.
269
128
  if (!existsSync(absPath)) {
270
129
  logHookActivity('protect-spec-files', 'PreToolUse', 'ok:absent');
271
130
  return null;
272
131
  }
273
132
 
274
- const mutableFields = MUTABLE_SPEC_FIELDS[filename];
275
-
276
- // Files with mutable progress fields (e.g. tasks.json): allow a full Write
277
- // that touches only those fields. Anything else stays blocked.
278
- if (mutableFields) {
279
- const toolName = getToolName(payload);
280
-
281
- // Build the proposed document from either a full Write or a targeted Edit,
282
- // then semantic-diff it against the current file on disk. Any failure to
283
- // read / parse / apply → fail-open (pass), never a false block. O arquivo
284
- // EXISTE aqui (a inexistência já foi decidida acima), então este catch
285
- // volta a significar só o que dizia originalmente: não deu para diffar.
286
- let oldDoc, newDoc;
287
- try {
288
- const original = readFileSync(absPath, 'utf-8');
289
- oldDoc = JSON.parse(original);
290
-
291
- if (toolName === 'Edit') {
292
- // Simulate the edit: apply old_string → new_string to the current file.
293
- const oldStr = payload?.tool_input?.old_string;
294
- const newStr = payload?.tool_input?.new_string;
295
- if (typeof oldStr !== 'string' || typeof newStr !== 'string') throw new Error('no edit strings');
296
- const edited = payload?.tool_input?.replace_all
297
- ? original.split(oldStr).join(newStr)
298
- : original.replace(oldStr, newStr);
299
- newDoc = JSON.parse(edited);
300
- } else {
301
- const newContent = payload?.tool_input?.content;
302
- if (typeof newContent !== 'string') throw new Error('no content');
303
- newDoc = JSON.parse(newContent);
304
- }
305
- } catch {
306
- logHookActivity('protect-spec-files', 'PreToolUse', 'ok');
307
- return null;
308
- }
309
-
310
- const violations = diffPlanContract(oldDoc, newDoc, mutableFields);
311
-
312
- // Schema check on the proposed doc: even mutable-field writes must keep a
313
- // valid status enum (a leaf agent writing status:"completed" was the
314
- // production failure this closes). Only NEWLY introduced schema errors
315
- // block — a pre-existing violation must not deadlock progress writes.
316
- const preExisting = new Set(validateTasksDoc(oldDoc).errors);
317
- violations.push(...validateTasksDoc(newDoc).errors.filter(e => !preExisting.has(e)));
318
-
319
- // Planned-vs-real outputs diff: changing `outputs` is allowed, but the
320
- // divergence must be explained in `notes` in the same edit.
321
- violations.push(...outputsDivergenceViolations(oldDoc, newDoc));
322
-
323
- if (violations.length === 0) {
324
- // Only mutable progress fields changed — a legitimate implement-phase
325
- // update via Write OR Edit.
326
- logHookActivity('protect-spec-files', 'PreToolUse', 'ok');
327
- return null;
328
- }
329
-
330
- logHookActivity('protect-spec-files', 'PreToolUse', 'blocked');
331
- return blockResult(
332
- `MORPH-SPEC: '${filename}' write rejected — the '${requiredGate}' gate has been approved.\n` +
333
- `This ${toolName} violates the plan contract or the tasks schema:\n` +
334
- violations.map(v => ` • ${v}`).join('\n') + '\n\n' +
335
- `During the implement phase only these fields may change: ${mutableFields.join(', ')}\n` +
336
- `(status must stay in the schema enum, and outputs that diverge from the plan\n` +
337
- `require an explanatory 'notes' in the same edit).\n` +
338
- `To change the plan itself, ask the user to revoke the '${requiredGate}' gate,\n` +
339
- `then re-present it for approval.`
340
- );
341
- }
342
-
343
- // ── Arquivo totalmente travado (spec.md, design) ────────────
133
+ // ── Arquivo totalmente travado (plan.md, design) ────────────
344
134
  // O corpo continua congelado. A ÚNICA passagem é a errata append-only:
345
135
  // corpo byte-idêntico + texto novo sob `## Errata` no fim. Qualquer falha
346
- // de leitura/reconstrução cai no bloqueio (o conservador aqui é BLOQUEAR —
347
- // ao contrário do ramo do tasks.json, onde não conseguir diffar significa
348
- // não ter como provar violação; aqui a regra base é "não se edita").
136
+ // de leitura/reconstrução cai no bloqueio: a regra base é "não se edita".
349
137
  //
350
138
  // O arquivo EXISTE neste ponto — a inexistência foi decidida lá em cima, e
351
139
  // é a única coisa que este catch NÃO significa mais. ENOENT tratado como
@@ -368,13 +156,13 @@ export async function run(payload) {
368
156
  logHookActivity('protect-spec-files', 'PreToolUse', 'blocked');
369
157
  return blockResult(
370
158
  `MORPH-SPEC: '${filename}' is locked — the '${requiredGate}' gate has been approved.\n` +
371
- `Editing approved specs breaks the spec contract.\n\n` +
159
+ `Editing an approved artifact breaks its contract.\n\n` +
372
160
  (errataCheck ? `Esta mudança não é uma errata válida: ${errataCheck.why}.\n\n` : '') +
373
- `Corrigir um FATO ERRADO sobre o código (a spec afirma algo que o código\n` +
374
- `desmente) não exige revogar o gate: acrescente um bloco \`${ERRATA_HEADING}\` no\n` +
161
+ `Corrigir um FATO ERRADO sobre o código (o texto aprovado afirma algo que o\n` +
162
+ `código desmente) não exige revogar o gate: acrescente sob \`${ERRATA_HEADING}\` no\n` +
375
163
  `FIM do arquivo, deixando o corpo intacto — é append-only e fica na trilha.\n\n` +
376
- `Mudar o ESCOPO acordado é outra coisa: peça ao usuário para revogar o gate\n` +
377
- `'${requiredGate}', edite, e re-apresente o gate para aprovação.`
164
+ `Mudar o ESCOPO acordado é outra coisa: \`morph-spec unapprove ${featureName} ${requiredGate}\`,\n` +
165
+ `edite, rode plan-check quando for o plan.md, e re-apresente o gate para aprovação.`
378
166
  );
379
167
  } catch {
380
168
  // Fail-open
@@ -6,15 +6,18 @@
6
6
  * the agent to think about state. Fail-open: any error returns {} silently.
7
7
  *
8
8
  * Triggers:
9
- * - Write/Edit of .morph/features/{feature}/0-proposal/proposal.md → register feature
9
+ * - Write/Edit of .morph/features/{feature}/2-plan/plan.md → register feature
10
10
  * - Write/Edit of .morph/features/{feature}/{phase}/{output} → mark output created
11
11
  * - AskUserQuestion answered with option starting "aprovado" → advance gate
12
12
  */
13
13
 
14
+ import { existsSync } from 'fs';
15
+ import { join } from 'path';
14
16
  import { loadState, stateExists } from '../../shared/state-reader.js';
15
17
  import { persistFeature, setGateApproved, createDefaultFeature, readFeatureFile } from '../../shared/state-writer.js';
16
18
  import { resolveMorphRoot } from '../../shared/payload-utils.js';
17
19
  import { contextResult, runStandalone } from '../../shared/hook-response.js';
20
+ import { gateFromQuestion } from '../../shared/gate-question.js';
18
21
  import {
19
22
  locateFeatureForQuestion,
20
23
  allWorkTrees,
@@ -23,7 +26,6 @@ import {
23
26
  } from '../../shared/feature-locator.js';
24
27
 
25
28
  const PHASE_FOLDERS = {
26
- '0-proposal': 'proposal',
27
29
  '1-design': 'design',
28
30
  '1-ui': 'uiux', // legacy alias — still recognized during folder transition
29
31
  '2-plan': 'plan',
@@ -92,24 +94,10 @@ function isApprovalAnswer(answer) {
92
94
  );
93
95
  }
94
96
 
95
- function gateFromQuestion(question) {
96
- if (typeof question !== 'string') return null;
97
- // Checked BEFORE the generic patterns below: the pinned uiux template is
98
- // "Gate uiux — Design de {feature} aprovado?" (morph-uiux/SKILL.md) — it
99
- // must never fall through to 'plan' via a stray "plan"/"plano" substring in
100
- // the feature name. `/gate\s+uiux/i` alone is unambiguous either way.
101
- if (/gate\s+uiux/i.test(question)) return 'uiux';
102
- const lower = question.toLowerCase();
103
- if (lower.includes('gate 1') || lower.includes('proposta')) return 'proposal';
104
- if (lower.includes('gate 2') || lower.includes('plano') || lower.includes('plan')) return 'plan';
105
- if (lower.includes('gate 3') || lower.includes('review')) return 'review';
106
- return null;
107
- }
108
-
109
97
  // Gate monotonicity — mirror of src/core/state/gate-order.js (hooks are
110
98
  // installed standalone and cannot import from src/). Keep in sync:
111
- // proposal → plan → review; uiux requires proposal.
112
- const GATE_PREREQ = { proposal: [], uiux: ['proposal'], plan: ['proposal'], review: ['plan'] };
99
+ // plan first; uiux and review require plan.
100
+ const GATE_PREREQ = { plan: [], uiux: ['plan'], review: ['plan'] };
113
101
  function unmetGatePrereqs(gate, gates) {
114
102
  const isApproved = (g) => gates?.[g]?.approved === true || gates?.[g] === true;
115
103
  return (GATE_PREREQ[gate] || []).filter((p) => !isApproved(p));
@@ -216,7 +204,17 @@ function handleAskUserQuestion(input, sessionRoot) {
216
204
  // (`morph-spec approve`) produce one consistent shape
217
205
  // ({ approved, timestamp, approvedBy }) in the authoritative feature.json —
218
206
  // and a manual approval stays distinguishable from a policy auto-approval.
219
- setGateApproved(featureName, gate, { approvedBy: 'manual' }, root);
207
+ // Same contract as `approve`'s --self-assessed: a human may ship Gate 3 without the
208
+ // independent review reports, but the degradation is stamped, never silent.
209
+ const selfAssessed = gate === 'review' && feature?.workType !== 'hotfix'
210
+ && ['verifier.md', 'reviewer.md'].some((f) => !existsSync(join(root, '.morph', 'features', featureName, '4-review', f)));
211
+ if (selfAssessed) {
212
+ warnings.push(
213
+ `⚠️ [morph-spec] Gate 3 de "${featureName}" aprovado SEM os relatórios de review independentes (verifier.md, reviewer.md) — ` +
214
+ `registrado como selfAssessed (aparece no list/fleet).`
215
+ );
216
+ }
217
+ setGateApproved(featureName, gate, { approvedBy: 'manual', ...(selfAssessed ? { selfAssessed: true } : {}) }, root);
220
218
  }
221
219
  return warnings.length ? warnings.join('\n') : null;
222
220
  }
@@ -95,11 +95,11 @@ function classifyReadKind(target) {
95
95
  if (inFeature) {
96
96
  const rel = inFeature[1];
97
97
  const PLAN_ARTIFACT_BASENAMES = new Set([
98
- 'proposal.md', 'spec.md', 'tasks.json',
99
- 'decisions.md', 'recap.md', 'handoff.md',
98
+ 'plan.md', 'checks.json',
99
+ 'handoff.md',
100
100
  ]);
101
101
  const base = rel.split('/').pop();
102
- if (PLAN_ARTIFACT_BASENAMES.has(base) || rel.includes('usecases/') || rel.includes('reports/')) {
102
+ if (PLAN_ARTIFACT_BASENAMES.has(base) || rel.includes('reports/')) {
103
103
  return 'plan-artifact';
104
104
  }
105
105
  }
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * Stop Hook: phase-transition alert, recap/tasks audit, session usage.
4
+ * Stop Hook: phase-transition alert, slice report audit, session usage.
5
5
  *
6
6
  * Phase transition tracking: stores last-seen phase per feature in
7
7
  * .morph/memory/phase-watch.json so the alert fires exactly once.
@@ -19,97 +19,34 @@ import { contextResult, runStandalone } from '../../shared/hook-response.js';
19
19
  import { logHookActivity, logEvent } from '../../shared/activity-logger.js';
20
20
  import { appendTelemetryEvent } from '../../shared/telemetry-logger.js';
21
21
  import { readTranscriptUsage, usageDelta } from '../../shared/transcript-usage.js';
22
+ import { sliceProgress, sliceReportPath, inspectReport } from '../../shared/slice-progress.js';
22
23
 
23
24
  /**
24
- * Audit recap.md against tasks.json + standard-citation rule.
25
- *
26
- * Returns an array of human-readable warnings. Empty array = audit clean.
27
- *
28
- * Two checks:
29
- * 1. Task IDs that appear as a "## T<n>" heading in recap.md but still
30
- * carry status != 'done' in tasks.json — claim/state divergence.
31
- * 2. Task headings without a "**Standard:**" line nearby (within 10 lines
32
- * after the heading) — no standard citation = unauditable decision.
25
+ * Slices whose report is final but lacks what the report contract asks for:
26
+ * prose, the files touched, every scenario the slice covers. Advisory: the
27
+ * same rule `report-check` applies when the orchestrator resumes control.
33
28
  */
34
- function auditRecapAndTasks(featurePath) {
29
+ function auditSliceReports(featurePath) {
30
+ const progress = sliceProgress(featurePath);
31
+ if (!progress) return [];
35
32
  const warnings = [];
36
- const recapPath = join(featurePath, '3-implement', 'recap.md');
37
- const tasksJsonPath = join(featurePath, '2-plan', 'tasks.json');
38
- if (!existsSync(recapPath) || !existsSync(tasksJsonPath)) return warnings;
39
-
40
- let recap, tasksJson;
41
- try {
42
- recap = readFileSync(recapPath, 'utf-8');
43
- tasksJson = JSON.parse(readFileSync(tasksJsonPath, 'utf-8'));
44
- } catch {
45
- return warnings;
46
- }
47
-
48
- const tasks = Array.isArray(tasksJson.tasks) ? tasksJson.tasks : [];
49
- if (tasks.length === 0) return warnings;
50
-
51
- // Index recap by task ID — collect line numbers where each task is mentioned
52
- // as completed. Supports two formats agents commonly use:
53
- // (a) "## T<n>" — heading style (per the SKILL.md template)
54
- // (b) "| T<n> | ... done" — markdown table row with done in same row
55
- const lines = recap.split('\n');
56
- const recapMentions = new Map(); // id → starting line index
57
- const headingRe = /^##\s+(T\d+)\b/;
58
- const tableRowRe = /^\|\s*(T\d+)\s*\|.*\bdone\b/i;
59
- lines.forEach((ln, i) => {
60
- const heading = ln.match(headingRe);
61
- if (heading && !recapMentions.has(heading[1])) {
62
- recapMentions.set(heading[1], i);
63
- return;
64
- }
65
- const tableRow = ln.match(tableRowRe);
66
- if (tableRow && !recapMentions.has(tableRow[1])) {
67
- recapMentions.set(tableRow[1], i);
68
- }
69
- });
70
-
71
- // Check 1: status drift — task mentioned as done in recap but tasks.json
72
- // still shows it as pending/in_progress.
73
- for (const t of tasks) {
74
- if (recapMentions.has(t.id) && t.status !== 'done') {
75
- warnings.push(
76
- `tasks.json drift: ${t.id} appears as done in recap.md but tasks.json shows status="${t.status}". Run writeTasksJson to sync.`
77
- );
33
+ for (const slice of progress.slices) {
34
+ if (slice.status !== 'feito') continue;
35
+ const { missing } = inspectReport(sliceReportPath(featurePath, slice.id), { covers: slice.covers });
36
+ if (missing.length) {
37
+ const hints = missing.map((id) => MISSING_HINT[id] ?? `cite ${id} pelo id`);
38
+ warnings.push(`relatório de ${slice.id} sem: ${missing.join(', ')} — ${hints.join('; ')} (formato na skill morph-apply; morph-spec report-check)`);
78
39
  }
79
40
  }
80
-
81
- // Check 2: missing Standard citation per task mention. Skip pure
82
- // table-row mentions (they're a summary, the heading-style block
83
- // is where the standard belongs) — only flag when the agent did
84
- // not produce a heading block at all for a task that is also marked
85
- // done in tasks.json.
86
- const standardRe = /\*\*Standard:\*\*|\bStandard\s*consulted\b|\bstandards?\s+used\b/i;
87
- const headingMentions = new Map();
88
- lines.forEach((ln, i) => {
89
- const m = ln.match(headingRe);
90
- if (m && !headingMentions.has(m[1])) headingMentions.set(m[1], i);
91
- });
92
- for (const t of tasks) {
93
- if (t.status !== 'done') continue;
94
- const lineIdx = headingMentions.get(t.id);
95
- if (lineIdx === undefined) {
96
- // Task is done but has no heading block at all — recap is summary-only.
97
- warnings.push(
98
- `recap.md task ${t.id} (status=done in tasks.json) has no detailed "## ${t.id}" block. Per morph-implement SKILL.md, each done task needs a heading block with a "**Standard:** ..." citation so the reviewer can audit the decision.`
99
- );
100
- continue;
101
- }
102
- const window = lines.slice(lineIdx, lineIdx + 12).join('\n');
103
- if (!standardRe.test(window)) {
104
- warnings.push(
105
- `recap.md task ${t.id} is missing a "**Standard:** ..." citation within 12 lines of its heading. Cite which standard from STANDARDS.json justified the implementation (or "ad-hoc (see decisions.md ADR-N)" if intentional).`
106
- );
107
- }
108
- }
109
-
110
41
  return warnings;
111
42
  }
112
43
 
44
+ const MISSING_HINT = {
45
+ what: '2-3 frases do que foi feito e como foi provado',
46
+ files: 'linha **Arquivos:** com todo arquivo criado ou modificado',
47
+ partial: '1ª linha PARCIAL: a fatia não terminou',
48
+ };
49
+
113
50
  // Phases where compaction is strongly recommended before starting
114
51
  const COMPACT_TRIGGER_PHASES = new Set(['implement']);
115
52
 
@@ -123,7 +60,7 @@ const HEAVY_SOURCE_PHASES = new Set(['plan', 'uiux']);
123
60
  * `payload.session_id` for the throttle key) — the one thing only the
124
61
  * payload carries.
125
62
  * @returns {Promise<object|null>} a hookSpecificOutput advisory (phase
126
- * transition / build failure / recap drift), or null (pass/silent).
63
+ * transition / build failure / incomplete slice report), or null (pass/silent).
127
64
  */
128
65
  export async function run(payload) {
129
66
  try {
@@ -171,7 +108,7 @@ export async function run(payload) {
171
108
  `⚡ Phase advanced: ${lastPhase} → implement`,
172
109
  ``,
173
110
  `💡 Recommended before starting implementation:`,
174
- ` Run /compact to free up context — spec.md and tasks.json`,
111
+ ` Run /compact to free up context — plan.md and the design`,
175
112
  ` loaded during planning take significant space. A compact now gives`,
176
113
  ` Claude a clean slate focused on writing code.`,
177
114
  ``,
@@ -179,9 +116,8 @@ export async function run(payload) {
179
116
  );
180
117
  }
181
118
 
182
- // ── Recap + tasks.json audit ──────────────────────────────────────────────
183
- // Surface drift between recap.md and tasks.json, plus missing standard
184
- // citations. Advisory warnings only — Stop hook never blocks.
119
+ // ── Slice report audit ────────────────────────────────────────────────────
120
+ // Advisory warnings only — Stop hook never blocks.
185
121
  //
186
122
  // The same audit re-runs every Stop; without dedup it re-injects the same
187
123
  // ~500-token block turn after turn even when nothing changed. Hash-gate it:
@@ -190,7 +126,7 @@ export async function run(payload) {
190
126
  // hash, so a later recurrence — even an identical one — is reported fresh.
191
127
  try {
192
128
  const featurePath = join(cwd, '.morph', 'features', name);
193
- const recapWarnings = auditRecapAndTasks(featurePath);
129
+ const reportWarnings = auditSliceReports(featurePath);
194
130
 
195
131
  const warnedHashesPath = join(memDir, 'warned-hashes.json');
196
132
  let warnedHashes = {};
@@ -198,17 +134,17 @@ export async function run(payload) {
198
134
  if (existsSync(warnedHashesPath)) warnedHashes = JSON.parse(readFileSync(warnedHashesPath, 'utf-8'));
199
135
  } catch { /* corrupt file — start fresh */ }
200
136
 
201
- if (recapWarnings.length > 0) {
202
- const hash = createHash('sha1').update(recapWarnings.join('\n')).digest('hex');
203
- if (warnedHashes[name]?.recap !== hash) {
204
- warnings.push('📋 Recap audit:');
205
- for (const w of recapWarnings) warnings.push(` • ${w}`);
206
- warnedHashes[name] = { ...warnedHashes[name], recap: hash };
137
+ if (reportWarnings.length > 0) {
138
+ const hash = createHash('sha1').update(reportWarnings.join('\n')).digest('hex');
139
+ if (warnedHashes[name]?.reports !== hash) {
140
+ warnings.push('📋 Relatórios de fatia:');
141
+ for (const w of reportWarnings) warnings.push(` • ${w}`);
142
+ warnedHashes[name] = { ...warnedHashes[name], reports: hash };
207
143
  try { writeFileSync(warnedHashesPath, JSON.stringify(warnedHashes, null, 2), 'utf-8'); } catch { /* non-blocking */ }
208
144
  }
209
145
  // same hash as last time it fired → already surfaced, stay silent
210
- } else if (warnedHashes[name]?.recap) {
211
- delete warnedHashes[name].recap;
146
+ } else if (warnedHashes[name]?.reports) {
147
+ delete warnedHashes[name].reports;
212
148
  try { writeFileSync(warnedHashesPath, JSON.stringify(warnedHashes, null, 2), 'utf-8'); } catch { /* non-blocking */ }
213
149
  }
214
150
  } catch {
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * warn-bash-source-write — PreToolUse Bash, warn only: a project source file written
4
+ * by the shell (`writeTargets`) skips every Write/Edit guard (phase folders,
5
+ * protected files, comment-guard). The command still runs.
6
+ */
7
+ import { contextResult, runStandalone } from '../../shared/hook-response.js';
8
+ import { stateExists } from '../../shared/state-reader.js';
9
+ import { writeTargets } from '../../shared/command-tokens.js';
10
+ import { isProjectSource } from '../../shared/source-files.js';
11
+
12
+ /**
13
+ * @param {object} payload - parsed PreToolUse stdin payload (Bash matcher)
14
+ * @returns {Promise<object|null>} additionalContext naming the files, or null
15
+ */
16
+ export async function run(payload) {
17
+ try {
18
+ if (!stateExists()) return null;
19
+ const sources = writeTargets(payload?.tool_input?.command || '').filter(isProjectSource);
20
+ if (!sources.length) return null;
21
+ return contextResult(
22
+ `⚠️ Código do projeto escrito pelo Bash (${sources.join(', ')}): os guards de escrita (fase, arquivo protegido, comment-guard) ` +
23
+ 'só enxergam Write/Edit. Reescreva esse arquivo por Write/Edit.',
24
+ 'PreToolUse',
25
+ );
26
+ } catch {
27
+ return null;
28
+ }
29
+ }
30
+
31
+ await runStandalone(run);