@geraldmaron/construct 1.0.12 → 1.0.15

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 (337) hide show
  1. package/README.md +14 -2
  2. package/bin/construct +262 -14
  3. package/bin/construct-postinstall.mjs +23 -2
  4. package/commands/build/feature.md +0 -6
  5. package/commands/build/fix.md +0 -6
  6. package/commands/design/access.md +0 -6
  7. package/commands/design/flow.md +0 -6
  8. package/commands/design/ui.md +0 -6
  9. package/commands/measure/experiment.md +0 -6
  10. package/commands/measure/metrics.md +0 -6
  11. package/commands/measure/results.md +0 -6
  12. package/commands/plan/api.md +0 -6
  13. package/commands/plan/challenge.md +0 -6
  14. package/commands/plan/decide.md +0 -6
  15. package/commands/plan/feature.md +0 -7
  16. package/commands/plan/requirements.md +0 -6
  17. package/commands/remember/context.md +0 -6
  18. package/commands/remember/handoff.md +0 -6
  19. package/commands/remember/runbook.md +0 -6
  20. package/commands/review/code.md +0 -6
  21. package/commands/review/quality.md +0 -6
  22. package/commands/review/security.md +0 -6
  23. package/commands/ship/ready.md +0 -6
  24. package/commands/ship/release.md +0 -6
  25. package/commands/ship/status.md +0 -6
  26. package/commands/understand/docs.md +0 -6
  27. package/commands/understand/research.md +0 -6
  28. package/commands/understand/this.md +0 -6
  29. package/commands/understand/why.md +0 -6
  30. package/commands/work/clean.md +0 -6
  31. package/commands/work/drive.md +0 -6
  32. package/commands/work/optimize-prompts.md +0 -6
  33. package/commands/work/parallel-review.md +0 -6
  34. package/lib/auto-docs.mjs +11 -7
  35. package/lib/beads-client.mjs +19 -1
  36. package/lib/comment-lint.mjs +5 -1
  37. package/lib/context-state.mjs +15 -3
  38. package/lib/contracts/validate.mjs +54 -10
  39. package/lib/contracts/violation-log.mjs +154 -0
  40. package/lib/dashboard-static.mjs +7 -4
  41. package/lib/doc-stamp.mjs +16 -0
  42. package/lib/docs-verify.mjs +1 -8
  43. package/lib/doctor/watchers/bd-watch.mjs +6 -2
  44. package/lib/document-extract/docling-client.mjs +114 -0
  45. package/lib/document-extract/docling-sidecar.py +122 -0
  46. package/lib/document-extract/whisper-client.mjs +79 -0
  47. package/lib/document-extract.mjs +73 -8
  48. package/lib/document-ingest.mjs +26 -4
  49. package/lib/embed/daemon.mjs +26 -1
  50. package/lib/embed/docs-lifecycle.mjs +19 -0
  51. package/lib/embed/inbox.mjs +85 -2
  52. package/lib/embed/recommendation-store.mjs +29 -0
  53. package/lib/flavors/loader.mjs +1 -1
  54. package/lib/gates-audit.mjs +18 -12
  55. package/lib/handoffs/contract.mjs +2 -2
  56. package/lib/hooks/_lib/input.mjs +52 -0
  57. package/lib/hooks/adaptive-lint.mjs +4 -0
  58. package/lib/hooks/agent-tracker.mjs +59 -15
  59. package/lib/hooks/audit-reads.mjs +83 -42
  60. package/lib/hooks/audit-trail.mjs +28 -18
  61. package/lib/hooks/bash-output-logger.mjs +8 -2
  62. package/lib/hooks/block-no-verify.mjs +4 -0
  63. package/lib/hooks/ci-status-check.mjs +4 -0
  64. package/lib/hooks/comment-lint.mjs +6 -4
  65. package/lib/hooks/config-protection.mjs +4 -0
  66. package/lib/hooks/context-watch.mjs +4 -0
  67. package/lib/hooks/context-window-recovery.mjs +4 -0
  68. package/lib/hooks/dep-audit.mjs +12 -5
  69. package/lib/hooks/doc-coupling-check.mjs +5 -1
  70. package/lib/hooks/edit-accumulator.mjs +25 -10
  71. package/lib/hooks/edit-error-recovery.mjs +4 -0
  72. package/lib/hooks/edit-guard.mjs +4 -0
  73. package/lib/hooks/guard-bash.mjs +4 -0
  74. package/lib/hooks/mcp-audit.mjs +4 -0
  75. package/lib/hooks/mcp-health-check.mjs +4 -0
  76. package/lib/hooks/model-fallback.mjs +7 -11
  77. package/lib/hooks/policy-engine.mjs +4 -0
  78. package/lib/hooks/post-merge-docs-check.mjs +4 -0
  79. package/lib/hooks/post-merge-tracking.mjs +82 -0
  80. package/lib/hooks/pre-compact.mjs +4 -0
  81. package/lib/hooks/pre-push-gate.mjs +84 -231
  82. package/lib/hooks/proactive-activation.mjs +5 -2
  83. package/lib/hooks/readme-age-check.mjs +4 -0
  84. package/lib/hooks/registry-sync.mjs +35 -20
  85. package/lib/hooks/rule-verifier.mjs +3 -0
  86. package/lib/hooks/scan-secrets.mjs +4 -0
  87. package/lib/hooks/session-optimize.mjs +4 -0
  88. package/lib/hooks/session-reflect.mjs +4 -0
  89. package/lib/hooks/session-start.mjs +49 -2
  90. package/lib/hooks/session-tracking-refresh.mjs +70 -0
  91. package/lib/hooks/stop-notify.mjs +13 -2
  92. package/lib/hooks/stop-typecheck.mjs +4 -0
  93. package/lib/hooks/test-watch.mjs +8 -4
  94. package/lib/ingest/chunker.mjs +94 -0
  95. package/lib/ingest/pipeline.mjs +53 -0
  96. package/lib/ingest/store.mjs +82 -0
  97. package/lib/init-unified.mjs +90 -23
  98. package/lib/intake/attribution.mjs +77 -0
  99. package/lib/intake/intake-config.mjs +3 -0
  100. package/lib/intake/manifest.mjs +107 -0
  101. package/lib/intake/poll-lock.mjs +136 -0
  102. package/lib/intake/prepare.mjs +42 -4
  103. package/lib/knowledge/search.mjs +12 -0
  104. package/lib/logging/rotate.mjs +394 -0
  105. package/lib/mcp/server.mjs +142 -1
  106. package/lib/mcp/tools/project.mjs +2 -2
  107. package/lib/mcp/tools/skills.mjs +6 -3
  108. package/lib/mcp/tools/telemetry.mjs +1 -1
  109. package/lib/mcp/tools/workflow.mjs +6 -2
  110. package/lib/observation-store.mjs +14 -5
  111. package/lib/ollama-manager.mjs +32 -28
  112. package/lib/opencode-config.mjs +10 -3
  113. package/lib/orchestration/routing-tables.mjs +176 -0
  114. package/lib/orchestration-policy.mjs +18 -106
  115. package/lib/parity.mjs +48 -7
  116. package/lib/profiles/lifecycle.mjs +19 -1
  117. package/lib/project-init-shared.mjs +11 -5
  118. package/lib/project-root.mjs +104 -0
  119. package/lib/prompt-composer.js +11 -1
  120. package/lib/roles/catalog.mjs +1 -1
  121. package/lib/roles/event-bus.mjs +29 -9
  122. package/lib/roles/router.mjs +8 -7
  123. package/lib/runtime/uv-bootstrap.mjs +129 -0
  124. package/lib/runtime/whisper-bootstrap.mjs +102 -0
  125. package/lib/server/index.mjs +53 -10
  126. package/lib/server/static/index.html +1 -15
  127. package/lib/specialists/postconditions.mjs +1 -1
  128. package/lib/status.mjs +1 -1
  129. package/lib/storage/backup.mjs +2 -2
  130. package/lib/sync/skill-frontmatter.mjs +113 -21
  131. package/lib/telemetry/intent-verifications.mjs +16 -3
  132. package/lib/telemetry/skill-calls.mjs +12 -3
  133. package/lib/tracking-surfaces.mjs +377 -0
  134. package/lib/validators/skills.mjs +86 -54
  135. package/lib/worker/trace.mjs +19 -2
  136. package/package.json +10 -6
  137. package/personas/construct.md +4 -7
  138. package/platforms/claude/settings.template.json +29 -28
  139. package/rules/common/beads-hygiene.md +3 -9
  140. package/rules/common/code-review.md +3 -6
  141. package/rules/common/coding-style.md +3 -6
  142. package/rules/common/comments.md +3 -7
  143. package/rules/common/commit-approval.md +3 -6
  144. package/rules/common/cx-agent-routing.md +3 -7
  145. package/rules/common/cx-skill-routing.md +3 -3
  146. package/rules/common/doc-ownership.md +3 -8
  147. package/rules/common/efficiency.md +3 -8
  148. package/rules/common/framing.md +3 -8
  149. package/rules/common/git-workflow.md +3 -5
  150. package/rules/common/no-fabrication.md +4 -12
  151. package/rules/common/patterns.md +3 -5
  152. package/rules/common/release-gates.md +3 -8
  153. package/rules/common/research.md +3 -8
  154. package/rules/common/review-before-change.md +3 -9
  155. package/rules/common/security.md +3 -6
  156. package/rules/common/skill-composition.md +3 -7
  157. package/rules/common/testing.md +3 -6
  158. package/rules/golang/coding-style.md +1 -5
  159. package/rules/golang/hooks.md +1 -5
  160. package/rules/golang/patterns.md +1 -5
  161. package/rules/golang/security.md +1 -5
  162. package/rules/golang/testing.md +1 -5
  163. package/rules/python/coding-style.md +1 -5
  164. package/rules/python/hooks.md +1 -5
  165. package/rules/python/patterns.md +1 -5
  166. package/rules/python/security.md +1 -5
  167. package/rules/python/testing.md +1 -5
  168. package/rules/swift/coding-style.md +1 -5
  169. package/rules/swift/hooks.md +1 -5
  170. package/rules/swift/patterns.md +1 -5
  171. package/rules/swift/security.md +1 -5
  172. package/rules/swift/testing.md +1 -5
  173. package/rules/typescript/coding-style.md +1 -5
  174. package/rules/typescript/hooks.md +1 -5
  175. package/rules/typescript/patterns.md +1 -5
  176. package/rules/typescript/security.md +1 -5
  177. package/rules/typescript/testing.md +1 -5
  178. package/rules/web/coding-style.md +3 -5
  179. package/rules/web/design-quality.md +3 -5
  180. package/rules/web/hooks.md +3 -5
  181. package/rules/web/patterns.md +3 -5
  182. package/rules/web/performance.md +3 -5
  183. package/rules/web/security.md +3 -5
  184. package/rules/web/testing.md +3 -5
  185. package/scripts/sync-specialists.mjs +269 -75
  186. package/skills/ai/agent-dev.md +4 -5
  187. package/skills/ai/llm-security.md +4 -5
  188. package/skills/ai/ml-ops.md +4 -5
  189. package/skills/ai/orchestration-workflow.md +4 -5
  190. package/skills/ai/prompt-and-eval.md +4 -5
  191. package/skills/ai/prompt-optimizer.md +4 -6
  192. package/skills/ai/rag-system.md +4 -5
  193. package/skills/architecture/api-design.md +4 -5
  194. package/skills/architecture/caching.md +4 -5
  195. package/skills/architecture/cloud-native.md +4 -5
  196. package/skills/architecture/message-queue.md +4 -5
  197. package/skills/architecture/security-arch.md +4 -5
  198. package/skills/compliance/ai-disclosure.md +4 -5
  199. package/skills/compliance/data-privacy.md +4 -5
  200. package/skills/compliance/license-audit.md +4 -5
  201. package/skills/compliance/regulatory-review.md +4 -5
  202. package/skills/development/cpp.md +4 -5
  203. package/skills/development/go.md +4 -5
  204. package/skills/development/java.md +4 -5
  205. package/skills/development/kotlin.md +4 -5
  206. package/skills/development/mobile-crossplatform.md +4 -5
  207. package/skills/development/python.md +4 -5
  208. package/skills/development/rust.md +4 -5
  209. package/skills/development/shell.md +4 -5
  210. package/skills/development/swift.md +4 -5
  211. package/skills/development/typescript.md +4 -5
  212. package/skills/devops/ci-cd.md +4 -5
  213. package/skills/devops/containerization.md +4 -5
  214. package/skills/devops/cost-optimization.md +4 -5
  215. package/skills/devops/data-engineering.md +4 -5
  216. package/skills/devops/database.md +4 -5
  217. package/skills/devops/dependency-management.md +4 -5
  218. package/skills/devops/devsecops.md +4 -5
  219. package/skills/devops/git-workflow.md +4 -5
  220. package/skills/devops/incident-response.md +4 -5
  221. package/skills/devops/monorepo.md +4 -5
  222. package/skills/devops/observability.md +4 -5
  223. package/skills/devops/performance.md +4 -5
  224. package/skills/devops/testing.md +4 -5
  225. package/skills/docs/adr-workflow.md +4 -7
  226. package/skills/docs/backlog-proposal-workflow.md +4 -3
  227. package/skills/docs/customer-profile-workflow.md +4 -3
  228. package/skills/docs/document-ingest-workflow.md +4 -3
  229. package/skills/docs/evidence-ingest-workflow.md +4 -3
  230. package/skills/docs/init-docs.md +4 -5
  231. package/skills/docs/init-project.md +4 -5
  232. package/skills/docs/prd-workflow.md +4 -7
  233. package/skills/docs/prfaq-workflow.md +4 -3
  234. package/skills/docs/product-intelligence-review.md +4 -3
  235. package/skills/docs/product-intelligence-workflow.md +4 -6
  236. package/skills/docs/product-signal-workflow.md +4 -5
  237. package/skills/docs/research-workflow.md +4 -5
  238. package/skills/docs/runbook-workflow.md +4 -5
  239. package/skills/docs/strategy-workflow.md +4 -5
  240. package/skills/exploration/dependency-graph-reading.md +4 -7
  241. package/skills/exploration/repo-map.md +4 -5
  242. package/skills/exploration/tracer-bullet-method.md +4 -7
  243. package/skills/exploration/unknown-codebase-onboarding.md +4 -7
  244. package/skills/frameworks/django.md +4 -5
  245. package/skills/frameworks/nextjs.md +4 -5
  246. package/skills/frameworks/react.md +4 -5
  247. package/skills/frameworks/spring-boot.md +4 -5
  248. package/skills/frontend-design/accessibility.md +4 -5
  249. package/skills/frontend-design/component-patterns.md +4 -5
  250. package/skills/frontend-design/engineering.md +4 -5
  251. package/skills/frontend-design/state-management.md +4 -5
  252. package/skills/frontend-design/ui-aesthetics.md +4 -5
  253. package/skills/frontend-design/ux-principles.md +4 -5
  254. package/skills/operating/change-management.md +4 -8
  255. package/skills/operating/incident-response.md +4 -8
  256. package/skills/operating/oncall-rotation.md +4 -7
  257. package/skills/operating/orchestration-reference.md +4 -10
  258. package/skills/quality-gates/review-work.md +4 -5
  259. package/skills/quality-gates/verify-change.md +4 -5
  260. package/skills/quality-gates/verify-module.md +4 -5
  261. package/skills/quality-gates/verify-quality.md +4 -5
  262. package/skills/quality-gates/verify-security.md +4 -5
  263. package/skills/roles/architect.ai-systems.md +6 -9
  264. package/skills/roles/architect.data.md +6 -9
  265. package/skills/roles/architect.enterprise.md +6 -9
  266. package/skills/roles/architect.integration.md +6 -9
  267. package/skills/roles/architect.md +7 -14
  268. package/skills/roles/architect.platform.md +6 -9
  269. package/skills/roles/data-analyst.experiment.md +6 -9
  270. package/skills/roles/data-analyst.md +6 -9
  271. package/skills/roles/data-analyst.product-intelligence.md +7 -9
  272. package/skills/roles/data-analyst.product.md +6 -9
  273. package/skills/roles/data-analyst.telemetry.md +7 -9
  274. package/skills/roles/data-engineer.pipeline.md +6 -9
  275. package/skills/roles/data-engineer.vector-retrieval.md +7 -9
  276. package/skills/roles/data-engineer.warehouse.md +6 -9
  277. package/skills/roles/debugger.md +6 -9
  278. package/skills/roles/designer.accessibility.md +6 -9
  279. package/skills/roles/designer.md +7 -9
  280. package/skills/roles/engineer.ai.md +6 -9
  281. package/skills/roles/engineer.data.md +6 -9
  282. package/skills/roles/engineer.md +9 -9
  283. package/skills/roles/engineer.platform.md +6 -9
  284. package/skills/roles/operator.docs.md +6 -9
  285. package/skills/roles/operator.md +9 -9
  286. package/skills/roles/operator.release.md +6 -9
  287. package/skills/roles/operator.sre.md +6 -9
  288. package/skills/roles/orchestrator.md +6 -9
  289. package/skills/roles/product-manager.ai-product.md +6 -9
  290. package/skills/roles/product-manager.business-strategy.md +6 -9
  291. package/skills/roles/product-manager.enterprise.md +6 -9
  292. package/skills/roles/product-manager.growth.md +7 -9
  293. package/skills/roles/product-manager.md +7 -9
  294. package/skills/roles/product-manager.platform.md +6 -9
  295. package/skills/roles/product-manager.product.md +6 -9
  296. package/skills/roles/qa.ai-eval.md +8 -9
  297. package/skills/roles/qa.api-contract.md +7 -9
  298. package/skills/roles/qa.data-pipeline.md +7 -9
  299. package/skills/roles/qa.md +7 -9
  300. package/skills/roles/qa.test-automation.md +6 -9
  301. package/skills/roles/qa.web-ui.md +7 -9
  302. package/skills/roles/researcher.explorer.md +6 -9
  303. package/skills/roles/researcher.md +8 -9
  304. package/skills/roles/researcher.ux.md +6 -9
  305. package/skills/roles/reviewer.devil-advocate.md +6 -9
  306. package/skills/roles/reviewer.evaluator.md +6 -9
  307. package/skills/roles/reviewer.md +9 -9
  308. package/skills/roles/reviewer.trace.md +6 -9
  309. package/skills/roles/security.ai.md +7 -9
  310. package/skills/roles/security.appsec.md +6 -9
  311. package/skills/roles/security.cloud.md +6 -9
  312. package/skills/roles/security.legal-compliance.md +6 -9
  313. package/skills/roles/security.md +7 -9
  314. package/skills/roles/security.privacy.md +7 -9
  315. package/skills/roles/security.supply-chain.md +7 -9
  316. package/skills/security/blue-team.md +4 -5
  317. package/skills/security/code-audit.md +4 -5
  318. package/skills/security/pentest.md +4 -5
  319. package/skills/security/red-team.md +4 -5
  320. package/skills/security/threat-intel.md +4 -5
  321. package/skills/security/vuln-research.md +4 -5
  322. package/skills/strategy/competitive-landscape.md +4 -8
  323. package/skills/strategy/market-research-methods.md +4 -8
  324. package/skills/strategy/narrative-arc.md +4 -8
  325. package/skills/strategy/pricing-positioning.md +4 -8
  326. package/skills/utility/clean-code.md +4 -5
  327. package/specialists/policy-inventory.json +14 -0
  328. package/specialists/registry.json +158 -13
  329. package/lib/hooks/env-check.mjs +0 -83
  330. package/lib/hooks/read-tracker.mjs +0 -61
  331. package/lib/policy/unified-gates.mjs +0 -96
  332. package/lib/server/static/assets/index-ab25c707.js +0 -70
  333. package/lib/server/static/assets/index-f0c80a2b.css +0 -1
  334. package/lib/specialist-contracts-enforce.mjs +0 -158
  335. package/rules/common/agents.md +0 -28
  336. package/rules/common/development-workflow.md +0 -32
  337. package/rules/common/performance.md +0 -55
