Watch
1
0
Fork
You've already forked SouveraineOS
0
SouveraineOS/docs/tasks/79-project-state.md
Fimeg a8caf27c14 project-state: weekly evidence, carried state, and an agent's door
Five weeks of forge evidence per week rather than a rolling window, so a
finding survives under one key and "closed after three weeks" is computable.
Rules run deterministically; agent.py hands the packet to whoever writes the
prose and never calls a model itself.

Model choice fails closed — no default, no substitute, and the exact named
model must be served or the pass refuses to run.
2026-08-18 19:53:19 -04:00

11 KiB

TASK-79 — project state: what became true, and what stopped being true

Status: first slice built and exercised 2026-08-18 over five weeks, 2026-W30 to 2026-W34. Internal; nothing publishes. Repos: SouveraineOS (the tools and the store). Reads the forge and the local clones; writes nothing outside this repo. Connects to: saf/state.md (the hand-written spine this reads and never writes), docs/tasks/README.md (the declared state it diffs), ~/Projects/Gitea Pipeline/TASK-02-the-ledger-pipeline.md D4/D5 (the narrator and the quality pass — same seam, different question), ~/Projects/Web/CaseyTunturi.com/scripts/ledger/collect.py (the ancestor).

The question this answers, and the one it does not

The ledger on caseytunturi.com answers what happened — day-bucketed, 30-day rolling, overwritten in place. It is a record of a man at work and it is good at that. It cannot answer what became true, because nothing carries across the window: every week is an island, and a 31-day-old finding is simply gone.

This answers what is true now, what stopped being true, and how long it has been unresolved. Same forge, different shape: evidence is kept per week and never overwritten, and a finding survives from one week to the next under a stable key. Statements the five-week run can already make:

  • task 8 left the index, held W30 to W32, closed after two weeks.
  • task 15, Idle coordinator, has been Blocked or gated for four weeks.
  • souveraine#1 has been unmerged for 38 days and is the oldest decision open.
  • souveraine CI was regressed in W33 and mixed in W34 — the finding turned.

Why here and not in the ledger's repo

The ledger's output repo is the public site. Its CI builds and deploys on every push, so an analytical artifact living there is one careless import away from being published. SouveraineOS already owns project state — saf/state.md is the resume anchor and docs/tasks/README.md is the work index — and it deploys nowhere. The forge answers 403 to anonymous requests, so the store is behind a login even though the repo is not marked private.

Promotion to either public surface stays a separate, deliberate act: caseytunturi.com for engineering history, souveraineai.com for current capability. One evidence base, two editorial surfaces, neither automatic.

Shape

forge API + local clones
        ↓  collect.py            deterministic, no model
project-state/evidence/<week>.json
        ↓  synthesize.py         rules first, model second
project-state/state.json         durable, carried forward
project-state/history/<week>.json
project-state/reports/<week>.md  prose, disposable

Nothing schedules it. The tools name their own gaps instead:

collect.py --gaps [--since 2026-W30]   weeks holding no evidence
collect.py --backfill --since 2026-W30 collect every one of them, in order
synthesize.py --pending                weeks collected but not passed over
synthesize.py --catch-up               pass over them oldest first

Order matters — carry-forward is sequential, so --catch-up is the entry point and --week warns when it would skip ahead of the state.

The deterministic/model split is the point. A model is never asked to discover a fact code can read; it gets the computed findings and looks only for what crosses between them. Its findings are marked by: model, live in their own section of the report, and are dropped unless every evidence id they cite exists in that week's evidence and spans two kinds of it.

The prose is regenerable — --report-only rewrites any week's report from the stored state and evidence, so a future model can rewrite history without the original report ever having been authoritative.

Evidence, measured 2026-08-18

W30 W31 W32 W33 W34 (partial)
Commits 226 287 201 260 43
CI runs 98 133 143 185 25
Declared changes 255 56 18 37 116

16 repos listed, ~95 API calls a week, five weeks collected in 20 s, zero disk.

Three things the collector inherits from the ledger because they were paid for once already: the limit=50 page clamp (Gitea ignores a larger limit and hid 24 of 74 repos for months), the branch-tip prefilter (a branch whose tip predates the window cannot hold work inside it), and per-repo SHA dedup.

Two it does not:

  • The Actions API. /repos/{repo}/actions/runs ignores limit and page and returns the whole history, so it is fetched once and sliced. Paging it loops on the same rows. 02-the-ledger.md lists CI state as an unbuilt later increment; this is that increment, joined to commits by head_sha.
  • Redaction. Evidence is stored raw because it never leaves the forge. --redact applies the ledger's patterns for anything promoted outward.

Dimensions

The SAF's own sections, plus the two it has no page for:

