Mount CliChatSurface in ChatView for cli-backed chat sessions (sessions carrying
cliExecutorAdapterId): the message-pane + composer region is delegated to the
surface (transcript/raw-terminal toggle for hybrid/native adapters, terminal-only
for the generic adapter), while regular sessions keep the standard composer. The
existing message list and composer JSX are captured once as render thunks and
passed through, so there is no parallel message/composer UI.
Add a narrow telemetry seam: TelemetryHub gains an optional onEvent tap (also
settable post-construction via setEventListener) invoked with each sanitized
event after routing — best-effort, a throwing listener never breaks ingest. This
is the seam the CliChatSessionRunner uses to build the durable transcript from
the same sanitized events the hook route already feeds the hub, without the hub
becoming a general subscriber bus.
Fix the stale @fusion/engine vi.mocks across dashboard tests: object-literal
mocks that fully replace the module now also return listCliAdapterDescriptors
(added by U15's cli-agent-settings route, evaluated at module load). Mocks that
spread importOriginal/importActual already pick it up.
Tests: new ChatView.cli-mount.test.tsx (cli session → CliChatSurface, regular
session → normal composer, generic → terminal-only); telemetry-hub onEvent tap
coverage. chat-attachment-routes, chat-cli-sessions, cli-agent-hooks-route,
ChatView.cli-toggle all green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add `cli-agent` as a task-execute executor kind. A workflow node with
`config.executor === "cli-agent"` drives an engine-owned CLI coding agent
through the execute step via the new cli-agent/task-session.ts orchestration:
spawn in the worktree, issue the hook token + write hook scripts, inject the
prompt after readiness, subscribe to the state machine, and resolve on a
positive completion signal (R20 gating). Config is snapshotted at launch; the
PTY is reaped (completed) at the in-review handoff.
Executor seam: runGraphCustomNode gains a cli-agent branch delegating to
runCliAgentNode; the hard-cancel/abort path (awaitAbortInFlightTaskWork +
abortAllInFlight) claims and SIGKILLs the CLI session as a first-class surface,
marking it killed (never resume-eligible). Re-entry kills any prior live
session and launches fresh; follow-up resumes the recorded native session id
when supported. A PTY-pool ceiling surfaces as a typed task value, not a stall.
Node-config typing extended minimally (WorkflowNodeExecutorKind /
WorkflowNodeExecutorConfig in @fusion/core).
Tests: cli-agent/__tests__/task-session.test.ts (12) and
__tests__/cli-agent-executor.test.ts (9) cover AE1/AE5, hard cancel, re-entry,
follow-up, config snapshot, ceiling, and the generic-tier confirm-advance path,
using scripted adapters + a mock PTY seam. Engine typecheck clean; full
src/cli-agent suite + new tests green (144 passing).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Suspend-and-handoff passthrough from the Ink TUI to a cli-agent PTY session:
mint an attach ticket, open the cli-session WebSocket, enter the alternate
screen + raw mode, stream WS scrollback/data frames to stdout, frame stdin
bytes into input messages, propagate resizes, and ACK consumed bytes for flow
control. Detach chord Ctrl-] restores the terminal and remounts Ink; a dropped
WS surfaces the error and restores the terminal cleanly.
Untrusted terminal output is neutralized through the same hardening filter the
dashboard WS bridge uses (re-exported from @fusion/dashboard) so OSC 52, non-
http(s) OSC 8 links, and device-status queries are stripped before reaching the
host TTY — the riskiest leg, since the host terminal honors more sequences than
xterm.js. CJK/double-width bytes pass through verbatim.
- packages/cli/src/commands/dashboard-tui/terminal-attach.ts (passthrough loop +
injectable WS transport for tests)
- controller.openTerminalAttach() Ink integration (unmount/run/remount)
- adds `ws` runtime dep to packages/cli
- re-exports neutralizeTerminalOutput/flushTerminalOutput from @fusion/dashboard
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pure engine code (no HTTP) for the CLI agent executor:
- state-machine.ts: authoritative per-session state machine implementing the
HTD diagram (starting→ready→busy⇄waitingOnInput→done; done→busy follow-up;
dead-classification choice → killed/userExited/authFailed/resuming; resume cap
of 2 with backoff → needsAttention). Positive completion is distinct from idle
(idle never produces done); inactivity stall backstop re-armed by output/
telemetry events (no fixed turn timeout); termination classification helper for
all five paths; per-turn latches reset between turns. Persists every transition
via CliSessionStore (the transient `resuming` machine state maps onto the U1
`dead` store enum) and exposes a throttled `onStateChange` subscription for the
later SSE bridge — no dashboard imports.
- telemetry-hub.ts: in-process ingestion contract (ingest(sessionId, event)) for
the U17 route and log-tailing adapters. Mints high-entropy per-session hook
tokens (issueToken/validateToken/invalidate); rebuilds the registry only from
live sessions in CliSessionStore so stale tokens for non-live sessions never
validate; a token validates only for its own session. Bounds everything
ingested: per-event size caps, per-turn count caps (lifecycle events exempt),
ANSI/control stripping before pattern matching, and secret redaction that
survives chunk boundaries via a held-back carry window (redactSecrets from
@fusion/core).
Tests: 35 new (state-machine.test.ts, telemetry-hub.test.ts) covering AE1/AE2,
stall backstop, all termination paths, resume caps, token registry, two-turn
latch reset, oversized capping, ANSI stripping, and cross-chunk redaction.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Engine-owned PTY lifecycle for CLI agent sessions:
- adapter.ts: CliAgentAdapter interface (launch/env-allowlist builders,
capability flags, readiness detection, injection formatter, resume builder,
telemetry wiring) + CliAdapterRegistry with typed unknown/duplicate errors.
- session-manager.ts: CliSessionManager owning node-pty processes via the U16
shared loader. Byte-bounded scrollback ring (default ~512KB), single
serialized write queue shared by injections + user input (FIFO, deferral in
quiet windows), latest-active-client resize, scoped-SIGKILL process registry
on process exit (never port 4040), explicit async attach interface
(scrollback + AsyncIterable<Uint8Array> + write/resize/detach),
requestPause/requestResume watermark hooks, separate concurrency pool with
typed CliConcurrencyLimitError at the ceiling.
- Security: bracketed paste only when ?2004h observed; unconditional control-char
neutralization on the raw path; user keystrokes bypass neutralization.
- Persists lifecycle into the U1 CliSessionStore (create on spawn, update
state/termination).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- restore customFields on unarchive; reconcile all occupants on field-schema edits (store.ts)
- serialize per-field saves + controlled inputs in TaskFieldsSection (race fixes)
- fn_workflow_get includes layout; Array.isArray guards in validateCodeNodeSources
- per-instance graphStepActiveContext keying; rebase in instance worktree; clear run-once memo on RETHINK
- GET /api/step-parsers + registry-backed parser select (plugin parsers reachable from editor)
- translate new workflowNodes/workflowFields strings across all 5 non-en locales
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Capture the 4-list registration requirement for bundled plugins
(Settings catalog, dashboard fallback set, CLI auto-install list,
tsup staging) in docs/solutions/, and add the Bundled Plugin concept
to CONCEPTS.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>