@polymorphism-tech/morph-spec 9.0.0 → 9.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (304) hide show
  1. package/CLAUDE.md +8 -9
  2. package/README.md +38 -36
  3. package/bin/morph-spec.js +4 -62
  4. package/bin/validate.js +1 -39
  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 +1 -1
  9. package/framework/MORPH.md +1 -1
  10. package/framework/agents.json +12 -4
  11. package/framework/commands/morph-apply.md +1 -1
  12. package/framework/commands/morph-status.md +1 -1
  13. package/framework/docs/CLI.md +6 -17
  14. package/framework/hooks/claude-code/core/block-destructive-worktree-removal.js +6 -49
  15. package/framework/hooks/claude-code/core/block-raw-worktree-add.js +2 -35
  16. package/framework/hooks/claude-code/core/chain.js +4 -65
  17. package/framework/hooks/claude-code/core/comment-guard.js +2 -16
  18. package/framework/hooks/claude-code/core/enforce-phase-writes.js +6 -50
  19. package/framework/hooks/claude-code/core/gate-guard.js +7 -67
  20. package/framework/hooks/claude-code/core/graph-refresh-kick.js +3 -24
  21. package/framework/hooks/claude-code/core/heavy-gate.js +3 -21
  22. package/framework/hooks/claude-code/core/heavy-release.js +2 -13
  23. package/framework/hooks/claude-code/core/loop-detect.js +2 -31
  24. package/framework/hooks/claude-code/core/pre-bash-denylist.js +0 -4
  25. package/framework/hooks/claude-code/core/protect-spec-files.js +7 -67
  26. package/framework/hooks/claude-code/core/state-sync.js +11 -67
  27. package/framework/hooks/claude-code/core/telemetry-log.js +12 -93
  28. package/framework/hooks/claude-code/core/validate-completion.js +11 -57
  29. package/framework/hooks/claude-code/core/warn-bash-source-write.js +1 -9
  30. package/framework/hooks/claude-code/core/worktree-context.js +10 -110
  31. package/framework/hooks/claude-code/core/worktree-create.js +6 -70
  32. package/framework/hooks/claude-code/core/worktree-env-inject.js +5 -56
  33. package/framework/hooks/claude-code/core/worktree-remove.js +5 -31
  34. package/framework/hooks/shared/activity-logger.js +9 -89
  35. package/framework/hooks/shared/agent-reap.js +2 -17
  36. package/framework/hooks/shared/build-check.js +6 -75
  37. package/framework/hooks/shared/claim-registry.js +9 -119
  38. package/framework/hooks/shared/command-tokens.js +6 -51
  39. package/framework/hooks/shared/comment-rules.js +5 -20
  40. package/framework/hooks/shared/comment-scanner.js +6 -25
  41. package/framework/hooks/shared/comment-stripper.js +11 -85
  42. package/framework/hooks/shared/e2e-check.js +0 -0
  43. package/framework/hooks/shared/errata.js +2 -15
  44. package/framework/hooks/shared/feature-locator.js +11 -95
  45. package/framework/hooks/shared/frontend-locations.js +1 -3
  46. package/framework/hooks/shared/gate-question.js +1 -7
  47. package/framework/hooks/shared/git-exclude.js +3 -20
  48. package/framework/hooks/shared/git-paths.js +8 -104
  49. package/framework/hooks/shared/graph-freshness.js +22 -166
  50. package/framework/hooks/shared/graph-refresh.js +21 -149
  51. package/framework/hooks/shared/heavy-command.js +4 -12
  52. package/framework/hooks/shared/hook-response.js +8 -83
  53. package/framework/hooks/shared/morph-cli.js +8 -52
  54. package/framework/hooks/shared/payload-utils.js +10 -132
  55. package/framework/hooks/shared/phase-utils.js +1 -42
  56. package/framework/hooks/shared/redact-secrets.js +4 -15
  57. package/framework/hooks/shared/reparse-scan.js +6 -66
  58. package/framework/hooks/shared/slice-progress.js +4 -23
  59. package/framework/hooks/shared/source-files.js +4 -17
  60. package/framework/hooks/shared/state-reader.js +13 -183
  61. package/framework/hooks/shared/state-writer.js +8 -52
  62. package/framework/hooks/shared/stdin-reader.js +1 -11
  63. package/framework/hooks/shared/telemetry-logger.js +7 -67
  64. package/framework/hooks/shared/transcript-usage.js +7 -29
  65. package/framework/hooks/shared/worktree-root.js +7 -29
  66. package/framework/rules/csharp-standards.md +1 -1
  67. package/framework/rules/frontend-standards.md +1 -0
  68. package/framework/rules/hangfire-standards.md +1 -1
  69. package/framework/rules/neon-standards.md +1 -1
  70. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  71. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  72. package/framework/skills/core/morph-apply/SKILL.md +2 -2
  73. package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
  74. package/framework/skills/core/morph-machine/SKILL.md +1 -1
  75. package/framework/skills/core/morph-media/SKILL.md +1 -1
  76. package/framework/skills/core/morph-plan/SKILL.md +2 -2
  77. package/framework/skills/core/morph-review/SKILL.md +1 -1
  78. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  79. package/framework/skills/core/morph-uiux/SKILL.md +29 -12
  80. package/framework/standards/STANDARDS.json +34 -12
  81. package/framework/standards/STANDARDS.meta.json +16 -2
  82. package/framework/standards/ai-agents/agent-archetypes.md +2 -2
  83. package/framework/standards/ai-agents/agent-session.md +1 -1
  84. package/framework/standards/ai-agents/conversational-agent-with-phases.md +4 -4
  85. package/framework/standards/ai-agents/durable-workflows-hitl.md +3 -3
  86. package/framework/standards/ai-agents/guardrails.md +1 -1
  87. package/framework/standards/ai-agents/multi-agent-patterns.md +2 -2
  88. package/framework/standards/ai-agents/observability-patterns.md +1 -1
  89. package/framework/standards/ai-agents/production.md +1 -1
  90. package/framework/standards/ai-agents/prompt-sources.md +2 -2
  91. package/framework/standards/ai-agents/providers/model-registry.md +1 -1
  92. package/framework/standards/ai-agents/rag-custom-pgvector.md +2 -2
  93. package/framework/standards/ai-agents/setup.md +2 -2
  94. package/framework/standards/ai-agents/sweet-spot.md +2 -2
  95. package/framework/standards/ai-agents/tool-adapters.md +0 -2
  96. package/framework/standards/ai-agents/vector-data-extensions.md +4 -4
  97. package/framework/standards/ai-agents/workflows.md +1 -1
  98. package/framework/standards/architecture/code-comments.md +1 -1
  99. package/framework/standards/architecture/vertical-slice/vertical-slice.md +1 -1
  100. package/framework/standards/backend/api/validation.md +1 -1
  101. package/framework/standards/backend/dotnet/vsa-handler-patterns.md +5 -5
  102. package/framework/standards/backend/integrations/hangfire/hangfire-jobs.md +1 -1
  103. package/framework/standards/backend/integrations/neon-auth/neon-auth.md +2 -2
  104. package/framework/standards/data/nosql/cache/redis.md +1 -1
  105. package/framework/standards/data/nosql/cosmos-db.md +3 -3
  106. package/framework/standards/frontend/admin-app/forms.md +1 -1
  107. package/framework/standards/frontend/design-system/art-direction.md +88 -0
  108. package/framework/standards/frontend/design-system/premium-finish.md +77 -2
  109. package/framework/standards/infrastructure/neon/neon-pgvector.md +5 -5
  110. package/framework/templates/CLAUDE.md.core.template +3 -3
  111. package/framework/templates/REGISTRY.json +9 -9
  112. package/framework/templates/docs/ui-mockups.md +4 -0
  113. package/package.json +1 -1
  114. package/src/commands/context/context-refresh.js +0 -16
  115. package/src/commands/dashboard/dashboard.js +2 -8
  116. package/src/commands/e2e/e2e.js +17 -104
  117. package/src/commands/graph/affected.js +0 -14
  118. package/src/commands/graph/build.js +11 -50
  119. package/src/commands/graph/explain.js +0 -16
  120. package/src/commands/graph/status.js +1 -12
  121. package/src/commands/mcp/mcp-setup.js +4 -62
  122. package/src/commands/observability/cost.js +5 -57
  123. package/src/commands/observability/telemetry.js +1 -14
  124. package/src/commands/project/branch.js +4 -45
  125. package/src/commands/project/claim.js +4 -48
  126. package/src/commands/project/doctor.js +48 -302
  127. package/src/commands/project/env.js +1 -27
  128. package/src/commands/project/fleet.js +3 -32
  129. package/src/commands/project/init.js +4 -68
  130. package/src/commands/project/install-plugin-cmd.js +0 -1
  131. package/src/commands/project/standards.js +1 -62
  132. package/src/commands/project/status.js +0 -53
  133. package/src/commands/project/update.js +31 -239
  134. package/src/commands/project/worktree.js +108 -882
  135. package/src/commands/scaffold/scaffold.js +0 -25
  136. package/src/commands/state/advance.js +14 -85
  137. package/src/commands/state/approve.js +14 -96
  138. package/src/commands/state/archive.js +12 -64
  139. package/src/commands/state/auto-approval.js +2 -5
  140. package/src/commands/state/create.js +22 -109
  141. package/src/commands/state/delete.js +11 -63
  142. package/src/commands/state/finish.js +98 -475
  143. package/src/commands/state/gate-check.js +22 -132
  144. package/src/commands/state/gate-decision.js +3 -39
  145. package/src/commands/state/handoff.js +3 -30
  146. package/src/commands/state/list.js +14 -84
  147. package/src/commands/state/report-check.js +8 -58
  148. package/src/commands/state/retype.js +4 -26
  149. package/src/commands/validation/index.js +0 -3
  150. package/src/commands/validation/mutate.js +2 -5
  151. package/src/commands/validation/validate-feature.js +5 -94
  152. package/src/commands/validation/verify.js +15 -98
  153. package/src/core/paths/output-schema.js +2 -101
  154. package/src/core/state/feature-identity.js +1 -25
  155. package/src/core/state/gate-order.js +0 -27
  156. package/src/core/state/lifecycle-audit.js +8 -100
  157. package/src/core/state/state-manager.js +22 -304
  158. package/src/core/templates/template-registry.js +2 -97
  159. package/src/core/templates/template-renderer.js +13 -93
  160. package/src/core/templates/template-seed.js +0 -1
  161. package/src/core/work-types.js +1 -36
  162. package/src/dashboard/app/App.jsx +1 -8
  163. package/src/dashboard/app/components/ActivityFeed.jsx +0 -5
  164. package/src/dashboard/app/components/CommandPalette.jsx +0 -7
  165. package/src/dashboard/app/components/DetailPanel.jsx +1 -5
  166. package/src/dashboard/app/components/EntityLink.jsx +0 -12
  167. package/src/dashboard/app/components/ErrorBoundary.jsx +1 -8
  168. package/src/dashboard/app/components/KnowledgeMarkdown.jsx +0 -11
  169. package/src/dashboard/app/components/Markdown.jsx +0 -9
  170. package/src/dashboard/app/components/PhaseRail.jsx +0 -11
  171. package/src/dashboard/app/components/SearchFilter.jsx +0 -4
  172. package/src/dashboard/app/components/Sidebar.jsx +0 -5
  173. package/src/dashboard/app/hooks/useMorphQuery.js +1 -11
  174. package/src/dashboard/app/hooks/usePref.js +1 -5
  175. package/src/dashboard/app/lib/format.js +0 -2
  176. package/src/dashboard/app/lib/phases.js +1 -5
  177. package/src/dashboard/app/lib/project.js +0 -12
  178. package/src/dashboard/app/pages/AgentMap.jsx +1 -8
  179. package/src/dashboard/app/pages/FeatureDetail.jsx +1 -5
  180. package/src/dashboard/app/pages/Features.jsx +1 -8
  181. package/src/dashboard/app/pages/Guide.jsx +0 -1
  182. package/src/dashboard/app/pages/HookInspector.jsx +2 -23
  183. package/src/dashboard/app/pages/PhasePipeline.jsx +0 -3
  184. package/src/dashboard/app/pages/Portfolio.jsx +0 -1
  185. package/src/dashboard/app/pages/RulesMap.jsx +1 -16
  186. package/src/dashboard/app/pages/SkillsLibrary.jsx +1 -16
  187. package/src/dashboard/app/pages/Standards.jsx +2 -17
  188. package/src/dashboard/scanner/activity-scanner.js +10 -69
  189. package/src/dashboard/scanner/agents-scanner.js +1 -23
  190. package/src/dashboard/scanner/docs-scanner.js +0 -6
  191. package/src/dashboard/scanner/feature-status.js +2 -5
  192. package/src/dashboard/scanner/features-scanner.js +10 -66
  193. package/src/dashboard/scanner/hooks-scanner.js +5 -28
  194. package/src/dashboard/scanner/index.js +0 -4
  195. package/src/dashboard/scanner/portfolio.js +5 -29
  196. package/src/dashboard/scanner/project-api.js +8 -54
  197. package/src/dashboard/scanner/rules-scanner.js +1 -2
  198. package/src/dashboard/scanner/skills-scanner.js +2 -12
  199. package/src/dashboard/scanner/standards-scanner.js +2 -6
  200. package/src/dashboard/vite-plugin-morph-data.js +5 -19
  201. package/src/lib/ai/ai-checks.js +7 -52
  202. package/src/lib/ai/ai-project-scan.js +14 -108
  203. package/src/lib/ai/code-audit.js +7 -63
  204. package/src/lib/ai/pin-drift.js +8 -51
  205. package/src/lib/ai/registry-audit.js +11 -90
  206. package/src/lib/ci/ci-verify.js +4 -19
  207. package/src/lib/ci/github-ci.js +9 -37
  208. package/src/lib/ci/workflow-scaffold.js +2 -10
  209. package/src/lib/detectors/claude-config-detector.js +13 -74
  210. package/src/lib/detectors/index.js +4 -50
  211. package/src/lib/e2e/compose-env.js +5 -66
  212. package/src/lib/e2e/stack-teardown.js +6 -40
  213. package/src/lib/generators/architecture-scanner.js +18 -156
  214. package/src/lib/generators/handoff-generator.js +9 -53
  215. package/src/lib/generators/scaffold-generator.js +5 -101
  216. package/src/lib/generators/settings-generator.js +4 -37
  217. package/src/lib/git/branch-classifier.js +11 -84
  218. package/src/lib/git/default-branch.js +14 -169
  219. package/src/lib/git/evidence-staleness.js +1 -0
  220. package/src/lib/git/gh.js +2 -17
  221. package/src/lib/git/pending-prs.js +6 -37
  222. package/src/lib/git/pr-files.js +3 -28
  223. package/src/lib/git/pr-index.js +3 -34
  224. package/src/lib/git/tree-fingerprint.js +3 -15
  225. package/src/lib/git/tree-state.js +6 -33
  226. package/src/lib/graph/god-nodes.js +2 -22
  227. package/src/lib/graph/graph-health.js +3 -24
  228. package/src/lib/graph/graph-loader.js +5 -56
  229. package/src/lib/graph/graphify-runtime.js +7 -37
  230. package/src/lib/graph/graphifyignore.js +8 -74
  231. package/src/lib/graph/sanitize.js +16 -136
  232. package/src/lib/heavy/cleanup.js +1 -1
  233. package/src/lib/installers/effects.js +8 -93
  234. package/src/lib/installers/mcp-installer.js +9 -169
  235. package/src/lib/installers/memory-installer.js +4 -27
  236. package/src/lib/installers/templates-reconcile.js +5 -60
  237. package/src/lib/mutation/dotnet-introspection.js +20 -106
  238. package/src/lib/mutation/subset.js +1 -4
  239. package/src/lib/plan/artifact-vocabulary.js +5 -25
  240. package/src/lib/plan/checks.js +5 -31
  241. package/src/lib/plan/plan-parser.js +1 -6
  242. package/src/lib/projects/registry.js +7 -44
  243. package/src/lib/review-reports.js +5 -23
  244. package/src/lib/risk-detector.js +39 -173
  245. package/src/lib/specs/adr-index.js +6 -34
  246. package/src/lib/specs/frontmatter.js +2 -15
  247. package/src/lib/specs/gfm-table.js +1 -7
  248. package/src/lib/specs/revocations.js +1 -14
  249. package/src/lib/specs/screen-checklist.js +47 -124
  250. package/src/lib/stack/dotnet-build-ignores.js +2 -6
  251. package/src/lib/stack/stack-profile.js +1 -28
  252. package/src/lib/stack/stack-scan.js +1 -8
  253. package/src/lib/stack-filter.js +4 -27
  254. package/src/lib/standards/registry.js +1 -7
  255. package/src/lib/standards/sections.js +2 -13
  256. package/src/lib/standards/verified-against.js +19 -191
  257. package/src/lib/tasks/eval-node.js +21 -169
  258. package/src/lib/tasks/linux-runner.js +18 -142
  259. package/src/lib/tasks/test-nodes.js +43 -240
  260. package/src/lib/tasks/test-runner.js +43 -341
  261. package/src/lib/telemetry/metrics.js +7 -36
  262. package/src/lib/telemetry/telemetry-reader.js +13 -125
  263. package/src/lib/telemetry/verify-event.js +2 -30
  264. package/src/lib/validators/code-budget/code-budget-validator.js +3 -43
  265. package/src/lib/validators/code-budget/index.js +0 -7
  266. package/src/lib/validators/contracts/contract-compliance-validator.js +2 -24
  267. package/src/lib/validators/contracts/index.js +0 -6
  268. package/src/lib/validators/nextjs/index.js +0 -5
  269. package/src/lib/validators/nextjs/next-component-validator.js +4 -81
  270. package/src/lib/validators/packages/index.js +0 -6
  271. package/src/lib/validators/packages/package-validator.js +6 -56
  272. package/src/lib/validators/reuse/index.js +0 -8
  273. package/src/lib/validators/reuse/reuse-validator.js +11 -101
  274. package/src/lib/validators/reuse/symbol-scanner.js +6 -62
  275. package/src/lib/validators/traceability-check.js +5 -63
  276. package/src/lib/validators/validation-runner.js +20 -125
  277. package/src/lib/validators/verify-runner.js +52 -358
  278. package/src/lib/work-type-detector.js +5 -67
  279. package/src/lib/worktree/docker-audit.js +17 -136
  280. package/src/lib/worktree/env-allocator.js +10 -98
  281. package/src/lib/worktree/feature-truth.js +12 -115
  282. package/src/lib/worktree/finish-backup.js +32 -242
  283. package/src/lib/worktree/fleet-scanner.js +13 -95
  284. package/src/lib/worktree/ignored-triage.js +15 -91
  285. package/src/lib/worktree/vscode-settings.js +2 -3
  286. package/src/lib/worktree/worktree-audit.js +21 -213
  287. package/src/lib/worktree/worktree-include.js +3 -5
  288. package/src/scripts/global-install.js +3 -9
  289. package/src/scripts/install-plugin.js +1 -13
  290. package/src/scripts/setup-infra.js +17 -147
  291. package/src/utils/agents-installer.js +28 -275
  292. package/src/utils/banner.js +4 -25
  293. package/src/utils/claude-md-injector.js +1 -37
  294. package/src/utils/claude-settings-manager.js +1 -52
  295. package/src/utils/file-copier.js +11 -95
  296. package/src/utils/hooks-installer.js +21 -298
  297. package/src/utils/skills-installer.js +4 -101
  298. package/src/utils/version-checker.js +4 -83
  299. package/framework/commands/morph-proposal.md +0 -9
  300. package/src/commands/utils/index.js +0 -3
  301. package/src/lib/detectors/design-system-detector.js +0 -188
  302. package/src/lib/generators/design-system-generator.js +0 -508
  303. package/src/lib/specs/old-format.js +0 -68
  304. package/src/utils/agent-id-mapper.js +0 -11
