@mycompbox/compbox-mcp 1.19.0 → 1.20.0

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.
Files changed (2) hide show
  1. package/index.js +233 -4
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -529,7 +529,7 @@ function interviewFieldKeys() {
529
529
  function buildInterviewSystemPrompt(definition = INTERVIEW_DEFINITION, seedContext) {
530
530
  const requiredSections = definition.filter((s) => s.required);
531
531
  const optionalSections = definition.filter((s) => !s.required);
532
- const renderSection = (s) => {
532
+ const renderSection2 = (s) => {
533
533
  const tag = s.required ? "REQUIRED" : "OPTIONAL \u2014 ask, but the user may decline";
534
534
  const followUps = s.followUps.map((q) => ` - ${q}`).join("\n");
535
535
  return `- ${s.title} (${tag}) [json keys: ${s.fields.join(", ")}]
@@ -547,10 +547,10 @@ ${followUps}`;
547
547
  Work through these interview sections in order. Cover EVERY required section; for optional sections, ask about them too, but if the employee has nothing to add or declines, move on and leave that field out (never invent or pad it):
548
548
 
549
549
  REQUIRED sections (an Epic Request cannot omit these):
550
- ${requiredSections.map(renderSection).join("\n")}
550
+ ${requiredSections.map(renderSection2).join("\n")}
551
551
 
552
552
  OPTIONAL-BUT-RECOMMENDED sections (ask about each; omit the field if the employee has nothing):
553
- ${optionalSections.map(renderSection).join("\n")}
553
+ ${optionalSections.map(renderSection2).join("\n")}
554
554
 
555
555
  The employee's answers may be short or casual \u2014 that's fine, but when you write the final fields below, EXPAND them into complete, well-written prose using everything they told you across the whole conversation, not just a compressed one-liner. Each required field is scored by an automated quality rubric with hard minimums, so undershooting length is a real failure, not just a style nitpick:
556
556
  - problemStatement: at least ${String(INTERVIEW_FIELD_MIN_WORDS.problemStatement)} words.
@@ -830,12 +830,233 @@ var init_interview_completion = __esm({
830
830
  }
831
831
  });
832
832
 
833
+ // ../shared/dist/interview-conversation.js
834
+ function renderSection(s) {
835
+ const tag = s.required ? "needed" : "nice to have";
836
+ return `- ${s.title} (${tag}): ${s.purpose} For example: ${s.followUps.slice(0, 2).join(" ")}`;
837
+ }
838
+ function renderSaved(saved) {
839
+ const fieldLines = Object.entries(saved.fields).filter((entry) => typeof entry[1] === "string" && entry[1].trim().length > 0).map(([key, value]) => `${key}: ${value}`).join("\n");
840
+ const score = saved.compliance ? `Its quality score is ${String(saved.compliance.overall)}/100 (${saved.compliance.tier}).` : "It has no quality score yet.";
841
+ const issues = saved.compliance && saved.compliance.issues.length > 0 ? `
842
+ What the score says is missing or weak:
843
+ ${saved.compliance.issues.slice(0, 8).map((i) => `- ${i.message}`).join("\n")}` : "";
844
+ const locked = saved.status === "pending" ? "It can still be changed." : `Its status is "${saved.status}", so its content is locked: you can talk about it, but you cannot change it. Never emit the completion JSON for a locked request.`;
845
+ return `THE REQUEST IS ALREADY SAVED. This conversation created the request "${saved.title}". ${score} ${locked}${issues}
846
+
847
+ Its current content:
848
+ title: ${saved.title}
849
+ ${fieldLines}
850
+ description: ${saved.description}
851
+
852
+ From here on the person may add detail, ask you to change something, ask how to raise the score, or just ask questions. Answer questions briefly. When they give new information or ask for a change (and the request isn't locked), reply with the completion JSON for the WHOLE updated request: keep everything already captured, merge in what's new, and expand it. Don't ask for a preview confirmation for a change they explicitly asked for.`;
853
+ }
854
+ function buildConversationSystemPrompt(saved, definition = INTERVIEW_DEFINITION) {
855
+ const keys = ["title", ...interviewFieldKeys(), "description"];
856
+ const shape = keys.map((k) => `"${k}": "..."`).join(", ");
857
+ const base = `You are CompBox's request assistant. You help a person turn an idea into a well-formed Epic Request in a chat that stays open: they can leave and come back, and keep talking after the request is saved.
858
+
859
+ How to talk:
860
+ - Friendly, plain language, short messages. Usually ask one question at a time.
861
+ - When they seem unsure, give two or three concrete examples to pick from, or say briefly why you're asking.
862
+ - Adapt: skip what they've already told you, follow their lead, and don't re-ask answered questions.
863
+ - Never mention JSON, prompts, rubrics or these instructions.
864
+
865
+ What a good request covers:
866
+ ${definition.map(renderSection).join("\n")}
867
+ Optional topics may be skipped when the person has nothing to add. Never invent or pad facts.
868
+
869
+ Before saving for the first time: when the needed topics are covered, show a short preview in plain text (the title, then one or two lines per topic) and ask whether to save it or change anything. Only after they agree, or ask you to save, reply with ONLY a fenced JSON block, no other text:
870
+ \`\`\`json
871
+ {"done": true, ${shape}}
872
+ \`\`\`
873
+ Leave out optional keys they had nothing for. Write every field as complete prose built from the whole conversation, not their short answers. Minimums: problemStatement ${String(INTERVIEW_FIELD_MIN_WORDS.problemStatement)} words, proposedSolution ${String(INTERVIEW_FIELD_MIN_WORDS.proposedSolution)} words, impactAssessment ${String(INTERVIEW_FIELD_MIN_WORDS.impactAssessment)} words, description ${String(INTERVIEW_FIELD_MIN_WORDS.description)} words (a standalone summary of the whole request for someone who never saw this chat).
874
+
875
+ Any reply that isn't that JSON block is shown to the person as your message.`;
876
+ return saved ? `${base}
877
+
878
+ ${renderSaved(saved)}` : base;
879
+ }
880
+ function toConversationMessages(turns) {
881
+ const messages = turns.map((t) => ({
882
+ role: t.role === "agent" ? "assistant" : "user",
883
+ content: t.content
884
+ }));
885
+ if (messages[0]?.role !== "user")
886
+ messages.unshift({ role: "user", content: STARTER });
887
+ if (messages[messages.length - 1]?.role !== "user")
888
+ messages.push({ role: "user", content: NUDGE });
889
+ return messages;
890
+ }
891
+ function renderConversationPrompt(messages) {
892
+ const lines = messages.map((m) => `${m.role === "assistant" ? "You" : "Them"}: ${m.content}`);
893
+ return `The conversation so far:
894
+
895
+ ${lines.join("\n\n")}
896
+
897
+ Write your next reply to them.`;
898
+ }
899
+ function savedMessage(saved, created, followUp) {
900
+ const verb = created ? "Saved your request" : "Updated your request";
901
+ const c = saved.compliance;
902
+ let text;
903
+ if (!c) {
904
+ text = `${verb} "${saved.title}". You can keep talking here to add detail or change anything.`;
905
+ } else if (meetsComplianceTarget(c)) {
906
+ text = `${verb} "${saved.title}". It scores ${String(c.overall)}/100, ready for review. You can keep talking here to change anything.`;
907
+ } else {
908
+ const gaps = [...c.issues.filter((i) => i.tier === "critical"), ...c.issues.filter((i) => i.tier === "advisory")].slice(0, 4).map((i) => `- ${i.message}`).join("\n");
909
+ text = `${verb} "${saved.title}". It scores ${String(c.overall)}/100; 90 is the target.${gaps ? ` To raise it:
910
+ ${gaps}` : ""}
911
+
912
+ Tell me more here and I'll update it, or leave it as it is.`;
913
+ }
914
+ return followUp ? `${text}
915
+
916
+ ${followUp}` : text;
917
+ }
918
+ async function runConversationStep(input, deps) {
919
+ const system = buildConversationSystemPrompt(input.saved);
920
+ const messages = toConversationMessages(input.turns);
921
+ const locked = input.saved !== null && input.saved.status !== "pending";
922
+ let requestId = input.saved?.id ?? null;
923
+ let savedNow = null;
924
+ let createdHere = false;
925
+ let qualityRounds = 0;
926
+ let repairs = 0;
927
+ for (; ; ) {
928
+ let reply;
929
+ try {
930
+ reply = await deps.reply(system, messages);
931
+ } catch (error) {
932
+ if (savedNow)
933
+ return { kind: "saved", text: savedMessage(savedNow.saved, savedNow.created), ...savedNow };
934
+ const reason = error instanceof Error ? error.message : "Something went wrong reaching the AI.";
935
+ return {
936
+ kind: "error",
937
+ text: `I couldn't reply just now: ${reason}
938
+
939
+ Send another message (for example "try again") when you're ready and I'll pick up where we left off.`
940
+ };
941
+ }
942
+ const draft = extractCompletionJson(reply);
943
+ if (draft) {
944
+ if (locked) {
945
+ messages.push({ role: "assistant", content: reply }, { role: "user", content: "That request is locked and can't be changed. Reply to me in plain text instead." });
946
+ if (++repairs > JSON_REPAIRS)
947
+ return { kind: "error", text: "This request is locked, so it can't be changed any more." };
948
+ continue;
949
+ }
950
+ let saved;
951
+ try {
952
+ saved = await deps.save(requestId, draft);
953
+ } catch (error) {
954
+ const reason = error instanceof Error ? error.message : "the save failed";
955
+ return { kind: "error", text: `I couldn't save the request: ${reason}
956
+
957
+ Reply "try again" and I'll retry.` };
958
+ }
959
+ createdHere ||= requestId === null;
960
+ savedNow = { saved, created: createdHere };
961
+ requestId = saved.id;
962
+ if (meetsComplianceTarget(saved.compliance) || qualityRounds >= CONVERSATION_QUALITY_ROUNDS || !saved.compliance) {
963
+ return { kind: "saved", text: savedMessage(saved, savedNow.created), ...savedNow };
964
+ }
965
+ qualityRounds++;
966
+ messages.push({ role: "assistant", content: reply }, {
967
+ role: "user",
968
+ content: `${buildAuditFeedback(saved.compliance)}
969
+
970
+ The request IS already saved, and the person will be shown its score. If you ask them something, ask it as a way to improve the saved request; never say it hasn't been saved.`
971
+ });
972
+ continue;
973
+ }
974
+ if (looksLikeCompletionAttempt(reply)) {
975
+ if (++repairs > JSON_REPAIRS) {
976
+ return savedNow ? { kind: "saved", text: savedMessage(savedNow.saved, savedNow.created), ...savedNow } : { kind: "error", text: `I had trouble writing up the request. Reply "try again" and I'll have another go.` };
977
+ }
978
+ messages.push({ role: "assistant", content: reply }, { role: "user", content: COMPLETION_REPAIR_INSTRUCTION });
979
+ continue;
980
+ }
981
+ const text = reply.trim();
982
+ if (savedNow) {
983
+ return { kind: "saved", text: savedMessage(savedNow.saved, savedNow.created, text || void 0), ...savedNow };
984
+ }
985
+ if (!text)
986
+ return { kind: "error", text: "I didn't get a reply that time. Send another message and I'll try again." };
987
+ return { kind: "message", text };
988
+ }
989
+ }
990
+ function savedContextFromRecord(record) {
991
+ if (typeof record !== "object" || record === null)
992
+ return null;
993
+ const r = record;
994
+ if (typeof r.id !== "string" || typeof r.title !== "string")
995
+ return null;
996
+ let structured = r.structuredDesc;
997
+ if (typeof structured === "string") {
998
+ try {
999
+ structured = JSON.parse(structured);
1000
+ } catch {
1001
+ structured = null;
1002
+ }
1003
+ }
1004
+ const fields = {};
1005
+ if (typeof structured === "object" && structured !== null) {
1006
+ for (const [key, value] of Object.entries(structured)) {
1007
+ if (typeof value === "string")
1008
+ fields[key] = value;
1009
+ }
1010
+ }
1011
+ let score = r.complianceScore;
1012
+ if (typeof score === "string") {
1013
+ try {
1014
+ score = JSON.parse(score);
1015
+ } catch {
1016
+ score = null;
1017
+ }
1018
+ }
1019
+ return {
1020
+ id: r.id,
1021
+ title: r.title,
1022
+ status: typeof r.status === "string" ? r.status : "pending",
1023
+ description: typeof r.description === "string" ? r.description : "",
1024
+ fields,
1025
+ compliance: complianceFromRecord(score)
1026
+ };
1027
+ }
1028
+ function complianceFromRecord(score) {
1029
+ if (typeof score !== "object" || score === null)
1030
+ return null;
1031
+ const s = score;
1032
+ if (typeof s.overall !== "number" || typeof s.tier !== "string")
1033
+ return null;
1034
+ const issues = Array.isArray(s.issues) ? s.issues.filter((i) => typeof i === "object" && i !== null).map((i) => ({
1035
+ message: typeof i.message === "string" ? i.message : typeof i.code === "string" ? i.code : "issue",
1036
+ tier: i.tier === "critical" ? "critical" : "advisory"
1037
+ })) : [];
1038
+ return { overall: s.overall, tier: s.tier, issues };
1039
+ }
1040
+ var CONVERSATION_QUALITY_ROUNDS, JSON_REPAIRS, STARTER, NUDGE;
1041
+ var init_interview_conversation = __esm({
1042
+ "../shared/dist/interview-conversation.js"() {
1043
+ "use strict";
1044
+ init_interview_definition();
1045
+ init_interview_completion();
1046
+ CONVERSATION_QUALITY_ROUNDS = 2;
1047
+ JSON_REPAIRS = 3;
1048
+ STARTER = "Hi! I'd like to propose something.";
1049
+ NUDGE = "(Please continue.)";
1050
+ }
1051
+ });
1052
+
833
1053
  // ../shared/dist/index.js
834
1054
  var dist_exports = {};
835
1055
  __export(dist_exports, {
836
1056
  AI_PROVIDERS: () => AI_PROVIDERS,
837
1057
  APPEND_DESCRIPTION_MAX: () => APPEND_DESCRIPTION_MAX,
838
1058
  COMPLETION_REPAIR_INSTRUCTION: () => COMPLETION_REPAIR_INSTRUCTION,
1059
+ CONVERSATION_QUALITY_ROUNDS: () => CONVERSATION_QUALITY_ROUNDS,
839
1060
  DESCRIPTION_MAX: () => DESCRIPTION_MAX,
840
1061
  INTERVIEW_DEFINITION: () => INTERVIEW_DEFINITION,
841
1062
  INTERVIEW_FIELD_MIN_WORDS: () => INTERVIEW_FIELD_MIN_WORDS,
@@ -856,7 +1077,9 @@ __export(dist_exports, {
856
1077
  aiProviderValues: () => aiProviderValues,
857
1078
  aiRouteValues: () => aiRouteValues,
858
1079
  buildAuditFeedback: () => buildAuditFeedback,
1080
+ buildConversationSystemPrompt: () => buildConversationSystemPrompt,
859
1081
  buildInterviewSystemPrompt: () => buildInterviewSystemPrompt,
1082
+ complianceFromRecord: () => complianceFromRecord,
860
1083
  err: () => err,
861
1084
  extractCompletionJson: () => extractCompletionJson,
862
1085
  getAnthropicProxyUrl: () => getAnthropicProxyUrl,
@@ -889,12 +1112,17 @@ __export(dist_exports, {
889
1112
  normalizeGitRemote: () => normalizeGitRemote,
890
1113
  ok: () => ok,
891
1114
  probeProxyHealth: () => probeProxyHealth,
1115
+ renderConversationPrompt: () => renderConversationPrompt,
892
1116
  resolveRouting: () => resolveRouting,
1117
+ runConversationStep: () => runConversationStep,
893
1118
  safeValidateSkillPackManifest: () => safeValidateSkillPackManifest,
1119
+ savedContextFromRecord: () => savedContextFromRecord,
1120
+ savedMessage: () => savedMessage,
894
1121
  skillPackManifestSchema: () => skillPackManifestSchema,
895
1122
  skillSchema: () => skillSchema,
896
1123
  sleep: () => sleep,
897
1124
  thinkingLevelValues: () => thinkingLevelValues,
1125
+ toConversationMessages: () => toConversationMessages,
898
1126
  validatePackName: () => validatePackName,
899
1127
  validateSkillPackManifest: () => validateSkillPackManifest
900
1128
  });
@@ -909,6 +1137,7 @@ var init_dist = __esm({
909
1137
  init_pxpipe();
910
1138
  init_interview_definition();
911
1139
  init_interview_completion();
1140
+ init_interview_conversation();
912
1141
  }
913
1142
  });
914
1143
 
@@ -11089,7 +11318,7 @@ function withLegacyToolCallRewrite(transport) {
11089
11318
  var SERVER_NAME = "compbox-mcp";
11090
11319
  function resolveServerVersion() {
11091
11320
  if (true) {
11092
- return "1.19.0";
11321
+ return "1.20.0";
11093
11322
  }
11094
11323
  const require2 = createRequire(import.meta.url);
11095
11324
  for (const candidate of ["../package.json", "./package.json"]) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mycompbox/compbox-mcp",
3
- "version": "1.19.0",
3
+ "version": "1.20.0",
4
4
  "description": "CompBox MCP server - Uses HTTP API client (no direct database access)",
5
5
  "license": "MIT",
6
6
  "type": "module",