@adhdev/daemon-core 0.9.82-rc.9 → 0.9.82-rc.91

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 (67) hide show
  1. package/dist/cli-adapters/provider-cli-adapter.d.ts +2 -0
  2. package/dist/cli-adapters/provider-cli-parse.d.ts +1 -0
  3. package/dist/cli-adapters/provider-cli-shared.d.ts +2 -0
  4. package/dist/commands/router.d.ts +22 -0
  5. package/dist/config/mesh-config.d.ts +66 -1
  6. package/dist/index.d.ts +13 -6
  7. package/dist/index.js +5417 -1207
  8. package/dist/index.js.map +1 -1
  9. package/dist/index.mjs +5381 -1193
  10. package/dist/index.mjs.map +1 -1
  11. package/dist/installer.d.ts +1 -4
  12. package/dist/launch.d.ts +1 -1
  13. package/dist/logging/async-batch-writer.d.ts +10 -0
  14. package/dist/mesh/beads-db.d.ts +18 -0
  15. package/dist/mesh/mesh-active-work.d.ts +60 -0
  16. package/dist/mesh/mesh-events.d.ts +29 -5
  17. package/dist/mesh/mesh-fast-forward.d.ts +39 -0
  18. package/dist/mesh/mesh-host-ownership.d.ts +9 -0
  19. package/dist/mesh/mesh-ledger.d.ts +38 -1
  20. package/dist/mesh/mesh-work-queue.d.ts +27 -5
  21. package/dist/mesh/refine-config.d.ts +176 -0
  22. package/dist/providers/chat-message-normalization.d.ts +1 -0
  23. package/dist/providers/cli-provider-instance.d.ts +2 -1
  24. package/dist/repo-mesh-types.d.ts +46 -0
  25. package/dist/status/reporter.d.ts +2 -0
  26. package/package.json +3 -1
  27. package/src/boot/daemon-lifecycle.ts +1 -0
  28. package/src/cli-adapters/provider-cli-adapter.ts +91 -3
  29. package/src/cli-adapters/provider-cli-parse.d.ts +1 -0
  30. package/src/cli-adapters/provider-cli-parse.ts +4 -0
  31. package/src/cli-adapters/provider-cli-runtime.ts +3 -1
  32. package/src/cli-adapters/provider-cli-shared.d.ts +2 -0
  33. package/src/cli-adapters/provider-cli-shared.ts +20 -10
  34. package/src/commands/chat-commands.ts +472 -15
  35. package/src/commands/cli-manager.ts +126 -0
  36. package/src/commands/handler.ts +8 -1
  37. package/src/commands/mesh-coordinator.ts +13 -143
  38. package/src/commands/router.ts +2687 -435
  39. package/src/config/chat-history.ts +9 -7
  40. package/src/config/mesh-config.ts +245 -1
  41. package/src/daemon/dev-cli-debug.ts +10 -1
  42. package/src/detection/ide-detector.ts +26 -16
  43. package/src/index.ts +31 -5
  44. package/src/installer.d.ts +1 -1
  45. package/src/installer.ts +8 -6
  46. package/src/launch.d.ts +1 -1
  47. package/src/launch.ts +37 -28
  48. package/src/logging/async-batch-writer.ts +55 -0
  49. package/src/logging/logger.ts +2 -1
  50. package/src/mesh/beads-db.ts +176 -0
  51. package/src/mesh/coordinator-prompt.ts +30 -7
  52. package/src/mesh/mesh-active-work.ts +255 -0
  53. package/src/mesh/mesh-events.ts +400 -47
  54. package/src/mesh/mesh-fast-forward.ts +430 -0
  55. package/src/mesh/mesh-host-ownership.ts +73 -0
  56. package/src/mesh/mesh-ledger.ts +138 -1
  57. package/src/mesh/mesh-work-queue.ts +199 -137
  58. package/src/mesh/refine-config.ts +356 -0
  59. package/src/providers/chat-message-normalization.ts +7 -12
  60. package/src/providers/cli-provider-instance.ts +93 -14
  61. package/src/providers/ide-provider-instance.ts +17 -3
  62. package/src/providers/provider-loader.ts +10 -4
  63. package/src/providers/read-chat-contract.ts +1 -1
  64. package/src/providers/version-archive.ts +38 -20
  65. package/src/repo-mesh-types.ts +51 -0
  66. package/src/status/reporter.ts +15 -0
  67. package/src/system/host-memory.ts +29 -12
