Repository navigation
Releases: passioncode-ai/project-observatory-dashboard
Release list
v0.24.5
The leak scan reads every agent's transcripts (Codex, Hermes and opencode beside Claude Code) and a scan its
deadline cuts short still reports what it saw. Cleanup never removes local work it cannot restore, and the disk
guard registers worktrees cheaply before its expensive passes. Findings say when the tick, the harness scan or the
guard could not measure, and RAM and swap refresh on their own. Lessons become their own record kind; OpenRouter
spend per issued key; a DigitalOcean database door. The companion plugin observatory-log moves to 0.24.5: its
handling-secrets skill names the DigitalOcean door, the provider tools that return a credential by design, and
the header door's --server-url.
Added
-
The leak scan reads every agent's transcripts, not only Claude's (I-5, PB-032 remainder).
Before this, a value echoed into a Codex rollout was neither a sighting nor named as unread.
paths.transcript_homes()names each home, with the tool's own source as evidence:- Claude Code: the
sessionssource, andhistory.jsonlin$CLAUDE_CONFIG_DIR/~/.claude. - Codex: in
$CODEX_HOME/~/.codex,sessions/andarchived_sessions/(rollout-*.jsonl,
and the compressed.jsonl.zst, read with Python 3.14's zstd) andhistory.jsonl; in
$CODEX_SQLITE_HOME, thestate_*,logs_*,thread_history_*,memories*,queue_*,
goals_*andagent_message_board_*.sqlitedatabases. - Hermes:
$HERMES_HOME, or~/.hermeswith$HERMES_DATA_DIR_SUFFIX, and its profiles:
state.dbandsessions/*.json|*.jsonl. - opencode: in
$XDG_DATA_HOME/opencode, theopencode*.dbdatabases or$OPENCODE_DB, and the
legacystorage/**/*.json.
An absent home is not a degradation. These are named in
not_scannedwith the reason: a home that
exists but will not read, a profile that is a symbolic link, a database that will not open, and a
.zstthis Python cannot decode.coverage.transcript_homessays which homes wereabsent,
scanned,partial(cut by the step's deadline) orunreadable. Sightings name the file or the
table.column, never the value. - Claude Code: the
-
A database's mark hides fewer rows from the leak scan (I-5). Each table's signature is kept
beside its mark: the inode, highest rowid, count, rowid sum and sum of squares (two primes),
highest update value, and digests of the newest row and of the rows at the update maximum (one
digest for the whole set past 64 rows). These are now read again:- rows in a store replaced by another file;
- rows whose set of rowids under the mark changed (some deleted, or one inserted under the mark
with a new rowid); - the deleted newest row's rowid when it is reused;
- a row rewritten in place whose update column moved, or stayed at its maximum with a new digest
(the column preferred finest-first:updated_at_msbeforeupdated_at). A bulk update is
reported once, not every tick.
The weekly complete pass is still the only one to see:
- a row replaced under its own rowid below the newest row, in a table with no update column;
- an edit that moves no column (Codex
UPDATE threads SET title); - an edit below a future-dated maximum;
- an update column holding mixed types;
- a change of rowids whose count and both sums collide (impractical, not impossible);
- Codex
thread_items, which is upserted in place; its rollouts are read incrementally.
Rows are read 500 at a time, each page its own statement. The signature's aggregates each scan
the whole table in one statement, so on a rollback-journal database a writer can wait for one
such scan. Codex, opencode and Hermes run their stores in WAL mode by default, where a reader
does not block a writer. -
A damaged table is a hole, never a clean table (I-5). Any error from a table used to be
taken for a WITHOUT ROWID table, and that table's read then stopped without a word: a damaged
page left the storescannedwith nothing degraded. Only a table declaredWITHOUT ROWIDis
read whole now; SQLite itself says which tables those are (pragma_table_list), so a comment in
the CREATE statement cannot. Any other error names the store innot_scannedas read only in
part, with each table and the error's type, never SQLite's message, which can quote a cell.
Its other tables are still read, and its marks do not move.Other inputs that used to fail are now handled:
- Text that is not UTF-8 is read as its bytes.
- A table name that is not UTF-8 is a hole. A column name that is not UTF-8 is a hole of its
own, and the rest of its table is still read. - VIRTUAL generated columns are left out of every read, so one that fails no longer blinds its
table. STORED generated columns are read, because their values are on disk. - A column named
rowidno longer stands in for the real rowid. Rows are paged on whichever of
rowid,_rowid_andoidthe table leaves free; one that takes all three is read whole. - A store that fails in any other way is "stopped while reading ()". Its sightings
counted so far stand, itsrows_readcounts the rows it read, and the rest of the scan
and its report go on. - A mark in the state file that is not a rowid is treated as absent, and its table is read
from the start. VARCHAR(n)andNCHAR(n)columns are read, by SQLite's TEXT affinity rule.
Every SQLite store error is now reported by its type only, including "would not open" and a
malformed schema, whose message quotes the stored CREATE statement. Before, those two copied
SQLite's whole message into the report, thesecret.leak_scan_blindfinding and stdout.Not read: rowids of 0 or below, and text kept in a column without text affinity. A
WITHOUT ROWIDtable is read whole every pass. -
Upgrade cost, and two changed findings. The first passes after the upgrade read every Codex
rollout in the 14-day window, both prompt histories and every transcript database whole, and
re-read the tracked tree, the built page and the two engine stores once. The first tick or two
may therefore raisesecret.leak_scan_partial. A store that will not open is now an unreadable
source, which raisessecret.leak_scan_blindfrominfotowarning. -
What one issued OpenRouter key cost, by day and model (INB-019):
openrouter.py spend NAME [--date YYYY-MM-DD] [--json]reads the provider's activity report through the management key that issued the key,
filtered by that key's hash — the last 30 completed UTC days, or one day. The hash goes to the provider only and
never to the output. A refused or unreadable report is said as such and is never printed as $0.
read_activity()is its own function, so a project's spend can call it. An agent whose provider call ended
without a request id can reconcile it from this record. -
A lesson is its own record kind, for every project (
observatory_lesson): a retro entry's class —
classKey,title,trigger,check,nouns— with its origin, the project's pseudonymous key
(p-and 12 hex digits of its root commit, never aproject:<slug>id) and the evidence commit (kept as
commit:<12 hex>). The arguments are what task-pipeline'slessons.py emit-observatory --tool observatory_lessonprints (where the engine lacks the tool, the emitter writeslesson:notes through
observatory_record);tests/lesson_emitter_fixture.jsonholds that shape and the published schema is
checked against it. One record per (classKey, project): a repeat revises it or writes nothing. A
path, a URL, an e-mail address, a line break or aclassKeythe redactor would change is refused before any
write (invalid-lesson, every reason listed); credential shapes and known values are redacted. A promoted
lesson that cites a new commit, or whose evidence stops resolving in two known projects, is archived and
replaced by a new proposal (outcome: replaced), which is promoted again only once its commit resolves.
Writtenproposed, confidence 0.5, local stdio only. The tool publishes its outputSchema. -
observatory_searchandobservatory_recalltakekinds, applied inside the candidate query before the
300-match window, and every hit names itskind; a lesson hit carries its fields aslesson. Across projects
the search abstained with the window full of per-session turn records (measured:count 0, abstain: true);
withkinds: ["lesson"]a lesson among 400 such rows is found. -
Lessons are corroborated by a second project:
tools/corroborate.pypromotes a proposed lesson to
observedasagent:lesson-corroboratorwhen another, distinct project records the sameclassKeyand both
commits resolve — each asked of git in the registered clone whose root commit gives its project key. A key no
registered clone carries is no witness: its lesson stays proposed, listed as "commit not resolvable here". The
same project twice is one witness;supportedstays the operator's.corroboration.jsonlists lessons apart
from refusals (lessons: promoted, waiting, unresolvable, contradicted), andcounts.proposedno longer
counts them. -
Lessons are kept 365 days while proposed, and for good once observed (
retention.json→
ledger.kinds.lesson); any kind can carry its own horizons there, read throughretention.horizon_forby
the review queue's countdown and prose, theledger.review_expiringfinding and the health page's digest,
so none announces a proposed lesson's erasure at the general 90 days. -
tools/migrate_lessons.pyconverts lessons written earlier asobservatory_recordnotes (lesson:
statements) into lesson records, as a new revision of the same record;--dry-runis the default.
Lessons are agents' work memory and stay in Observatory; an export of project-grade lessons to Fabric project
memory is a later seam ([agent memory → Lessons](docs/design/AG...
v0.24.4
Inside the disk guard the git tier plans branches and worktrees only and fits its deadline, so a scheduled pass under load reaches its removals. The companion plugin observatory-log is unchanged.
Fixed
- Inside the guard, the git tier plans branches and worktrees only, inside its deadline (OBS-63): on the first
0.24.3 pass it planned 116 actions and reached none — its listing of what processes hold and a walk for build
output (some 18,000 directories) took its whole share. The guard's own build-output class already covers that
walk, so the guard asks for--git-only; the listing is bounded by the deadline; the result names
plan_seconds.
Verify
gpg --verify SHA256SUMS.asc SHA256SUMS # key 63B30DC324BD697487AA31944FAFB8AEC803B6A7
shasum -a 256 -c SHA256SUMS --ignore-missing
gh attestation verify <file> --owner passioncode-ai --signer-repo passioncode-ai/.githubThe organization's public key: https://github.com/passioncode-ai/.github/blob/main/release-signing/passioncode-release-signing.asc
v0.24.3
The disk guard's git tier removes merged and pushed branches and clean worktrees under load again: its re-check takes one listing of what processes hold per 20 seconds instead of one per worktree. The companion plugin observatory-log is unchanged.
Fixed
- The git tier's re-check takes one listing per 20 seconds, not one per worktree (OBS-63): at load 300–700 each
full listing of what processes hold took tens of seconds, and on 0.24.x the guard's git tier spent its whole share
re-checking the first worktrees — no branch or worktree was removed by a scheduled pass. Run by hand on the same
machine, the tier removed 95 merged and 3 pushed branches and 7 clean worktrees.
Verify
gpg --verify SHA256SUMS.asc SHA256SUMS # key 63B30DC324BD697487AA31944FAFB8AEC803B6A7
shasum -a 256 -c SHA256SUMS --ignore-missing
gh attestation verify <file> --owner passioncode-ai --signer-repo passioncode-ai/.githubThe organization's public key: https://github.com/passioncode-ai/.github/blob/main/release-signing/passioncode-release-signing.asc
v0.24.2
A cache command never runs past its class's share, so the disk guard's git tier — merged and pushed branches, clean worktrees and their dependencies — keeps the time 0.24.1 set aside for it. The companion plugin observatory-log is unchanged.
Fixed
- A cache command never runs past its class's share (OBS-63): on 0.24.1 three cache commands, each inside its
own 90–300 s timeout, ran 271 s together and took the git tier's time again. A command's timeout is now what is
left of the share, and none starts with less than 30 s left; the rest waits for the next pass.
Verify
gpg --verify SHA256SUMS.asc SHA256SUMS # key 63B30DC324BD697487AA31944FAFB8AEC803B6A7
shasum -a 256 -c SHA256SUMS --ignore-missing
gh attestation verify <file> --owner passioncode-ai --signer-repo passioncode-ai/.githubThe organization's public key: https://github.com/passioncode-ai/.github/blob/main/release-signing/passioncode-release-signing.asc
v0.24.1
The disk guard's git tier — merged and pushed branches, clean worktrees and their dependencies — gets its own
share of every pass under disk pressure; on 0.24.0 the path classes could spend the whole run before it. The companion
plugin observatory-log is unchanged.
Fixed
- The disk guard's git tier keeps its own share of the run (OBS-63): the first scheduled 0.24.0 pass, at
critical, spent 425 of its 480 s on the six path classes and skipped the git tier — merged and pushed branches
and clean worktrees with their dependencies, where most of the space is. The tier now hasgit_tier_seconds
(120 s by default, at most a third of the run) kept back before the classes split the rest.
Changed
-
The collectors take turns (OBS-52): after
integrity, the tick runs its collectors least recently started
first, not in a fixed order. Under load the collectors in front used the window and the ones at the back were
not startedtick after tick: on one machine (load average 26–30) the leak scan and the lifecycle watch had not
run for two days. A step that was not started keeps its place at the front of the next tick; a collector that
reads another's output this tick still runs after it (READS_FROM, held equal to the artefact graph by
tests/test_pipeline.py). Start times are kept instore/tick-rotation.json;tick.jsonreports
collector_orderandnot_started.Added to these notes on 2026-10-12: this change shipped in 0.24.1 (#231) but was missing here, and the
CHANGELOG.md packaged in 0.24.1–0.24.4 lists it under 0.24.0. The release's files are unchanged.
Verify
gpg --verify SHA256SUMS.asc SHA256SUMS # key 63B30DC324BD697487AA31944FAFB8AEC803B6A7
shasum -a 256 -c SHA256SUMS --ignore-missing
gh attestation verify <file> --owner passioncode-ai --signer-repo passioncode-ai/.githubThe organization's public key: https://github.com/passioncode-ai/.github/blob/main/release-signing/passioncode-release-signing.asc
v0.24.0
The disk guard keeps up with a disk that fills fast and never removes what a session still uses: the maintenance pass runs every 15 minutes, under pressure the guard acts on every pass, and a fresh, shared listing of what processes hold — open files, maps, executables, command lines — is re-checked at every removal, under one lock for every cleanup. One project answers to many names (aliases, project.resolve). The doors grow: claim brings a key minted elsewhere under the OpenRouter door, a project's own key comes from its spend policy, and Cloudflare gains workers-routes and redirect-rules. The meter grows: the estate's month measured, grant.check declared, a usage report. The harness scan names plaintext credentials in agent tools' configs. The companion plugin observatory-log is unchanged.
Changed
-
leaks,git-hygieneandagentstop before their deadline instead of being killed (OBS-52), reading
OBSERVATORY_STEP_DEADLINEas the folder scan,integrityand the plugins already do:leaksstarts a file only while two minutes are left and a SQLite store only while one is. Offsets of what it
read are kept; what it did not read is named indegradedas unmeasured, never clean, and a store's mark does
not move.git-hygienemeasures the least recently measured checkouts first. The ones it did not reach carry their last
measurement, markedstale, so no checkout starves.agentstarts a model call only while a whole call (150 s) fits, so a paid answer is never lost. The run
report names the deadline, and the deltas wait for the next run.
-
Trust tiers, written down (Fabric ADR-0144 amendment 4): on the operator's own machines (T0, and enrolled
machines T1) the meter and the doors never demand a delegation token —slotsRequireGrant: when-tokenand
keyserverRequiresDelegation: offstay the settings there; the strict ones are for a machine that serves another
person's agents (T2). Design:docs/design/METER.mdM0.
Added
-
Cloudflare door:
--preset workers-routes --zone <zone>— Zone Read + Workers Routes Write on one zone,
into a vault slot, verified by listing the zone's routes with the new value.workers-editdeploys a script
but cannot route a hostname to it; this is that one zone right, without DNS. First use: moving a
static site to a Worker. -
openrouter.py claim NAME --where PATH, key on stdin — brings an OpenRouter key this door did not mint (one
made by hand in the dashboard) under the door, sodisable,limitandrevokereach it. The value is hashed,
never stored or printed; the provider's row is read by that hash, and a label alone (prefix plus three
characters, which collide) is accepted only when it is the single match in every listing and the ceiling the
key reports for itself agrees. A claimed key is revoked through the door, never rotated to nowhere.
A claimed key is identified by its hash, so a rename in the provider's dashboard never reads as a deletion; a
bulkrotatenames it and goes on;revokerefuses it while a consumer reads its label; the label path needs
every listing whole and a ceiling to compare; a claim re-checks the ledger it writes. -
Cloudflare door:
--preset redirect-rules --zone <zone>— Zone Read + Dynamic URL Redirects Write (Single
Redirects) on one zone, into a vault slot, verified by reading the zone'shttp_request_dynamic_redirect
entrypoint with the new value. A zone with no redirect yet answers 404 there to a token that may read it and
403 to one that may not, so for this preset alone a 404 counts as the proof. -
A usage report (OBS-53; contract DEC-0021): the server declares
surfaces.usageand answers
GET /fabric/v1/usagewith the service token — the last 31 days of the assistant's paid model calls by provider
and model, from a receipt written at every charge (tokens and cost with its basis, never a prompt or a caller).
The fabric-service schemas are vendored again at contract7931c1d; the kit's conformance probe now runs
usage.report. -
Plaintext credentials in agent tools' configs (OBS-74): the harness scan names, per file, the credential-shaped
values kept by Hermes, opencode, Codex, Claude Code's MCP blocks, Gemini and Cursor — names only, never a value —
asharness.plaintext_secret, with the vault anduse_secret.pyas the remedy. Design:docs/design/HARNESS.md
H15. -
A project's own key, from its spend policy (OBS-58):
openrouter.py issue-for-policy --policy FILEreads a
contractspend-policy(DEC-0037) and issues, for eachproject-keysource, a key for that project into the vault
slot the source names. The key's ceiling is the project limit, and it resets monthly. A slot of another project is
refused as a borrow (spend-policy rule 1). A policy with no project limit needs an explicit--limit, because a key
is never unbounded. A ceiling above the project limit is refused. A second run is idempotent: it only moves a
ceiling the policy changed. -
grant.checkis declared infabric-agent.json(OBS-67; manifest revision 9): effectnone, its
input and output schemas published atv0.24.1, a probe that a call carrying no delegation token is denied
principal-unproven. Fabric's registry reads the meter from the service descriptor; the token-carrying call
goes to the same tool on the memory-http door. -
The estate's month, measured (OBS-56): the new step
scan-spendsums this month's spend of every OpenRouter
account the door holds (GET /activity, per account, never a key) intostore/raw/spend.json. The meter counts
it, plus open reservations, against L0 (USD 1,000 by default), and the findingspend.estate_monthwarns at
80 % and is critical at the limit. Measured on the operator's machine on 2026-10-11: $262.97 across four
accounts. -
The harness scan reads hooks and skill use (OBS-61): every hook the user's settings and each enabled
plugin declare (event, matcher, source, program, a digest of the command and never the command), checked
against the skill rules, with the findingharness.hook_risk; and each skill's invocations over the last 30
days of transcripts, so the listing overflow names the unused skills first; and each project's own
.claude/skills, counted on top of the user level (harness.project_listing_overflowwhen they alone push a
session over the budget); and Codex's skills (~/.codex/skillsand the shared~/.agents/skills). Design:
docs/design/HARNESS.mdH11–H14. -
One project under many names (OBS-65). The operator curates
config/identifiers.json
(another system's name → the canonicalproject:/product:id, with the name's owner); the
emitter publishesregistry/identifiers.jsonand oneknown_asrelation per alias. A name
given two targets, or contradicting a measured repository, deployment or domain, is a
published conflict, never a silent pick.project.resolve(MCPobservatory_resolve, Fabric
capability pinned atv0.24.0) answers any name with the one canonical id, exact match only;
an ambiguous or conflicted name is a typed refusal. Agents propose aliases through
observatory_propose(aliases); accepting one never rewrites an existing row.
Fixed
- The disk guard frees more, on any system (OBS-63): its pressure now lowers the git auto tier's thresholds
(pressureinconfig/cleanup.json: clean worktrees and merged branches after 3 days atlow, 1 day at
critical; the manual tier never moves), it sweeps npx's package cache and macOS diagnostic reports, and it
knows what is held on Linux withoutlsof(from/proc) and on Windows (a folder that cannot be renamed aside is
held, and nothing of it is removed) — before, withoutlsofit removed no path at all. - The disk guard keeps up with a disk that fills fast (OBS-63): the maintenance pass runs every 15 minutes
(it was hourly), and the guard acts on every pass once free space is below thelowline (hourly while it is
normal); it now also clears application and tool cache folders (~/Library/Caches,~/.cache) untouched for
7 / 2 / 1 days — the folders a hand cleanup freed 10 GB from — keeping downloaded browsers and SDKs a tool does
not fetch again by itself (cache_keep); and its per-class cap is 20,000, since the run budget bounds its time.
Measured on one machine: free space fell from 6 GB to 0.3 GB inside one hourly gap. - The disk guard finishes inside its pass (OBS-63): the first scheduled pass on 0.23.0 killed it at the
job's 1200 s ceiling, under launchd's background priority, and recorded onlyexit 124— no report, no
journal. The job's ceiling is now 600 s and the guard stops itself at--budget(the job passes 480 s;
run_budget_seconds, 900 s, by hand), shares that time fairly between its classes, acts before it refreshes
any survey, writes each action tostore/logs/disk-guard.jsonlas it happens, and always writes its report,
naming what the budget left for the next pass. - The disk guard, second iteration (OBS-63; design
docs/design/DISK-GUARD.md): a review of the first one
found it could remove what a session still used, and could misreport what it did. Now:- Held means open files, memory maps, executables, working and root directories and paths named on a
process's command line (holders.py:lsofwith its escaped names decoded,/procon Linux, a rename probe
on Windows), taken fresh for every class; every removal re-checks its target, still unheld and unchanged since
the plan, and a target that changed is skipped. - Every cleanup takes one lock (
store/cleanup.lock): the tick, the guard andfull cleanupnever act at
once, and a guard pass that finds the lock held is recorded asbusy. - A privacy-guarded folder is never opened, an entry another account owns is never touched, and a tracked
direc...
- Held means open files, memory maps, executables, working and root directories and paths named on a
v0.23.0
The disk guard keeps free space by itself, every hour, without touching anything a process holds; the meter
answers whether a delegated call may spend; a vault slot is released only under a grant that names it; and the
harness scan reports what every agent session loads. Releases are now approved by checks, not by a click. The
companion plugin observatory-log is unchanged.
Added
-
The disk guard (OBS-63): with
features.auto_cleanupon, the hourly maintenance job runs
tools/disk_guard.pyfirst in every pass, before the update and the backup — no longer only from the tick,
whose cleanup step a loaded machine could leave unreached for a day. Free space sets the pressure (normal,
lowunder 25 GB,criticalunder 10 GB) and the pressure moves every threshold: the cache tools' own cleanup
(uv,pnpm, Homebrew, Docker only while its daemon already runs;npm cache verify,uv cache clean, Go and
Yarn fromlow), git-ignored build output of checkouts idle 14 / 3 / 1 days (by checkout, commit or an
uncommitted file, never the index), temporary entries, Trash items older than 30 days (3 atcritical),
abandoned browser code-sign clones, then the cleanup's git auto tier. Nothing a process holds open or works in
is removed: onelsoflisting per run, re-checked at each removal, and without it no path goes. Freed space is
the disk's own before/after count (freed_mb), notdu, which counts an APFS clone in full.
full disk-guard [--apply] [--level L] [--large-files]runs it by hand. The Machine page and
observatory_machine(section: guard) show the last run, the largest files under the home directory and the
running simulators with the command that stops each;machine.guard_idlewarns when the guard has not reported
for three hours, andmachine.disk_lownames the guard's run, the swap holders and the largest files.
Configuration:config/cleanup.json→guard. Supersedes the Space-page draft (OBS-15). -
The meter (OBS-55; Fabric ADR-0144 step 3, contract DEC-0037/0038/0039):
grant.checkon the memory-http
door answers whether a call may spend. The caller presents the onward delegation token Fabric's hub minted for
project-observatoryinAuthorization(never in an argument); the meter verifies it offline against the
estate's Ed25519 keys (delegation.py, the contract's verifier table DL-13), reads the grant from the hub's
grant directory, and reserves the amount against the grant'scall,task,total,project,day,week
andmonthwindows and the estate's monthly total (L0, USD 1,000 by default) and provider-key ceilings, in one
transaction. It answers a receipt, or a denial naming the code, the level and the window.full grant --status,
--check(token on stdin) and--settle. The ledger records no person and no chain. Configuration:
config/meter.json. Design:docs/design/METER.md. -
A vault slot only under a grant (OBS-57): an agent hands
use_secret.pyits delegation token in
FABRIC_DELEGATION_TOKEN(scopesecret.use).run,serveandheaderthen release a slot only when a grant
the token names hascredentialSource {kind: project-key, ref: PROJECT/ENV/NAME}, never read a.env, and
remove the token before the child starts.slotsRequireGrantinconfig/meter.json:when-token(default
until Fabric's issuer ships),always,off. -
The agent harness scan (
integrations.harness,sources.harness_root; off until turned on): what every
Claude Code and Codex session loads before the first word. It counts each skill's entry in the session's skill
listing against a budget (OBSERVATORY_HARNESS_BUDGET_CHARS, default 30,000 characters) and names the
descriptions that no longer fit, largest first; flags an instruction file over 200 lines and the settings that
load every MCP tool upfront; and runs deterministic rules over skill files — hidden Unicode, concealment
directives, download-and-execute, a credential read beside a send, opaque content — weighed by where the skill
comes from. Files are read, never run. A skill whose files change is reported until
full harness --accept KEY; an ack of a rule hit lapses when the skill changes. Design:docs/design/HARNESS.md.
Fixed
full updaterefusing with "the workspace changed while staged" now names the paths that moved, so the writer
to stop can be found (#200).
Verify
gpg --verify SHA256SUMS.asc SHA256SUMS # key 63B30DC324BD697487AA31944FAFB8AEC803B6A7
shasum -a 256 -c SHA256SUMS --ignore-missing
gh attestation verify <file> --owner passioncode-ai --signer-repo passioncode-ai/.githubThe organization's public key: https://github.com/passioncode-ai/.github/blob/main/release-signing/passioncode-release-signing.asc
v0.22.0
Project Observatory on Windows and Linux: a desktop app with a signed Windows installer, the tick, the server
and maintenance as per-user Task Scheduler tasks or systemd units, updates that work on Windows, and the backup
passphrase in the Credential Manager. The Mac app is universal again for Intel Macs. The companion plugin
observatory-log moves to 0.22.0.
Changed
- The vendored fabric-service kit is fabric-agent-adapter 0.8.2 (was 0.5.0): Windows and Linux support
(contract DEC-0032, DEC-0033) — the instance lock, the token-file ACL rule, Task Scheduler and systemd in
the descriptor — which the always-on server needs on Windows (W4b).serverd --uninstallrelies on the
kit's own wait for launchd to unload the job. The conformance probe's two new checks the server does not
declare yet are NOT_RUN (usage.report: OBS-53).
Added
- The Windows and Linux app (W8a,
desktop/, Tauri 2): opens on this workspace's dashboard — live when its
server answers, else the built pages under a banner with «Start server», else «Build the dashboard»; Settings
for the program, the workspace and the language; first-run messages that name the install orfull init
step; stays in the tray when its window closes. Built as an NSIS installer and an AppImage/.deb by
.github/workflows/desktop.yml; the release builds and Authenticode-signs it (W9). It updates itself from the
release'slatest.json, each package minisign-signed: «Restart to update» in the menu and the tray (W8c). The
assistant window (Ctrl+Shift+A) asks the workspace's assistant, with each answer's evidence and Stop (W8b). - Updates on Windows (W6):
full update --applystops and restarts the tick and server tasks (systemd units
on Linux) around the install, and replaces theproject-observatory.exeit may be running from — moved aside
first, put back if the install fails. - The server's always-on state off macOS (W4c): «Start server» in an app restarts the Task Scheduler task
or systemd unit of an installed server instead of callinglaunchctl;server.silentwatches that
installation too. - Windows paths and tools (W7): the default workspace is
%LOCALAPPDATA%\project-observatory-full(an
existing~\.local\shareone stays); tools npm installs as.cmd(heroku,claude) start by name; the
CLI and the scheduled jobs write UTF-8. README → Install has the PowerShell steps. - The backup passphrase in the Windows Credential Manager (W5): a generic credential per workspace, kept
for this account on this machine (CredWriteW/CredReadW, never roaming, the value never an argument);
a store that does not answer is not an empty one. The file fallback is owner-only on Windows too (a
protected ACL instead of mode bits). osschedule: background jobs under Windows Task Scheduler and systemd (W4a). One job description, a
per-user task created from XML (InteractiveToken, no stored password,pythonw.exethrough
tools/scheduled.py, so no console window opens) or a systemd user unit;tools/install_launchd.pyand
tools/serverd.py --installuse it off macOS, the server's fabric-service descriptor namingtask-scheduler
orsystemd(W4b).
Fixed
- The Mac app is universal.
macos/scripts/build-app.shbuildsarm64andx86_64and refuses an
executable missing either slice; up to 0.21.0 the release carried the runner's architecture only, so
the app did not open on an Intel Mac (PassionCode.ai platforms rule PL-01).
Verify
gpg --verify SHA256SUMS.asc SHA256SUMS # key 63B30DC324BD697487AA31944FAFB8AEC803B6A7
shasum -a 256 -c SHA256SUMS --ignore-missing
gh attestation verify <file> --owner passioncode-ai --signer-repo passioncode-ai/.githubThe organization's public key: https://github.com/passioncode-ai/.github/blob/main/release-signing/passioncode-release-signing.asc
v0.21.0
The engine runs on Windows, and a check of the agent on a heavily loaded machine fixed what it
said wrongly about itself and what kept its own tail from running. The companion plugin
observatory-log moves to 0.21.0.
Added
- The engine on Windows (docs/design/WINDOWS-LINUX.md, W0–W3). One module per OS seam, each
with a test that runs the same on every OS and refuses the POSIX-only call anywhere else:
oslocks(file locks —LockFileExon Windows),osproc(process groups, the stop signals an
OS has, the assistant's deadline withoutSIGALRM; on Windowsos.kill(pid, 0)terminates the
process, so checking a job killed it),osprivacy(owner-only files through the file's ACL;
descriptors opened in binary mode, without which Windows turns every LF into CRLF). The tick is
Python (tools/tick.py;tools/tick.shhands over to it), and a whole tick runs on the
windowsCI job. Windows needs Python 3.13 or newer.
Fixed
- A busy machine no longer reads as a blank dashboard. Under a load average near 300 the
page's script did not finish within 10 s and smoke reported a criticaldashboard.blankabout a
page that renders in 0.13 s of processor. A timed-out script is blank only when it has used a
5 s processor budget; a starved one is tried once more with 60 s, then recorded as not measured
(dashboard.unverified, information). - The tail's cheap optional steps run before the heavy ones. The index, retention (the
erasure), the fixture sweep, corroboration, the ledger export and the lost-projects record —
seconds each — were not started tick after tick behindpluginsand the agent's model calls.
What the agent proposes is now indexed and exported on the next tick. - A plugin stopped at its limit keeps what it measured, and is told its deadline
(OBSERVATORY_STEP_DEADLINE).disk-usagewalked every tree twice and lost every row at the
300 s limit, so it ran again on every tick and cost the tail 300 s each half hour; it now walks
each folder once, measures the projects measured longest ago first and stops between projects
(51 s for this machine's estate). - The store's integrity check is bounded.
integrity_checkruns once a day and after any
verdict that was notok,quick_checkin between; the step stops itself at its deadline and
recordsunmeasured(astore.integrity_unmeasuredrow naming the last full pass) instead of
being killed with nothing written. Its limit is 240 s: one tick spent 785 s there and left the
collectors no time. - The public-release check reads engine paths the same on Windows (
as_posix()).
Verify
gpg --verify SHA256SUMS.asc SHA256SUMS # key 63B30DC324BD697487AA31944FAFB8AEC803B6A7
shasum -a 256 -c SHA256SUMS --ignore-missing
gh attestation verify <file> --owner passioncode-ai --signer-repo passioncode-ai/.githubThe organization's public key: https://github.com/passioncode-ai/.github/blob/main/release-signing/passioncode-release-signing.asc
v0.20.1
A patch release from a read-only audit of 0.20.0 running on a working machine. The companion plugin
observatory-log moves to 0.20.1.
Fixed
- Findings and the dashboard run again on a slow machine. No tick had finished for 13 hours:
with the collectors held back, the tail's optional steps — plugins, rollup, the agent's model
calls, the vector index, retention and review — still ran before the findings and the dashboard
and used the last of the ceiling (14 ticks in a row ended insidepluginsandagent). They now
stop two minutes before the ceiling (at most a twelfth of it), leaving the core — from the merge
to the findings, the dashboard, the notice and the registry commit — its time; a step that did
not run leaves the previous run's output. - Health says why a tick did not finish. A tick the supervisor stopped at its ceiling was
reported as "killed (a restart, a sleep, a signal)"; the supervisor's own record is read now. - The 3-of-10 unfinished-ticks alert reaches the service card and
doctor, not only the MCP
survey (tick.history_warning). - A restored workspace keeps its files' times. A rollback dated every file at the restore,
and the tick's daily refreshes slipped by the time since the snapshot. - The maintenance record follows a person's update. After a manual
full updateit still
named the last automatic one beside the newer engine; it now recordsupdated-by-person. - The changelog's 0.19.3 section has its heading back — 0.19.4's version bump had overwritten
it — and a test now refuses a gap or a repeat among a minor version's headings.
Documentation
- The README says how the Mac app keeps itself current; the onboarding guide describes how the
tick's ceiling is shared out and whattick.recentandtick.history_warningmean;AGENTS.md
states the app's 15-minute check.
Verify
gpg --verify SHA256SUMS.asc SHA256SUMS # key 63B30DC324BD697487AA31944FAFB8AEC803B6A7
shasum -a 256 -c SHA256SUMS --ignore-missing
gh attestation verify <file> --owner passioncode-ai --signer-repo passioncode-ai/.githubThe organization's public key: https://github.com/passioncode-ai/.github/blob/main/release-signing/passioncode-release-signing.asc