@productbrain/mcp 0.0.1-beta.39 → 0.0.1-beta.40

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.
@@ -25,11 +25,11 @@ import {
25
25
  startAgentSession,
26
26
  trackWriteTool,
27
27
  translateStaleToolNames
28
- } from "./chunk-7VJP2IMS.js";
28
+ } from "./chunk-M264FY2V.js";
29
29
  import {
30
30
  trackQualityCheck,
31
31
  trackQualityVerdict
32
- } from "./chunk-TB24VJ4Z.js";
32
+ } from "./chunk-P7ABQEFK.js";
33
33
 
34
34
  // src/server.ts
35
35
  import { McpServer as McpServer2 } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -85,6 +85,23 @@ function registerKnowledgeTools(server) {
85
85
  },
86
86
  async ({ entryId, name, status: rawStatus, workflowStatus: rawWorkflowStatus, data, order, canonicalKey, autoPublish, changeNote }) => {
87
87
  requireWriteAccess();
88
+ const PROMOTED_FIELDS = ["status", "workflowStatus", "name", "order", "canonicalKey"];
89
+ const topLevelByField = { status: rawStatus, workflowStatus: rawWorkflowStatus, name, order, canonicalKey };
90
+ const confusedFields = [];
91
+ if (data) {
92
+ for (const field of PROMOTED_FIELDS) {
93
+ if (field in data && topLevelByField[field] === void 0) {
94
+ confusedFields.push(field);
95
+ }
96
+ }
97
+ }
98
+ const fieldsProvided = [];
99
+ if (name !== void 0) fieldsProvided.push("name");
100
+ if (rawStatus !== void 0) fieldsProvided.push("status");
101
+ if (rawWorkflowStatus !== void 0) fieldsProvided.push("workflowStatus");
102
+ if (data !== void 0) fieldsProvided.push("data");
103
+ if (order !== void 0) fieldsProvided.push("order");
104
+ if (canonicalKey !== void 0) fieldsProvided.push("canonicalKey");
88
105
  let status = rawStatus;
89
106
  let workflowStatus = rawWorkflowStatus;
90
107
  let deprecationWarning;
@@ -120,13 +137,25 @@ function registerKnowledgeTools(server) {
120
137
  `Internal ID: ${id}`,
121
138
  `**Workspace:** ${wsCtx.workspaceSlug} (${wsCtx.workspaceId})`
122
139
  ];
140
+ if (fieldsProvided.length > 0) {
141
+ responseLines.push(`**Fields provided:** ${fieldsProvided.join(", ")}`);
142
+ } else {
143
+ responseLines.push("");
144
+ responseLines.push("\u26A0\uFE0F No fields to update were provided \u2014 only `entryId` was set.");
145
+ }
123
146
  if (deprecationWarning) {
124
147
  responseLines.push("");
125
148
  responseLines.push(deprecationWarning);
126
149
  }
150
+ if (confusedFields.length > 0) {
151
+ responseLines.push("");
152
+ for (const field of confusedFields) {
153
+ responseLines.push(`\u26A0\uFE0F \`data.${field}\` detected \u2014 did you mean the top-level \`${field}\` parameter?`);
154
+ }
155
+ }
127
156
  return {
128
157
  content: [{ type: "text", text: responseLines.join("\n") }],
129
- structuredContent: { entryId: id, versionMode }
158
+ structuredContent: { entryId: id, versionMode, fieldsProvided, confusedFields }
130
159
  };
131
160
  }
132
161
  );
