@geraldmaron/construct 1.0.14 → 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 (259) hide show
  1. package/README.md +6 -0
  2. package/bin/construct +3 -3
  3. package/commands/build/feature.md +0 -6
  4. package/commands/build/fix.md +0 -6
  5. package/commands/design/access.md +0 -6
  6. package/commands/design/flow.md +0 -6
  7. package/commands/design/ui.md +0 -6
  8. package/commands/measure/experiment.md +0 -6
  9. package/commands/measure/metrics.md +0 -6
  10. package/commands/measure/results.md +0 -6
  11. package/commands/plan/api.md +0 -6
  12. package/commands/plan/challenge.md +0 -6
  13. package/commands/plan/decide.md +0 -6
  14. package/commands/plan/feature.md +0 -7
  15. package/commands/plan/requirements.md +0 -6
  16. package/commands/remember/context.md +0 -6
  17. package/commands/remember/handoff.md +0 -6
  18. package/commands/remember/runbook.md +0 -6
  19. package/commands/review/code.md +0 -6
  20. package/commands/review/quality.md +0 -6
  21. package/commands/review/security.md +0 -6
  22. package/commands/ship/ready.md +0 -6
  23. package/commands/ship/release.md +0 -6
  24. package/commands/ship/status.md +0 -6
  25. package/commands/understand/docs.md +0 -6
  26. package/commands/understand/research.md +0 -6
  27. package/commands/understand/this.md +0 -6
  28. package/commands/understand/why.md +0 -6
  29. package/commands/work/clean.md +0 -6
  30. package/commands/work/drive.md +0 -6
  31. package/commands/work/optimize-prompts.md +0 -6
  32. package/commands/work/parallel-review.md +0 -6
  33. package/lib/beads-client.mjs +19 -1
  34. package/lib/comment-lint.mjs +5 -1
  35. package/lib/context-state.mjs +15 -3
  36. package/lib/contracts/validate.mjs +54 -10
  37. package/lib/contracts/violation-log.mjs +154 -0
  38. package/lib/document-extract/docling-client.mjs +114 -0
  39. package/lib/document-extract/docling-sidecar.py +122 -0
  40. package/lib/document-extract/whisper-client.mjs +79 -0
  41. package/lib/document-extract.mjs +73 -8
  42. package/lib/document-ingest.mjs +26 -4
  43. package/lib/embed/daemon.mjs +6 -1
  44. package/lib/flavors/loader.mjs +1 -1
  45. package/lib/handoffs/contract.mjs +2 -2
  46. package/lib/hooks/session-start.mjs +1 -1
  47. package/lib/ingest/chunker.mjs +94 -0
  48. package/lib/ingest/pipeline.mjs +53 -0
  49. package/lib/ingest/store.mjs +82 -0
  50. package/lib/knowledge/search.mjs +12 -0
  51. package/lib/mcp/server.mjs +142 -1
  52. package/lib/mcp/tools/skills.mjs +6 -3
  53. package/lib/mcp/tools/workflow.mjs +6 -2
  54. package/lib/observation-store.mjs +14 -5
  55. package/lib/ollama-manager.mjs +32 -28
  56. package/lib/prompt-composer.js +11 -1
  57. package/lib/runtime/uv-bootstrap.mjs +129 -0
  58. package/lib/runtime/whisper-bootstrap.mjs +102 -0
  59. package/lib/server/index.mjs +22 -1
  60. package/lib/server/static/index.html +1 -1
  61. package/lib/specialists/postconditions.mjs +1 -1
  62. package/lib/sync/skill-frontmatter.mjs +113 -21
  63. package/lib/tracking-surfaces.mjs +2 -0
  64. package/lib/validators/skills.mjs +86 -54
  65. package/package.json +5 -5
  66. package/personas/construct.md +4 -7
  67. package/platforms/claude/settings.template.json +1 -1
  68. package/rules/common/beads-hygiene.md +3 -9
  69. package/rules/common/code-review.md +3 -6
  70. package/rules/common/coding-style.md +3 -6
  71. package/rules/common/comments.md +3 -7
  72. package/rules/common/commit-approval.md +3 -6
  73. package/rules/common/cx-agent-routing.md +3 -7
  74. package/rules/common/cx-skill-routing.md +3 -3
  75. package/rules/common/doc-ownership.md +3 -8
  76. package/rules/common/efficiency.md +3 -8
  77. package/rules/common/framing.md +3 -8
  78. package/rules/common/git-workflow.md +3 -5
  79. package/rules/common/no-fabrication.md +4 -12
  80. package/rules/common/patterns.md +3 -5
  81. package/rules/common/release-gates.md +3 -8
  82. package/rules/common/research.md +3 -8
  83. package/rules/common/review-before-change.md +3 -9
  84. package/rules/common/security.md +3 -6
  85. package/rules/common/skill-composition.md +3 -7
  86. package/rules/common/testing.md +3 -6
  87. package/rules/golang/coding-style.md +1 -5
  88. package/rules/golang/hooks.md +1 -5
  89. package/rules/golang/patterns.md +1 -5
  90. package/rules/golang/security.md +1 -5
  91. package/rules/golang/testing.md +1 -5
  92. package/rules/python/coding-style.md +1 -5
  93. package/rules/python/hooks.md +1 -5
  94. package/rules/python/patterns.md +1 -5
  95. package/rules/python/security.md +1 -5
  96. package/rules/python/testing.md +1 -5
  97. package/rules/swift/coding-style.md +1 -5
  98. package/rules/swift/hooks.md +1 -5
  99. package/rules/swift/patterns.md +1 -5
  100. package/rules/swift/security.md +1 -5
  101. package/rules/swift/testing.md +1 -5
  102. package/rules/typescript/coding-style.md +1 -5
  103. package/rules/typescript/hooks.md +1 -5
  104. package/rules/typescript/patterns.md +1 -5
  105. package/rules/typescript/security.md +1 -5
  106. package/rules/typescript/testing.md +1 -5
  107. package/rules/web/coding-style.md +3 -5
  108. package/rules/web/design-quality.md +3 -5
  109. package/rules/web/hooks.md +3 -5
  110. package/rules/web/patterns.md +3 -5
  111. package/rules/web/performance.md +3 -5
  112. package/rules/web/security.md +3 -5
  113. package/rules/web/testing.md +3 -5
  114. package/skills/ai/agent-dev.md +4 -5
  115. package/skills/ai/llm-security.md +4 -5
  116. package/skills/ai/ml-ops.md +4 -5
  117. package/skills/ai/orchestration-workflow.md +4 -5
  118. package/skills/ai/prompt-and-eval.md +4 -5
  119. package/skills/ai/prompt-optimizer.md +4 -6
  120. package/skills/ai/rag-system.md +4 -5
  121. package/skills/architecture/api-design.md +4 -5
  122. package/skills/architecture/caching.md +4 -5
  123. package/skills/architecture/cloud-native.md +4 -5
  124. package/skills/architecture/message-queue.md +4 -5
  125. package/skills/architecture/security-arch.md +4 -5
  126. package/skills/compliance/ai-disclosure.md +4 -5
  127. package/skills/compliance/data-privacy.md +4 -5
  128. package/skills/compliance/license-audit.md +4 -5
  129. package/skills/compliance/regulatory-review.md +4 -5
  130. package/skills/development/cpp.md +4 -5
  131. package/skills/development/go.md +4 -5
  132. package/skills/development/java.md +4 -5
  133. package/skills/development/kotlin.md +4 -5
  134. package/skills/development/mobile-crossplatform.md +4 -5
  135. package/skills/development/python.md +4 -5
  136. package/skills/development/rust.md +4 -5
  137. package/skills/development/shell.md +4 -5
  138. package/skills/development/swift.md +4 -5
  139. package/skills/development/typescript.md +4 -5
  140. package/skills/devops/ci-cd.md +4 -5
  141. package/skills/devops/containerization.md +4 -5
  142. package/skills/devops/cost-optimization.md +4 -5
  143. package/skills/devops/data-engineering.md +4 -5
  144. package/skills/devops/database.md +4 -5
  145. package/skills/devops/dependency-management.md +4 -5
  146. package/skills/devops/devsecops.md +4 -5
  147. package/skills/devops/git-workflow.md +4 -5
  148. package/skills/devops/incident-response.md +4 -5
  149. package/skills/devops/monorepo.md +4 -5
  150. package/skills/devops/observability.md +4 -5
  151. package/skills/devops/performance.md +4 -5
  152. package/skills/devops/testing.md +4 -5
  153. package/skills/docs/adr-workflow.md +4 -7
  154. package/skills/docs/backlog-proposal-workflow.md +4 -3
  155. package/skills/docs/customer-profile-workflow.md +4 -3
  156. package/skills/docs/document-ingest-workflow.md +4 -3
  157. package/skills/docs/evidence-ingest-workflow.md +4 -3
  158. package/skills/docs/init-docs.md +4 -5
  159. package/skills/docs/init-project.md +4 -5
  160. package/skills/docs/prd-workflow.md +4 -7
  161. package/skills/docs/prfaq-workflow.md +4 -3
  162. package/skills/docs/product-intelligence-review.md +4 -3
  163. package/skills/docs/product-intelligence-workflow.md +4 -6
  164. package/skills/docs/product-signal-workflow.md +4 -5
  165. package/skills/docs/research-workflow.md +4 -5
  166. package/skills/docs/runbook-workflow.md +4 -5
  167. package/skills/docs/strategy-workflow.md +4 -5
  168. package/skills/exploration/dependency-graph-reading.md +4 -7
  169. package/skills/exploration/repo-map.md +4 -5
  170. package/skills/exploration/tracer-bullet-method.md +4 -7
  171. package/skills/exploration/unknown-codebase-onboarding.md +4 -7
  172. package/skills/frameworks/django.md +4 -5
  173. package/skills/frameworks/nextjs.md +4 -5
  174. package/skills/frameworks/react.md +4 -5
  175. package/skills/frameworks/spring-boot.md +4 -5
  176. package/skills/frontend-design/accessibility.md +4 -5
  177. package/skills/frontend-design/component-patterns.md +4 -5
  178. package/skills/frontend-design/engineering.md +4 -5
  179. package/skills/frontend-design/state-management.md +4 -5
  180. package/skills/frontend-design/ui-aesthetics.md +4 -5
  181. package/skills/frontend-design/ux-principles.md +4 -5
  182. package/skills/operating/change-management.md +4 -8
  183. package/skills/operating/incident-response.md +4 -8
  184. package/skills/operating/oncall-rotation.md +4 -7
  185. package/skills/operating/orchestration-reference.md +4 -10
  186. package/skills/quality-gates/review-work.md +4 -5
  187. package/skills/quality-gates/verify-change.md +4 -5
  188. package/skills/quality-gates/verify-module.md +4 -5
  189. package/skills/quality-gates/verify-quality.md +4 -5
  190. package/skills/quality-gates/verify-security.md +4 -5
  191. package/skills/roles/architect.ai-systems.md +6 -9
  192. package/skills/roles/architect.data.md +6 -9
  193. package/skills/roles/architect.enterprise.md +6 -9
  194. package/skills/roles/architect.integration.md +6 -9
  195. package/skills/roles/architect.md +7 -14
  196. package/skills/roles/architect.platform.md +6 -9
  197. package/skills/roles/data-analyst.experiment.md +6 -9
  198. package/skills/roles/data-analyst.md +6 -9
  199. package/skills/roles/data-analyst.product-intelligence.md +7 -9
  200. package/skills/roles/data-analyst.product.md +6 -9
  201. package/skills/roles/data-analyst.telemetry.md +7 -9
  202. package/skills/roles/data-engineer.pipeline.md +6 -9
  203. package/skills/roles/data-engineer.vector-retrieval.md +7 -9
  204. package/skills/roles/data-engineer.warehouse.md +6 -9
  205. package/skills/roles/debugger.md +6 -9
  206. package/skills/roles/designer.accessibility.md +6 -9
  207. package/skills/roles/designer.md +7 -9
  208. package/skills/roles/engineer.ai.md +6 -9
  209. package/skills/roles/engineer.data.md +6 -9
  210. package/skills/roles/engineer.md +9 -9
  211. package/skills/roles/engineer.platform.md +6 -9
  212. package/skills/roles/operator.docs.md +6 -9
  213. package/skills/roles/operator.md +9 -9
  214. package/skills/roles/operator.release.md +6 -9
  215. package/skills/roles/operator.sre.md +6 -9
  216. package/skills/roles/orchestrator.md +6 -9
  217. package/skills/roles/product-manager.ai-product.md +6 -9
  218. package/skills/roles/product-manager.business-strategy.md +6 -9
  219. package/skills/roles/product-manager.enterprise.md +6 -9
  220. package/skills/roles/product-manager.growth.md +7 -9
  221. package/skills/roles/product-manager.md +7 -9
  222. package/skills/roles/product-manager.platform.md +6 -9
  223. package/skills/roles/product-manager.product.md +6 -9
  224. package/skills/roles/qa.ai-eval.md +8 -9
  225. package/skills/roles/qa.api-contract.md +7 -9
  226. package/skills/roles/qa.data-pipeline.md +7 -9
  227. package/skills/roles/qa.md +7 -9
  228. package/skills/roles/qa.test-automation.md +6 -9
  229. package/skills/roles/qa.web-ui.md +7 -9
  230. package/skills/roles/researcher.explorer.md +6 -9
  231. package/skills/roles/researcher.md +8 -9
  232. package/skills/roles/researcher.ux.md +6 -9
  233. package/skills/roles/reviewer.devil-advocate.md +6 -9
  234. package/skills/roles/reviewer.evaluator.md +6 -9
  235. package/skills/roles/reviewer.md +9 -9
  236. package/skills/roles/reviewer.trace.md +6 -9
  237. package/skills/roles/security.ai.md +7 -9
  238. package/skills/roles/security.appsec.md +6 -9
  239. package/skills/roles/security.cloud.md +6 -9
  240. package/skills/roles/security.legal-compliance.md +6 -9
  241. package/skills/roles/security.md +7 -9
  242. package/skills/roles/security.privacy.md +7 -9
  243. package/skills/roles/security.supply-chain.md +7 -9
  244. package/skills/security/blue-team.md +4 -5
  245. package/skills/security/code-audit.md +4 -5
  246. package/skills/security/pentest.md +4 -5
  247. package/skills/security/red-team.md +4 -5
  248. package/skills/security/threat-intel.md +4 -5
  249. package/skills/security/vuln-research.md +4 -5
  250. package/skills/strategy/competitive-landscape.md +4 -8
  251. package/skills/strategy/market-research-methods.md +4 -8
  252. package/skills/strategy/narrative-arc.md +4 -8
  253. package/skills/strategy/pricing-positioning.md +4 -8
  254. package/skills/utility/clean-code.md +4 -5
  255. package/specialists/policy-inventory.json +14 -0
  256. package/lib/specialist-contracts-enforce.mjs +0 -172
  257. package/rules/common/agents.md +0 -28
  258. package/rules/common/development-workflow.md +0 -32
  259. package/rules/common/performance.md +0 -55
