@adhdev/daemon-core 0.9.82-rc.136 → 0.9.82-rc.138

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/chat/source-machine.d.ts +166 -0
  2. package/dist/chat/source-resolver.d.ts +104 -0
  3. package/dist/cli-adapters/cli-script-runner.d.ts +45 -0
  4. package/dist/cli-adapters/cli-state-engine.d.ts +169 -0
  5. package/dist/cli-adapters/provider-cli-adapter.d.ts +72 -74
  6. package/dist/cli-adapters/provider-cli-parse.d.ts +1 -0
  7. package/dist/cli-adapters/provider-cli-shared.d.ts +5 -0
  8. package/dist/config/chat-history.d.ts +1 -0
  9. package/dist/index.d.ts +3 -3
  10. package/dist/index.js +3507 -2288
  11. package/dist/index.js.map +1 -1
  12. package/dist/index.mjs +3515 -2301
  13. package/dist/index.mjs.map +1 -1
  14. package/dist/mesh/beads-db.d.ts +54 -0
  15. package/dist/mesh/contracts.d.ts +164 -0
  16. package/dist/mesh/mesh-active-work.d.ts +7 -1
  17. package/dist/mesh/mesh-events.d.ts +10 -4
  18. package/dist/mesh/mesh-ledger.d.ts +21 -1
  19. package/dist/mesh/mesh-refine-status.d.ts +2 -3
  20. package/dist/mesh/mesh-work-queue.d.ts +17 -0
  21. package/dist/mesh/worktree-bootstrap-config.d.ts +2 -4
  22. package/dist/providers/contracts.d.ts +19 -0
  23. package/dist/providers/read-chat-contract.d.ts +29 -0
  24. package/dist/providers/transcript-v2.d.ts +176 -0
  25. package/dist/repo-mesh-types.d.ts +5 -0
  26. package/dist/shared-types.d.ts +7 -0
  27. package/dist/status/snapshot.d.ts +1 -0
  28. package/dist/types.d.ts +5 -0
  29. package/package.json +1 -1
  30. package/src/chat/source-machine.ts +534 -0
  31. package/src/chat/source-resolver.ts +0 -0
  32. package/src/chat/subscription-updates.ts +9 -0
  33. package/src/cli-adapters/cli-script-runner.ts +145 -0
  34. package/src/cli-adapters/cli-state-engine.ts +1054 -0
  35. package/src/cli-adapters/provider-cli-adapter.d.ts +0 -1
  36. package/src/cli-adapters/provider-cli-adapter.ts +413 -1399
  37. package/src/cli-adapters/provider-cli-parse.ts +3 -0
  38. package/src/cli-adapters/provider-cli-shared.ts +17 -1
  39. package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +17 -1
  40. package/src/cli-adapters/terminal-backends/xterm-backend.ts +8 -1
  41. package/src/commands/chat-commands.ts +715 -368
  42. package/src/commands/router.ts +22 -2
  43. package/src/config/chat-history.ts +43 -16
  44. package/src/git/git-worktree.ts +8 -1
  45. package/src/index.ts +3 -2
  46. package/src/mesh/beads-db.ts +305 -2
  47. package/src/mesh/contracts.ts +329 -0
  48. package/src/mesh/coordinator-prompt.ts +12 -17
  49. package/src/mesh/mesh-active-work.ts +162 -59
  50. package/src/mesh/mesh-events.ts +198 -53
  51. package/src/mesh/mesh-ledger.ts +321 -105
  52. package/src/mesh/mesh-refine-status.ts +2 -3
  53. package/src/mesh/mesh-work-queue.ts +116 -120
  54. package/src/mesh/worktree-bootstrap-config.ts +17 -4
  55. package/src/providers/contracts.ts +19 -0
  56. package/src/providers/provider-loader.ts +21 -7
  57. package/src/providers/provider-schema.ts +12 -0
  58. package/src/providers/read-chat-contract.ts +74 -14
  59. package/src/providers/transcript-v2.ts +567 -0
  60. package/src/repo-mesh-types.ts +10 -0
  61. package/src/shared-types.ts +7 -0
  62. package/src/status/snapshot.ts +35 -11
  63. package/src/types.ts +5 -0
