@adhdev/daemon-core 0.9.82-rc.552 → 0.9.82-rc.554

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 (39) hide show
  1. package/dist/cli-adapter-types.d.ts +29 -0
  2. package/dist/cli-adapters/provider-cli-adapter.d.ts +80 -1
  3. package/dist/cli-adapters/provider-cli-shared.d.ts +55 -0
  4. package/dist/cli-adapters/terminal-screen.d.ts +5 -0
  5. package/dist/commands/stream-commands.d.ts +31 -0
  6. package/dist/index.d.ts +1 -1
  7. package/dist/index.js +789 -15
  8. package/dist/index.js.map +1 -1
  9. package/dist/index.mjs +785 -12
  10. package/dist/index.mjs.map +1 -1
  11. package/dist/mesh/mesh-ledger.d.ts +1 -1
  12. package/dist/mesh/mesh-refine-gates.d.ts +86 -0
  13. package/dist/providers/cli-provider-instance.d.ts +97 -0
  14. package/dist/providers/provider-instance.d.ts +9 -0
  15. package/dist/providers/spec/adapter.d.ts +7 -0
  16. package/dist/providers/spec/cli-adapter.d.ts +64 -0
  17. package/dist/providers/spec/fsm-driver.d.ts +11 -0
  18. package/dist/repo-mesh-types.d.ts +23 -0
  19. package/package.json +3 -3
  20. package/src/cli-adapter-types.ts +29 -0
  21. package/src/cli-adapters/provider-cli-adapter.ts +135 -0
  22. package/src/cli-adapters/provider-cli-shared.ts +172 -0
  23. package/src/cli-adapters/terminal-screen.ts +5 -0
  24. package/src/commands/handler.ts +4 -0
  25. package/src/commands/router-refine.ts +40 -1
  26. package/src/commands/router.ts +15 -0
  27. package/src/commands/stream-commands.ts +125 -0
  28. package/src/index.ts +1 -0
  29. package/src/mesh/coordinator-prompt.ts +2 -0
  30. package/src/mesh/mesh-events-utils.ts +15 -0
  31. package/src/mesh/mesh-ledger.ts +6 -0
  32. package/src/mesh/mesh-refine-gates.ts +256 -0
  33. package/src/providers/cli-provider-instance.ts +277 -6
  34. package/src/providers/provider-instance-manager.ts +11 -0
  35. package/src/providers/provider-instance.ts +10 -0
  36. package/src/providers/spec/adapter.ts +7 -0
  37. package/src/providers/spec/cli-adapter.ts +122 -0
  38. package/src/providers/spec/fsm-driver.ts +5 -0
  39. package/src/repo-mesh-types.ts +35 -0
@@ -175,6 +175,13 @@ export class TerminalAdapter {
175
175
  return { row: pos.row, col: pos.col };
176
176
  }
177
177
 
