@productbrain/mcp 0.0.1-beta.156 → 0.0.1-beta.158

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.
@@ -257,6 +257,86 @@ function trackFieldQualityWarning(workspaceId, props) {
257
257
  } catch {
258
258
  }
259
259
  }
260
+ function trackSessionCaptureRate(workspaceId, props) {
261
+ if (!client) return;
262
+ try {
263
+ client.capture({
264
+ distinctId,
265
+ event: "session_capture_rate",
266
+ properties: {
267
+ ...props,
268
+ workspace_id: workspaceId,
269
+ source_system: "mcp-server",
270
+ $groups: { workspace: workspaceId }
271
+ }
272
+ });
273
+ } catch {
274
+ }
275
+ }
276
+ function trackZeroCaptureAuditFired(workspaceId, props) {
277
+ if (!client) return;
278
+ try {
279
+ client.capture({
280
+ distinctId,
281
+ event: "zero_capture_audit_fired",
282
+ properties: {
283
+ ...props,
284
+ workspace_id: workspaceId,
285
+ source_system: "mcp-server",
286
+ $groups: { workspace: workspaceId }
287
+ }
288
+ });
289
+ } catch {
290
+ }
291
+ }
292
+ function trackCaptureContractMiss(workspaceId, props) {
293
+ if (!client) return;
294
+ try {
295
+ client.capture({
296
+ distinctId,
297
+ event: "capture_contract_miss",
298
+ properties: {
299
+ ...props,
300
+ workspace_id: workspaceId,
301
+ source_system: "mcp-server",
302
+ $groups: { workspace: workspaceId }
303
+ }
304
+ });
305
+ } catch {
306
+ }
307
+ }
308
+ function trackWriteBackHintServed(workspaceId, props) {
309
+ if (!client) return;
310
+ try {
311
+ client.capture({
312
+ distinctId,
313
+ event: "write_back_hint_served",
314
+ properties: {
315
+ ...props,
316
+ workspace_id: workspaceId,
317
+ source_system: "mcp-server",
318
+ $groups: { workspace: workspaceId }
319
+ }
320
+ });
321
+ } catch {
322
+ }
323
+ }
324
+ function trackCommitErrorByCode(workspaceId, props) {
325
+ if (!client) return;
326
+ try {
327
+ client.capture({
328
+ distinctId,
329
+ event: "commit_error_by_code",
330
+ properties: {
331
+ ...props,
332
+ workspace_id: workspaceId,
333
+ source_system: "mcp-server",
334
+ $groups: { workspace: workspaceId }
335
+ }
336
+ });
337
+ } catch {
338
+ }
339
+ }
260
340
  function getPostHogClient() {
261
341
  return client;
262
342
  }
@@ -354,10 +434,15 @@ export {
354
434
  trackCollectionClassified,
355
435
  trackFieldGuidanceApplied,
356
436
  trackFieldQualityWarning,
437
+ trackSessionCaptureRate,
438
+ trackZeroCaptureAuditFired,
439
+ trackCaptureContractMiss,
440
+ trackWriteBackHintServed,
441
+ trackCommitErrorByCode,
357
442
  getPostHogClient,
358
443
  shutdownAnalytics,
359
444
  MCP_NPX_PACKAGE,
360
445
  resolveClient,
361
446
  writeClientConfig
362
447
  };
