AMPARO_INFERENCE_PROVIDER now accepts a catalog id or alias — anthropic, openai, moonshot (kimi), deepseek, qwen, glm, mistral, groq, openrouter, gemini (its OpenAI-compatible endpoint), ollama, lmstudio, or custom — each carrying its default endpoint and model; an unknown id errors with the catalog list instead of a bare parse failure./chat/completions are stripped, a bare host gains /v1, and a URL without a scheme and host is rejected with a clear error.think, format, web_search) no longer leak into the payload — strict endpoints (Moonshot, DeepSeek-class) stop rejecting requests with HTTP 400 — and JSON-schema requests map to the standard response_format field. The native Ollama path keeps think.reasoning_content round-trips into the next request for providers the catalog marks as reasoning (Kimi, DeepSeek, Qwen, GLM), so multi-turn tool calling keeps working; other providers never see the field.[infer] status line (boot banner and wizard summary) names the catalog label and model — e.g. Moonshot AI (Kimi) · kimi-k3.amparo tui and amparo code run full-screen on Windows — Windows Terminal and the legacy console host — over the native console API: raw keystrokes as UTF-16 records, VT processing on stdout, quick-edit mode cleared for the session and restored on exit.amparo on a terminal opens the one-time setup wizard (first run), then the interactive TUI; piped stdin keeps the usage print and exit 2.amparo run error now points at amparo — the setup wizard is one command away.amparo update check compares the running binary against amparo.ellmstack.dev/version.json; the site now serves a public changelog page (/changelog), a machine-readable version endpoint (/version.json), and an RSS feed (/feed.xml), all generated at deploy time; the TUI boot banner and the web operator surface show a dismissible notice when a newer build is published.edit_file's output key lines_changed is renamed to line_delta — it always carried a delta, never a count, and now says so.parse_build_errors strips ANSI CSI sequences before matching, so a build spawned under CARGO_TERM_COLOR=always (the CI gate's environment) counts and reports errors exactly like a plain one.M13, landed: the coding surface — amparo code opens a directory as an
interactive editor (file tree with git marks, diff-accept edits through
the gate chain, a streaming build/shell pane), and the reveal program's
platform verification and cross-surface handoff land alongside it: CI on
three OSes at the pinned MSRV, the fan-out approval hub, and the Telegram
receiver.
amparo code [DIR] (the M13 coding surface): an alternate-screen editor — a file tree with the workspace's git marks, open-file viewing, diff-accept edits (with a file open, e submits one instruction as a one-shot agent turn behind the same gate chain as amparo run; the proposed diff renders and each hunk is accepted or rejected), and a run pane (b runs the detected build, ! runs one shell command) that streams the build/shell output live, scrollable, with esc-abort. Piped, it degrades to a one-shot report.amparo-tools: run_command_streaming / run_build_streaming stream the child's stdout/stderr through a line callback (the first line is the command echo); both share the confined-spawn helpers with execute(), so the blocklist, env hygiene and timeouts cannot drift between the two paths.--approval-endpoint, the TUI renders the approval card locally *and* publishes it to the hub — the endpoint is now a publication target, not an exclusive web gate. Deny wins and is latched at the hub; a surface timeout is a deny.amparo chat telegram --receiver URL relays the hub's pending approvals to the operator chat with inline approve/deny buttons and posts the press back (AMPARO_APPROVAL_TOKEN is the hub credential). The live drill covered the phone-approve and Telegram-press paths against the deployed hub.[exec] line renders duration_ms, which every tool hardcoded to 0 — multi-second calls (fetch_url, builds) displayed as instant. execute_call (the CLI run / chat loop path) now stamps real elapsed time per attempt, matching what the MCP path's registry already did./tmp and /dev/shm on Unix, the system temp directory on Windows (which has no /dev/shm twin). The default-workspace fallback when HOME is unset is now the system temp dir instead of /tmp.idna_adapter 1.2.2 and icu 2.3.x require 1.86/1.88). The lock now resolves MSRV-compatible versions via resolver.incompatible-rust-versions = "fallback" in .cargo/config.toml, and the CI gates run --locked so dependency drift fails loudly instead of re-resolving.run_command on Windows: the shell tool hardcoded bash -c, which on Windows resolves to the WSL shim and fails without a WSL distro — commands never ran and the working-directory echo was absent. The tool now runs cmd /C on Windows (the cwd echo is cd with no arguments, cmd's equivalent of pwd); the Unix bash path is unchanged.serde_json::to_string twice) — the identity on Unix, the measured body encoding on Windows. The tool itself ran fine (right cwd, exit 0) throughout.FILE_SHARE_DELETE on Windows.run_with timeout now kills the child and carries its partial stdout/stderr in the panic (a stall and a slow runner look identical from outside; the bytes tell them apart), and the TUI ! shell test accepts the platform ExitStatus wording — "exit status" on Unix, "exit code" on Windows.cargo test but does not set CARGO_BIN_EXE_<name> (that arrived in a later cargo), so the real-process e2e suites (cli_e2e, chat_e2e, bin_e2e) now fall back to locating the binaries next to the test executables in the profile directory./tmp are symlinks into /private/…, and a not-yet-existing write target can never canonicalize, so every fresh workspace write was silently denied. The workspace check is also component-wise now: it appended / to the root string, which can never match a Windows \-separated path, so every relative file write on Windows was denied at resolution.read_file not-found message is OS-independent: a missing file now reports File not found: <path> on every platform instead of the raw io error text, which differs per OS ("No such file" vs "The system cannot find the file specified") — the agent reasons over this string, and it must not shift underneath it. Other io errors keep the OS text./etc/passwd, which is drive-relative (not absolute) on Windows and so sailed through validation. The fixture is now platform-aware — a Windows absolute path trips the rule on Windows. The wizard test had the mirror-image shape: its /tmp/… workspace answer is root-relative on Windows and the wizard (correctly) joins the runner's drive prefix on, so the assertion now checks the contract — an absolute path carrying the workspace name — instead of the Unix literal.chat_e2e round trips with 30 s; loaded 2-core CI runners (especially Windows) blew both on scheduler contention alone, and the e2e children were killed mid-round-trip. Budgets are now 30 s and 120 s — a polling ceiling, not a correctness window: a stalled driver still fails, just not prematurely. The ledger tests' fetch targets also moved from refused-connection ports to accept-and-drop loopback listeners, a deterministic offline failure that closes in milliseconds on every OS.now + 2 s, but the schedule call runs only after a spawn_agent turn AND a child sub-task — on loaded Windows runners the turn outran the instant, the schedule tool honestly refused the past instant (fail-closed), and the store was empty at the assert. The instant is now ten minutes out; the synthetic scan drives the fire without waiting.M12, landed: the ecosystem terminal — Amparo writes directly into both siblings. The TUI stores memories in an Engram vault and manages the Guardrail Console's org policy rules, with credential setup delegated to the sibling CLIs.
/api/orgs/{id}/policies (list, create, toggle, remove) plus a key-scoped GET /api/orgs/current. The console proxy rewrites /check verdicts for rules matching the check's tool_name — the engine's verdict is preserved in engine_verdict, the deny is attributed (org_rule_id, reason "org policy: …"), and the rewrite applies in audit and enforce modes alike.OrgPolicyClient (amparo-tools/src/org_policy.rs): a thin reqwest client for the console's org-rules API, configured from AMPARO_POLICY_KEY + AMPARO_CONSOLE_POLICY_URL (default https://guardrail.elai-intelligence.com). Every failure degrades to a [policy] not connected line with a hint — never a panic, nothing written locally.amparo tui): /memory add stores into the resolved memory backend (Engram when wired — verbatim, skips surfaced honestly) and /memory search retrieves; `/policy list|deny|toggle|enforce|audit` drives the console's org rules and surfaces the console's error text verbatim (including the Pro-plan tier gate). A new ! <command> escape runs a shell command from the prompt — the delegation path for ! guardrail link.amparo wizard): step 3 gains the console URL (AMPARO_CONSOLE_POLICY_URL), and steps 3/4 print delegation guidance — the sibling CLI found on PATH ("guardrail link pairs this machine") or its install one-liner.verdict: deny with the engine's verdict preserved → enforce flip reflected in /api/orgs/current → rule removal restores the engine verdict.M11, landed: adoption — Engram and Guardrail go native, and the web
surface goes live (see the README "Native integrations" section and
docs/trial-bundle.md).
amparo-tools/src/engram_store.rs): EngramStore implements the Memory trait over engramd's REST surface (POST /memories/search → MemoryEntry, POST /memories, GET /health probe). Env-gated behind AMPARO_MEMORY_BACKEND=engram with AMPARO_ENGRAM_URL (default http://127.0.0.1:8787) and an optional AMPARO_ENGRAM_KEY; wired at all four registry sites (CLI run, MCP serve, chat driver + dispatch). Engram stays behind the trait — recommended, never a dependency. A daemon that is down at startup or mid-run degrades to the built-in store with one [memory] warn; never fatal.amparo doctor probes both companions: the Engram check (--engram-url, AMPARO_ENGRAM_URL, or the default when the engram backend is configured) does one real GET /health; an unreachable daemon is a problem that names the degradation. The Guardrail check's --probe does one real /check and reports audit mode ("policy: … in audit mode — verdicts are advisory") when the engine answers enforced: false — a visible mode, never a finding.amparo.ellmstack.dev — the deployment promised in docs/web-surface.md: the thin MCP bridge on the site box against the v0.9.0 approval seam (--approval-endpoint, 60 s fail-closed), sandboxed systemd unit, Caddy vhost, grey-cloud DNS, Let's Encrypt. The app holds no policy keys — the spawned amparo mcp-serve process holds them. Deployment-only: no crate surface changed beyond the probes above.M10, landed: coordination & surfaces — the blackboard, notifications,
rollback hints, the web-approval seam, and the CLI scheduler (see
docs/m10-coordination-surfaces.md).
amparo-tools/src/blackboard.rs): blackboard_read (Observational) / blackboard_write (LocalMutating) over <workspace>/.amparo/blackboard/board.jsonl — append-only rows, last write per key wins on read, every row kept. Shared across the delegation chain via the registry clone; rows carry no writer identity (arguments are model-chosen) — the trusted writer (the loop's task id) rides in the [bus] event.send_notification (amparo-tools/src/notification.rs): an ExternalEffector tool behind a NotificationTransport seam — StderrTransport (default: `[notification] to <destination>: <message>) and WebhookTransport (POSTs {"destination", "message"} JSON). amparo run --webhook-url URL` wires the webhook; the chat hosts wire platform transports; the approval copy names the destination; a transport failure is a failed tool result, never a task crash.ToolExecutor::rollback(&call) returns a display-only RollbackSpec { undo, markers } — computed pre-execution, rendered as a [rollback] line and in the destructive-call approval copy, never executed. Destructive file calls back up the previous contents to <path>.amparo-bak before writing (fail-closed: no backup, no write).--approval-endpoint (W4, amparo-agent/src/web_approval.rs): the web-approval seam per docs/web-surface.md (contract v3) — POST the full ApprovalRequest (arguments, reasons, blast_radius, session_label, rollback hint) and poll {endpoint}/{call_id} for the decision; 60 s timeout, 1 s poll interval, every failure mode fails closed. Flag on amparo run and amparo mcp-serve (http(s) validated; mutually exclusive with --auto-approve); the MCP server's gate_and_dispatch gained preflight classification + session label so web payloads over MCP carry them. registers spawn_agent (opt-in; absent or 0` = no spawn tool) under one shared budget across the spawn chain; amparo run always registers schedule (top-level tasks only), and due_scan fires due cli promises at run start — concurrently with the main task, through the same gate chain, sharing provider/policy/approval/flags; --resume is a run start too. Missed past the grace window = fail-closed. A fire is a fresh task with a reduced tool set (no spawn_agent, no schedule).spawn_agent is no longer absent from every non-chat surface: the MCP server exposes it as an explicit opt-in (--max-sub-agents N, shared budget, still gated, still denied without an approver). It remains absent from amparo chat dispatch (no session, no delegation chain, no audit).schedule is no longer chat-only: amparo run registers it with the process-scoped scheduler — best-effort by design (fires at run start only; the chat host keeps the 30 s ticker).M9, landed: verification & QA — the QC council beside policy, the
operator's sweep, and the audit-mode notice (see
docs/m9-verification-qa.md).
amparo-agent/src/qc.rs): deterministic rule auditors that run beside policy — after the candidate final answer, before the verification prompt. Four rules over the run's own records: unexecuted_tool_claim (the answer cites a registry tool that never executed), evidence (executed calls exceed tool results still in context), cost_honesty (a $ inference figure diverges from the chars/4 accounting estimate; leaf runs only), and pii_shape (residual PII shapes as category counts — values never enter a finding, I6). Verdicts are advisory (Approved / WithFindings); findings append to the verification prompt as issues to check ("ignore any that are wrong"), and verification stays the model's call. AgentEvent::QcAudit (render + sink arms), a [qc] audit #N … tracing line, and in-memory stats counters (logged, never persisted).amparo doctor (amparo-cli/src/doctor.rs): one deterministic, read-only sweep — workspace existence + writability, ledger readability (unparseable lines flagged), session checkpoints (Running older than 7 days = stale), notebook/skills/schedule JSONL parseability, policy-engine reachability (TCP dial; --probe sends one real /check and consumes one engine check), and the --chat-config TOML. Exit 0 healthy / 1 problems / 2 usage; check lines on stdout, problems on stderr; cron-able. The sweep reports and never repairs.amparo-policy): AuditNoticeEngine<E> prints the exact promised line — `policy engine is in audit mode; verdicts are advisory` (docs/trial-bundle.md §"graceful degradation") — once per process, on the first verdict carrying wire::AUDIT_ONLY_MARKER. Display-only (I1): verdicts pass through untouched. Wired at all three surfaces (run, mcp-serve, chat — the driver shares one flag across its per-task engines).WirePolicyEngine::with_session_id): every /check request carries the session id (omitted when absent — never sent as null). amparo run --session-id ID (default: the task id; on --resume, the checkpoint's original id) and amparo mcp-serve --session-id ID (optional); the chat driver tags platform:user_id.MockPolicy wire responder, default-session-id e2e, and wire-level session-id serialization tests — 669 tests across the workspace at the W5 gate.M8, landed: sub-agents and scheduling behind the gate chain — no member
exits the gate chain, and a model is never the approver (see
docs/m8-swarms.md).
amparo-agent): estimate_tokens (chars/4, the standard approximation — deterministic and uniform across providers; an estimate, not provider billing), accumulated per turn into AgentReport.tokens_estimated and tool_calls (counted at dispatch). `AgentConfig. cost_per_million_tokens: Option<f64> (default Some(3.0)`, a stated mid-range model assumption; None omits the cost line, counts still shown). Rendered as `~$0.04 in inference (estimate, chars/4, $3/1M tokens)` — the method states its assumptions.Agent::with_task_id); child ids chain ({parent}.{n}: sess-123.1, sess-123.1.1). `Checkpoint.parent_task_id: Option<String> (additive, serde default); LedgerRow.task_id` / parent_task_id (additive — pre-M8 rows parse with None); LedgerSink::new gains (task_id, parent_task_id). ApprovalRequest.session_label: Option<String> (None at non-agent sites) renders `[session] sub-agent sess-123.1 of task sess-123 wants to run:` in CLI approvals and the same label in chat approval messages — display-only (I1), the gate is unchanged. TaskStarted/TaskComplete gain an optional task_id in their render lines.spawn_agent (amparo-agent/src/spawn.rs, registered by the hosts): param task (required), trust tier ExternalEffector — creating an acting entity always asks a human. The child is a fresh Agent sharing the parent's provider, policy engine, approval gate, event sink, registry, PathPolicy, checkpoint store, tenant, trust ceiling, and config (the rate knob inherits); child.run() executes inside the tool executor and the child's report returns as the tool result. The budget is shared across generations (Arc<Mutex<usize>>) and fails closed: past the limit the result is swarm budget exhausted: N sub-agents max and no agent exists. SubAgentSpawned event renders [spawn] {child} under {parent}: … (render + NotebookSink arms).sub_agent: BlastRadius::SubAgent (severity 2; the classes renumber 0–5) with the note `spawns a sub-agent that acts under the same gate chain; classify maps spawn_agent` to it (static override). Display-only.--max-sub-agents N on amparo run — default 4; 0 means the tool is not registered (swarms off); garbage or negative → usage error, exit 2. Chat: UserProfile.swarm: Option<SwarmProfile> ([users."…".swarm] with max_sub_agents, default 4, and schedule, default false) — per-profile → per-tenant (I2), unknown keys rejected. spawn_agent is deliberately absent from the MCP and amparo chat dispatch surfaces.schedule (amparo-chat): a persisted promise, not an execution. `ScheduledTask { id, tenant, requester, task (PII-stripped at write — I6), at: RFC 3339, status: pending | fired | missed, result } at <workspace>/.amparo/schedule/<id>.json` (atomic tmp + rename); ScheduleTool params at + task, tier ExternalEffector, preflight system_wide; registered only in the chat driver, per-profile schedule: true, after with_spawn_agent (children never inherit it). The driver's 30 s ticker (Amparo's first background loop; MissedTickBehavior::Delay, startup scan) runs the pure due-scan (due_scan(tasks, now, grace), 60 s grace) and fires each due promise back through the full gate chain as its original requester — fresh Agent, continuity off, ledger + checkpoints attached, wrapped in TimeoutApprovalGate (90 s) over the chat gate (60 s): firing while nobody is present auto-denies, never silently ahead of the gate. Past the grace window a promise is marked missed — fail-closed, never fired late — and the requester is notified; a fire notifies `[schedule] {id} fired — {answer}`. The fire path carries no skills/case-library/spawn/ schedule — a promise fires one task.amparo schedule list|cancel — the operator's window on the queue: list prints every promise; cancel moves a pending promise to cancelled (cancelled by the operator) — a status change, never a deletion. Exit codes follow the amparo run contract (2 usage / 1 runtime / 0 ok).[swarm] in the CLI, appended to the final answer in chat.M7's two deliberate exclusions, landed: the WASM eval sandbox and the
ledger quota lever (see docs/m7b-sandbox-quota.md).
amparo-sandbox (new crate): SandboxRuntime — fuel-metered, deterministic WASM execution (10M fuel, 4 MB module, 4 MB memory, 30 s wall clock, 4 concurrent; all with_* builders), with SandboxError (invalid wasm, base64, execution, fuel, memory, module-too-large, compile, timeout, import-rejection) and SandboxResult (output, elapsed, fuel consumed, memory used). No imports and no WASI: a module with any import is rejected at compile time, so there is no host surface to escape through. The ABI: the module exports memory and `axiom_eval(i32 input_ptr, i32 input_len, i32 output_ptr, i32 output_cap) -> i32`; the input JSON is written at offset 0 (input region 0 .. OUTPUT_BASE, 256 KB) and the module writes its JSON output at output_ptr (output region OUTPUT_BASE .. OUTPUT_BASE + OUTPUT_CAP, 256 KB — the two regions never overlap), returning bytes written (≥ 0) or −1. New deps: wasmtime 16 (cranelift only, scoped to this crate), base64; wat dev-only.eval_wasm — the tool (EvalWasmTool): wasm_base64 (required; modules arrive base64-encoded — LLMs cannot emit raw bytes) and input (optional, default {}). Trust tier ExternalEffector, so executing untrusted code always asks a human even though the sandbox keeps the reach small. Every failure path returns a failed ToolResult with an explanatory message, never a panic. Hosts register the tool themselves (the use_skill precedent): `amparo run, the chat driver's per-task registry, amparo chat dispatch`, and MCP serve (auto-deny there until an operator allows). The default registry stays wasmtime-free — pinned at its 17 tools by a test.classify labels eval_wasm read_only despite its ExternalEffector tier — a pure, bounded computation that observes and modifies nothing outside its own sandbox. Display-only (I1): the tier, the gate and the approval all stay exactly as they were.eval_wasm writes no ledger row — it never leaves the machine; the LedgerSink's "what left the machine" contract is pinned by a test.amparo-privacy: LedgerQuota and LedgerStore::open_with_quota (the existing open = unbounded, unchanged). After write + flush, an over-quota file rotates: the oldest rows drop until the file is ~max_bytes / 2 (one rotation per burst, not per append), the rewrite is atomic (tmp + rename), and a rotation marker row — LedgerKind::Rotated with dropped_rows — is appended as the newest row, so it survives the rotation it describes: the reviewer sees exactly what was lost and why (I3). The newest data row is always kept. LedgerSummary gains rotations and rows_dropped.<privacy>/quota beside the ledger — a bounded open writes it, an unbounded open removes it, so the reviewer surface never reports a bound that is not currently enforced. New free functions recorded_quota and read_ledger: amparo privacy reads through them and never opens a store, so reading never creates, rewrites or rotates the ledger.--ledger-max-bytes N on amparo run — plain bytes or K/M/G suffixes (1024-based); zero, garbage or overflow → usage error, exit 2. amparo privacy reports the bound: ledger: <bytes> bytes (quota <N> | unbounded) and, when rotations exist, rotations: <n> (rows dropped <m>); the tail renders marker rows as rotated dropped <m> rows. in the TOML chat config (zero rejected at load — None` is how a profile says "unbounded"). The driver opens each tenant's ledger with the quota; tenants rotate independently, and allowlist mode stays unbounded.Instrumentation & hardening (M7): the always-on privacy ledger, session
persistence, and preflight blast-radius classification — three
instruments that answer, for every executed action, *who allowed it and
under what policy* (see docs/m7-instrumentation.md).
amparo-privacy: LedgerKind (network_call / pii_strip), LedgerRow, the append-only LedgerStore (one JSON line per row, flushed; read_all skips a crash's partial final line; summary() aggregates), privacy_dir and site_host_only (scheme + host only — path, query and fragment dropped). Rows land in <workspace>/.amparo/privacy/ledger.jsonl. Always-on in both hosts, --growth or not: the LedgerSink in amparo-agent (an EventSink consumer) tracks web_search, fetch_url and run_command from request to execution and appends one row per execution attempt — tool, host at most (never a path, query or command), outcome (ok / error / denied), and the human gate's answer (human_approved / human_denied, absent when no human was asked). A human denial writes its row *immediately* (the execution never happens; the denial is the audit answer), a policy-blocked call writes none, and observational/local tools write none. PII strips are rows too — per-category counts, never values — via the new AgentEvent::PrivacyStripped. Open/write failures warn once per task ([ledger] …) and never fail the task.amparo privacy [--workspace DIR] [--tenant T] [--last N] — the reviewer's front door: summary first (network calls, PII strips, human-approved, human-denied, per-tool and per-category breakdowns), then the newest rows (default 10, tenant cli). Exit codes follow the amparo run contract: 2 usage, 1 runtime, 0 ok.amparo-agent: BlastRadius (read_only < workspace_local < network < system_wide < destructive) and classify — the tool's trust tier seeds the class; argument inspection can only raise it (a run_command matching PathPolicy::check_command_blocked → destructive; network-transfer utilities curl/wget/nc/scp/rsync/ssh as whole shell words → at least network; file tools given an absolute path outside the workspace, /tmp and /dev/shm → at least system_wide; an unknown tool → system_wide). The label rides on ApprovalRequest::blast_radius (Option — non-agent sites pass None) and renders in the approval copy: a `[preflight] blast radius: …` line in CLI approvals, the same line in chat approval messages. Display-only by design (I1): classification runs after the gate has decided and feeds nothing back — a wrong label can only misdescribe the approval text, never allow or block anything.amparo-agent: Checkpoint (version 1; tenant, task_id sess-<nanos>-<pid>, started_at, PII-stripped prompt, status running/complete/failed, the conversation without system-role messages and with tool_calls arguments stripped, LoopState — last_tool_name, same_tool_count, empty_turn_retried, last_good_summary, used_tool_names, steps_used — and the stripped final answer), the CheckpointStore trait and JsonCheckpointStore at <workspace>/.amparo/sessions/<tenant ':' → '-'>/<task_id>.json — one file per task, atomic tmp + rename, no index ("latest" = scan by started_at), corrupt files skipped with a warn. A snapshot saves once per loop iteration and at every terminal return (a crash loses at most one turn); failures warn, never fail the task. Agent::resume re-prepends the *current* system prompt (I5 — checkpoints store none), restores the loop state, emits TaskResumed (never TaskStarted, and a resume opens no new notebook record — the checkpoint is the session trail), and the resumed loop re-judges every call through the gate chain. Every fresh CLI run checkpoints too — a crash mid-task is resumable.amparo run --resume — resumes the newest incomplete checkpoint for tenant cli; no task argument (with one → usage error, exit 2), none → exit 1, a stale Running (> 7 days) is skipped with a warn, never resumed. Configuration comes from the current flags.continuity_context — the last 6 user/assistant messages (tool messages skipped), truncated, plus the task's last good tool summary, as one string — injected via Agent::with_continuity as one user-role message between the system prompt and the task prompt. Per-tenant only; chat never reads Running checkpoints. Chat tasks checkpoint through the same store, one format for both hosts.amparo-cli (the tracing 0.1 crate, already a workspace dependency): the binary installs a ~50-line Subscriber that prints agent warn/error events to stderr as-is (each already carries its [tag]), so a checkpoint save failure or a corrupt session file is visible to the operator; info/debug stay silent.Controlled growth (M6): the lab notebook, the verification case library,
gated skills with metrics and retirement, and rollup + archival over the
cold archive — one --growth opt-in, PII-stripped records, the cold
archive never modified.
amparo-notebook crate — the lab notebook (M6a): every completed or failed task is recorded as a PII-stripped, tenant-tagged JSON run record (task text, tool-sequence hash, per-call gate log, verification, truncated answer, duration, token-cost estimate), written through the EventSink seam into the Memory trait. Includes NotebookSink (the event consumer) and JsonlStore (append-only local store).--growth / --no-growth on amparo run and amparo chat — records land in <workspace>/.amparo/notebook/records.jsonl, tagged cli or platform:user_id. Off by default: without --growth, no record file is ever created.FanoutSink in amparo-agent — fans every event out to several sinks (plus truncate/TRUNCATE re-exports).CaseLibrary / EvidenceCase / evidence_section in amparo-agent (the Agent::with_case_library seam) and CaseRetriever in amparo-notebook — prior same-tenant records are retrieved into the self-verification prompt as read-only observations, never instructions, never in the action loop. --growth now also reads: on both amparo run and amparo chat, prior cli / platform:user_id records feed the verification prompt (still off by default).SkillSpec / SkillStep / UseSkillTool in amparo-tools; loop-side expansion in amparo-agent (the Agent::with_skills seam) — use_skill expands into its ordered steps, each gated, executed and recorded individually, so a skill can never grant its steps an exemption. SkillSet + Proposer in amparo-notebook (per-tenant adoptions under <workspace>/.amparo/skills/; recurring VERIFIED sequences distill into inert proposals). New amparo skill add|propose|list|show|adopt CLI: candidates are operator-authored TOML, adoption runs the same policy check as a tool call plus human approval rendering the full step plan, and nothing is adopted automatically. Skill execution requires --growth on amparo run / amparo chat (write + read + act — still off by default); without it, use_skill is never registered.dry_run_gate in amparo-agent dry-runs a call through the gate chain without executing or asking approval (Escalate is not drift); the adoption log is now SkillLogEvent (adopt/retire events, adopt rows byte-identical to M6c, last event per skill wins the fold). Startup drift re-check at every --growth task start (run + chat, the task's own policy engine, trust ceiling and registry) — a skill whose step plan would now be denied retires before registration, with a retire event and a [growth] notice (a write failure warns, never fails the task). New amparo skill check|retire: check re-runs drift then performance (VERIFIED rate below --min-verified-rate 0.5 over the last --window 20 uses, 3-use floor) against every adopted skill, retires by default (--dry-run reports only), writes rechecks.jsonl, and exits 0 even when retirements fire (cron-able); retire <name> [--reason ...] is the operator lever and refuses unknown names. amparo skill list shows a compact metrics tail; show keeps retired skills inspectable with status, metrics, the last policy re-check and the full retirement history. Retirement is always disable + notify, never deletion.<workspace>/.amparo/notebook/, beside the untouched records.jsonl: hot.jsonl (same MemoryEntry shape, **same id and created_at as the cold entry**, content = the record capped by the ladder — final answer → 300 chars, per-step reasons/summaries → 120, first 12 tool calls, task text → 120, then skeleton rungs — so --max-bytes (default 4096, floor 1024) is a guarantee), hot-hashes.jsonl (the (tenant_id, tool_sequence_hash) dedupe index), rollup.json (RollupState — promotion offset and last-fold stamp, atomic saves), promoted.jsonl (operator promotions) and rollup.lock (cross-process; task-start rollups skip silently when held, operator commands report busy, stale >10 min reclaimed). Every --growth task start (amparo run and amparo chat) promotes the cold tail into the hot layer — byte-offset tracked, each record scanned once; the predicate: a gate event of interest (approval, denial, escalation) or a tool sequence not yet represented — and folds daily (24 h since the last fold; rows older than --days, default 90, fold into the cold archive; operator-promoted rows are exempt). CaseRetriever now reads the hot layer under --growth; the cold archive is never modified. New `amparo notebook list|promote|rollup CLI: list` shows the cold archive newest-first with promotion state, promote <id> pins one record (idempotent, exit 0; unknown ids exit 1), rollup forces promote + fold (cron-able, exit 0; --dry-run reports the same numbers and writes nothing). Sync-relay guidance documented in docs/m6-controlled-growth.md §4.1: the relay moves the hot layer and .amparo/skills/*; records.jsonl stays local and authoritative.Multi-tenant identity: the chat face moves from a single-operator allowlist to a TOML tenant directory, with per-user policy, trust ceilings, workspaces and attributed approvals.
--chat-config <path> / AMPARO_CHAT_CONFIG, the flag wins): [users."platform:user_id"] sections are the tenant directory — a user without an entry is refused exactly as an unlisted user is today. Per-user trust_ceiling (falling back to the --trust-ceiling flag) and per-user workspace (a relative subpath under the workspace root, defaulting to users/<platform>-<user_id>/; absolute and .. paths are rejected at load). The file is read once at startup; an empty directory and the legacy AMPARO_CHAT_ALLOWLIST interplay are both warned about at startup.--policy-url, every task gets a fresh engine session-tagged platform:user_id, so engine-side audit rows carry the chat user (legacy allowlist mode included).PathPolicy injection (PathPolicy::from_root, with_policy on the 14 workspace-bound tools, default_registry_with_policy) — never process-global environment mutation. The default users/ path is re-checked per task so a hostile user id cannot escape the root.ApprovalButtonPress carries the pressing user's id; presses from anyone but the requester are rejected with a polite toast and never consume the pending approval.amparo chat telegram process against mock Telegram and LLM servers: config flag/env precedence and exit codes, the per-user workspace proven with pwd, the wrong-user toast followed by the requester's own deciding press, refusal of unknown users, and a per-user ceiling blocking the tool without ever asking.ChatDriver::new takes Tenants (directory or legacy allowlist) and PolicySource (shared engine or per-task wire engine); allowlisted() becomes allows(), and per-task parts are resolved before the busy claim so a refused user never holds a chat busy.Chat adapters: the agent now runs from Telegram, Discord, and Slack — the same loop, gate chain and approval seam behind one transport trait, with inline-button approval.
amparo-chat): one ChatTransport seam (text out, approval messages with inline buttons, outcome edits, a receive loop), a ChatDriver that turns a normalized message into a per-task agent run (allowlist, one task per chat, panic-proof task boundary), an ApprovalRouter for routing button presses back to the waiting gate (an atomic take — a second press is already decided), a ChatApprovalGate with inline Approve/Deny buttons and a 60 s auto-deny timeout, and a ChatEventSink forwarding the agent's [tag] progress lines into the chat (the final answer bypasses the sink, so it can never be lost).answerCallbackQuery acks, 409/401 → exit), Discord via the gateway websocket (message intents, heartbeat discipline, resume-first reconnects, message components, interaction callbacks), and Slack via Socket Mode (envelope acks before processing, block-kit buttons, bot-echo suppression). Every approval flows through the same inline Approve/Deny buttons.amparo chat telegram|discord|slack — the amparo CLI's fourth subcommand. Tokens come from AMPARO_CHAT_* environment variables (never argv); AMPARO_CHAT_ALLOWLIST is a fail-closed single-operator allowlist (empty = every message refused); AMPARO_CHAT_TELEGRAM_BASE points the Telegram adapter at a self-hosted Bot API server.format_event moved into amparo-agent, so the CLI and every chat platform render identical [tag] lines.amparo chat telegram process against in-test mock LLM and Telegram servers: message → approval keyboard → button press → outcome edit → final answer, with the tool executed against a real workspace.First release: the amparo CLI, the agent loop, and the MCP surface — an open
agent that acts under policy, bring your own LLM.
amparo-inference): one InferenceProvider trait; OpenAIProvider for any OpenAI-compatible endpoint (Ollama, vLLM, OpenRouter, Together, Groq — including an Ollama-native /api/chat branch); AnthropicProvider for the native Anthropic Messages API, translated to the same contract (incl. tool_use/tool_result and SSE streaming). Fail-closed configuration, per-request and stream-idle timeouts, a max_tokens clamp, and an optional model allowlist.amparo-agent): native tool_calls (not text-parsed ReAct) behind the deny-by-default gate chain — trust ceiling → policy gate → human approval — with parallel tool batches, retry ×2, max steps plus conversation trimming, and VERIFIED/INCOMPLETE self-verification. Events flow through an EventSink seam; privacy runs per turn (PII strip/restore).amparo-policy): the PolicyEngine seam with a deny-all default, WirePolicyEngine for the open policy-check wire protocol (POST /check {tool_name, target} → {verdict, reason, enforced}), and AllowAllPolicyEngine as the explicit named opt-in.amparo-tools): the registry plus the portable tool set (web, filesystem, shell, git, tests, build, memory), each with a trust tier that drives the approval gate.amparo-mcp): McpServer exposes an Amparo registry to external clients over stdio JSON-RPC 2.0 — every tools/call runs the same gate chain; McpClient spawns an external server and mounts its tools at ExternalEffector, so remote tools cannot skip approval. Ships the amparo-mcp-serve binary.amparo-memory, amparo-privacy): a memory interface with a built-in store (Engram is the recommended backend, never a dependency); privacy policy evaluation with blocked/allowed domain routing and the Secure Minions PII strip/restore primitives.amparo-cli, the amparo binary): amparo run "task" drives the loop end-to-end with fail-closed BYO-LLM environment wiring, interactive terminal approval (y/N, 60 s timeout, EOF/non-terminal stdin auto-denies) with --auto-approve/--auto-deny overrides; amparo mcp-serve is the same implementation as the standalone amparo-mcp-serve binary; amparo version prints the version. stdout carries the final answer only — progress, gate decisions and the report go to stderr.#![warn(missing_docs)] on every crate, so an undocumented public item cannot ship; the two-gate policy (cargo test --workspace and cargo doc --workspace --no-deps, both with zero warnings) is documented in VERSIONING.md; MSRV declared at Rust 1.85.openssl-sys is out of the dependency graph, so cargo install needs no OpenSSL headers.The repository is private; there is no crates.io publication yet. Install from source:
```sh cargo install --path crates/amparo-cli ```