178
+ /** Current terminal geometry (columns × rows). Tracked here rather than
179
+ * read off the screen buffer so a resize is reflected immediately, before
180
+ * the next repaint. Consumed by the mesh_read_terminal viewport read. */
181
+ getScreenSize(): { cols: number; rows: number } {
182
+ return { cols: this.cols, rows: this.rows };
183
+ }
184
+
178
185
  send_keys(text: string): void {
179
186
  this.recordEvent('input', capPreview(escapeControl(text)), text.length);
180
187
  this.pty?.write(text);
@@ -23,10 +23,17 @@ import { lastContiguousNumberedBlock } from './evaluator.js';
23
23
  import { executeNativeHistory } from './native-history-executor.js';
24
24
  import { detectBackgroundTaskActive } from './background-task-detector.js';
25
25
  import * as fs from 'node:fs';
26
+ import { createHash } from 'node:crypto';
26
27
  import type { NativeHistoryConfig, Control, ControlAction } from './types.js';
27
28
  import type { CliAdapter, CliAdapterStatus } from '../../cli-adapter-types.js';
28
29
  import type { ChatMessage } from '../../types.js';
29
30
  import type { PtyTransportFactory } from '../../cli-adapters/pty-transport.js';
31
+ import {
32
+ encodeMeshSendKeys,
33
+ truncateToByteTailByLine,
34
+ type MeshSendKeyItem,
35
+ type MeshSendKeyName,
36
+ } from '../../cli-adapters/provider-cli-shared.js';
30
37
  import { LOG } from '../../logging/logger.js';
31
38
  import {
32
39
  buildClaudeInteractiveTuiAnswerSteps,
@@ -277,6 +284,121 @@ export class SpecCliAdapter implements CliAdapter {
277
284
  return this.spawned && !this.exited;
278
285
  }
279
286
 
287
+ // Process liveness for the MESH-STALL-WATCH watchdog (checkMeshWorkerStall).
288
+ // The spec path drives the child through the transport/driver rather than a
289
+ // directly-held ptyProcess handle, so liveness is tracked by the spawned/exited
290
+ // lifecycle flags — the same pair isReady() uses. A spawned, not-yet-exited
291
+ // session is alive. ProviderCliAdapter exposes the equivalent via `ptyProcess !== null`.
292
+ isAlive(): boolean {
293
+ return this.spawned && !this.exited;
294
+ }
295
+
296
+ // MESH-READ-TERMINAL / MESH-SEND-KEYS byte caps — same envelope as
297
+ // ProviderCliAdapter (32KiB default view, 64KiB absolute hard cap). Bytes,
298
+ // not chars: a multi-byte-glyph screen can exceed an MCP payload cap while
299
+ // the char count still looks safe.
300
+ private static readonly TERMINAL_SNAPSHOT_DEFAULT_MAX_BYTES = 32 * 1024;
301
+ private static readonly TERMINAL_SNAPSHOT_ABSOLUTE_MAX_BYTES = 64 * 1024;
302
+
303
+ /**
304
+ * MESH-READ-TERMINAL (feature 2: RAW terminal read). Least-privilege read
305
+ * of the CURRENT rendered viewport for mesh_read_terminal on the spec path
306
+ * (claude-cli / antigravity / codex-cli — the native-source providers that
307
+ * route through SpecCliAdapter). Mirrors ProviderCliAdapter.getTerminalScreenSnapshot:
308
+ * - returns ONLY the driver's current viewport snapshot, the cursor
309
+ * position and the terminal geometry — NO scrollback, NO parser/FSM
310
+ * state, NO debug buffers;
311
+ * - the payload is byte-bounded (UTF-8) with bottom-tail preservation so a
312
+ * screen of multi-byte glyphs can never exceed the MCP payload cap;
313
+ * - `hash` is over the FULL untruncated viewport so a caller can detect a
314
+ * screen change across polls even when the returned text was truncated.
315
+ *
316
+ * SECURITY: the raw viewport can carry tokens / command args / env / user
317
+ * data. Callers MUST gate this on mesh ownership and MUST NOT log the text.
318
+ */
319
+ getTerminalScreenSnapshot(maxBytes = SpecCliAdapter.TERMINAL_SNAPSHOT_DEFAULT_MAX_BYTES): {
320
+ text: string;
321
+ cursor: { col: number; row: number };
322
+ cols: number;
323
+ rows: number;
324
+ truncated: boolean;
325
+ originalBytes: number;
326
+ returnedBytes: number;
327
+ hash: string;
328
+ } {
329
+ const cap = Math.min(
330
+ SpecCliAdapter.TERMINAL_SNAPSHOT_ABSOLUTE_MAX_BYTES,
331
+ Math.max(1024, Math.floor(maxBytes) || SpecCliAdapter.TERMINAL_SNAPSHOT_DEFAULT_MAX_BYTES),
332
+ );
333
+ let rawViewport = '';
334
+ try { rawViewport = this.driver.snapshot() || ''; } catch { rawViewport = ''; }
335
+ let cursor = { row: 0, col: 0 };
336
+ try { cursor = this.driver.getCursorPosition(); } catch { /* keep 0,0 */ }
337
+ // getScreenSize is optional on ISpecDriver; a test double may omit it.
338
+ let size = { cols: 0, rows: 0 };
339
+ try { size = this.driver.getScreenSize?.() ?? size; } catch { /* keep 0,0 */ }
340
+ const truncation = truncateToByteTailByLine(rawViewport, cap);
341
+ const hash = createHash('sha256').update(rawViewport, 'utf8').digest('hex').slice(0, 16);
342
+ return {
343
+ text: truncation.text,
344
+ cursor: { col: cursor.col, row: cursor.row },
345
+ cols: size.cols,
346
+ rows: size.rows,
347
+ truncated: truncation.truncated,
348
+ originalBytes: truncation.originalBytes,
349
+ returnedBytes: truncation.returnedBytes,
350
+ hash,
351
+ };
352
+ }
353
+
354
+ /**
355
+ * MESH-SEND-KEYS (feature 3: key injection). Inject a STRUCTURED key
356
+ * sequence into the spec-driven PTY for mesh_send_keys. Mirrors
357
+ * ProviderCliAdapter.injectKeys' modal fail-closed guard, then writes the
358
+ * whole encoded sequence in ONE pty_write dispatch (text+ENTER is a single
359
+ * contiguous string, so a submit key can never be separated from the text
360
+ * it submits).
361
+ *
362
+ * The spec path drives the child through the FsmDriver, not a directly-held
363
+ * ptyProcess — there is no adapter-level echo-gate/submit-retry FIFO to race
364
+ * against here (the driver serializes its own writes), so the only guard is
365
+ * the modal fail-closed: a NON-destructive injection into an actionable
366
+ * approval modal is refused (use mesh_approve) unless explicitly overridden.
367
+ * A destructive ESC/CTRL_C dismisses rather than confirms, so it is allowed
368
+ * past this gate (the tool layer owns the destructive double-gate + audit).
369
+ * This method NEVER logs the literal text — only key enums / byte length.
370
+ */
371
+ async injectKeys(
372
+ items: MeshSendKeyItem[],
373
+ opts: { allowModalOverride?: boolean } = {},
374
+ ): Promise<
375
+ | { ok: true; keys: MeshSendKeyName[]; hasDestructive: boolean; submits: boolean; bytes: number }
376
+ | { ok: false; refused: 'submit_race' | 'actionable_modal'; keys: MeshSendKeyName[]; hasDestructive: boolean }
377
+ > {
378
+ if (!this.spawned || this.exited) throw new Error(`${this.cliName} is not running`);
379
+ const encoded = encodeMeshSendKeys(items);
380
+
381
+ // Modal fail-closed — a NON-destructive injection while parked on an
382
+ // actionable approval modal is refused so a modal choice can't be
383
+ // confirmed via send_keys and bypass the approval policy.
384
+ const modalActive = this.latestState?.status === 'approval';
385
+ if (modalActive && !encoded.hasDestructive && !opts.allowModalOverride) {
386
+ LOG.warn('SpecAdapter', `[${this.cliType}] send_keys refused (actionable_modal): keys=${encoded.keys.join(',')} — use mesh_approve`);
387
+ return { ok: false, refused: 'actionable_modal', keys: encoded.keys, hasDestructive: encoded.hasDestructive };
388
+ }
389
+
390
+ // Atomic write: the full encoded sequence goes out in ONE pty_write.
391
+ this.driver.dispatch({ kind: 'pty_write', data: encoded.sequence });
392
+ LOG.info('SpecAdapter', `[${this.cliType}] send_keys injected keys=${encoded.keys.join(',') || '(text-only)'} bytes=${Buffer.byteLength(encoded.sequence, 'utf8')} destructive=${encoded.hasDestructive}`);
393
+ return {
394
+ ok: true,
395
+ keys: encoded.keys,
396
+ hasDestructive: encoded.hasDestructive,
397
+ submits: encoded.submits,
398
+ bytes: Buffer.byteLength(encoded.sequence, 'utf8'),
399
+ };
400
+ }
401
+
280
402
  setOnStatusChange(cb: () => void): void {
281
403
  this.statusCallback = cb;
282
404
  }
@@ -122,6 +122,10 @@ export interface ISpecDriver {
122
122
  snapshot(): string;
123
123
  getCursorPosition(): { row: number; col: number };
124
124
  getScreen(): string;
125
+ /** Current terminal geometry (columns × rows). Optional so a non-Fsm
126
+ * ISpecDriver implementation (test doubles) need not provide it; the
127
+ * mesh_read_terminal path falls back to a 0×0 geometry when absent. */
128
+ getScreenSize?(): { cols: number; rows: number };
125
129
  getSpecPath(): string;
126
130
  shutdown(): void;
127
131
  getStateHistory(): ReadonlyArray<DriverHistoryEntry>;
@@ -443,6 +447,7 @@ export class FsmDriver implements ISpecDriver {
443
447
  snapshot(): string { return this.adapter.snapshot(); }
444
448
  getCursorPosition(): { row: number; col: number } { return this.adapter.getCursorPosition(); }
445
449
  getScreen(): string { return this.adapter.snapshot(); }
450
+ getScreenSize(): { cols: number; rows: number } { return this.adapter.getScreenSize(); }
446
451
 
447
452
  /** Scrollback-inclusive screen as line array — used only for modal/button
448
453
  * content extraction so a tall prompt's off-screen anchors stay matchable.
@@ -350,6 +350,17 @@ export interface RepoMeshPolicy {
350
350
  * A node policy may override this per-node (RepoMeshNodePolicy.delegatedWorkerAutoApprove).
351
351
  */
352
352
  delegatedWorkerAutoApprove?: boolean;
353
+ /**
354
+ * MESH-SEND-KEYS (feature 3): opt-in to allow the coordinator to inject
355
+ * DESTRUCTIVE keys (CTRL_C / ESC) into a worker PTY via mesh_send_keys. These
356
+ * can kill or derail the worker process, and delegatedWorkerAutoApprove is a
357
+ * TOOL-CONSENT policy, not a PTY-input authorization — so a destructive key
358
+ * injection additionally requires this explicit mesh-owner opt-in AND a
359
+ * per-call confirm_destructive=true. Defaults to false (destructive keys
360
+ * refused). Non-destructive keys (text/ENTER/arrows/TAB/BACKSPACE) are
361
+ * unaffected. A node policy may override per-node.
362
+ */
363
+ allowSendKeysDestructive?: boolean;
353
364
  /**
354
365
  * What to do with delegated session-host records for a node when it is removed.
355
366
  * Defaults to 'preserve' so completed work can be reviewed later and live
@@ -473,6 +484,11 @@ export interface RepoMeshNodePolicy {
473
484
  * precedence over the mesh-level policy for worker sessions launched onto this node.
474
485
  */
475
486
  delegatedWorkerAutoApprove?: boolean;
487
+ /**
488
+ * MESH-SEND-KEYS (feature 3): per-node override for
489
+ * RepoMeshPolicy.allowSendKeysDestructive.
490
+ */
491
+ allowSendKeysDestructive?: boolean;
476
492
  /**
477
493
  * Optional associated/external repos that must be checked alongside this node.
478
494
  * These are explicit policy/config entries only; Repo Mesh does not auto-discover
@@ -734,6 +750,25 @@ export function resolveDelegatedWorkerAutoApprove(
734
750
  return true;
735
751
  }
736
752
 
753
+ /**
754
+ * MESH-SEND-KEYS (feature 3): resolve whether DESTRUCTIVE key injection
755
+ * (CTRL_C/ESC via mesh_send_keys) is permitted for a node. Node policy overrides
756
+ * mesh policy; DEFAULTS TO FALSE (fail-closed) — a destructive key still requires
757
+ * a per-call confirm_destructive=true on top of this opt-in.
758
+ */
759
+ export function resolveAllowSendKeysDestructive(
760
+ meshPolicy?: Pick<RepoMeshPolicy, 'allowSendKeysDestructive'> | null,
761
+ nodePolicy?: Pick<RepoMeshNodePolicy, 'allowSendKeysDestructive'> | null,
762
+ ): boolean {
763
+ if (typeof nodePolicy?.allowSendKeysDestructive === 'boolean') {
764
+ return nodePolicy.allowSendKeysDestructive;
765
+ }
766
+ if (typeof meshPolicy?.allowSendKeysDestructive === 'boolean') {
767
+ return meshPolicy.allowSendKeysDestructive;
768
+ }
769
+ return false;
770
+ }
771
+
737
772
  /**
738
773
  * Resolve the enforced per-(node, provider) maxParallel cap from a node's resolved
739
774
  * capability slots, or undefined when no matching slot declares a finite cap. Used