@adhdev/daemon-core 0.9.82-rc.160 → 0.9.82-rc.162
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-adapter-types.d.ts +14 -1
- package/dist/commands/mesh-coordinator.d.ts +72 -1
- package/dist/config/chat-history.d.ts +2 -0
- package/dist/config/mesh-config.d.ts +3 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +4924 -1411
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +4976 -1476
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/coordinator-prompt.d.ts +30 -0
- package/dist/mesh/coordinator-registry.d.ts +35 -1
- package/dist/providers/cli-provider-instance.d.ts +1 -1
- package/dist/providers/contracts.d.ts +48 -0
- package/dist/providers/native-history/antigravity-cli-transcript.d.ts +1 -1
- package/dist/providers/native-history/claude-cli-transcript.d.ts +1 -1
- package/dist/providers/native-history/codex-cli-transcript.d.ts +1 -1
- package/dist/providers/native-history/dispatcher.d.ts +24 -0
- package/dist/providers/native-history/hermes-cli-transcript.d.ts +30 -0
- package/dist/providers/native-history/index.d.ts +2 -0
- package/dist/providers/spec/adapter.d.ts +56 -0
- package/dist/providers/spec/cli-adapter.d.ts +76 -0
- package/dist/providers/spec/driver.d.ts +148 -0
- package/dist/providers/spec/evaluator.d.ts +47 -0
- package/dist/providers/spec/loader.d.ts +14 -0
- package/dist/providers/spec/native-history-executor.d.ts +39 -0
- package/dist/providers/spec/route.d.ts +4 -0
- package/dist/providers/spec/schema.gen.d.ts +507 -0
- package/dist/providers/spec/types.d.ts +211 -0
- package/dist/repo-mesh-types.d.ts +33 -1
- package/dist/sessions/registry.d.ts +3 -0
- package/package.json +2 -1
- package/src/cli-adapter-types.ts +15 -1
- package/src/commands/chat-commands.ts +150 -12
- package/src/commands/cli-manager.ts +11 -0
- package/src/commands/mesh-coordinator.ts +235 -1
- package/src/commands/router.ts +238 -50
- package/src/config/chat-history.ts +11 -3
- package/src/config/mesh-config.ts +16 -1
- package/src/index.ts +19 -0
- package/src/mesh/coordinator-prompt.ts +164 -8
- package/src/mesh/coordinator-registry.ts +50 -4
- package/src/providers/cli-provider-instance.ts +8 -3
- package/src/providers/contracts.ts +53 -0
- package/src/providers/native-history/antigravity-cli-transcript.ts +2 -2
- package/src/providers/native-history/claude-cli-transcript.ts +1 -1
- package/src/providers/native-history/codex-cli-transcript.ts +1 -1
- package/src/providers/native-history/dispatcher.ts +227 -0
- package/src/providers/native-history/hermes-cli-transcript.ts +230 -0
- package/src/providers/native-history/index.ts +7 -0
- package/src/providers/provider-loader.ts +126 -3
- package/src/providers/sdk/v1/schemas/cli/provider.schema.json +13 -0
- package/src/providers/spec/adapter.ts +168 -0
- package/src/providers/spec/cli-adapter.ts +318 -0
- package/src/providers/spec/driver.ts +498 -0
- package/src/providers/spec/evaluator.ts +268 -0
- package/src/providers/spec/loader.ts +130 -0
- package/src/providers/spec/native-history-executor.ts +612 -0
- package/src/providers/spec/route.ts +51 -0
- package/src/providers/spec/schema.gen.ts +507 -0
- package/src/providers/spec/schema.json +210 -0
- package/src/providers/spec/types.ts +230 -0
- package/src/repo-mesh-types.ts +33 -1
- package/src/sessions/registry.ts +3 -0
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto'
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
|
|
2
3
|
import * as os from 'node:os'
|
|
3
4
|
import { isAbsolute, join, resolve } from 'node:path'
|
|
4
|
-
import
|
|
5
|
+
import { LOG } from '../logging/logger.js'
|
|
6
|
+
import type {
|
|
7
|
+
MeshCoordinatorMcpConfigFormat,
|
|
8
|
+
MeshCoordinatorSystemPromptInjection,
|
|
9
|
+
ProviderModule,
|
|
10
|
+
} from '../providers/contracts.js'
|
|
5
11
|
|
|
6
12
|
export interface MeshCoordinatorMcpServerLaunch {
|
|
7
13
|
command: string
|
|
@@ -249,3 +255,231 @@ function resolveMcpPort(explicitPort?: number): number | undefined {
|
|
|
249
255
|
const parsed = Number(raw)
|
|
250
256
|
return Number.isInteger(parsed) && parsed > 0 ? parsed : undefined
|
|
251
257
|
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Apply a provider's declared system-prompt injection rule, mutating `cliArgs`
|
|
261
|
+
* and `launchEnv` in place and writing any required workspace context file.
|
|
262
|
+
*
|
|
263
|
+
* Replaces the previous hard-coded `if (cliType === 'claude-cli') ... else if
|
|
264
|
+
* (cliType === 'hermes-cli') ...` branches in router.ts. Adding a new CLI is
|
|
265
|
+
* now provider.v1.json data, not a router edit.
|
|
266
|
+
*
|
|
267
|
+
* Failures are non-fatal: log and continue with whatever was applied so the
|
|
268
|
+
* coordinator session still launches, just without the prompt for that
|
|
269
|
+
* provider. A missing/unknown rule means "skip injection" — safe by default
|
|
270
|
+
* (the previous fallback unconditionally pushed --append-system-prompt onto
|
|
271
|
+
* every non-Claude CLI, which crashed agy on launch).
|
|
272
|
+
*/
|
|
273
|
+
export interface CoordinatorInjectionEffect {
|
|
274
|
+
/** Absolute path the daemon wrote a wrapper-blocked file to. Only set for
|
|
275
|
+
* context_file injection. R48 schedules a strip after launch so workers
|
|
276
|
+
* don't see the wrapper on disk; R47's unregister cleanup uses it as a
|
|
277
|
+
* fallback if the timer never fires (process crash, etc). */
|
|
278
|
+
contextFilePath?: string
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export function applyMeshCoordinatorSystemPromptInjection(
|
|
282
|
+
systemPrompt: string,
|
|
283
|
+
injection: MeshCoordinatorSystemPromptInjection | undefined,
|
|
284
|
+
ctx: { cliArgs: string[]; launchEnv: Record<string, string>; workspace: string; cliType: string },
|
|
285
|
+
): CoordinatorInjectionEffect {
|
|
286
|
+
if (!systemPrompt || !injection) return {}
|
|
287
|
+
return applyInjectionRule(systemPrompt, injection, ctx)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function applyInjectionRule(
|
|
291
|
+
systemPrompt: string,
|
|
292
|
+
injection: MeshCoordinatorSystemPromptInjection,
|
|
293
|
+
ctx: { cliArgs: string[]; launchEnv: Record<string, string>; workspace: string; cliType: string },
|
|
294
|
+
): CoordinatorInjectionEffect {
|
|
295
|
+
switch (injection.mode) {
|
|
296
|
+
case 'cli_arg': {
|
|
297
|
+
if (!injection.flag) return {}
|
|
298
|
+
ctx.cliArgs.push(injection.flag, systemPrompt)
|
|
299
|
+
return {}
|
|
300
|
+
}
|
|
301
|
+
case 'config_override': {
|
|
302
|
+
if (!injection.flag || !injection.template) return {}
|
|
303
|
+
const rendered = injection.template
|
|
304
|
+
.replace(/\{prompt_json\}/g, JSON.stringify(systemPrompt))
|
|
305
|
+
.replace(/\{prompt\}/g, systemPrompt)
|
|
306
|
+
ctx.cliArgs.push(injection.flag, rendered)
|
|
307
|
+
return {}
|
|
308
|
+
}
|
|
309
|
+
case 'env_var': {
|
|
310
|
+
if (!injection.name) return {}
|
|
311
|
+
ctx.launchEnv[injection.name] = systemPrompt
|
|
312
|
+
return {}
|
|
313
|
+
}
|
|
314
|
+
case 'context_file': {
|
|
315
|
+
if (!injection.path) return {}
|
|
316
|
+
const target = isAbsolute(injection.path)
|
|
317
|
+
? injection.path
|
|
318
|
+
: join(ctx.workspace, injection.path)
|
|
319
|
+
const wrapper = injection.wrapper && injection.wrapper.includes('{prompt}')
|
|
320
|
+
? injection.wrapper
|
|
321
|
+
: '{prompt}'
|
|
322
|
+
// Prepend a short managed-by hint inside the wrapper block so a user
|
|
323
|
+
// opening AGENTS.md / GEMINI.md immediately understands the block is
|
|
324
|
+
// auto-regenerated by the daemon on every coordinator launch. The hint
|
|
325
|
+
// sits between the opening sentinel and the prompt body, so the stable
|
|
326
|
+
// sentinels declared in provider.v1.json are untouched and the
|
|
327
|
+
// idempotent replace regex above still matches on relaunches.
|
|
328
|
+
const managedNote =
|
|
329
|
+
'> _Managed by adhdev mesh coordinator — do not hand-edit this block. ' +
|
|
330
|
+
'Changes inside the sentinels are overwritten on next coordinator launch._'
|
|
331
|
+
const promptWithNote = `${managedNote}\n\n${systemPrompt}`
|
|
332
|
+
const rendered = wrapper.replace(/\{prompt\}/g, promptWithNote)
|
|
333
|
+
// If the wrapper has a stable opening sentinel, treat everything up to
|
|
334
|
+
// the matching closing sentinel as our previously-written block and
|
|
335
|
+
// replace it. Otherwise just append. The marker is the first non-
|
|
336
|
+
// placeholder line of the wrapper; this keeps relaunches idempotent
|
|
337
|
+
// without forcing spec authors to declare an explicit marker.
|
|
338
|
+
const sentinel = wrapper.split('{prompt}')[0].trim()
|
|
339
|
+
try {
|
|
340
|
+
if (existsSync(target)) {
|
|
341
|
+
const existing = readFileSync(target, 'utf-8')
|
|
342
|
+
if (sentinel && existing.includes(sentinel)) {
|
|
343
|
+
const closing = wrapper.split('{prompt}')[1]?.trim()
|
|
344
|
+
const safeOpen = sentinel.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
345
|
+
const safeClose = closing
|
|
346
|
+
? closing.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
347
|
+
: ''
|
|
348
|
+
const re = closing
|
|
349
|
+
? new RegExp(`${safeOpen}[\\s\\S]*?${safeClose}`, 'g')
|
|
350
|
+
: new RegExp(`${safeOpen}[\\s\\S]*$`, 'g')
|
|
351
|
+
writeFileSync(target, existing.replace(re, rendered), 'utf-8')
|
|
352
|
+
} else {
|
|
353
|
+
writeFileSync(target, `${existing}\n\n${rendered}`, 'utf-8')
|
|
354
|
+
}
|
|
355
|
+
} else {
|
|
356
|
+
writeFileSync(target, rendered, 'utf-8')
|
|
357
|
+
}
|
|
358
|
+
LOG.info('MeshCoordinator', `Wrote coordinator prompt to ${target} (${ctx.cliType})`)
|
|
359
|
+
return { contextFilePath: target }
|
|
360
|
+
} catch (error: any) {
|
|
361
|
+
LOG.warn('MeshCoordinator', `Could not write ${target}: ${error?.message || error}`)
|
|
362
|
+
return {}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
default:
|
|
366
|
+
// Unknown future mode — skip silently. Adding the new mode is a spec-
|
|
367
|
+
// language extension, not a runtime crash.
|
|
368
|
+
return {}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Strip the daemon's wrapper block from a context_file we previously wrote.
|
|
374
|
+
*
|
|
375
|
+
* Used in two places:
|
|
376
|
+
* 1. R48 inject-then-remove: ~5s after spawn, after agy/gemini have read
|
|
377
|
+
* the file into their in-memory system-prompt cache. Removing it from
|
|
378
|
+
* disk at that point doesn't affect the running coordinator but keeps
|
|
379
|
+
* worker sessions (or fresh non-coordinator launches) in the same
|
|
380
|
+
* workspace from picking up our wrapper.
|
|
381
|
+
* 2. R47 unregister fallback: if the timer never fires (process crash,
|
|
382
|
+
* kill -9), coordinator-registry.unregisterMeshCoordinator runs the
|
|
383
|
+
* same logic when its entry is dropped.
|
|
384
|
+
*
|
|
385
|
+
* Idempotent: missing file is fine, missing sentinels are fine, returns
|
|
386
|
+
* silently. Leaves user-authored content outside the sentinels intact.
|
|
387
|
+
* Deletes the file outright if our wrapper was the only content.
|
|
388
|
+
*/
|
|
389
|
+
export function stripCoordinatorWrapperFile(filePath: string): void {
|
|
390
|
+
const OPEN = '<!-- adhdev-mesh-coordinator-prompt -->'
|
|
391
|
+
const CLOSE = '<!-- /adhdev-mesh-coordinator-prompt -->'
|
|
392
|
+
try {
|
|
393
|
+
if (!existsSync(filePath)) return
|
|
394
|
+
const existing = readFileSync(filePath, 'utf-8')
|
|
395
|
+
const openIdx = existing.indexOf(OPEN)
|
|
396
|
+
if (openIdx < 0) return
|
|
397
|
+
const closeIdx = existing.indexOf(CLOSE, openIdx)
|
|
398
|
+
if (closeIdx < 0) return
|
|
399
|
+
const remaining = (existing.slice(0, openIdx) + existing.slice(closeIdx + CLOSE.length))
|
|
400
|
+
.replace(/^\s*\n+/, '')
|
|
401
|
+
.replace(/\n+\s*$/, '')
|
|
402
|
+
if (!remaining.trim()) {
|
|
403
|
+
try {
|
|
404
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
405
|
+
const fs = require('node:fs')
|
|
406
|
+
fs.unlinkSync(filePath)
|
|
407
|
+
} catch { /* best-effort */ }
|
|
408
|
+
} else {
|
|
409
|
+
writeFileSync(filePath, remaining + '\n', 'utf-8')
|
|
410
|
+
}
|
|
411
|
+
} catch { /* best-effort */ }
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
export interface PtyExecResult {
|
|
415
|
+
exitCode: number | null
|
|
416
|
+
signal: number | null
|
|
417
|
+
output: string
|
|
418
|
+
timedOut: boolean
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Run a one-shot CLI command under a real PTY and collect its output.
|
|
423
|
+
*
|
|
424
|
+
* Some provider mcp-registration commands (`agy mcp add`, future bubbletea
|
|
425
|
+
* TUIs) refuse to run without `/dev/tty`. Daemon-side `execFileSync` runs
|
|
426
|
+
* pipe-only, so those commands fail with errors like
|
|
427
|
+
* `bubbletea: error opening TTY: open /dev/tty: device not configured`
|
|
428
|
+
* and silently skip the registration, leaving the launched session
|
|
429
|
+
* without the adhdev-mesh MCP tools — which is exactly what we saw with
|
|
430
|
+
* agy coordinators.
|
|
431
|
+
*
|
|
432
|
+
* Wrapping the registration through node-pty gives the child a real PTY,
|
|
433
|
+
* so the bubbletea check passes. We close stdin immediately and just
|
|
434
|
+
* collect stdout until the process exits or the timeout fires.
|
|
435
|
+
*/
|
|
436
|
+
export async function execUnderPty(
|
|
437
|
+
command: string,
|
|
438
|
+
args: string[],
|
|
439
|
+
options: { cwd?: string; env?: Record<string, string>; timeoutMs?: number } = {},
|
|
440
|
+
): Promise<PtyExecResult> {
|
|
441
|
+
let ptyLib: any
|
|
442
|
+
try {
|
|
443
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
444
|
+
ptyLib = require('node-pty')
|
|
445
|
+
} catch (error: any) {
|
|
446
|
+
throw new Error(`node-pty is not available: ${error?.message || error}`)
|
|
447
|
+
}
|
|
448
|
+
const env = { ...(options.env ?? (process.env as Record<string, string>)), TERM: 'xterm-256color' }
|
|
449
|
+
const timeoutMs = typeof options.timeoutMs === 'number' && options.timeoutMs > 0 ? options.timeoutMs : 20_000
|
|
450
|
+
return new Promise<PtyExecResult>((resolveResult) => {
|
|
451
|
+
let child: any
|
|
452
|
+
try {
|
|
453
|
+
child = ptyLib.spawn(command, args, {
|
|
454
|
+
name: 'xterm-256color',
|
|
455
|
+
cols: 120,
|
|
456
|
+
rows: 30,
|
|
457
|
+
cwd: options.cwd ?? process.cwd(),
|
|
458
|
+
env,
|
|
459
|
+
})
|
|
460
|
+
} catch (error: any) {
|
|
461
|
+
resolveResult({ exitCode: null, signal: null, output: String(error?.message || error), timedOut: false })
|
|
462
|
+
return
|
|
463
|
+
}
|
|
464
|
+
let buffer = ''
|
|
465
|
+
let settled = false
|
|
466
|
+
const timer = setTimeout(() => {
|
|
467
|
+
if (settled) return
|
|
468
|
+
settled = true
|
|
469
|
+
try { child.kill() } catch { /* ignore */ }
|
|
470
|
+
resolveResult({ exitCode: null, signal: null, output: buffer, timedOut: true })
|
|
471
|
+
}, timeoutMs)
|
|
472
|
+
child.onData((chunk: string) => {
|
|
473
|
+
buffer += chunk
|
|
474
|
+
if (buffer.length > 256 * 1024) {
|
|
475
|
+
buffer = buffer.slice(-128 * 1024)
|
|
476
|
+
}
|
|
477
|
+
})
|
|
478
|
+
child.onExit(({ exitCode, signal }: { exitCode: number; signal?: number }) => {
|
|
479
|
+
if (settled) return
|
|
480
|
+
settled = true
|
|
481
|
+
clearTimeout(timer)
|
|
482
|
+
resolveResult({ exitCode, signal: signal ?? null, output: buffer, timedOut: false })
|
|
483
|
+
})
|
|
484
|
+
})
|
|
485
|
+
}
|
package/src/commands/router.ts
CHANGED
|
@@ -38,7 +38,7 @@ import { createInteractionId, getRecentDebugTrace, recordDebugTrace } from '../l
|
|
|
38
38
|
import { getSessionHostSurfaceKind, partitionSessionHostRecords } from '../session-host/runtime-surface.js';
|
|
39
39
|
import { createHermesManualMeshCoordinatorSetup, resolveMeshCoordinatorSetup } from './mesh-coordinator.js';
|
|
40
40
|
import { buildSessionEntries } from '../status/builders.js';
|
|
41
|
-
import { registerMeshCoordinator } from '../mesh/coordinator-registry.js';
|
|
41
|
+
import { registerMeshCoordinator, getCoordinatorForSession } from '../mesh/coordinator-registry.js';
|
|
42
42
|
import { handleMeshForwardEvent, drainPendingMeshCoordinatorEvents, getPendingMeshCoordinatorEvents, queuePendingMeshCoordinatorEvent } from '../mesh/mesh-events.js';
|
|
43
43
|
import { buildMeshHostRequiredFailure, normalizeMeshDaemonRole, resolveMeshHostStatus } from '../mesh/mesh-host-ownership.js';
|
|
44
44
|
import { fastForwardMeshNode } from '../mesh/mesh-fast-forward.js';
|
|
@@ -4077,6 +4077,116 @@ export class DaemonCommandRouter {
|
|
|
4077
4077
|
};
|
|
4078
4078
|
}
|
|
4079
4079
|
|
|
4080
|
+
// Session-info popup data. Aggregates whatever the daemon knows
|
|
4081
|
+
// about a single live session into one envelope so the dashboard
|
|
4082
|
+
// doesn't need to stitch together status + coordinator registry +
|
|
4083
|
+
// session registry on the client. Includes the actual system
|
|
4084
|
+
// prompt that was injected at launch when the session is a mesh
|
|
4085
|
+
// coordinator — that's the "what prompt did the agent see?"
|
|
4086
|
+
// question the info-icon dialog is meant to answer.
|
|
4087
|
+
case 'get_session_info': {
|
|
4088
|
+
const sessionId = typeof args?.targetSessionId === 'string' ? args.targetSessionId.trim()
|
|
4089
|
+
: typeof args?.sessionId === 'string' ? args.sessionId.trim() : '';
|
|
4090
|
+
if (!sessionId) return { success: false, error: 'targetSessionId required' };
|
|
4091
|
+
const target = this.deps.sessionRegistry.get(sessionId);
|
|
4092
|
+
if (!target) return { success: false, error: 'Session not found', sessionId };
|
|
4093
|
+
const adapter = this.deps.cliManager.findAdapter(target.providerType, { instanceKey: sessionId })?.adapter;
|
|
4094
|
+
const runtimeMeta = (adapter && typeof (adapter as any).getRuntimeMetadata === 'function')
|
|
4095
|
+
? (adapter as any).getRuntimeMetadata()
|
|
4096
|
+
: undefined;
|
|
4097
|
+
const coord = getCoordinatorForSession(sessionId);
|
|
4098
|
+
const providerMetaForSession = this.deps.providerLoader.resolve?.(target.providerType) || this.deps.providerLoader.getMeta(target.providerType);
|
|
4099
|
+
return {
|
|
4100
|
+
success: true,
|
|
4101
|
+
session: {
|
|
4102
|
+
sessionId,
|
|
4103
|
+
providerType: target.providerType,
|
|
4104
|
+
providerName: providerMetaForSession?.name,
|
|
4105
|
+
transport: target.transport,
|
|
4106
|
+
workspace: (target as any).workspace,
|
|
4107
|
+
spawnedAtMs: (target as any).spawnedAtMs,
|
|
4108
|
+
providerSessionId: (target as any).providerSessionId,
|
|
4109
|
+
runtimeMetadata: runtimeMeta,
|
|
4110
|
+
},
|
|
4111
|
+
coordinator: coord ? {
|
|
4112
|
+
meshId: coord.meshId,
|
|
4113
|
+
startedAt: coord.startedAt,
|
|
4114
|
+
cliType: coord.cliType,
|
|
4115
|
+
systemPrompt: coord.systemPrompt,
|
|
4116
|
+
extraSystemPrompt: coord.extraSystemPrompt,
|
|
4117
|
+
injection: coord.injection,
|
|
4118
|
+
mcpConfigPath: coord.mcpConfigPath,
|
|
4119
|
+
} : null,
|
|
4120
|
+
};
|
|
4121
|
+
}
|
|
4122
|
+
|
|
4123
|
+
// ── User-level coordinator-prompt files (~/.adhdev/coordinator-prompts/).
|
|
4124
|
+
// These live on this daemon's filesystem and never sync to the
|
|
4125
|
+
// cloud / other daemons — they're per-machine config. The
|
|
4126
|
+
// Settings page in the dashboard reads/writes via these two
|
|
4127
|
+
// commands instead of going through fs from the browser.
|
|
4128
|
+
case 'list_coordinator_prompts': {
|
|
4129
|
+
const fs = await import('node:fs');
|
|
4130
|
+
const path = await import('node:path');
|
|
4131
|
+
const os = await import('node:os');
|
|
4132
|
+
const dir = path.join(os.homedir(), '.adhdev', 'coordinator-prompts');
|
|
4133
|
+
const entries: Record<string, { override: string; append: string }> = {};
|
|
4134
|
+
try {
|
|
4135
|
+
if (fs.existsSync(dir)) {
|
|
4136
|
+
for (const name of fs.readdirSync(dir)) {
|
|
4137
|
+
// Bucket files into <key>.{md|append.md}; ignore others
|
|
4138
|
+
// so a stray README or .DS_Store doesn't show up.
|
|
4139
|
+
const matchOverride = name.match(/^([a-zA-Z0-9_.-]+)\.md$/);
|
|
4140
|
+
const matchAppend = name.match(/^([a-zA-Z0-9_.-]+)\.append\.md$/);
|
|
4141
|
+
// append-pattern wins when both match (file is `.append.md`).
|
|
4142
|
+
const m = matchAppend || matchOverride;
|
|
4143
|
+
if (!m) continue;
|
|
4144
|
+
const isAppend = !!matchAppend;
|
|
4145
|
+
const key = m[1];
|
|
4146
|
+
const full = path.join(dir, name);
|
|
4147
|
+
let content = '';
|
|
4148
|
+
try { content = fs.readFileSync(full, 'utf8'); } catch { /* skip */ }
|
|
4149
|
+
if (!entries[key]) entries[key] = { override: '', append: '' };
|
|
4150
|
+
if (isAppend) entries[key].append = content;
|
|
4151
|
+
else entries[key].override = content;
|
|
4152
|
+
}
|
|
4153
|
+
}
|
|
4154
|
+
} catch (error: any) {
|
|
4155
|
+
return { success: false, error: error?.message || String(error) };
|
|
4156
|
+
}
|
|
4157
|
+
return { success: true, dir, entries };
|
|
4158
|
+
}
|
|
4159
|
+
|
|
4160
|
+
case 'write_coordinator_prompt': {
|
|
4161
|
+
const fs = await import('node:fs');
|
|
4162
|
+
const path = await import('node:path');
|
|
4163
|
+
const os = await import('node:os');
|
|
4164
|
+
const key = typeof args?.key === 'string' ? args.key.trim() : '';
|
|
4165
|
+
const kind = args?.kind === 'append' ? 'append' : 'override';
|
|
4166
|
+
const content = typeof args?.content === 'string' ? args.content : '';
|
|
4167
|
+
// Whitelist key chars so a malicious caller can't write
|
|
4168
|
+
// ../../etc/passwd. Same charset readUserPromptFile accepts.
|
|
4169
|
+
if (!key || !/^[a-zA-Z0-9_.-]+$/.test(key)) {
|
|
4170
|
+
return { success: false, error: 'key must match [a-zA-Z0-9_.-]+' };
|
|
4171
|
+
}
|
|
4172
|
+
const dir = path.join(os.homedir(), '.adhdev', 'coordinator-prompts');
|
|
4173
|
+
const filename = kind === 'append' ? `${key}.append.md` : `${key}.md`;
|
|
4174
|
+
const full = path.join(dir, filename);
|
|
4175
|
+
try {
|
|
4176
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
4177
|
+
if (content.trim()) {
|
|
4178
|
+
fs.writeFileSync(full, content, { encoding: 'utf8', mode: 0o600 });
|
|
4179
|
+
} else if (fs.existsSync(full)) {
|
|
4180
|
+
// Empty content = "reset to default" — delete the file
|
|
4181
|
+
// so the daemon's readUserPromptFile path falls through.
|
|
4182
|
+
fs.unlinkSync(full);
|
|
4183
|
+
}
|
|
4184
|
+
return { success: true, path: full, kind, key };
|
|
4185
|
+
} catch (error: any) {
|
|
4186
|
+
return { success: false, error: error?.message || String(error) };
|
|
4187
|
+
}
|
|
4188
|
+
}
|
|
4189
|
+
|
|
4080
4190
|
case 'mark_session_seen': {
|
|
4081
4191
|
const sessionId = args?.sessionId;
|
|
4082
4192
|
if (!sessionId || typeof sessionId !== 'string') {
|
|
@@ -4743,7 +4853,14 @@ export class DaemonCommandRouter {
|
|
|
4743
4853
|
delete (policy as any).providerPriority;
|
|
4744
4854
|
}
|
|
4745
4855
|
}
|
|
4746
|
-
const
|
|
4856
|
+
const patch: Record<string, unknown> = { policy: policy as any };
|
|
4857
|
+
if (typeof args?.systemPrompt === 'string') {
|
|
4858
|
+
const trimmed = (args.systemPrompt as string).trim();
|
|
4859
|
+
patch.systemPrompt = trimmed || undefined;
|
|
4860
|
+
} else if (args?.systemPrompt === null) {
|
|
4861
|
+
patch.systemPrompt = undefined;
|
|
4862
|
+
}
|
|
4863
|
+
const node = updateNode(meshId, nodeId, patch as any);
|
|
4747
4864
|
if (!node) return { success: false, error: 'Mesh node not found' };
|
|
4748
4865
|
return { success: true, node };
|
|
4749
4866
|
} catch (e: any) {
|
|
@@ -5147,6 +5264,19 @@ export class DaemonCommandRouter {
|
|
|
5147
5264
|
case 'launch_mesh_coordinator': {
|
|
5148
5265
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
5149
5266
|
let cliType = typeof args?.cliType === 'string' ? args.cliType.trim() : '';
|
|
5267
|
+
// Optional per-launch system-prompt addition. Dashboard or API
|
|
5268
|
+
// callers (e.g. when spawning a mesh-node-specific coordinator)
|
|
5269
|
+
// can pass extra context that gets appended to the rendered
|
|
5270
|
+
// default prompt under the "## Additional Context" section.
|
|
5271
|
+
// Going through buildCoordinatorSystemPrompt's userInstruction
|
|
5272
|
+
// means user-level override files (~/.adhdev/coordinator-prompts)
|
|
5273
|
+
// and this per-launch addition compose cleanly: an override
|
|
5274
|
+
// wins outright, but if there's no override, the default
|
|
5275
|
+
// prompt + the optional append.md file + this extra context
|
|
5276
|
+
// all stack in declared order.
|
|
5277
|
+
const extraSystemPrompt = typeof args?.extraSystemPrompt === 'string'
|
|
5278
|
+
? args.extraSystemPrompt.trim()
|
|
5279
|
+
: '';
|
|
5150
5280
|
if (!meshId) return { success: false, error: 'meshId required' };
|
|
5151
5281
|
|
|
5152
5282
|
try {
|
|
@@ -5257,7 +5387,7 @@ export class DaemonCommandRouter {
|
|
|
5257
5387
|
// Build coordinator prompt first — fail closed on errors.
|
|
5258
5388
|
let cliCmdSystemPrompt = '';
|
|
5259
5389
|
try {
|
|
5260
|
-
cliCmdSystemPrompt = buildCoordinatorSystemPrompt({ mesh, coordinatorCliType: cliType });
|
|
5390
|
+
cliCmdSystemPrompt = buildCoordinatorSystemPrompt({ mesh, coordinatorCliType: cliType, userInstruction: extraSystemPrompt || undefined });
|
|
5261
5391
|
} catch (error: any) {
|
|
5262
5392
|
const message = error?.message || String(error);
|
|
5263
5393
|
LOG.error('MeshCoordinator', `Failed to build coordinator prompt: ${message}`);
|
|
@@ -5269,53 +5399,45 @@ export class DaemonCommandRouter {
|
|
|
5269
5399
|
};
|
|
5270
5400
|
}
|
|
5271
5401
|
|
|
5272
|
-
// Run the provider's MCP registration command
|
|
5402
|
+
// Run the provider's MCP registration command under a
|
|
5403
|
+
// PTY. Some providers (agy, future bubbletea CLIs)
|
|
5404
|
+
// refuse to run without /dev/tty, so pipe-only
|
|
5405
|
+
// execFileSync silently no-ops the registration and
|
|
5406
|
+
// the coordinator ends up without any mcp tools. With
|
|
5407
|
+
// a real PTY the registration goes through and the
|
|
5408
|
+
// exit code tells us whether it actually persisted.
|
|
5409
|
+
let mcpRegistrationOk = false;
|
|
5273
5410
|
try {
|
|
5274
|
-
const {
|
|
5411
|
+
const { execUnderPty } = await import('./mesh-coordinator.js');
|
|
5275
5412
|
const cmdParts = coordinatorSetup.command.trim().split(/\s+/);
|
|
5276
5413
|
const [regCmd, ...regArgs] = cmdParts;
|
|
5277
|
-
LOG.info('MeshCoordinator', `Running MCP registration: ${coordinatorSetup.command}`);
|
|
5278
|
-
|
|
5414
|
+
LOG.info('MeshCoordinator', `Running MCP registration (pty): ${coordinatorSetup.command}`);
|
|
5415
|
+
const ptyResult = await execUnderPty(regCmd, regArgs, { cwd: workspace, timeoutMs: 20_000 });
|
|
5416
|
+
if (ptyResult.timedOut) {
|
|
5417
|
+
LOG.warn('MeshCoordinator', `MCP registration timed out — last output:\n${ptyResult.output.slice(-2000)}`);
|
|
5418
|
+
} else if (ptyResult.exitCode === 0) {
|
|
5419
|
+
mcpRegistrationOk = true;
|
|
5420
|
+
LOG.info('MeshCoordinator', `MCP registration succeeded (exit=0)`);
|
|
5421
|
+
} else {
|
|
5422
|
+
// Non-fatal — many providers return non-zero on duplicate registration.
|
|
5423
|
+
LOG.warn('MeshCoordinator', `MCP registration exit=${ptyResult.exitCode} signal=${ptyResult.signal} — output:\n${ptyResult.output.slice(-2000)}`);
|
|
5424
|
+
}
|
|
5279
5425
|
} catch (error: any) {
|
|
5280
|
-
|
|
5281
|
-
LOG.warn('MeshCoordinator', `MCP registration command failed (may be pre-registered): ${error?.message || error}`);
|
|
5426
|
+
LOG.warn('MeshCoordinator', `MCP registration command failed: ${error?.message || error}`);
|
|
5282
5427
|
}
|
|
5283
5428
|
|
|
5284
|
-
// Inject system prompt
|
|
5285
|
-
// Codex: -c 'instructions="..."' CLI config override
|
|
5286
|
-
// Gemini: write GEMINI.md to workspace (auto-loaded as context)
|
|
5429
|
+
// Inject system prompt declaratively from provider.v1.json.
|
|
5287
5430
|
const cliCmdArgs: string[] = [];
|
|
5288
5431
|
const cliCmdEnv: Record<string, string> = {};
|
|
5432
|
+
let cliCmdContextFilePath: string | undefined;
|
|
5289
5433
|
if (cliCmdSystemPrompt) {
|
|
5290
|
-
|
|
5291
|
-
|
|
5292
|
-
|
|
5293
|
-
|
|
5294
|
-
|
|
5295
|
-
|
|
5296
|
-
|
|
5297
|
-
try {
|
|
5298
|
-
const { writeFileSync: wfs, existsSync: efs, readFileSync: rfs } = await import('node:fs');
|
|
5299
|
-
const geminiMdPath = `${workspace}/GEMINI.md`;
|
|
5300
|
-
const marker = '<!-- adhdev-mesh-coordinator-prompt -->';
|
|
5301
|
-
const markerEnd = '<!-- /adhdev-mesh-coordinator-prompt -->';
|
|
5302
|
-
const block = `${marker}\n${cliCmdSystemPrompt}\n${markerEnd}`;
|
|
5303
|
-
if (efs(geminiMdPath)) {
|
|
5304
|
-
const existing = rfs(geminiMdPath, 'utf-8');
|
|
5305
|
-
// Replace existing block or append
|
|
5306
|
-
const replaced = existing.replace(
|
|
5307
|
-
new RegExp(`${marker}[\\s\\S]*?${markerEnd}`, 'g'),
|
|
5308
|
-
block,
|
|
5309
|
-
);
|
|
5310
|
-
wfs(geminiMdPath, replaced.includes(marker) ? replaced : `${existing}\n\n${block}`);
|
|
5311
|
-
} else {
|
|
5312
|
-
wfs(geminiMdPath, block);
|
|
5313
|
-
}
|
|
5314
|
-
LOG.info('MeshCoordinator', `Wrote coordinator prompt to ${workspace}/GEMINI.md`);
|
|
5315
|
-
} catch (e: any) {
|
|
5316
|
-
LOG.warn('MeshCoordinator', `Could not write GEMINI.md: ${e?.message || e}`);
|
|
5317
|
-
}
|
|
5318
|
-
}
|
|
5434
|
+
const { applyMeshCoordinatorSystemPromptInjection } = await import('./mesh-coordinator.js');
|
|
5435
|
+
const effect = applyMeshCoordinatorSystemPromptInjection(
|
|
5436
|
+
cliCmdSystemPrompt,
|
|
5437
|
+
providerMeta?.meshCoordinator?.systemPromptInjection,
|
|
5438
|
+
{ cliArgs: cliCmdArgs, launchEnv: cliCmdEnv, workspace, cliType },
|
|
5439
|
+
);
|
|
5440
|
+
cliCmdContextFilePath = effect.contextFilePath;
|
|
5319
5441
|
}
|
|
5320
5442
|
|
|
5321
5443
|
const cliCmdLaunch: any = await this.deps.cliManager.handleCliCommand('launch_cli', {
|
|
@@ -5326,6 +5448,22 @@ export class DaemonCommandRouter {
|
|
|
5326
5448
|
settings: { meshCoordinatorFor: meshId },
|
|
5327
5449
|
});
|
|
5328
5450
|
|
|
5451
|
+
// R48 inject-then-remove. Spawn was just kicked off above; agy and
|
|
5452
|
+
// gemini-cli read AGENTS.md / GEMINI.md exactly once at startup and
|
|
5453
|
+
// cache it for the rest of the session, so we can safely strip
|
|
5454
|
+
// the wrapper from disk shortly after launch. That keeps any
|
|
5455
|
+
// worker session launched into the same workspace later from
|
|
5456
|
+
// picking up our wrapper block.
|
|
5457
|
+
if (cliCmdLaunch?.success && cliCmdContextFilePath) {
|
|
5458
|
+
const stripPath = cliCmdContextFilePath;
|
|
5459
|
+
setTimeout(() => {
|
|
5460
|
+
void import('./mesh-coordinator.js').then(({ stripCoordinatorWrapperFile }) => {
|
|
5461
|
+
stripCoordinatorWrapperFile(stripPath);
|
|
5462
|
+
LOG.info('MeshCoordinator', `Stripped wrapper from ${stripPath} after launch settle (cli_command)`);
|
|
5463
|
+
}).catch(() => { /* best-effort */ });
|
|
5464
|
+
}, 5000);
|
|
5465
|
+
}
|
|
5466
|
+
|
|
5329
5467
|
if (!cliCmdLaunch?.success) {
|
|
5330
5468
|
return { success: false, error: cliCmdLaunch?.error || 'Failed to launch CLI session' };
|
|
5331
5469
|
}
|
|
@@ -5333,7 +5471,23 @@ export class DaemonCommandRouter {
|
|
|
5333
5471
|
LOG.info('MeshCoordinator', `Launched ${cliType} coordinator (cli_command) for mesh ${meshId}`);
|
|
5334
5472
|
const cliCmdSessionId = cliCmdLaunch.sessionId || cliCmdLaunch.id;
|
|
5335
5473
|
if (cliCmdSessionId) {
|
|
5336
|
-
|
|
5474
|
+
const cliCmdInjectionDecl = providerMeta?.meshCoordinator?.systemPromptInjection;
|
|
5475
|
+
registerMeshCoordinator({
|
|
5476
|
+
meshId,
|
|
5477
|
+
sessionId: cliCmdSessionId,
|
|
5478
|
+
workspace,
|
|
5479
|
+
startedAt: Date.now(),
|
|
5480
|
+
cliType,
|
|
5481
|
+
systemPrompt: cliCmdSystemPrompt || undefined,
|
|
5482
|
+
extraSystemPrompt: extraSystemPrompt || undefined,
|
|
5483
|
+
injection: cliCmdInjectionDecl ? {
|
|
5484
|
+
mode: cliCmdInjectionDecl.mode,
|
|
5485
|
+
target: 'flag' in cliCmdInjectionDecl ? cliCmdInjectionDecl.flag
|
|
5486
|
+
: 'name' in cliCmdInjectionDecl ? cliCmdInjectionDecl.name
|
|
5487
|
+
: 'path' in cliCmdInjectionDecl ? cliCmdInjectionDecl.path
|
|
5488
|
+
: undefined,
|
|
5489
|
+
} : undefined,
|
|
5490
|
+
});
|
|
5337
5491
|
}
|
|
5338
5492
|
try {
|
|
5339
5493
|
const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
|
|
@@ -5351,7 +5505,7 @@ export class DaemonCommandRouter {
|
|
|
5351
5505
|
cliType,
|
|
5352
5506
|
workspace,
|
|
5353
5507
|
sessionId: cliCmdSessionId,
|
|
5354
|
-
mcpRegistered:
|
|
5508
|
+
mcpRegistered: mcpRegistrationOk,
|
|
5355
5509
|
};
|
|
5356
5510
|
}
|
|
5357
5511
|
|
|
@@ -5372,7 +5526,7 @@ export class DaemonCommandRouter {
|
|
|
5372
5526
|
// broken mesh state is visible instead of silently launching with weaker rules.
|
|
5373
5527
|
let systemPrompt = '';
|
|
5374
5528
|
try {
|
|
5375
|
-
systemPrompt = buildCoordinatorSystemPrompt({ mesh, coordinatorCliType: cliType });
|
|
5529
|
+
systemPrompt = buildCoordinatorSystemPrompt({ mesh, coordinatorCliType: cliType, userInstruction: extraSystemPrompt || undefined });
|
|
5376
5530
|
} catch (error: any) {
|
|
5377
5531
|
const message = error?.message || String(error);
|
|
5378
5532
|
LOG.error('MeshCoordinator', `Failed to build coordinator prompt: ${message}`);
|
|
@@ -5487,12 +5641,15 @@ export class DaemonCommandRouter {
|
|
|
5487
5641
|
launchEnv.HERMES_HOME = dirname(mcpConfigPath);
|
|
5488
5642
|
launchEnv.HERMES_IGNORE_USER_CONFIG = '';
|
|
5489
5643
|
}
|
|
5644
|
+
let autoImportContextFilePath: string | undefined;
|
|
5490
5645
|
if (systemPrompt) {
|
|
5491
|
-
|
|
5492
|
-
|
|
5493
|
-
|
|
5494
|
-
|
|
5495
|
-
|
|
5646
|
+
const { applyMeshCoordinatorSystemPromptInjection } = await import('./mesh-coordinator.js');
|
|
5647
|
+
const effect = applyMeshCoordinatorSystemPromptInjection(
|
|
5648
|
+
systemPrompt,
|
|
5649
|
+
providerMeta?.meshCoordinator?.systemPromptInjection,
|
|
5650
|
+
{ cliArgs, launchEnv, workspace, cliType },
|
|
5651
|
+
);
|
|
5652
|
+
autoImportContextFilePath = effect.contextFilePath;
|
|
5496
5653
|
}
|
|
5497
5654
|
if (cliType === 'claude-cli') {
|
|
5498
5655
|
cliArgs.push('--mcp-config', coordinatorSetup.configPath);
|
|
@@ -5511,6 +5668,20 @@ export class DaemonCommandRouter {
|
|
|
5511
5668
|
}
|
|
5512
5669
|
});
|
|
5513
5670
|
|
|
5671
|
+
// R48 inject-then-remove. See the cli_command branch for context;
|
|
5672
|
+
// same idea: strip the wrapper from disk ~5s after launch so the
|
|
5673
|
+
// user's AGENTS.md / GEMINI.md is untouched the moment any
|
|
5674
|
+
// worker session opens up in the same workspace.
|
|
5675
|
+
if (launchResult?.success && autoImportContextFilePath) {
|
|
5676
|
+
const stripPath = autoImportContextFilePath;
|
|
5677
|
+
setTimeout(() => {
|
|
5678
|
+
void import('./mesh-coordinator.js').then(({ stripCoordinatorWrapperFile }) => {
|
|
5679
|
+
stripCoordinatorWrapperFile(stripPath);
|
|
5680
|
+
LOG.info('MeshCoordinator', `Stripped wrapper from ${stripPath} after launch settle (auto_import)`);
|
|
5681
|
+
}).catch(() => { /* best-effort */ });
|
|
5682
|
+
}, 5000);
|
|
5683
|
+
}
|
|
5684
|
+
|
|
5514
5685
|
if (!launchResult?.success) {
|
|
5515
5686
|
return { success: false, error: launchResult?.error || 'Failed to launch CLI session' };
|
|
5516
5687
|
}
|
|
@@ -5518,7 +5689,24 @@ export class DaemonCommandRouter {
|
|
|
5518
5689
|
LOG.info('MeshCoordinator', `Launched ${cliType} coordinator for mesh ${meshId} in ${workspace}`);
|
|
5519
5690
|
const launchSessionId = launchResult.sessionId || launchResult.id;
|
|
5520
5691
|
if (launchSessionId) {
|
|
5521
|
-
|
|
5692
|
+
const autoImportInjectionDecl = providerMeta?.meshCoordinator?.systemPromptInjection;
|
|
5693
|
+
registerMeshCoordinator({
|
|
5694
|
+
meshId,
|
|
5695
|
+
sessionId: launchSessionId,
|
|
5696
|
+
workspace,
|
|
5697
|
+
startedAt: Date.now(),
|
|
5698
|
+
cliType,
|
|
5699
|
+
systemPrompt: systemPrompt || undefined,
|
|
5700
|
+
extraSystemPrompt: extraSystemPrompt || undefined,
|
|
5701
|
+
mcpConfigPath,
|
|
5702
|
+
injection: autoImportInjectionDecl ? {
|
|
5703
|
+
mode: autoImportInjectionDecl.mode,
|
|
5704
|
+
target: 'flag' in autoImportInjectionDecl ? autoImportInjectionDecl.flag
|
|
5705
|
+
: 'name' in autoImportInjectionDecl ? autoImportInjectionDecl.name
|
|
5706
|
+
: 'path' in autoImportInjectionDecl ? autoImportInjectionDecl.path
|
|
5707
|
+
: undefined,
|
|
5708
|
+
} : undefined,
|
|
5709
|
+
});
|
|
5522
5710
|
}
|
|
5523
5711
|
|
|
5524
5712
|
// Record coordinator launch in task ledger
|