@@ -0,0 +1,430 @@
1
+ import type { GitRepoStatus, GitSubmoduleStatus } from '../git/git-types.js';
2
+ import { getGitRepoStatus } from '../git/git-status.js';
3
+ import { GitCommandError, runGit } from '../git/git-executor.js';
4
+
5
+ export interface MeshFastForwardNodeArgs {
6
+ nodeId?: string;
7
+ meshId?: string;
8
+ workspace: string;
9
+ branch?: string;
10
+ execute?: boolean;
11
+ dryRun?: boolean;
12
+ updateSubmodules?: boolean;
13
+ submoduleIgnorePaths?: string[];
14
+ timeoutMs?: number;
15
+ }
16
+
17
+ export interface MeshFastForwardPlannedStep {
18
+ operation: 'refresh_upstream' | 'verify_clean_worktree' | 'verify_fast_forward' | 'merge_ff_only' | 'submodule_update' | 'verify_post_status';
19
+ description: string;
20
+ safe: true;
21
+ willMutateWorktree: boolean;
22
+ }
23
+
24
+ export interface MeshFastForwardResult {
25
+ success: boolean;
26
+ code: string;
27
+ nodeId?: string;
28
+ meshId?: string;
29
+ workspace: string;
30
+ allowed: boolean;
31
+ dryRun: boolean;
32
+ willRun: boolean;
33
+ executed: boolean;
34
+ updateSubmodules: boolean;
35
+ blockingReasons: string[];
36
+ plannedSteps: MeshFastForwardPlannedStep[];
37
+ current?: GitRepoStatus;
38
+ preStatus?: GitRepoStatus;
39
+ postStatus?: GitRepoStatus;
40
+ finalBranchConvergenceState?: Record<string, unknown>;
41
+ operationError?: string;
42
+ ledgerError?: string;
43
+ }
44
+
45
+ type MeshFastForwardBase = Pick<
46
+ MeshFastForwardResult,
47
+ 'workspace' | 'dryRun' | 'updateSubmodules' | 'plannedSteps'
48
+ > & Pick<Partial<MeshFastForwardResult>, 'nodeId' | 'meshId'>;
49
+
50
+ const STATUS_OPTIONS = { refreshUpstream: true, includeSubmodules: true, timeoutMs: 15_000 } as const;
51
+
52
+ export async function fastForwardMeshNode(args: MeshFastForwardNodeArgs): Promise<MeshFastForwardResult> {
53
+ const workspace = typeof args.workspace === 'string' ? args.workspace.trim() : '';
54
+ const nodeId = normalizeOptionalString(args.nodeId);
55
+ const meshId = normalizeOptionalString(args.meshId);
56
+ const requestedBranch = normalizeOptionalString(args.branch);
57
+ const updateSubmodules = args.updateSubmodules === true;
58
+ const dryRun = args.dryRun === true || args.execute !== true;
59
+ const plannedSteps = buildPlannedSteps(updateSubmodules);
60
+ const base: MeshFastForwardBase = {
61
+ ...(nodeId ? { nodeId } : {}),
62
+ ...(meshId ? { meshId } : {}),
63
+ workspace,
64
+ dryRun,
65
+ updateSubmodules,
66
+ plannedSteps,
67
+ };
68
+
69
+ if (!workspace) {
70
+ return block(base, 'invalid_workspace', ['workspace_required']);
71
+ }
72
+
73
+ const current = await getGitRepoStatus(workspace, {
74
+ ...STATUS_OPTIONS,
75
+ submoduleIgnorePaths: args.submoduleIgnorePaths,
76
+ timeoutMs: args.timeoutMs ?? STATUS_OPTIONS.timeoutMs,
77
+ });
78
+
79
+ const earlyBlockers = collectPreflightBlockers(current, requestedBranch);
80
+ if (earlyBlockers.length > 0) {
81
+ return {
82
+ ...block(base, chooseBlockCode(current, earlyBlockers), earlyBlockers),
83
+ current,
84
+ finalBranchConvergenceState: buildConvergenceState(current, codeToConvergenceStatus(chooseBlockCode(current, earlyBlockers))),
85
+ };
86
+ }
87
+
88
+ if (current.behind === 0) {
89
+ const result: MeshFastForwardResult = {
90
+ ...base,
91
+ success: true,
92
+ code: 'already_up_to_date',
93
+ allowed: true,
94
+ willRun: false,
95
+ executed: false,
96
+ blockingReasons: [],
97
+ current,
98
+ preStatus: current,
99
+ postStatus: current,
100
+ finalBranchConvergenceState: buildConvergenceState(current, 'up_to_date'),
101
+ };
102
+ await appendFastForwardLedger(result, 'noop');
103
+ return result;
104
+ }
105
+
106
+ const ancestorCheck = await verifyHeadIsAncestorOfUpstream(workspace, current.upstream || '', args.timeoutMs);
107
+ if (!ancestorCheck.ok) {
108
+ const result: MeshFastForwardResult = {
109
+ ...block(base, 'non_fast_forward', ['head_is_not_ancestor_of_upstream']),
110
+ current,
111
+ preStatus: current,
112
+ operationError: ancestorCheck.error,
113
+ finalBranchConvergenceState: buildConvergenceState(current, 'not_mergeable'),
114
+ };
115
+ await appendFastForwardLedger(result, 'blocked');
116
+ return result;
117
+ }
118
+
119
+ if (dryRun) {
120
+ const result: MeshFastForwardResult = {
121
+ ...base,
122
+ success: true,
123
+ code: 'fast_forward_available',
124
+ allowed: true,
125
+ willRun: false,
126
+ executed: false,
127
+ blockingReasons: [],
128
+ current,
129
+ preStatus: current,
130
+ finalBranchConvergenceState: buildConvergenceState(current, 'fast_forward_available'),
131
+ };
132
+ return result;
133
+ }
134
+
135
+ try {
136
+ await runGit(workspace, ['merge', '--ff-only', current.upstream || ''], { timeoutMs: args.timeoutMs ?? 30_000 });
137
+ } catch (error) {
138
+ const result: MeshFastForwardResult = {
139
+ ...block(base, 'merge_ff_only_failed', ['merge_ff_only_failed']),
140
+ current,
141
+ preStatus: current,
142
+ operationError: formatGitError(error),
143
+ finalBranchConvergenceState: buildConvergenceState(current, 'not_mergeable'),
144
+ };
145
+ await appendFastForwardLedger(result, 'failed');
146
+ return result;
147
+ }
148
+
149
+ let postStatus = await getGitRepoStatus(workspace, {
150
+ ...STATUS_OPTIONS,
151
+ submoduleIgnorePaths: args.submoduleIgnorePaths,
152
+ timeoutMs: args.timeoutMs ?? STATUS_OPTIONS.timeoutMs,
153
+ });
154
+
155
+ const submoduleIssues = collectSubmoduleBlockers(postStatus, 'post');
156
+ let submoduleFollowUpRequired = false;
157
+ let operationError: string | undefined;
158
+ if (submoduleIssues.length > 0) {
159
+ if (updateSubmodules) {
160
+ try {
161
+ await runGit(workspace, ['submodule', 'update', '--init', '--recursive'], { timeoutMs: args.timeoutMs ?? 60_000 });
162
+ postStatus = await getGitRepoStatus(workspace, {
163
+ ...STATUS_OPTIONS,
164
+ submoduleIgnorePaths: args.submoduleIgnorePaths,
165
+ timeoutMs: args.timeoutMs ?? STATUS_OPTIONS.timeoutMs,
166
+ });
167
+ } catch (error) {
168
+ operationError = formatGitError(error);
169
+ }
170
+ } else {
171
+ submoduleFollowUpRequired = true;
172
+ }
173
+ }
174
+
175
+ const postBlockers = collectPostExecutionBlockers(postStatus);
176
+ if (operationError) postBlockers.push('submodule_update_failed');
177
+ if (submoduleFollowUpRequired) postBlockers.push('submodule_update_required');
178
+
179
+ const success = postBlockers.length === 0 || submoduleFollowUpRequired;
180
+ const code = postBlockers.length === 0
181
+ ? 'fast_forward_applied'
182
+ : submoduleFollowUpRequired
183
+ ? 'fast_forward_applied_submodule_update_required'
184
+ : 'post_verify_failed';
185
+ const result: MeshFastForwardResult = {
186
+ ...base,
187
+ success,
188
+ code,
189
+ allowed: true,
190
+ willRun: true,
191
+ executed: true,
192
+ blockingReasons: postBlockers,
193
+ current,
194
+ preStatus: current,
195
+ postStatus,
196
+ ...(operationError ? { operationError } : {}),
197
+ finalBranchConvergenceState: buildConvergenceState(
198
+ postStatus,
199
+ postBlockers.length === 0 ? 'fast_forwarded' : submoduleFollowUpRequired ? 'follow_up_required' : 'post_verify_failed',
200
+ ),
201
+ };
202
+ await appendFastForwardLedger(result, success ? 'executed' : 'failed');
203
+ return result;
204
+ }
205
+
206
+ function buildPlannedSteps(updateSubmodules: boolean): MeshFastForwardPlannedStep[] {
207
+ const steps: MeshFastForwardPlannedStep[] = [
208
+ {
209
+ operation: 'refresh_upstream',
210
+ description: 'Refresh the tracked upstream remote ref before trusting ahead/behind state.',
211
+ safe: true,
212
+ willMutateWorktree: false,
213
+ },
214
+ {
215
+ operation: 'verify_clean_worktree',
216
+ description: 'Require clean staged/modified/untracked/deleted/renamed/conflict/stash/submodule state.',
217
+ safe: true,
218
+ willMutateWorktree: false,
219
+ },
220
+ {
221
+ operation: 'verify_fast_forward',
222
+ description: 'Require ahead=0, behind>0, and HEAD to be an ancestor of the upstream ref.',
223
+ safe: true,
224
+ willMutateWorktree: false,
225
+ },
226
+ {
227
+ operation: 'merge_ff_only',
228
+ description: 'Apply git merge --ff-only against the tracked upstream; no force, reset, rebase, push, or deploy.',
229
+ safe: true,
230
+ willMutateWorktree: true,
231
+ },
232
+ ];
233
+ if (updateSubmodules) {
234
+ steps.push({
235
+ operation: 'submodule_update',
236
+ description: 'If the fast-forward changes gitlinks, run git submodule update --init --recursive and re-verify submodules.',
237
+ safe: true,
238
+ willMutateWorktree: true,
239
+ });
240
+ }
241
+ steps.push({
242
+ operation: 'verify_post_status',
243
+ description: 'Re-read daemon-owned git status and report final branch convergence state.',
244
+ safe: true,
245
+ willMutateWorktree: false,
246
+ });
247
+ return steps;
248
+ }
249
+
250
+ function collectPreflightBlockers(status: GitRepoStatus, requestedBranch?: string): string[] {
251
+ const blockers: string[] = [];
252
+ if (!status.isGitRepo) blockers.push('not_git_repo');
253
+ if (!status.branch) blockers.push('detached_head_or_unknown_branch');
254
+ if (requestedBranch && status.branch !== requestedBranch) blockers.push('branch_mismatch');
255
+ if (!status.upstream) blockers.push('upstream_missing');
256
+ if (status.upstreamStatus !== 'fresh') blockers.push('upstream_not_fresh');
257
+ if (status.hasConflicts) blockers.push('conflicts_present');
258
+ if (status.staged > 0) blockers.push('staged_changes_present');
259
+ if (status.modified > 0) blockers.push('modified_changes_present');
260
+ if (status.untracked > 0) blockers.push('untracked_changes_present');
261
+ if (status.deleted > 0) blockers.push('deleted_changes_present');
262
+ if (status.renamed > 0) blockers.push('renamed_changes_present');
263
+ if (status.stashCount > 0) blockers.push('stash_entries_present');
264
+ blockers.push(...collectSubmoduleBlockers(status, 'pre'));
265
+ if (status.ahead > 0 && status.behind > 0) {
266
+ blockers.push('branch_diverged_from_upstream');
267
+ blockers.push('branch_has_local_commits');
268
+ } else if (status.ahead > 0) blockers.push('branch_has_local_commits');
269
+ return blockers;
270
+ }
271
+
272
+ function collectPostExecutionBlockers(status: GitRepoStatus): string[] {
273
+ const blockers: string[] = [];
274
+ if (!status.isGitRepo) blockers.push('post_not_git_repo');
275
+ if (status.hasConflicts) blockers.push('post_conflicts_present');
276
+ if (status.ahead !== 0) blockers.push('post_branch_ahead');
277
+ if (status.behind !== 0) blockers.push('post_branch_still_behind');
278
+ if (status.staged > 0 || status.modified > 0 || status.untracked > 0 || status.deleted > 0 || status.renamed > 0) {
279
+ blockers.push('post_working_tree_not_clean');
280
+ }
281
+ if (status.stashCount > 0) blockers.push('post_stash_entries_present');
282
+ blockers.push(...collectSubmoduleBlockers(status, 'post'));
283
+ return blockers;
284
+ }
285
+
286
+ function collectSubmoduleBlockers(status: GitRepoStatus, phase: 'pre' | 'post'): string[] {
287
+ const submodules = Array.isArray(status.submodules) ? status.submodules : [];
288
+ const blockers: string[] = [];
289
+ for (const submodule of submodules) {
290
+ if (submodule.error) blockers.push(`${phase}_submodule_status_error:${submodule.path}`);
291
+ if (submodule.dirty) blockers.push(`${phase}_submodule_dirty:${submodule.path}`);
292
+ if (submodule.outOfSync) blockers.push(`${phase}_submodule_out_of_sync:${submodule.path}`);
293
+ }
294
+ return blockers;
295
+ }
296
+
297
+ function chooseBlockCode(status: GitRepoStatus, blockers: string[]): string {
298
+ if (blockers.includes('not_git_repo')) return 'not_git_repo';
299
+ if (blockers.includes('branch_mismatch')) return 'branch_mismatch';
300
+ if (blockers.includes('upstream_missing')) return 'upstream_missing';
301
+ if (blockers.includes('upstream_not_fresh')) return 'upstream_not_fresh';
302
+ if (blockers.some((reason) => reason.includes('submodule'))) return 'submodule_not_clean';
303
+ if (blockers.includes('branch_diverged_from_upstream')) return 'branch_diverged';
304
+ if (blockers.includes('branch_has_local_commits') || status.ahead > 0) return 'branch_ahead';
305
+ if (blockers.some((reason) => reason.includes('changes') || reason.includes('conflicts') || reason.includes('stash'))) return 'dirty_worktree';
306
+ return 'preflight_blocked';
307
+ }
308
+
309
+ function codeToConvergenceStatus(code: string): string {
310
+ if (code === 'branch_diverged' || code === 'branch_ahead' || code === 'non_fast_forward') return 'not_mergeable';
311
+ if (code === 'dirty_worktree' || code === 'submodule_not_clean') return 'blocked_review';
312
+ return 'blocked';
313
+ }
314
+
315
+ async function verifyHeadIsAncestorOfUpstream(workspace: string, upstream: string, timeoutMs?: number): Promise<{ ok: boolean; error?: string }> {
316
+ if (!upstream) return { ok: false, error: 'missing upstream' };
317
+ try {
318
+ await runGit(workspace, ['merge-base', '--is-ancestor', 'HEAD', upstream], { timeoutMs: timeoutMs ?? 15_000 });
319
+ return { ok: true };
320
+ } catch (error) {
321
+ return { ok: false, error: formatGitError(error) };
322
+ }
323
+ }
324
+
325
+ function block(
326
+ base: MeshFastForwardBase,
327
+ code: string,
328
+ blockingReasons: string[],
329
+ ): MeshFastForwardResult {
330
+ const normalizedReasons = normalizeBlockingReasons(blockingReasons);
331
+ return {
332
+ ...base,
333
+ success: false,
334
+ code,
335
+ allowed: false,
336
+ willRun: false,
337
+ executed: false,
338
+ blockingReasons: normalizedReasons,
339
+ };
340
+ }
341
+
342
+ function normalizeBlockingReasons(reasons: string[]): string[] {
343
+ const normalized = new Set<string>();
344
+ for (const reason of reasons) {
345
+ normalized.add(reason);
346
+ }
347
+ if ([
348
+ 'conflicts_present',
349
+ 'staged_changes_present',
350
+ 'modified_changes_present',
351
+ 'untracked_changes_present',
352
+ 'deleted_changes_present',
353
+ 'renamed_changes_present',
354
+ ].some((reason) => normalized.has(reason))) {
355
+ normalized.add('working_tree_not_clean');
356
+ }
357
+ return Array.from(normalized);
358
+ }
359
+
360
+ function buildConvergenceState(status: GitRepoStatus, convergenceStatus: string): Record<string, unknown> {
361
+ return {
362
+ status: convergenceStatus,
363
+ branch: status.branch,
364
+ headCommit: status.headCommit,
365
+ upstream: status.upstream,
366
+ ahead: status.ahead,
367
+ behind: status.behind,
368
+ dirty: status.staged + status.modified + status.untracked + status.deleted + status.renamed > 0 || status.hasConflicts,
369
+ stashCount: status.stashCount,
370
+ submodules: summarizeSubmodules(status.submodules),
371
+ };
372
+ }
373
+
374
+ function summarizeSubmodules(submodules: GitSubmoduleStatus[] | undefined): Array<Record<string, unknown>> {
375
+ return (submodules || []).map((submodule) => ({
376
+ path: submodule.path,
377
+ commit: submodule.commit,
378
+ dirty: submodule.dirty,
379
+ outOfSync: submodule.outOfSync,
380
+ ...(submodule.error ? { error: submodule.error } : {}),
381
+ }));
382
+ }
383
+
384
+ function normalizeOptionalString(value: unknown): string | undefined {
385
+ return typeof value === 'string' && value.trim() ? value.trim() : undefined;
386
+ }
387
+
388
+ function formatGitError(error: unknown): string {
389
+ if (error instanceof GitCommandError) {
390
+ return error.stderr || error.stdout || error.message;
391
+ }
392
+ if (error instanceof Error) return error.message;
393
+ return String(error);
394
+ }
395
+
396
+ async function appendFastForwardLedger(result: MeshFastForwardResult, outcome: 'noop' | 'blocked' | 'executed' | 'failed'): Promise<void> {
397
+ if (!result.meshId) return;
398
+ try {
399
+ const { appendLedgerEntry } = await import('./mesh-ledger.js');
400
+ appendLedgerEntry(result.meshId, {
401
+ kind: 'direct_fast_forward',
402
+ ...(result.nodeId ? { nodeId: result.nodeId } : {}),
403
+ payload: {
404
+ operation: 'mesh_fast_forward_node',
405
+ outcome,
406
+ code: result.code,
407
+ workspace: result.workspace,
408
+ allowed: result.allowed,
409
+ dryRun: result.dryRun,
410
+ willRun: result.willRun,
411
+ executed: result.executed,
412
+ branch: result.postStatus?.branch ?? result.current?.branch,
413
+ upstream: result.postStatus?.upstream ?? result.current?.upstream,
414
+ before: result.current ? {
415
+ headCommit: result.current.headCommit,
416
+ ahead: result.current.ahead,
417
+ behind: result.current.behind,
418
+ } : undefined,
419
+ after: result.postStatus ? {
420
+ headCommit: result.postStatus.headCommit,
421
+ ahead: result.postStatus.ahead,
422
+ behind: result.postStatus.behind,
423
+ } : undefined,
424
+ blockingReasons: result.blockingReasons,
425
+ },
426
+ });
427
+ } catch (error) {
428
+ result.ledgerError = error instanceof Error ? error.message : String(error);
429
+ }
430
+ }
@@ -0,0 +1,73 @@
1
+ import type { RepoMeshDaemonRole, RepoMeshHostMetadata, RepoMeshHostStatus } from '../repo-mesh-types.js';
2
+
3
+ function readObject(value: unknown): Record<string, unknown> | null {
4
+ return value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : null;
5
+ }
6
+
7
+ function readString(value: unknown): string | undefined {
8
+ return typeof value === 'string' && value.trim() ? value.trim() : undefined;
9
+ }
10
+
11
+ export function normalizeMeshDaemonRole(value: unknown): RepoMeshDaemonRole | undefined {
12
+ return value === 'host' || value === 'member' ? value : undefined;
13
+ }
14
+
15
+ export function resolveMeshHostStatus(mesh: unknown): RepoMeshHostStatus {
16
+ const meshRecord = readObject(mesh);
17
+ const raw = readObject(meshRecord?.meshHost);
18
+ const role = normalizeMeshDaemonRole(raw?.role) ?? 'host';
19
+ const pairing = readObject(raw?.pairing);
20
+ const normalized: RepoMeshHostStatus = {
21
+ role,
22
+ canOwnCoordinator: role === 'host',
23
+ canOwnQueue: role === 'host',
24
+ defaulted: !raw,
25
+ };
26
+ const hostDaemonId = readString(raw?.hostDaemonId);
27
+ const hostNodeId = readString(raw?.hostNodeId);
28
+ const hostAddress = readString(raw?.hostAddress);
29
+ if (hostDaemonId) normalized.hostDaemonId = hostDaemonId;
30
+ if (hostNodeId) normalized.hostNodeId = hostNodeId;
31
+ if (hostAddress) normalized.hostAddress = hostAddress;
32
+ if (pairing) {
33
+ const status = pairing.status === 'pairing' || pairing.status === 'paired' || pairing.status === 'rejected' || pairing.status === 'revoked'
34
+ ? pairing.status
35
+ : 'not_configured';
36
+ normalized.pairing = {
37
+ status,
38
+ ...(readString(pairing.tokenId) ? { tokenId: readString(pairing.tokenId) } : {}),
39
+ ...(readString(pairing.joinedAt) ? { joinedAt: readString(pairing.joinedAt) } : {}),
40
+ ...(readString(pairing.lastPairedAt) ? { lastPairedAt: readString(pairing.lastPairedAt) } : {}),
41
+ ...(readString(pairing.lastRejectedAt) ? { lastRejectedAt: readString(pairing.lastRejectedAt) } : {}),
42
+ ...(readString(pairing.expiresAt) ? { expiresAt: readString(pairing.expiresAt) } : {}),
43
+ };
44
+ }
45
+ return normalized;
46
+ }
47
+
48
+ export function isMeshHostOwner(mesh: unknown): boolean {
49
+ return resolveMeshHostStatus(mesh).role === 'host';
50
+ }
51
+
52
+ export function buildMeshHostRequiredFailure(mesh: unknown, operation: string): Record<string, unknown> {
53
+ const meshHost = resolveMeshHostStatus(mesh);
54
+ return {
55
+ success: false,
56
+ code: 'mesh_host_required',
57
+ error: `Mesh Host daemon required for ${operation}; member daemons must pair with the host and cannot own coordinator/queue mutations.`,
58
+ meshHost,
59
+ };
60
+ }
61
+
62
+ export function requireMeshHostQueueOwner(opts?: { ownerRole?: RepoMeshDaemonRole }): void {
63
+ if (opts?.ownerRole === 'member') {
64
+ throw new Error('Mesh Host daemon required to mutate mesh queue; member daemons must use the host-owned queue.');
65
+ }
66
+ }
67
+
68
+ export function createDefaultMeshHostMetadata(): RepoMeshHostMetadata {
69
+ return {
70
+ role: 'host',
71
+ pairing: { status: 'not_configured' },
72
+ };
73
+ }
@@ -13,7 +13,7 @@
13
13
  * Safety: mode 0o600, atomic append via appendFileSync
