U3: per-run IR resolution feeds the core column-agent resolver at the
runCustomNode seam; override supersedes node agent/model/persona wholesale,
defer fills bare nodes only; adoption and fallback are audited via logEntry;
raw-CLI nodes log a skip. Also fixes the customInstructions persona drift —
node-level executor:"agent" persona injection now uses the typed
soul/instructionsText fields (KTD-6).
U1+U2 of the column-agent plan: WorkflowColumnAgent on WorkflowIrColumn
(defer/override), template-subgraph column validation, v2-only-feature
registration, plugin-sdk type parity, and the shared core resolver with
instanceNodeId format ownership moved to core.
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>
Main went red when the fast-tests quality-backfill projects (PR #1385)
landed alongside the workflow-columns stream (PR #1424) — the new test
projects were written against pre-stream code:
- TaskFieldsSection.css toggle knob used background: #fff, violating the
theme-token assertion in AgentListModal's styling-parity test; use
var(--card) per the SkillsView toggle convention
- ListView.test.tsx api mock lacked fetchBoardWorkflows (TaskDetailModal
now calls it on mount)
- chat.test.ts and routes-agent-import.test.ts @fusion/core mocks lacked
registerTraitHookImpl (engine merge-trait registers hooks at import)
- auto-merge-toggle-blank.mobile and board-mobile-initial-render used
vi.runAllTimers(), which never terminates now that sse-bus starts a
keepalive setInterval; use vi.runOnlyPendingTimers()
Both quality-backfill projects now pass fully: 7151/7151 across 414
files.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Bundled fallback assertions for dependency-graph/reports now require
the bundled.js entry-file suffix instead of just containing the id
- Add route-level fallback cases for dist/index.js and src/index.ts
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Add heal block to createPluginRouter's enable handler so it matches
routes.ts (directory-path registrations re-pointed at entry files)
- Test the 400 "no loadable entry file" install branch
- Add a real-fs drift-guard test asserting the CLI and @fusion/core
copies of resolvePluginEntryPath resolve identically
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Plugins installed from Settings → Built-in Plugins registered the
manifest directory as the plugin path, but since FN-4128 the loader
requires a loadable entry FILE (Node ESM cannot import directories),
so enabling failed with "Plugin entry must be a file, got directory".
Only the CLI startup path had been migrated to entry-file resolution,
which is why CLI-auto-installed plugins worked and Settings installs
never did.
- Add resolvePluginEntryPath (bundled.js → dist/index.js →
src/index.ts) to @fusion/core; the CLI keeps its local copy (its
test fs mocks don't reach externalized core) with sync comments
both ways.
- Register the resolved entry file in both dashboard install routes;
400 with a clear message when a package has no loadable entry.
- Heal legacy directory-path registrations on enable, mirroring the
CLI's startup heal, so existing broken rows recover from the UI
without a restart.
- Route tests: assert installs register entry files, cover the
enable-route heal, and update existing install tests to the
entry-file contract.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Update test assertions for bare model IDs
- Add deduplication guard for models with same bare ID
- Add validation for empty model IDs after prefix stripping
- Add test for API key forwarded as env var to spawn
- Add test for deduplication and empty model ID guard
- Extract shared handleOpencodeGoApiKeySaved helper
- Add changeset for the published package
normalizeOpencodeGoModel was prefixing model IDs with 'opencode-go/'
(e.g. 'opencode-go/deepseek-v4-flash'), but the Pi SDK sends the model
id field as the model name in API requests. The OpenCode API expects
bare model names (e.g. 'deepseek-v4-flash'), not prefixed ones.
Now strips the 'opencode/' or 'opencode-go/' prefix entirely so the
registered model ID matches what the API expects.