@codazen/harmonica-mcp 3.1.0 → 3.2.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/dist/index.js +181 -8
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -22444,7 +22444,8 @@ var CHECK_MAX_SCORES = {
22444
22444
  pii_scan: 100,
22445
22445
  portfolio_coherence: 5,
22446
22446
  revision_quality: 5,
22447
- drop_quality: 5
22447
+ drop_quality: 5,
22448
+ root_coda_quality: 5
22448
22449
  };
22449
22450
 
22450
22451
  // ../../libs/harmonica-services/src/mcp/tools/beat-quality-tools.ts
@@ -22648,6 +22649,17 @@ var NOTE_TYPE_LABELS = {
22648
22649
  decision: "Decisions",
22649
22650
  document: "Documents"
22650
22651
  };
22652
+ function formatDeliveryPolicy(policy, unresolvedReason) {
22653
+ const LABELS = {
22654
+ openPullRequest: "Open a pull request",
22655
+ requestAgentReview: "Request the agent code review",
22656
+ watchPullRequest: "Watch the pull request to green"
22657
+ };
22658
+ const verdict = (value) => value === true ? "yes \u2014 proceed without asking" : value === false ? "NO \u2014 prohibited, say so rather than asking" : "not set \u2014 ask first";
22659
+ const lines = Object.keys(LABELS).map((step) => `- ${LABELS[step]}: ${verdict(policy?.[step] ?? void 0)}`);
22660
+ const footer = unresolvedReason ? [`- _Policy could not be resolved (${unresolvedReason}) \u2014 ask first; this is not proof that no policy is set._`] : [];
22661
+ return ["## Agent Delivery Policy", ...lines, ...footer].join("\n");
22662
+ }
22651
22663
  function formatProjectContext(project, notes, orgCoda) {
22652
22664
  const sections = [];
22653
22665
  sections.push(`# Project: ${project.title}`);
@@ -22657,6 +22669,8 @@ function formatProjectContext(project, notes, orgCoda) {
22657
22669
  const branch = project.repoDefaultBranch ?? "main";
22658
22670
  sections.push(`**Repo:** ${project.repoOwner}/${project.repoName} (branch: ${branch})`);
22659
22671
  }
22672
+ sections.push(`
22673
+ ${formatDeliveryPolicy(project.agentDeliveryPolicy)}`);
22660
22674
  if (orgCoda) {
22661
22675
  sections.push(`
22662
22676
  ## Organization Coda
@@ -23255,7 +23269,14 @@ ${formatBeatDetail(beat)}${similarityWarning}` }] };
23255
23269
  return { content: [{ type: "text", text: `Revision not found: ${revisionId}` }], isError: true };
23256
23270
  }
23257
23271
  await assertBeatInOrg(client, revision.beatId, ctx.orgId);