363
- //# sourceMappingURL=chunk-X3S5UTTZ.js.map
448
+ //# sourceMappingURL=chunk-KGGSEIZ3.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/analytics.ts","../src/cli/config-writer.ts"],"sourcesContent":["/**\n * PostHog analytics for SynergyOS maintainers — tracks MCP usage (sessions, tool calls).\n * Not user-facing. Key is injected at build time via SYNERGYOS_POSTHOG_KEY.\n * Override with POSTHOG_MCP_KEY for self-hosted deployments.\n */\n\nimport { userInfo } from \"node:os\";\nimport { PostHog } from \"posthog-node\";\n\nlet client: PostHog | null = null;\nlet distinctId = \"anonymous\";\n\nconst POSTHOG_HOST = \"https://eu.i.posthog.com\";\n\n/** Injected at build time: SYNERGYOS_POSTHOG_KEY env when running `npm run build`/publish. */\ndeclare const __SYNERGYOS_POSTHOG_KEY__: string;\n\n/** Only write to stderr when MCP_DEBUG=1 for quieter default DX. */\nfunction log(msg: string): void {\n if (process.env.MCP_DEBUG === \"1\") {\n process.stderr.write(msg);\n }\n}\n\nfunction getBuildTimeKey(): string {\n try {\n return __SYNERGYOS_POSTHOG_KEY__;\n } catch {\n // Not replaced by bundler (e.g. running via tsx in tests) — treat as absent.\n return \"\";\n }\n}\n\nexport function initAnalytics(): void {\n const apiKey = process.env.POSTHOG_MCP_KEY || getBuildTimeKey();\n if (!apiKey) {\n log(\"[MCP-ANALYTICS] No PostHog key — tracking disabled (set SYNERGYOS_POSTHOG_KEY at build time for publish)\\n\");\n return;\n }\n\n client = new PostHog(apiKey, {\n host: POSTHOG_HOST,\n flushAt: 1,\n flushInterval: 5000,\n featureFlagsPollingInterval: 30_000,\n });\n distinctId = process.env.MCP_USER_ID || fallbackDistinctId();\n\n log(`[MCP-ANALYTICS] Initialized — host=${POSTHOG_HOST} distinctId=${distinctId}\\n`);\n}\n\nfunction fallbackDistinctId(): string {\n try {\n return userInfo().username;\n } catch {\n return `os-${process.pid}`;\n }\n}\n\nexport function trackSessionStarted(\n workspaceId: string,\n serverVersion: string,\n): void {\n if (!client) return;\n client.capture({\n distinctId,\n event: \"mcp_session_started\",\n properties: {\n workspace_id: workspaceId,\n server_version: serverVersion,\n source: \"mcp-server\",\n $groups: { workspace: workspaceId },\n },\n });\n}\n\nexport function trackToolCall(\n fn: string,\n status: \"ok\" | \"error\",\n durationMs: number,\n workspaceId: string,\n errorMsg?: string,\n): void {\n const properties: Record<string, unknown> = {\n tool: fn,\n status,\n duration_ms: durationMs,\n workspace_id: workspaceId,\n source: \"mcp-server\",\n $groups: { workspace: workspaceId },\n };\n if (errorMsg) properties.error = errorMsg;\n\n if (!client) return;\n client.capture({\n distinctId,\n event: \"mcp_tool_called\",\n properties,\n });\n}\n\nexport function trackSetupStarted(): void {\n if (!client) return;\n client.capture({\n distinctId,\n event: \"mcp_setup_started\",\n properties: {\n source: \"mcp-server\",\n platform: process.platform,\n },\n });\n}\n\nexport function trackSetupCompleted(\n chosenClient: string,\n outcome: \"config_written\" | \"config_existed\" | \"snippet_shown\" | \"write_error\",\n): void {\n if (!client) return;\n client.capture({\n distinctId,\n event: \"mcp_setup_completed\",\n properties: {\n client: chosenClient,\n outcome,\n source: \"mcp-server\",\n platform: process.platform,\n },\n });\n}\n\nexport function trackQualityVerdict(\n workspaceId: string,\n props: {\n entry_id: string;\n entry_type: string;\n tier: string;\n context: string;\n passed: boolean;\n source: string;\n criteria_total: number;\n criteria_failed: number;\n llm_scheduled: boolean;\n },\n): void {\n if (!client) return;\n client.capture({\n distinctId,\n event: \"quality_verdict_generated\",\n properties: {\n ...props,\n workspace_id: workspaceId,\n source_system: \"mcp-server\",\n $groups: { workspace: workspaceId },\n },\n });\n}\n\nexport function trackQualityCheck(\n workspaceId: string,\n props: {\n entry_id: string;\n entry_type: string;\n tier: string;\n passed: boolean;\n source: string;\n llm_status?: string;\n llm_duration_ms?: number;\n llm_error?: string;\n has_roger_martin: boolean;\n },\n): void {\n if (!client) return;\n client.capture({\n distinctId,\n event: \"quality_verdict_checked\",\n properties: {\n ...props,\n workspace_id: workspaceId,\n source_system: \"mcp-server\",\n $groups: { workspace: workspaceId },\n },\n });\n}\n\nexport type ClassifierReasonCategory =\n | \"auto-routed\"\n | \"low-confidence\"\n | \"ambiguous\"\n | \"non-provisioned\";\n\ntype CaptureClassifierTelemetryProps = {\n predicted_collection: string;\n confidence: number;\n auto_routed: boolean;\n reason_category: ClassifierReasonCategory;\n explicit_collection_provided: boolean;\n};\n\nfunction trackCaptureClassifierEvent(\n event: \"mcp_capture_classifier_evaluated\" | \"mcp_capture_classifier_auto_routed\" | \"mcp_capture_classifier_fallback\",\n workspaceId: string,\n props: CaptureClassifierTelemetryProps,\n): void {\n if (!client) return;\n try {\n client.capture({\n distinctId,\n event,\n properties: {\n ...props,\n workspace_id: workspaceId,\n source_system: \"mcp-server\",\n $groups: { workspace: workspaceId },\n },\n });\n } catch {\n // Analytics are advisory and must never break capture flow.\n }\n}\n\nexport function trackCaptureClassifierEvaluated(\n workspaceId: string,\n props: CaptureClassifierTelemetryProps,\n): void {\n trackCaptureClassifierEvent(\"mcp_capture_classifier_evaluated\", workspaceId, props);\n}\n\nexport function trackCaptureClassifierAutoRouted(\n workspaceId: string,\n props: CaptureClassifierTelemetryProps,\n): void {\n trackCaptureClassifierEvent(\"mcp_capture_classifier_auto_routed\", workspaceId, props);\n}\n\nexport function trackCaptureClassifierFallback(\n workspaceId: string,\n props: CaptureClassifierTelemetryProps,\n): void {\n trackCaptureClassifierEvent(\"mcp_capture_classifier_fallback\", workspaceId, props);\n}\n\n/** GLO-26 / TEN-156: every SSOT commit for PostHog funnels (split auto vs manual). */\nexport function trackChainEntryCommitted(\n workspaceId: string,\n props: {\n entry_id: string;\n collection?: string;\n commit_method: \"auto\" | \"manual\";\n surface:\n | \"mcp_commit_tool\"\n | \"mcp_capture\"\n | \"mcp_wrapup\";\n },\n): void {\n if (!client) return;\n try {\n client.capture({\n distinctId,\n event: \"chain_entry_committed\",\n properties: {\n workspace_id: workspaceId,\n source_system: \"mcp-server\",\n $groups: { workspace: workspaceId },\n ...props,\n },\n });\n } catch {\n // Analytics must never break the tool path.\n }\n}\n\nexport function trackKnowledgeGap(\n workspaceId: string,\n props: {\n query: string;\n tool: string;\n action: string;\n gap_type: \"search_zero\" | \"context_task_empty\" | \"context_entry_isolated\" | \"context_graph_empty\";\n collection_scope?: string;\n },\n): void {\n if (!client) return;\n try {\n client.capture({\n distinctId,\n event: \"knowledge_gap_detected\",\n properties: {\n ...props,\n query: props.query.slice(0, 200),\n workspace_id: workspaceId,\n source_system: \"mcp-server\",\n $groups: { workspace: workspaceId },\n },\n });\n } catch {\n // Analytics must never break the tool response path.\n }\n}\n\n// ── BET-272 S6 / STD-155: Capture intelligence observability ────────────────\n\n/** Fires when formative quality hints are returned at capture time. */\nexport function trackCaptureQualityHints(\n workspaceId: string,\n props: {\n collection: string;\n hint_count: number;\n hint_fields: string[];\n },\n): void {\n if (!client) return;\n try {\n client.capture({\n distinctId,\n event: \"mcp_capture_quality_hints\",\n properties: {\n ...props,\n workspace_id: workspaceId,\n source_system: \"mcp-server\",\n $groups: { workspace: workspaceId },\n },\n });\n } catch {\n // Analytics must never break capture flow.\n }\n}\n\n/** Fires when relation suggestions are returned at capture time. */\nexport function trackCaptureRelationSuggestions(\n workspaceId: string,\n props: {\n collection: string;\n suggestion_count: number;\n relation_types: string[];\n avg_confidence: number;\n },\n): void {\n if (!client) return;\n try {\n client.capture({\n distinctId,\n event: \"mcp_capture_relation_suggestions\",\n properties: {\n ...props,\n workspace_id: workspaceId,\n source_system: \"mcp-server\",\n $groups: { workspace: workspaceId },\n },\n });\n } catch {\n // Analytics must never break capture flow.\n }\n}\n\n// ── BET-288 S0: Collection classification confusion matrix telemetry ────────\n\n/**\n * Fires on every collection classification (auto-routed, fallback, explicit-provided).\n * Captures the full confusion signal: predicted collection, runner-up, thinkingLayer,\n * and confidence scores. This is the baseline measurement for BET-288 algorithm changes.\n */\nexport function trackCollectionClassified(\n workspaceId: string,\n props: {\n collection_slug: string;\n thinking_layer: string | null;\n confidence: number;\n classified_by: \"llm\" | \"heuristic\";\n confidence_tier: \"high\" | \"medium\" | \"low\";\n alternative_slug: string | null;\n alternative_confidence: number | null;\n explicit_collection_provided: boolean;\n auto_routed: boolean;\n },\n): void {\n if (!client) return;\n try {\n client.capture({\n distinctId,\n event: \"collection_classified\",\n properties: {\n ...props,\n workspace_id: workspaceId,\n source_system: \"mcp-server\",\n $groups: { workspace: workspaceId },\n },\n });\n } catch {\n // Analytics must never break the capture flow.\n }\n}\n\n// ── BET-289 S5: Field-level writing guidance telemetry ────────────────────────\n\n/** Fires when field guidance is injected into a capture prompt or response. */\nexport function trackFieldGuidanceApplied(\n workspaceId: string,\n props: {\n collection: string;\n guided_field_count: number;\n },\n): void {\n if (!client) return;\n try {\n client.capture({\n distinctId,\n event: \"field_guidance_applied\",\n properties: {\n ...props,\n workspace_id: workspaceId,\n source_system: \"mcp-server\",\n $groups: { workspace: workspaceId },\n },\n });\n } catch {\n // Analytics must never break the capture flow.\n }\n}\n\n/** Fires when commit-time heuristic detects field guidance violations. */\nexport function trackFieldQualityWarning(\n workspaceId: string,\n props: {\n warning_count: number;\n warning_types: string[];\n },\n): void {\n if (!client) return;\n try {\n client.capture({\n distinctId,\n event: \"field_quality_warning\",\n properties: {\n ...props,\n workspace_id: workspaceId,\n source_system: \"mcp-server\",\n $groups: { workspace: workspaceId },\n },\n });\n } catch {\n // Analytics must never break the commit flow.\n }\n}\n\n/** Fires when skipGuidanceCheck is used to bypass guidance validation. */\nexport function trackFieldQualityOverride(\n workspaceId: string,\n): void {\n if (!client) return;\n try {\n client.capture({\n distinctId,\n event: \"field_quality_override\",\n properties: {\n workspace_id: workspaceId,\n source_system: \"mcp-server\",\n $groups: { workspace: workspaceId },\n },\n });\n } catch {\n // Analytics must never break the commit flow.\n }\n}\n\n// ── WP-306 S3: Write-Back Measurement (capture rate insights) ──────────────\n\n/** Fires on session close to track session capture rate. */\nexport function trackSessionCaptureRate(\n workspaceId: string,\n props: {\n entries_created: number;\n entries_modified: number;\n relations_created: number;\n had_captures: boolean;\n },\n): void {\n if (!client) return;\n try {\n client.capture({\n distinctId,\n event: \"session_capture_rate\",\n properties: {\n ...props,\n workspace_id: workspaceId,\n source_system: \"mcp-server\",\n $groups: { workspace: workspaceId },\n },\n });\n } catch {\n // Analytics must never break session closure.\n }\n}\n\n/** Fires when captureAudit is triggered (activity but no captures). */\nexport function trackZeroCaptureAuditFired(\n workspaceId: string,\n props: {\n suggestion_count: number;\n },\n): void {\n if (!client) return;\n try {\n client.capture({\n distinctId,\n event: \"zero_capture_audit_fired\",\n properties: {\n ...props,\n workspace_id: workspaceId,\n source_system: \"mcp-server\",\n $groups: { workspace: workspaceId },\n },\n });\n } catch {\n // Analytics must never break wrapup flow.\n }\n}\n\n// ── WP-316 S1b: Capture contract observability ──────────────────────────────\n\n/** Fires when a capture-contract resource request references an unknown collection slug. */\nexport function trackCaptureContractMiss(\n workspaceId: string,\n props: {\n slug: string;\n },\n): void {\n if (!client) return;\n try {\n client.capture({\n distinctId,\n event: \"capture_contract_miss\",\n properties: {\n ...props,\n workspace_id: workspaceId,\n source_system: \"mcp-server\",\n $groups: { workspace: workspaceId },\n },\n });\n } catch {\n // Analytics must never break the resource response path.\n }\n}\n\n/** Fires when writeBackHints are included in an orient response. */\nexport function trackWriteBackHintServed(\n workspaceId: string,\n props: {\n hint_count: number;\n has_task: boolean;\n },\n): void {\n if (!client) return;\n try {\n client.capture({\n distinctId,\n event: \"write_back_hint_served\",\n properties: {\n ...props,\n workspace_id: workspaceId,\n source_system: \"mcp-server\",\n $groups: { workspace: workspaceId },\n },\n });\n } catch {\n // Analytics must never break orient flow.\n }\n}\n\n/**\n * WP-316 S1a: Fires when commit-entry fails with a structured error code.\n * Enables observability on which validation errors block commits most often.\n */\nexport function trackCommitErrorByCode(\n workspaceId: string,\n props: {\n error_code: string;\n missing_field_count: number;\n field_error_count: number;\n entry_id: string;\n },\n): void {\n if (!client) return;\n try {\n client.capture({\n distinctId,\n event: \"commit_error_by_code\",\n properties: {\n ...props,\n workspace_id: workspaceId,\n source_system: \"mcp-server\",\n $groups: { workspace: workspaceId },\n },\n });\n } catch {\n // Analytics must never break the commit error path.\n }\n}\n\nexport function getPostHogClient(): PostHog | null {\n return client;\n}\n\nexport async function shutdownAnalytics(): Promise<void> {\n await client?.shutdown();\n}\n","/**\n * Multi-client MCP config detection and writer.\n *\n * Supports:\n * - Cursor: .cursor/mcp.json in cwd (project-level)\n * - Claude Desktop: ~/Library/Application Support/Claude/claude_desktop_config.json (macOS)\n * %APPDATA%/Claude/claude_desktop_config.json (Windows)\n *\n * The writer reads existing config, merges the new server entry (never\n * overwrites existing entries), and writes back. Falls back to printing\n * a snippet for unsupported OS or unknown formats.\n */\n\nimport { existsSync, readFileSync, writeFileSync, mkdirSync } from \"node:fs\";\nimport { join, dirname } from \"node:path\";\nimport { homedir, platform } from \"node:os\";\n\nexport interface McpClientInfo {\n name: string;\n configPath: string;\n}\n\nconst SERVER_ENTRY_KEY = \"Product Brain\";\nconst LEGACY_ENTRY_KEY = \"productbrain\";\n\n/**\n * Canonical npx package specifier. Update here when exiting beta.\n * Frontend mirror: src/lib/constants/mcp.ts\n * Business rule: BR-84 (Chain)\n */\nexport const MCP_NPX_PACKAGE = \"@productbrain/mcp@beta\";\n\nfunction buildServerEntry(apiKey: string) {\n return {\n command: \"npx\",\n args: [\"-y\", MCP_NPX_PACKAGE],\n env: { PRODUCTBRAIN_API_KEY: apiKey },\n };\n}\n\n// ── Detection ───────────────────────────────────────────────────────────\n\nfunction getCursorConfigPath(): string {\n return join(process.cwd(), \".cursor\", \"mcp.json\");\n}\n\nfunction getClaudeDesktopConfigPath(): string | null {\n const os = platform();\n if (os === \"darwin\") {\n return join(\n homedir(),\n \"Library\",\n \"Application Support\",\n \"Claude\",\n \"claude_desktop_config.json\",\n );\n }\n if (os === \"win32\") {\n const appData = process.env.APPDATA ?? join(homedir(), \"AppData\", \"Roaming\");\n return join(appData, \"Claude\", \"claude_desktop_config.json\");\n }\n // Linux: no official Claude Desktop location yet\n return null;\n}\n\nexport function resolveClient(name: \"Cursor\" | \"Claude Desktop\"): McpClientInfo | null {\n if (name === \"Cursor\") {\n return { name, configPath: getCursorConfigPath() };\n }\n const configPath = getClaudeDesktopConfigPath();\n return configPath ? { name, configPath } : null;\n}\n\n// ── Writing ─────────────────────────────────────────────────────────────\n\nfunction readJsonSafe(path: string): Record<string, any> {\n if (!existsSync(path)) return {};\n try {\n return JSON.parse(readFileSync(path, \"utf-8\"));\n } catch {\n return {};\n }\n}\n\n/**\n * Write or merge the Product Brain server entry into a client config file.\n * Migrates legacy \"productbrain\" key to \"Product Brain\" when present.\n * Returns true if the config was written, false if already present.\n */\nexport async function writeClientConfig(\n client: McpClientInfo,\n apiKey: string,\n): Promise<boolean> {\n const config = readJsonSafe(client.configPath);\n\n const serversKey = \"mcpServers\";\n if (!config[serversKey]) config[serversKey] = {};\n\n // Migrate legacy \"productbrain\" key or update existing Product Brain with new API key\n if (config[serversKey][LEGACY_ENTRY_KEY]) {\n const legacy = config[serversKey][LEGACY_ENTRY_KEY];\n config[serversKey][SERVER_ENTRY_KEY] = {\n ...buildServerEntry(apiKey),\n env: { ...legacy.env, PRODUCTBRAIN_API_KEY: legacy.env?.PRODUCTBRAIN_API_KEY ?? apiKey },\n };\n delete config[serversKey][LEGACY_ENTRY_KEY];\n } else {\n const existing = config[serversKey][SERVER_ENTRY_KEY];\n config[serversKey][SERVER_ENTRY_KEY] = existing\n ? { ...existing, env: { ...existing.env, PRODUCTBRAIN_API_KEY: apiKey } }\n : buildServerEntry(apiKey);\n }\n\n const dir = dirname(client.configPath);\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n\n writeFileSync(client.configPath, JSON.stringify(config, null, 2) + \"\\n\", \"utf-8\");\n return true;\n}\n"],"mappings":"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","names":["client"]}
@@ -3,17 +3,22 @@ import {
3
3
  trackCaptureClassifierAutoRouted,
4
4
  trackCaptureClassifierEvaluated,
5
5
  trackCaptureClassifierFallback,
6
+ trackCaptureContractMiss,
6
7
  trackCaptureQualityHints,
7
8
  trackCaptureRelationSuggestions,
8
9
  trackChainEntryCommitted,
9
10
  trackCollectionClassified,
11
+ trackCommitErrorByCode,
10
12
  trackFieldGuidanceApplied,
11
13
  trackFieldQualityWarning,
12
14
  trackKnowledgeGap,
13
15
  trackQualityCheck,
14
16
  trackQualityVerdict,
15
- trackToolCall
16
- } from "./chunk-X3S5UTTZ.js";
17
+ trackSessionCaptureRate,
18
+ trackToolCall,
19
+ trackWriteBackHintServed,
20
+ trackZeroCaptureAuditFired
21
+ } from "./chunk-KGGSEIZ3.js";
17
22
 
