@adhdev/daemon-core 0.9.82-rc.485 → 0.9.82-rc.487
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/git/git-status.d.ts +23 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +343 -55
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +342 -55
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/mesh-queue-assignment.d.ts +4 -0
- package/dist/mesh/mesh-refine-gates.d.ts +29 -0
- package/dist/mesh/mesh-work-queue.d.ts +9 -0
- package/dist/repo-mesh-types.d.ts +18 -2
- package/dist/shared-types.d.ts +8 -0
- package/dist/status/snapshot.d.ts +1 -0
- package/package.json +3 -3
- package/src/commands/handler.ts +21 -1
- package/src/commands/high-family/mesh-coordinator-launch.ts +17 -1
- package/src/commands/high-family/mesh-status.ts +8 -0
- package/src/commands/med-family/cli-agent.ts +29 -0
- package/src/commands/router-refine.ts +27 -2
- package/src/git/git-status.ts +84 -29
- package/src/index.ts +1 -1
- package/src/mesh/coordinator-prompt.ts +4 -1
- package/src/mesh/mesh-queue-assignment.ts +164 -27
- package/src/mesh/mesh-refine-gates.ts +113 -7
- package/src/mesh/mesh-work-queue.ts +14 -0
- package/src/repo-mesh-types.ts +28 -3
- package/src/shared-types.ts +8 -0
- package/src/status/builders.ts +45 -2
- package/src/status/snapshot.ts +1 -1
|
@@ -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,
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* keep working. `CommandRouterResult` is imported type-only from router.ts
|
|
12
12
|
* (erased at compile time — no runtime import cycle).
|
|
13
13
|
*/
|
|
14
|
+
import type { ChangedPackageClassification } from '../git/git-status.js';
|
|
14
15
|
import type { WorktreeBootstrapState } from '../mesh/worktree-bootstrap-config.js';
|
|
15
16
|
import type { CommandRouterResult } from '../commands/router.js';
|
|
16
17
|
export type MeshCoordinatorConfigFormat = 'claude_mcp_json' | 'hermes_config_yaml';
|
|
@@ -53,6 +54,19 @@ type MeshRefineValidationSummary = {
|
|
|
53
54
|
};
|
|
54
55
|
/** M2-2: deprecation notices from the refine config (e.g. bootstrapCommands). */
|
|
55
56
|
deprecationWarnings?: string[];
|
|
57
|
+
/**
|
|
58
|
+
* Coarse daemon-vs-web change-impact used to scope the validation command set.
|
|
59
|
+
* When `isDaemonAffecting === false`, daemon-scoped commands are recorded in
|
|
60
|
+
* `commandsRun` with `skipped: true, skipReason: 'unaffected_daemon_scope'`
|
|
61
|
+
* rather than executed; web + typecheck commands always run. Absent when no
|
|
62
|
+
* change-impact was threaded in (legacy: full command set runs).
|
|
63
|
+
*/
|
|
64
|
+
changeImpact?: {
|
|
65
|
+
isDaemonAffecting: boolean;
|
|
66
|
+
affectedPackages: string[];
|
|
67
|
+
/** displayCommands skipped because the daemon scope is unaffected. */
|
|
68
|
+
skippedDaemonCommands?: string[];
|
|
69
|
+
};
|
|
56
70
|
};
|
|
57
71
|
type MeshRefineStageStatus = 'passed' | 'failed' | 'skipped';
|
|
58
72
|
type MeshRefinePatchEquivalenceSummary = {
|
|
@@ -277,6 +291,13 @@ export interface RefineContext {
|
|
|
277
291
|
baseBranch: string;
|
|
278
292
|
baseHead: string;
|
|
279
293
|
branchHead: string;
|
|
294
|
+
/**
|
|
295
|
+
* Coarse daemon-vs-web change-impact for baseHead..branchHead, resolved in the
|
|
296
|
+
* resolve_refs stage and threaded into the validation gate to scope its command
|
|
297
|
+
* set. `undefined` means "could not classify" → the gate fails open and runs ALL
|
|
298
|
+
* commands (never skip on uncertainty).
|
|
299
|
+
*/
|
|
300
|
+
changeImpact?: ChangedPackageClassification;
|
|
280
301
|
validationSummary: Awaited<ReturnType<typeof runMeshRefineValidationGate>>;
|
|
281
302
|
patchEquivalence: Awaited<ReturnType<typeof runMeshRefinePatchEquivalenceGate>>;
|
|
282
303
|
submoduleReachability: Awaited<ReturnType<typeof runMeshRefineSubmoduleReachabilityGate>>;
|
|
@@ -424,5 +445,13 @@ export declare function runMeshRefineValidationGate(mesh: any, workspace: string
|
|
|
424
445
|
persistedBootstrapState?: WorktreeBootstrapState | null;
|
|
425
446
|
/** M2-2: called after an inherit-mode bootstrap run so the caller can persist the new state. */
|
|
426
447
|
onBootstrapStateChange?: (state: WorktreeBootstrapState) => void;
|
|
448
|
+
/**
|
|
449
|
+
* Coarse daemon-vs-web change-impact for the branch (resolve_refs computes it
|
|
450
|
+
* over baseHead..branchHead). When provided and `isDaemonAffecting === false`,
|
|
451
|
+
* daemon-scoped validation commands are skipped (web + typecheck still run).
|
|
452
|
+
* When omitted or `isDaemonAffecting === true`, the full command set runs —
|
|
453
|
+
* fail-open to full validation on any uncertainty.
|
|
454
|
+
*/
|
|
455
|
+
changeImpact?: ChangedPackageClassification;
|
|
427
456
|
}): Promise<MeshRefineValidationSummary>;
|
|
428
457
|
export {};
|
|
@@ -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;
|
package/dist/shared-types.d.ts
CHANGED
|
@@ -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.
|
|
3
|
+
"version": "0.9.82-rc.487",
|
|
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.487",
|
|
51
|
+
"@adhdev/session-host-core": "0.9.82-rc.487",
|
|
52
52
|
"@agentclientprotocol/sdk": "^0.16.1",
|
|
53
53
|
"ajv": "^8.20.0",
|
|
54
54
|
"ajv-formats": "^3.0.1",
|
package/src/commands/handler.ts
CHANGED
|
@@ -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<{
|
|
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
|
|
@@ -17,6 +17,8 @@ import { handleMeshForwardEvent, queuePendingMeshCoordinatorEvent } from '../mes
|
|
|
17
17
|
import { analyzeMeshRefineNodeChangeArea, orderMeshRefineBatchNodes } from '../mesh/mesh-refine-batch.js';
|
|
18
18
|
import type { WorktreeBootstrapState } from '../mesh/worktree-bootstrap-config.js';
|
|
19
19
|
import { DEFAULT_MESH_POLICY } from '../repo-mesh-types.js';
|
|
20
|
+
import { classifyChangedPackages } from '../git/git-status.js';
|
|
21
|
+
import type { ChangedPackageClassification } from '../git/git-status.js';
|
|
20
22
|
import { readStringValue } from '../mesh/mesh-node-identity.js';
|
|
21
23
|
import {
|
|
22
24
|
alignRefinerySubmodulesAfterMerge,
|
|
@@ -312,7 +314,24 @@ export async function refineResolveRefsStage(self: DaemonCommandRouter,
|
|
|
312
314
|
const { stdout: branchHeadStdout } = await execFileAsync('git', ['rev-parse', branch], { cwd: node.workspace, encoding: 'utf8' });
|
|
313
315
|
const baseHead = baseHeadRaw;
|
|
314
316
|
const branchHead = branchHeadStdout.trim();
|
|
315
|
-
|
|
317
|
+
|
|
318
|
+
// Coarse daemon-vs-web change-impact for baseHead..branchHead, computed
|
|
319
|
+
// against the worktree so the same policy (.adhdev/change-impact.*) as the
|
|
320
|
+
// stale-build detector applies. Threaded onto ctx so the validation gate
|
|
321
|
+
// can scope its command set: a web-only branch skips daemon-scoped commands.
|
|
322
|
+
// FAIL-OPEN: any classification error leaves changeImpact undefined → the
|
|
323
|
+
// gate runs the full command set (never skip on uncertainty).
|
|
324
|
+
let changeImpact: ChangedPackageClassification | undefined;
|
|
325
|
+
try {
|
|
326
|
+
changeImpact = await classifyChangedPackages(node.workspace, baseHead, branchHead);
|
|
327
|
+
} catch {
|
|
328
|
+
changeImpact = undefined;
|
|
329
|
+
}
|
|
330
|
+
recordMeshRefineStage(refineStages, 'resolve_refs', 'passed', resolveStarted, {
|
|
331
|
+
branch, baseBranch, baseHead, branchHead,
|
|
332
|
+
...(changeImpact ? { changeImpact } : {}),
|
|
333
|
+
...(fetchWarning ? { fetchWarning } : {}),
|
|
334
|
+
});
|
|
316
335
|
|
|
317
336
|
return {
|
|
318
337
|
kind: 'continue',
|
|
@@ -330,6 +349,7 @@ export async function refineResolveRefsStage(self: DaemonCommandRouter,
|
|
|
330
349
|
baseBranch,
|
|
331
350
|
baseHead,
|
|
332
351
|
branchHead,
|
|
352
|
+
changeImpact,
|
|
333
353
|
validationSummary: undefined as any,
|
|
334
354
|
patchEquivalence: undefined as any,
|
|
335
355
|
submoduleReachability: undefined as any,
|
|
@@ -346,6 +366,9 @@ export async function refineValidationStage(self: DaemonCommandRouter, ctx: Refi
|
|
|
346
366
|
const { mesh, node, branch, baseBranch, refineStages } = ctx;
|
|
347
367
|
const validationStarted = Date.now();
|
|
348
368
|
const validationSummary = await runMeshRefineValidationGate(mesh, node.workspace, {
|
|
369
|
+
// (a) Scope the validation command set by coarse change-impact (resolved
|
|
370
|
+
// in resolve_refs). Undefined → gate runs the full command set (fail-open).
|
|
371
|
+
changeImpact: ctx.changeImpact,
|
|
349
372
|
// M2-2: consume the node's persisted bootstrap state; persist re-runs.
|
|
350
373
|
persistedBootstrapState: (node as any).worktreeBootstrap as WorktreeBootstrapState | undefined,
|
|
351
374
|
onBootstrapStateChange: (state) => {
|
|
@@ -369,7 +392,9 @@ export async function refineValidationStage(self: DaemonCommandRouter, ctx: Refi
|
|
|
369
392
|
: undefined;
|
|
370
393
|
const buildValidationFailedError = (): string => {
|
|
371
394
|
const base = validationSummary.failureCode === 'missing_dependencies'
|
|
372
|
-
? 'Refinery validation dependencies are missing; merge/refine was not attempted.
|
|
395
|
+
? 'Refinery validation dependencies are missing for a change-affected package; merge/refine was not attempted. '
|
|
396
|
+
+ 'To make this self-service, either (1) configure .adhdev/worktree_bootstrap.json (or validation.bootstrapCommands in .adhdev/refine.json) so Refinery installs deps before validation, '
|
|
397
|
+
+ 'or (2) converge the branch via the documented manual fast-forward-only bypass (rebase onto the fetched base, verify strict ancestry, then push ff-only) instead of the refine gate.'
|
|
373
398
|
: validationSummary.failureCode === 'dependency_bootstrap_failed'
|
|
374
399
|
? 'Refinery dependency/bootstrap command failed; merge/refine was not attempted.'
|
|
375
400
|
: validationSummary.failureCode === 'spawn_resolution_failed'
|
package/src/git/git-status.ts
CHANGED
|
@@ -438,6 +438,54 @@ function isNonRuntimeRootFile(file: string, policy: ResolvedChangeImpactPolicy):
|
|
|
438
438
|
return false;
|
|
439
439
|
}
|
|
440
440
|
|
|
441
|
+
/** Coarse change-impact verdict produced from a changed-file list. */
|
|
442
|
+
export interface ChangedPackageClassification {
|
|
443
|
+
isDaemonAffecting: boolean;
|
|
444
|
+
affectedPackages: string[];
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Pure bucketer: classify an already-collected changed-file list into the coarse
|
|
449
|
+
* daemon-vs-web verdict, per the resolved policy. Shared by classifyDaemonBuildChange
|
|
450
|
+
* (buildCommit..HEAD) and classifyChangedPackages (arbitrary ref range) so the
|
|
451
|
+
* daemon/web boundary logic lives in exactly one place. An empty list stays
|
|
452
|
+
* conservative (daemon-affecting) so an actionable warning is never suppressed.
|
|
453
|
+
*/
|
|
454
|
+
function classifyChangedFileList(
|
|
455
|
+
files: string[],
|
|
456
|
+
policy: ResolvedChangeImpactPolicy,
|
|
457
|
+
): ChangedPackageClassification {
|
|
458
|
+
if (files.length === 0) {
|
|
459
|
+
// No file diff (e.g. only merge metadata) — nothing actionable, but stay
|
|
460
|
+
// conservative and treat as daemon-affecting so we don't suppress a real warning.
|
|
461
|
+
return { isDaemonAffecting: true, affectedPackages: [] };
|
|
462
|
+
}
|
|
463
|
+
const pkgs = new Set<string>();
|
|
464
|
+
// A non-package path that is NOT a recognized benign root file (marker/doc).
|
|
465
|
+
// Only these force daemon-affecting; benign markers/docs are ignored so a
|
|
466
|
+
// gitlink-moving root commit over a marker-only oss commit no longer over-warns.
|
|
467
|
+
let sawRuntimeAmbiguousNonPackage = false;
|
|
468
|
+
for (const file of files) {
|
|
469
|
+
const match = file.match(/(?:^|\/)packages\/([^/]+)\//);
|
|
470
|
+
if (!match) {
|
|
471
|
+
if (!isNonRuntimeRootFile(file, policy)) sawRuntimeAmbiguousNonPackage = true;
|
|
472
|
+
continue;
|
|
473
|
+
}
|
|
474
|
+
pkgs.add(match[1]);
|
|
475
|
+
}
|
|
476
|
+
const affectedPackages = [...pkgs].sort();
|
|
477
|
+
// Daemon-affecting if: any runtime-ambiguous non-package file changed, any
|
|
478
|
+
// unknown package changed, or any explicit daemon-runtime package changed.
|
|
479
|
+
// The daemon is unaffected only when every changed file is either a known
|
|
480
|
+
// web-only package or a recognized benign root file (and at least one such
|
|
481
|
+
// file changed) — i.e. nothing runtime-ambiguous remains. Unlisted/new packages
|
|
482
|
+
// therefore stay daemon-affecting (fail-safe default preserved).
|
|
483
|
+
const allBenign =
|
|
484
|
+
!sawRuntimeAmbiguousNonPackage &&
|
|
485
|
+
affectedPackages.every((p) => policy.webOnlyPackages.has(p) && !policy.daemonRuntimePackages.has(p));
|
|
486
|
+
return { isDaemonAffecting: !allBenign, affectedPackages };
|
|
487
|
+
}
|
|
488
|
+
|
|
441
489
|
/**
|
|
442
490
|
* Determine whether the changes between buildCommit..HEAD touch any daemon-runtime
|
|
443
491
|
* package, per the resolved policy. Returns isDaemonAffecting:true conservatively
|
|
@@ -449,47 +497,54 @@ async function classifyDaemonBuildChange(
|
|
|
449
497
|
buildCommit: string,
|
|
450
498
|
options: GitStatusOptions,
|
|
451
499
|
policy: ResolvedChangeImpactPolicy,
|
|
452
|
-
): Promise<
|
|
500
|
+
): Promise<ChangedPackageClassification> {
|
|
453
501
|
try {
|
|
454
502
|
const diff = await runGit(repoPath, ['diff', '--name-only', `${buildCommit}..HEAD`], options);
|
|
455
503
|
const files = diff.stdout
|
|
456
504
|
.split('\n')
|
|
457
505
|
.map((line) => line.trim())
|
|
458
506
|
.filter(Boolean);
|
|
459
|
-
|
|
460
|
-
// No file diff (e.g. only merge metadata) — nothing actionable, but stay
|
|
461
|
-
// conservative and treat as daemon-affecting so we don't suppress a real warning.
|
|
462
|
-
return { isDaemonAffecting: true, affectedPackages: [] };
|
|
463
|
-
}
|
|
464
|
-
const pkgs = new Set<string>();
|
|
465
|
-
// A non-package path that is NOT a recognized benign root file (marker/doc).
|
|
466
|
-
// Only these force daemon-affecting; benign markers/docs are ignored so a
|
|
467
|
-
// gitlink-moving root commit over a marker-only oss commit no longer over-warns.
|
|
468
|
-
let sawRuntimeAmbiguousNonPackage = false;
|
|
469
|
-
for (const file of files) {
|
|
470
|
-
const match = file.match(/(?:^|\/)packages\/([^/]+)\//);
|
|
471
|
-
if (!match) {
|
|
472
|
-
if (!isNonRuntimeRootFile(file, policy)) sawRuntimeAmbiguousNonPackage = true;
|
|
473
|
-
continue;
|
|
474
|
-
}
|
|
475
|
-
pkgs.add(match[1]);
|
|
476
|
-
}
|
|
477
|
-
const affectedPackages = [...pkgs].sort();
|
|
478
|
-
// Daemon-affecting if: any runtime-ambiguous non-package file changed, any
|
|
479
|
-
// unknown package changed, or any explicit daemon-runtime package changed.
|
|
480
|
-
// The daemon is unaffected only when every changed file is either a known
|
|
481
|
-
// web-only package or a recognized benign root file (and at least one such
|
|
482
|
-
// file changed) — i.e. nothing runtime-ambiguous remains.
|
|
483
|
-
const allBenign =
|
|
484
|
-
!sawRuntimeAmbiguousNonPackage &&
|
|
485
|
-
affectedPackages.every((p) => policy.webOnlyPackages.has(p) && !policy.daemonRuntimePackages.has(p));
|
|
486
|
-
return { isDaemonAffecting: !allBenign, affectedPackages };
|
|
507
|
+
return classifyChangedFileList(files, policy);
|
|
487
508
|
} catch {
|
|
488
509
|
// diff probe failed → can't prove web-only; stay conservative.
|
|
489
510
|
return { isDaemonAffecting: true, affectedPackages: [] };
|
|
490
511
|
}
|
|
491
512
|
}
|
|
492
513
|
|
|
514
|
+
/**
|
|
515
|
+
* Ref-parameterized change-impact classification for a repo/worktree, reusing the
|
|
516
|
+
* exact daemon-vs-web bucketing that the stale-build detector uses — but over an
|
|
517
|
+
* arbitrary `fromRef..toRef` range (e.g. a refine base head → branch head) instead
|
|
518
|
+
* of the live daemon's build commit → HEAD, and WITHOUT any daemonBuildInfo caching.
|
|
519
|
+
*
|
|
520
|
+
* Policy is resolved the same way as getGitRepoStatus: an explicit
|
|
521
|
+
* `options.changeImpactConfig` wins; otherwise the repo's `.adhdev/change-impact.*`
|
|
522
|
+
* is auto-loaded; otherwise the built-in ADHDev default policy applies. The
|
|
523
|
+
* classification uses `git diff --name-only fromRef..toRef`.
|
|
524
|
+
*
|
|
525
|
+
* FAIL-OPEN on error: if the diff can't be collected (bad ref, not a repo), the
|
|
526
|
+
* caller should treat "no verdict" as "run everything" — so we throw rather than
|
|
527
|
+
* returning a misleading benign verdict. Callers wrap this in try/catch and leave
|
|
528
|
+
* changeImpact undefined on failure. Unclassified/new packages still default to
|
|
529
|
+
* isDaemonAffecting:true (never silently skipped).
|
|
530
|
+
*/
|
|
531
|
+
export async function classifyChangedPackages(
|
|
532
|
+
repoPath: string,
|
|
533
|
+
fromRef: string,
|
|
534
|
+
toRef: string,
|
|
535
|
+
options: GitStatusOptions = {},
|
|
536
|
+
): Promise<ChangedPackageClassification> {
|
|
537
|
+
const repo = await resolveGitRepository(repoPath, options);
|
|
538
|
+
const { config } = resolveChangeImpactConfigForRepo(repo.repoRoot, options);
|
|
539
|
+
const policy = resolveChangeImpactPolicy(config);
|
|
540
|
+
const diff = await runGit(repoPath, ['diff', '--name-only', `${fromRef}..${toRef}`], options);
|
|
541
|
+
const files = diff.stdout
|
|
542
|
+
.split('\n')
|
|
543
|
+
.map((line) => line.trim())
|
|
544
|
+
.filter(Boolean);
|
|
545
|
+
return classifyChangedFileList(files, policy);
|
|
546
|
+
}
|
|
547
|
+
|
|
493
548
|
/**
|
|
494
549
|
* Resolve the Change Impact config to apply for this status read. Priority:
|
|
495
550
|
* - options.changeImpactConfig === null → force built-in default policy (no load).
|
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).
|