@productbrain/mcp 0.0.1-beta.6 → 0.0.1-beta.7

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/dist/index.js CHANGED
@@ -3,7 +3,6 @@ import {
3
3
  bootstrap,
4
4
  closeAgentSession,
5
5
  getAgentSessionId,
6
- getApiKeyScope,
7
6
  getAuditLog,
8
7
  getWorkspaceContext,
9
8
  getWorkspaceId,
@@ -21,7 +20,7 @@ import {
21
20
  shutdownAnalytics,
22
21
  startAgentSession,
23
22
  trackSessionStarted
24
- } from "./chunk-YLVX2CON.js";
23
+ } from "./chunk-6ZSCQINU.js";
25
24
 
26
25
  // src/index.ts
27
26
  import { readFileSync as readFileSync3 } from "fs";
@@ -717,6 +716,111 @@ Use \`suggest-links\` to discover potential connections, or \`relate-entries\` t
717
716
  return { content: [{ type: "text", text: lines.join("\n") }] };
718
717
  }
719
718
  );
719
+ server2.registerTool(
720
+ "create-collection",
721
+ {
722
+ title: "Create Collection",
723
+ description: "Create a new knowledge collection in the workspace. Collections define the structure for entries (like Notion databases or Capacity boards). Provide a slug, name, and field schema.\n\nUse this when setting up a workspace or when the user wants to track a new type of knowledge. Use `list-collections` first to see what already exists.",
724
+ inputSchema: {
725
+ slug: z.string().describe("URL-safe identifier, e.g. 'glossary', 'tech-debt', 'api-endpoints'"),
726
+ name: z.string().describe("Display name, e.g. 'Glossary', 'Tech Debt', 'API Endpoints'"),
727
+ description: z.string().optional().describe("What this collection is for"),
728
+ icon: z.string().optional().describe("Emoji icon for the collection"),
729
+ fields: z.array(z.object({
730
+ key: z.string().describe("Field key, e.g. 'description', 'severity', 'status'"),
731
+ label: z.string().describe("Display label, e.g. 'Description', 'Severity'"),
732
+ type: z.string().describe("Field type: 'string', 'select', 'array', 'number', 'boolean'"),
733
+ required: z.boolean().optional().describe("Whether this field is required"),
734
+ options: z.array(z.string()).optional().describe("Options for 'select' type fields"),
735
+ searchable: z.boolean().optional().describe("Whether this field is included in full-text search")
736
+ })).describe("Field definitions for the collection schema")
737
+ },
738
+ annotations: { destructiveHint: false }
739
+ },
740
+ async ({ slug, name, description, icon, fields }) => {
741
+ requireWriteAccess();
742
+ try {
743
+ await mcpMutation("chain.createCollection", {
744
+ slug,
745
+ name,
746
+ description,
747
+ icon,
748
+ fields,
749
+ isDefault: false,
750
+ createdBy: getAgentSessionId() ? `agent:${getAgentSessionId()}` : "mcp"
751
+ });
752
+ const fieldList = fields.map((f) => ` - \`${f.key}\` (${f.type}${f.required ? ", required" : ""}${f.searchable ? ", searchable" : ""})`).join("\n");
753
+ return {
754
+ content: [{
755
+ type: "text",
756
+ text: `# Collection Created: ${name}
757
+
758
+ **Slug:** \`${slug}\`
759
+ ` + (description ? `**Description:** ${description}
760
+ ` : "") + `
761
+ **Fields:**
762
+ ${fieldList}
763
+
764
+ You can now capture entries: \`capture collection="${slug}" name="..." description="..."\``
765
+ }]
766
+ };
767
+ } catch (error) {
768
+ const msg = error instanceof Error ? error.message : String(error);
769
+ if (msg.includes("already exists")) {
770
+ return {
771
+ content: [{
772
+ type: "text",
773
+ text: `Collection \`${slug}\` already exists. Use \`update-collection\` to modify it, or choose a different slug.`
774
+ }]
775
+ };
776
+ }
777
+ throw error;
778
+ }
779
+ }
780
+ );
781
+ server2.registerTool(
782
+ "update-collection",
783
+ {
784
+ title: "Update Collection",
785
+ description: "Update an existing collection's name, description, icon, or field schema. Only provide the fields you want to change. Use `list-collections` to see current state.",
786
+ inputSchema: {
787
+ slug: z.string().describe("Collection slug to update, e.g. 'glossary', 'tech-debt'"),
788
+ name: z.string().optional().describe("New display name"),
789
+ description: z.string().optional().describe("New description"),
790
+ icon: z.string().optional().describe("New emoji icon"),
791
+ fields: z.array(z.object({
792
+ key: z.string(),
793
+ label: z.string(),
794
+ type: z.string(),
795
+ required: z.boolean().optional(),
796
+ options: z.array(z.string()).optional(),
797
+ searchable: z.boolean().optional()
798
+ })).optional().describe("Replacement field schema (replaces all fields \u2014 include existing fields you want to keep)")
799
+ },
800
+ annotations: { destructiveHint: false }
801
+ },
802
+ async ({ slug, name, description, icon, fields }) => {
803
+ requireWriteAccess();
804
+ await mcpMutation("chain.updateCollection", {
805
+ slug,
806
+ ...name !== void 0 && { name },
807
+ ...description !== void 0 && { description },
808
+ ...icon !== void 0 && { icon },
809
+ ...fields !== void 0 && { fields }
810
+ });
811
+ const changes = [name && "name", description && "description", icon && "icon", fields && "fields"].filter(Boolean).join(", ");
812
+ return {
813
+ content: [{
814
+ type: "text",
815
+ text: `# Collection Updated: \`${slug}\`
816
+
817
+ Changed: ${changes || "no changes"}.
818
+
819
+ Use \`list-collections\` to verify the result.`
820
+ }]
821
+ };
822
+ }
823
+ );
720
824
  server2.registerTool(
721
825
  "commit-entry",
722
826
  {
@@ -729,7 +833,7 @@ Use \`suggest-links\` to discover potential connections, or \`relate-entries\` t
729
833
  },
730
834
  async ({ entryId }) => {
731
835
  requireWriteAccess();
732
- const { runContradictionCheck } = await import("./smart-capture-QGIC5N47.js");
836
+ const { runContradictionCheck } = await import("./smart-capture-SEINMTTR.js");
733
837
  const entry = await mcpQuery("chain.getEntry", { entryId });
734
838
  if (!entry) {
735
839
  return {
@@ -876,6 +980,8 @@ var CALL_CATEGORIES = {
876
980
  "chain.createLabel": "label",
877
981
  "chain.updateLabel": "label",
878
982
  "chain.deleteLabel": "label",
983
+ "chain.createCollection": "write",
984
+ "chain.updateCollection": "write",
879
985
  "chain.listCollections": "meta",
880
986
  "chain.getCollection": "meta",
881
987
  "chain.listLabels": "meta",
@@ -1054,7 +1160,7 @@ function registerHealthTools(server2) {
1054
1160
  "orient",
1055
1161
  {
1056
1162
  title: "Orient \u2014 Start Here",
1057
- description: "The single entry point for starting a session. Returns everything needed in one call: workspace identity, readiness score, prior session summaries, architecture/governance constraints, open tensions, and suggested next actions.\n\nUse this FIRST. One call to orient replaces 3\u20135 individual tool calls.\n\nCompleting orientation unlocks write tools for the active session.",
1163
+ description: "The single entry point for starting a session. Returns workspace context with a single recommended next action for low-readiness workspaces, or a standup-style briefing for established workspaces.\n\nUse this FIRST. One call to orient replaces 3\u20135 individual tool calls.\n\nCompleting orientation unlocks write tools for the active session.",
1058
1164
  annotations: { readOnlyHint: true }
1059
1165
  },
1060
1166
  async () => {
@@ -1067,15 +1173,13 @@ function registerHealthTools(server2) {
1067
1173
  errors.push(`Workspace: ${e.message}`);
1068
1174
  }
1069
1175
  let priorSessions = [];
1070
- let maxSessions = 3;
1071
1176
  if (wsCtx) {
1072
1177
  try {
1073
- priorSessions = await mcpQuery("agent.recentSessions", { limit: maxSessions });
1178
+ priorSessions = await mcpQuery("agent.recentSessions", { limit: 3 });
1074
1179
  } catch {
1075
1180
  }
1076
1181
  }
1077
1182
  let constraintEntries = [];
1078
- let maxConstraints = 8;
1079
1183
  try {
1080
1184
  const [archEntries, ruleEntries, decisionEntries] = await Promise.all([
1081
1185
  mcpQuery("chain.listEntries", { collectionSlug: "architecture" }),
@@ -1087,7 +1191,7 @@ function registerHealthTools(server2) {
1087
1191
  ...(ruleEntries ?? []).filter((e) => e.status === "Active" || e.status === "active"),
1088
1192
  ...(decisionEntries ?? []).filter((e) => e.status === "Decided" || e.status === "active")
1089
1193
  ];
1090
- constraintEntries = committed.sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0)).slice(0, maxConstraints);
1194
+ constraintEntries = committed.sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0)).slice(0, 8);
1091
1195
  } catch {
1092
1196
  }
1093
1197
  let openTensions = [];
@@ -1102,109 +1206,100 @@ function registerHealthTools(server2) {
1102
1206
  } catch (e) {
1103
1207
  errors.push(`Readiness: ${e.message}`);
1104
1208
  }
1105
- const TOKEN_LIMIT = 6e3;
1106
- const CHAR_PER_TOKEN = 4;
1107
- const CHAR_LIMIT = TOKEN_LIMIT * CHAR_PER_TOKEN;
1108
1209
  const lines = [];
1109
- let charCount = 0;
1110
- function addLine(line) {
1111
- lines.push(line);
1112
- charCount += line.length + 1;
1113
- }
1114
- function addLines(ls) {
1115
- for (const l of ls) addLine(l);
1116
- }
1210
+ const isLowReadiness = readiness && readiness.score < 50;
1117
1211
  if (wsCtx) {
1118
- addLine(`# ${wsCtx.workspaceName}`);
1119
- addLine(`_Workspace \`${wsCtx.workspaceSlug}\` \u2014 Product Brain is healthy._`);
1212
+ lines.push(`# ${wsCtx.workspaceName}`);
1213
+ lines.push(`_Workspace \`${wsCtx.workspaceSlug}\` \u2014 Product Brain is healthy._`);
1120
1214
  } else {
1121
- addLine("# Workspace");
1122
- addLine("_Could not resolve workspace._");
1123
- }
1124
- addLine("");
1125
- if (readiness) {
1126
- const scoreBar = "\u2588".repeat(Math.round(readiness.score / 10)) + "\u2591".repeat(10 - Math.round(readiness.score / 10));
1127
- addLine(`## Readiness: ${readiness.score}%`);
1128
- addLine(`${scoreBar} ${readiness.passedChecks}/${readiness.totalChecks} requirements`);
1129
- addLine("");
1130
- if (readiness.gaps && readiness.gaps.length > 0) {
1131
- addLine("### Gaps");
1132
- for (const gap of readiness.gaps) {
1133
- addLine(`- [ ] **${gap.label}** (${gap.current}/${gap.required}) \u2014 _${gap.guidance}_`);
1134
- }
1135
- addLine("");
1215
+ lines.push("# Workspace");
1216
+ lines.push("_Could not resolve workspace._");
1217
+ }
1218
+ lines.push("");
1219
+ if (isLowReadiness && wsCtx?.createdAt) {
1220
+ const ageDays = Math.floor((Date.now() - wsCtx.createdAt) / (1e3 * 60 * 60 * 24));
1221
+ if (ageDays >= 30) {
1222
+ lines.push(`Your workspace has been around for ${ageDays} days but is only ${readiness.score}% ready.`);
1223
+ lines.push("Let's close the gaps \u2014 or if the current structure doesn't fit, we can reshape it.");
1224
+ lines.push("");
1136
1225
  }
1137
1226
  }
1138
- if (openTensions.length > 0) {
1139
- addLine(`## Open Tensions (${openTensions.length})`);
1140
- for (const t of openTensions) {
1141
- const id = t.entryId ?? "(no ID)";
1142
- const prio = t.data?.priority ?? "";
1143
- addLine(`- \`${id}\` ${t.name}${prio ? ` _(${prio})_` : ""}`);
1227
+ if (isLowReadiness) {
1228
+ lines.push(`Readiness: ${readiness.score}% (${readiness.passedChecks}/${readiness.totalChecks}).`);
1229
+ lines.push("");
1230
+ const gaps = readiness.gaps ?? [];
1231
+ if (gaps.length > 0) {
1232
+ const gap = gaps[0];
1233
+ const ctaMap = {
1234
+ "strategy-vision": "Tell me what you're building \u2014 your vision, mission, and north star \u2014 and I'll capture it.",
1235
+ "architecture-layers": "Describe your architecture in a few sentences and I'll capture it.",
1236
+ "glossary-foundation": "What are the key terms your team uses? Tell me a few and I'll add them to the glossary.",
1237
+ "decisions-documented": "What's a recent significant decision your team made? I'll document it with the rationale.",
1238
+ "tensions-tracked": "What's a friction point or pain point you're dealing with? I'll capture it as a tension."
1239
+ };
1240
+ const cta = ctaMap[gap.id] ?? `Tell me about your ${gap.label.toLowerCase()} and I'll capture it.`;
1241
+ lines.push("## Recommended next step");
1242
+ lines.push(`**${gap.label}** (${gap.current}/${gap.required})`);
1243
+ lines.push("");
1244
+ lines.push(cta);
1245
+ lines.push("");
1246
+ lines.push('_Everything stays as a draft until you confirm. Say "commit" or "looks good" to promote to the Chain._');
1247
+ lines.push("");
1248
+ const remainingGaps = gaps.length - 1;
1249
+ if (remainingGaps > 0 || openTensions.length > 0) {
1250
+ lines.push(`_${remainingGaps > 0 ? `${remainingGaps} more gap${remainingGaps === 1 ? "" : "s"}` : ""}${remainingGaps > 0 && openTensions.length > 0 ? " and " : ""}${openTensions.length > 0 ? `${openTensions.length} open tension${openTensions.length === 1 ? "" : "s"}` : ""} \u2014 ask "show full status" for details._`);
1251
+ lines.push("");
1252
+ }
1253
+ }
1254
+ } else if (readiness) {
1255
+ lines.push(`Readiness: ${readiness.score}% (${readiness.passedChecks}/${readiness.totalChecks}).`);
1256
+ lines.push("");
1257
+ const briefingItems = [];
1258
+ if (openTensions.length > 0) {
1259
+ const topTension = openTensions[0];
1260
+ briefingItems.push(`**${openTensions.length} open tension${openTensions.length === 1 ? "" : "s"}** \u2014 top: ${topTension.name}`);
1261
+ }
1262
+ if (priorSessions.length > 0) {
1263
+ const last = priorSessions[0];
1264
+ const date = new Date(last.startedAt).toISOString().split("T")[0];
1265
+ const created = Array.isArray(last.entriesCreated) ? last.entriesCreated.length : last.entriesCreated ?? 0;
1266
+ const modified = Array.isArray(last.entriesModified) ? last.entriesModified.length : last.entriesModified ?? 0;
1267
+ briefingItems.push(`**Last session** (${date}): ${created} created, ${modified} modified`);
1144
1268
  }
1145
- addLine("");
1146
- }
1147
- if (charCount > CHAR_LIMIT * 0.6) {
1148
- maxSessions = 2;
1149
- maxConstraints = 5;
1150
- priorSessions = priorSessions.slice(0, maxSessions);
1151
- constraintEntries = constraintEntries.slice(0, maxConstraints);
1152
- }
1153
- if (priorSessions.length > 0) {
1154
- addLine(`## Prior Agent Sessions (last ${priorSessions.length})`);
1155
- for (const s of priorSessions) {
1156
- const date = new Date(s.startedAt).toISOString().split("T")[0];
1157
- const created = Array.isArray(s.entriesCreated) ? s.entriesCreated.length : s.entriesCreated ?? 0;
1158
- const modified = Array.isArray(s.entriesModified) ? s.entriesModified.length : s.entriesModified ?? 0;
1159
- const stats = `${created} created, ${modified} modified, ${s.relationsCreated ?? 0} relations`;
1160
- const gates = (s.gateFailures ?? 0) > 0 ? `, ${s.gateFailures} gate failures` : "";
1161
- const warns = (s.contradictionWarnings ?? 0) > 0 ? `, ${s.contradictionWarnings} contradiction warnings` : "";
1162
- addLine(`- **${date}** (${s.status}, by ${s.initiatedBy ?? "unknown"}) \u2014 ${stats}${gates}${warns}`);
1269
+ if (readiness.gaps?.length > 0) {
1270
+ briefingItems.push(`**${readiness.gaps.length} gap${readiness.gaps.length === 1 ? "" : "s"}** remaining`);
1163
1271
  }
1164
- addLine("");
1165
- }
1166
- if (constraintEntries.length > 0) {
1167
- addLine(`## Active Constraints (${constraintEntries.length})`);
1168
- addLine("_Architecture, business rules, and decisions that govern this workspace._");
1169
- addLine("");
1170
- for (const c of constraintEntries) {
1171
- const id = c.entryId ?? "(no ID)";
1172
- const col = c.collectionSlug ?? "";
1173
- const desc = c.data?.description ?? c.data?.rationale ?? "";
1174
- const truncated = desc.length > 100 ? desc.slice(0, 100) + "..." : desc;
1175
- addLine(`- \`${id}\` **${c.name}** [${col}] \u2014 ${truncated}`);
1272
+ if (constraintEntries.length > 0) {
1273
+ briefingItems.push(`**${constraintEntries.length} active constraint${constraintEntries.length === 1 ? "" : "s"}** (architecture, rules, decisions)`);
1176
1274
  }
1177
- addLine("");
1275
+ if (briefingItems.length > 0) {
1276
+ lines.push("## Briefing");
1277
+ for (const item of briefingItems) {
1278
+ lines.push(`- ${item}`);
1279
+ }
1280
+ lines.push("");
1281
+ }
1282
+ lines.push("What would you like to work on?");
1283
+ lines.push("");
1178
1284
  }
1179
1285
  if (errors.length > 0) {
1180
- addLine("## Errors");
1181
- for (const err of errors) addLine(`- ${err}`);
1182
- addLine("");
1286
+ lines.push("## Errors");
1287
+ for (const err of errors) lines.push(`- ${err}`);
1288
+ lines.push("");
1183
1289
  }
1184
1290
  if (agentSessionId) {
1185
1291
  try {
1186
1292
  await mcpCall("agent.markOriented", { sessionId: agentSessionId });
1187
1293
  setSessionOriented(true);
1188
- const scope = getApiKeyScope();
1189
- const sessionInfo = `Session ${agentSessionId}.`;
1190
- const readinessInfo = readiness ? `${readiness.score}% readiness.` : "";
1191
- const tensionInfo = openTensions.length > 0 ? `${openTensions.length} open tensions.` : "No open tensions.";
1192
- let sessionSummary = "";
1193
- if (priorSessions.length > 0) {
1194
- const lastSession = priorSessions[0];
1195
- const created = Array.isArray(lastSession.entriesCreated) ? lastSession.entriesCreated.length : lastSession.entriesCreated ?? 0;
1196
- const modified = Array.isArray(lastSession.entriesModified) ? lastSession.entriesModified.length : lastSession.entriesModified ?? 0;
1197
- sessionSummary = `Last session: ${created} created, ${modified} modified.`;
1198
- }
1199
- addLine("---");
1200
- addLine(`Orientation complete. ${sessionInfo} Write tools now available. ${readinessInfo} ${tensionInfo} ${sessionSummary}`);
1294
+ lines.push("---");
1295
+ lines.push(`Orientation complete. Session ${agentSessionId}. Write tools available.`);
1201
1296
  } catch {
1202
- addLine("---");
1203
- addLine("_Warning: Could not mark session as oriented. Write tools may be restricted._");
1297
+ lines.push("---");
1298
+ lines.push("_Warning: Could not mark session as oriented. Write tools may be restricted._");
1204
1299
  }
1205
1300
  } else {
1206
- addLine("---");
1207
- addLine("_No active agent session. Call `agent-start` to begin a tracked session._");
1301
+ lines.push("---");
1302
+ lines.push("_No active agent session. Call `agent-start` to begin a tracked session._");
1208
1303
  }
1209
1304
  return { content: [{ type: "text", text: lines.join("\n") }] };
1210
1305
  }
@@ -3500,9 +3595,9 @@ function buildPresetMenu(wsCtx) {
3500
3595
  const lines = [
3501
3596
  `# Welcome to ${wsCtx.workspaceName}`,
3502
3597
  "",
3503
- "Your workspace is fresh \u2014 let's set it up. **What are you building?**",
3598
+ "Your workspace is fresh \u2014 let's get it set up together.",
3504
3599
  "",
3505
- "Choose a preset to tailor your collections:",
3600
+ "**Tell me: what are you building?** Describe it in a sentence or two and I'll help you pick the right structure. Or choose a preset to start from:",
3506
3601
  ""
3507
3602
  ];
3508
3603
  for (const p of presets) {
@@ -3512,7 +3607,7 @@ function buildPresetMenu(wsCtx) {
3512
3607
  "",
3513
3608
  'Call `start` again with your choice, e.g.: `start preset="software-product"`',
3514
3609
  "",
3515
- "_You can always add, remove, or customize collections later._"
3610
+ "_These are starting points. You can add, remove, or customize collections at any time using `create-collection` and `update-collection`._"
3516
3611
  );
3517
3612
  return lines.join("\n");
3518
3613
  }
@@ -3565,46 +3660,57 @@ Available presets: ${listPresets().map((p) => `\`${p.id}\``).join(", ")}`;
3565
3660
  }
3566
3661
  lines.push(
3567
3662
  "",
3568
- "## What's next?",
3663
+ "## Let's activate your workspace",
3569
3664
  "",
3570
- "Start capturing knowledge. Here are example commands for your collections:",
3571
- ""
3572
- );
3573
- for (const col of preset.collections.slice(0, 3)) {
3574
- lines.push(
3575
- `- \`capture collection="${col.slug}" name="..." description="..."\` \u2014 add to ${col.name}`
3576
- );
3577
- }
3578
- lines.push(
3665
+ "I'll help you load knowledge step by step. Everything stays as a draft until you confirm it.",
3666
+ "",
3667
+ "**First:** Tell me what you're building in one or two sentences. I'll capture it as your product vision.",
3668
+ "",
3669
+ `_After that, we'll add a few key terms to your glossary and any important decisions. Each entry is a draft \u2014 say "commit" or "looks good" when you're happy with it, and I'll promote it to the Chain (SSOT)._`,
3579
3670
  "",
3580
- "After capturing a few entries, use `suggest-links` to connect them into a knowledge graph.",
3671
+ "You can also customize your structure anytime: `create-collection`, `update-collection`, or `list-collections`.",
3581
3672
  "",
3582
3673
  "---",
3583
- `Orientation complete. Write tools are available.`
3674
+ "Orientation complete. Write tools are available."
3584
3675
  );
3585
3676
  return lines.join("\n");
3586
3677
  }
3678
+ function computeWorkspaceAge(createdAt) {
3679
+ if (!createdAt) return { ageDays: 0, isNeglected: false };
3680
+ const ageDays = Math.floor((Date.now() - createdAt) / (1e3 * 60 * 60 * 24));
3681
+ return { ageDays, isNeglected: ageDays >= 30 };
3682
+ }
3683
+ function pickNextAction(gaps, openTensions, priorSessions) {
3684
+ if (gaps.length === 0 && openTensions.length === 0) return null;
3685
+ if (gaps.length > 0) {
3686
+ const gap = gaps[0];
3687
+ const ctaMap = {
3688
+ "strategy-vision": "Tell me what you're building \u2014 your vision, mission, and north star \u2014 and I'll capture it.",
3689
+ "architecture-layers": "Describe your architecture in a few sentences and I'll capture it.",
3690
+ "glossary-foundation": "What are the key terms your team uses? Tell me a few and I'll add them to the glossary.",
3691
+ "decisions-documented": "What's a recent significant decision your team made? I'll document it with the rationale.",
3692
+ "tensions-tracked": "What's a friction point or pain point you're dealing with? I'll capture it as a tension."
3693
+ };
3694
+ const cta = ctaMap[gap.id] ?? `Tell me about your ${gap.label.toLowerCase()} and I'll capture it.`;
3695
+ return { action: gap.label, cta };
3696
+ }
3697
+ if (openTensions.length > 0) {
3698
+ const t = openTensions[0];
3699
+ return {
3700
+ action: `Open tension: ${t.name}`,
3701
+ cta: "Want to discuss this tension or capture a decision about it?"
3702
+ };
3703
+ }
3704
+ return null;
3705
+ }
3587
3706
  async function buildOrientResponse(wsCtx, agentSessionId, errors) {
3707
+ const wsFullCtx = await getWorkspaceContext();
3708
+ const { ageDays, isNeglected } = computeWorkspaceAge(wsFullCtx.createdAt);
3588
3709
  let priorSessions = [];
3589
3710
  try {
3590
3711
  priorSessions = await mcpQuery("agent.recentSessions", { limit: 3 });
3591
3712
  } catch {
3592
3713
  }
3593
- let constraintEntries = [];
3594
- try {
3595
- const [archEntries, ruleEntries, decisionEntries] = await Promise.all([
3596
- mcpQuery("chain.listEntries", { collectionSlug: "architecture" }),
3597
- mcpQuery("chain.listEntries", { collectionSlug: "business-rules" }),
3598
- mcpQuery("chain.listEntries", { collectionSlug: "decisions" })
3599
- ]);
3600
- const committed = [
3601
- ...(archEntries ?? []).filter((e) => e.status === "active" || e.status === "healthy"),
3602
- ...(ruleEntries ?? []).filter((e) => e.status === "Active" || e.status === "active"),
3603
- ...(decisionEntries ?? []).filter((e) => e.status === "Decided" || e.status === "active")
3604
- ];
3605
- constraintEntries = committed.sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0)).slice(0, 8);
3606
- } catch {
3607
- }
3608
3714
  let openTensions = [];
3609
3715
  try {
3610
3716
  const tensions = await mcpQuery("chain.listEntries", { collectionSlug: "tensions" });
@@ -3618,53 +3724,63 @@ async function buildOrientResponse(wsCtx, agentSessionId, errors) {
3618
3724
  errors.push(`Readiness: ${e.message}`);
3619
3725
  }
3620
3726
  const lines = [];
3727
+ const isLowReadiness = readiness && readiness.score < 50;
3728
+ const isHighReadiness = readiness && readiness.score >= 50;
3621
3729
  lines.push(`# ${wsCtx.workspaceName}`);
3622
3730
  lines.push(`_Workspace \`${wsCtx.workspaceSlug}\` \u2014 Product Brain is healthy._`);
3623
3731
  lines.push("");
3624
- if (readiness) {
3625
- const scoreBar = "\u2588".repeat(Math.round(readiness.score / 10)) + "\u2591".repeat(10 - Math.round(readiness.score / 10));
3626
- lines.push(`## Readiness: ${readiness.score}%`);
3627
- lines.push(`${scoreBar} ${readiness.passedChecks}/${readiness.totalChecks} requirements`);
3732
+ if (isLowReadiness && isNeglected) {
3733
+ lines.push(`Your workspace has been around for ${ageDays} days but is only ${readiness.score}% ready.`);
3734
+ lines.push("Let's close the gaps \u2014 or if the current structure doesn't fit, we can reshape it.");
3628
3735
  lines.push("");
3629
- if (readiness.gaps?.length > 0) {
3630
- lines.push("### Gaps");
3631
- for (const gap of readiness.gaps) {
3632
- lines.push(`- [ ] **${gap.label}** (${gap.current}/${gap.required}) \u2014 _${gap.guidance}_`);
3633
- }
3736
+ } else if (isLowReadiness) {
3737
+ lines.push(`Readiness: ${readiness.score}%. Let's get your workspace active.`);
3738
+ lines.push("");
3739
+ }
3740
+ if (isLowReadiness) {
3741
+ const nextAction = pickNextAction(readiness.gaps ?? [], openTensions, priorSessions);
3742
+ if (nextAction) {
3743
+ lines.push("## Recommended next step");
3744
+ lines.push(`**${nextAction.action}**`);
3745
+ lines.push("");
3746
+ lines.push(nextAction.cta);
3634
3747
  lines.push("");
3748
+ lines.push('_Everything stays as a draft until you confirm. Say "commit" or "looks good" to promote to the Chain._');
3749
+ lines.push("");
3750
+ const remainingGaps = (readiness.gaps ?? []).length - 1;
3751
+ if (remainingGaps > 0 || openTensions.length > 0) {
3752
+ lines.push(`_${remainingGaps > 0 ? `${remainingGaps} more gap${remainingGaps === 1 ? "" : "s"}` : ""}${remainingGaps > 0 && openTensions.length > 0 ? " and " : ""}${openTensions.length > 0 ? `${openTensions.length} open tension${openTensions.length === 1 ? "" : "s"}` : ""} \u2014 ask "show full status" for details._`);
3753
+ lines.push("");
3754
+ }
3635
3755
  }
3636
- }
3637
- if (openTensions.length > 0) {
3638
- lines.push(`## Open Tensions (${openTensions.length})`);
3639
- for (const t of openTensions) {
3640
- const id = t.entryId ?? "(no ID)";
3641
- const prio = t.data?.priority ?? "";
3642
- lines.push(`- \`${id}\` ${t.name}${prio ? ` _(${prio})_` : ""}`);
3756
+ } else if (isHighReadiness) {
3757
+ if (readiness) {
3758
+ lines.push(`Readiness: ${readiness.score}% (${readiness.passedChecks}/${readiness.totalChecks}).`);
3643
3759
  }
3644
- lines.push("");
3645
- }
3646
- if (priorSessions.length > 0) {
3647
- lines.push(`## Prior Agent Sessions (last ${priorSessions.length})`);
3648
- for (const s of priorSessions) {
3649
- const date = new Date(s.startedAt).toISOString().split("T")[0];
3650
- const created = Array.isArray(s.entriesCreated) ? s.entriesCreated.length : s.entriesCreated ?? 0;
3651
- const modified = Array.isArray(s.entriesModified) ? s.entriesModified.length : s.entriesModified ?? 0;
3652
- const stats = `${created} created, ${modified} modified, ${s.relationsCreated ?? 0} relations`;
3653
- lines.push(`- **${date}** (${s.status}, by ${s.initiatedBy ?? "unknown"}) \u2014 ${stats}`);
3760
+ const briefingItems = [];
3761
+ if (openTensions.length > 0) {
3762
+ const topTension = openTensions[0];
3763
+ briefingItems.push(`**${openTensions.length} open tension${openTensions.length === 1 ? "" : "s"}** \u2014 top: ${topTension.name}`);
3654
3764
  }
3655
- lines.push("");
3656
- }
3657
- if (constraintEntries.length > 0) {
3658
- lines.push(`## Active Constraints (${constraintEntries.length})`);
3659
- lines.push("_Architecture, business rules, and decisions that govern this workspace._");
3660
- lines.push("");
3661
- for (const c of constraintEntries) {
3662
- const id = c.entryId ?? "(no ID)";
3663
- const col = c.collectionSlug ?? "";
3664
- const desc = c.data?.description ?? c.data?.rationale ?? "";
3665
- const truncated = desc.length > 100 ? desc.slice(0, 100) + "..." : desc;
3666
- lines.push(`- \`${id}\` **${c.name}** [${col}] \u2014 ${truncated}`);
3765
+ if (priorSessions.length > 0) {
3766
+ const last = priorSessions[0];
3767
+ const date = new Date(last.startedAt).toISOString().split("T")[0];
3768
+ const created = Array.isArray(last.entriesCreated) ? last.entriesCreated.length : last.entriesCreated ?? 0;
3769
+ const modified = Array.isArray(last.entriesModified) ? last.entriesModified.length : last.entriesModified ?? 0;
3770
+ briefingItems.push(`**Last session** (${date}): ${created} created, ${modified} modified`);
3771
+ }
3772
+ if (readiness?.gaps?.length > 0) {
3773
+ briefingItems.push(`**${readiness.gaps.length} gap${readiness.gaps.length === 1 ? "" : "s"}** remaining`);
3774
+ }
3775
+ if (briefingItems.length > 0) {
3776
+ lines.push("");
3777
+ lines.push("## Briefing");
3778
+ for (const item of briefingItems) {
3779
+ lines.push(`- ${item}`);
3780
+ }
3781
+ lines.push("");
3667
3782
  }
3783
+ lines.push("What would you like to work on?");
3668
3784
  lines.push("");
3669
3785
  }
3670
3786
  if (errors.length > 0) {
@@ -3676,11 +3792,9 @@ async function buildOrientResponse(wsCtx, agentSessionId, errors) {
3676
3792
  try {
3677
3793
  await mcpCall("agent.markOriented", { sessionId: agentSessionId });
3678
3794
  setSessionOriented(true);
3679
- const readinessInfo = readiness ? `${readiness.score}% readiness.` : "";
3680
- const tensionInfo = openTensions.length > 0 ? `${openTensions.length} open tensions.` : "No open tensions.";
3681
3795
  lines.push("---");
3682
3796
  lines.push(
3683
- `Orientation complete. Session ${agentSessionId}. Write tools now available. ${readinessInfo} ${tensionInfo}`
3797
+ `Orientation complete. Session ${agentSessionId}. Write tools available.`
3684
3798
  );
3685
3799
  } catch {
3686
3800
  lines.push("---");
@@ -4223,14 +4337,26 @@ var server = new McpServer2(
4223
4337
  " 4. Drill in: use `get-entry` for full details \u2014 data, labels, relations, history.",
4224
4338
  " 5. Context: use `gather-context` with an entryId or a task description.",
4225
4339
  " 6. Capture: use `capture` to create entries \u2014 auto-links and scores in one call.",
4226
- " 7. Commit: use `commit-entry` to promote drafts to SSOT.",
4340
+ " 7. Commit: use `commit-entry` to promote drafts to SSOT \u2014 only when the user confirms.",
4227
4341
  " 8. Connect: use `suggest-links` then `relate-entries` to build the graph.",
4228
4342
  " 9. Close: call `agent-close` when done \u2014 records session activity.",
4229
4343
  "",
4230
4344
  "Write tools (capture, update-entry, relate-entries, commit-entry) require:",
4231
4345
  " - An active agent session (call agent-start)",
4232
4346
  " - Completed orientation (call orient)",
4233
- " - A readwrite API key scope"
4347
+ " - A readwrite API key scope",
4348
+ "",
4349
+ "Commit-on-confirm: always capture as draft first and show the user what was captured.",
4350
+ "Only call `commit-entry` when the user explicitly confirms (e.g. 'commit', 'looks good', 'yes').",
4351
+ "This builds trust \u2014 the Chain (main) is SSOT; nothing goes there without user consent.",
4352
+ "",
4353
+ "Workspace setup: use `create-collection` and `update-collection` to shape the workspace",
4354
+ "structure with the user. Ask what they need to track; presets are starting points, not fixed.",
4355
+ "",
4356
+ "Personalization: if you have context about the user from memory (prior work, recent",
4357
+ "conversations, team context), use it to personalize recommendations. For example,",
4358
+ "'Based on your recent pitch reviews, the gap most likely to matter is X.'",
4359
+ "The orient/start output gives you the workspace state; your memory fills in the human context."
4234
4360
  ].join("\n")
4235
4361
  }
4236
4362
  );