@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.
- package/dist/cli-adapter-types.d.ts +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +594 -78
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +593 -83
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/contracts.d.ts +1 -1
- package/dist/mesh/mesh-active-work.d.ts +1 -1
- package/dist/mesh/mesh-delivery-policy.d.ts +126 -0
- package/dist/mesh/{beads-db.d.ts → mesh-runtime-store.d.ts} +68 -2
- package/dist/mesh/mesh-work-queue.d.ts +3 -3
- package/dist/providers/provider-instance.d.ts +1 -1
- package/dist/providers/spec/driver.d.ts +4 -1
- package/dist/providers/spec/schema.gen.d.ts +46 -0
- package/dist/providers/spec/types.d.ts +39 -0
- package/dist/shared-types-extra.d.ts +1 -1
- package/dist/status/normalize.d.ts +1 -1
- package/dist/status/normalize.js +1 -0
- package/dist/status/normalize.js.map +1 -1
- package/dist/status/normalize.mjs +1 -0
- package/dist/status/normalize.mjs.map +1 -1
- package/package.json +1 -1
- package/src/cli-adapter-types.ts +1 -0
- package/src/cli-adapters/cli-state-engine.ts +44 -2
- package/src/index.ts +4 -0
- package/src/mesh/contracts.ts +1 -1
- package/src/mesh/mesh-active-work.ts +8 -8
- package/src/mesh/mesh-delivery-policy.ts +298 -0
- package/src/mesh/mesh-events.ts +64 -15
- package/src/mesh/{beads-db.ts → mesh-runtime-store.ts} +249 -7
- package/src/mesh/mesh-work-queue.ts +33 -33
- package/src/providers/cli-provider-instance.ts +31 -8
- package/src/providers/provider-instance.ts +1 -1
- package/src/providers/spec/driver.ts +34 -3
- package/src/providers/spec/evaluator.ts +32 -3
- package/src/providers/spec/schema.gen.ts +22 -2
- package/src/providers/spec/schema.json +1 -0
- package/src/providers/spec/types.ts +39 -0
- package/src/providers/types/interactive-prompt.ts +21 -7
- package/src/shared-types-extra.ts +1 -1
- 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(
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
353
|
-
if (
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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';
|
package/src/status/normalize.ts
CHANGED
|
@@ -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';
|