@@ -0,0 +1,394 @@
1
+ /**
2
+ * lib/logging/rotate.mjs — shared file-rotation primitive.
3
+ *
4
+ * Two consumers need bounded log files: the trace writer
5
+ * (`.cx/traces/<date>.jsonl`, capped to avoid >100MB single-file commits
6
+ * that GitHub rejects) and the embed daemon stdout log
7
+ * (`~/.cx/runtime/embed-daemon.log`, capped so a stuck "Telemetry skipped"
8
+ * message can't fill the disk).
9
+ *
10
+ * `appendWithRotation` is for code paths that own the writes (the trace
11
+ * writer). `rotateIfOversized` is for externally-written files where Construct
12
+ * code only polls (the OS-supervisor stdout redirect — Construct never writes
13
+ * the embed log directly).
14
+ *
15
+ * Rotated segments are named `<base>.<n><ext>` where `n` starts at 1 and
16
+ * counts upward; with `gzip: true` segments get a `.gz` suffix and are
17
+ * compressed lazily on rotation. `maxSegments` (when set) drops the oldest
18
+ * `n>=maxSegments` segments on every rotation so the directory stays bounded.
19
+ */
20
+
21
+ import { existsSync, statSync, renameSync, readdirSync, readFileSync, unlinkSync, appendFileSync, createReadStream, createWriteStream, mkdirSync } from 'node:fs';
22
+ import path from 'node:path';
23
+ import { createGzip, gunzipSync } from 'node:zlib';
24
+ import { pipeline } from 'node:stream/promises';
25
+
26
+ /**
27
+ * Parse `<base>.<ext>` from a full path. `app.log` → base=app, ext=.log.
28
+ * `2026-05-28.jsonl` → base=2026-05-28, ext=.jsonl.
29
+ */
30
+ function splitBase(filePath) {
31
+ const dir = path.dirname(filePath);
32
+ const ext = path.extname(filePath);
33
+ const base = path.basename(filePath, ext);
34
+ return { dir, base, ext };
35
+ }
36
+
37
+ /**
38
+ * Enumerate existing rotated segments for a given file, sorted ascending by
39
+ * segment index. Includes both compressed and uncompressed forms.
40
+ */
41
+ function listSegments(filePath) {
42
+ const { dir, base, ext } = splitBase(filePath);
43
+ if (!existsSync(dir)) return [];
44
+
45
+ // Pattern: <base>.<n>[<ext>][.gz]. Match any segment that starts with base
46
+ // and a digit suffix; tolerate both `.1.jsonl` and `.1.jsonl.gz`.
47
+
48
+ const pattern = new RegExp(`^${escapeRegex(base)}\\.(\\d+)${escapeRegex(ext)}(\\.gz)?$`);
49
+ const entries = readdirSync(dir)
50
+ .map((name) => {
51
+ const m = name.match(pattern);
52
+ return m ? { name, index: Number(m[1]), gzipped: !!m[2] } : null;
53
+ })
54
+ .filter(Boolean)
55
+ .sort((a, b) => a.index - b.index);
56
+ return entries.map((e) => ({ ...e, fullPath: path.join(dir, e.name) }));
57
+ }
58
+
59
+ function escapeRegex(s) {
60
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
61
+ }
62
+
63
+ /**
64
+ * Rename current → next-segment, optionally gzipping the previous tail. Old
65
+ * segments past `maxSegments` are deleted. Returns the new segment path or
66
+ * null when no rotation happened.
67
+ */
68
+ export async function rotateIfOversized(filePath, { maxBytes, maxSegments = 0, gzip = false } = {}) {
69
+ if (!maxBytes || maxBytes <= 0) return null;
70
+ if (!existsSync(filePath)) return null;
71
+
72
+ let stat;
73
+ try { stat = statSync(filePath); }
74
+ catch { return null; }
75
+ if (!stat.isFile() || stat.size < maxBytes) return null;
76
+
77
+ const { dir, base, ext } = splitBase(filePath);
78
+ const existing = listSegments(filePath);
79
+ const nextIndex = (existing[existing.length - 1]?.index ?? 0) + 1;
80
+
81
+ // Move current → <base>.<nextIndex><ext>. Then optionally gzip the rotated
82
+ // file. Doing the rename first means a concurrent writer that reopens the
83
+ // path lands on a fresh file with no data loss.
84
+
85
+ const rotatedPlain = path.join(dir, `${base}.${nextIndex}${ext}`);
86
+ renameSync(filePath, rotatedPlain);
87
+
88
+ let finalPath = rotatedPlain;
89
+ if (gzip) {
90
+ finalPath = `${rotatedPlain}.gz`;
91
+ try {
92
+ await pipeline(createReadStream(rotatedPlain), createGzip(), createWriteStream(finalPath));
93
+ try { unlinkSync(rotatedPlain); } catch { /* already cleaned up */ }
94
+ } catch {
95
+ // Compression failed; leave the plain rotated segment in place rather
96
+ // than losing data.
97
+
98
+ finalPath = rotatedPlain;
99
+ }
100
+ }
101
+
102
+ pruneSegments(filePath, maxSegments);
103
+ return finalPath;
104
+ }
105
+
106
+ /**
107
+ * Drop oldest segments past `maxSegments`. With maxSegments=0, keep every
108
+ * segment (no pruning). Always preserves the active file (which has no
109
+ * numeric suffix and is therefore not in the segment list).
110
+ */
111
+ export function pruneSegments(filePath, maxSegments = 0) {
112
+ if (!maxSegments || maxSegments <= 0) return [];
113
+ const segments = listSegments(filePath);
114
+ if (segments.length <= maxSegments) return [];
115
+ const drop = segments.slice(0, segments.length - maxSegments);
116
+ for (const seg of drop) {
117
+ try { unlinkSync(seg.fullPath); } catch { /* already gone */ }
118
+ }
119
+ return drop.map((s) => s.fullPath);
120
+ }
121
+
122
+ /**
123
+ * Append `line` to `filePath`, rotating beforehand if the file would exceed
124
+ * `maxBytes`. Creates the parent directory on first append.
125
+ *
126
+ * Synchronous + best-effort gzip. The cost of compressing on rotation is
127
+ * amortized over `maxBytes` worth of writes, so the rotation tail rarely
128
+ * blocks the calling code path. When gzip:false rotation is just a rename.
129
+ */
130
+ export async function appendWithRotation(filePath, line, { maxBytes, maxSegments = 0, gzip = false } = {}) {
131
+ const dir = path.dirname(filePath);
132
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
133
+
134
+ if (maxBytes && maxBytes > 0 && existsSync(filePath)) {
135
+ try {
136
+ const stat = statSync(filePath);
137
+ // Rotate when this append would cross the cap, not after — keeps every
138
+ // single segment strictly under `maxBytes`.
139
+
140
+ if (stat.isFile() && stat.size + Buffer.byteLength(line) >= maxBytes) {
141
+ await rotateIfOversized(filePath, { maxBytes: 0, maxSegments, gzip });
142
+ }
143
+ } catch { /* fall through to plain append */ }
144
+ }
145
+
146
+ appendFileSync(filePath, line);
147
+ }
148
+
149
+ /**
150
+ * Sync variant for hot paths that can't `await` rotation. Skips gzip (which
151
+ * requires streams) but still renames + prunes. Use this in trace writers and
152
+ * other latency-sensitive append loops.
153
+ */
154
+ export function appendWithRotationSync(filePath, line, { maxBytes, maxSegments = 0 } = {}) {
155
+ const dir = path.dirname(filePath);
156
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
157
+
158
+ if (maxBytes && maxBytes > 0 && existsSync(filePath)) {
159
+ try {
160
+ const stat = statSync(filePath);
161
+ if (stat.isFile() && stat.size + Buffer.byteLength(line) >= maxBytes) {
162
+ const { dir: d, base, ext } = splitBase(filePath);
163
+ const existing = listSegments(filePath);
164
+ const nextIndex = (existing[existing.length - 1]?.index ?? 0) + 1;
165
+ renameSync(filePath, path.join(d, `${base}.${nextIndex}${ext}`));
166
+ pruneSegments(filePath, maxSegments);
167
+ }
168
+ } catch { /* fall through */ }
169
+ }
170
+
171
+ appendFileSync(filePath, line);
172
+ }
173
+
174
+ /**
175
+ * Per-channel resource limits. Every Construct logger has a documented
176
+ * cap here so disk usage is bounded by design, not by hope. New loggers
177
+ * MUST be registered; the `appendBounded` helper refuses to write to an
178
+ * unregistered channel.
179
+ *
180
+ * Sizes are conservative defaults chosen for a single-developer machine.
181
+ * Override via env var on a per-channel basis when the workload genuinely
182
+ * exceeds these (CI runners, multi-tenant team installs, long-lived
183
+ * background daemons).
184
+ */
185
+ export const LIMITS = {
186
+ // Trace shards under .cx/traces/<date>.jsonl — capped below GitHub's
187
+ // 100 MB single-file ceiling. Bead construct-1vv5.
188
+
189
+ trace: {
190
+ maxBytes: 100 * 1024 * 1024,
191
+ maxSegments: 0, // keep all history; rotation is for the size cap, not retention
192
+ gzip: false,
193
+ envOverride: 'CONSTRUCT_TRACE_MAX_MB',
194
+ },
195
+
196
+ // OS-supervised stdout log at ~/.cx/runtime/embed-daemon.log. Bead
197
+ // construct-88i. Rotation is poll-style via the daemon scheduler.
198
+
199
+ 'embed-daemon-log': {
200
+ maxBytes: 50 * 1024 * 1024,
201
+ maxSegments: 5,
202
+ gzip: true,
203
+ envOverride: 'CONSTRUCT_EMBED_LOG_MAX_MB',
204
+ },
205
+
206
+ // Per-edit audit of file reads. High traffic in active sessions.
207
+ // ~/.cx/audit-reads.jsonl.
208
+
209
+ 'audit-reads': {
210
+ maxBytes: 25 * 1024 * 1024,
211
+ maxSegments: 4,
212
+ gzip: true,
213
+ envOverride: 'CONSTRUCT_AUDIT_READS_MAX_MB',
214
+ },
215
+
216
+ // Per-skill-call telemetry. ~/.cx/skill-calls.jsonl.
217
+
218
+ 'skill-calls': {
219
+ maxBytes: 25 * 1024 * 1024,
220
+ maxSegments: 4,
221
+ gzip: true,
222
+ envOverride: 'CONSTRUCT_SKILL_CALLS_MAX_MB',
223
+ },
224
+
225
+ // Agent-dispatch log written by `lib/hooks/agent-tracker.mjs`. Path: ~/.cx/agent-log.jsonl.
226
+
227
+ 'agent-log': {
228
+ maxBytes: 25 * 1024 * 1024,
229
+ maxSegments: 4,
230
+ gzip: true,
231
+ envOverride: 'CONSTRUCT_AGENT_LOG_MAX_MB',
232
+ },
233
+
234
+ // Pending role invocations across all projects. ~/.cx/role-pending.jsonl.
235
+
236
+ 'role-pending': {
237
+ maxBytes: 10 * 1024 * 1024,
238
+ maxSegments: 2,
239
+ gzip: true,
240
+ envOverride: 'CONSTRUCT_ROLE_PENDING_MAX_MB',
241
+ },
242
+
243
+ // Intent verifications. ~/.cx/intent-verifications.jsonl.
244
+
245
+ 'intent-verifications': {
246
+ maxBytes: 10 * 1024 * 1024,
247
+ maxSegments: 2,
248
+ gzip: true,
249
+ envOverride: 'CONSTRUCT_INTENT_VERIFICATIONS_MAX_MB',
250
+ },
251
+
252
+ // Contract postcondition violations. ~/.cx/contract-violations.jsonl.
253
+
254
+ 'contract-violations': {
255
+ maxBytes: 10 * 1024 * 1024,
256
+ maxSegments: 2,
257
+ gzip: true,
258
+ envOverride: 'CONSTRUCT_CONTRACT_VIOLATIONS_MAX_MB',
259
+ },
260
+
261
+ // Bash-output warning flags appended by the bash-output-logger hook on
262
+ // every Bash tool use over the size threshold. ~/.cx/warn-flags.txt.
263
+
264
+ 'bash-warn-flags': {
265
+ maxBytes: 5 * 1024 * 1024,
266
+ maxSegments: 2,
267
+ gzip: false,
268
+ envOverride: 'CONSTRUCT_BASH_WARN_FLAGS_MAX_MB',
269
+ },
270
+
271
+ // Per-turn cost ledger written by the Stop hook. Cross-project (so the
272
+ // user has one place to see spend across every project) — each entry
273
+ // carries a projectId tag so readers can split by project. Path:
274
+ // ~/.cx/session-cost.jsonl.
275
+
276
+ 'session-cost': {
277
+ maxBytes: 25 * 1024 * 1024,
278
+ maxSegments: 4,
279
+ gzip: true,
280
+ envOverride: 'CONSTRUCT_SESSION_COST_MAX_MB',
281
+ },
282
+
283
+ // Tamper-evident audit trail of every mutation Construct (or a dispatched
284
+ // subagent) makes. Project-scoped. Path: <project>/.cx/audit-trail.jsonl.
285
+
286
+ 'audit-trail': {
287
+ maxBytes: 50 * 1024 * 1024,
288
+ maxSegments: 4,
289
+ gzip: true,
290
+ envOverride: 'CONSTRUCT_AUDIT_TRAIL_MAX_MB',
291
+ },
292
+
293
+ // Pending typecheck queue written by the edit-accumulator hook on every
294
+ // Edit/Write of a TS/JS file. Path: ~/.cx/pending-typecheck.txt.
295
+
296
+ 'edit-accumulator': {
297
+ maxBytes: 5 * 1024 * 1024,
298
+ maxSegments: 2,
299
+ gzip: false,
300
+ envOverride: 'CONSTRUCT_EDIT_ACCUMULATOR_MAX_MB',
301
+ },
302
+ };
303
+
304
+ /**
305
+ * Resolve the effective cap for a channel, honoring env-var overrides
306
+ * (interpreted as megabytes). Returns the byte budget; 0 disables rotation.
307
+ */
308
+ function resolveCap(channel, env = process.env) {
309
+ const def = LIMITS[channel];
310
+ if (!def) throw new Error(`appendBounded: unknown channel "${channel}". Register it in lib/logging/rotate.mjs#LIMITS.`);
311
+ const raw = def.envOverride ? env[def.envOverride] : undefined;
312
+ if (raw === undefined) return def.maxBytes;
313
+ const mb = Number(raw);
314
+ if (!Number.isFinite(mb) || mb < 0) return def.maxBytes;
315
+ return Math.floor(mb * 1024 * 1024);
316
+ }
317
+
318
+ /**
319
+ * Append a line to `filePath` with rotation governed by the named channel's
320
+ * registered limit. Synchronous; renames the active file on overflow and
321
+ * prunes oldest segments past the channel's maxSegments. The registry
322
+ * guarantees nothing grows unbounded — every logger appending to a known
323
+ * channel inherits its documented disk budget.
324
+ */
325
+ export function appendBounded(channel, filePath, line, env = process.env) {
326
+ const def = LIMITS[channel];
327
+ if (!def) throw new Error(`appendBounded: unknown channel "${channel}". Register it in lib/logging/rotate.mjs#LIMITS.`);
328
+ const maxBytes = resolveCap(channel, env);
329
+ return appendWithRotationSync(filePath, line, {
330
+ maxBytes,
331
+ maxSegments: def.maxSegments,
332
+ });
333
+ }
334
+
335
+ /**
336
+ * Last non-empty line written under the channel, looking across the active
337
+ * file and the rotated segments. Resolves the right value for chain-hash
338
+ * fields (`prev_line_hash`) on the first write after a rotation, where the
339
+ * active file is empty but the chain head lives at the tail of the most
340
+ * recent rotated segment (`<base>.<n><ext>` with the highest `n`, possibly
341
+ * `.gz`-suffixed).
342
+ *
343
+ * Sync. Reads the WHOLE most-recent gzipped segment in memory when it has
344
+ * to fall back to one; segment caps in LIMITS keep the worst case bounded
345
+ * (audit-trail tops out at 50 MB raw, ~5–10 MB gzipped).
346
+ *
347
+ * Returns the line WITHOUT its trailing newline, or null when no data has
348
+ * ever been written for the channel.
349
+ */
350
+ export function readLastLineAcrossSegments(filePath) {
351
+ if (existsSync(filePath)) {
352
+ const lastFromActive = readLastLineOfFile(filePath);
353
+ if (lastFromActive !== null) return lastFromActive;
354
+ }
355
+ const segments = listSegments(filePath);
356
+ if (segments.length === 0) return null;
357
+ const newest = segments[segments.length - 1];
358
+ try {
359
+ const raw = readFileSync(newest.fullPath);
360
+ const decoded = newest.gzipped ? gunzipSync(raw) : raw;
361
+ return lastLineOfBuffer(decoded);
362
+ } catch {
363
+ return null;
364
+ }
365
+ }
366
+
367
+ function lastLineOfBuffer(buf) {
368
+ let end = buf.length;
369
+ while (end > 0 && buf[end - 1] === 0x0a) end -= 1;
370
+ if (end === 0) return null;
371
+ let start = end - 1;
372
+ while (start > 0 && buf[start - 1] !== 0x0a) start -= 1;
373
+ return buf.slice(start, end).toString('utf8');
374
+ }
375
+
376
+ function readLastLineOfFile(filePath) {
377
+ try {
378
+ if (statSync(filePath).size === 0) return null;
379
+
380
+ // Read the whole file and walk back to the last newline boundary. A
381
+ // bounded tail read (e.g. last 8 KB) would truncate JSONL records that
382
+ // exceed the window — the audit-trail's `content_hash` field can put a
383
+ // record over 50 KB — and a truncated string would break the
384
+ // prev_line_hash chain it feeds. Per-channel size caps in LIMITS bound
385
+ // the worst-case allocation here.
386
+
387
+ return lastLineOfBuffer(readFileSync(filePath));
388
+ } catch {
389
+ return null;
390
+ }
391
+ }
392
+
393
+ /** Re-export for convenient testing. */
394
+ export const __test = { listSegments, splitBase, resolveCap, readLastLineOfFile };
@@ -116,7 +116,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
116
116
  },
