@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.
Files changed (63) hide show
  1. package/dist/cli-adapter-types.d.ts +14 -1
  2. package/dist/commands/mesh-coordinator.d.ts +72 -1
  3. package/dist/config/chat-history.d.ts +2 -0
  4. package/dist/config/mesh-config.d.ts +3 -0
  5. package/dist/index.d.ts +11 -0
  6. package/dist/index.js +4924 -1411
  7. package/dist/index.js.map +1 -1
  8. package/dist/index.mjs +4976 -1476
  9. package/dist/index.mjs.map +1 -1
  10. package/dist/mesh/coordinator-prompt.d.ts +30 -0
  11. package/dist/mesh/coordinator-registry.d.ts +35 -1
  12. package/dist/providers/cli-provider-instance.d.ts +1 -1
  13. package/dist/providers/contracts.d.ts +48 -0
  14. package/dist/providers/native-history/antigravity-cli-transcript.d.ts +1 -1
  15. package/dist/providers/native-history/claude-cli-transcript.d.ts +1 -1
  16. package/dist/providers/native-history/codex-cli-transcript.d.ts +1 -1
  17. package/dist/providers/native-history/dispatcher.d.ts +24 -0
  18. package/dist/providers/native-history/hermes-cli-transcript.d.ts +30 -0
  19. package/dist/providers/native-history/index.d.ts +2 -0
  20. package/dist/providers/spec/adapter.d.ts +56 -0
  21. package/dist/providers/spec/cli-adapter.d.ts +76 -0
  22. package/dist/providers/spec/driver.d.ts +148 -0
  23. package/dist/providers/spec/evaluator.d.ts +47 -0
  24. package/dist/providers/spec/loader.d.ts +14 -0
  25. package/dist/providers/spec/native-history-executor.d.ts +39 -0
  26. package/dist/providers/spec/route.d.ts +4 -0
  27. package/dist/providers/spec/schema.gen.d.ts +507 -0
  28. package/dist/providers/spec/types.d.ts +211 -0
  29. package/dist/repo-mesh-types.d.ts +33 -1
  30. package/dist/sessions/registry.d.ts +3 -0
  31. package/package.json +2 -1
  32. package/src/cli-adapter-types.ts +15 -1
  33. package/src/commands/chat-commands.ts +150 -12
  34. package/src/commands/cli-manager.ts +11 -0
  35. package/src/commands/mesh-coordinator.ts +235 -1
  36. package/src/commands/router.ts +238 -50
  37. package/src/config/chat-history.ts +11 -3
  38. package/src/config/mesh-config.ts +16 -1
  39. package/src/index.ts +19 -0
  40. package/src/mesh/coordinator-prompt.ts +164 -8
  41. package/src/mesh/coordinator-registry.ts +50 -4
  42. package/src/providers/cli-provider-instance.ts +8 -3
  43. package/src/providers/contracts.ts +53 -0
  44. package/src/providers/native-history/antigravity-cli-transcript.ts +2 -2
  45. package/src/providers/native-history/claude-cli-transcript.ts +1 -1
  46. package/src/providers/native-history/codex-cli-transcript.ts +1 -1
  47. package/src/providers/native-history/dispatcher.ts +227 -0
  48. package/src/providers/native-history/hermes-cli-transcript.ts +230 -0
  49. package/src/providers/native-history/index.ts +7 -0
  50. package/src/providers/provider-loader.ts +126 -3
  51. package/src/providers/sdk/v1/schemas/cli/provider.schema.json +13 -0
  52. package/src/providers/spec/adapter.ts +168 -0
  53. package/src/providers/spec/cli-adapter.ts +318 -0
  54. package/src/providers/spec/driver.ts +498 -0
  55. package/src/providers/spec/evaluator.ts +268 -0
  56. package/src/providers/spec/loader.ts +130 -0
  57. package/src/providers/spec/native-history-executor.ts +612 -0
  58. package/src/providers/spec/route.ts +51 -0
  59. package/src/providers/spec/schema.gen.ts +507 -0
  60. package/src/providers/spec/schema.json +210 -0
  61. package/src/providers/spec/types.ts +230 -0
  62. package/src/repo-mesh-types.ts +33 -1
  63. package/src/sessions/registry.ts +3 -0
