Skip to content
Open
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
4 changes: 2 additions & 2 deletions okf/DEVIATIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,10 +179,10 @@ the body survives and `_rev` re-derives (§14: a former edit's revHash changes,
the body text does not). Once a bundle has cycled once, every body is already
`base:null`, so further cycles preserve even `_rev`.

**okf §10 `compact` injects the `@auto-okf/store` `initVault` rather than
**okf §10 `compact` injects the `@auto-okf/store` `Bundle.init` rather than
importing it.** Consistent with the rest of `@auto-okf/faces` ("the substrate
is injected — nothing here imports the substrate"), `compact(vault, opts)`
takes `opts.initVault` (the store factory) instead of adding a hard
takes `opts.initBundle` (the store factory) instead of adding a hard
`@auto-okf/store` dependency to the faces package. The CLI (`okf/cli`) wires
the two together. The predecessor is never deleted by compact — its path is
returned for the operator to archive/destroy out-of-band (§10 step 5).
Expand Down
16 changes: 8 additions & 8 deletions okf/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,16 @@ A multi-writer knowledge bundle on
markdown, agents and humans co-writing one vault.

```js
import { initVault } from '@auto-okf/store'
import { Bundle } from '@auto-okf/store'
import { materialize } from '@auto-okf/faces'

const vault = await initVault('./vault') // the op log is the source of truth
const { tag: id } = await vault.createConcept('guides/getting-started', 'guide')
await vault.setField(id, 'title', 'Getting started')
await vault.setBody(id, null, 'Read the [architecture](/guides/architecture) first.')
await vault.update()
await materialize(vault, './bundle') // guides/getting-started.md + index.md + log.md
await vault.close()
const bundle = await Bundle.init('./storage') // the op log is the source of truth
const { tag: id } = await bundle.createConcept('guides/getting-started', 'guide')
await bundle.setField(id, 'title', 'Getting started')
await bundle.setBody(id, null, 'Read the [architecture](/guides/architecture) first.')
await bundle.update()
await materialize(bundle, './bundle') // guides/getting-started.md + index.md + log.md
await bundle.close()
```

Open `./bundle` in any markdown reader and it is a plain OKF bundle;
Expand Down
13 changes: 13 additions & 0 deletions okf/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,19 @@ pnpm add @auto-okf/cli # or run the repo-local bin: okf/cli/bin/auto-okf.js
There is no content-write command: content flows in through `ingest`/`watch`
(edit the markdown) or through the `@auto-okf/store` API (agents).

## Verifying content is in the log

A vault has no privileged bundle directory; the emission manifest
(`.okf-manifest.json`) lives in each target directory, so materializing to
a scratch directory never disturbs another bundle:

```sh
$ mkdir ./scratch && auto-okf materialize ./vault ./scratch
```

An empty scratch dir that fills on materialize is proof the content is in
the log — everything that appears came from it.

## Exit codes

`0` success · `1` operational error (including a governance op that apply
Expand Down
Empty file modified okf/cli/bin/auto-okf.js
100644 → 100755
Empty file.
14 changes: 7 additions & 7 deletions okf/cli/lib/run.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { join } from 'node:path'
import b4a from 'b4a'
import { command, flag, arg, summary, header, footer, bail, validate } from 'paparam'
import { keyspace as K } from '@auto-okf/core'
import { initVault, openVault, joinVault, METADATA_FILE } from '@auto-okf/store'
import { Bundle, METADATA_FILE } from '@auto-okf/store'
import { materialize, createIngestWatcher, wanted, compact } from '@auto-okf/faces'

const TITLE = 'auto-okf — multi-writer OKF bundles on autobee (okf SPEC §13)'
Expand Down Expand Up @@ -295,7 +295,7 @@ async function withVault(dir, opts, fn) {
`'${dir}' is not a vault (no ${METADATA_FILE}); run \`auto-okf init\` or \`auto-okf join\` first`
)
}
const vault = await openVault(dir, opts)
const vault = await Bundle.open(dir, opts)
try {
return await fn(vault)
} finally {
Expand Down Expand Up @@ -329,7 +329,7 @@ async function cmdInit({ args, flags }) {
if (flags.encryptionKey) {
opts.encryptionKey = hexKey(flags.encryptionKey, '--encryption-key')
}
const vault = await initVault(dir, opts)
const vault = await Bundle.init(dir, opts)
try {
printIdentity(vault)
console.log(`storage: ${dir}`)
Expand All @@ -356,8 +356,8 @@ async function cmdJoin({ args, flags }) {
throw new UsageError('a vault key is required to join fresh storage')
}
const vault = pinned && key === undefined
? await openVault(dir, opts)
: await joinVault(dir, hexKey(key, 'vault key'), opts)
? await Bundle.open(dir, opts)
: await Bundle.join(dir, hexKey(key, 'vault key'), opts)
try {
printIdentity(vault)
console.log('hand the writer key to an indexer (`auto-okf add-writer`), then replicate')
Expand Down Expand Up @@ -469,7 +469,7 @@ async function cmdWatch({ args, flags }) {
`'${dir}' is not a vault (no ${METADATA_FILE}); run \`auto-okf init\` or \`auto-okf join\` first`
)
}
const vault = await openVault(dir)
const vault = await Bundle.open(dir)
let watcher = null
let swarmed = false
try {
Expand Down Expand Up @@ -545,7 +545,7 @@ async function cmdCompact({ args, flags }) {
const faceDir = flags.face || `${outDir}.face`
return withVault(dir, {}, async (vault) => {
const res = await compact(vault, {
initVault,
initBundle: Bundle.init,
toDir: faceDir,
vaultDir: outDir,
force: flags.force === true
Expand Down
62 changes: 62 additions & 0 deletions okf/docs/decisions/ADR-001-path-scoped-ingest.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# ADR-001: Path-scoped ingest — `ingest <vault> <bundle> [paths…]`

## Status
Accepted

## Date
2026-07-31

## Context
From the six-writer confusion experiment (AGENT-CONFUSION-LOG.md, finding
F1; log kept outside this repo at
`0x/lpmq/0/elements-of-documentation/`): the CLI's only ingest form scans
the entire bundle. Under concurrent writers this produced a complete
three-perspective incident — one agent refused the command ("to avoid
folding other agents' possibly half-written pending files") and dropped
to the store API; a second ran it and ingested a third agent's pending
file as a side effect ("If the-clerk's file had been mid-write, I would
have ingested a partial draft"); the third then saw its own ingest report
`0 op(s) from 0 file(s)` (see ADR-003).

The concurrent-writer case is in-scope by design: the flagship example is
four agents on one vault.

## Decision
Accept optional trailing bundle-relative paths (or globs) that restrict
the ingest scan set:

```sh
auto-okf ingest ./vault ./bundle drafts/mine.md # only this file
auto-okf ingest ./vault ./bundle # unchanged: full scan
```

A named path that does not exist, or that resolves outside the bundle
root, is an error (exit 1, path named, no ops appended). The zero-path
form is byte-identical in behavior to today — extension, not
modification.

## Alternatives Considered

### Per-file claim/lock protocol in the bundle
Rejected. The experiment recorded zero corruption and zero lock errors
across ~17 concurrent invocations (F6); the failure mode is *selection
and reporting*, not safety. Locks import the coordination complexity the
log exists to avoid.

### Make per-file ingest the default / refuse multi-file sweeps
Rejected. Breaks the documented single-human workflow (edit many files,
ingest once).

## Consequences
- SPEC §9: the ingest scan-set definition gains "restricted to the
argument set when paths are given." §13: CLI surface line updated.
- Agents get concurrent-writer politeness without leaving the CLI (the
workaround today is the store API's `ingestFile`, which one agent had
to discover unaided).

## Acceptance
- [ ] Two files pending; path-scoped ingest of one emits ops for that
file only; the other remains pending-ingest.
- [ ] Path outside the bundle root → exit 1, zero ops appended.
- [ ] Zero-path invocation unchanged (existing invariant suite passes
untouched).
67 changes: 67 additions & 0 deletions okf/docs/decisions/ADR-002-status-command.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# ADR-002: `status` — per-file vault/bundle state porcelain

## Status
Accepted

## Date
2026-07-31

## Context
The strongest cross-agent signal in the confusion experiment (findings
F1/F5): three of six first-contact agents read this repo's *source* to
answer what are all per-file state questions. One diagnosed a
`0 op(s) from 0 file(s)` result by reading `okf/faces/lib/ingest.js`
("the wording gave no hint the file was already ingested vs. never
seen"); another grepped `fsatomic.js` to learn the emission manifest is
per-directory; a third read docs mid-flight to confirm materialize would
not clobber a foreign pending file. Agents source-dive where the surface
is ambiguous; humans mostly won't.

## Decision
Add a read-only command:

```sh
auto-okf status <vault> <bundle> [--json]
```

One line per file, one state from a fixed vocabulary:

| state | meaning |
| --- | --- |
| `in-log` | on-disk bytes match the log's canonical projection (hash match) |
| `pending-ingest` | hand-authored/edited; not yet in the log |
| `dirty` | in the log but on-disk bytes diverge from the projection |
| `absent` | in the log, missing from disk (materialize would restore) |
| `foreign` | not in the log, not manifest-tracked (e.g. another writer's WIP) |

`--json` emits the same as a machine-readable array. Strictly read-only:
appends no ops, writes no files, touches no manifest.

## Alternatives Considered

### Fold flag-queue reporting into `status`
Deferred (open question Q3 in the proposals doc). `flags` already
exists; one command, one question.

### Richer prose messages on existing commands instead
Insufficient alone — messages answer the question for the command you
ran; `status` answers it before you run anything (and ADR-003 improves
the message anyway).

## Consequences
- The state vocabulary becomes contract (Hyrum's law applies to the
`--json` schema especially — it will be the most machine-depended-on
surface in the CLI; review it with API-design care, once).
- SPEC §9 names the state vocabulary (it exists implicitly in the
ingest/materialize rules today); §13 adds the command.
- The scratch-materialize verification idiom four agents independently
invented becomes unnecessary for state questions (still valid for
proof-from-log; see ADR-007).

## Acceptance
- [ ] Reproduce the swept-writer incident: write a file, have a second
process ingest+materialize it, run `status` → `in-log`, not
`pending-ingest`.
- [ ] A foreign mid-write file reports `foreign` and is not ingested.
- [ ] `--json` parses and is identical across two consecutive runs with
no intervening ops.
48 changes: 48 additions & 0 deletions okf/docs/decisions/ADR-003-ingest-report-split.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# ADR-003: Ingest report distinguishes "already in log" from "nothing found"

## Status
Accepted

## Date
2026-07-31

## Context
Confusion finding F1 (victim side): `ingest complete: 0 op(s) from 0
file(s)` is emitted both when the bundle has no candidates and when every
candidate is already converged (hash match against the emission
manifest). An agent whose brand-new file had been swept into the log by a
concurrent writer's whole-bundle ingest received this message and "had to
read source to distinguish" already-ingested from never-seen — the
scanner computes the hash-match skips either way; it just doesn't say so.

## Decision
Report the skip count:

```
ingest complete: 0 op(s) from 0 file(s); 7 file(s) already up to date
```

Message-text change only; exit codes untouched. Land now while the
message is young — the packages are unpublished, so external
message-parsing dependents are ~nil, and ADR-002's `--json` is the
durable machine answer regardless.

## Alternatives Considered

### `--json` on ingest instead of rewording
Not instead — the human/log-reading case deserves the plain sentence;
structured output arrives with ADR-002 where the evidence is.

### Leave it to `status` (ADR-002)
`status` answers the question when you think to ask it; the ingest
report is what you are already looking at when the confusion strikes.

## Consequences
- SPEC §9 ingest-reporting sentence updated.
- The one-line report becomes slightly longer only when skips occurred.

## Acceptance
- [ ] The swept-writer scenario re-run prints a nonzero
"already up to date" count.
- [ ] An empty bundle prints no skip clause (or "0", decided once,
consistently).
63 changes: 63 additions & 0 deletions okf/docs/decisions/ADR-004-materialize-exit-codes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# ADR-004: Materialize exit-code taxonomy — 0 clean / 3 pending-only / 1 failure

## Status
Accepted

## Date
2026-07-31

## Context
Confusion finding F3: a materialize that wrote everything the caller
owned and correctly left a concurrent writer's pending file in place
exited 1 — the agent logged that it "momentarily looked like a failure."
Under multi-writer use, a *normal* state (someone else mid-authoring)
surfaces as failure on a *healthy* operation.

The implementation is a single line — `return report.ok ? 0 : 1`
(`okf/cli/lib/run.js`, the N3 comment) — and the amendment window was
verified open (proposals doc Q2, resolved 2026-07-31):

- Library tests (`okf/test/faces-preservation.test.js`) assert on the
`report.pendingIngest` object, not the exit code — unaffected.
- The CLI test asserts exit 1 only for "not a vault" (real failure) and
exit 0 for clean materialize — both preserved.
- No test or harness code anywhere pins exit 1 for the pending case.
- The packages are unpublished; external Hyrum exposure is ~nil.

## Decision
Three named outcomes, in the SPEC:

| exit | meaning |
| --- | --- |
| 0 | projected everything; no pending files encountered |
| 3 | projected everything owned; N pending-ingest file(s) left in place — stderr lists them prefixed `notice:`, not `error:` |
| 1 | real failure (I/O, corrupt vault, bad args) |

Scripts treating any nonzero as failure degrade gracefully (exit 3 is
still nonzero — today's semantics). The `unsafe` and `stale` report
lists must also be placed in the taxonomy: `stale` (left in place, per
spec) accompanies exit 0/3 as a notice; `unsafe` paths (a §9 defense)
stay on the failure side unless the SPEC review concludes otherwise —
decided in this ADR's spec delta, once.

## Alternatives Considered

### Exit 0 + notice for pending
Reads best for agents, but silently un-fails any pipeline that *relied*
on the stop. Rejected as the riskier flip; exit 3 keeps the stop while
naming the state.

### Leave exit 1, document harder
The Auditor agent had read the docs and still flinched. An exit code is
the API here; prose can't fix a conflated code.

## Consequences
- SPEC §9 (N3 behavior) + §13 updated; CHANGELOG line for the 1 → 3
narrowing.
- One new CLI test pins each of the three codes (the pending case has no
test today — this ADR adds the missing one).

## Acceptance
- [ ] Foreign-pending scenario exits 3 with `notice:` stderr lines.
- [ ] "Not a vault" and I/O failure still exit 1.
- [ ] Clean run still exits 0.
Loading