@sellable/mcp 0.1.731 → 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
  });
@@ -1484,6 +1499,12 @@ async function selectWorkspaceExactTarget(input, workspacePlanOverride, options
1484
1499
  workspacePlan,
1485
1500
  };
1486
1501
  }
1502
+ // fix(112ak): empty-candidate terminal path — a ready-surplus gap resolves to
1503
+ // the truthful loaded terminal instead of the actionable_supply_not_queued
1504
+ // defect class (see readySurplusLoadedSelection).
1505
+ const readyLoadedSelection = readySurplusLoadedSelection(workspacePlan);
1506
+ if (readyLoadedSelection)
1507
+ return readyLoadedSelection;
1487
1508
  return {
1488
1509
  status: "blocked",
1489
1510
  ...classifyWorkspaceUnavailableTerminal(workspacePlan, (excludedTargets.length > 0 ||
@@ -1491,7 +1512,7 @@ async function selectWorkspaceExactTarget(input, workspacePlanOverride, options
1491
1512
  blockedExactTargets.length > 0
1492
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."
1493
1514
  : "The fresh workspace queue did not expose one exact campaign/table edge at globalActionQueue[0]. No refill run or mutation was started.") +
1494
- workspaceTypedBlockersNote(workspacePlan)),
1515
+ workspaceTypedBlockersNote(workspacePlan), coordinatorExcludedEdges),
1495
1516
  workspacePlan,
1496
1517
  };
1497
1518
  }
@@ -1662,6 +1683,7 @@ async function selectWorkspaceExactTarget(input, workspacePlanOverride, options
1662
1683
  blocked.blocker === "loaded_awaiting_scheduler") {
1663
1684
  return false;
1664
1685
  }
1686
+ recordExcludedEdge(exactSelectedTarget, blocked, "post_rebind");
1665
1687
  return true;
1666
1688
  })) {
1667
1689
  continue;
@@ -1774,12 +1796,18 @@ async function selectWorkspaceExactTarget(input, workspacePlanOverride, options
1774
1796
  workspacePlan,
1775
1797
  };
1776
1798
  }
1799
+ // fix(112ak): filtered-candidate terminal path — a ready-surplus gap resolves to
1800
+ // the truthful loaded terminal instead of the actionable_supply_not_queued defect
1801
+ // class (see readySurplusLoadedSelection).
1802
+ const readyLoadedSelection = readySurplusLoadedSelection(workspacePlan);
1803
+ if (readyLoadedSelection)
1804
+ return readyLoadedSelection;
1777
1805
  return {
1778
1806
  status: "blocked",
1779
1807
  ...classifyWorkspaceUnavailableTerminal(workspacePlan, (workspaceCoverage.remainingReadyOrProjectedGap !== 0
1780
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.`
1781
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.`) +
1782
- workspaceTypedBlockersNote(workspacePlan)),
1810
+ workspaceTypedBlockersNote(workspacePlan), coordinatorExcludedEdges),
1783
1811
  workspacePlan,
1784
1812
  };
1785
1813
  }
@@ -1820,17 +1848,126 @@ function yoloCoverage(plan) {
1820
1848
  targetShapeRevision: stringValue(recordValue(plan)?.targetShapeRevision),
1821
1849
  };
1822
1850
  }