@@ -166,7 +195,7 @@ ${formatted}` }]
166
195
  },
167
196
  async ({ entryId }) => {
168
197
  requireWriteAccess();
169
- const { runContradictionCheck } = await import("./smart-capture-E53YEHHO.js");
198
+ const { runContradictionCheck } = await import("./smart-capture-GH4CXVVX.js");
170
199
  const entry = await mcpQuery("chain.getEntry", { entryId });
171
200
  if (!entry) {
172
201
  return {
@@ -2828,7 +2857,8 @@ var DIMENSIONS = [
2828
2857
  "elements",
2829
2858
  "architecture",
2830
2859
  "risks",
2831
- "boundaries"
2860
+ "boundaries",
2861
+ "done_when"
2832
2862
  ];
2833
2863
  var DIMENSION_LABELS = {
2834
2864
  problem_clarity: "Problem Clarity",
@@ -2836,7 +2866,8 @@ var DIMENSION_LABELS = {
2836
2866
  elements: "Element Decomposition",
2837
2867
  architecture: "Architecture Grounding",
2838
2868
  risks: "Risk Coverage",
2839
- boundaries: "Boundary Specification"
2869
+ boundaries: "Boundary Specification",
2870
+ done_when: "Done-When Quality"
2840
2871
  };
2841
2872
  var SUGGESTED_ORDER = [
2842
2873
  "problem_clarity",
@@ -2844,7 +2875,16 @@ var SUGGESTED_ORDER = [
2844
2875
  "elements",
2845
2876
  "architecture",
2846
2877
  "risks",
2847
- "boundaries"
2878
+ "boundaries",
2879
+ "done_when"
2880
+ ];
2881
+ var PHASE_ORDER = [
2882
+ "context",
2883
+ "framing",
2884
+ "elements",
2885
+ "derisking",
2886
+ "validation",
2887
+ "capture"
2848
2888
  ];
2849
2889
  var PHASE_LABELS = {
2850
2890
  context: "Phase 0: Gather Context",
@@ -2854,18 +2894,39 @@ var PHASE_LABELS = {
2854
2894
  validation: "Phase 4: Validate & Contract",
2855
2895
  capture: "Phase 5: Capture & Commit"
2856
2896
  };
2857
- function inferPhase(scorecard, isSmallBatch = false) {
2858
- if (scorecard.problem_clarity === 0 && scorecard.appetite === 0) return "context";
2859
- if (scorecard.problem_clarity < 6 || scorecard.appetite < 6) return "framing";
2860
- const archGate = isSmallBatch ? true : scorecard.architecture >= 4;
2861
- if (scorecard.elements < 6 || !archGate) return "elements";
2862
- if (scorecard.risks < 6 || scorecard.boundaries < 4) return "derisking";
2863
- const activeDims = activeDimensions(isSmallBatch);
2864
- const allAboveThreshold = activeDims.every((d) => scorecard[d] >= 4);
2865
- if (!allAboveThreshold) return "derisking";
2866
- const highQuality = activeDims.every((d) => scorecard[d] >= 6);
2867
- if (!highQuality) return "validation";
2868
- return "capture";
2897
+ function contentFloor(content, isSmallBatch) {
2898
+ const hasElements = content.elementCount >= 1;
2899
+ const hasDerisking = content.riskCount >= 1 || content.noGoCount >= 1;
2900
+ const hasArchOrSkipped = isSmallBatch || !!content.hasArchitectureText;
2901
+ const wellShaped = content.elementCount >= 2 && hasDerisking && hasArchOrSkipped;
2902
+ if (wellShaped && content.riskCount >= 1 && content.noGoCount >= 2) return "validation";
2903
+ if (hasDerisking) return "derisking";
2904
+ if (hasElements) return "elements";
2905
+ return "context";
2906
+ }
2907
+ function laterPhase(a, b) {
2908
+ return PHASE_ORDER.indexOf(a) >= PHASE_ORDER.indexOf(b) ? a : b;
2909
+ }
2910
+ function inferPhase(scorecard, isSmallBatch = false, content) {
2911
+ let scorePhase;
2912
+ if (scorecard.problem_clarity === 0 && scorecard.appetite === 0) scorePhase = "context";
2913
+ else if (scorecard.problem_clarity < 6 || scorecard.appetite < 6) scorePhase = "framing";
2914
+ else {
2915
+ const archGate = isSmallBatch ? true : scorecard.architecture >= 4;
2916
+ if (scorecard.elements < 6 || !archGate) scorePhase = "elements";
2917
+ else if (scorecard.risks < 6 || scorecard.boundaries < 4 || scorecard.done_when < 4) scorePhase = "derisking";
2918
+ else {
2919
+ const activeDims = activeDimensions(isSmallBatch);
2920
+ const allAboveThreshold = activeDims.every((d) => scorecard[d] >= 4);
2921
+ if (!allAboveThreshold) scorePhase = "derisking";
2922
+ else {
2923
+ const highQuality = activeDims.every((d) => scorecard[d] >= 6);
2924
+ scorePhase = highQuality ? "capture" : "validation";
2925
+ }
2926
+ }
2927
+ }
2928
+ if (!content) return scorePhase;
2929
+ return laterPhase(scorePhase, contentFloor(content, isSmallBatch));
2869
2930
  }
2870
2931
  function activeDimensions(isSmallBatch) {
2871
2932
  return isSmallBatch ? SUGGESTED_ORDER.filter((d) => d !== "architecture") : SUGGESTED_ORDER;
@@ -3007,6 +3068,20 @@ var ARCH_QUALITY_SIGNALS = [
3007
3068
  "decouple",
3008
3069
  "isolat"
3009
3070
  ];
3071
+ var DONE_WHEN_SIGNALS = [
3072
+ "done when",
3073
+ "acceptance criteria",
3074
+ "verified",
3075
+ "test plan",
3076
+ "validation",
3077
+ "rollback",
3078
+ "gate",
3079
+ "threshold",
3080
+ "metric",
3081
+ "success",
3082
+ "p95",
3083
+ "p99"
3084
+ ];
3010
3085
  function countMatches(text, signals) {
3011
3086
  const lower = text.toLowerCase();
3012
3087
  return signals.filter((s) => lower.includes(s)).length;
@@ -3018,20 +3093,27 @@ function scoreProblemClarity(ctx) {
3018
3093
  const text = ctx.dimensionTexts.problem_clarity;
3019
3094
  const missing = [];
3020
3095
  const satisfied = [];
3096
+ const criteria = [];
3021
3097
  let score = 0;
3022
- if (countMatches(text, WORKAROUND_SIGNALS) > 0) {
3098
+ const hasWorkaround = countMatches(text, WORKAROUND_SIGNALS) > 0;
3099
+ criteria.push({ label: "Current workaround described", met: hasWorkaround, weight: 3 });
3100
+ if (hasWorkaround) {
3023
3101
  satisfied.push("Current workaround described");
3024
3102
  score += 3;
3025
3103
  } else {
3026
3104
  missing.push("Describe the current workaround \u2014 how do people deal with this today?");
3027
3105
  }
3028
- if (countMatches(text, AFFECTED_SIGNALS) > 0) {
3106
+ const hasAffected = countMatches(text, AFFECTED_SIGNALS) > 0;
3107
+ criteria.push({ label: "Affected people identified", met: hasAffected, weight: 2 });
3108
+ if (hasAffected) {
3029
3109
  satisfied.push("Affected people identified");
3030
3110
  score += 2;
3031
3111
  } else {
3032
3112
  missing.push("Who experiences this problem? Be specific about the role or persona.");
3033
3113
  }
3034
- if (countMatches(text, FREQUENCY_SIGNALS) > 0) {
3114
+ const hasFrequency = countMatches(text, FREQUENCY_SIGNALS) > 0;
3115
+ criteria.push({ label: "Frequency or severity stated", met: hasFrequency, weight: 2 });
3116
+ if (hasFrequency) {
3035
3117
  satisfied.push("Frequency or severity stated");
3036
3118
  score += 2;
3037
3119
  } else {
@@ -3039,58 +3121,75 @@ function scoreProblemClarity(ctx) {
3039
3121
  }
3040
3122
  if (ctx.existingEntryIds.length > 0) {
3041
3123
  satisfied.push(`Differentiated from ${ctx.existingEntryIds.length} existing entries`);
3124
+ criteria.push({ label: "Differentiated from existing entries", met: true, weight: 2 });
3042
3125
  score += 2;
3043
3126
  } else if (/\b(TEN|DEC|ENT|FEAT|BET)-\w+/i.test(text)) {
3044
3127
  satisfied.push("References existing Chain entries");
3128
+ criteria.push({ label: "References existing Chain entries", met: true, weight: 1 });
3045
3129
  score += 1;
3046
3130
  } else {
3047
3131
  missing.push("How does this differ from existing tensions or bets on the Chain?");
3132
+ criteria.push({ label: "Differentiated from existing entries", met: false, weight: 2 });
3048
3133
  }
3049
- if (text.length > 200) {
3134
+ const isSubstantive = text.length > 200;
3135
+ criteria.push({ label: "Substantive description provided", met: isSubstantive, weight: 1 });
3136
+ if (isSubstantive) {
3050
3137
  satisfied.push("Substantive description provided");
3051
3138
  score += 1;
3052
3139
  }
3053
- return { score: clamp(score, 0, 10), missing, satisfied };
3140
+ return { score: clamp(score, 0, 10), missing, satisfied, criteria };
3054
3141
  }
3055
3142
  function scoreAppetite(ctx) {
3056
3143
  const text = ctx.dimensionTexts.appetite;
3057
3144
  const missing = [];
3058
3145
  const satisfied = [];
3146
+ const criteria = [];
3059
3147
  let score = 0;
3060
3148
  const hasSizeDecl = /small\s*batch|big\s*batch|[1-6]\s*week/i.test(text);
3061
3149
  const hasTimeSignals = countMatches(text, APPETITE_SIGNALS) > 0;
3062
3150
  if (hasSizeDecl) {
3063
3151
  satisfied.push("Size and timeframe declared");
3152
+ criteria.push({ label: "Size and timeframe declared", met: true, weight: 4 });
3064
3153
  score += 4;
3065
3154
  } else if (hasTimeSignals) {
3066
3155
  satisfied.push("Time constraint mentioned");
3156
+ criteria.push({ label: "Size and timeframe declared", met: false, weight: 4 });
3157
+ criteria.push({ label: "Time constraint mentioned", met: true, weight: 2 });
3067
3158
  score += 2;
3068
3159
  missing.push("Declare the batch size explicitly \u2014 Small Batch (1-2 weeks) or Big Batch (6 weeks).");
3069
3160
  } else {
3161
+ criteria.push({ label: "Size and timeframe declared", met: false, weight: 4 });
3070
3162
  missing.push("Set a time constraint \u2014 how long is this bet allowed to take?");
3071
3163
  }
3072
- if (countMatches(text, SCOPE_SIGNALS) > 0) {
3164
+ const hasTradeoffs = countMatches(text, SCOPE_SIGNALS) > 0;
3165
+ criteria.push({ label: "Scope bounded with trade-offs", met: hasTradeoffs, weight: 2 });
3166
+ if (hasTradeoffs) {
3073
3167
  satisfied.push("Scope bounded with trade-offs");
3074
3168
  score += 2;
3075
3169
  } else {
3076
3170
  missing.push("What trade-offs are you making? What's the 80% cut?");
3077
3171
  }
3078
- if (/phase\s*[a-c1-3]|phased|milestone/i.test(text)) {
3172
+ const hasPhased = /phase\s*[a-c1-3]|phased|milestone/i.test(text);
3173
+ criteria.push({ label: "Phased delivery strategy", met: hasPhased, weight: 2 });
3174
+ if (hasPhased) {
3079
3175
  satisfied.push("Phased delivery strategy");
3080
3176
  score += 2;
3081
3177
  }
3082
- if (/gate|validate|dogfood|benchmark|circuit.?breaker/i.test(text)) {
3178
+ const hasGate = /gate|validate|dogfood|benchmark|circuit.?breaker/i.test(text);
3179
+ criteria.push({ label: "Validation gate defined", met: hasGate, weight: 2 });
3180
+ if (hasGate) {
3083
3181
  satisfied.push("Validation gate defined");
3084
3182
  score += 2;
3085
3183
  } else {
3086
3184
  missing.push("How will you know if this bet is working before full commitment?");
3087
3185
  }
3088
- return { score: clamp(score, 0, 10), missing, satisfied };
3186
+ return { score: clamp(score, 0, 10), missing, satisfied, criteria };
3089
3187
  }
3090
3188
  function scoreElements(ctx) {
3091
3189
  const text = ctx.dimensionTexts.elements;
3092
3190
  const missing = [];
3093
3191
  const satisfied = [];
3192
+ const criteria = [];
3094
3193
  let score = 0;
3095
3194
  const headingCount = (text.match(/###\s*Element\s*\d/gi) ?? []).length;
3096
3195
  const inlineCount = (text.match(/element\s*\d|component\s*\d|piece\s*\d/gi) ?? []).length;
@@ -3098,52 +3197,76 @@ function scoreElements(ctx) {
3098
3197
  const numberedCount = (text.match(/^\s*\d+[.)]\s/gm) ?? []).length;
3099
3198
  const namedCount = (text.match(/\belement\s+\d|[A-Z][a-z]+ (?:Engine|Store|Gate|Manager|Service|Module|Handler|Layer|Registry|Pipeline)\b/g) ?? []).length;
3100
3199
  const textDescribed = Math.max(headingCount, inlineCount, ordinalCount, numberedCount, namedCount);
3200
+ let elemMet = true;
3201
+ let elemWeight = 0;
3101
3202
  if (ctx.elementCount >= 3) {
3102
3203
  satisfied.push(`${ctx.elementCount} elements captured on Chain`);
3204
+ elemWeight = 5;
3103
3205
  score += 5;
3104
3206
  } else if (ctx.elementCount > 0) {
3105
3207
  satisfied.push(`${ctx.elementCount} element(s) captured`);
3208
+ elemWeight = ctx.elementCount * 2;
3106
3209
  score += ctx.elementCount * 2;
3107
3210
  if (ctx.elementCount < 3) {
3108
3211
  missing.push(`Identify ${3 - ctx.elementCount} more solution elements for adequate decomposition.`);
3109
3212
  }
3110
3213
  } else if (textDescribed >= 3) {
3111
3214
  satisfied.push(`${textDescribed} elements described in text`);
3215
+ elemWeight = 4;
3112
3216
  score += 4;
3113
3217
  missing.push("Capture these elements as typed entries on the Chain.");
3114
3218
  } else if (textDescribed > 0) {
3115
3219
  satisfied.push(`${textDescribed} element(s) described in text`);
3220
+ elemWeight = textDescribed * 2;
3116
3221
  score += textDescribed * 2;
3117
3222
  missing.push("Identify more solution elements \u2014 aim for 3+ independently describable pieces.");
3118
3223
  } else {
3224
+ elemMet = false;
3225
+ elemWeight = 5;
3119
3226
  missing.push("Identify solution elements \u2014 breadboard-level pieces, each independently describable.");
3120
3227
  }
3228
+ criteria.push({ label: "Solution elements identified", met: elemMet, weight: elemWeight });
3121
3229
  const totalElements = Math.max(ctx.elementCount, textDescribed);
3230
+ const decompMet = totalElements >= 2;
3231
+ let decompWeight;
3122
3232
  if (totalElements >= 3) {
3123
3233
  satisfied.push("Multiple independently describable pieces");
3234
+ decompWeight = 3;
3124
3235
  score += 3;
3125
3236
  } else if (totalElements >= 2) {
3126
3237
  satisfied.push("Multiple independently describable pieces");
3238
+ decompWeight = 2;
3127
3239
  score += 2;
3240
+ } else {
3241
+ decompWeight = 3;
3128
3242
  }
3129
- if (ctx.governanceCount > 0) {
3243
+ criteria.push({ label: "Adequate decomposition (3+ pieces)", met: decompMet, weight: decompWeight });
3244
+ const govMet = ctx.governanceCount > 0;
3245
+ criteria.push({ label: "Governance constraints applied", met: govMet, weight: 1 });
3246
+ if (govMet) {
3130
3247
  satisfied.push(`${ctx.governanceCount} governance entries constrain the solution`);
3131
3248
  score += 1;
3132
3249
  }
3133
- return { score: clamp(score, 0, 10), missing, satisfied };
3250
+ return { score: clamp(score, 0, 10), missing, satisfied, criteria };
3134
3251
  }
3135
3252
  function scoreArchitecture(ctx) {
3136
3253
  const text = ctx.dimensionTexts.architecture;
3137
3254
  const missing = [];
3138
3255
  const satisfied = [];
3256
+ const criteria = [];
3139
3257
  let score = 0;
3140
- if (ctx.hasArchitectureText) {
3258
+ const hasArchSection = ctx.hasArchitectureText;
3259
+ criteria.push({ label: "Architecture section present", met: hasArchSection, weight: 3 });
3260
+ if (hasArchSection) {
3141
3261
  satisfied.push("Architecture section present in bet");
3142
3262
  score += 3;
3143
3263
  } else {
3144
3264
  missing.push("Where does this live in the architecture? Name each layer and what belongs where.");
3145
3265
  }
3146
3266
  const archSignals = countMatches(text, ARCHITECTURE_SIGNALS);
3267
+ const archVocabMet = archSignals > 0;
3268
+ const archVocabWeight = archSignals >= 3 ? 2 : archSignals > 0 ? 1 : 2;
3269
+ criteria.push({ label: "Architecture vocabulary used", met: archVocabMet, weight: archVocabWeight });
3147
3270
  if (archSignals >= 3) {
3148
3271
  satisfied.push("Architecture vocabulary used");
3149
3272
  score += 2;
@@ -3152,101 +3275,181 @@ function scoreArchitecture(ctx) {
3152
3275
  } else {
3153
3276
  missing.push("Describe the API boundary, modules, and services involved.");
3154
3277
  }
3155
- if (countMatches(text, ARCH_QUALITY_SIGNALS) > 0) {
3278
+ const hasDependencyDir = countMatches(text, ARCH_QUALITY_SIGNALS) > 0;
3279
+ criteria.push({ label: "Dependency direction specified", met: hasDependencyDir, weight: 2 });
3280
+ if (hasDependencyDir) {
3156
3281
  satisfied.push("Dependency direction / boundaries specified");
3157
3282
  score += 2;
3158
3283
  } else {
3159
3284
  missing.push("What dependencies does this create or remove? Specify direction.");
3160
3285
  }
3161
- if (/mermaid|diagram|flowchart|graph/i.test(text)) {
3286
+ const hasDiagram = /mermaid|diagram|flowchart|graph/i.test(text);
3287
+ criteria.push({ label: "Visual architecture diagram", met: hasDiagram, weight: 2 });
3288
+ if (hasDiagram) {
3162
3289
  satisfied.push("Visual architecture representation");
3163
3290
  score += 2;
3164
3291
  } else {
3165
3292
  missing.push("Add an architecture diagram (Mermaid) showing layers and data flow.");
3166
3293
  }
3167
- if (ctx.governanceCount > 0) {
3294
+ const govMet = ctx.governanceCount > 0;
3295
+ criteria.push({ label: "Checked against governance", met: govMet, weight: 1 });
3296
+ if (govMet) {
3168
3297
  satisfied.push("Architecture checked against governance");
3169
3298
  score += 1;
3170
3299
  }
3171
- return { score: clamp(score, 0, 10), missing, satisfied };
3300
+ return { score: clamp(score, 0, 10), missing, satisfied, criteria };
3172
3301
  }
3173
3302
  function scoreRisks(ctx) {
3174
3303
  const text = ctx.dimensionTexts.risks;
3175
3304
  const missing = [];
3176
3305
  const satisfied = [];
3306
+ const criteria = [];
3177
3307
  let score = 0;
3178
3308
  const riskSignalCount = countMatches(text, RISK_SIGNALS);
3179
3309
  const textRiskCount = (text.match(/rabbit hole|risk:|risk \d|R\d:/gi) ?? []).length;
3180
3310
  const totalRisks = Math.max(ctx.riskCount, textRiskCount, Math.min(riskSignalCount, 3));
3311
+ let riskMet = true;
3312
+ let riskWeight = 0;
3181
3313
  if (ctx.riskCount >= 2) {
3182
3314
  satisfied.push(`${ctx.riskCount} risks captured on Chain`);
3315
+ riskWeight = 4;
3183
3316
  score += 4;
3184
3317
  } else if (totalRisks >= 2) {
3185
3318
  satisfied.push(`${totalRisks} risks identified`);
3319
+ riskWeight = 3;
3186
3320
  score += 3;
3187
3321
  if (ctx.riskCount === 0) missing.push("Capture identified risks as entries on the Chain.");
3188
3322
  } else if (totalRisks > 0 || riskSignalCount > 0) {
3189
3323
  satisfied.push("Risk language present");
3324
+ riskWeight = 2;
3190
3325
  score += 2;
3191
3326
  missing.push("Identify more risks \u2014 aim for 2+ rabbit holes with mitigations.");
3192
3327
  } else {
3328
+ riskMet = false;
3329
+ riskWeight = 4;
3193
3330
  missing.push("Name the rabbit holes \u2014 what could go wrong, what's unknown?");
3194
3331
  }
3332
+ criteria.push({ label: "Risks identified", met: riskMet, weight: riskWeight });
3195
3333
  const mitigationCount = countMatches(text, MITIGATION_SIGNALS);
3334
+ let mitigMet = mitigationCount > 0;
3335
+ let mitigWeight;
3196
3336
  if (mitigationCount >= 2) {
3197
3337
  satisfied.push("Multiple mitigations specified");
3338
+ mitigWeight = 3;
3198
3339
  score += 3;
3199
3340
  } else if (mitigationCount > 0) {
3200
3341
  satisfied.push("Mitigation present");
3342
+ mitigWeight = 2;
3201
3343
  score += 2;
3202
3344
  missing.push("Each risk should have a mitigation or explicit acceptance.");
3203
3345
  } else {
3346
+ mitigMet = false;
3347
+ mitigWeight = 3;
3204
3348
  missing.push("Each risk needs a mitigation or explicit acceptance.");
3205
3349
  }
3206
- if (/codebase|architecture|coupling|dependency|schema|migration/i.test(text)) {
3350
+ criteria.push({ label: "Mitigations specified", met: mitigMet, weight: mitigWeight });
3351
+ const hasCodebaseRisks = /codebase|architecture|coupling|dependency|schema|migration/i.test(text);
3352
+ criteria.push({ label: "Codebase-level risks identified", met: hasCodebaseRisks, weight: 2 });
3353
+ if (hasCodebaseRisks) {
3207
3354
  satisfied.push("Codebase-level risks identified");
3208
3355
  score += 2;
3209
3356
  } else {
3210
3357
  missing.push("Are there high-risk areas in the codebase? Check architecture and dependencies.");
3211
3358
  }
3212
- if (totalRisks >= 3) {
3359
+ const thorough = totalRisks >= 3;
3360
+ criteria.push({ label: "Thorough risk coverage", met: thorough, weight: 1 });
3361
+ if (thorough) {
3213
3362
  satisfied.push("Thorough risk coverage");
3214
3363
  score += 1;
3215
3364
  }
3216
- return { score: clamp(score, 0, 10), missing, satisfied };
3365
+ return { score: clamp(score, 0, 10), missing, satisfied, criteria };
3217
3366
  }
3218
3367
  function scoreBoundaries(ctx) {
3219
3368
  const text = ctx.dimensionTexts.boundaries;
3220
3369
  const missing = [];
3221
3370
  const satisfied = [];
3371
+ const criteria = [];
3222
3372
  let score = 0;
3223
3373
  const textNoGos = Math.max(countMatches(text, NOGO_SIGNALS), (text.match(/\bwon'?t\b|\bwill not\b/gi) ?? []).length);
3224
- const totalNoGos = Math.min(ctx.noGoCount + textNoGos, 10);
3374
+ const totalNoGos = Math.max(ctx.noGoCount, textNoGos);
3375
+ let nogoMet = totalNoGos > 0;
3376
+ let nogoWeight;
3225
3377
  if (totalNoGos >= 3) {
3226
3378
  satisfied.push(`${totalNoGos} explicit no-gos declared`);
3379
+ nogoWeight = 5;
3227
3380
  score += 5;
3228
3381
  } else if (totalNoGos > 0) {
3229
3382
  satisfied.push(`${totalNoGos} no-go(s) declared`);
3383
+ nogoWeight = totalNoGos * 2;
3230
3384
  score += totalNoGos * 2;
3231
3385
  missing.push("Add more explicit no-gos to prevent scope creep.");
3232
3386
  } else {
3387
+ nogoMet = false;
3388
+ nogoWeight = 5;
3233
3389
  missing.push("Declare what you're NOT building \u2014 explicit no-gos prevent scope creep.");
3234
3390
  }
3235
- if (/scope creep|prevent|boundary|limit|constrain|excluded/i.test(text)) {
3391
+ criteria.push({ label: "No-gos declared", met: nogoMet, weight: nogoWeight });
3392
+ const hasScopeCreep = /scope creep|prevent|boundary|limit|constrain|excluded/i.test(text);
3393
+ criteria.push({ label: "Scope creep prevention language", met: hasScopeCreep, weight: 2 });
3394
+ if (hasScopeCreep) {
3236
3395
  satisfied.push("Scope creep prevention language");
3237
3396
  score += 2;
3238
3397
  }
3239
- if (/direction|instead.*will|rather.*than|not.*but/i.test(text)) {
3398
+ const hasDirectional = /direction|instead.*will|rather.*than|not.*but/i.test(text);
3399
+ criteria.push({ label: "Directional no-gos", met: hasDirectional, weight: 2 });
3400
+ if (hasDirectional) {
3240
3401
  satisfied.push("Each no-go prevents scope creep in a specific direction");
3241
3402
  score += 2;
3242
3403
  } else if (totalNoGos > 0) {
3243
3404
  missing.push("Each no-go should prevent scope creep in a specific direction.");
3244
3405
  }
3245
- if (totalNoGos >= 5) {
3406
+ const comprehensive = totalNoGos >= 5;
3407
+ criteria.push({ label: "Comprehensive boundary specification", met: comprehensive, weight: 1 });
3408
+ if (comprehensive) {
3246
3409
  satisfied.push("Comprehensive boundary specification");
3247
3410
  score += 1;
3248
3411
  }
3249
- return { score: clamp(score, 0, 10), missing, satisfied };
3412
+ return { score: clamp(score, 0, 10), missing, satisfied, criteria };
3413
+ }
3414
+ function scoreDoneWhen(ctx) {
3415
+ const text = ctx.dimensionTexts.done_when ?? "";
3416
+ const missing = [];
3417
+ const satisfied = [];
3418
+ const criteria = [];
3419
+ let score = 0;
3420
+ const hasSection = text.trim().length > 0;
3421
+ criteria.push({ label: "Done-when section present", met: hasSection, weight: 3 });
3422
+ if (hasSection) {
3423
+ satisfied.push("Done-when section present");
3424
+ score += 3;
3425
+ } else {
3426
+ missing.push("Define explicit done-when criteria.");
3427
+ }
3428
+ const hasMeasurableTargets = /\b\d+%|\bp\d{2}|\b>=?\s*\d+|\b<=?\s*\d+|target|threshold|SLO|SLA|median/i.test(text);
3429
+ criteria.push({ label: "Measurable targets defined", met: hasMeasurableTargets, weight: 3 });
3430
+ if (hasMeasurableTargets) {
3431
+ satisfied.push("Measurable targets defined");
3432
+ score += 3;
3433
+ } else if (hasSection) {
3434
+ missing.push("Add measurable targets (thresholds, percentages, latency targets, or numeric criteria).");
3435
+ }
3436
+ const hasValidationPlan = /\btest|verify|validation|contract test|rollback|drill|gate/i.test(text);
3437
+ criteria.push({ label: "Validation plan specified", met: hasValidationPlan, weight: 2 });
3438
+ if (hasValidationPlan) {
3439
+ satisfied.push("Validation plan specified");
3440
+ score += 2;
3441
+ } else if (hasSection) {
3442
+ missing.push("Specify how success will be validated (tests, gates, rollback drills).");
3443
+ }
3444
+ const hasSignals = countMatches(text, DONE_WHEN_SIGNALS) >= 3;
3445
+ criteria.push({ label: "Operational signal coverage", met: hasSignals, weight: 2 });
3446
+ if (hasSignals) {
3447
+ satisfied.push("Operational signal coverage");
3448
+ score += 2;
3449
+ } else if (hasSection) {
3450
+ missing.push("Include operational signals (metrics, gates, and release thresholds).");
3451
+ }
3452
+ return { score: clamp(score, 0, 10), missing, satisfied, criteria };
3250
3453
  }
3251
3454
  var SCORERS = {
3252
3455
  problem_clarity: scoreProblemClarity,
@@ -3254,7 +3457,8 @@ var SCORERS = {
3254
3457
  elements: scoreElements,
3255
3458
  architecture: scoreArchitecture,
3256
3459
  risks: scoreRisks,
3257
- boundaries: scoreBoundaries
3460
+ boundaries: scoreBoundaries,
3461
+ done_when: scoreDoneWhen
3258
3462
  };
3259
3463
  function scoreDimension(dimension, ctx) {
3260
3464
  const raw = SCORERS[dimension](ctx);
@@ -3281,9 +3485,27 @@ function buildScorecard(ctx) {
3281
3485
  elements: results.elements.score,
3282
3486
  architecture: results.architecture.score,
3283
3487
  risks: results.risks.score,
3284
- boundaries: results.boundaries.score
3488
+ boundaries: results.boundaries.score,
3489
+ done_when: results.done_when.score
3285
3490
  };
3286
3491
  }
3492
+ function buildDetailedScorecard(ctx, opts) {
3493
+ const results = scoreAll(ctx);
3494
+ const scorecard = {};
3495
+ const criteria = {};
3496
+ for (const dim of DIMENSIONS) {
3497
+ scorecard[dim] = results[dim].score;
3498
+ criteria[dim] = results[dim].criteria;
3499
+ }
3500
+ if (opts?.isSmallBatch) scorecard.architecture = -1;
3501
+ return { scorecard, criteria };
3502
+ }
3503
+ function isCaptureReady(scorecard, isSmallBatch) {
3504
+ const completed = completedDimensions(scorecard, 6, isSmallBatch);
3505
+ const dims = activeDimensions(isSmallBatch);
3506
+ const ready = completed.length >= (isSmallBatch ? 5 : 6) && dims.every((d) => scorecard[d] >= 4);
3507
+ return { ready, completed };
3508
+ }
3287
3509
  function inferActiveDimension(scorecard, isSmallBatch = false) {
3288
3510
  const order = activeDimensions(isSmallBatch);
3289
3511
  const firstZero = order.find((d) => scorecard[d] === 0);
@@ -3358,45 +3580,51 @@ function extractPhrase(text, maxLen) {
3358
3580
  function capitalize(s) {
3359
3581
  return s.charAt(0).toUpperCase() + s.slice(1);
3360
3582
  }
3583
+ function isBalanced(text) {
3584
+ const opens = (text.match(/[("']/g) ?? []).length;
3585
+ const closes = (text.match(/[)"']/g) ?? []).length;
3586
+ return Math.abs(opens - closes) <= 1;
3587
+ }
3361
3588
  var PHASE_INVESTIGATIONS = {
3362
- context: (betName) => ({
3589
+ context: (searchTerm) => ({
3363
3590
  phase: "context",
3364
3591
  dimension: "problem_clarity",
3365
3592
  tasks: [
3366
- { target: "chain", query: `Search for entries related to: ${betName}`, purpose: "Find existing tensions, decisions, and bets that overlap" },
3593
+ { target: "chain", query: `Search for entries related to: ${searchTerm}`, purpose: "Find existing tensions, decisions, and bets that overlap" },
3367
3594
  { target: "chain", query: "List active governance: principles, standards, business-rules", purpose: "Surface constraints the solution must honor" },
3368
- { target: "codebase", query: `Search for code related to: ${betName}`, purpose: "Understand current implementation and pain points" }
3595
+ { target: "codebase", query: `Search for code related to: ${searchTerm}`, purpose: "Understand current implementation and pain points" }
3369
3596
  ],
3370
3597
  proposalGuidance: "Synthesize findings into a 3-5 line context brief: what the Chain knows, what the codebase reveals, what governance applies.",
3371
3598
  reactionPrompt: "Here's what I found. Does this match what you're seeing, or is the problem different from what the Chain suggests?"
3372
3599
  }),
3373
- framing: (betName) => ({
3600
+ framing: (searchTerm) => ({
3374
3601
  phase: "framing",
3375
3602
  dimension: "problem_clarity",
3376
3603
  tasks: [
3377
- { target: "codebase", query: `Find workarounds, TODOs, or hacks related to: ${betName}`, purpose: "Surface concrete evidence of the problem" },
3378
- { target: "chain", query: "Search for related tensions and user complaints", purpose: "Quantify how often this problem occurs" }
3604
+ { target: "codebase", query: `Find workarounds, TODOs, or hacks related to: ${searchTerm}`, purpose: "Surface concrete evidence of the problem" },
3605
+ { target: "chain", query: `Search for tensions related to: ${searchTerm}`, purpose: "Quantify how often this problem occurs" }
3379
3606
  ],
3380
3607
  proposalGuidance: "Propose a problem statement based on codebase evidence. Include who's affected and what the workaround looks like in the code.",
3381
3608
  reactionPrompt: "Based on the codebase, here's a draft problem statement. Does this capture it, or is the real problem different?"
3382
3609
  }),
3383
- elements: (betName) => ({
3610
+ elements: (searchTerm, _betEntryId, elementNames) => ({
3384
3611
  phase: "elements",
3385
3612
  dimension: "elements",
3386
3613
  tasks: [
3387
- { target: "codebase", query: `Find modules, services, and components that would be affected by: ${betName}`, purpose: "Map the solution to existing architecture" },
3614
+ { target: "codebase", query: `Find modules, services, and components that would be affected by: ${searchTerm}`, purpose: "Map the solution to existing architecture" },
3615
+ ...elementNames?.length ? [{ target: "codebase", query: `Locate existing code paths for these elements: ${elementNames.join(", ")}`, purpose: "Ground each proposed element in current implementation" }] : [],
3388
3616
  { target: "architecture", query: "Identify layer boundaries, API contracts, and dependency directions", purpose: "Ground elements in real architecture" },
3389
3617
  { target: "chain", query: "Check DEC-31, STA-3, and other architecture standards", purpose: "Ensure elements respect existing decisions" }
3390
3618
  ],
3391
3619
  proposalGuidance: "Propose 3-5 solution elements at breadboard level. Each element should name the layer it lives in, what it does, and how it connects to existing code.",
3392
3620
  reactionPrompt: "I've mapped out these solution elements based on the codebase. Which feel right? Which need adjustment?"
3393
3621
  }),
3394
- derisking: (betName) => ({
3622
+ derisking: (searchTerm, _betEntryId, elementNames) => ({
3395
3623
  phase: "derisking",
3396
3624
  dimension: "risks",
3397
3625
  tasks: [
3398
- { target: "codebase", query: `Find fragile code, tight coupling, or missing tests near: ${betName}`, purpose: "Surface technical risks from code" },
3399
- { target: "codebase", query: "Check for performance-sensitive paths, database queries, and external dependencies", purpose: "Identify performance and reliability risks" },
3626
+ { target: "codebase", query: `Find fragile code, tight coupling, or missing tests near: ${searchTerm}`, purpose: "Surface technical risks from code" },
3627
+ ...elementNames?.length ? [{ target: "codebase", query: `Check implementation complexity for: ${elementNames.join(", ")}`, purpose: "Assess element-specific risks" }] : [{ target: "codebase", query: "Check for performance-sensitive paths, database queries, and external dependencies", purpose: "Identify performance and reliability risks" }],
3400
3628
  { target: "chain", query: "Search for related tensions and past decisions that constrain this solution", purpose: "Surface risks from existing constraints" }
3401
3629
  ],
3402
3630
  proposalGuidance: "Propose rabbit holes with severity and mitigations. Each risk should cite specific code or Chain evidence. Also propose no-gos \u2014 what should be explicitly excluded.",
@@ -3405,9 +3633,10 @@ var PHASE_INVESTIGATIONS = {
3405
3633
  validation: () => null,
3406
3634
  capture: () => null
3407
3635
  };
3408
- function buildInvestigationBrief(phase, betName, betEntryId) {
3636
+ function buildInvestigationBrief(phase, betName, betEntryId, betProblem, elementNames) {
3637
+ const searchTerm = betProblem?.slice(0, 200) || betName;
3409
3638
  const builder = PHASE_INVESTIGATIONS[phase];
3410
- return builder(betName, betEntryId);
3639
+ return builder(searchTerm, betEntryId, elementNames);
3411
3640
  }
3412
3641
  function generateBuildContract(ctx) {
3413
3642
  const lines = ["## Build Contract"];
@@ -3438,7 +3667,65 @@ function generateBuildContract(ctx) {
3438
3667
  return lines.join("\n");
3439
3668
  }
3440
3669
 
3670
+ // src/tools/facilitate-validation.ts
3671
+ function computeCommitBlockers(opts) {
3672
+ const { betEntryId, relations, betData, sessionDrafts } = opts;
3673
+ const blockers = [];
3674
+ const str = (key) => (betData[key] ?? "").trim();
3675
+ const hasStrategyLink = relations.some((r) => r.type === "commits_to");
3676
+ if (!hasStrategyLink) {
3677
+ blockers.push({
3678
+ entryId: betEntryId,
3679
+ blocker: "Missing strategy link",
3680
+ fix: `relations action=create from=${betEntryId} to=<strategy> type=commits_to`
3681
+ });
3682
+ }
3683
+ const MIN_FIELD_LENGTH = 20;
3684
+ if (str("problem").length < MIN_FIELD_LENGTH) {
3685
+ blockers.push({
3686
+ entryId: betEntryId,
3687
+ blocker: str("problem") ? "Problem statement too short (< 20 chars)" : "Missing problem statement",
3688
+ fix: `update-entry entryId="${betEntryId}" data.problem="<problem description>"`
3689
+ });
3690
+ }
3691
+ if (!str("appetite")) {
3692
+ blockers.push({
3693
+ entryId: betEntryId,
3694
+ blocker: "Missing appetite",
3695
+ fix: `update-entry entryId="${betEntryId}" data.appetite="<appetite declaration>"`
3696
+ });
3697
+ }
3698
+ if (str("elements").length < MIN_FIELD_LENGTH) {
3699
+ blockers.push({
3700
+ entryId: betEntryId,
3701
+ blocker: str("elements") ? "Solution elements too short (< 20 chars)" : "Missing solution elements",
3702
+ fix: `facilitate action=respond betEntryId="${betEntryId}" dimension="elements"`
3703
+ });
3704
+ }
3705
+ const hasFeatures = sessionDrafts.some((d) => d.collection === "features");
3706
+ if (!hasFeatures && str("elements").length >= MIN_FIELD_LENGTH) {
3707
+ blockers.push({
3708
+ entryId: betEntryId,
3709
+ blocker: "Elements described but no feature entries linked in constellation",
3710
+ fix: `facilitate action=respond betEntryId="${betEntryId}" capture={type:"element", name:"...", description:"..."}`
3711
+ });
3712
+ }
3713
+ for (const draft of sessionDrafts) {
3714
+ if (!draft.name || draft.name.trim().length === 0) {
3715
+ blockers.push({
3716
+ entryId: betEntryId,
3717
+ blocker: `Draft constellation entry is missing a name`,
3718
+ fix: `update-entry entryId="<draftId>" name="<name>"`
3719
+ });
3720
+ }
3721
+ }
3722
+ return blockers;
3723
+ }
3724
+
3441
3725
  // src/tools/facilitate-format.ts
3726
+ function formatCriteriaLine(criteria) {
3727
+ return criteria.map((c) => `${c.met ? "\u2713" : "\u25CB"} ${c.label} (${c.weight}pt)`).join(" \xB7 ");
3728
+ }
3442
3729
  function appendElement(existing, element) {
3443
3730
  const existingCount = (existing?.match(/###\s*Element\s*\d/gi) ?? []).length;
3444
3731
  const num = existingCount + 1;
@@ -3470,30 +3757,35 @@ ${line}` : line;
3470
3757
  }
3471
3758
 
3472
3759
  // src/tools/facilitate.ts
3473
- var FACILITATE_ACTIONS = ["start", "respond", "score", "resume"];
3760
+ var FACILITATE_ACTIONS = ["start", "respond", "score", "resume", "commit-constellation"];
3761
+ var captureItemSchema = z12.object({
3762
+ type: z12.enum(["element", "risk", "noGo", "decision"]).describe("What to capture"),
3763
+ name: z12.string().describe("Entry name"),
3764
+ description: z12.string().describe("Entry description"),
3765
+ theme: z12.string().optional().describe("Risk theme (for risk type)")
3766
+ });
3474
3767
  var facilitateSchema = z12.object({
3475
3768
  action: z12.enum(FACILITATE_ACTIONS).describe(
3476
- "'start': create draft bet and begin session. 'respond': process user input, score, capture, return coaching. 'score': return current scorecard without advancing. 'resume': reconstruct session from existing bet entry."
3769
+ "'start': create draft bet and begin session. 'respond': process user input, score, capture, return coaching. 'score': return current scorecard without advancing. 'resume': reconstruct session from existing bet entry. 'commit-constellation': atomically commit a bet and all its linked draft entries in one call. Requires betEntryId."
3477
3770
  ),
3478
3771
  sessionType: z12.enum(["shape"]).default("shape").optional().describe("Session type. Only 'shape' in v1."),
3479
- betEntryId: z12.string().optional().describe("Bet entry ID for resume action."),
3772
+ betEntryId: z12.string().optional().describe("Bet entry ID. Required for resume/score. For respond: omit on first call after start (entry created automatically); required for subsequent calls."),
3773
+ operationId: z12.string().optional().describe("Optional idempotency key for commit-constellation retries."),
3480
3774
  userInput: z12.string().optional().describe("User's input text for respond action."),
3481
3775
  dimension: z12.string().optional().describe("Explicit dimension to score against (e.g. 'problem_clarity'). If omitted, inferred from scorecard."),
3482
3776
  betName: z12.string().optional().describe("Name for the bet (used in start action)."),
3483
3777
  source: z12.enum(["user", "agent_proposal", "user_reaction"]).default("user").optional().describe("ENT-59: Source of the input text. 'user' = typed by user (default, full score weight). 'agent_proposal' = agent-generated proposal (discounted score). 'user_reaction' = user reacting to agent proposal (full weight)."),
3484
- capture: z12.object({
3485
- type: z12.enum(["element", "risk", "noGo", "decision"]).describe("What to capture"),
3486
- name: z12.string().describe("Entry name"),
3487
- description: z12.string().describe("Entry description"),
3488
- theme: z12.string().optional().describe("Risk theme (for risk type)")
3489
- }).optional().describe("Entry to capture alongside the respond action.")
3778
+ capture: z12.union([
3779
+ captureItemSchema,
3780
+ z12.array(captureItemSchema).max(15)
3781
+ ]).optional().describe("Entry or entries to capture alongside the respond action. Accepts a single object or an array (max 15) for batch capture.")
3490
3782
  });
