@adhdev/mesh-shared 1.0.29 → 1.0.30-rc.2
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.d.ts +1 -0
- package/dist/index.js +166 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +162 -1
- package/dist/index.mjs.map +1 -1
- package/dist/mesh-tool-names.d.ts +2 -2
- package/dist/slot-proposal.d.ts +147 -0
- package/package.json +1 -1
- package/src/index.ts +1 -0
- package/src/mesh-tool-names.ts +1 -0
- package/src/slot-proposal.ts +327 -0
package/src/mesh-tool-names.ts
CHANGED
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI auto-detect → capability-slot / MAGI-panel PROPOSAL generator.
|
|
3
|
+
*
|
|
4
|
+
* Detection of installed CLI providers already exists per node (the status
|
|
5
|
+
* snapshot's `availableProviders`), and applying a slot profile already exists
|
|
6
|
+
* (`mesh_node_slots_set`, dry-run by default). What was missing is the bit in
|
|
7
|
+
* between: turning "these CLIs are installed on this node" into a concrete
|
|
8
|
+
* `NodeCapabilitySlot[]` draft the operator can review. This module is that
|
|
9
|
+
* bridge, and nothing more — it is a PURE proposal generator. It never writes;
|
|
10
|
+
* the caller feeds its output into the existing dry-run/approve tools.
|
|
11
|
+
*
|
|
12
|
+
* ─── Why the mapping is a static table ───────────────────────────────────────
|
|
13
|
+
*
|
|
14
|
+
* Provider manifests carry NO difficulty or capability-grade information. The
|
|
15
|
+
* fields that look like they might (`modelOptions`, `thinkingLevelOptions`)
|
|
16
|
+
* describe what a provider ACCEPTS, not what it is GOOD AT — a provider listing
|
|
17
|
+
* `opus` says nothing about whether opus should get the hard tasks. So there is
|
|
18
|
+
* no honest way to derive difficulty from the manifest today.
|
|
19
|
+
*
|
|
20
|
+
* The table below is therefore seeded from the operator's real, in-use slot
|
|
21
|
+
* configuration rather than from a guess. That makes it a starting point with
|
|
22
|
+
* actual provenance, and it is deliberately isolated in ONE constant so the
|
|
23
|
+
* planned usage-data-driven replacement has a single, obvious swap point.
|
|
24
|
+
*
|
|
25
|
+
* Kept in the dependency-free mesh-shared leaf because both daemon-core (which
|
|
26
|
+
* has the detection data) and mcp-server (which owns the propose/apply tools)
|
|
27
|
+
* need it, and it is pure data + pure functions on plain objects.
|
|
28
|
+
*/
|
|
29
|
+
import {
|
|
30
|
+
normalizeNodeCapabilitySlot,
|
|
31
|
+
type MeshTaskDifficulty,
|
|
32
|
+
type NodeCapabilitySlot,
|
|
33
|
+
} from './brain-routing'
|
|
34
|
+
import type { MagiSlot } from './magi'
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* One provider's seeded slot recipe. A provider may map to MORE THAN ONE slot
|
|
38
|
+
* (claude-cli does: a wide sonnet slot plus a narrow opus slot for the hard
|
|
39
|
+
* work), which is why the table's values are arrays.
|
|
40
|
+
*/
|
|
41
|
+
export interface CliSlotRecipe {
|
|
42
|
+
/** Optional model to pin on the slot. Best-effort at launch. */
|
|
43
|
+
model?: string
|
|
44
|
+
/** Optional thinking level, in the provider's own vocabulary. */
|
|
45
|
+
thinkingLevel?: string
|
|
46
|
+
/** Difficulty range this slot handles. Empty = general-purpose. */
|
|
47
|
+
difficulty?: MeshTaskDifficulty[]
|
|
48
|
+
/** Per-slot concurrency cap. */
|
|
49
|
+
maxParallel?: number
|
|
50
|
+
/**
|
|
51
|
+
* Set when this recipe is an unvalidated guess rather than a transcription
|
|
52
|
+
* of a slot the operator actually runs. Surfaced on the proposal so the
|
|
53
|
+
* reviewer knows which lines to scrutinize.
|
|
54
|
+
*/
|
|
55
|
+
provisional?: boolean
|
|
56
|
+
/** Short human-readable justification, echoed into the proposal. */
|
|
57
|
+
rationale?: string
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* ★ THE MAPPING TABLE — the single swap point.
|
|
62
|
+
*
|
|
63
|
+
* Seeded (2026-08-03) from the operator's live slot configuration, on the
|
|
64
|
+
* reasoning that a transcription of what is actually in production beats an
|
|
65
|
+
* invented heuristic. Replace wholesale once usage data (success rate, cost,
|
|
66
|
+
* turn latency per provider×difficulty) can drive it.
|
|
67
|
+
*
|
|
68
|
+
* Every entry except `hermes-cli` reflects a real configured slot. `hermes-cli`
|
|
69
|
+
* is a conservative GUESS — see its `provisional` flag.
|
|
70
|
+
*/
|
|
71
|
+
export const CLI_SLOT_RECIPES: Readonly<Record<string, readonly CliSlotRecipe[]>> = Object.freeze<Record<string, readonly CliSlotRecipe[]>>({
|
|
72
|
+
'claude-cli': [
|
|
73
|
+
{
|
|
74
|
+
model: 'sonnet',
|
|
75
|
+
thinkingLevel: 'high',
|
|
76
|
+
difficulty: ['medium', 'easy'],
|
|
77
|
+
maxParallel: 5,
|
|
78
|
+
rationale: 'Primary workhorse — widest parallelism for routine work.',
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
model: 'opus',
|
|
82
|
+
thinkingLevel: 'high',
|
|
83
|
+
difficulty: ['difficult'],
|
|
84
|
+
maxParallel: 1,
|
|
85
|
+
rationale: 'Reserved for hard tasks; capped at 1 to bound cost.',
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
'kimi': [
|
|
89
|
+
{
|
|
90
|
+
model: 'kimi-code/k3',
|
|
91
|
+
difficulty: ['medium', 'difficult'],
|
|
92
|
+
maxParallel: 2,
|
|
93
|
+
rationale: 'Independent second opinion on mid/hard work.',
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
'codex-cli': [
|
|
97
|
+
{
|
|
98
|
+
difficulty: ['medium', 'difficult', 'freeform'],
|
|
99
|
+
maxParallel: 2,
|
|
100
|
+
rationale: 'Broad range including freeform; no model pin.',
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
'antigravity-cli': [
|
|
104
|
+
{
|
|
105
|
+
model: 'Gemini 3.1 Pro (High)',
|
|
106
|
+
difficulty: ['easy'],
|
|
107
|
+
maxParallel: 2,
|
|
108
|
+
rationale: 'Cheap capacity for easy tasks.',
|
|
109
|
+
},
|
|
110
|
+
],
|
|
111
|
+
'cursor-cli': [
|
|
112
|
+
{
|
|
113
|
+
model: 'auto',
|
|
114
|
+
difficulty: ['easy'],
|
|
115
|
+
maxParallel: 1,
|
|
116
|
+
rationale: 'Easy tasks only; auto model selection.',
|
|
117
|
+
},
|
|
118
|
+
],
|
|
119
|
+
'hermes-cli': [
|
|
120
|
+
{
|
|
121
|
+
difficulty: ['medium'],
|
|
122
|
+
maxParallel: 2,
|
|
123
|
+
provisional: true,
|
|
124
|
+
// NOTE: ESTIMATE, NOT OBSERVED. hermes-cli is absent from the live
|
|
125
|
+
// slot configuration this table was seeded from, so `medium` is a
|
|
126
|
+
// conservative placement rather than a transcription. Revisit once
|
|
127
|
+
// it has real usage data.
|
|
128
|
+
rationale: 'ESTIMATE — no live slot to transcribe; conservative mid placement. Adjust after real use.',
|
|
129
|
+
},
|
|
130
|
+
],
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Fallback for a detected CLI provider absent from {@link CLI_SLOT_RECIPES}.
|
|
135
|
+
* Deliberately timid: one general-purpose slot at the lowest parallelism, so an
|
|
136
|
+
* unrecognized provider can be used but never silently soaks up the queue.
|
|
137
|
+
*/
|
|
138
|
+
export const UNKNOWN_CLI_SLOT_RECIPE: Readonly<CliSlotRecipe> = Object.freeze<CliSlotRecipe>({
|
|
139
|
+
difficulty: ['medium'],
|
|
140
|
+
maxParallel: 1,
|
|
141
|
+
provisional: true,
|
|
142
|
+
rationale: 'Unrecognized provider — conservative default (medium, maxParallel 1). Review before relying on it.',
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
/** A detected, installed CLI provider on one node — the generator's input. */
|
|
146
|
+
export interface DetectedCliProvider {
|
|
147
|
+
/** Provider type id, e.g. 'claude-cli'. */
|
|
148
|
+
type: string
|
|
149
|
+
/** Human-readable name, for display in the proposal. */
|
|
150
|
+
displayName?: string
|
|
151
|
+
/** Detected version, when known. Display only — never affects the mapping. */
|
|
152
|
+
version?: string
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** One proposed slot plus why it was proposed. */
|
|
156
|
+
export interface ProposedSlotEntry {
|
|
157
|
+
slot: NodeCapabilitySlot
|
|
158
|
+
/** True when the provider had no table entry and took the conservative fallback. */
|
|
159
|
+
unknownProvider: boolean
|
|
160
|
+
/** True when the recipe behind this slot is flagged as an unvalidated estimate. */
|
|
161
|
+
provisional: boolean
|
|
162
|
+
rationale?: string
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** The full slot proposal for one node, including what a write would DESTROY. */
|
|
166
|
+
export interface SlotProposal {
|
|
167
|
+
/** The draft slot list — a WHOLESALE replacement for the node's policy.slots. */
|
|
168
|
+
proposedSlots: NodeCapabilitySlot[]
|
|
169
|
+
/** Per-slot provenance, index-aligned with `proposedSlots`. */
|
|
170
|
+
entries: ProposedSlotEntry[]
|
|
171
|
+
/** Provider types detected but not present in the mapping table. */
|
|
172
|
+
unknownProviders: string[]
|
|
173
|
+
/** Provider types whose proposal rests on an unvalidated estimate. */
|
|
174
|
+
provisionalProviders: string[]
|
|
175
|
+
/**
|
|
176
|
+
* Slots currently configured on the node that the proposal does NOT
|
|
177
|
+
* reproduce — i.e. what applying this proposal would DELETE. Slot writes are
|
|
178
|
+
* wholesale replacements, so an operator-hand-tuned slot absent from the
|
|
179
|
+
* detection-derived draft is silently destroyed unless it is named here.
|
|
180
|
+
*/
|
|
181
|
+
droppedSlots: NodeCapabilitySlot[]
|
|
182
|
+
/** Provider types that appear in `droppedSlots` but in no proposed slot at all. */
|
|
183
|
+
droppedProviders: string[]
|
|
184
|
+
/** True when applying the proposal would remove at least one existing slot. */
|
|
185
|
+
destructive: boolean
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Stable key identifying a slot's identity for current-vs-proposed diffing. */
|
|
189
|
+
function slotKey(slot: NodeCapabilitySlot): string {
|
|
190
|
+
return [
|
|
191
|
+
slot.provider,
|
|
192
|
+
slot.model ?? '',
|
|
193
|
+
slot.thinkingLevel ?? '',
|
|
194
|
+
[...(slot.difficulty ?? [])].sort().join('|'),
|
|
195
|
+
[...(slot.capability ?? [])].sort().join('|'),
|
|
196
|
+
slot.maxParallel ?? '',
|
|
197
|
+
].join('')
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Dedupe detected providers by type, preserving first-seen order. */
|
|
201
|
+
function dedupeDetected(detected: readonly DetectedCliProvider[]): DetectedCliProvider[] {
|
|
202
|
+
const seen = new Set<string>()
|
|
203
|
+
const out: DetectedCliProvider[] = []
|
|
204
|
+
for (const d of detected) {
|
|
205
|
+
const type = typeof d?.type === 'string' ? d.type.trim() : ''
|
|
206
|
+
if (!type || seen.has(type)) continue
|
|
207
|
+
seen.add(type)
|
|
208
|
+
out.push({ ...d, type })
|
|
209
|
+
}
|
|
210
|
+
return out
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Build a capability-slot proposal from a node's detected CLI providers.
|
|
215
|
+
*
|
|
216
|
+
* Pure and total: zero detections yields an empty proposal (never throws), which
|
|
217
|
+
* the caller should treat as "nothing to propose" rather than "replace the
|
|
218
|
+
* node's slots with nothing".
|
|
219
|
+
*
|
|
220
|
+
* `currentSlots` is optional but strongly recommended — it is the only way the
|
|
221
|
+
* returned proposal can report what a wholesale write would destroy.
|
|
222
|
+
*/
|
|
223
|
+
export function buildSlotProposal(
|
|
224
|
+
detected: readonly DetectedCliProvider[],
|
|
225
|
+
currentSlots: readonly NodeCapabilitySlot[] = [],
|
|
226
|
+
): SlotProposal {
|
|
227
|
+
const providers = dedupeDetected(detected ?? [])
|
|
228
|
+
const proposedSlots: NodeCapabilitySlot[] = []
|
|
229
|
+
const entries: ProposedSlotEntry[] = []
|
|
230
|
+
const unknownProviders: string[] = []
|
|
231
|
+
const provisionalProviders: string[] = []
|
|
232
|
+
|
|
233
|
+
for (const provider of providers) {
|
|
234
|
+
const known = CLI_SLOT_RECIPES[provider.type]
|
|
235
|
+
const recipes: readonly CliSlotRecipe[] = known ?? [UNKNOWN_CLI_SLOT_RECIPE]
|
|
236
|
+
const isUnknown = !known
|
|
237
|
+
if (isUnknown) unknownProviders.push(provider.type)
|
|
238
|
+
|
|
239
|
+
let providerProvisional = false
|
|
240
|
+
for (const recipe of recipes) {
|
|
241
|
+
// Normalize through the SAME normalizer the daemon applies on write, so
|
|
242
|
+
// a proposal can never preview a shape the write would reshape.
|
|
243
|
+
const slot = normalizeNodeCapabilitySlot({
|
|
244
|
+
provider: provider.type,
|
|
245
|
+
model: recipe.model,
|
|
246
|
+
thinkingLevel: recipe.thinkingLevel,
|
|
247
|
+
difficulty: recipe.difficulty,
|
|
248
|
+
maxParallel: recipe.maxParallel,
|
|
249
|
+
})
|
|
250
|
+
if (!slot) continue
|
|
251
|
+
const provisional = recipe.provisional === true
|
|
252
|
+
if (provisional) providerProvisional = true
|
|
253
|
+
proposedSlots.push(slot)
|
|
254
|
+
entries.push({
|
|
255
|
+
slot,
|
|
256
|
+
unknownProvider: isUnknown,
|
|
257
|
+
provisional,
|
|
258
|
+
...(recipe.rationale ? { rationale: recipe.rationale } : {}),
|
|
259
|
+
})
|
|
260
|
+
}
|
|
261
|
+
if (providerProvisional) provisionalProviders.push(provider.type)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// What a wholesale write would destroy: every currently-configured slot with
|
|
265
|
+
// no exact counterpart in the draft.
|
|
266
|
+
const proposedKeys = new Set(proposedSlots.map(slotKey))
|
|
267
|
+
const droppedSlots = currentSlots.filter(slot => !proposedKeys.has(slotKey(slot)))
|
|
268
|
+
const proposedProviders = new Set(proposedSlots.map(s => s.provider))
|
|
269
|
+
const droppedProviders = [...new Set(
|
|
270
|
+
droppedSlots.map(s => s.provider).filter(p => !proposedProviders.has(p)),
|
|
271
|
+
)]
|
|
272
|
+
|
|
273
|
+
return {
|
|
274
|
+
proposedSlots,
|
|
275
|
+
entries,
|
|
276
|
+
unknownProviders,
|
|
277
|
+
provisionalProviders,
|
|
278
|
+
droppedSlots,
|
|
279
|
+
droppedProviders,
|
|
280
|
+
destructive: droppedSlots.length > 0,
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Build a MAGI panel proposal from the same detections.
|
|
286
|
+
*
|
|
287
|
+
* ─── Deliberately narrow ──────────────────────────────────────────────────────
|
|
288
|
+
*
|
|
289
|
+
* MAGI's value is provider INDEPENDENCE: replicas from different providers
|
|
290
|
+
* (ideally different machines) answering the same question, so agreement means
|
|
291
|
+
* something. Detection tells us which providers exist — that is exactly enough
|
|
292
|
+
* to propose one panel of distinct providers, and no more.
|
|
293
|
+
*
|
|
294
|
+
* What detection does NOT tell us is which provider suits which review KIND
|
|
295
|
+
* (rca vs design vs claim_audit). Nothing in any manifest grades a provider for
|
|
296
|
+
* root-cause analysis over design review, and inventing a per-kind assignment
|
|
297
|
+
* would fabricate a rationale that does not exist. So this proposes ONE panel of
|
|
298
|
+
* the detected providers and leaves the kind binding to the operator; the caller
|
|
299
|
+
* decides which `task_kind` to bind it to via the existing dry-run tool.
|
|
300
|
+
*
|
|
301
|
+
* Ordering follows {@link CLI_SLOT_RECIPES} insertion order (recipe-known
|
|
302
|
+
* providers first, in table order), so the panel leads with the providers whose
|
|
303
|
+
* suitability is actually attested.
|
|
304
|
+
*/
|
|
305
|
+
export function buildMagiPanelProposal(
|
|
306
|
+
detected: readonly DetectedCliProvider[],
|
|
307
|
+
opts: { nodeId?: string; maxSlots?: number } = {},
|
|
308
|
+
): MagiSlot[] {
|
|
309
|
+
const providers = dedupeDetected(detected ?? [])
|
|
310
|
+
const tableOrder = Object.keys(CLI_SLOT_RECIPES)
|
|
311
|
+
const rank = (type: string): number => {
|
|
312
|
+
const i = tableOrder.indexOf(type)
|
|
313
|
+
return i === -1 ? Number.MAX_SAFE_INTEGER : i
|
|
314
|
+
}
|
|
315
|
+
const ordered = [...providers].sort((a, b) => rank(a.type) - rank(b.type))
|
|
316
|
+
const limit = Number.isFinite(opts.maxSlots) && (opts.maxSlots as number) > 0
|
|
317
|
+
? Math.floor(opts.maxSlots as number)
|
|
318
|
+
: ordered.length
|
|
319
|
+
|
|
320
|
+
return ordered.slice(0, limit).map((p): MagiSlot => ({
|
|
321
|
+
...(opts.nodeId ? { nodeId: opts.nodeId } : {}),
|
|
322
|
+
provider: p.type,
|
|
323
|
+
// A model is intentionally NOT pinned: the panel's job is cross-provider
|
|
324
|
+
// independence, and pinning models here would silently couple the panel
|
|
325
|
+
// to this table's cost assumptions rather than to review quality.
|
|
326
|
+
}))
|
|
327
|
+
}
|