finish the plan: the remaining phase 1, 2, 5 and 6 items, the SDK world, and the deferred tests - #96
Merged
Merged
Conversation
The crate carried both a 281-line `lib.rs` with every pyclass inline and five module files defining the same types. Because `lib.rs` never declared the modules, those five files compiled into nothing: dead duplicates that would drift from the definitions actually in use. `lib.rs` becomes the table of contents the crate-structure rule requires, the module files are what compiles, and the Python module entry point moves into its own file beside them. No behaviour change: the same four classes and the same version string are registered. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018M415coMKAv5V2xJQsaSf8
Plan 1.9 says the agent loop runs sense, inbox, react, scan, respond_cfp, sacrifice without early return between the steps. `decide` still had sense and inbox as `else if`, so a primed surface starved the inbox; the scan hit overwrote `tick_action` with its own descriptor; and sacrifice only ran when the scan had run, so an inbox hit skipped the B9 final consideration. `Decision` now carries `surface` next to `tick_action`, sense fills only that, inbox always runs, the scan runs when no task was claimed, and sacrifice always runs. `TickReport` gains `surface_reaction`, attached by the gather phase, so one report carries both descriptors instead of one erasing the other. Test: a primed surface and an open task in the same beat produce one report with the surface reaction and the claimed task's action. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018M415coMKAv5V2xJQsaSf8
Plan 1.3 says `FormationConstraints` carries `MomentumConfig` "so it is per formation like every other constraint". It stalled because the constraint struct is `Serialize + Type` while the config derived only `Deserialize`. `MomentumConfig` and `TierThreshold` now derive `Serialize`, `Deserialize`, `Type` and `PartialEq`, which is what the constraint struct needs, and `FormationConstraints` carries the table. `Formation::new` builds its `MomentumState` with `with_config` from its own constraints, so two formations deployed with different thresholds each promote on their own numbers. No generated TypeScript changed: `FormationConstraints` is not part of the exported command or event surface (tauri-specta `.export()` is disabled in `tauri/apps/desktop/src-tauri/src/lib.rs` pending specta-rs/specta#455), and nothing under `tauri/packages/types/src/generated` references it. Test: two formations, same two clean actions, different Cold rows — the eager one reaches Warming, the patient one stays Cold. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018M415coMKAv5V2xJQsaSf8
Plan 1.5 specifies `[cooperation.pacing]` with the peak threshold, sustain and relax seconds, decay rate and the four stress weights, for the reason Left 4 Dead ships every Director number as a cvar or `DirectorOptions` field: tuning happens after play, not before. They were `const`s. New `pacing/config.rs` holds `PacingConfig` with those eight fields, the plan's defaults kept as named consts and documented against Booth's deck. `PacingManager` carries one and reads every number from it; `FormationConstraints` carries one too, so it reaches the manager the same way the momentum thresholds do. Test: a formation tuned to peak at 0.1 with a 60-second sustain peaks on one failure and does not fade after the default four seconds, while a default manager given the same sample stays in BuildUp. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018M415coMKAv5V2xJQsaSf8
Plan 1.3 gives the momentum window `handoffs` / `handoffs_ok` and a `handoff_rate`, and the 1.15 work noted the gap: "handoff has no completion event to hook", so the rate was permanently zero and promotion could not see the point COOPERATION.pdf §20 calls the place most cooperative failures occur. New `handoff/completion.rs`: `HandoffCompletion` (pattern, from, to, success), `HandoffType::pattern/from/to`, `HandoffResult::succeeded` (false only for `Failed`), and a `HandoffLog` the formation shares behind an `Arc` because `dispatch_handoff` takes `&self`. Every dispatch records one completion. The momentum step drains the log each tick, emits a `CooperationEvent::HandoffCompleted` per record, and counts them into the tick's `TickCounts`, so `RunWindow::handoff_rate()` now reflects real handoffs. A poisoned lock drops the statistic, never the work. Also carries `cargo fmt` over the two preceding commits. Tests: record/drain round trip and outcome mapping; a Direct dispatch leaves exactly one successful completion on the formation's log; three completions, two ok, put a 2/3 handoff rate in the window. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018M415coMKAv5V2xJQsaSf8
`collect_tools` enumerated connector actions only, so a model could send a Telegram message but could not see, let alone steer, a formation. Every verb in the runtime registry is now a tool under a `platform` pseudo-connector: - `platform__formation_pause` etc., published from `platform_verbs()` with the registry's own description and `input_schema`. `.` is not legal in a tool name, so the dotted verb name maps through `PlatformVerb::tool_segment` and back through `find_verb_by_tool_segment`. - Name resolution in `execute_tool_call` routes the `platform` namespace to `operations::platform::run_platform_verb` — the runtime, not the connector registry. That function is new and delegates every branch to the runtime operation the chat command already calls. - Read-only verbs (list, get, status) run straight through; every other verb blocks on the same `ApprovalGate` a connector write blocks on, and is refused outright when no gate is wired. - Platform tools are emitted before connector tools so they survive `MAX_TOOLS_HARD_CAP` truncation, and only when the bot has a `RuntimeState` to run them against. Tests assert `platform__formation_pause` is published, that it needs `writes_with_approval` exactly as a connector write does, and that no published tool name contains an assign — the drum rule, enforced where a model can reach. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018M415coMKAv5V2xJQsaSf8
…anguage (plan 5.4)
The catalogue was pinned to `const CHAT_LOCALE = "en"` and seven of the
eight locale files were empty stubs, so chat understood one language
while the interface spoke eight.
- `es`, `fr`, `pt`, `tl` and `ar` now carry real sentence templates for
all twenty platform verbs, with the same slots the English file uses
(`{formation}`, `{connector}`, `{intent}`, `{id}`, `{key}`,
`{value}`, `{adapter}`) — filled from live state at match time, never
hard-coded. Tagalog is written in the Taglish register technical
commands are actually typed in.
- `ja` and `th` stay stubs, and their files now say why: the NLU
tokenizer splits on non-alphanumeric characters, and neither language
puts spaces between words, so a template would collapse to one token
and could never match. Populating them is blocked on word
segmentation (lindera / ICU), not on translation. Both fall back to
English, which at least matches loan words and the formation name.
- `build_catalog` takes the speaker: their `language` preference picks
the sentence file, region tags narrow to the base language, and
anything uncovered falls back to English.
Tests: every translated locale covers every verb (no half-translated
file), and no translated phrase invents or drops a slot.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018M415coMKAv5V2xJQsaSf8
Changing the model swapped `RuntimeState::ai_adapter` (an `ArcSwap`), which rule dispatch reads through the capability bridge — but the chat fallback held `Bot::ai_adapter`, the snapshot taken when the bot was built. So a model change landed on rules immediately and on chat only after a lock and unlock rebuilt the bot. `ai_fallback` now loads the same swappable handle when a runtime is wired, falling back to the built-with adapter for headless and test bots that have none. The availability check and the tool runner both see the current adapter. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018M415coMKAv5V2xJQsaSf8
Finding 83 / ALIGNMENT-PLAN 2.7: `crates/springtale-wit` shipped the
cooperation world, but `sdk/connector-sdk` had no `.wit` at all, so the
sandbox work rested on hand-written WebAssembly text and the SDK's
documented ABI was the only statement of the contract.
`sdk/connector-sdk/wit/connector.wit` states it. Two worlds, because
host imports are gated by the manifest and not by the language:
`connector` (exports `guest`, imports nothing) for a connector that
declares no capabilities, and `networked-connector` which adds the
`host` interface. `host` carries exactly one function, mirroring the
one `register_http_request` actually provides, with the -1/-2 return
values named as `access-error` variants. WASI imports are documented
rather than declared: the host links the `wasi:http/proxy` set plus
`wasi:cli/{exit,environment,terminal-*}` and `wasi:filesystem`, all
against a `WasiCtx` that grants nothing, and never `wasi:sockets`.
Declaring that world here would need the `wasi:*` packages vendored and
would imply a grant the host does not make.
`connector-hello-wasm` is now a `wasm32-wasip2` component built against
that world with `wit-bindgen`, so the actions it exports and the
`read_only` bits in its manifest are checked by the type system rather
than by comment. Both example and SDK gain an empty `[workspace]` table
so they resolve their own graph from any checkout path.
The 2.7 gap is closed: the tier cache's positive test no longer proves
linkage against hand-written WAT. It `include_bytes!`s the built
component from `prebuilt/`, links it against the WASI p2 linker,
instantiates it, and calls `execute` — lifting the result into a
host-side mirror of the WIT record, so a renamed field fails the typed
lookup. `greet` returns its greeting and an unknown action comes back
through `action-result` instead of trapping. The artefact is checked in
so the test needs no wasm target, and no Python or JavaScript
toolchain, at test time; `prebuilt/README.md` says how to regenerate it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018M415coMKAv5V2xJQsaSf8
ALIGNMENT-PLAN 5.5, row 2: `autolock.rs`, `commands/safety.rs`, `quick_hide.rs` and `tray.rs` had no tests, because every decision was tangled with a Tauri or OS call. The decisions now live in `src/policy/`, the call sites are thin, and the decisions have tests. - `policy::autolock::AutoLockTimer` is the timer state machine: `Disabled` when the timeout is zero, otherwise `Counting` until idle reaches the threshold and then `Locked`. Activity resets the countdown; a backwards clock reads as zero idle rather than underflowing. - `policy::disguise` picks the window title and the tray disguise from the safety config, with the real icon id and tooltip as named constants. - `policy::shortcut::quick_hide_candidates` builds the accelerator ladder, fallbacks included. 20 unit tests over the three modules. Behaviour is unchanged: the exact-zero-disables and exact-threshold-fires boundaries and the fallback order are preserved. The tap-count threshold named in the plan row is not extracted. It is not Rust — it lives in `tauri/apps/desktop/src/Colony.tsx` (the 1500 ms window and the `panic_tap_count` comparison), and no Rust code receives those clicks, so a Rust implementation would be uncalled code and fake coverage. The daemon-side bound on the configured value stays in `springtale-runtime::operations::safety`. Testing the frontend counter needs a vitest task against `Colony.tsx`. Drive-by: `sidecar.rs` had `login` appended after its test module, which fails `clippy::items_after_test_module`; the test module moved to the end of the file, no logic touched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018M415coMKAv5V2xJQsaSf8
ALIGNMENT-PLAN 5.5, row 5: neither the Python bindings nor the WASM SDK example was built anywhere, so both could rot silently. `wasm-sdk` adds the `wasm32-wasip2` target, builds `sdk/examples/connector-hello-wasm` against the SDK's WIT world, checks that the fresh build and the checked-in `prebuilt/` artefact both carry the component preamble rather than a core-module one, and runs the sandbox tests that load the component. The wasip2 target emits a component directly, so the job needs no `wasm-tools`, no Node and no Python. `python-bindings` builds the maturin wheel and smoke-imports it. The check is not a bare `import springtale`: it asserts `__version__` and every class the `#[pymodule]` declares, so a binding dropped from the module fails the job instead of passing silently. It uses the runner's preinstalled Python in a venv rather than adding `actions/setup-python` — no new third-party action enters the trust set, and every action in both jobs is one this file already pins by digest. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018M415coMKAv5V2xJQsaSf8
`/mcp`, `POST /vault/unlock` and `/openapi.json` were served but carried no annotation, so the document the daemon publishes did not describe every route the daemon answers, and the generated TypeScript could not see them either. `/mcp` is documented for what it is — a Streamable HTTP MCP endpoint carrying JSON-RPC 2.0 — and deliberately not by restating MCP's own schema, which the specification versions, not this daemon. The annotation sits on the router constructor because the endpoint is a nested service rather than a handler. `/vault/unlock` takes a typed body whose passphrase is a `SecretString` described to the wire as a password string. It gains a command-line surface at the same time: `springtale vault unlock`, because a locked daemon serves three routes and nothing else, so the dashboard SPA cannot load to unlock it and a headless instance had no way back after an auto-lock short of a restart. The passphrase is read from the terminal, never from a flag. `/openapi.json` and the browser half of `/mcp` and `/vault/unlock` are recorded in the not-surfaced list with their reasons. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018M415coMKAv5V2xJQsaSf8
Three commands opened the store directly behind a running daemon. `bot pair-init` now calls `POST /bot/pair-init`. The pairing rows live in the daemon's store, so the daemon is the writer and the command line is a client of it, like every other daemon-backed verb (plan 2.2). `bot panic-unpair` stays offline, deliberately. It is reached when the paired device or account is in the wrong hands, from whatever terminal the user has recovered — precisely when springtaled may be dead, wedged, or itself the thing that was taken. It deletes rows a running daemon then stops finding, so there is nothing for it to have been told. Same reasoning as `springtale panic`. `author` stays offline too: `author add --self` registers this instance's connector-signing identity on first run, before `springtale server start` has ever been typed. What was not acceptable was a second implementation of the registry, so it is gone — every read, write and validation now goes through `springtale_runtime::operations::authors`, the same functions and the same `trusted-author:` rows the `/authors` routes use. One registry, one implementation, reached from a socket or from a terminal. Both decisions are written down in the not-surfaced list. The exemption ledger — the open product decision this closes — is now empty. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018M415coMKAv5V2xJQsaSf8
`update_intent`, `propose_intent`, `cast_vote`, `add_member`,
`remove_member` and `run_command` took `Json<serde_json::Value>` and
hand-plucked fields out of it. The bodies are now `IntentBody`,
`CastVoteBody`, `MemberBody` and `RunCommandBody` — required fields with
no silent defaults, `ToSchema` for the contract, and a missing field is
a rejection at deserialization rather than a value the handler invents.
`params` on `run-command` stays optional because a command with no
parameters is a real case; nothing else is.
Regenerating the document off the annotations found a caller that had
never worked: the web provider's `castFormationVote` posted `{ choice }`
to a route that reads `voter` and `approve`, so every browser ballot was
a 400. The `DataProvider` signature now carries the two fields the route
takes, which is the point of generating the contract rather than
describing it twice.
`tauri/packages/types/openapi.json` and `src/api.ts` are regenerated the
way the contract job does, covering this and the two commits before
it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018M415coMKAv5V2xJQsaSf8
`scripts/cli-routes.sh` grepped path string literals out of the command sources, which answered a weaker question than the surface check asks. A path in a comment counted as a verb. A verb that built its path from a constant or a `match` did not count at all. The list was a guess about code rather than a statement by the program. `springtale dump-commands` is that statement: it walks its own clap tree at runtime and prints every verb with the routes it calls. The tree half cannot drift — it is the parser. The route half is declared beside the verb in `surface.rs` and held to the tree by a unit test, so a new subcommand cannot be added without saying what it talks to and a deleted one cannot leave a ghost behind. A verb that declares no routes is saying it runs offline, which is itself checkable. The script reads the dump, asks cargo for the binary rather than trusting whatever is in target/, and still fails loudly on an empty list — and on a verb with no declaration, the new way for the extractor to be broken. The drum rule is untouched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018M415coMKAv5V2xJQsaSf8
ALIGNMENT-PLAN 5.5, row 4: the command line had one test, so any rename, drop, or type change in a `--json` body was a silent breaking change for every script parsing it. `output::render_json` is split out of `print_json` — the exact function `--json` prints through — so tests assert the real path rather than a parallel one. The inline `serde_json::json!` bodies and the inline table closures scattered through the command modules are now named functions, which is what lets a test tie the JSON key contract to the code the CLI actually runs. 114 tests over every subcommand family with a `--json` mode: connector, rule, events, formation, approval, agent, auth, session, memory, execution, recipe, workspace, canvas, onboarding, chat, send, safety, data, doctor, fix, login, bot pairing, panic, travel, vault duress, crypto rotate, healthcheck, server, cooperation, and the redaction in `config ai get`. Each asserts the top-level key set and the type of each field, and per-item keys for list output — a renamed field fails. The pretty-print pass-through family is covered by one test that a daemon document survives `render_json` unchanged. Not covered, with reason: `init` ignores `--json` by design, `mcp serve` is a stdio JSON-RPC bridge with no `--json` path, and `recipe export`/`render` emit TOML. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018M415coMKAv5V2xJQsaSf8
ALIGNMENT-PLAN 5.5, row 3: the rustls mTLS transport — the component that carries inter-node traffic for people whose safety depends on it — had no tests at all. `crates/springtale-transport/tests/http_transport.rs` drives the real `HttpTransport`, not a reimplementation: - two nodes with an in-test CA round-trip a message in both directions, asserting sender node id, message id and payload; - a send to an unknown peer is rejected as `ConnectionFailed` naming the node; - a client that trusts the wrong authority is refused — the real `send()` fails, the server inbox stays empty, and a raw rustls probe against the same live server pins the reason to `InvalidCertificate(UnknownIssuer)` rather than any error at all; - a client presenting a certificate from the wrong authority is likewise refused, pinned to a fatal certificate alert (TLS 1.3 surfaces the server's client-cert rejection after the handshake, so it is asserted on the application-data exchange); - the hybrid post-quantum group is asserted from handshake state: `negotiated_key_exchange_group()` must be `X25519MLKEM768`. Both negative tests were mutation-checked: pointed at a matching CA, they fail, so they are not passing on an incidental error. `rcgen` was already a hardcoded dev-dependency of this crate; it moves to a workspace pin like every other dependency, `default-features = false` with `crypto,pem,ring` so it stays on the workspace's ring backend — no aws-lc-rs, no OpenSSL, and `Cargo.lock` is unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018M415coMKAv5V2xJQsaSf8
…anges (plan 6.5) The server advertised the listChanged capability and never sent the notification, so a client cached a stale tool list until it reconnected. The runtime owns a protocol-free `ToolCatalogNotifier` (a broadcast sender on `RuntimeState`, the shape `canvas_tx` already uses) that every operation mutating the LIVE registry publishes to — setup, install-wasm, reload, enable, disable, remove and remove-cascade — each fired right after the registry changes rather than after fallible follow-up work. `install_connector` deliberately does not: it registers a manifest row and leaves `tools/list` unchanged. `ServerHandler::on_initialized` spawns one forwarder per connected client holding that client's peer and its own subscription, filters by the server's connector scope, and calls `notify_tool_list_changed`. It exits when the transport closes or the runtime drops the channel, so dead peers prune themselves; a failed send on a live session does not. The loop is generic over a small sink trait so it is tested without a transport. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018M415coMKAv5V2xJQsaSf8
… 6.4)
The route hard-coded Telegram: on a `callback_query_received` trigger it
reached into the body and called `answer_callback_query` itself.
`WebhookIngest` gains `acks` — `WebhookAck { action, input }` values a
connector asks the ingress to run back on it. Telegram's
`ingest_update` emits its own callback answer (mirroring what the
polling chat source already did), and the route just loops over the
acks through the same capability-checked `registry.execute`.
Verification, responses, event log and trigger dispatch are unchanged
and in the same order. A test scans the route module's own source for
connector names so the coupling cannot come back.
Two things ride along in this commit because they interleave with the
above inside the same two files and hunks cannot be staged separately
here: the Kick replay store check in the route (see the replay commit),
and a real bug — the old code read the callback id at the body's top
level, which a genuine Telegram Update never has, so webhook-mode
button presses were never acknowledged. The connector now reads
`callback_query.id` and still accepts the old shape.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018M415coMKAv5V2xJQsaSf8
`extract_connector_configs` matched fourteen literal keys, so a connector added later could not be configured from the file at all — the daemon silently ignored its table. It now enumerates the factory registry (`factory::config_keys()` over the `inventory` entries), so whatever is installed is configurable, and accepts `[connectors.<name>]` as well as the historical bare `[<name>]`. No alias map was needed: all fourteen old keys are exact factory config keys, and a test asserts that and says to add an alias if one ever diverges. A second test extracts a key outside the old fourteen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018M415coMKAv5V2xJQsaSf8
The replay cache was a process-memory `Mutex<ReplayCache>`, so a daemon reload or a vault unlock reopened the window an attacker could replay a captured webhook in. It moves to the store, reusing the existing `dedupe_seen` table through `StorageBackend::dedupe_check` (atomic insert-or-ignore with LRU eviction) rather than adding a table. Keys are blake3-hashed, per that table's documented privacy invariant. Connectors declare the key with a new optional `webhook_replay_key(headers)` on the connector and host traits — WASM keeps the `None` default — so the check stays generic; Kick returns `Kick-Event-Message-Id` and keeps only its timestamp check. The route runs the check after signature verification and before the event log, and fails closed on a store error. No connector gained a `springtale-store` dependency: the check lives in `springtale-connector`, which already depends on it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018M415coMKAv5V2xJQsaSf8
The shell kept the sidecar's child handle and nothing watched it. If
springtaled died after signalling ready, the window held a stale port
and token, with no restart, no health path, and no sign anything was
wrong.
`Daemon` now carries the event receiver out of `start`, and
`supervise()` drains it: on `Terminated` or stream close it clears the
stored `DaemonHandle` and emits `DaemonStopped { code }`. A deliberate
stop (lock_vault) finds the slot already taken and stays silent, so
locking does not raise an alarm. The frontend listens through a small
IPC module and renders a notice while a session is on screen; strings
are in all eight locales.
Also kills the child when `login` fails, which previously orphaned a
daemon holding an unlocked vault. No restart supervisor — detection and
an honest signal only.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018M415coMKAv5V2xJQsaSf8
ALIGNMENT-PLAN 5.5, row 1: `operations/travel.rs` and `operations/safety.rs` had zero tests. These are the operations a user runs when crossing a border or when someone is at the door, so an untested regression here is the worst kind this project can ship. `travel.rs` gains five tests: a prepared backup reopens under the vault; the prepared tree leaves no plaintext behind; prepare→restore round-trips vault, database and config; a missing backup returns not-found; and a restore with the wrong passphrase writes nothing rather than half-restoring. `panic_wipe` gets its own integration binary. It resolves paths internally through `springtale_store::paths`, so the only way to test it is to redirect `XDG_DATA_HOME`, and `#![forbid(unsafe_code)]` on the library rules out `unsafe env::set_var` inside a `#[cfg(test)]` module. The test asserts the resolved data directory is inside the temporary root before wiping anything, so it can never reach a real vault, then asserts the vault, database, `-wal`, `-shm` and config are all unreadable afterwards. All three are mutation-checked: disabling the config wipe in `prepare` fails two tests, disabling it in `panic_wipe` fails that one. The round-trip test exposed something worth recording: `travel::prepare` copies only the `.db` file, so anything still in the SQLite write-ahead log at departure is not in the backup. The test closes the writer handle to force a checkpoint before preparing. A `prepare` on a live, uncheckpointed store would back up stale data — a separate fix. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018M415coMKAv5V2xJQsaSf8
`travel prepare` copied the `.db` file and wiped the originals. A SQLite database in write-ahead-log mode keeps recent commits in a sidecar file, so everything written since the last automatic checkpoint was left behind: the traveller carried a backup that looked complete and silently predated their last work, and the wipe then destroyed the only copy. The store gains a checkpoint method, a no-op for backends with no such log and a truncating checkpoint for SQLite, and `prepare` calls it first. The test writes a row through the live handle, backs up, restores, and asserts the row is there; it fails without the checkpoint. Found while writing the tests this plan deferred. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018M415coMKAv5V2xJQsaSf8
| permissions: | ||
| contents: read | ||
| steps: | ||
| - uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2 |
| - uses: dtolnay/rust-toolchain@6bed0761d98439e5a578e2877258200ad565ba87 # stable | ||
| with: | ||
| targets: wasm32-wasip2 | ||
| - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 |
| permissions: | ||
| contents: read | ||
| steps: | ||
| - uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2 |
| with: | ||
| persist-credentials: false | ||
| - uses: dtolnay/rust-toolchain@6bed0761d98439e5a578e2877258200ad565ba87 # stable | ||
| - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 |
…e the document Two things the new CI jobs caught, which is what they are for. The wheel declares a Python source directory that was never created, so `maturin build` failed outright: the crate promised curated type stubs and shipped none. The package now exists, with stubs for the four classes the extension registers and the marker that makes them count. The webhook route's description changed when it stopped knowing connectors by name, and the committed contract still carried the old text. Regenerated from the handlers, along with its TypeScript. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018M415coMKAv5V2xJQsaSf8
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Everything the verification pass left open, finished rather than listed. Twenty-seven commits, grouped below by what they close.
Phase 1, the four pieces its own sections specified and never got. The agent loop can now report a surface reaction and a claimed task from the same beat: sense and inbox are no longer mutually exclusive, a scan no longer overwrites the surface descriptor, and sacrifice always runs. Momentum thresholds became a per-formation constraint, which is what the section asked for; the derive mismatch that stalled it is resolved rather than worked around. Pacing weights and timings became configuration with the same defaults, for the reason the section gives: the game this is modelled on ships every director number as a tunable. Handoff success is measured, so the rate in the momentum window means something instead of always reading zero.
The contract now covers every route. The protocol endpoint, the vault unlock route and the document itself were served but unannotated, so they were missing from the contract and its generated types; all three are described now. The surface check reads a real machine-readable dump of the command tree and the route each verb calls, instead of grepping path literals out of source, and it still fails loudly on an empty list. Six formation handlers took untyped bodies and hand-plucked fields; they take typed requests with no silent defaults. Regenerating the contract exposed a live bug: the browser posted a vote in a shape the route did not read, so every ballot cast from the dashboard was rejected. Fixed on both sides.
No surface writes behind the daemon's back any more. Pairing goes through a route. The two commands that stay offline do so deliberately and are written down with their reasons: an emergency unpair has to work from a recovered terminal when the daemon is the thing that was taken, and registering a signing identity happens before a daemon exists. The author registry no longer keeps a parallel implementation; it goes through the same operation the API uses. The gap ledger is empty.
Chat is a real surface. The tool loop learned the platform verbs, so a model can steer a formation through the same registry the command line and the check script read, with read-only verbs running unattended and everything else crossing the approval gate. Sentence templates exist per locale and are matched in the speaker's language. The chat path reads the live model handle instead of a snapshot taken when the bot was built, so changing the model no longer waits for a lock and unlock.
Connectors are no longer known by name in the daemon. The webhook route dispatches through the trait rather than special-casing one connector's callback answer, and any installed connector can be configured from the file, not the fourteen that were hard-coded. The protocol server tells clients when the tool list changes. Kick's replay window survives a restart instead of reopening. The desktop notices its daemon dying rather than talking to nothing.
The SDK has an interface description. The connector world is written, the example is a real component built against it, and the sandbox test now loads that component and calls it, closing the "proves linkage, not execution" gap recorded earlier.
The deferred tests are written. Travel and panic wipe, the desktop safety logic, the mutual-TLS transport including the post-quantum group, the command line's output shapes, and CI jobs for the Python wheel and the WebAssembly example.
One real defect found while writing them. Travel mode copied the database file without folding in the write-ahead log, so a backup taken before wiping the originals silently predated the traveller's last work. It check points first now. The test writes through the live handle, backs up, restores, and asserts the row survived; it fails without the fix.
Also: the Python bindings crate had five module files that were never declared, duplicating everything inlined in its root, so they compiled into nothing and were free to drift. The modules are now the code.
Verification: workspace build,
cargo clippy --workspace --all-targets -D warningsclean,cargo fmt --checkclean, the full workspace suite green, the surface check passing at 127 routes, and frontend typecheck, lint and build clean.🤖 Generated with Claude Code
https://claude.ai/code/session_018M415coMKAv5V2xJQsaSf8