@geraldmaron/construct 1.0.12 → 1.0.14

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 +8 -2
  2. package/bin/construct +259 -11
  3. package/bin/construct-postinstall.mjs +23 -2
  4. package/lib/auto-docs.mjs +11 -7
  5. package/lib/dashboard-static.mjs +7 -4
  6. package/lib/doc-stamp.mjs +16 -0
  7. package/lib/docs-verify.mjs +1 -8
  8. package/lib/doctor/watchers/bd-watch.mjs +6 -2
  9. package/lib/embed/daemon.mjs +20 -0
  10. package/lib/embed/docs-lifecycle.mjs +19 -0
  11. package/lib/embed/inbox.mjs +85 -2
  12. package/lib/embed/recommendation-store.mjs +29 -0
  13. package/lib/gates-audit.mjs +18 -12
  14. package/lib/hooks/_lib/input.mjs +52 -0
  15. package/lib/hooks/adaptive-lint.mjs +4 -0
  16. package/lib/hooks/agent-tracker.mjs +59 -15
  17. package/lib/hooks/audit-reads.mjs +83 -42
  18. package/lib/hooks/audit-trail.mjs +28 -18
  19. package/lib/hooks/bash-output-logger.mjs +8 -2
  20. package/lib/hooks/block-no-verify.mjs +4 -0
  21. package/lib/hooks/ci-status-check.mjs +4 -0
  22. package/lib/hooks/comment-lint.mjs +6 -4
  23. package/lib/hooks/config-protection.mjs +4 -0
  24. package/lib/hooks/context-watch.mjs +4 -0
  25. package/lib/hooks/context-window-recovery.mjs +4 -0
  26. package/lib/hooks/dep-audit.mjs +12 -5
  27. package/lib/hooks/doc-coupling-check.mjs +5 -1
  28. package/lib/hooks/edit-accumulator.mjs +25 -10
  29. package/lib/hooks/edit-error-recovery.mjs +4 -0
  30. package/lib/hooks/edit-guard.mjs +4 -0
  31. package/lib/hooks/guard-bash.mjs +4 -0
  32. package/lib/hooks/mcp-audit.mjs +4 -0
  33. package/lib/hooks/mcp-health-check.mjs +4 -0
  34. package/lib/hooks/model-fallback.mjs +7 -11
  35. package/lib/hooks/policy-engine.mjs +4 -0
  36. package/lib/hooks/post-merge-docs-check.mjs +4 -0
  37. package/lib/hooks/post-merge-tracking.mjs +82 -0
  38. package/lib/hooks/pre-compact.mjs +4 -0
  39. package/lib/hooks/pre-push-gate.mjs +84 -231
  40. package/lib/hooks/proactive-activation.mjs +5 -2
  41. package/lib/hooks/readme-age-check.mjs +4 -0
  42. package/lib/hooks/registry-sync.mjs +35 -20
  43. package/lib/hooks/rule-verifier.mjs +3 -0
  44. package/lib/hooks/scan-secrets.mjs +4 -0
  45. package/lib/hooks/session-optimize.mjs +4 -0
  46. package/lib/hooks/session-reflect.mjs +4 -0
  47. package/lib/hooks/session-start.mjs +48 -1
  48. package/lib/hooks/session-tracking-refresh.mjs +70 -0
  49. package/lib/hooks/stop-notify.mjs +13 -2
  50. package/lib/hooks/stop-typecheck.mjs +4 -0
  51. package/lib/hooks/test-watch.mjs +8 -4
  52. package/lib/init-unified.mjs +90 -23
  53. package/lib/intake/attribution.mjs +77 -0
  54. package/lib/intake/intake-config.mjs +3 -0
  55. package/lib/intake/manifest.mjs +107 -0
  56. package/lib/intake/poll-lock.mjs +136 -0
  57. package/lib/intake/prepare.mjs +42 -4
  58. package/lib/logging/rotate.mjs +394 -0
  59. package/lib/mcp/tools/project.mjs +2 -2
  60. package/lib/mcp/tools/telemetry.mjs +1 -1
  61. package/lib/opencode-config.mjs +10 -3
  62. package/lib/orchestration/routing-tables.mjs +176 -0
  63. package/lib/orchestration-policy.mjs +18 -106
  64. package/lib/parity.mjs +48 -7
  65. package/lib/profiles/lifecycle.mjs +19 -1
  66. package/lib/project-init-shared.mjs +11 -5
  67. package/lib/project-root.mjs +104 -0
  68. package/lib/roles/catalog.mjs +1 -1
  69. package/lib/roles/event-bus.mjs +29 -9
  70. package/lib/roles/router.mjs +8 -7
  71. package/lib/server/index.mjs +31 -9
  72. package/lib/server/static/index.html +1 -15
  73. package/lib/specialist-contracts-enforce.mjs +24 -10
  74. package/lib/status.mjs +1 -1
  75. package/lib/storage/backup.mjs +2 -2
  76. package/lib/telemetry/intent-verifications.mjs +16 -3
  77. package/lib/telemetry/skill-calls.mjs +12 -3
  78. package/lib/tracking-surfaces.mjs +375 -0
  79. package/lib/worker/trace.mjs +19 -2
  80. package/package.json +6 -2
  81. package/platforms/claude/settings.template.json +28 -27
  82. package/scripts/sync-specialists.mjs +269 -75
  83. package/specialists/registry.json +158 -13
  84. package/lib/hooks/env-check.mjs +0 -83
  85. package/lib/hooks/read-tracker.mjs +0 -61
  86. package/lib/policy/unified-gates.mjs +0 -96
  87. package/lib/server/static/assets/index-ab25c707.js +0 -70
  88. package/lib/server/static/assets/index-f0c80a2b.css +0 -1
