@polymorphism-tech/morph-spec 8.47.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 (377) hide show
  1. package/CLAUDE.md +51 -43
  2. package/README.md +39 -38
  3. package/bin/morph-spec.js +41 -113
  4. package/bin/validate.js +1 -147
  5. package/claude-plugin.json +1 -1
  6. package/docs/CHEATSHEET.md +1 -1
  7. package/docs/QUICKSTART.md +1 -1
  8. package/framework/CLAUDE.md +41 -101
  9. package/framework/MORPH.md +95 -252
  10. package/framework/agents.json +86 -21
  11. package/framework/commands/morph-apply.md +5 -28
  12. package/framework/commands/morph-archive.md +1 -1
  13. package/framework/commands/morph-hotfix.md +8 -8
  14. package/framework/commands/morph-infra.md +1 -1
  15. package/framework/commands/morph-preflight.md +2 -3
  16. package/framework/commands/morph-status.md +4 -4
  17. package/framework/docs/CLI.md +574 -286
  18. package/framework/evals/review-checklist.md +82 -41
  19. package/framework/hooks/README.md +10 -11
  20. package/framework/hooks/claude-code/core/block-destructive-worktree-removal.js +6 -49
  21. package/framework/hooks/claude-code/core/block-raw-worktree-add.js +2 -35
  22. package/framework/hooks/claude-code/core/chain.js +4 -65
  23. package/framework/hooks/claude-code/core/comment-guard.js +3 -17
  24. package/framework/hooks/claude-code/core/denylist.json +33 -3
  25. package/framework/hooks/claude-code/core/enforce-phase-writes.js +15 -63
  26. package/framework/hooks/claude-code/core/gate-guard.js +9 -78
  27. package/framework/hooks/claude-code/core/graph-refresh-kick.js +3 -24
  28. package/framework/hooks/claude-code/core/heavy-gate.js +3 -21
  29. package/framework/hooks/claude-code/core/heavy-release.js +2 -13
  30. package/framework/hooks/claude-code/core/loop-detect.js +2 -31
  31. package/framework/hooks/claude-code/core/pre-bash-denylist.js +36 -2
  32. package/framework/hooks/claude-code/core/protect-spec-files.js +25 -297
  33. package/framework/hooks/claude-code/core/state-sync.js +15 -85
  34. package/framework/hooks/claude-code/core/telemetry-log.js +15 -96
  35. package/framework/hooks/claude-code/core/validate-completion.js +37 -147
  36. package/framework/hooks/claude-code/core/warn-bash-source-write.js +23 -0
  37. package/framework/hooks/claude-code/core/worktree-context.js +15 -114
  38. package/framework/hooks/claude-code/core/worktree-create.js +6 -70
  39. package/framework/hooks/claude-code/core/worktree-env-inject.js +5 -56
  40. package/framework/hooks/claude-code/core/worktree-remove.js +5 -31
  41. package/framework/hooks/claude-code/statusline.py +55 -41
  42. package/framework/hooks/package.json +3 -0
  43. package/framework/hooks/shared/activity-logger.js +9 -89
  44. package/framework/hooks/shared/agent-reap.js +2 -17
  45. package/framework/hooks/shared/build-check.js +11 -76
  46. package/framework/hooks/shared/claim-registry.js +9 -119
  47. package/framework/hooks/shared/command-tokens.js +168 -44
  48. package/framework/hooks/shared/comment-rules.js +5 -20
  49. package/framework/hooks/shared/comment-scanner.js +11 -43
  50. package/framework/hooks/shared/comment-stripper.js +11 -85
  51. package/framework/hooks/shared/e2e-check.js +0 -0
  52. package/framework/hooks/shared/errata.js +59 -0
  53. package/framework/hooks/shared/feature-locator.js +11 -95
  54. package/framework/hooks/shared/frontend-locations.js +4 -0
  55. package/framework/hooks/shared/gate-question.js +12 -0
  56. package/framework/hooks/shared/git-exclude.js +3 -20
  57. package/framework/hooks/shared/git-paths.js +8 -104
  58. package/framework/hooks/shared/graph-freshness.js +22 -166
  59. package/framework/hooks/shared/graph-refresh.js +21 -149
  60. package/framework/hooks/shared/heavy-command.js +4 -12
  61. package/framework/hooks/shared/hook-response.js +8 -83
  62. package/framework/hooks/shared/morph-cli.js +8 -52
  63. package/framework/hooks/shared/payload-utils.js +10 -132
  64. package/framework/hooks/shared/phase-utils.js +14 -72
  65. package/framework/hooks/shared/redact-secrets.js +4 -15
  66. package/framework/hooks/shared/reparse-scan.js +6 -66
  67. package/framework/hooks/shared/slice-progress.js +99 -0
  68. package/framework/hooks/shared/source-files.js +4 -17
  69. package/framework/hooks/shared/spawn-command.js +23 -0
  70. package/framework/hooks/shared/state-reader.js +51 -247
  71. package/framework/hooks/shared/state-writer.js +9 -54
  72. package/framework/hooks/shared/stdin-reader.js +1 -11
  73. package/framework/hooks/shared/telemetry-logger.js +7 -67
  74. package/framework/hooks/shared/transcript-usage.js +7 -29
  75. package/framework/hooks/shared/worktree-root.js +7 -29
  76. package/framework/mcp/registry.json +3 -4
  77. package/framework/phases.json +23 -39
  78. package/framework/rules/csharp-standards.md +1 -1
  79. package/framework/rules/frontend-standards.md +1 -0
  80. package/framework/rules/hangfire-standards.md +1 -1
  81. package/framework/rules/neon-standards.md +1 -1
  82. package/framework/schemas/config.schema.json +14 -9
  83. package/framework/schemas/feature.schema.json +15 -50
  84. package/framework/skills/README.md +3 -4
  85. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  86. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  87. package/framework/skills/core/morph-apply/SKILL.md +149 -222
  88. package/framework/skills/core/morph-hotfix/SKILL.md +33 -42
  89. package/framework/skills/core/morph-machine/SKILL.md +2 -2
  90. package/framework/skills/core/morph-media/SKILL.md +1 -1
  91. package/framework/skills/core/morph-plan/SKILL.md +137 -299
  92. package/framework/skills/core/morph-review/SKILL.md +111 -136
  93. package/framework/skills/core/morph-standards/SKILL.md +27 -39
  94. package/framework/skills/core/morph-uiux/SKILL.md +94 -152
  95. package/framework/standards/STANDARDS.json +109 -1219
  96. package/framework/standards/STANDARDS.meta.json +1319 -0
  97. package/framework/standards/ai-agents/agent-archetypes.md +2 -2
  98. package/framework/standards/ai-agents/agent-session.md +1 -1
  99. package/framework/standards/ai-agents/conversational-agent-with-phases.md +4 -4
  100. package/framework/standards/ai-agents/durable-workflows-hitl.md +3 -3
  101. package/framework/standards/ai-agents/evals-with-cache.md +1 -2
  102. package/framework/standards/ai-agents/guardrails.md +1 -1
  103. package/framework/standards/ai-agents/multi-agent-patterns.md +2 -2
  104. package/framework/standards/ai-agents/observability-patterns.md +1 -1
  105. package/framework/standards/ai-agents/production.md +1 -1
  106. package/framework/standards/ai-agents/prompt-sources.md +2 -2
  107. package/framework/standards/ai-agents/providers/model-registry.md +1 -1
  108. package/framework/standards/ai-agents/rag-custom-pgvector.md +2 -2
  109. package/framework/standards/ai-agents/setup.md +3 -3
  110. package/framework/standards/ai-agents/sweet-spot.md +2 -2
  111. package/framework/standards/ai-agents/tool-adapters.md +0 -2
  112. package/framework/standards/ai-agents/vector-data-extensions.md +4 -4
  113. package/framework/standards/ai-agents/workflows.md +1 -1
  114. package/framework/standards/architecture/code-comments.md +7 -7
  115. package/framework/standards/architecture/vertical-slice/vertical-slice.md +1 -1
  116. package/framework/standards/backend/api/validation.md +1 -1
  117. package/framework/standards/backend/dotnet/vsa-handler-patterns.md +5 -5
  118. package/framework/standards/backend/integrations/hangfire/hangfire-jobs.md +1 -1
  119. package/framework/standards/backend/integrations/neon-auth/neon-auth.md +2 -2
  120. package/framework/standards/data/nosql/cache/redis.md +1 -1
  121. package/framework/standards/data/nosql/cosmos-db.md +3 -3
  122. package/framework/standards/frontend/admin-app/forms.md +1 -1
  123. package/framework/standards/frontend/design-system/art-direction.md +88 -0
  124. package/framework/standards/frontend/design-system/premium-finish.md +77 -2
  125. package/framework/standards/infrastructure/neon/neon-pgvector.md +5 -5
  126. package/framework/standards/integration/mcp/mcp-tools.md +7 -7
  127. package/framework/templates/CLAUDE.md.core.template +3 -3
  128. package/framework/templates/README.md +2 -3
  129. package/framework/templates/REGISTRY.json +15 -179
  130. package/framework/templates/docs/plan.md +72 -0
  131. package/framework/templates/docs/ui-mockups.md +4 -0
  132. package/framework/templates/dotnet/ai-kit/README.md +1 -9
  133. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Morph.AiKit.Tests.csproj +0 -10
  134. package/framework/templates/meta-prompts/squad-leaders/backend-squad.md +1 -1
  135. package/framework/templates/meta-prompts/squad-leaders/frontend-squad.md +1 -1
  136. package/framework/templates/meta-prompts/squad-leaders/squad-leader.md +1 -1
  137. package/framework/templates/meta-prompts/validators/pre-commit-validator.md +3 -4
  138. package/package.json +1 -1
  139. package/src/commands/context/context-refresh.js +0 -16
  140. package/src/commands/dashboard/dashboard.js +2 -8
  141. package/src/commands/e2e/e2e.js +18 -105
  142. package/src/commands/graph/affected.js +0 -14
  143. package/src/commands/graph/build.js +11 -50
  144. package/src/commands/graph/explain.js +0 -16
  145. package/src/commands/graph/status.js +1 -12
  146. package/src/commands/mcp/mcp-setup.js +4 -62
  147. package/src/commands/observability/cost.js +17 -111
  148. package/src/commands/observability/telemetry.js +1 -14
  149. package/src/commands/project/branch.js +4 -45
  150. package/src/commands/project/claim.js +4 -48
  151. package/src/commands/project/doctor-procs.js +1 -1
  152. package/src/commands/project/doctor.js +53 -307
  153. package/src/commands/project/env.js +1 -27
  154. package/src/commands/project/fleet.js +6 -35
  155. package/src/commands/project/heavy.js +3 -3
  156. package/src/commands/project/init.js +8 -68
  157. package/src/commands/project/install-plugin-cmd.js +0 -1
  158. package/src/commands/project/setup-infra-cmd.js +2 -1
  159. package/src/commands/project/stack-scan.js +58 -0
  160. package/src/commands/project/standards.js +65 -72
  161. package/src/commands/project/status.js +28 -121
  162. package/src/commands/project/update.js +31 -223
  163. package/src/commands/project/worktree.js +109 -883
  164. package/src/commands/scaffold/scaffold.js +0 -25
  165. package/src/commands/state/advance.js +65 -120
  166. package/src/commands/state/approve.js +87 -229
  167. package/src/commands/state/archive.js +23 -139
  168. package/src/commands/state/auto-approval.js +65 -0
  169. package/src/commands/state/create.js +49 -109
  170. package/src/commands/state/delete.js +11 -63
  171. package/src/commands/state/finish.js +105 -494
  172. package/src/commands/state/gate-check.js +74 -183
  173. package/src/commands/state/gate-decision.js +8 -42
  174. package/src/commands/state/handoff.js +4 -31
  175. package/src/commands/state/list.js +21 -91
  176. package/src/commands/state/report-check.js +134 -338
  177. package/src/commands/state/retype.js +12 -28
  178. package/src/commands/validation/index.js +0 -3
  179. package/src/commands/validation/mutate.js +79 -19
  180. package/src/commands/validation/plan-check.js +81 -0
  181. package/src/commands/validation/validate-feature.js +52 -216
  182. package/src/commands/validation/verify.js +45 -160
  183. package/src/core/paths/output-schema.js +9 -155
  184. package/src/core/state/feature-identity.js +1 -25
  185. package/src/core/state/gate-order.js +2 -30
  186. package/src/core/state/lifecycle-audit.js +8 -100
  187. package/src/core/state/state-manager.js +32 -395
  188. package/src/core/templates/template-registry.js +2 -97
  189. package/src/core/templates/template-renderer.js +13 -93
  190. package/src/core/templates/template-seed.js +21 -0
  191. package/src/core/work-types.js +10 -45
  192. package/src/dashboard/app/App.jsx +1 -8
  193. package/src/dashboard/app/components/ActivityFeed.jsx +2 -8
  194. package/src/dashboard/app/components/CommandPalette.jsx +4 -11
  195. package/src/dashboard/app/components/DetailPanel.jsx +1 -5
  196. package/src/dashboard/app/components/EntityLink.jsx +0 -12
  197. package/src/dashboard/app/components/ErrorBoundary.jsx +1 -8
  198. package/src/dashboard/app/components/KnowledgeMarkdown.jsx +0 -11
  199. package/src/dashboard/app/components/Markdown.jsx +0 -9
  200. package/src/dashboard/app/components/PhaseRail.jsx +0 -11
  201. package/src/dashboard/app/components/SearchFilter.jsx +0 -4
  202. package/src/dashboard/app/components/Sidebar.jsx +0 -5
  203. package/src/dashboard/app/hooks/useMorphQuery.js +1 -11
  204. package/src/dashboard/app/hooks/usePref.js +1 -5
  205. package/src/dashboard/app/lib/format.js +0 -2
  206. package/src/dashboard/app/lib/phases.js +3 -9
  207. package/src/dashboard/app/lib/project.js +0 -12
  208. package/src/dashboard/app/pages/Activity.jsx +1 -1
  209. package/src/dashboard/app/pages/AgentMap.jsx +1 -8
  210. package/src/dashboard/app/pages/FeatureDetail.jsx +139 -133
  211. package/src/dashboard/app/pages/Features.jsx +9 -16
  212. package/src/dashboard/app/pages/Guide.jsx +2 -3
  213. package/src/dashboard/app/pages/HookInspector.jsx +2 -23
  214. package/src/dashboard/app/pages/PhasePipeline.jsx +3 -7
  215. package/src/dashboard/app/pages/Portfolio.jsx +0 -1
  216. package/src/dashboard/app/pages/RulesMap.jsx +1 -16
  217. package/src/dashboard/app/pages/SkillsLibrary.jsx +1 -16
  218. package/src/dashboard/app/pages/Standards.jsx +2 -17
  219. package/src/dashboard/scanner/activity-scanner.js +14 -83
  220. package/src/dashboard/scanner/agents-scanner.js +1 -23
  221. package/src/dashboard/scanner/docs-scanner.js +1 -7
  222. package/src/dashboard/scanner/feature-status.js +2 -5
  223. package/src/dashboard/scanner/features-scanner.js +92 -113
  224. package/src/dashboard/scanner/hooks-scanner.js +5 -28
  225. package/src/dashboard/scanner/index.js +0 -4
  226. package/src/dashboard/scanner/portfolio.js +5 -29
  227. package/src/dashboard/scanner/project-api.js +8 -54
  228. package/src/dashboard/scanner/rules-scanner.js +1 -2
  229. package/src/dashboard/scanner/skills-scanner.js +2 -12
  230. package/src/dashboard/scanner/standards-scanner.js +4 -7
  231. package/src/dashboard/vite-plugin-morph-data.js +5 -19
  232. package/src/lib/ai/ai-checks.js +7 -52
  233. package/src/lib/ai/ai-project-scan.js +14 -108
  234. package/src/lib/ai/code-audit.js +7 -63
  235. package/src/lib/ai/pin-drift.js +8 -51
  236. package/src/lib/ai/registry-audit.js +11 -90
  237. package/src/lib/ci/ci-verify.js +14 -13
  238. package/src/lib/ci/github-ci.js +9 -37
  239. package/src/lib/ci/workflow-scaffold.js +2 -10
  240. package/src/lib/detectors/claude-config-detector.js +13 -74
  241. package/src/lib/detectors/index.js +8 -59
  242. package/src/lib/e2e/compose-env.js +5 -66
  243. package/src/lib/e2e/stack-teardown.js +6 -40
  244. package/src/lib/generators/architecture-scanner.js +18 -156
  245. package/src/lib/generators/handoff-generator.js +37 -112
  246. package/src/lib/generators/scaffold-generator.js +5 -101
  247. package/src/lib/generators/settings-generator.js +11 -122
  248. package/src/lib/git/branch-classifier.js +11 -84
  249. package/src/lib/git/default-branch.js +14 -169
  250. package/src/lib/git/evidence-staleness.js +2 -1
  251. package/src/lib/git/gh.js +2 -17
  252. package/src/lib/git/pending-prs.js +6 -37
  253. package/src/lib/git/pr-files.js +3 -28
  254. package/src/lib/git/pr-index.js +3 -34
  255. package/src/lib/git/tree-fingerprint.js +17 -17
  256. package/src/lib/git/tree-state.js +6 -33
  257. package/src/lib/graph/god-nodes.js +2 -22
  258. package/src/lib/graph/graph-health.js +3 -24
  259. package/src/lib/graph/graph-loader.js +5 -56
  260. package/src/lib/graph/graphify-runtime.js +7 -37
  261. package/src/lib/graph/graphifyignore.js +8 -74
  262. package/src/lib/graph/sanitize.js +16 -136
  263. package/src/lib/heavy/cleanup.js +2 -6
  264. package/src/lib/installers/effects.js +8 -93
  265. package/src/lib/installers/mcp-installer.js +9 -169
  266. package/src/lib/installers/memory-installer.js +8 -33
  267. package/src/lib/installers/templates-reconcile.js +5 -60
  268. package/src/lib/mutation/commands.js +3 -2
  269. package/src/lib/mutation/dotnet-introspection.js +281 -0
  270. package/src/lib/mutation/plan.js +14 -6
  271. package/src/lib/mutation/runner.js +1 -1
  272. package/src/lib/mutation/subset.js +47 -13
  273. package/src/lib/plan/artifact-vocabulary.js +40 -0
  274. package/src/lib/plan/checks.js +208 -0
  275. package/src/lib/plan/plan-parser.js +204 -0
  276. package/src/lib/plan/plan-template.js +18 -0
  277. package/src/lib/projects/registry.js +7 -44
  278. package/src/lib/review-reports.js +204 -0
  279. package/src/lib/risk-detector.js +131 -274
  280. package/src/lib/specs/adr-index.js +38 -85
  281. package/src/lib/specs/design-seed.js +18 -0
  282. package/src/lib/specs/frontmatter.js +31 -0
  283. package/src/lib/specs/gfm-table.js +11 -0
  284. package/src/lib/specs/revocations.js +31 -102
  285. package/src/lib/specs/screen-checklist.js +56 -132
  286. package/src/lib/stack/dotnet-build-ignores.js +43 -0
  287. package/src/lib/stack/stack-profile.js +1 -28
  288. package/src/lib/stack/stack-scan.js +260 -0
  289. package/src/lib/stack-filter.js +4 -27
  290. package/src/lib/standards/registry.js +37 -0
  291. package/src/lib/standards/sections.js +47 -0
  292. package/src/lib/standards/verified-against.js +19 -191
  293. package/src/lib/tasks/eval-node.js +23 -179
  294. package/src/lib/tasks/linux-runner.js +18 -142
  295. package/src/lib/tasks/test-nodes.js +43 -313
  296. package/src/lib/tasks/test-runner.js +107 -447
  297. package/src/lib/telemetry/metrics.js +13 -103
  298. package/src/lib/telemetry/telemetry-reader.js +13 -238
  299. package/src/lib/telemetry/verify-event.js +4 -34
  300. package/src/lib/validators/code-budget/code-budget-validator.js +4 -44
  301. package/src/lib/validators/code-budget/index.js +0 -7
  302. package/src/lib/validators/contracts/contract-compliance-validator.js +4 -26
  303. package/src/lib/validators/contracts/index.js +0 -6
  304. package/src/lib/validators/nextjs/index.js +0 -5
  305. package/src/lib/validators/nextjs/next-component-validator.js +4 -81
  306. package/src/lib/validators/packages/index.js +0 -6
  307. package/src/lib/validators/packages/package-validator.js +6 -56
  308. package/src/lib/validators/reuse/index.js +0 -8
  309. package/src/lib/validators/reuse/reuse-validator.js +24 -137
  310. package/src/lib/validators/reuse/symbol-scanner.js +6 -62
  311. package/src/lib/validators/traceability-check.js +54 -156
  312. package/src/lib/validators/validation-runner.js +20 -125
  313. package/src/lib/validators/verify-runner.js +152 -575
  314. package/src/lib/work-type-detector.js +5 -67
  315. package/src/lib/worktree/docker-audit.js +17 -137
  316. package/src/lib/worktree/env-allocator.js +10 -98
  317. package/src/lib/worktree/feature-truth.js +12 -115
  318. package/src/lib/worktree/finish-backup.js +32 -242
  319. package/src/lib/worktree/fleet-scanner.js +31 -126
  320. package/src/lib/worktree/ignored-triage.js +15 -91
  321. package/src/lib/worktree/vscode-settings.js +2 -3
  322. package/src/lib/worktree/worktree-audit.js +21 -213
  323. package/src/lib/worktree/worktree-include.js +3 -5
  324. package/src/scripts/global-install.js +3 -9
  325. package/src/scripts/install-plugin.js +1 -13
  326. package/src/scripts/setup-infra.js +17 -138
  327. package/src/utils/agents-installer.js +92 -272
  328. package/src/utils/banner.js +4 -25
  329. package/src/utils/claude-md-injector.js +1 -37
  330. package/src/utils/claude-settings-manager.js +1 -87
  331. package/src/utils/file-copier.js +32 -90
  332. package/src/utils/hooks-installer.js +26 -296
  333. package/src/utils/skills-installer.js +4 -102
  334. package/src/utils/version-checker.js +4 -83
  335. package/framework/commands/morph-proposal.md +0 -57
  336. package/framework/evals/trace-schema.md +0 -67
  337. package/framework/hooks/claude-code/core/tasks-json-guard.js +0 -83
  338. package/framework/hooks/claude-code/core/trace-autogen.js +0 -277
  339. package/framework/hooks/git/pre-commit/specs.sh +0 -50
  340. package/framework/hooks/shared/tasks-validator.js +0 -111
  341. package/framework/rules/morph-task-tracking.md +0 -54
  342. package/framework/rules/morph-workflow.md +0 -244
  343. package/framework/schemas/tasks.schema.json +0 -79
  344. package/framework/skills/core/morph-brainstorming/SKILL.md +0 -161
  345. package/framework/skills/core/morph-implement/SKILL.md +0 -194
  346. package/framework/skills/core/morph-proposal/SKILL.md +0 -152
  347. package/framework/skills/core/morph-stack-scan/SKILL.md +0 -145
  348. package/framework/templates/docs/proposal.md +0 -192
  349. package/framework/templates/docs/spec.md +0 -57
  350. package/framework/templates/docs/ui-components.md +0 -52
  351. package/framework/templates/docs/ui-design-system.md +0 -76
  352. package/framework/templates/docs/ui-flows.md +0 -167
  353. package/framework/templates/examples/spec-examples.md +0 -91
  354. package/framework/templates/feature/decisions.md +0 -124
  355. package/framework/templates/feature/recap.md +0 -145
  356. package/framework/templates/tasks.template.json +0 -17
  357. package/src/commands/state/usecase.js +0 -143
  358. package/src/commands/tasks/dag.js +0 -181
  359. package/src/commands/utils/index.js +0 -3
  360. package/src/lib/agents/output-collector.js +0 -76
  361. package/src/lib/detectors/design-system-detector.js +0 -188
  362. package/src/lib/evaluator-report.js +0 -104
  363. package/src/lib/generators/design-system-generator.js +0 -508
  364. package/src/lib/specs/acervo-format.js +0 -70
  365. package/src/lib/specs/acervo-lifecycle.js +0 -264
  366. package/src/lib/specs/done-criteria-check.js +0 -200
  367. package/src/lib/specs/usecase-check.js +0 -301
  368. package/src/lib/specs/usecase-promoter.js +0 -301
  369. package/src/lib/specs/usecase-registry.js +0 -237
  370. package/src/lib/tasks/dispatch-planner.js +0 -319
  371. package/src/lib/tasks/dotnet-test-introspection.js +0 -632
  372. package/src/lib/tasks/mermaid-renderer.js +0 -132
  373. package/src/lib/tasks/task-classifier.js +0 -30
  374. package/src/lib/tasks/task-parser.js +0 -209
  375. package/src/lib/tasks/tasks-json-parser.js +0 -296
  376. package/src/lib/tasks/test-scope.js +0 -597
  377. package/src/utils/agent-id-mapper.js +0 -11
