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
87 changes: 87 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,93 @@
All notable changes to **basou** are recorded here. The project follows
[Semantic Versioning](https://semver.org/spec/v2.0.0.html) starting with v0.1.0.

## Unreleased

### Added

- **The board page draws each repo's files in its repos table**, after the
commits, on the board measured on the spot and on a record's page: the
files a measure sees in the working tree, as `repos[].files` counts them.
The measurement and every version of the record held them, and no table
drew them. One not measured is drawn as not measured.
- **The board page says under Claude's and Codex's time that each is a
union**: the time two sessions of the tool overlap counts once, so either
is less than the sum of its sessions' times when they overlap, as the
active time beside them is. `no Codex session` is drawn as before.

### Changed

- **`basou board measure` leaves out of `repos[].files` a symlink that
leaves the repository.** One such symlink in a repo's working tree,
tracked or untracked and not ignored, as an agent's instruction file
linked in from another repo is (`AGENTS.md -> ../<repo>/AGENTS.md`, with
`CLAUDE.md -> AGENTS.md` beside it), made `files` null at every
measurement, with `'AGENTS.md' is a symlink to outside the repository`,
and a built-in value takes no `exclude`. `files` now counts the other
files, and is a number on a repo that holds such symlinks. A path that
cannot be read, or a directory git could not list in full, still makes it
null with its reason, and a measure the board declares (a `file_count` of
`**`) is still not measured for such a symlink, which its `exclude` can
leave out. A symlink whose end cannot be reached (a directory on the way
that may not be entered) is left out too when its text, or that of a link
in the repository it leads to, leaves the repository. The `repos` method
is now 3, so on every board the next measure fires the axis review's
trigger (e) once, as a raised method does, and the next record marks the
repos section `method_changed`.
- **`basou board record` refuses an `http` value's `final_url` that has a
user, a query, a fragment or a hidden character, and the script
`basou board guide` writes leaves them out.** Behind a sign-in (a redirect to a login page) the URL
curl ended at carries a token signed anew each time: the observation
showed as moved at every record, and the record, kept in git, held a token
a secret scanner may flag. The script prints `final_url` as that URL's
scheme, host (with a port that is not the scheme's own) and path, and a
null with the error `the URL curl ended at is not an absolute http or
https URL` when it is not one. Record refuses a `final_url` that holds a
`?` or a `#`, a user name or a password, or a character a terminal hides
(as it refuses one in a string of the other kinds):
`observed.shop.value.final_url: must be an absolute http or https URL with no user, query, fragment or hidden character, received "https://example.com/login?token=..."`.
The script prints the URL as `new URL` reads it, so on the next record an
observation shows as moved once wherever the `final_url` it held differs
from that: a query, a fragment (curl keeps one of the declared URL) or a
user dropped, a port that is the scheme's own (`:443`) dropped, a scheme
or host in capitals made lower case, a host outside ASCII in its ASCII
form (`xn--`), a character such as `{` escaped. A token in the path
itself (`/login;jsessionid=...`) is kept. Records already written are not
checked again, and the board page draws an `http` value they hold as its
parts as before.
- **`basou board guide` asks for every `manual` observation of a record in
one message**: for each key, what the operator can do as it is written
(the line to run and where, or the page to open) and what to send back,
with a line that answers several keys given once, naming the keys it
answers. A board of ten `manual` observations that one query answers had
the agent ask ten times. The operator's turns in the guide list them as
one item, with each key under it.
- **`basou board guide`, and the comment `basou board init` prints above
`milestones`, say a milestone is a day on which something happened** (a
release, a launch, the start of the work), never a plan or a target date.
They said only what was reached, and also that a board moved from another
keeps its milestones, which a start among them did not fit. A moved board
keeps the milestones it had that happened, and leaves out those that were
only planned.

### Fixed

- **The board page says what its per-day time is divided by.** It is the
active time over the days from the start to today, both included (the
rows of the daily chart), one more than the elapsed days in the tile
beside it, so a page showing `Elapsed 105 days` and `Per day 3h 22m` for
358h 17m could not be checked by hand. The tile now says `over 106 days,
today included`, beside the time a day worked. `docs/spec/board.md`
defines it.
- **`basou board guide` quotes an observation's `Source:` as its command
does.** A `github_check` source wrote the check name as it is
(`-f check_name=Workers Builds: my-app`), which a shell reads as other
words than the command's `-f check_name='Workers Builds: my-app'`. Each
argument of a source is now quoted when a shell would read it otherwise,
as the command quotes it, and a plain name (`owner/repo`, `quality.yml`,
`main`) is written as before, so the sources of existing boards change
only where a name needed quoting.

## 0.72.0 — 2026-10-20

### Added
Expand Down
83 changes: 62 additions & 21 deletions docs/spec/board.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
>
> This page describes the shapes of basou 0.71.0: `board_version` 3 and
> `record_version` 3, with what `basou board record` refuses of an
> observation's value as of basou 0.72.0. When a later release changes a
> observation's value as of basou 0.73.0. When a later release changes a
> shape, it raises the version and this page with it.

This page says what each key of these shapes holds, which keys are required,
Expand Down Expand Up @@ -251,7 +251,8 @@ previous record and usable by a ratio or a total.
- A symlink is followed when it ends at a file inside the repository. One
that ends nowhere or at a directory is not a file and is left out, as a
submodule is; one that leaves the repository makes any measure that matches
it not measured.
it not measured (a repo's built-in `files` leaves it out instead, see
[`repos`](#52-repos)).
- basou measures with git and reads files, with no shell. It does not fetch,
write a lock or start a file system monitor.

Expand Down Expand Up @@ -525,10 +526,12 @@ never part of a command.
U+200B to U+200F, U+2028 to U+202E, U+2060 to U+2069, U+FEFF); a number of
a kind above that is not
a whole number from 0 to 9007199254740991; and an `http` value out of its
shape ([below](#http)). What a string says is not held to a grammar (a
version's, or the words GitHub uses), so a new word is never refused. A
`manual` value, and a null, are not checked. Only a board that declares
observations has them checked ([3.2](#32-observed)).
shape ([below](#http)), which from basou 0.73.0 includes a `final_url`
with a user, a query, a fragment or a character a terminal hides. What a
string says is not held to a
grammar (a version's, or the words GitHub uses), so a new word is never
refused. A `manual` value, and a null, are not checked. Only a board that
declares observations has them checked ([3.2](#32-observed)).

The keys:

Expand Down Expand Up @@ -587,14 +590,32 @@ The value recorded:
|---|---|---|
| `status` | always | a whole number from 100 to 599: the status of the last response |
| `redirects` | always | a whole number from 0 to 10: how many redirects led to it |
| `final_url` | always | an absolute `http://` or `https://` URL: the URL it ended at |
| `final_url` | always | an absolute `http://` or `https://` URL with no user, query, fragment or hidden character: the URL it ended at, as its scheme, its host (with a port that is not the scheme's own) and its path, as `new URL` reads them |
| `words` | with `words` | a list of `true` or `false`, one for each word, in the order declared: whether the body holds it |
| `count` | with `count_pattern` | a whole number from 0 to 9007199254740991: how many times it matches the body (0 when it does not) |
| `capture` | with `capture_pattern` | a string: what the first group holds in the first match, as it is, spaces and all; null when nothing matches or the group takes no part |

From basou 0.72.0, record refuses an `http` value with a key the
observation does not declare a read of, or one it declares left out, and a
read that is not null when the status is not 2xx.
read that is not null when the status is not 2xx. From basou 0.73.0, it
refuses a `final_url` that holds a `?` or a `#`, a user name or a password,
or a character a terminal hides (as for a string of the other kinds), and
the guide's command leaves them out of the URL curl ended at: behind a
sign-in (a redirect to a login page) they can carry a token made anew each
time, which would show the observation as moved at every record and put the
token in a record kept in git. A record written before keeps the value it
holds, and the board page draws it as its parts.

- **The command reads the URL as `new URL` does**: a scheme and a host in
lower case, a host outside ASCII in its ASCII form (`xn--`), a port that is
the scheme's own left out, and the path with its dot segments resolved and
what a URL may not hold as it is escaped (`%2e%2e` or a backslash in the
path curl ended at reads as a step up or a `/`). The value is the same for
the same URL.
- **A token in the path itself is kept**: a session id a server writes into
the path for a client that keeps no cookie (`/login;jsessionid=...`) is
part of the path, so the observation shows as moved at each record and the
token is in the record. Make such an observation `manual` instead.

- **A 4xx or a 5xx is a value**, not an observation not made: the status is
what was observed.
Expand All @@ -608,13 +629,13 @@ read that is not null when the status is not 2xx.
fails (the name does not resolve, TLS fails, 20 seconds pass, or it takes
more than 10 redirects), when its status is not one of an HTTP response
(100 to 599, such as a server answering 600, which curl prints as it is),
or when what curl left cannot be read. From basou 0.72.0 the guide's
command prints the whole entry for the record's input on one line, with
what a terminal would act on or hide escaped: the value, or the null and
the error. It gives the null,
rather than count, at an empty match the declaration could not refuse: a
`count_pattern` that matches the empty string only next to some text, as
`\b` or a lookaround can.
when the URL it ended at is not an `http://` or `https://` URL (from
basou 0.73.0), or when what curl left cannot be read. From basou 0.72.0
the guide's command prints the whole entry for the record's input on one
line, with what a terminal would act on or hide escaped: the value, or the
null and the error. It gives the null, rather than count, at an empty
match the declaration could not refuse: a `count_pattern` that matches the
empty string only next to some text, as `\b` or a lookaround can.

The keys of the value are basou's words, so the check of the record's input
for the names of other workspaces ([3.1](#31-how-it-is-checked)) looks at
Expand Down Expand Up @@ -782,9 +803,11 @@ A mapping from an observation's key to what was found:
It is refused where it is not a JSON value: it nests more than 100 deep,
holds a number too large to be held, or has a key named `__proto__`. From
basou 0.72.0, when the board declares observations, it is refused too where
it is not in the shape of its kind ([2.7](#27-observe)); a `manual` value
and a null are not checked. With none declared, the keys of `observed` are
free and their values are not checked. A refusal says where in the value
it is not in the shape of its kind ([2.7](#27-observe)), and from basou
0.73.0 where an `http` value's `final_url` has a user, a query or a
fragment ([`http`](#http)); a `manual` value and a null are not checked.
With none declared, the keys of `observed` are free and their values are
not checked. A refusal says where in the value
(`observed["site.top"].value.status`); one of what a key holds shows what
it received, as up to 60 characters of its JSON (an escape counted as one)
with what a terminal would act on or hide escaped. A value that is not a
Expand Down Expand Up @@ -975,7 +998,9 @@ is raised only when the same input would give a value of it another way,
never for an added field or a new key of the declaration. A method that
differs from the previous record's fires the axis review's trigger (e), and
the values of that section in `diff` are marked `method_changed`. In 0.71.0
they are `portfolio` 1 and every other section 2.
they are `portfolio` 1 and every other section 2. From basou 0.73.0 `repos`
is 3: its `files` leaves out a symlink that leaves the repository
([5.2](#52-repos)).

### 5.2 `repos`

Expand All @@ -989,13 +1014,25 @@ A list, in the manifest's order:
| `branch` | the branch HEAD is on |
| `last_commit` | HEAD's committer time as git recorded it: ISO 8601 with its offset |
| `commits` | the commits reachable from HEAD, merges and all |
| `files` | the files a measure sees in the working tree (tracked, and untracked but not ignored, on disk): a `file_count` of `**` |
| `files` | the files a measure sees in the working tree (tracked, and untracked but not ignored, on disk), as a `file_count` of `**` counts them, less each symlink that leaves the repository (below) |
| `uncommitted` | the paths `git status` names: each untracked file on its own, not its directory, and a rename as the two paths it touches |
| `base` | the branch of origin HEAD is compared with, as `origin/<branch>`: the one `refs/remotes/origin/HEAD` names, or `origin/main` when it names none |
| `behind` | the commits on `base` that HEAD does not have, as of the last fetch |
| `ahead` | the commits HEAD has that `base` does not, as of the last fetch |
| `fetched_at` | when this worktree's `FETCH_HEAD` was last written, in UTC (below) |

- **`files` leaves out a symlink that leaves the repository**, such as an
agent's instruction file linked in from another repo (tracked, or
untracked and not ignored): what it ends at cannot be told to be a file
without looking outside the repository, and a built-in value takes no
`exclude`. When what it ends at cannot be reached (a directory on the way
that may not be entered), it is still left out when its text, or that of
a link in the repository it leads to, leaves the repository. A
`file_count` of `**` is not measured for one. A path that cannot be read,
or a directory git could not list in full, still makes `files` null, with
the reason at `repos[<path>].files`. Up to `repos` method 2 (before basou
0.73.0), such a symlink made `files` null too, with the reason `'<path>' is
a symlink to outside the repository`.
- **An untracked directory counts every file in it**, in `files` and in
`uncommitted` alike, and so does any measure whose globs reach it: what a
repo holds but does not own (a build's output, a tool's cache) is counted
Expand Down Expand Up @@ -1109,7 +1146,7 @@ counts.
|---|---|
| `start` | the declared `start` |
| `time_zone` | the declared time zone or this host's, as `Intl` names it (a declared `utc` is held as `UTC`; the record's `declaration` keeps it as written) |
| `elapsed_days` | the days from the start to today. **Not in the digest** |
| `elapsed_days` | the days from the start to today: today less the start, 0 on the first day. **Not in the digest** |
| `active_ms` | `{ union, claude, codex }`: active time in milliseconds, every session's intervals merged, Claude Code's among themselves, and Codex's among themselves |
| `output_tokens` | the output tokens of the sessions that recorded them (reasoning not included) |
| `sessions_without_tokens` | the Claude Code and Codex imports that recorded no tokens, so that `output_tokens` is a floor while this is above 0 |
Expand All @@ -1123,6 +1160,10 @@ counts.
sessions cannot be read whole.
- `active_ms.codex` is null **with no entry** when there is no Codex session
at all, where 0 is a Codex that did nothing.
- **Per day**, on the board page, is `active_ms.union` divided by the rows of
`daily`: the days from the start to today, both included, which is one more
than `elapsed_days` (on the first day, the time so far is the day's). The
page says what it divided by.
- A repo's `commits` is null, with an entry at `effort.commits[<path>]`, when
its history is not known.

Expand Down
Loading
Loading