@geraldmaron/construct 1.0.11 → 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.
- package/README.md +55 -18
- package/bin/construct +259 -11
- package/bin/construct-postinstall.mjs +23 -2
- package/lib/auto-docs.mjs +23 -22
- package/lib/cli-commands.mjs +2 -0
- package/lib/dashboard-static.mjs +7 -4
- package/lib/doc-stamp.mjs +16 -0
- package/lib/docs-verify.mjs +1 -8
- package/lib/doctor/watchers/bd-watch.mjs +6 -2
- package/lib/embed/daemon.mjs +20 -0
- package/lib/embed/docs-lifecycle.mjs +19 -0
- package/lib/embed/inbox.mjs +85 -2
- package/lib/embed/recommendation-store.mjs +29 -0
- package/lib/gates-audit.mjs +18 -12
- package/lib/hooks/_lib/input.mjs +52 -0
- package/lib/hooks/adaptive-lint.mjs +4 -0
- package/lib/hooks/agent-tracker.mjs +59 -15
- package/lib/hooks/audit-reads.mjs +83 -42
- package/lib/hooks/audit-trail.mjs +28 -18
- package/lib/hooks/bash-output-logger.mjs +8 -2
- package/lib/hooks/block-no-verify.mjs +4 -0
- package/lib/hooks/ci-status-check.mjs +4 -0
- package/lib/hooks/comment-lint.mjs +6 -4
- package/lib/hooks/config-protection.mjs +4 -0
- package/lib/hooks/context-watch.mjs +4 -0
- package/lib/hooks/context-window-recovery.mjs +4 -0
- package/lib/hooks/dep-audit.mjs +12 -5
- package/lib/hooks/doc-coupling-check.mjs +5 -1
- package/lib/hooks/edit-accumulator.mjs +25 -10
- package/lib/hooks/edit-error-recovery.mjs +4 -0
- package/lib/hooks/edit-guard.mjs +4 -0
- package/lib/hooks/guard-bash.mjs +4 -0
- package/lib/hooks/mcp-audit.mjs +4 -0
- package/lib/hooks/mcp-health-check.mjs +4 -0
- package/lib/hooks/model-fallback.mjs +7 -11
- package/lib/hooks/policy-engine.mjs +4 -0
- package/lib/hooks/post-merge-docs-check.mjs +4 -0
- package/lib/hooks/post-merge-tracking.mjs +82 -0
- package/lib/hooks/pre-compact.mjs +4 -0
- package/lib/hooks/pre-push-gate.mjs +84 -231
- package/lib/hooks/proactive-activation.mjs +5 -2
- package/lib/hooks/readme-age-check.mjs +4 -0
- package/lib/hooks/registry-sync.mjs +35 -20
- package/lib/hooks/rule-verifier.mjs +3 -0
- package/lib/hooks/scan-secrets.mjs +4 -0
- package/lib/hooks/session-optimize.mjs +4 -0
- package/lib/hooks/session-reflect.mjs +4 -0
- package/lib/hooks/session-start.mjs +48 -1
- package/lib/hooks/session-tracking-refresh.mjs +70 -0
- package/lib/hooks/stop-notify.mjs +13 -2
- package/lib/hooks/stop-typecheck.mjs +4 -0
- package/lib/hooks/test-watch.mjs +8 -4
- package/lib/init-unified.mjs +90 -23
- package/lib/intake/attribution.mjs +77 -0
- package/lib/intake/intake-config.mjs +3 -0
- package/lib/intake/manifest.mjs +107 -0
- package/lib/intake/poll-lock.mjs +136 -0
- package/lib/intake/prepare.mjs +42 -4
- package/lib/logging/rotate.mjs +394 -0
- package/lib/mcp/tools/project.mjs +2 -2
- package/lib/mcp/tools/telemetry.mjs +1 -1
- package/lib/opencode-config.mjs +10 -3
- package/lib/orchestration/routing-tables.mjs +176 -0
- package/lib/orchestration-policy.mjs +18 -106
- package/lib/parity.mjs +48 -7
- package/lib/profiles/lifecycle.mjs +19 -1
- package/lib/project-init-shared.mjs +11 -5
- package/lib/project-root.mjs +104 -0
- package/lib/roles/catalog.mjs +1 -1
- package/lib/roles/event-bus.mjs +29 -9
- package/lib/roles/router.mjs +8 -7
- package/lib/server/index.mjs +31 -9
- package/lib/server/static/index.html +1 -15
- package/lib/specialist-contracts-enforce.mjs +24 -10
- package/lib/status.mjs +1 -1
- package/lib/storage/backup.mjs +2 -2
- package/lib/sync/skill-frontmatter.mjs +71 -0
- package/lib/telemetry/intent-verifications.mjs +16 -3
- package/lib/telemetry/skill-calls.mjs +12 -3
- package/lib/tracking-surfaces.mjs +375 -0
- package/lib/worker/trace.mjs +19 -2
- package/package.json +6 -2
- package/platforms/claude/settings.template.json +28 -27
- package/scripts/sync-specialists.mjs +296 -94
- package/specialists/registry.json +158 -13
- package/lib/hooks/env-check.mjs +0 -83
- package/lib/hooks/read-tracker.mjs +0 -61
- package/lib/policy/unified-gates.mjs +0 -96
- package/lib/server/static/assets/index-ab25c707.js +0 -70
- package/lib/server/static/assets/index-f0c80a2b.css +0 -1
package/lib/roles/event-bus.mjs
CHANGED
|
@@ -1,28 +1,34 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* lib/roles/event-bus.mjs — append-only event log
|
|
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
|
-
|
|
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
|
|
25
|
-
if (!existsSync(
|
|
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 = {
|
|
99
|
+
export const _paths = { eventsPath };
|
package/lib/roles/router.mjs
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* lib/roles/router.mjs — pure event → persona resolver.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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 {
|
|
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 =
|
|
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
|
|
27
|
+
return ownerForEvent(eventType);
|
|
27
28
|
}
|
package/lib/server/index.mjs
CHANGED
|
@@ -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, '
|
|
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, '
|
|
778
|
+
? join(root, 'specialists')
|
|
779
779
|
: category === 'agents'
|
|
780
|
-
? join(root, '
|
|
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(
|
|
1411
|
-
res.end(JSON.stringify({
|
|
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(
|
|
1417
|
-
res.end(JSON.stringify({
|
|
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(
|
|
1445
|
-
res.end(JSON.stringify({
|
|
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 & 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
|
|
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
|
-
|
|
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
|
-
|
|
62
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
161
|
+
const file = logFile();
|
|
162
|
+
if (!existsSync(file)) return [];
|
|
149
163
|
try {
|
|
150
164
|
const cutoff = Date.now() - windowMs;
|
|
151
|
-
return readFileSync(
|
|
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, '
|
|
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 });
|
package/lib/storage/backup.mjs
CHANGED
|
@@ -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(), '
|
|
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(), '
|
|
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');
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/sync/skill-frontmatter.mjs — Build Anthropic Agent Skills frontmatter
|
|
3
|
+
* for SKILL.md files emitted by sync-specialists.
|
|
4
|
+
*
|
|
5
|
+
* Anthropic Skills require YAML frontmatter with at minimum `name` and
|
|
6
|
+
* `description`. The description gates skill selection — without it, the
|
|
7
|
+
* loader silently skips the file (the user-reported bug: 141 files dropped).
|
|
8
|
+
* Source Construct skills carry an HTML comment header with the description;
|
|
9
|
+
* extractSkillDescription pulls it out.
|
|
10
|
+
*
|
|
11
|
+
* Also exports a conservative stripLeadingFrontmatter helper so we never
|
|
12
|
+
* emit double-frontmatter when the source body already has its own block.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const HTML_COMMENT_RE = /^<!--([\s\S]*?)-->/;
|
|
16
|
+
// Matches the source skill comment header shape: `skills/path/foo.md (Title)
|
|
17
|
+
// Use when X.\n...rest of comment...`. Group 1 captures the optional title,
|
|
18
|
+
// group 2 captures all subsequent prose inside the comment. No /m flag — `$`
|
|
19
|
+
// matches end of input, not end of line, so multi-line "Use when..." prose
|
|
20
|
+
// is captured in full and firstSentence() picks the first terminator.
|
|
21
|
+
|
|
22
|
+
const SOURCE_HEADER_RE = /^skills\/[\w./-]+\s*(?:\(([^)]+)\))?\s*([\s\S]*?)$/;
|
|
23
|
+
|
|
24
|
+
// Anthropic Skills cap descriptions at 1024 chars; this cap is tighter so
|
|
25
|
+
// the YAML stays readable in `construct skills list` output too.
|
|
26
|
+
const DESCRIPTION_MAX = 240;
|
|
27
|
+
|
|
28
|
+
export function buildSkillFrontmatter(name, sourceContent) {
|
|
29
|
+
const skillName = String(name).replace(/\//g, '.');
|
|
30
|
+
const description = extractSkillDescription(sourceContent) || `Construct skill: ${skillName}`;
|
|
31
|
+
const safeDescription = description.replace(/\n+/g, ' ').slice(0, DESCRIPTION_MAX).replace(/"/g, "'");
|
|
32
|
+
return `---\nname: ${skillName}\ndescription: "${safeDescription}"\n---\n`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function extractSkillDescription(content) {
|
|
36
|
+
if (!content) return null;
|
|
37
|
+
const commentMatch = content.match(HTML_COMMENT_RE);
|
|
38
|
+
if (commentMatch) {
|
|
39
|
+
const inner = commentMatch[1].trim();
|
|
40
|
+
const headerMatch = inner.match(SOURCE_HEADER_RE);
|
|
41
|
+
if (headerMatch) {
|
|
42
|
+
const prose = (headerMatch[2] || '').trim();
|
|
43
|
+
if (prose) return firstSentence(prose);
|
|
44
|
+
}
|
|
45
|
+
const lines = inner.split('\n').map((l) => l.trim()).filter(Boolean);
|
|
46
|
+
if (lines.length >= 2) return firstSentence(lines.slice(1).join(' '));
|
|
47
|
+
}
|
|
48
|
+
const body = content.replace(HTML_COMMENT_RE, '').trim();
|
|
49
|
+
const paragraphs = body.split(/\n\s*\n/);
|
|
50
|
+
for (const p of paragraphs) {
|
|
51
|
+
const trimmed = p.trim();
|
|
52
|
+
if (trimmed.startsWith('#')) continue;
|
|
53
|
+
if (trimmed) return firstSentence(trimmed);
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function firstSentence(text) {
|
|
59
|
+
const match = text.match(/^[\s\S]*?[.!?](?:\s|$)/);
|
|
60
|
+
return (match ? match[0] : text).trim();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function stripLeadingFrontmatter(content) {
|
|
64
|
+
if (!content) return content;
|
|
65
|
+
if (!content.startsWith('---\n') && !content.startsWith('---\r\n')) return content;
|
|
66
|
+
const closeIdx = content.indexOf('\n---', 4);
|
|
67
|
+
if (closeIdx === -1) return content;
|
|
68
|
+
const afterClose = content.indexOf('\n', closeIdx + 1);
|
|
69
|
+
if (afterClose === -1) return '';
|
|
70
|
+
return content.slice(afterClose + 1);
|
|
71
|
+
}
|
|
@@ -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
|
-
|
|
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
|
-
|
|
73
|
+
appendBounded('intent-verifications', logPath, JSON.stringify(entry) + '\n');
|
|
61
74
|
} catch {
|
|
62
|
-
// Telemetry must never throw — the routing path doesn't await
|
|
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
|
-
|
|
62
|
+
appendBounded('skill-calls', logPath, JSON.stringify(entry) + '\n');
|
|
54
63
|
} catch {
|
|
55
|
-
// Append-only telemetry;
|
|
56
|
-
//
|
|
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
|
|