@adhdev/daemon-core 0.9.82-rc.484 → 0.9.82-rc.486

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.
@@ -51,6 +51,10 @@ export declare function __resolveSchedulingStrategyForTests(mesh: any): RepoMesh
51
51
  * scheduling pipeline can be unit-tested without standing up live CLI sessions. */
52
52
  export declare function __orderEligibleNodesForTests(meshId: string, strategy: RepoMeshSchedulingStrategy, nodes: RankableNode[], opts?: {
53
53
  bumpCursor?: boolean;
54
+ task?: {
55
+ difficulty?: string;
56
+ requiredTags?: string[];
57
+ };
54
58
  }): RankableNode[];
55
59
  /** One idle session eligible to claim a queued task, together with the resolved
56
60
  * mesh node record it belongs to. Local candidates come from live CLI instances,
@@ -126,6 +126,15 @@ export interface MeshWorkQueueEntry {
126
126
  * setConfigOption('thought_level')). Rides in payload JSON. Best-effort like model.
127
127
  */
128
128
  thinkingLevel?: string;
129
+ /**
130
+ * SLOT-ROUTING (ORCHESTRATION_NODE_SLOTS.md): the coordinator's difficulty
131
+ * classification for this task ('easy'|'medium'|'difficult'|'freeform'),
132
+ * PERSISTED on the entry so the scheduler can match it against node capability
133
+ * slots at assignment time. Previously an enqueue-only option consumed to
134
+ * resolve model/thinkingLevel and then discarded; keeping it lets task→node
135
+ * fitness matching run. Absent on tasks enqueued without a difficulty.
136
+ */
137
+ difficulty?: string;
129
138
  /**
130
139
  * M1: why this task is held back (e.g. "dependency_failed:<taskId>").
131
140
  * Only set by the system on dependency failure under the 'block' policy;
@@ -13,7 +13,7 @@
13
13
  import type { GitRepoStatus, GitCompactSummary } from './git/git-types.js';
14
14
  import type { MeshMissionSummary, MeshMissionSlimSummary } from './mesh/mesh-missions.js';
15
15
  import type { MeshMagiActivitySummary } from './mesh/mesh-magi-status.js';
16
- import type { MagiKindPanelMap, DifficultyBrainMap } from '@adhdev/mesh-shared';
16
+ import type { MagiKindPanelMap, DifficultyBrainMap, NodeCapabilitySlot } from '@adhdev/mesh-shared';
17
17
  export interface RepoMesh {
18
18
  id: string;
19
19
  name: string;
@@ -114,7 +114,7 @@ export type RepoMeshMagiSessionCleanupMode = 'preserve' | 'stop_and_delete';
114
114
  * Distribution is explicit opt-in: a strategy other than 'first_eligible' must be
115
115
  * configured for any load-spreading to occur.
116
116
  */
117
- export type RepoMeshSchedulingStrategy = 'first_eligible' | 'least_loaded' | 'round_robin' | 'priority_only';
117
+ export type RepoMeshSchedulingStrategy = 'first_eligible' | 'least_loaded' | 'round_robin' | 'priority_only' | 'fitness';
118
118
  export declare const MESH_SCHEDULING_STRATEGIES: RepoMeshSchedulingStrategy[];
119
119
  export declare const DEFAULT_MESH_SCHEDULING_STRATEGY: RepoMeshSchedulingStrategy;
120
120
  /**
@@ -310,8 +310,22 @@ export interface RepoMeshNodePolicy {
310
310
  * enforced as an additional, stricter-wins constraint on top of the global
311
311
  * maxParallelTasks/taskMode caps. Missing/empty: the node behaves exactly as
312
312
  * before (global caps only). Routing is governed solely by required_tags.
313
+ *
314
+ * SUPERSEDED by `slots` (ORCHESTRATION_NODE_SLOTS.md). Kept for back-compat:
315
+ * when `slots` is absent, providerRoles + providerPriority + the machine-global
316
+ * difficultyBrains are auto-derived into slots via deriveSlotsFromLegacy.
313
317
  */
