Skip to content
Merged
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
62 changes: 62 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,68 @@ All notable changes to **basou** are recorded here. The project follows
- **`@basou/sdk` tells such an entry apart by the error's class instead of
its message.** `getSession`, `readEvents` and `streamEvents` return `null`,
an empty list and nothing for it, as before.
- **A session's `events.jsonl` or `session.yaml` that is a symlink, whatever
it points to, or anything but a regular file is not followed.** basou read
through such a file, reading a log or a `session.yaml` from outside the
store, and a FIFO there made `basou verify`, `basou board measure` and every
command that reads the session's events wait without end. basou never
creates either form. Nothing is now read through it or waits on it, and
nothing is appended or written through it:
- `basou verify` reports the session `tampered` with a new `reason`,
`events_not_a_file` or `yaml_not_a_file`, and `event_count` `0`. It used
to follow the file and report what it found behind it — `verified`, exit
`0`, for an intact log behind a symlink — or wait on a FIFO. A
`session.yaml` that is a regular file is still read beside an
`events.jsonl` that is not, so a newer basou's session is `unsupported`
as before, and `session_yaml_invalid` is reported as on any row. `session
rechain` skips such a session with the same reasons and exits non-zero,
and an in-place re-import skips it as a prior whose chain fails
verification.
- The commands that read the session by its id stop with `<file> of session
<id> is not a regular file; a symlink or anything else there is not
followed`: `session show`, `approval show`, `approve` and `reject` (which
leave the approval pending), `session note`, `decision record`, a `task`
command attaching to the session, and `exec` and `run` for the session
they started. `approve` and `reject` went on when they could not read the
`session.yaml`; they stop for one that is not a regular file. `basou
view`'s session page answers `404` with the message for a `session.yaml`,
and shows the session without its events for an `events.jsonl`.
- The commands that list or summarize sessions leave out a session whose
`session.yaml` is such, with a warning naming its full id
(`session_yaml_not_a_file`); one whose `events.jsonl` is such is kept, its
events unreadable (`events_jsonl_unreadable`). `import` warns that a
session whose `session.yaml` is such may be imported again, and `task
refresh-linkage` keeps a link to it.
- A directory where `events.jsonl` should be, which these commands reported
as a failure to read it, is one of these cases: `basou verify` reports the
session `tampered` instead of stopping without output, and `report
generate` counts it `tampered` instead of leaving it out of the integrity
tally.
- **`docs/spec/compatibility.md` bounds a command coming to refuse a layout
basou never creates,** the one way an invocation moves from `0` to a
non-zero exit within `1.x`, as this change does: nothing changes on a
layout basou creates, the refusal names what it refuses, and the changelog
lists it. A `basou verify` row that passed may come to fail as `tampered`
under that bound, beside the existing rule that a new `reason` only refines
a status.
- **`basou board measure` raises the method of `integrity`, `trail`,
`review_gaps`, `freshness` and `effort` to 2,** as each counts such a
session differently: `integrity` counts it as `basou verify` judges it, and
each other section is not measured when a file it reads is such — `trail`,
`review_gaps` and `effort` read the `events.jsonl`, and `review_gaps`,
`freshness` and `effort` the `session.yaml`.
- **`@basou/core` exports `SessionFileNotAFileError`, `lstatSessionFile` and
the types `SessionFileName` and `SessionFileKind`.** The event readers,
`readSessionYaml` and the chained append throw the error, which carries the
`sessionId`, the `file` and the `kind` (`symlink` or `not_a_file`).
`SessionSkipReason` gains `session_yaml_not_a_file`, and `RechainResult`
gains `events_not_a_file` and `yaml_not_a_file`.
- **`@basou/sdk` does not follow such a file either.** `listSessions` and
`getSession` leave out a session whose `session.yaml` is such, as one whose
`session.yaml` cannot be read; `readEvents` and `streamEvents` return no
events for an `events.jsonl` that is such, where they read through it or
waited on it, and report it to `onDiagnostic` as an unreadable log
(`events_jsonl_unreadable`), as `stats` and the renderers do.

## 0.69.0 — 2026-10-16

