@adhdev/daemon-core 0.9.82-rc.9 → 0.9.82-rc.90
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli-adapters/provider-cli-adapter.d.ts +2 -0
- package/dist/cli-adapters/provider-cli-parse.d.ts +1 -0
- package/dist/cli-adapters/provider-cli-shared.d.ts +2 -0
- package/dist/commands/router.d.ts +22 -0
- package/dist/config/mesh-config.d.ts +66 -1
- package/dist/index.d.ts +13 -6
- package/dist/index.js +5395 -1197
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +5359 -1183
- package/dist/index.mjs.map +1 -1
- package/dist/installer.d.ts +1 -4
- package/dist/launch.d.ts +1 -1
- package/dist/logging/async-batch-writer.d.ts +10 -0
- package/dist/mesh/beads-db.d.ts +18 -0
- package/dist/mesh/mesh-active-work.d.ts +60 -0
- package/dist/mesh/mesh-events.d.ts +29 -5
- package/dist/mesh/mesh-fast-forward.d.ts +39 -0
- package/dist/mesh/mesh-host-ownership.d.ts +9 -0
- package/dist/mesh/mesh-ledger.d.ts +38 -1
- package/dist/mesh/mesh-work-queue.d.ts +27 -5
- package/dist/mesh/refine-config.d.ts +176 -0
- package/dist/providers/chat-message-normalization.d.ts +1 -0
- package/dist/providers/cli-provider-instance.d.ts +2 -1
- package/dist/repo-mesh-types.d.ts +46 -0
- package/dist/status/reporter.d.ts +2 -0
- package/package.json +3 -1
- package/src/boot/daemon-lifecycle.ts +1 -0
- package/src/cli-adapters/provider-cli-adapter.ts +91 -3
- package/src/cli-adapters/provider-cli-parse.d.ts +1 -0
- package/src/cli-adapters/provider-cli-parse.ts +4 -0
- package/src/cli-adapters/provider-cli-runtime.ts +3 -1
- package/src/cli-adapters/provider-cli-shared.d.ts +2 -0
- package/src/cli-adapters/provider-cli-shared.ts +20 -10
- package/src/commands/chat-commands.ts +454 -15
- package/src/commands/cli-manager.ts +126 -0
- package/src/commands/handler.ts +8 -1
- package/src/commands/mesh-coordinator.ts +13 -143
- package/src/commands/router.ts +2687 -435
- package/src/config/chat-history.ts +9 -7
- package/src/config/mesh-config.ts +245 -1
- package/src/daemon/dev-cli-debug.ts +10 -1
- package/src/detection/ide-detector.ts +26 -16
- package/src/index.ts +31 -5
- package/src/installer.d.ts +1 -1
- package/src/installer.ts +8 -6
- package/src/launch.d.ts +1 -1
- package/src/launch.ts +37 -28
- package/src/logging/async-batch-writer.ts +55 -0
- package/src/logging/logger.ts +2 -1
- package/src/mesh/beads-db.ts +176 -0
- package/src/mesh/coordinator-prompt.ts +30 -7
- package/src/mesh/mesh-active-work.ts +243 -0
- package/src/mesh/mesh-events.ts +400 -47
- package/src/mesh/mesh-fast-forward.ts +430 -0
- package/src/mesh/mesh-host-ownership.ts +73 -0
- package/src/mesh/mesh-ledger.ts +138 -1
- package/src/mesh/mesh-work-queue.ts +199 -137
- package/src/mesh/refine-config.ts +356 -0
- package/src/providers/chat-message-normalization.ts +3 -1
- package/src/providers/cli-provider-instance.ts +91 -13
- package/src/providers/ide-provider-instance.ts +17 -3
- package/src/providers/provider-loader.ts +10 -4
- package/src/providers/read-chat-contract.ts +1 -1
- package/src/providers/version-archive.ts +38 -20
- package/src/repo-mesh-types.ts +51 -0
- package/src/status/reporter.ts +15 -0
- 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
|
+
}
|
package/src/mesh/mesh-ledger.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* Safety: mode 0o600, atomic append via appendFileSync
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import { existsSync, mkdirSync, readFileSync,
|
|
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',
|