Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,28 @@

## 1.0.0-beta.13 — 2026-08-07

### Features
- Add `failproofai flush` — deliver what is already spooled, now. The collector is unhurried on purpose (a batch is swept once it is older than two minutes, at most 64 per pass, on a 60-second cadence), which is right for a backlog and exactly wrong for somebody standing at a dashboard waiting to see their own events: from there "not delivered yet" and "not working" look identical. The command asks the daemon for a pass with no minimum age and no cap, and `--wait` waits until the spool drains or its timeout expires, so a script can flush and then assert. It re-sends nothing — for history the collector already read past, that is still `backfill`. (#663)
- Rename the cloud-policy vocabulary: a **generation** is now a **deployment**, and a policy **revision** is a **version**. Both were words the product used nowhere else — a customer reads "deployment 7" and "version 3" without a glossary. The rename goes through the wire format and the on-disk manifest, not just the labels, and lands with the matching AgentEye change: a server and a daemon that disagree on these names means the fleet stops reconciling, silently. (#663)
- Capture sessions from more than one location per agent CLI. Every source watched exactly the place its own installer puts it — `~/.claude/projects`, `~/.hermes/state.db` — which is right for one machine and wrong for every other arrangement: a second profile, a mounted team share, a container's home beside the host's, an agent an operator relocated. Those hold real sessions and nothing collected them. `failproofai harness add-path <harness> [<label>=]<path>` (plus `list` / `remove-path`) writes `[collector.sources.<harness>] extra_paths` in `config.toml`; `FAILPROOFAI_<HARNESS>_EXTRA_PATHS` overrides it for containers. Covers all twelve harnesses — thirteen sources, since `claude` carries its subagent transcripts, which share a root and would otherwise be silently dropped under an added path. Grammar, folder-name label fallback and `<label>-<agentId>` namespacing match AgentEye's collector (`--openclaw-extra-path`, `--hermes-extra-path`) rather than inventing a second answer to what an extra path is. Three things are structural rather than incidental: **each labelled path is its own task**, because `roots` is walked by one task with one `Params` and the label lives there — sharing would capture both under one id; each gets **its own cursor directory**, because the store writes its whole map atomically and two instances sharing one clobber each other's watermark and both re-read from zero after every restart; and each gets **its own health key**, or they overwrite each other's record and `root_present` alternates, destroying the "absent root versus idle source" distinction that file exists to draw — the exact failure Hermes hit with its per-profile databases. The label is applied in `SpoolWriter::push`, the one point every event from every source passes through (where `machine_id` and `user` are already stamped for the same reason), and **not** in either engine: a SQLite format is handed `params.agent_id` only as a fallback and derives the real id from the row (`devin::agent_id` returns `devin-<project>` and never consults the fallback for a session that has a working directory, which is all of them), so prefixing there would namespace only the sessions that failed to derive an id. A path overlapping one already captured is refused at startup rather than collected twice under two ids, and an unrecognised `[collector.sources.*]` table is reported instead of silently capturing nothing. A machine with nothing configured is unchanged, byte for byte, including its `config.toml`. Adding or removing a path needs no restart and no sudo: the daemon's collector manager already re-reads `config.toml` on an interval and cycles the collector whenever the resolved `CollectorConfig` changes, and `sources` rides that by living inside the compared value — verified live (task count 21 → 23 within one interval, and the first transcript under the new path reached the server) and pinned by an e2e test that drives the real binary against a hand-edited config. (#667)

### Fixes
- Make the hook path able to read a `schemaVersion: 1` active manifest it already claimed to accept. The reader listed 1 as supported "for files a pre-rename beta daemon left behind", then read only the post-rename field names — so every genuine v1 file threw `active manifest deployment is invalid` and the acceptance was unreachable. The Rust reader of the same file handles it with `#[serde(alias)]`; this is the TypeScript half of that pair, and it was the half missing. The failure shape is the one this module's own header warns about: the daemon reconciles happily, `active.json` is correct on disk, and the hook path alone refuses — cloud policy stops being enforced while every other signal reports the machine healthy, and recovery needs a successful poll, so an offline machine stays exposed. The fixtures hid it: every `schemaVersion: 1` fixture paired that version with post-rename field names, a shape no writer ever produced. (#PR)
- Rename the daemon's own contract document (`crates/CLOUD_POLICIES.md`) and the live cross-repo pairing harness, both of which the rename missed. The document's desired-state example was a payload the daemon now rejects, and the harness still read `revision` off a publish response, sent `revision` in two deploy bodies, and asserted an ingested event carried `cloud_generation` — so it would have failed against the renamed server while proving nothing about the new contract. (#PR)
- Move the cloud-policy desired state to `schemaVersion: 2`, and accept 1 only from disk. The v1 payload named its fields `generation` and `revision`; after the rename it carried neither, at the same version number — same endpoint, same version, different shape, which is the one thing a schema version exists to prevent. AgentEye now emits 2 (FailproofAI/agenteye#559) and this daemon accepts both: 2 from the server, 1 solely so a `desired-state.json` or `active.json` written by an earlier beta daemon still parses off disk, since both structs carry `deny_unknown_fields` and a refusal there would leave the machine unable to read its own persisted state and quietly not enforcing. For the same reason the field aliases stay on the persisted `ActiveDeployment`/`ActivePolicy` and were REMOVED from the wire `DesiredState`/`DesiredPolicy` — no server can emit the old spelling, so tolerating it there would be dead code, and a silently-accepted stale field is how the two sides drift apart again. The TypeScript hook reader accepts the same pair; it previously accepted only 1, which meant the daemon reconciled happily, wrote a correct `active.json`, and the hook path alone refused it — cloud policy silently unenforced while every other signal read healthy. Verified against the real server end to end: publish, deploy, pull, verify, activate, deny. (#663)
- Stop ← hanging the setup wizard. `BACK` is a symbol the shared prompt handler injects when a prompt opts into back navigation, so it is not a value of any prompt's own result type — and `multiSelect`'s summary calls `values.includes(...)`, which throws `TypeError` on a symbol. It threw *inside* `finish`, before the promise resolved, so pressing ← never settled it and the wizard stopped responding to input entirely; `selectOne` did not throw but rendered the literal text `Symbol(failproofai.back)` as the user's answer. Handled once in `collapse()` rather than in each `summaryFor`, so no prompt has to know about a symbol it never declared. (#663)
- Keep cloud-policy state written before the deployment/version rename readable across an upgrade. `ActiveDeployment` and `ActivePolicy` carry `deny_unknown_fields`, so an upgraded daemon failed to parse its own `active.json` on three counts at once — `generation` unrecognised, `deployment` missing, and the same again for every policy's `revision`. The machine silently lost the deployment it was enforcing until a poll succeeded, which on a fail-closed machine is precisely the gap this subsystem exists to close. Both spellings are now accepted, on the persisted state and on the desired state the server sends. (#663)
- Make `failproofai flush` refuse on a platform with no daemon instead of reporting success. It skipped the daemon check, wrote the request file and exited 0 — but nothing reads it there, so the only symptom was data that never arrived. It now refuses before writing anything, which is what `failproofai config` already does on the same platforms. (#663)
- Stop an upgrade from layout 1 silently emptying the user's policy selection. Layout 1 kept it at `~/.failproofai/policies-config.json` and layout 2 keeps it at `policies/local-policies/policies-config.json` — and BOTH were on the reset list, so moving between them discarded every builtin the user had enabled, every explicit `customPoliciesPaths` entry and every `policyParams` value. The reset runs unattended from `checkLayoutForCli` on the first command after an upgrade, and the machine still read as configured afterwards (`isConfigured()` is a union that sees the agent CLIs' untouched settings files) — so the wizard never re-asked, and hooks kept firing against a policy set that had quietly become the default one. That is the same silent enforcement gap `migrateConventionPolicies()` and `migrateHookActivity()` already exist to close, by a third route. The carry is deliberately narrow, and stays inside the standing wipe-and-re-setup decision: everything derived (cursors, spool, health, audit cache) still goes and is rebuilt, and what moves is only what a person typed and nothing regenerates. It is an ALLOWLIST rather than a copy, because layout 1's file also carried a `collector` block in camelCase and layout 2 moved those settings to `[collector]` in `config.toml` in snake_case — carrying it would put a key into the new file that nothing reads, looking preserved and behaving absent, which is the exact bug `fpai-collect`'s `Settings` documents. It runs in two phases (read before the deletions, write after) because its source AND its destination's parent are both on the reset list; writing first would have the reset delete the carry moments after it happened, which the note on `hookActivityDir()` records happening once already. A reset FROM the current layout no longer clears `local-policies/` at all — that is not a layout migration, and there it would discard a current, valid selection. An existing layout-2 config is never overwritten by the layout-1 file beside it. (#667)
- Make `failproofai config` refuse setup on an unsupported platform (Windows, today) instead of completing it unenforced. The wizard used to skip the daemon requirement and finish anyway, leaving the machine reading as configured while enforcing in-process with no fail-closed guarantee — now it prints why and exits 1 before drawing a single prompt, writing nothing. (#664)
- Cut roughly a second of dead wait out of installing the daemon. `waitForDaemonRunning` slept a flat 750ms and then read the service status once, which was wrong in both directions: the socket is up in ~13ms and answers a real hook in ~125ms, so a healthy machine sat there with the answer already in hand — twice, because the repair path uninstalls and reinstalls — while a daemon that died at 100ms went unnoticed until the sleep was over. The window is now polled: leaving `running` fails immediately, and the wait ends as soon as the daemon accepts a connection, which is stronger evidence it did not die at startup than "still active after an arbitrary sleep". (#663)
- Let the setup wizard go back a step. Cancelling and going back were both `null`, so the only way to change an earlier answer was to abandon setup and start over; `←` on the harness step now re-asks the policy step with the previous selection still ticked. The policy step itself offers no `←` — the step before it is frequently not asked at all (a single scope choice is stated, not prompted), so it would sometimes go nowhere. (#663)
- List cloud-managed policies in `failproofai policies`. They enforce on the machine exactly like builtins and convention policies, and nothing showed them — so the command answered "what is enforcing here?" with a subset, and the policies an operator pushed to a fleet were precisely the ones invisible to the person standing at the machine. `observe` deployments render as OBS rather than ON, because their verdict is discarded, and the section says outright that these are not switchable with `--uninstall`. (#663)
- Stop the API-key prompt printing one copy of itself per character typed. `\r\x1b[2K` erases the row the cursor is on and nothing above it, so a line wider than the terminal wrapped, the erase reached only its last row, and every keystroke left the previous rows behind — pasting a 40-character key stacked 40 prompts down the screen. The prompt now truncates to one physical row. (#663)
- Offer the cloud connection first in the setup wizard, and preselect it. Connecting is what most people running the wizard came to do; staying local is one keystroke away and neither option's copy changed. (#663)
- Say "harnesses" rather than "AI assistants" throughout setup — the wizard protects agent CLIs, and the word it used for them matched no other surface. (#663)
- Document the commands and flags that `--help` never mentioned: `backfill` (absent entirely, with `--since` and `--dry-run`), the new `flush`, and the whole `config` cloud surface — `--connect`, `--token`, `--machine-id`, `--machine-label`, `--no-transcripts`, `--disconnect`, `--status`, `--pause`, `--resume`. A flag nobody can discover is a flag that does not exist. (#663)
- Drop the "safety net" metaphor from the setup wizard's intro, which now names the tool it is setting up. A metaphor tells a first-time user nothing about what the next four steps will do to their machine, and this is the first line they see. (#663)

## 1.0.0-beta.12 — 2026-08-07

Expand Down
30 changes: 30 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -1152,13 +1152,43 @@ src/hooks/
all 11 CLIs, recoverable only by hand-editing
~/.failproofai/policies-config.json
manager.ts policies --install / --uninstall / list
harness-cli.ts `failproofai harness add-path/remove-path/list` —
edits [collector.sources.<harness>] extra_paths in
config.toml and NOTHING else. The grammar, `~`
expansion, overlap rejection and <label>-<agentId>
namespacing all live in the daemon
(crates/fpai-collect/src/extra_paths.rs); this side
validates only the HARNESS NAME, because that is
the one failure with no other detector — a typo'd
table is valid TOML that captures nothing.
HARNESS_KEYS here and in main.rs are two
hand-maintained copies of one list; a test reads
the Rust source to keep them identical
src/index.ts Public API entry point → compiled to dist/index.js
dist/index.js CJS bundle (built by `bun run build`; shipped in npm pkg)
dist/cli.mjs Bundled bin/failproofai.mjs (bun run build:cli)
dist/worker.mjs Bundled bin/failproofai-worker.mjs (bun run build:worker) —
plain Node can't resolve raw .ts specifiers, so the warm
worker needs this bundle just like the CLI does
Cargo.toml Rust workspace root (resolver "3", shared [workspace.package])
crates/fpai-collect/src/extra_paths.rs
Per-source extra capture paths: `label=path`
grammar, folder-name label fallback, and the
rejections that each prevent a SILENT failure — a
path overlapping a default root is collected twice
under two agent ids, and two entries sharing a
label share a cursor directory whose whole map is
written atomically, so both re-read from zero after
every restart. Each accepted path becomes its OWN
task (file_source / sqlite_harness in main.rs) with
its own cursor dir and health key — Hermes reached
this shape first, via per-profile databases. The
<label>-<agentId> prefix is applied in
SpoolWriter::push, never in an engine: a SQLite
format derives its real id from the row and takes
params.agent_id only as a fallback, so prefixing
there namespaces just the sessions that failed to
derive one
crates/fpai-ipc/ Wire protocol shared by the daemon and its tests: length-
prefixed JSON framing, protocolVersion envelope, peer-
credential checks (see crates/PROTOCOL.md)
Expand Down
Loading