@adhdev/daemon-standalone 1.0.41-rc.4 → 1.0.41-rc.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -35953,6 +35953,23 @@ var require_dist3 = __commonJS({
35953
35953
  }
35954
35954
  return total;
35955
35955
  }
35956
+ function resolveSlotMaxParallel(slots, providerType, model, modelMatchesSlot) {
35957
+ const wanted = typeof providerType === "string" ? providerType.trim().toLowerCase() : "";
35958
+ if (!wanted) return void 0;
35959
+ if (!Array.isArray(slots)) return void 0;
35960
+ const wantedModel = typeof model === "string" && model.trim() ? model.trim() : void 0;
35961
+ let total;
35962
+ for (const slot of slots) {
35963
+ if (!slot || typeof slot !== "object") continue;
35964
+ const type2 = typeof slot.provider === "string" ? slot.provider.trim().toLowerCase() : "";
35965
+ if (!type2 || type2 !== wanted) continue;
35966
+ if (!modelMatchesSlot(wantedModel, slot)) continue;
35967
+ const raw = Number(slot.maxParallel);
35968
+ if (!Number.isFinite(raw) || raw < 0) continue;
35969
+ total = (total ?? 0) + Math.floor(raw);
35970
+ }
35971
+ return total;
35972
+ }
35956
35973
  var MESH_SCHEDULING_STRATEGIES;
35957
35974
  var DEFAULT_MESH_SCHEDULING_STRATEGY;
35958
35975
  var MESH_CONVERGE_REFINE_TAG;
@@ -36539,10 +36556,10 @@ var require_dist3 = __commonJS({
36539
36556
  }
36540
36557
  function getDaemonBuildInfo() {
36541
36558
  if (cached2) return cached2;
36542
- const commit = readInjected(true ? "da2ff48eebaf23f8f0ebc61f9e17fd9fd317d0d0" : void 0) ?? "unknown";
36543
- const commitShort = readInjected(true ? "da2ff48e" : void 0) ?? (commit !== "unknown" ? commit.slice(0, 7) : "unknown");
36544
- const version2 = readInjected(true ? "1.0.41-rc.4" : void 0) ?? readInjected(typeof process !== "undefined" ? process.env?.ADHDEV_PKG_VERSION : void 0) ?? "unknown";
36545
- const builtAt = readInjected(true ? "2026-08-09T07:23:13.303Z" : void 0);
36559
+ const commit = readInjected(true ? "1fe844996962df9a0388208dec2db3160c2b9635" : void 0) ?? "unknown";
36560
+ const commitShort = readInjected(true ? "1fe84499" : void 0) ?? (commit !== "unknown" ? commit.slice(0, 7) : "unknown");
36561
+ const version2 = readInjected(true ? "1.0.41-rc.6" : void 0) ?? readInjected(typeof process !== "undefined" ? process.env?.ADHDEV_PKG_VERSION : void 0) ?? "unknown";
36562
+ const builtAt = readInjected(true ? "2026-08-09T11:53:30.826Z" : void 0);
36546
36563
  cached2 = builtAt ? { commit, commitShort, version: version2, builtAt } : { commit, commitShort, version: version2 };
36547
36564
  return cached2;
36548
36565
  }
@@ -42104,11 +42121,7 @@ ${error48.message || ""}`;
42104
42121
  DAEMON_ID_PREFIXES = ["daemon_", "standalone_"];
42105
42122
  MAGI_RAW_ANSWER_CAP = 4e3;
42106
42123
  MESH_TASK_DIFFICULTIES = ["easy", "medium", "difficult", "freeform"];
42107
- DEFAULT_DIFFICULTY_BRAINS = {
42108
- easy: { model: "haiku", thinkingLevel: "low" },
42109
- medium: { model: "sonnet", thinkingLevel: "medium" },
42110
- difficult: { model: "opus", thinkingLevel: "high" }
42111
- };
42124
+ DEFAULT_DIFFICULTY_BRAINS = {};
42112
42125
  CLI_SLOT_RECIPES = Object.freeze({
42113
42126
  "claude-cli": [
42114
42127
  {
@@ -47229,6 +47242,7 @@ Next step: ${nextStep}`;
47229
47242
  delete entry.assignedNodeId;
47230
47243
  delete entry.assignedSessionId;
47231
47244
  delete entry.assignedProviderType;
47245
+ delete entry.assignedModel;
47232
47246
  delete entry.dispatchTimestamp;
47233
47247
  entry.dispatchNonce = (entry.dispatchNonce || 0) + 1;
47234
47248
  delete entry.attemptId;
@@ -47328,6 +47342,7 @@ Next step: ${nextStep}`;
47328
47342
  delete entry.assignedNodeId;
47329
47343
  delete entry.assignedSessionId;
47330
47344
  delete entry.assignedProviderType;
47345
+ delete entry.assignedModel;
47331
47346
  delete entry.dispatchTimestamp;
47332
47347
  entry.dispatchNonce = (entry.dispatchNonce || 0) + 1;
47333
47348
  entry.strandedReclaimCount = reclaims;
@@ -47596,6 +47611,77 @@ Next step: ${nextStep}`;
47596
47611
  REDRIVE_SUPERSEDE_WINDOW_MS = 5 * 6e4;
47597
47612
  }