memory runtime reflection identity surface sensorium release reliability federation adoption

A commit is placed by scoring its changed paths and its subject against a keyword table, weighted alike — paths alone file compaction: target the active conversation under the server module it edits, and the subject is the author saying which subsystem he thinks he moved. On W33: 198 by paths, 29 by subject, 22 by repository default, 11 placed nothing and are reported as unassigned rather than guessed at. Every row keeps its basis and its paths, so any placement can be argued with.

One dimension per commit is a simplification. A crash fix in the shell is both surface and reliability and counts once.

State vocabulary, and the resolution rule

milestone_reached regressed blocked decision_required advanced unchanged — assigned by rule:

  • a task's section in the index (Live / In progress / Blocked or gated / Open) gives its standing state; a move backwards is regressed, and leaving the index entirely is milestone_reached;
  • decision_required comes from Casey's own phrasing in the row — verdict owed, decisions are Casey's, scope and deliverable unanswered;
  • a repository whose last run of the week failed is regressed;
  • a pull unmerged past fourteen days is decision_required.

A finding the week says nothing about stays open. Only positive evidence closes one — a task gone from the index, a pull merged. The first two-week run resolved four CI findings because the second week had no runs for those repos, which is the difference between a state document and a diff.

Findings that describe one week — a CI week, a dimension's activity, a release cut — do not carry at all; they lapse with their week. Findings that describe a thing — a task, a pull, an issue — carry until something closes them.

Task findings track the index snapshot, not only its diff. Tracking the diff alone made every restatement a permanent finding and the task itself invisible; the index is the state, the diff is the movement, and weeks_in_section is how "this remains the oldest blocker" gets computed.

Provenance

Each finding carries provenance — deterministic rule name or model, endpoint locality resolved from the URL rather than asserted, timestamp, and the week its evidence came from — plus a human block (verdict, note, edited) that is empty and exists so a promote/reject pass has somewhere to write.

synthesize.py --explain <id> prints a finding and every evidence row it cites, in full. That is the trace path, and it is one command.

The analyst, measured

ANALYST_URL / ANALYST_KEY / ANALYST_MODEL, any OpenAI-compatible endpoint. No URL means no model pass and a complete deterministic report — the same "never a failed run" contract the ledger's narrator has.

Against Fable on llama-swap, 2026-08-18:

  • Warm, it answers in 6 s. The 14 s empty first call recorded in TASK-02 D4 was not llama-swap loading the model. Fable is a reasoning model: it fills reasoning_content first and leaves content empty when the budget runs out before it concludes. At max_tokens: 250 — what collect.py's narrate() sets — it can never answer. The ledger's narrator has that bug now.
  • The full week as a brief is 39k characters; Fable spent 3000 tokens reasoning over it, returned nothing, and took 323 s. Sampling ten commit subjects per dimension cuts the brief to 20k and the run to ~200 s.
  • Reasoning length varies run to run and can still eat the budget, so a truncated answer is salvaged object by object — every complete finding before the cut is kept.
  • Quality is the open problem. On W33 it returned 21 findings, of which ~15 were commentary on the deterministic findings rather than on the project. A tightened charter and a gate requiring two kinds of evidence cut that to four. Of those four: one good (CI runs that neither pass nor fail nearly equal the passes, which the counts support), one arguable, two wrong — one cites a success run as evidence of failure, another cites an unrelated doc commit. A 9B is not a good analyst for this. The seam works; the model is Casey's call.

Acceptance

  • Weeks collected deterministically, every row carrying a stable id.
  • A durable state file carrying a finding across weeks under one key, with first_seen, weeks_open, weeks_in_section, and resolution only on positive evidence.
  • Carry-forward exercised over five real weeks, not designed.
  • A human-readable report where each finding names its evidence.
  • One command traces a finding back to every row it rests on.
  • The tools name their own unfilled weeks rather than assuming a schedule.
  • An analyst whose findings survive review, or the pass explicitly parked.
  • A cadence decision: on demand, or a Monday job on the same runner.
  • A human verdict written into a finding — the seam exists, nothing uses it.

Open, and Casey's to answer

  • Which model holds the analyst. Fable is honest and slow and wrong about half the time on this. The substrate's own config already reaches bifrost, deepseek, zai and claude; the deterministic half needs none of them.
  • Where a promoted finding goes. The seam exists; the editorial call does not.
  • Whether the store is committed here. It is project state, it belongs with saf/state.md, and it is behind the forge login — but it is a machine-written tree in a repository whose other files are hand-written.

Noticed while building this, not part of it

docs/tasks/README.md publishes 10.10.20.123, 172.16.42.1 and an ssh key path into this repo. Behind the forge login, so not the leak TASK-02 D8 closed — same class, and it sits in the file this task diffs every week.