@geraldmaron/construct 1.0.6 → 1.0.8
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 +39 -34
- package/bin/construct +1403 -96
- package/commands/build/feature.md +1 -1
- package/commands/build/fix.md +1 -1
- package/commands/design/access.md +1 -1
- package/commands/design/flow.md +1 -1
- package/commands/design/ui.md +1 -1
- package/commands/measure/experiment.md +1 -1
- package/commands/measure/metrics.md +1 -1
- package/commands/measure/results.md +1 -1
- package/commands/plan/api.md +1 -1
- package/commands/plan/challenge.md +1 -1
- package/commands/plan/decide.md +1 -1
- package/commands/plan/feature.md +1 -1
- package/commands/plan/requirements.md +1 -1
- package/commands/remember/context.md +1 -1
- package/commands/remember/handoff.md +1 -1
- package/commands/remember/runbook.md +1 -1
- package/commands/review/code.md +1 -1
- package/commands/review/quality.md +1 -1
- package/commands/review/security.md +1 -1
- package/commands/ship/ready.md +1 -1
- package/commands/ship/release.md +1 -1
- package/commands/ship/status.md +1 -1
- package/commands/understand/docs.md +1 -1
- package/commands/understand/this.md +1 -1
- package/commands/understand/why.md +1 -1
- package/commands/work/clean.md +1 -1
- package/commands/work/drive.md +1 -1
- package/commands/work/optimize-prompts.md +2 -2
- package/commands/work/parallel-review.md +1 -1
- package/db/schema/007_tags.sql +30 -0
- package/db/schema/008_skill_usage.sql +24 -0
- package/db/schema/009_scheduler.sql +14 -0
- package/examples/internal/roles/architect/bad/clever-plan-without-contracts.md +1 -1
- package/examples/internal/roles/architect/golden/explicit-tradeoff-before-plan.md +1 -1
- package/examples/internal/roles/engineer/bad/speculative-abstraction.md +1 -1
- package/examples/internal/roles/engineer/golden/read-before-write.md +1 -1
- package/examples/internal/roles/qa/bad/coverage-theater.md +1 -1
- package/examples/internal/roles/qa/golden/regression-gate.md +1 -1
- package/examples/seed-observations/decisions.md +1 -1
- package/lib/audit-skills.mjs +3 -3
- package/lib/auto-docs.mjs +8 -8
- package/lib/boundary.mjs +126 -0
- package/lib/cache-strategy-google.js +26 -31
- package/lib/cli-commands.mjs +276 -122
- package/lib/comment-lint.mjs +139 -1
- package/lib/completions.mjs +1 -1
- package/lib/config/schema.mjs +15 -1
- package/lib/contracts/validate.mjs +371 -0
- package/lib/daemons/contract.mjs +210 -0
- package/lib/distill.mjs +22 -4
- package/lib/doc-stamp.mjs +48 -0
- package/lib/docs-verify.mjs +59 -6
- package/lib/doctor/cli.mjs +16 -1
- package/lib/doctor/index.mjs +3 -1
- package/lib/doctor/watchers/consistency.mjs +360 -0
- package/lib/doctor/watchers/mcp-protocol.mjs +232 -0
- package/lib/document-extract.mjs +420 -17
- package/lib/document-ingest.mjs +2 -0
- package/lib/embed/cli.mjs +124 -3
- package/lib/embed/daemon.mjs +43 -4
- package/lib/embed/docs-lifecycle.mjs +1 -1
- package/lib/embed/inbox.mjs +50 -3
- package/lib/embed/role-framing.mjs +3 -3
- package/lib/embed/scheduler.mjs +33 -5
- package/lib/env-config.mjs +9 -3
- package/lib/evaluator-optimizer.mjs +2 -3
- package/lib/extractors/calendar.mjs +173 -0
- package/lib/extractors/shared/drop-info.mjs +22 -0
- package/lib/extractors/transcript.mjs +0 -0
- package/lib/gates-audit.mjs +8 -2
- package/lib/git-hooks/prepare-commit-msg +1 -1
- package/lib/headhunt.mjs +2 -2
- package/lib/hooks/agent-tracker.mjs +6 -11
- package/lib/hooks/comment-lint.mjs +16 -0
- package/lib/hooks/guard-bash.mjs +35 -14
- package/lib/hooks/mcp-audit.mjs +14 -1
- package/lib/hooks/pre-compact.mjs +92 -5
- package/lib/hooks/pre-push-gate.mjs +41 -8
- package/lib/hooks/proactive-activation.mjs +0 -14
- package/lib/hooks/registry-sync.mjs +2 -2
- package/lib/hooks/rule-verifier.mjs +217 -0
- package/lib/hooks/session-optimize.mjs +2 -1
- package/lib/hooks/stop-notify.mjs +7 -6
- package/lib/host-capabilities.mjs +10 -1
- package/lib/hygiene/scan.mjs +141 -0
- package/lib/init-unified.mjs +73 -65
- package/lib/install/stage-project.mjs +4 -4
- package/lib/intake/classify.mjs +309 -58
- package/lib/intake/daemon.mjs +121 -0
- package/lib/intake/filesystem-queue.mjs +31 -6
- package/lib/intake/intake-config.mjs +2 -1
- package/lib/intake/postgres-queue.mjs +45 -3
- package/lib/intake/prepare.mjs +3 -1
- package/lib/intake/quarantine.mjs +205 -0
- package/lib/intake/session-prelude.mjs +7 -1
- package/lib/intake/traceability.mjs +90 -0
- package/lib/knowledge/postgres-search.mjs +132 -0
- package/lib/knowledge/research-store.mjs +2 -0
- package/lib/knowledge/search.mjs +14 -4
- package/lib/maintenance/cleanup.mjs +315 -0
- package/lib/mcp/memory-bridge.mjs +276 -0
- package/lib/mcp/server.mjs +11 -3
- package/lib/mcp/tools/skills.mjs +32 -11
- package/lib/mcp/tools/workflow.mjs +25 -0
- package/lib/mcp-catalog.json +12 -8
- package/lib/mcp-platform-config.mjs +16 -8
- package/lib/migrations/index.mjs +106 -0
- package/lib/migrations/v1-baseline.mjs +33 -0
- package/lib/model-registry.mjs +1 -1
- package/lib/observation-store.mjs +9 -4
- package/lib/opencode-runtime-plugin.mjs +1 -1
- package/lib/orchestration-policy.mjs +4 -4
- package/lib/outcomes/record.mjs +2 -0
- package/lib/overrides/resolver.mjs +3 -3
- package/lib/parity.mjs +4 -4
- package/lib/policy/engine.mjs +2 -2
- package/lib/profiles/lifecycle.mjs +1 -1
- package/lib/profiles/rebrand.mjs +46 -0
- package/lib/project-init-shared.mjs +12 -0
- package/lib/prompt-metadata.mjs +4 -4
- package/lib/provider-capabilities.js +20 -7
- package/lib/providers/auth-manager.mjs +58 -17
- package/lib/providers/circuit-breaker.mjs +14 -0
- package/lib/providers/contract.mjs +67 -3
- package/lib/providers/creds.mjs +219 -0
- package/lib/providers/scaffold.mjs +107 -0
- package/lib/reflect.mjs +49 -12
- package/lib/role-preload.mjs +5 -0
- package/lib/roles/catalog.mjs +1 -1
- package/lib/roles/manifest.mjs +2 -2
- package/lib/scheduler/index.mjs +112 -0
- package/lib/scheduler/solo.mjs +183 -0
- package/lib/server/index.mjs +29 -29
- package/lib/server/insights.mjs +14 -12
- package/lib/server/langfuse-login.mjs +58 -0
- package/lib/server/static/assets/index-ab25c707.js +1 -1
- package/lib/session-store.mjs +6 -4
- package/lib/setup.mjs +14 -3
- package/lib/{agent-contracts-enforce.mjs → specialist-contracts-enforce.mjs} +4 -4
- package/lib/{agent-contracts.mjs → specialist-contracts.mjs} +9 -8
- package/lib/{agents → specialists}/postconditions.mjs +3 -3
- package/lib/{agents → specialists}/schema.mjs +6 -6
- package/lib/status.mjs +13 -11
- package/lib/storage/backup.mjs +2 -2
- package/lib/tags/lifecycle.mjs +278 -0
- package/lib/tags/vocabulary.mjs +140 -0
- package/lib/telemetry/client.mjs +5 -1
- package/lib/telemetry/otel-tracer.mjs +184 -0
- package/lib/telemetry/skill-calls.mjs +73 -12
- package/lib/uninstall/uninstall.mjs +1 -1
- package/lib/update.mjs +1 -1
- package/lib/validator.mjs +57 -56
- package/lib/version.mjs +51 -0
- package/lib/worker/trace.mjs +5 -1
- package/lib/workflows/instantiate.mjs +320 -0
- package/package.json +16 -3
- package/personas/construct.md +5 -3
- package/platforms/claude/CLAUDE.md +1 -1
- package/rules/common/development-workflow.md +2 -1
- package/rules/common/no-fabrication.md +69 -0
- package/rules/common/review-before-change.md +58 -0
- package/scripts/{sync-agents.mjs → sync-specialists.mjs} +171 -68
- package/skills/ai/prompt-optimizer.md +3 -3
- package/skills/exploration/dependency-graph-reading.md +98 -0
- package/skills/exploration/tracer-bullet-method.md +71 -0
- package/skills/exploration/unknown-codebase-onboarding.md +91 -0
- package/skills/operating/change-management.md +91 -0
- package/skills/operating/incident-response.md +75 -0
- package/skills/operating/oncall-rotation.md +95 -0
- package/skills/operating/orchestration-reference.md +2 -2
- package/skills/strategy/competitive-landscape.md +75 -0
- package/skills/strategy/market-research-methods.md +87 -0
- package/skills/strategy/narrative-arc.md +77 -0
- package/skills/strategy/pricing-positioning.md +94 -0
- package/specialists/contracts.json +1002 -0
- package/specialists/contracts.schema.json +83 -0
- package/specialists/policy-inventory.json +160 -0
- package/{agents → specialists}/prompts/cx-accessibility.md +3 -1
- package/{agents → specialists}/prompts/cx-ai-engineer.md +3 -1
- package/{agents → specialists}/prompts/cx-architect.md +3 -1
- package/{agents → specialists}/prompts/cx-business-strategist.md +3 -1
- package/{agents → specialists}/prompts/cx-data-analyst.md +3 -1
- package/{agents → specialists}/prompts/cx-data-engineer.md +3 -1
- package/{agents → specialists}/prompts/cx-debugger.md +3 -1
- package/{agents → specialists}/prompts/cx-designer.md +3 -1
- package/{agents → specialists}/prompts/cx-devil-advocate.md +3 -1
- package/{agents → specialists}/prompts/cx-docs-keeper.md +4 -2
- package/{agents → specialists}/prompts/cx-engineer.md +4 -2
- package/{agents → specialists}/prompts/cx-evaluator.md +3 -1
- package/{agents → specialists}/prompts/cx-explorer.md +3 -1
- package/{agents → specialists}/prompts/cx-legal-compliance.md +3 -1
- package/{agents → specialists}/prompts/cx-operations.md +3 -1
- package/{agents → specialists}/prompts/cx-orchestrator.md +4 -2
- package/{agents → specialists}/prompts/cx-platform-engineer.md +3 -1
- package/{agents → specialists}/prompts/cx-product-manager.md +3 -1
- package/{agents → specialists}/prompts/cx-qa.md +3 -1
- package/{agents → specialists}/prompts/cx-rd-lead.md +3 -1
- package/{agents → specialists}/prompts/cx-release-manager.md +3 -1
- package/{agents → specialists}/prompts/cx-researcher.md +3 -1
- package/{agents → specialists}/prompts/cx-reviewer.md +4 -2
- package/{agents → specialists}/prompts/cx-security.md +3 -1
- package/{agents → specialists}/prompts/cx-sre.md +4 -2
- package/{agents → specialists}/prompts/cx-test-automation.md +3 -1
- package/{agents → specialists}/prompts/cx-trace-reviewer.md +5 -3
- package/{agents → specialists}/prompts/cx-ux-researcher.md +3 -1
- package/{agents → specialists}/registry.json +651 -418
- package/{agents → specialists}/role-manifests.json +8 -8
- package/templates/docs/construct_guide.md +6 -6
- package/templates/docs/skill-artifact.md +1 -1
- package/templates/provider-scaffold/health.test.mjs +30 -0
- package/templates/provider-scaffold/index.mjs +48 -0
- package/templates/workflows/cross-team-handoff.yml +85 -0
- package/templates/workflows/engineering-onboarding.yml +77 -0
- package/templates/workflows/new-feature.yml +53 -0
- package/agents/contracts.json +0 -387
- /package/{agents → specialists}/teams.json +0 -0
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/maintenance/cleanup.mjs — Construct's self-maintenance primitives.
|
|
3
|
+
*
|
|
4
|
+
* Every artifact Construct writes has a lifecycle. This module collects the
|
|
5
|
+
* primitives that enforce that lifecycle: rotate oversized logs, prune
|
|
6
|
+
* rotated segments, age-out cache directories. Each primitive is pure and
|
|
7
|
+
* returns a structured summary; runFullCleanup orchestrates them with sane
|
|
8
|
+
* defaults.
|
|
9
|
+
*
|
|
10
|
+
* Triggered from two entry points:
|
|
11
|
+
* - `construct cleanup` — manual sweep, optionally --dry-run
|
|
12
|
+
* - bin/construct startup check — automatic when the installed version
|
|
13
|
+
* differs from ~/.construct/.cleanup-stamp,
|
|
14
|
+
* so an upgrade reclaims disk without
|
|
15
|
+
* requiring a manual command.
|
|
16
|
+
*
|
|
17
|
+
* Scope is deliberately narrow: only artifacts whose retention is owned by
|
|
18
|
+
* the runtime (logs, caches, rotated segments). Knowledge stores
|
|
19
|
+
* (.cx/observations/, .cx/handoffs/, .cx/intake/) require explicit retention
|
|
20
|
+
* policy and are left for a follow-up after policy is agreed.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import fs from 'node:fs';
|
|
24
|
+
import os from 'node:os';
|
|
25
|
+
import path from 'node:path';
|
|
26
|
+
|
|
27
|
+
const HARD_BUDGET_MS = 5000;
|
|
28
|
+
|
|
29
|
+
// Size caps in bytes. Each entry: [filename relative to ~/.cx/, maxBytes].
|
|
30
|
+
// Files that exceed maxBytes are truncated (head-preserving rotation is not
|
|
31
|
+
// useful for append-only event logs — the most-recent events are what matter,
|
|
32
|
+
// not the oldest; tail-preserving truncation keeps the last N% by recopying).
|
|
33
|
+
const JSONL_LOG_CAPS_BYTES = {
|
|
34
|
+
'audit.jsonl': 25 * 1024 * 1024,
|
|
35
|
+
'contract-violations.jsonl': 25 * 1024 * 1024,
|
|
36
|
+
'doctor-log.jsonl': 25 * 1024 * 1024,
|
|
37
|
+
'role-pending.jsonl': 5 * 1024 * 1024,
|
|
38
|
+
'role-events.jsonl': 25 * 1024 * 1024,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const EMBED_LOG_DEFAULT_MAX_MB = 50;
|
|
42
|
+
const EMBED_LOG_DEFAULT_KEEP = 3;
|
|
43
|
+
const EMBED_LOG_HARD_CAP_MAX_MB = 500;
|
|
44
|
+
const EMBED_LOG_HARD_CAP_KEEP = 20;
|
|
45
|
+
|
|
46
|
+
function fileSizeOrZero(p) {
|
|
47
|
+
try { return fs.statSync(p).size; } catch { return 0; }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function rmSafe(p, dryRun) {
|
|
51
|
+
if (dryRun) return;
|
|
52
|
+
try { fs.rmSync(p, { force: true }); } catch (err) {
|
|
53
|
+
process.stderr.write(`[cleanup] failed to remove ${p}: ${err.message}\n`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Tail-preserving truncate: keeps the last `keepBytes` of `file`, drops the
|
|
59
|
+
* rest. Used for append-only event logs where recent events are valuable but
|
|
60
|
+
* historical bloat is not.
|
|
61
|
+
*/
|
|
62
|
+
function tailTruncate(file, keepBytes, dryRun) {
|
|
63
|
+
if (dryRun) return;
|
|
64
|
+
let stat;
|
|
65
|
+
try { stat = fs.statSync(file); } catch { return; }
|
|
66
|
+
if (stat.size <= keepBytes) return;
|
|
67
|
+
const start = stat.size - keepBytes;
|
|
68
|
+
const buf = Buffer.alloc(keepBytes);
|
|
69
|
+
const fd = fs.openSync(file, 'r');
|
|
70
|
+
try { fs.readSync(fd, buf, 0, keepBytes, start); } finally { fs.closeSync(fd); }
|
|
71
|
+
// Find the first newline so we don't start mid-record.
|
|
72
|
+
const nl = buf.indexOf(0x0a);
|
|
73
|
+
const payload = nl >= 0 && nl < buf.length - 1 ? buf.subarray(nl + 1) : buf;
|
|
74
|
+
fs.writeFileSync(file, payload);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function rotationConfig(env = process.env) {
|
|
78
|
+
const maxMbRaw = Number.parseInt(env.CONSTRUCT_EMBED_LOG_MAX_MB ?? '', 10);
|
|
79
|
+
const keepRaw = Number.parseInt(env.CONSTRUCT_EMBED_LOG_KEEP ?? '', 10);
|
|
80
|
+
const maxMb = Number.isFinite(maxMbRaw) && maxMbRaw > 0 ? Math.min(maxMbRaw, EMBED_LOG_HARD_CAP_MAX_MB) : EMBED_LOG_DEFAULT_MAX_MB;
|
|
81
|
+
const keep = Number.isFinite(keepRaw) && keepRaw >= 0 ? Math.min(keepRaw, EMBED_LOG_HARD_CAP_KEEP) : EMBED_LOG_DEFAULT_KEEP;
|
|
82
|
+
return { maxBytes: maxMb * 1024 * 1024, keep };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Walk the embed log + every rotated segment (.1, .2, ...) and drop the ones
|
|
87
|
+
* that exceed the per-segment cap OR sit beyond the keep count.
|
|
88
|
+
*
|
|
89
|
+
* Unlike rotateEmbedLogIfNeeded in lib/embed/cli.mjs (which is called at
|
|
90
|
+
* daemon spawn time and only rotates the live log), this is the post-upgrade
|
|
91
|
+
* cleanup: it handles the case where an existing install has segments that
|
|
92
|
+
* were oversized before the cap existed.
|
|
93
|
+
*/
|
|
94
|
+
export function cleanupEmbedLog({ homeDir = os.homedir(), env = process.env, dryRun = false } = {}) {
|
|
95
|
+
const dir = path.join(homeDir, '.cx', 'runtime');
|
|
96
|
+
const baseLog = path.join(dir, 'embed-daemon.log');
|
|
97
|
+
const { maxBytes, keep } = rotationConfig(env);
|
|
98
|
+
|
|
99
|
+
const summary = { removed: [], truncated: [], freedBytes: 0, baseLog };
|
|
100
|
+
|
|
101
|
+
// Every existing segment (.log, .log.1, .log.2, ...)
|
|
102
|
+
if (!fs.existsSync(dir)) return summary;
|
|
103
|
+
const entries = fs.readdirSync(dir).filter(n => n === 'embed-daemon.log' || n.startsWith('embed-daemon.log.'));
|
|
104
|
+
|
|
105
|
+
for (const name of entries) {
|
|
106
|
+
const full = path.join(dir, name);
|
|
107
|
+
const size = fileSizeOrZero(full);
|
|
108
|
+
|
|
109
|
+
// Drop segments beyond the keep horizon entirely.
|
|
110
|
+
const dotMatch = name.match(/^embed-daemon\.log\.(\d+)$/);
|
|
111
|
+
if (dotMatch && Number.parseInt(dotMatch[1], 10) > keep) {
|
|
112
|
+
summary.removed.push({ path: full, size });
|
|
113
|
+
summary.freedBytes += size;
|
|
114
|
+
rmSafe(full, dryRun);
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// The live log: if oversized, truncate to 0 (it will rotate cleanly on next
|
|
119
|
+
// daemon spawn via rotateEmbedLogIfNeeded). Truncating in place keeps any
|
|
120
|
+
// open daemon FDs valid (writes resume at byte 0 on next O_APPEND write).
|
|
121
|
+
if (name === 'embed-daemon.log' && size > maxBytes) {
|
|
122
|
+
summary.truncated.push({ path: full, sizeWas: size });
|
|
123
|
+
summary.freedBytes += size;
|
|
124
|
+
if (!dryRun) {
|
|
125
|
+
try {
|
|
126
|
+
fs.truncateSync(full, 0);
|
|
127
|
+
} catch (err) {
|
|
128
|
+
process.stderr.write(`[cleanup] failed to truncate ${full}: ${err.message}\n`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Rotated segments that are individually oversized: drop them. They are
|
|
135
|
+
// archived noise; keeping them around defeats the cap.
|
|
136
|
+
if (dotMatch && size > maxBytes) {
|
|
137
|
+
summary.removed.push({ path: full, size });
|
|
138
|
+
summary.freedBytes += size;
|
|
139
|
+
rmSafe(full, dryRun);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return summary;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Apply per-file size caps to the user-level JSONL event logs at ~/.cx/.
|
|
148
|
+
* Truncates oldest events rather than removing the whole file so the most
|
|
149
|
+
* recent records survive.
|
|
150
|
+
*/
|
|
151
|
+
export function cleanupJsonlLogs({ homeDir = os.homedir(), dryRun = false } = {}) {
|
|
152
|
+
const dir = path.join(homeDir, '.cx');
|
|
153
|
+
const summary = { truncated: [], freedBytes: 0 };
|
|
154
|
+
if (!fs.existsSync(dir)) return summary;
|
|
155
|
+
|
|
156
|
+
for (const [name, capBytes] of Object.entries(JSONL_LOG_CAPS_BYTES)) {
|
|
157
|
+
const file = path.join(dir, name);
|
|
158
|
+
const size = fileSizeOrZero(file);
|
|
159
|
+
if (size <= capBytes) continue;
|
|
160
|
+
// Retain the most recent half-cap so headroom exists before next truncate.
|
|
161
|
+
const keepBytes = Math.floor(capBytes / 2);
|
|
162
|
+
summary.truncated.push({ path: file, sizeWas: size, sizeNow: keepBytes });
|
|
163
|
+
summary.freedBytes += size - keepBytes;
|
|
164
|
+
tailTruncate(file, keepBytes, dryRun);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return summary;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Age out ~/.construct/cache/ entries older than `maxAgeDays`. Cache hits are
|
|
172
|
+
* idempotent and re-fetched on demand, so pruning is safe.
|
|
173
|
+
*/
|
|
174
|
+
export function cleanupCacheDir({ homeDir = os.homedir(), maxAgeDays = 30, dryRun = false } = {}) {
|
|
175
|
+
const dir = path.join(homeDir, '.construct', 'cache');
|
|
176
|
+
const summary = { removed: [], freedBytes: 0 };
|
|
177
|
+
if (!fs.existsSync(dir)) return summary;
|
|
178
|
+
|
|
179
|
+
const cutoff = Date.now() - (maxAgeDays * 24 * 60 * 60 * 1000);
|
|
180
|
+
const walk = (current) => {
|
|
181
|
+
let entries;
|
|
182
|
+
try { entries = fs.readdirSync(current, { withFileTypes: true }); } catch { return; }
|
|
183
|
+
for (const entry of entries) {
|
|
184
|
+
const full = path.join(current, entry.name);
|
|
185
|
+
if (entry.isDirectory()) { walk(full); continue; }
|
|
186
|
+
let stat;
|
|
187
|
+
try { stat = fs.statSync(full); } catch { continue; }
|
|
188
|
+
if (stat.mtimeMs < cutoff) {
|
|
189
|
+
summary.removed.push({ path: full, size: stat.size });
|
|
190
|
+
summary.freedBytes += stat.size;
|
|
191
|
+
rmSafe(full, dryRun);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
walk(dir);
|
|
196
|
+
return summary;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Orchestrator. Runs each primitive under a hard time budget and returns an
|
|
201
|
+
* aggregated summary. Individual failures are non-fatal: a primitive that
|
|
202
|
+
* throws is recorded as an error in the summary but does not block the rest.
|
|
203
|
+
*/
|
|
204
|
+
export function runFullCleanup({ homeDir = os.homedir(), env = process.env, dryRun = false } = {}) {
|
|
205
|
+
const startedAt = Date.now();
|
|
206
|
+
const summary = {
|
|
207
|
+
dryRun,
|
|
208
|
+
startedAt: new Date(startedAt).toISOString(),
|
|
209
|
+
durationMs: 0,
|
|
210
|
+
freedBytes: 0,
|
|
211
|
+
embedLog: null,
|
|
212
|
+
jsonlLogs: null,
|
|
213
|
+
cacheDir: null,
|
|
214
|
+
errors: [],
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const safelyRun = (label, fn) => {
|
|
218
|
+
if (Date.now() - startedAt > HARD_BUDGET_MS) {
|
|
219
|
+
summary.errors.push({ step: label, error: 'time budget exceeded; skipped' });
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
try { return fn(); }
|
|
223
|
+
catch (err) { summary.errors.push({ step: label, error: err.message }); return null; }
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
summary.embedLog = safelyRun('embedLog', () => cleanupEmbedLog({ homeDir, env, dryRun }));
|
|
227
|
+
summary.jsonlLogs = safelyRun('jsonlLogs', () => cleanupJsonlLogs({ homeDir, dryRun }));
|
|
228
|
+
summary.cacheDir = safelyRun('cacheDir', () => cleanupCacheDir({ homeDir, dryRun }));
|
|
229
|
+
|
|
230
|
+
for (const part of [summary.embedLog, summary.jsonlLogs, summary.cacheDir]) {
|
|
231
|
+
if (part?.freedBytes) summary.freedBytes += part.freedBytes;
|
|
232
|
+
}
|
|
233
|
+
summary.durationMs = Date.now() - startedAt;
|
|
234
|
+
return summary;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Format bytes for one-line operator output. 1234567 → "1.2 MB".
|
|
239
|
+
*/
|
|
240
|
+
export function formatBytes(bytes) {
|
|
241
|
+
if (!bytes || bytes < 0) return '0 B';
|
|
242
|
+
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
243
|
+
let n = bytes;
|
|
244
|
+
let unit = 0;
|
|
245
|
+
while (n >= 1024 && unit < units.length - 1) { n /= 1024; unit += 1; }
|
|
246
|
+
return `${n.toFixed(unit === 0 ? 0 : 1)} ${units[unit]}`;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ---------------------------------------------------------------------------
|
|
250
|
+
// Version stamp — drives automatic cleanup on upgrade.
|
|
251
|
+
//
|
|
252
|
+
// ~/.construct/.cleanup-stamp records the last version that ran cleanup.
|
|
253
|
+
// On every CLI invocation, bin/construct compares the current package
|
|
254
|
+
// version to the stamp. A mismatch triggers a single cleanup pass and
|
|
255
|
+
// writes the new version into the stamp. The first ever invocation also
|
|
256
|
+
// triggers cleanup (no stamp present) — this is what unblocks existing
|
|
257
|
+
// installs that already have 34 GB logs sitting around.
|
|
258
|
+
// ---------------------------------------------------------------------------
|
|
259
|
+
|
|
260
|
+
const STAMP_RELATIVE = path.join('.construct', '.cleanup-stamp');
|
|
261
|
+
|
|
262
|
+
export function stampPath(homeDir = os.homedir()) {
|
|
263
|
+
return path.join(homeDir, STAMP_RELATIVE);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export function readVersionStamp({ homeDir = os.homedir() } = {}) {
|
|
267
|
+
const p = stampPath(homeDir);
|
|
268
|
+
try { return JSON.parse(fs.readFileSync(p, 'utf8')); }
|
|
269
|
+
catch { return null; }
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export function writeVersionStamp({ homeDir = os.homedir(), version, summary } = {}) {
|
|
273
|
+
const p = stampPath(homeDir);
|
|
274
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
275
|
+
const payload = {
|
|
276
|
+
version,
|
|
277
|
+
ranAt: new Date().toISOString(),
|
|
278
|
+
freedBytes: summary?.freedBytes ?? 0,
|
|
279
|
+
durationMs: summary?.durationMs ?? 0,
|
|
280
|
+
};
|
|
281
|
+
fs.writeFileSync(p, JSON.stringify(payload, null, 2));
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Run cleanup if and only if the installed version differs from the stamp.
|
|
286
|
+
* Returns { ran: bool, reason, summary?, previousVersion? }.
|
|
287
|
+
*
|
|
288
|
+
* Designed to be called at CLI startup. Never throws — cleanup failures are
|
|
289
|
+
* surfaced via the returned summary and never block the actual command.
|
|
290
|
+
*/
|
|
291
|
+
export function maybeRunCleanupOnUpgrade({
|
|
292
|
+
homeDir = os.homedir(),
|
|
293
|
+
env = process.env,
|
|
294
|
+
currentVersion,
|
|
295
|
+
force = false,
|
|
296
|
+
} = {}) {
|
|
297
|
+
if (env.CONSTRUCT_DISABLE_AUTO_CLEANUP === '1') {
|
|
298
|
+
return { ran: false, reason: 'disabled via CONSTRUCT_DISABLE_AUTO_CLEANUP' };
|
|
299
|
+
}
|
|
300
|
+
const stamp = readVersionStamp({ homeDir });
|
|
301
|
+
if (!force && stamp && stamp.version === currentVersion) {
|
|
302
|
+
return { ran: false, reason: 'version matches stamp' };
|
|
303
|
+
}
|
|
304
|
+
let summary;
|
|
305
|
+
try { summary = runFullCleanup({ homeDir, env, dryRun: false }); }
|
|
306
|
+
catch (err) { return { ran: false, reason: `cleanup threw: ${err.message}` }; }
|
|
307
|
+
try { writeVersionStamp({ homeDir, version: currentVersion, summary }); }
|
|
308
|
+
catch (err) { /* stamp write failure is non-fatal */ void err; }
|
|
309
|
+
return {
|
|
310
|
+
ran: true,
|
|
311
|
+
reason: stamp ? 'version changed' : 'first run',
|
|
312
|
+
previousVersion: stamp?.version ?? null,
|
|
313
|
+
summary,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* lib/mcp/memory-bridge.mjs — stdio MCP bridge for cass-memory (cm).
|
|
4
|
+
*
|
|
5
|
+
* Editor MCP clients (OpenCode, Claude Code) expect a host to satisfy the MCP
|
|
6
|
+
* HTTP+SSE handshake — GET with `Accept: text/event-stream`, POST `initialize`,
|
|
7
|
+
* `notifications/initialized`, `ping`. cass-memory v0.2.x ships an HTTP server
|
|
8
|
+
* that answers `tools/list`, `tools/call`, `resources/list`, `resources/read`
|
|
9
|
+
* over POST but rejects the handshake calls (405 on GET, "Unsupported method"
|
|
10
|
+
* on POST initialize/ping). That single gap surfaces in editors as
|
|
11
|
+
* "memory SSE error: Non-200 status code (405)" and the memory tools never
|
|
12
|
+
* register.
|
|
13
|
+
*
|
|
14
|
+
* Sits between editor and cm. Speaks the canonical 2026 MCP stdio transport
|
|
15
|
+
* (newline-delimited JSON-RPC on stdin/stdout, logs on stderr), answers the
|
|
16
|
+
* handshake natively, and forwards real work (tools/resources) to cm over its
|
|
17
|
+
* existing HTTP API. Stays up across cm restarts; surfaces backend outages as
|
|
18
|
+
* JSON-RPC `-32603` errors rather than crashing, so the editor can retry
|
|
19
|
+
* without losing the channel.
|
|
20
|
+
*
|
|
21
|
+
* Env:
|
|
22
|
+
* CONSTRUCT_MEMORY_BRIDGE_URL cm base URL (default http://127.0.0.1:8765/)
|
|
23
|
+
* CONSTRUCT_MEMORY_BRIDGE_AUTOSTART set to 1 to spawn `cm serve` if unreachable
|
|
24
|
+
* CONSTRUCT_MEMORY_BRIDGE_TIMEOUT_MS per-request timeout (default 5000)
|
|
25
|
+
*
|
|
26
|
+
* Stdout is reserved for protocol frames. All diagnostic output goes to stderr.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { createInterface } from 'node:readline';
|
|
30
|
+
import { spawn } from 'node:child_process';
|
|
31
|
+
import { readFileSync } from 'node:fs';
|
|
32
|
+
import { dirname, join } from 'node:path';
|
|
33
|
+
import { fileURLToPath } from 'node:url';
|
|
34
|
+
|
|
35
|
+
const PROTOCOL_VERSION = '2024-11-05';
|
|
36
|
+
const SERVER_NAME = 'construct-memory-bridge';
|
|
37
|
+
|
|
38
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
39
|
+
const __dirname = dirname(__filename);
|
|
40
|
+
|
|
41
|
+
function readPackageVersion() {
|
|
42
|
+
try {
|
|
43
|
+
const pkgPath = join(__dirname, '..', '..', 'package.json');
|
|
44
|
+
return JSON.parse(readFileSync(pkgPath, 'utf8')).version || '0.0.0';
|
|
45
|
+
} catch {
|
|
46
|
+
return '0.0.0';
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const SERVER_VERSION = readPackageVersion();
|
|
51
|
+
|
|
52
|
+
function normalizeBackendUrl(raw) {
|
|
53
|
+
const value = (raw && String(raw).trim()) || 'http://127.0.0.1:8765/';
|
|
54
|
+
return value.endsWith('/') ? value : `${value}/`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const BACKEND_URL = normalizeBackendUrl(process.env.CONSTRUCT_MEMORY_BRIDGE_URL);
|
|
58
|
+
const REQUEST_TIMEOUT_MS = (() => {
|
|
59
|
+
const raw = Number(process.env.CONSTRUCT_MEMORY_BRIDGE_TIMEOUT_MS);
|
|
60
|
+
return Number.isFinite(raw) && raw > 0 ? raw : 5000;
|
|
61
|
+
})();
|
|
62
|
+
const AUTOSTART = process.env.CONSTRUCT_MEMORY_BRIDGE_AUTOSTART === '1';
|
|
63
|
+
|
|
64
|
+
const FORWARDED_METHODS = new Set([
|
|
65
|
+
'tools/list',
|
|
66
|
+
'tools/call',
|
|
67
|
+
'resources/list',
|
|
68
|
+
'resources/read',
|
|
69
|
+
'prompts/list',
|
|
70
|
+
'prompts/get',
|
|
71
|
+
]);
|
|
72
|
+
|
|
73
|
+
function log(...parts) {
|
|
74
|
+
process.stderr.write(`[memory-bridge] ${parts.join(' ')}\n`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function writeFrame(frame) {
|
|
78
|
+
process.stdout.write(`${JSON.stringify(frame)}\n`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function jsonRpcResult(id, result) {
|
|
82
|
+
return { jsonrpc: '2.0', id, result };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function jsonRpcError(id, code, message, data) {
|
|
86
|
+
const err = { code, message };
|
|
87
|
+
if (data !== undefined) err.data = data;
|
|
88
|
+
return { jsonrpc: '2.0', id, error: err };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// cm forwarding uses fetch with an AbortController so a stuck backend never
|
|
92
|
+
// strands the editor. Failed call returns a JSON-RPC -32603 error frame, not a
|
|
93
|
+
// thrown exception, so the bridge process stays available for retries.
|
|
94
|
+
|
|
95
|
+
async function forwardToBackend(method, params, id) {
|
|
96
|
+
const controller = new AbortController();
|
|
97
|
+
const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
98
|
+
try {
|
|
99
|
+
const body = { jsonrpc: '2.0', id: id ?? 0, method, params: params ?? {} };
|
|
100
|
+
const res = await fetch(BACKEND_URL, {
|
|
101
|
+
method: 'POST',
|
|
102
|
+
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
|
103
|
+
body: JSON.stringify(body),
|
|
104
|
+
signal: controller.signal,
|
|
105
|
+
});
|
|
106
|
+
if (!res.ok) {
|
|
107
|
+
return jsonRpcError(
|
|
108
|
+
id,
|
|
109
|
+
-32603,
|
|
110
|
+
`memory backend returned HTTP ${res.status} for ${method}`,
|
|
111
|
+
{ backend: BACKEND_URL, status: res.status },
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
const text = await res.text();
|
|
115
|
+
let parsed;
|
|
116
|
+
try {
|
|
117
|
+
parsed = JSON.parse(text);
|
|
118
|
+
} catch {
|
|
119
|
+
return jsonRpcError(id, -32603, `memory backend returned non-JSON for ${method}`, { backend: BACKEND_URL });
|
|
120
|
+
}
|
|
121
|
+
if (parsed && Object.prototype.hasOwnProperty.call(parsed, 'error')) {
|
|
122
|
+
return { jsonrpc: '2.0', id, error: parsed.error };
|
|
123
|
+
}
|
|
124
|
+
return jsonRpcResult(id, parsed && parsed.result !== undefined ? parsed.result : parsed);
|
|
125
|
+
} catch (err) {
|
|
126
|
+
const reason = err?.name === 'AbortError' ? 'timeout' : (err?.message || String(err));
|
|
127
|
+
return jsonRpcError(id, -32603, `memory backend unreachable at ${BACKEND_URL} (${reason})`, { backend: BACKEND_URL });
|
|
128
|
+
} finally {
|
|
129
|
+
clearTimeout(timer);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Autostart only fires when CONSTRUCT_MEMORY_BRIDGE_AUTOSTART=1. Detached
|
|
134
|
+
// spawn keeps cm alive after the editor disconnects.
|
|
135
|
+
|
|
136
|
+
let autostartAttempted = false;
|
|
137
|
+
|
|
138
|
+
function maybeAutostartCm() {
|
|
139
|
+
if (!AUTOSTART || autostartAttempted) return;
|
|
140
|
+
autostartAttempted = true;
|
|
141
|
+
try {
|
|
142
|
+
const port = (() => {
|
|
143
|
+
try {
|
|
144
|
+
const u = new URL(BACKEND_URL);
|
|
145
|
+
return u.port || '8765';
|
|
146
|
+
} catch {
|
|
147
|
+
return '8765';
|
|
148
|
+
}
|
|
149
|
+
})();
|
|
150
|
+
const child = spawn('cm', ['serve', '--port', String(port)], {
|
|
151
|
+
detached: true,
|
|
152
|
+
stdio: 'ignore',
|
|
153
|
+
});
|
|
154
|
+
child.unref();
|
|
155
|
+
log(`autostart: spawned cm serve --port ${port}`);
|
|
156
|
+
} catch (err) {
|
|
157
|
+
log(`autostart failed: ${err?.message || err}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Handshake methods resolve natively. cm cannot answer them; forwarding would
|
|
162
|
+
// return the same "Unsupported method" surface that triggered this bug.
|
|
163
|
+
|
|
164
|
+
function handleInitialize(id) {
|
|
165
|
+
return jsonRpcResult(id, {
|
|
166
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
167
|
+
capabilities: { tools: {}, resources: {} },
|
|
168
|
+
serverInfo: { name: SERVER_NAME, version: SERVER_VERSION },
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function handlePing(id) {
|
|
173
|
+
return jsonRpcResult(id, {});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function handleShutdown(id) {
|
|
177
|
+
try { process.stderr.write(''); } catch { /* stderr already closed */ }
|
|
178
|
+
if (id !== undefined && id !== null) writeFrame(jsonRpcResult(id, null));
|
|
179
|
+
setImmediate(() => process.exit(0));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function dispatch(message) {
|
|
183
|
+
if (!message || typeof message !== 'object') return null;
|
|
184
|
+
if (message.jsonrpc !== '2.0') return null;
|
|
185
|
+
|
|
186
|
+
const { id, method, params } = message;
|
|
187
|
+
const isNotification = id === undefined || id === null;
|
|
188
|
+
|
|
189
|
+
if (method === 'notifications/initialized' || method === 'notifications/cancelled') {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
if (method === 'initialize') return handleInitialize(id);
|
|
193
|
+
if (method === 'ping') return handlePing(id);
|
|
194
|
+
if (method === 'shutdown') return handleShutdown(id);
|
|
195
|
+
|
|
196
|
+
if (FORWARDED_METHODS.has(method)) {
|
|
197
|
+
maybeAutostartCm();
|
|
198
|
+
return forwardToBackend(method, params, id);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (isNotification) return null;
|
|
202
|
+
return jsonRpcError(id, -32601, `Method not found: ${method}`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async function handleLine(line) {
|
|
206
|
+
const trimmed = line.trim();
|
|
207
|
+
if (!trimmed) return;
|
|
208
|
+
let message;
|
|
209
|
+
try {
|
|
210
|
+
message = JSON.parse(trimmed);
|
|
211
|
+
} catch (err) {
|
|
212
|
+
writeFrame(jsonRpcError(null, -32700, `Parse error: ${err?.message || err}`));
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
try {
|
|
216
|
+
const response = await dispatch(message);
|
|
217
|
+
if (response) writeFrame(response);
|
|
218
|
+
} catch (err) {
|
|
219
|
+
if (message && message.id !== undefined && message.id !== null) {
|
|
220
|
+
writeFrame(jsonRpcError(message.id, -32603, `Internal bridge error: ${err?.message || err}`));
|
|
221
|
+
} else {
|
|
222
|
+
log(`unhandled error on notification: ${err?.message || err}`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function startBridge({ input = process.stdin, output = process.stdout } = {}) {
|
|
228
|
+
if (output !== process.stdout) {
|
|
229
|
+
const writer = (frame) => output.write(`${JSON.stringify(frame)}\n`);
|
|
230
|
+
return runLoop(input, writer);
|
|
231
|
+
}
|
|
232
|
+
return runLoop(input, (frame) => process.stdout.write(`${JSON.stringify(frame)}\n`));
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function runLoop(input, writer) {
|
|
236
|
+
const rl = createInterface({ input, crlfDelay: Infinity });
|
|
237
|
+
rl.on('line', async (line) => {
|
|
238
|
+
const trimmed = line.trim();
|
|
239
|
+
if (!trimmed) return;
|
|
240
|
+
let message;
|
|
241
|
+
try {
|
|
242
|
+
message = JSON.parse(trimmed);
|
|
243
|
+
} catch (err) {
|
|
244
|
+
writer(jsonRpcError(null, -32700, `Parse error: ${err?.message || err}`));
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
try {
|
|
248
|
+
const response = await dispatch(message);
|
|
249
|
+
if (response) writer(response);
|
|
250
|
+
} catch (err) {
|
|
251
|
+
if (message && message.id !== undefined && message.id !== null) {
|
|
252
|
+
writer(jsonRpcError(message.id, -32603, `Internal bridge error: ${err?.message || err}`));
|
|
253
|
+
} else {
|
|
254
|
+
log(`unhandled error on notification: ${err?.message || err}`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
rl.on('close', () => process.exit(0));
|
|
259
|
+
return rl;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const isMain = (() => {
|
|
263
|
+
try { return import.meta.url === `file://${process.argv[1]}`; } catch { return false; }
|
|
264
|
+
})();
|
|
265
|
+
|
|
266
|
+
if (isMain) {
|
|
267
|
+
log(`starting (backend=${BACKEND_URL}, autostart=${AUTOSTART ? 'on' : 'off'})`);
|
|
268
|
+
startBridge();
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export const _testing = {
|
|
272
|
+
dispatch,
|
|
273
|
+
handleLine,
|
|
274
|
+
BACKEND_URL,
|
|
275
|
+
SERVER_VERSION,
|
|
276
|
+
};
|
package/lib/mcp/server.mjs
CHANGED
|
@@ -14,6 +14,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
14
14
|
import { realpathSync } from 'node:fs';
|
|
15
15
|
import { loadToolkitEnv } from '../toolkit-env.mjs';
|
|
16
16
|
import { loadConstructEnv } from '../env-config.mjs';
|
|
17
|
+
import { withGenAiSpan, GenAiAttrs, extractTraceContext, injectTraceContext } from '../telemetry/otel-tracer.mjs';
|
|
17
18
|
|
|
18
19
|
// Apply config.env values to process.env, letting config.env win over shell env
|
|
19
20
|
// so telemetry/OpenRouter credentials are always correct regardless of host env.
|
|
@@ -40,6 +41,7 @@ import {
|
|
|
40
41
|
import {
|
|
41
42
|
workflowInit, workflowAddTask, workflowUpdateTask,
|
|
42
43
|
workflowNeedsMainInput, workflowValidate, workflowImportPlan,
|
|
44
|
+
workflowContractValidate,
|
|
43
45
|
} from './tools/workflow.mjs';
|
|
44
46
|
import {
|
|
45
47
|
cxTrace, cxTraceUpdate, cxScore, sessionUsage, efficiencySnapshot,
|
|
@@ -414,11 +416,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
414
416
|
},
|
|
415
417
|
{
|
|
416
418
|
name: 'broker_check',
|
|
417
|
-
description: 'Queries the MCP broker policy gate for a pending action without executing it. Agents call this BEFORE attempting a high-risk action so the response (allowed / approvalRequired / reason / source) can be surfaced to the user in the agent\'s voice rather than triggering a denial after the fact. In solo mode the broker is off by default — returns `brokerActive: false` with `allowed: true` so the call is cheap and agents don\'t waste tokens on an inactive gate. Always emits a `tool.called` trace event for audit-trail parity. Reads
|
|
419
|
+
description: 'Queries the MCP broker policy gate for a pending action without executing it. Agents call this BEFORE attempting a high-risk action so the response (allowed / approvalRequired / reason / source) can be surfaced to the user in the agent\'s voice rather than triggering a denial after the fact. In solo mode the broker is off by default — returns `brokerActive: false` with `allowed: true` so the call is cheap and agents don\'t waste tokens on an inactive gate. Always emits a `tool.called` trace event for audit-trail parity. Reads specialists/role-manifests.json fence rules in team / enterprise mode.',
|
|
418
420
|
inputSchema: {
|
|
419
421
|
type: 'object',
|
|
420
422
|
properties: {
|
|
421
|
-
role: { type: 'string', description: 'Persona name (e.g. `engineer`, `security`). Must match a key in
|
|
423
|
+
role: { type: 'string', description: 'Persona name (e.g. `engineer`, `security`). Must match a key in specialists/role-manifests.json for team / enterprise mode.' },
|
|
422
424
|
project: { type: 'string', description: 'Optional project scope for the decision.' },
|
|
423
425
|
tool: { type: 'string', description: 'The tool the agent wants to invoke (e.g. `github`, `fs`).' },
|
|
424
426
|
action: { type: 'string', description: 'The action on that tool (e.g. `create_pr`, `edit:lib/foo.mjs`).' },
|
|
@@ -430,7 +432,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
430
432
|
},
|
|
431
433
|
{
|
|
432
434
|
name: 'agent_contract',
|
|
433
|
-
description: 'Looks up explicit agent-to-agent service contracts (from
|
|
435
|
+
description: 'Looks up explicit agent-to-agent service contracts (from specialists/contracts.json). Specialists should call this at the start of a handoff to see the expected input shape, preconditions, and what postconditions they must satisfy. Use without args to get all contracts; pass producer/consumer to narrow; pass id for a specific contract.',
|
|
434
436
|
inputSchema: {
|
|
435
437
|
type: 'object',
|
|
436
438
|
properties: {
|
|
@@ -874,6 +876,8 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
874
876
|
cwd: { type: 'string', description: 'Project root (default: server cwd).' },
|
|
875
877
|
top_k: { type: 'number', description: 'Max communities to return (default 5).' },
|
|
876
878
|
min_size: { type: 'number', description: 'Skip communities smaller than this (default 2).' },
|
|
879
|
+
tags: { type: 'array', items: { type: 'string' }, description: 'Restrict entity extraction to observations carrying these tags.' },
|
|
880
|
+
tag_match: { type: 'string', enum: ['any', 'all'], description: 'Tag match mode: any (default) or all.' },
|
|
877
881
|
},
|
|
878
882
|
},
|
|
879
883
|
},
|
|
@@ -905,6 +909,9 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
905
909
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
906
910
|
const { name, arguments: args = {} } = request.params;
|
|
907
911
|
|
|
912
|
+
// Extract W3C traceparent from params._meta (SEP-414 propagation).
|
|
913
|
+
const parentCtx = await extractTraceContext(request.params?._meta || {});
|
|
914
|
+
|
|
908
915
|
let result;
|
|
909
916
|
try {
|
|
910
917
|
if (name === 'agent_health') result = agentHealth(args);
|
|
@@ -934,6 +941,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
934
941
|
else if (name === 'workflow_update_task') result = workflowUpdateTask(args);
|
|
935
942
|
else if (name === 'workflow_needs_main_input') result = workflowNeedsMainInput(args);
|
|
936
943
|
else if (name === 'workflow_validate') result = workflowValidate(args);
|
|
944
|
+
else if (name === 'workflow_contract_validate') result = await workflowContractValidate(args);
|
|
937
945
|
else if (name === 'workflow_import_plan') result = workflowImportPlan(args);
|
|
938
946
|
else if (name === 'list_teams') result = listTeams(opts);
|
|
939
947
|
else if (name === 'get_team') result = getTeam(args, opts);
|