47598
47613
  });
47614
+ function normalizeLiteral(model) {
47615
+ return model.toLowerCase().replace(/[_/\\().,]+/g, " ").replace(/-+/g, " ").replace(/\s+/g, " ").trim();
47616
+ }
47617
+ function canonicalizeModelName(model) {
47618
+ if (typeof model !== "string") return void 0;
47619
+ const literal2 = normalizeLiteral(model);
47620
+ if (!literal2) return void 0;
47621
+ const tokens = literal2.split(" ").filter((t) => t && !MODIFIER_WORDS.has(t));
47622
+ const family = CLAUDE_FAMILIES.find((f) => tokens.includes(f));
47623
+ let version2;
47624
+ if (family) {
47625
+ const after = tokens.slice(tokens.indexOf(family) + 1).filter((t) => /^\d+$/.test(t));
47626
+ if (after.length) version2 = after.join(".");
47627
+ else {
47628
+ const dotted = tokens.find((t) => /^\d+(\.\d+)+$/.test(t));
47629
+ if (dotted) version2 = dotted;
47630
+ }
47631
+ }
47632
+ return { family, version: version2, literal: literal2 };
47633
+ }
47634
+ function modelNamesEquivalent(a, b) {
47635
+ const ca = canonicalizeModelName(a);
47636
+ const cb = canonicalizeModelName(b);
47637
+ if (!ca || !cb) return false;
47638
+ if (ca.family && cb.family) {
47639
+ if (ca.family !== cb.family) return false;
47640
+ if (ca.version && cb.version) return ca.version === cb.version;
47641
+ return true;
47642
+ }
47643
+ if (ca.family || cb.family) return false;
47644
+ return ca.literal === cb.literal;
47645
+ }
47646
+ function isModelAllowedBySlot(model, slot) {
47647
+ if (!slot) return true;
47648
+ const declared = typeof slot.model === "string" ? slot.model.trim() : "";
47649
+ const requested = typeof model === "string" ? model.trim() : "";
47650
+ if (!declared) return !requested;
47651
+ if (!requested) return true;
47652
+ return modelNamesEquivalent(requested, declared);
47653
+ }
47654
+ function decideSlotForModel(input) {
47655
+ const { requestedModel, slots } = input;
47656
+ const declaring = slots.filter((s2) => isModelAllowedBySlot(requestedModel, s2.slot));
47657
+ if (!declaring.length) {
47658
+ const declaredModels = slots.map((s2) => typeof s2.slot.model === "string" && s2.slot.model.trim() ? s2.slot.model.trim() : "(provider default)").filter((v, i, a) => a.indexOf(v) === i);
47659
+ return { outcome: "notify", reason: SLOT_MODEL_ABSENT_SKIP_REASON, declaredModels };
47660
+ }
47661
+ const free = declaring.find((s2) => s2.available);
47662
+ if (free) {
47663
+ const declared = typeof free.slot.model === "string" && free.slot.model.trim() ? free.slot.model.trim() : void 0;
47664
+ return { outcome: "run", slot: free.slot, model: declared };
47665
+ }
47666
+ return {
47667
+ outcome: "wait",
47668
+ reason: SLOT_MODEL_BUSY_SKIP_REASON,
47669
+ busySlots: declaring.map((s2) => s2.slot)
47670
+ };
47671
+ }
47672
+ var CLAUDE_FAMILIES;
47673
+ var MODIFIER_WORDS;
47674
+ var SLOT_MODEL_BUSY_SKIP_REASON;
47675
+ var SLOT_MODEL_ABSENT_SKIP_REASON;
47676
+ var init_slot_model_enforcement = __esm2({
47677
+ "src/mesh/slot-model-enforcement.ts"() {
47678
+ "use strict";
47679
+ CLAUDE_FAMILIES = ["opus", "sonnet", "haiku"];
47680
+ MODIFIER_WORDS = /* @__PURE__ */ new Set(["thinking", "latest", "preview", "claude", "anthropic"]);
47681
+ SLOT_MODEL_BUSY_SKIP_REASON = "slot_for_model_busy";
47682
+ SLOT_MODEL_ABSENT_SKIP_REASON = "no_slot_declares_requested_model";
47683
+ }
47684
+ });
47599
47685
  function loadDatabaseCtor() {
47600
47686
  if (DatabaseCtor) return DatabaseCtor;
47601
47687
  DatabaseCtor = loadBetterSqlite3();
@@ -47729,6 +47815,7 @@ Next step: ${nextStep}`;
47729
47815
  init_mesh_ledger();
47730
47816
  init_mesh_retention_config();
47731
47817
  init_mesh_work_queue();
47818
+ init_slot_model_enforcement();
47732
47819
  init_dist();
47733
47820
  loggedMigrationFailure = false;
47734
47821
  loggedStrayCleanup = false;
@@ -48526,6 +48613,43 @@ Next step: ${nextStep}`;
48526
48613
  * rows (no provider stamp) and other providers on the same node do not consume
48527
48614
  * this provider's budget, so the cap is fully backward compatible.
48528
48615
  */
48616
+ /**
48617
+ * Active assignments charged to ONE SLOT — the (provider, model) pair whose
48618
+ * `maxParallel` is being enforced — on this node.
48619
+ *
48620
+ * A row with NO `assignedModel` (claimed by an older daemon, or via an idle/event
48621
+ * drain that cannot know the launched model) counts against EVERY slot of its
48622
+ * provider. That is deliberately conservative: skipping such a row would let a
48623
+ * pre-upgrade opus task go uncounted and admit a second one past a cap of 1, which
48624
+ * is the over-subscription this cap exists to prevent. The cost is that a mixed
48625
+ * fleet can refuse slightly early, which is the safe direction.
48626
+ *
48627
+ * Model comparison goes through modelNamesEquivalent so `opus`,
48628
+ * `claude-opus-4-6` and `Claude Opus 4.6 (Thinking)` are one slot rather than
48629
+ * three separate budgets (the canon-identity defect class).
48630
+ */
48631
+ activeSlotAssignmentCount(meshId, nodeId, providerType, assignedModel) {
48632
+ const rows = this.db.prepare(`
48633
+ SELECT payload FROM mesh_queue
48634
+ WHERE mesh_id = ? AND status = 'assigned' AND assigned_node_id = ?
48635
+ `).all(meshId, nodeId);
48636
+ let count = 0;
48637
+ for (const row of rows) {
48638
+ try {
48639
+ const entry = JSON.parse(row.payload);
48640
+ if (entry.assignedProviderType !== providerType) continue;
48641
+ const rowModel = typeof entry.assignedModel === "string" ? entry.assignedModel.trim() : "";
48642
+ if (!rowModel) {
48643
+ count += 1;
48644
+ continue;
48645
+ }
48646
+ if (!assignedModel) continue;
48647
+ if (modelNamesEquivalent(rowModel, assignedModel)) count += 1;
48648
+ } catch {
48649
+ }
48650
+ }
48651
+ return count;
48652
+ }
48529
48653
  activeProviderAssignmentCount(meshId, nodeId, providerType) {
48530
48654
  const rows = this.db.prepare(`
48531
48655
  SELECT payload FROM mesh_queue
@@ -48552,6 +48676,11 @@ Next step: ${nextStep}`;
48552
48676
  if (providerType && typeof providerMaxParallel === "number" && Number.isFinite(providerMaxParallel) && providerMaxParallel >= 0 && this.activeProviderAssignmentCount(meshId, nodeId, providerType) >= providerMaxParallel) {
48553
48677
  return null;
48554
48678
  }
48679
+ const assignedModel = typeof opts?.assignedModel === "string" ? opts.assignedModel.trim() : "";
48680
+ const slotMaxParallel = opts?.slotMaxParallel;
48681
+ if (providerType && typeof slotMaxParallel === "number" && Number.isFinite(slotMaxParallel) && slotMaxParallel >= 0 && this.activeSlotAssignmentCount(meshId, nodeId, providerType, assignedModel) >= slotMaxParallel) {
48682
+ return null;
48683
+ }
48555
48684
  const nodeIdForms = expandDaemonIdForms(nodeId);
48556
48685
  const nodePinnedPlaceholders = nodeIdForms.map(() => "?").join(", ");
48557
48686
  const parseTier = (query, ...params) => {
@@ -48607,6 +48736,7 @@ Next step: ${nextStep}`;
48607
48736
  entry.assignedNodeId = nodeId;
48608
48737
  entry.assignedSessionId = sessionId;
48609
48738
  if (providerType) entry.assignedProviderType = providerType;
48739
+ if (assignedModel) entry.assignedModel = assignedModel;
48610
48740
  if (opts?.assignedTranscriptProfile) entry.assignedTranscriptProfile = opts.assignedTranscriptProfile;
48611
48741
  entry.dispatchTimestamp = now;
48612
48742
  entry.dispatchNonce = (entry.dispatchNonce || 0) + 1;
@@ -51693,24 +51823,27 @@ Default branch: \`${mesh.defaultBranch}\`` : ""}`);
51693
51823
  } catch {
51694
51824
  brains = {};
51695
51825
  }
51826
+ const brainMap = brains ?? {};
51696
51827
  const lines = [
51697
- "## Brain presets",
51828
+ "## Task difficulty",
51698
51829
  "",
51699
- "When you pass `difficulty` on `mesh_enqueue_task`, it resolves to this model / thinking level (an explicit model/thinkingLevel on the task overrides it). Pick easy for trivial work to save tokens, difficult for hard reasoning.",
51700
- ""
51830
+ "Pass `difficulty` on `mesh_enqueue_task` (`easy` / `medium` / `difficult` / `freeform`) to describe how hard the work is. It is a ROUTING HINT: it is matched against each node's capability slots, so a task goes to a slot configured for that difficulty.",
51831
+ "",
51832
+ '**The slot decides the model and thinking level \u2014 not the difficulty.** `difficulty: "difficult"` does not mean "use opus"; it means "route to a slot that handles difficult work", and that slot\'s own model/thinking is what launches. So classify honestly by how hard the task is, and change what a difficulty RUNS ON by editing the node\'s slots (`mesh_node_slots_set`), never by picking a different difficulty. Passing an explicit `model`/`thinkingLevel` still overrides everything for one task.'
51701
51833
  ];
51702
- for (const key2 of MESH_TASK_DIFFICULTIES) {
51703
- const slot = brains[key2];
51704
- if (!slot || !slot.provider && !slot.model && !slot.thinkingLevel) {
51705
- lines.push(`- **${key2}**: (no preset \u2014 ordinary routing)`);
51706
- continue;
51834
+ const configured = MESH_TASK_DIFFICULTIES.map((key2) => [key2, brainMap[key2]]).filter(([, slot]) => !!slot && (!!slot.provider || !!slot.model || !!slot.thinkingLevel));
51835
+ if (configured.length) {
51836
+ lines.push("");
51837
+ lines.push("This mesh additionally has EXPLICIT difficulty presets configured, which stamp a model/thinking at enqueue time (a difficulty-matched slot still overrides a preset value):");
51838
+ lines.push("");
51839
+ for (const [key2, slot] of configured) {
51840
+ const parts = [
51841
+ slot.provider ? `provider: \`${slot.provider}\`` : "",
51842
+ slot.model ? `model: \`${slot.model}\`` : "",
51843
+ slot.thinkingLevel ? `thinking: \`${slot.thinkingLevel}\`` : ""
51844
+ ].filter(Boolean).join(" | ");
51845
+ lines.push(`- **${key2}**: ${parts}`);
51707
51846
  }
51708
- const parts = [
51709
- slot.provider ? `provider: \`${slot.provider}\`` : "",
51710
- slot.model ? `model: \`${slot.model}\`` : "",
51711
- slot.thinkingLevel ? `thinking: \`${slot.thinkingLevel}\`` : ""
51712
- ].filter(Boolean).join(" | ");
51713
- lines.push(`- **${key2}**: ${parts}`);
51714
51847
  }
