@geraldmaron/construct 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (352) hide show
  1. package/.env.example +69 -0
  2. package/LICENSE +21 -0
  3. package/README.md +344 -0
  4. package/agents/prompts/cx-accessibility.md +29 -0
  5. package/agents/prompts/cx-ai-engineer.md +31 -0
  6. package/agents/prompts/cx-architect.md +40 -0
  7. package/agents/prompts/cx-business-strategist.md +25 -0
  8. package/agents/prompts/cx-data-analyst.md +30 -0
  9. package/agents/prompts/cx-data-engineer.md +32 -0
  10. package/agents/prompts/cx-debugger.md +27 -0
  11. package/agents/prompts/cx-designer.md +33 -0
  12. package/agents/prompts/cx-devil-advocate.md +36 -0
  13. package/agents/prompts/cx-docs-keeper.md +50 -0
  14. package/agents/prompts/cx-engineer.md +36 -0
  15. package/agents/prompts/cx-evaluator.md +26 -0
  16. package/agents/prompts/cx-explorer.md +38 -0
  17. package/agents/prompts/cx-legal-compliance.md +25 -0
  18. package/agents/prompts/cx-operations.md +28 -0
  19. package/agents/prompts/cx-orchestrator.md +30 -0
  20. package/agents/prompts/cx-platform-engineer.md +25 -0
  21. package/agents/prompts/cx-product-manager.md +30 -0
  22. package/agents/prompts/cx-qa.md +42 -0
  23. package/agents/prompts/cx-rd-lead.md +25 -0
  24. package/agents/prompts/cx-release-manager.md +31 -0
  25. package/agents/prompts/cx-researcher.md +31 -0
  26. package/agents/prompts/cx-reviewer.md +36 -0
  27. package/agents/prompts/cx-security.md +37 -0
  28. package/agents/prompts/cx-sre.md +26 -0
  29. package/agents/prompts/cx-test-automation.md +35 -0
  30. package/agents/prompts/cx-trace-reviewer.md +76 -0
  31. package/agents/prompts/cx-ux-researcher.md +30 -0
  32. package/agents/registry.json +879 -0
  33. package/agents/teams.json +94 -0
  34. package/bin/construct +613 -0
  35. package/commands/build/feature.md +21 -0
  36. package/commands/build/fix.md +20 -0
  37. package/commands/design/access.md +19 -0
  38. package/commands/design/flow.md +16 -0
  39. package/commands/design/ui.md +18 -0
  40. package/commands/measure/experiment.md +18 -0
  41. package/commands/measure/metrics.md +18 -0
  42. package/commands/measure/results.md +18 -0
  43. package/commands/plan/api.md +19 -0
  44. package/commands/plan/challenge.md +18 -0
  45. package/commands/plan/decide.md +20 -0
  46. package/commands/plan/feature.md +49 -0
  47. package/commands/plan/requirements.md +24 -0
  48. package/commands/remember/context.md +26 -0
  49. package/commands/remember/handoff.md +19 -0
  50. package/commands/remember/runbook.md +20 -0
  51. package/commands/review/code.md +21 -0
  52. package/commands/review/quality.md +19 -0
  53. package/commands/review/security.md +16 -0
  54. package/commands/ship/ready.md +19 -0
  55. package/commands/ship/release.md +19 -0
  56. package/commands/ship/status.md +18 -0
  57. package/commands/understand/docs.md +22 -0
  58. package/commands/understand/research.md +22 -0
  59. package/commands/understand/this.md +23 -0
  60. package/commands/understand/why.md +18 -0
  61. package/commands/work/clean.md +32 -0
  62. package/commands/work/drive.md +69 -0
  63. package/commands/work/optimize-prompts.md +46 -0
  64. package/commands/work/parallel-review.md +40 -0
  65. package/db/migrations/001_init.sql +38 -0
  66. package/langfuse/docker-compose.yml +82 -0
  67. package/lib/audit-skills.mjs +118 -0
  68. package/lib/auto-docs.mjs +293 -0
  69. package/lib/cli-commands.mjs +409 -0
  70. package/lib/codex-config.mjs +114 -0
  71. package/lib/comment-lint.mjs +191 -0
  72. package/lib/completions.mjs +170 -0
  73. package/lib/context-state.mjs +99 -0
  74. package/lib/cost.mjs +213 -0
  75. package/lib/distill.mjs +403 -0
  76. package/lib/efficiency.mjs +139 -0
  77. package/lib/env-config.mjs +53 -0
  78. package/lib/eval-harness.mjs +59 -0
  79. package/lib/features.mjs +209 -0
  80. package/lib/headhunt.mjs +597 -0
  81. package/lib/hooks/adaptive-lint.mjs +112 -0
  82. package/lib/hooks/agent-tracker.mjs +50 -0
  83. package/lib/hooks/bootstrap-guard.mjs +90 -0
  84. package/lib/hooks/comment-lint.mjs +26 -0
  85. package/lib/hooks/config-protection.mjs +52 -0
  86. package/lib/hooks/console-warn.mjs +43 -0
  87. package/lib/hooks/context-window-recovery.mjs +90 -0
  88. package/lib/hooks/continuation-enforcer.mjs +72 -0
  89. package/lib/hooks/dep-audit.mjs +155 -0
  90. package/lib/hooks/drive-guard.mjs +89 -0
  91. package/lib/hooks/edit-accumulator.mjs +36 -0
  92. package/lib/hooks/edit-error-recovery.mjs +46 -0
  93. package/lib/hooks/edit-guard.mjs +109 -0
  94. package/lib/hooks/env-check.mjs +80 -0
  95. package/lib/hooks/guard-bash.mjs +83 -0
  96. package/lib/hooks/mcp-audit.mjs +57 -0
  97. package/lib/hooks/mcp-health-check.mjs +51 -0
  98. package/lib/hooks/mcp-task-scope.mjs +47 -0
  99. package/lib/hooks/model-fallback.mjs +105 -0
  100. package/lib/hooks/pre-compact.mjs +212 -0
  101. package/lib/hooks/pre-push-gate.mjs +129 -0
  102. package/lib/hooks/read-tracker.mjs +129 -0
  103. package/lib/hooks/registry-sync.mjs +23 -0
  104. package/lib/hooks/scan-secrets.mjs +76 -0
  105. package/lib/hooks/session-start.mjs +95 -0
  106. package/lib/hooks/stop-notify.mjs +191 -0
  107. package/lib/hooks/stop-typecheck.mjs +83 -0
  108. package/lib/hooks/task-completed-guard.mjs +43 -0
  109. package/lib/hooks/teammate-idle-guard.mjs +54 -0
  110. package/lib/hooks/workflow-guard.mjs +62 -0
  111. package/lib/host-capabilities.mjs +123 -0
  112. package/lib/init-docs.mjs +382 -0
  113. package/lib/mcp/server.mjs +1123 -0
  114. package/lib/mcp-catalog.json +243 -0
  115. package/lib/mcp-manager.mjs +433 -0
  116. package/lib/mcp-platform-config.mjs +104 -0
  117. package/lib/model-router.mjs +810 -0
  118. package/lib/opencode-config.mjs +44 -0
  119. package/lib/opencode-runtime-plugin.mjs +909 -0
  120. package/lib/opencode-telemetry.mjs +433 -0
  121. package/lib/orchestration-policy.mjs +258 -0
  122. package/lib/prompt-composer.mjs +196 -0
  123. package/lib/prompt-metadata.mjs +68 -0
  124. package/lib/review.mjs +429 -0
  125. package/lib/role-preload.mjs +52 -0
  126. package/lib/schemas/decision.json +50 -0
  127. package/lib/schemas/implementation.json +51 -0
  128. package/lib/schemas/review-report.json +32 -0
  129. package/lib/schemas/test-report.json +43 -0
  130. package/lib/server/index.mjs +334 -0
  131. package/lib/server/static/app.js +841 -0
  132. package/lib/server/static/index.html +820 -0
  133. package/lib/service-manager.mjs +225 -0
  134. package/lib/setup.mjs +319 -0
  135. package/lib/status.mjs +707 -0
  136. package/lib/storage/backend.mjs +44 -0
  137. package/lib/storage/embeddings.mjs +70 -0
  138. package/lib/storage/hybrid-query.mjs +184 -0
  139. package/lib/storage/sql-store.mjs +55 -0
  140. package/lib/storage/state-source.mjs +101 -0
  141. package/lib/storage/sync.mjs +164 -0
  142. package/lib/storage/vector-store.mjs +59 -0
  143. package/lib/telemetry/backends/langfuse.mjs +58 -0
  144. package/lib/telemetry/backends/noop.mjs +16 -0
  145. package/lib/telemetry/langfuse-ingest.mjs +108 -0
  146. package/lib/telemetry/langfuse-model-sync.mjs +270 -0
  147. package/lib/telemetry/team-rollup.mjs +143 -0
  148. package/lib/toolkit-env.mjs +30 -0
  149. package/lib/validator.mjs +181 -0
  150. package/lib/workflow-state.mjs +794 -0
  151. package/package.json +53 -0
  152. package/personas/construct.md +94 -0
  153. package/platforms/claude/CLAUDE.md +30 -0
  154. package/platforms/claude/settings.template.json +472 -0
  155. package/platforms/opencode/config.template.json +65 -0
  156. package/platforms/opencode/plugins/construct-fallback.js +5 -0
  157. package/platforms/opencode/sync-config.mjs +69 -0
  158. package/rules/common/agents.md +55 -0
  159. package/rules/common/code-review.md +129 -0
  160. package/rules/common/coding-style.md +95 -0
  161. package/rules/common/comments.md +139 -0
  162. package/rules/common/cx-agent-routing.md +61 -0
  163. package/rules/common/cx-skill-routing.md +11 -0
  164. package/rules/common/development-workflow.md +49 -0
  165. package/rules/common/git-workflow.md +29 -0
  166. package/rules/common/hooks.md +35 -0
  167. package/rules/common/patterns.md +36 -0
  168. package/rules/common/performance.md +71 -0
  169. package/rules/common/security.md +34 -0
  170. package/rules/common/testing.md +62 -0
  171. package/rules/golang/coding-style.md +37 -0
  172. package/rules/golang/hooks.md +22 -0
  173. package/rules/golang/patterns.md +50 -0
  174. package/rules/golang/security.md +39 -0
  175. package/rules/golang/testing.md +36 -0
  176. package/rules/python/coding-style.md +47 -0
  177. package/rules/python/hooks.md +24 -0
  178. package/rules/python/patterns.md +44 -0
  179. package/rules/python/security.md +35 -0
  180. package/rules/python/testing.md +43 -0
  181. package/rules/swift/coding-style.md +52 -0
  182. package/rules/swift/hooks.md +25 -0
  183. package/rules/swift/patterns.md +71 -0
  184. package/rules/swift/security.md +38 -0
  185. package/rules/swift/testing.md +50 -0
  186. package/rules/typescript/coding-style.md +204 -0
  187. package/rules/typescript/hooks.md +27 -0
  188. package/rules/typescript/patterns.md +57 -0
  189. package/rules/typescript/security.md +33 -0
  190. package/rules/typescript/testing.md +23 -0
  191. package/rules/web/coding-style.md +101 -0
  192. package/rules/web/design-quality.md +68 -0
  193. package/rules/web/hooks.md +125 -0
  194. package/rules/web/patterns.md +84 -0
  195. package/rules/web/performance.md +69 -0
  196. package/rules/web/security.md +62 -0
  197. package/rules/web/testing.md +60 -0
  198. package/rules/zh/README.md +113 -0
  199. package/rules/zh/agents.md +55 -0
  200. package/rules/zh/code-review.md +129 -0
  201. package/rules/zh/coding-style.md +53 -0
  202. package/rules/zh/development-workflow.md +49 -0
  203. package/rules/zh/git-workflow.md +29 -0
  204. package/rules/zh/hooks.md +35 -0
  205. package/rules/zh/patterns.md +36 -0
  206. package/rules/zh/performance.md +60 -0
  207. package/rules/zh/security.md +34 -0
  208. package/rules/zh/testing.md +34 -0
  209. package/skills/ai/agent-dev.md +102 -0
  210. package/skills/ai/llm-security.md +105 -0
  211. package/skills/ai/ml-ops.md +169 -0
  212. package/skills/ai/orchestration-workflow.md +87 -0
  213. package/skills/ai/prompt-and-eval.md +114 -0
  214. package/skills/ai/prompt-optimizer.md +114 -0
  215. package/skills/ai/rag-system.md +104 -0
  216. package/skills/architecture/api-design.md +100 -0
  217. package/skills/architecture/caching.md +101 -0
  218. package/skills/architecture/cloud-native.md +97 -0
  219. package/skills/architecture/message-queue.md +100 -0
  220. package/skills/architecture/security-arch.md +105 -0
  221. package/skills/development/cpp.md +127 -0
  222. package/skills/development/go.md +129 -0
  223. package/skills/development/java.md +134 -0
  224. package/skills/development/kotlin.md +140 -0
  225. package/skills/development/mobile-crossplatform.md +144 -0
  226. package/skills/development/python.md +109 -0
  227. package/skills/development/rust.md +127 -0
  228. package/skills/development/shell.md +127 -0
  229. package/skills/development/swift.md +129 -0
  230. package/skills/development/typescript.md +129 -0
  231. package/skills/devops/ci-cd.md +108 -0
  232. package/skills/devops/containerization.md +106 -0
  233. package/skills/devops/cost-optimization.md +105 -0
  234. package/skills/devops/data-engineering.md +181 -0
  235. package/skills/devops/database.md +95 -0
  236. package/skills/devops/dependency-management.md +91 -0
  237. package/skills/devops/devsecops.md +96 -0
  238. package/skills/devops/git-workflow.md +100 -0
  239. package/skills/devops/incident-response.md +167 -0
  240. package/skills/devops/monorepo.md +87 -0
  241. package/skills/devops/observability.md +103 -0
  242. package/skills/devops/performance.md +104 -0
  243. package/skills/devops/testing.md +104 -0
  244. package/skills/docs/adr-workflow.md +32 -0
  245. package/skills/docs/backlog-proposal-workflow.md +19 -0
  246. package/skills/docs/customer-profile-workflow.md +22 -0
  247. package/skills/docs/evidence-ingest-workflow.md +23 -0
  248. package/skills/docs/init-docs.md +160 -0
  249. package/skills/docs/init-project.md +50 -0
  250. package/skills/docs/prd-workflow.md +57 -0
  251. package/skills/docs/prfaq-workflow.md +25 -0
  252. package/skills/docs/product-intelligence-review.md +22 -0
  253. package/skills/docs/product-intelligence-workflow.md +62 -0
  254. package/skills/docs/product-signal-workflow.md +27 -0
  255. package/skills/docs/research-workflow.md +28 -0
  256. package/skills/docs/runbook-workflow.md +24 -0
  257. package/skills/exploration/repo-map.md +297 -0
  258. package/skills/frameworks/django.md +159 -0
  259. package/skills/frameworks/nextjs.md +148 -0
  260. package/skills/frameworks/react.md +132 -0
  261. package/skills/frameworks/spring-boot.md +165 -0
  262. package/skills/frontend-design/accessibility.md +153 -0
  263. package/skills/frontend-design/component-patterns.md +111 -0
  264. package/skills/frontend-design/engineering.md +122 -0
  265. package/skills/frontend-design/state-management.md +118 -0
  266. package/skills/frontend-design/ui-aesthetics.md +102 -0
  267. package/skills/frontend-design/ux-principles.md +126 -0
  268. package/skills/quality-gates/review-work.md +90 -0
  269. package/skills/quality-gates/verify-change.md +94 -0
  270. package/skills/quality-gates/verify-module.md +96 -0
  271. package/skills/quality-gates/verify-quality.md +93 -0
  272. package/skills/quality-gates/verify-security.md +95 -0
  273. package/skills/roles/architect.ai-systems.md +37 -0
  274. package/skills/roles/architect.data.md +37 -0
  275. package/skills/roles/architect.enterprise.md +37 -0
  276. package/skills/roles/architect.integration.md +37 -0
  277. package/skills/roles/architect.md +68 -0
  278. package/skills/roles/architect.platform.md +37 -0
  279. package/skills/roles/data-analyst.experiment.md +37 -0
  280. package/skills/roles/data-analyst.md +68 -0
  281. package/skills/roles/data-analyst.product-intelligence.md +37 -0
  282. package/skills/roles/data-analyst.product.md +37 -0
  283. package/skills/roles/data-analyst.telemetry.md +37 -0
  284. package/skills/roles/data-engineer.pipeline.md +37 -0
  285. package/skills/roles/data-engineer.vector-retrieval.md +37 -0
  286. package/skills/roles/data-engineer.warehouse.md +37 -0
  287. package/skills/roles/debugger.md +68 -0
  288. package/skills/roles/designer.accessibility.md +43 -0
  289. package/skills/roles/designer.md +68 -0
  290. package/skills/roles/engineer.ai.md +49 -0
  291. package/skills/roles/engineer.data.md +49 -0
  292. package/skills/roles/engineer.md +85 -0
  293. package/skills/roles/engineer.platform.md +49 -0
  294. package/skills/roles/operator.docs.md +43 -0
  295. package/skills/roles/operator.md +68 -0
  296. package/skills/roles/operator.release.md +43 -0
  297. package/skills/roles/operator.sre.md +43 -0
  298. package/skills/roles/orchestrator.md +66 -0
  299. package/skills/roles/product-manager.ai-product.md +37 -0
  300. package/skills/roles/product-manager.business-strategy.md +43 -0
  301. package/skills/roles/product-manager.enterprise.md +37 -0
  302. package/skills/roles/product-manager.growth.md +37 -0
  303. package/skills/roles/product-manager.md +67 -0
  304. package/skills/roles/product-manager.platform.md +37 -0
  305. package/skills/roles/product-manager.product.md +37 -0
  306. package/skills/roles/qa.ai-eval.md +37 -0
  307. package/skills/roles/qa.api-contract.md +37 -0
  308. package/skills/roles/qa.data-pipeline.md +37 -0
  309. package/skills/roles/qa.md +68 -0
  310. package/skills/roles/qa.test-automation.md +49 -0
  311. package/skills/roles/qa.web-ui.md +37 -0
  312. package/skills/roles/researcher.explorer.md +43 -0
  313. package/skills/roles/researcher.md +68 -0
  314. package/skills/roles/researcher.ux.md +43 -0
  315. package/skills/roles/reviewer.devil-advocate.md +43 -0
  316. package/skills/roles/reviewer.evaluator.md +43 -0
  317. package/skills/roles/reviewer.md +68 -0
  318. package/skills/roles/reviewer.trace.md +43 -0
  319. package/skills/roles/security.ai.md +37 -0
  320. package/skills/roles/security.appsec.md +37 -0
  321. package/skills/roles/security.cloud.md +37 -0
  322. package/skills/roles/security.legal-compliance.md +49 -0
  323. package/skills/roles/security.md +68 -0
  324. package/skills/roles/security.privacy.md +37 -0
  325. package/skills/roles/security.supply-chain.md +37 -0
  326. package/skills/routing.md +139 -0
  327. package/skills/security/blue-team.md +83 -0
  328. package/skills/security/code-audit.md +94 -0
  329. package/skills/security/pentest.md +84 -0
  330. package/skills/security/red-team.md +81 -0
  331. package/skills/security/threat-intel.md +89 -0
  332. package/skills/security/vuln-research.md +87 -0
  333. package/skills/utility/clean-code.md +165 -0
  334. package/sync-agents.mjs +899 -0
  335. package/templates/docs/adr.md +27 -0
  336. package/templates/docs/backlog-proposal.md +26 -0
  337. package/templates/docs/customer-profile.md +34 -0
  338. package/templates/docs/evidence-brief.md +37 -0
  339. package/templates/docs/incident-report.md +46 -0
  340. package/templates/docs/memo.md +27 -0
  341. package/templates/docs/meta-prd.md +56 -0
  342. package/templates/docs/one-pager.md +24 -0
  343. package/templates/docs/prd-business.md +61 -0
  344. package/templates/docs/prd-platform.md +62 -0
  345. package/templates/docs/prd.md +47 -0
  346. package/templates/docs/prfaq.md +31 -0
  347. package/templates/docs/product-intelligence-report.md +31 -0
  348. package/templates/docs/research-brief.md +29 -0
  349. package/templates/docs/rfc-platform.md +60 -0
  350. package/templates/docs/rfc.md +46 -0
  351. package/templates/docs/runbook.md +33 -0
  352. package/templates/docs/signal-brief.md +25 -0
