@sellable/mcp 0.1.732 → 0.1.733

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.
@@ -1712,8 +1712,8 @@ export declare function executeRefillSendsCommand(input?: RefillSendsCommandInpu
1712
1712
  readOnly: boolean;
1713
1713
  ok: boolean;
1714
1714
  status: "blocked";
1715
- code: "workspace_exact_target_unavailable" | "workspace_exact_target_contract_missing" | "exact_target_config_unavailable" | "exact_target_config_mismatch" | "exact_target_projection_mismatch" | "new_campaign_required" | "actionable_supply_not_queued";
1716
- blocker: "workspace_exact_target_unavailable" | "workspace_exact_target_contract_missing" | "exact_target_config_unavailable" | "exact_target_config_mismatch" | "exact_target_projection_mismatch" | "new_campaign_required" | "actionable_supply_not_queued";
1715
+ code: "workspace_exact_target_unavailable" | "workspace_exact_target_contract_missing" | "exact_target_config_unavailable" | "exact_target_config_mismatch" | "exact_target_projection_mismatch" | "new_campaign_required" | "actionable_supply_not_queued" | "source_supply_exhausted_at_rubric";
1716
+ blocker: "workspace_exact_target_unavailable" | "workspace_exact_target_contract_missing" | "exact_target_config_unavailable" | "exact_target_config_mismatch" | "exact_target_projection_mismatch" | "new_campaign_required" | "actionable_supply_not_queued" | "source_supply_exhausted_at_rubric";
1717
1717
  note: string;
1718
1718
  targetPlan: unknown;
1719
1719
  exactTargetPlan: {} | null;
@@ -2931,8 +2931,8 @@ export declare function executeRefillSendsCommand(input?: RefillSendsCommandInpu
2931
2931
  } | {
2932
2932
  ok: boolean;
2933
2933
  status: "blocked";
2934
- code: "workspace_exact_target_unavailable" | "workspace_exact_target_contract_missing" | "exact_target_config_unavailable" | "exact_target_config_mismatch" | "exact_target_projection_mismatch" | "new_campaign_required" | "actionable_supply_not_queued";
2935
- blocker: "workspace_exact_target_unavailable" | "workspace_exact_target_contract_missing" | "exact_target_config_unavailable" | "exact_target_config_mismatch" | "exact_target_projection_mismatch" | "new_campaign_required" | "actionable_supply_not_queued";
2934
+ code: "workspace_exact_target_unavailable" | "workspace_exact_target_contract_missing" | "exact_target_config_unavailable" | "exact_target_config_mismatch" | "exact_target_projection_mismatch" | "new_campaign_required" | "actionable_supply_not_queued" | "source_supply_exhausted_at_rubric";
2935
+ blocker: "workspace_exact_target_unavailable" | "workspace_exact_target_contract_missing" | "exact_target_config_unavailable" | "exact_target_config_mismatch" | "exact_target_projection_mismatch" | "new_campaign_required" | "actionable_supply_not_queued" | "source_supply_exhausted_at_rubric";
2936
2936
  note: string;
2937
2937
  targetPlan: unknown;
2938
2938
  exactTargetPlan: {} | null;
@@ -454,7 +454,7 @@ function buildRefillSendsCommand(input) {
454
454
  workflowAsset: {
455
455
  subskillName: "refill-sends-workflow",
456
456
  assetPath: "core/flow.v1.json",
457
- expectedVersion: "v1.8",
457
+ expectedVersion: "v1.9",
458
458
  },
459
459
  yolo,
460
460
  executionMode,
@@ -502,7 +502,7 @@ function buildRefillSendsCommand(input) {
502
502
  },
503
503
  firstOperationalSteps: [
504
504
  'Load get_subskill_prompt({ subskillName: "refill-sends-workflow" }) before any product operation.',
505
- 'Load get_subskill_asset({ subskillName: "refill-sends-workflow", assetPath: "core/flow.v1.json" }) before any product operation; continue chunks until hasMore is false, parse JSON, and require exactly workflow:"refill-sends-workflow" and version:"v1.8". Report the verified version before D1. Any other or unverified version is workflow_version_mismatch and must stop before get_refill_target_plan or mutation.',
505
+ 'Load get_subskill_asset({ subskillName: "refill-sends-workflow", assetPath: "core/flow.v1.json" }) before any product operation; continue chunks until hasMore is false, parse JSON, and require exactly workflow:"refill-sends-workflow" and version:"v1.9". Report the verified version before D1. Any other or unverified version is workflow_version_mismatch and must stop before get_refill_target_plan or mutation.',
506
506
  "A skill cannot create or invoke /goal by itself. If this refill is already running inside an active Codex goal, keep that goal open until every selected sender lane is horizon-filled by projected coverage (sent + scheduled), Christian explicitly stops/statuses the run, or a concrete non-scheduler blocker appears.",
507
507
  `Call get_refill_target_plan({ intent: "${intent}"${workspaceId ? `, workspaceId: "${workspaceId}"` : ""}${input.campaignId ? `, campaignId: "${input.campaignId}"` : ""}${input.tableId ? `, tableId: "${input.tableId}"` : ""}${senderIds.length > 0 ? `, senderIds: ${JSON.stringify(senderIds)}` : ""}${senderNames.length > 0
508
508
  ? `, senderNames: ${JSON.stringify(senderNames)}`
@@ -1395,6 +1395,20 @@ async function selectWorkspaceExactTarget(input, workspacePlanOverride, options
1395
1395
  const prefilterBlockedTargets = blockedExactTargets.filter((blocked) => blocked.blocker !== "complete" &&
1396
1396
  blocked.blocker !== "capped_by_scheduler");
1397
1397
  const seenTargetKeys = new Set();
1398
+ // fix(112ak): every refusal is recorded so a non-empty-census terminal can
1399
+ // name the concrete per-lane blocker instead of reporting a bare dead end.
1400
+ const coordinatorExcludedEdges = [];
1401
+ const recordExcludedEdge = (candidate, blocked, stage) => {
1402
+ coordinatorExcludedEdges.push({
1403
+ campaignId: candidate.campaignId,
1404
+ tableId: candidate.tableId,
1405
+ senderIds: candidate.senderIds,
1406
+ actionTypes: candidate.actionTypes,
1407
+ actionKey: blocked?.actionKey ?? candidate.actionKey ?? null,
1408
+ blocker: blocked?.blocker ?? null,
1409
+ stage,
1410
+ });
1411
+ };
1398
1412
  const targetCandidates = actionTargets.filter((candidate) => {
1399
1413
  const key = workspaceExactTargetKey(candidate);
1400
1414
  if (seenTargetKeys.has(key))
@@ -1424,6 +1438,7 @@ async function selectWorkspaceExactTarget(input, workspacePlanOverride, options
1424
1438
  workspaceActionFamily(candidate.actionKey) === "run_scheduler_sweep") {
1425
1439
  return false;
1426
1440
  }
1441
+ recordExcludedEdge(candidate, blocked, "prefilter");
1427
1442
  return true;
1428
1443
  }));
1429
1444
  });
@@ -1497,7 +1512,7 @@ async function selectWorkspaceExactTarget(input, workspacePlanOverride, options
1497
1512
  blockedExactTargets.length > 0
1498
1513
  ? "The fresh workspace queue did not expose another exact campaign/table edge after excluding the structurally exhausted target. No refill run or mutation was started."
1499
1514
  : "The fresh workspace queue did not expose one exact campaign/table edge at globalActionQueue[0]. No refill run or mutation was started.") +
1500
- workspaceTypedBlockersNote(workspacePlan)),
1515
+ workspaceTypedBlockersNote(workspacePlan), coordinatorExcludedEdges),
1501
1516
  workspacePlan,
1502
1517
  };
1503
1518
  }
@@ -1668,6 +1683,7 @@ async function selectWorkspaceExactTarget(input, workspacePlanOverride, options
1668
1683
  blocked.blocker === "loaded_awaiting_scheduler") {
1669
1684
  return false;
1670
1685
  }
1686
+ recordExcludedEdge(exactSelectedTarget, blocked, "post_rebind");
1671
1687
  return true;
1672
1688
  })) {
1673
1689
  continue;
@@ -1791,7 +1807,7 @@ async function selectWorkspaceExactTarget(input, workspacePlanOverride, options
1791
1807
  ...classifyWorkspaceUnavailableTerminal(workspacePlan, (workspaceCoverage.remainingReadyOrProjectedGap !== 0
1792
1808
  ? `The fresh workspace plan still has ${workspaceCoverage.remainingReadyOrProjectedGap ?? "an unknown number of"} rows to prepare, but its executable exact-target candidates were all complete or scheduler-loaded. Refusing a false loaded_awaiting_scheduler terminal; the planner must expose the remaining actionable lane or a concrete blocker.`
1793
1809
  : `The fresh workspace queue remained incomplete, but all ${completedExactTargetCount} exposed exact targets were already complete and no active preparation or import work was pending. No identical refill fence was reopened.`) +
1794
- workspaceTypedBlockersNote(workspacePlan)),
1810
+ workspaceTypedBlockersNote(workspacePlan), coordinatorExcludedEdges),
1795
1811
  workspacePlan,
1796
1812
  };
1797
1813
  }
@@ -1832,43 +1848,109 @@ function yoloCoverage(plan) {
1832
1848
  targetShapeRevision: stringValue(recordValue(plan)?.targetShapeRevision),
1833
1849
  };
1834
1850
  }
1835
- // fix(112n): two different failure classes were collapsing into the one
1836
- // generic workspace_exact_target_unavailable terminal. (1) Receipts prove
1837
- // every existing lane's supply is spent and only a new campaign can serve the
1838
- // remaining gap — but new_campaign_required was reachable ONLY via a
1839
- // planner-proposed create_connection_campaign head, which Signal Discovery
1840
- // workspaces never get (no automatic cold rung), so Dotwork o112 ended
1841
- // generic instead of actionable. (2) Actionable row frontiers still exist but
1842
- // no executable edge surfaced (Amplify o112: 23 approval candidates behind a
1843
- // starved queue) — a planner exposure defect that must be loud, typed, and
1844
- // retryable, never a quiet dead-end.
1851
+ function laneFrontierIsRubricTerminal(frontier) {
1852
+ const diagnosis = recordValue(recordValue(frontier)?.failureDiagnosis);
1853
+ if (!diagnosis)
1854
+ return false;
1855
+ if (diagnosis.retryable !== false)
1856
+ return false;
1857
+ if (stringValue(diagnosis.stage) !== "icp_or_rubric_rejection")
1858
+ return false;
1859
+ const status = stringValue(diagnosis.status);
1860
+ return status === "zero_yield" || status === "low_yield";
1861
+ }
1862
+ // One frontier receipt describes one campaign table's lane, however many senders
1863
+ // share it. Fall back to sender identity only when the receipt did not name the
1864
+ // table it measured, so independent lanes still sum.
1865
+ function workspaceLaneFrontierKey(lane) {
1866
+ return lane.campaignId && lane.tableId
1867
+ ? `table:${lane.campaignId}:${lane.tableId}:${lane.actionType ?? ""}`
1868
+ : `sender:${lane.senderId ?? ""}:${lane.actionType ?? ""}`;
1869
+ }
1870
+ function workspaceLaneFrontier(planRecord, receipt, frontier, stageCounts) {
1871
+ const senderId = stringValue(planRecord?.senderId) ?? stringValue(receipt?.senderId);
1872
+ const diagnosis = recordValue(frontier?.failureDiagnosis);
1873
+ const evidence = recordValue(diagnosis?.evidence);
1874
+ const rejectionCounts = recordValue(evidence?.counts);
1875
+ return {
1876
+ senderId,
1877
+ senderIds: senderId ? [senderId] : [],
1878
+ actionType: stringValue(planRecord?.actionType) ?? stringValue(receipt?.actionType),
1879
+ campaignId: stringValue(receipt?.selectedCampaignId),
1880
+ tableId: stringValue(receipt?.selectedTableId),
1881
+ approval: Math.max(numberValue(receipt?.approvalCandidates) ?? 0, numberValue(stageCounts?.needsApproval) ?? 0),
1882
+ generate: numberValue(stageCounts?.needsGenerate) ?? 0,
1883
+ enrich: numberValue(stageCounts?.needsEnrich) ?? 0,
1884
+ rubric: numberValue(stageCounts?.needsRubric) ?? 0,
1885
+ hasMoreFrontierRows: frontier?.hasMoreFrontierRows === true,
1886
+ checkedRows: numberValue(evidence?.checkedRows) ?? null,
1887
+ rubricRejections: numberValue(rejectionCounts?.rubric_rejection) ?? null,
1888
+ };
1889
+ }
1890
+ // Keep the deepest observation of a shared table lane so a terminal quotes the
1891
+ // scan that actually proved exhaustion, not the shallowest — but carry EVERY
1892
+ // sender the lane covers so no enrolled sender is left unexplained.
1893
+ function mergeNonConvertibleLane(lanes, laneKey, lane) {
1894
+ const seen = lanes.get(laneKey);
1895
+ if (!seen) {
1896
+ lanes.set(laneKey, lane);
1897
+ return;
1898
+ }
1899
+ const senderIds = [...new Set([...seen.senderIds, ...lane.senderIds])];
1900
+ if ((lane.checkedRows ?? 0) > (seen.checkedRows ?? 0)) {
1901
+ lanes.set(laneKey, { ...lane, senderIds });
1902
+ }
1903
+ else {
1904
+ seen.senderIds = senderIds;
1905
+ }
1906
+ }
1907
+ // Same table lane observed twice: keep the richest observation rather than
1908
+ // summing duplicates of one frontier.
1909
+ function mergeConvertibleLane(lanes, laneKey, lane) {
1910
+ const existing = lanes.get(laneKey);
1911
+ if (!existing) {
1912
+ lanes.set(laneKey, lane);
1913
+ return;
1914
+ }
1915
+ existing.approval = Math.max(existing.approval, lane.approval);
1916
+ existing.generate = Math.max(existing.generate, lane.generate);
1917
+ existing.enrich = Math.max(existing.enrich, lane.enrich);
1918
+ existing.rubric = Math.max(existing.rubric, lane.rubric);
1919
+ existing.hasMoreFrontierRows =
1920
+ existing.hasMoreFrontierRows || lane.hasMoreFrontierRows;
1921
+ existing.senderIds = [...new Set([...existing.senderIds, ...lane.senderIds])];
1922
+ }
1845
1923
  function workspaceActionableSupplyCensus(plan) {
1846
1924
  const target = yoloTarget(plan);
1847
1925
  const senderPlansRaw = target?.senderPlans;
1848
1926
  const senderPlans = Array.isArray(senderPlansRaw) ? senderPlansRaw : [];
1849
- let approvalFrontier = 0;
1850
- let generateFrontier = 0;
1851
- let enrichFrontier = 0;
1852
- let rubricFrontier = 0;
1853
- let frontierHasMoreRows = false;
1927
+ const convertibleByLane = new Map();
1928
+ const nonConvertibleByLane = new Map();
1854
1929
  for (const senderPlan of senderPlans) {
1855
- const receipt = recordValue(recordValue(senderPlan)?.refillReceipt);
1930
+ const planRecord = recordValue(senderPlan);
1931
+ const receipt = recordValue(planRecord?.refillReceipt);
1856
1932
  const frontier = recordValue(receipt?.existingRowFrontier);
1857
1933
  const stageCounts = recordValue(frontier?.stageCounts);
1858
- approvalFrontier += Math.max(numberValue(receipt?.approvalCandidates) ?? 0, numberValue(stageCounts?.needsApproval) ?? 0);
1859
- generateFrontier += numberValue(stageCounts?.needsGenerate) ?? 0;
1860
- enrichFrontier += numberValue(stageCounts?.needsEnrich) ?? 0;
1861
- rubricFrontier += numberValue(stageCounts?.needsRubric) ?? 0;
1862
- if (frontier?.hasMoreFrontierRows === true)
1863
- frontierHasMoreRows = true;
1934
+ const lane = workspaceLaneFrontier(planRecord, receipt, frontier, stageCounts);
1935
+ const laneKey = workspaceLaneFrontierKey(lane);
1936
+ if (laneFrontierIsRubricTerminal(frontier)) {
1937
+ mergeNonConvertibleLane(nonConvertibleByLane, laneKey, lane);
1938
+ continue;
1939
+ }
1940
+ mergeConvertibleLane(convertibleByLane, laneKey, lane);
1864
1941
  }
1942
+ const convertibleLanes = [...convertibleByLane.values()];
1943
+ const nonConvertibleLanes = [...nonConvertibleByLane.values()];
1944
+ const sum = (pick) => convertibleLanes.reduce((total, lane) => total + pick(lane), 0);
1865
1945
  return {
1866
- approvalFrontier,
1867
- generateFrontier,
1868
- enrichFrontier,
1869
- rubricFrontier,
1870
- frontierHasMoreRows,
1946
+ approvalFrontier: sum((lane) => lane.approval),
1947
+ generateFrontier: sum((lane) => lane.generate),
1948
+ enrichFrontier: sum((lane) => lane.enrich),
1949
+ rubricFrontier: sum((lane) => lane.rubric),
1950
+ frontierHasMoreRows: convertibleLanes.some((lane) => lane.hasMoreFrontierRows),
1871
1951
  senderPlanCount: senderPlans.length,
1952
+ convertibleLanes,
1953
+ nonConvertibleLanes,
1872
1954
  };
1873
1955
  }
1874
1956
  // fix(112ak, OWNER closing invariant): the workspace terminal classifier must
@@ -1897,6 +1979,34 @@ function workspaceGapBearingActionableSupply(plan) {
1897
1979
  let gapSenderCount = 0;
1898
1980
  let readySurplusGapSenders = 0;
1899
1981
  let opaqueGapSenders = 0;
1982
+ // fix(112ak, supply validation + de-duplication): the gap-bearing census is the
1983
+ // authority behind the OWNER closing invariant, so it must count CONVERTIBLE
1984
+ // rows ONCE. Two live inflations broke that on the 2026-07-29 sweep.
1985
+ //
1986
+ // 1. Double counting. Every sender enrolled on a shared campaign reports the
1987
+ // SAME table frontier: sellable's "681 enrich" was one 68-row frontier on
1988
+ // campaign cmrcd4x4a counted 10x (nine send_invite lanes plus one
1989
+ // send_inmail_closed lane on the same table). Sums are now keyed by the
1990
+ // campaign/table/lane the receipt was actually measured on; plans without
1991
+ // table identity keep per-sender identity so independent lanes still sum.
1992
+ //
1993
+ // 2. Non-convertible rows counted as supply. Clover's lanes reported
1994
+ // stageCounts of ~0 with hasMoreFrontierRows true while their own receipts
1995
+ // carried failureDiagnosis {stage:"icp_or_rubric_rejection",
1996
+ // retryable:false} over the 4,993 rubric-rejected rows behind the scan
1997
+ // window. hasMoreFrontierRows is a SCAN-WINDOW flag, not evidence that
1998
+ // anything behind it can convert. Such a lane no longer inflates the
1999
+ // census; it is carried as non-convertible (with every sender it covers)
2000
+ // so the terminal can name a truthful exhaustion instead of the defect
2001
+ // class. Its bucket still counts honestly below, so a rubric-terminal lane
2002
+ // with no ready rows reads opaque and never fakes a loaded terminal.
2003
+ //
2004
+ // Only the receipt's own non-retryable rubric verdict disqualifies a lane.
2005
+ // Retryable diagnoses (sellable's dependency_blocked/retryable:true) and other
2006
+ // stages (Dotwork's approval_blocked over a real needsApproval row) stay
2007
+ // convertible, so the 112af-c/112ai masked-supply admission is untouched.
2008
+ const convertibleByLane = new Map();
2009
+ const nonConvertibleByLane = new Map();
1900
2010
  for (const senderPlan of senderPlans) {
1901
2011
  const rec = recordValue(senderPlan);
1902
2012
  const gap = numberValue(rec?.remainingProjectedGap);
@@ -1908,26 +2018,37 @@ function workspaceGapBearingActionableSupply(plan) {
1908
2018
  const receipt = recordValue(rec?.refillReceipt);
1909
2019
  const frontier = recordValue(receipt?.existingRowFrontier);
1910
2020
  const stageCounts = recordValue(frontier?.stageCounts);
1911
- const approval = Math.max(numberValue(receipt?.approvalCandidates) ?? 0, numberValue(stageCounts?.needsApproval) ?? 0);
1912
- const generate = numberValue(stageCounts?.needsGenerate) ?? 0;
1913
- const enrich = numberValue(stageCounts?.needsEnrich) ?? 0;
1914
- const rubric = numberValue(stageCounts?.needsRubric) ?? 0;
1915
- const laneActionable = approval + generate + enrich + rubric;
1916
- const laneFrontier = frontier?.hasMoreFrontierRows === true;
2021
+ const lane = workspaceLaneFrontier(rec, receipt, frontier, stageCounts);
2022
+ const rubricTerminal = laneFrontierIsRubricTerminal(frontier);
2023
+ const laneActionable = rubricTerminal
2024
+ ? 0
2025
+ : lane.approval + lane.generate + lane.enrich + lane.rubric;
2026
+ const laneFrontierFlag = rubricTerminal ? false : lane.hasMoreFrontierRows;
1917
2027
  const laneReady = numberValue(stageCounts?.ready) ?? 0;
1918
- approvalFrontier += approval;
1919
- generateFrontier += generate;
1920
- enrichFrontier += enrich;
1921
- rubricFrontier += rubric;
1922
- if (laneFrontier)
2028
+ const laneKey = workspaceLaneFrontierKey(lane);
2029
+ if (rubricTerminal) {
2030
+ mergeNonConvertibleLane(nonConvertibleByLane, laneKey, lane);
2031
+ }
2032
+ else {
2033
+ mergeConvertibleLane(convertibleByLane, laneKey, lane);
2034
+ }
2035
+ if (laneFrontierFlag)
1923
2036
  frontierHasMoreRows = true;
1924
- if (laneActionable === 0 && !laneFrontier) {
2037
+ if (laneActionable === 0 && !laneFrontierFlag) {
1925
2038
  if (laneReady > 0)
1926
2039
  readySurplusGapSenders += 1;
1927
2040
  else
1928
2041
  opaqueGapSenders += 1;
1929
2042
  }
1930
2043
  }
2044
+ const convertibleLanes = [...convertibleByLane.values()];
2045
+ const nonConvertibleLanes = [...nonConvertibleByLane.values()];
2046
+ for (const lane of convertibleLanes) {
2047
+ approvalFrontier += lane.approval;
2048
+ generateFrontier += lane.generate;
2049
+ enrichFrontier += lane.enrich;
2050
+ rubricFrontier += lane.rubric;
2051
+ }
1931
2052
  const actionableRows = approvalFrontier + generateFrontier + enrichFrontier + rubricFrontier;
1932
2053
  return {
1933
2054
  approvalFrontier,
@@ -1939,6 +2060,8 @@ function workspaceGapBearingActionableSupply(plan) {
1939
2060
  gapSenderCount,
1940
2061
  readySurplusGapSenders,
1941
2062
  opaqueGapSenders,
2063
+ convertibleLanes,
2064
+ nonConvertibleLanes,
1942
2065
  };
1943
2066
  }
1944
2067
  // fix(112ak): when the workspace gap is owed entirely by gap-bearing lanes whose
@@ -1968,23 +2091,106 @@ function readySurplusLoadedSelection(workspacePlan) {
1968
2091
  }
1969
2092
  return null;
1970
2093
  }
1971
- function classifyWorkspaceUnavailableTerminal(plan, contextNote) {
2094
+ // fix(112ak): a receipt-proven rubric-terminal frontier is a truthful source
2095
+ // exhaustion, so name it per lane instead of leaving the terminal generic.
2096
+ function workspaceRubricExhaustionNote(lanes) {
2097
+ const parts = lanes.slice(0, 12).map((lane) => {
2098
+ const scope = [
2099
+ lane.campaignId ?? lane.senderId,
2100
+ lane.actionType,
2101
+ lane.senderIds.length > 0 ? `${lane.senderIds.length} sender(s)` : null,
2102
+ ]
2103
+ .filter((part) => Boolean(part))
2104
+ .join(":");
2105
+ const detail = [
2106
+ typeof lane.checkedRows === "number"
2107
+ ? `${lane.checkedRows} checked`
2108
+ : null,
2109
+ typeof lane.rubricRejections === "number"
2110
+ ? `${lane.rubricRejections} rubric-rejected`
2111
+ : null,
2112
+ ]
2113
+ .filter((part) => Boolean(part))
2114
+ .join(", ");
2115
+ return detail
2116
+ ? `${scope || "lane"} (${detail})`
2117
+ : scope || "unnamed sender lane";
2118
+ });
2119
+ if (parts.length === 0)
2120
+ return "";
2121
+ const overflow = lanes.length - parts.length;
2122
+ return ` Per-lane exhaustion evidence: ${parts.join("; ")}${overflow > 0 ? `; +${overflow} more lanes` : ""}.`;
2123
+ }
2124
+ function coordinatorExcludedEdgesNote(edges) {
2125
+ if (edges.length === 0)
2126
+ return "";
2127
+ const seen = new Set();
2128
+ const parts = [];
2129
+ for (const edge of edges) {
2130
+ const key = `${edge.campaignId}:${edge.tableId}:${edge.actionTypes.join(",")}:${edge.actionKey ?? ""}:${edge.blocker ?? ""}`;
2131
+ if (seen.has(key))
2132
+ continue;
2133
+ seen.add(key);
2134
+ const lane = edge.actionTypes.join("/") || "lane";
2135
+ const senders = edge.senderIds.length > 0
2136
+ ? `${edge.senderIds.length} sender(s)`
2137
+ : "no named sender";
2138
+ parts.push(`${edge.campaignId}:${lane} (${senders}) refused as ${edge.blocker ?? "already-decided"}${edge.actionKey ? ` on ${edge.actionKey}` : ""} at ${edge.stage}`);
2139
+ if (parts.length >= 8)
2140
+ break;
2141
+ }
2142
+ const overflow = edges.length - parts.length;
2143
+ return ` Exact edges this run refused: ${parts.join("; ")}${overflow > 0 ? `; +${overflow} more` : ""}.`;
2144
+ }
2145
+ function classifyWorkspaceUnavailableTerminal(plan, contextNote, excludedEdges = []) {
1972
2146
  // fix(112ak): gap-gate the actionable_supply signal — only supply behind a
1973
2147
  // gap-bearing lane proves the planner failed to expose actionable work. A
1974
2148
  // covered sender's surplus frontier/stage rows do NOT justify the defect class.
1975
2149
  const census = workspaceGapBearingActionableSupply(plan);
1976
2150
  const actionableRows = census.actionableRows;
1977
2151
  if (actionableRows > 0 || census.frontierHasMoreRows) {
2152
+ // fix(112ak, OWNER closing invariant): a non-empty census may never end as
2153
+ // a bare defect terminal — the coordinator must dispatch an edge or NAME
2154
+ // the concrete per-lane blocker. Every edge this run refused is already
2155
+ // recorded with its sender/lane/actionKey and the receipt that refused it,
2156
+ // so surface that instead of claiming no edge was exposed. On the live
2157
+ // 2026-07-29 sellable run the planner DID expose a prepare edge; the
2158
+ // coordinator dropped it because the exact preflight re-issued an
2159
+ // actionKey already proven exact_action_complete, then reported "no
2160
+ // executable exact edge" — true only of the coordinator, not the planner.
2161
+ const exclusionNote = coordinatorExcludedEdgesNote(excludedEdges);
1978
2162
  return {
1979
2163
  code: "actionable_supply_not_queued",
1980
2164
  note: `Actionable existing-row supply remains (${census.approvalFrontier} approval, ` +
1981
2165
  `${census.generateFrontier} generate, ${census.enrichFrontier} enrich, ` +
1982
2166
  `${census.rubricFrontier} rubric candidates across gap-bearing sender lanes` +
1983
2167
  `${census.frontierHasMoreRows ? ", plus unprocessed frontier rows" : ""}) ` +
1984
- "but the fresh workspace queue exposed no executable exact edge for it. " +
2168
+ `${exclusionNote
2169
+ ? "and every exact edge the planner exposed for it was refused by this run."
2170
+ : "but the fresh workspace queue exposed no executable exact edge for it."} ` +
1985
2171
  "This is retryable: re-run refill_sends with a fresh command; if it repeats, " +
1986
2172
  "report it as a planner-exposure defect. " +
1987
- contextNote,
2173
+ contextNote +
2174
+ exclusionNote,
2175
+ };
2176
+ }
2177
+ // fix(112ak): the convertible census is empty and the only frontier left is
2178
+ // receipt-proven rubric-terminal (Clover 2026-07-29: nine lanes, all
2179
+ // non-retryable icp_or_rubric_rejection, 4,993 rubric-rejected rows behind
2180
+ // the scan window). That is a truthful source exhaustion, not the
2181
+ // planner-exposure defect class and not a generic dead end.
2182
+ if (census.nonConvertibleLanes.length > 0) {
2183
+ return {
2184
+ code: "source_supply_exhausted_at_rubric",
2185
+ note: `Every remaining existing-row frontier is receipt-proven exhausted at the ICP/rubric ` +
2186
+ `stage across ${census.nonConvertibleLanes.length} campaign/table lane(s): the preparation ` +
2187
+ "receipts report a non-retryable zero/low-yield rubric rejection, so the rows behind " +
2188
+ "the scan window cannot convert into sends. No campaign was created and no source was " +
2189
+ "expanded — widening the rubric or adding fresh source rows is the only remaining fill " +
2190
+ "path; surface this to the user. " +
2191
+ contextNote +
2192
+ workspaceRubricExhaustionNote(census.nonConvertibleLanes) +
2193
+ coordinatorExcludedEdgesNote(excludedEdges),
1988
2194
  };
1989
2195
  }
1990
2196
  const coverage = yoloCoverage(plan);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sellable/mcp",
3
- "version": "0.1.732",
3
+ "version": "0.1.733",
4
4
  "type": "module",
5
5
  "description": "Sellable MCP server for Claude Code, Codex, and Hermes campaign workflows",
6
6
  "main": "dist/index.js",
@@ -75,7 +75,7 @@ get_subskill_asset({ subskillName: "refill-sends-workflow", assetPath: "core/flo
75
75
  ```
76
76
 
77
77
  Continue chunks until `hasMore:false`; require
78
- exactly `workflow:"refill-sends-workflow"` and `version:"v1.8"`, then report
78
+ exactly `workflow:"refill-sends-workflow"` and `version:"v1.9"`, then report
79
79
  that verified version before D1. Any other or unverified version is
80
80
  `workflow_version_mismatch`: stop before `get_refill_target_plan` or mutation.
81
81
  Route intent is immutable:
@@ -484,9 +484,13 @@ flowchart TD
484
484
 
485
485
  One scheduler-loaded exact lane is not a workspace terminal: skip it and
486
486
  continue other planner-ranked lanes. Do not keep polling after the coordinator
487
- returns workspace `loaded_awaiting_scheduler`, which is valid only when every
488
- remaining exact target is complete or scheduler-owned and workspace
489
- `remainingReadyOrProjectedGap` is zero. It is not permission for a host-owned
487
+ returns workspace `loaded_awaiting_scheduler`. That terminal is admissible by
488
+ either path: every remaining exact target is complete or scheduler-owned and
489
+ workspace `remainingReadyOrProjectedGap` is zero, OR every gap-bearing sender
490
+ lane is already prepared and its residue is `ready_to_schedule` surplus, so the
491
+ remaining gap is scheduler/pacing-bound rather than unexposed supply — on that
492
+ second path `remainingReadyOrProjectedGap` is not necessarily zero. Neither path
493
+ means unexposed actionable supply, and neither is permission for a host-owned
490
494
  loop. The sole refill-ladder expansion
491
495
  that asks the user in `--yolo` is `new_campaign_required`; refill itself never
492
496
  creates that campaign. `new_campaign_required` is also the truthful terminal
@@ -497,7 +501,17 @@ remaining gap and ask the user; a future handler may automate the creation. A
497
501
  (approval/generate/enrich/rubric or unprocessed frontier rows) still exist but
498
502
  no executable exact edge surfaced: retry once with a fresh command, and if it
499
503
  repeats report it as a planner-exposure defect instead of treating the
500
- workspace as exhausted.
504
+ workspace as exhausted. That census counts CONVERTIBLE rows only, de-duplicated
505
+ per campaign/table lane — a frontier whose own preparation receipt carries a
506
+ non-retryable `icp_or_rubric_rejection` diagnosis is proven unable to convert,
507
+ so it never inflates the census. When such receipts are all that remain, the
508
+ terminal is `source_supply_exhausted_at_rubric`: a truthful source exhaustion,
509
+ not a defect and not retryable. Report the per-lane evidence from the note and
510
+ tell the user that widening the rubric or adding fresh source rows is the only
511
+ remaining fill path. Whenever a terminal reports a non-empty census, it also
512
+ names every exact edge the run refused (`Exact edges this run refused: ...`) —
513
+ surface those named blockers rather than describing the workspace as having no
514
+ available work.
501
515
 
502
516
  ## Non-negotiable authority and proof
503
517
 
@@ -24,7 +24,7 @@ In `--yolo`, the coordinator may
24
24
  perform every fresh planner-ranked safe transition among existing campaigns,
25
25
  lanes, and source families. It may not create a new campaign.
26
26
 
27
- The host must load the entire asset and verify exactly `version:"v1.8"` before
27
+ The host must load the entire asset and verify exactly `version:"v1.9"` before
28
28
  D1. It reports that verified version explicitly. A different or unverified
29
29
  version is `workflow_version_mismatch` and stops before planner or mutation.
30
30
 
@@ -32,7 +32,7 @@ version is `workflow_version_mismatch` and stops before planner or mutation.
32
32
 
33
33
  ```mermaid
34
34
  flowchart TD
35
- A["Receive exact workspace, sender/action selectors, and date envelope"] --> ASSET["Verify complete flow.v1.json, workflow refill-sends-workflow, exact version v1.8"]
35
+ A["Receive exact workspace, sender/action selectors, and date envelope"] --> ASSET["Verify complete flow.v1.json, workflow refill-sends-workflow, exact version v1.9"]
36
36
  ASSET --> D1["Call get_refill_target_plan read-only with immutable route intent"]
37
37
  D1 --> RENDER["Render sender/campaign waterfall, coverage ledger, bounded side effects, forbidden actions, and stop condition"]
38
38
  RENDER --> D2["Call refill_sends with workspaceCoordinator true and displayed revision/action pins"]
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "v1.8",
2
+ "version": "v1.9",
3
3
  "workflow": "refill-sends-workflow",
4
4
  "principle": "The planner chooses one exact next action and refill_sends alone executes it. Every bounded action is receipted and followed by a canonical replan. Yolo may traverse every planner-ranked existing campaign, lane, and source family; only new campaign creation requires the user.",
5
5
  "requiredBootstrap": [
@@ -17,7 +17,7 @@
17
17
  "mustReadUntil": { "hasMore": false },
18
18
  "validation": {
19
19
  "workflow": "refill-sends-workflow",
20
- "exactVersion": "v1.8",
20
+ "exactVersion": "v1.9",
21
21
  "requiredTopLevelKeys": [
22
22
  "states",
23
23
  "gateOrder",
@@ -430,11 +430,12 @@
430
430
  ],
431
431
  "terminalContract": {
432
432
  "complete": "Projected sent plus scheduler-owned scheduled coverage meets the canonical target.",
433
- "loaded_awaiting_scheduler": "Every remaining exact target is complete or scheduler-owned and workspace remainingReadyOrProjectedGap is zero after the bounded scheduler wait; stop without a host poll loop.",
434
- "blocked_retryable": "Return the exact blocker, receipt identity, and safe retry condition.",
433
+ "loaded_awaiting_scheduler": "Terminal, no continuation. Admissible by EITHER path: (a) every remaining exact target is complete or scheduler-owned and workspace remainingReadyOrProjectedGap is zero after the bounded scheduler wait; or (b) every gap-bearing sender lane is already prepared and its residue is ready_to_schedule surplus, so the remaining gap is scheduler/pacing-bound (ready rows awaiting placement) rather than unexposed supply — remainingReadyOrProjectedGap is NOT necessarily zero on this path. Neither path means unexposed actionable supply. Report the ready inventory and stop without a host poll loop.",
434
+ "blocked_retryable": "Retryable. Return the exact blocker, receipt identity, and safe retry condition. Specific blockers (for example no_forward_progress, provider outages, lease_lost) are reported UNDER this class, not as their own terminal classes.",
435
435
  "lanes_exhausted": "Internal handoff only when another pinned existing target remains; continue with next_exact_target.",
436
436
  "new_campaign_required": "Every existing campaign, lane, and source family is spent or infeasible; ask whether to create the proposed campaign. Refill never creates it.",
437
- "actionable_supply_not_queued": "Actionable existing-row supply (approval/generate/enrich/rubric candidates or unprocessed frontier rows) remains but the fresh workspace queue exposed no executable exact edge. Retry with a fresh command once; if it repeats, report a planner-exposure defect with the census from the note."
437
+ "source_supply_exhausted_at_rubric": "Terminal and NOT retryable — do not re-run and do not treat it as blocked_retryable. Every remaining existing-row frontier is receipt-proven exhausted at the ICP/rubric stage: the preparation receipts carry a non-retryable icp_or_rubric_rejection diagnosis, so the rows behind the scan window cannot convert into sends no matter how many times the command runs. Distinct from new_campaign_required: the source rows exist, they simply cannot pass the current rubric. Report the per-lane evidence from the note (campaign/table lane, senders covered, rows checked, rows rubric-rejected) and tell the user the only remaining fill paths are widening the rubric or adding fresh source supply. Refill changes neither on its own.",
438
+ "actionable_supply_not_queued": "Actionable existing-row supply (approval/generate/enrich/rubric candidates or unprocessed frontier rows) remains but no executable exact edge was dispatched for it. The census counts CONVERTIBLE rows only, de-duplicated per campaign/table lane; frontiers proven non-convertible by their own receipts are reported as source_supply_exhausted_at_rubric instead. When the run refused edges the planner did expose, the note NAMES each one (campaign:lane, sender count, actionKey, and the refusal point) — report those named blockers, not just the census. Retry with a fresh command once; if it repeats, report a planner-exposure defect with the census and the named refused edges from the note."
438
439
  },
439
440
  "yoloMode": {
440
441
  "approvalSource": "explicit_yolo_flag",