51715
51848
  return lines.join("\n");
51716
51849
  }
@@ -51776,9 +51909,11 @@ ${rules.join("\n")}`;
51776
51909
  - **Never use local sub-agents.** Do NOT spawn your runtime's own sub-agents (e.g. Claude Code's Task/Explore/Agent tools, or any equivalent in-process agent-spawning tool) to read code, investigate, run RCA, or implement. Such sub-agents execute on the coordinator's machine, outside the mesh \u2014 they escape mesh parallelism, the ledger/audit trail, node capability profiles, and worktree isolation, and leave no \`mesh_task_history\` record. ALL code reading, analysis, RCA, and implementation must be delegated to mesh nodes via \`mesh_enqueue_task\` / \`mesh_send_task\` (use \`task_mode: "live_debug_readonly"\` for read-only investigation), or cross-verified via \`mesh_magi_review\` for read-only fan-out. The coordinator's own actions are limited to \`mesh_*\` tool orchestration and synthesizing results.
51777
51910
  - **Front-load task messages.** Include everything the agent needs (files, problem, expected fix) in \`mesh_enqueue_task\` / \`mesh_send_task\`. Append a structured result request at the end: ask the worker to conclude with a JSON block containing \`status\`, \`changedFiles\`, \`gitStatus\`, \`validationResults\`, \`errors\`, \`nextAction\`. The daemon parses this automatically; you can read it from \`mesh_task_history\`.
51778
51911
  - **Reuse idle sessions.** For follow-up, retry, commit/push, or cleanup on the same issue, send only the delta to the existing idle session. Start a fresh session only when: (a) branch/worktree isolation is required, (b) the existing session had a dispatch failure or provider mismatch, (c) the transcript/runtime is contaminated or interrupted, (d) the user explicitly asks for a different provider/session, or (e) **the delta is a genuinely NEW subject rather than a continuation** \u2014 a new topic appended to an existing session can be dropped or re-run as the previous task, so give it its own task even when a session sits idle. Continuation of the same issue in an already-idle session is allowed and preferred \u2014 this rule blocks concurrent unrelated work interleaved into a live (still-generating) session, not sequential same-issue follow-ups. The test is subject continuity, not timing: carrying an investigation forward into its own fix is the SAME subject and belongs in that session (Workflow 3f), while an unrelated bug is a new subject even if the same session just went idle.
51912
+ - **Nodes are separate machines with separate checkouts \u2014 not interchangeable execution slots.** Each node is a different physical computer with its own clone of the repo. Work done on another node must be committed, pushed, and pulled back before this machine sees it, and since RELEASE/DEPLOY runs on the coordinator's own machine, sending a code change elsewhere buys a round trip out and another one back. So **default to this coordinator's own machine for code changes** \u2014 its local node (base or a worktree cloned from it). Routing to a DIFFERENT machine is the exception and needs a reason, of which there are exactly two: (a) **platform-specific verification** that cannot be done here \u2014 win32 PATH/registry, a clean install/uninstall on that OS, that machine's package-manager state; or (b) **parallelizing read-only investigation** across machines. "That node is idle" is not a reason. If you catch yourself dispatching a fix to another machine without (a) or (b), route it here instead.
51913
+ - **Don't split investigation from the fix.** When a task will plainly end in a code change, dispatch it as \`code_change\` from the start rather than a read-only investigation you hand off afterwards. The session that did the investigating already holds the findings; making a second session redo that context \u2014 especially on a different machine, where the findings have to be rewritten into a new task message \u2014 is pure loss. Carrying an investigation forward into its own fix is the SAME subject and belongs in that session (see the idle-session reuse rule). Split only when the fix genuinely belongs on another machine for reason (a) above.
51779
51914
  - **Base nodes are reserved for environment-specific testing.** Do NOT use a base node for a general code change. If a task does not strictly test OS- or machine-specific physical behavior (win32 PATH/registry, clean install/uninstall on one OS, that machine's package-manager state, OS-dependent runtime behavior), you MUST clone a worktree with \`mesh_clone_node\` and assign the task there; pin genuine environment tasks to the base with \`required_tags\`/\`target_node_id\` instead. Having several nodes available is NOT branch isolation \u2014 every base node is one shared checkout of the same branch \u2014 and cloning is ~10s with auto-launch starting the session, so there is no dispatch-cost reason to skip it.
51780
51915
  - **Worktree affinity.** A worktree is a durable per-branch workspace; keep all of a branch's code_change/fix/review work on its worktree node. Target it with \`required_tags: ["worktree=<branch>"]\` or \`target_node_id\`. Get the id/branch from the \`mesh_clone_node\` result or a live \`mesh_status\` \u2014 the Configured Nodes snapshot won't list a worktree cloned after launch. Untargeted same-branch follow-ups drift to the base node. Only \`convergence\` (merge/push) runs on the base, never pinned to the worktree.
51781
- - **Classify task difficulty to save tokens.** For each task you enqueue, judge its execution difficulty and pass \`difficulty\`: \`easy\` (extraction, renames, doc tweaks, trivial fixes), \`medium\` (ordinary feature/bugfix work), \`difficult\` (architecture, tricky debugging, multi-file refactors, subtle reasoning), or \`freeform\`. The mesh's per-difficulty brain preset then runs easy tasks on a cheaper model at low reasoning effort and hard tasks on a stronger model at high effort \u2014 real token savings on simple work. The current presets are shown in the "Brain presets" section below. You may still pass an explicit \`model\`/\`thinkingLevel\` to override the preset for one task.
51916
+ - **Classify task difficulty honestly.** For each task you enqueue, judge its execution difficulty and pass \`difficulty\`: \`easy\` (extraction, renames, doc tweaks, trivial fixes), \`medium\` (ordinary feature/bugfix work), \`difficult\` (architecture, tricky debugging, multi-file refactors, subtle reasoning), or \`freeform\`. This is a ROUTING HINT matched against node capability slots \u2014 the matched slot's OWN model/thinking is what launches, so difficulty does not name a model. Do NOT inflate or deflate difficulty to reach a model you want: fix the node's slots instead (\`mesh_node_slots_set\`). See the "Task difficulty" section below. You may still pass an explicit \`model\`/\`thinkingLevel\` to override for one task.
51782
51917
  - **Retune node profiles when routing is a poor fit \u2014 but only with approval.** A node's capability slots (its provider/model/thinking + difficulty range + capability tags, seen via \`mesh_node_slots_list\`) are what task\u2192node fitness routing matches against. If you notice a persistent mismatch \u2014 e.g. every \`difficult\` task lands on a node whose only slot is a cheap model, or a capability a node clearly has isn't declared \u2014 you MAY propose a slot change with \`mesh_node_slots_set\` (write=false). That returns current-vs-proposed; present that diff to the user with a one-line reason and apply (write=true) ONLY after they approve. It is a WHOLESALE replacement of the node's slots, so include the slots you want to keep. Never rewrite a node's profile silently or without a clear routing reason.
51783
51918
  - **Bootstrap a node's slots from what's actually installed.** When a node has NO slots configured (routing then falls back to "first available provider"), or CLI agents were newly installed on it, call \`mesh_node_slots_propose({ node_id })\` instead of hand-writing a profile. It detects the node's installed CLI agents and drafts a slot list from them \u2014 read-only, it never writes. Present its \`proposedSlots\` with the \`droppedSlots\` / \`destructive\` fields it reports (a wholesale write would delete any existing hand-tuned slot the draft doesn't reproduce, including providers not currently on PATH), then apply with \`mesh_node_slots_set({ slots: proposedSlots, write: true })\` after approval. It flags \`unknownProvider\` / \`provisional\` slots whose placement is a conservative guess rather than an attested one \u2014 call those out rather than presenting them as settled.
51784
51919
  - **Respect explicit provider requests.** Map: Hermes \u2192 \`hermes-cli\`, Claude/Claude Code \u2192 \`claude-cli\`, Codex \u2192 \`codex-cli\`, Gemini \u2192 \`gemini-cli\`, Antigravity \u2192 \`antigravity-cli\`. Never substitute the coordinator's own runtime.
@@ -59929,77 +60064,6 @@ The mesh has no work in flight. For each mission, decide its outcome: continue i
59929
60064
  ]);
59930
60065
  }
59931
60066
  });
59932
- function normalizeLiteral(model) {
59933
- return model.toLowerCase().replace(/[_/\\().,]+/g, " ").replace(/-+/g, " ").replace(/\s+/g, " ").trim();
59934
- }
59935
- function canonicalizeModelName(model) {
59936
- if (typeof model !== "string") return void 0;
59937
- const literal2 = normalizeLiteral(model);
59938
- if (!literal2) return void 0;
59939
- const tokens = literal2.split(" ").filter((t) => t && !MODIFIER_WORDS.has(t));
59940
- const family = CLAUDE_FAMILIES.find((f) => tokens.includes(f));
59941
- let version2;
59942
- if (family) {
59943
- const after = tokens.slice(tokens.indexOf(family) + 1).filter((t) => /^\d+$/.test(t));
59944
- if (after.length) version2 = after.join(".");
59945
- else {
59946
- const dotted = tokens.find((t) => /^\d+(\.\d+)+$/.test(t));
59947
- if (dotted) version2 = dotted;
59948
- }
59949
- }
59950
- return { family, version: version2, literal: literal2 };
59951
- }
59952
- function modelNamesEquivalent(a, b) {
59953
- const ca = canonicalizeModelName(a);
59954
- const cb = canonicalizeModelName(b);
59955
- if (!ca || !cb) return false;
59956
- if (ca.family && cb.family) {
59957
- if (ca.family !== cb.family) return false;
59958
- if (ca.version && cb.version) return ca.version === cb.version;
59959
- return true;
59960
- }
59961
- if (ca.family || cb.family) return false;
59962
- return ca.literal === cb.literal;
59963
- }
59964
- function isModelAllowedBySlot(model, slot) {
59965
- if (!slot) return true;
59966
- const declared = typeof slot.model === "string" ? slot.model.trim() : "";
59967
- const requested = typeof model === "string" ? model.trim() : "";
59968
- if (!declared) return !requested;
59969
- if (!requested) return true;
59970
- return modelNamesEquivalent(requested, declared);
59971
- }
59972
- function decideSlotForModel(input) {
59973
- const { requestedModel, slots } = input;
59974
- const declaring = slots.filter((s2) => isModelAllowedBySlot(requestedModel, s2.slot));
59975
- if (!declaring.length) {
59976
- const declaredModels = slots.map((s2) => typeof s2.slot.model === "string" && s2.slot.model.trim() ? s2.slot.model.trim() : "(provider default)").filter((v, i, a) => a.indexOf(v) === i);
59977
- return { outcome: "notify", reason: SLOT_MODEL_ABSENT_SKIP_REASON, declaredModels };
59978
- }
59979
- const free = declaring.find((s2) => s2.available);
59980
- if (free) {
59981
- const declared = typeof free.slot.model === "string" && free.slot.model.trim() ? free.slot.model.trim() : void 0;
59982
- return { outcome: "run", slot: free.slot, model: declared };
59983
- }
59984
- return {
59985
- outcome: "wait",
59986
- reason: SLOT_MODEL_BUSY_SKIP_REASON,
59987
- busySlots: declaring.map((s2) => s2.slot)
59988
- };
59989
- }
59990
- var CLAUDE_FAMILIES;
59991
- var MODIFIER_WORDS;
59992
- var SLOT_MODEL_BUSY_SKIP_REASON;
59993
- var SLOT_MODEL_ABSENT_SKIP_REASON;
59994
- var init_slot_model_enforcement = __esm2({
59995
- "src/mesh/slot-model-enforcement.ts"() {
59996
- "use strict";
59997
- CLAUDE_FAMILIES = ["opus", "sonnet", "haiku"];
59998
- MODIFIER_WORDS = /* @__PURE__ */ new Set(["thinking", "latest", "preview", "claude", "anthropic"]);
59999
- SLOT_MODEL_BUSY_SKIP_REASON = "slot_for_model_busy";
60000
- SLOT_MODEL_ABSENT_SKIP_REASON = "no_slot_declares_requested_model";
60001
- }
60002
- });
60003
60067
  function encodeDuplicateMeshDispatchCode(holderSessionId) {
60004
60068
  const holder = typeof holderSessionId === "string" ? holderSessionId.trim() : "";
60005
60069
  return holder ? `${DUPLICATE_MESH_DISPATCH_CODE}:${holder}` : DUPLICATE_MESH_DISPATCH_CODE;
@@ -60378,12 +60442,18 @@ The mesh has no work in flight. For each mission, decide its outcome: continue i
60378
60442
  return false;
60379
60443
  }
60380
60444
  const capabilityTags = buildMeshNodeCapabilityTags(node, providerType);
60381
- const providerMaxParallel = resolveProviderMaxParallel(resolveNodeCapabilitySlots(node, meshId), providerType);
60445
+ const nodeSlotsForCap = resolveNodeCapabilitySlots(node, meshId);
60446
+ const providerMaxParallel = resolveProviderMaxParallel(nodeSlotsForCap, providerType);
60447
+ const assignedModel = typeof routingDecision?.resolvedModel === "string" && routingDecision.resolvedModel.trim() ? routingDecision.resolvedModel.trim() : void 0;
60448
+ const claimingSlot = nodeSlotsForCap.find((s2) => s2.provider?.trim() === providerType && isModelAllowedBySlot(assignedModel, s2));
60449
+ const slotMaxParallel = claimingSlot ? resolveSlotMaxParallel(nodeSlotsForCap, providerType, claimingSlot.model, isModelAllowedBySlot) : void 0;
60382
60450
  const nodeIsWorktree = node?.isLocalWorktree === true;
60383
60451
  const assignedTranscriptProfile = resolveClaimingSessionTranscriptProfile(components, sessionId);
60384
60452
  const task = claimNextTask(meshId, nodeId, sessionId, capabilityTags, {
60385
60453
  providerType,
60386
60454
  ...providerMaxParallel !== void 0 ? { providerMaxParallel } : {},
60455
+ ...assignedModel ? { assignedModel } : {},
60456
+ ...slotMaxParallel !== void 0 ? { slotMaxParallel } : {},
60387
60457
  nodeIsWorktree,
60388
60458
  ...assignedTranscriptProfile ? { assignedTranscriptProfile } : {}
60389
60459
  });
@@ -60878,6 +60948,13 @@ The mesh has no work in flight. For each mission, decide its outcome: continue i
60878
60948
  score += quotaBonus;
60879
60949
  return score;
60880
60950
  }
60951
+ function orderSlotsForProviderSelection(slots, meshId, nodeId, node, task, quotaBonusByProvider) {
60952
+ return [...slots].sort((a, b) => {
60953
+ const capDelta = Number(slotHasCapacity(meshId, nodeId, node, b)) - Number(slotHasCapacity(meshId, nodeId, node, a));
60954
+ if (capDelta !== 0) return capDelta;
60955
+ return scoreSlotForTask(b, task, quotaBonusByProvider?.[b.provider] ?? 0) - scoreSlotForTask(a, task, quotaBonusByProvider?.[a.provider] ?? 0);
60956
+ });
60957
+ }
60881
60958
  function bestSlotForTask(node, task, meshId, quotaBonusByProvider) {
60882
60959
  const slots = resolveNodeCapabilitySlots(node, meshId);
60883
60960
  if (!slots.length) return null;
@@ -60948,12 +61025,31 @@ The mesh has no work in flight. For each mission, decide its outcome: continue i
60948
61025
  function activeProviderAssignedCount(meshId, nodeId, providerType) {
60949
61026
  return getQueue(meshId, { status: ["assigned"] }).filter((task) => daemonIdsEquivalent(task.assignedNodeId, nodeId) && task.assignedProviderType === providerType).length;
60950
61027
  }
60951
- function slotHasCapacity(meshId, nodeId, node, slot) {
61028
+ function activeSlotAssignedCount(meshId, nodeId, providerType, slot) {
61029
+ return getQueue(meshId, { status: ["assigned"] }).filter((task) => {
61030
+ if (!daemonIdsEquivalent(task.assignedNodeId, nodeId)) return false;
61031
+ if (task.assignedProviderType !== providerType) return false;
61032
+ const assignedModel = task.assignedModel;
61033
+ if (typeof assignedModel !== "string" || !assignedModel.trim()) return true;
61034
+ return isModelAllowedBySlot(assignedModel, slot);
61035
+ }).length;
61036
+ }
61037
+ function slotCapacityRemaining(meshId, nodeId, node, slot) {
60952
61038
  const providerType = typeof slot.provider === "string" ? slot.provider.trim() : "";
60953
- if (!providerType) return false;
60954
- const cap = resolveProviderMaxParallel(resolveNodeCapabilitySlots(node, meshId), providerType);
60955
- if (cap === void 0) return true;
60956
- return activeProviderAssignedCount(meshId, nodeId, providerType) < cap;
61039
+ if (!providerType) return { available: false };
61040
+ const slots = resolveNodeCapabilitySlots(node, meshId);
61041
+ const slotCap = resolveSlotMaxParallel(slots, providerType, slot.model, isModelAllowedBySlot);
61042
+ if (slotCap !== void 0 && activeSlotAssignedCount(meshId, nodeId, providerType, slot) >= slotCap) {
61043
+ return { available: false, slotCap };
61044
+ }
61045
+ const providerCap = resolveProviderMaxParallel(slots, providerType);
61046
+ if (providerCap !== void 0 && activeProviderAssignedCount(meshId, nodeId, providerType) >= providerCap) {
61047
+ return { available: false, slotCap, providerCap };
61048
+ }
61049
+ return { available: true, slotCap, providerCap };
61050
+ }
61051
+ function slotHasCapacity(meshId, nodeId, node, slot) {
61052
+ return slotCapacityRemaining(meshId, nodeId, node, slot).available;
60957
61053
  }
60958
61054
  function sessionHasActiveAssignment(meshId, sessionId) {
60959
61055
  if (getQueue(meshId, { status: ["assigned"] }).some((task) => sessionIdsEquivalent(task.assignedSessionId, sessionId))) {
@@ -61098,7 +61194,7 @@ The mesh has no work in flight. For each mission, decide its outcome: continue i
61098
61194
  const slots = resolveNodeCapabilitySlots(node, meshId);
61099
61195
  if (!slots.length) return { reason: "missing_provider_priority" };
61100
61196
  const quotaBonusByProvider = task ? quotaSpreadBonusByProvider(node, quotaRouting) : void 0;
61101
- const orderedSlots = task ? [...slots].sort((a, b) => scoreSlotForTask(b, task, quotaBonusByProvider?.[b.provider] ?? 0) - scoreSlotForTask(a, task, quotaBonusByProvider?.[a.provider] ?? 0)) : slots;
61197
+ const orderedSlots = task ? orderSlotsForProviderSelection(slots, meshId ?? "", nodeId, node, task, quotaBonusByProvider) : slots;
61102
61198
  const failed = [];
61103
61199
  for (const slot of orderedSlots) {
61104
61200
  const requestedType = slot.provider;
@@ -79236,6 +79332,7 @@ ${lastSnapshot}`;
79236
79332
  var import_promises4 = require("fs/promises");
