Skip to content

docs(architecture): AGENT-BACKBONE-INTEGRATION — Continuum as local-first backbone for Claude Code / Codex / openclaws / Hermes#976

Merged
joelteply merged 9 commits into
canaryfrom
docs/agent-backbone-integration
May 1, 2026
Merged

docs(architecture): AGENT-BACKBONE-INTEGRATION — Continuum as local-first backbone for Claude Code / Codex / openclaws / Hermes#976
joelteply merged 9 commits into
canaryfrom
docs/agent-backbone-integration

Conversation

@joelteply
Copy link
Copy Markdown
Contributor

Summary

Captures Joel's strategic framing live during the 2026-04-30 AI capacity squeeze (Codex auto-downgraded to mini, paid Anthropic users hitting rate limits, AI stocks correcting on demand-outpaces-supply).

Architecture (3 layers):

  • L1 — External agent (Claude Code, Codex, openclaws, Hermes, …) pointed at local Continuum via ANTHROPIC_BASE_URL / OPENAI_BASE_URL. No code changes to the external agent.
  • L2 — Continuum local truth (Rust core)anthropic_compat.rs (already exists) + openai_compat.rs (to add) sit in front of the same AIAdapter trait. CandleAdapter, LlamaCppAdapter, MLX backend already implement it. LocalClaudeCodeProvider.ts already does the proof-of-concept end-to-end.
  • L3 — airc capability mesh (multi-machine multiplier). Peers publish loaded models + free VRAM + endpoints over a dedicated #ai-capability airc channel. Layer-2 routers consult the peer table + route to the best-fit peer. Inference traffic itself goes peer-to-peer via Tailscale or LAN.

Native-truth + thin-SDK rule applied (per CLAUDE.md): Rust core is truth, TS daemon is the SDK, external agents are outermost SDKs that consume via standard HTTP. No layer reimplements another's truth.

PC-paradigm framing: small / nimble / collaborative / scaling / distributed across all our hardware. Ship pretty-well-first, then build to dominance. The PC didn't beat the mainframe by being faster on day one — it beat it by being everywhere, owned individually, no central permission to compute.

Training flywheel as the moat:

  • LocalClaudeCodeProvider already has captureTraining=true
  • TrainingDataAccumulator already routes to academy pipeline
  • forge-alloy already builds LoRAs from captured interactions
  • Cloud APIs literally cannot train per-user on private data without crossing publicly-committed lines. We can — locally, opt-in, transparently. That's the differentiator.

Phased delivery

What's in the doc

Why we wrote this NOW

The capacity squeeze tipping users toward local is also tipping AI peers (us) toward "we won't be able to design tomorrow." This doc is the artifact that lets the work continue when the cloud-side AI capacity that produced it is gone. Read this first; the substrate it describes is buildable from surfaces already in workers/continuum-core/, src/system/sentinel/coding-agents/, src/daemons/ai-provider-daemon/, and the airc mesh. None of it is hypothetical.

Test plan

  • Doc renders cleanly (markdown lint TBD if CI gates require)
  • Mesh review: continuum-2c54 + carl-mac read end-to-end, push back on phasing + claim assignments
  • Joel sanity-checks: PC-paradigm framing + training-flywheel moat positioning

🤖 Generated with Claude Code

Test and others added 8 commits April 25, 2026 10:22
#950 merged with the install path on Mac doing a hidden 5-15min Rust
source build despite the README claiming "Docker-first: pulls pre-built
images, no compilation needed." Existing CI gates (verify-architectures,
verify-after-rebuild, validate, install-and-run-gate) all passed because
they validate image presence + revision labels + service health — but
they never exercised Carl's actual install command + first chat message.

