@adhdev/daemon-core 0.9.82-rc.506 → 0.9.82-rc.508
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 +87 -10
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +87 -10
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/coordinator-prompt.d.ts +21 -0
- package/dist/mesh/model-provider-compat.d.ts +44 -0
- package/package.json +3 -3
- package/src/commands/high-family/mesh-coordinator-launch.ts +15 -2
- package/src/mesh/coordinator-prompt.ts +73 -2
- package/src/mesh/mesh-queue-assignment.ts +20 -1
- package/src/mesh/model-provider-compat.ts +73 -0
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
* a static prompt, which is also fine.
|
|
22
22
|
*/
|
|
23
23
|
import type { LocalMeshEntry, RepoMeshStatus } from '../repo-mesh-types.js';
|
|
24
|
+
import type { MagiKindPanelMap } from '@adhdev/mesh-shared';
|
|
24
25
|
/**
|
|
25
26
|
* Cheap, locally-derived "what just happened" snapshot for the coordinator
|
|
26
27
|
* prompt. Built at launch from the local ledger + work-queue stats — no remote
|
|
@@ -91,5 +92,25 @@ export interface CoordinatorPromptContext {
|
|
|
91
92
|
* section.
|
|
92
93
|
*/
|
|
93
94
|
operatingNotes?: CoordinatorOperatingNote[];
|
|
95
|
+
/**
|
|
96
|
+
* Machine-local MAGI kind-panel bindings (`~/.adhdev/meshes.json`
|
|
97
|
+
* `magiKindPanels`), read live at launch. Omitted / empty / all-empty →
|
|
98
|
+
* no "## Configured MAGI panels" section, so a mesh with no MAGI configured
|
|
99
|
+
* renders identically to before. Threaded in the same systematic way as the
|
|
100
|
+
* brain presets: read machine-local config at launch, render a pure section.
|
|
101
|
+
*/
|
|
102
|
+
magiKindPanels?: MagiKindPanelMap;
|
|
94
103
|
}
|
|
95
104
|
export declare function buildCoordinatorSystemPrompt(ctx: CoordinatorPromptContext): string;
|
|
105
|
+
/**
|
|
106
|
+
* Render the machine-local MAGI kind-panel bindings so the coordinator KNOWS
|
|
107
|
+
* which cross-verification panels (rca / design / claim_audit / freeform) are
|
|
108
|
+
* actually configured on this machine. Without this the coordinator only sees
|
|
109
|
+
* the `mesh_magi_*` tools in the static table and has no idea MAGI is set up.
|
|
110
|
+
*
|
|
111
|
+
* Pure — takes the panels map (read live at launch, mirroring how brain presets
|
|
112
|
+
* read getDifficultyBrains). Returns null (section OMITTED) when nothing usable
|
|
113
|
+
* is configured: undefined/null map, or every kind maps to an empty slot list.
|
|
114
|
+
* That keeps a MAGI-less mesh's prompt byte-identical to before.
|
|
115
|
+
*/
|
|
116
|
+
export declare function buildMagiKindPanelsSection(panels: MagiKindPanelMap | undefined | null): string | null;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider ↔ model compatibility guard for brain/slot-derived launch models.
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS EXISTS: the difficulty→brain presets (and MAGI slots) carry
|
|
5
|
+
* provider-agnostic model strings like `opus`/`sonnet`/`haiku`, which are
|
|
6
|
+
* Anthropic (Claude) model names. When a task lands on a node whose provider is
|
|
7
|
+
* NOT Anthropic-backed — `codex-cli` (ChatGPT), `antigravity-cli`, `hermes-cli`
|
|
8
|
+
* — forwarding a `claude-*` model as the launch arg makes that provider convert
|
|
9
|
+
* it to e.g. `-c model='claude-...'`, and a ChatGPT-account codex then rejects
|
|
10
|
+
* the launch with a 400. The model string was never meant to be forced onto a
|
|
11
|
+
* provider that can't honor it (presets are "best-effort at launch").
|
|
12
|
+
*
|
|
13
|
+
* The invariant this enforces: **an Anthropic (`claude-*`) model is never passed
|
|
14
|
+
* as a launch argument to a non-Anthropic provider.** When a preset/slot model is
|
|
15
|
+
* incompatible with the resolved provider, drop the model (the provider then uses
|
|
16
|
+
* its own default) and keep only the provider-neutral axis (thinkingLevel).
|
|
17
|
+
*
|
|
18
|
+
* This is deliberately conservative: it only strips a model that is *known* to be
|
|
19
|
+
* Anthropic when the provider is *known* to be non-Anthropic. Unknown models and
|
|
20
|
+
* unknown providers pass through unchanged so a legitimately provider-specific
|
|
21
|
+
* model (e.g. an operator who configured `gpt-5-codex` on a codex slot) is not
|
|
22
|
+
* clobbered.
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* True when `providerType` is an Anthropic-backed provider (Claude). Matching is
|
|
26
|
+
* case-insensitive and tolerant of surrounding whitespace. An empty/undefined
|
|
27
|
+
* provider is treated as non-Anthropic (unknown → don't assume Claude).
|
|
28
|
+
*/
|
|
29
|
+
export declare function isAnthropicProvider(providerType: string | undefined | null): boolean;
|
|
30
|
+
/**
|
|
31
|
+
* True when `model` names an Anthropic (Claude) model — the provider-agnostic
|
|
32
|
+
* brain-preset aliases (`opus`/`sonnet`/`haiku`) or any explicit `claude-*` id.
|
|
33
|
+
* Case-insensitive. Non-Anthropic and unknown models return false so they pass
|
|
34
|
+
* the compatibility check unchanged.
|
|
35
|
+
*/
|
|
36
|
+
export declare function isAnthropicModel(model: string | undefined | null): boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Compatibility check for a brain/slot-derived launch model against the provider
|
|
39
|
+
* it would launch on. Returns false ONLY for the concrete failure mode we guard:
|
|
40
|
+
* an Anthropic model routed to a non-Anthropic provider. Everything else
|
|
41
|
+
* (no model, non-Anthropic model, Anthropic provider, unknown provider) is
|
|
42
|
+
* compatible so nothing legitimate is stripped.
|
|
43
|
+
*/
|
|
44
|
+
export declare function isModelCompatibleWithProvider(model: string | undefined | null, providerType: string | undefined | null): boolean;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adhdev/daemon-core",
|
|
3
|
-
"version": "0.9.82-rc.
|
|
3
|
+
"version": "0.9.82-rc.508",
|
|
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.
|
|
51
|
-
"@adhdev/session-host-core": "0.9.82-rc.
|
|
50
|
+
"@adhdev/mesh-shared": "0.9.82-rc.508",
|
|
51
|
+
"@adhdev/session-host-core": "0.9.82-rc.508",
|
|
52
52
|
"@agentclientprotocol/sdk": "^0.16.1",
|
|
53
53
|
"ajv": "^8.20.0",
|
|
54
54
|
"ajv-formats": "^3.0.1",
|
|
@@ -138,6 +138,19 @@ export const meshCoordinatorLaunchHandlers: Record<string, HighFamilyHandler> =
|
|
|
138
138
|
} catch { return undefined; }
|
|
139
139
|
};
|
|
140
140
|
|
|
141
|
+
// MAGI panels: load the machine-local kind-panel bindings so the
|
|
142
|
+
// coordinator prompt auto-lists which cross-verification panels
|
|
143
|
+
// (rca / design / claim_audit / freeform) are configured. Same
|
|
144
|
+
// systematic pattern as the brain presets — read machine-local
|
|
145
|
+
// config at launch. Best-effort: a read failure or empty map just
|
|
146
|
+
// omits the "## Configured MAGI panels" section.
|
|
147
|
+
const loadMagiKindPanelsBestEffort = async () => {
|
|
148
|
+
try {
|
|
149
|
+
const { listMagiKindPanels } = await import('../../config/mesh-config.js');
|
|
150
|
+
return listMagiKindPanels();
|
|
151
|
+
} catch { return undefined; }
|
|
152
|
+
};
|
|
153
|
+
|
|
141
154
|
// Support inline mesh data from cloud (bypasses local meshes.json lookup)
|
|
142
155
|
let mesh: any;
|
|
143
156
|
if (args?.inlineMesh && typeof args.inlineMesh === 'object') {
|
|
@@ -264,7 +277,7 @@ export const meshCoordinatorLaunchHandlers: Record<string, HighFamilyHandler> =
|
|
|
264
277
|
// Build coordinator prompt first — fail closed on errors.
|
|
265
278
|
let cliCmdSystemPrompt = '';
|
|
266
279
|
try {
|
|
267
|
-
cliCmdSystemPrompt = buildCoordinatorSystemPrompt({ mesh: effectiveMesh, coordinatorCliType: cliType, userInstruction: extraSystemPrompt || undefined, missionSection: buildMissionSectionBestEffort(mesh.id), recentActivity: await buildRecentActivityBestEffort(mesh.id), operatingNotes: await buildEffectiveOperatingNotes(mesh.id) });
|
|
280
|
+
cliCmdSystemPrompt = buildCoordinatorSystemPrompt({ mesh: effectiveMesh, coordinatorCliType: cliType, userInstruction: extraSystemPrompt || undefined, missionSection: buildMissionSectionBestEffort(mesh.id), recentActivity: await buildRecentActivityBestEffort(mesh.id), operatingNotes: await buildEffectiveOperatingNotes(mesh.id), magiKindPanels: await loadMagiKindPanelsBestEffort() });
|
|
268
281
|
} catch (error: any) {
|
|
269
282
|
const message = error?.message || String(error);
|
|
270
283
|
LOG.error('MeshCoordinator', `Failed to build coordinator prompt: ${message}`);
|
|
@@ -484,7 +497,7 @@ export const meshCoordinatorLaunchHandlers: Record<string, HighFamilyHandler> =
|
|
|
484
497
|
// broken mesh state is visible instead of silently launching with weaker rules.
|
|
485
498
|
let systemPrompt = '';
|
|
486
499
|
try {
|
|
487
|
-
systemPrompt = buildCoordinatorSystemPrompt({ mesh: effectiveMesh, coordinatorCliType: cliType, userInstruction: extraSystemPrompt || undefined, missionSection: buildMissionSectionBestEffort(mesh.id), recentActivity: await buildRecentActivityBestEffort(mesh.id), operatingNotes: await buildEffectiveOperatingNotes(mesh.id) });
|
|
500
|
+
systemPrompt = buildCoordinatorSystemPrompt({ mesh: effectiveMesh, coordinatorCliType: cliType, userInstruction: extraSystemPrompt || undefined, missionSection: buildMissionSectionBestEffort(mesh.id), recentActivity: await buildRecentActivityBestEffort(mesh.id), operatingNotes: await buildEffectiveOperatingNotes(mesh.id), magiKindPanels: await loadMagiKindPanelsBestEffort() });
|
|
488
501
|
} catch (error: any) {
|
|
489
502
|
const message = error?.message || String(error);
|
|
490
503
|
LOG.error('MeshCoordinator', `Failed to build coordinator prompt: ${message}`);
|
|
@@ -34,6 +34,7 @@ import type {
|
|
|
34
34
|
import { mergeAndNormalizePolicy } from '../repo-mesh-types.js';
|
|
35
35
|
import { getDifficultyBrains } from '../config/mesh-config.js';
|
|
36
36
|
import { MESH_TASK_DIFFICULTIES } from '@adhdev/mesh-shared';
|
|
37
|
+
import type { MagiKindPanelMap, MagiSlot, MagiTaskKind } from '@adhdev/mesh-shared';
|
|
37
38
|
|
|
38
39
|
/**
|
|
39
40
|
* Cheap, locally-derived "what just happened" snapshot for the coordinator
|
|
@@ -109,6 +110,14 @@ export interface CoordinatorPromptContext {
|
|
|
109
110
|
* section.
|
|
110
111
|
*/
|
|
111
112
|
operatingNotes?: CoordinatorOperatingNote[];
|
|
113
|
+
/**
|
|
114
|
+
* Machine-local MAGI kind-panel bindings (`~/.adhdev/meshes.json`
|
|
115
|
+
* `magiKindPanels`), read live at launch. Omitted / empty / all-empty →
|
|
116
|
+
* no "## Configured MAGI panels" section, so a mesh with no MAGI configured
|
|
117
|
+
* renders identically to before. Threaded in the same systematic way as the
|
|
118
|
+
* brain presets: read machine-local config at launch, render a pure section.
|
|
119
|
+
*/
|
|
120
|
+
magiKindPanels?: MagiKindPanelMap;
|
|
112
121
|
}
|
|
113
122
|
|
|
114
123
|
/**
|
|
@@ -284,6 +293,11 @@ Repository: \`${mesh.repoIdentity}\`${mesh.defaultBranch ? `\nDefault branch: \`
|
|
|
284
293
|
// ── Brain presets (difficulty → model/thinking) ──
|
|
285
294
|
sections.push(buildBrainPresetsSection());
|
|
286
295
|
|
|
296
|
+
// ── Configured MAGI panels (machine-local magiKindPanels) — only present
|
|
297
|
+
// when at least one task_kind has a non-empty slot list. ──
|
|
298
|
+
const magiSection = buildMagiKindPanelsSection(ctx.magiKindPanels);
|
|
299
|
+
if (magiSection) sections.push(magiSection);
|
|
300
|
+
|
|
287
301
|
// ── Tools ──
|
|
288
302
|
sections.push(TOOLS_SECTION);
|
|
289
303
|
|
|
@@ -634,6 +648,58 @@ function buildBrainPresetsSection(): string {
|
|
|
634
648
|
return lines.join('\n');
|
|
635
649
|
}
|
|
636
650
|
|
|
651
|
+
/**
|
|
652
|
+
* Render the machine-local MAGI kind-panel bindings so the coordinator KNOWS
|
|
653
|
+
* which cross-verification panels (rca / design / claim_audit / freeform) are
|
|
654
|
+
* actually configured on this machine. Without this the coordinator only sees
|
|
655
|
+
* the `mesh_magi_*` tools in the static table and has no idea MAGI is set up.
|
|
656
|
+
*
|
|
657
|
+
* Pure — takes the panels map (read live at launch, mirroring how brain presets
|
|
658
|
+
* read getDifficultyBrains). Returns null (section OMITTED) when nothing usable
|
|
659
|
+
* is configured: undefined/null map, or every kind maps to an empty slot list.
|
|
660
|
+
* That keeps a MAGI-less mesh's prompt byte-identical to before.
|
|
661
|
+
*/
|
|
662
|
+
export function buildMagiKindPanelsSection(panels: MagiKindPanelMap | undefined | null): string | null {
|
|
663
|
+
if (!panels) return null;
|
|
664
|
+
// Keep only kinds with a non-empty slot list; drop empty/undefined bindings.
|
|
665
|
+
const configured = (Object.entries(panels) as Array<[MagiTaskKind, MagiSlot[] | undefined]>)
|
|
666
|
+
.filter(([, slots]) => Array.isArray(slots) && slots.length > 0) as Array<[MagiTaskKind, MagiSlot[]]>;
|
|
667
|
+
if (configured.length === 0) return null;
|
|
668
|
+
|
|
669
|
+
const lines = [
|
|
670
|
+
'## Configured MAGI panels',
|
|
671
|
+
'',
|
|
672
|
+
'These machine-local MAGI kind-panels are configured on this mesh — read-only cross-verification quorums:',
|
|
673
|
+
'',
|
|
674
|
+
];
|
|
675
|
+
|
|
676
|
+
for (const [kind, slots] of configured) {
|
|
677
|
+
const replicaCount = slots.reduce((sum, s) => sum + (s.n && s.n > 0 ? s.n : 1), 0);
|
|
678
|
+
const label = replicaCount === slots.length
|
|
679
|
+
? `${slots.length} ${slots.length === 1 ? 'slot' : 'slots'}`
|
|
680
|
+
: `${replicaCount} replicas`;
|
|
681
|
+
const rendered = slots.map(renderMagiSlot).join(', ');
|
|
682
|
+
lines.push(`- **${kind}** (${label}): ${rendered}`);
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
lines.push('');
|
|
686
|
+
lines.push('Use these via `mesh_magi_review` (the `task_kind` is REQUIRED — it selects BOTH the output schema and the panel). The live authoritative slot list is `mesh_magi_kind_panel_list`. MAGI worker replicas are read-only and typically do NOT have mesh MCP tools exposed, so for live timing / tool-behavior claims you MUST gather the primary evidence yourself and use MAGI only for independent source-level corroboration.');
|
|
687
|
+
|
|
688
|
+
return lines.join('\n');
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/** Render one MAGI slot as `provider[@nodeId][ (model, tags…, xN)]`. */
|
|
692
|
+
function renderMagiSlot(slot: MagiSlot): string {
|
|
693
|
+
let s = slot.provider;
|
|
694
|
+
if (slot.nodeId) s += `@${slot.nodeId}`;
|
|
695
|
+
const extra: string[] = [];
|
|
696
|
+
if (slot.model) extra.push(`model: ${slot.model}`);
|
|
697
|
+
if (slot.capabilityTags && slot.capabilityTags.length) extra.push(`tags: ${slot.capabilityTags.join('+')}`);
|
|
698
|
+
if (slot.n && slot.n > 1) extra.push(`×${slot.n}`);
|
|
699
|
+
if (extra.length) s += ` (${extra.join(', ')})`;
|
|
700
|
+
return s;
|
|
701
|
+
}
|
|
702
|
+
|
|
637
703
|
function buildPolicySection(policy: RepoMeshPolicy): string {
|
|
638
704
|
const rules: string[] = [];
|
|
639
705
|
if (policy.requirePreTaskCheckpoint) rules.push('- Create a git checkpoint **before** starting each task');
|
|
@@ -738,7 +804,9 @@ Follow these recovery rules:
|
|
|
738
804
|
1. **If "Retry recommended"**: Check \`mesh_view_queue\` first — the daemon may have auto-requeued. If not, re-launch the session on the same node (\`mesh_launch_session\`), then resend the original task (\`mesh_send_task\`). The system message includes the original task text.
|
|
739
805
|
2. **If "Max retries exceeded"**: Do NOT retry on the same node. Either reassign the task to a different node, or inform the user that the task requires manual intervention.
|
|
740
806
|
3. **If no recovery context**: The stop may be intentional (normal completion). Use \`mesh_read_chat\` once to verify, then move on.
|
|
741
|
-
4. **Always record what happened**: After handling a failure, briefly note the outcome in your report to the user
|
|
807
|
+
4. **Always record what happened**: After handling a failure, briefly note the outcome in your report to the user.
|
|
808
|
+
5. **Stuck-but-done vs actually-stuck**: If a delegated session appears stuck but has already produced a verified final summary or diff, stop polling noisy tool/terminal transcript bubbles. Verify with \`mesh_git_status\` or a checkpoint and proceed to landing.
|
|
809
|
+
6. **Refinery falsely blocks a verified-clean branch — manual fast-forward convergence**: When \`mesh_refine_node\` falsely blocks a verified-clean branch (stale preflight, or the submodule-gitlink trivial-fast-forward misjudgment), bypass the refine tool and converge by strict fast-forward — (1) rebase the submodule commit onto the submodule \`origin/main\`, (2) push the submodule ff-only (verify \`git merge-base --is-ancestor\` first), (3) rebase the root branch and re-bump the submodule pointer so the root diff stays non-empty, (4) push the root ff-only. NEVER force-push or reset; abort and report on any non-fast-forward.`;
|
|
742
810
|
|
|
743
811
|
const ONBOARDING_SECTION = `## Onboarding / Reinit
|
|
744
812
|
|
|
@@ -771,7 +839,7 @@ function buildRulesSection(coordinatorCliType?: string): string {
|
|
|
771
839
|
|
|
772
840
|
- **Route, don't implement.** Delegate all code reading, analysis, and execution to node agents. Never read source files or run commands in the coordinator — keep context lean.
|
|
773
841
|
- **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\`.
|
|
774
|
-
- **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
|
|
842
|
+
- **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, or (d) the user explicitly asks for a different provider/session. Continuation of the same issue in an already-idle session is allowed and preferred — this rule blocks concurrent unrelated work interleaved into a live (still-generating) session, not sequential same-issue follow-ups.
|
|
775
843
|
- **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.
|
|
776
844
|
- **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.
|
|
777
845
|
- **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.
|
|
@@ -779,12 +847,15 @@ function buildRulesSection(coordinatorCliType?: string): string {
|
|
|
779
847
|
- **Verify via git, not source.** Use \`mesh_git_status\` to confirm side effects. Treat agent summaries as self-reports, not verification.
|
|
780
848
|
- **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).
|
|
781
849
|
- **Check history first.** Call \`mesh_task_history\` at session start to avoid duplicate work and inform recovery. On failure, read task history before retrying.
|
|
850
|
+
- **Don't reopen already-done work after a resume.** Before reopening a reported issue after context compaction or session resume, check current git state and recent session context. If another session has already completed the work, continue from the existing diff/commit instead of starting a duplicate investigation.
|
|
782
851
|
- **Sequence shared-base-moving merges.** Parallel dispatch is encouraged, but merging one worktree can advance another in-flight worktree's base — especially a shared submodule pointer — turning a clean fast-forward into a diverged rebase (patch-equivalence correctly blocks this). Before merging an in-flight worktree while siblings are also in flight, land in an intentional order, re-clone long-running worktrees from the advanced base, or expect to manually rebase + ff-only the laggards; merging an independent fix mid-flight can strand siblings into a rebase.
|
|
783
852
|
- **Converge branches.** After worktree tasks: refine/fast-forward, or classify as \`pushed_feature_branch_needs_merge\` / \`blocked_review\` / \`cleanup_candidate\` / \`not_mergeable\`. Clean up with \`mesh_remove_node\`.
|
|
784
853
|
- **Refinery is config-driven.** \`mesh_refine_node\` must run validation from \`.adhdev/refine.{json,yaml,yml}\` or \`repo-mesh.refine.*\`. Heuristics are scaffolding only.
|
|
785
854
|
- **Submodule reachability = publish-needed.** \`submodule_reachability_failed\` → classify as \`blocked_review\`, request user approval to push to submodule main, then rerun \`mesh_refine_node\`.
|
|
786
855
|
- **Honor per-node instructions.** When a node carries a 📌 Node instruction in the nodes section, include the relevant parts of that instruction in the task message you send to that node. Don't paraphrase the instruction into your own words — quote it verbatim so the worker agent sees exactly what the user wrote.
|
|
787
856
|
- **Mission status does not update itself.** When a mission's tasks are all done or the work is abandoned, explicitly call \`mesh_mission_upsert\` to set status \`completed\` or \`abandoned\`. Never leave a finished mission in \`active\`. All-cancelled tasks with no further work → \`abandoned\`.
|
|
857
|
+
- **Don't spawn a nested coordinator for simple inspection.** Do not spawn a nested coordinator-like agent for simple inspection tasks. If delegation is required, use explicit provider selection and a fully self-contained, bounded task instruction.
|
|
858
|
+
- **Keep internal traffic out of the transcript.** Internal tool calls, status events, control messages, and debug output must not appear as ordinary user-visible chat transcript content unless explicitly marked user-facing by the producing agent.
|
|
788
859
|
- **Never fabricate tool results.** Always call the actual tool.
|
|
789
860
|
- **Keep the user informed.** One or two sentences after each delegation round.${coordinatorNote}
|
|
790
861
|
|
|
@@ -23,6 +23,7 @@ import { queuePendingMeshCoordinatorEvent, retractPendingDispatchBlockedEvent }
|
|
|
23
23
|
import { isWorktreeBootstrapStaleRunning, shouldDeferDispatchForBootstrap } from './worktree-bootstrap-config.js';
|
|
24
24
|
import { isWithinCloneBootstrapGrace } from './mesh-clone-grace.js';
|
|
25
25
|
import { beginTaskDispatchInFlight, endTaskDispatchInFlight } from './mesh-task-inflight.js';
|
|
26
|
+
import { isModelCompatibleWithProvider } from './model-provider-compat.js';
|
|
26
27
|
|
|
27
28
|
/**
|
|
28
29
|
* CANON: the single canonical coordinator-daemon id this daemon stamps onto every
|
|
@@ -2056,9 +2057,27 @@ async function maybeAutoLaunchOneQueueSession(components: DaemonComponents, mesh
|
|
|
2056
2057
|
// Slot-derived model/thinking: an explicit task.model/thinkingLevel
|
|
2057
2058
|
// (resolved from the enqueue-time brain) still wins; the matched
|
|
2058
2059
|
// slot fills only what the task left blank (ORCHESTRATION_NODE_SLOTS.md).
|
|
2059
|
-
const
|
|
2060
|
+
const rawEffectiveModel = (typeof task.model === 'string' && task.model.trim()) ? task.model.trim() : resolved.model;
|
|
2060
2061
|
const effectiveThinkingLevel = (typeof task.thinkingLevel === 'string' && task.thinkingLevel.trim()) ? task.thinkingLevel.trim() : resolved.thinkingLevel;
|
|
2061
2062
|
|
|
2063
|
+
// CODEX-400 GUARD: the difficulty→brain presets (and MAGI slots) carry
|
|
2064
|
+
// provider-agnostic Anthropic model aliases (opus/sonnet/haiku). Now that
|
|
2065
|
+
// resolved.providerType is definitively known, drop the model if it is a
|
|
2066
|
+
// Claude model but the provider is NOT Anthropic-backed (codex-cli /
|
|
2067
|
+
// antigravity-cli / hermes-cli): forwarding `claude-*` as an initialModel
|
|
2068
|
+
// makes those providers convert it to `-c model='claude-...'`, and a
|
|
2069
|
+
// ChatGPT-account codex then rejects the launch with a 400. Stripping it
|
|
2070
|
+
// lets the provider fall back to its own default model; the provider-neutral
|
|
2071
|
+
// thinkingLevel axis is preserved. This is the single authoritative point
|
|
2072
|
+
// that enforces the invariant across every model source (preset, slot,
|
|
2073
|
+
// explicit) because both remote and local launch consume effectiveModel below.
|
|
2074
|
+
const effectiveModel = isModelCompatibleWithProvider(rawEffectiveModel, resolved.providerType)
|
|
2075
|
+
? rawEffectiveModel
|
|
2076
|
+
: undefined;
|
|
2077
|
+
if (rawEffectiveModel && effectiveModel === undefined) {
|
|
2078
|
+
LOG.info('MeshQueue', `CODEX-400 GUARD: dropped incompatible launch model '${rawEffectiveModel}' for non-Anthropic provider '${resolved.providerType}' on node ${nodeId} (task ${task.id}); provider will use its own default model`);
|
|
2079
|
+
}
|
|
2080
|
+
|
|
2062
2081
|
// Don't spawn a session for a (node, provider) already at its declared
|
|
2063
2082
|
// maxParallel cap — it would launch only to fail the claim. The claim
|
|
2064
2083
|
// transaction enforces the cap regardless; this just avoids a doomed launch.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider ↔ model compatibility guard for brain/slot-derived launch models.
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS EXISTS: the difficulty→brain presets (and MAGI slots) carry
|
|
5
|
+
* provider-agnostic model strings like `opus`/`sonnet`/`haiku`, which are
|
|
6
|
+
* Anthropic (Claude) model names. When a task lands on a node whose provider is
|
|
7
|
+
* NOT Anthropic-backed — `codex-cli` (ChatGPT), `antigravity-cli`, `hermes-cli`
|
|
8
|
+
* — forwarding a `claude-*` model as the launch arg makes that provider convert
|
|
9
|
+
* it to e.g. `-c model='claude-...'`, and a ChatGPT-account codex then rejects
|
|
10
|
+
* the launch with a 400. The model string was never meant to be forced onto a
|
|
11
|
+
* provider that can't honor it (presets are "best-effort at launch").
|
|
12
|
+
*
|
|
13
|
+
* The invariant this enforces: **an Anthropic (`claude-*`) model is never passed
|
|
14
|
+
* as a launch argument to a non-Anthropic provider.** When a preset/slot model is
|
|
15
|
+
* incompatible with the resolved provider, drop the model (the provider then uses
|
|
16
|
+
* its own default) and keep only the provider-neutral axis (thinkingLevel).
|
|
17
|
+
*
|
|
18
|
+
* This is deliberately conservative: it only strips a model that is *known* to be
|
|
19
|
+
* Anthropic when the provider is *known* to be non-Anthropic. Unknown models and
|
|
20
|
+
* unknown providers pass through unchanged so a legitimately provider-specific
|
|
21
|
+
* model (e.g. an operator who configured `gpt-5-codex` on a codex slot) is not
|
|
22
|
+
* clobbered.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Provider types that are Anthropic-backed (accept `claude-*` / opus/sonnet/haiku
|
|
27
|
+
* model names). Everything else is treated as non-Anthropic for the guard.
|
|
28
|
+
*/
|
|
29
|
+
const ANTHROPIC_PROVIDER_TYPES: ReadonlySet<string> = new Set([
|
|
30
|
+
'claude-cli',
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* True when `providerType` is an Anthropic-backed provider (Claude). Matching is
|
|
35
|
+
* case-insensitive and tolerant of surrounding whitespace. An empty/undefined
|
|
36
|
+
* provider is treated as non-Anthropic (unknown → don't assume Claude).
|
|
37
|
+
*/
|
|
38
|
+
export function isAnthropicProvider(providerType: string | undefined | null): boolean {
|
|
39
|
+
const p = typeof providerType === 'string' ? providerType.trim().toLowerCase() : '';
|
|
40
|
+
return p.length > 0 && ANTHROPIC_PROVIDER_TYPES.has(p);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* True when `model` names an Anthropic (Claude) model — the provider-agnostic
|
|
45
|
+
* brain-preset aliases (`opus`/`sonnet`/`haiku`) or any explicit `claude-*` id.
|
|
46
|
+
* Case-insensitive. Non-Anthropic and unknown models return false so they pass
|
|
47
|
+
* the compatibility check unchanged.
|
|
48
|
+
*/
|
|
49
|
+
export function isAnthropicModel(model: string | undefined | null): boolean {
|
|
50
|
+
const m = typeof model === 'string' ? model.trim().toLowerCase() : '';
|
|
51
|
+
if (!m) return false;
|
|
52
|
+
if (m.startsWith('claude') || m.startsWith('anthropic')) return true;
|
|
53
|
+
// Provider-agnostic brain-preset aliases (DEFAULT_DIFFICULTY_BRAINS) are Claude
|
|
54
|
+
// model families; a bare `opus`/`sonnet`/`haiku` (optionally with a suffix like
|
|
55
|
+
// `opus-4` or `sonnet-4-5`) means an Anthropic model.
|
|
56
|
+
return /^(opus|sonnet|haiku)(\b|[-_])/.test(m);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Compatibility check for a brain/slot-derived launch model against the provider
|
|
61
|
+
* it would launch on. Returns false ONLY for the concrete failure mode we guard:
|
|
62
|
+
* an Anthropic model routed to a non-Anthropic provider. Everything else
|
|
63
|
+
* (no model, non-Anthropic model, Anthropic provider, unknown provider) is
|
|
64
|
+
* compatible so nothing legitimate is stripped.
|
|
65
|
+
*/
|
|
66
|
+
export function isModelCompatibleWithProvider(
|
|
67
|
+
model: string | undefined | null,
|
|
68
|
+
providerType: string | undefined | null,
|
|
69
|
+
): boolean {
|
|
70
|
+
if (!isAnthropicModel(model)) return true; // non-Claude model → no constraint
|
|
71
|
+
if (isAnthropicProvider(providerType)) return true; // Claude model on Claude provider → fine
|
|
72
|
+
return false; // Claude model on non-Claude provider → block
|
|
73
|
+
}
|