18
23
  // src/auth.ts
19
24
  import { AsyncLocalStorage } from "async_hooks";
@@ -83,11 +88,17 @@ var DEFAULT_CLOUD_URL = "https://gateway.productbrain.io";
83
88
  var McpCallError = class extends Error {
84
89
  status;
85
90
  code;
86
- constructor(message, status, code) {
91
+ /** WP-316 S1a: Structured commit validation — required field keys missing from entry.data. */
92
+ missingRequiredFields;
93
+ /** WP-316 S1a: Structured commit validation — field-level data errors. */
94
+ fieldErrors;
95
+ constructor(message, status, code, missingRequiredFields, fieldErrors) {
87
96
  super(message);
88
97
  this.name = "McpCallError";
89
98
  this.status = status;
90
99
  this.code = code;
100
+ this.missingRequiredFields = missingRequiredFields;
101
+ this.fieldErrors = fieldErrors;
91
102
  }
92
103
  };
93
104
  var CACHE_TTL_MS = 6e4;
@@ -324,7 +335,13 @@ async function mcpCall(fn, args = {}) {
324
335
  if (!res.ok || json.error) {
325
336
  const msg = json.error ?? "unknown error";
326
337
  audit(fn, "error", Date.now() - start, json.code ? `${msg} [${json.code}]` : msg);
327
- throw new McpCallError(`MCP call "${fn}" failed (${res.status}): ${msg}`, res.status, json.code);
338
+ throw new McpCallError(
339
+ `MCP call "${fn}" failed (${res.status}): ${msg}`,
340
+ res.status,
341
+ json.code,
342
+ Array.isArray(json.missingRequiredFields) ? json.missingRequiredFields : void 0,
343
+ Array.isArray(json.fieldErrors) ? json.fieldErrors : void 0
344
+ );
328
345
  }
329
346
  audit(fn, "ok", Date.now() - start);
330
347
  const data = json.data;
@@ -748,7 +765,13 @@ function asMcpCallError(err) {
748
765
  const code = err.code;
749
766
  const match = MCP_CALL_ERROR_RE.exec(err.message);
750
767
  if (match) {
751
- return { code, cleanMessage: match[1] };
768
+ const e = err;
769
+ return {
770
+ code,
771
+ cleanMessage: match[1],
772
+ missingRequiredFields: Array.isArray(e.missingRequiredFields) ? e.missingRequiredFields : void 0,
773
+ fieldErrors: Array.isArray(e.fieldErrors) ? e.fieldErrors : void 0
774
+ };
752
775
  }
753
776
  }
754
777
  return void 0;
@@ -815,9 +838,19 @@ function classifyError(err) {
815
838
  }
816
839
  const mcpErr = asMcpCallError(err);
817
840
  if (mcpErr) {
818
- const { code: convexCode, cleanMessage } = mcpErr;
841
+ const { code: convexCode, cleanMessage, missingRequiredFields, fieldErrors } = mcpErr;
819
842
  if (convexCode === "VALIDATION_FAILED") {
820
- return { code: "VALIDATION_ERROR", message: cleanMessage };
843
+ const hasDiagnostics = missingRequiredFields?.length || fieldErrors?.length;
844
+ return {
845
+ code: "VALIDATION_ERROR",
846
+ message: cleanMessage,
847
+ ...hasDiagnostics ? {
848
+ diagnostics: {
849
+ ...missingRequiredFields?.length ? { missingRequiredFields } : {},
850
+ ...fieldErrors?.length ? { fieldErrors } : {}
851
+ }
852
+ } : {}
853
+ };
821
854
  }
822
855
  if (convexCode === "NOT_FOUND") {
823
856
  return {
@@ -976,7 +1009,9 @@ function withEnvelope(handler) {
976
1009
  classified.code,
977
1010
  classified.message,
978
1011
  classified.recovery,
979
- classified.availableActions
1012
+ classified.availableActions,
1013
+ // WP-316 S1a: pass structured diagnostics (e.g. missingRequiredFields, fieldErrors)
1014
+ classified.diagnostics
980
1015
  ),
981
1016
  _meta: { durationMs }
982
1017
  };
@@ -2803,6 +2838,16 @@ Use \`entries action=get\` to inspect the existing entry, or \`update-entry\` to
2803
2838
  type: f.type,
2804
2839
  ...f.required && { required: true }
2805
2840
  }));
2841
+ const captureContractForEnvelope = {
2842
+ requiredFields: (col.fields ?? []).filter((f) => f.required === true).map((f) => f.key),
2843
+ allFields: (col.fields ?? []).map((f) => ({
2844
+ key: f.key,
2845
+ required: f.required === true,
2846
+ type: f.type,
2847
+ ...f.helpText ? { helpText: f.helpText } : {}
2848
+ })),
2849
+ ...col.captureExpectation ? { captureExpectation: col.captureExpectation } : {}
2850
+ };
2806
2851
  const totalMs = Date.now() - timingStart;
2807
2852
  const timing = {
2808
2853
  classifyMs: tAfterClassify - timingStart,
@@ -2840,6 +2885,8 @@ Use \`entries action=get\` to inspect the existing entry, or \`update-entry\` to
2840
2885
  },
2841
2886
  next
2842
2887
  ),
2888
+ // WP-316 S1a: Populate contract field so agents know required fields upfront.
2889
+ contract: captureContractForEnvelope,
2843
2890
  _meta: { timing }
2844
2891
  }
2845
2892
  };
@@ -3849,11 +3896,30 @@ ${formatted}` }],
3849
3896
  });
