@adhdev/daemon-core 0.9.77-rc.1 → 0.9.77-rc.11
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/cli-adapters/provider-cli-adapter.d.ts +2 -0
- package/dist/cli-adapters/provider-cli-shared.d.ts +14 -4
- package/dist/commands/mesh-coordinator.d.ts +8 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +1018 -216
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1014 -224
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/mesh-events.d.ts +11 -1
- package/dist/mesh/mesh-ledger.d.ts +90 -0
- package/dist/mesh/mesh-sync.d.ts +10 -0
- package/dist/mesh/mesh-work-queue.d.ts +50 -0
- package/dist/repo-mesh-types.d.ts +6 -0
- package/dist/shared-types.d.ts +12 -0
- package/package.json +1 -1
- package/src/cli-adapters/provider-cli-adapter.ts +10 -4
- package/src/cli-adapters/provider-cli-shared.ts +14 -4
- package/src/commands/mesh-coordinator.ts +28 -6
- package/src/commands/router.ts +222 -0
- package/src/commands/stream-commands.ts +8 -1
- package/src/index.ts +11 -0
- package/src/mesh/coordinator-prompt.ts +27 -12
- package/src/mesh/mesh-events.ts +200 -1
- package/src/mesh/mesh-ledger.ts +378 -0
- package/src/mesh/mesh-sync.ts +32 -0
- package/src/mesh/mesh-work-queue.ts +164 -0
- package/src/repo-mesh-types.ts +7 -0
- package/src/shared-types.ts +12 -0
- package/src/status/builders.ts +13 -0
|
@@ -8,6 +8,17 @@ export interface PendingMeshCoordinatorEvent {
|
|
|
8
8
|
}
|
|
9
9
|
/** Drain and return all pending coordinator events, clearing the queue. */
|
|
10
10
|
export declare function drainPendingMeshCoordinatorEvents(): PendingMeshCoordinatorEvent[];
|
|
11
|
+
export declare function tryAssignQueueTask(components: {
|
|
12
|
+
cliManager: any;
|
|
13
|
+
}, meshId: string, nodeId: string, sessionId: string, providerType: string): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Triggers a queue check for all nodes in the mesh.
|
|
16
|
+
* Called when a new task is enqueued, in case nodes are already idle.
|
|
17
|
+
*/
|
|
18
|
+
export declare function triggerMeshQueue(components: {
|
|
19
|
+
instanceManager: any;
|
|
20
|
+
cliManager: any;
|
|
21
|
+
}, meshId: string): void;
|
|
11
22
|
export declare function handleMeshForwardEvent(components: DaemonComponents, payload: Record<string, unknown>): {
|
|
12
23
|
success: boolean;
|
|
13
24
|
forwarded: number;
|
|
@@ -15,6 +26,5 @@ export declare function handleMeshForwardEvent(components: DaemonComponents, pay
|
|
|
15
26
|
} | {
|
|
16
27
|
success: boolean;
|
|
17
28
|
error: string;
|
|
18
|
-
forwarded?: undefined;
|
|
19
29
|
};
|
|
20
30
|
export declare function setupMeshEventForwarding(components: DaemonComponents): void;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mesh Task Ledger — GasTown-inspired append-only JSONL task history
|
|
3
|
+
*
|
|
4
|
+
* Records all mesh orchestration events (task dispatch, completion, failure,
|
|
5
|
+
* checkpoint, node lifecycle) as an append-only JSONL file per mesh.
|
|
6
|
+
*
|
|
7
|
+
* Inspired by GasTown's "Beads" pattern: every action is a versioned record
|
|
8
|
+
* that persists across agent sessions, enabling recovery, auditing, and
|
|
9
|
+
* continuity when individual sessions fail or context windows are exhausted.
|
|
10
|
+
*
|
|
11
|
+
* Storage: ~/.adhdev/mesh-ledger/<meshId>.jsonl
|
|
12
|
+
* Format: One JSON object per line, newest entries appended at end
|
|
13
|
+
* Safety: mode 0o600, atomic append via appendFileSync
|
|
14
|
+
*/
|
|
15
|
+
import { EventEmitter } from 'events';
|
|
16
|
+
export type MeshLedgerKind = 'task_dispatched' | 'task_completed' | 'task_failed' | 'task_stalled' | 'task_approval_needed' | 'session_launched' | 'session_stopped' | 'checkpoint_created' | 'node_cloned' | 'node_removed' | 'coordinator_started' | 'recovery_attempted';
|
|
17
|
+
export interface MeshLedgerEntry {
|
|
18
|
+
id: string;
|
|
19
|
+
meshId: string;
|
|
20
|
+
timestamp: string;
|
|
21
|
+
kind: MeshLedgerKind;
|
|
22
|
+
nodeId?: string;
|
|
23
|
+
sessionId?: string;
|
|
24
|
+
providerType?: string;
|
|
25
|
+
payload: Record<string, unknown>;
|
|
26
|
+
}
|
|
27
|
+
export interface MeshLedgerSummary {
|
|
28
|
+
meshId: string;
|
|
29
|
+
totalEntries: number;
|
|
30
|
+
taskDispatched: number;
|
|
31
|
+
taskCompleted: number;
|
|
32
|
+
taskFailed: number;
|
|
33
|
+
taskStalled: number;
|
|
34
|
+
sessionLaunched: number;
|
|
35
|
+
checkpointCreated: number;
|
|
36
|
+
lastActivityAt: string | null;
|
|
37
|
+
recentFailures: number;
|
|
38
|
+
}
|
|
39
|
+
export interface ReadLedgerOptions {
|
|
40
|
+
tail?: number;
|
|
41
|
+
since?: string;
|
|
42
|
+
kind?: MeshLedgerKind[];
|
|
43
|
+
}
|
|
44
|
+
export declare function getLedgerDir(): string;
|
|
45
|
+
/**
|
|
46
|
+
* Append a new entry to the mesh ledger.
|
|
47
|
+
* Handles file creation, rotation on size overflow, and atomic writes.
|
|
48
|
+
*/
|
|
49
|
+
export declare const meshLedgerEvents: EventEmitter<[never]>;
|
|
50
|
+
export declare function appendLedgerEntry(meshId: string, partial: Omit<MeshLedgerEntry, 'id' | 'meshId' | 'timestamp'>): MeshLedgerEntry;
|
|
51
|
+
/**
|
|
52
|
+
* Append entries received from the cloud to the local ledger.
|
|
53
|
+
* This skips deduplicated entries and just writes new ones.
|
|
54
|
+
*/
|
|
55
|
+
export declare function appendRemoteLedgerEntries(meshId: string, entries: MeshLedgerEntry[]): void;
|
|
56
|
+
/**
|
|
57
|
+
* Read ledger entries with optional filtering.
|
|
58
|
+
*/
|
|
59
|
+
export declare function readLedgerEntries(meshId: string, opts?: ReadLedgerOptions): MeshLedgerEntry[];
|
|
60
|
+
/**
|
|
61
|
+
* Get a summary of mesh activity from the ledger.
|
|
62
|
+
*/
|
|
63
|
+
export declare function getLedgerSummary(meshId: string): MeshLedgerSummary;
|
|
64
|
+
export interface SessionRecoveryContext {
|
|
65
|
+
/** The original task message that was dispatched to this session/node */
|
|
66
|
+
lastTaskMessage: string | null;
|
|
67
|
+
/** The node that was running the failed task */
|
|
68
|
+
failedNodeId: string | null;
|
|
69
|
+
/** Session ID of the failed session */
|
|
70
|
+
failedSessionId: string | null;
|
|
71
|
+
/** Provider used for the failed session */
|
|
72
|
+
failedProviderType: string | null;
|
|
73
|
+
/** Number of consecutive failures for this node (within recent window) */
|
|
74
|
+
consecutiveNodeFailures: number;
|
|
75
|
+
/** Number of times this specific task was attempted (matched by truncated message prefix) */
|
|
76
|
+
taskAttemptCount: number;
|
|
77
|
+
/** Whether a retry is recommended based on maxRetries policy */
|
|
78
|
+
retryRecommended: boolean;
|
|
79
|
+
/** Human-readable recovery advice for the coordinator */
|
|
80
|
+
advice: string;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Build recovery context for a failed session.
|
|
84
|
+
* Looks up the ledger to find the original task, count failures, and advise on retry.
|
|
85
|
+
*/
|
|
86
|
+
export declare function getSessionRecoveryContext(meshId: string, opts: {
|
|
87
|
+
sessionId?: string;
|
|
88
|
+
nodeId?: string;
|
|
89
|
+
maxRetries?: number;
|
|
90
|
+
}): SessionRecoveryContext;
|
package/dist/mesh/mesh-sync.d.ts
CHANGED
|
@@ -26,6 +26,12 @@ export interface MeshSyncTransport {
|
|
|
26
26
|
}>;
|
|
27
27
|
/** DELETE /api/v1/repo-meshes/:id */
|
|
28
28
|
deleteRemoteMesh(meshId: string): Promise<void>;
|
|
29
|
+
/** POST /api/v1/repo-meshes/:id/ledger/sync */
|
|
30
|
+
syncMeshLedger?(meshId: string, data: {
|
|
31
|
+
newEntries: any[];
|
|
32
|
+
}): Promise<{
|
|
33
|
+
missingEntries: any[];
|
|
34
|
+
}>;
|
|
29
35
|
}
|
|
30
36
|
export interface RemoteMeshRecord {
|
|
31
37
|
id: string;
|
|
@@ -49,3 +55,7 @@ export interface MeshSyncResult {
|
|
|
49
55
|
* Pull remote meshes that don't exist locally.
|
|
50
56
|
*/
|
|
51
57
|
export declare function syncMeshes(transport: MeshSyncTransport): Promise<MeshSyncResult>;
|
|
58
|
+
/**
|
|
59
|
+
* Sync the task ledger for a specific mesh.
|
|
60
|
+
*/
|
|
61
|
+
export declare function syncMeshLedger(meshId: string, transport: MeshSyncTransport): Promise<void>;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export type MeshTaskStatus = 'pending' | 'assigned' | 'completed' | 'failed';
|
|
2
|
+
export interface MeshWorkQueueEntry {
|
|
3
|
+
id: string;
|
|
4
|
+
meshId: string;
|
|
5
|
+
message: string;
|
|
6
|
+
status: MeshTaskStatus;
|
|
7
|
+
/** If specified, only this node can claim the task (used by legacy mesh_send_task) */
|
|
8
|
+
targetNodeId?: string;
|
|
9
|
+
/** The node that actually claimed and is executing the task */
|
|
10
|
+
assignedNodeId?: string;
|
|
11
|
+
/** The session currently executing the task */
|
|
12
|
+
assignedSessionId?: string;
|
|
13
|
+
createdAt: string;
|
|
14
|
+
updatedAt: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Add a new task to the mesh queue.
|
|
18
|
+
*/
|
|
19
|
+
export declare function enqueueTask(meshId: string, message: string, opts?: {
|
|
20
|
+
targetNodeId?: string;
|
|
21
|
+
}): MeshWorkQueueEntry;
|
|
22
|
+
/**
|
|
23
|
+
* Get all tasks in the queue, optionally filtered by status.
|
|
24
|
+
*/
|
|
25
|
+
export declare function getQueue(meshId: string, opts?: {
|
|
26
|
+
status?: MeshTaskStatus[];
|
|
27
|
+
}): MeshWorkQueueEntry[];
|
|
28
|
+
/**
|
|
29
|
+
* Find the next pending task that this node is allowed to claim, and mark it as assigned.
|
|
30
|
+
*/
|
|
31
|
+
export declare function claimNextTask(meshId: string, nodeId: string, sessionId: string): MeshWorkQueueEntry | null;
|
|
32
|
+
/**
|
|
33
|
+
* Update the status of a specific task.
|
|
34
|
+
* Used when a session completes, fails, or stalls.
|
|
35
|
+
*/
|
|
36
|
+
export declare function updateTaskStatus(meshId: string, taskId: string, status: MeshTaskStatus): MeshWorkQueueEntry | null;
|
|
37
|
+
/**
|
|
38
|
+
* Update the status of the task currently assigned to a specific session.
|
|
39
|
+
*/
|
|
40
|
+
export declare function updateSessionTaskStatus(meshId: string, sessionId: string, status: MeshTaskStatus): MeshWorkQueueEntry | null;
|
|
41
|
+
export interface MeshWorkQueueStats {
|
|
42
|
+
pending: number;
|
|
43
|
+
assigned: number;
|
|
44
|
+
completed: number;
|
|
45
|
+
failed: number;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Return aggregate queue statistics for the given mesh.
|
|
49
|
+
*/
|
|
50
|
+
export declare function getMeshQueueStats(meshId: string): MeshWorkQueueStats;
|
|
@@ -62,6 +62,12 @@ export interface RepoMeshPolicy {
|
|
|
62
62
|
* runtimes are never stopped/deleted unless the mesh owner opts in.
|
|
63
63
|
*/
|
|
64
64
|
sessionCleanupOnNodeRemove?: RepoMeshSessionCleanupMode;
|
|
65
|
+
/**
|
|
66
|
+
* Maximum number of automatic retry recommendations for a failed task on the
|
|
67
|
+
* same node before the daemon advises the coordinator to escalate or reassign.
|
|
68
|
+
* Defaults to 1 (allow one retry). Set to 0 to disable auto-recovery advice.
|
|
69
|
+
*/
|
|
70
|
+
maxTaskRetries?: number;
|
|
65
71
|
}
|
|
66
72
|
export interface RepoMeshRelatedRepo {
|
|
67
73
|
/** Stable display label for an explicitly configured associated checkout. */
|
package/dist/shared-types.d.ts
CHANGED
|
@@ -292,6 +292,12 @@ export interface SessionEntry {
|
|
|
292
292
|
seenCompletionMarker?: string;
|
|
293
293
|
surfaceHidden?: boolean;
|
|
294
294
|
settings?: Record<string, any>;
|
|
295
|
+
meshQueueStats?: {
|
|
296
|
+
pending: number;
|
|
297
|
+
assigned: number;
|
|
298
|
+
completed: number;
|
|
299
|
+
failed: number;
|
|
300
|
+
};
|
|
295
301
|
}
|
|
296
302
|
/**
|
|
297
303
|
* Compact session metadata stored in UserSessionDO and reused by server-side
|
|
@@ -330,6 +336,12 @@ export interface CompactSessionEntry {
|
|
|
330
336
|
providerControls?: ProviderControlSchema[];
|
|
331
337
|
summaryMetadata?: ProviderSummaryMetadata;
|
|
332
338
|
settings?: Record<string, any>;
|
|
339
|
+
meshQueueStats?: {
|
|
340
|
+
pending: number;
|
|
341
|
+
assigned: number;
|
|
342
|
+
completed: number;
|
|
343
|
+
failed: number;
|
|
344
|
+
};
|
|
333
345
|
}
|
|
334
346
|
export type VersionUpdateReason = 'force_update_below' | 'major_minor_mismatch' | 'patch_mismatch' | 'daemon_ahead';
|
|
335
347
|
export type ReleaseChannel = 'stable' | 'preview';
|
package/package.json
CHANGED
|
@@ -195,6 +195,8 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
195
195
|
|
|
196
196
|
// ─── CLI Scripts (script-based parsing) ───
|
|
197
197
|
private cliScripts: CliScripts;
|
|
198
|
+
/** Per-session opaque state object created by cliScripts.createState(), reset on stop. */
|
|
199
|
+
private scriptState: unknown = null;
|
|
198
200
|
private runtimeSettings: Record<string, any> = {};
|
|
199
201
|
/** Full accumulated rendered PTY transcript for parser/readback use */
|
|
200
202
|
private accumulatedBuffer: string = '';
|
|
@@ -477,6 +479,9 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
477
479
|
this.cliScripts = scripts;
|
|
478
480
|
this.parsedStatusCache = null;
|
|
479
481
|
this.parseErrorMessage = null;
|
|
482
|
+
// Initialize per-session state: createState() is called once here and on script reload.
|
|
483
|
+
// The returned object lives until the PTY exits (scriptState = null on exit).
|
|
484
|
+
this.scriptState = typeof scripts.createState === 'function' ? scripts.createState() : null;
|
|
480
485
|
const scriptNames = listCliScriptNames(scripts);
|
|
481
486
|
LOG.info('CLI', `[${this.cliType}] CLI scripts injected: [${scriptNames.join(', ')}]`);
|
|
482
487
|
}
|
|
@@ -610,6 +615,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
610
615
|
this.ready = false;
|
|
611
616
|
this.startupParseGate = false;
|
|
612
617
|
this.spawnAt = 0;
|
|
618
|
+
this.scriptState = null;
|
|
613
619
|
this.onStatusChange?.();
|
|
614
620
|
});
|
|
615
621
|
|
|
@@ -1470,7 +1476,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1470
1476
|
scope: this.currentTurnScope,
|
|
1471
1477
|
runtimeSettings: this.runtimeSettings,
|
|
1472
1478
|
});
|
|
1473
|
-
const session = this.cliScripts.parseSession({ ...input, tail, tailScreen: buildCliScreenSnapshot(tail) });
|
|
1479
|
+
const session = this.cliScripts.parseSession(this.scriptState, { ...input, tail, tailScreen: buildCliScreenSnapshot(tail) });
|
|
1474
1480
|
this.parseErrorMessage = null;
|
|
1475
1481
|
return session && typeof session === 'object' ? session : null;
|
|
1476
1482
|
} catch (e: any) {
|
|
@@ -1485,7 +1491,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1485
1491
|
if (!this.cliScripts?.detectStatus) return null;
|
|
1486
1492
|
try {
|
|
1487
1493
|
const screenText = this.terminalScreen.getText();
|
|
1488
|
-
const status = this.cliScripts.detectStatus({
|
|
1494
|
+
const status = this.cliScripts.detectStatus(this.scriptState, {
|
|
1489
1495
|
tail: text.slice(-500),
|
|
1490
1496
|
screenText,
|
|
1491
1497
|
rawBuffer: this.accumulatedRawBuffer,
|
|
@@ -1505,7 +1511,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1505
1511
|
try {
|
|
1506
1512
|
const screenText = this.terminalScreen.getText();
|
|
1507
1513
|
const buffer = screenText || this.accumulatedBuffer;
|
|
1508
|
-
return this.cliScripts.parseApproval({
|
|
1514
|
+
return this.cliScripts.parseApproval(this.scriptState, {
|
|
1509
1515
|
buffer,
|
|
1510
1516
|
screenText,
|
|
1511
1517
|
rawBuffer: this.accumulatedRawBuffer,
|
|
@@ -1640,7 +1646,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1640
1646
|
scope: this.currentTurnScope,
|
|
1641
1647
|
runtimeSettings: this.runtimeSettings,
|
|
1642
1648
|
});
|
|
1643
|
-
return await Promise.resolve(fn({
|
|
1649
|
+
return await Promise.resolve(fn(this.scriptState, {
|
|
1644
1650
|
...input,
|
|
1645
1651
|
args: args && typeof args === 'object' ? { ...args } : {},
|
|
1646
1652
|
}));
|
|
@@ -48,11 +48,21 @@ export interface ParsedSession {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
export interface CliScripts {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
/**
|
|
52
|
+
* Optional state factory. Called once per CLI session start (or script reload).
|
|
53
|
+
* The returned object is passed as the first argument to detectStatus, parseApproval,
|
|
54
|
+
* and parseSession on every invocation, allowing scripts to maintain per-session state
|
|
55
|
+
* (e.g. last-seen status, approval fingerprints, stability counters).
|
|
56
|
+
*
|
|
57
|
+
* Scripts that don't define createState() receive null as the state argument,
|
|
58
|
+
* making this change fully backward compatible.
|
|
59
|
+
*/
|
|
60
|
+
createState?: () => unknown;
|
|
61
|
+
parseSession?: (state: unknown, input: CliScriptInput & { tail?: string; tailScreen?: CliScreenSnapshot }) => ParsedSession | null;
|
|
62
|
+
detectStatus?: (state: unknown, input: CliStatusInput) => string | null;
|
|
63
|
+
parseApproval?: (state: unknown, input: CliApprovalInput) => { message: string; buttons: string[] } | null;
|
|
54
64
|
resolveAction?: (data: any) => string;
|
|
55
|
-
[name: string]: ((input: any) => any) | undefined;
|
|
65
|
+
[name: string]: ((state: unknown, input: any) => any) | ((data: any) => any) | (() => unknown) | undefined;
|
|
56
66
|
}
|
|
57
67
|
|
|
58
68
|
export interface CliScreenLine {
|
|
@@ -28,6 +28,15 @@ export type MeshCoordinatorSetup =
|
|
|
28
28
|
instructions: string
|
|
29
29
|
template: string
|
|
30
30
|
}
|
|
31
|
+
| {
|
|
32
|
+
/** Provider registers MCP via its own CLI command (e.g. `codex mcp add` / `gemini mcp add`). */
|
|
33
|
+
kind: 'cli_command'
|
|
34
|
+
serverName: string
|
|
35
|
+
/** The rendered shell command to execute before launching the coordinator session. */
|
|
36
|
+
command: string
|
|
37
|
+
requiresRestart: boolean
|
|
38
|
+
instructions: string
|
|
39
|
+
}
|
|
31
40
|
| {
|
|
32
41
|
kind: 'unsupported'
|
|
33
42
|
reason: string
|
|
@@ -152,6 +161,24 @@ export function resolveMeshCoordinatorSetup(options: ResolveMeshCoordinatorSetup
|
|
|
152
161
|
if (!instructions || !template?.trim()) {
|
|
153
162
|
return { kind: 'unsupported', reason: 'Provider manual MCP setup is missing instructions or template' }
|
|
154
163
|
}
|
|
164
|
+
const renderedTemplate = renderMeshCoordinatorTemplate(template, {
|
|
165
|
+
meshId,
|
|
166
|
+
workspace,
|
|
167
|
+
serverName,
|
|
168
|
+
adhdevMcpCommand: options.adhdevMcpCommand || DEFAULT_ADHDEV_MCP_COMMAND,
|
|
169
|
+
})
|
|
170
|
+
// Detect if the template is a runnable CLI command (single line, no YAML/JSON structure).
|
|
171
|
+
// If so, use cli_command kind so the daemon can execute it automatically.
|
|
172
|
+
const isCliCommand = !renderedTemplate.trim().includes('\n') && !renderedTemplate.trim().startsWith('{')
|
|
173
|
+
if (isCliCommand) {
|
|
174
|
+
return {
|
|
175
|
+
kind: 'cli_command',
|
|
176
|
+
serverName,
|
|
177
|
+
command: renderedTemplate.trim(),
|
|
178
|
+
requiresRestart: mcpConfig.requiresRestart === true,
|
|
179
|
+
instructions: instructions,
|
|
180
|
+
}
|
|
181
|
+
}
|
|
155
182
|
return {
|
|
156
183
|
kind: 'manual',
|
|
157
184
|
serverName,
|
|
@@ -159,12 +186,7 @@ export function resolveMeshCoordinatorSetup(options: ResolveMeshCoordinatorSetup
|
|
|
159
186
|
configPathCommand: mcpConfig.configPathCommand,
|
|
160
187
|
requiresRestart: mcpConfig.requiresRestart === true,
|
|
161
188
|
instructions,
|
|
162
|
-
template:
|
|
163
|
-
meshId,
|
|
164
|
-
workspace,
|
|
165
|
-
serverName,
|
|
166
|
-
adhdevMcpCommand: options.adhdevMcpCommand || DEFAULT_ADHDEV_MCP_COMMAND,
|
|
167
|
-
}),
|
|
189
|
+
template: renderedTemplate,
|
|
168
190
|
}
|
|
169
191
|
}
|
|
170
192
|
|
package/src/commands/router.ts
CHANGED
|
@@ -1315,6 +1315,22 @@ export class DaemonCommandRouter {
|
|
|
1315
1315
|
}
|
|
1316
1316
|
}
|
|
1317
1317
|
|
|
1318
|
+
case 'get_mesh_ledger': {
|
|
1319
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
1320
|
+
if (!meshId) return { success: false, error: 'meshId required' };
|
|
1321
|
+
try {
|
|
1322
|
+
const { readLedgerEntries, getLedgerSummary } = await import('../mesh/mesh-ledger.js');
|
|
1323
|
+
const tail = typeof args?.tail === 'number' ? args.tail : 20;
|
|
1324
|
+
const since = typeof args?.since === 'string' ? args.since : undefined;
|
|
1325
|
+
const kind = Array.isArray(args?.kind) ? args.kind.filter((k: any) => typeof k === 'string') : undefined;
|
|
1326
|
+
const entries = readLedgerEntries(meshId, { tail, since, kind });
|
|
1327
|
+
const summary = getLedgerSummary(meshId);
|
|
1328
|
+
return { success: true, entries, summary };
|
|
1329
|
+
} catch (e: any) {
|
|
1330
|
+
return { success: false, error: e.message };
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1318
1334
|
case 'add_mesh_node': {
|
|
1319
1335
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
1320
1336
|
const workspace = typeof args?.workspace === 'string' ? args.workspace.trim() : '';
|
|
@@ -1394,6 +1410,65 @@ export class DaemonCommandRouter {
|
|
|
1394
1410
|
}
|
|
1395
1411
|
}
|
|
1396
1412
|
|
|
1413
|
+
case 'refine_mesh_node': {
|
|
1414
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
1415
|
+
const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
|
|
1416
|
+
if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
|
|
1417
|
+
try {
|
|
1418
|
+
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
|
|
1419
|
+
const mesh = meshRecord?.mesh;
|
|
1420
|
+
const node = mesh?.nodes?.find((n: any) => n.id === nodeId || n.nodeId === nodeId);
|
|
1421
|
+
if (!node) return { success: false, error: `Node '${nodeId}' not found in mesh` };
|
|
1422
|
+
|
|
1423
|
+
if (!node.isLocalWorktree || !node.workspace) {
|
|
1424
|
+
return { success: false, error: `Refinery requires a local worktree node` };
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
const sourceNode = node.clonedFromNodeId
|
|
1428
|
+
? mesh?.nodes.find((n: any) => n.id === node.clonedFromNodeId || n.nodeId === node.clonedFromNodeId)
|
|
1429
|
+
: mesh?.nodes.find((n: any) => !n.isLocalWorktree);
|
|
1430
|
+
const repoRoot = sourceNode?.repoRoot || sourceNode?.workspace;
|
|
1431
|
+
if (!repoRoot) return { success: false, error: 'Source node repoRoot not found' };
|
|
1432
|
+
|
|
1433
|
+
const { execFile } = await import('node:child_process');
|
|
1434
|
+
const { promisify } = await import('node:util');
|
|
1435
|
+
const execFileAsync = promisify(execFile);
|
|
1436
|
+
|
|
1437
|
+
const { stdout: branchStdout } = await execFileAsync('git', ['branch', '--show-current'], { cwd: node.workspace, encoding: 'utf8' });
|
|
1438
|
+
const branch = branchStdout.trim();
|
|
1439
|
+
if (!branch) return { success: false, error: 'Could not determine branch of the worktree node' };
|
|
1440
|
+
|
|
1441
|
+
const { stdout: baseBranchStdout } = await execFileAsync('git', ['branch', '--show-current'], { cwd: repoRoot, encoding: 'utf8' });
|
|
1442
|
+
const baseBranch = baseBranchStdout.trim();
|
|
1443
|
+
|
|
1444
|
+
try {
|
|
1445
|
+
await execFileAsync('git', ['merge', '--no-ff', branch, '-m', `Auto-merge branch '${branch}' via Refinery`], { cwd: repoRoot, encoding: 'utf8' });
|
|
1446
|
+
} catch (e: any) {
|
|
1447
|
+
return { success: false, error: `Merge failed (conflicts?): ${e.message}` };
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
const removeResult = await this.execute('remove_mesh_node', {
|
|
1451
|
+
meshId,
|
|
1452
|
+
nodeId,
|
|
1453
|
+
sessionCleanupMode: 'kill',
|
|
1454
|
+
inlineMesh: args?.inlineMesh,
|
|
1455
|
+
});
|
|
1456
|
+
|
|
1457
|
+
try {
|
|
1458
|
+
const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
|
|
1459
|
+
appendLedgerEntry(meshId, {
|
|
1460
|
+
kind: 'node_removed',
|
|
1461
|
+
nodeId,
|
|
1462
|
+
payload: { refined: true, mergedBranch: branch, into: baseBranch },
|
|
1463
|
+
});
|
|
1464
|
+
} catch {}
|
|
1465
|
+
|
|
1466
|
+
return { success: true, merged: true, branch, into: baseBranch, removeResult };
|
|
1467
|
+
} catch (e: any) {
|
|
1468
|
+
return { success: false, error: e.message };
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1397
1472
|
case 'remove_mesh_node': {
|
|
1398
1473
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
1399
1474
|
const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
|
|
@@ -1436,6 +1511,19 @@ export class DaemonCommandRouter {
|
|
|
1436
1511
|
const { removeNode } = await import('../config/mesh-config.js');
|
|
1437
1512
|
removed = removeNode(meshId, nodeId);
|
|
1438
1513
|
}
|
|
1514
|
+
|
|
1515
|
+
// Record in task ledger
|
|
1516
|
+
if (removed) {
|
|
1517
|
+
try {
|
|
1518
|
+
const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
|
|
1519
|
+
appendLedgerEntry(meshId, {
|
|
1520
|
+
kind: 'node_removed',
|
|
1521
|
+
nodeId,
|
|
1522
|
+
payload: { worktree: !!node?.isLocalWorktree, sessionCleanupMode },
|
|
1523
|
+
});
|
|
1524
|
+
} catch { /* ledger append is best-effort */ }
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1439
1527
|
return { success: true, removed, ...(sessionCleanup ? { sessionCleanup } : {}) };
|
|
1440
1528
|
} catch (e: any) {
|
|
1441
1529
|
return { success: false, error: e.message };
|
|
@@ -1498,6 +1586,16 @@ export class DaemonCommandRouter {
|
|
|
1498
1586
|
if (!node) return { success: false, error: 'Failed to register worktree node' };
|
|
1499
1587
|
}
|
|
1500
1588
|
|
|
1589
|
+
// Record in task ledger
|
|
1590
|
+
try {
|
|
1591
|
+
const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
|
|
1592
|
+
appendLedgerEntry(meshId, {
|
|
1593
|
+
kind: 'node_cloned',
|
|
1594
|
+
nodeId: node.id,
|
|
1595
|
+
payload: { sourceNodeId, branch: result.branch, worktreePath: result.worktreePath },
|
|
1596
|
+
});
|
|
1597
|
+
} catch { /* ledger append is best-effort */ }
|
|
1598
|
+
|
|
1501
1599
|
return {
|
|
1502
1600
|
success: true,
|
|
1503
1601
|
node,
|
|
@@ -1508,6 +1606,19 @@ export class DaemonCommandRouter {
|
|
|
1508
1606
|
return { success: false, error: e.message };
|
|
1509
1607
|
}
|
|
1510
1608
|
}
|
|
1609
|
+
case 'trigger_mesh_queue': {
|
|
1610
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
1611
|
+
if (!meshId) return { success: false, error: 'meshId required' };
|
|
1612
|
+
try {
|
|
1613
|
+
const { triggerMeshQueue } = await import('../mesh/mesh-events.js');
|
|
1614
|
+
if (meshId) {
|
|
1615
|
+
triggerMeshQueue(this.deps as any, meshId);
|
|
1616
|
+
}
|
|
1617
|
+
return { success: true };
|
|
1618
|
+
} catch (e: any) {
|
|
1619
|
+
return { success: false, error: e.message };
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1511
1622
|
|
|
1512
1623
|
// ─── Mesh Coordinator Launch ───
|
|
1513
1624
|
case 'launch_mesh_coordinator': {
|
|
@@ -1600,6 +1711,105 @@ export class DaemonCommandRouter {
|
|
|
1600
1711
|
};
|
|
1601
1712
|
}
|
|
1602
1713
|
|
|
1714
|
+
// ─── CLI-command MCP registration (Codex, Gemini CLI) ───────────
|
|
1715
|
+
if (coordinatorSetup.kind === 'cli_command') {
|
|
1716
|
+
// Build coordinator prompt first — fail closed on errors.
|
|
1717
|
+
let cliCmdSystemPrompt = '';
|
|
1718
|
+
try {
|
|
1719
|
+
cliCmdSystemPrompt = buildCoordinatorSystemPrompt({ mesh, coordinatorCliType: cliType });
|
|
1720
|
+
} catch (error: any) {
|
|
1721
|
+
const message = error?.message || String(error);
|
|
1722
|
+
LOG.error('MeshCoordinator', `Failed to build coordinator prompt: ${message}`);
|
|
1723
|
+
return {
|
|
1724
|
+
success: false,
|
|
1725
|
+
code: 'mesh_coordinator_prompt_failed',
|
|
1726
|
+
error: `Failed to build Repo Mesh coordinator prompt: ${message}`,
|
|
1727
|
+
meshId, cliType, workspace,
|
|
1728
|
+
};
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
// Run the provider's MCP registration command.
|
|
1732
|
+
try {
|
|
1733
|
+
const { execFileSync: execCmdSync } = await import('node:child_process');
|
|
1734
|
+
const cmdParts = coordinatorSetup.command.trim().split(/\s+/);
|
|
1735
|
+
const [regCmd, ...regArgs] = cmdParts;
|
|
1736
|
+
LOG.info('MeshCoordinator', `Running MCP registration: ${coordinatorSetup.command}`);
|
|
1737
|
+
execCmdSync(regCmd, regArgs, { stdio: 'pipe', timeout: 15_000 });
|
|
1738
|
+
} catch (error: any) {
|
|
1739
|
+
// Non-fatal — server may already be registered (providers return exit 1 on duplicate).
|
|
1740
|
+
LOG.warn('MeshCoordinator', `MCP registration command failed (may be pre-registered): ${error?.message || error}`);
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
// Inject system prompt using provider-native methods.
|
|
1744
|
+
// Codex: -c 'instructions="..."' CLI config override
|
|
1745
|
+
// Gemini: write GEMINI.md to workspace (auto-loaded as context)
|
|
1746
|
+
const cliCmdArgs: string[] = [];
|
|
1747
|
+
const cliCmdEnv: Record<string, string> = {};
|
|
1748
|
+
if (cliCmdSystemPrompt) {
|
|
1749
|
+
if (cliType === 'codex-cli') {
|
|
1750
|
+
// Codex reads `developer_instructions` from config.toml as system instructions.
|
|
1751
|
+
// The -c flag overrides a config key for this session only.
|
|
1752
|
+
cliCmdArgs.push('-c', `developer_instructions=${JSON.stringify(cliCmdSystemPrompt)}`);
|
|
1753
|
+
} else if (cliType === 'gemini-cli') {
|
|
1754
|
+
// Gemini CLI auto-loads GEMINI.md from CWD as project context.
|
|
1755
|
+
// Write a temporary GEMINI.md to the workspace before launch.
|
|
1756
|
+
try {
|
|
1757
|
+
const { writeFileSync: wfs, existsSync: efs, readFileSync: rfs } = await import('node:fs');
|
|
1758
|
+
const geminiMdPath = `${workspace}/GEMINI.md`;
|
|
1759
|
+
const marker = '<!-- adhdev-mesh-coordinator-prompt -->';
|
|
1760
|
+
const markerEnd = '<!-- /adhdev-mesh-coordinator-prompt -->';
|
|
1761
|
+
const block = `${marker}\n${cliCmdSystemPrompt}\n${markerEnd}`;
|
|
1762
|
+
if (efs(geminiMdPath)) {
|
|
1763
|
+
const existing = rfs(geminiMdPath, 'utf-8');
|
|
1764
|
+
// Replace existing block or append
|
|
1765
|
+
const replaced = existing.replace(
|
|
1766
|
+
new RegExp(`${marker}[\\s\\S]*?${markerEnd}`, 'g'),
|
|
1767
|
+
block,
|
|
1768
|
+
);
|
|
1769
|
+
wfs(geminiMdPath, replaced.includes(marker) ? replaced : `${existing}\n\n${block}`);
|
|
1770
|
+
} else {
|
|
1771
|
+
wfs(geminiMdPath, block);
|
|
1772
|
+
}
|
|
1773
|
+
LOG.info('MeshCoordinator', `Wrote coordinator prompt to ${workspace}/GEMINI.md`);
|
|
1774
|
+
} catch (e: any) {
|
|
1775
|
+
LOG.warn('MeshCoordinator', `Could not write GEMINI.md: ${e?.message || e}`);
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
const cliCmdLaunch: any = await this.deps.cliManager.handleCliCommand('launch_cli', {
|
|
1781
|
+
cliType,
|
|
1782
|
+
dir: workspace,
|
|
1783
|
+
cliArgs: cliCmdArgs.length > 0 ? cliCmdArgs : undefined,
|
|
1784
|
+
env: Object.keys(cliCmdEnv).length > 0 ? cliCmdEnv : undefined,
|
|
1785
|
+
settings: { meshCoordinatorFor: meshId },
|
|
1786
|
+
});
|
|
1787
|
+
|
|
1788
|
+
if (!cliCmdLaunch?.success) {
|
|
1789
|
+
return { success: false, error: cliCmdLaunch?.error || 'Failed to launch CLI session' };
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
LOG.info('MeshCoordinator', `Launched ${cliType} coordinator (cli_command) for mesh ${meshId}`);
|
|
1793
|
+
try {
|
|
1794
|
+
const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
|
|
1795
|
+
appendLedgerEntry(meshId, {
|
|
1796
|
+
kind: 'coordinator_started',
|
|
1797
|
+
sessionId: cliCmdLaunch.sessionId || cliCmdLaunch.id,
|
|
1798
|
+
providerType: cliType,
|
|
1799
|
+
payload: { workspace },
|
|
1800
|
+
});
|
|
1801
|
+
} catch { /* best-effort */ }
|
|
1802
|
+
|
|
1803
|
+
return {
|
|
1804
|
+
success: true,
|
|
1805
|
+
meshId,
|
|
1806
|
+
cliType,
|
|
1807
|
+
workspace,
|
|
1808
|
+
sessionId: cliCmdLaunch.sessionId || cliCmdLaunch.id,
|
|
1809
|
+
mcpRegistered: true,
|
|
1810
|
+
};
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1603
1813
|
const configFormat = coordinatorSetup.configFormat as MeshCoordinatorConfigFormat;
|
|
1604
1814
|
if (configFormat !== 'claude_mcp_json' && configFormat !== 'hermes_config_yaml') {
|
|
1605
1815
|
return {
|
|
@@ -1756,6 +1966,18 @@ export class DaemonCommandRouter {
|
|
|
1756
1966
|
}
|
|
1757
1967
|
|
|
1758
1968
|
LOG.info('MeshCoordinator', `Launched ${cliType} coordinator for mesh ${meshId} in ${workspace}`);
|
|
1969
|
+
|
|
1970
|
+
// Record coordinator launch in task ledger
|
|
1971
|
+
try {
|
|
1972
|
+
const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
|
|
1973
|
+
appendLedgerEntry(meshId, {
|
|
1974
|
+
kind: 'coordinator_started',
|
|
1975
|
+
sessionId: launchResult.sessionId || launchResult.id,
|
|
1976
|
+
providerType: cliType,
|
|
1977
|
+
payload: { workspace },
|
|
1978
|
+
});
|
|
1979
|
+
} catch { /* ledger append is best-effort */ }
|
|
1980
|
+
|
|
1759
1981
|
return {
|
|
1760
1982
|
success: true,
|
|
1761
1983
|
meshId,
|