@@ -1,30 +1,16 @@
1
1
  /**
2
- * The dashboard's view of the target project's features — the ONLY scanner
3
- * that reads the project rather than the framework package.
4
- *
5
- * Every rule here is borrowed, never re-encoded:
6
- * - which tree owns a feature → `resolveFeatureTree` (feature-truth.js)
7
- * - the current phase → `derivePhaseFrom` (state-reader.js)
8
- * - gate order → `GATE_PREREQ` (gate-order.js)
9
- * - the evaluator's verdict → `evaluatorVerdictFromText` (evaluator-report.js)
10
- * - path confinement → `canonicalPath`/`isInside` (test-runner.js)
11
- * - tasks.json validity → `validateTasksJson` (tasks-json-parser.js)
12
- *
13
- * Three places a feature can live: the root `.morph/features/{f}`, a worktree
14
- * `{worktrees}/{wt}/.morph/features/{f}`, and `.morph/archive/{f}` once
15
- * `finish` ran. The root copy of a feature built in a worktree is its
16
- * birth-instant skeleton and never changes again, so for an active feature the
17
- * owner tree wins. Only active features pay for that question — it spawns git.
18
- *
19
- * Read-only and fail-open: a corrupt file degrades one feature, never the scan.
2
+ * The root copy of a feature built in a worktree is its birth skeleton, so for an
3
+ * active feature the owner tree wins. Only active features ask: it spawns git.
20
4
  */
