@adhdev/daemon-core 0.9.82-rc.195 → 0.9.82-rc.197

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 (41) hide show
  1. package/dist/cli-adapter-types.d.ts +1 -0
  2. package/dist/index.d.ts +2 -0
  3. package/dist/index.js +594 -78
  4. package/dist/index.js.map +1 -1
  5. package/dist/index.mjs +593 -83
  6. package/dist/index.mjs.map +1 -1
  7. package/dist/mesh/contracts.d.ts +1 -1
  8. package/dist/mesh/mesh-active-work.d.ts +1 -1
  9. package/dist/mesh/mesh-delivery-policy.d.ts +126 -0
  10. package/dist/mesh/{beads-db.d.ts → mesh-runtime-store.d.ts} +68 -2
  11. package/dist/mesh/mesh-work-queue.d.ts +3 -3
  12. package/dist/providers/provider-instance.d.ts +1 -1
  13. package/dist/providers/spec/driver.d.ts +4 -1
  14. package/dist/providers/spec/schema.gen.d.ts +46 -0
  15. package/dist/providers/spec/types.d.ts +39 -0
  16. package/dist/shared-types-extra.d.ts +1 -1
  17. package/dist/status/normalize.d.ts +1 -1
  18. package/dist/status/normalize.js +1 -0
  19. package/dist/status/normalize.js.map +1 -1
  20. package/dist/status/normalize.mjs +1 -0
  21. package/dist/status/normalize.mjs.map +1 -1
  22. package/package.json +1 -1
  23. package/src/cli-adapter-types.ts +1 -0
  24. package/src/cli-adapters/cli-state-engine.ts +44 -2
  25. package/src/index.ts +4 -0
  26. package/src/mesh/contracts.ts +1 -1
  27. package/src/mesh/mesh-active-work.ts +8 -8
  28. package/src/mesh/mesh-delivery-policy.ts +298 -0
  29. package/src/mesh/mesh-events.ts +64 -15
  30. package/src/mesh/{beads-db.ts → mesh-runtime-store.ts} +249 -7
  31. package/src/mesh/mesh-work-queue.ts +33 -33
  32. package/src/providers/cli-provider-instance.ts +31 -8
  33. package/src/providers/provider-instance.ts +1 -1
  34. package/src/providers/spec/driver.ts +34 -3
  35. package/src/providers/spec/evaluator.ts +32 -3
  36. package/src/providers/spec/schema.gen.ts +22 -2
  37. package/src/providers/spec/schema.json +1 -0
  38. package/src/providers/spec/types.ts +39 -0
  39. package/src/providers/types/interactive-prompt.ts +21 -7
  40. package/src/shared-types-extra.ts +1 -1
  41. package/src/status/normalize.ts +2 -0
@@ -41,6 +41,7 @@ import type { PtyTransportFactory } from '../../cli-adapters/pty-transport.js';
41
41
  import { evaluate, type SpecEvaluation, type TraceEntry } from './evaluator.js';
42
42
  import { loadSpec } from './loader.js';
43
43
  import type { CliSpec, Control, DelegateTrigger } from './types.js';
44
+ import { LOG } from '../../logging/logger.js';
44
45
 
45
46
  export type DashboardEvent =
46
47
  | { kind: 'pty_data'; chunk: string }
@@ -148,10 +149,32 @@ export function matchesCompletionIdleRule(spec: CliSpec, ev: SpecEvaluation, scr
148
149
  }
149
150
  }
150
151
 