@@ -1,28 +1,34 @@
1
1
  /**
2
- * lib/roles/event-bus.mjs — append-only event log at ~/.cx/events.jsonl.
2
+ * lib/roles/event-bus.mjs — append-only event log per project.
3
3
  *
4
4
  * Hook processes emit failure/signal events here. The router + gateway read
5
5
  * recent events to decide escalation. Lines are rotated to keep the file
6
6
  * bounded. Fingerprint = sha1(type + project + first summary line) so the
7
7
  * same failure dedups across emits.
8
+ *
9
+ * Path resolution: events.jsonl lives at <project>/.cx/ when the cwd sits
10
+ * inside a Construct project, otherwise at ~/.cx/. Without per-project
11
+ * isolation, fingerprints from project A suppress real events in project B.
12
+ * Override the directory with CONSTRUCT_ROLES_ROOT for tests.
8
13
  */
9
14
 
10
15
  import { createHash } from 'node:crypto';
11
16
  import { existsSync, readFileSync, appendFileSync, writeFileSync, mkdirSync } from 'node:fs';
12
17
  import { homedir } from 'node:os';
13
- import { join } from 'node:path';
18
+ import { dirname, join } from 'node:path';
19
+ import { resolveProjectScopedPath } from '../project-root.mjs';
14
20
 
15
21
  const MAX_LINES = 1000;
16
22
 
17
- function rootDir() {
18
- return process.env.CONSTRUCT_ROLES_ROOT || join(homedir(), '.cx');
19
- }
20
23
  function eventsPath() {
21
- return join(rootDir(), 'events.jsonl');
24
+ if (process.env.CONSTRUCT_ROLES_ROOT) {
25
+ return join(process.env.CONSTRUCT_ROLES_ROOT, 'events.jsonl');
26
+ }
27
+ return resolveProjectScopedPath('events.jsonl', { ensureDir: false });
22
28
  }
23
29
  function ensureDir() {
24
- const r = rootDir();
25
- if (!existsSync(r)) mkdirSync(r, { recursive: true });
30
+ const dir = dirname(eventsPath());
31
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
26
32
  }
27
33
 