3850
3897
  } catch {
3851
3898
  }
3852
- const result = await mcpMutation("chain.commitEntry", {
3853
- entryId,
3854
- author: getAgentSessionId() ? `agent:${getAgentSessionId()}` : void 0,
3855
- sessionId: getAgentSessionId() ?? void 0
3856
- });
3899
+ let result;
3900
+ try {
3901
+ result = await mcpMutation("chain.commitEntry", {
3902
+ entryId,
3903
+ author: getAgentSessionId() ? `agent:${getAgentSessionId()}` : void 0,
3904
+ sessionId: getAgentSessionId() ?? void 0
3905
+ });
3906
+ } catch (commitErr) {
3907
+ const errCode = commitErr?.code;
3908
+ if (errCode === "VALIDATION_FAILED") {
3909
+ try {
3910
+ const wsCtx2 = await getWorkspaceContext();
3911
+ const e = commitErr;
3912
+ trackCommitErrorByCode(wsCtx2.workspaceId, {
3913
+ error_code: errCode,
3914
+ missing_field_count: e.missingRequiredFields?.length ?? 0,
3915
+ field_error_count: e.fieldErrors?.length ?? 0,
3916
+ entry_id: entryId
3917
+ });
3918
+ } catch {
3919
+ }
3920
+ }
3921
+ throw commitErr;
3922
+ }
3857
3923
  const docId = result?._id ?? entry._id;
