@geraldmaron/construct 1.2.3 → 1.3.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 (504) hide show
  1. package/README.md +7 -10
  2. package/bin/construct +235 -209
  3. package/bin/construct-postinstall.mjs +0 -21
  4. package/commands/work/optimize-prompts.md +1 -1
  5. package/examples/distribution/sources/adr.md +2 -2
  6. package/examples/seed-observations/decisions.md +1 -1
  7. package/lib/artifact-loop-core.mjs +412 -0
  8. package/lib/artifact-manifest.mjs +6 -0
  9. package/lib/artifact-release-gate.mjs +115 -49
  10. package/lib/audit-rules.mjs +2 -4
  11. package/lib/audit-skills.mjs +32 -20
  12. package/lib/audit-specialists.mjs +44 -26
  13. package/lib/auto-docs.mjs +6 -5
  14. package/lib/brand-prose.mjs +3 -3
  15. package/lib/brand-tokens.mjs +2 -2
  16. package/lib/certification/artifact-fixtures.mjs +4 -1
  17. package/lib/certification/demo-parity.mjs +6 -32
  18. package/lib/certification/real-llm-scenarios.mjs +58 -13
  19. package/lib/certification/role-cards.mjs +7 -6
  20. package/lib/certification/role-overlays.mjs +4 -4
  21. package/lib/certification/runner.mjs +9 -2
  22. package/lib/certification/skill-inventory.mjs +34 -22
  23. package/lib/certification/skill-scenarios.mjs +21 -8
  24. package/lib/certification/specialist-contracts.mjs +4 -3
  25. package/lib/certification/specialist-scenarios.mjs +7 -6
  26. package/lib/certification/status.mjs +5 -4
  27. package/lib/cli-commands.mjs +55 -54
  28. package/lib/comment-lint.mjs +1 -5
  29. package/lib/completions.mjs +20 -2
  30. package/lib/config/legacy-config-migration.mjs +90 -24
  31. package/lib/config/schema.mjs +5 -5
  32. package/lib/config/source-targets.mjs +80 -0
  33. package/lib/contracts/validate.mjs +119 -51
  34. package/lib/decisions/golden.mjs +3 -2
  35. package/lib/decisions/registry.mjs +11 -6
  36. package/lib/demo-project.mjs +188 -0
  37. package/lib/demo-recording.mjs +9 -34
  38. package/lib/demo-script.mjs +2 -2
  39. package/lib/demo-surface.mjs +13 -131
  40. package/lib/demo-tour-renderer.mjs +92 -0
  41. package/lib/demo.mjs +69 -54
  42. package/lib/diagram-export.mjs +63 -14
  43. package/lib/doctor/source-checkout.mjs +3 -4
  44. package/lib/doctor/watchers/consistency.mjs +90 -22
  45. package/lib/doctor/watchers/credential-parity.mjs +97 -0
  46. package/lib/doctor/watchers/mcp-protocol.mjs +1 -1
  47. package/lib/doctor/watchers/service-health.mjs +4 -32
  48. package/lib/document-export.mjs +73 -9
  49. package/lib/document-extract.mjs +10 -198
  50. package/lib/embed/artifact.mjs +2 -3
  51. package/lib/embed/cli.mjs +1 -1
  52. package/lib/embed/config.mjs +1 -2
  53. package/lib/embed/customer-profiles.mjs +1 -1
  54. package/lib/embed/daemon.mjs +5 -6
  55. package/lib/embed/demand-fetch.mjs +114 -2
  56. package/lib/embed/docs-lifecycle.mjs +9 -5
  57. package/lib/embed/intake-metrics.mjs +2 -2
  58. package/lib/embed/notifications.mjs +2 -3
  59. package/lib/embed/recommendation-store.mjs +25 -22
  60. package/lib/embed/role-framing.mjs +4 -9
  61. package/lib/embedded-contract/capability.mjs +4 -2
  62. package/lib/embedded-contract/contract-version.mjs +1 -1
  63. package/lib/env-config.mjs +2 -2
  64. package/lib/flavors/loader.mjs +21 -19
  65. package/lib/graph/build-from-registry.mjs +31 -7
  66. package/lib/graph/staleness.mjs +1 -1
  67. package/lib/headhunt.mjs +82 -35
  68. package/lib/hooks/agent-tracker.mjs +2 -2
  69. package/lib/hooks/config-protection.mjs +1 -1
  70. package/lib/hooks/registry-sync.mjs +3 -3
  71. package/lib/host-capabilities.mjs +5 -5
  72. package/lib/improvement/controller.mjs +2 -2
  73. package/lib/init-unified.mjs +19 -21
  74. package/lib/intake/classify.mjs +26 -0
  75. package/lib/intake/daemon.mjs +2 -3
  76. package/lib/intake/prepare.mjs +10 -3
  77. package/lib/intake/session-prelude.mjs +1 -1
  78. package/lib/intake/tables/creative.mjs +9 -9
  79. package/lib/intake/tables/operations.mjs +4 -4
  80. package/lib/intake/tables/research.mjs +2 -2
  81. package/lib/integrations/intake-integrations.mjs +9 -10
  82. package/lib/knowledge/research-store.mjs +2 -2
  83. package/lib/mcp/server.mjs +104 -61
  84. package/lib/mcp/tool-budget.mjs +1 -46
  85. package/lib/mcp/tool-recovery.mjs +44 -0
  86. package/lib/mcp/tools/artifact-author.mjs +36 -0
  87. package/lib/mcp/tools/find-tool.mjs +86 -0
  88. package/lib/mcp/tools/orchestration-run.mjs +107 -69
  89. package/lib/mcp/tools/project.mjs +4 -3
  90. package/lib/mcp/tools/{profile.mjs → scope.mjs} +54 -52
  91. package/lib/mcp/tools/skills.mjs +39 -22
  92. package/lib/mcp/tools/telemetry.mjs +2 -1
  93. package/lib/mcp/tools/workflow.mjs +1 -1
  94. package/lib/mcp-platform-config.mjs +7 -5
  95. package/lib/migrations/index.mjs +4 -2
  96. package/lib/migrations/v2-unified-registry.mjs +35 -0
  97. package/lib/model-router.mjs +203 -36
  98. package/lib/models/catalog.mjs +25 -9
  99. package/lib/models/execution-capability-profile.mjs +3 -3
  100. package/lib/models/execution-policy.mjs +7 -6
  101. package/lib/models/provider-poll.mjs +18 -4
  102. package/lib/opencode-config.mjs +56 -1
  103. package/lib/opencode-runtime-plugin.mjs +13 -10
  104. package/lib/oracle/artifact-gate.mjs +14 -4
  105. package/lib/oracle/cli.mjs +2 -0
  106. package/lib/oracle/dispatch.mjs +18 -1
  107. package/lib/oracle/execute.mjs +39 -7
  108. package/lib/oracle/index.mjs +1 -1
  109. package/lib/oracle/org-graph.mjs +10 -5
  110. package/lib/oracle/policy.mjs +6 -0
  111. package/lib/oracle/read-model.mjs +103 -6
  112. package/lib/oracle/reconcile.mjs +31 -4
  113. package/lib/oracle/remediation-dispatch.mjs +53 -0
  114. package/lib/oracle/routing.mjs +5 -0
  115. package/lib/oracle/synthesize.mjs +80 -3
  116. package/lib/orchestration/routing-tables.mjs +44 -10
  117. package/lib/orchestration/runtime.mjs +2 -0
  118. package/lib/orchestration/worker.mjs +1 -1
  119. package/lib/orchestration-policy.mjs +351 -62
  120. package/lib/overrides/resolver.mjs +1 -12
  121. package/lib/parity.mjs +37 -11
  122. package/lib/policy/engine.mjs +80 -15
  123. package/lib/prompt-composer.js +51 -15
  124. package/lib/prompt-metadata.mjs +3 -2
  125. package/lib/providers/connection-probe.mjs +58 -0
  126. package/lib/providers/copilot-auth.mjs +1 -1
  127. package/lib/providers/credential-bootstrap.mjs +1 -1
  128. package/lib/providers/op-run.mjs +3 -4
  129. package/lib/providers/secret-resolver.mjs +31 -0
  130. package/lib/publish-tooling.mjs +3 -11
  131. package/lib/publish.mjs +12 -29
  132. package/lib/reconcile/mcp-entry-reconcile.mjs +14 -9
  133. package/lib/reflect.mjs +2 -2
  134. package/lib/registry/agent-manifest.mjs +190 -0
  135. package/lib/registry/assemble.mjs +133 -0
  136. package/lib/registry/catalog.mjs +171 -0
  137. package/lib/registry/cli.mjs +590 -6
  138. package/lib/registry/consolidation.mjs +5 -4
  139. package/lib/registry/docs-sync.mjs +67 -0
  140. package/lib/registry/loader.mjs +147 -0
  141. package/lib/registry/org-io.mjs +76 -0
  142. package/lib/registry/retired-paths.mjs +71 -0
  143. package/lib/registry/surface-map.mjs +5 -7
  144. package/lib/registry/validator.mjs +438 -0
  145. package/lib/research-execution-policy.mjs +173 -0
  146. package/lib/roles/catalog.mjs +4 -9
  147. package/lib/roles/fence.mjs +107 -1
  148. package/lib/roles/flavor-bindings.mjs +68 -0
  149. package/lib/roles/gateway.mjs +140 -29
  150. package/lib/roles/manifest.mjs +22 -13
  151. package/lib/roles/router.mjs +1 -1
  152. package/lib/scopes/enrich.mjs +67 -0
  153. package/lib/scopes/hooks.mjs +12 -0
  154. package/lib/{profiles → scopes}/lifecycle.mjs +70 -70
  155. package/lib/scopes/loader.mjs +104 -0
  156. package/lib/scopes/rebrand.mjs +32 -0
  157. package/lib/scopes/research-profile.mjs +16 -0
  158. package/lib/scopes/teams.mjs +98 -0
  159. package/lib/service-manager.mjs +1 -117
  160. package/lib/setup-prompts.mjs +1 -1
  161. package/lib/setup.mjs +6 -26
  162. package/lib/skills/composition-graph.mjs +98 -0
  163. package/lib/skills/router.mjs +80 -0
  164. package/lib/specialist-contracts.mjs +17 -18
  165. package/lib/specialists/postconditions.mjs +2 -2
  166. package/lib/specialists/prompt-schema.mjs +6 -5
  167. package/lib/specialists/roster.mjs +7 -12
  168. package/lib/specialists/schema.mjs +9 -6
  169. package/lib/status.mjs +25 -90
  170. package/lib/storage/file-lock.mjs +6 -3
  171. package/lib/telemetry/skill-calls.mjs +1 -1
  172. package/lib/templates/doc-presentation.mjs +63 -0
  173. package/lib/templates/visual-requirements.mjs +35 -2
  174. package/lib/term-format.mjs +107 -2
  175. package/lib/test-env-setup.mjs +21 -0
  176. package/lib/ui/components.mjs +42 -0
  177. package/lib/ui/glyphs.mjs +58 -0
  178. package/lib/ui/links.mjs +115 -0
  179. package/lib/ui/theme.mjs +108 -0
  180. package/lib/uninstall/uninstall.mjs +2 -1
  181. package/lib/validator.mjs +45 -8
  182. package/lib/validators/skill-effectiveness.mjs +174 -0
  183. package/lib/validators/skills.mjs +1 -1
  184. package/package.json +12 -8
  185. package/personas/construct.md +12 -3
  186. package/platforms/claude/CLAUDE.md +1 -1
  187. package/rules/common/beads-hygiene.md +52 -0
  188. package/rules/common/neurodivergent-output.md +4 -7
  189. package/rules/common/research.md +2 -0
  190. package/scripts/sync-specialists.mjs +260 -49
  191. package/skills/ai/prompt-optimizer.md +3 -3
  192. package/skills/brand/output-vibe.md +48 -0
  193. package/skills/docs/adr-workflow.md +3 -0
  194. package/skills/docs/backlog-proposal-workflow.md +3 -0
  195. package/skills/docs/codebase-research-workflow.md +5 -2
  196. package/skills/docs/customer-profile-workflow.md +3 -0
  197. package/skills/docs/document-ingest-workflow.md +3 -0
  198. package/skills/docs/evidence-ingest-workflow.md +4 -1
  199. package/skills/docs/init-project.md +1 -1
  200. package/skills/docs/prd-workflow.md +1 -1
  201. package/skills/docs/prfaq-workflow.md +3 -0
  202. package/skills/docs/product-intelligence-workflow.md +4 -1
  203. package/skills/docs/product-signal-workflow.md +3 -0
  204. package/skills/docs/research-workflow.md +13 -6
  205. package/skills/docs/runbook-workflow.md +3 -0
  206. package/skills/docs/strategy-workflow.md +3 -0
  207. package/skills/docs/user-research-workflow.md +6 -3
  208. package/skills/operating/orchestration-reference.md +1 -1
  209. package/skills/roles/{engineer.ai.md → ai-engineer.md} +2 -2
  210. package/skills/roles/architect.ai-systems.md +1 -1
  211. package/skills/roles/architect.data.md +1 -1
  212. package/skills/roles/architect.enterprise.md +1 -1
  213. package/skills/roles/architect.integration.md +1 -1
  214. package/skills/roles/architect.md +1 -1
  215. package/skills/roles/architect.platform.md +1 -1
  216. package/skills/roles/{product-manager.business-strategy.md → business-strategist.md} +3 -3
  217. package/skills/roles/data-analyst.experiment.md +1 -1
  218. package/skills/roles/data-analyst.md +1 -1
  219. package/skills/roles/data-analyst.product-intelligence.md +1 -1
  220. package/skills/roles/data-analyst.product.md +1 -1
  221. package/skills/roles/data-analyst.telemetry.md +1 -1
  222. package/skills/roles/{engineer.data.md → data-engineer.md} +2 -2
  223. package/skills/roles/data-engineer.pipeline.md +2 -2
  224. package/skills/roles/data-engineer.vector-retrieval.md +2 -2
  225. package/skills/roles/data-engineer.warehouse.md +2 -2
  226. package/skills/roles/debugger.md +1 -1
  227. package/skills/roles/designer.accessibility.md +1 -1
  228. package/skills/roles/designer.md +1 -1
  229. package/skills/roles/{reviewer.devil-advocate.md → devil-advocate.md} +2 -2
  230. package/skills/roles/{operator.docs.md → docs-keeper.md} +3 -3
  231. package/skills/roles/engineer.md +3 -7
  232. package/skills/roles/{reviewer.evaluator.md → evaluator.md} +2 -2
  233. package/skills/roles/{researcher.explorer.md → explorer.md} +2 -2
  234. package/skills/roles/{operator.md → operations.md} +2 -5
  235. package/skills/roles/orchestrator.md +1 -1
  236. package/skills/roles/{engineer.platform.md → platform-engineer.md} +2 -2
  237. package/skills/roles/product-manager.ai-product.md +1 -1
  238. package/skills/roles/product-manager.enterprise.md +1 -1
  239. package/skills/roles/product-manager.growth.md +1 -2
  240. package/skills/roles/product-manager.md +1 -2
  241. package/skills/roles/product-manager.platform.md +1 -1
  242. package/skills/roles/product-manager.product.md +1 -1
  243. package/skills/roles/qa.ai-eval.md +1 -1
  244. package/skills/roles/qa.api-contract.md +1 -1
  245. package/skills/roles/qa.data-pipeline.md +1 -1
  246. package/skills/roles/qa.md +1 -1
  247. package/skills/roles/qa.web-ui.md +1 -1
  248. package/skills/roles/{operator.release.md → release-manager.md} +3 -3
  249. package/skills/roles/researcher.md +1 -1
  250. package/skills/roles/reviewer.md +1 -1
  251. package/skills/roles/security.ai.md +1 -1
  252. package/skills/roles/security.appsec.md +1 -1
  253. package/skills/roles/security.cloud.md +1 -1
  254. package/skills/roles/security.legal-compliance.md +1 -1
  255. package/skills/roles/security.md +1 -1
  256. package/skills/roles/security.privacy.md +1 -1
  257. package/skills/roles/security.supply-chain.md +1 -1
  258. package/skills/roles/{operator.sre.md → sre.md} +3 -3
  259. package/skills/roles/{qa.test-automation.md → test-automation.md} +2 -2
  260. package/skills/roles/{reviewer.trace.md → trace-reviewer.md} +2 -2
  261. package/skills/roles/{researcher.ux.md → ux-researcher.md} +2 -2
  262. package/skills/routing.json +18 -0
  263. package/skills/routing.md +1 -1
  264. package/specialists/artifact-manifest.json +2 -1
  265. package/specialists/audit-enrichments.json +14 -14
  266. package/specialists/org/contracts/accessibility-to-qa.json +37 -0
  267. package/specialists/org/contracts/any-to-business-strategist.json +57 -0
  268. package/specialists/org/contracts/any-to-debugger.json +38 -0
  269. package/specialists/org/contracts/any-to-designer.json +33 -0
  270. package/specialists/org/contracts/any-to-docs-keeper.json +34 -0
  271. package/specialists/org/contracts/any-to-explorer.json +39 -0
  272. package/specialists/org/contracts/any-to-sre-incident.json +33 -0
  273. package/specialists/org/contracts/any-to-trace-reviewer.json +32 -0
  274. package/specialists/org/contracts/architect-to-ai-engineer.json +32 -0
  275. package/specialists/org/contracts/architect-to-data-engineer.json +52 -0
  276. package/specialists/org/contracts/architect-to-devil-advocate.json +38 -0
  277. package/specialists/org/contracts/architect-to-engineer.json +34 -0
  278. package/specialists/org/contracts/architect-to-evaluator.json +59 -0
  279. package/specialists/org/contracts/architect-to-legal-compliance.json +55 -0
  280. package/specialists/org/contracts/architect-to-operations.json +45 -0
  281. package/specialists/org/contracts/architect-to-platform-engineer.json +42 -0
  282. package/specialists/org/contracts/business-strategist-to-product-manager.json +39 -0
  283. package/specialists/org/contracts/construct-to-orchestrator.json +36 -0
  284. package/specialists/org/contracts/construct-to-rd-lead.json +53 -0
  285. package/specialists/org/contracts/data-analyst-to-product-manager.json +43 -0
  286. package/specialists/org/contracts/data-engineer-to-platform-engineer.json +36 -0
  287. package/specialists/org/contracts/designer-to-accessibility.json +36 -0
  288. package/specialists/org/contracts/engineer-to-qa.json +34 -0
  289. package/specialists/org/contracts/engineer-to-reviewer.json +52 -0
  290. package/specialists/org/contracts/explorer-to-engineer.json +38 -0
  291. package/specialists/org/contracts/legal-compliance-to-release-manager.json +43 -0
  292. package/specialists/org/contracts/platform-engineer-to-engineer.json +31 -0
  293. package/specialists/org/contracts/product-manager-to-architect.json +71 -0
  294. package/specialists/org/contracts/product-manager-to-data-analyst.json +49 -0
  295. package/specialists/org/contracts/product-manager-to-ux-researcher.json +51 -0
  296. package/specialists/org/contracts/qa-to-release-manager.json +42 -0
  297. package/specialists/org/contracts/qa-to-test-automation.json +42 -0
  298. package/specialists/org/contracts/rd-lead-to-architect.json +38 -0
  299. package/specialists/org/contracts/researcher-to-architect.json +40 -0
  300. package/specialists/org/contracts/researcher-to-product-manager.json +57 -0
  301. package/specialists/org/contracts/reviewer-to-security.json +41 -0
  302. package/specialists/org/contracts/sre-to-release-manager.json +36 -0
  303. package/specialists/org/contracts/test-automation-to-engineer.json +34 -0
  304. package/specialists/org/contracts/trace-reviewer-to-sre.json +41 -0
  305. package/specialists/org/contracts/user-to-construct.json +30 -0
  306. package/specialists/org/groups/engineering-group.json +43 -0
  307. package/specialists/org/groups/governance-group.json +38 -0
  308. package/specialists/org/groups/operations-group.json +41 -0
  309. package/specialists/org/groups/product-group.json +46 -0
  310. package/specialists/org/groups/quality-group.json +42 -0
  311. package/specialists/org/groups/strategy-group.json +41 -0
  312. package/specialists/org/policies/action-approval.json +13 -0
  313. package/specialists/org/policies/agents-routing.json +13 -0
  314. package/specialists/org/policies/architecture.json +20 -0
  315. package/specialists/org/policies/bash-safety.json +13 -0
  316. package/specialists/org/policies/beads-hygiene.json +13 -0
  317. package/specialists/org/policies/bootstrap-state.json +13 -0
  318. package/specialists/org/policies/code-review.json +13 -0
  319. package/specialists/org/policies/coding-style.json +13 -0
  320. package/specialists/org/policies/comments.json +13 -0
  321. package/specialists/org/policies/commit-approval.json +13 -0
  322. package/specialists/org/policies/contract-preconditions.json +13 -0
  323. package/specialists/org/policies/deployment.json +20 -0
  324. package/specialists/org/policies/description.json +14 -0
  325. package/specialists/org/policies/design-approval.json +19 -0
  326. package/specialists/org/policies/doc-ownership.json +13 -0
  327. package/specialists/org/policies/file-path-fence.json +13 -0
  328. package/specialists/org/policies/framing.json +13 -0
  329. package/specialists/org/policies/git-workflow.json +13 -0
  330. package/specialists/org/policies/incident-response.json +15 -0
  331. package/specialists/org/policies/intake-triage.json +15 -0
  332. package/specialists/org/policies/neurodivergent-output.json +13 -0
  333. package/specialists/org/policies/no-fabrication.json +13 -0
  334. package/specialists/org/policies/patterns.json +13 -0
  335. package/specialists/org/policies/quality-gate-approval.json +17 -0
  336. package/specialists/org/policies/release-gates.json +13 -0
  337. package/specialists/org/policies/research-evidence.json +13 -0
  338. package/specialists/org/policies/review-before-change.json +13 -0
  339. package/specialists/org/policies/rollback.json +15 -0
  340. package/specialists/org/policies/scope-change.json +20 -0
  341. package/specialists/org/policies/secret-scan.json +13 -0
  342. package/specialists/org/policies/security-approval.json +17 -0
  343. package/specialists/org/policies/security.json +13 -0
  344. package/specialists/org/policies/session-efficiency.json +13 -0
  345. package/specialists/org/policies/skill-routing.json +13 -0
  346. package/specialists/org/policies/strategic-prioritization.json +17 -0
  347. package/specialists/org/policies/testing.json +13 -0
  348. package/specialists/org/policies/tool-invisibility.json +14 -0
  349. package/specialists/org/scopes/creative.json +54 -0
  350. package/specialists/org/scopes/operations.json +51 -0
  351. package/specialists/org/scopes/research.json +60 -0
  352. package/specialists/org/scopes/rnd.json +81 -0
  353. package/specialists/org/specialists/cx-accessibility.json +69 -0
  354. package/specialists/org/specialists/cx-ai-engineer.json +75 -0
  355. package/specialists/org/specialists/cx-architect.json +89 -0
  356. package/specialists/org/specialists/cx-business-strategist.json +72 -0
  357. package/specialists/org/specialists/cx-data-analyst.json +68 -0
  358. package/specialists/org/specialists/cx-data-engineer.json +71 -0
  359. package/specialists/org/specialists/cx-debugger.json +75 -0
  360. package/specialists/org/specialists/cx-designer.json +85 -0
  361. package/specialists/org/specialists/cx-devil-advocate.json +71 -0
  362. package/specialists/org/specialists/cx-docs-keeper.json +82 -0
  363. package/specialists/org/specialists/cx-engineer.json +106 -0
  364. package/specialists/org/specialists/cx-evaluator.json +69 -0
  365. package/specialists/org/specialists/cx-explorer.json +69 -0
  366. package/specialists/org/specialists/cx-legal-compliance.json +74 -0
  367. package/specialists/org/specialists/cx-operations.json +72 -0
  368. package/specialists/org/specialists/cx-oracle.json +46 -0
  369. package/specialists/org/specialists/cx-orchestrator.json +81 -0
  370. package/specialists/org/specialists/cx-platform-engineer.json +80 -0
  371. package/specialists/org/specialists/cx-product-manager.json +102 -0
  372. package/specialists/org/specialists/cx-qa.json +79 -0
  373. package/specialists/org/specialists/cx-rd-lead.json +73 -0
  374. package/specialists/org/specialists/cx-release-manager.json +77 -0
  375. package/specialists/org/specialists/cx-researcher.json +82 -0
  376. package/specialists/org/specialists/cx-reviewer.json +71 -0
  377. package/specialists/org/specialists/cx-security.json +91 -0
  378. package/specialists/org/specialists/cx-sre.json +94 -0
  379. package/specialists/org/specialists/cx-test-automation.json +69 -0
  380. package/specialists/org/specialists/cx-trace-reviewer.json +69 -0
  381. package/specialists/org/specialists/cx-ux-researcher.json +68 -0
  382. package/specialists/org/teams/accessibility-team.json +39 -0
  383. package/specialists/org/teams/design-team.json +40 -0
  384. package/specialists/org/teams/engineering-team.json +50 -0
  385. package/specialists/org/teams/governance-team.json +41 -0
  386. package/specialists/org/teams/operations-team.json +46 -0
  387. package/specialists/org/teams/product-management-team.json +45 -0
  388. package/specialists/org/teams/quality-team.json +49 -0
  389. package/specialists/org/teams/research-team.json +39 -0
  390. package/specialists/org/teams/strategy-team.json +48 -0
  391. package/specialists/org/teams/ux-research-team.json +39 -0
  392. package/specialists/prompts/_shared/validation-contract.md +1 -1
  393. package/specialists/prompts/_team-template.md +10 -0
  394. package/specialists/prompts/cx-accessibility.md +1 -0
  395. package/specialists/prompts/cx-ai-engineer.md +1 -1
  396. package/specialists/prompts/cx-business-strategist.md +1 -1
  397. package/specialists/prompts/cx-data-engineer.md +1 -1
  398. package/specialists/prompts/cx-designer.md +1 -0
  399. package/specialists/prompts/cx-devil-advocate.md +1 -1
  400. package/specialists/prompts/cx-docs-keeper.md +1 -1
  401. package/specialists/prompts/cx-evaluator.md +1 -1
  402. package/specialists/prompts/cx-explorer.md +1 -1
  403. package/specialists/prompts/cx-operations.md +1 -1
  404. package/specialists/prompts/cx-oracle.md +7 -3
  405. package/specialists/prompts/cx-orchestrator.md +17 -1
  406. package/specialists/prompts/cx-platform-engineer.md +1 -1
  407. package/specialists/prompts/cx-product-manager.md +2 -0
  408. package/specialists/prompts/cx-release-manager.md +1 -1
  409. package/specialists/prompts/cx-researcher.md +7 -4
  410. package/specialists/prompts/cx-sre.md +1 -1
  411. package/specialists/prompts/cx-test-automation.md +2 -2
  412. package/specialists/prompts/cx-trace-reviewer.md +3 -3
  413. package/specialists/prompts/cx-ux-researcher.md +2 -1
  414. package/templates/demos/scripts/architecture-review-adr.json +30 -0
  415. package/templates/demos/scripts/capability-contract.json +25 -0
  416. package/templates/demos/scripts/intake-triage.json +25 -0
  417. package/templates/demos/scripts/profile-doctor-health.json +25 -0
  418. package/templates/demos/tapes/agentic-platforms-prd.tape +2 -2
  419. package/templates/demos/tapes/architecture-review-adr.tape +44 -0
  420. package/templates/demos/tapes/capability-contract.tape +39 -0
  421. package/templates/demos/tapes/intake-triage.tape +39 -0
  422. package/templates/demos/tapes/profile-doctor-health.tape +39 -0
  423. package/templates/distribution/construct-brand.typ +23 -18
  424. package/templates/distribution/construct-decision.typ +1 -1
  425. package/templates/distribution/construct-pdf.typ +1 -1
  426. package/templates/distribution/construct-prd.typ +1 -1
  427. package/templates/distribution/construct-research.typ +1 -1
  428. package/templates/distribution/mermaid-puppeteer.json +8 -0
  429. package/templates/docs/persona-artifact.md +1 -1
  430. package/templates/docs/prd.md +6 -0
  431. package/apps/chat/engine/ai-sdk-agent.mjs +0 -183
  432. package/apps/chat/engine/loop-driver.mjs +0 -211
  433. package/apps/chat/engine/models.mjs +0 -122
  434. package/apps/chat/engine/provider-adapters.mjs +0 -171
  435. package/apps/chat/engine/tools/permission.mjs +0 -54
  436. package/apps/chat/engine/tools/primitives.mjs +0 -180
  437. package/apps/chat/engine/tools/registry.mjs +0 -122
  438. package/apps/chat/engine/turn-controls.mjs +0 -70
  439. package/lib/boundary.mjs +0 -127
  440. package/lib/certification/dashboard-api.mjs +0 -71
  441. package/lib/chat/cli.mjs +0 -333
  442. package/lib/chat/command-suggest.mjs +0 -161
  443. package/lib/chat/commands.mjs +0 -215
  444. package/lib/chat/config.mjs +0 -142
  445. package/lib/chat/context-compactor.mjs +0 -250
  446. package/lib/chat/context-continuation.mjs +0 -253
  447. package/lib/chat/continuation-source.mjs +0 -58
  448. package/lib/chat/demo-guide.mjs +0 -61
  449. package/lib/chat/design-tokens.mjs +0 -91
  450. package/lib/chat/desktop-binary.mjs +0 -81
  451. package/lib/chat/desktop-build.mjs +0 -130
  452. package/lib/chat/desktop-launcher.mjs +0 -133
  453. package/lib/chat/evidence.mjs +0 -145
  454. package/lib/chat/export.mjs +0 -74
  455. package/lib/chat/harness/driver.mjs +0 -91
  456. package/lib/chat/list-picker.mjs +0 -112
  457. package/lib/chat/model-picker.mjs +0 -356
  458. package/lib/chat/openrouter-fallback.mjs +0 -151
  459. package/lib/chat/permission-prompt.mjs +0 -33
  460. package/lib/chat/picker-catalog.mjs +0 -45
  461. package/lib/chat/policy-telemetry.mjs +0 -34
  462. package/lib/chat/present.mjs +0 -246
  463. package/lib/chat/session-context.mjs +0 -39
  464. package/lib/chat/session-persist.mjs +0 -73
  465. package/lib/chat/session-restore.mjs +0 -71
  466. package/lib/chat/session-settings.mjs +0 -53
  467. package/lib/chat/system-prompt.mjs +0 -52
  468. package/lib/chat/transparency.mjs +0 -93
  469. package/lib/chat/tui/color-scheme.mjs +0 -42
  470. package/lib/chat/tui/markdown.mjs +0 -123
  471. package/lib/chat/tui/presentation.mjs +0 -100
  472. package/lib/chat/tui/render.mjs +0 -500
  473. package/lib/chat/tui/turn-block.mjs +0 -284
  474. package/lib/chat/tui/turn-present.mjs +0 -18
  475. package/lib/chat/tui/turn-state.mjs +0 -88
  476. package/lib/chat/tui/usage.mjs +0 -122
  477. package/lib/chat/web-commands.mjs +0 -146
  478. package/lib/chat/web-launcher.mjs +0 -63
  479. package/lib/chat/web-picker-keys.mjs +0 -46
  480. package/lib/chat/web-session.mjs +0 -159
  481. package/lib/config/alias.mjs +0 -57
  482. package/lib/dashboard-demo.mjs +0 -71
  483. package/lib/dashboard-static.mjs +0 -175
  484. package/lib/install/desktop-binary-download.mjs +0 -88
  485. package/lib/profiles/loader.mjs +0 -123
  486. package/lib/profiles/rebrand.mjs +0 -46
  487. package/lib/server/auth.mjs +0 -169
  488. package/lib/server/chat-loop.mjs +0 -622
  489. package/lib/server/chat.mjs +0 -336
  490. package/lib/server/cors.mjs +0 -77
  491. package/lib/server/csrf.mjs +0 -103
  492. package/lib/server/demo-preview.mjs +0 -63
  493. package/lib/server/index.mjs +0 -2641
  494. package/lib/server/insights.mjs +0 -780
  495. package/lib/server/langfuse-login.mjs +0 -58
  496. package/lib/server/rate-limit.mjs +0 -93
  497. package/lib/server/telemetry-login.mjs +0 -100
  498. package/lib/server/webhook.mjs +0 -512
  499. package/specialists/contracts.json +0 -1032
  500. package/specialists/contracts.schema.json +0 -83
  501. package/specialists/policy-inventory.json +0 -188
  502. package/specialists/registry.json +0 -1412
  503. package/specialists/role-manifests.json +0 -217
  504. package/specialists/teams.json +0 -94