79237
79333
  var import_node_path2 = require("path");
79238
79334
  var import_node_util4 = require("util");
79335
+ init_repo_mesh_types();
79239
79336
  init_mesh_config();
79240
79337
  var import_fs12 = require("fs");
79241
79338
  var import_path11 = require("path");
@@ -79730,6 +79827,7 @@ ${lastSnapshot}`;
79730
79827
  );
79731
79828
  }
79732
79829
  if (operation === "clone_worktree") {
79830
+ const planWarnings = [];
79733
79831
  if (!compatibleMesh) {
79734
79832
  return failure2(
79735
79833
  "mesh_not_found",
@@ -79738,14 +79836,20 @@ ${lastSnapshot}`;
79738
79836
  { discovery, membership }
79739
79837
  );
79740
79838
  }
79741
- if (discovery.dirty) {
79839
+ const dirtyBehavior = mergeAndNormalizePolicy(compatibleMesh.policy, void 0).dirtyWorkspaceBehavior;
79840
+ if (discovery.dirty && dirtyBehavior === "block") {
79742
79841
  return failure2(
79743
79842
  "dirty_workspace",
79744
79843
  `Workspace has ${discovery.changedFileCount} uncommitted change(s); a cloned worktree would not include them.`,
79745
- "Commit or stash the changes, then re-run the clone plan.",
79844
+ "Commit or stash the changes, then re-run the clone plan. (This mesh's dirtyWorkspaceBehavior is 'block'; set it to 'warn' to allow cloning from a dirty workspace.)",
79746
79845
  { discovery, membership }
79747
79846
  );
79748
79847
  }
79848
+ if (discovery.dirty) {
79849
+ planWarnings.push(
79850
+ `Source workspace has ${discovery.changedFileCount} uncommitted change(s). The new worktree is created from HEAD, so uncommitted changes are NOT included in it \u2014 commit them first if the cloned branch needs them.`
79851
+ );
79852
+ }
79749
79853
  const branch = (options.branch || "").trim();
79750
79854
  if (!branch || !await git(discovery.repoRoot, ["check-ref-format", "--branch", branch], true)) {
79751
79855
  return failure2(
@@ -79773,8 +79877,11 @@ ${lastSnapshot}`;
79773
79877
  compatibleMesh: meshSummary(compatibleMesh),
79774
79878
  plan: {
79775
79879
  kind: "clone_new_worktree",
79776
- summary: `Create a clean isolated worktree node from '${sourceNode.id}' on branch '${branch}'.`,
79777
- requiresClean: true,
79880
+ summary: `Create an isolated worktree node from '${sourceNode.id}' on branch '${branch}'.`,
79881
+ // Only a 'block' mesh actually requires a clean source; under the
79882
+ // default 'warn' a dirty workspace is advisory, so reporting true here
79883
+ // would misdescribe the plan the caller is approving.
79884
+ requiresClean: dirtyBehavior === "block",
79778
79885
  approvalRequired: true,
79779
79886
  steps: [{
79780
79887
  command: "clone_mesh_node",
@@ -79791,7 +79898,8 @@ ${lastSnapshot}`;
79791
79898
  alternatives: [{ kind: "add_existing_workspace", summary: "Register the supplied checkout without creating a worktree.", requiresClean: false }]
79792
79899
  },
79793
79900
  suggestedConfig: suggestedConfig2,
79794
- note: "Read-only plan only. No mesh, config, branch, remote or worktree state was changed."
79901
+ note: "Read-only plan only. No mesh, config, branch, remote or worktree state was changed.",
79902
+ ...planWarnings.length ? { warnings: planWarnings } : {}
79795
79903
  };
79796
79904
  }
79797
79905
  const suggestedConfig = runMeshInit(compatibleMesh || {}, discovery.repoRoot, options.detectedProviders || []);