3858
3924
  const wsCtx = await getWorkspaceContext();
3859
3925
  const isProposal = result?.status === "proposal_created";
@@ -3942,6 +4008,17 @@ ${formatted}` }],
3942
4008
  lines.push(` \u2192 ${epistemic.action}`);
3943
4009
  }
3944
4010
  }
4011
+ let captureContract;
4012
+ try {
4013
+ const collSlug = entry.collectionSlug ?? entry.collection?.slug;
4014
+ if (collSlug) {
4015
+ captureContract = await mcpQuery(
4016
+ "chain.getCaptureContract",
4017
+ { collectionSlug: collSlug }
4018
+ ) ?? void 0;
4019
+ }
4020
+ } catch {
4021
+ }
3945
4022
  const next = isProposal ? [
3946
4023
  { tool: "entries", description: "View entry", parameters: { action: "get", entryId } }
3947
4024
  ] : [
@@ -3949,21 +4026,25 @@ ${formatted}` }],
3949
4026
  { tool: "entries", description: "View committed entry", parameters: { action: "get", entryId } }
3950
4027
  ];
3951
4028
  const summary = isProposal ? `Proposal created for ${entryId} (${entry.name}). Awaiting consent.` : `Committed ${entryId} (${entry.name}) to the Chain.`;