314
318
  providerRoles?: RepoMeshProviderRole[];
319
+ /**
320
+ * Node capability slots (ORCHESTRATION_NODE_SLOTS.md) — the ordered "Preferred
321
+ * AI tools" profile that is the single source of truth for task routing, MAGI
322
+ * fan-out, and orchestrator-proposed edits. Each slot bundles provider + model
323
+ * + thinkingLevel + difficulty range + capability tags + per-slot maxParallel.
324
+ * Order = preference. When absent, the scheduler derives slots from the legacy
325
+ * providerPriority/providerRoles/difficultyBrains (deriveSlotsFromLegacy) so
326
+ * existing nodes keep working without reconfiguration.
327
+ */
328
+ slots?: NodeCapabilitySlot[];
315
329
  /**
316
330
  * Per-node override for RepoMeshPolicy.delegatedWorkerAutoApprove. When set, takes
317
331
  * precedence over the mesh-level policy for worker sessions launched onto this node.
@@ -858,6 +872,8 @@ export interface RepoMeshNodeStatus {
858
872
  activeSessions: string[];
859
873
  activeSessionDetails?: RepoMeshSessionStatus[];
860
874
  providerPriority?: string[];
875
+ /** Explicitly-configured node capability slots (ORCHESTRATION_NODE_SLOTS.md). */
876
+ slots?: NodeCapabilitySlot[];
861
877
  launchReady?: boolean;
862
878
  /** True when the node is clean, ahead=0, behind>0, and safe for fast-forward consideration. */
863
879
  autoFastForwardEligible?: boolean;