package/lib/status.mjs CHANGED
@@ -1,11 +1,12 @@
1
1
  /**
2
- * lib/status.mjs — Project health summary for the Construct dashboard and CLI.
2
+ * lib/status.mjs — Project health summary for the Construct CLI.
3
3
  *
4
4
  * Reads workflow state, plan.md, tracker config, and MCP surface to produce a
5
5
  * structured health object. Called by `construct status`, the MCP status tool,
6
6
  * and the session-start bootstrap to surface blocked tasks and open questions.
7
7
  */
8
8
  import { readFileSync, statSync, existsSync, readdirSync } from 'node:fs';
9
+ import { loadRegistry } from './registry/loader.mjs';
9
10
  import { join, dirname } from 'node:path';
10
11
  import { homedir } from 'node:os';
11
12
  import { fileURLToPath } from 'node:url';
@@ -23,6 +24,8 @@ import { readEfficiencyLog, summarizeEfficiencyData } from './efficiency.mjs';
23
24
  import { triggerAutoBackfillIfSparse } from './telemetry/backfill.mjs';
24
25
  import { resolveTraceBackend, telemetryProviderLabel } from './telemetry/client.mjs';
25
26
  import { doctorRoot } from './config/xdg.mjs';
27
+ import { resolveUiColors } from './ui/theme.mjs';
28
+ import { applyLinks, terminalLinksEnabled } from './ui/links.mjs';
26
29
  const TOTAL_BYTES_WARNING_THRESHOLD = 750_000;