4029
+ const commitSuccessEnvelope = success(
4030
+ summary,
4031
+ {
4032
+ entryId,
4033
+ name: entry.name,
4034
+ outcome: isProposal ? "proposal_created" : "committed",
4035
+ qualityVerdict: coachingResult?.verdict ?? void 0,
4036
+ source: coachingResult?.source ?? void 0,
4037
+ contradictions: advisoryWarnings.length,
4038
+ ...epistemic ? { epistemicStatus: epistemic } : {}
4039
+ },
4040
+ next
4041
+ );
4042
+ if (captureContract) {
4043
+ commitSuccessEnvelope.contract = captureContract;
4044
+ }
3952
4045
  return {
3953
4046
  content: [{ type: "text", text: lines.join("\n") }],
3954
- structuredContent: success(
3955
- summary,
3956
- {
3957
- entryId,
3958
- name: entry.name,
3959
- outcome: isProposal ? "proposal_created" : "committed",
3960
- qualityVerdict: coachingResult?.verdict ?? void 0,
3961
- source: coachingResult?.source ?? void 0,
3962
- contradictions: advisoryWarnings.length,
3963
- ...epistemic ? { epistemicStatus: epistemic } : {}
3964
- },
3965
- next
3966
- )
4047
+ structuredContent: commitSuccessEnvelope
3967
4048
  };
3968
4049
  })
3969
4050
  );
@@ -7260,6 +7341,30 @@ async function runWrapupReview() {
7260
7341
  lines.push("- **Commit all:** call `session-wrapup` with action `commit-all`");
7261
7342
  lines.push("- **Skip:** call `session action=close` \u2014 drafts remain for next session's orient recovery.");
7262
7343
  }
7344
+ if (data.captureAudit) {
7345
+ lines.push("");
7346
+ lines.push("### Capture Audit");
7347
+ lines.push("");
7348
+ lines.push(`_${data.captureAudit.message}_`);
7349
+ if (data.captureAudit.suggestions.length > 0) {
7350
+ lines.push("");
7351
+ lines.push("**What to consider capturing:**");
7352
+ for (const s of data.captureAudit.suggestions) {
7353
+ lines.push(`- **${s.collectionSlug}**: ${s.hint}`);
7354
+ }
7355
+ }
7356
+ lines.push("");
7357
+ }
7358
+ lines.push("");
7359
+ lines.push(`> **${data.closeNudge}**`);
7360
+ if (data.captureAudit) {
7361
+ const wsCtx = await getWorkspaceContext();
7362
+ if (wsCtx) {
7363
+ trackZeroCaptureAuditFired(wsCtx.workspaceId, {
7364
+ suggestion_count: data.captureAudit.suggestions.length
7365
+ });
7366
+ }
7367
+ }
7263
7368
  const gapCount = getSessionGaps().length;
7264
7369
  return { text: lines.join("\n"), data, suggestions, gapCount, coherenceVerdict, persistentGaps };
7265
7370
  }
@@ -7583,13 +7688,22 @@ async function handleClose() {
7583
7688
  }
7584
7689
  await closeAgentSession();
7585
7690
  clearSessionGaps();
7691
+ const created = session?.entriesCreated?.length ?? 0;
7692
+ const modified = session?.entriesModified?.length ?? 0;
7693
+ const relations = session?.relationsCreated ?? 0;
7694
+ const wsCtx = await getWorkspaceContext();
7695
+ if (wsCtx) {
7696
+ trackSessionCaptureRate(wsCtx.workspaceId, {
7697
+ entries_created: created,
7698
+ entries_modified: modified,
7699
+ relations_created: relations,
7700
+ had_captures: created > 0
7701
+ });
7702
+ }
7586
7703
  const lines = [
7587
7704
  `Session ${sessionId} closed.`,
7588
7705
  ""
7589
7706
  ];
