This document defines what basou commits to under semantic versioning, so that adopters know which surfaces they can build on and which are still internal.
basou is pre-1.0 today. The 0.x line is being driven toward a 1.0
release aimed at broad adoption — the point at which the guarantees below are
formally committed. This document describes the policy that 1.0 freezes; it is
written before the freeze because parts of it (notably the on-disk format gate)
cannot be retrofitted onto an already-frozen contract.
At 1.0, semantic versioning applies to exactly three surfaces:
- The
basouCLI — the set of commands and subcommands, their flags, exit codes, the documented--jsonoutput shapes of the commands that offer one, and the documented JSON input shapes of the commands that read one from stdin or an input-file flag (today:basou decision capture,basou review record, and the envelope ofbasou session import, whose version follows its own rule). An input shape is a contract for the same reason an output shape is — something else produces it, and tightening what is accepted breaks that producer exactly as surely as dropping a field breaks a consumer. Accepting MORE is additive; accepting less is not. Forbasou decision captureandbasou review record, the documented shape is theInput formatsection of the command's--help, which is contract rather than presentation: the fields, their JSON types, which are required, and the values a field enumerates. Whether areposentry ofbasou review recordnames a repository root on the machine is judged by the rulebasou review-gapsbinds a record with, and may change with that rule. - The
@basou/sdkpackage — its exported read-only API for reading a workspace's provenance. - The
.basou/on-disk format — the durable file schemas (manifest, session, event, approval, task) and the JSON Schemas published alongside them.
Within a 1.x line these surfaces change only additively — a new command,
a new optional flag, a new optional field, a new SDK export, an added field in a
--json payload. Removing or changing the meaning of anything on a guaranteed
surface is a breaking change and requires a major bump (2.0). The sections
below, and the extension rules of schemas
§7.3
for the on-disk format, state the exceptions to that rule and what bounds each
of them.
Within 1.x a flag may come to accept a value it refuses today, and that is
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, 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 split of the
non-zero values
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
file basou reads its input from — --file on basou decision capture, basou review record and basou note, --from-file on basou task new and basou session note, and --from on basou session import. Each of them refuses
- today, and an input-file flag added later either refuses it too or reads
standard input for it from the start. If one comes to accept it, - will mean
standard input, never a file named -. A file named - is read by naming it
./-.
basou --version prints the semver version as its first whitespace-delimited
token, and that token is guaranteed. Everything after it identifies the build
— the commit it was compiled from, and that commit's date — and may change at
any minor, including gaining or losing fields.
The distinction is worth stating because the output is scraped: a release check
that reads the first token keeps working across 1.x, and one that compares the
whole line to a bare version string does not. Compare the first token.
The build identity is there because a version number cannot answer the question people actually ask when something behaves unexpectedly — which build is this. Two builds of one version differ only by commit, and that is the case that has actually gone unnoticed in practice.
What an exit code guarantees is exactly this: 0 means the command
succeeded, and any other value means it failed — except for the two kinds of
command below, which follow their own rule. Every other command exits 1 on a
failure it reports, and a signal that ends it is reported the way a shell
reports any process a signal ends (128 plus the signal's number) — except
that basou view and basou refresh --watch take SIGINT and SIGTERM as
the request to stop, and exit 0 on them. The 1 is not promised: a caller
that tests for exactly 1 may see another non-zero value within 1.x.
The hook handlers exit 0 once their command line parses. basou hook stop and basou hook session-start are run by an agent's hook runner, and a
hook that fails must not stop the agent, so once their arguments parse they
exit 0 whatever the payload, the transcript or the workspace, including when
they fail. Their outcome is not reported by their exit code. A command line
that does not parse (an unknown option, a missing value) exits 1 as on any
command, and a signal ends them as it ends any other process.
basou exec and basou run <adapter> pass their child's exit code
through, because a wrapper that changed it would break a script that
branches on the child's own values (grep exits 1 for no match and 2 for
an error, for instance).
- When the child exits with a code, basou exits with that code.
- When the child is ended by a signal, basou exits with
128plus a signal's number: theSIGINTorSIGTERMbasou itself received while the child ran, when one reached it, and otherwise the signal that ended the child. Any other signal sent to basou ends basou as it ends any process, and the run'scommand_executedevent is not written.basou exec --timeoutends the child withSIGTERM, thenSIGKILL. The value is the same on every POSIX platform forSIGHUP(129),SIGINT(130),SIGQUIT(131),SIGKILL(137) andSIGTERM(143). Any other signal gives128plus its number on the platform basou runs on, the value a POSIX shell such assh,bashorzshreports there. Those numbers differ between platforms —138isSIGUSR1on Linux andSIGBUSon macOS — so such a value is comparable only on one platform; the signal names recorded on the run'scommand_executedevent (signal,received_signal) say which signal it was. - A signal Node has no name for —
SIGEMTon macOS, the real-time signals on Linux — reaches basou as if the child had exited0, and is outside the promise above. Today basou then exits0, records anexit_codeof0with nosignal, and marks the session completed. None of that is promised: a later basou, or a later Node, may report such a signal. A reader cannot rule one out from anexit_codeof0. - When basou itself fails, it exits
1— including when it fails after the child has exited, so a child that exited0is then reported as1. A caller cannot tell a basou failure apart from a child that exited1, and should not read1as "the child ran and failed".
The non-zero values of every command except these four and the experimental
commands may be split within a 1.x line, so that a failure can say more than that it failed — for instance,
that a command refused its input before writing anything, so resending a
corrected input is safe. The carve-out is bounded by four things:
- A split never moves an invocation across zero. It changes which non-zero
value a failure exits with, never whether it exits
0. - A new value only divides a failure that already exists, and is
documented with the write state it leaves behind: whether the command had
written anything, to the
.basou/store or to any other file, before it stopped. - Values are allocated once for the whole CLI. A new value is between
2and125—126,127and everything above128keep the meanings a shell gives them — and one value means the same thing on every command that uses it. - It applies to the values of the exit code only — not to the CLI's
flags or to any
--jsonoutput shape, which stay under the additive rule above.
An experimental command exits 0 when it succeeds
and 1 on every failure it reports, and takes no value from this allocation.
A signal ends it as it ends any other command.
One thing about stdout is left open: a failed command is not promised to print
nothing. A documented --json shape that a command prints when it exits
non-zero — basou verify --json reporting a tampered session, for instance —
stays under the additive rule like any other. Decide whether a command
succeeded from its exit code, not from whether it printed anything.
The status and reason of a basou verify --json row may gain values within
a 1.x line. The additive rule above names added fields, not added values, and
verify's set of verdicts has to grow as its checks do — signing will need one.
The carve-out is bounded by three things:
- A new
statusvalue always fails the command. It exits non-zero and never means a session passed;verified,unchained,empty,incompleteandin_progressremain the only values that exit0. - A new
reasononly refines an existingstatus, and never changes whether that status fails the command. - A consumer reads a value it does not know as not verified. A consumer
that switches exhaustively on
statusorreasonshould keep a branch for that.
unsupported, for a session a newer basou wrote, was added this way before
1.0. The row, its fields and today's values of both are listed in schemas
§7.5.
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 bounds; the
reason it gains names what is not followed.
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). 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:
- 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.
- The refusal names what it refuses: an error or a warning naming the
entry, or a
tamperedrow whosereasonsays what is not followed. - 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.
basou session import and basou import require the envelope's
schema_version to equal exactly one value, published as the const in
session-import.schema.json. That value may move within a 1.x line, and a
producer holding the previous one is refused.
This is stated rather than left to the general rule, because the general rule
would freeze it until 2.0 and that is not the intent. The envelope describes a
payload basou accepts at a boundary it does not write; when the durable formats
it carries narrow, the envelope narrows with them, and pinning the envelope's
version until a major would mean either a stale envelope accepting payloads the
importer can no longer store, or a major bump for a change nothing on disk
notices.
The carve-out is bounded by three things, and it is only defensible with all three:
- The refusal is loud. A wrong version is an immediate error naming the value received, the value expected and the artifact to read — never a silently dropped or partially imported payload.
- The version is discoverable at runtime.
@basou/coreships the artifact, so a producer can read theconstfrom the installed package instead of hard-coding it. A producer that pins the value by hand has opted out of this. - It applies to the envelope's own version only — not to the CLI's flags,
its exit codes, or any
--jsonoutput shape, which stay under the rules above.
Schemas §7.3
allows two changes to a durable format only behind a version gate: widening a
required field's domain (making it nullable, or giving an enumerated field a
new value), and narrowing one to refuse a set that is empty. Both may be made
within a 1.x line, under that gate.
This is stated rather than left to the general rule, because the general rule
would read "additive" from the side of a consumer of a shape, and what the
format guarantees is a different thing. Across 1.x, a newer basou reads every
document an older one wrote, with the meaning it was written with. The reverse
is not guaranteed — forward tolerance is a design goal (see On-disk format
versioning) — and changes the extension rules
already allow without a gate lose lines in an older reader just as a widened
value does: a new event type, and a new optional field on an event variant
that rejects unknown keys, are both dropped by a basou that does not know them.
Forbidding only the gated changes would therefore not protect an older reader.
It would close the one route that ships a read rule.
The carve-out is bounded by five things, and it is only defensible with all five:
- The format's version moves. Its
schema_versionis bumped, its published$idmoves with it, and the artifact the previous$idserved is kept underschemas/retired/. - A read rule that interprets a value is code, not only prose. When the
rule says how to read a stored value, rather than that nothing already
stored changes, it has one implementation, exported from
@basou/coreand re-exported from@basou/sdk, asreadObservedDurationis forcommand_executed.duration_ms. - No value already on disk changes meaning. A bump may only assign a meaning to a value the field could not hold before.
- The changelog says what an older reader does. The change is listed as breaking to that format, and names the releases of basou that cannot read what it newly writes and what they do instead: skip the line or the document, or fail the command that reads it.
- It applies to the on-disk format, to the types
@basou/sdkre-exports from it, and to a--jsonpayload that prints stored values as they are stored (basou session show --jsonprints a session's events, for instance) — not to the CLI's flags, its exit codes, the rest of any--jsonoutput shape, or the SDK's functions. A function whose signature names a format type (readEventsreturnsEvent[]) carries the change through that type and is otherwise unchanged.
The consequences are accepted rather than avoided. A union type the SDK
re-exports from the format (the change_type of FileChangedEvent, for
instance) can gain a member at a minor, and a field can become nullable; a
consumer that checks such a type exhaustively should keep a branch for a value
it does not know. And a reader older than the basou that wrote a store skips or
fails on what it cannot read: keep @basou/sdk at least as new as the CLI that
writes the store it reads, and upgrade together every host that shares a store
through ~/.basou/hosts.yaml.
basou review-gaps and basou decision gaps are advisory surfacers: they
answer "what looks like it was missed", they write nothing, and they enforce
nothing. They sit on the guaranteed CLI surface — the commands and their flags
stay under the additive rule — with one carve-out for the --json payload:
- The payload's field set and the meaning of its counts may change within a
1.xline. What a surfacer looks at is an ongoing judgement about what reads as a gap — a population boundary, a ground for excluding an entry, a caveat about what could not be read — and freezing today's answer to2.0would mean either never improving it or bumping the major to do so. - What is guaranteed is that the payload stays self-describing. Every
boundary the run applied is reported in the payload itself (for instance
scope), so a consumer reads the run's own account of what it checked rather than assuming last release's rule. A count never silently changes meaning while keeping its name: a ground that changes gets a new name. - It applies to these commands'
--jsonpayloads only — not to their existence, their flags, or their exit codes, which stay under the rules above.
A consumer that needs a frozen shape should read the store through
@basou/sdk and apply its own rule.
For an imported session, part of session.yaml's related_files is observed
from git rather than read from the transcript (schemas
§5.2). Deciding which of a repository's commits are the
session's own is an ongoing judgement that rests on how git records its
operations: which reflogs are read, which of git's reflog messages count as a
commit being created, the cases in which no limit is applied, and the limits
§5.2 lists. That attribution may change within a 1.x line.
- What is guaranteed is the six properties §5.2 lists — five things the observed files never contain and one thing they always do — and a change to the attribution keeps all six.
- A change to the attribution is listed in the changelog. The stored list does not record which release derived it, so the changelog is the only place that says a list may differ because the rule changed rather than the work.
- It applies to the observed half of
related_filesonly — not to the paths taken from the transcript's own tool calls, not tofile_changedevents, and not to any other field ofsession.yaml.
Every error class @basou/sdk defines extends BasouSdkError, and today each
of them is a refusal: an ambiguous id prefix, a path with no usable workspace,
or a directory of the store the SDK does not read through. What a caller can
rely on is what it reads off such an error: its class, tested with
instanceof; its name, the name of the class as the SDK exports it,
which the SDK sets from a string so that a build that renames classes does not
change it; the fields the class documents (.root, .input); and which
reads throw it, and when, as the class and Workspace document it. Every
other failure propagates from @basou/core or its dependencies as it is — a
malformed record, a record a newer basou wrote, an I/O failure — and its class
and message are not covered.
The classes are nominal to TypeScript. Only an instance of a class or of one
of its subclasses is assignable where the class is typed — not a sibling, a
parent, a plain Error or an object literal of the same shape — so a field
added to a class is additive like any other new field. Two installed copies of
@basou/sdk at different versions therefore have distinct error types, as
their classes are already distinct at runtime: an error from one copy is not an
instanceof the other's class. A library that takes or returns SDK errors
should declare @basou/sdk as a peer dependency, so that it shares the
application's copy.
Four things about an SDK error are not guaranteed, and may change within a
1.x line:
- The message. It is written for a person to read and may be reworded at
any release. A
StoreUnsafeError's message is the one@basou/coregives, and core is not a guaranteed surface. Tell errors apart by class and by documented field, never by matching the message. - The constructors. An SDK error is for the SDK to throw and a caller to
catch; constructing one is not covered. A constructor may gain a required
parameter — when a class gains a field the SDK fills, for instance — and a
test that stands in for the SDK by constructing its errors has to follow
that change. Code that holds a class only to test
instanceofagainst it should type it by what it constructs, asabstract new (...args: never) => StoreUnsafeError, rather than astypeof StoreUnsafeError, which also names the constructor's parameters. - The
cause. When an SDK error carries the error it was made from, that is oncause, for diagnosis. Whether there is one, and what it is, may change: the SDK may come to detect a condition itself rather than retype core's error. - Extending a class. A consumer's subclass of an SDK error class is not covered: a field the SDK adds can collide with one the subclass declares, and the subclass calls a constructor that is not guaranteed.
The errors may grow. Within a 1.x line:
- A failure that propagates today may come to have an SDK class of its
own, extending
BasouSdkError— a record a newer basou wrote, for instance. - A condition may come to throw a subclass of the class it throws today,
for part of that condition — a
.basou/that is a symlink, apart from one that is missing, for instance. - A store the SDK comes to read gets a
StoreUnsafeErrorsubclass of its own, and a read may begin to throw for a store it did not read before. - A read may come to refuse, or to stop following, an entry it reads
through today when the entry is in a form basou never creates: a symlink
under
.basou/outside a view placed there, something other than a directory where basou makes a directory, or something other than a file where a file belongs (an approval file that is a directory, for instance) (workspace). A refused directory of a store throws that store'sStoreUnsafeError. An entry that is not followed is treated as a session entry that is not a directory is today: it is left out of a list and looked up asnull, and a prefix it shares with a record is ambiguous, so a prefix lookup of that record throwsAmbiguousIdError. Where a record has two entries, as an approval has a pending and a resolved file, the one that is not followed is passed over and the record is read from the other.
The carve-out is bounded by three things:
- Nothing changes on a layout basou creates. Items 1 and 2 only retype an
error a call already throws. Items 3 and 4 apply only to a layout basou
never creates: on a workspace basou laid out, a read does not begin to
throw a
StoreUnsafeError, and does not begin to leave an entry out. - A new class is nominal from its first release. It extends the class
whose condition it divides,
StoreUnsafeErrorfor a store, orBasouSdkError; it carries a brand of its own, so that no class it does not extend is assignable to it; and itsnameis its class's name, set from a string. - The changelog lists each change, and the class and
Workspacedocumentation list which reads throw which class.
A consumer handles the classes it knows and keeps a branch for the rest.
instanceof keeps matching a subclass added later, so catching
StoreUnsafeError handles every store's refusal, including a store added
later; a switch on name does not, so it needs a default branch, for a class
it does not know and for a subclass of one it does.
-
@basou/coreis published on npm but is not a semver-guaranteed API. Core exists so the CLI and SDK can build on it and so advanced consumers can embed basou, but the bulk of its exports are CLI-internal planning helpers (for example the archive, gitignore, and retrofit planners) that would become a permanent constraint if frozen. Depend on@basou/sdkfor a stable read API. A named subset of core may be promoted to the guaranteed surface in a future release; until then, treat core as internal. -
The CLI's human-facing prose is presentation, not contract. The orientation narrative, nudges, and other rendered prose may be refined at any time — do not scrape it. Machine consumers have two covered read paths instead:
@basou/sdk, and the--jsonoutput of the commands that offer it, other than the experimental commands below (part of the guaranteed CLI surface above). -
Experimental commands are outside every guarantee in this document, except the ones stated here. Each row of the table below names a top-level command. It and every subcommand under it, including one added later, are experimental; a subcommand or flag added to a command that is not in the table is guaranteed from its first release. An experimental command may change its flags, its output and input shapes, and the files it reads or writes, or disappear, at any release. This overrides the deprecation policy, including the
0.xfreeze. What still holds:- Its exit codes follow the rule stated for experimental commands under exit codes, which replaces the allocation for them.
- The invariants.
- It may read the
.basou/store but writes nothing under.basou/. - Only an invocation of a listed command is exempt. A guaranteed command
keeps every guarantee whatever an experimental command has written: it
does not fail, exit differently, or change a documented
--jsonshape because such a file exists, is missing or has changed shape. It may show what one wrote only in prose or on abasou viewpage.
A command can be experimental only from the release that introduces it; an existing command is never moved onto this list. Promotion is one-way: a command leaves the list in a minor release, and from that release the normal rules apply to it, together with the files and shapes the promoting release names as guaranteed; anything it does not name stays outside every guarantee. A listed command may instead be removed at any release, and its row with it. Removal is not promotion, and a name that returns later is a new command.
--helpmarks each of these commands[experimental]; the table, not the mark, is what decides.Command Experimental since basou board0.65.0 -
basou view's HTTP routes and pages are a local UI, not an API. Read the trail through@basou/sdkor the--jsonoutput of a command that is not experimental instead.
Each durable file carries a schema_version (the manifest additionally records
a basou_version). This tracks the on-disk format major, which is
decoupled from the npm / product version:
- Shipping product
1.0.0does not bump the format major. The format major stays at0. Seeingschema_version: 0.xon a1.0+ install is therefore expected and does not mean the format is unstable. - A MINOR bump within format major 0 can still be breaking, and one has
happened:
0.2.0madecommand_executed.duration_msnullable (see schemas §7.3). So0.xdoes not by itself mean "nothing incompatible has changed since0.1". Read the minor. More such minors may follow within a1.xline of the product, under the bounds in The on-disk format may make its gated changes at a minor. - basou reads format major 0: it accepts any
0.x.yschema_versionand gates a higher / unknown major (1.x.y+) with an explicit "upgrade basou" error rather than a cryptic field-level parse failure. That gate is major-only, so it does not catch a breaking MINOR: a basou at 0.41.0 or earlier rejects a0.2.0event with a nullduration_msand drops the line, losing the whole event rather than reporting an upgrade. Hosts that share a store through~/.basou/hosts.yamlshould therefore be upgraded together. The manifest, task and approval records are loose objects that keep unknown fields when basou rewrites them, so a newer minor's additive fields survive that (basou init --forcereplaces the manifest rather than rewriting it).session.yamlis a loose object too, exceptintegrity, which rejects a key it does not know and so makes the whole session unreadable. Adding a key to it therefore moves the session'sschema_version(schemas §7.3), and abasou verifyfrom0.57.0on reports such a sessionunsupportedrather than tampered (0.56.0and earlier report it tampered). An imported session'ssession.yamlis rebuilt from its source whenever the source grows, keeping onlytask_idandsummary, so a field a newer minor added does not survive a re-import. Events keep an unknown field only insideapproval_requested.action, which passes it through. At the top level, a few event variants are intentionally strict and reject a line carrying a key they do not know, and every other variant accepts the line but drops the unknown field from what it reads — and a re-import writes the events it keeps from what it read. So forward tolerance within major 0 is a design goal, not a per-record guarantee. - The published JSON Schemas for the durable formats carry the matching
pattern(^0\.\d+\.\d+$) in place of an exactconst, so a cross-language validator enforces the same major. (Cache schemas keep an exactconst— see below.)
This gate behavior is itself part of the frozen format contract. It is defined
before the 1.0 freeze because a forward-compatible acceptor cannot be added
after the version is pinned to an exact literal — an old reader would already
reject anything it had not seen.
Migration machinery is deliberately deferred. Only the gate is frozen now; the transform that would carry an old format major forward to a new one is future work, to be introduced when the format first changes incompatibly.
Caches are exempt. Derived cache files are pinned to an exact literal version and rebuilt on mismatch, so they are not part of the forward-compatible durable contract.
For the 0.x line the CLI surface is already treated as frozen — commands and
flags are stable. When a flag becomes obsolete before 1.0, it is kept as a
deprecated no-op (still accepted, prints a warning that it is now ignored)
rather than removed, so an existing script that passes the flag keeps working
instead of erroring on an unknown option. Deprecated no-op flags are removed at
1.0. None of this paragraph applies to the experimental commands listed
under What is not guaranteed.
Example:
basou init --repo-urlbecame a no-op whenproject.repository_urlwas removed from the manifest (a value nothing read and that drifted silently). The flag is accepted-and-ignored through0.xand dropped at1.0.
The same policy covers a field that becomes required on a documented JSON
input shape. Within a 1.x line that is a breaking change and waits for
2.0; the path below is how it is introduced, at a major or before 1.0. It
is warned about for at least one release first — the item is
still accepted and still written, and the full text of the eventual error goes
to stderr — before omitting it becomes an error. The warning release is a
chance to be told, not a guarantee of being told: a producer that upgrades
straight past it, or that never reads stderr, meets the error without having
seen the warning. So a producer should keep its input until the command exits
0. That rule rests only on the exit-code guarantee above, and so it holds
whether or not a warning ever reached the producer. A non-zero exit does not by
itself say that nothing was written.
Example:
"kind"onbasou decision capture. It was optional, and omitting it filed an unfinished direction as a settled decision, which never enters the open-track list: the next decision takes its place, instead of it being held until closed — a failure with no symptom.0.47.0warned on every omission and still wrote the item; from0.48.0, released the next day, it is an error, and omitting it on ANY item refuses the whole batch. Through the warning release an explicit"kind"was carried onto the event, so an item written WITHOUT a declared vessel stays distinguishable from one written with it. Events recorded before0.47predate that distinction: an absentkindthere means only that nothing was recorded, not that nothing was declared.
Refusing the whole batch, rather than writing the valid items and reporting the
rejected indices, is what keeps the success line honest: a partial write reports
success while holding fewer items than the caller handed over, which is the same
shape of silent miscount the requirement was introduced to remove. It also keeps
the rule consistent with every other validation this command performs — a
malformed field has always refused the batch. Every refusal ends by naming the
number of items NOT written, so it can never be read as a partial success, and a
missing "kind" names the offending indices — up to ten, with any remainder
collapsed into a stated count.
A field that becomes required on one command does not oblige every command that
writes the same event to grow the same requirement. decision_recorded has
three writers — the transcript importer, basou decision capture and basou decision record — and only capture reads a JSON input shape. record's
vessel is the --track flag, which has no way to state "explicitly not a
track"; the importer declares no vessel at all and cannot be given one. The
requirement therefore lands on the input shape. record instead names the
vessel it used on its receipt, which needs no new flag on a surface this line
treats as frozen.
Since this policy was adopted (0.31.0), a flag has also kept what it
accepts, with one exception: - as the file of an input-file flag, refused
before 1.0 so that 1.0 does not freeze it as a file name in the way of
the reservation. Four flags read it as a file named
- until the release after 0.68.4: --file on basou decision capture,
basou review record and basou note, and --from on basou session import. It was refused outright, with no warning release. A warning release
exists so that a caller does not lose input it cannot easily make again; here
the input is a file, which stays on disk and is read as ./-, as the refusal
says. No other value is refused this way before 1.0.
After 1.0, removing or changing the meaning of any guaranteed-surface element
requires a major bump; additions within a line remain backward-compatible.
These are properties of the design, not of any particular release, and are not expected to change across major versions:
- Local-first and zero-network — the workspace trail lives under
.basou/next to your code; optional integrations may also write user-level files (~/.claude/,~/.codex/), and an experimental command may write files beside a file of yours that it reads, never under.basou/(basou board recordwrites arecords/directory beside itsboard.yaml); those files are not part of the trail, though they may hold what the trail holds. Everything stays on-machine — nothing is sent off-machine. - Adopt, not rip — adoption is non-destructive and reversible, and the
adoption / wiring generators (
sync,adopt, gitignore, symlinks) are dry-run-by-default. - Runtime does not depend on an LLM — triggering and derivation use deterministic proxies only.