@geraldmaron/construct 1.0.17 → 1.0.19

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 (88) hide show
  1. package/README.md +6 -4
  2. package/bin/construct +26 -3
  3. package/commands/design/flow.md +2 -0
  4. package/commands/design/ui.md +2 -0
  5. package/db/schema/003_observation_reconciliation.sql +14 -0
  6. package/lib/bootstrap/resources.mjs +0 -1
  7. package/lib/cli-commands.mjs +33 -5
  8. package/lib/comment-lint.mjs +44 -0
  9. package/lib/contracts/validate.mjs +106 -0
  10. package/lib/decisions/enforced-baseline.json +23 -0
  11. package/lib/decisions/golden.mjs +87 -0
  12. package/lib/decisions/precedence.mjs +46 -0
  13. package/lib/decisions/registry.mjs +469 -0
  14. package/lib/deployment/parity-contract.mjs +148 -0
  15. package/lib/embed/cli.mjs +11 -0
  16. package/lib/embed/conflict-detection.mjs +4 -4
  17. package/lib/embed/customer-profiles.mjs +1 -1
  18. package/lib/embed/reconcile.mjs +60 -0
  19. package/lib/embed/semantic.mjs +5 -3
  20. package/lib/gates-audit.mjs +2 -2
  21. package/lib/hooks/config-protection.mjs +22 -3
  22. package/lib/hooks/guard-bash.mjs +1 -1
  23. package/lib/init-docs.mjs +1 -0
  24. package/lib/mode-commands.mjs +6 -8
  25. package/lib/observation-store.mjs +16 -2
  26. package/lib/opencode-telemetry.mjs +1 -1
  27. package/lib/roles/cli.mjs +10 -2
  28. package/lib/roles/gateway.mjs +50 -1
  29. package/lib/scheduler/index.mjs +31 -0
  30. package/lib/server/index.mjs +13 -3
  31. package/lib/server/static/index.html +1 -1
  32. package/lib/setup.mjs +6 -0
  33. package/lib/storage/hybrid-query.mjs +49 -38
  34. package/lib/storage/rrf.mjs +42 -0
  35. package/lib/storage/vector-client.mjs +18 -3
  36. package/lib/telemetry/backends/local.mjs +1 -1
  37. package/lib/telemetry/skill-calls.mjs +1 -1
  38. package/lib/templates/visual-requirements.mjs +84 -0
  39. package/package.json +12 -5
  40. package/rules/common/comments.md +3 -0
  41. package/rules/common/no-fabrication.md +3 -0
  42. package/rules/common/precedence.md +19 -0
  43. package/rules/common/research-sources.md +32 -0
  44. package/rules/common/research.md +59 -2
  45. package/skills/roles/data-engineer.pipeline.md +13 -1
  46. package/skills/roles/debugger.md +9 -0
  47. package/skills/roles/designer.accessibility.md +13 -3
  48. package/skills/roles/designer.md +10 -0
  49. package/skills/roles/engineer.platform.md +8 -0
  50. package/skills/roles/operator.md +10 -1
  51. package/skills/roles/operator.release.md +8 -0
  52. package/skills/roles/operator.sre.md +10 -1
  53. package/skills/roles/orchestrator.md +9 -2
  54. package/skills/roles/product-manager.business-strategy.md +10 -1
  55. package/skills/roles/researcher.explorer.md +12 -1
  56. package/skills/roles/researcher.ux.md +13 -1
  57. package/skills/roles/reviewer.devil-advocate.md +14 -2
  58. package/skills/roles/reviewer.evaluator.md +17 -4
  59. package/skills/roles/reviewer.trace.md +12 -1
  60. package/skills/roles/security.legal-compliance.md +8 -0
  61. package/skills/roles/security.md +11 -0
  62. package/specialists/contracts.json +18 -0
  63. package/specialists/prompts/cx-architect.md +1 -0
  64. package/specialists/prompts/cx-designer.md +1 -1
  65. package/specialists/prompts/cx-operations.md +1 -0
  66. package/specialists/prompts/cx-rd-lead.md +1 -0
  67. package/specialists/prompts/cx-researcher.md +4 -2
  68. package/specialists/prompts/cx-sre.md +1 -0
  69. package/specialists/registry.json +2 -2
  70. package/templates/docs/backlog-proposal.md +1 -1
  71. package/templates/docs/customer-profile.md +1 -1
  72. package/templates/docs/evidence-brief.md +5 -1
  73. package/templates/docs/incident-report.md +37 -21
  74. package/templates/docs/prfaq.md +2 -2
  75. package/templates/docs/product-intelligence-report.md +1 -1
  76. package/templates/docs/research-brief.md +8 -6
  77. package/templates/docs/research-finding.md +32 -7
  78. package/templates/docs/rfc.md +13 -1
  79. package/templates/docs/runbook.md +20 -1
  80. package/templates/docs/signal-brief.md +4 -1
  81. package/templates/docs/skill-artifact.md +27 -7
  82. package/templates/docs/strategy.md +23 -2
  83. package/templates/docs/test-plan.md +96 -0
  84. package/lib/bootstrap/lazy-install.mjs +0 -161
  85. package/lib/embed/jobs/vector-sync.mjs +0 -198
  86. package/lib/knowledge/postgres-search.mjs +0 -132
  87. package/lib/services/pattern-promotion-service.mjs +0 -167
  88. package/lib/storage/unified-storage.mjs +0 -550