27
30
 
28
31
  function readJSON(path) {
@@ -250,9 +253,9 @@ function parsePort(value, fallback) {
250
253
  }
251
254
 
252
255
  async function probeHttp(url, { method = 'GET', headers, body, timeout = 5000 } = {}) {
253
- // 5s is generous on purpose. The dashboard runs many concurrent requests when
254
- // a page first loads (status + insights + registry + auth + …), which can stall
255
- // its own event loop just long enough that a 2s timeout fires on a healthy
256
+ // 5s is generous on purpose. A local service can batch many concurrent requests
257
+ // at startup, which can stall its own event loop just long enough that a 2s
258
+ // timeout fires on a healthy
256
259
  // localhost service. Reachability is a yes/no signal — a hung remote dies
257
260
  // visibly via ECONNREFUSED long before this, so the extra headroom is free.
258
261
  const controller = new AbortController();
@@ -539,89 +542,11 @@ function traceBackendDefinition(env) {
539
542
  };
540
543
  }
541
544
 
542
- /**
543
- * In-process self-check for when Construct is serving its own dashboard.
544
- * Rather than skipping the probe and asserting "healthy", we actually measure
545
- * the process state and surface real problems.
546
- *
547
- * Checks:
548
- * - Heap usage — warn at 80%, critical at 95%
549
- * - Process uptime — surface as context
550
- * - Embed daemon — read state file and report if stale or crashed
551
- *
552
- * Returns { status: 'healthy'|'degraded'|'unavailable', message: string }
553
- */
554
- function buildSelfCheck() {
555
- const problems = [];
556
- const notes = [];
557
-
558
- // Memory
559
- try {
560
- const mem = process.memoryUsage();
561
- const heapPct = mem.heapUsed / mem.heapTotal;
562
- if (heapPct >= 0.95) {
563
- problems.push(`heap ${Math.round(heapPct * 100)}% (critical)`);
564
- } else if (heapPct >= 0.80) {
565
- problems.push(`heap ${Math.round(heapPct * 100)}% (high)`);
566
- } else {
567
- notes.push(`heap ${Math.round(heapPct * 100)}%`);
568
- }
569
- } catch { /* non-fatal */ }
570
-
571
- // Uptime
572
- try {
573
- const uptimeSec = process.uptime();
574
- const h = Math.floor(uptimeSec / 3600);
575
- const m = Math.floor((uptimeSec % 3600) / 60);
576
- notes.push(`up ${h}h${m}m`);
577
- } catch { /* non-fatal */ }
578
-
579
- // Embed daemon state
580
- try {
581
- const stateFile = join(doctorRoot(), 'runtime', 'embed-daemon.json');
582
- if (existsSync(stateFile)) {
583
- const state = JSON.parse(readFileSync(stateFile, 'utf8'));
584
- if (state.status === 'running' && state.pid) {
585
- // Verify the PID is actually alive
586
- try {
587
- process.kill(state.pid, 0);
588
- notes.push(`embed daemon pid ${state.pid}`);
589
- } catch {
590
- problems.push(`embed daemon pid ${state.pid} is not running (state file stale)`);
591
- }
592
- } else if (state.status === 'stopped') {
593
- notes.push('embed daemon stopped');
594
- }
595
- }
596
- } catch { /* non-fatal */ }
597
-
598
- if (problems.length) {
599
- return {
600
- status: problems.some(p => p.includes('critical')) ? 'unavailable' : 'degraded',
601
- message: problems.join('; ') + (notes.length ? ` — ${notes.join(', ')}` : ''),
602
- };
603
- }
604
-
605
- return {
606
- status: 'healthy',
607
- message: ['Serving status API', ...notes].join(' — '),
608
- };
609
- }
610
-
611
- function serviceDefinitions(env, dashboardPort, selfDashboard) {
545
+ function serviceDefinitions(env) {
612
546
  const memoryPort = parsePort(env.MEMORY_PORT, 8765);
613
547
  const bridgePort = parsePort(env.BRIDGE_PORT, 5173);
614
548
 
615
549
  return [
616
- {
617
- id: 'dashboard',
618
- name: 'Dashboard',
619
- url: `http://127.0.0.1:${dashboardPort}`,
620
- runtime: 'live',
621
- note: selfDashboard ? 'Current process' : 'Dashboard API',
622
- healthyMessage: selfDashboard ? 'Serving status API' : 'Reachable',
623
- selfCheck: selfDashboard ? buildSelfCheck() : null,
624
- },
625
550
  traceBackendDefinition(env),
626
551
  {
627
552
  id: 'memory',
@@ -652,23 +577,25 @@ export async function buildStatus({
652
577
  cwd = process.cwd(),
653
578
  homeDir = homedir(),
654
579
  env = process.env,
655
- dashboardPort,
656
- selfDashboard = false,
657
580
  probeService = defaultProbeService,
658
581
  } = {}) {
659
582
  if (!rootDir) throw new Error('rootDir is required');
660
583
 
661
584
  const mergedEnv = loadConstructEnv({ rootDir, homeDir, env });
662
- const resolvedDashboardPort = parsePort(dashboardPort ?? mergedEnv.DASHBOARD_PORT, 4242);
663
585
 
664
586
  const pkg = readJSON(join(rootDir, 'package.json')) ?? {};
665
- const registry = readJSON(join(rootDir, 'specialists', 'registry.json')) ?? {};
587
+ let registry;
588
+ try {
589
+ registry = loadRegistry({ rootDir });
590
+ } catch {
591
+ registry = { teams: {}, specialists: {}, contracts: {}, policies: {} };
592
+ }
666
593
  const settings = readJSON(join(homeDir, '.claude', 'settings.json')) ?? {};
667
594
  const features = await checkAllFeatures({ homeDir, cwd, env: mergedEnv });
668
595
  const pluginRegistry = loadPluginRegistry({ cwd, homeDir, rootDir, env: mergedEnv });
669
596
  const services = [];
670
597
 
671
- for (const definition of serviceDefinitions(mergedEnv, resolvedDashboardPort, selfDashboard)) {
598
+ for (const definition of serviceDefinitions(mergedEnv)) {
672
599
  if (definition.selfCheck) {
673
600
  const check = definition.selfCheck;
674
601
  services.push({
@@ -734,7 +661,7 @@ export async function buildStatus({
734
661
  : null;
735
662
 
736
663
  const prefix = `${registry.prefix ?? 'cx'}-`;
737
- const specialists = (registry.specialists ?? []).map((specialist) => ({
664
+ const specialists = Object.values(registry.specialists ?? {}).map((specialist) => ({
738
665
  name: `${prefix}${specialist.name}`,
739
666
  description: specialist.description ?? '',
740
667
  modelTier: specialist.modelTier ?? 'standard',
@@ -922,7 +849,15 @@ export function formatStatusReport(status) {
922
849
  lines.push(` ${serviceIcon('degraded')} manifest error — ${error}`);
923
850
  }
924
851
  }
925
- return `${lines.join('\n')}\n`;
852
+
853
+ // Service URLs and repo paths become Cmd-clickable on an interactive stream; on
854
+ // a pipe or in CI links resolve off and colors are empty, so the report stays
855
+ // byte-identical for machine consumers and tests.
856
+
857
+ const colors = resolveUiColors();
858
+ const enabled = terminalLinksEnabled(process.env, { stream: process.stdout });
859
+ const rendered = lines.map((line) => applyLinks(line, colors, { enabled })).join('\n');
860
+ return `${rendered}\n`;
926
861
  }
927
862
 
928
863
  export async function printStatus(options = {}) {
@@ -31,6 +31,7 @@ const LOCK_TIMEOUT_MS = 5_000;
31
31
  const POLL_INTERVAL_MS = 50;
32
32
 
33
33
  const heldLocks = new Set();
34
+ const SLEEP_ARRAY = new Int32Array(new SharedArrayBuffer(4));
34
35
 
35
36
  function isHolderAlive(holderPid) {
36
37
  if (!holderPid || !Number.isFinite(holderPid)) return false;
@@ -77,6 +78,10 @@ function release(lockPath) {
77
78
  heldLocks.delete(lockPath);
78
79
  }
79
80
 
81
+ function sleepSync(ms) {
82
+ Atomics.wait(SLEEP_ARRAY, 0, 0, ms);
83
+ }
84
+
80
85
  let exitHandlerInstalled = false;
81
86
  function installExitHandler() {
82
87
  if (exitHandlerInstalled) return;
@@ -124,9 +129,7 @@ export function withFileLockSync(filePath, fn) {
124
129
 
125
130
  while (!tryAcquire(lockPath)) {
126
131
  if (Date.now() >= deadline) break;
127
- // Crude sleep — node has no native sync sleep, so we busy-wait briefly.
128
- const stop = Date.now() + POLL_INTERVAL_MS;
129
- while (Date.now() < stop) { /* spin */ }
132
+ sleepSync(POLL_INTERVAL_MS);
130
133
  }
131
134
 
132
135
  try {
@@ -31,7 +31,7 @@ export const DEFAULT_LOG_PATH = path.join(doctorRoot(), 'skill-calls.jsonl');
31
31
  * Fire-and-forget log of a skill load event.
32
32
  *
33
33
  * @param {object} event
34
- * @param {string} event.skillId — path-relative-to-skills/ without the .md, e.g. "roles/engineer.platform"
34
+ * @param {string} event.skillId — path-relative-to-skills/ without the .md, e.g. "roles/platform-engineer"
35
35
  * @param {'mcp'|'prompt-composer'|'role-preload'|'validation'|'other'} event.source
36
36
  * @param {string} [event.callerContext] — optional free-form context (agent name, MCP client id, etc.)
37
37
  * @param {object} [opts]
@@ -0,0 +1,63 @@
1
+ /**
2
+ * lib/templates/doc-presentation.mjs — Presentation lint for Construct artifacts.
3
+ *
4
+ * Complements lintDocStructure with spacing, bullet walls, heading hierarchy,
5
+ * and diagram heuristics aligned with skills/brand/output-vibe.md.
6
+ */
7
+ import { readFileSync } from 'node:fs';
8
+
9
+ export function lintDocPresentation(body, { type } = {}) {
10
+ const errors = [];
11
+ const warnings = [];
12
+ const lines = body.split(/\r?\n/);
13
+
14
+ const h1Count = (body.match(/^#\s+/gm) || []).length;
15
+ if (h1Count > 1) errors.push('multiple H1 headings (use single title)');
16
+
17
+ let consecutiveBullets = 0;
18
+ let maxConsecutiveBullets = 0;
19
+ for (const line of lines) {
20
+ if (/^\s*[-*]\s+/.test(line)) {
21
+ consecutiveBullets++;
22
+ maxConsecutiveBullets = Math.max(maxConsecutiveBullets, consecutiveBullets);
23
+ } else if (line.trim() !== '') {
24
+ consecutiveBullets = 0;
25
+ }
26
+ }
27
+ if (maxConsecutiveBullets > 7) {
28
+ errors.push(`bullet wall: ${maxConsecutiveBullets} consecutive bullets (max 7 without prose bridge)`);
29
+ }
30
+
31
+ if (/\n{4,}/.test(body)) {
32
+ warnings.push('more than three consecutive blank lines');
33
+ }
34
+
35
+ for (let i = 0; i < lines.length; i++) {
36
+ if (/^##\s+/.test(lines[i]) && i > 0 && lines[i - 1].trim() !== '') {
37
+ errors.push(`missing blank line before heading: ${lines[i].trim()}`);
38
+ break;
39
+ }
40
+ }
41
+
42
+ if (/```mermaid[\s\S]*?flowchart/i.test(body)) {
43
+ if (!/error|fail|rollback|escalat/i.test(body)) {
44
+ warnings.push('flowchart may lack error/rollback path (output-vibe expects one non-happy path)');
45
+ }
46
+ }
47
+
48
+ if (/!\[[^\]]*\]\([^)]+\)/.test(body)) {
49
+ if (/!\[\s*\]\(/.test(body)) errors.push('image missing alt text');
50
+ }
51
+
52
+ if (type?.startsWith('prd') && !/\bFR-\d/i.test(body) && !/^## Requirements/m.test(body)) {
53
+ warnings.push('prd-family: consider FR-* or ## Requirements section');
54
+ }
55
+
56
+ return { errors, warnings };
57
+ }
58
+
59
+ export function lintDocPresentationFile(filePath, type) {
60
+ const raw = readFileSync(filePath, 'utf8');
61
+ const body = raw.replace(/^---\n[\s\S]*?\n---\n/, '');
62
+ return lintDocPresentation(body, { type });
63
+ }
@@ -6,6 +6,7 @@
6
6
  * Paired with docs/guides/concepts/doc-visual-matrix.md; pinned by template tests.
7
7
  */
8
8
 
9
+ import { readFileSync } from 'node:fs';
9
10
  import { validateArtifactPostconditions } from '../contracts/validate.mjs';
10
11
  import {
11
12
  structureRequirementsFromManifest,
@@ -42,6 +43,31 @@ export function resolveTemplatePath(type, rootDir) {
42
43
  return `templates/docs/${type}.md`;
43
44
  }
44
45
 
46
+ function hasMarkdownHeading(body, titles) {
47
+ const targets = new Set(titles.map((title) => title.trim().toLowerCase()));
48
+ const headingRe = /^#{1,6}\s+(.+)$/gm;
49
+ let match;
50
+ while ((match = headingRe.exec(body))) {
51
+ if (targets.has(match[1].trim().toLowerCase())) return true;
52
+ }
53
+ return false;
54
+ }
55
+
56
+ export function lintPrdDeliveryDepth(body) {
57
+ const errors = [];
58
+ const hasRequirements = hasMarkdownHeading(body, ['Requirements', 'Functional requirements'])
59
+ || /\*\*FR-\d+\.\d+\*\*:|\bFR-\d+\.\d+:/.test(body);
60
+ if (!hasRequirements) {
61
+ errors.push('prd missing functional requirements (## Requirements or FR-* entries)');
62
+ }
63
+ const hasAcceptance = hasMarkdownHeading(body, ['Acceptance criteria', 'Acceptance'])
64
+ || /\*Acceptance(\*:|:\*)|\*\*Acceptance\*\*:/i.test(body);
65
+ if (!hasAcceptance) {
66
+ errors.push('prd missing acceptance criteria (## Acceptance criteria or *Acceptance* / *Acceptance:* markers)');
67
+ }
68
+ return errors;
69
+ }
70
+
45
71
  export function lintDocStructure(filePath, type) {
46
72
  const sectionChecks = (STRUCTURE_REQUIREMENTS[type] || []).map((section) => ({
47
73
  id: `${type}-section-${section}`,
@@ -49,8 +75,15 @@ export function lintDocStructure(filePath, type) {
49
75
  section,
50
76
  }));
51
77
  const postconditions = [...sectionChecks, ...(VISUAL_REQUIREMENTS[type] || [])];
52
- if (postconditions.length === 0) return [];
53
- return validateArtifactPostconditions({ contract: { postconditions }, artifactPath: filePath });
78
+ let errors = postconditions.length === 0
79
+ ? []
80
+ : validateArtifactPostconditions({ contract: { postconditions }, artifactPath: filePath });
81
+ if (type === 'prd') {
82
+ const raw = readFileSync(filePath, 'utf8');
83
+ const body = raw.replace(/^---\n[\s\S]*?\n---\n/, '');
84
+ errors = [...errors, ...lintPrdDeliveryDepth(body)];
85
+ }
86
+ return errors;
54
87
  }
55
88
 
56
89
  export function lintDocVisuals(filePath, type) {
@@ -9,12 +9,15 @@
9
9
  */
10
10
 
11
11
  const ESC = "[";
12
+ const ANSI_TOKEN_RE = /\x1b\[[0-9;?]*[ -\/]*[@-~]|\x1b\]8;;.*?(?:\x07|\x1b\\)/gs;
12
13
 
13
14
  const CODES = { bold: "1", dim: "2", reset: "0", red: "31", green: "32", yellow: "33", cyan: "36" };
14
15
 
15
16
  const PALETTE_KEYS = Object.keys(CODES);
16
17
 
17
18
  const EMPTY_PALETTE = Object.freeze(Object.fromEntries(PALETTE_KEYS.map((k) => [k, ""])));
19
+ const GRAPHEME_SEGMENTER = globalThis.Intl?.Segmenter ? new Intl.Segmenter(undefined, { granularity: "grapheme" }) : null;
20
+ const ZERO_WIDTH_RE = /^(?:\p{Mark}|\p{Control}|\p{Format})$/u;
18
21
 
19
22
  // Color is opt-in by the caller AND gated on an interactive, color-capable stream. Any
20
23
  // NO_COLOR value and TERM=dumb force plain text; a non-TTY stream (pipe, file, CI) does too.
@@ -43,6 +46,108 @@ export function termWidth(stream = process.stdout, { fallback = 80, max = 100 }
43
46
  return Math.min(cols, max);
44
47
  }
45
48
 
49
+ function graphemes(text) {
50
+ const value = String(text);
51
+ if (!value) return [];
52
+ if (!GRAPHEME_SEGMENTER) return [...value];
53
+ return Array.from(GRAPHEME_SEGMENTER.segment(value), (segment) => segment.segment);
54
+ }
55
+
56
+ function isFullwidthCodePoint(codePoint) {
57
+ return codePoint >= 0x1100 && (
58
+ codePoint <= 0x115f ||
59
+ codePoint === 0x2329 ||
60
+ codePoint === 0x232a ||
61
+ (codePoint >= 0x2e80 && codePoint <= 0xa4cf && codePoint !== 0x303f) ||
62
+ (codePoint >= 0xac00 && codePoint <= 0xd7a3) ||
63
+ (codePoint >= 0xf900 && codePoint <= 0xfaff) ||
64
+ (codePoint >= 0xfe10 && codePoint <= 0xfe19) ||
65
+ (codePoint >= 0xfe30 && codePoint <= 0xfe6f) ||
66
+ (codePoint >= 0xff00 && codePoint <= 0xff60) ||
67
+ (codePoint >= 0xffe0 && codePoint <= 0xffe6) ||
68
+ (codePoint >= 0x1f300 && codePoint <= 0x1f64f) ||
69
+ (codePoint >= 0x1f680 && codePoint <= 0x1f6ff) ||
70
+ (codePoint >= 0x1f900 && codePoint <= 0x1f9ff) ||
71
+ (codePoint >= 0x20000 && codePoint <= 0x3fffd)
72
+ );
73
+ }
74
+
75
+ function graphemeWidth(grapheme) {
76
+ const value = String(grapheme);
77
+ if (!value) return 0;
78
+ if (!GRAPHEME_SEGMENTER && value.length === 1) {
79
+ const codePoint = value.codePointAt(0);
80
+ if (codePoint === undefined) return 0;
81
+ if (ZERO_WIDTH_RE.test(value)) return 0;
82
+ return isFullwidthCodePoint(codePoint) ? 2 : 1;
83
+ }
84
+ if ([...value].every((char) => ZERO_WIDTH_RE.test(char))) return 0;
85
+ for (const char of value) {
86
+ const codePoint = char.codePointAt(0);
87
+ if (codePoint !== undefined && isFullwidthCodePoint(codePoint)) return 2;
88
+ }
89
+ return 1;
90
+ }
91
+
92
+ export function displayWidth(text) {
93
+ let width = 0;
94
+ for (const token of graphemes(stripAnsi(text))) width += graphemeWidth(token);
95
+ return width;
96
+ }
97
+
98
+ export function clipToWidth(text, width) {
99
+ const input = String(text);
100
+ if (width <= 0 || input === "") return "";
101
+ if (displayWidth(input) <= width) return input;
102
+
103
+ const parts = [];
104
+ let visible = 0;
105
+ let searchIndex = 0;
106
+ let sawAnsi = false;
107
+ let openHyperlink = false;
108
+
109
+ for (const match of input.matchAll(ANSI_TOKEN_RE)) {
110
+ const token = match[0];
111
+ const index = match.index ?? searchIndex;
112
+ const chunk = input.slice(searchIndex, index);
113
+ for (const grapheme of graphemes(chunk)) {
114
+ const nextWidth = graphemeWidth(grapheme);
115
+ if (visible + nextWidth > width) {
116
+ if (sawAnsi) parts.push("\x1b[0m");
117
+ if (openHyperlink) parts.push("\x1b]8;;\x07");
118
+ return parts.join("");
119
+ }
120
+ parts.push(grapheme);
121
+ visible += nextWidth;
122
+ }
123
+
124
+ parts.push(token);
125
+ sawAnsi = true;
126
+ if (token.startsWith("\x1b]8;;")) openHyperlink = token !== "\x1b]8;;\x07" && token !== "\x1b]8;;\x1b\\";
127
+ searchIndex = index + token.length;
128
+ }
129
+
130
+ const tail = input.slice(searchIndex);
131
+ for (const grapheme of graphemes(tail)) {
132
+ const nextWidth = graphemeWidth(grapheme);
133
+ if (visible + nextWidth > width) {
134
+ if (sawAnsi) parts.push("\x1b[0m");
135
+ if (openHyperlink) parts.push("\x1b]8;;\x07");
136
+ return parts.join("");
137
+ }
138
+ parts.push(grapheme);
139
+ visible += nextWidth;
140
+ }
141
+
142
+ return parts.join("");
143
+ }
144
+
145
+ export function padToWidth(text, width) {
146
+ const input = String(text);
147
+ const padding = Math.max(0, width - displayWidth(input));
148
+ return padding ? `${input}${" ".repeat(padding)}` : input;
149
+ }
150
+
46
151
  // Word-wrap one paragraph to width. Existing newlines stay as hard breaks; a single word
47
152
  // longer than width is never split — it sits on its own line and overflows rather than
48
153
  // becoming two unreadable fragments.
@@ -58,7 +163,7 @@ export function wrapText(text, width = termWidth()) {
58
163
  for (const word of hardLine.split(/\s+/)) {
59
164
  if (line === "") {
60
165
  line = word;
61
- } else if (line.length + 1 + word.length <= width) {
166
+ } else if (displayWidth(line) + 1 + displayWidth(word) <= width) {
62
167
  line += ` ${word}`;
63
168
  } else {
64
169
  out.push(line);
@@ -71,5 +176,5 @@ export function wrapText(text, width = termWidth()) {
71
176
  }
72
177
 
73
178
  export function stripAnsi(text) {
74
- return String(text).replace(/\[[0-9;]*m/g, "");
179
+ return String(text).replace(ANSI_TOKEN_RE, "");
75
180
  }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * lib/test-env-setup.mjs — Shared test environment setup.
3
+ *
4
+ * Centralizes XDG environment clearing for all test runners so the logic
5
+ * stays in sync across scripts/run-tests.mjs, npm test:functional, and
6
+ * direct node --test invocations.
7
+ *
8
+ * Tests isolate user state by overriding HOME to a tmp dir, but lib/config/xdg.mjs
9
+ * honors an absolute XDG_CONFIG_HOME/STATE_HOME/CACHE_HOME over HOME. A CI runner or
10
+ * dev shell that exports those vars routes every test's config/state/cache to one
11
+ * shared real location, collapsing per-test HOME isolation: suites cross-pollute (a
12
+ * seeded dashboard token leaks into "config absent" assertions) and writes can land
13
+ * in real host state. Clear them so resolution falls back to each test's HOME, as on
14
+ * a stock local machine; tests that exercise XDG set it explicitly per call.
15
+ */
16
+
17
+ export function clearXdgVars(env = process.env) {
18
+ delete env.XDG_CONFIG_HOME;
19
+ delete env.XDG_STATE_HOME;
20
+ delete env.XDG_CACHE_HOME;
21
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * lib/ui/components.mjs — branded line primitives shared across surfaces.
3
+ *
4
+ * Status lines and section labels drawn from the shared glyph registry and a
5
+ * resolved color palette. Every primitive returns a string (never writes) so
6
+ * callers compose freely, and degrades to plain labeled text when color is
7
+ * unavailable so meaning never depends on a hue or glyph alone.
8
+ */
9
+
10
+ import { UNICODE_GLYPHS } from './glyphs.mjs';
11
+
12
+ // A status line pairs a tinted glyph with a message. Tints map to semantic palette
13
+ // keys so success/warn/danger/info read consistently with the rest of the surface.
14
+
15
+ export function statusLine(message, { colors = {}, tint = 'muted', glyph = '•', indent = ' ' } = {}) {
16
+ const color = colors[tint] || colors.muted || '';
17
+ return `${indent}${color}${glyph}${colors.reset || ''} ${message}`;
18
+ }
19
+
20
+ export function ok(message, { colors = {}, glyphs = UNICODE_GLYPHS } = {}) {
21
+ return statusLine(message, { colors, tint: 'ok', glyph: glyphs.check });
22
+ }
23
+
24
+ export function warn(message, { colors = {} } = {}) {
25
+ return statusLine(message, { colors, tint: 'warn', glyph: '⚠' });
26
+ }
27
+
28
+ export function info(message, { colors = {}, glyphs = UNICODE_GLYPHS } = {}) {
29
+ return statusLine(message, { colors, tint: 'highlight', glyph: glyphs.arrow });
30
+ }
31
+
32
+ export function fail(message, { colors = {}, glyphs = UNICODE_GLYPHS } = {}) {
33
+ return statusLine(message, { colors, tint: 'danger', glyph: glyphs.cross });
34
+ }
35
+
36
+ // A section label is a tinted glyph plus a heading that opens a logical block in
37
+ // flowing command output, lighter than a full box.
38
+
39
+ export function section(label, { colors = {}, tint = 'highlight', glyph = '▸' } = {}) {
40
+ const color = colors[tint] || colors.highlight || '';
41
+ return `${color}${colors.bold || ''}${glyph} ${label}${colors.reset || ''}`;
42
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * lib/ui/glyphs.mjs — single glyph registry for every construct surface.
3
+ *
4
+ * One source of truth for status marks, spinner frames, bullets, and box-drawing
5
+ * characters, with an ASCII fallback for terminals that cannot render Unicode.
6
+ * The CLI command surface and adapter diagnostics resolve glyphs here so they never
7
+ * drift apart. Tool status marks map raw status strings to the registry.
8
+ */
9
+
10
+ export const UNICODE_GLYPHS = Object.freeze({
11
+ spinner: ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'],
12
+ bullet: '•',
13
+ check: '✓',
14
+ cross: '✗',
15
+ arrow: '→',
16
+ chevron: '›',
17
+ dot: '·',
18
+ diamond: '◆',
19
+ play: '▸',
20
+ boxH: '─',
21
+ boxV: '│',
22
+ boxTL: '╭',
23
+ boxTR: '╮',
24
+ boxBL: '╰',
25
+ boxBR: '╯',
26
+ });
27
+
28
+ export const ASCII_GLYPHS = Object.freeze({
29
+ spinner: ['|', '/', '-', '\\'],
30
+ bullet: '*',
31
+ check: 'OK',
32
+ cross: 'X',
33
+ arrow: '->',
34
+ chevron: '>',
35
+ dot: '.',
36
+ diamond: '*',
37
+ play: '>',
38
+ boxH: '-',
39
+ boxV: '|',
40
+ boxTL: '+',
41
+ boxTR: '+',
42
+ boxBL: '+',
43
+ boxBR: '+',
44
+ });
45
+
46
+ export function glyphs({ ascii = false } = {}) {
47
+ return ascii ? ASCII_GLYPHS : UNICODE_GLYPHS;
48
+ }
49
+
50
+ // Tool lifecycle status resolves to a stable mark: done, failed, running, else
51
+ // pending. Callers pass the glyph set so ASCII terminals stay legible.
52
+
53
+ export function statusGlyph(status, set = UNICODE_GLYPHS) {
54
+ if (status === 'completed') return set.check;
55
+ if (status === 'failed') return set.cross;
56
+ if (status === 'in_progress') return set.chevron;
57
+ return set.dot;
58
+ }