@adhdev/daemon-core 0.9.82-rc.187 → 0.9.82-rc.189
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/boot/daemon-lifecycle.d.ts +1 -0
- package/dist/commands/cli-manager.d.ts +2 -1
- package/dist/commands/router.d.ts +5 -1
- package/dist/git/git-commands.d.ts +2 -0
- package/dist/git/git-types.d.ts +2 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +459 -38
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +458 -38
- package/dist/index.mjs.map +1 -1
- package/dist/providers/cli-provider-instance.d.ts +4 -0
- package/dist/providers/contracts.d.ts +31 -0
- package/dist/providers/sdk/v1/types/common/index.d.ts +35 -1
- package/dist/providers/spec/adapter.d.ts +4 -0
- package/dist/providers/spec/driver.d.ts +10 -1
- package/dist/providers/spec/evaluator.d.ts +9 -1
- package/dist/providers/spec/schema.gen.d.ts +38 -0
- package/dist/providers/spec/types.d.ts +25 -0
- package/dist/repo-mesh-types.d.ts +6 -0
- package/package.json +1 -1
- package/src/boot/daemon-lifecycle.ts +2 -0
- package/src/commands/chat-commands.ts +26 -0
- package/src/commands/cli-manager.ts +52 -14
- package/src/commands/router.ts +35 -4
- package/src/git/git-commands.ts +20 -2
- package/src/git/git-status.ts +35 -6
- package/src/git/git-types.ts +2 -0
- package/src/index.ts +1 -1
- package/src/mesh/mesh-events.ts +7 -0
- package/src/providers/cli-provider-instance.ts +110 -9
- package/src/providers/contracts.d.ts +55 -0
- package/src/providers/contracts.ts +35 -0
- package/src/providers/provider-schema.ts +56 -1
- package/src/providers/sdk/v1/schemas/cli/provider.schema.json +46 -0
- package/src/providers/sdk/v1/types/common/index.ts +19 -0
- package/src/providers/spec/adapter.ts +8 -0
- package/src/providers/spec/driver.ts +74 -2
- package/src/providers/spec/evaluator.ts +39 -3
- package/src/providers/spec/schema.gen.ts +28 -1
- package/src/providers/spec/schema.json +26 -2
- package/src/providers/spec/types.ts +25 -0
- package/src/repo-mesh-types.ts +6 -0
|
@@ -132,6 +132,37 @@ export function resolveSubmitDelayMs(specBeforeSubmit: number | undefined, text:
|
|
|
132
132
|
return Math.max(spec, SUBMIT_DELAY_FLOOR_MS + linesBonus);
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
+
export function matchesCompletionIdleRule(spec: CliSpec, ev: SpecEvaluation, screen: string): string | null {
|
|
136
|
+
const rule = spec.debounce?.completion_idle_after;
|
|
137
|
+
if (!rule?.regex) return null;
|
|
138
|
+
const haystack = rule.section
|
|
139
|
+
? ev.sections.find(section => section.id === rule.section)?.text ?? ''
|
|
140
|
+
: screen;
|
|
141
|
+
if (!haystack) return null;
|
|
142
|
+
try {
|
|
143
|
+
const regex = new RegExp(rule.regex, rule.flags || '');
|
|
144
|
+
const match = haystack.match(regex);
|
|
145
|
+
return match?.[0] || null;
|
|
146
|
+
} catch {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function matchesCompletionIdleTargetState(spec: CliSpec, ev: SpecEvaluation, screen: string): boolean {
|
|
152
|
+
const target = spec.states.find(state => state.id === spec.default_state)
|
|
153
|
+
?? spec.states.find(state => state.id === 'idle');
|
|
154
|
+
if (!target?.when?.regex) return false;
|
|
155
|
+
const haystack = target.when.section
|
|
156
|
+
? ev.sections.find(section => section.id === target.when.section)?.text ?? ''
|
|
157
|
+
: screen;
|
|
158
|
+
if (!haystack) return false;
|
|
159
|
+
try {
|
|
160
|
+
return new RegExp(target.when.regex, target.when.flags || 'i').test(haystack);
|
|
161
|
+
} catch {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
135
166
|
export class SpecDriver {
|
|
136
167
|
private spec!: CliSpec;
|
|
137
168
|
private adapter!: TerminalAdapter;
|
|
@@ -157,6 +188,8 @@ export class SpecDriver {
|
|
|
157
188
|
* because the evaluator already moved past busy by the time the hold
|
|
158
189
|
* kicks in. */
|
|
159
190
|
private lastBusyState: SpecEvaluation['state'] | null = null;
|
|
191
|
+
private completionIdleFirstSeenAt = 0;
|
|
192
|
+
private completionIdleKey = '';
|
|
160
193
|
/** Timer that re-runs evaluate() once the hold window expires. Needed
|
|
161
194
|
* because the PTY stops emitting once the agent finishes; without an
|
|
162
195
|
* explicit wake-up there's nothing to trigger the busy → idle
|
|
@@ -214,6 +247,10 @@ export class SpecDriver {
|
|
|
214
247
|
return this.adapter.snapshot();
|
|
215
248
|
}
|
|
216
249
|
|
|
250
|
+
getCursorPosition(): { row: number; col: number } {
|
|
251
|
+
return this.adapter.getCursorPosition();
|
|
252
|
+
}
|
|
253
|
+
|
|
217
254
|
shutdown(): void {
|
|
218
255
|
for (const t of this.delegateTimers.values()) clearTimeout(t);
|
|
219
256
|
this.delegateTimers.clear();
|
|
@@ -283,7 +320,8 @@ export class SpecDriver {
|
|
|
283
320
|
|
|
284
321
|
private reevaluate(forceEmit = false): void {
|
|
285
322
|
const screen = this.adapter.snapshot();
|
|
286
|
-
const
|
|
323
|
+
const cursor = this.adapter.getCursorPosition();
|
|
324
|
+
const ev = evaluate(this.spec, screen, cursor);
|
|
287
325
|
|
|
288
326
|
// Busy hold: many TUIs flicker between busy and idle every frame
|
|
289
327
|
// (claude in particular — its token counter appears and disappears
|
|
@@ -305,6 +343,40 @@ export class SpecDriver {
|
|
|
305
343
|
evState = this.lastBusyState ?? evState;
|
|
306
344
|
}
|
|
307
345
|
}
|
|
346
|
+
const completionIdleRule = this.spec.debounce?.completion_idle_after;
|
|
347
|
+
let busyWakeMs = busyHoldMs;
|
|
348
|
+
if (evState.id === 'busy' && completionIdleRule) {
|
|
349
|
+
const completionKey = matchesCompletionIdleRule(this.spec, ev, screen);
|
|
350
|
+
if (completionKey) {
|
|
351
|
+
const now = Date.now();
|
|
352
|
+
if (completionKey !== this.completionIdleKey) {
|
|
353
|
+
this.completionIdleKey = completionKey;
|
|
354
|
+
this.completionIdleFirstSeenAt = now;
|
|
355
|
+
}
|
|
356
|
+
const holdMs = Math.max(0, completionIdleRule.hold_ms || 0);
|
|
357
|
+
const ageMs = now - this.completionIdleFirstSeenAt;
|
|
358
|
+
if (ageMs >= holdMs) {
|
|
359
|
+
if (matchesCompletionIdleTargetState(this.spec, ev, screen)) {
|
|
360
|
+
const idle = this.spec.states.find(state => state.id === this.spec.default_state)
|
|
361
|
+
?? this.spec.states.find(state => state.id === 'idle');
|
|
362
|
+
evState = idle
|
|
363
|
+
? { id: idle.id, label: idle.label, title: null }
|
|
364
|
+
: { id: 'idle', label: 'Ready', title: null };
|
|
365
|
+
} else {
|
|
366
|
+
busyWakeMs = Math.min(busyWakeMs, 1000);
|
|
367
|
+
}
|
|
368
|
+
} else {
|
|
369
|
+
busyWakeMs = Math.min(busyWakeMs, Math.max(holdMs - ageMs, 0));
|
|
370
|
+
}
|
|
371
|
+
} else {
|
|
372
|
+
this.completionIdleKey = '';
|
|
373
|
+
this.completionIdleFirstSeenAt = 0;
|
|
374
|
+
}
|
|
375
|
+
} else if (evState.id !== 'busy') {
|
|
376
|
+
this.completionIdleKey = '';
|
|
377
|
+
this.completionIdleFirstSeenAt = 0;
|
|
378
|
+
}
|
|
379
|
+
|
|
308
380
|
if (evState.id === 'busy') {
|
|
309
381
|
this.lastBusyAt = Date.now();
|
|
310
382
|
this.lastBusyState = evState;
|
|
@@ -313,7 +385,7 @@ export class SpecDriver {
|
|
|
313
385
|
// footer settles), so without an explicit timer the driver
|
|
314
386
|
// never wakes up to downshift to idle and the dashboard sees
|
|
315
387
|
// status stuck at generating long after the turn ended.
|
|
316
|
-
this.scheduleBusyExpiry(
|
|
388
|
+
this.scheduleBusyExpiry(busyWakeMs);
|
|
317
389
|
}
|
|
318
390
|
|
|
319
391
|
const changed = forceEmit
|
|
@@ -139,6 +139,7 @@ function matchState(
|
|
|
139
139
|
sections: ResolvedSection[],
|
|
140
140
|
fullScreen: string,
|
|
141
141
|
trace: TraceEntry[],
|
|
142
|
+
cursor?: { row: number; col: number },
|
|
142
143
|
): { matched: boolean; title: string | null } {
|
|
143
144
|
const haystack = sectionText(sections, state.when.section, fullScreen);
|
|
144
145
|
const re = compileRegex(state.when);
|
|
@@ -146,7 +147,31 @@ function matchState(
|
|
|
146
147
|
trace.push({ kind: 'state_skip', text: `state[${state.id}] when ${state.when.section ?? '*'}~/${state.when.regex}/ no match` });
|
|
147
148
|
return { matched: false, title: null };
|
|
148
149
|
}
|
|
149
|
-
|
|
150
|
+
|
|
151
|
+
// Cursor-position guards: check row/col bounds when the state declares them.
|
|
152
|
+
// Guards are skipped entirely when the caller did not supply a cursor position
|
|
153
|
+
// (cursor === undefined) so existing pure-text evaluation is unaffected.
|
|
154
|
+
if (cursor !== undefined) {
|
|
155
|
+
const w = state.when;
|
|
156
|
+
if (w.cursor_row_min !== undefined && cursor.row < w.cursor_row_min) {
|
|
157
|
+
trace.push({ kind: 'state_skip', text: `state[${state.id}] cursor row ${cursor.row} < cursor_row_min ${w.cursor_row_min}` });
|
|
158
|
+
return { matched: false, title: null };
|
|
159
|
+
}
|
|
160
|
+
if (w.cursor_row_max !== undefined && cursor.row > w.cursor_row_max) {
|
|
161
|
+
trace.push({ kind: 'state_skip', text: `state[${state.id}] cursor row ${cursor.row} > cursor_row_max ${w.cursor_row_max}` });
|
|
162
|
+
return { matched: false, title: null };
|
|
163
|
+
}
|
|
164
|
+
if (w.cursor_col_min !== undefined && cursor.col < w.cursor_col_min) {
|
|
165
|
+
trace.push({ kind: 'state_skip', text: `state[${state.id}] cursor col ${cursor.col} < cursor_col_min ${w.cursor_col_min}` });
|
|
166
|
+
return { matched: false, title: null };
|
|
167
|
+
}
|
|
168
|
+
if (w.cursor_col_max !== undefined && cursor.col > w.cursor_col_max) {
|
|
169
|
+
trace.push({ kind: 'state_skip', text: `state[${state.id}] cursor col ${cursor.col} > cursor_col_max ${w.cursor_col_max}` });
|
|
170
|
+
return { matched: false, title: null };
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
trace.push({ kind: 'state_match', text: `state[${state.id}] matched via ${state.when.section ?? '*'}~/${state.when.regex}/${cursor !== undefined ? ` cursor=(${cursor.row},${cursor.col})` : ''}` });
|
|
150
175
|
|
|
151
176
|
let title: string | null = null;
|
|
152
177
|
if (state.extract_title) {
|
|
@@ -217,19 +242,30 @@ function extractModal(
|
|
|
217
242
|
// Public evaluator
|
|
218
243
|
// ────────────────────────────────────────────────────────────────────────────
|
|
219
244
|
|
|
220
|
-
export function evaluate(
|
|
245
|
+
export function evaluate(
|
|
246
|
+
spec: CliSpec,
|
|
247
|
+
screenText: string,
|
|
248
|
+
/** Optional cursor position (0-based row and col). When supplied, states
|
|
249
|
+
* with cursor_row_min/max or cursor_col_min/max predicates are filtered.
|
|
250
|
+
* When omitted, cursor predicates are ignored and evaluation is text-only
|
|
251
|
+
* (backward-compatible with all existing specs and call sites). */
|
|
252
|
+
cursor?: { row: number; col: number },
|
|
253
|
+
): SpecEvaluation {
|
|
221
254
|
const trace: TraceEntry[] = [];
|
|
222
255
|
const lines = screenText.split('\n');
|
|
223
256
|
const sections = resolveSections(spec, lines);
|
|
224
257
|
for (const s of sections) {
|
|
225
258
|
trace.push({ kind: 'section', text: `section[${s.id}] lines [${s.fromLine}, ${s.toLine}) (${s.toLine - s.fromLine} lines)` });
|
|
226
259
|
}
|
|
260
|
+
if (cursor !== undefined) {
|
|
261
|
+
trace.push({ kind: 'section', text: `cursor (${cursor.row}, ${cursor.col})` });
|
|
262
|
+
}
|
|
227
263
|
|
|
228
264
|
let activeState: { id: string; label: string; title: string | null } | null = null;
|
|
229
265
|
let modal: ModalSnapshot | null = null;
|
|
230
266
|
|
|
231
267
|
for (const st of spec.states) {
|
|
232
|
-
const { matched, title } = matchState(st, sections, screenText, trace);
|
|
268
|
+
const { matched, title } = matchState(st, sections, screenText, trace, cursor);
|
|
233
269
|
if (!matched) continue;
|
|
234
270
|
const extractedModal = extractModal(st, sections, screenText, title, trace);
|
|
235
271
|
// If the state declares modal_buttons but extraction failed (button
|
|
@@ -130,7 +130,18 @@ export const SCHEMA = {
|
|
|
130
130
|
"additionalProperties": false,
|
|
131
131
|
"properties": {
|
|
132
132
|
"busy_hold_ms": { "type": "integer", "minimum": 0 },
|
|
133
|
-
"startup_grace_ms": { "type": "integer", "minimum": 0 }
|
|
133
|
+
"startup_grace_ms": { "type": "integer", "minimum": 0 },
|
|
134
|
+
"completion_idle_after": {
|
|
135
|
+
"type": "object",
|
|
136
|
+
"additionalProperties": false,
|
|
137
|
+
"required": ["regex", "hold_ms"],
|
|
138
|
+
"properties": {
|
|
139
|
+
"section": { "type": "string", "minLength": 1 },
|
|
140
|
+
"regex": { "type": "string", "minLength": 1 },
|
|
141
|
+
"flags": { "type": "string" },
|
|
142
|
+
"hold_ms": { "type": "integer", "minimum": 0 }
|
|
143
|
+
}
|
|
144
|
+
}
|
|
134
145
|
}
|
|
135
146
|
}
|
|
136
147
|
},
|
|
@@ -196,6 +207,22 @@ export const SCHEMA = {
|
|
|
196
207
|
"flags": {
|
|
197
208
|
"type": "string",
|
|
198
209
|
"default": "i"
|
|
210
|
+
},
|
|
211
|
+
"cursor_row_min": {
|
|
212
|
+
"type": "integer",
|
|
213
|
+
"minimum": 0
|
|
214
|
+
},
|
|
215
|
+
"cursor_row_max": {
|
|
216
|
+
"type": "integer",
|
|
217
|
+
"minimum": 0
|
|
218
|
+
},
|
|
219
|
+
"cursor_col_min": {
|
|
220
|
+
"type": "integer",
|
|
221
|
+
"minimum": 0
|
|
222
|
+
},
|
|
223
|
+
"cursor_col_max": {
|
|
224
|
+
"type": "integer",
|
|
225
|
+
"minimum": 0
|
|
199
226
|
}
|
|
200
227
|
}
|
|
201
228
|
},
|
|
@@ -59,7 +59,27 @@
|
|
|
59
59
|
"default": [],
|
|
60
60
|
"items": { "$ref": "#/definitions/delegateTrigger" }
|
|
61
61
|
},
|
|
62
|
-
"native_history": { "$ref": "#/definitions/nativeHistory" }
|
|
62
|
+
"native_history": { "$ref": "#/definitions/nativeHistory" },
|
|
63
|
+
"cli_version_range": { "type": "string", "minLength": 1 },
|
|
64
|
+
"debounce": {
|
|
65
|
+
"type": "object",
|
|
66
|
+
"additionalProperties": false,
|
|
67
|
+
"properties": {
|
|
68
|
+
"busy_hold_ms": { "type": "integer", "minimum": 0 },
|
|
69
|
+
"startup_grace_ms": { "type": "integer", "minimum": 0 },
|
|
70
|
+
"completion_idle_after": {
|
|
71
|
+
"type": "object",
|
|
72
|
+
"additionalProperties": false,
|
|
73
|
+
"required": ["regex", "hold_ms"],
|
|
74
|
+
"properties": {
|
|
75
|
+
"section": { "type": "string", "minLength": 1 },
|
|
76
|
+
"regex": { "type": "string", "minLength": 1 },
|
|
77
|
+
"flags": { "type": "string" },
|
|
78
|
+
"hold_ms": { "type": "integer", "minimum": 0 }
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
63
83
|
},
|
|
64
84
|
"definitions": {
|
|
65
85
|
"size": {
|
|
@@ -91,7 +111,11 @@
|
|
|
91
111
|
"properties": {
|
|
92
112
|
"section": { "type": "string" },
|
|
93
113
|
"regex": { "type": "string", "minLength": 1 },
|
|
94
|
-
"flags": { "type": "string", "default": "i" }
|
|
114
|
+
"flags": { "type": "string", "default": "i" },
|
|
115
|
+
"cursor_row_min": { "type": "integer", "minimum": 0 },
|
|
116
|
+
"cursor_row_max": { "type": "integer", "minimum": 0 },
|
|
117
|
+
"cursor_col_min": { "type": "integer", "minimum": 0 },
|
|
118
|
+
"cursor_col_max": { "type": "integer", "minimum": 0 }
|
|
95
119
|
}
|
|
96
120
|
},
|
|
97
121
|
"sectionPattern": {
|
|
@@ -19,6 +19,21 @@ export interface SectionRegex {
|
|
|
19
19
|
section?: string;
|
|
20
20
|
regex: string;
|
|
21
21
|
flags?: string;
|
|
22
|
+
/**
|
|
23
|
+
* Optional cursor-position guards. When present, the state is only
|
|
24
|
+
* considered matched if the terminal cursor row/column satisfies the
|
|
25
|
+
* bounds (0-based, inclusive). Missing or undefined means "no constraint".
|
|
26
|
+
*
|
|
27
|
+
* Use case: distinguish modal zone from body zone for TUIs that use
|
|
28
|
+
* cursor position rather than distinct text to locate the active prompt
|
|
29
|
+
* (e.g. Antigravity cursor lands in modal_zone rows 8-31 when approval
|
|
30
|
+
* is visible, never in body rows 0-7). Without this guard, body text
|
|
31
|
+
* containing "Do you want to proceed?" could false-positive a modal match.
|
|
32
|
+
*/
|
|
33
|
+
cursor_row_min?: number;
|
|
34
|
+
cursor_row_max?: number;
|
|
35
|
+
cursor_col_min?: number;
|
|
36
|
+
cursor_col_max?: number;
|
|
22
37
|
}
|
|
23
38
|
|
|
24
39
|
export interface SectionPattern {
|
|
@@ -227,5 +242,15 @@ export interface CliSpec {
|
|
|
227
242
|
* once the window passes and an idle state has actually been
|
|
228
243
|
* observed. */
|
|
229
244
|
startup_grace_ms?: number;
|
|
245
|
+
/** Treat a provider-specific completion marker as idle after it has
|
|
246
|
+
* remained visible for hold_ms. This handles TUIs that leave their
|
|
247
|
+
* last spinner glyph next to a completed timer, causing the normal
|
|
248
|
+
* busy regex to keep matching after the turn is done. */
|
|
249
|
+
completion_idle_after?: {
|
|
250
|
+
section?: string;
|
|
251
|
+
regex: string;
|
|
252
|
+
flags?: string;
|
|
253
|
+
hold_ms: number;
|
|
254
|
+
};
|
|
230
255
|
};
|
|
231
256
|
}
|
package/src/repo-mesh-types.ts
CHANGED
|
@@ -359,11 +359,17 @@ export interface RepoMeshSessionStatus {
|
|
|
359
359
|
sessionId: string;
|
|
360
360
|
providerType?: string;
|
|
361
361
|
state?: string;
|
|
362
|
+
chatStatus?: string;
|
|
362
363
|
lifecycle?: 'starting' | 'running' | 'stopping' | 'stopped' | 'failed' | 'interrupted';
|
|
363
364
|
surfaceKind?: 'live_runtime' | 'recovery_snapshot' | 'inactive_record';
|
|
364
365
|
recoveryState?: string | null;
|
|
365
366
|
workspace?: string | null;
|
|
366
367
|
title?: string | null;
|
|
368
|
+
role?: string | null;
|
|
369
|
+
isSelfCoordinator?: boolean;
|
|
370
|
+
statusNote?: string | null;
|
|
371
|
+
createdAt?: string | null;
|
|
372
|
+
startedAt?: string | null;
|
|
367
373
|
lastActivityAt?: string | null;
|
|
368
374
|
isCached?: boolean;
|
|
369
375
|
}
|