Expand Down
38 changes: 35 additions & 3 deletions docs/spec/compatibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,12 @@ additive, as accepting more of an input shape is — but only when every
invocation accepted before is parsed and behaves as before. Making an option's
value optional does not qualify: `--file --json` reads a file named `--json`
today, and would parse `--json` as a flag instead. Accepting a refused value
moves the invocations it accepts from a non-zero exit to `0`, the one way an
invocation crosses zero within `1.x`; a [split of the non-zero
values](#exit-codes-zero-is-success-and-a-failures-value-may-be-refined) never does.
moves the invocations it accepts from a non-zero exit to `0`, one of the two
ways an invocation crosses zero within `1.x`; the other, in the opposite
direction, is a command [coming to refuse a layout basou never
creates](#a-layout-basou-never-creates-may-come-to-be-refused). A [split of the
non-zero values](#exit-codes-zero-is-success-and-a-failures-value-may-be-refined)
never crosses zero.

One such value has its meaning fixed now, so that accepting it later cannot
surprise anyone: `-` as the file of an input-file flag, a flag that names a
Expand Down Expand Up @@ -181,6 +184,35 @@ The carve-out is bounded by three things:
`1.0`. The row, its fields and today's values of both are listed in [schemas
§7.5](schemas.md#75-event-log-integrity-hash-chain--head-anchor).

A row whose status passes may also come to fail, as `tampered`, but only for
a session in a layout basou never creates, as [the next
section](#a-layout-basou-never-creates-may-come-to-be-refused) bounds; the
`reason` it gains names what is not followed.

### A layout basou never creates may come to be refused

Within `1.x` a command may come to refuse, or to stop following, an entry of
the `.basou/` store in a form basou never creates — a symlink, or anything
other than the directory or regular file basou writes there — and so fail, or
report a failure, where it succeeded before. A `basou verify` row that passed
then fails as `tampered`: a log read through a symlink as `verified`, say. The
SDK's reads follow their own form of this rule ([item 4
below](#sdk-errors-the-class-is-the-contract-the-message-is-not)). This is the
one way an invocation moves from `0` to a non-zero exit within `1.x`, and it is
bounded by three things:

1. **Nothing changes on a layout basou creates.** On a workspace basou laid
out, a command that succeeded still succeeds, and a row that passed still
passes.
2. **The refusal names what it refuses:** an error or a warning naming the
entry, or a `tampered` row whose `reason` says what is not followed.
3. **The changelog lists each such change.**

`basou verify`'s `symlink` and `not_a_directory` reasons, for an entry named as
a session that is not a directory, and its `events_not_a_file` and
`yaml_not_a_file` reasons, for a session file that is not a regular file, were
added this way before `1.0`.

### The import envelope version may move at a minor

`basou session import` and `basou import` require the envelope's
Expand Down
30 changes: 25 additions & 5 deletions docs/spec/schemas.md
Original file line number Diff line number Diff line change
Expand Up @@ -986,7 +986,7 @@ Per-session verdicts:
| `incomplete` | chained log but `session.yaml` is entirely absent (an import crashed between the two writes); a re-import repairs it | 0 |
| `in_progress` | chained log on a still-live session (`initialized` / `running` / `waiting_approval`); the internal chain is verified, the mutable tail and not-yet-written anchor are forgiven | 0 |
| `unsupported` | `session.yaml` was written by a newer basou, whose rules verify does not know: its format major is not 0 (nothing is judged, the log included), or it is a `0.x.y` newer than the one this basou writes and the verdict would otherwise be `tampered`; upgrade basou to verify it | non-zero |
| `tampered` | a real break: bad back-pointer or genesis, foreign `session_id`, torn tail (on an at-rest session), blank or malformed line, anchor missing / mismatching, an `integrity` key left behind with no chained log, on a chained log a `session.yaml` that verify cannot read what it needs from (`yaml_unreadable`, below), or a symlink or file where the session's directory should be (`symlink`, `not_a_directory`) | non-zero |
| `tampered` | a real break: bad back-pointer or genesis, foreign `session_id`, torn tail (on an at-rest session), blank or malformed line, anchor missing / mismatching, an `integrity` key left behind with no chained log, on a chained log a `session.yaml` that verify cannot read what it needs from (`yaml_unreadable`, below), a symlink or file where the session's directory should be (`symlink`, `not_a_directory`), or a symlink or anything but a regular file where its `events.jsonl` or `session.yaml` should be (`events_not_a_file`, `yaml_not_a_file`) | non-zero |

**What verify reads from `session.yaml`.** Three fields, each against its own
schema: the `schema_version` format gate, the `integrity` anchor, and the
Expand Down Expand Up @@ -1018,7 +1018,8 @@ Whether the whole document loads is reported beside the verdict:
`"session_yaml_invalid": true` on the row of `basou verify --json`, and a note
on the human-readable line, whenever `session.yaml` exists but does not parse
or fails the full session schema — on `unsupported` and `yaml_unreadable` rows
as well as beside a verdict decided on the three fields. The commands that
as well as beside a verdict decided on the three fields — unless verify does
not read it at all, as for a layout it does not follow (below). The commands that
read the whole document skip such a session with the same code; they also skip
an I/O failure under it, which makes verify abort instead.

Expand Down Expand Up @@ -1076,24 +1077,41 @@ import of a source, so before importing a session as new it warns once,
naming every such entry, that a session there may be imported again. `task
reconcile` and `task refresh-linkage` leave a reference to it as it is.

Inside a session's directory, an `events.jsonl` or a `session.yaml` that is a
symlink, whatever it points to, or anything but a regular file — a directory,
or a FIFO, which a read would wait on — is not followed either, and the log is
not read. Its row is `tampered` with `reason` `events_not_a_file` or
`yaml_not_a_file`, naming the file; when both files are such, either may be
given. A `session.yaml` that is a regular file is still read beside an
`events.jsonl` that is not, so its version decides as above — `unsupported`
at a format major other than 0 or at a newer `0.x.y` — and
`session_yaml_invalid` is reported as on any row. Like a `symlink` row, such a
row judges the store's layout, not the log, and putting the file back as a
regular file repairs it. `session rechain` skips such a session with reason
`events_not_a_file` or `yaml_not_a_file` and exits non-zero, and an in-place
re-import skips it as a prior whose chain fails verification. What the other
commands do with it is in
[workspace](workspace.md#a-sessions-files-are-not-followed).

The workspace's own `.basou/sessions` is not followed either: when it is a
symlink or a file, the commands that read or write the workspace's sessions
stop with an error saying so — `basou verify` included, which then prints no
array. A federated mirror, another host's store registered in
`~/.basou/hosts.yaml`, is a read-only copy kept outside the store to begin
with, so its `.basou/sessions` may be a symlink; an entry in it that is not a
directory is still not followed.
directory is still not followed, and neither is a session file in it that is
not a regular file.

Each row is an object with these fields:

| Field | Value | Present | Meaning |
|---|---|---|---|
| `session_id` | string | always | the session's full id (the entry's name), also when `--session` was given a prefix |
| `status` | string | always | the verdict, from the table above |
| `event_count` | non-negative integer | always | the complete (newline-terminated) lines of `events.jsonl`, as this basou splits the file; an unterminated tail is not counted, and a missing or empty file counts `0`. On an `unsupported` row it counts lines, not necessarily the newer writer's events; on a `symlink` or `not_a_directory` row it is `0`, as nothing is read |
| `event_count` | non-negative integer | always | the complete (newline-terminated) lines of `events.jsonl`, as this basou splits the file; an unterminated tail is not counted, and a missing or empty file counts `0`. On an `unsupported` row it counts lines, not necessarily the newer writer's events; on a `symlink`, `not_a_directory`, `events_not_a_file` or `yaml_not_a_file` row, and on an `unsupported` row whose `events.jsonl` is not a regular file, it is `0`, as the log is not read |
| `reason` | string | always on `tampered` and `incomplete`; today on no other status | what broke, from the table below; read it together with `status`, never instead of it |
| `line` | positive integer | when one line of the log broke | the 1-based number of the first line that broke; for `torn_tail`, the number the unterminated tail would have |
| `session_yaml_invalid` | `true` | when `session.yaml` exists but does not load as a whole document | described above; the field is left out rather than set to `false` |
| `session_yaml_invalid` | `true` | when `session.yaml` exists but does not load as a whole document | described above; the field is left out rather than set to `false`, and where verify does not read `session.yaml` — on a `symlink`, `not_a_directory` or `yaml_not_a_file` row, and on an `events_not_a_file` row whose `session.yaml` is not a regular file either |

`reason` takes these values today. `unsupported` carries neither `reason` nor
`line` today, also where it stands in for a `tampered` result at a newer
Expand All @@ -1117,6 +1135,8 @@ and never changes whether that status fails the command.
| `yaml_missing` | `incomplete` | no | a chained log has no `session.yaml` |
| `symlink` | `tampered` | no | the entry named as the session is a symlink, whatever it points to; it is not followed |
| `not_a_directory` | `tampered` | no | the entry named as the session is neither a directory nor a symlink (a file, for instance) |
| `events_not_a_file` | `tampered` | no | the session's `events.jsonl` is a symlink, whatever it points to, or anything but a regular file (a directory, a FIFO); it is not followed |
| `yaml_not_a_file` | `tampered` | no | the session's `session.yaml` is a symlink, whatever it points to, or anything but a regular file; it is not followed |

**Reading the output.** The shape changes only as
[compatibility](compatibility.md#basou-verify-verdicts-may-gain-values-on-the-failing-side)
Expand Down
58 changes: 58 additions & 0 deletions docs/spec/workspace.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,64 @@ through it:
- `orient`, `handoff generate`, `report generate` and `basou view` leave it
out of what they show.

### A session's files are not followed

basou writes a session's `events.jsonl` and `session.yaml` as regular files
and never as anything else. When either of them, inside a session's
directory, is a symlink, whatever it points to, or anything other than a
regular file — a directory, or a FIFO, which a read would wait on — it is not
followed: no command reads through it or waits on it, and the commands that
add to an existing session or finalize one write nothing through it. This
holds for a federated mirror's sessions too, although its `.basou/sessions`
may be a symlink. An absent file is not refused: a session with no
`events.jsonl` has no events, as before.

- `basou verify` reports the session `tampered` with `reason`
`events_not_a_file` or `yaml_not_a_file` — or `unsupported`, when only the
`events.jsonl` is such and the `session.yaml` was written by a newer basou —
and `session rechain` skips it with reason `events_not_a_file` or
`yaml_not_a_file` and exits non-zero; see [schemas](schemas.md).
- The commands that read the session by its id stop with `<file> of session
<id> is not a regular file; a symlink or anything else there is not
followed`, before they record anything: `session show`, `approval show`,
`approval approve` and `reject` (which leave the approval pending), and the
commands that record into an existing session or finalize one — `session
note`, `decision record`, a `task` command attaching to a session, and
`exec` and `run` for the session they started, which they check at each
event they record and when they finalize it. `basou view`'s session page
answers `404` with that message for a `session.yaml`; for an
`events.jsonl` it shows the session with an empty timeline marked as
degraded, as for any log it cannot read.
- The commands that load sessions to list or summarize them leave out a
session whose `session.yaml` is such, with a warning naming its full id:
`session list` says `Skipped <id>: session.yaml is not a regular file (a
symlink or anything else is not followed)`, and `orient`, `stats`, `handoff
generate`, `decisions generate`, `report generate`, `review-gaps` and
`decision gaps` say `Skipped <id>: session_yaml_not_a_file`. A session whose
`events.jsonl` is such is kept, and its events are treated as unreadable, as
for any log that cannot be read (`events_jsonl_unreadable`).
- `import` cannot tell whether a session whose `session.yaml` is such holds an
earlier import of a source, so before importing a session as new it warns
once, naming every such session, that a session there may be imported
again. An in-place re-import of a session whose `events.jsonl` or
`session.yaml` is such is skipped, as one whose prior chain fails
verification. `task refresh-linkage` leaves a link to a session whose
`session.yaml` is such as it is.
- `basou board measure` counts such a session in `integrity` as `basou
verify` judges it. Each other section is not measured when a file it reads
is such: `trail`, `review_gaps` and `effort` read the `events.jsonl`, and
`review_gaps`, `freshness` and `effort` the `session.yaml`.
- In the SDK, `listSessions` and `getSession` leave out a session whose
`session.yaml` is such, as one whose `session.yaml` cannot be read, and
report it to `onDiagnostic`; `readEvents` and `streamEvents` still read the
`events.jsonl` of such a session. For an `events.jsonl` that is such, they
return no events and report it to `onDiagnostic` as an unreadable log
(`events_jsonl_unreadable`), the words `stats` and the renderers use for
it.

Like the checks of the store's directories, this detects a file already in
such a form; it does not race-proof the filesystem.

### tasks/ details

- Listing tasks reads `.basou/tasks/index.json` (a small JSON cache of
Expand Down
Loading
Loading