21
5
 
22
6
  import { readFileSync, existsSync, readdirSync, statSync } from 'fs';
23
7
  import { join, basename, isAbsolute } from 'path';
24
- import { derivePhaseFrom, PHASE_ARTIFACTS } from '../../../framework/hooks/shared/state-reader.js';
8
+ import { derivePhaseFrom, diskArtifactProbe, PHASE_ARTIFACTS } from '../../../framework/hooks/shared/state-reader.js';
25
9
  import { GATE_PREREQ } from '../../core/state/gate-order.js';
26
- import { validateTasksJson } from '../../lib/tasks/tasks-json-parser.js';
27
- import { evaluatorVerdictFromText } from '../../lib/evaluator-report.js';
10
+ import { sliceProgress, checksPath } from '../../../framework/hooks/shared/slice-progress.js';
11
+ import {
12
+ reviewReportsFromDir, requiredReviewRoles, combinedVerdict, allBlockers, checksCoverage, reviewFileName, REVIEW_ROLES,
13
+ } from '../../lib/review-reports.js';
28
14
  import { resolveFeatureTree } from '../../lib/worktree/feature-truth.js';
29
15
  import { containerDirs } from '../../../framework/hooks/shared/worktree-root.js';
30
16
  import { canonicalPath, isInside, readMorphConfig } from '../../lib/tasks/test-runner.js';