@@ -6,10 +6,26 @@
6
6
  * pulling new models, and configuring Ollama as a provider.
7
7
  */
8
8
 
9
- import { execSync } from 'node:child_process';
9
+ import { execFileSync } from 'node:child_process';
10
10
  import { join } from 'node:path';
11
11
  import { homedir } from 'node:os';
12
12
 
13
+ // argv-array form for every shell-out: no shell interpolation, model names and
14
+ // JSON bodies are passed as discrete arguments / stdin so user-supplied values
15
+ // cannot break out of the command.
16
+
17
+ function curlGet(url, { timeout } = {}) {
18
+ return execFileSync('curl', ['-s', url], { encoding: 'utf8', timeout });
19
+ }
20
+
21
+ function curlPost(url, body, { timeout } = {}) {
22
+ return execFileSync('curl', ['-s', url, '--data-binary', '@-'], {
23
+ encoding: 'utf8',
24
+ timeout,
25
+ input: typeof body === 'string' ? body : JSON.stringify(body),
26
+ });
27
+ }
28
+
13
29
  const OLLAMA_BASE_URL = process.env.OLLAMA_BASE_URL || process.env.OLLAMA_HOST || 'http://localhost:11434';
14
30
 
15
31
  /**
@@ -17,14 +33,11 @@ const OLLAMA_BASE_URL = process.env.OLLAMA_BASE_URL || process.env.OLLAMA_HOST |
17
33
  */