@@ -0,0 +1,329 @@
1
+ /**
2
+ * Mesh contract v2 — first-class identity, scopes, and protocol version.
3
+ *
4
+ * Replaces the implicit conventions that grew across mesh-events.ts,
5
+ * mesh-work-queue.ts, mesh-ledger.ts, and mesh-tools.ts where the same
6
+ * concept (a coordinator, a session, a task status) appeared in different
7
+ * shapes per file. The audit found:
8
+ *
9
+ * - PendingMeshCoordinatorEvent had no targetCoordinatorDaemonId. All
10
+ * events were broadcast to every coordinator that drained them. Two
11
+ * coordinators sharing a mesh would each receive every event,
12
+ * leading to duplicate completion handling and missed targeting.
13
+ * - drainPendingMeshCoordinatorEvents accepted only meshId. The caller's
14
+ * coordinator identity was never available, so per-coordinator
15
+ * routing was impossible by construction.
16
+ * - Session identifier keys diverged across stores: targetSessionId /
17
+ * assignedSessionId / instanceId / runtimeSessionId / providerSessionId.
18
+ * resolveEventSessionId() tried four fallbacks per call.
19
+ * - No protocol version on the JSONL ledger or BeadsDB. Schema
20
+ * evolutions had no guard rail.
21
+ * - mesh_reconcile_ledger existed as a routine recovery tool, not as
22
+ * an incident-response escape hatch. That itself signals the routing
23
+ * layer cannot be trusted.
24
+ *
25
+ * This module introduces the types; the actual wiring lands in B2 (data
26
+ * model + 3-way transactional store), B3 (MCP layer enforcement), and B4
27
+ * (frontend consumption). B1 is intentionally non-breaking — it adds the
28
+ * types and leaves runtime behaviour unchanged.
29
+ */
30
+
31
+ /** Provider type identifier (e.g. 'claude-cli', 'codex-cli', 'roo-code').
32
+ * Free-form string; no shared enum exists in daemon-core yet. */
33
+ type ProviderType = string;
34
+
35
+ // ─── Protocol version ────────────────────────────────────────────────────
36
+
37
+ export const MESH_PROTOCOL_VERSION_V1 = '1.0' as const;
38
+ export const MESH_PROTOCOL_VERSION_V2 = '2.0' as const;
39
+
40
+ export type MeshProtocolVersion =
41
+ | typeof MESH_PROTOCOL_VERSION_V1
42
+ | typeof MESH_PROTOCOL_VERSION_V2;
43
+
44
+ export const SUPPORTED_MESH_PROTOCOL_VERSIONS: readonly MeshProtocolVersion[] = [
45
+ MESH_PROTOCOL_VERSION_V1,
46
+ MESH_PROTOCOL_VERSION_V2,
47
+ ] as const;
48
+
49
+ export function isSupportedMeshProtocolVersion(value: unknown): value is MeshProtocolVersion {
50
+ return typeof value === 'string'
51
+ && (SUPPORTED_MESH_PROTOCOL_VERSIONS as readonly string[]).includes(value);
52
+ }
53
+
54
+ // ─── Coordinator identity ────────────────────────────────────────────────
55
+
56
+ /**
57
+ * Identity of a mesh coordinator. Required (non-optional) on every dispatch
58
+ * and drain operation in v2 — opaque/missing identity is the audit's
59
+ * smoking gun for routing failures and is structurally banned by the v2
60
+ * types.
61
+ *
62
+ * - daemonId: the machineId of the daemon hosting the coordinator. Stable
63
+ * across coordinator restarts on the same machine.
64
+ * - coordinatorRunId: a UUID generated when the coordinator process starts.
65
+ * Stable for the coordinator's lifetime, fresh on restart. Required so
66
+ * two coordinators on the same daemon (one CLI, one MCP) can be told
67
+ * apart without conflating their drains.
68
+ * - sessionId: optional CLI coordinator session identifier (instanceId).
69
+ * Present when the coordinator is itself a CLI session, absent for
70
+ * pure MCP coordinators.
71
+ */
72
+ export interface CoordinatorIdentity {
73
+ readonly daemonId: string;
74
+ readonly coordinatorRunId: string;
75
+ readonly sessionId?: string;
76
+ }
77
+
78
+ export function coordinatorIdentityEquals(a: CoordinatorIdentity, b: CoordinatorIdentity): boolean {
79
+ return a.daemonId === b.daemonId
80
+ && a.coordinatorRunId === b.coordinatorRunId
81
+ && (a.sessionId ?? '') === (b.sessionId ?? '');
82
+ }
83
+
84
+ export function coordinatorIdentityKey(identity: CoordinatorIdentity): string {
85
+ // Stable string form for map keys and ledger payloads. Avoids the
86
+ // {a:b, c:d} JSON.stringify ordering trap by enforcing field order.
87
+ return `${identity.daemonId}|${identity.coordinatorRunId}|${identity.sessionId ?? ''}`;
88
+ }
89
+
90
+ // ─── Session handle ──────────────────────────────────────────────────────
91
+
92
+ /**
93
+ * Unified mesh session identifier. v1 referred to "the session" with five
94
+ * different field names depending on which store you were reading; v2
95
+ * collapses them into one explicit handle that carries every disambiguator
96
+ * a consumer might need.
97
+ *
98
+ * - nodeId: the mesh node this session belongs to (FK into mesh node table).
99
+ * - sessionId: provider-instance identifier (the runtime session id).
100
+ * - providerType: which provider category/type the session runs (e.g.
101
+ * 'claude-cli', 'codex-cli', 'roo-code').
102
+ * - coordinatorDaemonId: which coordinator dispatched the work that
103
+ * spawned this session. Used for completion event routing.
104
+ * - assignedAt: ms epoch when the session was bound to its current task.
105
+ */
106
+ export interface MeshSessionHandle {
107
+ readonly nodeId: string;
108
+ readonly sessionId: string;
109
+ readonly providerType: ProviderType;
110
+ readonly coordinatorDaemonId: string;
111
+ readonly assignedAt: number;
112
+ }
113
+
114
+ export function meshSessionHandleKey(handle: MeshSessionHandle): string {
115
+ return `${handle.nodeId}|${handle.sessionId}`;
116
+ }
117
+
118
+ // ─── Task status (canonical enum) ────────────────────────────────────────
119
+
120
+ /**
121
+ * Single canonical task status enum. v1 had at least three overlapping
122
+ * sets (queue / ledger / direct-dispatch). v2 consumers import from here.
123
+ */
124
+ export const MESH_TASK_STATUSES = [
125
+ 'pending',
126
+ 'assigned',
127
+ 'in_progress',
128
+ 'completed',
129
+ 'failed',
130
+ 'cancelled',
131
+ ] as const;
132
+ export type MeshTaskStatus = typeof MESH_TASK_STATUSES[number];
133
+
134
+ export function isMeshTaskStatus(value: unknown): value is MeshTaskStatus {
135
+ return typeof value === 'string'
136
+ && (MESH_TASK_STATUSES as readonly string[]).includes(value);
137
+ }
138
+
139
+ // ─── Event scope ─────────────────────────────────────────────────────────
140
+
141
+ /**
142
+ * Explicit scope for pending coordinator events. v1 had no scope: every
143
+ * event was broadcast to every drainer. v2 forces producers to declare
144
+ * intent.
145
+ *
146
+ * - 'unicast': delivered to exactly one coordinator (intendedFor). Other
147
+ * coordinators' drains skip it.
148
+ * - 'broadcast': delivered to every coordinator on the mesh. Used for
149
+ * system-wide signals (e.g. mesh-wide policy changes). The v1 default
150
+ * becomes explicit here so audit tools can flag unintended broadcasts.
151
+ * - 'system': delivered to the daemon-level handler, not coordinators.
152
+ * Reserved for infrastructure events that no coordinator should see
153
+ * (e.g. ledger reconciliation outcomes).
154
+ */
155
+ export const MESH_EVENT_SCOPES = ['unicast', 'broadcast', 'system'] as const;
156
+ export type MeshEventScope = typeof MESH_EVENT_SCOPES[number];
157
+
158
+ export function isMeshEventScope(value: unknown): value is MeshEventScope {
159
+ return typeof value === 'string'
160
+ && (MESH_EVENT_SCOPES as readonly string[]).includes(value);
161
+ }
162
+
163
+ // ─── Pending coordinator event v2 ────────────────────────────────────────
164
+
165
+ /**
166
+ * v2 shape of a pending coordinator event. Strict supersets of the v1
167
+ * shape — every v1 field is preserved so existing readers do not break;
168
+ * v2 fields (scope, dispatchedBy, intendedFor, protocolVersion) are
169
+ * additive and consulted by v2-aware drainers only.
170
+ *
171
+ * Mixed v1/v2 events coexist during the rollout window. B2 fully cuts
172
+ * over once every coordinator drain is v2-aware.
173
+ */
174
+ export interface PendingMeshCoordinatorEventV2 {
175
+ // v1 fields (preserved exactly)
176
+ readonly event: string;
177
+ readonly meshId: string;
178
+ readonly nodeLabel: string;
179
+ readonly nodeId?: string;
180
+ readonly workspace?: string;
181
+ readonly metadataEvent: Record<string, unknown>;
182
+ readonly coordinatorMessage?: string;
183
+ readonly queuedAt: number;
184
+
185
+ // v2 additions
186
+ readonly protocolVersion: MeshProtocolVersion;
187
+ readonly scope: MeshEventScope;
188
+ readonly dispatchedBy: CoordinatorIdentity;
189
+ /**
190
+ * Required when scope === 'unicast', forbidden otherwise. Drainers MUST
191
+ * skip unicast events whose intendedFor does not equal their own identity.
192
+ */
193
+ readonly intendedFor?: CoordinatorIdentity;
194
+ }
195
+
196
+ // ─── Ledger entry v2 ─────────────────────────────────────────────────────
197
+
198
+ /**
199
+ * v2 ledger entry additions. The originatingCoordinator field is the
200
+ * source of truth that lets completion events route back to the
201
+ * coordinator that dispatched the task. v1's "worker settings carried it
202
+ * if it happened to be set" approach is replaced.
203
+ */
204
+ export interface MeshLedgerOriginatingCoordinatorV2 {
205
+ readonly originatingCoordinator: CoordinatorIdentity;
206
+ readonly protocolVersion: MeshProtocolVersion;
207
+ }
208
+
209
+ // ─── Validation primitives ───────────────────────────────────────────────
210
+
211
+ export class MeshContractViolationError extends Error {
212
+ readonly violationPath: string;
213
+ readonly protocolVersion: MeshProtocolVersion;
214
+ constructor(
215
+ protocolVersion: MeshProtocolVersion,
216
+ violationPath: string,
217
+ detail: string,
218
+ ) {
219
+ super(`mesh contract ${protocolVersion} violation at ${violationPath}: ${detail}`);
220
+ this.name = 'MeshContractViolationError';
221
+ this.violationPath = violationPath;
222
+ this.protocolVersion = protocolVersion;
223
+ }
224
+ }
225
+
226
+ function isNonEmptyString(value: unknown): value is string {
227
+ return typeof value === 'string' && value.length > 0;
228
+ }
229
+
230
+ export function assertCoordinatorIdentity(raw: unknown, path: string): CoordinatorIdentity {
231
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
232
+ throw new MeshContractViolationError(MESH_PROTOCOL_VERSION_V2, path, 'must be an object');
233
+ }
234
+ const obj = raw as Record<string, unknown>;
235
+ if (!isNonEmptyString(obj.daemonId)) {
236
+ throw new MeshContractViolationError(MESH_PROTOCOL_VERSION_V2, `${path}.daemonId`, 'must be a non-empty string');
237
+ }
238
+ if (!isNonEmptyString(obj.coordinatorRunId)) {
239
+ throw new MeshContractViolationError(MESH_PROTOCOL_VERSION_V2, `${path}.coordinatorRunId`, 'must be a non-empty string');
240
+ }
241
+ const sessionId = obj.sessionId;
242
+ if (sessionId !== undefined && !isNonEmptyString(sessionId)) {
243
+ throw new MeshContractViolationError(MESH_PROTOCOL_VERSION_V2, `${path}.sessionId`, 'must be a non-empty string when provided');
244
+ }
245
+ return sessionId !== undefined
246
+ ? { daemonId: obj.daemonId, coordinatorRunId: obj.coordinatorRunId, sessionId }
247
+ : { daemonId: obj.daemonId, coordinatorRunId: obj.coordinatorRunId };
248
+ }
249
+
250
+ export function assertPendingMeshCoordinatorEventV2(raw: unknown, path = '$'): PendingMeshCoordinatorEventV2 {
251
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
252
+ throw new MeshContractViolationError(MESH_PROTOCOL_VERSION_V2, path, 'must be an object');
253
+ }
254
+ const obj = raw as Record<string, unknown>;
255
+ if (!isSupportedMeshProtocolVersion(obj.protocolVersion)) {
256
+ throw new MeshContractViolationError(MESH_PROTOCOL_VERSION_V2, `${path}.protocolVersion`, `must be one of ${SUPPORTED_MESH_PROTOCOL_VERSIONS.join(', ')}`);
257
+ }
258
+ if (!isMeshEventScope(obj.scope)) {
259
+ throw new MeshContractViolationError(MESH_PROTOCOL_VERSION_V2, `${path}.scope`, `must be one of ${MESH_EVENT_SCOPES.join(', ')}`);
260
+ }
261
+ if (!isNonEmptyString(obj.event)) {
262
+ throw new MeshContractViolationError(MESH_PROTOCOL_VERSION_V2, `${path}.event`, 'must be a non-empty string');
263
+ }
264
+ if (!isNonEmptyString(obj.meshId)) {
265
+ throw new MeshContractViolationError(MESH_PROTOCOL_VERSION_V2, `${path}.meshId`, 'must be a non-empty string');
266
+ }
267
+ const dispatchedBy = assertCoordinatorIdentity(obj.dispatchedBy, `${path}.dispatchedBy`);
268
+
269
+ if (obj.scope === 'unicast') {
270
+ if (!obj.intendedFor) {
271
+ throw new MeshContractViolationError(MESH_PROTOCOL_VERSION_V2, `${path}.intendedFor`, 'unicast scope requires intendedFor');
272
+ }
273
+ } else if (obj.intendedFor !== undefined) {
274
+ throw new MeshContractViolationError(MESH_PROTOCOL_VERSION_V2, `${path}.intendedFor`, 'only unicast scope may set intendedFor');
275
+ }
276
+ const intendedFor = obj.intendedFor
277
+ ? assertCoordinatorIdentity(obj.intendedFor, `${path}.intendedFor`)
278
+ : undefined;
279
+
280
+ const metadata = obj.metadataEvent && typeof obj.metadataEvent === 'object' && !Array.isArray(obj.metadataEvent)
281
+ ? (obj.metadataEvent as Record<string, unknown>)
282
+ : null;
283
+ if (!metadata) {
284
+ throw new MeshContractViolationError(MESH_PROTOCOL_VERSION_V2, `${path}.metadataEvent`, 'must be an object');
285
+ }
286
+ const queuedAt = typeof obj.queuedAt === 'number' && Number.isFinite(obj.queuedAt) ? obj.queuedAt : null;
287
+ if (queuedAt === null) {
288
+ throw new MeshContractViolationError(MESH_PROTOCOL_VERSION_V2, `${path}.queuedAt`, 'must be a finite number');
289
+ }
290
+
291
+ return {
292
+ event: obj.event,
293
+ meshId: obj.meshId,
294
+ nodeLabel: isNonEmptyString(obj.nodeLabel) ? obj.nodeLabel : '',
295
+ nodeId: typeof obj.nodeId === 'string' ? obj.nodeId : undefined,
296
+ workspace: typeof obj.workspace === 'string' ? obj.workspace : undefined,
297
+ metadataEvent: metadata,
298
+ coordinatorMessage: typeof obj.coordinatorMessage === 'string' ? obj.coordinatorMessage : undefined,
299
+ queuedAt,
300
+ protocolVersion: obj.protocolVersion,
301
+ scope: obj.scope,
302
+ dispatchedBy,
303
+ ...(intendedFor ? { intendedFor } : {}),
304
+ };
305
+ }
306
+
307
+ // ─── Routing helper ──────────────────────────────────────────────────────
308
+
309
+ /**
310
+ * Decide whether a v2 pending event should be delivered to the given drainer.
311
+ * Centralised so every drain implementation uses the same rule.
312
+ *
313
+ * - 'broadcast': always delivered.
314
+ * - 'system': never delivered to coordinators (system handler only).
315
+ * - 'unicast': delivered iff intendedFor matches drainer identity.
316
+ *
317
+ * v1 events (no scope / no protocolVersion) are treated as broadcast for
318
+ * backward compatibility during rollout; B3 tightens this so v1 events
319
+ * are quarantined to a dedicated drain endpoint instead.
320
+ */
321
+ export function shouldDeliverPendingEventToCoordinator(
322
+ event: PendingMeshCoordinatorEventV2,
323
+ drainer: CoordinatorIdentity,
324
+ ): boolean {
325
+ if (event.scope === 'system') return false;
326
+ if (event.scope === 'broadcast') return true;
327
+ if (!event.intendedFor) return false;
328
+ return coordinatorIdentityEquals(event.intendedFor, drainer);
329
+ }
@@ -209,21 +209,16 @@ function buildRulesSection(coordinatorCliType?: string): string {
209
209
 
210
210
  return `## Rules
211
211
 
212
- - **Minimize coordinator context.** The coordinator's job is routing, not implementing. Do not read source files, run commands, or analyze code directly delegate all of that to node agents. Your context should stay lean.
213
- - **Delegate analysis too.** If you need to understand a bug or explore the codebase, send that investigation as a task to the queue or a node. Do not do it yourself.
214
- - **Respect explicit provider requests.** If the user names an agent/provider, pass the matching provider type to \`mesh_launch_session\`: Hermes \`hermes-cli\`, Claude Code/Claude \`claude-cli\`, Codex \`codex-cli\`, Gemini \`gemini-cli\`, Antigravity \`antigravity-cli\`. Never substitute \`claude-cli\` just because the coordinator itself is Claude Code.
215
- - **Front-load new task messages.** When calling \`mesh_enqueue_task\` or \`mesh_send_task\` for a new task, include everything the agent needs: what files to touch, what the problem is, what the fix should look like. The agent won't ask follow-up questions.
216
- - **Avoid context-wasting restarts.** For follow-up, retry, commit/push, preview, or cleanup work on the same issue, prefer the existing idle session and send only the delta from its last verified state. Start a fresh chat/session only for genuinely independent work, explicit provider/user request, unsafe transcript contamination, or required branch/worktree isolation.
217
- - **Don't inspect code.** Treat delegated agent summaries as self-reports, not verification. Verify side effects via \`mesh_git_status\` (including related repo freshness when configured), not by reading source files.
218
- - **Don't over-parallelize.** Start with 1-2 concurrent tasks. Scale up if they succeed. Never launch a duplicate session or second worker solely because \`mesh_read_chat\` has no final assistant message while the delegated session is still showing tool/terminal activity.
219
- - **Handle failures with context.** If a task fails, check \`mesh_task_history\` first to see if this task was attempted before and how it failed. Read the chat to understand why, then decide: retry on the same node, reassign to a different node, or escalate to the user.
220
- - **Check history before starting.** At the beginning of a coordination session, call \`mesh_task_history\` to understand what was previously delegated and its outcomes. This prevents duplicate work and informs recovery decisions.
221
- - **Keep the user informed.** Report progress after each delegation round one or two sentences, not a narration.
222
- - **Respect node capabilities.** Don't send build tasks to read-only nodes. Don't push from nodes that aren't allowed to.
223
- - **Never fabricate tool results.** Always call the actual tool; never pretend you did.
224
- - **Clean up worktree nodes.** After a worktree task completes and its changes are merged or checkpointed, call \`mesh_remove_node\` to free resources.
225
- - **Do not strand completed branches.** A checkpointed or clean feature/worktree branch is not done by itself. Merge/refine it to the mesh default branch, fast-forward obvious clean behind-only branches with \`mesh_fast_forward_node\`, or explicitly report one of \`pushed_feature_branch_needs_merge\`, \`blocked_review\`, \`cleanup_candidate\`, or \`not_mergeable\` with the next action.
226
- - **Keep Refinery validation project-configurable.** \`mesh_refine_node\` must execute validation from repo mesh/refine config (for example \`.adhdev/refine.{json,yaml,yml}\`, \`.adhdev/repo-mesh-refine.*\`, or \`repo-mesh.refine.*\`). Heuristics are suggestions/scaffolding only, not the execution path.
227
- - **Treat submodule main reachability as publish-needed.** A \`submodule_reachability_failed\` refine result means the root gitlink points at a submodule commit that is not reachable from the configured submodule remote main branch. Do not treat feature-branch reachability as complete, retry validation blindly, or start code review first. Classify it as \`blocked_review\`, request user approval to push/publish the submodule commit to submodule main, then rerun \`mesh_refine_node\`, unless the mesh or repo refine config explicitly enabled \`allowAutoPublishSubmoduleMainCommits\` and Refinery reports exact path/commit/remote/branch evidence with post-publish verification.
228
- - **Name worktree branches meaningfully.** Use descriptive names like \`feat/auth-refactor\` or \`fix/build-123\`.${coordinatorNote}`;
212
+ - **Route, don't implement.** Delegate all code reading, analysis, and execution to node agents. Never read source files or run commands in the coordinatorkeep context lean.
213
+ - **Front-load task messages.** Include everything the agent needs (files, problem, expected fix) in \`mesh_enqueue_task\` / \`mesh_send_task\`. Append a structured result request at the end: ask the worker to conclude with a JSON block containing \`status\`, \`changedFiles\`, \`gitStatus\`, \`validationResults\`, \`errors\`, \`nextAction\`. The daemon parses this automatically; you can read it from \`mesh_task_history\`.
214
+ - **Reuse idle sessions.** For follow-up, retry, commit/push, or cleanup on the same issue, send only the delta to the existing idle session. Start fresh only for independent work, provider mismatch, transcript contamination, or required worktree isolation.
215
+ - **Respect explicit provider requests.** Map: Hermes \`hermes-cli\`, Claude/Claude Code \`claude-cli\`, Codex \`codex-cli\`, Gemini \`gemini-cli\`, Antigravity \`antigravity-cli\`. Never substitute the coordinator's own runtime.
216
+ - **Verify via git, not source.** Use \`mesh_git_status\` to confirm side effects. Treat agent summaries as self-reports, not verification.
217
+ - **Limit parallelism.** Start with 1–2 tasks; scale only on success. Never duplicate a session because \`mesh_read_chat\` shows no final message while tool/terminal activity is ongoing.
218
+ - **Check history first.** Call \`mesh_task_history\` at session start to avoid duplicate work and inform recovery. On failure, read task history before retrying.
219
+ - **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
+ - **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
+ - **Submodule reachability = publish-needed.** \`submodule_reachability_failed\` classify as \`blocked_review\`, request user approval to push to submodule main, then rerun \`mesh_refine_node\`.
222
+ - **Never fabricate tool results.** Always call the actual tool.
223
+ - **Keep the user informed.** One or two sentences after each delegation round.${coordinatorNote}`;
229
224
  }