@@ -0,0 +1,60 @@
1
+ /**
2
+ * lib/embed/reconcile.mjs — bring the Postgres observation index back in sync
3
+ * with the local source of truth.
4
+ *
5
+ * Observations are embedded inline on write (lib/observation-store.mjs), but
6
+ * the inline write is skipped whenever Postgres is unreachable at creation time
7
+ * (the local hashing-bow fallback is used instead), and a model change leaves
8
+ * every prior row embedded in a stale vector space. This pass is the safety
9
+ * net: for each local observation it compares the (content_hash, model) the
10
+ * pg row was embedded with against the live content + current model, and
11
+ * re-embeds ONLY the rows that are missing or stale. Idempotent — a second run
12
+ * with no changes re-embeds nothing.
13
+ *
14
+ * Reconciliation-on-sync is preferred over a fixed-interval polling cron (see
15
+ * docs/research/vector-search-best-practices.md): the pass runs where Construct
16
+ * already touches storage (the model-change re-embed, the storage CLI), keys
17
+ * idempotency on a content hash, and re-embeds on model change.
18
+ */
19
+
20
+ import { VectorClient } from '../storage/vector-client.mjs';
21
+ import { embedText as embedTextEngine, getEmbeddingModelInfo } from '../storage/embeddings-engine.mjs';
22
+ import { listObservations, getObservation, observationSearchText, observationContentHash } from '../observation-store.mjs';
23
+
24
+ export async function reconcileObservationEmbeddings(rootDir, { env = process.env, limit = Infinity, embed = embedTextEngine, modelId = null } = {}) {
25
+ const client = new VectorClient({ env });
26
+ const result = { checked: 0, reembedded: 0, model: null, skipped: null };
27
+
28
+ try {
29
+ if (!(await client.isHealthy()) || !(await client.isPgvectorEnabled())) {
30
+ result.skipped = 'no-pg';
31
+ return result;
32
+ }
33
+
34
+ const currentModel = modelId || (await getEmbeddingModelInfo({ env })).model;
35
+ result.model = currentModel;
36
+
37
+ const ids = (listObservations(rootDir, { limit: Number.MAX_SAFE_INTEGER }) || []).map((e) => e.id);
38
+ const fingerprints = await client.getObservationFingerprints(ids);
39
+
40
+ for (const id of ids) {
41
+ if (result.reembedded >= limit) break;
42
+ const obs = getObservation(rootDir, id);
43
+ if (!obs) continue;
44
+ result.checked += 1;
45
+
46
+ const searchText = observationSearchText(obs);
47
+ const hash = observationContentHash(searchText);
48
+ const fp = fingerprints.get(id);
49
+ if (fp && fp.contentHash === hash && fp.model === currentModel) continue;
50
+
51
+ const { embedding, model } = await embed(searchText, { env });
52
+ await client.storeObservation({ ...obs, embedding, contentHash: hash, model: model || currentModel });
53
+ result.reembedded += 1;
54
+ }
55
+
56
+ return result;
57
+ } finally {
58
+ await client.close();
59
+ }
60
+ }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * lib/embed/semantic.mjs — Semantic analysis for intake signals.
3
3
  *
4
- * Generates embeddings via @xenova/transformers, caches them to disk,
4
+ * Generates embeddings via @huggingface/transformers, caches them to disk,
5
5
  * computes cosine similarity, and clusters related signals by topic.
6
6
  * All local — no external API calls. Default model is all-MiniLM-L6-v2