117
117
  {
118
118
  name: 'extract_document_text',
119
- description: 'Extracts readable text from a local document path. Supports PDF on macOS plus common text and office document formats.',
119
+ description: 'Extracts readable text from a local document path. High-fidelity backend (docling Python sidecar via uv, whisper.cpp for audio/video) is wired into `construct ingest`; this MCP path returns the legacy regex extraction for backwards compatibility. Supports PDF, DOCX, XLSX, PPTX, HTML, plain text, email, and transcripts.',
120
120
  inputSchema: {
121
121
  type: 'object',
122
122
  properties: {
@@ -903,6 +903,147 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
903
903
  },
904
904
  },
905
905
  },
906
+ {
907
+ name: 'workflow_init',
908
+ description: 'Initialize a new workflow for the current project. Creates plan.md state if not already present and returns the initial workflow envelope.',
909
+ inputSchema: {
910
+ type: 'object',
911
+ properties: {
912
+ cwd: { type: 'string', description: 'Project root (default: server cwd).' },
913
+ title: { type: 'string', description: 'Workflow title shown in the plan header (default: "Untitled workflow").' },
914
+ spec_ref: { type: 'string', description: 'Optional reference to a spec/PRD/ADR id this workflow implements.' },
915
+ },
916
+ },
917
+ },
918
+ {
919
+ name: 'workflow_add_task',
920
+ description: 'Add a task to the current workflow. Pass `request` for intent-based routing (the classifier picks track + specialist) or pass explicit task fields (`key`, `title`, etc.) for manual entry.',
921
+ inputSchema: {
922
+ type: 'object',
923
+ properties: {
924
+ cwd: { type: 'string', description: 'Project root (default: server cwd).' },
925
+ request: { type: 'string', description: 'Natural-language task request; when present, intent-based routing is used and the explicit fields below act as overrides.' },
926
+ key: { type: 'string', description: 'Stable task key (e.g. T-001). Generated when omitted.' },
927
+ title: { type: 'string', description: 'Short task title.' },
928
+ phase: { type: 'string', description: 'Phase bucket (plan, build, validate, ship, etc.).' },
929
+ owner: { type: 'string', description: 'Specialist or persona that owns the task.' },
930
+ files: { type: 'array', items: { type: 'string' }, description: 'File paths this task touches.' },
931
+ readFirst: { type: 'array', items: { type: 'string' }, description: 'Files the owner should read before editing.' },
932
+ doNotChange: { type: 'array', items: { type: 'string' }, description: 'Files/regions the owner must not modify.' },
933
+ acceptanceCriteria: { type: 'array', items: { type: 'string' }, description: 'Acceptance criteria as a checklist.' },
934
+ verification: { type: 'string', description: 'Command(s) or description of how to verify the task is done.' },
935
+ dependsOn: { type: 'array', items: { type: 'string' }, description: 'Task keys this task depends on.' },
936
+ overlays: { type: 'array', items: { type: 'string' }, description: 'Role flavors that augment the owner persona for this task.' },
937
+ challengeRequired: { type: 'boolean', description: 'Force a cx-devil-advocate challenge before the task can complete.' },
938
+ challengeStatus: { type: 'string', description: 'Initial challenge status when seeded.' },
939
+ tokenBudget: { type: 'number', description: 'Per-task token budget for cost tracking.' },
940
+ status: { type: 'string', description: 'Initial status override.' },
941
+ },
942
+ },
943
+ },
944
+ {
945
+ name: 'workflow_update_task',
946
+ description: 'Update fields on an existing workflow task. Requires the task `key`. Only fields supplied are changed.',
947
+ inputSchema: {
948
+ type: 'object',
949
+ required: ['key'],
950
+ properties: {
951
+ cwd: { type: 'string', description: 'Project root (default: server cwd).' },
952
+ key: { type: 'string', description: 'Task key to update.' },
953
+ status: { type: 'string', description: 'New status (pending, in_progress, blocked_needs_user, blocked_by_dep, done, etc.).' },
954
+ owner: { type: 'string', description: 'New owner persona.' },
955
+ phase: { type: 'string', description: 'New phase bucket.' },
956
+ note: { type: 'string', description: 'Append-only progress note.' },
957
+ verification: { type: 'string', description: 'Updated verification description.' },
958
+ overlays: { type: 'array', items: { type: 'string' }, description: 'Replace the overlay list.' },
959
+ challengeRequired: { type: 'boolean', description: 'Toggle whether a challenge is required.' },
960
+ challengeStatus: { type: 'string', description: 'Update the challenge status (proposed, accepted, refused, etc.).' },
961
+ },
962
+ },
963
+ },
964
+ {
965
+ name: 'workflow_needs_main_input',
966
+ description: 'Mark a workflow task as blocked pending user input. Sets status to blocked_needs_user and writes a packet describing the blocker for the orchestrator to surface.',
967
+ inputSchema: {
968
+ type: 'object',
969
+ required: ['taskKey', 'blocker', 'question'],
970
+ properties: {
971
+ cwd: { type: 'string', description: 'Project root (default: server cwd).' },
972
+ taskKey: { type: 'string', description: 'Task key to mark blocked.' },
973
+ worker: { type: 'string', description: 'Specialist that needs input (default: current owner).' },
974
+ blocker: { type: 'string', description: 'One-line description of what is blocking progress.' },
975
+ question: { type: 'string', description: 'The specific question to put to the user.' },
976
+ },
977
+ },
978
+ },
979
+ {
980
+ name: 'workflow_validate',
981
+ description: 'Validate the current workflow state against the schema and run consistency checks (no orphan tasks, no circular dependencies, every owner resolves to a known persona).',
982
+ inputSchema: {
983
+ type: 'object',
984
+ properties: {
985
+ cwd: { type: 'string', description: 'Project root (default: server cwd).' },
986
+ },
987
+ },
988
+ },
989
+ {
990
+ name: 'workflow_status',
991
+ description: 'Return the full workflow snapshot for the current project: tasks, summary, alignment health, and the public-health surface used by the dashboard.',
992
+ inputSchema: {
993
+ type: 'object',
994
+ properties: {
995
+ cwd: { type: 'string', description: 'Project root (default: server cwd).' },
996
+ },
997
+ },
998
+ },
999
+ {
1000
+ name: 'workflow_contract_validate',
1001
+ description: 'Validate a producer→consumer handoff against specialists/contracts.json. Required when a specialist hands off to another role: enforces input.mustContain, output schema, disk-artifact postconditions, and binary postconditions per producer (rubber-stamp prevention, post-hoc threat-model prevention, etc.). Self-enforcing: a producer with binary rules MUST pass `packet`, or the call itself is a contract violation.',
1002
+ inputSchema: {
1003
+ type: 'object',
1004
+ required: ['producer', 'consumer'],
1005
+ properties: {
1006
+ producer: { type: 'string', description: 'Producer agent or persona name (e.g. cx-reviewer, cx-security).' },
1007
+ consumer: { type: 'string', description: 'Consumer agent or persona name receiving the handoff.' },
1008
+ id: { type: 'string', description: 'Optional contract id; overrides producer/consumer lookup.' },
1009
+ artifact: { type: 'object', description: 'The handoff payload to validate against the contract schema and disk-artifact postconditions.' },
1010
+ packet: { type: 'object', description: 'The producer\'s in-memory output packet. REQUIRED when the producer has binary postconditions; omitting it is itself a contract violation.' },
1011
+ enforcement: { type: 'string', enum: ['block', 'warn'], description: 'Enforcement mode (default: block). Use warn only when explicitly advisory.' },
1012
+ },
1013
+ },
1014
+ },
1015
+ {
1016
+ name: 'workflow_import_plan',
1017
+ description: 'Bulk-add tasks from a markdown plan to the current workflow. Parses headings and bullet structure to extract task titles, owners, and acceptance criteria.',
1018
+ inputSchema: {
1019
+ type: 'object',
1020
+ required: ['markdown'],
1021
+ properties: {
1022
+ cwd: { type: 'string', description: 'Project root (default: server cwd).' },
1023
+ markdown: { type: 'string', description: 'Plan markdown to parse.' },
1024
+ phase: { type: 'string', description: 'Phase bucket applied to all imported tasks.' },
1025
+ owner: { type: 'string', description: 'Default owner for tasks that do not specify one.' },
1026
+ readFirst: { type: 'array', items: { type: 'string' }, description: 'Default readFirst files applied to all imported tasks.' },
1027
+ doNotChange: { type: 'array', items: { type: 'string' }, description: 'Default doNotChange files applied to all imported tasks.' },
1028
+ acceptanceCriteria: { type: 'array', items: { type: 'string' }, description: 'Default acceptance criteria applied to all imported tasks.' },
1029
+ title: { type: 'string', description: 'Workflow title to set if the workflow is newly created.' },
1030
+ spec_ref: { type: 'string', description: 'Spec reference to associate with the workflow.' },
1031
+ },
1032
+ },
1033
+ },
1034
+ {
1035
+ name: 'cx_trace_telemetry',
1036
+ description: 'Record a single CX telemetry trace for an agent invocation. Use to log start/end, model used, token cost, and outcome verdict for performance review.',
1037
+ inputSchema: {
1038
+ type: 'object',
1039
+ required: ['agent', 'trace'],
1040
+ properties: {
1041
+ agent: { type: 'string', description: 'Agent or persona name being traced.' },
1042
+ trace: { type: 'object', description: 'Trace record: start_ts, end_ts, model, tokens, verdict, notes, etc.' },
1043
+ cwd: { type: 'string', description: 'Project root (default: server cwd).' },
1044
+ },
1045
+ },
1046
+ },
906
1047
  ],