7590
- const created = session?.entriesCreated?.length ?? 0;
7591
- const modified = session?.entriesModified?.length ?? 0;
7592
- const relations = session?.relationsCreated ?? 0;
7593
7707
  const gates = session?.gateFailures ?? 0;
7594
7708
  const warnings = session?.contradictionWarnings ?? 0;
7595
7709
  if (session) {
@@ -8370,7 +8484,7 @@ var IMPLEMENTATION_REVIEW_WORKFLOW_DESCRIPTOR = {
8370
8484
 
8371
8485
  ## Your Behavior
8372
8486
 
8373
- 1. **Tool-heavy, not chat-heavy.** Each round calls specific tools: orient, quality, verify, chain-review, context action=gather, review-against-rules. Use Context7 (query-docs) for relevant testing and framework docs \u2014 not for Chain truth.
8487
+ 1. **Tool-heavy, not chat-heavy.** Each round calls specific tools: orient, quality, verify, chain-review, context action=gather, review-against-rules. Use Context7 **CLI** (\`npx ctx7@latest library\` then \`docs\`) for relevant testing and framework docs \u2014 not for Chain truth. Do not use Context7 MCP.
8374
8488
  2. **Spawn sub-agents** for parallel review: (a) code/architecture review, (b) test honesty (did we edit tests to pass or do they validate real behavior?). Max 2 agents in parallel, 30s budget each. **Constrain each sub-agent to the stated review scope only.**
8375
8489
  3. **Fix-as-you-go (FEAT-172).** When you find a HIGH or MEDIUM issue: fix it immediately, verify (lints, types), then proceed. The user sees clean work, not a list of issues. Only defer if estimated >5 min \u2014 capture deferred items as tensions. If user says "skip", capture as tension and continue.
8376
8490
  4. **Chain is SSOT.** Call orient or start first. Use entries action=search and context action=gather to load relevant BETs, DECs, BRs. If findings conflict with Chain entries, report the conflict and cite the entry ID.
@@ -8387,7 +8501,7 @@ var IMPLEMENTATION_REVIEW_WORKFLOW_DESCRIPTOR = {
8387
8501
  - \`verify\` \u2014 glossary code mappings, cross-references vs codebase
8388
8502
  - \`review-against-rules\` (domain) \u2014 business rule compliance
8389
8503
  - \`context action=gather task="implementation review for [BET-ID]"\` \u2014 related knowledge
8390
- - Context7 \`query-docs\` \u2014 relevant testing and framework best practices (resolve-library-id first)
8504
+ - Context7 CLI \u2014 \`npx ctx7@latest library\` then \`npx ctx7@latest docs\` for framework/testing docs (not Chain truth)
8391
8505
  - \`session-wrapup\` \u2014 before close
8392
8506
 
8393
8507
  ## Sub-Agent Classification (for test failures)
@@ -8439,7 +8553,7 @@ This block is the final deliverable of the review. Everything else is supporting
8439
8553
  label: "Code & Test Honesty",
8440
8554
  type: "open",
8441
8555
  instruction: "Spawn sub-agents to review implementation and tests for the stated scope only. Fix every HIGH/MEDIUM finding before proceeding. Did we meet high standards? Are tests validating real behavior or did we edit them just to pass?",
8442
- facilitatorGuidance: "Restrict all review to the scope stated in Round 01 (by default, work in this conversation only). Spawn 1\u20132 sub-agents in parallel: (1) explore agent \u2014 code review, architecture boundaries, type-safety for scoped files only. (2) explore agent \u2014 test review for scoped code: are tests asserting real behavior or were they edited to pass? Pass the scope (files/BET) explicitly to sub-agents so they do not touch other work. Use Context7 (query-docs) for relevant testing and framework best practices if needed. Classify any test failures: staleness, regression, or flaky. **Fix-as-you-go (FEAT-172):** For every HIGH/MEDIUM finding: fix immediately, run lints, verify types. Only defer fixes estimated >5 min \u2014 capture deferred items as tensions on the Chain. After fixing, do a boy-scout pass on each scoped file: clean imports, improve naming, remove dead code. **Counter-Metric Mandate (STD-19):** When reporting test coverage, accuracy, or any quantitative result, include the denominator, what was excluded, and at least one counter-metric. Never report accuracy without recall. Never report pass rate without coverage. Synthesize: findings, fixes applied, implementation grade, test honesty verdict.",
8556
+ facilitatorGuidance: "Restrict all review to the scope stated in Round 01 (by default, work in this conversation only). Spawn 1\u20132 sub-agents in parallel: (1) explore agent \u2014 code review, architecture boundaries, type-safety for scoped files only. (2) explore agent \u2014 test review for scoped code: are tests asserting real behavior or were they edited to pass? Pass the scope (files/BET) explicitly to sub-agents so they do not touch other work. Use Context7 CLI (npx ctx7 library/docs) for relevant testing and framework best practices if needed. Classify any test failures: staleness, regression, or flaky. **Fix-as-you-go (FEAT-172):** For every HIGH/MEDIUM finding: fix immediately, run lints, verify types. Only defer fixes estimated >5 min \u2014 capture deferred items as tensions on the Chain. After fixing, do a boy-scout pass on each scoped file: clean imports, improve naming, remove dead code. **Counter-Metric Mandate (STD-19):** When reporting test coverage, accuracy, or any quantitative result, include the denominator, what was excluded, and at least one counter-metric. Never report accuracy without recall. Never report pass rate without coverage. Synthesize: findings, fixes applied, implementation grade, test honesty verdict.",
8443
8557
  outputSchema: {
8444
8558
  field: "codeAndTests",
8445
8559
  description: "Implementation grade, test honesty, refactor suggestions",
@@ -8515,7 +8629,7 @@ This block is the final deliverable of the review. Everything else is supporting
8515
8629
 
8516
8630
  1. **Tool failure**: Explain what failed. Continue with what you have. The conversation is the record.
8517
8631
  2. **Sub-agent timeout**: Proceed without that agent's output. Note what wasn't checked.
8518
- 3. **Context7 unavailable**: Skip library doc validation. Rely on Chain and codebase review.
8632
+ 3. **Context7 CLI unavailable** (e.g. auth/network): Skip library doc validation. Rely on Chain and codebase review.
8519
8633
  4. **MCP unreachable**: Run from conversation knowledge. Summarize findings. Offer to capture manually later.
8520
8634
 
8521
8635
  The review must never fail silently. Always end with BET/chain IDs.`
@@ -13249,6 +13363,15 @@ function registerOrientTool(server) {
13249
13363
  } catch {
13250
13364
  }
13251
13365
  const hasTaskGrounding = Boolean(task && orientEntries?.taskContext?.context?.length > 0);
13366
+ if (wsCtx && hasTaskGrounding && orientEntries?.writeBackHints) {
13367
+ const hints = Array.isArray(orientEntries.writeBackHints) ? orientEntries.writeBackHints : [];
13368
+ if (hints.length > 0) {
13369
+ trackWriteBackHintServed(wsCtx.workspaceId, {
13370
+ hint_count: hints.length,
13371
+ has_task: !!task
13372
+ });
13373
+ }
13374
+ }
13252
13375
  let openTensions = [];
13253
13376
  try {
13254
13377
  const tensions = await mcpQuery("chain.listEntries", { collectionSlug: "tensions" });
@@ -13327,6 +13450,13 @@ function registerOrientTool(server) {
13327
13450
  if (orientEntries?.taskContext && orientEntries.taskContext.context.length > 0) {
13328
13451
  lines.push(`Task context: ${orientEntries.taskContext.totalFound} relevant entries (${orientEntries.taskContext.confidence} confidence).`);
13329
13452
  }
13453
+ if (orientEntries?.writeBackHints && orientEntries.writeBackHints.length > 0) {
13454
+ lines.push("");
13455
+ lines.push("Write-back hints (what to capture this session):");
13456
+ for (const h of orientEntries.writeBackHints) {
13457
+ lines.push(` ${h.collectionSlug}: ${h.hint}`);
13458
+ }
13459
+ }
13330
13460
  if (orientEntries?.activeBets?.length > 0) {
13331
13461
  for (const e of orientEntries.activeBets) {
13332
13462
  const tensions = e.linkedTensions;
@@ -13574,6 +13704,16 @@ function registerOrientTool(server) {
13574
13704
  orientEntries.taskContext.constellationBetName
13575
13705
  ));
13576
13706
  }
13707
+ if (orientEntries?.writeBackHints && orientEntries.writeBackHints.length > 0) {
13708
+ lines.push("");
13709
+ lines.push("## Write-Back Hints");
13710
+ lines.push("_What to capture this session (derived from task context):_");
13711
+ lines.push("");
13712
+ for (const h of orientEntries.writeBackHints) {
13713
+ lines.push(`- **${h.collectionSlug}**: ${h.hint}`);
13714
+ }
13715
+ lines.push("");
13716
+ }
13577
13717
  if (orientEntries) {
13578
13718
  const result = await runAlignmentCheck(
13579
13719
  task,
@@ -14990,6 +15130,67 @@ ${entry.labels.map((l) => `- ${l.name ?? l.slug}`).join("\n")}`);
14990
15130
  };
14991
15131
  }
14992
15132
  );
15133
+ server.resource(
15134
+ "chain-collection-capture-contract",
15135
+ new ResourceTemplate("productbrain://collections/{slug}/capture-contract", {
15136
+ list: async () => {
15137
+ const collections = await mcpQuery("chain.listCollections") ?? [];
15138
+ return {
15139
+ resources: collections.map((c) => ({
15140
+ uri: `productbrain://collections/${c.slug}/capture-contract`,
15141
+ name: `${c.icon ?? ""} ${c.name} \u2014 capture contract`.trim()
15142
+ }))
15143
+ };
15144
+ }
15145
+ }),
15146
+ async (uri, { slug }) => {
15147
+ const collectionSlug = slug;
15148
+ let workspaceId = null;
15149
+ try {
15150
+ const ctx = await getWorkspaceContext();
15151
+ workspaceId = ctx.workspaceId;
15152
+ } catch {
15153
+ }
15154
+ let contract = null;
15155
+ try {
15156
+ contract = await mcpQuery(
15157
+ "chain.getCaptureContract",
15158
+ { collectionSlug }
15159
+ );
15160
+ } catch {
15161
+ contract = null;
15162
+ }
15163
+ if (!contract) {
15164
+ if (workspaceId) {
15165
+ trackCaptureContractMiss(workspaceId, { slug: collectionSlug });
15166
+ }
15167
+ return {
15168
+ contents: [{
15169
+ uri: uri.href,
15170
+ mimeType: "application/json",
15171
+ text: JSON.stringify({
15172
+ ok: false,
15173
+ error: `Collection '${collectionSlug}' not found in this workspace.`,
15174
+ hint: "Use `collections action=list` to see available collections and their slugs."
15175
+ })
15176
+ }]
15177
+ };
15178
+ }
15179
+ const result = {
15180
+ collectionSlug,
15181
+ requiredFields: contract.requiredFields,
15182
+ allFields: contract.allFields,
15183
+ captureExpectation: contract.captureExpectation
15184
+ };
15185
+ return {
15186
+ contents: [{
15187
+ uri: uri.href,
15188
+ mimeType: "application/json",
15189
+ text: JSON.stringify(result)
15190
+ }]
15191
+ };
15192
+ }
15193
+ );
14993
15194
  server.resource(
14994
15195
  "chain-search",
14995
15196
  new ResourceTemplate("productbrain://search/{query}", {
@@ -15319,4 +15520,4 @@ export {
15319
15520
  SERVER_VERSION,
15320
15521
  createProductBrainServer
15321
15522
  };
15322
- //# sourceMappingURL=chunk-GMSHWOXT.js.map
15523
+ //# sourceMappingURL=chunk-ZZHGJKLF.js.map