23258
- return { content: [{ type: "text", text: formatRevisionDetail(revision) }] };
23272
+ const resolution = await client.resolveDeliveryPolicy(revisionId);
23273
+ const policyBlock = `
23274
+
23275
+ ${formatDeliveryPolicy(
23276
+ resolution.resolved ? resolution.policy : void 0,
23277
+ resolution.resolved ? void 0 : resolution.reason
23278
+ )}`;
23279
+ return { content: [{ type: "text", text: formatRevisionDetail(revision) + policyBlock }] };
23259
23280
  } catch (err) {
23260
23281
  const message = err instanceof Error ? err.message : String(err);
23261
23282
  return { content: [{ type: "text", text: `Failed to get revision: ${message}` }], isError: true };
@@ -26200,6 +26221,7 @@ var ScopeNotebookApiSchema = external_exports.object({
26200
26221
  var NotebookResponseSchema = external_exports.object({ notebook: NotebookApiSchema }).passthrough();
26201
26222
  var NotebookScopesResponseSchema = external_exports.object({ scopes: external_exports.array(NotebookScopeRefSchema) }).passthrough();
26202
26223
  var ScopeNotebooksResponseSchema = external_exports.object({ notebooks: external_exports.array(ScopeNotebookApiSchema) }).passthrough();
26224
+ var NotebookSharingResponseSchema = external_exports.object({ guestView: external_exports.boolean() }).passthrough();
26203
26225
  var NotebookShortcutApiSchema = external_exports.object({
26204
26226
  notebookId: external_exports.string(),
26205
26227
  scopeType: NotebookScopeTypeSchema,
@@ -26325,7 +26347,8 @@ var CHECK_TARGET_TYPES = [
26325
26347
  "revision",
26326
26348
  "beat_version",
26327
26349
  "project",
26328
- "drop"
26350
+ "drop",
26351
+ "organization"
26329
26352
  ];
26330
26353
  var CheckTargetTypeSchema = external_exports.enum(CHECK_TARGET_TYPES);
26331
26354
  var CheckApiSchema = external_exports.object({
@@ -26577,7 +26600,7 @@ var StoredMessageSchema = external_exports.object({
26577
26600
  var ConversationsResponseSchema = external_exports.array(ConversationSummarySchema);
26578
26601
  var MessagesResponseSchema = external_exports.array(StoredMessageSchema);
26579
26602
  var StartSessionRequestBodySchema = external_exports.object({
26580
- projectId: external_exports.string().min(1),
26603
+ systemId: external_exports.string().min(1),
26581
26604
  message: external_exports.string().min(1),
26582
26605
  revisionId: external_exports.string().min(1).optional(),
26583
26606
  enableWebSearch: external_exports.boolean().optional()
@@ -28487,6 +28510,59 @@ Notes filed: ${createdNotes.length}`);
28487
28510
  }
28488
28511
 
28489
28512
  // ../../libs/harmonica-services/src/mcp/tools/organization-tools.ts
28513
+ var ROOT_CODA_POLL_INTERVAL_MS = 4e3;
28514
+ var ROOT_CODA_POLL_TIMEOUT_MS = 18e4;
28515
+ var ROOT_CODA_POLL_MAX_CONSECUTIVE_ERRORS = 3;
28516
+ async function pollForRootCodaCheck(client, taskId) {
28517
+ const deadline = Date.now() + ROOT_CODA_POLL_TIMEOUT_MS;
28518
+ let consecutiveErrors = 0;
28519
+ while (Date.now() < deadline) {
28520
+ const [task, err] = await client.getTask(taskId).then(
28521
+ (t) => [t, null],
28522
+ (e) => [null, e]
28523
+ );
28524
+ if (err) {
28525
+ consecutiveErrors++;
28526
+ if (consecutiveErrors >= ROOT_CODA_POLL_MAX_CONSECUTIVE_ERRORS) {
28527
+ throw new Error(`getTask failed ${consecutiveErrors} times: ${err instanceof Error ? err.message : String(err)}`);
28528
+ }
28529
+ await new Promise((r) => setTimeout(r, ROOT_CODA_POLL_INTERVAL_MS));
28530
+ continue;
28531
+ }
28532
+ consecutiveErrors = 0;
28533
+ if (!task) throw new Error(`Task ${taskId} not found`);
28534
+ if (task.status === "completed") {
28535
+ if (!task.result) throw new Error(`Task ${taskId} completed with no result`);
28536
+ const result = task.result;
28537
+ if (!Array.isArray(result.dimensions) || typeof result.overallScore !== "number") {
28538
+ throw new Error(`Task ${taskId} did not return a root Coda scorecard \u2014 was it a run_check task?`);
28539
+ }
28540
+ return result;
28541
+ }
28542
+ if (task.status === "failed") throw new Error(`Root Coda quality check failed: ${task.error ?? "unknown error"}`);
28543
+ await new Promise((r) => setTimeout(r, ROOT_CODA_POLL_INTERVAL_MS));
28544
+ }
28545
+ throw new Error(`Root Coda quality check timed out after ${ROOT_CODA_POLL_TIMEOUT_MS / 1e3}s`);
28546
+ }
28547
+ function formatRootCodaReport(check2) {
28548
+ const bar = (score, max) => {
28549
+ const filled = Math.max(0, Math.min(max, Math.round(score)));
28550
+ return "\u2588".repeat(filled) + "\u2591".repeat(max - filled);
28551
+ };
28552
+ const dims = check2.dimensions.map(
28553
+ (d) => `**${d.label}** ${bar(d.score, d.maxScore)} ${d.score}/${d.maxScore}
28554
+ ${d.rationale}${d.suggestions?.length ? `
28555
+ \u2022 ${d.suggestions.join("\n\u2022 ")}` : ""}`
28556
+ ).join("\n\n");
28557
+ return [
28558
+ `## Root Coda Quality \u2014 ${check2.targetId}`,
28559
+ `**Overall Score:** ${check2.overallScore}/5`,
28560
+ `**Summary:** ${check2.summary}`,
28561
+ check2.topSuggestion ? `**Top Suggestion:** ${check2.topSuggestion}` : "",
28562
+ "",
28563
+ dims
28564
+ ].filter(Boolean).join("\n");
28565
+ }
28490
28566
  function registerOrganizationTools(server, ctx, client) {
28491
28567
  server.tool(
28492
28568
  "get_organization",
@@ -28616,6 +28692,67 @@ function registerOrganizationTools(server, ctx, client) {
28616
28692
  return { content: [{ type: "text", text }] };
28617
28693
  }
28618
28694
  );
28695
+ server.tool(
28696
+ "compose_root_coda",
28697
+ "Generate and apply an Organization's root Coda (top-level purpose statement) from its name, industry, description, and existing Coda if any. Drafts via LLM, applies directly to the Organization, then runs a root_coda_quality check \u2014 if the first score is below 4.0, performs exactly one automatic self-revision pass incorporating the feedback before finishing. Returns a job ID \u2014 use get_job_status to track progress.",
28698
+ {
28699
+ orgId: external_exports.string().min(1).describe("The organization ID")
28700
+ },
28701
+ async ({ orgId }) => {
28702
+ try {
28703
+ const task = await client.composeRootCoda(orgId);
28704
+ return {
28705
+ content: [{
28706
+ type: "text",
28707
+ text: [
28708
+ "Root Coda composition queued.",
28709
+ "",
28710
+ `**Organization:** ${orgId}`,
28711
+ `**Task ID:** ${task.taskId}`,
28712
+ `**Status:** ${task.status}`,
28713
+ "",
28714
+ "The agent will gather context, draft a Coda, apply it to the Organization, and grade it \u2014 revising once automatically if the first score is below 4.0.",
28715
+ "Use `get_job_status` with the job ID to check progress."
28716
+ ].join("\n")
28717
+ }]
28718
+ };
28719
+ } catch (err) {
28720
+ const message = err instanceof Error ? err.message : String(err);
28721
+ return { content: [{ type: "text", text: `compose_root_coda failed: ${message}` }], isError: true };
28722
+ }
28723
+ }
28724
+ );
28725
+ server.tool(
28726
+ "check_root_coda_quality",
28727
+ "Run a six-dimension quality check on an Organization's root Coda (Discriminating, Attributed, Reciprocal, Bounded, Generative, Consequential). By default returns a taskId immediately \u2014 set wait=true to block and receive the full scorecard inline.",
28728
+ {
28729
+ orgId: external_exports.string().min(1).describe("The organization ID"),
28730
+ wait: external_exports.boolean().optional().describe("Block until the check completes and return the scorecard inline. Default: false.")
28731
+ },
28732
+ async ({ orgId, wait }) => {
28733
+ try {
28734
+ const { taskId } = await client.runCheck(orgId, "root_coda_quality", orgId);
28735
+ if (!wait) {
28736
+ return {
28737
+ content: [{
28738
+ type: "text",
28739
+ text: [
28740
+ `Root Coda quality check enqueued for ${orgId}.`,
28741
+ `Task ID: ${taskId}`,
28742
+ "",
28743
+ `Use get_job_status with jobId="${taskId}" to poll, or list_checks with checkType="root_coda_quality" to view results.`
28744
+ ].join("\n")
28745
+ }]
28746
+ };
28747
+ }
28748
+ const check2 = await pollForRootCodaCheck(client, taskId);
28749
+ return { content: [{ type: "text", text: formatRootCodaReport(check2) }] };
28750
+ } catch (err) {
28751
+ const message = err instanceof Error ? err.message : String(err);
28752
+ return { content: [{ type: "text", text: `Root Coda quality check failed: ${message}` }], isError: true };
28753
+ }
28754
+ }
28755
+ );
28619
28756
  }
28620
28757
 
28621
28758
  // ../../libs/harmonica-services/src/mcp/tools/plan-quality-tools.ts
@@ -30316,7 +30453,12 @@ function registerProjectTools(server, ctx, client) {
30316
30453
  repoOwner: external_exports.string().optional().describe("GitHub repository owner (org or user)"),
30317
30454
  repoName: external_exports.string().optional().describe("GitHub repository name"),
30318
30455
  repoDefaultBranch: external_exports.string().optional().describe('Default branch for code check (defaults to "main")'),
30319
- rateLimitOverrides: external_exports.record(external_exports.string(), external_exports.object({ maxPerHour: external_exports.number().min(0) })).optional().describe('Per-task-type rate limit overrides, e.g. {"agent_chat":{"maxPerHour":100}}. Overrides env var and compiled defaults.')
30456
+ rateLimitOverrides: external_exports.record(external_exports.string(), external_exports.object({ maxPerHour: external_exports.number().min(0) })).optional().describe('Per-task-type rate limit overrides, e.g. {"agent_chat":{"maxPerHour":100}}. Overrides env var and compiled defaults.'),
30457
+ agentDeliveryPolicy: external_exports.object({
30458
+ openPullRequest: external_exports.boolean().nullable().optional().describe("Open a pull request when the work is ready."),
30459
+ requestAgentReview: external_exports.boolean().nullable().optional().describe("Request the agent code review (the agent-review label)."),
30460
+ watchPullRequest: external_exports.boolean().nullable().optional().describe("Watch the pull request and drive it to green.")
30461
+ }).optional().describe("Delivery latitude for agents on this System: which pull-request steps they may take without asking. Each field is tri-state \u2014 omit it to leave it alone, true to let agents proceed, false to forbid the step outright, null to clear it back to asking.")
30320
30462
  };
30321
30463
  const updateSystemHandler = async ({ systemId, ...updates }) => {
30322
30464
  try {
@@ -31867,7 +32009,7 @@ function createHttpClient(config2) {
31867
32009
  const result = await request(
31868
32010
  "POST",
31869
32011
  `/api/organizations/${encodeURIComponent(sourceOrgId)}/transfer-project`,
31870
- { projectId, targetOrgId }
32012
+ { systemId: projectId, targetOrgId }
31871
32013
  );
31872
32014
  const project = result?.project ? {
31873
32015
  ...result.project,
@@ -32975,6 +33117,15 @@ function createHttpClient(config2) {
32975
33117
  if (!result?.taskId) throw new Error("Draft coda enqueue failed: no taskId returned");
32976
33118
  return { taskId: result.taskId, taskType: "draft_coda", status: result.status ?? "pending", projectId, createdAt: (/* @__PURE__ */ new Date()).toISOString() };
32977
33119
  },
33120
+ composeRootCoda: async (orgId) => {
33121
+ const result = await request(
33122
+ "POST",
33123
+ `/api/organizations/${encodeURIComponent(orgId)}/compose-root-coda`,
33124
+ {}
33125
+ );
33126
+ if (!result?.taskId) throw new Error("Compose root Coda enqueue failed: no taskId returned");
33127
+ return { taskId: result.taskId, taskType: "compose_root_coda", status: result.status ?? "pending", projectId: orgId, createdAt: (/* @__PURE__ */ new Date()).toISOString() };
33128
+ },
32978
33129
  validateAssumptions: async (projectId, noteIds) => {
32979
33130
  const result = await request(
32980
33131
  "POST",
@@ -33042,7 +33193,17 @@ function createHttpClient(config2) {
33042
33193
  return download;
33043
33194
  },
33044
33195
  importSystemSnapshot: (snapshot, options) => request("POST", "/api/systems/import", { ...snapshot, importOptions: options }, LONG_RUNNING_TIMEOUT_MS),
33045
- importSystemSnapshotFromUrl: (url2, options) => request("POST", "/api/systems/import", { snapshotUrl: url2, importOptions: options }, LONG_RUNNING_TIMEOUT_MS),
33196
+ importSystemSnapshotFromUrl: async (url2, options) => {
33197
+ const enqueued = await request(
33198
+ "POST",
33199
+ "/api/systems/import",
33200
+ { snapshotUrl: url2, importOptions: options }
33201
+ );
33202
+ if (!enqueued?.taskId) {
33203
+ throw new Error("importSystemSnapshotFromUrl: API did not return a taskId");
33204
+ }
33205
+ return pollTaskResult(enqueued.taskId);
33206
+ },
33046
33207
  // Embedding similarity
33047
33208
  embedProjectEntities: (projectId) => request("POST", `/api/systems/${encodeURIComponent(projectId)}/embeddings/generate`),
33048
33209
  findSimilarNotes: async (noteId, projectId, options) => {
@@ -33438,6 +33599,18 @@ function createHttpClient(config2) {
33438
33599
  // Systems owned directly by an Account (B-278 v17). A 404 here means either
33439
33600
  // no such Account or the feature flag is off; both read as "nothing to show",
33440
33601
  // which is why request() returning undefined maps to an empty list.
33602
+ // Delegates to the endpoint that calls the same service the direct adapter
33603
+ // uses, so neither adapter carries its own copy of the fail-closed rule.
33604
+ // request() maps a 404 to undefined; that is itself unresolvable, so it
33605
+ // becomes an explicit unresolved result rather than a thrown error.
33606
+ resolveDeliveryPolicy: async (revisionId) => {
33607
+ const res = await request(
33608
+ "GET",
33609
+ `/api/revisions/${encodeURIComponent(revisionId)}/delivery-policy`
33610
+ );
33611
+ if (!res) return { resolved: false, reason: "revision_not_found" };
33612
+ return res;
33613
+ },
33441
33614
  listAccountSystems: async (accountId) => {
33442
33615
  const res = await request("GET", `/api/accounts/${encodeURIComponent(accountId)}/systems`);
33443
33616
  return res?.systems ?? [];
@@ -33873,7 +34046,7 @@ function loadConfig() {
33873
34046
  };
33874
34047
  }
33875
34048
  async function main() {
33876
- console.error(`[harmonica-mcp] v${"3.1.0"} starting\u2026`);
34049
+ console.error(`[harmonica-mcp] v${"3.2.0"} starting\u2026`);
33877
34050
  const config2 = loadConfig();
33878
34051
  const client = createHttpClient({
33879
34052
  apiBaseUrl: config2.apiBaseUrl,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codazen/harmonica-mcp",
3
- "version": "3.1.0",
3
+ "version": "3.2.0",
4
4
  "description": "MCP server for Harmonica — connect any MCP-compatible AI assistant to Harmonica",
5
5
  "license": "MIT",
6
6
  "bin": {