@adhdev/daemon-core 0.9.82-rc.377 → 0.9.82-rc.379
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 +5 -0
- package/dist/cli-adapters/pty-write-chunking.d.ts +34 -0
- package/dist/commands/router.d.ts +3 -470
- package/dist/index.js +298 -218
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +299 -219
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/mesh-coordinator-config.d.ts +21 -0
- package/dist/mesh/mesh-node-identity.d.ts +289 -0
- package/dist/mesh/mesh-refine-gates.d.ts +428 -0
- package/dist/providers/spec/fsm-driver.d.ts +6 -4
- package/package.json +2 -2
- package/src/cli-adapters/provider-cli-adapter.ts +62 -1
- package/src/cli-adapters/pty-write-chunking.ts +106 -0
- package/src/commands/med-family/mesh-crud.ts +28 -1
- package/src/commands/router.ts +104 -3650
- package/src/mesh/mesh-coordinator-config.ts +97 -0
- package/src/mesh/mesh-node-identity.ts +1887 -0
- package/src/mesh/mesh-queue-assignment.ts +34 -0
- package/src/mesh/mesh-refine-gates.ts +1652 -0
- package/src/providers/spec/fsm-driver.ts +13 -26
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mesh refine validation gates & gitlink fast-forward evaluation
|
|
3
|
+
*
|
|
4
|
+
* Extracted from commands/router.ts (behavior-preserving move). Contains:
|
|
5
|
+
* - the MeshCoordinator config-format type
|
|
6
|
+
* - refine validation / patch-equivalence / effective-diff / submodule
|
|
7
|
+
* reachability gates and their summary types + job handles
|
|
8
|
+
* - gitlink trivial-fast-forward evaluation and submodule alignment helpers
|
|
9
|
+
*
|
|
10
|
+
* router.ts re-exports every public symbol from here so existing import paths
|
|
11
|
+
* keep working. `CommandRouterResult` is imported type-only from router.ts
|
|
12
|
+
* (erased at compile time — no runtime import cycle).
|
|
13
|
+
*/
|
|
14
|
+
import type { WorktreeBootstrapState } from '../mesh/worktree-bootstrap-config.js';
|
|
15
|
+
import type { CommandRouterResult } from '../commands/router.js';
|
|
16
|
+
export type MeshCoordinatorConfigFormat = 'claude_mcp_json' | 'hermes_config_yaml';
|
|
17
|
+
type MeshRefineValidationStatus = 'passed' | 'failed' | 'skipped';
|
|
18
|
+
type MeshRefineValidationSummary = {
|
|
19
|
+
status: MeshRefineValidationStatus;
|
|
20
|
+
required: true;
|
|
21
|
+
commandsRun: Array<Record<string, unknown>>;
|
|
22
|
+
bootstrapCommandsRun: Array<Record<string, unknown>>;
|
|
23
|
+
rejectedCommands: Array<Record<string, unknown>>;
|
|
24
|
+
skippedReason?: string;
|
|
25
|
+
failureKind?: string;
|
|
26
|
+
failureCode?: string;
|
|
27
|
+
/** Human-readable cause when failureKind === 'spawn_resolution_failed' (win32 .cmd shim, etc). */
|
|
28
|
+
spawnResolutionError?: string;
|
|
29
|
+
timeoutMs: number;
|
|
30
|
+
outputLimitBytes: number;
|
|
31
|
+
configSource?: string;
|
|
32
|
+
configSourceType?: string;
|
|
33
|
+
suggestions?: unknown[];
|
|
34
|
+
suggestedConfig?: unknown;
|
|
35
|
+
/**
|
|
36
|
+
* M2-3: the bootstrap stage recorded separately from validation so review
|
|
37
|
+
* surfaces can distinguish environment failures from validation failures.
|
|
38
|
+
* cached — worktree_bootstrap was 'ready' (staleInputs unchanged), skipped
|
|
39
|
+
* ran — worktree_bootstrap was stale/never-ran and re-ran successfully
|
|
40
|
+
* failed — bootstrap run failed (refine stops before validation)
|
|
41
|
+
* skipped — refine config validation.bootstrap === 'skip'
|
|
42
|
+
* legacy — deprecated validation.bootstrapCommands path was used
|
|
43
|
+
* not_configured — no bootstrap definition anywhere
|
|
44
|
+
*/
|
|
45
|
+
bootstrap?: {
|
|
46
|
+
stage: 'cached' | 'ran' | 'failed' | 'skipped' | 'legacy' | 'not_configured';
|
|
47
|
+
status?: string;
|
|
48
|
+
skipped?: boolean;
|
|
49
|
+
configSource?: string;
|
|
50
|
+
staleReason?: string;
|
|
51
|
+
error?: string;
|
|
52
|
+
commandsRun?: Array<Record<string, unknown>>;
|
|
53
|
+
};
|
|
54
|
+
/** M2-2: deprecation notices from the refine config (e.g. bootstrapCommands). */
|
|
55
|
+
deprecationWarnings?: string[];
|
|
56
|
+
};
|
|
57
|
+
type MeshRefineStageStatus = 'passed' | 'failed' | 'skipped';
|
|
58
|
+
type MeshRefinePatchEquivalenceSummary = {
|
|
59
|
+
status: MeshRefineStageStatus;
|
|
60
|
+
equivalent: boolean;
|
|
61
|
+
baseHead: string;
|
|
62
|
+
branchHead: string;
|
|
63
|
+
mergeBase?: string;
|
|
64
|
+
mergedTree?: string;
|
|
65
|
+
expectedPatchId?: string;
|
|
66
|
+
actualPatchId?: string;
|
|
67
|
+
durationMs: number;
|
|
68
|
+
error?: string;
|
|
69
|
+
stdout?: string;
|
|
70
|
+
stderr?: string;
|
|
71
|
+
actionableHint?: MeshRefineSubmoduleConflictHint;
|
|
72
|
+
/**
|
|
73
|
+
* Set when a `merge-tree` submodule conflict was reclassified as a trivial
|
|
74
|
+
* gitlink fast-forward and the gate passed via a synthesized merge tree.
|
|
75
|
+
*/
|
|
76
|
+
gitlinkTrivialFastForward?: {
|
|
77
|
+
resolved: boolean;
|
|
78
|
+
gitlinks: Array<{
|
|
79
|
+
path: string;
|
|
80
|
+
baseCommit?: string;
|
|
81
|
+
branchCommit?: string;
|
|
82
|
+
fastForward: boolean;
|
|
83
|
+
}>;
|
|
84
|
+
reason?: string;
|
|
85
|
+
};
|
|
86
|
+
};
|
|
87
|
+
type MeshRefineEffectiveDiffSummary = {
|
|
88
|
+
status: MeshRefineStageStatus;
|
|
89
|
+
/** True when there is at least one root-tree change between base and branch (incl. gitlink bumps). */
|
|
90
|
+
hasEffectiveDiff: boolean;
|
|
91
|
+
baseHead: string;
|
|
92
|
+
branchHead: string;
|
|
93
|
+
/** Root-level paths that differ between base and branch (capped). */
|
|
94
|
+
changedPaths?: string[];
|
|
95
|
+
/** Submodule paths with uncommitted/divergent commits but NO committed gitlink bump in the root tree. */
|
|
96
|
+
submoduleHints?: Array<{
|
|
97
|
+
path: string;
|
|
98
|
+
reason: string;
|
|
99
|
+
}>;
|
|
100
|
+
durationMs: number;
|
|
101
|
+
error?: string;
|
|
102
|
+
stdout?: string;
|
|
103
|
+
stderr?: string;
|
|
104
|
+
};
|
|
105
|
+
type MeshRefineSubmoduleConflictHint = {
|
|
106
|
+
kind: 'submodule_conflict';
|
|
107
|
+
message: string;
|
|
108
|
+
conflicts: Array<{
|
|
109
|
+
path: string;
|
|
110
|
+
baseCommit?: string;
|
|
111
|
+
branchCommit?: string;
|
|
112
|
+
}>;
|
|
113
|
+
nextSteps: string[];
|
|
114
|
+
};
|
|
115
|
+
type MeshRefineSubmoduleAlignmentSummary = {
|
|
116
|
+
status: 'passed' | 'failed' | 'skipped';
|
|
117
|
+
changedGitlinkPaths: string[];
|
|
118
|
+
outOfSyncPaths: string[];
|
|
119
|
+
updatedPaths: string[];
|
|
120
|
+
verifiedPaths: string[];
|
|
121
|
+
durationMs: number;
|
|
122
|
+
reason?: string;
|
|
123
|
+
command?: string;
|
|
124
|
+
error?: string;
|
|
125
|
+
stdout?: string;
|
|
126
|
+
stderr?: string;
|
|
127
|
+
};
|
|
128
|
+
type MeshRefineSubmoduleReachabilityEntry = {
|
|
129
|
+
path: string;
|
|
130
|
+
commit: string;
|
|
131
|
+
reachable: boolean;
|
|
132
|
+
publishRequired?: boolean;
|
|
133
|
+
autoPublishAllowed?: boolean;
|
|
134
|
+
autoPublishAttempted?: boolean;
|
|
135
|
+
autoPublishSucceeded?: boolean;
|
|
136
|
+
autoPublishVerified?: boolean;
|
|
137
|
+
autoPublishRefspec?: string;
|
|
138
|
+
autoPublishSkippedReason?: string;
|
|
139
|
+
importedFromWorktree?: boolean;
|
|
140
|
+
checkedLocal?: boolean;
|
|
141
|
+
localReachable?: boolean;
|
|
142
|
+
remote?: string;
|
|
143
|
+
remoteUrl?: string;
|
|
144
|
+
remoteReachable?: boolean;
|
|
145
|
+
remoteMainBranch?: string;
|
|
146
|
+
remoteMainReachable?: boolean;
|
|
147
|
+
fetchedFromOrigin?: boolean;
|
|
148
|
+
error?: string;
|
|
149
|
+
publishStdout?: string;
|
|
150
|
+
publishStderr?: string;
|
|
151
|
+
};
|
|
152
|
+
type MeshRefineSubmoduleReachabilitySummary = {
|
|
153
|
+
status: MeshRefineStageStatus;
|
|
154
|
+
checked: number;
|
|
155
|
+
unreachable: MeshRefineSubmoduleReachabilityEntry[];
|
|
156
|
+
entries: MeshRefineSubmoduleReachabilityEntry[];
|
|
157
|
+
durationMs: number;
|
|
158
|
+
autoPublishAllowed?: boolean;
|
|
159
|
+
autoPublishPolicySource?: string;
|
|
160
|
+
error?: string;
|
|
161
|
+
};
|
|
162
|
+
export type MeshRefineAsyncJobStatus = 'accepted' | 'completed' | 'failed';
|
|
163
|
+
export type MeshRefineJobHandle = {
|
|
164
|
+
success: true;
|
|
165
|
+
async: true;
|
|
166
|
+
status: MeshRefineAsyncJobStatus;
|
|
167
|
+
jobId: string;
|
|
168
|
+
interactionId: string;
|
|
169
|
+
meshId: string;
|
|
170
|
+
nodeId: string;
|
|
171
|
+
targetNodeId: string;
|
|
172
|
+
targetDaemonId?: string;
|
|
173
|
+
workspace?: string;
|
|
174
|
+
startedAt: string;
|
|
175
|
+
completedAt?: string;
|
|
176
|
+
duplicate?: boolean;
|
|
177
|
+
retryOfJobId?: string;
|
|
178
|
+
/**
|
|
179
|
+
* The coordinator daemon ID that initiated this refine job.
|
|
180
|
+
* When set, events for this job are scoped to that coordinator's
|
|
181
|
+
* pending-events queue instead of the shared broadcast queue.
|
|
182
|
+
*/
|
|
183
|
+
targetCoordinatorDaemonId?: string;
|
|
184
|
+
eventDelivery: {
|
|
185
|
+
pendingEvents: true;
|
|
186
|
+
ledger: true;
|
|
187
|
+
};
|
|
188
|
+
evidence: {
|
|
189
|
+
pendingEventsCommand: 'get_pending_mesh_events';
|
|
190
|
+
ledgerCommand: 'get_mesh_ledger_slice';
|
|
191
|
+
taskHistoryKind: 'task_dispatched' | 'task_completed' | 'task_failed';
|
|
192
|
+
};
|
|
193
|
+
};
|
|
194
|
+
export type MeshRefineTerminalJob = MeshRefineJobHandle & {
|
|
195
|
+
result?: Record<string, unknown>;
|
|
196
|
+
};
|
|
197
|
+
export type MeshRefineBatchJobStatus = 'accepted' | 'completed' | 'failed';
|
|
198
|
+
/**
|
|
199
|
+
* Async handle returned by the batch Refinery the instant a convergence run is
|
|
200
|
+
* accepted. Mirrors {@link MeshRefineJobHandle} (async:true / status:'accepted' +
|
|
201
|
+
* terminal pending-event + ledger delivery) but scopes a whole batch of sibling
|
|
202
|
+
* nodes rather than a single node. The synthetic `batchLabel` is used as the
|
|
203
|
+
* `nodeLabel` for the shared refine event/message renderer.
|
|
204
|
+
*/
|
|
205
|
+
export type MeshRefineBatchJobHandle = {
|
|
206
|
+
success: true;
|
|
207
|
+
async: true;
|
|
208
|
+
batch: true;
|
|
209
|
+
status: MeshRefineBatchJobStatus;
|
|
210
|
+
jobId: string;
|
|
211
|
+
interactionId: string;
|
|
212
|
+
meshId: string;
|
|
213
|
+
batchLabel: string;
|
|
214
|
+
nodeIds: string[];
|
|
215
|
+
nodeCount: number;
|
|
216
|
+
order: string[];
|
|
217
|
+
startedAt: string;
|
|
218
|
+
completedAt?: string;
|
|
219
|
+
duplicate?: boolean;
|
|
220
|
+
targetCoordinatorDaemonId?: string;
|
|
221
|
+
eventDelivery: {
|
|
222
|
+
pendingEvents: true;
|
|
223
|
+
ledger: true;
|
|
224
|
+
};
|
|
225
|
+
evidence: {
|
|
226
|
+
pendingEventsCommand: 'get_pending_mesh_events';
|
|
227
|
+
ledgerCommand: 'get_mesh_ledger_slice';
|
|
228
|
+
taskHistoryKind: 'task_dispatched' | 'task_completed' | 'task_failed';
|
|
229
|
+
};
|
|
230
|
+
};
|
|
231
|
+
export type MeshRefineBatchTerminalJob = MeshRefineBatchJobHandle & {
|
|
232
|
+
result?: Record<string, unknown>;
|
|
233
|
+
};
|
|
234
|
+
export declare function truncateValidationOutput(value: unknown): string;
|
|
235
|
+
/**
|
|
236
|
+
* A spawn-resolution failure is when the executable itself could not be found by
|
|
237
|
+
* the OS spawn boundary — `spawn <cmd> ENOENT` — as opposed to the command
|
|
238
|
+
* running and exiting non-zero. On win32 this is the .cmd-shim case: libuv's
|
|
239
|
+
* spawn search appends only .com/.exe, so a bare `npm`/`npx`/`tsc` (which are
|
|
240
|
+
* .cmd shims) ENOENTs even though it is installed. It carries no stderr, so it
|
|
241
|
+
* must be detected by error.code/syscall, not by string-matching output.
|
|
242
|
+
*/
|
|
243
|
+
export declare function isSpawnResolutionError(error: any): boolean;
|
|
244
|
+
export declare function describeSpawnError(error: any, command: string, spawnResolutionFailed: boolean): string;
|
|
245
|
+
export declare function recordMeshRefineStage(stages: Array<Record<string, unknown>>, stage: string, status: MeshRefineStageStatus, startedAt: number, details?: Record<string, unknown>): void;
|
|
246
|
+
export declare function buildSubmodulePublishRequiredNextStep(entries: MeshRefineSubmoduleReachabilityEntry[]): string;
|
|
247
|
+
/**
|
|
248
|
+
* Async git exec helper used across the synchronous-refine stage pipeline. Bound
|
|
249
|
+
* once in the orchestrator and threaded through RefineContext so every stage runs
|
|
250
|
+
* git the same way (execFile + promisify, utf8). Returns the child's stdout/stderr.
|
|
251
|
+
*/
|
|
252
|
+
export type RefineExecFileAsync = (file: string, args: string[], options: {
|
|
253
|
+
cwd: string;
|
|
254
|
+
encoding: 'utf8';
|
|
255
|
+
}) => Promise<{
|
|
256
|
+
stdout: string;
|
|
257
|
+
stderr: string;
|
|
258
|
+
}>;
|
|
259
|
+
/**
|
|
260
|
+
* Accumulated state shared by the synchronous-refine stages. The orchestrator
|
|
261
|
+
* (executeMeshRefineNodeSynchronously) seeds this in the resolve_refs stage and
|
|
262
|
+
* each later stage reads / extends it. `branchHead` and `patchEquivalence` are the
|
|
263
|
+
* only fields a stage mutates after creation (auto-rebase updates both), so they
|
|
264
|
+
* are carried on the mutable context rather than re-threaded through return types.
|
|
265
|
+
*/
|
|
266
|
+
export interface RefineContext {
|
|
267
|
+
meshId: string;
|
|
268
|
+
nodeId: string;
|
|
269
|
+
args: any;
|
|
270
|
+
refineStages: Array<Record<string, unknown>>;
|
|
271
|
+
execFileAsync: RefineExecFileAsync;
|
|
272
|
+
mesh: any;
|
|
273
|
+
node: any;
|
|
274
|
+
sourceNode: any;
|
|
275
|
+
repoRoot: string;
|
|
276
|
+
branch: string;
|
|
277
|
+
baseBranch: string;
|
|
278
|
+
baseHead: string;
|
|
279
|
+
branchHead: string;
|
|
280
|
+
validationSummary: Awaited<ReturnType<typeof runMeshRefineValidationGate>>;
|
|
281
|
+
patchEquivalence: Awaited<ReturnType<typeof runMeshRefinePatchEquivalenceGate>>;
|
|
282
|
+
submoduleReachability: Awaited<ReturnType<typeof runMeshRefineSubmoduleReachabilityGate>>;
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Stage outcome for the synchronous-refine pipeline. A stage either produces a
|
|
286
|
+
* terminal CommandRouterResult (an early-exit gate failure, or a successful
|
|
287
|
+
* already-merged short-circuit), in which case the orchestrator returns it
|
|
288
|
+
* immediately, or it returns `continue` with the (possibly extended) context for
|
|
289
|
+
* the next stage. This makes the orchestrator a flat sequence of stage calls
|
|
290
|
+
* while preserving the original body's exact early-return control flow.
|
|
291
|
+
*/
|
|
292
|
+
export type RefineStageOutcome = {
|
|
293
|
+
kind: 'terminal';
|
|
294
|
+
result: CommandRouterResult;
|
|
295
|
+
} | {
|
|
296
|
+
kind: 'continue';
|
|
297
|
+
ctx: RefineContext;
|
|
298
|
+
};
|
|
299
|
+
export declare function resolveRefineryAutoPublishSubmoduleMainCommits(mesh: any, workspace: string): {
|
|
300
|
+
enabled: boolean;
|
|
301
|
+
source?: string;
|
|
302
|
+
};
|
|
303
|
+
export declare function runMeshRefinePatchEquivalenceGate(repoRoot: string, baseHead: string, branchHead: string): Promise<MeshRefinePatchEquivalenceSummary>;
|
|
304
|
+
export type MeshWorktreePatchContainmentSummary = {
|
|
305
|
+
/** True only when merging worktreeHead into ref introduces no new patch. */
|
|
306
|
+
contained: boolean;
|
|
307
|
+
ref: string;
|
|
308
|
+
worktreeHead: string;
|
|
309
|
+
mergeBase?: string;
|
|
310
|
+
mergedTree?: string;
|
|
311
|
+
/** patch-id of (ref -> synthesized merge tree); empty string when nothing new is added. */
|
|
312
|
+
residualPatchId?: string;
|
|
313
|
+
durationMs: number;
|
|
314
|
+
/** Set when the check could not run (treated conservatively as NOT contained). */
|
|
315
|
+
error?: string;
|
|
316
|
+
};
|
|
317
|
+
/**
|
|
318
|
+
* Patch-equivalence containment check for the worktree force-cleanup convergence
|
|
319
|
+
* guard. Answers a narrower question than {@link runMeshRefinePatchEquivalenceGate}:
|
|
320
|
+
* "are the worktree branch's changes ALREADY present in `ref` (e.g. origin/main),
|
|
321
|
+
* even though the worktree HEAD's commit SHA is not an ancestor of ref?"
|
|
322
|
+
*
|
|
323
|
+
* This is the cherry-pick / squash / rebase case: the same content landed on the
|
|
324
|
+
* default ref under a different commit SHA, so `merge-base --is-ancestor` (the
|
|
325
|
+
* primary cleanup guard) reports the worktree as un-converged and refuses to
|
|
326
|
+
* remove it. Refinery already accepts patch-equivalent landings via merge-tree +
|
|
327
|
+
* patch-id; this brings the same notion of "convergence" to the cleanup guard.
|
|
328
|
+
*
|
|
329
|
+
* Mechanism: synthesize the merge of `worktreeHead` into `ref` (reusing the same
|
|
330
|
+
* trivial-gitlink-fast-forward handling as the refine gate) and compute the
|
|
331
|
+
* patch-id of (ref -> mergedTree). If that residual diff is EMPTY, merging the
|
|
332
|
+
* worktree adds nothing new on top of ref — its changes are already present there
|
|
333
|
+
* and the worktree is safe to remove. A non-empty residual means the worktree
|
|
334
|
+
* still carries content not in ref, so it is NOT contained and must stay blocked.
|
|
335
|
+
*
|
|
336
|
+
* Conservative by construction: any merge-tree / patch-id failure, a genuine
|
|
337
|
+
* (non-trivial) submodule conflict, or any thrown error yields `contained: false`
|
|
338
|
+
* so an exception can never widen the cleanup allow-list.
|
|
339
|
+
*/
|
|
340
|
+
export declare function checkWorktreeChangesPatchEquivalentInRef(repoRoot: string, ref: string, worktreeHead: string): Promise<MeshWorktreePatchContainmentSummary>;
|
|
341
|
+
/**
|
|
342
|
+
* No-op guard: detect a "silent no-op" merge before the Refinery merge runs.
|
|
343
|
+
*
|
|
344
|
+
* A silent no-op occurs when the refine target branch's ROOT tree is byte-identical
|
|
345
|
+
* to the merge base (origin/main). This is the trap where a submodule (e.g. oss) has
|
|
346
|
+
* real commits but the root branch never committed the gitlink (oss-pointer) bump, so
|
|
347
|
+
* the root diff Refinery would merge is empty. Merging that produces a merge commit with
|
|
348
|
+
* no content change — reported as "success" while the actual work never reaches main.
|
|
349
|
+
*
|
|
350
|
+
* A committed gitlink bump (the legitimate oss-pointer bump) DOES show up in the root
|
|
351
|
+
* tree diff (as a 160000-mode entry), so this guard does NOT block legitimate refines —
|
|
352
|
+
* it only fires when the root tree diff vs base is COMPLETELY empty.
|
|
353
|
+
*
|
|
354
|
+
* Runs after the patch-equivalence gate; the "already merged via other path" case
|
|
355
|
+
* (branch has real changes already present in base) is handled upstream and never
|
|
356
|
+
* reaches here, so an empty root diff at this point is genuinely a no-op.
|
|
357
|
+
*/
|
|
358
|
+
export declare function runMeshRefineEffectiveDiffGate(repoRoot: string, baseHead: string, branchHead: string): Promise<MeshRefineEffectiveDiffSummary>;
|
|
359
|
+
/**
|
|
360
|
+
* Result of evaluating whether a `git merge-tree --write-tree` submodule
|
|
361
|
+
* conflict is in fact a trivial gitlink fast-forward that should pass the
|
|
362
|
+
* patch-equivalence gate.
|
|
363
|
+
*
|
|
364
|
+
* `git merge-tree` (and `git merge` with the default recursive strategy)
|
|
365
|
+
* refuses to 3-way merge gitlinks unless the case is "trivial" — and it
|
|
366
|
+
* treats *any* gitlink that differs across merge-base/base/branch as
|
|
367
|
+
* non-trivial, even when the branch-side commit is a strict descendant of the
|
|
368
|
+
* base-side commit (i.e. a real fast-forward). Refinery only ever wants to
|
|
369
|
+
* accept the branch's recorded gitlink, so a fast-forwardable bump is safe to
|
|
370
|
+
* resolve to the branch side without any conflict.
|
|
371
|
+
*/
|
|
372
|
+
type GitlinkTrivialFastForwardEvaluation = {
|
|
373
|
+
/** True only when the merge-tree conflict is *fully* explained by trivial-ff gitlinks. */
|
|
374
|
+
trivial: boolean;
|
|
375
|
+
/** Why the evaluation declined to treat the conflict as trivial (set when trivial=false). */
|
|
376
|
+
reason?: string;
|
|
377
|
+
/** Per-path detail for the changed gitlinks that were inspected. */
|
|
378
|
+
gitlinks: Array<{
|
|
379
|
+
path: string;
|
|
380
|
+
baseCommit?: string;
|
|
381
|
+
branchCommit?: string;
|
|
382
|
+
fastForward: boolean;
|
|
383
|
+
}>;
|
|
384
|
+
};
|
|
385
|
+
/**
|
|
386
|
+
* Return the changed gitlink paths between base and branch whose advance is a
|
|
387
|
+
* strict fast-forward (the base-side commit is an ancestor of the branch-side
|
|
388
|
+
* commit inside that submodule's repo). These are the paths whose patch-id hunk
|
|
389
|
+
* may legitimately differ when base has advanced the same submodule, so they
|
|
390
|
+
* are safe to exclude from the patch-equivalence comparison. A non-ff (genuinely
|
|
391
|
+
* diverged) gitlink is deliberately excluded from this set so it still fails the
|
|
392
|
+
* gate.
|
|
393
|
+
*/
|
|
394
|
+
export declare function collectFastForwardGitlinkPaths(repoRoot: string, baseHead: string, branchHead: string): string[];
|
|
395
|
+
/**
|
|
396
|
+
* Decide whether a merge-tree submodule conflict between base and branch is a
|
|
397
|
+
* trivial gitlink fast-forward (and nothing else).
|
|
398
|
+
*
|
|
399
|
+
* The conflict is treated as trivial ONLY when:
|
|
400
|
+
* 1. at least one changed gitlink exists,
|
|
401
|
+
* 2. every changed gitlink fast-forwards (base-commit is an ancestor of the
|
|
402
|
+
* branch-commit inside that submodule's repo), and
|
|
403
|
+
* 3. the *only* paths that changed on both sides of the merge (i.e. the paths
|
|
404
|
+
* that could possibly produce a 3-way conflict — the intersection of
|
|
405
|
+
* mergeBase→base and mergeBase→branch changes) are gitlinks. Any
|
|
406
|
+
* overlapping non-gitlink path means a genuine content conflict could be
|
|
407
|
+
* hiding behind the submodule failure, so we keep the block.
|
|
408
|
+
*
|
|
409
|
+
* If any of these fail, the conflict is left as a genuine block. This never
|
|
410
|
+
* passes a regular-file conflict or a diverged (non-ff) gitlink.
|
|
411
|
+
*/
|
|
412
|
+
export declare function evaluateGitlinkTrivialFastForward(repoRoot: string, baseHead: string, branchHead: string): GitlinkTrivialFastForwardEvaluation;
|
|
413
|
+
export declare function alignRefinerySubmodulesAfterMerge(repoRoot: string, previousBaseHead: string, currentHead: string, options?: {
|
|
414
|
+
submoduleIgnorePaths?: string[];
|
|
415
|
+
}): Promise<MeshRefineSubmoduleAlignmentSummary>;
|
|
416
|
+
export declare function runMeshRefineSubmoduleReachabilityGate(repoRoot: string, mergedTree: string, options?: {
|
|
417
|
+
allowAutoPublishSubmoduleMainCommits?: boolean;
|
|
418
|
+
autoPublishPolicySource?: string;
|
|
419
|
+
worktreeRoot?: string;
|
|
420
|
+
}): Promise<MeshRefineSubmoduleReachabilitySummary>;
|
|
421
|
+
export declare function buildMeshRefineValidationPlan(mesh: any, workspace: string): Record<string, unknown>;
|
|
422
|
+
export declare function runMeshRefineValidationGate(mesh: any, workspace: string, opts?: {
|
|
423
|
+
/** M2-2: persisted node bootstrap state for staleness evaluation. */
|
|
424
|
+
persistedBootstrapState?: WorktreeBootstrapState | null;
|
|
425
|
+
/** M2-2: called after an inherit-mode bootstrap run so the caller can persist the new state. */
|
|
426
|
+
onBootstrapStateChange?: (state: WorktreeBootstrapState) => void;
|
|
427
|
+
}): Promise<MeshRefineValidationSummary>;
|
|
428
|
+
export {};
|
|
@@ -2,6 +2,7 @@ import { type SpecPtyEvent } from './adapter.js';
|
|
|
2
2
|
import type { PtyTransportFactory } from '../../cli-adapters/pty-transport.js';
|
|
3
3
|
import { type TraceEntry } from './evaluator.js';
|
|
4
4
|
import { type TransitionEval } from './fsm-evaluator.js';
|
|
5
|
+
import { chunkPreservingSurrogates as chunkPreservingSurrogatesShared } from '../../cli-adapters/pty-write-chunking.js';
|
|
5
6
|
export type DashboardEvent = {
|
|
6
7
|
kind: 'pty_data';
|
|
7
8
|
chunk: string;
|
|
@@ -155,10 +156,11 @@ export interface SpecDriverOpts {
|
|
|
155
156
|
extraCliArgs?: string[];
|
|
156
157
|
}
|
|
157
158
|
export declare function resolveSubmitDelayMs(specBeforeSubmit: number | undefined, text: string): number;
|
|
158
|
-
/**
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
|
|
159
|
+
/** Re-export of the shared surrogate-safe splitter so existing imports of
|
|
160
|
+
* `chunkPreservingSurrogates` from this module keep working. The implementation
|
|
161
|
+
* lives in ../../cli-adapters/pty-write-chunking so the spec driver and the
|
|
162
|
+
* legacy adapter share one definition. */
|
|
163
|
+
export declare const chunkPreservingSurrogates: typeof chunkPreservingSurrogatesShared;
|
|
162
164
|
export declare function guessExt(mime: string): string;
|
|
163
165
|
type HistoryEntry = DriverHistoryEntry;
|
|
164
166
|
export declare class FsmDriver implements ISpecDriver {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adhdev/daemon-core",
|
|
3
|
-
"version": "0.9.82-rc.
|
|
3
|
+
"version": "0.9.82-rc.379",
|
|
4
4
|
"description": "ADHDev daemon core — CDP, IDE detection, providers, command execution",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"author": "vilmire",
|
|
47
47
|
"license": "AGPL-3.0-or-later",
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@adhdev/mesh-shared": "0.9.82-rc.
|
|
49
|
+
"@adhdev/mesh-shared": "0.9.82-rc.379",
|
|
50
50
|
"@adhdev/session-host-core": "*",
|
|
51
51
|
"@agentclientprotocol/sdk": "^0.16.1",
|
|
52
52
|
"ajv": "^8.20.0",
|
|
@@ -27,6 +27,12 @@ import {
|
|
|
27
27
|
type PtyRuntimeTransport,
|
|
28
28
|
type PtyTransportFactory,
|
|
29
29
|
} from './pty-transport.js';
|
|
30
|
+
import {
|
|
31
|
+
WIN32_PTY_WRITE_CHUNK_CHARS,
|
|
32
|
+
WIN32_PTY_WRITE_CHUNK_GAP_MS,
|
|
33
|
+
chunkPreservingSurrogates,
|
|
34
|
+
shouldChunkWin32Write,
|
|
35
|
+
} from './pty-write-chunking.js';
|
|
30
36
|
import {
|
|
31
37
|
buildCliScreenSnapshot,
|
|
32
38
|
compactPromptText,
|
|
@@ -744,6 +750,11 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
744
750
|
`[${this.cliType}] Startup settled (${trigger}, stableMs=${stableMs}, modal=${!!startupModal}) providerDir=${this.providerResolutionMeta.providerDir || '-'} scriptDir=${this.providerResolutionMeta.scriptDir || '-'} scriptsPath=${this.providerResolutionMeta.scriptsPath || '-'}`
|
|
745
751
|
);
|
|
746
752
|
this.onStatusChange?.();
|
|
753
|
+
// Readiness barrier flush: a message queued because the session was not yet
|
|
754
|
+
// ready (sendMessageNow's not_ready_pending_prompt path) has no turn-completion
|
|
755
|
+
// event to trigger its flush. Now that the interactive prompt is up and we are
|
|
756
|
+
// idle, drain it. No-op when the queue is empty or we settled to a modal.
|
|
757
|
+
if (!startupModal) this.schedulePendingOutboundFlush();
|
|
747
758
|
}
|
|
748
759
|
|
|
749
760
|
private scheduleStartupSettleCheck(): void {
|
|
@@ -1153,9 +1164,42 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1153
1164
|
|
|
1154
1165
|
private async writeToPty(data: string): Promise<void> {
|
|
1155
1166
|
if (!this.ptyProcess) throw new Error(`${this.cliName} is not running`);
|
|
1167
|
+
// win32 ConPTY paced write: a single unbounded write beyond ~1KB overflows
|
|
1168
|
+
// the console input pipe and drops LEADING bytes (the "long message gets
|
|
1169
|
+
// truncated, head lost / tail kept" failure). Split a large payload into
|
|
1170
|
+
// bounded, surrogate-safe chunks written with a short gap so the console
|
|
1171
|
+
// reader keeps up. Small payloads (the common case — short prompts, lone
|
|
1172
|
+
// submit keys) still go out in a single write.
|
|
1173
|
+
//
|
|
1174
|
+
// The submit key, when present, is the TAIL of `data` (callers pass
|
|
1175
|
+
// `body + sendKey` for the atomic-submit paths). Because we chunk the
|
|
1176
|
+
// combined string, the submit key always rides in the SAME final write as
|
|
1177
|
+
// the body's tail — the win32 invariant that ConPTY recognizes it as a
|
|
1178
|
+
// submit — and is never emitted before the whole body has been written
|
|
1179
|
+
// (no partial-body submit). Body-only writes (wait_for_echo strategy) have
|
|
1180
|
+
// their submit key sent separately by the caller afterwards, unchanged.
|
|
1181
|
+
if (process.platform === 'win32' && shouldChunkWin32Write(data.length)) {
|
|
1182
|
+
await this.writeWin32Chunked(data);
|
|
1183
|
+
return;
|
|
1184
|
+
}
|
|
1156
1185
|
await this.ptyProcess.write(data);
|
|
1157
1186
|
}
|
|
1158
1187
|
|
|
1188
|
+
/** Write `data` to the PTY in bounded, surrogate-safe chunks with a short
|
|
1189
|
+
* inter-chunk gap (win32 paced write). Awaits each chunk's write and the gap
|
|
1190
|
+
* so the returned promise resolves only after the FINAL chunk (carrying any
|
|
1191
|
+
* trailing submit key) has been written. */
|
|
1192
|
+
private async writeWin32Chunked(data: string): Promise<void> {
|
|
1193
|
+
const chunks = chunkPreservingSurrogates(data, WIN32_PTY_WRITE_CHUNK_CHARS);
|
|
1194
|
+
for (let i = 0; i < chunks.length; i += 1) {
|
|
1195
|
+
if (!this.ptyProcess) throw new Error(`${this.cliName} is not running`);
|
|
1196
|
+
await this.ptyProcess.write(chunks[i]);
|
|
1197
|
+
if (i + 1 < chunks.length) {
|
|
1198
|
+
await new Promise<void>(resolve => setTimeout(resolve, WIN32_PTY_WRITE_CHUNK_GAP_MS));
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1159
1203
|
private resetPendingSendState(reason: string): void {
|
|
1160
1204
|
this.responseBuffer = '';
|
|
1161
1205
|
if (this.responseTimeout) { clearTimeout(this.responseTimeout); this.responseTimeout = null; }
|
|
@@ -1472,7 +1516,24 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1472
1516
|
LOG.info('CLI', `[${this.cliType}] sendMessage recovered idle prompt readiness`);
|
|
1473
1517
|
}
|
|
1474
1518
|
}
|
|
1475
|
-
if (!this.ready)
|
|
1519
|
+
if (!this.ready) {
|
|
1520
|
+
// Readiness barrier (queue-until-ready). A task dispatched the instant a
|
|
1521
|
+
// freshly-spawned session is launched can arrive BEFORE the PTY prints its
|
|
1522
|
+
// interactive prompt (this.ready flips ~2-6s later). Previously this threw
|
|
1523
|
+
// "not ready" and the delegated-task delivery promise requeued the task,
|
|
1524
|
+
// which on win32 raced the auto-launch cooldown and could strand the worker
|
|
1525
|
+
// idle with no work (the "first big message lost" failure). Instead, when the
|
|
1526
|
+
// caller allows queueing, BUFFER the message in the pending-outbound queue and
|
|
1527
|
+
// return — the startup-settle path flips this.ready and flushes the queue once
|
|
1528
|
+
// the prompt is actually up (see resolveStartupState → flushPendingOutboundQueue),
|
|
1529
|
+
// so the message is delivered late rather than dropped. A non-queueable caller
|
|
1530
|
+
// (e.g. an internal flush) still throws so it isn't silently swallowed.
|
|
1531
|
+
if (allowQueue) {
|
|
1532
|
+
this.enqueuePendingOutboundMessage(text, 'not_ready_pending_prompt');
|
|
1533
|
+
return;
|
|
1534
|
+
}
|
|
1535
|
+
throw new Error(`${this.cliName} not ready (status: ${this.engine.currentStatus})`);
|
|
1536
|
+
}
|
|
1476
1537
|
const parsedSessionStatus = typeof parsedStatusBeforeSend?.status === 'string'
|
|
1477
1538
|
? String(parsedStatusBeforeSend.status)
|
|
1478
1539
|
: '';
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared win32 ConPTY paced-write chunking.
|
|
3
|
+
*
|
|
4
|
+
* A single unbounded ConPTY `write()` can overflow the console input pipe and
|
|
5
|
+
* drop LEADING bytes once the payload exceeds ~1KB — the "long task message gets
|
|
6
|
+
* truncated (head lost, tail kept)" failure. The fix is to split a large body
|
|
7
|
+
* into bounded chunks written with a short inter-chunk gap so the console input
|
|
8
|
+
* buffer keeps up. Small bodies still go out in a single write.
|
|
9
|
+
*
|
|
10
|
+
* This module is the SINGLE source of truth for the chunk size / gap / surrogate-
|
|
11
|
+
* safe split so the two write paths that need it — the spec FsmDriver
|
|
12
|
+
* (writeWin32Body) and the legacy ProviderCliAdapter (writeToPty / submit paths)
|
|
13
|
+
* — cannot drift apart and regress on one side (the original bug: "one branch
|
|
14
|
+
* patched, the other not").
|
|
15
|
+
*/
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
// Defensive paced PTY write tuning. 1024 chars per chunk stays comfortably under
|
|
19
|
+
// the ConPTY input-pipe threshold; an 8ms gap lets the console reader drain
|
|
20
|
+
// between chunks without adding meaningful latency to a normal-sized prompt.
|
|
21
|
+
export const WIN32_PTY_WRITE_CHUNK_CHARS = 1024;
|
|
22
|
+
export const WIN32_PTY_WRITE_CHUNK_GAP_MS = 8;
|
|
23
|
+
|
|
24
|
+
/** Split `text` into chunks of at most `size` UTF-16 units without ever cutting
|
|
25
|
+
* between a high and low surrogate (which would corrupt an astral char — emoji,
|
|
26
|
+
* etc. — on the UTF-8 PTY write). */
|
|
27
|
+
export function chunkPreservingSurrogates(text: string, size: number): string[] {
|
|
28
|
+
const chunks: string[] = [];
|
|
29
|
+
let offset = 0;
|
|
30
|
+
while (offset < text.length) {
|
|
31
|
+
let end = Math.min(text.length, offset + size);
|
|
32
|
+
if (end < text.length) {
|
|
33
|
+
const code = text.charCodeAt(end - 1);
|
|
34
|
+
// Boundary lands on a high surrogate → pull back one so the pair stays
|
|
35
|
+
// together in the next chunk.
|
|
36
|
+
if (code >= 0xd800 && code <= 0xdbff) end -= 1;
|
|
37
|
+
}
|
|
38
|
+
if (end <= offset) end = Math.min(text.length, offset + size); // size 1 on a lone surrogate
|
|
39
|
+
chunks.push(text.slice(offset, end));
|
|
40
|
+
offset = end;
|
|
41
|
+
}
|
|
42
|
+
return chunks;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** True when a body of `length` UTF-16 units should be paced into multiple
|
|
46
|
+
* chunks on win32 rather than written in a single PTY write. */
|
|
47
|
+
export function shouldChunkWin32Write(length: number): boolean {
|
|
48
|
+
return length > WIN32_PTY_WRITE_CHUNK_CHARS;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Drive a paced, surrogate-safe chunked write of `text` over a `write(chunk)`
|
|
53
|
+
* sink, calling `onChunkWritten` after each chunk (e.g. to advance an input-
|
|
54
|
+
* activity timestamp) and `onDone` once the final chunk is out. The optional
|
|
55
|
+
* `setTimer` lets the caller own the timer handle (so it can be cleared on
|
|
56
|
+
* shutdown) and supply a custom scheduler in tests; it defaults to setTimeout.
|
|
57
|
+
*
|
|
58
|
+
* Bodies at or below the chunk threshold are written in a SINGLE write — the
|
|
59
|
+
* common case — so this is a no-op pacing wrapper for normal-sized prompts.
|
|
60
|
+
*
|
|
61
|
+
* Returns the chunks that will be written (useful for assertions/logging).
|
|
62
|
+
*/
|
|
63
|
+
export interface PacedWin32WriteOptions {
|
|
64
|
+
write: (chunk: string) => void;
|
|
65
|
+
onChunkWritten?: () => void;
|
|
66
|
+
onDone?: () => void;
|
|
67
|
+
/** Schedule the next chunk; must return a handle the caller can clear.
|
|
68
|
+
* Defaults to setTimeout. */
|
|
69
|
+
setTimer?: (fn: () => void, delayMs: number) => ReturnType<typeof setTimeout>;
|
|
70
|
+
/** Store the pending timer handle so the caller can clear it on shutdown. */
|
|
71
|
+
onTimer?: (handle: ReturnType<typeof setTimeout> | null) => void;
|
|
72
|
+
chunkChars?: number;
|
|
73
|
+
gapMs?: number;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function writeWin32Paced(text: string, opts: PacedWin32WriteOptions): string[] {
|
|
77
|
+
const chunkChars = opts.chunkChars ?? WIN32_PTY_WRITE_CHUNK_CHARS;
|
|
78
|
+
const gapMs = opts.gapMs ?? WIN32_PTY_WRITE_CHUNK_GAP_MS;
|
|
79
|
+
const setTimer = opts.setTimer ?? ((fn, delayMs) => setTimeout(fn, delayMs));
|
|
80
|
+
|
|
81
|
+
if (text.length <= chunkChars) {
|
|
82
|
+
opts.onTimer?.(null);
|
|
83
|
+
opts.write(text);
|
|
84
|
+
opts.onChunkWritten?.();
|
|
85
|
+
opts.onDone?.();
|
|
86
|
+
return [text];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const chunks = chunkPreservingSurrogates(text, chunkChars);
|
|
90
|
+
let idx = 0;
|
|
91
|
+
const writeNext = (): void => {
|
|
92
|
+
opts.onTimer?.(null);
|
|
93
|
+
if (idx >= chunks.length) { opts.onDone?.(); return; }
|
|
94
|
+
opts.write(chunks[idx]);
|
|
95
|
+
opts.onChunkWritten?.();
|
|
96
|
+
idx += 1;
|
|
97
|
+
if (idx < chunks.length) {
|
|
98
|
+
const handle = setTimer(writeNext, gapMs);
|
|
99
|
+
opts.onTimer?.(handle);
|
|
100
|
+
} else {
|
|
101
|
+
opts.onDone?.();
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
writeNext();
|
|
105
|
+
return chunks;
|
|
106
|
+
}
|