package/lib/status.mjs ADDED
@@ -0,0 +1,707 @@
1
+ /**
2
+ * lib/status.mjs — <one-line purpose>
3
+ *
4
+ * <2–6 line summary: what it does, who calls it, key side effects.>
5
+ */
6
+ import { readFileSync, statSync, existsSync, readdirSync } from 'node:fs';
7
+ import { join, dirname } from 'node:path';
8
+ import { homedir } from 'node:os';
9
+ import { fileURLToPath } from 'node:url';
10
+
11
+ import { inspectContextState } from './context-state.mjs';
12
+ import { inspectWorkflowHealth, loadWorkflow } from './workflow-state.mjs';
13
+ import { CLI_COMMANDS } from './cli-commands.mjs';
14
+ import { checkAllFeatures } from './features.mjs';
15
+ import { loadConstructEnv } from './env-config.mjs';
16
+ import { getActiveOverlays, getPromotionRequests } from './headhunt.mjs';
17
+ import { resolveExecutionContractModelMetadata, selectModelTierForWorkCategory } from './model-router.mjs';
18
+ import { readCostLog, summarizeCostData, normalizeCostEntry } from './cost.mjs';
19
+ import { readEfficiencyLog, summarizeEfficiencyData } from './efficiency.mjs';
20
+ import { describeSqlStore, sqlStoreHealth } from './storage/sql-store.mjs';
21
+ import { describeVectorStore } from './storage/vector-store.mjs';
22
+ import { describeSqlStoreHealth } from './storage/sql-store.mjs';
23
+ const TOTAL_BYTES_WARNING_THRESHOLD = 750_000;
24
+
25
+ function readJSON(path) {
26
+ if (!existsSync(path)) return null;
27
+ try {
28
+ return JSON.parse(readFileSync(path, 'utf8'));
29
+ } catch {
30
+ return null;
31
+ }
32
+ }
33
+
34
+
35
+ function readSessionEfficiency(homeDir) {
36
+ const stats = readJSON(join(homeDir, '.cx', 'session-efficiency.json'));
37
+ if (!stats) return null;
38
+
39
+ const readCount = Number(stats.readCount || 0);
40
+ const uniqueFileCount = Number(stats.uniqueFileCount || 0);
41
+ const repeatedReadCount = Number(stats.repeatedReadCount || 0);
42
+ const largeReadCount = Number(stats.largeReadCount || 0);
43
+ const totalBytesRead = Number(stats.totalBytesRead || 0);
44
+ const warnings = [];
45
+
46
+ let score = 1;
47
+ if (readCount > 0) {
48
+ score -= Math.min(0.35, repeatedReadCount * 0.04);
49
+ score -= Math.min(0.25, largeReadCount * 0.05);
50
+ if (totalBytesRead > 500_000) score -= 0.1;
51
+ if (uniqueFileCount > 25) score -= 0.05;
52
+ }
53
+
54
+ score = Math.max(0, Math.min(1, Math.round(score * 100) / 100));
55
+ let status = 'healthy';
56
+ if (score < 0.6) status = 'degraded';
57
+ else if (score < 0.8) status = 'configured';
58
+
59
+ if (totalBytesRead >= TOTAL_BYTES_WARNING_THRESHOLD) {
60
+ warnings.push(`High byte budget usage: ${Math.round(totalBytesRead / 1024)} KB read this session — compact context or switch to query-focused distill/retrieval before more broad reads.`);
61
+ }
62
+
63
+ const summary = [
64
+ `${readCount} reads`,
65
+ `${uniqueFileCount} files`,
66
+ repeatedReadCount ? `${repeatedReadCount} repeated` : null,
67
+ largeReadCount ? `${largeReadCount} large` : null,
68
+ totalBytesRead ? `${Math.round(totalBytesRead / 1024)} KB` : null,
69
+ ].filter(Boolean).join(' · ');
70
+
71
+ return {
72
+ status,
73
+ score,
74
+ readCount,
75
+ uniqueFileCount,
76
+ repeatedReadCount,
77
+ largeReadCount,
78
+ totalBytesRead,
79
+ summary,
80
+ warnings,
81
+ lastUpdatedAt: stats.lastUpdatedAt || null,
82
+ };
83
+ }
84
+
85
+ function readTelemetryRichness(homeDir) {
86
+ const stats = readJSON(join(homeDir, '.cx', 'session-telemetry.json'));
87
+ if (!stats) return null;
88
+
89
+ const total = Number(stats.total || 0);
90
+ const rich = Number(stats.rich || 0);
91
+ const partial = Number(stats.partial || 0);
92
+ const sparse = Number(stats.sparse || 0);
93
+ const derivedCoverage = total > 0 ? (rich + (partial * 0.5)) / total : 0;
94
+ const coverage = Number.isFinite(Number(stats.coverage)) && Number(stats.coverage) > 0
95
+ ? Number(stats.coverage)
96
+ : derivedCoverage;
97
+ const healthyRatio = total > 0 ? rich / total : 0;
98
+ const status = total === 0
99
+ ? 'configured'
100
+ : healthyRatio >= 0.75 || coverage >= 0.75
101
+ ? 'healthy'
102
+ : coverage >= 0.35
103
+ ? 'configured'
104
+ : 'degraded';
105
+
106
+ return {
107
+ status,
108
+ total,
109
+ rich,
110
+ partial,
111
+ sparse,
112
+ coverage,
113
+ summary: stats.summary || `${total} traces · rich ${rich} · partial ${partial} · sparse ${sparse}`,
114
+ };
115
+ }
116
+
117
+ export function readSessionUsage(homeDir) {
118
+ const entries = readCostLog(homeDir);
119
+ if (!entries.length) {
120
+ return {
121
+ status: 'unavailable',
122
+ interactions: 0,
123
+ inputTokens: 0,
124
+ outputTokens: 0,
125
+ totalTokens: 0,
126
+ totalCostUsd: 0,
127
+ summary: 'No token usage recorded yet',
128
+ lastInteraction: null,
129
+ source: join(homeDir, '.cx', 'session-cost.jsonl'),
130
+ lastUpdatedAt: null,
131
+ };
132
+ }
133
+
134
+ const lastEntry = entries[entries.length - 1] || {};
135
+ const normalizedLast = normalizeCostEntry(lastEntry);
136
+ const lastInteraction = {
137
+ timestamp: lastEntry.ts || null,
138
+ inputTokens: normalizedLast.inputTokens,
139
+ outputTokens: normalizedLast.outputTokens,
140
+ cacheReadInputTokens: normalizedLast.cacheReadInputTokens,
141
+ cacheCreationInputTokens: normalizedLast.cacheCreationInputTokens,
142
+ processedInputTokens: normalizedLast.processedInputTokens,
143
+ totalTokens: normalizedLast.totalTokens,
144
+ costUsd: normalizedLast.costUsd,
145
+ };
146
+
147
+ const data = summarizeCostData(entries);
148
+
149
+ return {
150
+ status: 'available',
151
+ interactions: data.interactions,
152
+ inputTokens: data.totalInputTokens,
153
+ outputTokens: data.totalOutputTokens,
154
+ cacheReadInputTokens: data.cacheReadInputTokens,
155
+ cacheCreationInputTokens: data.cacheCreationInputTokens,
156
+ processedInputTokens: data.processedInputTokens,
157
+ cachedTokens: data.cachedTokens,
158
+ cacheReadRate: data.cacheReadRate,
159
+ cacheHitRate: data.cacheReadRate,
160
+ totalTokens: data.totalTokens,
161
+ totalCostUsd: data.totalCostUsd,
162
+ summary: `${data.interactions} interaction${data.interactions === 1 ? '' : 's'} · ${data.totalTokens.toLocaleString()} tokens · $${data.totalCostUsd.toFixed(2)}`,
163
+ lastInteraction,
164
+ source: join(homeDir, '.cx', 'session-cost.jsonl'),
165
+ lastUpdatedAt: lastInteraction.timestamp,
166
+ };
167
+ }
168
+
169
+
170
+ function newestMtime(dir) {
171
+ if (!existsSync(dir)) return null;
172
+ let newest = 0;
173
+ try {
174
+ for (const file of readdirSync(dir)) {
175
+ const stat = statSync(join(dir, file));
176
+ if (stat.mtimeMs > newest) newest = stat.mtimeMs;
177
+ }
178
+ } catch {
179
+ return null;
180
+ }
181
+ return newest ? new Date(newest).toISOString() : null;
182
+ }
183
+
184
+ function listCommands(rootDir) {
185
+ const commandsDir = join(rootDir, 'commands');
186
+ if (!existsSync(commandsDir)) return [];
187
+ const result = [];
188
+ for (const domain of readdirSync(commandsDir).sort()) {
189
+ const domainPath = join(commandsDir, domain);
190
+ try {
191
+ if (!statSync(domainPath).isDirectory()) continue;
192
+ const commands = [];
193
+ for (const file of readdirSync(domainPath).sort()) {
194
+ if (!file.endsWith('.md')) continue;
195
+ const content = readFileSync(join(domainPath, file), 'utf8');
196
+ const match = content.match(/^---\r?\n[\s\S]*?description:\s*(.+?)\r?\n[\s\S]*?---/);
197
+ const description = match ? match[1].trim() : file.replace('.md', '');
198
+ commands.push({ name: file.replace('.md', ''), description, slash: `/${domain}:${file.replace('.md', '')}` });
199
+ }
200
+ if (commands.length) result.push({ domain, commands });
201
+ } catch {
202
+ continue;
203
+ }
204
+ }
205
+ return result;
206
+ }
207
+
208
+ function listSkills(rootDir) {
209
+ const skillsDir = join(rootDir, 'skills');
210
+ if (!existsSync(skillsDir)) return [];
211
+ const result = [];
212
+ for (const category of readdirSync(skillsDir)) {
213
+ const categoryPath = join(skillsDir, category);
214
+ try {
215
+ if (!statSync(categoryPath).isDirectory()) continue;
216
+ const files = readdirSync(categoryPath)
217
+ .filter((file) => file.endsWith('.md') || file.endsWith('.mjs'))
218
+ .filter((file) => file !== 'SKILL.md');
219
+ result.push({ category, files });
220
+ } catch {
221
+ continue;
222
+ }
223
+ }
224
+ return result;
225
+ }
226
+
227
+ function parsePort(value, fallback) {
228
+ const parsed = Number.parseInt(String(value ?? fallback), 10);
229
+ return Number.isFinite(parsed) ? parsed : fallback;
230
+ }
231
+
232
+ async function probeHttp(url, { method = 'GET', headers, body, timeout = 2000 } = {}) {
233
+ const controller = new AbortController();
234
+ const timer = setTimeout(() => controller.abort(), timeout);
235
+ try {
236
+ const res = await fetch(url, { method, headers, body, signal: controller.signal });
237
+ return { ok: res.ok, statusCode: res.status };
238
+ } catch (error) {
239
+ return { ok: false, error };
240
+ } finally {
241
+ clearTimeout(timer);
242
+ }
243
+ }
244
+
245
+ function normalizeProbeResult(result) {
246
+ if (typeof result === 'string') {
247
+ return { status: result };
248
+ }
249
+ return result ?? { status: 'unavailable', message: 'Probe failed' };
250
+ }
251
+
252
+ async function defaultProbeService(service) {
253
+ const result = service.method === 'POST'
254
+ ? await probeHttp(service.url, {
255
+ method: 'POST',
256
+ headers: { 'Content-Type': 'application/json' },
257
+ body: service.body,
258
+ })
259
+ : await probeHttp(service.url);
260
+
261
+ if (result.ok) return { status: 'healthy', message: service.healthyMessage ?? 'Reachable' };
262
+ if (result.statusCode) return { status: 'degraded', message: `HTTP ${result.statusCode}` };
263
+ if (result.error?.name === 'AbortError') return { status: 'unavailable', message: 'Timed out' };
264
+ if (result.error?.cause?.code === 'ECONNREFUSED' || result.error?.code === 'ECONNREFUSED') {
265
+ return { status: 'unavailable', message: 'Connection refused' };
266
+ }
267
+ return { status: 'unavailable', message: result.error?.message ?? 'Connection failed' };
268
+ }
269
+
270
+ async function fetchLangfuseTelemetryStatus(env, { timeout = 2500 } = {}) {
271
+ const baseUrl = (env.LANGFUSE_BASEURL ?? 'https://cloud.langfuse.com').replace(/\/$/, '');
272
+ const key = env.LANGFUSE_PUBLIC_KEY;
273
+ const secret = env.LANGFUSE_SECRET_KEY;
274
+ if (!key || !secret) return { status: 'unavailable', summary: 'Langfuse credentials not configured' };
275
+ const headers = {
276
+ Authorization: `Basic ${Buffer.from(`${key}:${secret}`).toString('base64')}`,
277
+ };
278
+ const controller = new AbortController();
279
+ const timer = setTimeout(() => controller.abort(), timeout);
280
+ try {
281
+ const res = await fetch(`${baseUrl}/api/public/traces?limit=25`, { headers, signal: controller.signal });
282
+ if (!res.ok) return { status: 'unavailable', summary: `Langfuse HTTP ${res.status}` };
283
+ const json = await res.json().catch(() => ({}));
284
+ const traces = Array.isArray(json.data) ? json.data : [];
285
+ const counts = { rich: 0, partial: 0, sparse: 0 };
286
+
287
+ for (const trace of traces) {
288
+ const observationCount = Number(trace?.observationCount ?? trace?.observations?.length ?? trace?.spanCount ?? trace?.generationCount ?? 0) || 0;
289
+ const hasInput = trace?.input != null;
290
+ const hasOutput = trace?.output != null;
291
+ const metaKeys = trace?.metadata && typeof trace.metadata === 'object' ? Object.keys(trace.metadata).length : 0;
292
+ const hasRichMetadata = metaKeys >= 5;
293
+ const hasPayload = hasInput || hasOutput || metaKeys > 0;
294
+ // Rich: has meaningful input/output AND (at least one observation OR rich metadata with 5+ keys).
295
+ // This correctly scores event-style traces (session, message) that carry metadata instead of spans.
296
+ if ((hasInput || hasOutput) && (observationCount >= 1 || hasRichMetadata)) counts.rich += 1;
297
+ else if (hasPayload || observationCount >= 1) counts.partial += 1;
298
+ else counts.sparse += 1;
299
+ }
300
+
301
+ const total = traces.length;
302
+ const coverage = total > 0 ? (counts.rich + counts.partial * 0.5) / total : 0;
303
+ let status = 'configured';
304
+ if (total === 0) status = 'configured';
305
+ else if (coverage >= 0.75) status = 'healthy';
306
+ else if (coverage >= 0.35) status = 'configured';
307
+ else status = 'degraded';
308
+
309
+ return {
310
+ status,
311
+ backend: 'langfuse',
312
+ total,
313
+ rich: counts.rich,
314
+ partial: counts.partial,
315
+ sparse: counts.sparse,
316
+ coverage: Number(coverage.toFixed(2)),
317
+ summary: total > 0
318
+ ? `Langfuse reachable · ${total} traces · rich ${counts.rich} · partial ${counts.partial} · sparse ${counts.sparse} · coverage ${(coverage * 100).toFixed(0)}%`
319
+ : 'Langfuse reachable · no traces yet',
320
+ };
321
+ } catch (err) {
322
+ const msg = err?.name === 'AbortError' ? 'timed out' : err.message;
323
+ return { status: 'unavailable', summary: `Langfuse ${msg}` };
324
+ } finally {
325
+ clearTimeout(timer);
326
+ }
327
+ }
328
+
329
+ function summarizeTelemetryHealth() {
330
+ return null;
331
+ }
332
+
333
+ function summarizeRuntime(services) {
334
+ const coreServices = services.filter((service) => service.impactsOverall !== false);
335
+ const optionalServices = services.filter((service) => service.impactsOverall === false);
336
+
337
+ const healthy = coreServices.filter((service) => service.status === 'healthy').length;
338
+ const degraded = coreServices.filter((service) => service.status === 'degraded').length;
339
+ const unavailable = coreServices.filter((service) => service.status === 'unavailable').length;
340
+ const optionalUnavailable = optionalServices.filter((service) => service.status !== 'healthy').length;
341
+
342
+ let status = 'healthy';
343
+ if (unavailable > 0) status = healthy > 0 ? 'degraded' : 'unavailable';
344
+ else if (degraded > 0) status = 'degraded';
345
+
346
+ const parts = [`${healthy}/${coreServices.length} core runtime surfaces reachable`];
347
+ if (degraded > 0) parts.push(`${degraded} degraded`);
348
+ if (unavailable > 0) parts.push(`${unavailable} unavailable`);
349
+ if (optionalUnavailable > 0) parts.push(`${optionalUnavailable} optional unavailable`);
350
+
351
+ return {
352
+ status,
353
+ healthy,
354
+ degraded,
355
+ unavailable,
356
+ summary: parts.join(' · '),
357
+ };
358
+ }
359
+
360
+ function summarizeIntegrations(features) {
361
+ const counts = features.reduce((acc, feature) => {
362
+ const key = feature.status ?? 'unknown';
363
+ acc[key] = (acc[key] ?? 0) + 1;
364
+ return acc;
365
+ }, {});
366
+
367
+ const summary = [
368
+ counts.healthy ? `${counts.healthy} live` : null,
369
+ counts.configured ? `${counts.configured} configured` : null,
370
+ counts.degraded ? `${counts.degraded} degraded` : null,
371
+ counts.unavailable ? `${counts.unavailable} unavailable` : null,
372
+ counts.disabled ? `${counts.disabled} disabled` : null,
373
+ ].filter(Boolean).join(' · ');
374
+
375
+ return { counts, summary: summary || 'No integrations detected' };
376
+ }
377
+
378
+ function workflowStatusSummary(workflow, cwd) {
379
+ const health = inspectWorkflowHealth(workflow, { cwd });
380
+ return {
381
+ cwd,
382
+ exists: health.exists,
383
+ status: health.alignment.status,
384
+ summary: health.summary,
385
+ state: workflow,
386
+ findings: health.alignment.findings,
387
+ };
388
+ }
389
+
390
+ export function buildPublicHealthSurface({
391
+ cwd = process.cwd(),
392
+ contextInspection = inspectContextState(cwd),
393
+ workflow = loadWorkflow(cwd),
394
+ executionContractModel = null,
395
+ } = {}) {
396
+ const workflowHealth = inspectWorkflowHealth(workflow, { cwd });
397
+
398
+ return {
399
+ activeTask: workflowHealth.activeTask,
400
+ context: {
401
+ hasFile: Boolean(contextInspection?.hasFile),
402
+ source: contextInspection?.source ?? 'missing',
403
+ savedAt: contextInspection?.savedAt ?? null,
404
+ summary: contextInspection?.summary ?? null,
405
+ },
406
+ workflow: {
407
+ exists: workflowHealth.exists,
408
+ phase: workflowHealth.phase,
409
+ lifecycleStatus: workflowHealth.lifecycleStatus,
410
+ currentTaskKey: workflowHealth.currentTaskKey,
411
+ summary: workflowHealth.summary,
412
+ },
413
+ alignment: workflowHealth.alignment,
414
+ metadataPresence: {
415
+ executionContractModel: Boolean(executionContractModel?.version),
416
+ contextState: contextInspection?.source === 'json',
417
+ },
418
+ };
419
+ }
420
+
421
+ function traceBackendDefinition(env) {
422
+ const langfuseUrl = (env.LANGFUSE_BASEURL ?? 'https://cloud.langfuse.com').replace(/\/$/, '');
423
+ return {
424
+ id: 'langfuse',
425
+ name: 'Langfuse',
426
+ url: langfuseUrl,
427
+ probeUrl: `${langfuseUrl}/api/public/health`,
428
+ runtime: 'live',
429
+ note: 'Trace backend',
430
+ healthyMessage: 'Reachable',
431
+ };
432
+ }
433
+
434
+ function serviceDefinitions(env, dashboardPort, selfDashboard) {
435
+ const memoryPort = parsePort(env.MEMORY_PORT, 8765);
436
+ const bridgePort = parsePort(env.BRIDGE_PORT, 5173);
437
+
438
+ return [
439
+ {
440
+ id: 'dashboard',
441
+ name: 'Dashboard',
442
+ url: `http://127.0.0.1:${dashboardPort}`,
443
+ runtime: 'live',
444
+ note: selfDashboard ? 'Current process' : 'Dashboard API',
445
+ healthyMessage: selfDashboard ? 'Serving status API' : 'Reachable',
446
+ selfHealthy: selfDashboard,
447
+ },
448
+ traceBackendDefinition(env),
449
+ {
450
+ id: 'memory',
451
+ name: 'Memory (cm)',
452
+ url: `http://127.0.0.1:${memoryPort}`,
453
+ probeUrl: `http://127.0.0.1:${memoryPort}/`,
454
+ method: 'POST',
455
+ body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/list' }),
456
+ runtime: 'live',
457
+ note: 'MCP-managed',
458
+ healthyMessage: 'Reachable',
459
+ },
460
+ {
461
+ id: 'opencode',
462
+ name: 'OpenCode',
463
+ url: `http://127.0.0.1:${bridgePort}`,
464
+ runtime: 'live',
465
+ note: 'Optional web UI',
466
+ healthyMessage: 'Reachable',
467
+ impactsOverall: false,
468
+ },
469
+ ];
470
+ }
471
+
472
+ export async function buildStatus({
473
+ rootDir,
474
+ cwd = process.cwd(),
475
+ homeDir = homedir(),
476
+ env = process.env,
477
+ dashboardPort,
478
+ selfDashboard = false,
479
+ probeService = defaultProbeService,
480
+ } = {}) {
481
+ if (!rootDir) throw new Error('rootDir is required');
482
+
483
+ const mergedEnv = loadConstructEnv({ rootDir, homeDir, env });
484
+ const resolvedDashboardPort = parsePort(dashboardPort ?? mergedEnv.DASHBOARD_PORT, 4242);
485
+
486
+ const pkg = readJSON(join(rootDir, 'package.json')) ?? {};
487
+ const registry = readJSON(join(rootDir, 'agents', 'registry.json')) ?? {};
488
+ const settings = readJSON(join(homeDir, '.claude', 'settings.json')) ?? {};
489
+ const features = await checkAllFeatures({ homeDir, env: mergedEnv });
490
+ const services = [];
491
+
492
+ for (const definition of serviceDefinitions(mergedEnv, resolvedDashboardPort, selfDashboard)) {
493
+ if (definition.selfHealthy) {
494
+ services.push({ ...definition, status: 'healthy', message: definition.healthyMessage ?? 'Reachable' });
495
+ continue;
496
+ }
497
+
498
+ const result = normalizeProbeResult(await probeService({
499
+ ...definition,
500
+ url: definition.probeUrl ?? definition.url,
501
+ }));
502
+
503
+ services.push({
504
+ ...definition,
505
+ status: result.status ?? 'unavailable',
506
+ message: result.message ?? '',
507
+ });
508
+ }
509
+
510
+ const runtime = summarizeRuntime(services);
511
+ const integrations = summarizeIntegrations(features);
512
+ const workflow = loadWorkflow(cwd);
513
+ const workflowSummary = workflowStatusSummary(workflow, cwd);
514
+ const contextInspection = inspectContextState(cwd);
515
+ const sessionEfficiency = readSessionEfficiency(homeDir);
516
+ const sessionUsage = readSessionUsage(homeDir);
517
+ const telemetryRichness = readTelemetryRichness(homeDir)
518
+ ?? summarizeTelemetryHealth()
519
+ ?? await fetchLangfuseTelemetryStatus(mergedEnv);
520
+ const efficiencyDigest = summarizeEfficiencyData(readEfficiencyLog(homeDir));
521
+ const activeOverlays = getActiveOverlays(cwd);
522
+ const sqlStore = describeSqlStore(mergedEnv);
523
+ const vectorStore = describeVectorStore(mergedEnv);
524
+ const sqlHealth = sqlStore.mode === 'postgres' ? await describeSqlStoreHealth(mergedEnv) : sqlStoreHealth(mergedEnv);
525
+ const promotionRequests = getPromotionRequests(cwd);
526
+ const executionContractModel = resolveExecutionContractModelMetadata({
527
+ envValues: mergedEnv,
528
+ registryModels: registry.models ?? {},
529
+ requestedTier: selectModelTierForWorkCategory(workflowSummary.state?.tasks?.find((task) => task.key === workflowSummary.state?.currentTaskKey)?.workCategory),
530
+ workCategory: null,
531
+ });
532
+ const publicHealth = buildPublicHealthSurface({
533
+ cwd,
534
+ contextInspection,
535
+ workflow,
536
+ executionContractModel,
537
+ });
538
+
539
+ const personas = (registry.personas ?? []).map((persona) => ({
540
+ name: persona.name,
541
+ displayName: persona.displayName ?? persona.name,
542
+ role: persona.role ?? '',
543
+ description: persona.description ?? '',
544
+ modelTier: persona.modelTier ?? 'standard',
545
+ }));
546
+
547
+ const prefix = `${registry.prefix ?? 'cx'}-`;
548
+ const specialists = (registry.agents ?? []).map((agent) => ({
549
+ name: `${prefix}${agent.name}`,
550
+ description: agent.description ?? '',
551
+ modelTier: agent.modelTier ?? 'standard',
552
+ }));
553
+
554
+ const hooks = [];
555
+ for (const [phase, entries] of Object.entries(settings.hooks ?? {})) {
556
+ for (const entry of Array.isArray(entries) ? entries : []) {
557
+ hooks.push({
558
+ id: `${phase.toLowerCase()}:${(entry.description ?? entry.command ?? '').toLowerCase().replace(/[^a-z0-9]/g, '-').slice(0, 40)}`,
559
+ phase,
560
+ description: entry.description ?? entry.command ?? '',
561
+ blocking: !entry.background,
562
+ });
563
+ }
564
+ }
565
+
566
+ return {
567
+ version: pkg.version ?? '1.0.0',
568
+ lastSync: newestMtime(join(homeDir, '.claude', 'agents')),
569
+ system: {
570
+ overall: runtime,
571
+ services,
572
+ integrations,
573
+ },
574
+ features,
575
+ personas,
576
+ specialists,
577
+ hooks,
578
+ skills: listSkills(rootDir),
579
+ commands: listCommands(rootDir),
580
+ cliCommands: CLI_COMMANDS,
581
+ mcpServers: Object.keys(settings.mcpServers ?? {}),
582
+ workflow: workflowSummary,
583
+ publicHealth,
584
+ storage: {
585
+ sql: sqlStore,
586
+ vector: vectorStore,
587
+ health: {
588
+ sql: sqlHealth,
589
+ vector: vectorStore.mode === 'remote' || vectorStore.mode === 'local'
590
+ ? { status: 'configured', message: `Vector retrieval configured (${vectorStore.mode})` }
591
+ : { status: 'unavailable', message: 'No vector index configured; using file-state only' },
592
+ },
593
+ },
594
+ executionContractModel,
595
+ sessionEfficiency,
596
+ efficiencyDigest,
597
+ sessionUsage,
598
+ telemetryRichness,
599
+ overlays: activeOverlays,
600
+ promotionRequests,
601
+ };
602
+ }
603
+
604
+ function serviceIcon(status) {
605
+ if (status === 'healthy') return '✓';
606
+ if (status === 'degraded' || status === 'configured') return '⚠';
607
+ return '✗';
608
+ }
609
+
610
+ function statusLabel(status) {
611
+ const labels = {
612
+ healthy: 'healthy',
613
+ degraded: 'degraded',
614
+ unavailable: 'unavailable',
615
+ configured: 'configured',
616
+ disabled: 'disabled',
617
+ pass: 'pass',
618
+ warn: 'warn',
619
+ fail: 'fail',
620
+ missing: 'missing',
621
+ };
622
+ return labels[status] ?? status;
623
+ }
624
+
625
+ export function formatStatusReport(status) {
626
+ const lines = [];
627
+ lines.push('Construct Status');
628
+ lines.push('════════════════');
629
+ lines.push('');
630
+ lines.push(`Overall: ${statusLabel(status.system.overall.status)} · ${status.system.overall.summary}`);
631
+ lines.push(`Workflow: ${statusLabel(status.workflow.status)} · ${status.workflow.summary}`);
632
+ if (status.executionContractModel?.selectedTier && status.executionContractModel?.selectedModel) {
633
+ lines.push(`Execution contract: ${status.executionContractModel.selectedTier} · ${status.executionContractModel.selectedModel} (${status.executionContractModel.selectedModelSource})`);
634
+ }
635
+ if (status.storage?.sql || status.storage?.vector) {
636
+ lines.push(`Storage: ${status.storage.sql?.mode ?? 'unknown'} SQL · ${status.storage.vector?.mode ?? 'unknown'} vector`);
637
+ lines.push(`Storage health: SQL ${status.storage.health?.sql?.status ?? 'unknown'} · vector ${status.storage.health?.vector?.status ?? 'unknown'}`);
638
+ }
639
+ if (status.sessionEfficiency) {
640
+ lines.push(`Efficiency: ${statusLabel(status.sessionEfficiency.status)} · score ${status.sessionEfficiency.score.toFixed(2)} · ${status.sessionEfficiency.summary}`);
641
+ for (const warning of status.sessionEfficiency.warnings ?? []) {
642
+ lines.push(`Warning: ${warning}`);
643
+ }
644
+ }
645
+ if (status.efficiencyDigest) {
646
+ lines.push(`Context: ${statusLabel(status.efficiencyDigest.status)} · ${status.efficiencyDigest.summary}`);
647
+ lines.push(` ${status.efficiencyDigest.recommendation}`);
648
+ }
649
+ if (status.sessionUsage) {
650
+ const cacheNote = status.sessionUsage.cacheHitRate > 0
651
+ ? ` · cache ${(status.sessionUsage.cacheHitRate * 100).toFixed(1)}% hit`
652
+ : '';
653
+ lines.push(`Usage: ${statusLabel(status.sessionUsage.status)} · ${status.sessionUsage.summary}${cacheNote}`);
654
+ if (status.sessionUsage.lastInteraction) {
655
+ lines.push(`Last interaction: ${status.sessionUsage.lastInteraction.totalTokens.toLocaleString()} tokens (${status.sessionUsage.lastInteraction.inputTokens.toLocaleString()} in / ${status.sessionUsage.lastInteraction.outputTokens.toLocaleString()} out)`);
656
+ }
657
+ }
658
+ if (status.telemetryRichness) {
659
+ lines.push(`Telemetry: ${statusLabel(status.telemetryRichness.status)} · ${status.telemetryRichness.summary}`);
660
+ }
661
+ if ((status.overlays ?? []).length > 0) {
662
+ lines.push(`Overlays: ${status.overlays.length} active`);
663
+ for (const overlay of status.overlays) {
664
+ lines.push(` - ${overlay.domain} · ${overlay.focus} · ${overlay.attachTo.join(', ')}`);
665
+ }
666
+ }
667
+ if ((status.promotionRequests ?? []).length > 0) {
668
+ lines.push(`Promotion requests: ${status.promotionRequests.length}`);
669
+ for (const request of status.promotionRequests) {
670
+ const challenge = request.challenge?.status ? ` · challenge ${request.challenge.status}` : '';
671
+ lines.push(` - ${request.domain} · ${request.status}${challenge}`);
672
+ }
673
+ }
674
+ lines.push('');
675
+ lines.push('Runtime');
676
+ for (const service of status.system.services) {
677
+ const suffix = service.note ? ` (${service.note})` : '';
678
+ const detail = service.message ? ` — ${service.message}` : '';
679
+ lines.push(` ${serviceIcon(service.status)} ${service.name.padEnd(14)} ${service.url}${suffix}${detail}`);
680
+ }
681
+ lines.push('');
682
+ lines.push(`Integrations: ${status.system.integrations.summary}`);
683
+ for (const feature of status.features) {
684
+ lines.push(` ${serviceIcon(feature.status)} ${feature.name.padEnd(22)} ${statusLabel(feature.status)} — ${feature.message}`);
685
+ }
686
+ return `${lines.join('\n')}\n`;
687
+ }
688
+
689
+ export async function printStatus(options = {}) {
690
+ const status = await buildStatus(options);
691
+ process.stdout.write(formatStatusReport(status));
692
+ }
693
+
694
+ if (process.argv[1] && process.argv[1].endsWith('/status.mjs')) {
695
+ const args = new Set(process.argv.slice(2));
696
+ const moduleDir = dirname(fileURLToPath(import.meta.url));
697
+ const rootDir = join(moduleDir, '..');
698
+ const status = await buildStatus({
699
+ rootDir,
700
+ cwd: process.cwd(),
701
+ });
702
+ if (args.has('--json')) {
703
+ process.stdout.write(`${JSON.stringify(status, null, 2)}\n`);
704
+ } else {
705
+ process.stdout.write(formatStatusReport(status));
706
+ }
707
+ }