1823
- // fix(112n): two different failure classes were collapsing into the one
1824
- // generic workspace_exact_target_unavailable terminal. (1) Receipts prove
1825
- // every existing lane's supply is spent and only a new campaign can serve the
1826
- // remaining gap — but new_campaign_required was reachable ONLY via a
1827
- // planner-proposed create_connection_campaign head, which Signal Discovery
1828
- // workspaces never get (no automatic cold rung), so Dotwork o112 ended
1829
- // generic instead of actionable. (2) Actionable row frontiers still exist but
1830
- // no executable edge surfaced (Amplify o112: 23 approval candidates behind a
1831
- // starved queue) — a planner exposure defect that must be loud, typed, and
1832
- // 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
+ }
1833
1923
  function workspaceActionableSupplyCensus(plan) {
1924
+ const target = yoloTarget(plan);
1925
+ const senderPlansRaw = target?.senderPlans;
1926
+ const senderPlans = Array.isArray(senderPlansRaw) ? senderPlansRaw : [];
1927
+ const convertibleByLane = new Map();
1928
+ const nonConvertibleByLane = new Map();
1929
+ for (const senderPlan of senderPlans) {
1930
+ const planRecord = recordValue(senderPlan);
1931
+ const receipt = recordValue(planRecord?.refillReceipt);
1932
+ const frontier = recordValue(receipt?.existingRowFrontier);
1933
+ const stageCounts = recordValue(frontier?.stageCounts);
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);
1941
+ }
1942
+ const convertibleLanes = [...convertibleByLane.values()];
1943
+ const nonConvertibleLanes = [...nonConvertibleByLane.values()];
1944
+ const sum = (pick) => convertibleLanes.reduce((total, lane) => total + pick(lane), 0);
1945
+ return {
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),
1951
+ senderPlanCount: senderPlans.length,
1952
+ convertibleLanes,
1953
+ nonConvertibleLanes,
1954
+ };
1955
+ }
1956
+ // fix(112ak, OWNER closing invariant): the workspace terminal classifier must
1957
+ // only count actionable supply / frontier signal from senders that still OWE
1958
+ // coverage. A COVERED sender (remainingProjectedGap 0) can carry surplus
1959
+ // hasMoreFrontierRows or surplus stage rows; that surplus must never be read as
1960
+ // "unexposed actionable supply" for a DIFFERENT sender's gap (Amplify t5:
1961
+ // Ali/Victor gap 0 but hasMoreFrontierRows true masked Sadhna's gap-10 lane
1962
+ // whose own stage census was empty and whose 10 rows were paid-InMail
1963
+ // ready_to_schedule surplus — so the workspace false-terminaled
1964
+ // actionable_supply_not_queued while every gap-bearing lane was actually
1965
+ // scheduler/pacing-bound). This re-derives the census over ONLY gap-bearing
1966
+ // senders and buckets each gap lane: real actionable supply, ready-surplus
1967
+ // (prepared rows awaiting the scheduler), or opaque (no ready/actionable/frontier
1968
+ // residue). A missing gap is treated as gap-bearing (conservative) so plans
1969
+ // without gap accounting keep the loud actionable classification.
1970
+ function workspaceGapBearingActionableSupply(plan) {
1834
1971
  const target = yoloTarget(plan);
1835
1972
  const senderPlansRaw = target?.senderPlans;
1836
1973
  const senderPlans = Array.isArray(senderPlansRaw) ? senderPlansRaw : [];
@@ -1839,50 +1976,229 @@ function workspaceActionableSupplyCensus(plan) {
1839
1976
  let enrichFrontier = 0;
1840
1977
  let rubricFrontier = 0;
1841
1978
  let frontierHasMoreRows = false;
1979
+ let gapSenderCount = 0;
1980
+ let readySurplusGapSenders = 0;
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();
1842
2010
  for (const senderPlan of senderPlans) {
1843
- const receipt = recordValue(recordValue(senderPlan)?.refillReceipt);
2011
+ const rec = recordValue(senderPlan);
2012
+ const gap = numberValue(rec?.remainingProjectedGap);
2013
+ // Only lanes that still owe coverage classify the workspace gap. A gap-0
2014
+ // (covered) sender's surplus never contributes; a missing gap is included.
2015
+ if (typeof gap === "number" && gap <= 0)
2016
+ continue;
2017
+ gapSenderCount += 1;
2018
+ const receipt = recordValue(rec?.refillReceipt);
1844
2019
  const frontier = recordValue(receipt?.existingRowFrontier);
1845
2020
  const stageCounts = recordValue(frontier?.stageCounts);
1846
- approvalFrontier += Math.max(numberValue(receipt?.approvalCandidates) ?? 0, numberValue(stageCounts?.needsApproval) ?? 0);
1847
- generateFrontier += numberValue(stageCounts?.needsGenerate) ?? 0;
1848
- enrichFrontier += numberValue(stageCounts?.needsEnrich) ?? 0;
1849
- rubricFrontier += numberValue(stageCounts?.needsRubric) ?? 0;
1850
- if (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;
2027
+ const laneReady = numberValue(stageCounts?.ready) ?? 0;
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)
1851
2036
  frontierHasMoreRows = true;
2037
+ if (laneActionable === 0 && !laneFrontierFlag) {
2038
+ if (laneReady > 0)
2039
+ readySurplusGapSenders += 1;
2040
+ else
2041
+ opaqueGapSenders += 1;
2042
+ }
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;
1852
2051
  }
2052
+ const actionableRows = approvalFrontier + generateFrontier + enrichFrontier + rubricFrontier;
1853
2053
  return {
1854
2054
  approvalFrontier,
1855
2055
  generateFrontier,
1856
2056
  enrichFrontier,
1857
2057
  rubricFrontier,
1858
2058
  frontierHasMoreRows,
1859
- senderPlanCount: senderPlans.length,
2059
+ actionableRows,
2060
+ gapSenderCount,
2061
+ readySurplusGapSenders,
2062
+ opaqueGapSenders,
2063
+ convertibleLanes,
2064
+ nonConvertibleLanes,
1860
2065
  };
1861
2066
  }
1862
- function classifyWorkspaceUnavailableTerminal(plan, contextNote) {
1863
- const census = workspaceActionableSupplyCensus(plan);
1864
- const actionableRows = census.approvalFrontier +
1865
- census.generateFrontier +
1866
- census.enrichFrontier +
1867
- census.rubricFrontier;
2067
+ // fix(112ak): when the workspace gap is owed entirely by gap-bearing lanes whose
2068
+ // residue is ready-surplus (prepared rows in stageCounts.ready, zero actionable
2069
+ // stage supply, no own frontier), the gap is scheduler/pacing-bound — the rows
2070
+ // are ready and the scheduler places them as capacity frees — NOT a
2071
+ // planner-exposure defect. Return the truthful loaded terminal instead of
2072
+ // actionable_supply_not_queued (Amplify t5: Sadhna's gap-10 lane was 10 paid-InMail
2073
+ // ready_to_schedule rows on a fully-scheduled day while COVERED senders carried
2074
+ // the only frontier signal). Gated on the gap-bearing census being empty-actionable
2075
+ // AND every gap lane ready-surplus (no opaque residue), so a genuinely-stuck or
2076
+ // genuinely-actionable gap still classifies honestly. Applied at BOTH selection
2077
+ // terminal sites (empty-candidate early path and the filtered-candidate path).
2078
+ function readySurplusLoadedSelection(workspacePlan) {
2079
+ const gapSupply = workspaceGapBearingActionableSupply(workspacePlan);
2080
+ if (gapSupply.gapSenderCount > 0 &&
2081
+ gapSupply.actionableRows === 0 &&
2082
+ !gapSupply.frontierHasMoreRows &&
2083
+ gapSupply.opaqueGapSenders === 0 &&
2084
+ gapSupply.readySurplusGapSenders > 0) {
2085
+ return {
2086
+ status: "loaded_awaiting_scheduler",
2087
+ note: "Every gap-bearing sender lane is already prepared and ready to schedule; the remaining workspace gap is scheduler/pacing-bound (ready rows awaiting placement), not unexposed actionable supply. No refill run or mutation was started." +
2088
+ workspaceTypedBlockersNote(workspacePlan),
2089
+ workspacePlan,
2090
+ };
2091
+ }
2092
+ return null;
2093
+ }
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 = []) {
2146
+ // fix(112ak): gap-gate the actionable_supply signal — only supply behind a
2147
+ // gap-bearing lane proves the planner failed to expose actionable work. A
2148
+ // covered sender's surplus frontier/stage rows do NOT justify the defect class.
2149
+ const census = workspaceGapBearingActionableSupply(plan);
2150
+ const actionableRows = census.actionableRows;
1868
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);
1869
2162
  return {
1870
2163
  code: "actionable_supply_not_queued",
1871
2164
  note: `Actionable existing-row supply remains (${census.approvalFrontier} approval, ` +
1872
2165
  `${census.generateFrontier} generate, ${census.enrichFrontier} enrich, ` +
1873
- `${census.rubricFrontier} rubric candidates across sender lanes` +
2166
+ `${census.rubricFrontier} rubric candidates across gap-bearing sender lanes` +
1874
2167
  `${census.frontierHasMoreRows ? ", plus unprocessed frontier rows" : ""}) ` +
1875
- "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."} ` +
1876
2171
  "This is retryable: re-run refill_sends with a fresh command; if it repeats, " +
1877
2172
  "report it as a planner-exposure defect. " +
1878
- 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),
1879
2194
  };
1880
2195
  }
1881
2196
  const coverage = yoloCoverage(plan);
1882
2197
  const gap = coverage.remainingReadyOrProjectedGap ?? coverage.remainingProjectedGap;
1883
2198
  // Claim receipt-proven exhaustion only when sender receipts were actually
1884
2199
  // present in the plan; a plan without senderPlans keeps the generic code.
1885
- if (census.senderPlanCount > 0 && typeof gap === "number" && gap > 0) {
2200
+ const senderPlanCount = workspaceActionableSupplyCensus(plan).senderPlanCount;
2201
+ if (senderPlanCount > 0 && typeof gap === "number" && gap > 0) {
1886
2202
  return {
1887
2203
  code: "new_campaign_required",
1888
2204
  note: `Receipts prove every existing planner-ranked campaign/lane/source family is ` +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sellable/mcp",
3
- "version": "0.1.731",
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",