@@ -1421,6 +1421,8 @@ function callProviderNativeHistoryRead(
1421
1421
  historySessionId: string | undefined,
1422
1422
  workspace?: string,
1423
1423
  excludeInProgressTurn?: boolean,
1424
+ sessionStartedAtMs?: number,
1425
+ envOverrides?: Record<string, string>,
1424
1426
  ): ProviderNativeHistoryReadResult | null {
1425
1427
  const fn = getProviderNativeHistoryScript(scripts, canonicalHistory, 'readSession');
1426
1428
  if (!fn) return null;
@@ -1433,7 +1435,9 @@ function callProviderNativeHistoryRead(
1433
1435
  format: canonicalHistory?.format,
1434
1436
  watchPath: canonicalHistory?.watchPath,
1435
1437
  excludeInProgressTurn: excludeInProgressTurn === true,
1436
- args: { sessionId: normalizedSessionId, historySessionId: normalizedSessionId, workspace, excludeInProgressTurn: excludeInProgressTurn === true },
1438
+ sessionStartedAtMs,
1439
+ envOverrides,
1440
+ args: { sessionId: normalizedSessionId, historySessionId: normalizedSessionId, workspace, excludeInProgressTurn: excludeInProgressTurn === true, sessionStartedAtMs, envOverrides },
1437
1441
  });
1438
1442
  if (!result || typeof result !== 'object') return null;
1439
1443
  const records = normalizeProviderNativeHistoryRecords(agentType, normalizedSessionId, (result as any).messages || (result as any).records);
@@ -1456,11 +1460,13 @@ function buildNativeHistoryReadResult(
1456
1460
  historySessionId: string | undefined,
1457
1461
  workspace?: string,
1458
1462
  excludeInProgressTurn?: boolean,
1463
+ sessionStartedAtMs?: number,
1464
+ envOverrides?: Record<string, string>,
1459
1465
  ): ProviderNativeHistoryReadResult | null {
1460
1466
  const normalizedSessionId = normalizeSavedHistorySessionId(historySessionId || '');
1461
1467
  const normalizedWorkspace = typeof workspace === 'string' ? workspace.trim() : '';
1462
1468
  if (!canonicalHistory || (!normalizedSessionId && !normalizedWorkspace) || !isNativeSourceCanonicalHistory(canonicalHistory)) return null;
1463
- return callProviderNativeHistoryRead(agentType, canonicalHistory, scripts, normalizedSessionId, workspace, excludeInProgressTurn);
1469
+ return callProviderNativeHistoryRead(agentType, canonicalHistory, scripts, normalizedSessionId, workspace, excludeInProgressTurn, sessionStartedAtMs, envOverrides);
1464
1470
  }
1465
1471
 
