# cap (taicpp) Port of the CAP section of `../tai.py` (`CAP_VERSION` ... `TAI_CAP_MANIFEST`), normative text in `../cap.md`. Namespace `detz::cap`, sources in `src/cap/`, one class per `.h`/`.cpp` pair. The C++ names are the Python names in camelCase; behavior is verified by running both programs (see Verification). ## Spec delta (this milestone) - Real `TAI_CAP_MANIFEST` replaces the one-line placeholder (`CapAgentManifest::TAI_CAP_MANIFEST`, `--cap-manifest` prints it plus the `print()` newline; byte-identical to `tai.py --cap-manifest`). - All Cap classes ported: events, errors, bindings, manifest, registry, session config, stream-json, speak session, the four drivers, answerer, session, fleet, CLI. The old `Orchestrator` grant/check placeholder is gone. - Layer `wire_*` (namespace `detz::wire`) supplies what Python's stdlib gave tai.py: `Json` (json), `WireToml` (tomllib), `WireRegex` (re), `WireProc` / `WirePty` (subprocess, PtyProc), `WireHttp` (urllib, http.client, http.server), `WireArgParse` (argparse), `WireShlex`, `WireDifflib`, `WireHash`, `WireText`, `WireOs`. ## Parity table (Python -> C++) | Python | C++ | |---|---| | `CAP_VERSION` `CAP_MANIFEST_SCHEMA` `CAP_PROFILE_CODING` `CAP_MANIFEST_REVISION` `CAP_PROBE_TIMEOUT` `CAP_ERROR_CODES` `CAP_ERROR_STRINGS` `CAP_SESSION_CONFIG_ENV` `CAP_SPEAK_MAX_TURNS` `CAP_SPEAK_DIFF_MAX` `CAP_PTY_SETTLE_MS` `CAP_PTY_IDLE_MS` `CAP_PTY_SUBMIT_GAP_MS` `CAP_PTY_RESUBMITS_MAX` `CAP_PTY_SCROLLBACK` `CAP_PTY_INIT_TIMEOUT` `CAP_STREAM_LINE_MAX` `CAP_STREAM_STDERR_KEEP` `CAP_SESSION_POLL` `CAP_TURN_TIMEOUT` `CAP_WORKTREE_DIR` `CAP_ROUTE_QUEUE_MAX` `CAP_FLEET_DIFF_MAX` `CAP_MENTION_RE` `CAP_ACP_VERSION` `CAP_ACP_TERMINALS_MAX` `CAP_ACP_TERM_BUF` `CAP_ACP_TERM_WAIT_MAX` `CAP_ACP_FS_MAX` `CAP_A2A_TIMEOUT` `CAP_ANSWER_TIMEOUT` `CAP_ANSWER_PARALLEL` `CAP_ANSWER_MARKER` `CAP_ANSWER_RISKS` | `cap_constants.h` (same names) | | `CAP_ERROR_SYMBOLS` | `capErrorNumeric()` in `cap_error.h` | | `TAI_CAP_MANIFEST` | `CapAgentManifest::TAI_CAP_MANIFEST()` | | `CapError` | `cap_error.h` | | `CapEvent` | `CapEvent` (`KINDS`, `DIRECTIONS`, `REQUIRED`, `ENUMS`, `make`, `validate`, `direction`) | | `CapBinding` | `CapBinding` (`ORDER`, `UNSUPPORTED`, `declared`, `select`) | | `CapManifest` | `CapManifest` | | `CapRegistry` | `CapRegistry` | | `CapSessionConfig` | `CapSessionConfig` | | `CapStreamJson` | `CapStreamJson` | | `CapSpeakSession`, `cap_interrupt`, `cap_speak_main` | `CapSpeakSession`, `CapInterrupt`, `CapSpeakMain` | | `CapDriver` | `CapDriver` (`CapDriver::create` in `cap_driver_factory.cpp`) | | `CapPtyDriver` | `CapPtyDriver` over `WirePty` and `CapVtScreen` (port of `TuiVtScreen`, `CapScreen` seam) | | `CapPipeReader`, `cap_proc_kill`, `cap_proc_reap`, `cap_stderr_drain` | `CapPipeReader`, `CapProcUtil`, `CapStderrDrain` | | `CapStreamDriver` `CapAcpDriver` `CapA2aDriver` | same names | | `CapAnswerer` | `CapAnswerer` | | `CapTurnResult` `CapSession` `CapFleet` | same names | | `cap_cli_main` `cap_cli_help` `cap_resolve_main` `cap_chat_main` `cap_run_main` `cap_fleet_plan` `cap_print_result` `cap_build_answerer` `cap_answer_flags` | `CapCli` static methods | ## Event kinds `cap.session.ready`, `cap.text_chunk`, `cap.tool_call.start`, `cap.tool_call.delta`, `cap.tool_call.end`, `cap.plan`, `cap.thought`, `cap.ask_user`, `cap.permission.request`, `cap.usage`, `cap.error`, `cap.artifact`, `cap.fs.change`, `cap.pty.raw_bytes`, `cap.user_input.inject`, `cap.cancel`, `cap.session.config`, `cap.ask_user.answer`, `cap.permission.response`. ## Errors Symbols: `cap_manifest_missing`, `cap_session_config_missing`, `cap_unsupported_capability`, `cap_budget_exceeded`, `cap_invalid_answer`, `cap_unknown_permission`, `cap_queued_input_unsupported`, `cap_cancel_unsupported`, `cap_binding_unavailable`, `cap_profile_unsupported`, `cap_scope_violation`, `cap_path_not_found`, `cap_io_error`, `cap_terminal_unavailable`, `cap_terminal_killed`. String codes: `rate_limit`, `auth_error`, `tool_timeout`, `context_overflow`, `model_unavailable`, `pty_died`, `parse_failed`, `git_dirty`, `test_failed`, `merge_conflict`, `language_server_down`. ## Seams (host-provided) `CapAgentPort` / `CapSpeakHost` (the agent for `--cap-speak`), `WireLog` (debug log sink), `WireApp::setVersion` (VERSION on the wire), `CapPtyDriver::setScreenFactory` (swap the VT screen). Details and the exact hooks: `docs/wiring/capmcp.md`. ## Differences from Python (all declared) - `CapAnswerer` runs `[binary, --auto, prompt]` directly; Python ran `[sys.executable, tai.py, ...]`. Default binary is `/proc/self/exe`. - `CapVtScreen` parses SGR but does not store colors/attributes (text-only consumer) and keeps no scrollback history. - Regex: ECMAScript engine with Python syntax translation. Python-only syntax beyond named groups, `\A`, `\Z`, `$`, bare `{`/`}`, leading `(?i)` is not translated; compile error texts differ from `re.error`. - argparse help is plain (no 3.14 color). - Dates in TOML come back as strings. - Terminal `terminal/output` keeps the `read(65536)` blocking-chunk behavior. - Signals: SIGINT/SIGTERM set a flag polled by waits (`CapInterrupt`). ## Model catalog and `@agent` mentions `CapModelCatalog` (`src/cap/cap_model_catalog.*`) lists agents from the `*.toml` manifests in the registry user and system dirs (cap `CAP_AGENT_MAX` 64) and checks each: manifest parses, binary on PATH. Status: `ready`, `no_binary`, `bad_manifest`, `no_manifest`. Entries are cached `CAP_CATALOG_TTL` (60) seconds; entries load in parallel. Model protocol (tai-specific extension, absent from the Python app): `[models] list = [...]` and `default` in the manifest, plus an optional probe, `[models] probe = ["bin", "--cap-models"]` or `capabilities.models = true` for ` --cap-models`. Probe output: JSON array of ids or `{id|name|model}` objects, `{"models": [...], "default": "..."}`, or one id per line. Bounds: `CAP_MODEL_PROBE_TIMEOUT` 5 s, `CAP_MODEL_PROBE_MAX_BYTES` 1 MiB, `CAP_MODEL_MAX` 500 per agent. A failed probe is recorded on the entry, never thrown. Tai's own manifest is unchanged and declares no models. Mention resolution (after bots, providers, backends, known model ids, `@auto`, exact agent, and file paths; so a real file is never shadowed): the typed text is fuzzy-matched with `FuzzyMatcher` ([fuzzy.md](fuzzy.md)). Order: exact agent name or model id; `head:tail` splits at the first `:`, head fuzzy to a ready agent, tail fuzzy to that agent's models (no floor, the agent already disambiguates); otherwise the whole text is ranked against every ready agent name and every model. Cross-namespace picks need a score of `CAP_FUZZY_FLOOR` (0.3); the best score wins and equal scores resolve shorter id then list order, never an error. Unusable agents (no binary, bad manifest) are considered only when no ready candidate reaches the floor and then report why. An agent without a model list passes the model through with a note. When the pick differs from the typed text a note line shows the resolution and score. With a prompt the turn runs on that agent (`CapMention`, one cached `CapSession` per agent/model/cwd, model sent via `startup.model_arg`); without a prompt the agent is pinned (`Agent::agent_pin`) until `@auto`. A miss prints the closest candidate with its score and the full per-agent report (status, binary, manifest, binding, models, default, probe error); an unknown tag appends the same report. Gaps: no TUI completion for agents yet; `tai --cap-models` is not implemented on tai's own side. ## Verification `tests/capmcp_diff.py` (JSON, TOML, str(), float repr, regex vs CPython), `tests/capmcp_cli_diff.py` (`--cap-manifest`, `--cap resolve|chat|run` incl. stream-json, PTY, ACP and A2A fake agents, argparse errors at four widths vs `tai.py`), `tests/capmcp_unit.cpp` (tables and drift check).