14
14
  */
15
15
 
16
- import { existsSync, mkdirSync, readFileSync, appendFileSync, statSync, renameSync } from 'fs';
16
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, statSync, renameSync } from 'fs';
17
17
  import { join } from 'path';
18
18
  import { randomUUID } from 'crypto';
19
19
  import { getConfigDir } from '../config/config.js';
@@ -31,11 +31,13 @@ export type MeshLedgerKind =
31
31
  | 'session_stopped'
32
32
  | 'checkpoint_created'
33
33
  | 'node_cloned'
34
+ | 'node_joined'
34
35
  | 'node_removed'
35
36
  | 'coordinator_started'
36
37
  | 'recovery_attempted'
37
38
  | 'ledger_replicated'
38
39
  | 'ledger_reconciled'
40
+ | 'direct_fast_forward'
39
41
  ;
40
42
 
41
43
  export interface MeshLedgerEntry {
@@ -61,6 +63,44 @@ export function isIntentionalCleanupStopEntry(entry: Pick<MeshLedgerEntry, 'kind
61
63
  || payload.source === 'mesh_remove_node');
62
64
  }
63
65
 
66
+ export type MeshWorkerResultStatus = 'completed' | 'failed' | 'blocked' | 'partial' | 'unknown';
67
+ export type MeshProcessArtifactKind = 'process' | 'log' | 'port' | 'window' | 'session' | 'file' | 'url' | 'other';
68
+
69
+ export interface MeshValidationResultArtifact {
70
+ command?: string;
71
+ status: 'passed' | 'failed' | 'skipped' | 'unknown';
72
+ durationMs?: number;
73
+ outputPath?: string;
74
+ summary?: string;
75
+ }
76
+
77
+ export interface MeshProcessArtifact {
78
+ kind: MeshProcessArtifactKind;
79
+ id?: string;
80
+ label?: string;
81
+ locator?: string;
82
+ pid?: number;
83
+ port?: number;
84
+ url?: string;
85
+ path?: string;
86
+ sessionId?: string;
87
+ keepRunning?: boolean;
88
+ metadata?: Record<string, unknown>;
89
+ }
90
+
91
+ export interface MeshWorkerResultArtifact {
92
+ status: MeshWorkerResultStatus;
93
+ classification?: string;
94
+ changedFiles: string[];
95
+ validationResults: MeshValidationResultArtifact[];
96
+ gitStatus?: Record<string, unknown>;
97
+ processArtifacts: MeshProcessArtifact[];
98
+ errors: string[];
99
+ nextAction?: string;
100
+ requiresUserAction: boolean;
101
+ source: 'explicit_metadata' | 'final_summary_json' | 'default';
102
+ }
103
+
64
104
  export interface MeshTaskCompletionEvidence {
65
105
  source: 'agent_status_event';
66
106
  event: 'agent:generating_completed' | 'agent:ready';
@@ -74,6 +114,7 @@ export interface MeshTaskCompletionEvidence {
74
114
  providerSessionId?: string;
75
115
  finalSummaryAvailable: boolean;
76
116
  };
117
+ workerResult: MeshWorkerResultArtifact;
77
118
  git: {
78
119
  status: 'deferred';
79
120
  reason: string;
@@ -96,6 +137,7 @@ export interface BuildTaskCompletionEvidenceOptions {
96
137
  providerType?: string;
97
138
  providerSessionId?: string;
98
139
  finalSummary?: string;
140
+ workerResult?: Record<string, unknown>;
99
141
  completedAt?: string;
100
142
  }
101
143
 
@@ -188,6 +230,100 @@ function getRotatedPath(meshId: string, index: number): string {
188
230
 
189
231
  // ─── Core API ───────────────────────────────────
190
232
 
233
+ function readNonEmptyString(value: unknown): string | undefined {
234
+ return typeof value === 'string' && value.trim() ? value.trim() : undefined;
235
+ }
236
+
237
+ function readStringArray(value: unknown): string[] {
238
+ if (!Array.isArray(value)) return [];
239
+ return value.map(item => readNonEmptyString(item)).filter(Boolean) as string[];
240
+ }
241
+
242
+ function extractJsonObjectFromSummary(summary?: string): Record<string, unknown> | undefined {
243
+ const text = readNonEmptyString(summary);
244
+ if (!text) return undefined;
245
+ const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/i);
246
+ const candidates = [fenced?.[1], text].filter(Boolean) as string[];
247
+ for (const candidate of candidates) {
248
+ const trimmed = candidate.trim();
249
+ if (!trimmed.startsWith('{') || !trimmed.endsWith('}')) continue;
250
+ try {
251
+ const parsed = JSON.parse(trimmed);
252
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) return parsed;
253
+ } catch { /* try next candidate */ }
254
+ }
255
+ return undefined;
256
+ }
257
+
258
+ function normalizeValidationResults(value: unknown): MeshValidationResultArtifact[] {
259
+ if (!Array.isArray(value)) return [];
260
+ return value
261
+ .filter(item => item && typeof item === 'object' && !Array.isArray(item))
262
+ .map((item: any) => {
263
+ const status = ['passed', 'failed', 'skipped', 'unknown'].includes(item.status) ? item.status : 'unknown';
264
+ return {
265
+ ...(readNonEmptyString(item.command) ? { command: readNonEmptyString(item.command) } : {}),
266
+ status,
267
+ ...(Number.isFinite(Number(item.durationMs)) ? { durationMs: Number(item.durationMs) } : {}),
268
+ ...(readNonEmptyString(item.outputPath) ? { outputPath: readNonEmptyString(item.outputPath) } : {}),
269
+ ...(readNonEmptyString(item.summary) ? { summary: readNonEmptyString(item.summary) } : {}),
270
+ };
271
+ });
272
+ }
273
+
274
+ function normalizeProcessArtifacts(value: unknown): MeshProcessArtifact[] {
275
+ if (!Array.isArray(value)) return [];
276
+ const kinds = new Set(['process', 'log', 'port', 'window', 'session', 'file', 'url', 'other']);
277
+ return value
278
+ .filter(item => item && typeof item === 'object' && !Array.isArray(item))
279
+ .map((item: any) => ({
280
+ kind: kinds.has(item.kind) ? item.kind : 'other',
281
+ ...(readNonEmptyString(item.id) ? { id: readNonEmptyString(item.id) } : {}),
282
+ ...(readNonEmptyString(item.label) ? { label: readNonEmptyString(item.label) } : {}),
283
+ ...(readNonEmptyString(item.locator) ? { locator: readNonEmptyString(item.locator) } : {}),
284
+ ...(Number.isFinite(Number(item.pid)) ? { pid: Number(item.pid) } : {}),
285
+ ...(Number.isFinite(Number(item.port)) ? { port: Number(item.port) } : {}),
286
+ ...(readNonEmptyString(item.url) ? { url: readNonEmptyString(item.url) } : {}),
287
+ ...(readNonEmptyString(item.path) ? { path: readNonEmptyString(item.path) } : {}),
288
+ ...(readNonEmptyString(item.sessionId) ? { sessionId: readNonEmptyString(item.sessionId) } : {}),
289
+ ...(typeof item.keepRunning === 'boolean' ? { keepRunning: item.keepRunning } : {}),
290
+ ...(item.metadata && typeof item.metadata === 'object' && !Array.isArray(item.metadata) ? { metadata: item.metadata as Record<string, unknown> } : {}),
291
+ }));
292
+ }
293
+
294
+ export function normalizeMeshWorkerResult(input?: Record<string, unknown>, source: MeshWorkerResultArtifact['source'] = 'explicit_metadata'): MeshWorkerResultArtifact {
295
+ const raw = input && typeof input === 'object' ? input : {};
296
+ const status = ['completed', 'failed', 'blocked', 'partial', 'unknown'].includes(String(raw.status))
297
+ ? raw.status as MeshWorkerResultStatus
298
+ : 'unknown';
299
+ const gitStatus = raw.gitStatus && typeof raw.gitStatus === 'object' && !Array.isArray(raw.gitStatus)
300
+ ? raw.gitStatus as Record<string, unknown>
301
+ : undefined;
302
+ return {
303
+ status,
304
+ ...(readNonEmptyString(raw.classification) ? { classification: readNonEmptyString(raw.classification) } : {}),
305
+ changedFiles: readStringArray(raw.changedFiles),
306
+ validationResults: normalizeValidationResults(raw.validationResults),
307
+ ...(gitStatus ? { gitStatus } : {}),
308
+ processArtifacts: normalizeProcessArtifacts(raw.processArtifacts),
309
+ errors: readStringArray(raw.errors),
310
+ ...(readNonEmptyString(raw.nextAction) ? { nextAction: readNonEmptyString(raw.nextAction) } : {}),
311
+ requiresUserAction: raw.requiresUserAction === true,
312
+ source,
313
+ };
314
+ }
315
+
316
+ function resolveWorkerResult(opts: BuildTaskCompletionEvidenceOptions): MeshWorkerResultArtifact {
317
+ if (opts.workerResult && typeof opts.workerResult === 'object') {
318
+ return normalizeMeshWorkerResult(opts.workerResult, 'explicit_metadata');
319
+ }
320
+ const parsed = extractJsonObjectFromSummary(opts.finalSummary);
321
+ if (parsed) {
322
+ return normalizeMeshWorkerResult(parsed, 'final_summary_json');
323
+ }
324
+ return normalizeMeshWorkerResult(undefined, 'default');
325
+ }
326
+
191
327
  export function buildTaskCompletionEvidence(opts: BuildTaskCompletionEvidenceOptions): MeshTaskCompletionEvidence {
192
328
  const providerSessionId = opts.providerSessionId?.trim() || undefined;
193
329
  const providerType = opts.providerType?.trim() || undefined;
@@ -204,6 +340,7 @@ export function buildTaskCompletionEvidence(opts: BuildTaskCompletionEvidenceOpt
204
340
  providerSessionId,
205
341
  finalSummaryAvailable: typeof opts.finalSummary === 'string' && opts.finalSummary.trim().length > 0,
206
342
  },
343
+ workerResult: resolveWorkerResult(opts),
207
344
  git: {
208
345
  status: 'deferred',
209
346
  reason: 'ordinary_completion_git_status_not_checked',