907
1048
  }));
908
1049
 
@@ -281,7 +281,7 @@ export function projectContext(args, { ROOT_DIR }) {
281
281
  const has_context_file = contextInspection.hasFile;
282
282
  const workflow = loadWorkflow(cwd);
283
283
  const mergedEnv = loadConstructEnv({ rootDir: ROOT_DIR, homeDir: homedir(), env: process.env });
284
- const registry = readJSON(join(ROOT_DIR, 'agents', 'registry.json')) ?? {};
284
+ const registry = readJSON(join(ROOT_DIR, 'specialists', 'registry.json')) ?? {};
285
285
  const executionContractModel = resolveExecutionContractModelMetadata({
286
286
  envValues: mergedEnv,
287
287
  registryModels: registry.models ?? {},
@@ -323,7 +323,7 @@ export function workflowStatus(args, { ROOT_DIR }) {
323
323
  const workflow = loadWorkflow(cwd);
324
324
  const workflowHealth = inspectWorkflowHealth(workflow, { cwd });
325
325
  const mergedEnv = loadConstructEnv({ rootDir: ROOT_DIR, homeDir: homedir(), env: process.env });
326
- const registry = readJSON(join(ROOT_DIR, 'agents', 'registry.json')) ?? {};
326
+ const registry = readJSON(join(ROOT_DIR, 'specialists', 'registry.json')) ?? {};
327
327
  const executionContractModel = resolveExecutionContractModelMetadata({
328
328
  envValues: mergedEnv,
329
329
  registryModels: registry.models ?? {},
@@ -7,7 +7,7 @@
7
7
  */
8
8
  import { readFileSync, readdirSync, existsSync } from 'node:fs';
9
9
  import { join } from 'node:path';
10
- import { execSync } from 'node:child_process';
10
+ import { execFileSync } from 'node:child_process';
11
11
  import { routeRequest, routeRequestVerified, requiresExecutiveApproval, TERMINAL_STATES } from '../../orchestration-policy.mjs';
12
12
  import { buildTaskPacketFromIntent } from '../../workflow-state.mjs';
13
13
  import { logSkillCall } from '../../telemetry/skill-calls.mjs';
@@ -73,12 +73,15 @@ export function searchSkills(args, { ROOT_DIR }) {
73
73
  if (!pattern) return { error: 'Missing pattern argument' };
74
74
  const skillsDir = join(ROOT_DIR, 'skills');
75
75
 
76
+ // argv-array form: pattern is a discrete argument, never interpreted by a
77
+ // shell. MCP tool inputs are model-controlled and must not reach a shell.
78
+
76
79
  try {
77
- const output = execSync(`rg -i "${pattern.replace(/"/g, '\\"')}" "${skillsDir}"`, { encoding: 'utf8' });
80
+ const output = execFileSync('rg', ['-i', pattern, skillsDir], { encoding: 'utf8' });
78
81
  return { results: output.split('\n').filter(Boolean) };
79
82
  } catch {
80
83
  try {
81
- const output = execSync(`grep -ri "${pattern.replace(/"/g, '\\"')}" "${skillsDir}"`, { encoding: 'utf8' });
84
+ const output = execFileSync('grep', ['-ri', pattern, skillsDir], { encoding: 'utf8' });
82
85
  return { results: output.split('\n').filter(Boolean) };
83
86
  } catch {
84
87
  return { results: [], note: 'No matches found or grep error' };
@@ -70,7 +70,7 @@ function resolveSessionContext() {
70
70
 
71
71
  export async function cxTrace(args, { ROOT_DIR }) {
72
72
  const ctx = resolveSessionContext();
73
- const registry = readJSON(join(ROOT_DIR, 'agents', 'registry.json')) ?? {};
73
+ const registry = readJSON(join(ROOT_DIR, 'specialists', 'registry.json')) ?? {};
74
74
  const registryModels = registry.models ?? {};
75
75
  const currentModels = readCurrentModels(join(ROOT_DIR, '.env'), registryModels, process.env);
76
76
  const route = typeof args.input === 'string' ? routeRequest({ request: args.input }) : null;
@@ -104,7 +104,10 @@ export function workflowValidate(args) {
104
104
  * consumer — agent or persona name receiving it
105
105
  * id — optional contract id (overrides producer/consumer lookup)
106
106
  * artifact — the handoff payload to validate
107
- * enforcement — 'warn' (default) or 'block'; 'block' returns ok:false on
107
+ * packet the producer's in-memory output packet. Required when the
108
+ * producer has binary postconditions; omitting it is itself
109
+ * a contract violation.
110
+ * enforcement — 'block' (default) or 'warn'; 'block' returns ok:false on
108
111
  * violation so the workflow can refuse to advance
109
112
  *
110
113
  * Returns { ok, status?, errors?, warnings?, contract } where status is set
@@ -117,7 +120,8 @@ export async function workflowContractValidate(args) {
117
120
  consumer: args.consumer,
118
121
  id: args.id,
119
122
  artifact: args.artifact,
120
- enforcement: args.enforcement || process.env.CONSTRUCT_CONTRACT_ENFORCEMENT || 'warn',
123
+ packet: args.packet,
124
+ enforcement: args.enforcement || 'block',
121
125
  });
122
126
  }
123
127