@@ -33,7 +19,6 @@ import { isFinished } from './feature-status.js';
33
19
  export const GATES = Object.keys(GATE_PREREQ);
34
20
  const FINISHED = new Set(['completed', 'abandoned']);
35
21
  const FOLDER_PHASE = Object.fromEntries(PHASE_ARTIFACTS.map(([folder, phase]) => [folder, phase]));
36
- const EVALUATOR_REPORT = '4-review/evaluator-report.md';
37
22
  const MAX_FILE_BYTES = 2 * 1024 * 1024;
38
23
  const MAX_DEPTH = 4;
39
24
 
@@ -47,10 +32,7 @@ function subdirs(dir) {
47
32
  } catch { return []; }
48
33
  }
49
34
 
50
- /**
51
- * Old feature.json files stamped a gate as a bare `true` (MORPH_OS). Lifting it
52
- * to `{approved: true}` is what lets the shared rules read them unchanged.
53
- */
35
+ /** Old feature.json files stamped a gate as a bare `true`. */
54
36
  export function normalizeGates(gates) {
55
37
  const out = {};
56
38
  for (const [name, value] of Object.entries(gates || {})) {
@@ -64,10 +46,6 @@ function normalizeFeature(fj) {
64
46
  return { ...f, approvalGates: normalizeGates(f.approvalGates || f.approvals) };
65
47
  }
66
48
 
67
- /**
68
- * Every place a feature lives, before deciding which one tells the truth.
69
- * @returns {Map<string, {root?: string, worktrees: string[], archive?: string}>}
70
- */
71
49
  function collectLocations(projectDir) {
72
50
  const found = new Map();
73
51
  const slot = (name) => {
@@ -92,17 +70,10 @@ function collectLocations(projectDir) {
92
70
  return found;
93
71
  }
94
72
 
95
- /**
96
- * Decide which directory is this feature's truth.
97
- *
98
- * @returns {{ location: 'root'|'worktree'|'archive', dir: string, feature: object,
99
- * ownerTree: string|null, rootStale: boolean }}
100
- */
101
73
  function resolveLocation(projectDir, name, loc, resolveTree) {
102
74
  const rootFj = loc.root ? readJson(join(loc.root, 'feature.json')) : null;
103
75
 
104
- // Archived: `finish` moved the artifacts. An old root copy may linger (MORPH_OS
105
- // keeps a feature.json per finished feature) — the archive holds the files.
76
+ // An old root copy may linger; the archive holds the files.
106
77
  const rootFinished = rootFj && (FINISHED.has(rootFj.status) || rootFj.archivedAt);
107
78
  if (loc.archive && (!rootFj || rootFinished)) {
108
79
  const archFj = readJson(join(loc.archive, 'feature.json'));
@@ -122,8 +93,7 @@ function resolveLocation(projectDir, name, loc, resolveTree) {
122
93
  const wtDir = join(tree, '.morph', 'features', name);
123
94
  const wtFj = readJson(join(wtDir, 'feature.json'));
124
95
  if (wtFj) return { location: 'worktree', dir: wtDir, feature: wtFj, ownerTree: tree, rootStale: false };
125
- // The owner tree exists but carries no copy of the feature: all we have is
126
- // the root skeleton, and the UI has to say so.
96
+ // The owner tree has no copy of the feature: only the root skeleton is left.
127
97
  return { location: 'root', dir: loc.root, feature: rootFj, ownerTree: tree, rootStale: true };
128
98
  }
129
99
  }
@@ -140,32 +110,23 @@ function resolveLocation(projectDir, name, loc, resolveTree) {
140
110
  };
141
111
  }
142
112
 
143
- /**
144
- * tasks.json as the dashboard needs it: the list even when the file breaks the
145
- * schema (a read-only viewer shows what is there), with the errors alongside.
146
- */
147
- export function readTasks(dir) {
148
- const path = join(dir, '2-plan', 'tasks.json');
149
- if (!existsSync(path)) return { list: [], errors: [], present: false };
150
- const data = readJson(path);
151
- if (!data) return { list: [], errors: ['tasks.json não é JSON válido'], present: true };
152
- const { valid, errors } = validateTasksJson(data);
153
- const list = Array.isArray(data.tasks) ? data.tasks.filter((t) => t && typeof t === 'object') : [];
154
- return { list, errors: valid ? [] : errors, present: true };
113
+ export function readSlices(dir) {
114
+ const progress = sliceProgress(dir);
115
+ if (!progress) return null;
116
+ const extracted = new Map((readJson(checksPath(dir))?.slices || []).filter((x) => x && x.id).map((x) => [x.id, x]));
117
+ const list = progress.slices.map((sl) => {
118
+ const x = extracted.get(sl.id) || {};
119
+ return {
120
+ ...sl,
121
+ persona: x.persona ?? null,
122
+ files: Array.isArray(x.files) ? x.files : [],
123
+ covers: Array.isArray(x.covers) ? x.covers : [],
124
+ };
125
+ });
126
+ return { counts: { total: progress.total, done: progress.done, partial: progress.partial, pending: progress.pending }, list };
155
127
  }
156
128
 
157
- function taskCounts(list) {
158
- const counts = { total: list.length, done: 0, inProgress: 0, blocked: 0, pending: 0, skipped: 0 };
159
- for (const t of list) {
160
- // `completed` is the pre-schema spelling of `done` (MORPH_OS archives carry both).
161
- if (t.status === 'done' || t.status === 'completed') counts.done += 1;
162
- else if (t.status === 'in_progress') counts.inProgress += 1;
163
- else if (t.status === 'blocked') counts.blocked += 1;
164
- else if (t.status === 'skipped') counts.skipped += 1;
165
- else counts.pending += 1;
166
- }
167
- return counts;
168
- }
129
+ const NO_SLICES = { total: 0, done: 0, partial: 0, pending: 0 };
169
130
 
170
131
  function gatesOf(feature) {
171
132
  const skipped = new Set(feature.skippedPhases || []);
@@ -185,20 +146,60 @@ function gatesOf(feature) {
185
146
  });
186
147
  }
187
148
 
188
- function evaluatorVerdict(dir) {
189
- const path = join(dir, ...EVALUATOR_REPORT.split('/'));
190
- if (!existsSync(path)) return null;
191
- try { return evaluatorVerdictFromText(readFileSync(path, 'utf8')); } catch { return null; }
149
+ // Security counts only when its report exists: deciding it is required takes git.
150
+ function reviewVerdict(dir, workType, reports = reviewReportsFromDir(dir)) {
151
+ const present = Object.values(reports).some((r) => r.status !== 'missing');
152
+ if (!present) return null;
153
+ const required = requiredReviewRoles({ workType, securityRequired: reports.security.status !== 'missing' });
154
+ return { verdict: combinedVerdict(reports, required), blockers: allBlockers(reports) };
155
+ }
156
+
157
+ export function reviewRows(reports, workType) {
158
+ const required = requiredReviewRoles({ workType, securityRequired: reports.security.status !== 'missing' });
159
+ return Object.keys(REVIEW_ROLES)
160
+ .filter((role) => required.includes(role) || reports[role].status !== 'missing')
161
+ .map((role) => {
162
+ const r = reports[role];
163
+ return {
164
+ role,
165
+ path: `4-review/${reviewFileName(role)}`,
166
+ status: r.status,
167
+ verdict: r.verdict ?? null,
168
+ blockers: r.blockers?.length ?? 0,
169
+ reviewedSha: r.reviewedSha ?? null,
170
+ };
171
+ });
172
+ }
173
+
174
+ /** `coverage` is `null` until a verifier report exists; only PROVADO covers. */
175
+ export function readChecks(dir, verifier) {
176
+ const checks = readJson(checksPath(dir))?.checks;
177
+ if (!Array.isArray(checks)) return null;
178
+ const list = checks.filter((c) => c && c.id);
179
+ const rows = verifier?.checks ?? null;
180
+ const byId = new Map((rows ?? []).map((r) => [r.id, r]));
181
+ const coverage = verifier && verifier.status !== 'missing'
182
+ ? { total: list.length, ...checksCoverage(list.map((c) => c.id), rows) }
183
+ : null;
184
+ return {
185
+ coverage,
186
+ list: list.map((c) => ({
187
+ id: c.id,
188
+ text: c.text ?? '',
189
+ proof: c.proof ?? null,
190
+ status: byId.get(c.id)?.status ?? null,
191
+ evidence: byId.get(c.id)?.evidence ?? null,
192
+ })),
193
+ };
192
194
  }
193
195
 
194
196
  function summarize(name, resolved) {
195
197
  const { location, dir, ownerTree, rootStale } = resolved;
196
198
  const feature = normalizeFeature(resolved.feature);
197
- const has = (rel) => existsSync(join(dir, ...rel.split('/')));
198
199
  const gates = gatesOf(feature);
199
200
  const reviewApproved = gates.find((g) => g.gate === 'review')?.approved === true;
200
- const tasks = readTasks(dir);
201
- const verdict = evaluatorVerdict(dir);
201
+ const slices = readSlices(dir);
202
+ const verdict = reviewVerdict(dir, feature.workType);
202
203
  const status = feature.status || (location === 'archive' ? 'completed' : 'draft');
203
204
  const finished = isFinished({ status, location });
204
205
  return {
@@ -210,29 +211,24 @@ function summarize(name, resolved) {
210
211
  dir,
211
212
  ownerTree,
212
213
  rootStale,
213
- phase: derivePhaseFrom(feature, has),
214
- inEvaluation: has(EVALUATOR_REPORT) && !reviewApproved,
214
+ phase: derivePhaseFrom(feature, diskArtifactProbe(dir)),
215
+ inEvaluation: verdict !== null && !reviewApproved,
215
216
  gates,
216
217
  awaiting: !finished && gates.some((g) => g.paused),
217
- tasks: taskCounts(tasks.list),
218
- // Titles of an active feature's tasks, for the global search. Finished
219
- // features leave them out: 170 archived plans would bloat every list reply.
220
- taskIndex: finished ? [] : tasks.list.map((t) => ({ id: t.id, title: t.title || '', status: t.status || 'pending' })),
218
+ slices: slices?.counts ?? NO_SLICES,
219
+ // Finished features leave slice titles out: archived plans would bloat every list reply.
220
+ sliceIndex: finished || !slices ? [] : slices.list.map((sl) => ({ id: sl.id, title: sl.title, status: sl.status })),
221
221
  verification: feature.verification
222
- ? { status: feature.verification.status ?? null, scope: feature.verification.scope ?? null, ts: feature.verification.ts ?? null }
222
+ ? { status: feature.verification.status ?? null, ts: feature.verification.ts ?? null }
223
223
  : null,
224
224
  verdict: verdict?.verdict ?? null,
225
- blockers: verdict?.verdict ? verdict.blockers.length : null,
225
+ blockers: verdict ? verdict.blockers.length : null,
226
226
  createdAt: feature.createdAt || null,
227
227
  updatedAt: feature.updatedAt || feature.archivedAt || feature.createdAt || null,
228
228
  archivedAt: feature.archivedAt || null,
229
229
  };
230
230
  }
231
231
 
232
- /**
233
- * Files of a feature, grouped by the phase their top folder belongs to.
234
- * @returns {Array<{path: string, phase: string|null, size: number, mtime: string}>}
235
- */
236
232
  export function listDocuments(dir) {
237
233
  const out = [];
238
234
  const walk = (abs, rel, depth) => {
@@ -262,9 +258,8 @@ export function listDocuments(dir) {
262
258
  }
263
259
 
264
260
  /**
265
- * Resolve one feature. `known` is where a previous scan already found it: the
266
- * list scan paid for the git calls, and opening a feature or one of its files
267
- * must not pay again. It is only trusted while its feature.json still reads.
261
+ * `known` is where a previous scan found the feature, so opening it does not pay
262
+ * for git again. It is only trusted while its feature.json still reads.
268
263
  */
269
264
  function resolveOne(projectDir, name, resolveTree, known) {
270
265
  if (known?.dir) {
@@ -276,13 +271,6 @@ function resolveOne(projectDir, name, resolveTree, known) {
276
271
  return resolveLocation(projectDir, name, loc, resolveTree);
277
272
  }
278
273
 
279
- /**
280
- * The feature list of a project.
281
- *
282
- * @param {string} projectDir
283
- * @param {{ resolveTree?: Function }} [opts] - seam for tests; defaults to
284
- * `resolveFeatureTree`
285
- */
286
274
  export function scanFeatures(projectDir, { resolveTree = resolveFeatureTree } = {}) {
287
275
  const config = readMorphConfig(projectDir);
288
276
  const features = [];
@@ -301,32 +289,28 @@ export function scanFeatures(projectDir, { resolveTree = resolveFeatureTree } =
301
289
  trust: config?.trust || null,
302
290
  dir: projectDir,
303
291
  initialized: existsSync(join(projectDir, '.morph')),
304
- // A project that has state.json but no feature.json anywhere predates the
305
- // per-feature state model — the dashboard says so instead of showing "empty".
292
+ // state.json with no feature.json anywhere predates the per-feature state model.
306
293
  legacyStateOnly: hasState && features.length === 0,
307
294
  },
308
295
  features,
309
296
  };
310
297
  }
311
298
 
312
- /**
313
- * One feature in full: summary + tasks with their verify, the gate
314
- * decisions, the verify nodes and the document tree.
315
- */
316
299
  export function readFeatureDetail(projectDir, name, { resolveTree = resolveFeatureTree, known } = {}) {
317
300
  const resolved = resolveOne(projectDir, name, resolveTree, known);
318
301
  if (!resolved) return null;
319
302
  const summary = summarize(name, resolved);
320
303
  const feature = normalizeFeature(resolved.feature);
321
- const tasks = readTasks(resolved.dir);
322
- const verdict = evaluatorVerdict(resolved.dir);
304
+ const slices = readSlices(resolved.dir);
305
+ const reports = reviewReportsFromDir(resolved.dir);
306
+ const verdict = reviewVerdict(resolved.dir, feature.workType, reports);
307
+ const checks = readChecks(resolved.dir, reports.verifier);
323
308
  return {
324
309
  ...summary,
325
- tasksList: tasks.list.map((t) => ({
326
- ...t,
327
- verification: feature.taskVerifications?.[t.id] || null,
328
- })),
329
- tasksErrors: tasks.errors,
310
+ slicesList: slices?.list ?? [],
311
+ checksList: checks?.list ?? [],
312
+ checksCoverage: checks?.coverage ?? null,
313
+ reviews: reviewRows(reports, feature.workType),
330
314
  gateDecisions: feature.gateDecisions || {},
331
315
  verificationNodes: feature.verification?.nodes || null,
332
316
  featureVerification: feature.verifications?.feature || null,
@@ -336,13 +320,8 @@ export function readFeatureDetail(projectDir, name, { resolveTree = resolveFeatu
336
320
  };
337
321
  }
338
322
 
339
- /**
340
- * The content of one file of a feature, confined to the feature's directory.
341
- * Junctions and win32 case are resolved by `canonicalPath` before the check,
342
- * so `..`, an absolute path or a link that leaves the folder are all refused.
343
- *
344
- * @returns {{ path: string, content: string, mtime: string } | { error: string, status: number }}
345
- */
323
+ // Junctions and win32 case are resolved by `canonicalPath` before the check,
324
+ // so `..`, an absolute path or a link that leaves the feature folder are refused.
346
325
  export function readFeatureFile(projectDir, name, relPath, { resolveTree = resolveFeatureTree, known } = {}) {
347
326
  if (typeof relPath !== 'string' || relPath === '') return { error: 'path ausente', status: 400 };
348
327
  if (isAbsolute(relPath) || /^[a-zA-Z]:/.test(relPath)) return { error: 'path fora da feature', status: 403 };
@@ -26,13 +26,7 @@ function extractImports(source) {
26
26
  return imports;
27
27
  }
28
28
 
29
- /**
30
- * Claude Code event name → the kebab-case key the Hook Inspector groups by.
31
- * `pre-compact` and `teammate-idle` have no MORPH hooks — those nodes render
32
- * empty on purpose. `worktree-create`/`worktree-remove` are the worktree
33
- * lifecycle events (v3.8.0) — outside the tool-call flow, drawn as their own
34
- * pair in HookInspector.jsx.
35
- */
29
+ /** `pre-compact` and `teammate-idle` have no MORPH hooks: those nodes render empty on purpose. */
36
30
  const EVENT_KEYS = {
37
31
  SessionStart: 'session-start',
38
32
  UserPromptSubmit: 'user-prompt',
@@ -47,20 +41,7 @@ const EVENT_KEYS = {
47
41
  WorktreeRemove: 'worktree-remove',
48
42
  };
49
43
 
50
- /**
51
- * Build filename → [{ event, matcher }] from the installer's MORPH_HOOKS.
52
- *
53
- * The event is NOT derivable from the path. Before the v4→v5 migration the
54
- * hooks sat in per-event folders, so `basename(dirname(file))` produced the
55
- * event key; the migration flattened everything into `core/`, and from then on
56
- * every hook reported `event: 'core'`. Nothing crashed — the Inspector groups
57
- * by event, found no key it knew, and rendered every node empty. Twelve hooks,
58
- * zero shown.
59
- *
60
- * `getHookDefinitions()` is the single source of truth (the same array the
61
- * installer writes into settings.local.json), so the registration and the
62
- * drawing can no longer disagree.
63
- */
44
+ /** The event is not derivable from the path: every hook sits in `core/`. */
64
45
  export function hookRegistrations(definitions = getHookDefinitions()) {
65
46
  const byFile = new Map();
66
47
  for (const def of definitions) {
@@ -69,10 +50,8 @@ export function hookRegistrations(definitions = getHookDefinitions()) {
69
50
  for (const h of def.hooks || []) {
70
51
  const file = basename(String(h.command || '').trim());
71
52
  if (!file.endsWith('.js')) continue;
72
- // Grupos consolidados apontam para chain.js com os hooks reais em
73
- // `args` (base names, na ordem de execução) — registre cada elo, não o
74
- // runner: é o elo que o Inspector precisa mostrar. chain.js em si não é
75
- // um hook de domínio e ficaria como nó fantasma em todo evento.
53
+ // Grupos consolidados apontam para chain.js com os elos em `args`: registre
54
+ // cada elo, senão chain.js vira nó fantasma em todo evento.
76
55
  const files = file === 'chain.js' && Array.isArray(h.args) && h.args.length
77
56
  ? h.args.map(a => `${a}.js`)
78
57
  : [file];
@@ -80,8 +59,7 @@ export function hookRegistrations(definitions = getHookDefinitions()) {
80
59
  if (!byFile.has(f)) byFile.set(f, []);
81
60
  const entries = byFile.get(f);
82
61
  const matcher = def.matcher || null;
83
- // Several raw events share one Inspector column (PostToolUse and its
84
- // Failure/Batch siblings): one node per column, not one per event.
62
+ // PostToolUse and its Failure/Batch siblings share one Inspector column.
85
63
  if (entries.some((e) => e.event === key && e.matcher === matcher)) continue;
86
64
  entries.push({ event: key, rawEvent: def.event, matcher });
87
65
  }
@@ -111,7 +89,6 @@ export function scanHooks(frameworkDir) {
111
89
  // PostToolUse and UserPromptSubmit, and must show under both.
112
90
  const regs = registrations.get(filename);
113
91
  if (!regs || regs.length === 0) {
114
- // On disk but never registered — real dead weight, and worth seeing.
115
92
  return [{ ...base, event: 'unregistered', matcher: null }];
116
93
  }
117
94
  return regs.map(r => ({ ...base, event: r.event, rawEvent: r.rawEvent, matcher: r.matcher }));
@@ -6,10 +6,6 @@ import { scanStandards } from './standards-scanner.js';
6
6
  import { scanRules } from './rules-scanner.js';
7
7
  import { scanDocs } from './docs-scanner.js';
8
8
 
9
- /**
10
- * The framework's knowledge, for the static virtual module. The project's
11
- * state (features, activity) is served live by project-api.js instead.
12
- */
13
9
  export function scanAll(frameworkDir) {
14
10
  const agents = scanAgents(frameworkDir);
15
11
  const phases = scanPhases(frameworkDir);
@@ -1,17 +1,6 @@
1
1
  /**
2
- * Many projects in one dashboard. Each project keeps its own live API
3
- * (project-api.js: features, activity, files), created on first use; this
4
- * module lists the projects, routes `/p/:id/…` to the right one, and answers
5
- * which project a changed file belongs to.
6
- *
7
- * The list is the registry (src/lib/projects/registry.js) plus the project the
8
- * dashboard was launched from, which is always shown even if it cannot be
9
- * registered. Projects found next to the registered ones are offered, never
10
- * added on their own.
11
- *
12
- * A cold project can take seconds to read (a 100k-event log). The portfolio
13
- * never waits for one: `summary` is served only for projects already in
14
- * memory, and the rest are read in the background (`warmNext`).
2
+ * The portfolio never waits for a cold project (a 100k-event log takes seconds):
3
+ * `summary` is served only from memory, the rest is read by `warmNext`.
15
4
  */
16
5
 
17
6
  import { dirname, basename } from 'path';
@@ -25,18 +14,12 @@ function slug(text) {
25
14
 
26
15
  const lower = (p) => (process.platform === 'win32' ? p.toLowerCase() : p);
27
16
 
28
- /**
29
- * @param {{ launchDir: string, registry?: object, createApi?: Function }} opts
30
- * `registry` and `createApi` are test seams.
31
- */
32
17
  export function createPortfolio({ launchDir, registry = registryModule, createApi = createProjectApi }) {
33
18
  const launchKey = registry.projectKey(launchDir);
34
- const apis = new Map(); // dir key -> project api
35
- // The last summary served per project: while a changed project is read
36
- // again, the portfolio keeps showing it instead of going back to "reading".
37
- const lastSummary = new Map(); // dir key -> summary
19
+ const apis = new Map();
20
+ // While a changed project is read again, its last summary keeps showing.
21
+ const lastSummary = new Map();
38
22
 
39
- /** The projects shown, with stable ids derived from the folder name. */
40
23
  function list() {
41
24
  const dirs = registry.readRegistry().projects.map((p) => ({ dir: p.dir, name: p.name, source: p.source }));
42
25
  if (!dirs.some((p) => lower(p.dir) === lower(launchKey))) {
@@ -61,7 +44,6 @@ export function createPortfolio({ launchDir, registry = registryModule, createAp
61
44
  return list().find((p) => p.id === id) || null;
62
45
  }
63
46
 
64
- /** The project a changed path belongs to — the deepest registered folder containing it. */
65
47
  function ownerOf(path) {
66
48
  const p = lower(String(path).replace(/\\/g, '/'));
67
49
  let best = null;
@@ -78,7 +60,6 @@ export function createPortfolio({ launchDir, registry = registryModule, createAp
78
60
  return registry.discoverProjects(roots).filter((s) => !projects.some((p) => lower(p.dir) === lower(s.dir)));
79
61
  }
80
62
 
81
- /** Read the next project that is not in memory yet. @returns {object|null} the project warmed */
82
63
  function warmNext() {
83
64
  const next = list().find((p) => !apiFor(p.dir).ready());
84
65
  if (!next) return null;
@@ -106,11 +87,6 @@ export function createPortfolio({ launchDir, registry = registryModule, createAp
106
87
 
107
88
  const json = (status, body) => ({ status, body });
108
89
 
109
- /**
110
- * @param {string} method
111
- * @param {string} url - relative to `/api/morph`
112
- * @param {object|null} [body] - parsed JSON of a POST
113
- */
114
90
  function handle(method, url, body = null) {
115
91
  const u = new URL(url, 'http://local');
116
92
  const parts = u.pathname.split('/').filter(Boolean);
@@ -1,11 +1,6 @@
1
1
  /**
2
- * The live half of the dashboard: the project's features and activity, served
3
- * as JSON by the Vite dev server (`/api/morph/*`) and refreshed when `.morph/`
4
- * changes on disk. The framework knowledge stays in the static virtual module.
5
- *
6
- * Two caches with two lifetimes. A running agent appends to `events.jsonl` on
7
- * every tool call, so a log change drops only the event cache — rescanning the
8
- * features (git calls for the active ones) on each tool call would be waste.
2
+ * Two caches with two lifetimes: an agent appends to `events.jsonl` on every tool
3
+ * call, so a log change drops only the event cache, never the feature scan.
9
4
  */
10
5
 
11
6
  import { join } from 'path';
@@ -16,17 +11,9 @@ import { containerDirs } from '../../../framework/hooks/shared/worktree-root.js'
16
11
  import { isFinished } from './feature-status.js';
17
12
  import { resolveFeatureTree } from '../../lib/worktree/feature-truth.js';
18
13
 
19
- /** How long a remembered "which tree owns this feature" answer is trusted without a structural change. */
20
14
  const TREE_TTL_MS = 60_000;
21
15
 
22
- /**
23
- * Which cache a changed path invalidates, or null when it is none of ours.
24
- * `.morph/framework/` is the installed framework copy (often a junction), and
25
- * it never changes what this API returns.
26
- *
27
- * @param {string} path
28
- * @returns {'activity'|'features'|null}
29
- */
16
+ /** `.morph/framework/` is the installed framework copy and never changes what this API returns. */
30
17
  export function classifyChange(path) {
31
18
  const p = String(path).replace(/\\/g, '/');
32
19
  if (!p.includes('/.morph/') && !p.endsWith('/.morph')) return null;
@@ -36,19 +23,12 @@ export function classifyChange(path) {
36
23
  return null;
37
24
  }
38
25
 
39
- /**
40
- * A full-page reload Vite would send for a file under a project's `.morph/`
41
- * (an agent writing a design .html). The dashboard's own code never lives
42
- * there, so such a reload is never needed.
43
- */
26
+ /** Vite would full-reload for a `.html` under `.morph/`; the dashboard's code never lives there. */
44
27
  export function isMorphDataReload(path) {
45
28
  return typeof path === 'string' && path.replace(/\\/g, '/').includes('/.morph/');
46
29
  }
47
30
 
48
- /**
49
- * The directories worth watching: the root's and each worktree's feature,
50
- * archive, log and config folders — never a whole worktree (its node_modules).
51
- */
31
+ /** Never a whole worktree: its node_modules. */
52
32
  export function watchTargets(projectDir) {
53
33
  const morphDirs = [join(projectDir, '.morph')];
54
34
  for (const container of containerDirs(projectDir)) {
@@ -72,12 +52,6 @@ function json(status, body) {
72
52
  return { status, body };
73
53
  }
74
54
 
75
- /**
76
- * @param {string} projectDir
77
- * @param {{ scanFeatures?: Function, readFeatureDetail?: Function,
78
- * readFeatureFile?: Function, loadEvents?: Function, resolveTree?: Function,
79
- * now?: () => number }} [deps] - test seams
80
- */
81
55
  export function createProjectApi(projectDir, deps = {}) {
82
56
  const scan = deps.scanFeatures || scanFeatures;
83
57
  const detail = deps.readFeatureDetail || readFeatureDetail;
@@ -87,10 +61,8 @@ export function createProjectApi(projectDir, deps = {}) {
87
61
  : {});
88
62
  const cache = { features: null };
89
63
 
90
- // Which tree owns a feature costs a few git calls (~0.3 s each on Windows)
91
- // and only changes when a file or folder appears or disappears — never when
92
- // an agent rewrites feature.json or tasks.json, which is what happens all
93
- // the time. Remembered per feature; a structural change or TTL_MS forgets it.
64
+ // Which tree owns a feature costs git calls (~0.3 s each on Windows) and only
65
+ // changes on add/remove, never on a rewrite: remembered until a structural event or TTL_MS.
94
66
  const resolveTree = deps.resolveTree || resolveFeatureTree;
95
67
  const now = deps.now || Date.now;
96
68
  const trees = new Map();
@@ -104,17 +76,9 @@ export function createProjectApi(projectDir, deps = {}) {
104
76
 
105
77
  const features = () => (cache.features ||= scan(projectDir, { resolveTree: rememberedTree }));
106
78
  const events = () => store.events();
107
- // Where the list scan found each feature: opening one reuses it instead of
108
- // asking git again.
109
79
  const known = (name) => features().features?.find((f) => f.name === name) || undefined;
110
80
 
111
- /**
112
- * @param {'features'|'activity'|'all'} scope
113
- * @param {string} [path] - the file that changed; a live log is then tailed
114
- * instead of reloading every log
115
- * @param {string} [event] - the watcher event; anything but 'change' (a file
116
- * or folder added or removed) may move a feature to another tree
117
- */
81
+ /** Any `event` but 'change' may move a feature to another tree. */
118
82
  function invalidate(scope, path, event) {
119
83
  if (scope === 'features' || scope === 'all') cache.features = null;
120
84
  if (scope === 'all' || (scope === 'features' && event !== 'change')) trees.clear();
@@ -122,10 +86,6 @@ export function createProjectApi(projectDir, deps = {}) {
122
86
  else if (scope === 'activity') store.changed(path);
123
87
  }
124
88
 
125
- /**
126
- * @param {string} url - path + query, relative to `/api/morph`
127
- * @returns {{ status: number, body: object }}
128
- */
129
89
  function handle(url) {
130
90
  const u = new URL(url, 'http://local');
131
91
  const parts = u.pathname.split('/').filter(Boolean).map(decodeURIComponent);
@@ -159,20 +119,14 @@ export function createProjectApi(projectDir, deps = {}) {
159
119
  }
160
120
  }
161
121
 
162
- /** Load everything once, so the first page a person opens does not pay for it. */
163
122
  function warm() {
164
123
  try { features(); events(); } catch { /* the first request will report it */ }
165
124
  }
166
125
 
167
- /** Cheap to answer now: the list and the events are both in memory. */
168
126
  function ready() {
169
127
  return cache.features !== null && store.ready();
170
128
  }
171
129
 
172
- /**
173
- * What the portfolio shows of this project: counts, the active features with
174
- * their last activity, and when anything last happened.
175
- */
176
130
  function summary() {
177
131
  const scan = features();
178
132
  const last = lastActivityByFeature(events());
@@ -20,8 +20,7 @@ function parseFrontmatter(content) {
20
20
 
21
21
  const ROUTE_LINE = /^- \*\*([a-z0-9-]+)\*\* \(`([^`]+\.md)`\)/;
22
22
 
23
- // Bullet form: - **<id>** (`<path under standards/>`) — <digest>. An `@path`
24
- // import inside .claude/rules resolves relative to the rule file and never loads.
23
+ // An `@path` import inside .claude/rules resolves relative to the rule file and never loads.
25
24
  export function parseStandardRoutes(content) {
26
25
  const routes = [];
27
26
  for (const line of content.split(/\r?\n/)) {