@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.
Files changed (42) hide show
  1. package/dist/boot/daemon-lifecycle.d.ts +1 -0
  2. package/dist/commands/cli-manager.d.ts +2 -1
  3. package/dist/commands/router.d.ts +5 -1
  4. package/dist/git/git-commands.d.ts +2 -0
  5. package/dist/git/git-types.d.ts +2 -0
  6. package/dist/index.d.ts +1 -1
  7. package/dist/index.js +459 -38
  8. package/dist/index.js.map +1 -1
  9. package/dist/index.mjs +458 -38
  10. package/dist/index.mjs.map +1 -1
  11. package/dist/providers/cli-provider-instance.d.ts +4 -0
  12. package/dist/providers/contracts.d.ts +31 -0
  13. package/dist/providers/sdk/v1/types/common/index.d.ts +35 -1
  14. package/dist/providers/spec/adapter.d.ts +4 -0
  15. package/dist/providers/spec/driver.d.ts +10 -1
  16. package/dist/providers/spec/evaluator.d.ts +9 -1
  17. package/dist/providers/spec/schema.gen.d.ts +38 -0
  18. package/dist/providers/spec/types.d.ts +25 -0
  19. package/dist/repo-mesh-types.d.ts +6 -0
  20. package/package.json +1 -1
  21. package/src/boot/daemon-lifecycle.ts +2 -0
  22. package/src/commands/chat-commands.ts +26 -0
  23. package/src/commands/cli-manager.ts +52 -14
  24. package/src/commands/router.ts +35 -4
  25. package/src/git/git-commands.ts +20 -2
  26. package/src/git/git-status.ts +35 -6
  27. package/src/git/git-types.ts +2 -0
  28. package/src/index.ts +1 -1
  29. package/src/mesh/mesh-events.ts +7 -0
  30. package/src/providers/cli-provider-instance.ts +110 -9
  31. package/src/providers/contracts.d.ts +55 -0
  32. package/src/providers/contracts.ts +35 -0
  33. package/src/providers/provider-schema.ts +56 -1
  34. package/src/providers/sdk/v1/schemas/cli/provider.schema.json +46 -0
  35. package/src/providers/sdk/v1/types/common/index.ts +19 -0
  36. package/src/providers/spec/adapter.ts +8 -0
  37. package/src/providers/spec/driver.ts +74 -2
  38. package/src/providers/spec/evaluator.ts +39 -3
  39. package/src/providers/spec/schema.gen.ts +28 -1
  40. package/src/providers/spec/schema.json +26 -2
  41. package/src/providers/spec/types.ts +25 -0
  42. 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 ev = evaluate(this.spec, screen);
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(busyHoldMs);
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
- trace.push({ kind: 'state_match', text: `state[${state.id}] matched via ${state.when.section ?? '*'}~/${state.when.regex}/` });
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(spec: CliSpec, screenText: string): SpecEvaluation {
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
  }
@@ -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
  }