28
34
  export function fingerprintOf(type, project, summary) {
@@ -50,6 +56,20 @@ export function emit(eventType, payload = {}) {
50
56
  return line;
51
57
  }
52
58
 
59
+ // Best-effort variant for writers that emit lifecycle events as a side
60
+ // effect of their primary work (e.g. recommendation-store.createRecommendation,
61
+ // docs-lifecycle.runDocsLifecycle). A bus write failure must not surface as
62
+ // an error in the writer that just succeeded — the primary artifact already
63
+ // landed. Returns the emitted line on success, null on failure.
64
+
65
+ export function emitBestEffort(eventType, payload = {}) {
66
+ try {
67
+ return emit(eventType, payload);
68
+ } catch {
69
+ return null;
70
+ }
71
+ }
72
+
53
73
  export function recent({ since = 0, type, fingerprint, limit = 200 } = {}) {
54
74
  const ep = eventsPath();
55
75
  if (!existsSync(ep)) return [];
@@ -76,4 +96,4 @@ function rotate() {
76
96
  } catch { /* rotate is best-effort */ }
77
97
  }
78
98
 
79
- export const _paths = { rootDir, eventsPath };
99
+ export const _paths = { eventsPath };
@@ -1,19 +1,20 @@
1
1
  /**
2
2
  * lib/roles/router.mjs — pure event → persona resolver.
3
3
  *
4
- * Uses EVENT_OWNERSHIP from orchestration-policy and validates against
5
- * the loaded role manifest. Returns null (no owner) silently if the
6
- * event has no declared owner or the persona is not yet onboarded —
7
- * letting us declare ownership ahead of full wiring.
4
+ * Reads ownership from the declarative routing tables built off of
5
+ * specialists/registry.json, then validates against the loaded role
6
+ * manifest. Returns null silently when the event has no declared owner
7
+ * or the persona is not yet onboarded — letting ownership be declared
8
+ * ahead of full wiring.
8
9
  */
9
10
 
10
- import { EVENT_OWNERSHIP } from '../orchestration-policy.mjs';
11
+ import { ownerForEvent } from '../orchestration/routing-tables.mjs';
11
12
  import { loadManifest, isOnboarded } from './manifest.mjs';
12
13
 
13
14
  export function route(event) {
14
15
  const type = event?.type;
15
16
  if (!type) return null;
16
- const ownerCxId = EVENT_OWNERSHIP[type];
17
+ const ownerCxId = ownerForEvent(type);
17
18
  if (!ownerCxId) return null;
18
19
  const personaId = ownerCxId.replace(/^cx-/, '');
19
20
  if (!isOnboarded(personaId)) return null;
@@ -23,5 +24,5 @@ export function route(event) {
23
24
  }
24
25
 
25
26
  export function ownerOf(eventType) {
26
- return EVENT_OWNERSHIP[eventType] || null;
27
+ return ownerForEvent(eventType);
27
28
  }
@@ -36,7 +36,7 @@ const PORT = parseInt(process.env.PORT ?? '4242', 10);
36
36
  const BIND_HOST = process.env.BIND_HOST ?? (process.env.NODE_ENV === 'production' ? '0.0.0.0' : '127.0.0.1');
37
37
 
38
38
  const STATIC_DIR = join(__dirname, 'static');
39
- const REGISTRY_FILE = join(ROOT_DIR, 'agents', 'registry.json');
39
+ const REGISTRY_FILE = join(ROOT_DIR, 'specialists', 'registry.json');
40
40
  const FEATURES_FILE = join(HOME, '.construct', 'features.json');
41
41
  const WORKFLOW_FILE = join(ROOT_DIR, 'plan.md');
42
42
  const SKILLS_DIR = join(ROOT_DIR, 'skills');
@@ -775,9 +775,9 @@ const server = createServer(async (req, res) => {
775
775
  try {
776
776
  const dir =
777
777
  category === 'contracts' || category === 'role-manifests'
778
- ? join(root, 'agents')
778
+ ? join(root, 'specialists')
779
779
  : category === 'agents'
780
- ? join(root, 'agents', 'prompts')
780
+ ? join(root, 'specialists', 'prompts')
781
781
  : join(root, category);
782
782
  if (existsSync(dir)) {
783
783
  const walk = (rel = '') => {
@@ -1406,15 +1406,32 @@ const server = createServer(async (req, res) => {
1406
1406
  env: { ...process.env, NO_COLOR: '1' },
1407
1407
  timeout: 5000,
1408
1408
  });
1409
+ // bd can return non-zero for legitimate reasons that don't warrant a
1410
+ // server 500: bd not installed, dolt lock held by a concurrent bd, no
1411
+ // .beads/ in cwd. Degrade gracefully — return an empty issue list with
1412
+ // `degraded: true` so the dashboard UI shows "no beads" instead of
1413
+ // breaking, and the dashboard-endpoints test (which sweeps every read
1414
+ // endpoint for 5xx) stays green when bd's runtime is wobbly.
1415
+
1409
1416
  if (result.status !== 0) {
1410
- res.writeHead(500, { 'Content-Type': 'application/json' });
1411
- res.end(JSON.stringify({ error: `bd list failed: ${(result.stderr || result.stdout || '').slice(0, 300)}` }));
1417
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1418
+ res.end(JSON.stringify({
1419
+ issues: [],
1420
+ counts: { total: 0, byStatus: {}, byPriority: {} },
1421
+ degraded: true,
1422
+ reason: (result.stderr || result.stdout || 'bd list failed').slice(0, 300).trim(),
1423
+ }));
1412
1424
  return;
1413
1425
  }
1414
1426
  let raw = [];
1415
1427
  try { raw = JSON.parse(result.stdout); } catch (parseErr) {
1416
- res.writeHead(500, { 'Content-Type': 'application/json' });
1417
- res.end(JSON.stringify({ error: `bd list JSON parse failed: ${parseErr.message}` }));
1428
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1429
+ res.end(JSON.stringify({
1430
+ issues: [],
1431
+ counts: { total: 0, byStatus: {}, byPriority: {} },
1432
+ degraded: true,
1433
+ reason: `bd list JSON parse failed: ${parseErr.message}`,
1434
+ }));
1418
1435
  return;
1419
1436
  }
1420
1437
  const issues = raw.map(obj => ({
@@ -1441,8 +1458,13 @@ const server = createServer(async (req, res) => {
1441
1458
  res.writeHead(200, { 'Content-Type': 'application/json' });
1442
1459
  res.end(JSON.stringify({ issues, counts: { total: issues.length, byStatus, byPriority } }));
1443
1460
  } catch (err) {
1444
- res.writeHead(500, { 'Content-Type': 'application/json' });
1445
- res.end(JSON.stringify({ error: err.message }));
1461
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1462
+ res.end(JSON.stringify({
1463
+ issues: [],
1464
+ counts: { total: 0, byStatus: {}, byPriority: {} },
1465
+ degraded: true,
1466
+ reason: err.message,
1467
+ }));
1446
1468
  }
1447
1469
  return;
1448
1470
  }
@@ -1,15 +1 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8" />
5
- <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
- <title>Construct Dashboard</title>
8
- <script type="module" crossorigin src="/assets/index-ab25c707.js"></script>
9
- <link rel="stylesheet" href="/assets/index-f0c80a2b.css">
10
- </head>
11
- <body>
12
- <div id="root"></div>
13
-
14
- </body>
15
- </html>
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>
@@ -23,15 +23,26 @@
23
23
  * identify which producer or consumer is at fault.
24
24
  */
25
25
 
26
- import { existsSync, mkdirSync, statSync, readFileSync, appendFileSync } from 'node:fs';
27
- import { join } from 'node:path';
26
+ import { existsSync, mkdirSync, statSync, readFileSync } from 'node:fs';
27
+ import { join, dirname } from 'node:path';
28
28
  import { homedir } from 'node:os';
29
29
  import { createHash } from 'node:crypto';
30
30
  import { getContractById, validatePacket } from './specialist-contracts.mjs';
31
+ import { appendBounded } from './logging/rotate.mjs';
32
+ import { resolveProjectScopedPath } from './project-root.mjs';
31
33
  import { validatePostconditions } from './specialists/postconditions.mjs';
32
34
 
33
35
  const CX_DIR = join(homedir(), '.cx');
34
- const LOG_FILE = join(CX_DIR, 'contract-violations.jsonl');
36
+ // contract-violations are PROJECT-SCOPED — a violation belongs to a
37
+ // specific contract chain in a specific project. Resolves to
38
+ // <project>/.cx/contract-violations.jsonl when inside a project, falling
39
+ // back to the legacy ~/.cx path for standalone invocations. Resolved on
40
+ // every call so cwd/HOME changes inside the same process (tests, harness
41
+ // reuse) route correctly.
42
+
43
+ function logFile() {
44
+ return resolveProjectScopedPath('contract-violations.jsonl', { ensureDir: false });
45
+ }
35
46
  const LAST_AGENT = join(CX_DIR, 'last-agent.json');
36
47
 
37
48
  export class ContractViolationError extends Error {
@@ -58,10 +69,11 @@ function readLastAgent() {
58
69
 
59
70
  function readPrevLineHash() {
60
71
  try {
61
- if (!existsSync(LOG_FILE)) return null;
62
- const size = statSync(LOG_FILE).size;
72
+ const file = logFile();
73
+ if (!existsSync(file)) return null;
74
+ const size = statSync(file).size;
63
75
  if (size === 0) return null;
64
- const tail = readFileSync(LOG_FILE, 'utf8').slice(Math.max(0, size - 2048));
76
+ const tail = readFileSync(file, 'utf8').slice(Math.max(0, size - 2048));
65
77
  const lines = tail.split('\n').filter(Boolean);
66
78
  return lines.length === 0 ? null : sha256(lines[lines.length - 1]);
67
79
  } catch { return null; }
@@ -69,7 +81,8 @@ function readPrevLineHash() {
69
81
 
70
82
  function logViolation(contractId, direction, missing, packet, extra = {}) {
71
83
  try {
72
- mkdirSync(CX_DIR, { recursive: true });
84
+ const file = logFile();
85
+ mkdirSync(dirname(file), { recursive: true });
73
86
  const record = {
74
87
  ts: new Date().toISOString(),
75
88
  agent: readLastAgent(),
@@ -80,7 +93,7 @@ function logViolation(contractId, direction, missing, packet, extra = {}) {
80
93
  prev_line_hash: readPrevLineHash(),
81
94
  ...extra,
82
95
  };
83
- appendFileSync(LOG_FILE, JSON.stringify(record) + '\n', 'utf8');
96
+ appendBounded('contract-violations', file, JSON.stringify(record) + '\n');
84
97
  } catch { /* logging is best-effort */ }
85
98
  }
86
99
 
@@ -145,10 +158,11 @@ export function enforcePacket(contractId, packet, direction = 'input') {
145
158
  * Read recent violations from the chain log. Returns oldest-first.
146
159
  */
147
160
  export function recentViolations({ windowMs = 24 * 60 * 60 * 1000 } = {}) {
148
- if (!existsSync(LOG_FILE)) return [];
161
+ const file = logFile();
162
+ if (!existsSync(file)) return [];
149
163
  try {
150
164
  const cutoff = Date.now() - windowMs;
151
- return readFileSync(LOG_FILE, 'utf8')
165
+ return readFileSync(file, 'utf8')
152
166
  .trim()
153
167
  .split('\n')
154
168
  .filter(Boolean)
package/lib/status.mjs CHANGED
@@ -664,7 +664,7 @@ export async function buildStatus({
664
664
  const resolvedDashboardPort = parsePort(dashboardPort ?? mergedEnv.DASHBOARD_PORT, 4242);
665
665
 
666
666
  const pkg = readJSON(join(rootDir, 'package.json')) ?? {};
667
- const registry = readJSON(join(rootDir, 'agents', 'registry.json')) ?? {};
667
+ const registry = readJSON(join(rootDir, 'specialists', 'registry.json')) ?? {};
668
668
  const settings = readJSON(join(homeDir, '.claude', 'settings.json')) ?? {};
669
669
  const features = await checkAllFeatures({ homeDir, cwd, env: mergedEnv });
670
670
  const pluginRegistry = loadPluginRegistry({ cwd, homeDir, rootDir, env: mergedEnv });
@@ -125,7 +125,7 @@ export async function createBackup({ includeSecrets = false, registryPath, destD
125
125
  }
126
126
 
127
127
  // ── Registry snapshot ─────────────────────────────────────────────────
128
- const regPath = registryPath || path.join(process.cwd(), 'agents', 'registry.json');
128
+ const regPath = registryPath || path.join(process.cwd(), 'specialists', 'registry.json');
129
129
  if (fs.existsSync(regPath)) {
130
130
  const dst = path.join(stageDir, 'registry.json');
131
131
  fs.copyFileSync(regPath, dst);
@@ -274,7 +274,7 @@ export async function restoreBackup(archivePath, { yes = false } = {}) {
274
274
  // Restore registry.json
275
275
  const stagReg = path.join(extractDir, 'registry.json');
276
276
  if (fs.existsSync(stagReg)) {
277
- const dest = path.join(process.cwd(), 'agents', 'registry.json');
277
+ const dest = path.join(process.cwd(), 'specialists', 'registry.json');
278
278
  fs.mkdirSync(path.dirname(dest), { recursive: true });
279
279
  fs.copyFileSync(stagReg, dest);
280
280
  restored.push('registry.json');
@@ -15,6 +15,14 @@
15
15
  import fs from 'node:fs';
16
16
  import os from 'node:os';
17
17
  import path from 'node:path';
18
+ import { appendBounded } from '../logging/rotate.mjs';
19
+ import { resolveProjectScopedPath } from '../project-root.mjs';
20
+
21
+ // intent-verifications are PROJECT-SCOPED — a verification belongs to a
22
+ // specific routing decision in a specific project. DEFAULT_LOG_PATH stays
23
+ // at the legacy user-scope location for back-compat with readers that
24
+ // imported the constant; the writer below resolves per-call so each
25
+ // invocation lands in the project's .cx/ when one is detected.
18
26
 
19
27
  export const DEFAULT_LOG_PATH = path.join(os.homedir(), '.cx', 'intent-verifications.jsonl');
20
28
 
@@ -38,7 +46,12 @@ export const DEFAULT_LOG_PATH = path.join(os.homedir(), '.cx', 'intent-verificat
38
46
  */
39
47
  export function logIntentVerification(event, opts = {}) {
40
48
  if (!event) return;
41
- const logPath = opts.logPath || DEFAULT_LOG_PATH;
49
+ // Resolve per-call: callers that explicitly pass opts.logPath win;
50
+ // otherwise prefer the project-scoped path when cwd is inside a
51
+ // Construct project, falling back to DEFAULT_LOG_PATH (~/.cx) outside.
52
+
53
+ const logPath = opts.logPath
54
+ || resolveProjectScopedPath('intent-verifications.jsonl', { ensureDir: false });
42
55
  const entry = {
43
56
  ts: new Date().toISOString(),
44
57
  requestExcerpt: String(event.request || '').slice(0, 200),
@@ -57,9 +70,9 @@ export function logIntentVerification(event, opts = {}) {
57
70
 
58
71
  try {
59
72
  fs.mkdirSync(path.dirname(logPath), { recursive: true });
60
- fs.appendFileSync(logPath, JSON.stringify(entry) + '\n', 'utf8');
73
+ appendBounded('intent-verifications', logPath, JSON.stringify(entry) + '\n');
61
74
  } catch {
62
- // Telemetry must never throw — the routing path doesn't await us anyway.
75
+ // Telemetry must never throw — the routing path doesn't await anyway.
63
76
  }
64
77
  }
65
78
 
@@ -17,6 +17,13 @@
17
17
  import fs from 'node:fs';
18
18
  import os from 'node:os';
19
19
  import path from 'node:path';
20
+ import { appendBounded } from '../logging/rotate.mjs';
21
+ import { resolveProjectScope } from '../project-root.mjs';
22
+
23
+ // Skill calls are CROSS-PROJECT telemetry — a single log lets you see which
24
+ // skills load hot across every project the user has run Construct in. Stays
25
+ // at user scope; each entry is tagged with `projectId` so a reader can
26
+ // attribute usage to a specific project without losing the global view.
20
27
 
21
28
  export const DEFAULT_LOG_PATH = path.join(os.homedir(), '.cx', 'skill-calls.jsonl');
22
29
 
@@ -37,10 +44,12 @@ export function logSkillCall(event, opts = {}) {
37
44
  if (!event || !event.skillId || !event.source) return;
38
45
 
39
46
  const logPath = opts.logPath || DEFAULT_LOG_PATH;
47
+ const scope = resolveProjectScope();
40
48
  const entry = {
41
49
  ts: new Date().toISOString(),
42
50
  skillId: event.skillId,
43
51
  source: event.source,
52
+ ...(scope?.projectId ? { projectId: scope.projectId } : {}),
44
53
  ...(event.callerContext ? { callerContext: event.callerContext } : {}),
45
54
  ...(event.latencyMs != null ? { latencyMs: event.latencyMs } : {}),
46
55
  ...(event.agentId ? { agentId: event.agentId } : {}),
@@ -50,10 +59,10 @@ export function logSkillCall(event, opts = {}) {
50
59
 
51
60
  try {
52
61
  fs.mkdirSync(path.dirname(logPath), { recursive: true });
53
- fs.appendFileSync(logPath, JSON.stringify(entry) + '\n', 'utf8');
62
+ appendBounded('skill-calls', logPath, JSON.stringify(entry) + '\n');
54
63
  } catch {
55
- // Append-only telemetry; if the user's home dir is unwritable we silently
56
- // skip rather than break the skill load that triggered us.
64
+ // Append-only telemetry; an unwritable home dir is a silent skip
65
+ // rather than a break on the skill load that triggered the call.
57
66
  }
58
67
  }
59
68