151
- export function matchesCompletionIdleTargetState(spec: CliSpec, ev: SpecEvaluation, screen: string): boolean {
152
+ export function matchesCompletionIdleTargetState(
153
+ spec: CliSpec,
154
+ ev: SpecEvaluation,
155
+ screen: string,
156
+ cursor?: { row: number; col: number },
157
+ ): boolean {
152
158
  const target = spec.states.find(state => state.id === spec.default_state)
153
159
  ?? spec.states.find(state => state.id === 'idle');
154
- if (!target?.when?.regex) return false;
160
+ if (!target?.when) return false;
161
+
162
+ // Cursor-only idle: if the target state has cursor guards but no regex,
163
+ // treat a cursor match alone as sufficient.
164
+ const hasCursorGuard = target.when.cursor_row_min !== undefined
165
+ || target.when.cursor_row_max !== undefined
166
+ || target.when.cursor_col_min !== undefined
167
+ || target.when.cursor_col_max !== undefined;
168
+ if (hasCursorGuard && cursor !== undefined) {
169
+ const { cursor_row_min, cursor_row_max, cursor_col_min, cursor_col_max } = target.when;
170
+ const cursorOk = (cursor_row_min === undefined || cursor.row >= cursor_row_min)
171
+ && (cursor_row_max === undefined || cursor.row <= cursor_row_max)
172
+ && (cursor_col_min === undefined || cursor.col >= cursor_col_min)
173
+ && (cursor_col_max === undefined || cursor.col <= cursor_col_max);
174
+ if (cursorOk) return true;
175
+ }
176
+
177
+ if (!target.when.regex) return false;
155
178
  const haystack = target.when.section
156
179
  ? ev.sections.find(section => section.id === target.when.section)?.text ?? ''
157
180
  : screen;
@@ -314,6 +337,7 @@ export class SpecDriver {
314
337
  if (this.busyExpiryTimer) clearTimeout(this.busyExpiryTimer);
315
338
  this.busyExpiryTimer = setTimeout(() => {
316
339
  this.busyExpiryTimer = null;
340
+ LOG.debug('SpecDriver', `[${this.opts.specPath.split('/').slice(-3).join('/')}] busyExpiry fired holdMs=${holdMs}`);
317
341
  this.reevaluate();
318
342
  }, Math.max(holdMs + 50, 100));
319
343
  }
@@ -352,11 +376,18 @@ export class SpecDriver {
352
376
  if (completionKey !== this.completionIdleKey) {
353
377
  this.completionIdleKey = completionKey;
354
378
  this.completionIdleFirstSeenAt = now;
379
+ LOG.debug('SpecDriver', `[${this.opts.specPath.split('/').slice(-3).join('/')}] completion_idle_after matched: key="${completionKey}"`);
355
380
  }
356
381
  const holdMs = Math.max(0, completionIdleRule.hold_ms || 0);
382
+ const forceAfterMs = typeof completionIdleRule.force_after_ms === 'number'
383
+ ? completionIdleRule.force_after_ms
384
+ : null;
357
385
  const ageMs = now - this.completionIdleFirstSeenAt;
358
386
  if (ageMs >= holdMs) {
359
- if (matchesCompletionIdleTargetState(this.spec, ev, screen)) {
387
+ const targetMatches = matchesCompletionIdleTargetState(this.spec, ev, screen, cursor);
388
+ const forced = !targetMatches && forceAfterMs !== null && ageMs >= holdMs + forceAfterMs;
389
+ LOG.debug('SpecDriver', `[${this.opts.specPath.split('/').slice(-3).join('/')}] completion_idle_after hold expired ageMs=${ageMs} targetState=${targetMatches} forced=${forced} screenTail="${screen.split(/\r?\n/).slice(-3).join('\\n').slice(-200)}"`);
390
+ if (targetMatches || forced) {
360
391
  const idle = this.spec.states.find(state => state.id === this.spec.default_state)
361
392
  ?? this.spec.states.find(state => state.id === 'idle');
362
393
  evState = idle
@@ -84,10 +84,39 @@ function resolveSections(spec: CliSpec, lines: string[]): ResolvedSection[] {
84
84
  for (const sec of spec.layout.sections) {
85
85
  let from = 0;
86
86
  let to = total;
87
- if (sec.from_top !== undefined) {
87
+ if (sec.anchor_regex !== undefined) {
88
+ try {
89
+ const re = new RegExp(sec.anchor_regex, sec.anchor_flags ?? '');
90
+ const prevRe = sec.anchor_context?.prev !== undefined
91
+ ? new RegExp(sec.anchor_context.prev, sec.anchor_context.prev_flags ?? '') : null;
92
+ const nextRe = sec.anchor_context?.next !== undefined
93
+ ? new RegExp(sec.anchor_context.next, sec.anchor_context.next_flags ?? '') : null;
94
+ const matches = (i: number) => re.test(lines[i])
95
+ && (prevRe === null || (i > 0 && prevRe.test(lines[i - 1])))
96
+ && (nextRe === null || (i < total - 1 && nextRe.test(lines[i + 1])));
97
+ let idx = -1;
98
+ if (sec.anchor_last) {
99
+ for (let i = total - 1; i >= 0; i--) { if (matches(i)) { idx = i; break; } }
100
+ } else {
101
+ for (let i = 0; i < total; i++) { if (matches(i)) { idx = i; break; } }
102
+ }
103
+ if (idx !== -1) {
104
+ from = idx;
105
+ to = total;
106
+ if (sec.until_regex !== undefined) {
107
+ try {
108
+ const ure = new RegExp(sec.until_regex, sec.until_regex_flags ?? '');
109
+ const end = lines.findIndex((l, i) => i > idx && ure.test(l));
110
+ if (end !== -1) to = end;
111
+ } catch { /* bad until_regex — extend to end */ }
112
+ } else if (sec.lines !== undefined) {
113
+ to = Math.min(total, from + sec.lines);
114
+ }
115
+ }
116
+ } catch { /* bad anchor_regex — fall through to defaults */ }
117
+ } else if (sec.from_top !== undefined) {
88
118
  from = resolveSize(sec.from_top, total);
89
- }
90
- if (sec.from_bottom !== undefined) {
119
+ } else if (sec.from_bottom !== undefined) {
91
120
  const sz = resolveSize(sec.from_bottom, total);
92
121
  from = total - sz;
93
122
  to = total;
@@ -125,6 +125,9 @@ export const SCHEMA = {
125
125
  "type": "string",
126
126
  "minLength": 1
127
127
  },
128
+ "requiresFinalAssistantBeforeIdle": {
129
+ "type": "boolean"
130
+ },
128
131
  "debounce": {
129
132
  "type": "object",
130
133
  "additionalProperties": false,
@@ -139,7 +142,8 @@ export const SCHEMA = {
139
142
  "section": { "type": "string", "minLength": 1 },
140
143
  "regex": { "type": "string", "minLength": 1 },
141
144
  "flags": { "type": "string" },
142
- "hold_ms": { "type": "integer", "minimum": 0 }
145
+ "hold_ms": { "type": "integer", "minimum": 0 },
146
+ "force_after_ms": { "type": "integer", "minimum": 0 }
143
147
  }
144
148
  }
145
149
  }
@@ -187,7 +191,23 @@ export const SCHEMA = {
187
191
  "type": "string"
188
192
  }
189
193
  }
190
- }
194
+ },
195
+ "anchor_regex": { "type": "string", "minLength": 1 },
196
+ "anchor_flags": { "type": "string" },
197
+ "anchor_last": { "type": "boolean" },
198
+ "anchor_context": {
199
+ "type": "object",
200
+ "additionalProperties": false,
201
+ "properties": {
202
+ "prev": { "type": "string" },
203
+ "prev_flags": { "type": "string" },
204
+ "next": { "type": "string" },
205
+ "next_flags": { "type": "string" }
206
+ }
207
+ },
208
+ "lines": { "type": "integer", "minimum": 1 },
209
+ "until_regex": { "type": "string", "minLength": 1 },
210
+ "until_regex_flags": { "type": "string" }
191
211
  }
192
212
  },
193
213
  "sectionRegex": {
@@ -61,6 +61,7 @@
61
61
  },
62
62
  "native_history": { "$ref": "#/definitions/nativeHistory" },
63
63
  "cli_version_range": { "type": "string", "minLength": 1 },
64
+ "requiresFinalAssistantBeforeIdle": { "type": "boolean" },
64
65
  "debounce": {
65
66
  "type": "object",
66
67
  "additionalProperties": false,
@@ -13,6 +13,33 @@ export interface Section {
13
13
  from_top?: Size;
14
14
  from_bottom?: Size;
15
15
  until?: { section: string };
16
+ /** Scan the screen for the first (or last, if anchor_last=true) line
17
+ * matching this regex and use that line as the section start.
18
+ * Combine with `lines`, `until_regex`, or leave alone (defaults to
19
+ * end-of-screen) to control how far it extends. */
20
+ anchor_regex?: string;
21
+ anchor_flags?: string;
22
+ /** If true, use the LAST matching line instead of the first. Useful
23
+ * when the anchor pattern appears multiple times (e.g. separator
24
+ * lines) and you want the one closest to the bottom. */
25
+ anchor_last?: boolean;
26
+ /** Optional neighbouring-line guards that must also match for the
27
+ * anchor to be accepted. Reduces false positives when the anchor
28
+ * pattern could appear elsewhere on screen (e.g. `>` in body text).
29
+ * `prev` checks the line immediately above; `next` the line below. */
30
+ anchor_context?: {
31
+ prev?: string;
32
+ prev_flags?: string;
33
+ next?: string;
34
+ next_flags?: string;
35
+ };
36
+ /** When used with anchor_regex: take at most this many lines from the
37
+ * anchor point. */
38
+ lines?: number;
39
+ /** When used with anchor_regex: extend until the first line matching
40
+ * this regex (exclusive). Takes precedence over `lines`. */
41
+ until_regex?: string;
42
+ until_regex_flags?: string;
16
43
  }
17
44
 
18
45
  export interface SectionRegex {
@@ -224,6 +251,13 @@ export interface CliSpec {
224
251
  notifications?: NotificationRule[];
225
252
  delegate?: DelegateTrigger[];
226
253
  native_history?: NativeHistoryConfig;
254
+ /**
255
+ * When true, the daemon's completed-finalization gate will not emit
256
+ * `generating_completed` until native history confirms the last message
257
+ * role is `assistant`. Prevents PTY quiet-period from triggering idle
258
+ * before the CLI has finished writing its response to disk.
259
+ */
260
+ requiresFinalAssistantBeforeIdle?: boolean;
227
261
  /**
228
262
  * Per-spec debounce knobs. Defaults are conservative (busy_hold_ms
229
263
  * 6000) and live in SpecDriver. Spec authors override here when a
@@ -251,6 +285,11 @@ export interface CliSpec {
251
285
  regex: string;
252
286
  flags?: string;
253
287
  hold_ms: number;
288
+ /** If the target-state check still fails this many ms after hold_ms
289
+ * expired, force an idle transition anyway. Guards against TUIs that
290
+ * show transient UI (pickers, option lists) after the completion
291
+ * marker appears, keeping the target-state regex from matching. */
292
+ force_after_ms?: number;
254
293
  };
255
294
  };
256
295
  }
@@ -349,15 +349,29 @@ export function buildClaudeInteractiveTuiAnswerSteps(
349
349
  if (question.multiSelect) throw new Error('Claude TUI multi-select prompts are not supported yet');
350
350
  const answer = response.answers[question.questionId];
351
351
  if (!answer) throw new Error(`Missing answer for ${question.questionId}`);
352
- if (answer.freeformText) throw new Error('Claude TUI freeform answers are not supported yet');
353
- if (answer.selectedLabels.length !== 1) throw new Error(`Expected one selected label for ${question.questionId}`);
354
- const selectedIndex = question.options.findIndex(option => option.label === answer.selectedLabels[0]);
355
- if (selectedIndex < 0) throw new Error(`Unknown option for ${question.questionId}: ${answer.selectedLabels[0]}`);
356
- for (let i = 0; i < selectedIndex; i += 1) {
357
- steps.push('\x1b[B');
352
+ const freeformText = answer.freeformText?.trim() ?? '';
353
+ if (freeformText) {
354
+ // Freeform: select the "Type something." option (always the last visible
355
+ // option before "Chat about this"), then type the text and confirm.
356
+ const typeOptionIndex = question.options.findIndex(o => /^Type something\.?$/i.test(o.label));
357
+ const optionNumber = typeOptionIndex >= 0 ? typeOptionIndex + 1 : question.options.length;
358
+ steps.push(String(optionNumber));
359
+ // Type the text character by character, then Enter to confirm.
360
+ for (const ch of freeformText) steps.push(ch);
361
+ steps.push('\r');
362
+ } else {
363
+ if (answer.selectedLabels.length !== 1) throw new Error(`Expected one selected label for ${question.questionId}`);
364
+ const selectedIndex = question.options.findIndex(option => option.label === answer.selectedLabels[0]);
365
+ if (selectedIndex < 0) throw new Error(`Unknown option for ${question.questionId}: ${answer.selectedLabels[0]}`);
366
+ // Use numeric key (1-based) to jump directly to the option. This avoids
367
+ // cursor-position drift between questions that arrow-key navigation suffers
368
+ // from — the TUI accepts a digit key to jump straight to that option index.
369
+ steps.push(String(selectedIndex + 1));
358
370
  }
359
- steps.push('\r');
360
371
  }
372
+ // After all questions are answered, Claude TUI shows a final confirm screen
373
+ // ("Submit answers" / "Cancel"). The first option is pre-selected, so a
374
+ // single Enter confirms and submits.
361
375
  steps.push('\r');
362
376
  return steps;
363
377
  }
@@ -21,7 +21,7 @@ export interface RuntimeAttachedClient {
21
21
  }
22
22
 
23
23
  /** Session status union (used by SessionEntry.status, legacy recent-launch metadata, etc.) */
24
- export type SessionStatus = 'idle' | 'generating' | 'waiting_approval' | 'error' | 'stopped' | 'starting' | 'panel_hidden' | 'not_monitored' | 'disconnected';
24
+ export type SessionStatus = 'idle' | 'generating' | 'waiting_approval' | 'waiting_choice' | 'error' | 'stopped' | 'starting' | 'panel_hidden' | 'not_monitored' | 'disconnected';
25
25
 
26
26
  /** Inbox bucket categories for recent sessions */
27
27
  export type RecentSessionBucket = 'needs_attention' | 'working' | 'task_complete' | 'idle';
@@ -5,6 +5,7 @@ export type ManagedStatus =
5
5
  | 'idle'
6
6
  | 'generating'
7
7
  | 'waiting_approval'
8
+ | 'waiting_choice'
8
9
  | 'error'
9
10
  | 'stopped'
10
11
  | 'starting'
@@ -151,6 +152,7 @@ export function normalizeManagedStatus(
151
152
 
152
153
  const normalized = String(status || 'idle').trim().toLowerCase();
153
154
  if (normalized === 'waiting_approval') return 'waiting_approval';
155
+ if (normalized === 'waiting_choice') return 'waiting_choice';
154
156
  if (WORKING_STATUSES.has(normalized)) return 'generating';
155
157
  if (normalized === 'error') return 'error';
156
158
  if (normalized === 'stopped') return 'stopped';