1466
1472
  function materializeNativeHistoryToMirror(
@@ -1517,6 +1523,8 @@ export function readProviderChatHistory(
1517
1523
  historyBehavior?: ProviderHistoryBehavior;
1518
1524
  scripts?: ProviderNativeHistoryScripts;
1519
1525
  excludeInProgressTurn?: boolean;
1526
+ sessionStartedAtMs?: number;
1527
+ envOverrides?: Record<string, string>;
1520
1528
  } = {},
1521
1529
  ): {
1522
1530
  messages: HistoryMessage[];
@@ -1530,7 +1538,7 @@ export function readProviderChatHistory(
1530
1538
  unavailableReason?: string;
1531
1539
  } {
1532
1540
  if (isNativeSourceCanonicalHistory(options.canonicalHistory) && (options.historySessionId || options.workspace)) {
1533
- const nativeResult = buildNativeHistoryReadResult(agentType, options.canonicalHistory, options.scripts, options.historySessionId, options.workspace, options.excludeInProgressTurn);
1541
+ const nativeResult = buildNativeHistoryReadResult(agentType, options.canonicalHistory, options.scripts, options.historySessionId, options.workspace, options.excludeInProgressTurn, options.sessionStartedAtMs, options.envOverrides);
1534
1542
  if (!nativeResult) return { messages: [], hasMore: false, source: 'native-unavailable' };
1535
1543
  return {
1536
1544
  ...pageHistoryRecords(agentType, nativeResult.records, options.offset || 0, options.limit || 30, options.excludeRecentCount || 0, options.historyBehavior),
@@ -484,7 +484,14 @@ export function removeNode(meshId: string, nodeId: string): boolean {
484
484
  export function updateNode(
485
485
  meshId: string,
486
486
  nodeId: string,
487
- opts: { userOverrides?: Partial<RepoMeshNodeCapabilities>; policy?: RepoMeshNodePolicy; worktreeBootstrap?: LocalMeshNodeEntry['worktreeBootstrap'] },
487
+ opts: {
488
+ userOverrides?: Partial<RepoMeshNodeCapabilities>;
489
+ policy?: RepoMeshNodePolicy;
490
+ worktreeBootstrap?: LocalMeshNodeEntry['worktreeBootstrap'];
491
+ /** Per-node instruction surfaced in the coordinator prompt. Pass an
492
+ * empty string or undefined to clear it. */
493
+ systemPrompt?: string;
494
+ },
488
495
  ): LocalMeshNodeEntry | undefined {
489
496
  const config = loadMeshConfig();
490
497
  const mesh = config.meshes.find(m => m.id === meshId);
@@ -496,6 +503,14 @@ export function updateNode(
496
503
  if (opts.userOverrides) node.userOverrides = { ...node.userOverrides, ...opts.userOverrides };
497
504
  if (opts.policy) node.policy = { ...node.policy, ...opts.policy };
498
505
  if (opts.worktreeBootstrap) node.worktreeBootstrap = opts.worktreeBootstrap;
506
+ if (Object.prototype.hasOwnProperty.call(opts, 'systemPrompt')) {
507
+ // Honor explicit clears: { systemPrompt: undefined } drops the field.
508
+ if (opts.systemPrompt && opts.systemPrompt.trim()) {
509
+ node.systemPrompt = opts.systemPrompt;
510
+ } else {
511
+ delete node.systemPrompt;
512
+ }
513
+ }
499
514
  mesh.updatedAt = new Date().toISOString();
500
515
  saveMeshConfig(config);
501
516
  return node;
package/src/index.ts CHANGED
@@ -467,6 +467,25 @@ export {
467
467
  type IpcStatusPayload,
468
468
  } from './ipc/local-ipc-server.js';
469
469
 
470
+ // ── CLI Spec (adhdev:cli/spec@1) ──
471
+ export { evaluate as evaluateSpec, evaluate } from './providers/spec/evaluator.js';
472
+ export { loadSpec, resolveSpecPath } from './providers/spec/loader.js';
473
+ export { createNativeHistoryDispatcher } from './providers/native-history/index.js';
474
+ export type { ReaderId } from './providers/native-history/index.js';
475
+ export {
476
+ readClaudeCliSession, readCodexCliSession,
477
+ readAntigravityCliSession, readHermesCliSession,
478
+ } from './providers/native-history/index.js';
479
+ export type {
480
+ CliSpec, SpecState, ControlAction, Control,
481
+ NotificationRule, DelegateTrigger, Section,
482
+ } from './providers/spec/types.js';
483
+ export type { SpecEvaluation, TraceEntry } from './providers/spec/evaluator.js';
484
+ export { SpecDriver } from './providers/spec/driver.js';
485
+ export type { DashboardEvent, DashboardCommand, SpecDriverOpts } from './providers/spec/driver.js';
486
+ export { TerminalAdapter } from './providers/spec/adapter.js';
487
+ export type { TerminalAdapterOpts, TerminalAdapterHandlers } from './providers/spec/adapter.js';
488
+
470
489
  // ── Provider SDK (v1) — selective re-exports for external tooling ──
471
490
  // Tooling (registry publish, dashboard validators, the e2e harness) needs
472
491
  // the manifest validator, the builder catalog, and the contract version.
@@ -8,8 +8,23 @@
8
8
  * 3. How to orchestrate work across nodes
9
9
  *
10
10
  * The prompt is generated dynamically from the current mesh state.
11
+ *
12
+ * User customization:
13
+ * ~/.adhdev/coordinator-prompts/<cliType>.md — full override
14
+ * ~/.adhdev/coordinator-prompts/<cliType>.append.md — appended to default
15
+ * ~/.adhdev/coordinator-prompts/default.md — full override (any CLI)
16
+ * ~/.adhdev/coordinator-prompts/default.append.md — appended to default (any CLI)
17
+ *
18
+ * CLI-specific files take precedence over default.* files. The override file
19
+ * still gets the node/policy facts substituted via the same {{placeholders}}
20
+ * the daemon understands; an override that doesn't reference them just gets
21
+ * a static prompt, which is also fine.
11
22
  */
12
23
 
24
+ import * as fs from 'node:fs';
25
+ import * as os from 'node:os';
26
+ import * as path from 'node:path';
27
+
13
28
  import type {
14
29
  LocalMeshEntry,
15
30
  RepoMeshPolicy,
@@ -27,8 +42,68 @@ export interface CoordinatorPromptContext {
27
42
  coordinatorCliType?: string;
28
43
  }
29
44
 
45
+ /**
46
+ * Compose the final coordinator prompt from four layers, in this precedence:
47
+ *
48
+ * 1. Per-launch `extraSystemPrompt` (always appended, as "## Additional
49
+ * Context"). Never wins as a base — it's launch-scope context.
50
+ * 2. Mesh-level append (`mesh.coordinator.systemPromptAppend` or the legacy
51
+ * `systemPromptSuffix`). Stacks after whichever base won.
52
+ * 3. User-file append (`~/.adhdev/coordinator-prompts/<cli>.append.md` or
53
+ * `default.append.md`). Also stacks; same placeholder expansion as the
54
+ * override path.
55
+ * 4. Base prompt, picked in this order:
56
+ * a. `mesh.coordinator.systemPromptOverride` (mesh-level override)
57
+ * b. user-file override (`~/.adhdev/coordinator-prompts/<cli>.md` or
58
+ * `default.md`)
59
+ * c. daemon default (assembled from identity/nodes/policy/tools/…)
60
+ *
61
+ * That layering lets a user customize prompts at three increasing scopes
62
+ * (machine, mesh, single launch) without losing the daemon's stock rules.
63
+ */
30
64
  export function buildCoordinatorSystemPrompt(ctx: CoordinatorPromptContext): string {
31
- const { mesh, status, userInstruction, coordinatorCliType } = ctx;
65
+ const { mesh, userInstruction, coordinatorCliType } = ctx;
66
+
67
+ // ── Pick the base prompt ──
68
+ const meshOverride = mesh.coordinator?.systemPromptOverride?.trim();
69
+ let base: string;
70
+ if (meshOverride) {
71
+ base = expandPromptPlaceholders(meshOverride, ctx);
72
+ } else {
73
+ const userOverride = readUserPromptFile(coordinatorCliType, 'md');
74
+ if (userOverride !== null) {
75
+ base = expandPromptPlaceholders(userOverride, ctx);
76
+ } else {
77
+ base = buildDefaultCoordinatorPrompt(ctx);
78
+ }
79
+ }
80
+
81
+ const sections: string[] = [base];
82
+
83
+ // ── User-level append runs after whichever base won ──
84
+ const userAppend = readUserPromptFile(coordinatorCliType, 'append.md');
85
+ if (userAppend !== null) {
86
+ sections.push(expandPromptPlaceholders(userAppend, ctx));
87
+ }
88
+
89
+ // ── Mesh-level append (prefer the new field, fall back to the legacy alias) ──
90
+ const meshAppend = (mesh.coordinator?.systemPromptAppend
91
+ ?? mesh.coordinator?.systemPromptSuffix)?.trim();
92
+ if (meshAppend) {
93
+ sections.push(expandPromptPlaceholders(meshAppend, ctx));
94
+ }
95
+
96
+ // ── Per-launch context lands last so it's the most recent thing the
97
+ // agent reads. Marked as Additional Context, not a rule update. ──
98
+ if (userInstruction) {
99
+ sections.push(`## Additional Context\n${userInstruction}`);
100
+ }
101
+
102
+ return sections.join('\n\n');
103
+ }
104
+
105
+ function buildDefaultCoordinatorPrompt(ctx: CoordinatorPromptContext): string {
106
+ const { mesh, status, coordinatorCliType } = ctx;
32
107
  const sections: string[] = [];
33
108
 
34
109
  // ── Identity ──
@@ -61,16 +136,77 @@ Repository: \`${mesh.repoIdentity}\`${mesh.defaultBranch ? `\nDefault branch: \`
61
136
  // ── Rules ──
62
137
  sections.push(buildRulesSection(coordinatorCliType));
63
138
 
64
- // ── User instruction ──
65
- if (userInstruction) {
66
- sections.push(`## Additional Context\n${userInstruction}`);
67
- }
139
+ return sections.join('\n\n');
140
+ }
68
141
 
69
- if (mesh.coordinator?.systemPromptSuffix) {
70
- sections.push(mesh.coordinator.systemPromptSuffix);
142
+ /**
143
+ * Look up a user-customization file under ~/.adhdev/coordinator-prompts/.
144
+ *
145
+ * Lookup order:
146
+ * 1. <cliType>.<suffix> — provider-specific
147
+ * 2. default.<suffix> — shared across providers
148
+ *
149
+ * Returns null when neither exists; an empty/whitespace-only file is also
150
+ * treated as "no override" so users can drop in a stub without affecting
151
+ * behavior. Read errors (permission, IO) are swallowed and logged-as-null
152
+ * intentionally: a broken override file should never block coordinator
153
+ * launch, it should just behave as if the file weren't there.
154
+ */
155
+ function readUserPromptFile(cliType: string | undefined, suffix: string): string | null {
156
+ const dir = path.join(os.homedir(), '.adhdev', 'coordinator-prompts');
157
+ const candidates: string[] = [];
158
+ if (cliType) candidates.push(path.join(dir, `${cliType}.${suffix}`));
159
+ candidates.push(path.join(dir, `default.${suffix}`));
160
+ for (const p of candidates) {
161
+ try {
162
+ const text = fs.readFileSync(p, 'utf8');
163
+ if (text.trim()) return text;
164
+ } catch { /* missing file is the common case — keep going */ }
71
165
  }
166
+ return null;
167
+ }
72
168
 
73
- return sections.join('\n\n');
169
+ /**
170
+ * Expand `{{placeholder}}` tokens against current mesh state.
171
+ *
172
+ * Tokens we support today:
173
+ * {{meshName}} — mesh.name
174
+ * {{repo}} — mesh.repoIdentity
175
+ * {{defaultBranch}} — mesh.defaultBranch or empty
176
+ * {{cliType}} — coordinator CLI type or empty
177
+ * {{nodes}} — full node section (status if known, otherwise config)
178
+ * {{policy}} — full policy section
179
+ * {{tools}} — the canonical tools table
180
+ * {{workflow}} — the canonical orchestration workflow
181
+ * {{rules}} — the canonical rules section (with coordinatorNote)
182
+ * {{toolExposurePreflight}} — the MCP-missing preflight reminder
183
+ *
184
+ * Unknown tokens are left as-is — that way typos are obvious in the rendered
185
+ * prompt instead of silently disappearing. Tokens are not recursive: an
186
+ * expanded value's own {{...}} content stays literal.
187
+ */
188
+ function expandPromptPlaceholders(template: string, ctx: CoordinatorPromptContext): string {
189
+ const { mesh, status, coordinatorCliType } = ctx;
190
+ const nodesSection = status?.nodes?.length
191
+ ? buildNodeStatusSection(status.nodes)
192
+ : mesh.nodes.length
193
+ ? buildNodeConfigSection(mesh)
194
+ : '## Nodes\nNo nodes configured yet. Ask the user to add nodes with `adhdev mesh add-node`.';
195
+ const replacements: Record<string, string> = {
196
+ meshName: mesh.name,
197
+ repo: mesh.repoIdentity,
198
+ defaultBranch: mesh.defaultBranch || '',
199
+ cliType: coordinatorCliType || '',
200
+ nodes: nodesSection,
201
+ policy: buildPolicySection({ ...DEFAULT_MESH_POLICY, ...(mesh.policy || {}) }),
202
+ tools: TOOLS_SECTION,
203
+ workflow: WORKFLOW_SECTION,
204
+ rules: buildRulesSection(coordinatorCliType),
205
+ toolExposurePreflight: TOOL_EXPOSURE_PREFLIGHT_SECTION,
206
+ };
207
+ return template.replace(/\{\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}/g, (m, key) => {
208
+ return Object.prototype.hasOwnProperty.call(replacements, key) ? replacements[key] : m;
209
+ });
74
210
  }
75
211
 
76
212
  // ─── Section Builders ───────────────────────────
@@ -97,6 +233,10 @@ function buildNodeStatusSection(nodes: RepoMeshNodeStatus[]): string {
97
233
  lines.push(`- ${healthIcon} **${n.machineLabel}** (nodeId: \`${n.nodeId}\`)`);
98
234
  lines.push(` workspace: \`${n.workspace}\`${context ? ` | ${context}` : ''} | ${branch} | ${sessions}`);
99
235
  if (n.error) lines.push(` ⚠️ ${n.error}`);
236
+ const nodePrompt = typeof (n as any).systemPrompt === 'string' ? (n as any).systemPrompt.trim() : '';
237
+ if (nodePrompt) {
238
+ lines.push(` 📌 Node instruction: ${indentFollowing(nodePrompt, ' ')}`);
239
+ }
100
240
  }
101
241
  return lines.join('\n');
102
242
  }
@@ -117,11 +257,26 @@ function buildNodeConfigSection(mesh: LocalMeshEntry): string {
117
257
  const explicitLabel = explicitMachineLabel ? ` label: **${explicitMachineLabel}** |` : '';
118
258
  const providerPriority = n.policy?.providerPriority?.length ? ` | providers: ${n.policy.providerPriority.join(', ')}` : '';
119
259
  lines.push(`- ${explicitLabel} nodeId: \`${n.id}\` | workspace: \`${n.workspace}\`${n.daemonId ? ` | daemon: \`${n.daemonId}\`` : ''}${providerPriority}${suffix}`);
260
+ const nodePrompt = typeof (n as any).systemPrompt === 'string' ? (n as any).systemPrompt.trim() : '';
261
+ if (nodePrompt) {
262
+ lines.push(` 📌 Node instruction: ${indentFollowing(nodePrompt, ' ')}`);
263
+ }
120
264
  }
121
265
  lines.push('', '_Use `mesh_status` to probe live health before delegating work._');
122
266
  return lines.join('\n');
123
267
  }
124
268
 
269
+ /**
270
+ * Indent every line after the first by `pad`. Used so multi-line node
271
+ * instructions still visually nest under the node bullet without the
272
+ * second line dangling at column zero.
273
+ */
274
+ function indentFollowing(text: string, pad: string): string {
275
+ const lines = text.split('\n');
276
+ if (lines.length === 1) return lines[0];
277
+ return [lines[0], ...lines.slice(1).map(l => pad + l)].join('\n');
278
+ }
279
+
125
280
  function buildPolicySection(policy: RepoMeshPolicy): string {
126
281
  const rules: string[] = [];
127
282
  if (policy.requirePreTaskCheckpoint) rules.push('- Create a git checkpoint **before** starting each task');
@@ -219,6 +374,7 @@ function buildRulesSection(coordinatorCliType?: string): string {
219
374
  - **Converge branches.** After worktree tasks: refine/fast-forward, or classify as \`pushed_feature_branch_needs_merge\` / \`blocked_review\` / \`cleanup_candidate\` / \`not_mergeable\`. Clean up with \`mesh_remove_node\`.
220
375
  - **Refinery is config-driven.** \`mesh_refine_node\` must run validation from \`.adhdev/refine.{json,yaml,yml}\` or \`repo-mesh.refine.*\`. Heuristics are scaffolding only.
221
376
  - **Submodule reachability = publish-needed.** \`submodule_reachability_failed\` → classify as \`blocked_review\`, request user approval to push to submodule main, then rerun \`mesh_refine_node\`.
377
+ - **Honor per-node instructions.** When a node carries a 📌 Node instruction in the nodes section, include the relevant parts of that instruction in the task message you send to that node. Don't paraphrase the instruction into your own words — quote it verbatim so the worker agent sees exactly what the user wrote.
222
378
  - **Never fabricate tool results.** Always call the actual tool.
223
379
  - **Keep the user informed.** One or two sentences after each delegation round.${coordinatorNote}`;
224
380
  }
@@ -17,6 +17,20 @@ export interface CoordinatorRegistryEntry {
17
17
  sessionId: string;
18
18
  workspace?: string;
19
19
  startedAt: number;
20
+ /** CLI type used to launch the coordinator (claude-cli / codex-cli / …). */
21
+ cliType?: string;
22
+ /** Final system prompt sent to the coordinator after all overrides, appends,
23
+ * and extraSystemPrompt have been applied. Surfaced via the session-info
24
+ * endpoint so users can audit exactly what prompt the agent saw. */
25
+ systemPrompt?: string;
26
+ /** Per-launch extraSystemPrompt the caller passed in, if any. Stored
27
+ * separately from `systemPrompt` so the UI can show what the user
28
+ * added vs. what the daemon's default template produced. */
29
+ extraSystemPrompt?: string;
30
+ /** How the prompt was actually injected (cli_arg / context_file / …). */
31
+ injection?: { mode: string; target?: string };
32
+ /** Path of the MCP config file the daemon wrote for this session. */
33
+ mcpConfigPath?: string;
20
34
  }
21
35
 
22
36
  const _registry = new Map<string, CoordinatorRegistryEntry>();
@@ -57,11 +71,43 @@ export function registerMeshCoordinator(entry: CoordinatorRegistryEntry): void {
57
71
  saveRegistry();
58
72
  }
59
73
 
60
- /** Remove a coordinator session by sessionId. Persists to disk. */
74
+ /** Remove a coordinator session by sessionId. Persists to disk.
75
+ *
76
+ * Also best-effort strips any context_file wrapper block we wrote into
77
+ * the workspace at launch time (AGENTS.md / GEMINI.md), because those
78
+ * files are auto-loaded by their CLIs on every subsequent launch — and
79
+ * a wrapper block surviving the coordinator session would silently
80
+ * inject the coordinator system prompt into ordinary non-coordinator
81
+ * sessions in the same workspace, which is exactly the bug we're
82
+ * fixing here.
83
+ *
84
+ * Stripping rules:
85
+ * - Look for the start sentinel anywhere in the file; if absent,
86
+ * leave the file alone (user-authored content).
87
+ * - If the wrapper block was the only thing in the file, delete the
88
+ * file outright so we don't leave behind an empty AGENTS.md.
89
+ * - Otherwise drop only the block between the sentinels and trim a
90
+ * leading/trailing blank line so we don't leave dangling separators.
91
+ */
61
92
  export function unregisterMeshCoordinator(sessionId: string): void {
62
- if (_registry.delete(sessionId)) {
63
- saveRegistry();
64
- }
93
+ const entry = _registry.get(sessionId);
94
+ if (!_registry.delete(sessionId)) return;
95
+ saveRegistry();
96
+ if (!entry) return;
97
+ const workspace = entry.workspace;
98
+ const target = entry.injection?.mode === 'context_file' && typeof entry.injection.target === 'string'
99
+ ? entry.injection.target
100
+ : '';
101
+ if (!workspace || !target) return;
102
+ // injection.target is workspace-relative (AGENTS.md / GEMINI.md); we don't
103
+ // resolve absolute paths here because we never wrote one — applyMesh-
104
+ // CoordinatorSystemPromptInjection joins via path.join(workspace, …).
105
+ const filePath = `${workspace.replace(/\/$/, '')}/${target}`;
106
+ try {
107
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
108
+ const { stripCoordinatorWrapperFile } = require('../commands/mesh-coordinator.js');
109
+ stripCoordinatorWrapperFile(filePath);
110
+ } catch { /* best-effort cleanup; never throw out of unregister */ }
65
111
  }
66
112
 
67
113
  /** Look up a coordinator entry by session ID. Returns undefined if not registered. */
@@ -15,6 +15,7 @@ import { assertProviderSupportsDeclaredInput, getEffectiveMessageInputSupport }
15
15
  import type { ProviderInstance, ProviderState, ProviderEvent, InstanceContext, ProviderErrorReason, HotChatSessionState, SessionModalState } from './provider-instance.js';
16
16
  import { ProviderCliAdapter } from '../cli-adapters/provider-cli-adapter.js';
17
17
  import type { CliProviderModule } from '../cli-adapters/provider-cli-adapter.js';
18
+ import { createCliAdapter } from './spec/route.js';
18
19
  import type { PtyRuntimeMetadata, PtyTransportFactory } from '../cli-adapters/pty-transport.js';
19
20
  import { StatusMonitor } from './status-monitor.js';
20
21
  import { ChatHistoryWriter, isNativeSourceCanonicalHistory, materializeProviderNativeHistory, readChatHistory, readProviderChatHistory } from '../config/chat-history.js';
@@ -389,7 +390,7 @@ export class CliProviderInstance implements ProviderInstance {
389
390
  this.providerSessionId = options?.providerSessionId;
390
391
  this.launchMode = options?.launchMode || 'new';
391
392
  this.onProviderSessionResolved = options?.onProviderSessionResolved;
392
- this.adapter = new ProviderCliAdapter(provider as CliProviderModule, workingDir, cliArgs, options?.extraEnv || {}, transportFactory);
393
+ this.adapter = createCliAdapter(provider as CliProviderModule, workingDir, cliArgs, options?.extraEnv || {}, transportFactory) as ProviderCliAdapter;
393
394
  if (this.providerSessionId) {
394
395
  this.adapter.updateRuntimeMeta({ providerSessionId: this.providerSessionId });
395
396
  }
@@ -727,13 +728,17 @@ export class CliProviderInstance implements ProviderInstance {
727
728
  };
728
729
  }
729
730
 
730
- getSessionModalState(): SessionModalState {
731
+ getSessionModalState(sessionId?: string): SessionModalState {
731
732
  const adapterStatus = this.adapter.getStatus({ allowParse: false });
732
733
  const autoApproveActive = adapterStatus.status === 'waiting_approval' && this.shouldAutoApprove();
733
734
  const visibleStatus = autoApproveActive ? 'generating' : adapterStatus.status;
734
735
  const dirName = this.workingDir.split('/').filter(Boolean).pop() || 'session';
735
736
  return {
736
- id: this.instanceId,
737
+ // Honor the caller-supplied sessionId — InstanceMgr rejects the
738
+ // projection when projected.id !== requested sessionId, and
739
+ // this.instanceId is the manager's internal key, not the public
740
+ // sessionId the dashboard subscribes by.
741
+ id: sessionId ?? this.instanceId,
737
742
  status: visibleStatus,
738
743
  title: dirName,
739
744
  activeModal: autoApproveActive ? null : adapterStatus.activeModal,
@@ -393,8 +393,61 @@ export interface ProviderMeshCoordinatorConfig {
393
393
  /** Copyable setup template. Supports {{meshId}}, {{adhdevMcpCommand}}, {{workspace}}, {{serverName}}. */
394
394
  template?: string;
395
395
  };
396
+ /**
397
+ * How the coordinator system prompt reaches the launched CLI. Replaces the
398
+ * old hard-coded `if (cliType === 'claude-cli') push --append-system-prompt`
399
+ * branches in router.ts: a new CLI now ships its injection rule in its
400
+ * provider.v1.json, no daemon code change needed. Users can override the
401
+ * rendered prompt or the injection mechanism per-provider; if omitted, no
402
+ * system prompt is injected (safe default — won't crash spawn with a flag
403
+ * the CLI doesn't recognize).
404
+ */
405
+ systemPromptInjection?: MeshCoordinatorSystemPromptInjection;
396
406
  }
397
407
 
408
+ /**
409
+ * Declarative description of how a CLI accepts a session-scoped system prompt.
410
+ *
411
+ * Modes:
412
+ * - cli_arg → push `flag` + prompt onto spawn args (Claude)
413
+ * - config_override → push `flag` + a templated key=value config override (Codex)
414
+ * - context_file → write prompt into a workspace markdown the CLI
415
+ * auto-loads as project context (Gemini, Antigravity)
416
+ * - env_var → expose prompt to the spawned process as $name (Hermes)
417
+ *
418
+ * The prompt text is templated with `{prompt}` (raw) or `{prompt_json}`
419
+ * (JSON-encoded for embedding inside config-override strings).
420
+ */
421
+ export type MeshCoordinatorSystemPromptInjection =
422
+ | {
423
+ mode: 'cli_arg';
424
+ /** Spawn-args flag, e.g. '--append-system-prompt'. The prompt becomes the next argv. */
425
+ flag: string;
426
+ }
427
+ | {
428
+ mode: 'config_override';
429
+ /** Spawn-args flag, e.g. '-c'. Followed by `template` with placeholders rendered. */
430
+ flag: string;
431
+ /** Template using {prompt} or {prompt_json}, e.g. 'developer_instructions={prompt_json}'. */
432
+ template: string;
433
+ }
434
+ | {
435
+ mode: 'context_file';
436
+ /** Workspace-relative file path the CLI auto-loads, e.g. 'AGENTS.md' or 'GEMINI.md'. */
437
+ path: string;
438
+ /**
439
+ * Optional wrapper around the prompt. Use `{prompt}` placeholder. Existing
440
+ * wrapper-delimited blocks are replaced rather than duplicated, so re-launching
441
+ * a coordinator doesn't pile up copies. If omitted, the prompt is appended raw.
442
+ */
443
+ wrapper?: string;
444
+ }
445
+ | {
446
+ mode: 'env_var';
447
+ /** Env-var name, e.g. 'HERMES_EPHEMERAL_SYSTEM_PROMPT'. */
448
+ name: string;
449
+ };
450
+
398
451
  export interface ProviderCompatibilityEntry {
399
452
  ideVersion: string;
400
453
  scriptDir: string;
@@ -397,11 +397,11 @@ function parsePbFile(
397
397
  *
398
398
  * Returns `null` when the file is missing, empty, or yields no parseable messages.
399
399
  */
400
- export async function readSession(
400
+ export function readSession(
401
401
  sessionPath: string,
402
402
  sessionId?: string,
403
403
  workspace?: string,
404
- ): Promise<NativeHistorySession | null> {
404
+ ): NativeHistorySession | null {
405
405
  if (!sessionPath || !path.isAbsolute(sessionPath)) return null;
406
406
  if (!fs.existsSync(sessionPath)) return null;
407
407
 
@@ -296,7 +296,7 @@ function parseTranscriptFile(
296
296
  * `sessionPath` is the absolute path to a `<uuid>.jsonl` file.
297
297
  * Returns `null` when the file is missing, empty, or yields no parseable messages.
298
298
  */
299
- export async function readSession(sessionPath: string): Promise<NativeHistorySession | null> {
299
+ export function readSession(sessionPath: string): NativeHistorySession | null {
300
300
  if (!sessionPath || !path.isAbsolute(sessionPath)) return null;
301
301
 
302
302
  const basename = path.basename(sessionPath, '.jsonl');
@@ -302,7 +302,7 @@ function parseSessionFile(
302
302
  * under ~/.codex/sessions/.
303
303
  * Returns `null` when the file is missing, empty, or yields no parseable messages.
304
304
  */
305
- export async function readSession(sessionPath: string): Promise<NativeHistorySession | null> {
305
+ export function readSession(sessionPath: string): NativeHistorySession | null {
306
306
  if (!sessionPath || !path.isAbsolute(sessionPath)) return null;
307
307
  if (!fs.existsSync(sessionPath)) return null;
308
308