7
7
  * (384-dimensional vectors, ~50MB on disk, fast inference).
@@ -23,7 +23,7 @@ import { homedir } from 'node:os';
23
23
 
24
24
  const CACHE_DIR = join(homedir(), '.cx', 'cache', 'embeddings');
25
25
  const INDEX_FILE = join(CACHE_DIR, 'index.json');
26
- const MODEL_NAME = 'all-MiniLM-L6-v2';
26
+ const MODEL_NAME = 'Xenova/all-MiniLM-L6-v2';
27
27
  const DIMENSIONS = 384;
28
28
  const LOG = process.env.CONSTRUCT_DEBUG_EMBED === '1';
29
29
 
@@ -35,7 +35,9 @@ let embedder = null;
35
35
  async function getEmbedder() {
36
36
  if (embedder !== null) return embedder;
37
37
  try {
38
- const { pipeline } = await import('@xenova/transformers');
38
+ const { pipeline, env: hfEnv } = await import('@huggingface/transformers');
39
+ hfEnv.allowLocalModels = true;
40
+ hfEnv.useBrowserCache = false;
39
41
  embedder = await pipeline('feature-extraction', MODEL_NAME, {
40
42
  quantized: true,
41
43
  });
@@ -76,8 +76,8 @@ function parseCIJobs(rootDir) {
76
76
  const path = join(dir, file);
77
77
  if (!existsSync(path)) continue;
78
78
  const yml = readFileSync(path, 'utf8');
79
- const jobNames = [...yml.matchAll(/^ name:\s*(.+?)\s*$/gm)].map((m) => m[1].trim());
80
- const stepNames = [...yml.matchAll(/^ - name:\s*(.+?)\s*$/gm)].map((m) => m[1].trim());
79
+ const jobNames = [...yml.matchAll(/^ {4}name:\s*(.+?)\s*$/gm)].map((m) => m[1].trim());
80
+ const stepNames = [...yml.matchAll(/^ {6}- name:\s*(.+?)\s*$/gm)].map((m) => m[1].trim());
81
81
  names.push(...jobNames, ...stepNames);
82
82
  }
83
83
  return names.map((n) => n.replace(/\$\{\{\s*matrix\.(\w+)\s*\}\}/g, (_, k) => `<${k}>`));
@@ -3,8 +3,10 @@
3
3
  * lib/hooks/config-protection.mjs — protects code-quality config from being weakened.
4
4
  *
5
5
  * Runs as PreToolUse on Edit|Write|MultiEdit. Blocks edits to eslint /
6
- * prettier / tsconfig / biome / stylelint configs. Those rules are a contract
7
- * with the codebase; weakening them is almost always a bug fix in disguise.
6
+ * prettier / tsconfig / biome / stylelint configs but only once the config
7
+ * is an established contract, i.e. tracked in git. A config that is not yet
8
+ * tracked is being introduced or authored: first-time setup and pre-commit
9
+ * iteration are allowed; weakening a committed config stays blocked.
8
10
  *
9
11
  * Meta-system files (hooks, registry, settings template, install/setup
10
12
  * entrypoints) are audited but NOT blocked. Editing them is normal work on
@@ -22,6 +24,8 @@
22
24
  * @exits 0 = pass | 2 = block tool call
23
25
  */
24
26
  import { readFileSync } from 'fs';
27
+ import { execFileSync } from 'child_process';
28
+ import { dirname } from 'path';
25
29
  import { logHookFailure } from './_lib/log.mjs';
26
30
 
27
31
  const filePath = process.env.TOOL_INPUT_FILE_PATH || (() => {
@@ -56,6 +60,21 @@ const META_FILES = [
56
60
  /(?:^|\/)tsconfig(\.[^/]+)?\.json$/i,
57
61
  ];
58
62
 
63
+ // A config is a contract worth protecting only once it is committed. `git
64
+ // ls-files --error-unmatch` exits 0 for a tracked path and non-zero otherwise,
65
+ // so an untracked (newly introduced) config is allowed through.
66
+
67
+ function isTrackedInGit(p) {
68
+ try {
69
+ execFileSync('git', ['ls-files', '--error-unmatch', p], {
70
+ cwd: dirname(p), stdio: 'ignore', timeout: 1000,
71
+ });
72
+ return true;
73
+ } catch {
74
+ return false;
75
+ }
76
+ }
77
+
59
78
  async function emitEvent(type, category, summary) {
60
79
  try {
61
80
  const { emitRoleEvent } = await import('../roles/hook-emit.mjs');
@@ -69,7 +88,7 @@ async function emitEvent(type, category, summary) {
69
88
  }
70
89
 
71
90
  const base = filePath.split('/').pop();
72
- if (PROTECTED.some(r => r.test(base))) {
91
+ if (PROTECTED.some(r => r.test(base)) && isTrackedInGit(filePath)) {
73
92
  process.stderr.write(
74
93
  `[config-protection] The code quality rules are protected. Fix the code to meet the existing standards — don't weaken the rules.\nFile: ${filePath}\n`
75
94
  );
@@ -136,7 +136,7 @@ if (process.env.CONSTRUCT_ROLES !== 'off') {
136
136
  verdict = checkAction({ personaId, action: 'bash', target: trimmed });
137
137
  }
138
138
  if (!verdict.allowed && verdict.reason === 'outside-fence') {
139
- const allowedList = (() => {
139
+ const allowedList = await (async () => {
140
140
  try { const { loadManifest } = await import('../roles/manifest.mjs'); const m = loadManifest(personaId); return m?.fence?.allowedCommands?.join(', ') || 'see role-manifests.json'; } catch { return 'see role-manifests.json'; }
141
141
  })();
142
142
  process.stderr.write(
package/lib/init-docs.mjs CHANGED
@@ -25,6 +25,7 @@ import { fileURLToPath } from "node:url";
25
25
 
26
26
  import { suggestDocsLaneForFile } from './docs-routing.mjs';
27
27
  import { stampFrontmatter } from "./doc-stamp.mjs";
28
+ import { multiSelect, selectOption } from './tty-prompts.mjs';
28
29
  import readline from "node:readline";
29
30
 
30
31
  const __dirname = fileURLToPath(new URL(".", import.meta.url));
@@ -9,6 +9,8 @@
9
9
  // Standardized command contracts across deployment modes
10
10
 
11
11
  import { spawnSync } from 'node:child_process';
12
+ import { existsSync, readFileSync } from 'node:fs';
13
+ import { basename, join } from 'node:path';
12
14
 
13
15
  export const MODE_COMMANDS = {
14
16
  // Intake queue operations
@@ -65,19 +67,15 @@ function getDeploymentMode() {
65
67
  function parseIntakeFiles(stdout) {
66
68
  return stdout.split('\n')
67
69
  .filter(Boolean)
68
- .map(f => ({ id: path.basename(f, '.json'), source: 'file' }));
70
+ .map(f => ({ id: basename(f, '.json'), source: 'file' }));
69
71
  }
70
72
 
71
73
  function readIntakeFile(id, options) {
72
- const fs = require('fs');
73
- const path = require('path');
74
- const filePath = path.join(options.cwd || process.cwd(), '.cx/intake/pending', `${id}.json`);
75
-
76
- if (!fs.existsSync(filePath)) {
74
+ const filePath = join(options.cwd || process.cwd(), '.cx/intake/pending', `${id}.json`);
75
+ if (!existsSync(filePath)) {
77
76
  return { error: `Intake item ${id} not found` };
78
77
  }
79
-
80
- return JSON.parse(fs.readFileSync(filePath, 'utf8'));
78
+ return JSON.parse(readFileSync(filePath, 'utf8'));
81
79
  }
82
80
 
83
81
  async function queryPostgresIntake(options) {
@@ -25,6 +25,18 @@ const OBS_DIR = '.cx/observations';
25
25
  const INDEX_FILE = 'index.json';
26
26
  const VECTORS_FILE = 'vectors.json';
27
27
  const MAX_INDEX = 1000;
28
+
29
+ // The exact text fed to the embedder, and its content hash. addObservation and
30
+ // the reconciliation pass MUST derive both the same way, so the (content_hash,
31
+ // model) fingerprint they compare is computed identically.
32
+
33
+ export function observationSearchText(obs) {
34
+ return [obs.summary, obs.content, ...(obs.tags || [])].filter(Boolean).join(' ');
35
+ }
36
+
37
+ export function observationContentHash(text) {
38
+ return crypto.createHash('sha256').update(String(text || '')).digest('hex');
39
+ }
28
40
  const MAX_SUMMARY = 500;
29
41
  const MAX_CONTENT = 2000;
30
42
  const MAX_TAGS = 10;
@@ -214,11 +226,13 @@ export async function addObservation(rootDir, {
214
226
  try {
215
227
  const client = new VectorClient();
216
228
  if (await client.isHealthy() && await client.isPgvectorEnabled()) {
217
- const searchText = [record.summary, record.content, ...record.tags].filter(Boolean).join(' ');
218
- const { embedding } = await embedTextEngine(searchText);
229
+ const searchText = observationSearchText(record);
230
+ const { embedding, model } = await embedTextEngine(searchText);
219
231
  await client.storeObservation({
220
232
  ...record,
221
233
  embedding,
234
+ contentHash: observationContentHash(searchText),
235
+ model,
222
236
  });
223
237
  await client.close();
224
238
  return record;
@@ -29,7 +29,7 @@ export function getIngestClient(env = process.env) {
29
29
  rootDir: env.CX_TOOLKIT_DIR,
30
30
  onError: (err) => {
31
31
  if (env.CONSTRUCT_TRACE_DEBUG === "1") {
32
- // eslint-disable-next-line no-console
32
+
33
33
  console.error("[construct-telemetry]", err?.message || err);
34
34
  }
35
35
  },
package/lib/roles/cli.mjs CHANGED
@@ -6,11 +6,13 @@
6
6
  * latest — show the most recent unresolved invocation (full brief)
7
7
  * show <fingerprint> — show one invocation by fingerprint
8
8
  * status — show onboarded personas + their event types
9
+ * resolve <fp> — mark one invocation resolved
10
+ * prune — compact the queue: drop resolved + TTL-expired entries
9
11
  * reset — clear the pending queue (manual recovery)
10
12
  */
11
13
 
12
14
  import { listOnboardedPersonas, loadManifest } from './manifest.mjs';
13
- import { listPending, markResolved, resetPending } from './gateway.mjs';
15
+ import { listPending, markResolved, resetPending, prunePending } from './gateway.mjs';
14
16
  import { recent as recentEvents } from './event-bus.mjs';
15
17
 
16
18
  function fmtTime(ts) {
@@ -106,6 +108,12 @@ export async function runCli(args) {
106
108
  return ok ? 0 : 1;
107
109
  }
108
110
 
111
+ if (sub === 'prune') {
112
+ const { removed, resolved, expired, fixtures, kept } = prunePending();
113
+ console.log(`Pruned ${removed} entr${removed === 1 ? 'y' : 'ies'} (${resolved} resolved, ${expired} expired, ${fixtures} fixture) · ${kept} remaining.`);
114
+ return 0;
115
+ }
116
+
109
117
  if (sub === 'reset') {
110
118
  resetPending();
111
119
  console.log('Pending queue cleared.');
@@ -113,6 +121,6 @@ export async function runCli(args) {
113
121
  }
114
122
 
115
123
  console.error(`Unknown role subcommand: ${sub}`);
116
- console.error('Usage: construct role [list|latest|show <fp>|status|resolve <fp>|reset]');
124
+ console.error('Usage: construct role [list|latest|show <fp>|status|resolve <fp>|prune|reset]');
117
125
  return 2;
118
126
  }
@@ -30,6 +30,7 @@ const DEFAULTS = {
30
30
  thresholdWindowMs: 10 * 60 * 1000,
31
31
  cooldownMs: 30 * 60 * 1000,
32
32
  rateCeilingPerHour: 3,
33
+ pendingTtlMs: 14 * 24 * 60 * 60 * 1000,
33
34
  };
34
35
 
35
36
  function ensureDir() {
@@ -226,10 +227,58 @@ export async function recordAndMaybeInvoke(eventType, payload = {}) {
226
227
  return { recorded: true, ...r };
227
228
  }
228
229
 
230
+ // A surfaced role invocation is a prompt to act, not a durable record (bd owns
231
+ // the durable issue). Unresolved entries older than the TTL are stale noise —
232
+ // e.g. pre-fixture-guard escalations that would otherwise surface at every
233
+ // session-start forever — so they drop out of listings and never expire bd work.
234
+
235
+ function isExpired(entry, now) {
236
+ return typeof entry.ts === 'number' && (now - entry.ts) > DEFAULTS.pendingTtlMs;
237
+ }
238
+
229
239
  export function listPending({ unresolved = true } = {}) {
230
240
  const entries = readPending();
231
241
  if (!unresolved) return entries;
232
- return entries.filter((e) => !e.resolvedAt);
242
+ const now = Date.now();
243
+ return entries.filter((e) => !e.resolvedAt && !isExpired(e, now));
244
+ }
245
+
246
+ // An entry that names an OS-tmp path is a pre-guard test-fixture escalation
247
+ // (shouldEscalate now blocks these at the source). Sweep them on prune so the
248
+ // historical noise — e.g. the cx-secrets fixtures — clears immediately rather
249
+ // than waiting out the TTL.
250
+
251
+ function referencesFixture(entry) {
252
+ for (const field of [entry.cwd, entry.project, entry.summary]) {
253
+ if (typeof field !== 'string') continue;
254
+ for (const token of field.split(/\s+/)) {
255
+ if (isTestFixturePath(token.replace(/[)\].,:;]+$/, ''))) return true;
256
+ }
257
+ }
258
+ return false;
259
+ }
260
+
261
+ // Compact the queue on disk: drop resolved, TTL-expired, and fixture-path
262
+ // entries. Idempotent; returns counts so the CLI and session-start can report
263
+ // what was reclaimed.
264
+
265
+ export function prunePending({ now = Date.now() } = {}) {
266
+ const entries = readPending();
267
+ const kept = [];
268
+ let resolved = 0;
269
+ let expired = 0;
270
+ let fixtures = 0;
271
+ for (const e of entries) {
272
+ if (e.resolvedAt) { resolved += 1; continue; }
273
+ if (isExpired(e, now)) { expired += 1; continue; }
274
+ if (referencesFixture(e)) { fixtures += 1; continue; }
275
+ kept.push(e);
276
+ }
277
+ const removed = resolved + expired + fixtures;
278
+ if (removed > 0) {
279
+ writeFileSync(pendingPath(), kept.length ? kept.map((e) => JSON.stringify(e)).join('\n') + '\n' : '');
280
+ }
281
+ return { removed, resolved, expired, fixtures, kept: kept.length };
233
282
  }
234
283
 
235
284
  // Drain events that accumulated outside a Claude session. Walks recent
@@ -130,3 +130,34 @@ registerJob({
130
130
  };
131
131
  },
132
132
  });
133
+
134
+ // The prompt-optimize loop reads telemetry traces and proposes prompt patches.
135
+ // Scheduled runs are advisory by construction: the argv never carries --apply, so
136
+ // a cadence surfaces proposals for review and can never auto-mutate a prompt. The
137
+ // existing --apply gate in scripts/optimize.mjs remains the only path that writes.
138
+
139
+ export const OPTIMIZE_JOB_ID = 'optimize-loop';
140
+
141
+ export function optimizeJobArgv() {
142
+ return [];
143
+ }
144
+
145
+ registerJob({
146
+ id: OPTIMIZE_JOB_ID,
147
+ schedule: '0 5 * * 1',
148
+ mode: 'solo',
149
+ handler: async ({ cwd, env }) => {
150
+ const argv = optimizeJobArgv();
151
+ if (argv.includes('--apply')) throw new Error('scheduled optimize must never auto-apply');
152
+ const { execFileSync } = await import('node:child_process');
153
+ const { fileURLToPath } = await import('node:url');
154
+ const { resolve, dirname } = await import('node:path');
155
+ const root = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
156
+ try {
157
+ const output = execFileSync('node', [resolve(root, 'scripts', 'optimize.mjs'), ...argv], { cwd, env, encoding: 'utf8' });
158
+ return { status: 'ok', applied: false, output: output.slice(0, 500) };
159
+ } catch (err) {
160
+ return { status: 'error', applied: false, error: err.message };
161
+ }
162
+ },
163
+ });
@@ -713,9 +713,19 @@ const server = createServer(async (req, res) => {
713
713
  return;
714
714
  }
715
715
 
716
- if (url.pathname === '/api/services/langfuse/login' && req.method === 'GET') {
717
- const { handleLangfuseLogin } = await import('./langfuse-login.mjs');
718
- await handleLangfuseLogin(req, res);
716
+ // Provider-agnostic telemetry-UI sign-in. An external backend configured via
717
+ // CONSTRUCT_TELEMETRY_URL uses the vendor-neutral bridge; with none set, fall
718
+ // back to the zero-touch local Langfuse stack. `/langfuse/login` stays as a
719
+ // back-compat alias of the canonical `/telemetry/login`.
720
+
721
+ if ((url.pathname === '/api/services/telemetry/login' || url.pathname === '/api/services/langfuse/login') && req.method === 'GET') {
722
+ if (process.env.CONSTRUCT_TELEMETRY_URL) {
723
+ const { handleTelemetryLogin } = await import('./telemetry-login.mjs');
724
+ await handleTelemetryLogin(req, res, {});
725
+ } else {
726
+ const { handleLangfuseLogin } = await import('./langfuse-login.mjs');
727
+ await handleLangfuseLogin(req, res);
728
+ }
719
729
  return;
720
730
  }
721
731
 
@@ -1 +1 @@
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>
1
+ <!DOCTYPE html><html lang="en" data-theme="dark" data-density="comfortable" data-motion="normal" class="geist_478ec610-module__Maz4kG__variable geist_d633ac5f-module__BboVrW__variable jetbrains_mono_dc177b0e-module__78ed6G__variable"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="preload" href="/_next/static/media/70bc3e132a0a741e-s.p.3t6q91iet4nsy.woff2" as="font" crossorigin="" type="font/woff2"/><link rel="preload" href="/_next/static/media/caa3a2e1cccd8315-s.p.0wgildi0cnwt9.woff2" as="font" crossorigin="" type="font/woff2"/><link rel="stylesheet" href="/_next/static/chunks/07baqf-hmk6oo.css" data-precedence="next"/><link rel="preload" as="script" fetchPriority="low" href="/_next/static/chunks/0i88v4ql6s7aw.js"/><script src="/_next/static/chunks/26rq-xwqn81a7.js" async=""></script><script src="/_next/static/chunks/1q2ld_2skrq5q.js" async=""></script><script src="/_next/static/chunks/41yml7ep3qrnx.js" async=""></script><script src="/_next/static/chunks/turbopack-1wenwimq-yd36.js" async=""></script><script src="/_next/static/chunks/39l2haooi0nh6.js" async=""></script><script src="/_next/static/chunks/1-8s9_t85wwr4.js" async=""></script><script src="/_next/static/chunks/0cangwgy_395t.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/0cz1d0mv5g_q7.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/0i88v4ql6s7aw.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[95327,[\"/_next/static/chunks/39l2haooi0nh6.js\",\"/_next/static/chunks/1-8s9_t85wwr4.js\"],\"AppShellClient\"]\n3:I[39756,[\"/_next/static/chunks/39l2haooi0nh6.js\",\"/_next/static/chunks/1-8s9_t85wwr4.js\"],\"default\"]\n4:I[37457,[\"/_next/static/chunks/39l2haooi0nh6.js\",\"/_next/static/chunks/1-8s9_t85wwr4.js\"],\"default\"]\n5:I[47257,[\"/_next/static/chunks/39l2haooi0nh6.js\",\"/_next/static/chunks/1-8s9_t85wwr4.js\"],\"ClientPageRoot\"]\n6:I[62453,[\"/_next/static/chunks/39l2haooi0nh6.js\",\"/_next/static/chunks/1-8s9_t85wwr4.js\",\"/_next/static/chunks/0cangwgy_395t.js\"],\"default\"]\n9:I[97367,[\"/_next/static/chunks/39l2haooi0nh6.js\",\"/_next/static/chunks/1-8s9_t85wwr4.js\"],\"OutletBoundary\"]\na:\"$Sreact.suspense\"\nc:I[97367,[\"/_next/static/chunks/39l2haooi0nh6.js\",\"/_next/static/chunks/1-8s9_t85wwr4.js\"],\"ViewportBoundary\"]\ne:I[97367,[\"/_next/static/chunks/39l2haooi0nh6.js\",\"/_next/static/chunks/1-8s9_t85wwr4.js\"],\"MetadataBoundary\"]\n10:I[68027,[\"/_next/static/chunks/39l2haooi0nh6.js\",\"/_next/static/chunks/1-8s9_t85wwr4.js\"],\"default\",1]\n:HL[\"/_next/static/chunks/07baqf-hmk6oo.css\",\"style\"]\n:HL[\"/_next/static/media/70bc3e132a0a741e-s.p.3t6q91iet4nsy.woff2\",\"font\",{\"crossOrigin\":\"\",\"type\":\"font/woff2\"}]\n:HL[\"/_next/static/media/caa3a2e1cccd8315-s.p.0wgildi0cnwt9.woff2\",\"font\",{\"crossOrigin\":\"\",\"type\":\"font/woff2\"}]\n"])</script><script>self.__next_f.push([1,"0:{\"P\":null,\"c\":[\"\",\"\"],\"q\":\"\",\"i\":false,\"f\":[[[\"\",{\"children\":[\"__PAGE__\",{}]},\"$undefined\",\"$undefined\",16],[[\"$\",\"$1\",\"c\",{\"children\":[[[\"$\",\"link\",\"0\",{\"rel\":\"stylesheet\",\"href\":\"/_next/static/chunks/07baqf-hmk6oo.css\",\"precedence\":\"next\",\"crossOrigin\":\"$undefined\",\"nonce\":\"$undefined\"}],[\"$\",\"script\",\"script-0\",{\"src\":\"/_next/static/chunks/39l2haooi0nh6.js\",\"async\":true,\"nonce\":\"$undefined\"}],[\"$\",\"script\",\"script-1\",{\"src\":\"/_next/static/chunks/1-8s9_t85wwr4.js\",\"async\":true,\"nonce\":\"$undefined\"}]],[\"$\",\"html\",null,{\"lang\":\"en\",\"data-theme\":\"dark\",\"data-density\":\"comfortable\",\"data-motion\":\"normal\",\"suppressHydrationWarning\":true,\"className\":\"geist_478ec610-module__Maz4kG__variable geist_d633ac5f-module__BboVrW__variable jetbrains_mono_dc177b0e-module__78ed6G__variable\",\"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\":[[\"$\",\"$1\",\"c\",{\"children\":[[\"$\",\"$L5\",null,{\"Component\":\"$6\",\"serverProvidedParams\":{\"searchParams\":{},\"params\":{},\"promises\":[\"$@7\",\"$@8\"]}}],[[\"$\",\"script\",\"script-0\",{\"src\":\"/_next/static/chunks/0cangwgy_395t.js\",\"async\":true,\"nonce\":\"$undefined\"}]],[\"$\",\"$L9\",null,{\"children\":[\"$\",\"$a\",null,{\"name\":\"Next.MetadataOutlet\",\"children\":\"$@b\"}]}]]}],{},null,false,null]},null,false,null],[\"$\",\"$1\",\"h\",{\"children\":[null,[\"$\",\"$Lc\",null,{\"children\":\"$Ld\"}],[\"$\",\"div\",null,{\"hidden\":true,\"children\":[\"$\",\"$Le\",null,{\"children\":[\"$\",\"$a\",null,{\"name\":\"Next.Metadata\",\"children\":\"$Lf\"}]}]}],[\"$\",\"meta\",null,{\"name\":\"next-size-adjust\",\"content\":\"\"}]]}],false]],\"m\":\"$undefined\",\"G\":[\"$10\",[[\"$\",\"link\",\"0\",{\"rel\":\"stylesheet\",\"href\":\"/_next/static/chunks/07baqf-hmk6oo.css\",\"precedence\":\"next\",\"crossOrigin\":\"$undefined\",\"nonce\":\"$undefined\"}]]],\"S\":true,\"h\":null,\"s\":\"$undefined\",\"l\":\"$undefined\",\"p\":\"$undefined\",\"d\":\"$undefined\",\"b\":\"piZVVlR5yulp6NxkELzTZ\"}\n"])</script><script>self.__next_f.push([1,"7:{}\n8:\"$0:f:0:1:1:children:0:props:children:0:props:serverProvidedParams:params\"\n"])</script><script>self.__next_f.push([1,"d:[[\"$\",\"meta\",\"0\",{\"charSet\":\"utf-8\"}],[\"$\",\"meta\",\"1\",{\"name\":\"viewport\",\"content\":\"width=device-width, initial-scale=1\"}]]\n"])</script><script>self.__next_f.push([1,"b:null\nf:[[\"$\",\"title\",\"0\",{\"children\":\"Construct — Dashboard\"}],[\"$\",\"meta\",\"1\",{\"name\":\"description\",\"content\":\"Local operations dashboard for Construct — approvals, health, knowledge, models, providers.\"}]]\n"])</script></body></html>
package/lib/setup.mjs CHANGED
@@ -794,7 +794,13 @@ export async function runSetup({ rootDir = ROOT_DIR, args = [], homeDir = HOME }
794
794
  console.log(' construct install --yes');
795
795
  }
796
796
 
797
+ // Install is machine setup: the global front-door agent must land on every
798
+ // user-scope surface (opencode/claude/codex/copilot) or a fresh machine fails
799
+ // cross-surface parity. Plain sync writes global hooks + project tier but not
800
+ // the front-door agent — `--global` is what populates it, so run both.
801
+
797
802
  runConstruct(['sync']);
803
+ runConstruct(['sync', '--global']);
798
804
  runConstruct(['doctor']);
799
805
 
800
806
  // Profile the CWD so per-host skill filtering layers downstream have