@adhdev/daemon-core 0.9.82-rc.217 → 0.9.82-rc.219
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/index.js +2238 -1141
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2235 -1139
- package/dist/index.mjs.map +1 -1
- package/dist/providers/spec/cli-adapter.d.ts +1 -0
- package/dist/providers/spec/driver.d.ts +50 -1
- package/dist/providers/spec/evaluator.d.ts +17 -1
- package/dist/providers/spec/fsm-driver.d.ts +110 -0
- package/dist/providers/spec/fsm-evaluator.d.ts +72 -0
- package/dist/providers/spec/fsm-loader.d.ts +14 -0
- package/dist/providers/spec/fsm-types.d.ts +91 -0
- package/package.json +1 -1
- package/src/commands/router.ts +192 -0
- package/src/providers/provider-loader.ts +24 -14
- package/src/providers/spec/cli-adapter.ts +62 -14
- package/src/providers/spec/driver.ts +55 -4
- package/src/providers/spec/evaluator.ts +10 -6
- package/src/providers/spec/fsm-driver.ts +663 -0
- package/src/providers/spec/fsm-evaluator.ts +255 -0
- package/src/providers/spec/fsm-loader.ts +102 -0
- package/src/providers/spec/fsm-types.ts +184 -0
|
@@ -18,11 +18,13 @@
|
|
|
18
18
|
*/
|
|
19
19
|
'use strict';
|
|
20
20
|
|
|
21
|
-
import { SpecDriver, type DashboardEvent } from './driver.js';
|
|
22
|
-
import {
|
|
21
|
+
import { SpecDriver, type DashboardEvent, type ISpecDriver } from './driver.js';
|
|
22
|
+
import { FsmDriver } from './fsm-driver.js';
|
|
23
23
|
import { executeNativeHistory } from './native-history-executor.js';
|
|
24
24
|
import { loadSpec } from './loader.js';
|
|
25
|
+
import * as fs from 'node:fs';
|
|
25
26
|
import type { CliSpec } from './types.js';
|
|
27
|
+
import type { NativeHistoryConfig, Control } from './types.js';
|
|
26
28
|
import type { CliAdapter, CliAdapterStatus } from '../../cli-adapter-types.js';
|
|
27
29
|
import type { ChatMessage } from '../../types.js';
|
|
28
30
|
import type { PtyTransportFactory } from '../../cli-adapters/pty-transport.js';
|
|
@@ -37,6 +39,14 @@ import {
|
|
|
37
39
|
type InteractivePromptResponse,
|
|
38
40
|
} from '../types/interactive-prompt.js';
|
|
39
41
|
|
|
42
|
+
/** Peek at the spec's $schema to choose the driver. Cheap header read. */
|
|
43
|
+
function detectV4Schema(specPath: string): boolean {
|
|
44
|
+
try {
|
|
45
|
+
const raw = JSON.parse(fs.readFileSync(specPath, 'utf8'));
|
|
46
|
+
return raw?.$schema === 'adhdev:cli/spec@4';
|
|
47
|
+
} catch { return false; }
|
|
48
|
+
}
|
|
49
|
+
|
|
40
50
|
function stripAnsi(text: string): string {
|
|
41
51
|
// eslint-disable-next-line no-control-regex
|
|
42
52
|
return String(text || '')
|
|
@@ -59,8 +69,14 @@ export class SpecCliAdapter implements CliAdapter {
|
|
|
59
69
|
*/
|
|
60
70
|
readonly chatMessagesOwnedExternally = true as const;
|
|
61
71
|
|
|
62
|
-
private driver:
|
|
63
|
-
|
|
72
|
+
private driver: ISpecDriver;
|
|
73
|
+
/** Common spec fields the adapter reads, present in both v3 and v4. */
|
|
74
|
+
private spec: {
|
|
75
|
+
id: string;
|
|
76
|
+
name: string;
|
|
77
|
+
control_bar?: Control[];
|
|
78
|
+
native_history?: NativeHistoryConfig;
|
|
79
|
+
};
|
|
64
80
|
private lastEvent: DashboardEvent | null = null;
|
|
65
81
|
private latestState: { id: string; label: string; title: string | null } | null = null;
|
|
66
82
|
private latestModal: { title: string | null; buttons: { index: number; label: string }[] } | null = null;
|
|
@@ -97,9 +113,30 @@ export class SpecCliAdapter implements CliAdapter {
|
|
|
97
113
|
extraEnv: Record<string, string>,
|
|
98
114
|
transportFactory?: PtyTransportFactory,
|
|
99
115
|
) {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
116
|
+
// Detect the spec schema version up front. v4 (FSM) and v3 (debounce)
|
|
117
|
+
// use different drivers behind the same ISpecDriver interface.
|
|
118
|
+
const isV4 = detectV4Schema(specPath);
|
|
119
|
+
|
|
120
|
+
if (isV4) {
|
|
121
|
+
// v4 loader runs inside FsmDriver; read the common header fields
|
|
122
|
+
// we need here directly from the parsed JSON.
|
|
123
|
+
const raw = JSON.parse(fs.readFileSync(specPath, 'utf8'));
|
|
124
|
+
this.spec = {
|
|
125
|
+
id: raw.id,
|
|
126
|
+
name: raw.name,
|
|
127
|
+
control_bar: raw.control_bar,
|
|
128
|
+
native_history: raw.native_history,
|
|
129
|
+
};
|
|
130
|
+
} else {
|
|
131
|
+
const res = loadSpec(specPath);
|
|
132
|
+
if (!res.ok) throw new Error(`spec invalid (${specPath}): ${res.errors.join('; ')}`);
|
|
133
|
+
this.spec = {
|
|
134
|
+
id: res.spec.id,
|
|
135
|
+
name: res.spec.name,
|
|
136
|
+
control_bar: res.spec.control_bar,
|
|
137
|
+
native_history: res.spec.native_history,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
103
140
|
this.cliType = this.spec.id;
|
|
104
141
|
this.cliName = this.spec.name;
|
|
105
142
|
this.workingDir = workingDir;
|
|
@@ -107,11 +144,11 @@ export class SpecCliAdapter implements CliAdapter {
|
|
|
107
144
|
|
|
108
145
|
// cli-manager.ts allocates providerSessionId per launch and threads
|
|
109
146
|
// it through resume.newSessionArgs as additional cliArgs (e.g.
|
|
110
|
-
// ["--session-id", "<uuid>"]). We must hand those to
|
|
147
|
+
// ["--session-id", "<uuid>"]). We must hand those to the driver
|
|
111
148
|
// so the agent uses the daemon's id, otherwise (claude case) the
|
|
112
149
|
// agent generates its own id and the chat-history pipeline can't
|
|
113
150
|
// pair the on-disk transcript with the live session.
|
|
114
|
-
|
|
151
|
+
const driverOpts = {
|
|
115
152
|
specPath,
|
|
116
153
|
workingDir,
|
|
117
154
|
extraEnv,
|
|
@@ -119,7 +156,8 @@ export class SpecCliAdapter implements CliAdapter {
|
|
|
119
156
|
emitTrace: false,
|
|
120
157
|
transportFactory,
|
|
121
158
|
extraCliArgs: cliArgs,
|
|
122
|
-
}
|
|
159
|
+
};
|
|
160
|
+
this.driver = isV4 ? new FsmDriver(driverOpts) : new SpecDriver(driverOpts);
|
|
123
161
|
this.driver.subscribe((ev) => this.handleEvent(ev));
|
|
124
162
|
}
|
|
125
163
|
|
|
@@ -362,6 +400,10 @@ export class SpecCliAdapter implements CliAdapter {
|
|
|
362
400
|
specPath: this.driver.getSpecPath(),
|
|
363
401
|
cursorPosition: this.driver.getCursorPosition(),
|
|
364
402
|
completionIdleDebounce: this.driver.getCompletionIdleDebounceState(),
|
|
403
|
+
// v4 FSM live transition table (null for v3 specs). Every outgoing
|
|
404
|
+
// transition from the current state with its per-condition match
|
|
405
|
+
// result + countdown — the canonical "why isn't it moving" answer.
|
|
406
|
+
fsm: this.driver.getFsmDebug?.() ?? null,
|
|
365
407
|
// Extended fields
|
|
366
408
|
name: this.cliName,
|
|
367
409
|
status: this.getStatus().status,
|
|
@@ -445,10 +487,10 @@ export class SpecCliAdapter implements CliAdapter {
|
|
|
445
487
|
}
|
|
446
488
|
}
|
|
447
489
|
|
|
448
|
-
private readCurrentScreenSections(
|
|
490
|
+
private readCurrentScreenSections(_screenText: string): Record<string, string> {
|
|
449
491
|
try {
|
|
450
|
-
const
|
|
451
|
-
return Object.fromEntries(
|
|
492
|
+
const sections = this.driver.getSections() ?? [];
|
|
493
|
+
return Object.fromEntries(sections.map(section => [section.id, section.text]));
|
|
452
494
|
} catch {
|
|
453
495
|
return {};
|
|
454
496
|
}
|
|
@@ -601,7 +643,13 @@ export class SpecCliAdapter implements CliAdapter {
|
|
|
601
643
|
providerSessionId: this.providerSessionId ?? null,
|
|
602
644
|
sections: this.driver.getSections?.() ?? null,
|
|
603
645
|
stateHistory: history,
|
|
604
|
-
specPath:
|
|
646
|
+
specPath: this.driver.getSpecPath?.() ?? null,
|
|
647
|
+
// v4 FSM live transition table — present only for FsmDriver. Lets
|
|
648
|
+
// the panel (and the daemon API) show, for the current instant,
|
|
649
|
+
// every outgoing transition with its per-condition match result
|
|
650
|
+
// and countdown. This is the canonical "why isn't it transitioning"
|
|
651
|
+
// answer — no screenshots needed.
|
|
652
|
+
fsm: this.driver.getFsmDebug?.() ?? null,
|
|
605
653
|
messages,
|
|
606
654
|
committedMessages: messages,
|
|
607
655
|
};
|
|
@@ -65,6 +65,45 @@ export type DashboardCommand =
|
|
|
65
65
|
| { kind: 'cancel' }
|
|
66
66
|
| { kind: 'shutdown' };
|
|
67
67
|
|
|
68
|
+
/** One state-history entry — the union of fields produced by the v3 SpecDriver
|
|
69
|
+
* (debounce-based) and the v4 FsmDriver (transition-based). The cli-adapter
|
|
70
|
+
* and debug panel read this shape from either driver. */
|
|
71
|
+
export interface DriverHistoryEntry {
|
|
72
|
+
stateId: string;
|
|
73
|
+
label: string;
|
|
74
|
+
at: number;
|
|
75
|
+
durationMs: number;
|
|
76
|
+
reason: string;
|
|
77
|
+
matchedStateId?: string;
|
|
78
|
+
matchedRules?: string[];
|
|
79
|
+
debounceKind?: string;
|
|
80
|
+
idleHoldMs?: number;
|
|
81
|
+
busyHoldMs?: number;
|
|
82
|
+
/** v4: the transition that fired, e.g. "idle→busy". */
|
|
83
|
+
via?: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** The surface the cli-adapter drives. Implemented by both SpecDriver (v3,
|
|
87
|
+
* debounce) and FsmDriver (v4, FSM). Lets the adapter hold either without
|
|
88
|
+
* branching on the concrete type. */
|
|
89
|
+
export interface ISpecDriver {
|
|
90
|
+
subscribe(listener: (ev: DashboardEvent) => void): () => void;
|
|
91
|
+
start(): void;
|
|
92
|
+
dispatch(cmd: DashboardCommand): void;
|
|
93
|
+
snapshot(): string;
|
|
94
|
+
getCursorPosition(): { row: number; col: number };
|
|
95
|
+
getScreen(): string;
|
|
96
|
+
getSpecPath(): string;
|
|
97
|
+
shutdown(): void;
|
|
98
|
+
getStateHistory(): ReadonlyArray<DriverHistoryEntry>;
|
|
99
|
+
getSections(): Array<{ id: string; text: string }> | null;
|
|
100
|
+
getLastBusyAt(): number;
|
|
101
|
+
hasIdleHoldPending(): boolean;
|
|
102
|
+
getCompletionIdleDebounceState(): { active: boolean; ageMs: number; holdMs: number; forceAfterMs: number } | null;
|
|
103
|
+
/** v4 only — present on FsmDriver. Returns the live transition table. */
|
|
104
|
+
getFsmDebug?(): unknown;
|
|
105
|
+
}
|
|
106
|
+
|
|
68
107
|
export interface SpecDriverOpts {
|
|
69
108
|
specPath: string;
|
|
70
109
|
workingDir: string;
|
|
@@ -195,7 +234,7 @@ export function matchesCompletionIdleTargetState(
|
|
|
195
234
|
}
|
|
196
235
|
}
|
|
197
236
|
|
|
198
|
-
export class SpecDriver {
|
|
237
|
+
export class SpecDriver implements ISpecDriver {
|
|
199
238
|
private spec!: CliSpec;
|
|
200
239
|
private adapter!: TerminalAdapter;
|
|
201
240
|
private listeners = new Set<(ev: DashboardEvent) => void>();
|
|
@@ -535,6 +574,19 @@ export class SpecDriver {
|
|
|
535
574
|
evState = this.lastBusyState ?? evState;
|
|
536
575
|
}
|
|
537
576
|
}
|
|
577
|
+
// Startup grace: suppress idle→busy transitions during the banner-paint
|
|
578
|
+
// window. The spec author sets startup_grace_ms to cover the time the
|
|
579
|
+
// terminal spends drawing its initial screen; any busy signal during
|
|
580
|
+
// that window is noise (cursor_above:changed, layout reflow, etc.).
|
|
581
|
+
const graceMs = this.spec.debounce?.startup_grace_ms ?? STARTUP_GRACE_MS;
|
|
582
|
+
const sinceStartMs = now - this.startedAtMs;
|
|
583
|
+
// Startup grace: suppress all busy transitions during the banner-paint
|
|
584
|
+
// window regardless of current state.
|
|
585
|
+
if (sinceStartMs < graceMs && evState.id === 'busy') {
|
|
586
|
+
LOG.info('SpecDriver', `[${this.opts.specPath.split('/').slice(-3).join('/')}] startup grace suppressed busy (sinceStart=${sinceStartMs}ms grace=${graceMs}ms)`);
|
|
587
|
+
evState = { id: this.spec.default_state ?? 'idle', label: 'Ready', title: null };
|
|
588
|
+
this.scheduleBusyExpiry(graceMs - sinceStartMs + 50);
|
|
589
|
+
}
|
|
538
590
|
// stable_ms gate: if the matched idle state has a changed:false/stable_ms
|
|
539
591
|
// condition, verify the region has been stable long enough. If not, pin
|
|
540
592
|
// to busy and schedule a re-evaluation when the stable window expires.
|
|
@@ -783,8 +835,7 @@ export class SpecDriver {
|
|
|
783
835
|
// reading during the banner paint as a real idle. After that,
|
|
784
836
|
// the first non-busy observation is a real prompt-ready signal
|
|
785
837
|
// and we drain any queued send_message calls.
|
|
786
|
-
const
|
|
787
|
-
const sinceStart = Date.now() - this.startedAtMs;
|
|
838
|
+
const sinceStart = now - this.startedAtMs;
|
|
788
839
|
if (!this.idleSeenOnce && evState.id !== 'busy' && sinceStart >= graceMs) {
|
|
789
840
|
this.idleSeenOnce = true;
|
|
790
841
|
const queued = this.pendingSends.splice(0);
|
|
@@ -1014,7 +1065,7 @@ function sectionTextFromSnapshot(spec: CliSpec, screen: string, sectionId: strin
|
|
|
1014
1065
|
return ev.sections.find(s => s.id === sectionId)?.text ?? null;
|
|
1015
1066
|
}
|
|
1016
1067
|
|
|
1017
|
-
function guessExt(mime: string): string {
|
|
1068
|
+
export function guessExt(mime: string): string {
|
|
1018
1069
|
if (/png/i.test(mime)) return '.png';
|
|
1019
1070
|
if (/jpe?g/i.test(mime)) return '.jpg';
|
|
1020
1071
|
if (/gif/i.test(mime)) return '.gif';
|
|
@@ -81,7 +81,7 @@ function resolveSize(size: number | string | undefined, total: number): number {
|
|
|
81
81
|
* Resolve v3 sections{} object into an ordered array of ResolvedSection.
|
|
82
82
|
* Two-pass: first anchor/positional, then apply `until` cross-references.
|
|
83
83
|
*/
|
|
84
|
-
function resolveSections(
|
|
84
|
+
export function resolveSections(
|
|
85
85
|
sectionsObj: Record<string, SectionDef>,
|
|
86
86
|
lines: string[],
|
|
87
87
|
): ResolvedSection[] {
|
|
@@ -167,7 +167,7 @@ function resolveSections(
|
|
|
167
167
|
return resolved;
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
-
function sectionText(sections: ResolvedSection[], sectionId: string | undefined, fullScreen: string): string {
|
|
170
|
+
export function sectionText(sections: ResolvedSection[], sectionId: string | undefined, fullScreen: string): string {
|
|
171
171
|
if (!sectionId) return fullScreen;
|
|
172
172
|
const found = sections.find(s => s.id === sectionId);
|
|
173
173
|
return found ? found.text : '';
|
|
@@ -193,7 +193,7 @@ function isAnyCondition(c: Condition): c is AnyCondition {
|
|
|
193
193
|
return 'any' in c;
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
-
function evaluateCondition(
|
|
196
|
+
export function evaluateCondition(
|
|
197
197
|
cond: Condition,
|
|
198
198
|
sections: ResolvedSection[],
|
|
199
199
|
fullScreen: string,
|
|
@@ -231,7 +231,11 @@ function evaluateCondition(
|
|
|
231
231
|
// changed:false means "region is currently stable" — stable_ms duration
|
|
232
232
|
// is enforced by the driver, not here.
|
|
233
233
|
const result = cond.changed ? didChange : !didChange;
|
|
234
|
-
|
|
234
|
+
const stableSuffix = cond.stable_ms != null ? ` stable_ms=${cond.stable_ms}` : '';
|
|
235
|
+
trace.push({
|
|
236
|
+
kind: result ? 'state_match' : 'state_skip',
|
|
237
|
+
text: `state[${stateId}] changed cond cursor_above=${cond.cursor_above} rows[${startRow},${endRow}) changed=${didChange} expected=${cond.changed}${stableSuffix} result=${result}`,
|
|
238
|
+
});
|
|
235
239
|
return result;
|
|
236
240
|
}
|
|
237
241
|
|
|
@@ -317,7 +321,7 @@ function matchState(
|
|
|
317
321
|
return { matched: true, title };
|
|
318
322
|
}
|
|
319
323
|
|
|
320
|
-
function extractTitle(
|
|
324
|
+
export function extractTitle(
|
|
321
325
|
rule: ExtractTitle,
|
|
322
326
|
sections: ResolvedSection[],
|
|
323
327
|
fullScreen: string,
|
|
@@ -358,7 +362,7 @@ function compileLinePattern(ref: { pattern: string; flags?: string }): RegExp {
|
|
|
358
362
|
return new RegExp(ref.pattern, flags);
|
|
359
363
|
}
|
|
360
364
|
|
|
361
|
-
function extractButtonsFromRule(
|
|
365
|
+
export function extractButtonsFromRule(
|
|
362
366
|
rule: ExtractButtons,
|
|
363
367
|
hay: string,
|
|
364
368
|
): { index: number; label: string; key: string }[] {
|