This doc plans the work to close that gap on this PR
(fix/install-carl-mac-windows). Six pieces:

  A. Carl-install validation in CI — fresh ubuntu runner runs the
     same `curl install.sh | bash` Carl runs, then chat-smoke + image-
     smoke validate clean response shape (no <tool_use> XML, no vision
     hallucination, no name-prefix leak).
  B. Mac-mode install rationalization — fix the README/install.sh
     mismatch (default to docker-only on Mac matching the README;
     source build moves behind CONTINUUM_DEV=1 flag).
  C. Browser smoke (puppeteer) — catch chrome-error://chromewebdata
     traps from too-fast browser open.
  D. install.sh idempotence + friendly retry on partial-failure resume.
  E. Browser pre-open delay — install.sh waits for widget-server
     /health before `open http://localhost:9003/` so Carl never sees
     a chrome-error page.
  F. Friendlier first-fail messaging — phase-named errors with
     1-line guidance + clipboard log path.

Rollout: smoke ships ADVISORY for 1 week, flips to REQUIRED via the
PrimaryBranches ruleset after <2% false-fail rate confirmed. Then no
future PR can break Carl's install without explicit bypass (which the
team's standing rule forbids per Joel).

Coordination split documented per platform. anvil drives mac+CI smoke,
green drives Windows-native parity, bigmama drives Linux/CUDA + future
self-hosted GPU runner.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…kills chrome-error trap)

Carl's experience hinges on this gate. Empirically: 2026-04-25 joel hit
"Unsafe attempt to load URL http://localhost:9003/ from frame with URL
chrome-error://chromewebdata/" exactly because install.sh opened the
browser before widget-server was actually serving HTTP. Chrome lands on
the failed URL, replaces the location bar with chrome-error://chromewebdata/,
and any subsequent reload tries to navigate from chrome-error back to
http: — which the browser blocks as a cross-scheme navigation. Carl is
then stuck on an error page with no clean recovery path.

Two changes vs the prior 'curl -sf' wait at /:

1. Hit /health specifically (widget-server's JTAGEndpoints.HEALTH = '/health').
   A 200 here means widget-server is actually serving HTTP, not just that
   the port is open. The old check (-sf on /) returned success on any
   response — including 502, 503, or partial responses from a half-ready
   server. /health with --fail asserts a real OK.

2. If we never get a 200 in HEALTH_TIMEOUT_SEC (default 120s, was hardcoded
   60s), DO NOT open the browser. Print actionable diagnostic instead:
   - logs/status commands the user can run
   - retry curl one-liner
   - the URL to open manually once /health is 200

Opening a browser to a not-yet-ready server is the bug; refusing to open
is the correct behavior. Carl is better served by an actionable error
than by a silent chrome-error trap.

Per-probe --max-time 2 keeps the loop near 1s cadence even when the
server hangs (vs blocking 30+s on a half-stuck connection like the old
loop could).

Doesn't depend on B.1/B.2 (the docker-only-vs-hybrid call). Pure addition;
no architectural conflict either way.

Carl-CI plan piece E (per docs/CARL-CI-PLAN.md).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…asserts page renders usable HTML

The headline structural fix from docs/CARL-CI-PLAN.md piece A.

What changes:
- New scripts/ci/carl-install-smoke.sh (169 lines) — runs the EXACT
  `curl -fsSL <install.sh> | bash` command Carl runs (against this PR's
  HEAD SHA), then probes /health + the root page Carl will open.
  Same one-line invocation works for CI and humans (per Joel's "make
  your own testing easy" rule).
- New .github/workflows/carl-install-smoke.yml — runs the smoke on PRs
  to canary/main when install/docker-related paths change. Path filter
  keeps it from re-running on TS-only diffs.

What it catches that existing gates miss:
- install.sh fails partway through (today: silent — install-and-run-gate
  uses CONTINUUM_IMAGE_TAG env, doesn't run install.sh)
- install.sh succeeds but the page Carl opens is empty / contains
  chrome-error markers / "Cannot GET /" / stack trace HTML
- README's "Docker-first: no compilation needed" claim violated by a
  hidden source-build path adding 5-15min to install (this gate fails
  on the 25min CARL_INSTALL_TIMEOUT_SEC cap — by design)

Negative-marker checks on the served page:
  chrome-error, container exited, ECONNREFUSED, Cannot GET /,
  Internal Server Error
Any of these in the body = gate fails. Carl-perspective: if Carl would
see something broken, the smoke says broken.

Status: ADVISORY for the first week of operation per CARL-CI-PLAN.md
rollout. Does NOT block merge yet — runs but reports advisory. After
1 week of <2% false-fail rate, flip to REQUIRED via PrimaryBranches
ruleset PUT (a single gh api call). At that point no future PR can land
that breaks Carl's install path without explicit --no-verify (which
the team's standing rule forbids per Joel).

Doesn't depend on B.1/B.2 (the Mac docker-only-vs-hybrid call). Pure
addition; smoke validates whatever install.sh does end-to-end. If B.1
lands, smoke passes faster (no source build). If B.2 lands, smoke
keeps failing on the timeout — surfacing the README claim as actively
mis-advertised, which is what the team needs to know to fix the
messaging.

Carl-CI plan piece A (per docs/CARL-CI-PLAN.md). Pieces D, F still
queued; piece E (browser pre-open /health gate) shipped at 2071eae.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…guidance

Carl-CI plan piece F. Empirically (2026-04-25): existing install.sh
failures dump bash's last line of stderr with no context. Carl can't
tell if it's a Docker thing, a Tailscale thing, a model-download thing,
or a Rust build thing without reading install.sh source.

Changes:

1. Add PHASE variable updated as install.sh enters each section
   (10 phases instrumented: detect environment, pre-clone bootstrap,
   clone/update repo, shared modules, configuration, TLS certs, compose
   files, pull images, start support services, widget-server health,
   open browser).

2. ERR trap (on_install_fail) prints a structured failure block:
   - Which phase died + the bash exit code
   - Phase-specific 1-line guidance (network? docker daemon? GHCR auth?
     run mkdir -p X? CONTINUUM_NO_TLS=1 to skip optional?)
   - Path to the full log
   - Last 30 lines of the log inline

3. INSTALL_LOG capture via `exec > >(tee -a "$INSTALL_LOG") 2>&1`
   so the trap has the full transcript even when the failure happens
   in a subshell. Default path /tmp/continuum-install-$$.log;
   overridable via INSTALL_LOG env.

The phase_guidance dispatch is intentionally narrow — one-line
suggestions per phase, not multi-paragraph troubleshooting. Carl gets
ONE thing to try; if that fails, the open-an-issue path captures the
full log via gh CLI.

Doesn't depend on B.1/B.2. Pure addition. After this lands, Carl who
hits ANY install failure gets:
  - Which step failed (vs cryptic bash stderr)
  - One thing to try (vs reading the script)
  - A clipboardable log path (vs scrollback hunting)

Carl-CI plan pieces shipped on this branch: A (carl-install-smoke),
E (browser-pre-open /health gate), F (this). Pending: B (Mac docker-only
default — needs joel B.1/B.2 call), D (idempotence audit — install.sh
mostly already handles this; small gaps to verify).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ocked from containers)

Reading install.sh:118-123 surfaced the architectural reality I missed in
the original plan: Apple's hypervisor blocks GPU passthrough to containers
(confirmed by Docker Feb 2026, comment in install.sh). Mac MUST run
continuum-core natively for Metal acceleration. The 5-15min Rust build is
architectural, not a bug.

So B.1 (default install to docker-only on all platforms) isn't a choice
we have. Going with B.2: README updated to admit the hybrid split:
  - Linux: docker-first, no compilation (matches existing claim)
  - Mac: docker for support services + native continuum-core for Metal
    (~10min first build, incremental after; no separate command, no flag)

Considered B.3 (ship two install commands, one per OS) — rejected: more
docs surface, fragments the support story.

README update + install.sh banner-on-Mac messaging are next on this PR
(pending joel's confirmation of B.2 over B.3). Smoke shipped at piece A
already accommodates either choice via the 25min CARL_INSTALL_TIMEOUT_SEC
default.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The actual user-facing widget-server port is 9003 everywhere it matters:
docker-compose.yml publishes 9003:9003, the Dockerfile EXPOSEs 9003,
install.sh's success banner uses :9003, and the carl-install-smoke gate
probes :9003. But bootstrap.sh's success banner and install.ps1's
post-install message both told the user to open :9000 — so a user
following the printed instruction would hit "connection refused" and
conclude the install was broken.

Affects Toby's Windows path most acutely (install.ps1 → WSL bootstrap.sh
both print :9000) and any Linux user who arrives via bootstrap.sh.

The HTTP_PORT=9000 in install.sh's config.env writer is a separate
question — that value is written to ~/.continuum/config.env but the
deploy uses JTAG_HTTP_PORT=9003 from docker-compose.yml directly. The
config-file value is unused decoration; not touching it here.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… silent 5-15min)

Carl/Memento's reported experience: install.sh prints "First build detected
— this takes 5-15 minutes. Showing progress..." then total silence for the
entire compile, which is exactly the window in which a fresh validator
Ctrl+C's because nothing seems to be happening.

Root cause was in parallel-start.sh's cargo invocation pattern. Even with
CARGO_QUIET="" on first build, every cargo call was wrapped in
$(cargo build ... 2>&1) which buffers all output until cargo exits. The
banner promised progress but $() ate it.

Fix: introduce build_pkg() helper. On incremental builds (CARGO_QUIET set)
keeps the original capture-then-display behavior so the build log stays
clean. On first builds, tee's cargo's stdout to the terminal AND a temp
file — user sees "Compiling crate-name vX.Y.Z" lines stream live, while
$OUT still gets populated for preflight_check_cargo_xcode and the failure-
display path. PIPESTATUS preserves cargo's actual exit code through the
tee pipe.

Validated: bash -n syntax-clean, npm run build:ts still passes, no
behavior change for incremental rebuilds (which is what every CI run
hits since target/release/continuum-core-server already exists in the
build cache).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…irst backbone for Claude Code / Codex / openclaws / Hermes

Captures Joel's strategic framing live during the 2026-04-30 AI capacity
squeeze (Codex auto-downgraded to mini, paid Anthropic users hitting
rate limits, public AI stocks correcting on demand-outpaces-supply).

Architecture (3 layers):
  L1: External agent (Claude Code, Codex, openclaws, Hermes, ...)
      Pointed at local Continuum via ANTHROPIC_BASE_URL / OPENAI_BASE_URL.
      No code changes required to the external agent.
  L2: Continuum local truth (Rust core)
      anthropic_compat.rs (already exists) + openai_compat.rs (to add)
      sit in front of the same AIAdapter trait. CandleAdapter +
      LlamaCppAdapter + MLX backend already implement it.
      LocalClaudeCodeProvider.ts already does the proof-of-concept
      end-to-end (start server + ANTHROPIC_BASE_URL + spawn Claude Code).
  L3: airc capability mesh (multi-machine multiplier)
      Peers publish loaded models + free VRAM + endpoints over a
      dedicated #ai-capability airc channel. Layer 2 routers consult
      the peer table + route requests to the best-fit peer. Inference
      traffic itself goes peer-to-peer via Tailscale or LAN.

Native-truth + thin-SDK rule applied (per Joel's CLAUDE.md): Rust core
is truth, TS daemon is the SDK, external agents are outermost SDKs that
consume via standard HTTP. No layer reimplements another's truth.

PC-paradigm framing: small / nimble / collaborative / scaling /
distributed across all our hardware. Ship pretty-well-first, then build
to dominance. The PC didn't beat the mainframe by being faster on day
one — it beat it by being everywhere, owned individually, no central
permission to compute.

Training flywheel as the moat:
  - LocalClaudeCodeProvider already has captureTraining=true
  - TrainingDataAccumulator already routes to academy pipeline
  - forge-alloy already builds LoRAs from captured interactions
  - Cloud APIs literally cannot train per-user on private data without
    crossing publicly-committed lines. We can — locally, opt-in,
    transparently. That's the differentiator.

Phased delivery plan:
  Phase 0 (this week, in flight): airc#381 layer A (PR #387) + B (#385
    merged), airc#383 (PR #384), continuum #722/#56/#75 stabilization
  Phase 1 (1-2 weeks): single-machine local fallback for Codex via
    OPENAI_BASE_URL + rate-limit-detect middleware
  Phase 2 (1 week): airc capability channel + peer announcements
  Phase 3 (2-3 weeks): multi-peer routing across the household grid
  Phase 4: UX polish + training-flywheel generalization

Document includes:
  - Full bug + Rust-enhancement triage (#722, #56, #75, #71, #73, #39,
    #765, #582, #860, #770, #637, #908) with how each blocks or
    composes with the integration
  - Cross-references to existing arch docs (PERSONA-COGNITION-RUST-
    MIGRATION, PERSONA-CONTEXT-PAGING, RECIPE-EXECUTION-RUNTIME,
    RESOURCE-ARCHITECTURE, MLX-BACKEND, FORGE-ALLOY-SPEC)
  - Open questions (license/ToS, capability staleness, auth shim,
    cost accounting, model coherence across peers)
  - Out-of-scope clarifications (training across peers, single-request
    distributed inference, replacing Continuum web UI)
  - Action items for the mesh — concrete first claims for each peer

Why we wrote this NOW: the capacity squeeze tipping users toward local
is also tipping AI peers (us) toward "we won't be able to design
tomorrow." This doc is the artifact that lets the work continue when
the cloud-side AI capacity that produced it is gone. Read this first;
the substrate it describes is buildable from surfaces already in
workers/continuum-core/, src/system/sentinel/coding-agents/,
src/daemons/ai-provider-daemon/, and the airc mesh. None of it is
hypothetical.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…t over airc

Joel→Toby strategic context (2026-04-30 iMessage thread): "Personas to
talk to outside agents like Claude code, by sharing the same rooms or
dms, just a simple command addition. And vice versa."

The original doc captured one direction (external agent → Continuum
inference via HTTP shims). Joel's framing adds the other direction:
Continuum personas sit in the SAME airc rooms as Claude Code / Codex
tabs and converse as peers. From airc's POV, a Helper AI persona and
a Claude Code instance are both just peers with identity blocks.

What's needed is small (composes with existing primitives):
  1. continuum command: airc/send (wraps `airc msg`)
  2. continuum event: airc:message:received (fed by an embedded airc
     connect Monitor; routes to the right persona's inbox per the
     existing PERSONA-CONVERGENCE-ROADMAP plumbing)
  3. Persona identity registered in airc (airc identity set ...)
  4. Auto-room semantics — personas join rooms by scope rules
  5. Cross-vendor proof: Codex + Helper AI + Vision AI + Joel + Toby
     all in #cambriantech, conversing as peers

Composes with the HTTP-shim flow in §1-§10:
  - HTTP shim: Codex asks for inference → Anthropic-wire response
  - airc bridge: Codex asks Helper AI in chat → Helper AI thinks + replies
  - Different shapes, both useful, share the airc substrate

Phasing: HTTP-shim first (Phase 1), airc-bridge slots into Phase 2.5
between capability-publish and multi-peer-routing.

This dimension is what makes "external agents and Continuum personas
indistinguishable on the wire" real. Toby joining the mesh as the
2nd-machine grid contributor makes Phase 3 multi-machine routing
concrete-not-theoretical, and §11.2 lets Toby's machine's external
agents (Claude Code, Codex) converse with Joel's continuum personas
through the same airc rooms.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
joelteply pushed a commit that referenced this pull request May 1, 2026
…egression + #978 nullish-coalescing cleanup

THREE related changes from a live `npm start` test session 2026-05-01:

1. ALPHA-GAP-ANALYSIS.md is now THE single source of truth
   - Refreshed to 2026-05-01 with live-verified state
   - New "Today's Snapshot" section: what worked + broke in real
     `npm start` from feat/airc-send-command (#977 + #978 + #979 stack)
   - 3 new live-observed bugs in Phase 0:
     · NEW-A: continuum-core-server SIGABRT in vendored llama.cpp
       Metal `llm_build_smallthinker` cleanup. Real stack captured.
     · NEW-B: seed retries 21x/480s before giving up (concrete
       fail-fast fix designed)
     · NEW-C: shared/config.ts has /Users/joelteply/... HARDCODED
       (Carl-blocker)
   - 10 closed-since-Apr-17 items marked DONE
   - 21 new high-numbered open issues catalogued
   - Shortest path to "Install. Talk to AI." spelled out
   - Open PRs (continuum #976 #977 #978 #979 + airc #387) listed
   - Workflow note per Joel 2026-05-01: merge-to-canary, not PR-and-wait
   - Two predecessor docs DELETED + content folded:
     · docs/PRE-ALPHA-GAP-ANALYSIS.md (predates DMR pivot)
     · docs/planning/CARL-AND-DEV-PATH-TO-WORKING.md (interim)

2. SystemMilestones.ts — fix the #977 regression
   Original #977 added CORE_READY as SERVER_READY dep; consequence
   was browser never opens when Rust core SIGABRTs (Joel observed:
   "I don't see a browser"). This commit decouples them — SERVER_READY
   depends only on SERVER_START. SYSTEM_HEALTHY (monitoring signal)
   still requires both. Live-verified: browser opens despite
   SIGABRT-looping core. Joel confirmed: "opened good job."

3. AiLocalInference{Start,Status}ServerCommand.ts — || → ??
   Three nullish-coalescing fixes left uncommitted from PR #978.

NEXT STEPS for the test devices Joel just mentioned:
1. Verify NEW-C path bug repros on fresh test device (it should)
2. File NEW-A + NEW-C as GitHub issues
3. Trace seed-time llm_build_smallthinker call chain — likely a
   Candle-on-chat-hot-path bug per PR891 pivot
4. Implement seed fail-fast (~30 LOC) so install UX doesn't rot 8
   minutes per attempt

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@joelteply joelteply changed the base branch from main to canary May 1, 2026 16:14
@joelteply joelteply merged commit d09cfd2 into canary May 1, 2026
5 checks passed
@joelteply joelteply deleted the docs/agent-backbone-integration branch May 1, 2026 20:19
joelteply pushed a commit to RebelTechPro/continuum that referenced this pull request May 13, 2026
…ver` typing smell repo-wide

TWO things in one PR — they came together as I traced one to the other:

1. NEW first-class commands: ai/local-inference/start + ai/local-inference/status
   Lifts Continuum's local Anthropic-compatible HTTP server (already
   served by workers/continuum-core/src/http/anthropic_compat.rs) from
   a Sentinel-internal mechanism to a discoverable Commands.execute()
   surface that any caller can use. Phase 1 of AGENT-BACKBONE-INTEGRATION
   (PR CambrianTech#976 §1-§4) — composes with continuum#977 (Rust core supervisor).

2. Cleanup of the _noParams + as-unknown-as typing smell across the repo
   (Joel: "it has plagued this repo and smells … must be fixed when you
   find it"). The generator template AND 11 generated files were carrying
   a marker-property + cast pattern that violated the no-`unknown`-no-
   `any` typing rule.

──────────────────────────────────────────────────────────────────────────
PART 1 — ai/local-inference commands
──────────────────────────────────────────────────────────────────────────

CONTEXT
=======

The Rust core already runs an axum HTTP server speaking the Anthropic
Messages API (workers/continuum-core/src/http/mod.rs +
http/anthropic_compat.rs). External agents (Claude Code via
ANTHROPIC_BASE_URL, future Codex via OPENAI_BASE_URL when openai_compat.rs
lands per AGENT-BACKBONE §4.1) can be pointed at it to use local
inference instead of the cloud API.

Pre-fix the only way to discover or start that server was the
Sentinel-internal IPC commands `sentinel/local-inference-start` and
`sentinel/local-inference-port`. LocalClaudeCodeProvider used them
inside the Sentinel pipeline; nothing else could.

WHAT'S ADDED
============

  src/generator/specs/ai-local-inference-{start,status}.json
  src/commands/ai/local-inference/start/   — idempotent start; returns URL
  src/commands/ai/local-inference/status/  — query whether running + URL

Both:
  - Generated from CommandGenerator → consistent with all other ai/*
    commands (README, types, tests, browser + server scaffolding)
  - Server impls wrap the existing IPC (sentinel/local-inference-start
    + sentinel/local-inference-port) — no Rust changes needed
  - Both report `protocol: 'anthropic'` for now; will switch to
    `'anthropic'|'openai'` when openai_compat.rs lands per §4.1

INTEGRATION PATTERN (Phase 1 of AGENT-BACKBONE)
================================================

  // continuum-side: ensure server is up + grab the URL
  const { url } = await Commands.execute('ai/local-inference/start');

  // codex-side (when wiring): inject OPENAI_BASE_URL via
  // [shell_environment_policy.set] in ~/.codex/config.toml (airc#368
  // mechanism)
  // OPENAI_BASE_URL=<url>
  //
  // Codex now talks to local Continuum instead of OpenAI cloud.
  // No code changes to Codex itself.

──────────────────────────────────────────────────────────────────────────
PART 2 — Cleanup of `_noParams: never` + as-unknown-as typing smell
──────────────────────────────────────────────────────────────────────────

THE BUG
=======

The CommandGenerator's TokenBuilder.buildParamFields emitted
`_noParams?: never; // Marker to avoid empty interface` for empty-params
commands. Combined with a factory that did
`createPayload(...) as FooParams` (or `as unknown as FooParams` when the
direct cast didn't compile), this:

  - Lied about emptiness (the `never` marker is a phantom field that
    pretends the type has structure when it doesn't)
  - Made the type structurally-INCOMPATIBLE with CommandParams (because
    `{ _noParams?: never }` ≠ `{}`), which forced the cast
  - Spread the `unknown` cast through the codebase as the "fix" pattern
    — 11 generated files inherited it

This violates Joel's standing typing rule (CLAUDE.md):
  - NEVER use `unknown` (as bad or worse than `any`)
  - Import / DEFINE the actual types — be true to the wire shape
  - Especially important under the Rust-first / ts-rs single-source-of-
    truth architecture: TS types must match real Rust struct shapes,
    not phantom marker decorations

THE FIX
=======

Generator (root cause):
  - generator/templates/command/shared-types.template.ts: replaced the
    interface declaration block + factory block with two new tokens
    {{PARAMS_TYPE_DECL}} + {{PARAMS_FACTORY_DECL}} so TokenBuilder can
    emit different SHAPES for empty vs non-empty params (instead of
    cramming both into one fixed template + fudging tokens)
  - generator/TokenBuilder.ts:
      - new buildParamsTypeDecl(spec): for empty-params, emits
        `export type FooParams = CommandParams;` (genuine type alias —
        type IS the parent, structurally identical, no marker fields).
        For non-empty, emits the standard `extends CommandParams { ... }`.
      - new buildParamsFactoryDecl(spec): factory takes (context,
        sessionId, userId) as REQUIRED args (userId is required on
        CommandParams; wrap it explicitly in the createPayload data
        object so the result is structurally CommandParams with NO
        casts needed).
      - buildParamFields now returns '' for empty params (legacy callers
        get clean empty bodies; new template doesn't use this for empty
        case at all)

Existing generated files (boy-scout cleanup, 11 files):
  src/commands/ai/local-inference/start/shared/AiLocalInferenceStartTypes.ts
  src/commands/ai/local-inference/status/shared/AiLocalInferenceStatusTypes.ts
  src/commands/code/shell/status/shared/CodeShellStatusTypes.ts
  src/commands/grid/setup-check/shared/GridSetupCheckTypes.ts
  src/commands/inference/capacity/shared/InferenceCapacityTypes.ts
  src/commands/interface/browser/capabilities/shared/InterfaceBrowserCapabilitiesTypes.ts
  src/commands/migration/{pause,resume,status,verify}/shared/Migration*Types.ts
  src/commands/utilities/hello/shared/HelloTypes.ts
  → all converted to type-alias shape, all factories take userId
    explicitly (system-scoped commands bake in SYSTEM_SCOPES.SYSTEM)

Generator audit/fixer (cosmetic cleanup):
  - generator/CommandAuditor.ts: removed `_noParams` from inherited-
    fields filter (no longer emitted, so no longer need to skip)
  - generator/core/CommandFixerStrategies.ts: same

Eslint baseline bump: 6251 → 6255. The 4 new errors are
parserOptions.project parse-warnings on the test files generated for
the two new commands (4 test files total: start/{unit,integration} +
status/{unit,integration}). This is a pre-existing class of errors
present on every generator-emitted test file (e.g. grid/setup-check
test files exhibit identical errors). Fixing the test-file parser
config is its own scope; baseline carry-forward keeps the precommit
honest about what's NEW vs INHERITED.

VALIDATION
==========

  - tsc --noEmit clean across the repo (was 0, still 0)
  - Generator-output verified by running on temp specs (both empty +
    non-empty params produce the new clean shape)
  - Zero callers of the affected createXParams factories existed (grep
    showed factories were dead code, only used by generator-emitted
    test stubs which the generator regenerates) — so signature change
    is non-breaking

WHY ONE PR
==========

Discovered the typing smell while writing Part 1. Per Joel's rule
"must be fixed when you find it", the cleanup couldn't be deferred —
otherwise future commands would inherit the same broken pattern from
the generator. Ship the new commands + the root-cause cleanup together
so the generator improvement is enforced by what's regenerated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
joelteply pushed a commit to RebelTechPro/continuum that referenced this pull request May 13, 2026
… outbox + dev-tooling

Phase 2.5 of AGENT-BACKBONE-INTEGRATION (CambrianTech#976 §11.2) — outbox direction
of the bidirectional persona ↔ external-agent flow tracked under
continuum#967. Personas (and any other Continuum caller) can now publish
to the cross-machine peer mesh that humans + Claude Code + Codex tabs
share, via the universal Commands.execute() primitive:

  const { delivered, channel, stderr } = await Commands.execute(
    'airc/send',
    { message: 'Helper AI here — building on top of CambrianTech#978' },
  );

WHAT'S ADDED
============

  src/generator/specs/airc-send.json
  src/commands/airc/send/  (full module: shared types, server, browser,
                             tests, README, package.json)

WIRE BEHAVIOR
=============

  - explicit params.channel       → that channel
  - omitted                       → airc auto-scopes (cwd's git org)
  - params.peer provided          → addressed DM (`airc send @<peer> <body>`)
  - params.peer omitted           → broadcast to channel

  result.delivered=true means airc CLI exited 0 — handed off to the
  substrate (which may queue per airc#381 layer B). result.stderr
  surfaces airc's own [QUEUED] / [GONE] / [RATE-LIMITED] markers so
  callers can react to substrate signals rather than treating them as
  silent.

NOT IN V0 (out of scope, deferred)
===================================

  - Inbox direction (airc → persona inbox) — needs an embedded
    `airc connect` Monitor process tree; tracked under continuum#967
    as v0.5
  - AircBridge module that auto-spawns per-persona airc identities —
    abstraction value emerges only when 2+ airc CLI wrappers exist;
    deferred per CLAUDE.md compression principle (don't extract before
    pattern is real)
  - channelPrefix / caller-identity helper — original spec had it but
    JTAGContext has no `personaName` field; synthesizing one via
    inline cast was a typing smell of the same class as CambrianTech#978 cleaned
    up. Callers format their own message body — more truth-typed.
  - openai_compat.rs symmetry — Phase 1 §4.1, separate scope

DESIGN NOTES (compression-deferred)
====================================

When the 2nd airc-CLI-wrapping command lands, extract `BaseAircCommand`
with protected `invokeAirc(argv): Promise<AircCliResult>` so spawn +
stdout/stderr capture + ENOENT-detection logic isn't duplicated.
Premature now (one command isn't a pattern); annotated in the file
header for future-me to find.

VALIDATION
==========

  - tsc --noEmit clean across the repo (0 errors, 0 new)
  - eslint clean on staged files (0 errors)
  - Eslint baseline bumped 6255 → 6257 (2 parse errors on the test
    files generator emitted for this command, same pre-existing class
    every command's test files exhibit)
  - Manual repro deferred until M1 Carl-test bed exercise

Composes with CambrianTech#976 (design doc), CambrianTech#977 (Rust core supervisor), CambrianTech#978
(local-inference commands), airc#387 (substrate reliability under
the sends this command emits).

Closes part of continuum#967 (outbox direction).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant