@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.
- package/dist/{chunk-XCKGFYDP.js → chunk-6WVRGNJU.js} +966 -351
- package/dist/chunk-6WVRGNJU.js.map +1 -0
- package/dist/{chunk-7VJP2IMS.js → chunk-M264FY2V.js} +492 -36
- package/dist/chunk-M264FY2V.js.map +1 -0
- package/dist/{chunk-TB24VJ4Z.js → chunk-P7ABQEFK.js} +33 -1
- package/dist/{chunk-TB24VJ4Z.js.map → chunk-P7ABQEFK.js.map} +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/http.js +6 -3
- package/dist/http.js.map +1 -1
- package/dist/index.js +15 -14
- package/dist/index.js.map +1 -1
- package/dist/{setup-AJCCLPQP.js → setup-RSGAAKJB.js} +2 -2
- package/dist/{smart-capture-E53YEHHO.js → smart-capture-GH4CXVVX.js} +13 -3
- package/package.json +1 -1
- package/dist/chunk-7VJP2IMS.js.map +0 -1
- package/dist/chunk-XCKGFYDP.js.map +0 -1
- /package/dist/{setup-AJCCLPQP.js.map → setup-RSGAAKJB.js.map} +0 -0
- /package/dist/{smart-capture-E53YEHHO.js.map → smart-capture-GH4CXVVX.js.map} +0 -0
|
@@ -25,11 +25,11 @@ import {
|
|
|
25
25
|
startAgentSession,
|
|
26
26
|
trackWriteTool,
|
|
27
27
|
translateStaleToolNames
|
|
28
|
-
} from "./chunk-
|
|
28
|
+
} from "./chunk-M264FY2V.js";
|
|
29
29
|
import {
|
|
30
30
|
trackQualityCheck,
|
|
31
31
|
trackQualityVerdict
|
|
32
|
-
} from "./chunk-
|
|
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-
|
|
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
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
const
|
|
2861
|
-
|
|
2862
|
-
if (
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: (
|
|
3589
|
+
context: (searchTerm) => ({
|
|
3363
3590
|
phase: "context",
|
|
3364
3591
|
dimension: "problem_clarity",
|
|
3365
3592
|
tasks: [
|
|
3366
|
-
{ target: "chain", query: `Search for entries related to: ${
|
|
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: ${
|
|
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: (
|
|
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: ${
|
|
3378
|
-
{ target: "chain", query:
|
|
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: (
|
|
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: ${
|
|
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: (
|
|
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: ${
|
|
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(
|
|
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
|
|
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.
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
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
|
|
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-
|
|
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,
|
|
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
|
|
3729
|
-
const
|
|
3730
|
-
const
|
|
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 =
|
|
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
|
|
3760
|
-
|
|
3761
|
-
|
|
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
|
-
|
|
3768
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3838
|
-
|
|
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} (
|
|
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
|
-
"
|
|
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
|
|
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:
|
|
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
|
-
|
|
3922
|
-
|
|
3923
|
-
|
|
3924
|
-
|
|
3925
|
-
|
|
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 \`${
|
|
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 (
|
|
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
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
|
|
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
|
-
|
|
3964
|
-
|
|
3965
|
-
{
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
|
|
3970
|
-
|
|
3971
|
-
|
|
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
|
-
|
|
3975
|
-
|
|
3976
|
-
|
|
3977
|
-
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
|
|
3981
|
-
|
|
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: `${
|
|
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
|
-
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
|
|
4034
|
-
|
|
4035
|
-
|
|
4036
|
-
}
|
|
4037
|
-
}
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
|
|
4051
|
-
|
|
4052
|
-
|
|
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(
|
|
4355
|
+
const refreshedBet = await loadBetEntry(betId);
|
|
4061
4356
|
const refreshedData = refreshedBet?.data ?? betData;
|
|
4062
|
-
const refreshedConstellation =
|
|
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 =
|
|
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 (
|
|
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:
|
|
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
|
|
4126
|
-
if (governanceCollections.has(
|
|
4127
|
-
alignment.push({ entryId:
|
|
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
|
|
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 =
|
|
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,
|
|
4441
|
+
const studioUrl = buildStudioUrl(wsCtx.workspaceSlug, betId);
|
|
4137
4442
|
let buildContract;
|
|
4138
4443
|
if (captureReady) {
|
|
4139
4444
|
const contractCtx = {
|
|
4140
|
-
betEntryId:
|
|
4141
|
-
governanceEntries: chainSurfaced.filter((e) => ["principles", "standards", "business-rules"].includes(e.collection)).map((e) => ({ entryId: e.entryId, name: e.
|
|
4142
|
-
relatedDecisions: chainSurfaced.filter((e) => e.collection === "decisions").map((e) => ({ entryId: e.entryId, name: e.
|
|
4143
|
-
relatedTensions: chainSurfaced.filter((e) => e.collection === "tensions").map((e) => ({ entryId: e.entryId, name: e.
|
|
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
|
|
4463
|
+
const betProblem = refreshedData.problem ?? "";
|
|
4149
4464
|
const betName = refreshedData.description ?? betEntry.name;
|
|
4150
|
-
const
|
|
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:
|
|
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:
|
|
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 &&
|
|
4190
|
-
const directive = buildDirective(scorecard, phase,
|
|
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
|
|
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
|
|
4237
|
-
const
|
|
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 =
|
|
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: {
|
|
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
|
|
4290
|
-
const
|
|
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 =
|
|
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**:
|
|
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
|
-
|
|
5147
|
-
|
|
5148
|
-
|
|
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 (
|
|
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
|
-
|
|
5158
|
-
|
|
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
|
|
5676
|
+
"Your workspace is fresh. Let me get oriented in your codebase.",
|
|
5172
5677
|
"",
|
|
5173
|
-
"**
|
|
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
|
-
|
|
5177
|
-
|
|
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
|
-
|
|
5441
|
-
|
|
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-
|
|
9674
|
+
//# sourceMappingURL=chunk-6WVRGNJU.js.map
|