@@ -1,32 +1,9 @@
1
- /**
2
- * Shared payload utilities for Claude Code Write/Edit hooks.
3
- *
4
- * Extracts file path and content from tool_input payloads,
5
- * normalizing between Write (content, file_path) and Edit (new_string, old_string, file_path).
6
- */
7
-
8
1
  import { createHash } from 'crypto';
9
2
  import { existsSync } from 'fs';
10
3
  import { dirname, join, isAbsolute } from 'path';
11
4
 
12
- /**
13
- * Resolve the project root that OWNS a given file: walk up from the file's
14
- * directory to the nearest ancestor containing one of `markers` (default
15
- * `package.json` — the tsc/build root). Falls back to `fallbackCwd` when
16
- * nothing matches or on any error.
17
- *
18
- * This exists so a PostToolUse hook typechecks/builds in the CORRECT root when
19
- * a subagent edits a file inside its own git worktree (`isolation: "worktree"`,
20
- * morph-apply §3b-w) instead of always assuming the main working tree. In the
21
- * normal (non-worktree) case it resolves to the same root as `process.cwd()`.
22
- *
23
- * Fail-open: never throws — returns `fallbackCwd` on any error.
24
- *
25
- * @param {string} filePath - edited file path (absolute or relative)
26
- * @param {string} fallbackCwd - root to use if resolution fails (usually process.cwd())
27
- * @param {string[]} [markers] - ancestor marker filenames to look for
28
- * @returns {string} the resolved root directory
29
- */
5
+ // A sub-agent may edit inside its own worktree: the build root comes from the
6
+ // file, not from the session's cwd.
30
7
  export function resolveRootForFile(filePath, fallbackCwd, markers = ['package.json']) {
31
8
  try {
32
9
  if (!filePath) return fallbackCwd;
@@ -48,33 +25,9 @@ export function resolveRootForFile(filePath, fallbackCwd, markers = ['package.js
48
25
  }
49
26
  }
50
27
 
51
- /**
52
- * Resolve the project root that OWNS a `.morph/features/...` path — the tree
53
- * whose `.morph/` the hook must read and write for that file.
54
- *
55
- * A hook process knows only where the SESSION lives (`process.cwd()` /
56
- * `CLAUDE_PROJECT_DIR`). That is not the same thing as where the FILE lives: a
57
- * session rooted in the primary repo can legitimately write to a feature that
58
- * lives in a secondary git worktree. Resolving state from the process instead
59
- * of the path is what made `enforce-phase-writes` block a legal `2-plan/` write
60
- * (the root still had the feature at `proposal`) while `protect-spec-files`
61
- * simultaneously went DARK there (the root had no approved gate to enforce) —
62
- * one defect, opposite directions.
63
- *
64
- * The anchor is exact, so no marker walk is needed: everything before the
65
- * file's own `.morph/features/` segment IS the owning root. `lastIndexOf` picks
66
- * the CLOSEST enclosing `.morph/` — the worktree's, not the root it nests under.
67
- *
68
- * Note this cannot be solved by locating the hook file itself: `worktree setup`
69
- * junctions `.morph/framework/` back to the primary root, so every worktree
70
- * executes the very same hook file. Only the target path identifies the tree.
71
- *
72
- * Fail-open: never throws — returns `fallbackCwd` for any non-feature path.
73
- *
74
- * @param {string} filePath - target file path (absolute or relative)
75
- * @param {string} fallbackCwd - root to use when the path owns no `.morph/`
76
- * @returns {string} the resolved project root
77
- */
28
+ // State is resolved from the target PATH, never the session: a session in the
29
+ // primary root may write a feature that lives in a worktree. `lastIndexOf` picks
30
+ // the closest enclosing `.morph/`.
78
31
  export function resolveMorphRoot(filePath, fallbackCwd) {
79
32
  try {
80
33
  if (!filePath) return fallbackCwd;
@@ -88,111 +41,41 @@ export function resolveMorphRoot(filePath, fallbackCwd) {
88
41
  }
89
42
  }
90
43
 
91
- /**
92
- * Is this an already-absolute path, on EITHER convention?
93
- *
94
- * `path.isAbsolute` is platform-bound: `C:/x` is absolute on win32 and relative
95
- * on posix. That asymmetry is a real defect here, not a theoretical one — a
96
- * Windows-shaped path reaching a posix `join()` produces `/cwd/C:/x`, silently
97
- * resolving state against a directory that cannot exist. Recognizing the drive
98
- * letter explicitly makes the answer the same everywhere (and is a no-op on
99
- * win32, where `isAbsolute` already accepts it).
100
- */
44
+ // `path.isAbsolute` is platform-bound: on posix `C:/x` would join into `/cwd/C:/x`.
101
45
  const isAbsolutePath = (p) => isAbsolute(p) || /^[a-zA-Z]:\//.test(p);
102
46
 
103
- /** Normalize to forward slashes and make absolute against `fallbackCwd`. */
104
47
  function toAbsolutePosix(filePath, fallbackCwd) {
105
48
  const p = String(filePath).replace(/\\/g, '/');
106
49
  return isAbsolutePath(p) ? p : join(fallbackCwd, p).replace(/\\/g, '/');
107
50
  }
108
51
 
109
- /**
110
- * Absolute path of a hook's target file, resolved against the root that owns
111
- * it. Pairs with `resolveMorphRoot` so a hook never mixes "the tree I read
112
- * state from" with "the file I read content from".
113
- *
114
- * @param {string} filePath
115
- * @param {string} fallbackCwd
116
- * @returns {string}
117
- */
118
52
  export function resolveTargetPath(filePath, fallbackCwd) {
119
53
  return toAbsolutePosix(filePath || '', fallbackCwd);
120
54
  }
121
55
 
122
- /**
123
- * Get the file path from a hook payload, normalizing backslashes.
124
- * @param {Object} payload - Hook payload from stdin
125
- * @returns {string} Normalized file path or empty string
126
- */
127
56
  export function getFilePath(payload) {
128
57
  const raw = payload?.tool_input?.file_path || payload?.tool_input?.path || '';
129
58
  return raw.replace(/\\/g, '/');
130
59
  }
131
60
 
132
- /**
133
- * Get the content being written or edited.
134
- * For Write: returns `content` (full file content).
135
- * For Edit: returns `new_string` (replacement text).
136
- * @param {Object} payload - Hook payload from stdin
137
- * @returns {string} Content string or empty string
138
- */
139
61
  export function getContentToValidate(payload) {
140
62
  const input = payload?.tool_input;
141
63
  if (!input) return '';
142
- // Write tool uses 'content', Edit tool uses 'new_string'
143
64
  return input.content || input.new_string || '';
144
65
  }
145
66
 
146
- /**
147
- * Check if this is a Write operation (full file content available).
148
- * @param {Object} payload - Hook payload from stdin
149
- * @returns {boolean} True if Write (content exists), false if Edit or unknown
150
- */
151
67
  export function isWriteOperation(payload) {
152
68
  return typeof payload?.tool_input?.content === 'string';
153
69
  }
154
70
 
155
- /**
156
- * Get the tool name from a hook payload.
157
- * @param {Object} payload - Hook payload from stdin
158
- * @returns {string} Tool name or 'unknown'
159
- */
160
71
  export function getToolName(payload) {
161
72
  return payload?.tool_name || 'unknown';
162
73
  }
163
74
 
164
- /** Assinatura de Bash: comando normalizado, truncado. Ver getCommandSignature. */
165
75
  const BASH_SIGNATURE_MAX = 120;
166
76
 
167
- /**
168
- * Build a normalized signature for loop detection.
169
- * - Edit/Write: "Edit:normalized/file/path#<sha1[0:8] do conteúdo>" ("Edit:path" sem conteúdo)
170
- * - Bash: "Bash:<normalized command, truncated>"
171
- * - Every other tool WITH input: "Tool:<truncated JSON of tool_input>"
172
- * - No tool_input at all: just the tool name
173
- *
174
- * The general (non-Edit/Write/Bash) case matters: a burst of `Read`/`Skill`/
175
- * `Grep`/`Agent` calls against DIFFERENT targets (phase-context loading,
176
- * post-task evaluation reads, parallel subagent dispatch) is normal work, not
177
- * a loop. Collapsing every one of them to the bare tool name is what made
178
- * loop-detect fire on exactly that legitimate traffic — a real, reported
179
- * false positive. Folding in the actual input (rather than hand-listing a
180
- * target field per tool) fixes it uniformly for every tool at once.
181
- *
182
- * **Bash ficou de fora daquele fix (2026-07-22) e continuou colapsando em DUAS
183
- * PALAVRAS**, o que é quase sempre o nome da ferramenta e nada mais:
184
- * `npm test -- a`, `npm test -- b` e `npm run build` viravam `Bash:npm test` /
185
- * `Bash:npm run`. Um ciclo legítimo de mutação — rodar o mesmo runner sobre
186
- * alvos diferentes, que é exatamente como se testa se um teste morde — disparava
187
- * o aviso de loop três vezes seguidas. Aviso que toca à toa é aviso que se
188
- * aprende a ignorar, e aí ele não serve nem quando está certo.
189
- *
190
- * O discriminador agora é o comando inteiro (normalizado e truncado), mesma
191
- * forma dos demais tools. Repetir o comando IDÊNTICO continua sendo loop.
192
- *
193
- * @param {Object} payload - Hook payload from stdin
194
- * @returns {string} Signature string
195
- */
77
+ // The input is part of the signature: the same tool on DIFFERENT targets is
78
+ // work, not a loop. Only an identical call repeats the signature.
196
79
  export function getCommandSignature(payload) {
197
80
  const tool = getToolName(payload);
198
81
  if (tool === 'unknown') return 'unknown';
@@ -203,11 +86,8 @@ export function getCommandSignature(payload) {
203
86
  if (tool === 'Edit' || tool === 'Write') {
204
87
  const path = (input.file_path || input.path || '').replace(/\\/g, '/');
205
88
  if (!path) return tool;
206
- // O caminho sozinho confundia TRABALHO com LOOP: editar regiões diferentes
207
- // de dois arquivos (a forma normal de uma correção que cruza teste e
208
- // fonte) dava só duas assinaturas e disparava o aviso. O conteúdo da
209
- // edição entra na assinatura; repetir a MESMA edição continua casando, e o
210
- // ping-pong A→B / B→A continua dando duas assinaturas — loop de verdade.
89
+ // O caminho sozinho confundia editar regiões diferentes com loop; o
90
+ // ping-pong A→B / B→A continua dando duas assinaturas.
211
91
  const body = tool === 'Write'
212
92
  ? input.content
213
93
  : (input.old_string !== undefined || input.new_string !== undefined)
@@ -218,8 +98,6 @@ export function getCommandSignature(payload) {
218
98
  }
219
99
 
220
100
  if (tool === 'Bash') {
221
- // Espaços colapsados para que a mesma invocação escrita com espaçamento
222
- // diferente continue casando — o alvo é o comando, não a digitação.
223
101
  const cmd = (input.command || '').replace(/\s+/g, ' ').trim();
224
102
  return cmd ? `Bash:${cmd.slice(0, BASH_SIGNATURE_MAX)}` : 'Bash';
225
103
  }
@@ -1,25 +1,16 @@
1
1
  // GENERATED by scripts/generate-refs.js — DO NOT EDIT manually
2
2
  // Source of truth: src/core/paths/output-schema.js
3
3
  // Regenerate with: node scripts/generate-refs.js
4
- /**
5
- * Shared phase utilities for Claude Code hooks.
6
- *
7
- * Maps phases to directories and output types.
8
- */
9
4
 
10
- /** Phase order */
11
- export const PHASE_ORDER = ["plan","uiux","implement","review","sync"];
5
+ export const PHASE_ORDER = ["plan","uiux","implement","review"];
12
6
 
13
- /** Map phase → allowed output subdirectory */
14
7
  export const PHASE_DIRS = {
15
8
  plan: '2-plan',
16
9
  uiux: '1-design',
17
10
  implement: '3-implement',
18
11
  review: '4-review',
19
- sync: '3-implement',
20
12
  };
21
13
 
22
- /** Map output type (camelCase) → phase that produces it */
23
14
  export const OUTPUT_PHASE_MAP = {
24
15
  plan: 'plan',
25
16
  checks: 'plan',
@@ -33,7 +24,6 @@ export const OUTPUT_PHASE_MAP = {
33
24
  securityReport: 'review',
34
25
  };
35
26
 
36
- /** Map filename → output type (camelCase) */
37
27
  export const FILENAME_TO_OUTPUT = {
38
28
  'plan.md': 'plan',
39
29
  'checks.json': 'checks',
@@ -46,7 +36,6 @@ export const FILENAME_TO_OUTPUT = {
46
36
  'security.md': 'securityReport',
47
37
  };
48
38
 
49
- /** Protected spec files and the approval gate that locks them */
50
39
  export const PROTECTED_SPEC_FILES = {
51
40
  'plan.md': 'plan',
52
41
  'checks.json': 'plan',
@@ -57,63 +46,33 @@ export const PROTECTED_SPEC_FILES = {
57
46
  };
58
47
 
59
48
 
60
- /**
61
- * Extract feature name from a .morph/features/{feature}/... path
62
- * @param {string} filePath - File path to analyze
63
- * @returns {string|null} Feature name or null
64
- */
65
49
  export function extractFeatureName(filePath) {
66
50
  const normalized = filePath.replace(/\\/g, '/');
67
51
  const match = normalized.match(/\.morph\/features\/([^/]+)\//);
68
52
  return match ? match[1] : null;
69
53
  }
70
54
 
71
- /**
72
- * Extract phase subdirectory from a .morph/features/{feature}/{phaseDir}/... path
73
- * @param {string} filePath - File path to analyze
74
- * @returns {string|null} Phase directory (e.g., '2-plan', '1-design') or null
75
- */
76
55
  export function extractPhaseDir(filePath) {
77
56
  const normalized = filePath.replace(/\\/g, '/');
78
57
  const match = normalized.match(/\.morph\/features\/[^/]+\/(\d+-[^/]+)\//);
79
58
  return match ? match[1] : null;
80
59
  }
81
60
 
82
- /**
83
- * Check if a file path is inside .morph/features/
84
- * @param {string} filePath
85
- * @returns {boolean}
86
- */
87
61
  export function isFeaturePath(filePath) {
88
62
  const normalized = filePath.replace(/\\/g, '/');
89
63
  return normalized.includes('.morph/features/');
90
64
  }
91
65
 
92
- /**
93
- * Check if a file path is inside .morph/framework/ (readonly)
94
- * @param {string} filePath
95
- * @returns {boolean}
96
- */
97
66
  export function isFrameworkPath(filePath) {
98
67
  const normalized = filePath.replace(/\\/g, '/');
99
68
  return normalized.includes('.morph/framework/');
100
69
  }
101
70
 
102
- /**
103
- * Check if a file path is state.json
104
- * @param {string} filePath
105
- * @returns {boolean}
106
- */
107
71
  export function isStatePath(filePath) {
108
72
  const normalized = filePath.replace(/\\/g, '/');
109
73
  return normalized.endsWith('.morph/state.json');
110
74
  }
111
75
 
112
- /**
113
- * Get the basename of a file path
114
- * @param {string} filePath
115
- * @returns {string}
116
- */
117
76
  export function getBasename(filePath) {
118
77
  const normalized = filePath.replace(/\\/g, '/');
119
78
  return normalized.split('/').pop();
@@ -1,14 +1,6 @@
1
- /**
2
- * Redaction of obvious secrets in free text that telemetry persists (Bash
3
- * commands, dispatch prompts). events.jsonl is copied into finish backups
4
- * under `.git/`, so whatever lands there outlives the session.
5
- *
6
- * Pattern-based by design: it catches a secret written NEXT TO its name
7
- * (`KEY=…`, `Authorization: …`, `user:pass@`) or in a well-known token shape.
8
- * A bare value with no name beside it is invisible to any pattern — which is
9
- * why tool OUTPUT (stdout/stderr, a failure's `error` body) is never persisted
10
- * at all instead of being redacted.
11
- */
1
+ // Catches a secret written next to its name or in a known token shape; a bare
2
+ // value is invisible to any pattern, which is why tool OUTPUT is never
3
+ // persisted at all instead of being redacted.
12
4
 
13
5
  export const REDACTED = '[REDACTED]';
14
6
 
@@ -43,10 +35,7 @@ const RULES = [
43
35
  [/\beyJ[A-Za-z0-9_-]{5,}\.eyJ[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]+/g, REDACTED],
44
36
  ];
45
37
 
46
- /**
47
- * @param {*} text
48
- * @returns {*} the text with secrets replaced by `[REDACTED]`; non-strings unchanged
49
- */
38
+ /** Non-strings pass through unchanged. */
50
39
  export function redactSecrets(text) {
51
40
  if (typeof text !== 'string' || text === '') return text;
52
41
  let out = text;
@@ -1,63 +1,16 @@
1
- /**
2
- * reparse-scan.js — exhaustive, fail-closed reparse-point (symlink/junction)
3
- * scan of a directory tree.
4
- *
5
- * This lives in `framework/hooks/shared/` rather than `src/` because BOTH sides
6
- * need it and the dependency direction in this repo only runs one way: `src/`
7
- * imports from `framework/hooks/shared/` (see `finish.js`'s `claim-registry`
8
- * and `e2e-check` imports), never the reverse — a hook shipped into a target
9
- * project has the package, not this repo's `src/` layout.
10
- *
11
- * Two callers, one question:
12
- * - `teardownWorktree` (src/commands/project/worktree.js) — may this worktree
13
- * be handed to `git worktree remove`?
14
- * - the `block-destructive-worktree-removal` PreToolUse hook — is the command
15
- * about to run going to traverse a link out of the worktree?
16
- *
17
- * WHY IT MUST BE EXHAUSTIVE — both `git worktree remove` and its `--force`
18
- * fallback FOLLOW reparse points out of the worktree and delete the PRIMARY
19
- * ROOT's real files. The plain removal is not the safe one: it refuses only
20
- * when it sees modified/untracked *tracked-scope* content, so a GITIGNORED
21
- * junction (`node_modules`, `.claude`) leaves `git status` empty, the removal
22
- * proceeds, exits 0 — and the target is gone.
23
- *
24
- * So the check cannot live in the `catch` of the plain removal (unreachable on
25
- * exactly the destructive path), and it cannot be a fixed list of the links
26
- * `setup` creates: a nested link inside a real `node_modules` (npm link, npm
27
- * workspaces) is invisible to any top-level inventory. We scan the whole tree
28
- * and NEVER recurse INTO a reparse point — descending one would walk the target
29
- * we are trying to protect (and could not terminate on a cycle).
30
- *
31
- * FAIL-CLOSED, deliberately: an empty result is NOT the same as a safe one. A
32
- * directory we could not read or an entry we could not lstat might BE (or
33
- * contain) a reparse point. Those answer `unprovable`, and every caller must
34
- * treat that exactly like a found link — decline. "I saw nothing" and "I proved
35
- * there is nothing" are different claims, and only the second may authorize a
36
- * removal. This is the one module in the shared/ directory that must NOT be
37
- * fail-open; the whole point is that uncertainty blocks.
38
- *
39
- * `lstat` reports a POSIX symlink and a Windows junction alike as a symbolic
40
- * link, so the scan behaves identically on both platforms — as does the
41
- * destruction it prevents.
42
- */
1
+ // `git worktree remove` (plain or --force) follows reparse points and deletes the
2
+ // primary root's files, nested links included. FAIL-CLOSED: an entry that cannot
3
+ // be read is `unprovable` and blocks exactly like a found link.
43
4
 
44
5
  import { readdirSync, lstatSync } from 'fs';
45
6
  import { join, relative } from 'path';
46
7
 
47
- /**
48
- * Every reparse point anywhere under `dir`, as relative paths.
49
- *
50
- * @param {string} dir
51
- * @param {string} [base] - root of the scan, for relative reporting
52
- * @param {{points: string[], unprovable: string[]}} [acc]
53
- * @returns {{points: string[], unprovable: string[]}} both empty = provably clean
54
- */
8
+ /** @returns {{points: string[], unprovable: string[]}} both empty = provably clean */
55
9
  export function findReparsePoints(dir, base = dir, acc = { points: [], unprovable: [] }) {
56
10
  let entries;
57
11
  try {
58
12
  entries = readdirSync(dir, { withFileTypes: true });
59
13
  } catch {
60
- // Unreadable (ACL, lock, EMFILE): we cannot prove what is in here.
61
14
  acc.unprovable.push((relative(base, dir) || '.').replace(/\\/g, '/'));
62
15
  return acc;
63
16
  }
@@ -71,28 +24,15 @@ export function findReparsePoints(dir, base = dir, acc = { points: [], unprovabl
71
24
  continue;
72
25
  }
73
26
  if (st.isSymbolicLink()) {
74
- // Forward slashes: every other path this command reports is normalized,
75
- // and these end up in JSON the caller may compare or display.
76
27
  acc.points.push(relative(base, p).replace(/\\/g, '/'));
77
- continue; // do NOT descend — that is the target, not the worktree
28
+ continue; // descending would walk the target, not the worktree
78
29
  }
79
30
  if (st.isDirectory() && entry.name !== '.git') findReparsePoints(p, base, acc);
80
31
  }
81
32
  return acc;
82
33
  }
83
34
 
84
- /**
85
- * The single question both callers actually ask: may this tree be handed to a
86
- * command that follows links?
87
- *
88
- * Collapses `points` and `unprovable` into one list precisely because callers
89
- * must not weigh them differently — `teardownWorktree` already concatenated
90
- * them, and a hook that treated `unprovable` as "probably fine" would reopen
91
- * the hole this module exists to close.
92
- *
93
- * @param {string} dir
94
- * @returns {{ safe: boolean, blockers: string[] }}
95
- */
35
+ // One list on purpose: callers must not weigh `unprovable` lighter than a link.
96
36
  export function scanForBlockers(dir) {
97
37
  const { points, unprovable } = findReparsePoints(dir);
98
38
  const blockers = [...points, ...unprovable];
@@ -14,21 +14,13 @@ export function sliceReportPath(featureDir, sliceId) {
14
14
  return join(featureDir, '3-implement', 'reports', `${sliceId}.md`);
15
15
  }
16
16
 
17
- /** Status of a slice from its report text: no report = pendente, PARCIAL first line = parcial, else feito. */
18
17
  export function sliceStatusFromReport(text) {
19
18
  if (text == null) return 'pendente';
20
19
  const first = String(text).split(/\r?\n/).find((l) => l.trim() !== '') ?? '';
21
20
  return PARTIAL_MARKER.test(first) ? 'parcial' : 'feito';
22
21
  }
23
22
 
24
- /**
25
- * Progress of a feature's slices.
26
- *
27
- * @param {string|null} checksText - the raw extracted checks file of the plan
28
- * @param {(sliceId: string) => string|null} readReport - report text, null when absent
29
- * @returns {{ total: number, done: number, partial: number, pending: number,
30
- * slices: { id: string, title: string, depends: string[], covers: string[], status: string }[] } | null} null when that file is unreadable
31
- */
23
+ /** @returns {{ total, done, partial, pending, slices } | null} null when the checks text is unreadable */
32
24
  export function sliceProgressFrom(checksText, readReport) {
33
25
  let slices;
34
26
  try {
@@ -58,23 +50,18 @@ function readOrNull(path) {
58
50
  }
59
51
  }
60
52
 
61
- /** Slice progress of the feature directory on disk. */
62
53
  export function sliceProgress(featureDir) {
63
54
  return sliceProgressFrom(readOrNull(checksPath(featureDir)), (id) => readOrNull(sliceReportPath(featureDir, id)));
64
55
  }
65
56
 
66
- /** Unfinished slices whose dependencies are all done: what the next dispatch can take. */
67
57
  export function readySlices(progress) {
68
58
  if (!progress) return [];
69
59
  const done = new Set(progress.slices.filter((s) => s.status === 'feito').map((s) => s.id));
70
60
  return progress.slices.filter((s) => s.status !== 'feito' && s.depends.every((d) => done.has(d)));
71
61
  }
72
62
 
73
- /**
74
- * What a slice report must carry, besides prose and its covered scenarios.
75
- * Loose on purpose: a false "complete" costs a read, a false "incomplete"
76
- * re-dispatches finished work.
77
- */
63
+ // Loose on purpose: a false "complete" costs a read, a false "incomplete"
64
+ // re-dispatches finished work.
78
65
  export const REPORT_MARKERS = [
79
66
  { id: 'files', re: /^\s*(?:[-*]\s*)?(?:\*\*|#+\s*)?arquivos\s*:?\**\s*:?/mi },
80
67
  ];
@@ -83,13 +70,7 @@ const MIN_PROSE_CHARS = 80;
83
70
 
84
71
  const scenarioRe = (id) => new RegExp(`(?<![\\w-])${id}(?![\\w-])`);
85
72
 
86
- /**
87
- * Inspect one report file: does it exist, which mandatory items it lacks,
88
- * and whether it is partial.
89
- *
90
- * @param {string} path
91
- * @param {{ covers?: string[] }} [slice] - every scenario the slice covers must be named
92
- */
73
+ /** Every scenario in `covers` must be named in the report. */
93
74
  export function inspectReport(path, { covers = [] } = {}) {
94
75
  const missingAll = ['what', 'files', ...covers];
95
76
  if (!existsSync(path)) return { exists: false, complete: false, missing: missingAll };
@@ -1,34 +1,21 @@
1
- /**
2
- * Which files count as the project's hand-written source. One reading for the
3
- * `reuse` and `code-budget` validators and the `comment-guard` hook, so the
4
- * three never disagree about the same file.
5
- *
6
- * @module source-files
7
- */
1
+ // One reading of "hand-written project source" for the `reuse` and
2
+ // `code-budget` validators and the `comment-guard` hook.
8
3
 
9
4
  const SCANNABLE = /\.(cs|ts|tsx|js|jsx|mjs|cjs)$/i;
10
5
 
11
- // `.morph/` is real: an archived feature's `contracts.cs` entered ProspectPRO's
12
- // graph as a second `RoulettePriority`.
6
+ // `.morph/` holds archived contracts that would duplicate real types.
13
7
  const NOT_SOURCE = /(^|\/)(?:\.morph|node_modules|dist|build|out|\.next|bin|obj|coverage)\//;
14
8
 
15
9
  const DECLARATION_FILE = /\.d\.ts$/i;
16
10
 
17
11
  const GENERATED = /(?:\.Designer|\.g|\.g\.i)\.cs$|ModelSnapshot\.cs$/i;
18
12
 
19
- /**
20
- * @param {string} sourceFile
21
- * @returns {boolean} project code in a scanned extension, tests included
22
- */
13
+ /** Tests included. */
23
14
  export function isProjectSource(sourceFile) {
24
15
  const f = String(sourceFile || '').replace(/\\/g, '/');
25
16
  return SCANNABLE.test(f) && !DECLARATION_FILE.test(f) && !NOT_SOURCE.test(f);
26
17
  }
27
18
 
28
- /**
29
- * @param {string} sourceFile
30
- * @returns {boolean} generated C# nobody writes by hand
31
- */
32
19
  export function isGenerated(sourceFile) {
33
20
  return GENERATED.test(String(sourceFile || ''));
34
21
  }