@@ -1,5 +1,5 @@
1
1
  import type { MeshLedgerEntry } from './mesh-ledger.js';
2
- import type { MeshWorkQueueEntry } from './mesh-work-queue.js';
2
+ import type { MeshWorkQueueEntry, DirectDispatchRecord } from './mesh-work-queue.js';
3
3
 
4
4
  export type MeshActiveWorkSource = 'queue' | 'direct';
5
5
  export type MeshActiveWorkStatus = 'pending' | 'assigned' | 'generating' | 'idle' | 'failed' | 'awaiting_approval';
@@ -69,6 +69,12 @@ export interface BuildMeshActiveWorkOptions {
69
69
  meshId: string;
70
70
  queue?: MeshWorkQueueEntry[];
71
71
  ledgerEntries?: MeshLedgerEntry[];
72
+ /**
73
+ * Active direct dispatches from BeadsDB. When provided, these are used instead of
74
+ * scanning ledger entries for direct dispatches — eliminates the O(n_ledger) scan.
75
+ * Falls back to ledger scanning when not provided.
76
+ */
77
+ directDispatches?: DirectDispatchRecord[];
72
78
  nodes?: any[];
73
79
  now?: number;
74
80
  /** Include terminal direct rows (idle/failed) for handoff/recent-work surfaces. Defaults false. */
@@ -218,66 +224,163 @@ export function buildMeshActiveWork(opts: BuildMeshActiveWorkOptions): { activeW
218
224
  });