18
34
  export function checkOllamaStatus() {
19
35
  try {
20
- execSync('which ollama', { stdio: 'pipe' });
36
+ execFileSync('which', ['ollama'], { stdio: 'pipe' });
21
37
  const installed = true;
22
-
38
+
23
39
  try {
24
- const response = execSync(`curl -s ${OLLAMA_BASE_URL}/api/tags`, {
25
- encoding: 'utf8',
26
- timeout: 3000
27
- });
40
+ const response = curlGet(`${OLLAMA_BASE_URL}/api/tags`, { timeout: 3000 });
28
41
  const data = JSON.parse(response);
29
42
  return {
30
43
  installed,
@@ -54,10 +67,7 @@ export function checkOllamaStatus() {
54
67
  */
55
68
  export function listModels() {
56
69
  try {
57
- const response = execSync(`curl -s ${OLLAMA_BASE_URL}/api/tags`, {
58
- encoding: 'utf8',
59
- timeout: 5000
60
- });
70
+ const response = curlGet(`${OLLAMA_BASE_URL}/api/tags`, { timeout: 5000 });
61
71
  const data = JSON.parse(response);
62
72
  return data.models || [];
63
73
  } catch (e) {
@@ -71,7 +81,7 @@ export function listModels() {
71
81
  export function pullModel(modelName, { verbose = false } = {}) {
72
82
  console.log(`Pulling ${modelName}...`);
73
83
  try {
74
- const output = execSync(`ollama pull ${modelName}`, {
84
+ const output = execFileSync('ollama', ['pull', modelName], {
75
85
  encoding: 'utf8',
76
86
  stdio: verbose ? 'inherit' : 'pipe'
77
87
  });
@@ -86,7 +96,7 @@ export function pullModel(modelName, { verbose = false } = {}) {
86
96
  */
87
97
  export function removeModel(modelName) {
88
98
  try {
89
- execSync(`ollama rm ${modelName}`, { encoding: 'utf8' });
99
+ execFileSync('ollama', ['rm', modelName], { encoding: 'utf8' });
90
100
  return { success: true };
91
101
  } catch (e) {
92
102
  return { success: false, error: e.message };
@@ -98,10 +108,7 @@ export function removeModel(modelName) {
98
108
  */
99
109
  export function showModel(modelName) {
100
110
  try {
101
- const response = execSync(`curl -s ${OLLAMA_BASE_URL}/api/show -d '{"name":"${modelName}"}'`, {
102
- encoding: 'utf8',
103
- timeout: 5000
104
- });
111
+ const response = curlPost(`${OLLAMA_BASE_URL}/api/show`, { name: modelName }, { timeout: 5000 });
105
112
  return JSON.parse(response);
106
113
  } catch (e) {
107
114
  throw new Error(`Failed to fetch model details: ${e.message}`);
@@ -113,14 +120,11 @@ export function showModel(modelName) {
113
120
  */
114
121
  export function testModel(modelName, prompt = 'Say hello in one sentence.') {
115
122
  try {
116
- const response = execSync(`curl -s ${OLLAMA_BASE_URL}/api/generate -d '${JSON.stringify({
117
- model: modelName,
118
- prompt: prompt,
119
- stream: false
120
- })}'`, {
121
- encoding: 'utf8',
122
- timeout: 30000
123
- });
123
+ const response = curlPost(
124
+ `${OLLAMA_BASE_URL}/api/generate`,
125
+ { model: modelName, prompt, stream: false },
126
+ { timeout: 30000 },
127
+ );
124
128
  const data = JSON.parse(response);
125
129
  return {
126
130
  success: true,
@@ -371,7 +375,7 @@ async function cmdOllamaSetup(args) {
371
375
  if (autoYes || process.platform === 'darwin') {
372
376
  console.log('Installing via Homebrew...');
373
377
  try {
374
- execSync('brew install ollama', { stdio: 'inherit' });
378
+ execFileSync('brew', ['install', 'ollama'], { stdio: 'inherit' });
375
379
  console.log('✅ Ollama installed');
376
380
  } catch (e) {
377
381
  console.error('❌ Installation failed. Please install manually: https://ollama.com');
@@ -390,11 +394,11 @@ async function cmdOllamaSetup(args) {
390
394
  console.log('⚠️ Ollama is not running');
391
395
  console.log('Starting Ollama service...');
392
396
  try {
393
- execSync('brew services start ollama', { stdio: 'pipe' });
397
+ execFileSync('brew', ['services', 'start', 'ollama'], { stdio: 'pipe' });
394
398
  console.log('✅ Ollama service started');
395
399
  // Wait for it to be ready
396
400
  console.log('Waiting for Ollama to be ready...');
397
- execSync('sleep 3');
401
+ execFileSync('sleep', ['3']);
398
402
  } catch (e) {
399
403
  console.log('You can start it manually with: ollama serve');
400
404
  }
@@ -94,7 +94,17 @@ function compactTokens(text, tokenLimit = 300, { modelId = 'default' } = {}) {
94
94
  function readPromptBody(promptFile, rootDir) {
95
95
  const filePath = path.join(rootDir, promptFile);
96
96
  if (!fs.existsSync(filePath)) return '';
97
- return fs.readFileSync(filePath, 'utf8').trim();
97
+ const raw = fs.readFileSync(filePath, 'utf8');
98
+ return stripLeadingYamlFrontmatter(raw).trim();
99
+ }
100
+
101
+ function stripLeadingYamlFrontmatter(content) {
102
+ if (!content.startsWith('---\n') && !content.startsWith('---\r\n')) return content;
103
+ const closeIdx = content.indexOf('\n---', 4);
104
+ if (closeIdx === -1) return content;
105
+ const afterClose = content.indexOf('\n', closeIdx + 1);
106
+ if (afterClose === -1) return '';
107
+ return content.slice(afterClose + 1);
98
108
  }
99
109
 
100
110
  function buildLearnedPatternsBlock(agentName, {
@@ -0,0 +1,129 @@
1
+ /**
2
+ * lib/runtime/uv-bootstrap.mjs — idempotent uv + docling venv provisioner.
3
+ *
4
+ * On first use: downloads uv via the official Astral installer, creates
5
+ * `<rootDir>/.cx/runtime/docling/.venv`, installs the pinned docling release.
6
+ * Subsequent calls are no-ops if the venv is already present.
7
+ *
8
+ * Best-practice notes (2026-06):
9
+ * - uv (Astral) is the consensus Python tooling, ~10× faster than pip, single
10
+ * binary, lockfile-based. OpenAI acquired Astral on 2026-03-19; the project
11
+ * remains Apache-2.0 and actively maintained, but single-vendor governance
12
+ * is a documented concern — flagged for re-evaluation if licensing shifts.
13
+ * - docling (IBM, MIT, donated to LF AI & Data early 2026) is the consensus
14
+ * multi-format doc parser for layout-aware MD/JSON output.
15
+ *
16
+ * Returns the path to the venv's python binary so callers can spawn the
17
+ * sidecar without relying on PATH state.
18
+ */
19
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
20
+ import { spawnSync } from 'node:child_process';
21
+ import path from 'node:path';
22
+ import os from 'node:os';
23
+
24
+ const DOCLING_PIN = '2.45.0';
25
+ const UV_INSTALL_URL = 'https://astral.sh/uv/install.sh';
26
+ const UV_TIMEOUT_MS = 120_000;
27
+ const VENV_TIMEOUT_MS = 240_000;
28
+ const INSTALL_TIMEOUT_MS = 600_000;
29
+
30
+ export function defaultRuntimeDir(cwd = process.cwd()) {
31
+ return path.join(cwd, '.cx', 'runtime', 'docling');
32
+ }
33
+
34
+ function which(bin) {
35
+ const result = spawnSync(process.platform === 'win32' ? 'where' : 'which', [bin], { encoding: 'utf8' });
36
+ if (result.status !== 0) return null;
37
+ return result.stdout.trim().split('\n')[0] || null;
38
+ }
39
+
40
+ function ensureUv(installDir) {
41
+ const fromPath = which('uv');
42
+ if (fromPath) return fromPath;
43
+ const cachedUv = path.join(installDir, 'bin', 'uv');
44
+ if (existsSync(cachedUv)) return cachedUv;
45
+ mkdirSync(installDir, { recursive: true });
46
+ const env = { ...process.env, UV_INSTALL_DIR: path.join(installDir, 'bin'), UV_NO_MODIFY_PATH: '1' };
47
+ const sh = spawnSync('sh', ['-c', `curl -LsSf ${UV_INSTALL_URL} | sh`], {
48
+ env,
49
+ timeout: UV_TIMEOUT_MS,
50
+ encoding: 'utf8',
51
+ });
52
+ if (sh.status !== 0) {
53
+ throw new Error(`uv install failed (${sh.status}): ${sh.stderr || sh.stdout}`);
54
+ }
55
+ if (!existsSync(cachedUv)) {
56
+ throw new Error(`uv install reported success but binary not found at ${cachedUv}`);
57
+ }
58
+ return cachedUv;
59
+ }
60
+
61
+ function pythonBinFor(venvDir) {
62
+ const candidates = process.platform === 'win32'
63
+ ? [path.join(venvDir, 'Scripts', 'python.exe')]
64
+ : [path.join(venvDir, 'bin', 'python'), path.join(venvDir, 'bin', 'python3')];
65
+ return candidates.find((p) => existsSync(p));
66
+ }
67
+
68
+ function readMarker(markerPath) {
69
+ try { return JSON.parse(readFileSync(markerPath, 'utf8')); }
70
+ catch { return null; }
71
+ }
72
+
73
+ function writeMarker(markerPath, payload) {
74
+ mkdirSync(path.dirname(markerPath), { recursive: true });
75
+ writeFileSync(markerPath, JSON.stringify(payload, null, 2) + '\n', 'utf8');
76
+ }
77
+
78
+ export function ensureDoclingVenv({ runtimeDir = defaultRuntimeDir(), force = false } = {}) {
79
+ mkdirSync(runtimeDir, { recursive: true });
80
+ const venvDir = path.join(runtimeDir, '.venv');
81
+ const markerPath = path.join(runtimeDir, '.install-marker.json');
82
+ const existingMarker = readMarker(markerPath);
83
+ const existingPython = pythonBinFor(venvDir);
84
+
85
+ if (!force && existingPython && existingMarker?.doclingVersion === DOCLING_PIN) {
86
+ return { pythonBin: existingPython, venvDir, runtimeDir, fresh: false };
87
+ }
88
+
89
+ const uv = ensureUv(runtimeDir);
90
+
91
+ const venvResult = spawnSync(uv, ['venv', venvDir, '--python', '3.11'], {
92
+ timeout: VENV_TIMEOUT_MS,
93
+ encoding: 'utf8',
94
+ });
95
+ if (venvResult.status !== 0) {
96
+ throw new Error(`uv venv failed (${venvResult.status}): ${venvResult.stderr}`);
97
+ }
98
+
99
+ const installResult = spawnSync(uv, ['pip', 'install', '--python', pythonBinFor(venvDir), `docling==${DOCLING_PIN}`], {
100
+ timeout: INSTALL_TIMEOUT_MS,
101
+ encoding: 'utf8',
102
+ });
103
+ if (installResult.status !== 0) {
104
+ throw new Error(`docling install failed (${installResult.status}): ${installResult.stderr}`);
105
+ }
106
+
107
+ const pythonBin = pythonBinFor(venvDir);
108
+ writeMarker(markerPath, {
109
+ doclingVersion: DOCLING_PIN,
110
+ installedAt: new Date().toISOString(),
111
+ pythonBin,
112
+ uvVersion: spawnSync(uv, ['--version'], { encoding: 'utf8' }).stdout.trim() || null,
113
+ platform: `${process.platform}-${process.arch}`,
114
+ node: process.version,
115
+ });
116
+
117
+ return { pythonBin, venvDir, runtimeDir, fresh: true };
118
+ }
119
+
120
+ export function describeDoclingRuntime({ runtimeDir = defaultRuntimeDir() } = {}) {
121
+ const venvDir = path.join(runtimeDir, '.venv');
122
+ return {
123
+ runtimeDir,
124
+ venvDir,
125
+ pythonBin: pythonBinFor(venvDir),
126
+ marker: readMarker(path.join(runtimeDir, '.install-marker.json')),
127
+ available: !!pythonBinFor(venvDir),
128
+ };
129
+ }
@@ -0,0 +1,102 @@
1
+ /**
2
+ * lib/runtime/whisper-bootstrap.mjs — provision whisper.cpp binary + ASR model.
3
+ *
4
+ * Strategy (2026-06):
5
+ * 1. Prefer system-installed whisper-cli (or legacy `main`) on PATH —
6
+ * `brew install whisper-cpp` (macOS, Metal-accelerated) or distro
7
+ * package on Linux.
8
+ * 2. Fall back to bundled `whisper-cli` cached at <runtimeDir>/bin/ if a
9
+ * previous bootstrap fetched one.
10
+ * 3. If neither, return a structured "not-available" descriptor — caller
11
+ * surfaces a user-actionable install hint rather than crashing.
12
+ *
13
+ * Model files are downloaded directly from Hugging Face the first time the
14
+ * default `base.en` (or env-overridden) model is requested. Models cache at
15
+ * <runtimeDir>/models/. base.en is ~150MB; smaller than the small.en or
16
+ * medium.en model that callers can opt into via CONSTRUCT_WHISPER_MODEL.
17
+ *
18
+ * No bundled distribution: whisper.cpp upstream does not publish stable
19
+ * pre-built CLI binaries across platforms, and building from source via
20
+ * cmake adds a heavyweight first-run path. Defer that to a future opt-in.
21
+ */
22
+ import { existsSync, mkdirSync, statSync, createWriteStream } from 'node:fs';
23
+ import { spawnSync } from 'node:child_process';
24
+ import { pipeline } from 'node:stream/promises';
25
+ import path from 'node:path';
26
+ import os from 'node:os';
27
+
28
+ const HF_MODEL_BASE_URL = 'https://huggingface.co/ggerganov/whisper.cpp/resolve/main';
29
+ const DEFAULT_MODEL = process.env.CONSTRUCT_WHISPER_MODEL || 'base.en';
30
+ const KNOWN_MODELS = new Set(['tiny', 'tiny.en', 'base', 'base.en', 'small', 'small.en', 'medium', 'medium.en', 'large-v3', 'large-v3-turbo']);
31
+
32
+ export function defaultRuntimeDir(cwd = process.cwd()) {
33
+ return path.join(cwd, '.cx', 'runtime', 'whisper');
34
+ }
35
+
36
+ function which(bin) {
37
+ const result = spawnSync(process.platform === 'win32' ? 'where' : 'which', [bin], { encoding: 'utf8' });
38
+ if (result.status !== 0) return null;
39
+ return result.stdout.trim().split('\n')[0] || null;
40
+ }
41
+
42
+ export function locateWhisperBinary({ runtimeDir = defaultRuntimeDir() } = {}) {
43
+ for (const candidate of ['whisper-cli', 'whisper-cpp', 'main']) {
44
+ const found = which(candidate);
45
+ if (found) return { binary: found, source: 'system', name: candidate };
46
+ }
47
+ const cached = path.join(runtimeDir, 'bin', 'whisper-cli');
48
+ if (existsSync(cached)) return { binary: cached, source: 'cached', name: 'whisper-cli' };
49
+ return null;
50
+ }
51
+
52
+ export function describeWhisperRuntime({ runtimeDir = defaultRuntimeDir() } = {}) {
53
+ const located = locateWhisperBinary({ runtimeDir });
54
+ const modelPath = modelPathFor(DEFAULT_MODEL, runtimeDir);
55
+ return {
56
+ runtimeDir,
57
+ binary: located,
58
+ defaultModel: DEFAULT_MODEL,
59
+ modelPath,
60
+ modelInstalled: existsSync(modelPath),
61
+ available: !!located,
62
+ };
63
+ }
64
+
65
+ export function installHint() {
66
+ if (process.platform === 'darwin') return 'brew install whisper-cpp';
67
+ if (process.platform === 'linux') return 'See https://github.com/ggml-org/whisper.cpp#quick-start (build from source via cmake or install via your distro package manager).';
68
+ return 'See https://github.com/ggml-org/whisper.cpp#quick-start';
69
+ }
70
+
71
+ function modelPathFor(modelName, runtimeDir) {
72
+ return path.join(runtimeDir, 'models', `ggml-${modelName}.bin`);
73
+ }
74
+
75
+ export async function ensureWhisperModel({ runtimeDir = defaultRuntimeDir(), model = DEFAULT_MODEL } = {}) {
76
+ if (!KNOWN_MODELS.has(model)) {
77
+ throw new Error(`unknown whisper model: ${model} (allowed: ${[...KNOWN_MODELS].join(', ')})`);
78
+ }
79
+ const target = modelPathFor(model, runtimeDir);
80
+ if (existsSync(target) && statSync(target).size > 1024) return target;
81
+ mkdirSync(path.dirname(target), { recursive: true });
82
+ const url = `${HF_MODEL_BASE_URL}/ggml-${model}.bin`;
83
+ const response = await fetch(url, { redirect: 'follow' });
84
+ if (!response.ok) {
85
+ throw new Error(`failed to download whisper model ${model}: ${response.status} ${response.statusText} (${url})`);
86
+ }
87
+ await pipeline(response.body, createWriteStream(target));
88
+ return target;
89
+ }
90
+
91
+ export async function ensureWhisperRuntime({ runtimeDir = defaultRuntimeDir(), model = DEFAULT_MODEL } = {}) {
92
+ mkdirSync(runtimeDir, { recursive: true });
93
+ const located = locateWhisperBinary({ runtimeDir });
94
+ if (!located) {
95
+ const err = new Error(`whisper-cli not found; install via: ${installHint()}`);
96
+ err.code = 'WHISPER_BINARY_MISSING';
97
+ err.installHint = installHint();
98
+ throw err;
99
+ }
100
+ const modelPath = await ensureWhisperModel({ runtimeDir, model });
101
+ return { binary: located.binary, source: located.source, modelPath, model };
102
+ }
@@ -33,7 +33,27 @@ const __dirname = fileURLToPath(new URL('.', import.meta.url));
33
33
  const ROOT_DIR = join(__dirname, '..', '..');
34
34
  const HOME = homedir();
35
35
  const PORT = parseInt(process.env.PORT ?? '4242', 10);
36
- const BIND_HOST = process.env.BIND_HOST ?? (process.env.NODE_ENV === 'production' ? '0.0.0.0' : '127.0.0.1');
36
+
37
+ // Default bind is loopback regardless of NODE_ENV. Exposing the dashboard to
38
+ // non-loopback interfaces is opt-in via BIND_HOST and gated on configured
39
+ // auth (see assertSafeBind below).
40
+
41
+ const BIND_HOST = process.env.BIND_HOST ?? '127.0.0.1';
42
+
43
+ function isLoopbackHost(host) {
44
+ return host === '127.0.0.1' || host === 'localhost' || host === '::1';
45
+ }
46
+
47
+ function assertSafeBind() {
48
+ if (isLoopbackHost(BIND_HOST)) return;
49
+ if (!isAuthConfigured()) {
50
+ process.stderr.write(
51
+ `[dashboard] refuses to bind to non-loopback host ${BIND_HOST} without auth configured. ` +
52
+ `Set CONSTRUCT_DASHBOARD_TOKEN (or run \`construct dashboard auth init\`) before exposing the dashboard.\n`,
53
+ );
54
+ process.exit(1);
55
+ }
56
+ }
37
57
 
38
58
  const STATIC_DIR = join(__dirname, 'static');
39
59
  const REGISTRY_FILE = join(ROOT_DIR, 'specialists', 'registry.json');
@@ -2237,6 +2257,7 @@ if (process.env.CX_AUTO_EMBED === '1') {
2237
2257
  setInterval(runEmbedSync, intervalMs).unref();
2238
2258
  console.log(`Embed scheduler active — syncing every ${intervalMs / 60_000} min`);
2239
2259
  }
2260
+ assertSafeBind();
2240
2261
  server.listen(PORT, BIND_HOST, () => {
2241
2262
  console.log(`Construct dashboard running at http://${BIND_HOST}:${PORT}`);
2242
2263
  });
@@ -1 +1 @@
1
- <!DOCTYPE html><!--02hC8wDNvTNKwi4iN6D_t--><html lang="en" data-theme="dark" data-density="comfortable" data-motion="normal" class="__variable_629dad __variable_246ccd __variable_3c557b"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="preload" href="/_next/static/media/22a5144ee8d83bca-s.p.woff2" as="font" crossorigin="" type="font/woff2"/><link rel="preload" href="/_next/static/media/bb3ef058b751a6ad-s.p.woff2" as="font" crossorigin="" type="font/woff2"/><link rel="stylesheet" href="/_next/static/css/47a506ef57811fb9.css" data-precedence="next"/><link rel="preload" as="script" fetchPriority="low" href="/_next/static/chunks/webpack-8e0503f8a36190b3.js"/><script src="/_next/static/chunks/87c73c54-24122e7b92478d00.js" async=""></script><script src="/_next/static/chunks/18-1e25b5692ec1d26d.js" async=""></script><script src="/_next/static/chunks/main-app-07d82ec6c92d7402.js" async=""></script><script src="/_next/static/chunks/9664-af80478aa73ba424.js" async=""></script><script src="/_next/static/chunks/8144-da56a4a291c0b21a.js" async=""></script><script src="/_next/static/chunks/app/layout-1a2d2674aa220229.js" async=""></script><script src="/_next/static/chunks/app/page-2568fbdd2b0e1eca.js" async=""></script><meta name="next-size-adjust" content=""/><title>Construct — Dashboard</title><meta name="description" content="Local operations dashboard for Construct — approvals, health, knowledge, models, providers."/><script src="/_next/static/chunks/polyfills-42372ed130431b0a.js" noModule=""></script></head><body><div hidden=""><!--$--><!--/$--></div><div class="shell"><header class="topbar"><a class="brand" href="/"><div class="mark"></div><div class="name">Construct<em>dashboard</em></div></a><div class="search-wrap"><label class="search"><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="7"></circle><line x1="21" y1="21" x2="16.5" y2="16.5"></line></svg><input readOnly="" placeholder="Search dashboard…"/><span class="kbd">⌘K</span></label></div><div class="top-actions"><button class="icon-btn" title="Toggle theme" type="button"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="4"></circle><line x1="12" y1="2" x2="12" y2="5"></line><line x1="12" y1="19" x2="12" y2="22"></line><line x1="2" y1="12" x2="5" y2="12"></line><line x1="19" y1="12" x2="22" y2="12"></line><line x1="4.5" y1="4.5" x2="6.5" y2="6.5"></line><line x1="17.5" y1="17.5" x2="19.5" y2="19.5"></line><line x1="4.5" y1="19.5" x2="6.5" y2="17.5"></line><line x1="17.5" y1="6.5" x2="19.5" y2="4.5"></line></svg></button><button class="icon-btn" title="Toggle density" type="button"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="3" y1="7" x2="21" y2="7"></line><line x1="3" y1="12" x2="21" y2="12"></line><line x1="3" y1="17" x2="21" y2="17"></line></svg></button><a class="icon-btn outlined" href="https://github.com/geraldmaron/construct" target="_blank" rel="noreferrer"><svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor"><path d="M12 .5C5.65.5.5 5.66.5 12.02c0 5.09 3.29 9.4 7.86 10.92.58.1.79-.25.79-.55v-2.13c-3.2.7-3.87-1.37-3.87-1.37-.52-1.32-1.27-1.67-1.27-1.67-1.04-.71.08-.7.08-.7 1.15.08 1.76 1.18 1.76 1.18 1.02 1.76 2.69 1.25 3.35.96.1-.74.4-1.25.73-1.54-2.55-.29-5.24-1.28-5.24-5.7 0-1.26.45-2.29 1.18-3.1-.12-.29-.51-1.46.11-3.04 0 0 .96-.31 3.16 1.18a10.9 10.9 0 0 1 5.74 0c2.2-1.49 3.16-1.18 3.16-1.18.62 1.58.23 2.75.12 3.04.74.81 1.18 1.84 1.18 3.1 0 4.43-2.69 5.41-5.25 5.69.41.36.78 1.05.78 2.13v3.16c0 .31.21.66.79.55A11.52 11.52 0 0 0 23.5 12.02C23.5 5.66 18.35.5 12 .5Z"></path></svg> <span style="font-size:11px">repo</span></a></div><div class="progress"></div></header><div class="body-grid"><aside class="sidebar"><div class="side-group"><div class="side-label">Overview</div><a class="side-link active" href="/"><span class="num">01</span><span>Home</span></a><a class="side-link" href="/doctor/"><span class="num">02</span><span>Doctor</span></a><a class="side-link" href="/resources/"><span class="num">03</span><span>Services</span></a></div><div class="side-group"><div class="side-label">Activity</div><a class="side-link" href="/audit/"><span class="num">01</span><span>Audit trail</span></a><a class="side-link" href="/snapshots/"><span class="num">02</span><span>Snapshots</span></a><a class="side-link" href="/performance/"><span class="num">03</span><span>Performance</span></a></div><div class="side-group"><div class="side-label">Work</div><a class="side-link" href="/approvals/"><span class="num">01</span><span>Approvals</span></a><a class="side-link" href="/workflow/"><span class="num">02</span><span>Workflow</span></a><a class="side-link" href="/beads/"><span class="num">03</span><span>Beads</span></a><a class="side-link" href="/artifacts/"><span class="num">04</span><span>Artifacts</span></a><a class="side-link" href="/intake/"><span class="num">05</span><span>Intake</span></a></div><div class="side-group"><div class="side-label">Specialists</div><a class="side-link" href="/agents/"><span class="num">01</span><span>Specialists</span></a><a class="side-link" href="/skills/"><span class="num">02</span><span>Skills</span></a><a class="side-link" href="/commands/"><span class="num">03</span><span>Slash commands</span></a><a class="side-link" href="/hooks/"><span class="num">04</span><span>Hooks</span></a><a class="side-link" href="/plugins/"><span class="num">05</span><span>Plugins</span></a></div><div class="side-group"><div class="side-label">Models &amp; Providers</div><a class="side-link" href="/models/"><span class="num">01</span><span>Models</span></a><a class="side-link" href="/providers/"><span class="num">02</span><span>Providers</span></a><a class="side-link" href="/mcp/"><span class="num">03</span><span>MCP servers</span></a></div><div class="side-group"><div class="side-label">Knowledge</div><a class="side-link" href="/knowledge/"><span class="num">01</span><span>Knowledge</span></a><a class="side-link" href="/editor/"><span class="num">02</span><span>Editor</span></a></div><div class="side-group"><div class="side-label">System</div><a class="side-link" href="/config/"><span class="num">01</span><span>Config</span></a><a class="side-link" href="/infrastructure/"><span class="num">02</span><span>Infrastructure</span></a></div><div class="side-foot">Local dashboard for Construct. Theme + density preferences persist.</div></aside><main class="main"><div class="page"><div class="eyebrow"><span class="dot"></span><span>overview · construct dashboard</span></div><h1 class="page-title">System state</h1><p class="page-lede">The runtime in one place. Approvals, services, model providers, knowledge corpus, and intake — each section deep-links to its dedicated surface.</p><div class="meta-strip"><span class="pill">mode: <!-- -->…</span><span>instance: <!-- -->…</span><span class="sep">·</span><span>embed: <!-- -->stopped</span></div><div class="body"><div class="hero-stats" style="margin-top:0;margin-bottom:24px"><div class="hero-stat"><span class="k">Services</span><span class="v">…</span><span style="display:block;font-size:11.5px;color:var(--muted);margin-top:4px">running</span></div><div class="hero-stat"><span class="k">Approvals</span><span class="v">…</span><span style="display:block;font-size:11.5px;color:var(--muted);margin-top:4px">pending</span></div><div class="hero-stat"><span class="k">Skills</span><span class="v">0</span><span style="display:block;font-size:11.5px;color:var(--muted);margin-top:4px">registered</span></div><div class="hero-stat"><span class="k">Commands</span><span class="v">0</span><span style="display:block;font-size:11.5px;color:var(--muted);margin-top:4px">slash</span></div></div><section class="section open"><button class="section-head" aria-expanded="true" type="button"><span class="chev"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="9 6 15 12 9 18"></polyline></svg></span><span style="display:flex;flex-direction:column;gap:6px"><span class="section-num">01</span><span class="section-title">Pending approvals</span></span><span class="section-aside"><span>empty</span></span></button><div class="tldr-row"><div class="tldr"><b>TL;DR</b>Items waiting on a human gate. High-risk mutations (work item creation, merge, doc publish, config changes) land here.</div></div><div class="section-body-wrap"><div class="section-body"><div class="section-body-inner"><div class="body"><div style="padding:24px;text-align:center;color:var(--muted);font-family:var(--mono);font-size:12px">Loading…</div></div></div></div></div></section><section class="section"><button class="section-head" aria-expanded="false" type="button"><span class="chev"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="9 6 15 12 9 18"></polyline></svg></span><span style="display:flex;flex-direction:column;gap:6px"><span class="section-num">02</span><span class="section-title">Services</span></span><span class="section-aside"><span>0/0 running</span></span></button><div class="tldr-row"><div class="tldr"><b>TL;DR</b>Local processes that back the runtime: Postgres, memory bridge, OpenCode bridge, dashboard, embed daemon.</div></div><div class="section-body-wrap"><div class="section-body"><div class="section-body-inner"><div class="body"><div style="padding:24px;text-align:center;color:var(--muted);font-family:var(--mono);font-size:12px">Loading…</div><p style="margin-top:12px"><a class="link" href="/resources/">Open services dashboard →</a></p></div></div></div></div></section><section class="section"><button class="section-head" aria-expanded="false" type="button"><span class="chev"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="9 6 15 12 9 18"></polyline></svg></span><span style="display:flex;flex-direction:column;gap:6px"><span class="section-num">03</span><span class="section-title">Credentials</span></span><span class="section-aside"><span>0 tracked</span></span></button><div class="tldr-row"><div class="tldr"><b>TL;DR</b>Provider API keys, billing posture, and 1Password references. Set them in /providers.</div></div><div class="section-body-wrap"><div class="section-body"><div class="section-body-inner"><div class="body"><p style="margin-top:12px"><a class="link" href="/providers/">Manage providers →</a></p></div></div></div></div></section><section class="section"><button class="section-head" aria-expanded="false" type="button"><span class="chev"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="9 6 15 12 9 18"></polyline></svg></span><span style="display:flex;flex-direction:column;gap:6px"><span class="section-num">04</span><span class="section-title">Quick links</span></span><span class="section-aside"></span></button><div class="tldr-row"><div class="tldr"><b>TL;DR</b>Deep links into every dashboard surface.</div></div><div class="section-body-wrap"><div class="section-body"><div class="section-body-inner"><div class="body"><div class="callout"><span class="clt-label">Where to next</span><div><p><a class="link" href="/doctor/">Doctor</a>,<!-- --> <a class="link" href="/knowledge/">Knowledge</a>,<!-- --> <a class="link" href="/intake/">Intake</a>,<!-- --> <a class="link" href="/workflow/">Workflow</a>,<!-- --> <a class="link" href="/agents/">Specialists</a>,<!-- --> <a class="link" href="/models/">Models</a>,<!-- --> <a class="link" href="/audit/">Audit</a>.</p></div></div></div></div></div></div></section></div></div><!--$--><!--/$--></main></div></div><script src="/_next/static/chunks/webpack-8e0503f8a36190b3.js" id="_R_" async=""></script><script>(self.__next_f=self.__next_f||[]).push([0])</script><script>self.__next_f.push([1,"1:\"$Sreact.fragment\"\n2:I[105,[\"9664\",\"static/chunks/9664-af80478aa73ba424.js\",\"8144\",\"static/chunks/8144-da56a4a291c0b21a.js\",\"7177\",\"static/chunks/app/layout-1a2d2674aa220229.js\"],\"AppShellClient\"]\n3:I[5341,[],\"\"]\n4:I[25,[],\"\"]\n5:I[1012,[],\"ClientPageRoot\"]\n6:I[9008,[\"9664\",\"static/chunks/9664-af80478aa73ba424.js\",\"8144\",\"static/chunks/8144-da56a4a291c0b21a.js\",\"8974\",\"static/chunks/app/page-2568fbdd2b0e1eca.js\"],\"default\"]\n9:I[5104,[],\"OutletBoundary\"]\nb:I[7158,[],\"AsyncMetadataOutlet\"]\nd:I[5104,[],\"ViewportBoundary\"]\nf:I[5104,[],\"MetadataBoundary\"]\n10:\"$Sreact.suspense\"\n12:I[4431,[],\"\"]\n:HL[\"/_next/static/media/22a5144ee8d83bca-s.p.woff2\",\"font\",{\"crossOrigin\":\"\",\"type\":\"font/woff2\"}]\n:HL[\"/_next/static/media/bb3ef058b751a6ad-s.p.woff2\",\"font\",{\"crossOrigin\":\"\",\"type\":\"font/woff2\"}]\n:HL[\"/_next/static/css/47a506ef57811fb9.css\",\"style\"]\n"])</script><script>self.__next_f.push([1,"0:{\"P\":null,\"b\":\"02hC8wDNvTNKwi4iN6D_t\",\"p\":\"\",\"c\":[\"\",\"\"],\"i\":false,\"f\":[[[\"\",{\"children\":[\"__PAGE__\",{}]},\"$undefined\",\"$undefined\",true],[\"\",[\"$\",\"$1\",\"c\",{\"children\":[[[\"$\",\"link\",\"0\",{\"rel\":\"stylesheet\",\"href\":\"/_next/static/css/47a506ef57811fb9.css\",\"precedence\":\"next\",\"crossOrigin\":\"$undefined\",\"nonce\":\"$undefined\"}]],[\"$\",\"html\",null,{\"lang\":\"en\",\"data-theme\":\"dark\",\"data-density\":\"comfortable\",\"data-motion\":\"normal\",\"suppressHydrationWarning\":true,\"className\":\"__variable_629dad __variable_246ccd __variable_3c557b\",\"children\":[\"$\",\"body\",null,{\"children\":[\"$\",\"$L2\",null,{\"nav\":[{\"label\":\"Overview\",\"items\":[{\"id\":\"home\",\"title\":\"Home\",\"href\":\"/\"},{\"id\":\"doctor\",\"title\":\"Doctor\",\"href\":\"/doctor\"},{\"id\":\"resources\",\"title\":\"Services\",\"href\":\"/resources\"}]},{\"label\":\"Activity\",\"items\":[{\"id\":\"audit\",\"title\":\"Audit trail\",\"href\":\"/audit\"},{\"id\":\"snapshots\",\"title\":\"Snapshots\",\"href\":\"/snapshots\"},{\"id\":\"performance\",\"title\":\"Performance\",\"href\":\"/performance\"}]},{\"label\":\"Work\",\"items\":[{\"id\":\"approvals\",\"title\":\"Approvals\",\"href\":\"/approvals\"},{\"id\":\"workflow\",\"title\":\"Workflow\",\"href\":\"/workflow\"},{\"id\":\"beads\",\"title\":\"Beads\",\"href\":\"/beads\"},{\"id\":\"artifacts\",\"title\":\"Artifacts\",\"href\":\"/artifacts\"},{\"id\":\"intake\",\"title\":\"Intake\",\"href\":\"/intake\"}]},{\"label\":\"Specialists\",\"items\":[{\"id\":\"agents\",\"title\":\"Specialists\",\"href\":\"/agents\"},{\"id\":\"skills\",\"title\":\"Skills\",\"href\":\"/skills\"},{\"id\":\"commands\",\"title\":\"Slash commands\",\"href\":\"/commands\"},{\"id\":\"hooks\",\"title\":\"Hooks\",\"href\":\"/hooks\"},{\"id\":\"plugins\",\"title\":\"Plugins\",\"href\":\"/plugins\"}]},{\"label\":\"Models \u0026 Providers\",\"items\":[{\"id\":\"models\",\"title\":\"Models\",\"href\":\"/models\"},{\"id\":\"providers\",\"title\":\"Providers\",\"href\":\"/providers\"},{\"id\":\"mcp\",\"title\":\"MCP servers\",\"href\":\"/mcp\"}]},{\"label\":\"Knowledge\",\"items\":[{\"id\":\"knowledge\",\"title\":\"Knowledge\",\"href\":\"/knowledge\"},{\"id\":\"editor\",\"title\":\"Editor\",\"href\":\"/editor\"}]},{\"label\":\"System\",\"items\":[{\"id\":\"config\",\"title\":\"Config\",\"href\":\"/config\"},{\"id\":\"infrastructure\",\"title\":\"Infrastructure\",\"href\":\"/infrastructure\"}]}],\"children\":[\"$\",\"$L3\",null,{\"parallelRouterKey\":\"children\",\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$L4\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":[[[\"$\",\"title\",null,{\"children\":\"404: This page could not be found.\"}],[\"$\",\"div\",null,{\"style\":{\"fontFamily\":\"system-ui,\\\"Segoe UI\\\",Roboto,Helvetica,Arial,sans-serif,\\\"Apple Color Emoji\\\",\\\"Segoe UI Emoji\\\"\",\"height\":\"100vh\",\"textAlign\":\"center\",\"display\":\"flex\",\"flexDirection\":\"column\",\"alignItems\":\"center\",\"justifyContent\":\"center\"},\"children\":[\"$\",\"div\",null,{\"children\":[[\"$\",\"style\",null,{\"dangerouslySetInnerHTML\":{\"__html\":\"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}\"}}],[\"$\",\"h1\",null,{\"className\":\"next-error-h1\",\"style\":{\"display\":\"inline-block\",\"margin\":\"0 20px 0 0\",\"padding\":\"0 23px 0 0\",\"fontSize\":24,\"fontWeight\":500,\"verticalAlign\":\"top\",\"lineHeight\":\"49px\"},\"children\":404}],[\"$\",\"div\",null,{\"style\":{\"display\":\"inline-block\"},\"children\":[\"$\",\"h2\",null,{\"style\":{\"fontSize\":14,\"fontWeight\":400,\"lineHeight\":\"49px\",\"margin\":0},\"children\":\"This page could not be found.\"}]}]]}]}]],[]],\"forbidden\":\"$undefined\",\"unauthorized\":\"$undefined\"}]}]}]}]]}],{\"children\":[\"__PAGE__\",[\"$\",\"$1\",\"c\",{\"children\":[[\"$\",\"$L5\",null,{\"Component\":\"$6\",\"searchParams\":{},\"params\":{},\"promises\":[\"$@7\",\"$@8\"]}],null,[\"$\",\"$L9\",null,{\"children\":[\"$La\",[\"$\",\"$Lb\",null,{\"promise\":\"$@c\"}]]}]]}],{},null,false]},null,false],[\"$\",\"$1\",\"h\",{\"children\":[null,[[\"$\",\"$Ld\",null,{\"children\":\"$Le\"}],[\"$\",\"meta\",null,{\"name\":\"next-size-adjust\",\"content\":\"\"}]],[\"$\",\"$Lf\",null,{\"children\":[\"$\",\"div\",null,{\"hidden\":true,\"children\":[\"$\",\"$10\",null,{\"fallback\":null,\"children\":\"$L11\"}]}]}]]}],false]],\"m\":\"$undefined\",\"G\":[\"$12\",[]],\"s\":false,\"S\":true}\n"])</script><script>self.__next_f.push([1,"7:{}\n8:\"$0:f:0:1:2:children:1:props:children:0:props:params\"\n"])</script><script>self.__next_f.push([1,"e:[[\"$\",\"meta\",\"0\",{\"charSet\":\"utf-8\"}],[\"$\",\"meta\",\"1\",{\"name\":\"viewport\",\"content\":\"width=device-width, initial-scale=1\"}]]\na:null\n"])</script><script>self.__next_f.push([1,"c:{\"metadata\":[[\"$\",\"title\",\"0\",{\"children\":\"Construct — Dashboard\"}],[\"$\",\"meta\",\"1\",{\"name\":\"description\",\"content\":\"Local operations dashboard for Construct — approvals, health, knowledge, models, providers.\"}]],\"error\":null,\"digest\":\"$undefined\"}\n"])</script><script>self.__next_f.push([1,"11:\"$c:metadata\"\n"])</script></body></html>
1
+ <!DOCTYPE html><!--qA_CTDVxjnG_0_bArvNYn--><html lang="en" data-theme="dark" data-density="comfortable" data-motion="normal" class="__variable_629dad __variable_246ccd __variable_3c557b"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="preload" href="/_next/static/media/22a5144ee8d83bca-s.p.woff2" as="font" crossorigin="" type="font/woff2"/><link rel="preload" href="/_next/static/media/bb3ef058b751a6ad-s.p.woff2" as="font" crossorigin="" type="font/woff2"/><link rel="stylesheet" href="/_next/static/css/47a506ef57811fb9.css" data-precedence="next"/><link rel="preload" as="script" fetchPriority="low" href="/_next/static/chunks/webpack-8e0503f8a36190b3.js"/><script src="/_next/static/chunks/87c73c54-24122e7b92478d00.js" async=""></script><script src="/_next/static/chunks/18-1e25b5692ec1d26d.js" async=""></script><script src="/_next/static/chunks/main-app-07d82ec6c92d7402.js" async=""></script><script src="/_next/static/chunks/9664-af80478aa73ba424.js" async=""></script><script src="/_next/static/chunks/8144-da56a4a291c0b21a.js" async=""></script><script src="/_next/static/chunks/app/layout-1a2d2674aa220229.js" async=""></script><script src="/_next/static/chunks/app/page-2568fbdd2b0e1eca.js" async=""></script><meta name="next-size-adjust" content=""/><title>Construct — Dashboard</title><meta name="description" content="Local operations dashboard for Construct — approvals, health, knowledge, models, providers."/><script src="/_next/static/chunks/polyfills-42372ed130431b0a.js" noModule=""></script></head><body><div hidden=""><!--$--><!--/$--></div><div class="shell"><header class="topbar"><a class="brand" href="/"><div class="mark"></div><div class="name">Construct<em>dashboard</em></div></a><div class="search-wrap"><label class="search"><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="7"></circle><line x1="21" y1="21" x2="16.5" y2="16.5"></line></svg><input readOnly="" placeholder="Search dashboard…"/><span class="kbd">⌘K</span></label></div><div class="top-actions"><button class="icon-btn" title="Toggle theme" type="button"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="4"></circle><line x1="12" y1="2" x2="12" y2="5"></line><line x1="12" y1="19" x2="12" y2="22"></line><line x1="2" y1="12" x2="5" y2="12"></line><line x1="19" y1="12" x2="22" y2="12"></line><line x1="4.5" y1="4.5" x2="6.5" y2="6.5"></line><line x1="17.5" y1="17.5" x2="19.5" y2="19.5"></line><line x1="4.5" y1="19.5" x2="6.5" y2="17.5"></line><line x1="17.5" y1="6.5" x2="19.5" y2="4.5"></line></svg></button><button class="icon-btn" title="Toggle density" type="button"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="3" y1="7" x2="21" y2="7"></line><line x1="3" y1="12" x2="21" y2="12"></line><line x1="3" y1="17" x2="21" y2="17"></line></svg></button><a class="icon-btn outlined" href="https://github.com/geraldmaron/construct" target="_blank" rel="noreferrer"><svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor"><path d="M12 .5C5.65.5.5 5.66.5 12.02c0 5.09 3.29 9.4 7.86 10.92.58.1.79-.25.79-.55v-2.13c-3.2.7-3.87-1.37-3.87-1.37-.52-1.32-1.27-1.67-1.27-1.67-1.04-.71.08-.7.08-.7 1.15.08 1.76 1.18 1.76 1.18 1.02 1.76 2.69 1.25 3.35.96.1-.74.4-1.25.73-1.54-2.55-.29-5.24-1.28-5.24-5.7 0-1.26.45-2.29 1.18-3.1-.12-.29-.51-1.46.11-3.04 0 0 .96-.31 3.16 1.18a10.9 10.9 0 0 1 5.74 0c2.2-1.49 3.16-1.18 3.16-1.18.62 1.58.23 2.75.12 3.04.74.81 1.18 1.84 1.18 3.1 0 4.43-2.69 5.41-5.25 5.69.41.36.78 1.05.78 2.13v3.16c0 .31.21.66.79.55A11.52 11.52 0 0 0 23.5 12.02C23.5 5.66 18.35.5 12 .5Z"></path></svg> <span style="font-size:11px">repo</span></a></div><div class="progress"></div></header><div class="body-grid"><aside class="sidebar"><div class="side-group"><div class="side-label">Overview</div><a class="side-link active" href="/"><span class="num">01</span><span>Home</span></a><a class="side-link" href="/doctor/"><span class="num">02</span><span>Doctor</span></a><a class="side-link" href="/resources/"><span class="num">03</span><span>Services</span></a></div><div class="side-group"><div class="side-label">Activity</div><a class="side-link" href="/audit/"><span class="num">01</span><span>Audit trail</span></a><a class="side-link" href="/snapshots/"><span class="num">02</span><span>Snapshots</span></a><a class="side-link" href="/performance/"><span class="num">03</span><span>Performance</span></a></div><div class="side-group"><div class="side-label">Work</div><a class="side-link" href="/approvals/"><span class="num">01</span><span>Approvals</span></a><a class="side-link" href="/workflow/"><span class="num">02</span><span>Workflow</span></a><a class="side-link" href="/beads/"><span class="num">03</span><span>Beads</span></a><a class="side-link" href="/artifacts/"><span class="num">04</span><span>Artifacts</span></a><a class="side-link" href="/intake/"><span class="num">05</span><span>Intake</span></a></div><div class="side-group"><div class="side-label">Specialists</div><a class="side-link" href="/agents/"><span class="num">01</span><span>Specialists</span></a><a class="side-link" href="/skills/"><span class="num">02</span><span>Skills</span></a><a class="side-link" href="/commands/"><span class="num">03</span><span>Slash commands</span></a><a class="side-link" href="/hooks/"><span class="num">04</span><span>Hooks</span></a><a class="side-link" href="/plugins/"><span class="num">05</span><span>Plugins</span></a></div><div class="side-group"><div class="side-label">Models &amp; Providers</div><a class="side-link" href="/models/"><span class="num">01</span><span>Models</span></a><a class="side-link" href="/providers/"><span class="num">02</span><span>Providers</span></a><a class="side-link" href="/mcp/"><span class="num">03</span><span>MCP servers</span></a></div><div class="side-group"><div class="side-label">Knowledge</div><a class="side-link" href="/knowledge/"><span class="num">01</span><span>Knowledge</span></a><a class="side-link" href="/editor/"><span class="num">02</span><span>Editor</span></a></div><div class="side-group"><div class="side-label">System</div><a class="side-link" href="/config/"><span class="num">01</span><span>Config</span></a><a class="side-link" href="/infrastructure/"><span class="num">02</span><span>Infrastructure</span></a></div><div class="side-foot">Local dashboard for Construct. Theme + density preferences persist.</div></aside><main class="main"><div class="page"><div class="eyebrow"><span class="dot"></span><span>overview · construct dashboard</span></div><h1 class="page-title">System state</h1><p class="page-lede">The runtime in one place. Approvals, services, model providers, knowledge corpus, and intake — each section deep-links to its dedicated surface.</p><div class="meta-strip"><span class="pill">mode: <!-- -->…</span><span>instance: <!-- -->…</span><span class="sep">·</span><span>embed: <!-- -->stopped</span></div><div class="body"><div class="hero-stats" style="margin-top:0;margin-bottom:24px"><div class="hero-stat"><span class="k">Services</span><span class="v">…</span><span style="display:block;font-size:11.5px;color:var(--muted);margin-top:4px">running</span></div><div class="hero-stat"><span class="k">Approvals</span><span class="v">…</span><span style="display:block;font-size:11.5px;color:var(--muted);margin-top:4px">pending</span></div><div class="hero-stat"><span class="k">Skills</span><span class="v">0</span><span style="display:block;font-size:11.5px;color:var(--muted);margin-top:4px">registered</span></div><div class="hero-stat"><span class="k">Commands</span><span class="v">0</span><span style="display:block;font-size:11.5px;color:var(--muted);margin-top:4px">slash</span></div></div><section class="section open"><button class="section-head" aria-expanded="true" type="button"><span class="chev"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="9 6 15 12 9 18"></polyline></svg></span><span style="display:flex;flex-direction:column;gap:6px"><span class="section-num">01</span><span class="section-title">Pending approvals</span></span><span class="section-aside"><span>empty</span></span></button><div class="tldr-row"><div class="tldr"><b>TL;DR</b>Items waiting on a human gate. High-risk mutations (work item creation, merge, doc publish, config changes) land here.</div></div><div class="section-body-wrap"><div class="section-body"><div class="section-body-inner"><div class="body"><div style="padding:24px;text-align:center;color:var(--muted);font-family:var(--mono);font-size:12px">Loading…</div></div></div></div></div></section><section class="section"><button class="section-head" aria-expanded="false" type="button"><span class="chev"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="9 6 15 12 9 18"></polyline></svg></span><span style="display:flex;flex-direction:column;gap:6px"><span class="section-num">02</span><span class="section-title">Services</span></span><span class="section-aside"><span>0/0 running</span></span></button><div class="tldr-row"><div class="tldr"><b>TL;DR</b>Local processes that back the runtime: Postgres, memory bridge, OpenCode bridge, dashboard, embed daemon.</div></div><div class="section-body-wrap"><div class="section-body"><div class="section-body-inner"><div class="body"><div style="padding:24px;text-align:center;color:var(--muted);font-family:var(--mono);font-size:12px">Loading…</div><p style="margin-top:12px"><a class="link" href="/resources/">Open services dashboard →</a></p></div></div></div></div></section><section class="section"><button class="section-head" aria-expanded="false" type="button"><span class="chev"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="9 6 15 12 9 18"></polyline></svg></span><span style="display:flex;flex-direction:column;gap:6px"><span class="section-num">03</span><span class="section-title">Credentials</span></span><span class="section-aside"><span>0 tracked</span></span></button><div class="tldr-row"><div class="tldr"><b>TL;DR</b>Provider API keys, billing posture, and 1Password references. Set them in /providers.</div></div><div class="section-body-wrap"><div class="section-body"><div class="section-body-inner"><div class="body"><p style="margin-top:12px"><a class="link" href="/providers/">Manage providers →</a></p></div></div></div></div></section><section class="section"><button class="section-head" aria-expanded="false" type="button"><span class="chev"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="9 6 15 12 9 18"></polyline></svg></span><span style="display:flex;flex-direction:column;gap:6px"><span class="section-num">04</span><span class="section-title">Quick links</span></span><span class="section-aside"></span></button><div class="tldr-row"><div class="tldr"><b>TL;DR</b>Deep links into every dashboard surface.</div></div><div class="section-body-wrap"><div class="section-body"><div class="section-body-inner"><div class="body"><div class="callout"><span class="clt-label">Where to next</span><div><p><a class="link" href="/doctor/">Doctor</a>,<!-- --> <a class="link" href="/knowledge/">Knowledge</a>,<!-- --> <a class="link" href="/intake/">Intake</a>,<!-- --> <a class="link" href="/workflow/">Workflow</a>,<!-- --> <a class="link" href="/agents/">Specialists</a>,<!-- --> <a class="link" href="/models/">Models</a>,<!-- --> <a class="link" href="/audit/">Audit</a>.</p></div></div></div></div></div></div></section></div></div><!--$--><!--/$--></main></div></div><script src="/_next/static/chunks/webpack-8e0503f8a36190b3.js" id="_R_" async=""></script><script>(self.__next_f=self.__next_f||[]).push([0])</script><script>self.__next_f.push([1,"1:\"$Sreact.fragment\"\n2:I[105,[\"9664\",\"static/chunks/9664-af80478aa73ba424.js\",\"8144\",\"static/chunks/8144-da56a4a291c0b21a.js\",\"7177\",\"static/chunks/app/layout-1a2d2674aa220229.js\"],\"AppShellClient\"]\n3:I[5341,[],\"\"]\n4:I[25,[],\"\"]\n5:I[1012,[],\"ClientPageRoot\"]\n6:I[9008,[\"9664\",\"static/chunks/9664-af80478aa73ba424.js\",\"8144\",\"static/chunks/8144-da56a4a291c0b21a.js\",\"8974\",\"static/chunks/app/page-2568fbdd2b0e1eca.js\"],\"default\"]\n9:I[5104,[],\"OutletBoundary\"]\nb:I[7158,[],\"AsyncMetadataOutlet\"]\nd:I[5104,[],\"ViewportBoundary\"]\nf:I[5104,[],\"MetadataBoundary\"]\n10:\"$Sreact.suspense\"\n12:I[4431,[],\"\"]\n:HL[\"/_next/static/media/22a5144ee8d83bca-s.p.woff2\",\"font\",{\"crossOrigin\":\"\",\"type\":\"font/woff2\"}]\n:HL[\"/_next/static/media/bb3ef058b751a6ad-s.p.woff2\",\"font\",{\"crossOrigin\":\"\",\"type\":\"font/woff2\"}]\n:HL[\"/_next/static/css/47a506ef57811fb9.css\",\"style\"]\n"])</script><script>self.__next_f.push([1,"0:{\"P\":null,\"b\":\"qA_CTDVxjnG_0_bArvNYn\",\"p\":\"\",\"c\":[\"\",\"\"],\"i\":false,\"f\":[[[\"\",{\"children\":[\"__PAGE__\",{}]},\"$undefined\",\"$undefined\",true],[\"\",[\"$\",\"$1\",\"c\",{\"children\":[[[\"$\",\"link\",\"0\",{\"rel\":\"stylesheet\",\"href\":\"/_next/static/css/47a506ef57811fb9.css\",\"precedence\":\"next\",\"crossOrigin\":\"$undefined\",\"nonce\":\"$undefined\"}]],[\"$\",\"html\",null,{\"lang\":\"en\",\"data-theme\":\"dark\",\"data-density\":\"comfortable\",\"data-motion\":\"normal\",\"suppressHydrationWarning\":true,\"className\":\"__variable_629dad __variable_246ccd __variable_3c557b\",\"children\":[\"$\",\"body\",null,{\"children\":[\"$\",\"$L2\",null,{\"nav\":[{\"label\":\"Overview\",\"items\":[{\"id\":\"home\",\"title\":\"Home\",\"href\":\"/\"},{\"id\":\"doctor\",\"title\":\"Doctor\",\"href\":\"/doctor\"},{\"id\":\"resources\",\"title\":\"Services\",\"href\":\"/resources\"}]},{\"label\":\"Activity\",\"items\":[{\"id\":\"audit\",\"title\":\"Audit trail\",\"href\":\"/audit\"},{\"id\":\"snapshots\",\"title\":\"Snapshots\",\"href\":\"/snapshots\"},{\"id\":\"performance\",\"title\":\"Performance\",\"href\":\"/performance\"}]},{\"label\":\"Work\",\"items\":[{\"id\":\"approvals\",\"title\":\"Approvals\",\"href\":\"/approvals\"},{\"id\":\"workflow\",\"title\":\"Workflow\",\"href\":\"/workflow\"},{\"id\":\"beads\",\"title\":\"Beads\",\"href\":\"/beads\"},{\"id\":\"artifacts\",\"title\":\"Artifacts\",\"href\":\"/artifacts\"},{\"id\":\"intake\",\"title\":\"Intake\",\"href\":\"/intake\"}]},{\"label\":\"Specialists\",\"items\":[{\"id\":\"agents\",\"title\":\"Specialists\",\"href\":\"/agents\"},{\"id\":\"skills\",\"title\":\"Skills\",\"href\":\"/skills\"},{\"id\":\"commands\",\"title\":\"Slash commands\",\"href\":\"/commands\"},{\"id\":\"hooks\",\"title\":\"Hooks\",\"href\":\"/hooks\"},{\"id\":\"plugins\",\"title\":\"Plugins\",\"href\":\"/plugins\"}]},{\"label\":\"Models \u0026 Providers\",\"items\":[{\"id\":\"models\",\"title\":\"Models\",\"href\":\"/models\"},{\"id\":\"providers\",\"title\":\"Providers\",\"href\":\"/providers\"},{\"id\":\"mcp\",\"title\":\"MCP servers\",\"href\":\"/mcp\"}]},{\"label\":\"Knowledge\",\"items\":[{\"id\":\"knowledge\",\"title\":\"Knowledge\",\"href\":\"/knowledge\"},{\"id\":\"editor\",\"title\":\"Editor\",\"href\":\"/editor\"}]},{\"label\":\"System\",\"items\":[{\"id\":\"config\",\"title\":\"Config\",\"href\":\"/config\"},{\"id\":\"infrastructure\",\"title\":\"Infrastructure\",\"href\":\"/infrastructure\"}]}],\"children\":[\"$\",\"$L3\",null,{\"parallelRouterKey\":\"children\",\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$L4\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":[[[\"$\",\"title\",null,{\"children\":\"404: This page could not be found.\"}],[\"$\",\"div\",null,{\"style\":{\"fontFamily\":\"system-ui,\\\"Segoe UI\\\",Roboto,Helvetica,Arial,sans-serif,\\\"Apple Color Emoji\\\",\\\"Segoe UI Emoji\\\"\",\"height\":\"100vh\",\"textAlign\":\"center\",\"display\":\"flex\",\"flexDirection\":\"column\",\"alignItems\":\"center\",\"justifyContent\":\"center\"},\"children\":[\"$\",\"div\",null,{\"children\":[[\"$\",\"style\",null,{\"dangerouslySetInnerHTML\":{\"__html\":\"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}\"}}],[\"$\",\"h1\",null,{\"className\":\"next-error-h1\",\"style\":{\"display\":\"inline-block\",\"margin\":\"0 20px 0 0\",\"padding\":\"0 23px 0 0\",\"fontSize\":24,\"fontWeight\":500,\"verticalAlign\":\"top\",\"lineHeight\":\"49px\"},\"children\":404}],[\"$\",\"div\",null,{\"style\":{\"display\":\"inline-block\"},\"children\":[\"$\",\"h2\",null,{\"style\":{\"fontSize\":14,\"fontWeight\":400,\"lineHeight\":\"49px\",\"margin\":0},\"children\":\"This page could not be found.\"}]}]]}]}]],[]],\"forbidden\":\"$undefined\",\"unauthorized\":\"$undefined\"}]}]}]}]]}],{\"children\":[\"__PAGE__\",[\"$\",\"$1\",\"c\",{\"children\":[[\"$\",\"$L5\",null,{\"Component\":\"$6\",\"searchParams\":{},\"params\":{},\"promises\":[\"$@7\",\"$@8\"]}],null,[\"$\",\"$L9\",null,{\"children\":[\"$La\",[\"$\",\"$Lb\",null,{\"promise\":\"$@c\"}]]}]]}],{},null,false]},null,false],[\"$\",\"$1\",\"h\",{\"children\":[null,[[\"$\",\"$Ld\",null,{\"children\":\"$Le\"}],[\"$\",\"meta\",null,{\"name\":\"next-size-adjust\",\"content\":\"\"}]],[\"$\",\"$Lf\",null,{\"children\":[\"$\",\"div\",null,{\"hidden\":true,\"children\":[\"$\",\"$10\",null,{\"fallback\":null,\"children\":\"$L11\"}]}]}]]}],false]],\"m\":\"$undefined\",\"G\":[\"$12\",[]],\"s\":false,\"S\":true}\n"])</script><script>self.__next_f.push([1,"7:{}\n8:\"$0:f:0:1:2:children:1:props:children:0:props:params\"\n"])</script><script>self.__next_f.push([1,"e:[[\"$\",\"meta\",\"0\",{\"charSet\":\"utf-8\"}],[\"$\",\"meta\",\"1\",{\"name\":\"viewport\",\"content\":\"width=device-width, initial-scale=1\"}]]\na:null\n"])</script><script>self.__next_f.push([1,"c:{\"metadata\":[[\"$\",\"title\",\"0\",{\"children\":\"Construct — Dashboard\"}],[\"$\",\"meta\",\"1\",{\"name\":\"description\",\"content\":\"Local operations dashboard for Construct — approvals, health, knowledge, models, providers.\"}]],\"error\":null,\"digest\":\"$undefined\"}\n"])</script><script>self.__next_f.push([1,"11:\"$c:metadata\"\n"])</script></body></html>
@@ -100,7 +100,7 @@ export const POSTCONDITIONS = {
100
100
  * @param {object} packet — the output packet about to hand off
101
101
  * @returns {{ ok: boolean, producer: string, failures: Array<{id, reason}> }}
102
102
  */
103
- export function validatePostconditions(producer, packet) {
103
+ export function validateBinaryPostconditions(producer, packet) {
104
104
  const rules = POSTCONDITIONS[producer] || [];
105
105
  if (rules.length === 0) return { ok: true, producer, failures: [] };
106
106
  const failures = [];