@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
@@ -0,0 +1,403 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * lib/distill.mjs — <one-line purpose>
4
+ *
5
+ * <2–6 line summary.>
6
+ */
7
+ import { readdirSync, statSync, readFileSync, writeFileSync, existsSync, mkdirSync, openSync, readSync, closeSync } from 'fs';
8
+ import { join, extname, relative, dirname, basename } from 'path';
9
+ import { execSync, spawnSync } from 'child_process';
10
+ import { homedir } from 'os';
11
+ import { getActiveOverlays } from './headhunt.mjs';
12
+
13
+ const TEXT_EXTS = new Set([
14
+ '.md', '.txt', '.rst', '.adoc', '.json', '.yaml', '.yml', '.toml',
15
+ '.js', '.mjs', '.ts', '.tsx', '.jsx', '.py', '.go', '.rs', '.sh', '.bash',
16
+ '.html', '.css', '.csv', '.xml', '.env', '.env.example', '.conf', '.ini', '.tf', '.hcl',
17
+ ]);
18
+
19
+ const SKIP_DIRS = new Set(['node_modules', '.git', '.next', 'dist', 'build', '__pycache__', '.cache']);
20
+ const MAX_FILE_BYTES = 80_000;
21
+ const MAX_DISCOVER_FILE_BYTES = 5_000_000;
22
+ const MAX_TOTAL_BYTES = 600_000;
23
+ const MAX_FILES = 80;
24
+ const DEFAULT_CHUNK_BYTES = 4_000;
25
+ const DEFAULT_CHUNK_OVERLAP = 400;
26
+
27
+ function discoverFiles(dir, maxDepth = 3, allowedExts = null) {
28
+ const files = [];
29
+ function walk(current, depth) {
30
+ if (depth > maxDepth) return;
31
+ let entries;
32
+ try { entries = readdirSync(current); } catch { return; }
33
+ for (const entry of entries.sort()) {
34
+ if (entry.startsWith('.') && entry !== '.env.example') continue;
35
+ const full = join(current, entry);
36
+ let stat;
37
+ try { stat = statSync(full); } catch { continue; }
38
+ if (stat.isDirectory()) {
39
+ if (!SKIP_DIRS.has(entry)) walk(full, depth + 1);
40
+ } else {
41
+ const ext = extname(entry).toLowerCase();
42
+ const match = allowedExts ? allowedExts.has(ext) : TEXT_EXTS.has(ext);
43
+ if (match && stat.size > 0 && stat.size <= MAX_DISCOVER_FILE_BYTES) files.push({ path: full, size: stat.size, ext });
44
+ }
45
+ }
46
+ }
47
+ walk(dir, 0);
48
+ return files;
49
+ }
50
+
51
+ function decodeBytes(buffer) {
52
+ return Buffer.from(buffer).toString('utf8').replace(/\u0000/g, '');
53
+ }
54
+
55
+ function readBoundedText(filePath, size) {
56
+ if (size <= MAX_FILE_BYTES) {
57
+ return {
58
+ content: readFileSync(filePath, 'utf8'),
59
+ truncated: false,
60
+ sampledBytes: size,
61
+ originalBytes: size,
62
+ };
63
+ }
64
+
65
+ const headBytes = Math.floor(MAX_FILE_BYTES * 0.6);
66
+ const tailBytes = MAX_FILE_BYTES - headBytes;
67
+ const fd = openSync(filePath, 'r');
68
+ try {
69
+ const headBuffer = Buffer.alloc(headBytes);
70
+ const tailBuffer = Buffer.alloc(tailBytes);
71
+ const headRead = readSync(fd, headBuffer, 0, headBytes, 0);
72
+ const tailRead = readSync(fd, tailBuffer, 0, tailBytes, Math.max(0, size - tailBytes));
73
+ const gap = size - headRead - tailRead;
74
+ return {
75
+ content: `${decodeBytes(headBuffer.subarray(0, headRead))}\n\n… [middle omitted: ${Math.max(0, gap)} bytes skipped from large file] …\n\n${decodeBytes(tailBuffer.subarray(0, tailRead))}`,
76
+ truncated: true,
77
+ sampledBytes: headRead + tailRead,
78
+ originalBytes: size,
79
+ };
80
+ } finally {
81
+ closeSync(fd);
82
+ }
83
+ }
84
+
85
+ function inferDocumentTitle(relPath, content) {
86
+ const firstHeading = content.match(/^(?:#{1,2}\s+.+|title:\s*.+)$/im)?.[0];
87
+ if (firstHeading) return firstHeading.replace(/^#{1,2}\s+|^title:\s*/i, '').trim();
88
+ return basename(relPath);
89
+ }
90
+
91
+ function detectHeadings(content) {
92
+ const headings = [];
93
+ for (const match of content.matchAll(/^(#{1,6}[ \t]+.+)$/gm)) {
94
+ headings.push({ index: match.index || 0, text: match[1].replace(/^#{1,6}[ \t]+/, '').trim() });
95
+ }
96
+ return headings;
97
+ }
98
+
99
+ function headingForOffset(headings, offset) {
100
+ let current = null;
101
+ for (const heading of headings) {
102
+ if (heading.index > offset) break;
103
+ current = heading.text;
104
+ }
105
+ return current;
106
+ }
107
+
108
+ function chunkContent(relPath, content, { chunkBytes = DEFAULT_CHUNK_BYTES, overlap = DEFAULT_CHUNK_OVERLAP, truncated = false, originalBytes = content.length, sampledBytes = content.length } = {}) {
109
+ const title = inferDocumentTitle(relPath, content);
110
+ const headings = detectHeadings(content);
111
+ const chunks = [];
112
+ let offset = 0;
113
+ let index = 0;
114
+ while (offset < content.length) {
115
+ const end = Math.min(content.length, offset + chunkBytes);
116
+ const text = content.slice(offset, end);
117
+ if (!text.trim()) break;
118
+ chunks.push({
119
+ id: `${relPath}#${index + 1}`,
120
+ rel: relPath,
121
+ title,
122
+ section: headingForOffset(headings, offset),
123
+ start: offset,
124
+ end,
125
+ text,
126
+ truncated,
127
+ originalBytes,
128
+ sampledBytes,
129
+ });
130
+ if (end >= content.length) break;
131
+ offset = Math.max(end - overlap, offset + 1);
132
+ index += 1;
133
+ }
134
+ return chunks;
135
+ }
136
+
137
+ function scoreChunk(chunk, queryTerms) {
138
+ if (queryTerms.length === 0) return 0;
139
+ const haystack = `${chunk.title || ''}\n${chunk.section || ''}\n${chunk.text}`.toLowerCase();
140
+ let score = 0;
141
+ for (const term of queryTerms) {
142
+ if (haystack.includes(term)) score += 1;
143
+ }
144
+ if (chunk.section && queryTerms.some((term) => chunk.section.toLowerCase().includes(term))) score += 1;
145
+ return score;
146
+ }
147
+
148
+ function selectChunks(chunks, query, maxChunks = 24) {
149
+ if (!query) return chunks.slice(0, maxChunks);
150
+ const queryTerms = Array.from(new Set(query.toLowerCase().split(/[^a-z0-9]+/).filter((term) => term.length >= 3)));
151
+ return chunks
152
+ .map((chunk) => ({ chunk, score: scoreChunk(chunk, queryTerms) }))
153
+ .filter(({ score }) => score > 0)
154
+ .sort((a, b) => b.score - a.score || a.chunk.rel.localeCompare(b.chunk.rel) || a.chunk.start - b.chunk.start)
155
+ .slice(0, maxChunks)
156
+ .map(({ chunk }) => chunk);
157
+ }
158
+
159
+ function evaluateSufficiency(chunks, query) {
160
+ if (!query) return { status: 'not_applicable', matchedTerms: [], missingTerms: [] };
161
+ const queryTerms = Array.from(new Set(query.toLowerCase().split(/[^a-z0-9]+/).filter((term) => term.length >= 3)));
162
+ if (queryTerms.length === 0) return { status: 'not_applicable', matchedTerms: [], missingTerms: [] };
163
+
164
+ const haystack = chunks.map((chunk) => `${chunk.title || ''}\n${chunk.section || ''}\n${chunk.text}`.toLowerCase()).join('\n');
165
+ const matchedTerms = queryTerms.filter((term) => haystack.includes(term));
166
+ const missingTerms = queryTerms.filter((term) => !haystack.includes(term));
167
+
168
+ let status = 'insufficient';
169
+ if (missingTerms.length === 0 && chunks.length > 0) status = 'sufficient';
170
+ else if (matchedTerms.length > 0 && chunks.length > 0) status = 'partial';
171
+
172
+ return { status, matchedTerms, missingTerms };
173
+ }
174
+
175
+ function readFiles(files, dir, options = {}) {
176
+ const chunks = [];
177
+ let totalBytes = 0;
178
+ let truncatedFileCount = 0;
179
+ for (const file of files) {
180
+ if (chunks.length >= MAX_FILES) break;
181
+ if (totalBytes >= MAX_TOTAL_BYTES) break;
182
+ try {
183
+ const bounded = readBoundedText(file.path, file.size);
184
+ totalBytes += bounded.sampledBytes;
185
+ if (bounded.truncated) truncatedFileCount += 1;
186
+ chunks.push(...chunkContent(relative(dir, file.path), bounded.content, {
187
+ ...options,
188
+ truncated: bounded.truncated,
189
+ originalBytes: bounded.originalBytes,
190
+ sampledBytes: bounded.sampledBytes,
191
+ }));
192
+ } catch { /* skip unreadable files */ }
193
+ }
194
+ return { chunks, totalBytes, truncatedFileCount };
195
+ }
196
+
197
+ function buildPrompt(dir, chunks, { format, query, mode }) {
198
+ const overlays = getActiveOverlays(dir);
199
+ const instructions = {
200
+ summary: `Produce a structured distillation with sections: Executive Summary, Key Themes, Decisions & Actions, Open Questions, File Index.`,
201
+ decisions: `Focus only on decisions, recommendations, owners, deadlines, blockers, and action items.`,
202
+ full: `Produce a comprehensive distillation with Executive Summary, Background & Context, Key Themes, Architecture & Design Decisions, Decisions & Actions, Risks & Open Questions, Glossary, and File Index.`,
203
+ extract: `Answer the query using only the provided evidence. Output sections: Answer, Evidence, Gaps, Sufficiency (sufficient | partial | insufficient).`,
204
+ };
205
+
206
+ const fileBlocks = chunks.map((chunk) => {
207
+ const meta = [
208
+ `source=${chunk.rel}`,
209
+ `chunk=${chunk.id}`,
210
+ `range=${chunk.start}-${chunk.end}`,
211
+ chunk.title ? `title=${chunk.title}` : null,
212
+ chunk.section ? `section=${chunk.section}` : null,
213
+ ].filter(Boolean).join(' | ');
214
+ return `### ${meta}\n\n${chunk.text}`;
215
+ }).join('\n\n');
216
+
217
+ const overlayBlock = overlays.length > 0
218
+ ? `Active domain overlays:\n${overlays.map((overlay) => `- ${overlay.domain}: ${overlay.objective}${overlay.scope ? ` (scope: ${overlay.scope})` : ''}`).join('\n')}\n\nUse overlays only as temporary scope guidance, not as permanent truth.\n\n`
219
+ : '';
220
+
221
+ return `You are analyzing document evidence from: ${dir}\n\nMode: ${mode}\n${query ? `Query: ${query}\n` : ''}${instructions[format] || instructions.summary}\n\nRules:\n- Use only provided evidence.\n- Prefer query-focused extraction when a query is provided.\n- Every substantive claim must cite one or more chunk IDs in inline form like [source: path#chunk].\n- If evidence is incomplete, say so explicitly.\n- Do not invent facts.\n\n${overlayBlock}---\n\n${fileBlocks}`;
222
+ }
223
+
224
+ function buildStructuredOutput(dir, chunks, { format, query, mode }) {
225
+ const overlays = getActiveOverlays(dir);
226
+ const sufficiency = evaluateSufficiency(chunks, query);
227
+ const files = new Map();
228
+ for (const chunk of chunks) {
229
+ if (!files.has(chunk.rel)) {
230
+ files.set(chunk.rel, {
231
+ file: chunk.rel,
232
+ title: chunk.title || basename(chunk.rel),
233
+ sections: new Set(),
234
+ chunkIds: [],
235
+ truncated: Boolean(chunk.truncated),
236
+ sampledBytes: Number(chunk.sampledBytes || 0),
237
+ originalBytes: Number(chunk.originalBytes || 0),
238
+ });
239
+ }
240
+ const entry = files.get(chunk.rel);
241
+ if (chunk.section) entry.sections.add(chunk.section);
242
+ entry.chunkIds.push(chunk.id);
243
+ entry.truncated = entry.truncated || Boolean(chunk.truncated);
244
+ entry.sampledBytes = Math.max(entry.sampledBytes, Number(chunk.sampledBytes || 0));
245
+ entry.originalBytes = Math.max(entry.originalBytes, Number(chunk.originalBytes || 0));
246
+ }
247
+
248
+ return {
249
+ mode,
250
+ format,
251
+ query: query || null,
252
+ overlays: overlays.map((overlay) => ({
253
+ id: overlay.id,
254
+ domain: overlay.domain,
255
+ objective: overlay.objective,
256
+ scope: overlay.scope,
257
+ attachTo: overlay.attachTo,
258
+ })),
259
+ fileCount: files.size,
260
+ chunkCount: chunks.length,
261
+ sufficiency,
262
+ files: Array.from(files.values()).map((entry) => ({
263
+ file: entry.file,
264
+ title: entry.title,
265
+ sections: Array.from(entry.sections),
266
+ chunkIds: entry.chunkIds,
267
+ truncated: entry.truncated,
268
+ sampledBytes: entry.sampledBytes,
269
+ originalBytes: entry.originalBytes,
270
+ })),
271
+ evidence: chunks.map((chunk) => ({
272
+ id: chunk.id,
273
+ file: chunk.rel,
274
+ title: chunk.title,
275
+ section: chunk.section,
276
+ citation: `[source: ${chunk.id}]`,
277
+ truncated: Boolean(chunk.truncated),
278
+ text: chunk.text,
279
+ })),
280
+ prompt: buildPrompt(dir, chunks, { format, query, mode }),
281
+ };
282
+ }
283
+
284
+ function hasClaude() {
285
+ try {
286
+ execSync('claude --version', { stdio: 'pipe', timeout: 3000 });
287
+ return true;
288
+ } catch {
289
+ return false;
290
+ }
291
+ }
292
+
293
+ function parseArgs(argv) {
294
+ const positional = argv.filter((arg) => !arg.startsWith('--'));
295
+ const flags = Object.fromEntries(argv.filter((arg) => arg.startsWith('--')).map((arg) => {
296
+ const [k, ...v] = arg.slice(2).split('=');
297
+ return [k, v.length ? v.join('=') : true];
298
+ }));
299
+ return { positional, flags };
300
+ }
301
+
302
+ export async function distill(dir, { format = 'summary', query = '', mode = 'auto', out = null, depth = 3, ext = null } = {}) {
303
+ const resolvedDir = dir.startsWith('/') ? dir : join(process.cwd(), dir);
304
+ if (!existsSync(resolvedDir)) throw new Error(`Directory not found: ${resolvedDir}`);
305
+
306
+ const allowedExts = ext ? new Set(ext.split(',').map((value) => value.trim().startsWith('.') ? value.trim() : `.${value.trim()}`)) : null;
307
+ const files = discoverFiles(resolvedDir, depth, allowedExts);
308
+ if (files.length === 0) throw new Error(`No readable text files found in ${resolvedDir}`);
309
+
310
+ process.stderr.write(`\n🔬 Distilling ${files.length} file(s) from ${resolvedDir}\n`);
311
+ if (files.length > MAX_FILES) process.stderr.write(` (scanning first ${MAX_FILES} files by name order)\n`);
312
+
313
+ const { chunks, totalBytes, truncatedFileCount } = readFiles(files, resolvedDir);
314
+ process.stderr.write(` Built ${chunks.length} chunk(s) · ${Math.round(totalBytes / 1024)} KB\n`);
315
+ if (truncatedFileCount > 0) process.stderr.write(` Sampled ${truncatedFileCount} large file(s) with bounded head/tail reads\n`);
316
+
317
+ const selectedChunks = query ? selectChunks(chunks, query) : chunks.slice(0, 24);
318
+ process.stderr.write(` Selected ${selectedChunks.length} chunk(s) for analysis${query ? ' using query-focused retrieval' : ''}\n`);
319
+
320
+ const structured = buildStructuredOutput(resolvedDir, selectedChunks, { format, query, mode });
321
+
322
+ if (mode === 'json') {
323
+ const output = `${JSON.stringify(structured, null, 2)}\n`;
324
+ if (out) {
325
+ try { mkdirSync(dirname(out), { recursive: true }); } catch { /* exists */ }
326
+ writeFileSync(out, output);
327
+ process.stderr.write(`✓ Distillation JSON written to: ${out}\n`);
328
+ } else {
329
+ process.stdout.write(output);
330
+ }
331
+ return structured;
332
+ }
333
+
334
+ const prompt = structured.prompt;
335
+ if (mode === 'prompt' || !hasClaude()) {
336
+ const tmpFile = out || join(homedir(), '.cx', 'distill-prompt.txt');
337
+ try { mkdirSync(dirname(tmpFile), { recursive: true }); } catch { /* exists */ }
338
+ writeFileSync(tmpFile, prompt);
339
+ process.stderr.write(`\n⚠ Prompt written to: ${tmpFile}\n`);
340
+ if (mode !== 'prompt' && !hasClaude()) process.stderr.write(' claude CLI not found. Paste the prompt into your preferred model interface.\n\n');
341
+ return structured;
342
+ }
343
+
344
+ process.stderr.write(' Invoking claude --print …\n\n');
345
+ const result = spawnSync('claude', ['--print', prompt], {
346
+ encoding: 'utf8',
347
+ maxBuffer: 10 * 1024 * 1024,
348
+ timeout: 120_000,
349
+ });
350
+
351
+ if (result.error) throw result.error;
352
+ const output = result.stdout || '';
353
+ const errOut = result.stderr || '';
354
+ if (result.status !== 0) {
355
+ if (errOut) process.stderr.write(`${errOut}\n`);
356
+ throw new Error(`claude exited with status ${result.status}`);
357
+ }
358
+
359
+ if (out) {
360
+ try { mkdirSync(dirname(out), { recursive: true }); } catch { /* exists */ }
361
+ writeFileSync(out, output);
362
+ process.stderr.write(`✓ Distillation written to: ${out}\n`);
363
+ } else {
364
+ process.stdout.write(output);
365
+ }
366
+
367
+ return structured;
368
+ }
369
+
370
+ export async function runDistillCli(argv = process.argv.slice(2)) {
371
+ const { positional, flags } = parseArgs(argv);
372
+ const dir = positional[0];
373
+ if (!dir) throw new Error('Usage: construct distill <dir> [--format=summary|decisions|full|extract] [--query=TEXT] [--mode=auto|prompt|json] [--out=FILE] [--depth=N] [--ext=LIST]');
374
+ return distill(dir, {
375
+ format: flags.format || 'summary',
376
+ query: typeof flags.query === 'string' ? flags.query : '',
377
+ mode: flags.mode || 'auto',
378
+ out: flags.out || null,
379
+ depth: flags.depth ? parseInt(flags.depth, 10) : 3,
380
+ ext: flags.ext || null,
381
+ });
382
+ }
383
+
384
+ const isMain = import.meta.url === `file://${process.argv[1]}`;
385
+ if (isMain) {
386
+ const { positional, flags } = parseArgs(process.argv.slice(2));
387
+ const dir = positional[0];
388
+ if (!dir) {
389
+ process.stderr.write('Usage: construct distill <dir> [--format=summary|decisions|full|extract] [--query=TEXT] [--mode=auto|prompt|json] [--out=FILE] [--depth=N] [--ext=LIST]\n');
390
+ process.exit(1);
391
+ }
392
+ distill(dir, {
393
+ format: flags.format || 'summary',
394
+ query: typeof flags.query === 'string' ? flags.query : '',
395
+ mode: flags.mode || 'auto',
396
+ out: flags.out || null,
397
+ depth: flags.depth ? parseInt(flags.depth, 10) : 3,
398
+ ext: flags.ext || null,
399
+ }).catch((err) => {
400
+ process.stderr.write(`Error: ${err.message}\n`);
401
+ process.exit(1);
402
+ });
403
+ }
@@ -0,0 +1,139 @@
1
+ /**
2
+ * lib/efficiency.mjs — session read-efficiency reporting.
3
+ *
4
+ * Reads ~/.cx/session-efficiency.json, summarizes repeated/large reads, and
5
+ * formats actionable recommendations for the construct efficiency command.
6
+ */
7
+ import { existsSync, readFileSync } from 'node:fs';
8
+ import { join } from 'node:path';
9
+
10
+ const HIGH_BYTES_THRESHOLD = 750_000;
11
+ const HIGH_REPEATED_RATIO = 0.4;
12
+ const HIGH_LARGE_READ_COUNT = 3;
13
+
14
+ export function readEfficiencyLog(homeDir) {
15
+ const filePath = join(homeDir, '.cx', 'session-efficiency.json');
16
+ if (!existsSync(filePath)) return null;
17
+ try {
18
+ return JSON.parse(readFileSync(filePath, 'utf8'));
19
+ } catch {
20
+ return null;
21
+ }
22
+ }
23
+
24
+ function topRepeatedFiles(files = {}) {
25
+ return Object.entries(files)
26
+ .map(([filePath, value]) => ({
27
+ path: filePath,
28
+ count: Number(value?.count || 0),
29
+ size: Number(value?.size || 0),
30
+ lastReadAt: value?.lastReadAt || null,
31
+ }))
32
+ .filter((entry) => entry.count > 1)
33
+ .sort((a, b) => b.count - a.count || b.size - a.size || a.path.localeCompare(b.path))
34
+ .slice(0, 10);
35
+ }
36
+
37
+ export function summarizeEfficiencyData(stats) {
38
+ if (!stats) {
39
+ return {
40
+ status: 'unavailable',
41
+ summary: 'No read-efficiency data recorded yet',
42
+ recommendation: 'Continue. Efficiency tracking will appear after file reads.',
43
+ readCount: 0,
44
+ uniqueFileCount: 0,
45
+ repeatedReadCount: 0,
46
+ largeReadCount: 0,
47
+ totalBytesRead: 0,
48
+ repeatedReadRatio: 0,
49
+ topRepeatedFiles: [],
50
+ lastUpdatedAt: null,
51
+ };
52
+ }
53
+
54
+ const readCount = Number(stats.readCount || 0);
55
+ const uniqueFileCount = Number(stats.uniqueFileCount || 0);
56
+ const repeatedReadCount = Number(stats.repeatedReadCount || 0);
57
+ const largeReadCount = Number(stats.largeReadCount || 0);
58
+ const totalBytesRead = Number(stats.totalBytesRead || 0);
59
+ const repeatedReadRatio = readCount > 0 ? Number((repeatedReadCount / readCount).toFixed(3)) : 0;
60
+ const repeatedFiles = topRepeatedFiles(stats.files);
61
+
62
+ let status = 'healthy';
63
+ let recommendation = 'Continue. Read pattern is compact enough for the current session.';
64
+ if (totalBytesRead >= HIGH_BYTES_THRESHOLD) {
65
+ status = 'degraded';
66
+ recommendation = 'Run construct distill with a focused query, then compact context before more broad exploration.';
67
+ } else if (repeatedReadRatio >= HIGH_REPEATED_RATIO || repeatedReadCount >= 5) {
68
+ status = 'degraded';
69
+ recommendation = 'Use rg to narrow the next file set, or run construct distill instead of re-reading the same files.';
70
+ } else if (largeReadCount >= HIGH_LARGE_READ_COUNT) {
71
+ status = 'configured';
72
+ recommendation = 'Prefer targeted reads under 400 lines before another large read.';
73
+ }
74
+
75
+ const summary = [
76
+ `${readCount} reads`,
77
+ `${uniqueFileCount} files`,
78
+ repeatedReadCount ? `${repeatedReadCount} repeated` : null,
79
+ largeReadCount ? `${largeReadCount} large` : null,
80
+ totalBytesRead ? `${Math.round(totalBytesRead / 1024)} KB` : null,
81
+ ].filter(Boolean).join(' · ');
82
+
83
+ return {
84
+ status,
85
+ summary,
86
+ recommendation,
87
+ readCount,
88
+ uniqueFileCount,
89
+ repeatedReadCount,
90
+ largeReadCount,
91
+ totalBytesRead,
92
+ repeatedReadRatio,
93
+ topRepeatedFiles: repeatedFiles,
94
+ lastUpdatedAt: stats.lastUpdatedAt || null,
95
+ };
96
+ }
97
+
98
+ export function buildCompactEfficiencyDigest(stats) {
99
+ const data = summarizeEfficiencyData(stats);
100
+ const repeated = data.topRepeatedFiles.slice(0, 3).map((file) => `${file.count}x ${file.path}`).join(' · ');
101
+
102
+ return {
103
+ ...data,
104
+ repeated,
105
+ compact: data.status === 'unavailable'
106
+ ? data.summary
107
+ : repeated
108
+ ? `${data.summary} · ${data.recommendation} · Hot spots: ${repeated}`
109
+ : `${data.summary} · ${data.recommendation}`,
110
+ };
111
+ }
112
+
113
+ export function formatEfficiencyReport(data, colors = {}) {
114
+ const C = { bold: '', dim: '', reset: '', green: '', yellow: '', red: '', ...colors };
115
+ const statusColor = data.status === 'healthy' ? C.green : data.status === 'configured' ? C.yellow : data.status === 'degraded' ? C.red : '';
116
+ const lines = [];
117
+ lines.push(`${C.bold}Construct Efficiency Report${C.reset}`);
118
+ lines.push('═══════════════════════════');
119
+ lines.push('');
120
+ lines.push(`Status: ${statusColor}${data.status}${C.reset}`);
121
+ lines.push(`Reads: ${data.readCount.toLocaleString()} (${data.uniqueFileCount.toLocaleString()} unique files)`);
122
+ lines.push(`Repeated reads: ${data.repeatedReadCount.toLocaleString()} (${(data.repeatedReadRatio * 100).toFixed(1)}%)`);
123
+ lines.push(`Large reads: ${data.largeReadCount.toLocaleString()}`);
124
+ lines.push(`Bytes read: ${Math.round(data.totalBytesRead / 1024).toLocaleString()} KB`);
125
+ if (data.lastUpdatedAt) lines.push(`${C.dim}Last updated: ${data.lastUpdatedAt}${C.reset}`);
126
+ lines.push('');
127
+ lines.push(`Recommendation: ${data.recommendation}`);
128
+
129
+ if (data.topRepeatedFiles.length > 0) {
130
+ lines.push('');
131
+ lines.push('Top Repeated Files:');
132
+ for (const file of data.topRepeatedFiles) {
133
+ lines.push(` ${String(file.count).padStart(3)}x ${file.path}`);
134
+ }
135
+ }
136
+
137
+ lines.push('');
138
+ return lines.join('\n');
139
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * lib/env-config.mjs — <one-line purpose>
3
+ *
4
+ * <2–6 line summary: what it does, who calls it, key side effects.>
5
+ */
6
+ import fs from 'node:fs';
7
+ import os from 'node:os';
8
+ import path from 'node:path';
9
+
10
+ export function parseEnvFile(filePath) {
11
+ if (!filePath || !fs.existsSync(filePath)) return {};
12
+ const env = {};
13
+ for (const rawLine of fs.readFileSync(filePath, 'utf8').split(/\r?\n/)) {
14
+ const line = rawLine.trim();
15
+ if (!line || line.startsWith('#')) continue;
16
+ const match = line.match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
17
+ if (!match) continue;
18
+ const [, key, rawValue] = match;
19
+ env[key] = rawValue.replace(/^["']|["']$/g, '');
20
+ }
21
+ return env;
22
+ }
23
+
24
+ export function getUserConfigDir(homeDir = os.homedir()) {
25
+ return path.join(homeDir, '.construct');
26
+ }
27
+
28
+ export function getUserEnvPath(homeDir = os.homedir()) {
29
+ return path.join(getUserConfigDir(homeDir), 'config.env');
30
+ }
31
+
32
+ export function ensureUserConfigDir(homeDir = os.homedir()) {
33
+ const dir = getUserConfigDir(homeDir);
34
+ fs.mkdirSync(dir, { recursive: true });
35
+ return dir;
36
+ }
37
+
38
+ export function writeEnvValues(filePath, values = {}) {
39
+ const existing = parseEnvFile(filePath);
40
+ const merged = { ...existing, ...values };
41
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
42
+ const content = Object.entries(merged)
43
+ .filter(([, value]) => value !== undefined && value !== null && value !== '')
44
+ .map(([key, value]) => `${key}=${value}`)
45
+ .join('\n');
46
+ fs.writeFileSync(filePath, `${content}\n`, 'utf8');
47
+ }
48
+
49
+ export function loadConstructEnv({ rootDir, homeDir = os.homedir(), env = process.env } = {}) {
50
+ const rootEnv = rootDir ? parseEnvFile(path.join(rootDir, '.env')) : {};
51
+ const userEnv = parseEnvFile(getUserEnvPath(homeDir));
52
+ return { ...rootEnv, ...userEnv, ...env };
53
+ }
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * lib/eval-harness.mjs — <one-line purpose>
4
+ *
5
+ * <2–6 line summary.>
6
+ */
7
+ import fs from "node:fs";
8
+ import path from "node:path";
9
+ import { fileURLToPath } from "node:url";
10
+
11
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
12
+ const root = path.join(__dirname, "..");
13
+
14
+ /**
15
+ * Basic evaluation harness for Construct agents.
16
+ * This script runs "dry runs" or simulated interactions against agent prompts
17
+ * to verify they would call the right tools or produce the right outcomes.
18
+ */
19
+
20
+ export async function runEval(agentName, input, expectedPatterns = []) {
21
+ const registry = JSON.parse(fs.readFileSync(path.join(root, "agents", "registry.json"), "utf8"));
22
+ const agent = registry.agents.find(a => a.name === agentName) ||
23
+ registry.personas.find(p => p.name === agentName);
24
+
25
+ if (!agent) throw new Error(`Agent ${agentName} not found in registry`);
26
+
27
+ // In a real production setup, this would call the LLM.
28
+ // For this harness, we are checking the "Static Intent" —
29
+ // can the agent logically fulfill the request based on its prompt?
30
+
31
+ const results = {
32
+ agent: agentName,
33
+ input,
34
+ passed: true,
35
+ findings: []
36
+ };
37
+
38
+ for (const pattern of expectedPatterns) {
39
+ const regex = new RegExp(pattern, "i");
40
+ const foundInPrompt = regex.test(agent.prompt || "");
41
+ const foundInShared = (registry.sharedGuidance || []).some(g => regex.test(g));
42
+
43
+ if (!foundInPrompt && !foundInShared) {
44
+ results.passed = false;
45
+ results.findings.push(`MISSING: Pattern "${pattern}" not supported by agent prompt or shared guidance`);
46
+ } else {
47
+ results.findings.push(`OK: Pattern "${pattern}" is supported`);
48
+ }
49
+ }
50
+
51
+ return results;
52
+ }
53
+
54
+ if (process.argv[1] && import.meta.url === `file://${process.argv[1]}`) {
55
+ // Simple example run
56
+ const testAgent = process.argv[2] || "engineer";
57
+ const result = await runEval(testAgent, "Fix the bug", ["Edit", "Read", "Bash"]);
58
+ console.log(JSON.stringify(result, null, 2));
59
+ }