219
225
  }
220
226
 
221
- const ledgerEntries = (opts.ledgerEntries || []).slice().sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
222
- const terminals = ledgerEntries.filter(entry => TERMINAL_LEDGER_KINDS.has(entry.kind) || entry.kind === 'task_approval_needed');
223
- for (const dispatch of ledgerEntries.filter(isDirectDispatch)) {
224
- const taskId = directDispatchTaskId(dispatch);
225
- const terminal = terminals
226
- .filter(entry => new Date(entry.timestamp).getTime() >= new Date(dispatch.timestamp).getTime())
227
- .find(entry => terminalMatchesDispatch(entry, dispatch, taskId));
228
- const terminalStatus = terminal ? statusFromTerminal(terminal) : undefined;
229
- const live = sessionStatusFromNodes(opts.nodes, dispatch.nodeId, dispatch.sessionId);
230
- const status = terminalStatus || live.status || 'assigned';
231
- const terminalRow = Boolean(terminal && terminal.kind !== 'task_approval_needed');
232
- // dispatchedToIdleSession is recorded by mesh_send_task when the target session was idle
233
- // at dispatch time. Combined with status === 'idle', it confirms no runtime transition.
234
- const dispatchedToIdleSession = dispatch.payload?.dispatchedToIdleSession === true;
235
- // Flag stale when no terminal event AND either:
236
- // - session is now idle (dispatch not acknowledged), OR
237
- // - no live status reported (node/session absent from live mesh), OR
238
- // - payload flagged idle-at-dispatch AND session is still not generating (redundant guard for clarity).
239
- const isNoTransition = !terminalStatus && !live.status;
240
- const isIdleUnacknowledged = status === 'idle';
241
- const ledgerOnlyStaleReason = !terminalRow && (isIdleUnacknowledged || isNoTransition || (dispatchedToIdleSession && isIdleUnacknowledged))
242
- ? 'direct task dispatch has no provider acknowledgement, transcript append, or active runtime transition'
243
- : undefined;
244
- const message = readString(dispatch.payload?.message) || readString(dispatch.payload?.summary) || '';
245
- const { title, summary } = summarizeMessage(message);
246
- // A dispatch is a fresh unacknowledged failure when the node and session both still exist
247
- // in the live mesh but the session never transitioned to generating. This is distinct from
248
- // historical/orphaned stale entries where the node or session is gone from the live mesh.
249
- // fresh-unacknowledged: ledgerOnlyStaleReason is set (session didn't ack) AND no live.staleReason (node/session is present)
250
- const isFreshUnacknowledged = Boolean(ledgerOnlyStaleReason && !live.staleReason);
251
- const record: MeshActiveWorkRecord = {
252
- taskId,
253
- source: 'direct',
254
- status,
255
- nodeId: dispatch.nodeId,
256
- sessionId: dispatch.sessionId,
257
- providerType: dispatch.providerType || readString(dispatch.payload?.providerType),
258
- taskTitle: readString(dispatch.payload?.taskTitle) || title,
259
- taskSummary: readString(dispatch.payload?.taskSummary) || summary,
260
- message,
261
- taskMode: readString(dispatch.payload?.taskMode),
262
- createdAt: dispatch.timestamp,
263
- updatedAt: terminal?.timestamp || dispatch.timestamp,
264
- dispatchedAt: dispatch.timestamp,
265
- elapsedMs: elapsedSince(dispatch.timestamp, now),
266
- terminal: terminalRow,
267
- terminalKind: terminal?.kind,
268
- terminalAt: terminal?.timestamp,
269
- staleReason: live.staleReason || ledgerOnlyStaleReason,
270
- ...(isFreshUnacknowledged ? { staleDispatchUnacknowledged: true } : {}),
271
- };
272
- if (terminalRow) {
273
- terminalDirectWork.push(record);
274
- if (opts.includeTerminalDirect !== true) continue;
227
+ // When BeadsDB direct dispatches are provided, use them for LOCAL dispatches (O(1) indexed).
228
+ // ALSO scan ledger for remote dispatches (P2P) whose taskIds are not in BeadsDB — these
229
+ // are never written to the local BeadsDB since they're dispatched from a remote daemon.
230
+ if (opts.directDispatches !== undefined) {
231
+ const dbTaskIds = new Set(opts.directDispatches.map(d => d.taskId));
232
+ for (const dispatch of opts.directDispatches) {
233
+ const live = sessionStatusFromNodes(opts.nodes, dispatch.nodeId ?? undefined, dispatch.sessionId ?? undefined);
234
+ const dbStatus = dispatch.status; // 'dispatched' | 'acked' | 'completed' | 'failed' | 'stale'
235
+ const isTerminal = dbStatus === 'completed' || dbStatus === 'failed' || dbStatus === 'stale';
236
+ const status: MeshActiveWorkStatus = isTerminal
237
+ ? (dbStatus === 'completed' ? 'idle' : 'failed')
238
+ : live.status || (dbStatus === 'acked' ? 'generating' : 'assigned');
239
+ const isNoTransition = !isTerminal && !live.status;
240
+ const isIdleUnacknowledged = status === 'idle' && !isTerminal;
241
+ const ledgerOnlyStaleReason = !isTerminal && (isIdleUnacknowledged || isNoTransition || (dispatch.dispatchedToIdleSession && isIdleUnacknowledged))
242
+ ? 'direct task dispatch has no provider acknowledgement, transcript append, or active runtime transition'
243
+ : undefined;
244
+ const isFreshUnacknowledged = Boolean(ledgerOnlyStaleReason && !live.staleReason);
245
+ const { title, summary } = summarizeMessage(dispatch.message || '');
246
+ const record: MeshActiveWorkRecord = {
247
+ taskId: dispatch.taskId,
248
+ source: 'direct',
249
+ status,
250
+ nodeId: dispatch.nodeId ?? undefined,
251
+ sessionId: dispatch.sessionId ?? undefined,
252
+ providerType: dispatch.providerType ?? undefined,
253
+ taskTitle: title,
254
+ taskSummary: summary,
255
+ message: dispatch.message,
256
+ taskMode: dispatch.taskMode ?? undefined,
257
+ createdAt: dispatch.dispatchedAt,
258
+ updatedAt: dispatch.updatedAt,
259
+ dispatchedAt: dispatch.dispatchedAt,
260
+ elapsedMs: elapsedSince(dispatch.dispatchedAt, now),
261
+ terminal: isTerminal,
262
+ terminalKind: isTerminal ? (dbStatus === 'completed' ? 'task_completed' : 'task_failed') : undefined,
263
+ terminalAt: isTerminal ? dispatch.updatedAt : undefined,
264
+ staleReason: live.staleReason || ledgerOnlyStaleReason,
265
+ ...(isFreshUnacknowledged ? { staleDispatchUnacknowledged: true } : {}),
266
+ };
267
+ if (isTerminal) {
268
+ terminalDirectWork.push(record);
269
+ if (opts.includeTerminalDirect !== true) continue;
270
+ }
271
+ if ((live.staleReason || ledgerOnlyStaleReason) && !isTerminal) {
272
+ staleDirectWork.push(record);
273
+ continue;
274
+ }
275
+ records.push(record);
276
+ }
277
+ // Also scan ledger for remote dispatches (via p2p_direct) whose taskIds are NOT in BeadsDB.
278
+ // Remote daemons write their own local BeadsDB; this coordinator's BeadsDB only has local dispatches.
279
+ const ledgerEntries = (opts.ledgerEntries || []).slice().sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
280
+ const terminals = ledgerEntries.filter(entry => TERMINAL_LEDGER_KINDS.has(entry.kind) || entry.kind === 'task_approval_needed');
281
+ for (const dispatch of ledgerEntries.filter(isDirectDispatch)) {
282
+ const taskId = directDispatchTaskId(dispatch);
283
+ if (dbTaskIds.has(taskId)) continue; // already covered by BeadsDB path above
284
+ const terminal = terminals
285
+ .filter(entry => new Date(entry.timestamp).getTime() >= new Date(dispatch.timestamp).getTime())
286
+ .find(entry => terminalMatchesDispatch(entry, dispatch, taskId));
287
+ const terminalStatus = terminal ? statusFromTerminal(terminal) : undefined;
288
+ const live = sessionStatusFromNodes(opts.nodes, dispatch.nodeId, dispatch.sessionId);
289
+ const status = terminalStatus || live.status || 'assigned';
290
+ const terminalRow = Boolean(terminal && terminal.kind !== 'task_approval_needed');
291
+ const dispatchedToIdleSession = dispatch.payload?.dispatchedToIdleSession === true;
292
+ const isNoTransition = !terminalStatus && !live.status;
293
+ const isIdleUnacknowledged = status === 'idle';
294
+ const ledgerOnlyStaleReason = !terminalRow && (isIdleUnacknowledged || isNoTransition || (dispatchedToIdleSession && isIdleUnacknowledged))
295
+ ? 'direct task dispatch has no provider acknowledgement, transcript append, or active runtime transition'
296
+ : undefined;
297
+ const message = readString(dispatch.payload?.message) || readString(dispatch.payload?.summary) || '';
298
+ const { title, summary } = summarizeMessage(message);
299
+ const isFreshUnacknowledged = Boolean(ledgerOnlyStaleReason && !live.staleReason);
300
+ const record: MeshActiveWorkRecord = {
301
+ taskId,
302
+ source: 'direct',
303
+ status,
304
+ nodeId: dispatch.nodeId,
305
+ sessionId: dispatch.sessionId,
306
+ providerType: dispatch.providerType || readString(dispatch.payload?.providerType),
307
+ taskTitle: readString(dispatch.payload?.taskTitle) || title,
308
+ taskSummary: readString(dispatch.payload?.taskSummary) || summary,
309
+ message,
310
+ taskMode: readString(dispatch.payload?.taskMode),
311
+ createdAt: dispatch.timestamp,
312
+ updatedAt: terminal?.timestamp || dispatch.timestamp,
313
+ dispatchedAt: dispatch.timestamp,
314
+ elapsedMs: elapsedSince(dispatch.timestamp, now),
315
+ terminal: terminalRow,
316
+ terminalKind: terminal?.kind,
317
+ terminalAt: terminal?.timestamp,
318
+ staleReason: live.staleReason || ledgerOnlyStaleReason,
319
+ ...(isFreshUnacknowledged ? { staleDispatchUnacknowledged: true } : {}),
320
+ };
321
+ if (terminalRow) {
322
+ terminalDirectWork.push(record);
323
+ if (opts.includeTerminalDirect !== true) continue;
324
+ }
325
+ if ((live.staleReason || ledgerOnlyStaleReason) && !terminalRow) {
326
+ staleDirectWork.push(record);
327
+ continue;
328
+ }
329
+ records.push(record);
275
330
  }
276
- if ((live.staleReason || ledgerOnlyStaleReason) && !terminalRow) {
277
- staleDirectWork.push(record);
278
- continue;
331
+ } else {
332
+ // Full ledger scan: no BeadsDB direct dispatches available (standalone mode or empty).
333
+ const ledgerEntries = (opts.ledgerEntries || []).slice().sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
334
+ const terminals = ledgerEntries.filter(entry => TERMINAL_LEDGER_KINDS.has(entry.kind) || entry.kind === 'task_approval_needed');
335
+ for (const dispatch of ledgerEntries.filter(isDirectDispatch)) {
336
+ const taskId = directDispatchTaskId(dispatch);
337
+ const terminal = terminals
338
+ .filter(entry => new Date(entry.timestamp).getTime() >= new Date(dispatch.timestamp).getTime())
339
+ .find(entry => terminalMatchesDispatch(entry, dispatch, taskId));
340
+ const terminalStatus = terminal ? statusFromTerminal(terminal) : undefined;
341
+ const live = sessionStatusFromNodes(opts.nodes, dispatch.nodeId, dispatch.sessionId);
342
+ const status = terminalStatus || live.status || 'assigned';
343
+ const terminalRow = Boolean(terminal && terminal.kind !== 'task_approval_needed');
344
+ const dispatchedToIdleSession = dispatch.payload?.dispatchedToIdleSession === true;
345
+ const isNoTransition = !terminalStatus && !live.status;
346
+ const isIdleUnacknowledged = status === 'idle';
347
+ const ledgerOnlyStaleReason = !terminalRow && (isIdleUnacknowledged || isNoTransition || (dispatchedToIdleSession && isIdleUnacknowledged))
348
+ ? 'direct task dispatch has no provider acknowledgement, transcript append, or active runtime transition'
349
+ : undefined;
350
+ const message = readString(dispatch.payload?.message) || readString(dispatch.payload?.summary) || '';
351
+ const { title, summary } = summarizeMessage(message);
352
+ const isFreshUnacknowledged = Boolean(ledgerOnlyStaleReason && !live.staleReason);
353
+ const record: MeshActiveWorkRecord = {
354
+ taskId,
355
+ source: 'direct',
356
+ status,
357
+ nodeId: dispatch.nodeId,
358
+ sessionId: dispatch.sessionId,
359
+ providerType: dispatch.providerType || readString(dispatch.payload?.providerType),
360
+ taskTitle: readString(dispatch.payload?.taskTitle) || title,
361
+ taskSummary: readString(dispatch.payload?.taskSummary) || summary,
362
+ message,
363
+ taskMode: readString(dispatch.payload?.taskMode),
364
+ createdAt: dispatch.timestamp,
365
+ updatedAt: terminal?.timestamp || dispatch.timestamp,
366
+ dispatchedAt: dispatch.timestamp,
367
+ elapsedMs: elapsedSince(dispatch.timestamp, now),
368
+ terminal: terminalRow,
369
+ terminalKind: terminal?.kind,
370
+ terminalAt: terminal?.timestamp,
371
+ staleReason: live.staleReason || ledgerOnlyStaleReason,
372
+ ...(isFreshUnacknowledged ? { staleDispatchUnacknowledged: true } : {}),
373
+ };
374
+ if (terminalRow) {
375
+ terminalDirectWork.push(record);
376
+ if (opts.includeTerminalDirect !== true) continue;
377
+ }
378
+ if ((live.staleReason || ledgerOnlyStaleReason) && !terminalRow) {
379
+ staleDirectWork.push(record);
380
+ continue;
381
+ }
382
+ records.push(record);
279
383
  }
280
- records.push(record);
281
384
  }
282
385
 
283
386
  records.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());