@adhdev/daemon-core 0.9.82-rc.135 → 0.9.82-rc.137
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-adapter-types.d.ts +1 -0
- package/dist/cli-adapters/cli-script-runner.d.ts +45 -0
- package/dist/cli-adapters/cli-state-engine.d.ts +154 -0
- package/dist/cli-adapters/provider-cli-adapter.d.ts +75 -74
- package/dist/cli-adapters/provider-cli-parse.d.ts +2 -0
- package/dist/cli-adapters/provider-cli-shared.d.ts +6 -0
- package/dist/config/chat-history.d.ts +1 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2624 -1946
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2627 -1954
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/beads-db.d.ts +54 -0
- package/dist/mesh/mesh-active-work.d.ts +7 -1
- package/dist/mesh/mesh-events.d.ts +10 -4
- package/dist/mesh/mesh-ledger.d.ts +21 -1
- package/dist/mesh/mesh-refine-status.d.ts +2 -3
- package/dist/mesh/mesh-work-queue.d.ts +17 -0
- package/dist/mesh/worktree-bootstrap-config.d.ts +2 -4
- package/dist/providers/approval-utils.d.ts +9 -0
- package/dist/repo-mesh-types.d.ts +5 -0
- package/package.json +1 -1
- package/src/cli-adapter-types.d.ts +1 -0
- package/src/cli-adapter-types.ts +1 -0
- package/src/cli-adapters/cli-script-runner.ts +145 -0
- package/src/cli-adapters/cli-state-engine.ts +957 -0
- package/src/cli-adapters/provider-cli-adapter.d.ts +1 -1
- package/src/cli-adapters/provider-cli-adapter.ts +377 -1387
- package/src/cli-adapters/provider-cli-parse.ts +6 -0
- package/src/cli-adapters/provider-cli-shared.ts +6 -0
- package/src/commands/chat-commands.ts +23 -1
- package/src/commands/cli-manager.ts +3 -1
- package/src/commands/router.ts +8 -0
- package/src/config/chat-history.ts +7 -3
- package/src/git/git-worktree.ts +8 -1
- package/src/index.ts +3 -2
- package/src/mesh/beads-db.ts +305 -2
- package/src/mesh/coordinator-prompt.ts +12 -17
- package/src/mesh/mesh-active-work.ts +162 -59
- package/src/mesh/mesh-events.ts +198 -53
- package/src/mesh/mesh-ledger.ts +321 -105
- package/src/mesh/mesh-refine-status.ts +2 -3
- package/src/mesh/mesh-work-queue.ts +116 -120
- package/src/mesh/worktree-bootstrap-config.ts +17 -4
- package/src/providers/approval-utils.ts +27 -0
- package/src/providers/cli-provider-instance.ts +26 -2
- package/src/providers/provider-schema.ts +2 -0
- package/src/repo-mesh-types.ts +10 -0
|
@@ -36,6 +36,8 @@ export function buildCliParseInput(options: {
|
|
|
36
36
|
recentOutputBuffer: string;
|
|
37
37
|
terminalScreenText: string;
|
|
38
38
|
workingDir?: string;
|
|
39
|
+
providerSessionId?: string;
|
|
40
|
+
historySessionId?: string;
|
|
39
41
|
baseMessages: CliChatMessage[];
|
|
40
42
|
partialResponse: string;
|
|
41
43
|
isWaitingForResponse?: boolean;
|
|
@@ -48,6 +50,8 @@ export function buildCliParseInput(options: {
|
|
|
48
50
|
recentOutputBuffer,
|
|
49
51
|
terminalScreenText,
|
|
50
52
|
workingDir,
|
|
53
|
+
providerSessionId,
|
|
54
|
+
historySessionId,
|
|
51
55
|
baseMessages,
|
|
52
56
|
partialResponse,
|
|
53
57
|
isWaitingForResponse,
|
|
@@ -70,6 +74,8 @@ export function buildCliParseInput(options: {
|
|
|
70
74
|
screenText,
|
|
71
75
|
workspace: workingDir,
|
|
72
76
|
workingDir,
|
|
77
|
+
providerSessionId,
|
|
78
|
+
historySessionId,
|
|
73
79
|
screen: buildCliScreenSnapshot(screenText),
|
|
74
80
|
bufferScreen: buildCliScreenSnapshot(buffer),
|
|
75
81
|
recentScreen: buildCliScreenSnapshot(recentBuffer),
|
|
@@ -108,6 +108,8 @@ export interface CliScriptInput {
|
|
|
108
108
|
screenText: string;
|
|
109
109
|
workspace?: string;
|
|
110
110
|
workingDir?: string;
|
|
111
|
+
providerSessionId?: string;
|
|
112
|
+
historySessionId?: string;
|
|
111
113
|
screen: CliScreenSnapshot;
|
|
112
114
|
bufferScreen: CliScreenSnapshot;
|
|
113
115
|
recentScreen: CliScreenSnapshot;
|
|
@@ -161,6 +163,10 @@ export interface CliProviderModule {
|
|
|
161
163
|
requirePromptEchoBeforeSubmit?: boolean;
|
|
162
164
|
/** Allow sending another prompt while the CLI is still generating so users can intervene mid-turn. */
|
|
163
165
|
allowInputDuringGeneration?: boolean;
|
|
166
|
+
/** When true, only transition to idle after the parsed transcript includes a final standard assistant message. */
|
|
167
|
+
requiresFinalAssistantBeforeIdle?: boolean;
|
|
168
|
+
/** When true, allow providers to augment stale snapshot data before parse. Reserved for future use. */
|
|
169
|
+
augmentStaleSnapshot?: boolean;
|
|
164
170
|
/** When provider-owned, daemon treats provider parser output as canonical transcript authority. */
|
|
165
171
|
transcriptAuthority?: 'provider' | 'daemon';
|
|
166
172
|
/** Full context lets provider-owned parsers canonicalize retained history instead of daemon prefix stitching. */
|
|
@@ -650,6 +650,7 @@ function readCliProviderNativeHistory(agentStr: string, args: {
|
|
|
650
650
|
excludeRecentCount: number;
|
|
651
651
|
historyBehavior?: ProviderModule['historyBehavior'];
|
|
652
652
|
scripts?: ProviderScripts;
|
|
653
|
+
excludeInProgressTurn?: boolean;
|
|
653
654
|
}): ReturnType<typeof readProviderChatHistory> & { lookup: 'session' | 'workspace' } {
|
|
654
655
|
if (!args.historySessionId) {
|
|
655
656
|
return {
|
|
@@ -669,6 +670,7 @@ function readCliProviderNativeHistory(agentStr: string, args: {
|
|
|
669
670
|
excludeRecentCount: args.excludeRecentCount,
|
|
670
671
|
historyBehavior: args.historyBehavior,
|
|
671
672
|
scripts: args.scripts as any,
|
|
673
|
+
excludeInProgressTurn: args.excludeInProgressTurn,
|
|
672
674
|
});
|
|
673
675
|
// Native transcripts are keyed by provider/runtime session identity. Falling
|
|
674
676
|
// back to workspace makes concurrent local Codex/Hermes sessions alias each
|
|
@@ -1533,6 +1535,7 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
|
|
|
1533
1535
|
excludeRecentCount: 0,
|
|
1534
1536
|
historyBehavior: provider?.historyBehavior,
|
|
1535
1537
|
scripts: provider?.scripts as any,
|
|
1538
|
+
excludeInProgressTurn: returnedStatus === 'waiting_approval',
|
|
1536
1539
|
});
|
|
1537
1540
|
} catch (error: any) {
|
|
1538
1541
|
const fallbackReason = `native_history_error:${error?.message || String(error)}`;
|
|
@@ -1593,8 +1596,16 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
|
|
|
1593
1596
|
&& nativeHistoryCoverage !== 'partial'
|
|
1594
1597
|
&& nativeHistoryCoverage !== 'unavailable'
|
|
1595
1598
|
&& safeMapping;
|
|
1596
|
-
|
|
1599
|
+
// Sticky native anchor: once native was confirmed for this session, keep using it
|
|
1600
|
+
// even if freshEnough flips false due to PTY buffer activity.
|
|
1601
|
+
const NATIVE_ANCHOR_TTL_MS = 30 * 60_000;
|
|
1602
|
+
const nativeAnchoredAt = (adapter as any).nativeHistoryAnchoredAt ?? 0;
|
|
1603
|
+
const nativeIsAnchored = nativeAnchoredAt > 0
|
|
1604
|
+
&& (Date.now() - nativeAnchoredAt) < NATIVE_ANCHOR_TTL_MS;
|
|
1605
|
+
const allowStaleNativeChatMessages = (adapter.cliType === 'antigravity-cli' || nativeIsAnchored)
|
|
1606
|
+
&& nativeUsableForChatMessages;
|
|
1597
1607
|
if (nativeUsableForChatMessages && (freshEnough || allowStaleNativeChatMessages)) {
|
|
1608
|
+
(adapter as any).nativeHistoryAnchoredAt = Date.now();
|
|
1598
1609
|
selectedMessages = finalizeStreamingMessagesWhenIdle(nativeMessages, returnedStatus);
|
|
1599
1610
|
selectedProviderSessionId = historyProviderSessionId || providerSessionId;
|
|
1600
1611
|
selectedTranscriptAuthority = 'provider';
|
|
@@ -1620,6 +1631,11 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
|
|
|
1620
1631
|
ptyStatusApprovalOnly: true,
|
|
1621
1632
|
});
|
|
1622
1633
|
} else {
|
|
1634
|
+
// Hard failure (no messages, partial coverage, or safeMapping broken) — clear anchor.
|
|
1635
|
+
// Do not clear on mere staleness: PTY can race ahead of native mtime legitimately.
|
|
1636
|
+
if (!nativeUsableForChatMessages && (adapter as any).nativeHistoryAnchoredAt) {
|
|
1637
|
+
(adapter as any).nativeHistoryAnchoredAt = 0;
|
|
1638
|
+
}
|
|
1623
1639
|
const liveCurrentRuntimePtySafe = isCurrentRuntimePtySafelyAttributed({
|
|
1624
1640
|
adapter,
|
|
1625
1641
|
helpers: h,
|
|
@@ -2770,6 +2786,12 @@ export async function handleResolveAction(h: CommandHelpers, args: any): Promise
|
|
|
2770
2786
|
if (buttonIndex < 0) {
|
|
2771
2787
|
return { success: false, error: 'Approval action did not match any visible button' };
|
|
2772
2788
|
}
|
|
2789
|
+
// Idempotency: if the adapter already resolved this approval within cooldown, report
|
|
2790
|
+
// stale_prompt rather than writing a second key to the PTY.
|
|
2791
|
+
if (typeof adapter.isApprovalRecentlyResolved === 'function' && adapter.isApprovalRecentlyResolved()) {
|
|
2792
|
+
LOG.info('Command', `[resolveAction] CLI PTY → stale_prompt (already resolved within cooldown)`);
|
|
2793
|
+
return { success: true, stalePrompt: true, buttonIndex, button: buttons[buttonIndex] ?? button };
|
|
2794
|
+
}
|
|
2773
2795
|
if (typeof adapter.resolveModal === 'function') {
|
|
2774
2796
|
adapter.resolveModal(buttonIndex);
|
|
2775
2797
|
} else {
|
|
@@ -553,7 +553,9 @@ export class DaemonCliManager {
|
|
|
553
553
|
providerSessionId,
|
|
554
554
|
attachExisting,
|
|
555
555
|
);
|
|
556
|
-
|
|
556
|
+
const adapter = new ProviderCliAdapter(resolvedProvider as CliProviderModule, workingDir, cliArgs, extraEnv || {}, transportFactory);
|
|
557
|
+
if (providerSessionId) adapter.updateRuntimeMeta({ providerSessionId });
|
|
558
|
+
return adapter;
|
|
557
559
|
}
|
|
558
560
|
|
|
559
561
|
throw new Error(`No CLI provider found for '${cliType}'. Create a provider.js in providers/cli/${cliType}/`);
|
package/src/commands/router.ts
CHANGED
|
@@ -1434,10 +1434,18 @@ function buildSubmodulePublishRequiredNextStep(entries: MeshRefineSubmoduleReach
|
|
|
1434
1434
|
|
|
1435
1435
|
function resolveRefineryAutoPublishSubmoduleMainCommits(mesh: any, workspace: string): { enabled: boolean; source?: string } {
|
|
1436
1436
|
if (mesh?.policy?.allowAutoPublishSubmoduleMainCommits === true) {
|
|
1437
|
+
process.stderr.write(
|
|
1438
|
+
`[adhdev-mesh] WARNING: allowAutoPublishSubmoduleMainCommits is ENABLED via mesh.policy. `
|
|
1439
|
+
+ `Refinery may push unreachable submodule commits to submodule origin/main without additional user approval.\n`,
|
|
1440
|
+
);
|
|
1437
1441
|
return { enabled: true, source: 'mesh.policy.allowAutoPublishSubmoduleMainCommits' };
|
|
1438
1442
|
}
|
|
1439
1443
|
const loaded = loadMeshRefineConfig(mesh, workspace);
|
|
1440
1444
|
if (loaded.config?.allowAutoPublishSubmoduleMainCommits === true) {
|
|
1445
|
+
process.stderr.write(
|
|
1446
|
+
`[adhdev-mesh] WARNING: allowAutoPublishSubmoduleMainCommits is ENABLED via ${loaded.path || loaded.source}. `
|
|
1447
|
+
+ `Refinery may push unreachable submodule commits to submodule origin/main without additional user approval.\n`,
|
|
1448
|
+
);
|
|
1441
1449
|
return { enabled: true, source: loaded.path || loaded.source };
|
|
1442
1450
|
}
|
|
1443
1451
|
return { enabled: false };
|
|
@@ -1397,6 +1397,7 @@ function callProviderNativeHistoryRead(
|
|
|
1397
1397
|
scripts: ProviderNativeHistoryScripts | undefined,
|
|
1398
1398
|
historySessionId: string | undefined,
|
|
1399
1399
|
workspace?: string,
|
|
1400
|
+
excludeInProgressTurn?: boolean,
|
|
1400
1401
|
): ProviderNativeHistoryReadResult | null {
|
|
1401
1402
|
const fn = getProviderNativeHistoryScript(scripts, canonicalHistory, 'readSession');
|
|
1402
1403
|
if (!fn) return null;
|
|
@@ -1408,7 +1409,8 @@ function callProviderNativeHistoryRead(
|
|
|
1408
1409
|
workspace,
|
|
1409
1410
|
format: canonicalHistory?.format,
|
|
1410
1411
|
watchPath: canonicalHistory?.watchPath,
|
|
1411
|
-
|
|
1412
|
+
excludeInProgressTurn: excludeInProgressTurn === true,
|
|
1413
|
+
args: { sessionId: normalizedSessionId, historySessionId: normalizedSessionId, workspace, excludeInProgressTurn: excludeInProgressTurn === true },
|
|
1412
1414
|
});
|
|
1413
1415
|
if (!result || typeof result !== 'object') return null;
|
|
1414
1416
|
const records = normalizeProviderNativeHistoryRecords(agentType, normalizedSessionId, (result as any).messages || (result as any).records);
|
|
@@ -1430,11 +1432,12 @@ function buildNativeHistoryReadResult(
|
|
|
1430
1432
|
scripts: ProviderNativeHistoryScripts | undefined,
|
|
1431
1433
|
historySessionId: string | undefined,
|
|
1432
1434
|
workspace?: string,
|
|
1435
|
+
excludeInProgressTurn?: boolean,
|
|
1433
1436
|
): ProviderNativeHistoryReadResult | null {
|
|
1434
1437
|
const normalizedSessionId = normalizeSavedHistorySessionId(historySessionId || '');
|
|
1435
1438
|
const normalizedWorkspace = typeof workspace === 'string' ? workspace.trim() : '';
|
|
1436
1439
|
if (!canonicalHistory || (!normalizedSessionId && !normalizedWorkspace) || !isNativeSourceCanonicalHistory(canonicalHistory)) return null;
|
|
1437
|
-
return callProviderNativeHistoryRead(agentType, canonicalHistory, scripts, normalizedSessionId, workspace);
|
|
1440
|
+
return callProviderNativeHistoryRead(agentType, canonicalHistory, scripts, normalizedSessionId, workspace, excludeInProgressTurn);
|
|
1438
1441
|
}
|
|
1439
1442
|
|
|
1440
1443
|
function materializeNativeHistoryToMirror(
|
|
@@ -1490,6 +1493,7 @@ export function readProviderChatHistory(
|
|
|
1490
1493
|
excludeRecentCount?: number;
|
|
1491
1494
|
historyBehavior?: ProviderHistoryBehavior;
|
|
1492
1495
|
scripts?: ProviderNativeHistoryScripts;
|
|
1496
|
+
excludeInProgressTurn?: boolean;
|
|
1493
1497
|
} = {},
|
|
1494
1498
|
): {
|
|
1495
1499
|
messages: HistoryMessage[];
|
|
@@ -1503,7 +1507,7 @@ export function readProviderChatHistory(
|
|
|
1503
1507
|
unavailableReason?: string;
|
|
1504
1508
|
} {
|
|
1505
1509
|
if (isNativeSourceCanonicalHistory(options.canonicalHistory) && (options.historySessionId || options.workspace)) {
|
|
1506
|
-
const nativeResult = buildNativeHistoryReadResult(agentType, options.canonicalHistory, options.scripts, options.historySessionId, options.workspace);
|
|
1510
|
+
const nativeResult = buildNativeHistoryReadResult(agentType, options.canonicalHistory, options.scripts, options.historySessionId, options.workspace, options.excludeInProgressTurn);
|
|
1507
1511
|
if (!nativeResult) return { messages: [], hasMore: false, source: 'native-unavailable' };
|
|
1508
1512
|
return {
|
|
1509
1513
|
...pageHistoryRecords(agentType, nativeResult.records, options.offset || 0, options.limit || 30, options.excludeRecentCount || 0, options.historyBehavior),
|
package/src/git/git-worktree.ts
CHANGED
|
@@ -116,8 +116,11 @@ export async function createWorktree(opts: WorktreeCreateOptions): Promise<Workt
|
|
|
116
116
|
});
|
|
117
117
|
} catch (error: any) {
|
|
118
118
|
const stderr = typeof error.stderr === 'string' ? error.stderr : '';
|
|
119
|
-
// Clean error messages for common failures
|
|
120
119
|
if (/already exists/i.test(stderr)) {
|
|
120
|
+
// Distinguish directory-collision (TOCTOU race) from branch-already-exists
|
|
121
|
+
if (existsSync(targetDir)) {
|
|
122
|
+
throw new Error(`Worktree target directory was created concurrently: ${targetDir}`);
|
|
123
|
+
}
|
|
121
124
|
throw new Error(`Branch '${branch}' already exists or is checked out in another worktree`);
|
|
122
125
|
}
|
|
123
126
|
throw new Error(`git worktree add failed: ${stderr.trim() || error.message}`);
|
|
@@ -170,6 +173,10 @@ export async function removeWorktree(repoRoot: string, worktreePath: string, opt
|
|
|
170
173
|
const stdout = typeof error.stdout === 'string' ? error.stdout : '';
|
|
171
174
|
const detail = `${stderr}\n${stdout}\n${error.message || ''}`;
|
|
172
175
|
if (opts.allowSubmoduleForceFallback && SUBMODULE_WORKTREE_REMOVE_RE.test(detail)) {
|
|
176
|
+
process.stderr.write(
|
|
177
|
+
`[adhdev-mesh] WARNING: git worktree remove --force fallback for submodule worktree '${worktreePath}'. `
|
|
178
|
+
+ `Any uncommitted changes inside submodules will be lost.\n`,
|
|
179
|
+
);
|
|
173
180
|
try {
|
|
174
181
|
await execFileAsync('git', ['worktree', 'remove', '--force', worktreePath], {
|
|
175
182
|
cwd: repoRoot,
|
package/src/index.ts
CHANGED
|
@@ -115,6 +115,7 @@ export type {
|
|
|
115
115
|
RepoMeshLedgerEntryStatus,
|
|
116
116
|
RepoMeshLedgerSummaryStatus,
|
|
117
117
|
RepoMeshLedgerStatus,
|
|
118
|
+
MeshAsyncJobLifecycle,
|
|
118
119
|
} from './repo-mesh-types.js';
|
|
119
120
|
export { DEFAULT_MESH_POLICY } from './repo-mesh-types.js';
|
|
120
121
|
|
|
@@ -195,8 +196,8 @@ export { buildMeshLedgerReconciliationEvidence, buildMeshLedgerReplicaEvidence }
|
|
|
195
196
|
export type { MeshLedgerReconciliationEvidence, MeshLedgerReplicaEvidence, MeshLedgerReplicaStatus } from './mesh/mesh-ledger-reconciliation.js';
|
|
196
197
|
|
|
197
198
|
// ── Mesh Work Queue (GUPP) ──
|
|
198
|
-
export { enqueueTask, getQueue, claimNextTask, updateTaskStatus, updateSessionTaskStatus, cancelTask, requeueTask, getMeshQueueStats, getMeshQueueRevision, normalizeMeshTaskMode, validateMeshTaskModeRequest } from './mesh/mesh-work-queue.js';
|
|
199
|
-
export type { MeshWorkQueueEntry, MeshTaskStatus, MeshTaskMode, MeshWorkQueueStats, MeshQueueMutationOptions, MeshTaskModeValidationResult } from './mesh/mesh-work-queue.js';
|
|
199
|
+
export { enqueueTask, getQueue, claimNextTask, updateTaskStatus, updateSessionTaskStatus, cancelTask, requeueTask, getMeshQueueStats, getMeshQueueRevision, normalizeMeshTaskMode, validateMeshTaskModeRequest, insertDirectDispatch, getActiveDirectDispatches, updateDirectDispatchStatus, cleanupTerminalDirectDispatches, markStaleDirectDispatches } from './mesh/mesh-work-queue.js';
|
|
200
|
+
export type { MeshWorkQueueEntry, MeshTaskStatus, MeshTaskMode, MeshWorkQueueStats, MeshQueueMutationOptions, MeshTaskModeValidationResult, DirectDispatchRecord } from './mesh/mesh-work-queue.js';
|
|
200
201
|
export { buildCompactStaleDirectWorkSummary, buildMeshActiveWork, buildMeshActiveWorkSummary } from './mesh/mesh-active-work.js';
|
|
201
202
|
export type { MeshActiveWorkRecord, MeshActiveWorkStatus, MeshActiveWorkSummary, MeshActiveWorkSource, MeshStaleDirectWorkSummary } from './mesh/mesh-active-work.js';
|
|
202
203
|
export { buildMeshAsyncRefineJobs } from './mesh/mesh-refine-status.js';
|
package/src/mesh/beads-db.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync } from 'fs';
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, statSync } from 'fs';
|
|
2
2
|
import { dirname, join } from 'path';
|
|
3
3
|
import { createRequire } from 'module';
|
|
4
4
|
import { getLedgerDir } from './mesh-ledger.js';
|
|
@@ -28,12 +28,18 @@ function legacyQueuePath(meshId: string): string {
|
|
|
28
28
|
export class BeadsDB {
|
|
29
29
|
private static instance: BeadsDB | undefined;
|
|
30
30
|
private readonly db: DatabaseHandle;
|
|
31
|
+
private readonly dbPath: string;
|
|
31
32
|
private readonly migratedMeshIds = new Set<string>();
|
|
33
|
+
private fingerprintSweepCounter = 0;
|
|
34
|
+
private walWriteCounter = 0;
|
|
35
|
+
private static readonly WAL_CHECK_INTERVAL = 500;
|
|
36
|
+
private static readonly WAL_MAX_BYTES = 50 * 1024 * 1024; // 50 MB
|
|
32
37
|
|
|
33
38
|
private constructor(dbPath: string) {
|
|
34
39
|
const dir = dirname(dbPath);
|
|
35
40
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
36
41
|
|
|
42
|
+
this.dbPath = dbPath;
|
|
37
43
|
this.db = new (loadDatabaseCtor())(dbPath);
|
|
38
44
|
this.db.pragma('journal_mode = WAL');
|
|
39
45
|
this.db.pragma('synchronous = NORMAL');
|
|
@@ -81,9 +87,72 @@ export class BeadsDB {
|
|
|
81
87
|
ON mesh_queue(mesh_id, status, created_at);
|
|
82
88
|
CREATE INDEX IF NOT EXISTS idx_mesh_queue_assignment
|
|
83
89
|
ON mesh_queue(mesh_id, assigned_node_id, assigned_session_id, status);
|
|
90
|
+
|
|
91
|
+
CREATE TABLE IF NOT EXISTS mesh_completion_fingerprints (
|
|
92
|
+
fingerprint TEXT PRIMARY KEY,
|
|
93
|
+
expires_at INTEGER NOT NULL
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
CREATE TABLE IF NOT EXISTS mesh_direct_dispatches (
|
|
97
|
+
task_id TEXT PRIMARY KEY,
|
|
98
|
+
mesh_id TEXT NOT NULL,
|
|
99
|
+
node_id TEXT,
|
|
100
|
+
session_id TEXT,
|
|
101
|
+
provider_type TEXT,
|
|
102
|
+
message TEXT NOT NULL,
|
|
103
|
+
task_mode TEXT,
|
|
104
|
+
via TEXT NOT NULL,
|
|
105
|
+
status TEXT NOT NULL DEFAULT 'dispatched',
|
|
106
|
+
dispatched_to_idle_session INTEGER NOT NULL DEFAULT 0,
|
|
107
|
+
dispatched_at TEXT NOT NULL,
|
|
108
|
+
updated_at TEXT NOT NULL
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
CREATE INDEX IF NOT EXISTS idx_direct_dispatches_mesh_session
|
|
112
|
+
ON mesh_direct_dispatches(mesh_id, session_id, status);
|
|
84
113
|
`);
|
|
85
114
|
}
|
|
86
115
|
|
|
116
|
+
hasCompletionFingerprint(fingerprint: string): boolean {
|
|
117
|
+
const now = Date.now();
|
|
118
|
+
const row = this.db
|
|
119
|
+
.prepare('SELECT 1 FROM mesh_completion_fingerprints WHERE fingerprint = ? AND expires_at > ?')
|
|
120
|
+
.get(fingerprint, now) as { 1: number } | undefined;
|
|
121
|
+
// Sweep expired fingerprints every 100 reads so stale rows don't accumulate
|
|
122
|
+
// even during read-heavy (non-write) periods when recordFingerprintSeen is idle.
|
|
123
|
+
if (++this.fingerprintSweepCounter >= 100) {
|
|
124
|
+
this.fingerprintSweepCounter = 0;
|
|
125
|
+
this.sweepExpiredFingerprints();
|
|
126
|
+
}
|
|
127
|
+
return row !== undefined;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
recordCompletionFingerprint(fingerprint: string, ttlMs: number): void {
|
|
131
|
+
const expiresAt = Date.now() + ttlMs;
|
|
132
|
+
this.db.prepare('INSERT OR REPLACE INTO mesh_completion_fingerprints (fingerprint, expires_at) VALUES (?, ?)')
|
|
133
|
+
.run(fingerprint, expiresAt);
|
|
134
|
+
this.maybeCheckpointWal();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
sweepExpiredFingerprints(): void {
|
|
138
|
+
this.db.prepare('DELETE FROM mesh_completion_fingerprints WHERE expires_at <= ?').run(Date.now());
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private maybeCheckpointWal(): void {
|
|
142
|
+
if (++this.walWriteCounter < BeadsDB.WAL_CHECK_INTERVAL) return;
|
|
143
|
+
this.walWriteCounter = 0;
|
|
144
|
+
try {
|
|
145
|
+
const walPath = `${this.dbPath}-wal`;
|
|
146
|
+
if (!existsSync(walPath)) return;
|
|
147
|
+
const size = statSync(walPath).size;
|
|
148
|
+
if (size < BeadsDB.WAL_MAX_BYTES) return;
|
|
149
|
+
process.stderr.write(
|
|
150
|
+
`[adhdev-mesh] WAL file ${Math.round(size / 1024 / 1024)}MB exceeds threshold; forcing checkpoint\n`,
|
|
151
|
+
);
|
|
152
|
+
this.db.pragma('wal_checkpoint(TRUNCATE)');
|
|
153
|
+
} catch { /* best-effort */ }
|
|
154
|
+
}
|
|
155
|
+
|
|
87
156
|
private ensureLegacyQueueMigrated(meshId: string): void {
|
|
88
157
|
if (this.migratedMeshIds.has(meshId)) return;
|
|
89
158
|
this.migratedMeshIds.add(meshId);
|
|
@@ -136,7 +205,8 @@ export class BeadsDB {
|
|
|
136
205
|
const rows = this.db
|
|
137
206
|
.prepare('SELECT id, status, updated_at FROM mesh_queue WHERE mesh_id = ? ORDER BY id ASC')
|
|
138
207
|
.all(meshId) as Array<{ id: string; status: string; updated_at: string }>;
|
|
139
|
-
|
|
208
|
+
// Tab as field delimiter (UUIDs and ISO timestamps never contain tabs).
|
|
209
|
+
return rows.map(row => `${row.id}\t${row.status}\t${row.updated_at}`).join('\n');
|
|
140
210
|
}
|
|
141
211
|
|
|
142
212
|
replaceQueue(meshId: string, queue: MeshWorkQueueEntry[]): void {
|
|
@@ -152,6 +222,7 @@ export class BeadsDB {
|
|
|
152
222
|
`);
|
|
153
223
|
deleteStmt.run(meshId);
|
|
154
224
|
for (const entry of queue) insert.run(this.toRow(entry));
|
|
225
|
+
this.maybeCheckpointWal();
|
|
155
226
|
}
|
|
156
227
|
|
|
157
228
|
deleteQueue(meshId: string): void {
|
|
@@ -159,6 +230,134 @@ export class BeadsDB {
|
|
|
159
230
|
this.migratedMeshIds.delete(meshId);
|
|
160
231
|
}
|
|
161
232
|
|
|
233
|
+
insertQueueEntry(entry: MeshWorkQueueEntry): void {
|
|
234
|
+
this.db.prepare(`
|
|
235
|
+
INSERT INTO mesh_queue (
|
|
236
|
+
id, mesh_id, status, target_node_id, target_session_id,
|
|
237
|
+
assigned_node_id, assigned_session_id, created_at, updated_at, payload
|
|
238
|
+
) VALUES (
|
|
239
|
+
@id, @meshId, @status, @targetNodeId, @targetSessionId,
|
|
240
|
+
@assignedNodeId, @assignedSessionId, @createdAt, @updatedAt, @payload
|
|
241
|
+
)
|
|
242
|
+
`).run(this.toRow(entry));
|
|
243
|
+
this.maybeCheckpointWal();
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
updateQueueEntry(entry: MeshWorkQueueEntry): void {
|
|
247
|
+
const now = new Date().toISOString();
|
|
248
|
+
entry.updatedAt = now;
|
|
249
|
+
this.db.prepare(`
|
|
250
|
+
UPDATE mesh_queue SET
|
|
251
|
+
status = @status,
|
|
252
|
+
target_node_id = @targetNodeId,
|
|
253
|
+
target_session_id = @targetSessionId,
|
|
254
|
+
assigned_node_id = @assignedNodeId,
|
|
255
|
+
assigned_session_id = @assignedSessionId,
|
|
256
|
+
updated_at = @updatedAt,
|
|
257
|
+
payload = @payload
|
|
258
|
+
WHERE id = @id AND mesh_id = @meshId
|
|
259
|
+
`).run(this.toRow(entry));
|
|
260
|
+
this.maybeCheckpointWal();
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
findQueueEntryById(meshId: string, id: string): MeshWorkQueueEntry | null {
|
|
264
|
+
this.ensureLegacyQueueMigrated(meshId);
|
|
265
|
+
const row = this.db.prepare(
|
|
266
|
+
'SELECT payload FROM mesh_queue WHERE id = ? AND mesh_id = ?'
|
|
267
|
+
).get(id, meshId) as { payload: string } | undefined;
|
|
268
|
+
return row ? JSON.parse(row.payload) as MeshWorkQueueEntry : null;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
hasActiveAssignment(meshId: string, sessionId: string, nodeId: string): boolean {
|
|
272
|
+
this.ensureLegacyQueueMigrated(meshId);
|
|
273
|
+
const row = this.db.prepare(`
|
|
274
|
+
SELECT 1 FROM mesh_queue
|
|
275
|
+
WHERE mesh_id = ? AND status = 'assigned'
|
|
276
|
+
AND (assigned_session_id = ? OR assigned_node_id = ?)
|
|
277
|
+
LIMIT 1
|
|
278
|
+
`).get(meshId, sessionId, nodeId);
|
|
279
|
+
return row !== undefined;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// O(1) claim: transaction ensures only one session claims a pending task
|
|
283
|
+
claimNextQueueTask(meshId: string, nodeId: string, sessionId: string): MeshWorkQueueEntry | null {
|
|
284
|
+
return this.transaction(() => {
|
|
285
|
+
this.ensureLegacyQueueMigrated(meshId);
|
|
286
|
+
if (this.hasActiveAssignment(meshId, sessionId, nodeId)) return null;
|
|
287
|
+
|
|
288
|
+
// Priority: session-targeted > node-targeted (no session) > unconstrained
|
|
289
|
+
const row = (
|
|
290
|
+
this.db.prepare(`
|
|
291
|
+
SELECT payload FROM mesh_queue
|
|
292
|
+
WHERE mesh_id = ? AND status = 'pending' AND target_session_id = ?
|
|
293
|
+
ORDER BY created_at ASC LIMIT 1
|
|
294
|
+
`).get(meshId, sessionId) as { payload: string } | undefined
|
|
295
|
+
) || (
|
|
296
|
+
this.db.prepare(`
|
|
297
|
+
SELECT payload FROM mesh_queue
|
|
298
|
+
WHERE mesh_id = ? AND status = 'pending' AND target_node_id = ? AND target_session_id IS NULL
|
|
299
|
+
ORDER BY created_at ASC LIMIT 1
|
|
300
|
+
`).get(meshId, nodeId) as { payload: string } | undefined
|
|
301
|
+
) || (
|
|
302
|
+
this.db.prepare(`
|
|
303
|
+
SELECT payload FROM mesh_queue
|
|
304
|
+
WHERE mesh_id = ? AND status = 'pending' AND target_node_id IS NULL AND target_session_id IS NULL
|
|
305
|
+
ORDER BY created_at ASC LIMIT 1
|
|
306
|
+
`).get(meshId) as { payload: string } | undefined
|
|
307
|
+
);
|
|
308
|
+
if (!row) return null;
|
|
309
|
+
|
|
310
|
+
const entry = JSON.parse(row.payload) as MeshWorkQueueEntry;
|
|
311
|
+
const now = new Date().toISOString();
|
|
312
|
+
entry.status = 'assigned';
|
|
313
|
+
entry.assignedNodeId = nodeId;
|
|
314
|
+
entry.assignedSessionId = sessionId;
|
|
315
|
+
entry.dispatchTimestamp = now;
|
|
316
|
+
entry.updatedAt = now;
|
|
317
|
+
|
|
318
|
+
this.db.prepare(`
|
|
319
|
+
UPDATE mesh_queue SET
|
|
320
|
+
status = 'assigned', assigned_node_id = ?, assigned_session_id = ?,
|
|
321
|
+
updated_at = ?, payload = ?
|
|
322
|
+
WHERE id = ? AND mesh_id = ?
|
|
323
|
+
`).run(nodeId, sessionId, now, JSON.stringify(entry), entry.id, meshId);
|
|
324
|
+
|
|
325
|
+
this.maybeCheckpointWal();
|
|
326
|
+
return entry;
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
getQueueStatsByStatus(meshId: string): { status: string; count: number }[] {
|
|
331
|
+
this.ensureLegacyQueueMigrated(meshId);
|
|
332
|
+
return this.db.prepare(
|
|
333
|
+
`SELECT status, COUNT(*) as count FROM mesh_queue WHERE mesh_id = ? GROUP BY status`
|
|
334
|
+
).all(meshId) as { status: string; count: number }[];
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
getActiveAssignmentDetails(meshId: string): Array<{ id: string; nodeId?: string; sessionId?: string; message: string }> {
|
|
338
|
+
this.ensureLegacyQueueMigrated(meshId);
|
|
339
|
+
const rows = this.db.prepare(`
|
|
340
|
+
SELECT assigned_node_id, assigned_session_id, payload
|
|
341
|
+
FROM mesh_queue WHERE mesh_id = ? AND status = 'assigned'
|
|
342
|
+
`).all(meshId) as Array<{ assigned_node_id: string | null; assigned_session_id: string | null; payload: string }>;
|
|
343
|
+
return rows.map(r => {
|
|
344
|
+
let id = '', message = '';
|
|
345
|
+
try { const e = JSON.parse(r.payload) as MeshWorkQueueEntry; id = e.id; message = e.message; } catch { /* ignore */ }
|
|
346
|
+
return { id, nodeId: r.assigned_node_id ?? undefined, sessionId: r.assigned_session_id ?? undefined, message };
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
findAssignedBySession(meshId: string, sessionId: string, occurredAtIso?: string): MeshWorkQueueEntry | null {
|
|
351
|
+
this.ensureLegacyQueueMigrated(meshId);
|
|
352
|
+
// Use updated_at (≈ dispatchTimestamp when status='assigned') for the occurredAt filter.
|
|
353
|
+
const sql = occurredAtIso
|
|
354
|
+
? `SELECT payload FROM mesh_queue WHERE mesh_id = ? AND assigned_session_id = ? AND status = 'assigned' AND updated_at <= ? ORDER BY updated_at DESC LIMIT 1`
|
|
355
|
+
: `SELECT payload FROM mesh_queue WHERE mesh_id = ? AND assigned_session_id = ? AND status = 'assigned' ORDER BY updated_at DESC LIMIT 1`;
|
|
356
|
+
const args: string[] = occurredAtIso ? [meshId, sessionId, occurredAtIso] : [meshId, sessionId];
|
|
357
|
+
const row = this.db.prepare(sql).get(...args as [string, string, string?]) as { payload: string } | undefined;
|
|
358
|
+
return row ? JSON.parse(row.payload) as MeshWorkQueueEntry : null;
|
|
359
|
+
}
|
|
360
|
+
|
|
162
361
|
private toRow(entry: MeshWorkQueueEntry): Record<string, unknown> {
|
|
163
362
|
return {
|
|
164
363
|
id: entry.id,
|
|
@@ -173,4 +372,108 @@ export class BeadsDB {
|
|
|
173
372
|
payload: JSON.stringify(entry),
|
|
174
373
|
};
|
|
175
374
|
}
|
|
375
|
+
|
|
376
|
+
// ── Direct Dispatch Tracking ─────────────────────────────────────────────
|
|
377
|
+
|
|
378
|
+
insertDirectDispatch(entry: {
|
|
379
|
+
taskId: string;
|
|
380
|
+
meshId: string;
|
|
381
|
+
nodeId?: string;
|
|
382
|
+
sessionId?: string;
|
|
383
|
+
providerType?: string;
|
|
384
|
+
message: string;
|
|
385
|
+
taskMode?: string;
|
|
386
|
+
via: string;
|
|
387
|
+
dispatchedToIdleSession?: boolean;
|
|
388
|
+
dispatchedAt: string;
|
|
389
|
+
}): void {
|
|
390
|
+
const now = new Date().toISOString();
|
|
391
|
+
this.db.prepare(`
|
|
392
|
+
INSERT OR REPLACE INTO mesh_direct_dispatches
|
|
393
|
+
(task_id, mesh_id, node_id, session_id, provider_type, message, task_mode, via,
|
|
394
|
+
status, dispatched_to_idle_session, dispatched_at, updated_at)
|
|
395
|
+
VALUES
|
|
396
|
+
(@taskId, @meshId, @nodeId, @sessionId, @providerType, @message, @taskMode, @via,
|
|
397
|
+
'dispatched', @dispatchedToIdle, @dispatchedAt, @updatedAt)
|
|
398
|
+
`).run({
|
|
399
|
+
taskId: entry.taskId,
|
|
400
|
+
meshId: entry.meshId,
|
|
401
|
+
nodeId: entry.nodeId ?? null,
|
|
402
|
+
sessionId: entry.sessionId ?? null,
|
|
403
|
+
providerType: entry.providerType ?? null,
|
|
404
|
+
message: entry.message,
|
|
405
|
+
taskMode: entry.taskMode ?? null,
|
|
406
|
+
via: entry.via,
|
|
407
|
+
dispatchedToIdle: entry.dispatchedToIdleSession ? 1 : 0,
|
|
408
|
+
dispatchedAt: entry.dispatchedAt,
|
|
409
|
+
updatedAt: now,
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
getActiveDirectDispatches(meshId: string): Array<{
|
|
414
|
+
taskId: string;
|
|
415
|
+
meshId: string;
|
|
416
|
+
nodeId: string | null;
|
|
417
|
+
sessionId: string | null;
|
|
418
|
+
providerType: string | null;
|
|
419
|
+
message: string;
|
|
420
|
+
taskMode: string | null;
|
|
421
|
+
via: string;
|
|
422
|
+
status: string;
|
|
423
|
+
dispatchedToIdleSession: boolean;
|
|
424
|
+
dispatchedAt: string;
|
|
425
|
+
updatedAt: string;
|
|
426
|
+
}> {
|
|
427
|
+
const rows = this.db.prepare(`
|
|
428
|
+
SELECT task_id, mesh_id, node_id, session_id, provider_type, message, task_mode, via,
|
|
429
|
+
status, dispatched_to_idle_session, dispatched_at, updated_at
|
|
430
|
+
FROM mesh_direct_dispatches
|
|
431
|
+
WHERE mesh_id = ? AND status NOT IN ('completed', 'failed', 'stale')
|
|
432
|
+
ORDER BY dispatched_at ASC
|
|
433
|
+
`).all(meshId) as Array<Record<string, unknown>>;
|
|
434
|
+
return rows.map(r => ({
|
|
435
|
+
taskId: r.task_id as string,
|
|
436
|
+
meshId: r.mesh_id as string,
|
|
437
|
+
nodeId: r.node_id as string | null,
|
|
438
|
+
sessionId: r.session_id as string | null,
|
|
439
|
+
providerType: r.provider_type as string | null,
|
|
440
|
+
message: r.message as string,
|
|
441
|
+
taskMode: r.task_mode as string | null,
|
|
442
|
+
via: r.via as string,
|
|
443
|
+
status: r.status as string,
|
|
444
|
+
dispatchedToIdleSession: (r.dispatched_to_idle_session as number) === 1,
|
|
445
|
+
dispatchedAt: r.dispatched_at as string,
|
|
446
|
+
updatedAt: r.updated_at as string,
|
|
447
|
+
}));
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
updateDirectDispatchStatus(meshId: string, sessionId: string, status: 'acked' | 'completed' | 'failed' | 'stale'): void {
|
|
451
|
+
if (!sessionId) return; // never update rows without a session binding
|
|
452
|
+
const now = new Date().toISOString();
|
|
453
|
+
this.db.prepare(`
|
|
454
|
+
UPDATE mesh_direct_dispatches
|
|
455
|
+
SET status = @status, updated_at = @updatedAt
|
|
456
|
+
WHERE mesh_id = @meshId AND session_id = @sessionId
|
|
457
|
+
AND session_id IS NOT NULL
|
|
458
|
+
AND status NOT IN ('completed', 'failed')
|
|
459
|
+
`).run({ status, meshId, sessionId, updatedAt: now });
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
cleanupTerminalDirectDispatches(olderThanMs: number): void {
|
|
463
|
+
const cutoff = new Date(Date.now() - olderThanMs).toISOString();
|
|
464
|
+
this.db.prepare(`
|
|
465
|
+
DELETE FROM mesh_direct_dispatches
|
|
466
|
+
WHERE status IN ('completed', 'failed', 'stale') AND updated_at < ?
|
|
467
|
+
`).run(cutoff);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
markStaleDirectDispatches(meshId: string, olderThanMs: number): void {
|
|
471
|
+
const cutoff = new Date(Date.now() - olderThanMs).toISOString();
|
|
472
|
+
const now = new Date().toISOString();
|
|
473
|
+
this.db.prepare(`
|
|
474
|
+
UPDATE mesh_direct_dispatches
|
|
475
|
+
SET status = 'stale', updated_at = ?
|
|
476
|
+
WHERE mesh_id = ? AND status = 'dispatched' AND dispatched_at < ?
|
|
477
|
+
`).run(now, meshId, cutoff);
|
|
478
|
+
}
|
|
176
479
|
}
|
|
@@ -209,21 +209,16 @@ function buildRulesSection(coordinatorCliType?: string): string {
|
|
|
209
209
|
|
|
210
210
|
return `## Rules
|
|
211
211
|
|
|
212
|
-
- **
|
|
213
|
-
- **
|
|
214
|
-
- **
|
|
215
|
-
- **
|
|
216
|
-
- **
|
|
217
|
-
- **
|
|
218
|
-
- **
|
|
219
|
-
- **
|
|
220
|
-
- **
|
|
221
|
-
- **
|
|
222
|
-
- **
|
|
223
|
-
- **
|
|
224
|
-
- **Clean up worktree nodes.** After a worktree task completes and its changes are merged or checkpointed, call \`mesh_remove_node\` to free resources.
|
|
225
|
-
- **Do not strand completed branches.** A checkpointed or clean feature/worktree branch is not done by itself. Merge/refine it to the mesh default branch, fast-forward obvious clean behind-only branches with \`mesh_fast_forward_node\`, or explicitly report one of \`pushed_feature_branch_needs_merge\`, \`blocked_review\`, \`cleanup_candidate\`, or \`not_mergeable\` with the next action.
|
|
226
|
-
- **Keep Refinery validation project-configurable.** \`mesh_refine_node\` must execute validation from repo mesh/refine config (for example \`.adhdev/refine.{json,yaml,yml}\`, \`.adhdev/repo-mesh-refine.*\`, or \`repo-mesh.refine.*\`). Heuristics are suggestions/scaffolding only, not the execution path.
|
|
227
|
-
- **Treat submodule main reachability as publish-needed.** A \`submodule_reachability_failed\` refine result means the root gitlink points at a submodule commit that is not reachable from the configured submodule remote main branch. Do not treat feature-branch reachability as complete, retry validation blindly, or start code review first. Classify it as \`blocked_review\`, request user approval to push/publish the submodule commit to submodule main, then rerun \`mesh_refine_node\`, unless the mesh or repo refine config explicitly enabled \`allowAutoPublishSubmoduleMainCommits\` and Refinery reports exact path/commit/remote/branch evidence with post-publish verification.
|
|
228
|
-
- **Name worktree branches meaningfully.** Use descriptive names like \`feat/auth-refactor\` or \`fix/build-123\`.${coordinatorNote}`;
|
|
212
|
+
- **Route, don't implement.** Delegate all code reading, analysis, and execution to node agents. Never read source files or run commands in the coordinator — keep context lean.
|
|
213
|
+
- **Front-load task messages.** Include everything the agent needs (files, problem, expected fix) in \`mesh_enqueue_task\` / \`mesh_send_task\`. Append a structured result request at the end: ask the worker to conclude with a JSON block containing \`status\`, \`changedFiles\`, \`gitStatus\`, \`validationResults\`, \`errors\`, \`nextAction\`. The daemon parses this automatically; you can read it from \`mesh_task_history\`.
|
|
214
|
+
- **Reuse idle sessions.** For follow-up, retry, commit/push, or cleanup on the same issue, send only the delta to the existing idle session. Start fresh only for independent work, provider mismatch, transcript contamination, or required worktree isolation.
|
|
215
|
+
- **Respect explicit provider requests.** Map: Hermes → \`hermes-cli\`, Claude/Claude Code → \`claude-cli\`, Codex → \`codex-cli\`, Gemini → \`gemini-cli\`, Antigravity → \`antigravity-cli\`. Never substitute the coordinator's own runtime.
|
|
216
|
+
- **Verify via git, not source.** Use \`mesh_git_status\` to confirm side effects. Treat agent summaries as self-reports, not verification.
|
|
217
|
+
- **Limit parallelism.** Start with 1–2 tasks; scale only on success. Never duplicate a session because \`mesh_read_chat\` shows no final message while tool/terminal activity is ongoing.
|
|
218
|
+
- **Check history first.** Call \`mesh_task_history\` at session start to avoid duplicate work and inform recovery. On failure, read task history before retrying.
|
|
219
|
+
- **Converge branches.** After worktree tasks: refine/fast-forward, or classify as \`pushed_feature_branch_needs_merge\` / \`blocked_review\` / \`cleanup_candidate\` / \`not_mergeable\`. Clean up with \`mesh_remove_node\`.
|
|
220
|
+
- **Refinery is config-driven.** \`mesh_refine_node\` must run validation from \`.adhdev/refine.{json,yaml,yml}\` or \`repo-mesh.refine.*\`. Heuristics are scaffolding only.
|
|
221
|
+
- **Submodule reachability = publish-needed.** \`submodule_reachability_failed\` → classify as \`blocked_review\`, request user approval to push to submodule main, then rerun \`mesh_refine_node\`.
|
|
222
|
+
- **Never fabricate tool results.** Always call the actual tool.
|
|
223
|
+
- **Keep the user informed.** One or two sentences after each delegation round.${coordinatorNote}`;
|
|
229
224
|
}
|