3491
3783
  function registerFacilitateTools(server) {
3492
3784
  server.registerTool(
3493
3785
  "facilitate",
3494
3786
  {
3495
3787
  title: "Facilitate \u2014 Coached Shaping",
3496
- description: "Server-controlled coached shaping session with real-time Chain capture.\n\n- **start**: Create a draft bet on the Chain and begin a coached session. Returns Studio URL, initial context, and first coaching prompt.\n- **respond**: Process user input \u2014 scores against 6 shaping rubrics (problem, appetite, elements, architecture, risks, boundaries), searches for overlap, captures to Chain, returns structured coaching response with phase tracking.\n- **score**: Return the current scorecard without advancing the session.\n- **resume**: Reconstruct session state from an existing bet entry on the Chain.\n\nThe structured response separates judgment (server) from coaching (agent) per DEC-56. Read the phase, scorecard, and coaching fields to determine what to say next. When captureReady is true, buildContract is auto-generated from Chain governance.",
3788
+ description: "Server-controlled coached shaping session with real-time Chain capture.\n\n- **start**: Create a draft bet on the Chain and begin a coached session. Returns Studio URL, initial context, and first coaching prompt.\n- **respond**: Process user input \u2014 scores against 7 shaping rubrics (problem, appetite, elements, architecture, risks, boundaries, done-when), searches for overlap, captures to Chain, returns structured coaching response with phase tracking.\n- **score**: Return the current scorecard without advancing the session.\n- **resume**: Reconstruct session state from an existing bet entry on the Chain.\n- **commit-constellation**: Atomically commit a bet and all its linked draft entries (features, tensions, decisions) in one call. Validates strategy link and required fields before committing anything. Replaces 9-13 sequential commit-entry calls.\n\nThe structured response separates judgment (server) from coaching (agent) per DEC-56. Read the phase, scorecard, and coaching fields to determine what to say next. When captureReady is true, buildContract is auto-generated from Chain governance.",
3497
3789
  inputSchema: facilitateSchema,
3498
3790
  annotations: {
3499
3791
  readOnlyHint: false,
@@ -3521,6 +3813,8 @@ function registerFacilitateTools(server) {
3521
3813
  return handleScore(parsed.data);
3522
3814
  case "resume":
3523
3815
  return handleResume(parsed.data);
3816
+ case "commit-constellation":
3817
+ return handleCommitConstellation(parsed.data);
3524
3818
  default:
3525
3819
  return {
3526
3820
  content: [{
@@ -3587,7 +3881,7 @@ function buildDirective(scorecard, phase, betEntryId, captureReady, activeDimens
3587
3881
  `2. **Review** \u2014 show the full pitch first`,
3588
3882
  `3. **Keep as draft** \u2014 not ready yet`,
3589
3883
  ``,
3590
- `If Commit: call \`commit-entry entryId="${b}"\`.`,
3884
+ `If Commit: call \`facilitate action=commit-constellation betEntryId="${b}"\` to publish the bet and all linked entries in one call.`,
3591
3885
  `If Review: call \`facilitate action=score betEntryId="${b}"\` for the detailed view.`,
3592
3886
  `If Draft: acknowledge and end the session.`
3593
3887
  ].join("\n");
@@ -3614,14 +3908,29 @@ function buildDirective(scorecard, phase, betEntryId, captureReady, activeDimens
3614
3908
  return null;
3615
3909
  }
3616
3910
  function emptyScorecard() {
3617
- return { problem_clarity: 0, appetite: 0, elements: 0, architecture: 0, risks: 0, boundaries: 0 };
3911
+ return { problem_clarity: 0, appetite: 0, elements: 0, architecture: 0, risks: 0, boundaries: 0, done_when: 0 };
3618
3912
  }
3619
3913
  function detectSmallBatch(appetiteText) {
3620
3914
  if (!appetiteText) return false;
3621
3915
  return /small\s*batch|1[-–]2\s*week|2[-–]week|one.?week|two.?week/i.test(appetiteText);
3622
3916
  }
3917
+ function buildCachedScoringContext(betData, constellation) {
3918
+ const cachedOverlapIds = typeof betData._overlapIds === "string" ? betData._overlapIds.split(",").filter((id) => id && id !== "_checked") : [];
3919
+ const cachedGovCount = typeof betData._governanceCount === "number" ? betData._governanceCount : 0;
3920
+ const syntheticChainSurfaced = Array.from({ length: cachedGovCount }, () => ({ collection: "principles" }));
3921
+ return buildScoringContext("", betData, constellation, cachedOverlapIds, syntheticChainSurfaced);
3922
+ }
3923
+ function extractContentEvidence(ctx) {
3924
+ return {
3925
+ elementCount: ctx.elementCount,
3926
+ riskCount: ctx.riskCount,
3927
+ noGoCount: ctx.noGoCount,
3928
+ hasArchitectureText: ctx.hasArchitectureText
3929
+ };
3930
+ }
3623
3931
  function emptyResponse(betEntryId, studioUrl) {
3624
3932
  return {
3933
+ version: 2,
3625
3934
  phase: "context",
3626
3935
  phaseLabel: PHASE_LABELS.context,
3627
3936
  scorecard: emptyScorecard(),
@@ -3632,7 +3941,7 @@ function emptyResponse(betEntryId, studioUrl) {
3632
3941
  sessionDrafts: [],
3633
3942
  coaching: {
3634
3943
  observation: "New shaping session started",
3635
- missing: ["Problem statement", "Appetite", "Solution elements", "Architecture", "Risks", "Boundaries"],
3944
+ missing: ["Problem statement", "Appetite", "Solution elements", "Architecture", "Risks", "Boundaries", "Done-when criteria"],
3636
3945
  pushback: "",
3637
3946
  suggestedQuestion: "Describe the problem you want to solve. Who's affected? What's the workaround today?",
3638
3947
  captureReady: false,
@@ -3701,7 +4010,8 @@ async function loadSessionDrafts(betEntryId, betInternalId) {
3701
4010
  entryId: entry.entryId,
3702
4011
  name: entry.name,
3703
4012
  collection: RELATION_TO_COLLECTION[rel.type] ?? "unknown",
3704
- relationType: rel.type
4013
+ relationType: rel.type,
4014
+ status: entry.status ?? "draft"
3705
4015
  });
3706
4016
  }
3707
4017
  } catch {
@@ -3712,7 +4022,7 @@ async function loadSessionDrafts(betEntryId, betInternalId) {
3712
4022
  return [];
3713
4023
  }
3714
4024
  }
3715
- function buildScoringContext(userInput, betData, constellation, overlapIds, chainSurfaced = [], activeDimension, source = "user") {
4025
+ function buildScoringContext(userInput, betData, constellation, overlapIds, chainResults = [], activeDimension, source = "user") {
3716
4026
  const str = (key) => betData[key] ?? "";
3717
4027
  const accParts = [
3718
4028
  str("problem"),
@@ -3721,16 +4031,17 @@ function buildScoringContext(userInput, betData, constellation, overlapIds, chai
3721
4031
  str("architecture"),
3722
4032
  str("rabbitHoles"),
3723
4033
  str("noGos"),
4034
+ str("done_when"),
4035
+ str("doneWhen"),
3724
4036
  str("solution"),
3725
4037
  userInput
3726
4038
  ];
3727
4039
  const noGoText = str("noGos");
3728
- const bulletNoGos = (noGoText.match(/^- \*\*/gm) ?? []).length;
3729
- const boundariesFull = [noGoText, activeDimension === "boundaries" ? userInput : ""].join("\n");
3730
- const wontNoGos = (boundariesFull.match(/\bwon'?t\b|\bwill not\b/gi) ?? []).length;
3731
- const noGoCount = Math.max(bulletNoGos, wontNoGos);
4040
+ const noGoCount = (noGoText.match(/^- \*\*/gm) ?? []).length;
4041
+ const elementsText = str("elements");
4042
+ const elementHeaderCount = (elementsText.match(/###\s*Element\s*\d/gi) ?? []).length;
3732
4043
  const governanceCollections = /* @__PURE__ */ new Set(["principles", "standards", "business-rules"]);
3733
- const governanceCount = chainSurfaced.filter((e) => governanceCollections.has(e.collection)).length;
4044
+ const governanceCount = chainResults.filter((e) => governanceCollections.has(e.collection)).length;
3734
4045
  const hasArchitectureText = str("architecture").length > 20;
3735
4046
  const join2 = (...parts) => parts.filter(Boolean).join("\n\n");
3736
4047
  const inputFor = (dim) => activeDimension === dim ? userInput : "";
@@ -3740,14 +4051,15 @@ function buildScoringContext(userInput, betData, constellation, overlapIds, chai
3740
4051
  elements: join2(str("elements"), str("solution"), inputFor("elements")),
3741
4052
  architecture: join2(str("architecture"), inputFor("architecture")),
3742
4053
  risks: join2(str("rabbitHoles"), inputFor("risks")),
3743
- boundaries: join2(str("noGos"), inputFor("boundaries"))
4054
+ boundaries: join2(str("noGos"), inputFor("boundaries")),
4055
+ done_when: join2(str("done_when"), str("doneWhen"), inputFor("done_when"))
3744
4056
  };
3745
4057
  return {
3746
4058
  userInput,
3747
4059
  accumulatedText: accParts.filter(Boolean).join("\n\n"),
3748
4060
  dimensionTexts,
3749
4061
  existingEntryIds: overlapIds,
3750
- elementCount: constellation.elementCount,
4062
+ elementCount: Math.max(constellation.elementCount, elementHeaderCount),
3751
4063
  riskCount: constellation.riskCount,
3752
4064
  noGoCount,
3753
4065
  governanceCount,
@@ -3756,34 +4068,19 @@ function buildScoringContext(userInput, betData, constellation, overlapIds, chai
3756
4068
  source
3757
4069
  };
3758
4070
  }
3759
- async function searchOverlap(text) {
3760
- if (text.length < 20) return [];
3761
- const query = text.slice(0, 200);
4071
+ async function searchChain(text, opts = {}) {
4072
+ const { maxResults = 8, excludeIds = [] } = opts;
4073
+ if (text.length < 10) return [];
4074
+ const query = text.slice(0, 300);
3762
4075
  try {
3763
4076
  const results = await mcpQuery(
3764
4077
  "chain.searchEntries",
3765
4078
  { query }
3766
4079
  );
3767
- return (results ?? []).filter((r) => r.entryId).slice(0, 5).map((r) => ({
3768
- entryId: r.entryId,
3769
- similarity: "text_match",
3770
- summary: `${r.name} [${r.collectionSlug ?? "unknown"}]`
3771
- }));
3772
- } catch {
3773
- return [];
3774
- }
3775
- }
3776
- async function gatherChainContext(text, betName) {
3777
- if (text.length < 10 && (!betName || betName.length < 5)) return [];
3778
- const searchText = (text.length >= 10 ? text : betName ?? "").slice(0, 300);
3779
- try {
3780
- const results = await mcpQuery(
3781
- "chain.searchEntries",
3782
- { query: searchText }
3783
- );
3784
- return (results ?? []).filter((e) => e.entryId).slice(0, 8).map((e) => ({
4080
+ const excluded = new Set(excludeIds);
4081
+ return (results ?? []).filter((e) => e.entryId && !excluded.has(e.entryId)).slice(0, maxResults).map((e) => ({
3785
4082
  entryId: e.entryId,
3786
- relevance: e.name,
4083
+ name: e.name,
3787
4084
  collection: e.collectionSlug ?? "unknown"
3788
4085
  }));
3789
4086
  } catch {
@@ -3817,7 +4114,6 @@ async function findAndLinkExisting(name, collectionSlug, betEntryId, relationTyp
3817
4114
  }
3818
4115
  async function handleStart2(args) {
3819
4116
  requireWriteAccess();
3820
- const wsCtx = await getWorkspaceContext();
3821
4117
  const betName = args.betName ?? "Untitled Bet (Shaping)";
3822
4118
  let orientContext = "";
3823
4119
  try {
@@ -3834,58 +4130,20 @@ async function handleStart2(args) {
3834
4130
  } catch {
3835
4131
  orientContext = "Could not load workspace context.";
3836
4132
  }
3837
- let betEntryId;
3838
- let docId;
3839
- const agentId = getAgentSessionId();
3840
- try {
3841
- const result = await mcpMutation(
3842
- "chain.createEntry",
3843
- {
3844
- collectionSlug: "bets",
3845
- name: betName,
3846
- status: "draft",
3847
- data: {
3848
- problem: "",
3849
- appetite: "",
3850
- elements: "",
3851
- rabbitHoles: "",
3852
- noGos: "",
3853
- architecture: "",
3854
- buildContract: "",
3855
- description: `Shaping session for: ${betName}`,
3856
- status: "shaping",
3857
- shapingSessionActive: true
3858
- },
3859
- createdBy: agentId ? `agent:${agentId}` : "facilitate",
3860
- sessionId: agentId ?? void 0
3861
- }
3862
- );
3863
- betEntryId = result.entryId;
3864
- docId = result.docId;
3865
- await recordSessionActivity({ entryCreated: docId });
3866
- } catch (err) {
3867
- const msg = err instanceof Error ? err.message : String(err);
3868
- return {
3869
- content: [{ type: "text", text: `Failed to create bet entry: ${msg}` }],
3870
- isError: true
3871
- };
3872
- }
3873
- const studioUrl = buildStudioUrl(wsCtx.workspaceSlug, betEntryId);
3874
- const response = emptyResponse(betEntryId, studioUrl);
3875
- const investigationBrief = buildInvestigationBrief("context", betName, betEntryId);
4133
+ const investigationBrief = buildInvestigationBrief("context", betName, "pending");
4134
+ const response = emptyResponse("pending", "");
3876
4135
  if (investigationBrief) {
3877
4136
  response.investigationBrief = investigationBrief;
3878
4137
  }
3879
4138
  const output = [
3880
4139
  `# Shaping Session Started`,
3881
4140
  "",
3882
- `**Bet:** ${betName} (\`${betEntryId}\`)`,
3883
- `**Studio:** ${studioUrl}`,
4141
+ `**Bet:** ${betName} (draft entry will be created when you describe the problem)`,
3884
4142
  `**Phase:** ${PHASE_LABELS.context}`,
3885
4143
  "",
3886
4144
  orientContext ? `**Workspace context:** ${orientContext}` : "",
3887
4145
  "",
3888
- "Session is active. The bet exists as a draft on the Chain.",
4146
+ "No entry created yet \u2014 the bet will be saved to the Chain once you describe the problem.",
3889
4147
  "",
3890
4148
  "---",
3891
4149
  "## Agent Directive",
@@ -3894,7 +4152,8 @@ async function handleStart2(args) {
3894
4152
  "Synthesize findings into a 3-5 line context brief, then present to the user.",
3895
4153
  "After presenting context, ask: **What's not working well? Describe the problem \u2014 who's affected, and what's the workaround today?**",
3896
4154
  "Keep your message to 5 lines or fewer. Heavy context degrades the shaping experience.",
3897
- `After the user responds, call \`facilitate action=respond betEntryId="${betEntryId}" dimension="problem_clarity"\` with their answer as userInput.`
4155
+ `After the user responds, call \`facilitate action=respond betName="${betName}" dimension="problem_clarity"\` with their answer as userInput.`,
4156
+ `Note: pass betName (not betEntryId) \u2014 the entry will be created on the first respond call.`
3898
4157
  ].filter(Boolean).join("\n");
3899
4158
  return {
3900
4159
  content: [{ type: "text", text: output }],
@@ -3903,163 +4162,212 @@ async function handleStart2(args) {
3903
4162
  }
3904
4163
  async function handleRespond(args) {
3905
4164
  requireWriteAccess();
3906
- const { userInput, betEntryId: argBetId, dimension: argDimension, capture: captureArg, source: argSource } = args;
4165
+ const { userInput, betEntryId: argBetId, dimension: argDimension, capture: rawCapture, source: argSource, betName: argBetName } = args;
3907
4166
  const source = argSource ?? "user";
4167
+ const captureItems = rawCapture ? Array.isArray(rawCapture) ? rawCapture : [rawCapture] : [];
3908
4168
  if (!userInput) {
3909
4169
  return {
3910
4170
  content: [{ type: "text", text: "`userInput` is required for respond action." }]
3911
4171
  };
3912
4172
  }
3913
- if (!argBetId) {
3914
- return {
3915
- content: [{ type: "text", text: "`betEntryId` is required for respond action. Use start first." }]
3916
- };
3917
- }
3918
4173
  const captureErrors = [];
3919
4174
  const entriesCreated = [];
3920
4175
  let relationsCreated = 0;
3921
- const [betEntry, constellation, overlap, chainSurfaced] = await Promise.all([
3922
- loadBetEntry(argBetId),
3923
- loadConstellationState(argBetId),
3924
- captureArg ? Promise.resolve([]) : searchOverlap(userInput),
3925
- gatherChainContext(userInput)
4176
+ let betId = argBetId;
4177
+ if (!betId) {
4178
+ const betName2 = argBetName ?? "Untitled Bet (Shaping)";
4179
+ const agentId = getAgentSessionId();
4180
+ try {
4181
+ const result = await mcpMutation(
4182
+ "chain.createEntry",
4183
+ {
4184
+ collectionSlug: "bets",
4185
+ name: betName2,
4186
+ status: "draft",
4187
+ data: {
4188
+ problem: userInput,
4189
+ appetite: "",
4190
+ elements: "",
4191
+ rabbitHoles: "",
4192
+ noGos: "",
4193
+ architecture: "",
4194
+ buildContract: "",
4195
+ description: `Shaping session for: ${betName2}`,
4196
+ status: "shaping",
4197
+ shapingSessionActive: true
4198
+ },
4199
+ createdBy: agentId ? `agent:${agentId}` : "facilitate",
4200
+ sessionId: agentId ?? void 0
4201
+ }
4202
+ );
4203
+ betId = result.entryId;
4204
+ await recordSessionActivity({ entryCreated: result.docId });
4205
+ } catch (err) {
4206
+ const msg = err instanceof Error ? err.message : String(err);
4207
+ return {
4208
+ content: [{ type: "text", text: `Failed to create bet entry: ${msg}` }],
4209
+ isError: true
4210
+ };
4211
+ }
4212
+ }
4213
+ const [betEntry, constellation, chainSurfaced] = await Promise.all([
4214
+ loadBetEntry(betId),
4215
+ loadConstellationState(betId),
4216
+ searchChain(userInput, { maxResults: 8 })
3926
4217
  ]);
3927
4218
  if (!betEntry) {
3928
4219
  return {
3929
- content: [{ type: "text", text: `Bet \`${argBetId}\` not found. Use start to create a session.` }]
4220
+ content: [{ type: "text", text: `Bet \`${betId}\` not found. Use start to create a session.` }]
3930
4221
  };
3931
4222
  }
3932
4223
  const betData = betEntry.data ?? {};
3933
- if (captureArg) {
4224
+ if (captureItems.length > 0) {
3934
4225
  const capAgentId = getAgentSessionId();
3935
4226
  const collectionMap = {
3936
4227
  element: { slug: "features", dataField: "description", relationType: "part_of" },
3937
4228
  risk: { slug: "tensions", dataField: "description", relationType: "constrains" },
3938
4229
  decision: { slug: "decisions", dataField: "rationale", relationType: "informs" }
3939
4230
  };
3940
- if (captureArg.type === "noGo") {
3941
- const updatedNoGos = appendNoGo(
3942
- betData.noGos,
3943
- { title: captureArg.name, explanation: captureArg.description }
3944
- );
3945
- try {
3946
- await mcpMutation("chain.updateEntry", {
3947
- entryId: argBetId,
3948
- data: { noGos: updatedNoGos },
3949
- changeNote: `Added no-go: ${captureArg.name}`
3950
- });
3951
- await recordSessionActivity({ entryModified: betEntry._id });
3952
- } catch (updErr) {
3953
- captureErrors.push({
3954
- operation: "update",
3955
- detail: `noGos field: ${updErr instanceof Error ? updErr.message : String(updErr)}`
3956
- });
3957
- }
3958
- } else {
3959
- const mapping = collectionMap[captureArg.type];
3960
- if (mapping) {
3961
- let capturedEntryId = null;
4231
+ let runningBetData = { ...betData };
4232
+ for (const item of captureItems) {
4233
+ if (item.type === "noGo") {
4234
+ const updatedNoGos = appendNoGo(
4235
+ runningBetData.noGos,
4236
+ { title: item.name, explanation: item.description }
4237
+ );
4238
+ runningBetData.noGos = updatedNoGos;
3962
4239
  try {
3963
- const result = await mcpMutation(
3964
- "chain.createEntry",
3965
- {
3966
- collectionSlug: mapping.slug,
3967
- name: captureArg.name,
3968
- status: "draft",
3969
- data: { [mapping.dataField]: captureArg.description },
3970
- createdBy: capAgentId ? `agent:${capAgentId}` : "facilitate",
3971
- sessionId: capAgentId ?? void 0
3972
- }
4240
+ await mcpMutation("chain.updateEntry", {
4241
+ entryId: betId,
4242
+ data: { noGos: updatedNoGos },
4243
+ changeNote: `Added no-go: ${item.name}`
4244
+ });
4245
+ await recordSessionActivity({ entryModified: betEntry._id });
4246
+ } catch (updErr) {
4247
+ captureErrors.push({
4248
+ operation: "update",
4249
+ detail: `noGos field: ${updErr instanceof Error ? updErr.message : String(updErr)}`
4250
+ });
4251
+ }
4252
+ continue;
4253
+ }
4254
+ const mapping = collectionMap[item.type];
4255
+ if (!mapping) continue;
4256
+ let capturedEntryId = null;
4257
+ try {
4258
+ const result = await mcpMutation(
4259
+ "chain.createEntry",
4260
+ {
4261
+ collectionSlug: mapping.slug,
4262
+ name: item.name,
4263
+ status: "draft",
4264
+ data: { [mapping.dataField]: item.description },
4265
+ createdBy: capAgentId ? `agent:${capAgentId}` : "facilitate",
4266
+ sessionId: capAgentId ?? void 0
4267
+ }
4268
+ );
4269
+ capturedEntryId = result.entryId;
4270
+ entriesCreated.push(result.entryId);
4271
+ await recordSessionActivity({ entryCreated: result.docId });
4272
+ } catch (createErr) {
4273
+ const msg = createErr instanceof Error ? createErr.message : String(createErr);
4274
+ if (msg.includes("Duplicate entry") || msg.includes("already exists")) {
4275
+ const fallback = await findAndLinkExisting(
4276
+ item.name,
4277
+ mapping.slug,
4278
+ betId,
4279
+ mapping.relationType
3973
4280
  );
3974
- capturedEntryId = result.entryId;
3975
- entriesCreated.push(result.entryId);
3976
- await recordSessionActivity({ entryCreated: result.docId });
3977
- } catch (createErr) {
3978
- const msg = createErr instanceof Error ? createErr.message : String(createErr);
3979
- if (msg.includes("Duplicate entry") || msg.includes("already exists")) {
3980
- const fallback = await findAndLinkExisting(
3981
- captureArg.name,
3982
- mapping.slug,
3983
- argBetId,
3984
- mapping.relationType
3985
- );
3986
- if (fallback) {
3987
- capturedEntryId = fallback.entryId;
3988
- entriesCreated.push(fallback.entryId);
3989
- if (fallback.linked) relationsCreated++;
3990
- captureErrors.push({
3991
- operation: "info",
3992
- detail: `Linked existing ${mapping.slug} entry \`${fallback.entryId}\` instead of creating duplicate.`
3993
- });
3994
- } else {
3995
- captureErrors.push({ operation: "capture", detail: `${captureArg.type}: ${msg}` });
3996
- }
4281
+ if (fallback) {
4282
+ capturedEntryId = fallback.entryId;
4283
+ entriesCreated.push(fallback.entryId);
4284
+ if (fallback.linked) relationsCreated++;
4285
+ captureErrors.push({
4286
+ operation: "info",
4287
+ detail: `Linked existing ${mapping.slug} entry \`${fallback.entryId}\` instead of creating duplicate.`
4288
+ });
3997
4289
  } else {
3998
- captureErrors.push({ operation: "capture", detail: `${captureArg.type}: ${msg}` });
4290
+ captureErrors.push({ operation: "capture", detail: `${item.type}: ${msg}` });
3999
4291
  }
4292
+ } else {
4293
+ captureErrors.push({ operation: "capture", detail: `${item.type}: ${msg}` });
4000
4294
  }
4001
- if (capturedEntryId) {
4002
- try {
4003
- await mcpMutation("chain.createEntryRelation", {
4004
- fromEntryId: capturedEntryId,
4005
- toEntryId: argBetId,
4006
- type: mapping.relationType
4007
- });
4008
- relationsCreated++;
4009
- await recordSessionActivity({ relationCreated: true });
4010
- } catch (relErr) {
4011
- const msg = relErr instanceof Error ? relErr.message : String(relErr);
4012
- if (!msg.includes("already exists") && !msg.includes("Duplicate")) {
4013
- captureErrors.push({
4014
- operation: "relation",
4015
- detail: `${mapping.relationType} from ${capturedEntryId} to ${argBetId}: ${msg}`
4016
- });
4017
- }
4018
- }
4019
- if (captureArg.type === "element") {
4020
- const updatedElements = appendElement(
4021
- betData.elements,
4022
- { name: captureArg.name, description: captureArg.description, entryId: capturedEntryId }
4023
- );
4024
- try {
4025
- await mcpMutation("chain.updateEntry", {
4026
- entryId: argBetId,
4027
- data: { elements: updatedElements },
4028
- changeNote: `Added element: ${captureArg.name}`
4029
- });
4030
- await recordSessionActivity({ entryModified: betEntry._id });
4031
- } catch (updErr) {
4032
- captureErrors.push({
4033
- operation: "update",
4034
- detail: `elements field: ${updErr instanceof Error ? updErr.message : String(updErr)}`
4035
- });
4036
- }
4037
- } else if (captureArg.type === "risk") {
4038
- const updatedRisks = appendRabbitHole(
4039
- betData.rabbitHoles,
4040
- { name: captureArg.name, description: captureArg.description, theme: captureArg.theme, entryId: capturedEntryId }
4041
- );
4042
- try {
4043
- await mcpMutation("chain.updateEntry", {
4044
- entryId: argBetId,
4045
- data: { rabbitHoles: updatedRisks },
4046
- changeNote: `Added risk: ${captureArg.name}`
4047
- });
4048
- await recordSessionActivity({ entryModified: betEntry._id });
4049
- } catch (updErr) {
4050
- captureErrors.push({
4051
- operation: "update",
4052
- detail: `rabbitHoles field: ${updErr instanceof Error ? updErr.message : String(updErr)}`
4053
- });
4054
- }
4055
- }
4295
+ }
4296
+ if (!capturedEntryId) continue;
4297
+ try {
4298
+ await mcpMutation("chain.createEntryRelation", {
4299
+ fromEntryId: capturedEntryId,
4300
+ toEntryId: betId,
4301
+ type: mapping.relationType
4302
+ });
4303
+ relationsCreated++;
4304
+ await recordSessionActivity({ relationCreated: true });
4305
+ } catch (relErr) {
4306
+ const msg = relErr instanceof Error ? relErr.message : String(relErr);
4307
+ if (!msg.includes("already exists") && !msg.includes("Duplicate")) {
4308
+ captureErrors.push({
4309
+ operation: "relation",
4310
+ detail: `${mapping.relationType} from ${capturedEntryId} to ${betId}: ${msg}`
4311
+ });
4312
+ }
4313
+ }
4314
+ if (item.type === "element") {
4315
+ const updatedElements = appendElement(
4316
+ runningBetData.elements,
4317
+ { name: item.name, description: item.description, entryId: capturedEntryId }
4318
+ );
4319
+ runningBetData.elements = updatedElements;
4320
+ try {
4321
+ await mcpMutation("chain.updateEntry", {
4322
+ entryId: betId,
4323
+ data: { elements: updatedElements },
4324
+ changeNote: `Added element: ${item.name}`
4325
+ });
4326
+ await recordSessionActivity({ entryModified: betEntry._id });
4327
+ } catch (updErr) {
4328
+ captureErrors.push({
4329
+ operation: "update",
4330
+ detail: `elements field: ${updErr instanceof Error ? updErr.message : String(updErr)}`
4331
+ });
4332
+ }
4333
+ } else if (item.type === "risk") {
4334
+ const updatedRisks = appendRabbitHole(
4335
+ runningBetData.rabbitHoles,
4336
+ { name: item.name, description: item.description, theme: item.theme, entryId: capturedEntryId }
4337
+ );
4338
+ runningBetData.rabbitHoles = updatedRisks;
4339
+ try {
4340
+ await mcpMutation("chain.updateEntry", {
4341
+ entryId: betId,
4342
+ data: { rabbitHoles: updatedRisks },
4343
+ changeNote: `Added risk: ${item.name}`
4344
+ });
4345
+ await recordSessionActivity({ entryModified: betEntry._id });
4346
+ } catch (updErr) {
4347
+ captureErrors.push({
4348
+ operation: "update",
4349
+ detail: `rabbitHoles field: ${updErr instanceof Error ? updErr.message : String(updErr)}`
4350
+ });
4056
4351
  }
4057
4352
  }
4058
4353
  }
4059
4354
  }
4060
- const refreshedBet = await loadBetEntry(argBetId);
4355
+ const refreshedBet = await loadBetEntry(betId);
4061
4356
  const refreshedData = refreshedBet?.data ?? betData;
4062
- const refreshedConstellation = captureArg ? await loadConstellationState(argBetId, refreshedBet?._id ?? betEntry._id) : constellation;
4357
+ const refreshedConstellation = captureItems.length > 0 ? await loadConstellationState(betId, refreshedBet?._id ?? betEntry._id) : constellation;
4358
+ const sessionDrafts = await loadSessionDrafts(betId, refreshedBet?._id ?? betEntry._id);
4359
+ const constellationEntryIds = sessionDrafts.map((d) => d.entryId);
4360
+ const alreadyCheckedOverlap = typeof refreshedData._overlapIds === "string";
4361
+ let overlap = [];
4362
+ if (!alreadyCheckedOverlap && captureItems.length === 0) {
4363
+ const problemText = refreshedData.problem ?? userInput;
4364
+ const overlapResults = await searchChain(problemText, { maxResults: 5, excludeIds: [betId, ...constellationEntryIds] });
4365
+ overlap = overlapResults.map((r) => ({
4366
+ entryId: r.entryId,
4367
+ similarity: "text_match",
4368
+ summary: `${r.name} [${r.collection}]`
4369
+ }));
4370
+ }
4063
4371
  const overlapIds = overlap.map((o) => o.entryId);
4064
4372
  const appetiteText = refreshedData.appetite ?? "";
4065
4373
  const isSmallBatch = detectSmallBatch(
@@ -4082,11 +4390,9 @@ async function handleRespond(args) {
4082
4390
  source
4083
4391
  );
4084
4392
  const dimensionResult = scoreDimension(activeDimension, scoringCtx);
4085
- const scorecard = buildScorecard(scoringCtx);
4086
- if (isSmallBatch) scorecard.architecture = -1;
4087
- const completed = completedDimensions(scorecard, 6, isSmallBatch);
4393
+ const { scorecard, criteria: scorecardCriteria } = buildDetailedScorecard(scoringCtx, { isSmallBatch });
4088
4394
  const nextDimension = inferActiveDimension(scorecard, isSmallBatch);
4089
- const phase = inferPhase(scorecard, isSmallBatch);
4395
+ const phase = inferPhase(scorecard, isSmallBatch, extractContentEvidence(scoringCtx));
4090
4396
  try {
4091
4397
  const fieldUpdates = {};
4092
4398
  const persist = (dim, field, minLen) => {
@@ -4097,8 +4403,8 @@ async function handleRespond(args) {
4097
4403
  persist("problem_clarity", "problem", 50);
4098
4404
  persist("appetite", "appetite", 20);
4099
4405
  persist("architecture", "architecture", 50);
4100
- if (overlapIds.length > 0 && !refreshedData._overlapIds) {
4101
- fieldUpdates._overlapIds = overlapIds.join(",");
4406
+ if (!refreshedData._overlapIds) {
4407
+ fieldUpdates._overlapIds = overlapIds.length > 0 ? overlapIds.join(",") : "_checked";
4102
4408
  }
4103
4409
  const govCount = chainSurfaced.filter(
4104
4410
  (e) => ["principles", "standards", "business-rules"].includes(e.collection)
@@ -4108,7 +4414,7 @@ async function handleRespond(args) {
4108
4414
  }
4109
4415
  if (Object.keys(fieldUpdates).length > 0) {
4110
4416
  await mcpMutation("chain.updateEntry", {
4111
- entryId: argBetId,
4417
+ entryId: betId,
4112
4418
  data: fieldUpdates,
4113
4419
  changeNote: `Updated ${Object.keys(fieldUpdates).join(", ")} from shaping session`
4114
4420
  });
@@ -4122,39 +4428,50 @@ async function handleRespond(args) {
4122
4428
  }
4123
4429
  const alignment = [];
4124
4430
  const governanceCollections = /* @__PURE__ */ new Set(["principles", "standards", "business-rules", "strategy", "bets"]);
4125
- for (const surfaced of chainSurfaced) {
4126
- if (governanceCollections.has(surfaced.collection)) {
4127
- alignment.push({ entryId: surfaced.entryId, relationship: `governs [${surfaced.collection}]` });
4431
+ for (const result of chainSurfaced) {
4432
+ if (governanceCollections.has(result.collection)) {
4433
+ alignment.push({ entryId: result.entryId, relationship: `governs [${result.collection}]` });
4128
4434
  }
4129
4435
  }
4130
- const dims = activeDimensions(isSmallBatch);
4131
- const captureReady = completed.length >= (isSmallBatch ? 3 : 4) && dims.every((d) => scorecard[d] > 0);
4436
+ const { ready: captureReady, completed } = isCaptureReady(scorecard, isSmallBatch);
4132
4437
  const observation = dimensionResult.satisfied.length > 0 ? `${DIMENSION_LABELS[activeDimension]}: ${dimensionResult.satisfied.join("; ")}` : `${DIMENSION_LABELS[activeDimension]}: needs more detail`;
4133
- const pushback = !captureArg && overlap.length > 0 ? `${overlap[0].summary} already exists on the Chain. How does your bet differ?` : dimensionResult.missing.length > 0 ? dimensionResult.missing[0] : "";
4438
+ const pushback = captureItems.length === 0 && overlap.length > 0 ? `${overlap[0].summary} already exists on the Chain. How does your bet differ?` : dimensionResult.missing.length > 0 ? dimensionResult.missing[0] : "";
4134
4439
  const suggestedQuestion = dimensionResult.missing.length > 1 ? dimensionResult.missing[1] : dimensionResult.missing.length > 0 ? dimensionResult.missing[0] : `${DIMENSION_LABELS[nextDimension]} is next \u2014 ready to move on?`;
4135
4440
  const wsCtx = await getWorkspaceContext();
4136
- const studioUrl = buildStudioUrl(wsCtx.workspaceSlug, argBetId);
4441
+ const studioUrl = buildStudioUrl(wsCtx.workspaceSlug, betId);
4137
4442
  let buildContract;
4138
4443
  if (captureReady) {
4139
4444
  const contractCtx = {
4140
- betEntryId: argBetId,
4141
- governanceEntries: chainSurfaced.filter((e) => ["principles", "standards", "business-rules"].includes(e.collection)).map((e) => ({ entryId: e.entryId, name: e.relevance, collection: e.collection })),
4142
- relatedDecisions: chainSurfaced.filter((e) => e.collection === "decisions").map((e) => ({ entryId: e.entryId, name: e.relevance })),
4143
- relatedTensions: chainSurfaced.filter((e) => e.collection === "tensions").map((e) => ({ entryId: e.entryId, name: e.relevance }))
4445
+ betEntryId: betId,
4446
+ governanceEntries: chainSurfaced.filter((e) => ["principles", "standards", "business-rules"].includes(e.collection)).map((e) => ({ entryId: e.entryId, name: e.name, collection: e.collection })),
4447
+ relatedDecisions: chainSurfaced.filter((e) => e.collection === "decisions").map((e) => ({ entryId: e.entryId, name: e.name })),
4448
+ relatedTensions: chainSurfaced.filter((e) => e.collection === "tensions").map((e) => ({ entryId: e.entryId, name: e.name }))
4144
4449
  };
4145
4450
  buildContract = generateBuildContract(contractCtx);
4146
4451
  }
4452
+ let commitBlockers;
4453
+ if (captureReady) {
4454
+ commitBlockers = computeCommitBlockers({
4455
+ betEntryId: betId,
4456
+ relations: refreshedConstellation.relations,
4457
+ betData: refreshedData,
4458
+ sessionDrafts
4459
+ });
4460
+ if (commitBlockers.length === 0) commitBlockers = void 0;
4461
+ }
4147
4462
  const suggested = suggestCaptures(scoringCtx, activeDimension);
4148
- const sessionDrafts = await loadSessionDrafts(argBetId, refreshedBet?._id ?? betEntry._id);
4463
+ const betProblem = refreshedData.problem ?? "";
4149
4464
  const betName = refreshedData.description ?? betEntry.name;
4150
- const investigationBrief = buildInvestigationBrief(phase, betName, argBetId) ?? void 0;
4465
+ const elementNames = (refreshedData.elements ?? "").match(/###\s*Element\s*\d+:\s*(.+)/gi)?.map((h) => h.replace(/###\s*Element\s*\d+:\s*/i, "").trim()) ?? [];
4466
+ const investigationBrief = buildInvestigationBrief(phase, betName, betId, betProblem, elementNames) ?? void 0;
4151
4467
  const response = {
4468
+ version: 2,
4152
4469
  phase,
4153
4470
  phaseLabel: PHASE_LABELS[phase],
4154
4471
  scorecard,
4155
4472
  overlap,
4156
4473
  alignment,
4157
- chainSurfaced,
4474
+ chainSurfaced: chainSurfaced.map((r) => ({ entryId: r.entryId, relevance: r.name, collection: r.collection })),
4158
4475
  suggestedCaptures: suggested,
4159
4476
  sessionDrafts,
4160
4477
  coaching: {
@@ -4166,7 +4483,7 @@ async function handleRespond(args) {
4166
4483
  nextDimension
4167
4484
  },
4168
4485
  captured: {
4169
- betEntryId: argBetId,
4486
+ betEntryId: betId,
4170
4487
  studioUrl,
4171
4488
  entriesCreated,
4172
4489
  relationsCreated
@@ -4175,10 +4492,12 @@ async function handleRespond(args) {
4175
4492
  navigation: {
4176
4493
  completedDimensions: completed,
4177
4494
  activeDimension,
4178
- suggestedOrder: dims
4495
+ suggestedOrder: activeDimensions(isSmallBatch)
4179
4496
  },
4180
4497
  buildContract,
4181
- investigationBrief
4498
+ investigationBrief,
4499
+ commitBlockers,
4500
+ scorecardCriteria
4182
4501
  };
4183
4502
  const STRUCTURED_DIMS = {
4184
4503
  elements: "element",
@@ -4186,17 +4505,20 @@ async function handleRespond(args) {
4186
4505
  boundaries: "noGo"
4187
4506
  };
4188
4507
  const expectedCaptureType = STRUCTURED_DIMS[activeDimension];
4189
- const dataLossWarning = expectedCaptureType && !captureArg ? `**WARNING:** You discussed ${activeDimension} but did not use the \`capture\` parameter \u2014 nothing was saved to the bet. Re-send with \`capture={type:"${expectedCaptureType}", name:"...", description:"..."}\` to persist this.` : "";
4190
- const directive = buildDirective(scorecard, phase, argBetId, captureReady, activeDimension, nextDimension);
4508
+ const dataLossWarning = expectedCaptureType && captureItems.length === 0 ? `**WARNING:** You discussed ${activeDimension} but did not use the \`capture\` parameter \u2014 nothing was saved to the bet. Re-send with \`capture={type:"${expectedCaptureType}", name:"...", description:"..."}\` to persist this.` : "";
4509
+ const directive = buildDirective(scorecard, phase, betId, captureReady, activeDimension, nextDimension);
4191
4510
  const summaryParts = [
4192
4511
  `# ${PHASE_LABELS[phase]} \u2014 ${DIMENSION_LABELS[activeDimension]}`,
4193
4512
  "",
4194
4513
  dataLossWarning,
4195
- `**Score:** ${scorecard[activeDimension]}/10 | **Phase:** ${PHASE_LABELS[phase]}`,
4514
+ `**Score:** ${DIMENSION_LABELS[activeDimension]}: ${scorecard[activeDimension]}/10 \u2014 ${formatCriteriaLine(dimensionResult.criteria)}`,
4515
+ `**Phase:** ${PHASE_LABELS[phase]}`,
4196
4516
  observation,
4197
4517
  pushback ? `**Pushback:** ${pushback}` : "",
4198
4518
  `**Next:** ${suggestedQuestion}`,
4199
4519
  captureReady ? "\n**Capture ready** \u2014 the bet has enough shape to finalize." : "",
4520
+ commitBlockers?.length ? `
4521
+ \u26A0 **Commit blockers (${commitBlockers.length}):** ${commitBlockers.map((b) => `${b.blocker} \u2192 \`${b.fix}\``).join("; ")}` : "",
4200
4522
  entriesCreated.length > 0 ? `**Captured:** ${entriesCreated.join(", ")}` : "",
4201
4523
  suggested.length > 0 ? `**Suggest capturing:** ${suggested.map((s) => `${s.type}: "${s.name}"`).join("; ")}` : "",
4202
4524
  sessionDrafts.length > 0 ? `**Session drafts (${sessionDrafts.length}):** ${sessionDrafts.map((d) => `\`${d.entryId}\` ${d.name}`).join(", ")}` : "",
@@ -4233,16 +4555,12 @@ async function handleScore(args) {
4233
4555
  const constellation = await loadConstellationState(betId, betEntry._id);
4234
4556
  const betData = betEntry.data ?? {};
4235
4557
  const isSmallBatch = detectSmallBatch(betData.appetite ?? "");
4236
- const cachedOverlapIds = typeof betData._overlapIds === "string" ? betData._overlapIds.split(",").filter(Boolean) : [];
4237
- const cachedGovCount = typeof betData._governanceCount === "number" ? betData._governanceCount : 0;
4238
- const syntheticChainSurfaced = Array.from({ length: cachedGovCount }, () => ({ collection: "principles" }));
4239
- const scoringCtx = buildScoringContext("", betData, constellation, cachedOverlapIds, syntheticChainSurfaced);
4240
- const scorecard = buildScorecard(scoringCtx);
4241
- if (isSmallBatch) scorecard.architecture = -1;
4558
+ const scoringCtx = buildCachedScoringContext(betData, constellation);
4559
+ const { scorecard, criteria: scorecardCriteria } = buildDetailedScorecard(scoringCtx, { isSmallBatch });
4242
4560
  const dims = activeDimensions(isSmallBatch);
4243
- const completed = completedDimensions(scorecard, 6, isSmallBatch);
4561
+ const { ready: captureReady, completed } = isCaptureReady(scorecard, isSmallBatch);
4244
4562
  const active = inferActiveDimension(scorecard, isSmallBatch);
4245
- const phase = inferPhase(scorecard, isSmallBatch);
4563
+ const phase = inferPhase(scorecard, isSmallBatch, extractContentEvidence(scoringCtx));
4246
4564
  const lines = [
4247
4565
  `# Scorecard \u2014 ${betEntry.name}`,
4248
4566
  `**Phase:** ${PHASE_LABELS[phase]}`,
@@ -4258,16 +4576,60 @@ async function handleScore(args) {
4258
4576
  lines.push(`| ${label} | ${score}/10 | ${status}${marker} |`);
4259
4577
  }
4260
4578
  lines.push("");
4579
+ lines.push("### Criteria Detail");
4580
+ for (const dim of dims) {
4581
+ const label = DIMENSION_LABELS[dim];
4582
+ lines.push(`**${label} (${scorecard[dim]}/10):** ${formatCriteriaLine(scorecardCriteria[dim])}`);
4583
+ }
4584
+ lines.push("");
4261
4585
  lines.push(`Completed: ${completed.length}/${dims.length}`);
4262
4586
  lines.push(`Next dimension: ${DIMENSION_LABELS[active]}`);
4263
4587
  const sessionDrafts = await loadSessionDrafts(betId, betEntry._id);
4588
+ const constellationDrafts = sessionDrafts.filter((d) => d.collection !== "unknown");
4264
4589
  if (sessionDrafts.length > 0) {
4265
4590
  lines.push("");
4266
4591
  lines.push(`**Session drafts (${sessionDrafts.length}):** ${sessionDrafts.map((d) => `\`${d.entryId}\` ${d.name}`).join(", ")}`);
4267
4592
  }
4593
+ const commitBlockers = computeCommitBlockers({
4594
+ betEntryId: betId,
4595
+ relations: constellation.relations,
4596
+ betData,
4597
+ sessionDrafts
4598
+ });
4599
+ if (captureReady) {
4600
+ lines.push("");
4601
+ lines.push("**Capture ready** \u2014 the bet has enough shape to finalize.");
4602
+ if (constellationDrafts.length > 0) {
4603
+ lines.push("");
4604
+ lines.push(`**Constellation entries to commit alongside the bet:**`);
4605
+ for (const draft of constellationDrafts) {
4606
+ lines.push(`- \`${draft.entryId}\` ${draft.name} [${draft.collection}]`);
4607
+ }
4608
+ lines.push("");
4609
+ lines.push("When committing, also commit these constellation entries to keep the graph consistent.");
4610
+ }
4611
+ }
4612
+ if (commitBlockers.length > 0) {
4613
+ lines.push("");
4614
+ lines.push(`\u26A0 **Commit blockers (${commitBlockers.length}):**`);
4615
+ for (const b of commitBlockers) {
4616
+ lines.push(`- ${b.blocker} \u2192 \`${b.fix}\``);
4617
+ }
4618
+ }
4268
4619
  return {
4269
4620
  content: [{ type: "text", text: lines.join("\n") }],
4270
- structuredContent: { scorecard, phase, phaseLabel: PHASE_LABELS[phase], completedDimensions: completed, nextDimension: active, sessionDrafts }
4621
+ structuredContent: {
4622
+ scorecard,
4623
+ phase,
4624
+ phaseLabel: PHASE_LABELS[phase],
4625
+ completedDimensions: completed,
4626
+ nextDimension: active,
4627
+ sessionDrafts,
4628
+ captureReady,
4629
+ constellationDrafts,
4630
+ commitBlockers: commitBlockers.length > 0 ? commitBlockers : void 0,
4631
+ scorecardCriteria
4632
+ }
4271
4633
  };
4272
4634
  }
4273
4635
  async function handleResume(args) {
@@ -4286,25 +4648,21 @@ async function handleResume(args) {
4286
4648
  const constellation = await loadConstellationState(betId, betEntry._id);
4287
4649
  const betData = betEntry.data ?? {};
4288
4650
  const isSmallBatch = detectSmallBatch(betData.appetite ?? "");
4289
- const cachedOverlapIds = typeof betData._overlapIds === "string" ? betData._overlapIds.split(",").filter(Boolean) : [];
4290
- const cachedGovCount = typeof betData._governanceCount === "number" ? betData._governanceCount : 0;
4291
- const syntheticChainSurfaced = Array.from({ length: cachedGovCount }, () => ({ collection: "principles" }));
4292
- const scoringCtx = buildScoringContext("", betData, constellation, cachedOverlapIds, syntheticChainSurfaced);
4293
- const scorecard = buildScorecard(scoringCtx);
4294
- if (isSmallBatch) scorecard.architecture = -1;
4651
+ const scoringCtx = buildCachedScoringContext(betData, constellation);
4652
+ const { scorecard, criteria: scorecardCriteria } = buildDetailedScorecard(scoringCtx, { isSmallBatch });
4295
4653
  const dims = activeDimensions(isSmallBatch);
4296
- const completed = completedDimensions(scorecard, 6, isSmallBatch);
4654
+ const { ready: captureReady, completed } = isCaptureReady(scorecard, isSmallBatch);
4297
4655
  const active = inferActiveDimension(scorecard, isSmallBatch);
4298
- const phase = inferPhase(scorecard, isSmallBatch);
4656
+ const phase = inferPhase(scorecard, isSmallBatch, extractContentEvidence(scoringCtx));
4299
4657
  const wsCtx = await getWorkspaceContext();
4300
4658
  const studioUrl = buildStudioUrl(wsCtx.workspaceSlug, betId);
4301
4659
  const constellationSummary = [];
4302
4660
  if (constellation.elementCount > 0) constellationSummary.push(`${constellation.elementCount} element(s)`);
4303
4661
  if (constellation.riskCount > 0) constellationSummary.push(`${constellation.riskCount} risk(s)`);
4304
4662
  if (constellation.decisionCount > 0) constellationSummary.push(`${constellation.decisionCount} decision(s)`);
4305
- const captureReady = completed.length >= (isSmallBatch ? 3 : 4) && dims.every((d) => scorecard[d] > 0);
4306
4663
  const sessionDrafts = await loadSessionDrafts(betId, betEntry._id);
4307
4664
  const response = {
4665
+ version: 2,
4308
4666
  phase,
4309
4667
  phaseLabel: PHASE_LABELS[phase],
4310
4668
  scorecard,
@@ -4332,7 +4690,8 @@ async function handleResume(args) {
4332
4690
  completedDimensions: completed,
4333
4691
  activeDimension: active,
4334
4692
  suggestedOrder: dims
4335
- }
4693
+ },
4694
+ scorecardCriteria
4336
4695
  };
4337
4696
  const directive = buildDirective(scorecard, phase, betId, captureReady, active, active);
4338
4697
  const scorecardLines = dims.map((dim) => {
@@ -4341,6 +4700,9 @@ async function handleResume(args) {
4341
4700
  const arrow = dim === active ? " \u25C0 next" : "";
4342
4701
  return `| ${DIMENSION_LABELS[dim]} | ${bar} ${s}/10${arrow} |`;
4343
4702
  });
4703
+ const criteriaDetail = dims.map(
4704
+ (dim) => `**${DIMENSION_LABELS[dim]} (${scorecard[dim]}/10):** ${formatCriteriaLine(scorecardCriteria[dim])}`
4705
+ );
4344
4706
  const output = [
4345
4707
  `# Session Resumed \u2014 ${betEntry.name}`,
4346
4708
  "",
@@ -4356,6 +4718,9 @@ async function handleResume(args) {
4356
4718
  "|---|---|",
4357
4719
  ...scorecardLines,
4358
4720
  "",
4721
+ "### Criteria Detail",
4722
+ ...criteriaDetail,
4723
+ "",
4359
4724
  `Continue with ${DIMENSION_LABELS[active]}?`,
4360
4725
  directive ? `
4361
4726
  ---
@@ -4367,6 +4732,139 @@ ${directive}` : ""
4367
4732
  structuredContent: response
4368
4733
  };
4369
4734
  }
4735
+ async function handleCommitConstellation(args) {
4736
+ requireWriteAccess();
4737
+ const betId = args.betEntryId;
4738
+ const operationId = args.operationId ?? `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
4739
+ if (!betId) {
4740
+ return {
4741
+ content: [{ type: "text", text: "`betEntryId` is required for commit-constellation action." }]
4742
+ };
4743
+ }
4744
+ const betEntry = await loadBetEntry(betId);
4745
+ if (!betEntry) {
4746
+ return {
4747
+ content: [{ type: "text", text: `Bet \`${betId}\` not found.` }]
4748
+ };
4749
+ }
4750
+ const betData = betEntry.data ?? {};
4751
+ const relations = await mcpQuery("chain.listEntryRelations", {
4752
+ entryId: betId
4753
+ });
4754
+ const sessionDrafts = await loadSessionDrafts(betId, betEntry._id);
4755
+ const commitBlockerItems = computeCommitBlockers({
4756
+ betEntryId: betId,
4757
+ relations,
4758
+ betData,
4759
+ sessionDrafts
4760
+ });
4761
+ if (commitBlockerItems.length > 0) {
4762
+ return {
4763
+ content: [{
4764
+ type: "text",
4765
+ text: [
4766
+ `# Cannot commit constellation`,
4767
+ "",
4768
+ `**${commitBlockerItems.length} blocker(s) found** \u2014 fix these before committing:`,
4769
+ ...commitBlockerItems.map((b) => `- ${b.blocker} \u2192 \`${b.fix}\``),
4770
+ "",
4771
+ "No entries were committed."
4772
+ ].join("\n")
4773
+ }],
4774
+ structuredContent: {
4775
+ operationId,
4776
+ blockers: commitBlockerItems,
4777
+ committedIds: [],
4778
+ alreadyCommittedIds: [],
4779
+ failedIds: [],
4780
+ conflictIds: [],
4781
+ totalRelations: relations.length
4782
+ }
4783
+ };
4784
+ }
4785
+ let contradictionWarnings = [];
4786
+ try {
4787
+ const { runContradictionCheck } = await import("./smart-capture-GH4CXVVX.js");
4788
+ const descField = betData.problem ?? betData.description ?? "";
4789
+ contradictionWarnings = await runContradictionCheck(
4790
+ betEntry.name ?? betId,
4791
+ descField
4792
+ );
4793
+ if (contradictionWarnings.length > 0) {
4794
+ await recordSessionActivity({ contradictionWarning: true });
4795
+ }
4796
+ } catch {
4797
+ }
4798
+ const author = getAgentSessionId() ? `agent:${getAgentSessionId()}` : void 0;
4799
+ const sessionId = getAgentSessionId() ?? void 0;
4800
+ let result;
4801
+ try {
4802
+ result = await mcpMutation("chain.batchCommitConstellation", {
4803
+ betEntryId: betId,
4804
+ author,
4805
+ sessionId,
4806
+ operationId
4807
+ });
4808
+ } catch (err) {
4809
+ const msg = err instanceof Error ? err.message : String(err);
4810
+ return {
4811
+ content: [{
4812
+ type: "text",
4813
+ text: `# Bet commit failed
4814
+
4815
+ \`${betId}\` could not be committed: ${msg}
4816
+
4817
+ No constellation entries were committed.`
4818
+ }],
4819
+ isError: true
4820
+ };
4821
+ }
4822
+ for (const entryId of result.committedIds) {
4823
+ await recordSessionActivity({ entryModified: entryId });
4824
+ }
4825
+ const lines = [];
4826
+ if (result.proposalCreated) {
4827
+ const linkedCount = result.committedIds.filter((id) => id !== betId).length;
4828
+ lines.push(
4829
+ `# Proposal created`,
4830
+ "",
4831
+ `Workspace uses consent-based governance. \`${betId}\` was submitted as a proposal \u2014 it will be committed when approved.`,
4832
+ `**${linkedCount} linked entries** were committed directly (proposals apply to the bet only).`
4833
+ );
4834
+ } else {
4835
+ lines.push(
4836
+ `# Published`,
4837
+ "",
4838
+ `**${result.committedIds.length} entries** committed to the Chain, **${result.totalRelations} connections** preserved.`,
4839
+ `\`${betId}\` and its full constellation are now source of truth.`
4840
+ );
4841
+ }
4842
+ if (contradictionWarnings.length > 0) {
4843
+ lines.push("", `\u26A0 **Contradiction warnings** (advisory):`, ...contradictionWarnings.map((w) => `- ${w}`));
4844
+ }
4845
+ if (result.committedIds.length > 0) {
4846
+ lines.push("", `**Committed:** ${result.committedIds.join(", ")}`);
4847
+ }
4848
+ if (result.alreadyCommittedIds.length > 0) {
4849
+ lines.push("", `**Already committed:** ${result.alreadyCommittedIds.join(", ")}`);
4850
+ }
4851
+ if (result.conflictIds.length > 0) {
4852
+ lines.push("", `**${result.conflictIds.length} conflicts:**`);
4853
+ for (const c of result.conflictIds) {
4854
+ lines.push(`- \`${c.entryId}\` ${c.name}: ${c.reason}`);
4855
+ }
4856
+ }
4857
+ if (result.failedIds.length > 0) {
4858
+ lines.push("", `**${result.failedIds.length} failed:**`);
4859
+ for (const f of result.failedIds) {
4860
+ lines.push(`- \`${f.entryId}\` ${f.name}: ${f.error}`);
4861
+ }
4862
+ }
4863
+ return {
4864
+ content: [{ type: "text", text: lines.join("\n") }],
4865
+ structuredContent: { ...result, contradictionWarnings }
4866
+ };
4867
+ }
4370
4868
 
4371
4869
  // src/tools/verify.ts
4372
4870
  import { existsSync, readFileSync } from "fs";
@@ -5120,7 +5618,7 @@ function registerStartTools(server) {
5120
5618
  "start",
5121
5619
  {
5122
5620
  title: "Start Product Brain",
5123
- description: "The zero-friction entry point. Say 'start PB' to begin.\n\n- **Fresh workspace**: asks what you're building, seeds tailored collections, and gets you ready to capture knowledge immediately.\n- **Existing workspace**: returns readiness score, recent activity, open tensions, and suggested next actions (same as orient).\n\nUse this as your first call. Replaces the need to call orient or health separately.",
5621
+ description: "The zero-friction entry point. Say 'start PB' to begin.\n\n- **Fresh workspace (blank)**: scans your codebase and seeds knowledge from what it finds.\n- **Early workspace (seeded)**: picks up where you left off with the top gaps to fill.\n- **Active workspace (grounded/connected)**: standup briefing with recent activity and open items.\n\nUse this as your first call. Replaces the need to call orient or health separately.",
5124
5622
  inputSchema: startSchema,
5125
5623
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }
5126
5624
  },
@@ -5143,45 +5641,107 @@ function registerStartTools(server) {
5143
5641
  ]
5144
5642
  };
5145
5643
  }
5146
- const isFresh = await detectFreshWorkspace();
5147
- if (isFresh && !preset) {
5148
- return { content: [{ type: "text", text: buildPresetMenu(wsCtx) }] };
5644
+ let stage = null;
5645
+ let readiness = null;
5646
+ try {
5647
+ readiness = await mcpQuery("chain.workspaceReadiness");
5648
+ stage = readiness?.stage ?? "blank";
5649
+ } catch {
5650
+ errors.push("Readiness check unavailable \u2014 showing workspace summary.");
5149
5651
  }
5150
- if (isFresh && preset) {
5652
+ if (stage === "blank" && preset) {
5151
5653
  return { content: [{ type: "text", text: await seedPreset(wsCtx, preset, agentSessionId) }] };
5152
5654
  }
5655
+ if (stage === "blank") {
5656
+ return { content: [{ type: "text", text: buildProjectScanResponse(wsCtx) }] };
5657
+ }
5658
+ if (stage === "seeded") {
5659
+ const text = await buildSeededResponse(wsCtx, readiness, agentSessionId);
5660
+ void mcpMutation("chain.setOnboardingCompleted", {}).catch(() => {
5661
+ });
5662
+ return { content: [{ type: "text", text }] };
5663
+ }
5664
+ if (stage === "grounded" || stage === "connected") {
5665
+ void mcpMutation("chain.setOnboardingCompleted", {}).catch(() => {
5666
+ });
5667
+ }
5153
5668
  return { content: [{ type: "text", text: await buildOrientResponse(wsCtx, agentSessionId, errors) }] };
5154
5669
  }
5155
5670
  );
5156
5671
  }
5157
- async function detectFreshWorkspace() {
5158
- try {
5159
- const entries = await mcpQuery("chain.listEntries", {});
5160
- const nonSystem = (entries ?? []).filter((e) => e.stratum !== "system");
5161
- return nonSystem.length === 0;
5162
- } catch {
5163
- return false;
5164
- }
5165
- }
5166
- function buildPresetMenu(wsCtx) {
5167
- const presets = listPresets();
5168
- const lines = [
5672
+ function buildProjectScanResponse(wsCtx) {
5673
+ return [
5169
5674
  `# Welcome to ${wsCtx.workspaceName}`,
5170
5675
  "",
5171
- "Your workspace is fresh \u2014 let's get it set up together.",
5676
+ "Your workspace is fresh. Let me get oriented in your codebase.",
5172
5677
  "",
5173
- "**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:",
5678
+ "**Please read these files and tell me what you find:**",
5679
+ "1. `README.md` (or equivalent \u2014 top-level docs)",
5680
+ "2. `package.json` / `pyproject.toml` / `Cargo.toml` (project manifest)",
5681
+ "3. Top-level source directories and their purpose",
5682
+ "",
5683
+ "From those, infer 5\u20138 product knowledge entries \u2014 things like:",
5684
+ "- **Product name and what it does** (1\u20132 sentences)",
5685
+ "- **Key technical decisions** already made (framework, DB, architecture)",
5686
+ "- **Core features** the codebase reveals",
5687
+ "- **Conventions** you notice (naming, patterns, standards)",
5688
+ "",
5689
+ "Present them as a numbered list:",
5690
+ "```",
5691
+ "1. [Entry name]: [1-sentence description]",
5692
+ "2. ...",
5693
+ "```",
5694
+ "",
5695
+ `Then ask: **"Which of these are accurate? Reply with numbers to skip (e.g. 'skip 2, 4') or say 'all good' to capture everything."**`,
5696
+ "",
5697
+ "Once confirmed, call `capture` for each entry with `autoCommit: false` \u2014 the user will review before anything hits the Chain.",
5698
+ "",
5699
+ '**If the codebase is sparse** (no README, empty project, monorepo root): say so and ask "Tell me about your product in a sentence \u2014 what does it do and who is it for?" instead.',
5700
+ "",
5701
+ "_Prefer 5 specific entries over 8 generic ones. Skip anything that would apply to any codebase._"
5702
+ ].join("\n");
5703
+ }
5704
+ async function buildSeededResponse(wsCtx, readiness, agentSessionId) {
5705
+ const lines = [
5706
+ `# ${wsCtx.workspaceName}`,
5707
+ "_Picking up where you left off._",
5174
5708
  ""
5175
5709
  ];
5176
- for (const p of presets) {
5177
- lines.push(`- **${p.name}** (\`${p.id}\`) \u2014 ${p.description} (${p.collectionCount} collections)`);
5710
+ const stage = readiness?.stage ?? "seeded";
5711
+ const score = readiness?.score;
5712
+ if (score !== void 0) {
5713
+ lines.push(`**Brain stage: ${stage}.** Readiness score: ${score}/100.`);
5714
+ lines.push("");
5715
+ }
5716
+ const gaps = readiness?.gaps ?? [];
5717
+ if (gaps.length > 0) {
5718
+ lines.push("## Top gaps to fill");
5719
+ for (const gap of gaps.slice(0, 3)) {
5720
+ const cta = gap.capabilityGuidance ?? gap.guidance ?? `Capture ${gap.label.toLowerCase()} to unlock this.`;
5721
+ lines.push(`**${gap.label}** \u2014 ${cta}`);
5722
+ }
5723
+ lines.push("");
5724
+ if (gaps.length > 3) {
5725
+ lines.push(`_${gaps.length - 3} more gap${gaps.length - 3 === 1 ? "" : "s"} \u2014 use \`health action=status\` for the full list._`);
5726
+ lines.push("");
5727
+ }
5728
+ } else {
5729
+ lines.push("No gaps detected \u2014 workspace is filling up nicely.");
5730
+ lines.push("");
5731
+ }
5732
+ lines.push("What would you like to work on?");
5733
+ lines.push("");
5734
+ if (agentSessionId) {
5735
+ try {
5736
+ await mcpCall("agent.markOriented", { sessionId: agentSessionId });
5737
+ setSessionOriented(true);
5738
+ lines.push("---");
5739
+ lines.push("Orientation complete. Write tools available.");
5740
+ } catch {
5741
+ lines.push("---");
5742
+ lines.push("_Warning: Could not mark session as oriented._");
5743
+ }
5178
5744
  }
5179
- lines.push(
5180
- "",
5181
- 'Call `start` again with your choice, e.g.: `start preset="software-product"`',
5182
- "",
5183
- "_These are starting points. You can add, remove, or customize collections at any time using `collections action=create` and `collections action=update`._"
5184
- );
5185
5745
  return lines.join("\n");
5186
5746
  }
5187
5747
  async function seedPreset(wsCtx, presetId, agentSessionId) {
@@ -5419,7 +5979,7 @@ async function buildOrientResponse(wsCtx, agentSessionId, errors) {
5419
5979
  try {
5420
5980
  const allEntries = await mcpQuery("chain.listEntries", {});
5421
5981
  const committed = (allEntries ?? []).filter(
5422
- (e) => e.status === "active"
5982
+ (e) => e.status === "active" && !e.seededByPlatform
5423
5983
  );
5424
5984
  const committedCollections = new Set(committed.map((e) => e.collectionId ?? e.collection));
5425
5985
  if (committed.length >= 10 && committedCollections.size >= 3) {
@@ -5437,10 +5997,8 @@ async function buildOrientResponse(wsCtx, agentSessionId, errors) {
5437
5997
  lines.push(`**View in Studio:** \`/w/${wsCtx.workspaceSlug}/studio\``);
5438
5998
  lines.push("");
5439
5999
  }
5440
- if (committed.length >= 10 && committedCollections.size >= 3) {
5441
- lines.push(`_Tip: Connect entries with \`graph action=suggest\` to unlock deeper context._`);
5442
- lines.push("");
5443
- }
6000
+ lines.push(`_Tip: Connect entries with \`graph action=suggest\` to unlock deeper context._`);
6001
+ lines.push("");
5444
6002
  }
5445
6003
  } catch {
5446
6004
  }
@@ -7902,7 +8460,64 @@ Browse: \`entries action=list collection=tracking-events\`.`
7902
8460
  );
7903
8461
  return sections.join("\n\n---\n\n");
7904
8462
  }
8463
+ var AGENT_CHEATSHEET = `# Product Brain \u2014 Agent Cheatsheet
8464
+
8465
+ ## Core Tools
8466
+ | Tool | Purpose | Key params |
8467
+ |---|---|---|
8468
+ | \`orient\` | Workspace context, governance, active bets | \u2014 (call at session start) |
8469
+ | \`capture\` | Create entry (draft) | \`collection\`, \`name\`, \`description\`, optional \`data\` |
8470
+ | \`entries\` | List / get / batch / search entries | \`action\` + \`entryId\` / \`query\` / \`collection\` |
8471
+ | \`update-entry\` | Update fields on an entry | \`entryId\`, optional \`name\`, \`status\`, \`workflowStatus\`, \`data\`, \`changeNote\` |
8472
+ | \`commit-entry\` | Promote draft \u2192 SSOT | \`entryId\` |
8473
+ | \`graph\` | Suggest / find relations | \`action\` + \`entryId\` |
8474
+ | \`relations\` | Create / batch-create / delete links | \`from\`, \`to\`, \`type\` |
8475
+ | \`context\` | Gather related knowledge | \`entryId\` or \`task\` |
8476
+ | \`collections\` | List / create / update collections | \`action\` |
8477
+ | \`labels\` | List / create / apply / remove labels | \`action\` |
8478
+ | \`quality\` | Score an entry | \`entryId\` |
8479
+ | \`session\` | Start / close agent session | \`action\` |
8480
+ | \`health\` | Check / audit / whoami | \`action\` |
8481
+ | \`facilitate\` | Shaping workflows | \`action\`: start, respond, status |
8482
+
8483
+ ## Collection Prefixes
8484
+ GLO (glossary), BR (business-rules), PRI (principles), STD (standards),
8485
+ DEC (decisions), STR (strategy), TEN (tensions), FEAT (features),
8486
+ BET (bets), INS (insights), ARCH (architecture), CIR (circles),
8487
+ ROL (roles), MAP (maps), MTRC (tracking-events), ST (semantic-types)
8488
+
8489
+ ## Valid Relation Types (21)
8490
+ informs, governs, surfaces_tension_in, defines_term_for, belongs_to,
8491
+ references, related_to, fills_slot, commits_to, informed_by, depends_on,
8492
+ conflicts_with, confused_with, replaces, part_of, constrains,
8493
+ governed_by, alternative_to, has_proposal, requests_promotion_of, resolves
8494
+
8495
+ ## Lifecycle Status
8496
+ All entries: \`draft\` | \`active\` | \`deprecated\` | \`archived\`
8497
+
8498
+ ## Workflow Status (per collection)
8499
+ - **tensions:** open \u2192 processing \u2192 decided \u2192 closed
8500
+ - **decisions:** pending \u2192 decided
8501
+ - **bets:** shaped \u2192 bet \u2192 building \u2192 shipped
8502
+ - **business-rules:** active | conflict | review
8503
+
8504
+ ## Key Patterns
8505
+ - **Capture flow:** \`capture\` \u2192 \`graph action=suggest\` \u2192 \`relations action=batch-create\` \u2192 \`commit-entry\`
8506
+ - Use \`workflowStatus\` (not \`status\`) for domain workflow state
8507
+ - \`data\` param is merged (not replaced) \u2014 safe for partial updates
8508
+ `;
7905
8509
  function registerResources(server) {
8510
+ server.resource(
8511
+ "agent-cheatsheet",
8512
+ "productbrain://agent-cheatsheet",
8513
+ async (uri) => ({
8514
+ contents: [{
8515
+ uri: uri.href,
8516
+ text: AGENT_CHEATSHEET,
8517
+ mimeType: "text/markdown"
8518
+ }]
8519
+ })
8520
+ );
7906
8521
  server.resource(
7907
8522
  "chain-orientation",
7908
8523
  "productbrain://orientation",
@@ -9056,4 +9671,4 @@ export {
9056
9671
  SERVER_VERSION,
9057
9672
  createProductBrainServer
9058
9673
  };
9059
- //# sourceMappingURL=chunk-XCKGFYDP.js.map
9674
+ //# sourceMappingURL=chunk-6WVRGNJU.js.map