@@ -287,6 +287,13 @@ export interface SessionEntry {
287
287
  completionMarker?: string;
288
288
  seenCompletionMarker?: string;
289
289
  surfaceHidden?: boolean;
290
+ /**
291
+ * User (or coordinator-policy) muted: suppress attention side-effects
292
+ * (notifications, toasts, completion audio) for this session WITHOUT removing
293
+ * it from the inbox list. Distinct from surfaceHidden (which collapses it from
294
+ * the list). Daemon-owned, in-memory; rides the status snapshot.
295
+ */
296
+ muted?: boolean;
290
297
  settings?: Record<string, any>;
291
298
  /**
292
299
  * True owning-daemon id for a session a coordinator synthesises into its own
@@ -361,6 +368,7 @@ export interface CompactSessionEntry {
361
368
  completionMarker?: string;
362
369
  seenCompletionMarker?: string;
363
370
  surfaceHidden?: boolean;
371
+ muted?: boolean;
364
372
  controlValues?: Record<string, string | number | boolean>;
365
373
  providerControls?: ProviderControlSchema[];
366
374
  summaryMetadata?: ProviderSummaryMetadata;
@@ -64,6 +64,7 @@ export interface RecentReadDebugSnapshot {
64
64
  messageUpdatedAt: number;
65
65
  }
66
66
  export declare function shouldEmitRecentReadDebugLog(cache: Map<string, string>, snapshot: RecentReadDebugSnapshot): boolean;
67
+ export declare function buildAvailableProviders(providerLoader: StatusSnapshotOptions['providerLoader']): AvailableProviderInfo[];
67
68
  export declare function buildMachineInfo(profile?: 'full' | 'live' | 'metadata'): MachineInfo;
68
69
  /**
69
70
  * Resolve the last user-visible (non-system) message for a session as a preview.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.9.82-rc.484",
3
+ "version": "0.9.82-rc.486",
4
4
  "description": "ADHDev daemon core — CDP, IDE detection, providers, command execution",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -47,8 +47,8 @@
47
47
  "author": "vilmire",
48
48
  "license": "AGPL-3.0-or-later",
49
49
  "dependencies": {
50
- "@adhdev/mesh-shared": "0.9.82-rc.484",
51
- "@adhdev/session-host-core": "0.9.82-rc.484",
50
+ "@adhdev/mesh-shared": "0.9.82-rc.486",
51
+ "@adhdev/session-host-core": "0.9.82-rc.486",
52
52
  "@agentclientprotocol/sdk": "^0.16.1",
53
53
  "ajv": "^8.20.0",
54
54
  "ajv-formats": "^3.0.1",
@@ -208,6 +208,13 @@ export class ProviderCliAdapter implements CliAdapter {
208
208
  private lastScreenSnapshot = '';
209
209
  private lastScreenText = '';
210
210
  private lastScreenSnapshotReadAt = Number.NEGATIVE_INFINITY;
211
+ // (FALSEIDLE Path-C) Count of CONSECUTIVE getStatus polls that observed a
212
+ // gate-eligible static-idle screen (detect=idle, no modal, quiet, empty
213
+ // partial buffer). For a mesh/autonomous worker we require several such
214
+ // polls in a row before confirming static-idle (see getStatus), so a
215
+ // single momentarily-silent point-sample of a still-live turn cannot flip
216
+ // it. Reset to 0 the instant any poll is ineligible.
217
+ private staticIdlePollStreak = 0;
211
218
 
212
219
  // Server log forwarding
213
220
  private serverConn: any = null;
@@ -287,6 +294,13 @@ export class ProviderCliAdapter implements CliAdapter {
287
294
  result: any;
288
295
  } | null = null;
289
296
  private static readonly SCREEN_SNAPSHOT_MIN_INTERVAL_MS = 250;
297
+ // (FALSEIDLE Path-C) Consecutive gate-eligible getStatus polls a mesh/autonomous
298
+ // session must show before the poll-static-idle confirm fires. 2 = one extra
299
+ // status tick of hysteresis: enough to reject a single momentary-silence
300
+ // point-sample of a still-live turn, cheap enough not to materially delay a
301
+ // genuine boot-wedge release (the wedge screen is stably static, so it clears
302
+ // every consecutive poll and confirms on the 2nd).
303
+ private static readonly STATIC_IDLE_POLL_CONFIRM_COUNT = 2;
290
304
 
291
305
  private readonly providerResolutionMeta: ProviderResolutionMeta;
292
306
 
@@ -394,6 +408,20 @@ export class ProviderCliAdapter implements CliAdapter {
394
408
  return this.timeouts.statusActivityHold;
395
409
  }
396
410
 
411
+ // (FALSEIDLE Path-C) Whether this session is a mesh worker or coordinator's
412
+ // own autonomous session. Mirrors CliProviderInstance.isAutonomousMeshSession
413
+ // over the runtimeSettings the instance mirrors down via updateRuntimeSettings
414
+ // (meshNodeFor / meshActiveTaskId / meshNodeId / launchedByCoordinator =
415
+ // isMeshWorkerSession, plus meshCoordinatorFor for the coordinator's own turn).
416
+ // Such a session has no human at the keyboard to correct a premature idle, so
417
+ // the poll-static-idle confirm is debounced for it (multiple consecutive idle
418
+ // polls) rather than fired on a single point-sample.
419
+ private isAutonomousMeshSession(): boolean {
420
+ const s = this.runtimeSettings;
421
+ return !!(s?.meshNodeFor || s?.meshActiveTaskId || s?.meshNodeId
422
+ || s?.launchedByCoordinator || s?.meshCoordinatorFor);
423
+ }
424
+
397
425
  // Resolved timeouts
398
426
  private readonly timeouts: Required<NonNullable<CliProviderModule['timeouts']>>;
399
427
 
@@ -956,15 +984,56 @@ export class ProviderCliAdapter implements CliAdapter {
956
984
  const quietForMs = this.lastNonEmptyOutputAt
957
985
  ? (now - this.lastNonEmptyOutputAt)
958
986
  : Number.MAX_SAFE_INTEGER;
987
+ let eligible = false;
959
988
  if (quietForMs >= this.getStatusActivityHoldMs()) {
960
989
  const screenText = this.terminalScreen.getText();
961
990
  const pollDetect = this.runDetectStatus(screenText || this.recentOutputBuffer);
962
991
  const pollModal = this.runParseApproval(screenText)
963
992
  || this.runParseApproval(this.recentOutputBuffer);
964
- if (pollDetect === 'idle' && !pollModal) {
993
+ // (FALSEIDLE Path-C) Final-assistant / pending-response discriminator.
994
+ // Paths A and B refuse to finalize a turn whose partial-response buffer
995
+ // is still non-empty (getCompletedFinalizationBlock 'partial_response_pending'
996
+ // / completionFinalAssistantEvidence turnClosed at cli-provider-instance.ts).
997
+ // Path C (this poll) previously OMITTED it, so a genuinely-live but
998
+ // momentarily-silent turn — silent thinking, a backgrounded/long tool child,
999
+ // the gap between two assistant bubbles — whose currentTurnScope anchor was
1000
+ // lost still satisfied the weaker gate and flipped to idle prematurely.
1001
+ // Require an EMPTY partial buffer here too. getPartialResponse() returns the
1002
+ // accumulated assistant stream while isWaitingForResponse (which
1003
+ // applyGenerating leaves set on the boot-banner wedge too), so this does NOT
1004
+ // reintroduce the D4b wedge: the attach/boot-banner seeds only the static
1005
+ // ready screen — no assistant turn ever streamed — so its partial buffer is
1006
+ // empty and the gate still releases it. A mid-turn quiet gap holds a
1007
+ // non-empty buffer and is deferred.
1008
+ const partial = this.getPartialResponse();
1009
+ const partialPending = typeof partial === 'string' && partial.trim().length > 0;
1010
+ eligible = pollDetect === 'idle' && !pollModal && !partialPending;
1011
+ }
1012
+ if (eligible) {
1013
+ // (FALSEIDLE Path-C) Debounce for autonomous mesh sessions. A worker /
1014
+ // coordinator has no human to correct a premature idle, and a single
1015
+ // runDetectStatus point-sample can land in a live turn's momentary silence.
1016
+ // Require STATIC_IDLE_POLL_CONFIRM_COUNT consecutive eligible polls before
1017
+ // confirming, so the FSM must observe a sustained static-idle screen — a
1018
+ // turn that resumes (fresh output, or a re-armed turn scope) resets the
1019
+ // streak. The status poll runs on the 30s-idle / 5s-generating heartbeat and
1020
+ // this getStatus gate is re-hit each dashboard status tick, so 2 confirms is
1021
+ // ~one extra tick of hysteresis — enough to reject a one-sample silence gap
1022
+ // without materially delaying a genuine boot-wedge release. Foreground /
1023
+ // attended sessions keep the single-poll confirm (a human is watching Send).
1024
+ const requiredStreak = this.isAutonomousMeshSession()
1025
+ ? ProviderCliAdapter.STATIC_IDLE_POLL_CONFIRM_COUNT
1026
+ : 1;
1027
+ this.staticIdlePollStreak += 1;
1028
+ if (this.staticIdlePollStreak >= requiredStreak) {
965
1029
  this.engine.confirmPollStaticIdle('poll_static_idle');
1030
+ this.staticIdlePollStreak = 0;
966
1031
  }
1032
+ } else {
1033
+ this.staticIdlePollStreak = 0;
967
1034
  }
1035
+ } else {
1036
+ this.staticIdlePollStreak = 0;
968
1037
  }
969
1038
  let effectiveStatus = this.projectEffectiveStatus(startupModal);
970
1039
  let effectiveModal = startupModal || this.engine.activeModal;
@@ -1080,7 +1080,14 @@ export class DaemonCommandHandler implements CommandHelpers {
1080
1080
  if (!fs.existsSync(installRoot)) return { success: true, providers: [] };
1081
1081
 
1082
1082
  const CATEGORIES = ['cli', 'ide', 'extension', 'acp'] as const;
1083
- const items: Array<{ type: string; category: string; version: string; path: string }> = [];
1083
+ const items: Array<{
1084
+ type: string;
1085
+ category: string;
1086
+ version: string;
1087
+ path: string;
1088
+ modelOptions?: string[];
1089
+ thinkingLevelOptions?: string[];
1090
+ }> = [];
1084
1091
 
1085
1092
  for (const category of CATEGORIES) {
1086
1093
  const categoryDir = path.join(installRoot, category);
@@ -1095,11 +1102,24 @@ export class DaemonCommandHandler implements CommandHelpers {
1095
1102
  if (!manifestPath) continue;
1096
1103
  try {
1097
1104
  const m = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
1105
+ // Surface the manifest's advisory model / thinking-level lists so
1106
+ // consumers of this endpoint (standalone New-session dialog, mesh
1107
+ // node slot editor) get the same provider-specific dropdowns the
1108
+ // status-snapshot path already carries — otherwise every provider
1109
+ // (codex included) falls back to a free-text Model field.
1110
+ const modelOptions = Array.isArray(m.modelOptions)
1111
+ ? m.modelOptions.filter((x: unknown): x is string => typeof x === 'string' && !!x.trim())
1112
+ : [];
1113
+ const thinkingLevelOptions = Array.isArray(m.thinkingLevelOptions)
1114
+ ? m.thinkingLevelOptions.filter((x: unknown): x is string => typeof x === 'string' && !!x.trim())
1115
+ : [];
1098
1116
  items.push({
1099
1117
  type,
1100
1118
  category,
1101
1119
  version: typeof m.providerVersion === 'string' ? m.providerVersion : '0.0.0',
1102
1120
  path: manifestPath,
1121
+ ...(modelOptions.length ? { modelOptions } : {}),
1122
+ ...(thinkingLevelOptions.length ? { thinkingLevelOptions } : {}),
1103
1123
  });
1104
1124
  } catch {
1105
1125
  // Corrupt manifest — skip but don't fail the whole listing.
@@ -48,6 +48,18 @@ export const meshCoordinatorLaunchHandlers: Record<string, HighFamilyHandler> =
48
48
  const extraSystemPrompt = typeof args?.extraSystemPrompt === 'string'
49
49
  ? args.extraSystemPrompt.trim()
50
50
  : '';
51
+ // Optional per-launch model / thinking-level override for the
52
+ // coordinator session. Passed straight through to launch_cli,
53
+ // which applies them best-effort for providers that support it
54
+ // (claude --effort, codex model_reasoning_effort). Blank / absent
55
+ // => provider default. This is a launch-time override only; it is
56
+ // NOT persisted to the mesh coordinator config.
57
+ const initialModel = typeof args?.initialModel === 'string' && args.initialModel.trim()
58
+ ? args.initialModel.trim()
59
+ : null;
60
+ const initialThinkingLevel = typeof args?.initialThinkingLevel === 'string' && args.initialThinkingLevel.trim()
61
+ ? args.initialThinkingLevel.trim()
62
+ : null;
51
63
  if (!meshId) return { success: false, error: 'meshId required' };
52
64
 
53
65
  try {
@@ -390,6 +402,8 @@ export const meshCoordinatorLaunchHandlers: Record<string, HighFamilyHandler> =
390
402
  cliArgs: cliCmdArgs.length > 0 ? cliCmdArgs : undefined,
391
403
  env: Object.keys(cliCmdEnv).length > 0 ? cliCmdEnv : undefined,
392
404
  settings: { meshCoordinatorFor: meshId },
405
+ ...(initialModel ? { initialModel } : {}),
406
+ ...(initialThinkingLevel ? { initialThinkingLevel } : {}),
393
407
  });
394
408
 
395
409
  // R48 inject-then-remove. Spawn was just kicked off above; agy and
@@ -609,7 +623,9 @@ export const meshCoordinatorLaunchHandlers: Record<string, HighFamilyHandler> =
609
623
  env: Object.keys(launchEnv).length > 0 ? launchEnv : undefined,
610
624
  settings: {
611
625
  meshCoordinatorFor: meshId
612
- }
626
+ },
627
+ ...(initialModel ? { initialModel } : {}),
628
+ ...(initialThinkingLevel ? { initialThinkingLevel } : {}),
613
629
  });
614
630
 
615
631
  // R48 inject-then-remove. See the cli_command branch for context;
@@ -18,6 +18,7 @@ import { getGitRepoStatus } from '../../git/git-status.js';
18
18
  import {
19
19
  normalizeMeshNodeId,
20
20
  daemonIdsEquivalent,
21
+ normalizeNodeCapabilitySlots,
21
22
  } from '@adhdev/mesh-shared';
22
23
  import {
23
24
  getPendingMeshCoordinatorEvents,
@@ -357,6 +358,13 @@ export const meshStatusHandlers: Record<string, HighFamilyHandler> = {
357
358
  ? { daemonBuildVersion: node.reportedDaemonBuildVersion }
358
359
  : {}),
359
360
  providerPriority,
361
+ // ORCHESTRATION_NODE_SLOTS.md: surface the node's capability
362
+ // slots so the dashboard slot editor can read them. Only
363
+ // emitted when explicitly configured (derived-from-legacy
364
+ // slots stay implicit — the editor shows the legacy fields).
365
+ ...(Array.isArray((node.policy as any)?.slots) && (node.policy as any).slots.length
366
+ ? { slots: normalizeNodeCapabilitySlots((node.policy as any).slots) }
367
+ : {}),
360
368
  activeSessions: [],
361
369
  activeSessionDetails: [],
362
370
  launchReady: false,
@@ -64,6 +64,35 @@ export const cliAgentHandlers: Record<string, MedFamilyHandler> = {
64
64
  return ctx.deps.cliManager.handleCliCommand('record_provider_pty', args);
65
65
  },
66
66
 
67
+ // Daemon-owned per-session user Mute/Hide. Replaces the old browser-local
68
+ // localStorage layer: the user's manual hide/mute for a conversation is stored
69
+ // in-memory on the live session's settings (userHidden / userMuted) and rides
70
+ // the SAME status snapshot pipeline as the coordinator-policy surfaceHidden
71
+ // flag, so every client of this daemon sees the same state. In-memory only —
72
+ // resets on daemon restart (coordinator-spawned sessions re-derive their hidden
73
+ // default from mesh policy on relaunch). Passing null/undefined for a field
74
+ // leaves it unchanged; pass an explicit boolean to set, or false to clear an
75
+ // earlier hide/mute (e.g. unmute a coordinator-spawned worker overrides the
76
+ // policy default until restart).
77
+ set_conversation_prefs: async (ctx: MedFamilyContext, args: any) => {
78
+ const sessionId = readStringValue(args?.sessionId, (args as any)?.targetSessionId, (args as any)?.instanceId);
79
+ if (!sessionId) return { success: false, error: 'sessionId required' };
80
+ const inst = ctx.deps.instanceManager.getInstance(sessionId);
81
+ if (!inst || typeof inst.updateSettings !== 'function') {
82
+ return { success: false, error: 'Session not found or does not support preferences' };
83
+ }
84
+ const patch: Record<string, unknown> = {};
85
+ if (typeof args?.hidden === 'boolean') patch.userHidden = args.hidden;
86
+ if (typeof args?.muted === 'boolean') patch.userMuted = args.muted;
87
+ if (!Object.keys(patch).length) return { success: false, error: 'Nothing to update (hidden and/or muted required)' };
88
+ inst.updateSettings(patch);
89
+ // Push a fresh status snapshot so all clients see the updated
90
+ // surfaceHidden/muted immediately (cloud path). The standalone server has a
91
+ // parallel broadcast gate keyed on the command name (see daemon-standalone).
92
+ ctx.deps.onStatusChange?.();
93
+ return { success: true, sessionId, ...patch };
94
+ },
95
+
67
96
  agent_command: async (ctx: MedFamilyContext, args: any) => {
68
97
  // Relay-safety stamp: a dispatch carrying meshContext.coordinatorDaemonId
69
98
  // (mesh_send_task / queue assignment over P2P) is the worker daemon's chance
package/src/index.ts CHANGED
@@ -381,7 +381,7 @@ export type {
381
381
  // ── Status ──
382
382
  export { DaemonStatusReporter } from './status/reporter.js';
383
383
  export { buildSessionEntries, findCdpManager, hasCdpManager, isCdpConnected } from './status/builders.js';
384
- export { buildStatusSnapshot, buildMachineInfo, getLastDisplayMessage } from './status/snapshot.js';
384
+ export { buildStatusSnapshot, buildMachineInfo, buildAvailableProviders, getLastDisplayMessage } from './status/snapshot.js';
385
385
  export { getDaemonBuildInfo } from './build-info.js';
386
386
  export type { DaemonBuildInfo } from './build-info.js';
387
387
  export { normalizeManagedStatus, isManagedStatusWorking, isManagedStatusWaiting, normalizeActiveChatData } from './status/normalize.js';
@@ -702,7 +702,9 @@ const TOOLS_SECTION = `## Available Tools
702
702
  | \`mesh_magi_review\` | Cross-verify a read-only investigation across a standing panel of independent mesh agents (different machines/providers) instead of a single worker |
703
703
  | \`mesh_magi_collect\` | Collect + synthesize a previously dispatched MAGI fan-out by its consensus group id (async companion to mesh_magi_review wait:false) |
704
704
  | \`mesh_magi_kind_panel_set\` | Bind a task_kind → MAGI kind-panel slots (the SOLE MAGI panel-resolution surface; machine-local, wholesale replacement — approve current-vs-new first) |
705
- | \`mesh_magi_kind_panel_list\` | List configured task_kind → MAGI kind-panel slot bindings (machine-local, read-only) |`;
705
+ | \`mesh_magi_kind_panel_list\` | List configured task_kind → MAGI kind-panel slot bindings (machine-local, read-only) |
706
+ | \`mesh_node_slots_list\` | List a node's capability slots (its AI-tool profile: provider/model/thinking + difficulty range + capability tags), read-only |
707
+ | \`mesh_node_slots_set\` | PROPOSE (dry-run) or APPLY a node's capability slots — how you autonomously retune a node's tool profile; WHOLESALE replacement, present current-vs-proposed and get user approval before write=true |`;
706
708
 
707
709
  const TOOL_EXPOSURE_PREFLIGHT_SECTION = `## Tool Exposure Preflight
708
710
 
@@ -773,6 +775,7 @@ function buildRulesSection(coordinatorCliType?: string): string {
773
775
  - **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 fresh only for independent work, provider mismatch, transcript contamination, or required worktree isolation.
774
776
  - **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 by targeting \`required_tags: ["worktree=<branch>"]\` or \`target_node_id\`. Get the id/branch from the \`mesh_clone_node\` result or a live \`mesh_status\` — 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.
775
777
  - **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 — 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.
778
+ - **Retune node profiles when routing is a poor fit — 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→node fitness routing matches against. If you notice a persistent mismatch — 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 — 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.
776
779
  - **Respect explicit provider requests.** Map: Hermes → \`hermes-cli\`, Claude/Claude Code → \`claude-cli\`, Codex → \`codex-cli\`, Gemini → \`gemini-cli\`, Antigravity → \`antigravity-cli\`. Never substitute the coordinator's own runtime.
777
780
  - **Verify via git, not source.** Use \`mesh_git_status\` to confirm side effects. Treat agent summaries as self-reports, not verification.
778
781
  - **Limit parallelism.** Start with 1–2 tasks; scale only on success. Never duplicate a session because \`mesh_read_chat\` shows no final message while tool/terminal activity is ongoing. This caps *concurrent* load — it does not mean serialize independent work: when a new, independent request arrives and there is headroom under \`maxParallelTasks\`, dispatch it right away rather than waiting for an in-flight task or a user nudge (read-only diagnosis especially, since it has no merge cost).