Skip to content
Draft
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
37 changes: 31 additions & 6 deletions .agents/skills/proof/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ repository. It has eight record types: **Effort**, **Issue**, **Finding**,
**Citation** stores a source or reference; and **Blob** stores attached
content such as a document, JSON, or image. Every record belongs to one
Effort. Create and update records through 18 typed mutations, and read them
through 6 bounded queries. Do not hand-edit record frontmatter, although you
through 7 bounded queries. Do not hand-edit record frontmatter, although you
may edit record bodies freely.

Read [glossary.md](./glossary.md) for the primitive and edge semantics before
Expand Down Expand Up @@ -169,13 +169,34 @@ an `artifact_path` to a rendered markdown digest (the evidence — spend one
Read on it, or grep it), `served_generation`, page info, and ≤10 executable
`hints`. Digests cap at 25 records / one-hop expansion / 50 edges / 64 KiB.

Pick the read by what you need, in this order:

1. **Locate a record by what it says: `proof find "<words>"`.** It ranks
records by their title, full body, and Blob payload, and prints the hit
cards inline in the JSON, so you need no second Read. Each card shows the
newest version of the record (or, with `--as-of`, the version current
then), its standing, the passage that matched, its supersession chain
with dates, and its links in both directions. Leave out `--effort` when
you do not know the Effort: hits then group by Effort, at most 3 each
unless one Effort clearly leads. The `missing` terms name query words no
hit contains. Ordinary words often land there; a missing distinctive
term, such as a name, a number, or a code reference, is the cue that the
answer may not be recorded.
2. **Read one record in full: `proof get <id>`.**
3. **Count or page through records: `proof records`.** `find` shows only
the top hits, so never count from it.

Browse digests (`list`, `records`, `relations`, `blocking-decisions`) excerpt
each body at 600 chars / 12 lines (`[…truncated]`). **`proof get` digests
always include the full record body** (still subject to the 64 KiB digest
byte cap). Zoom in with `get`, then Read/grep that digest — do not open
`.flatbread-proof/**/*.md` for normal full-body recall.

```bash
# Find records by what they say (one Effort, or all when --effort is left out)
flatbread proof find "tern history buffer precision" --effort <effortId>
flatbread proof find "shadow pool overflow" --as-of 2026-03-01T00:00:00Z

# What's gating this effort? (proposed Decisions deriving from open blocker Issues)
flatbread proof blocking-decisions <effortId>

Expand All @@ -201,7 +222,8 @@ flatbread proof brief <effortId>
Flags shared by reads: `--strict-min-generation <token>` (with optional
`--timeout-ms <ms>`, default 3000) and, on `list`/`records`/`relations`, `--limit`
(≤25) and `--cursor` (opaque `next_cursor` from a prior page; only valid for
the same query at the same generation).
the same query at the same generation). `find` takes `--limit` 1 to 10
(default 5) and has no cursor.

`proof list` is bounded Effort discovery. It defaults to `active`; valid
statuses are exactly `active`, `paused`, `completed`, and `abandoned`.
Expand Down Expand Up @@ -230,23 +252,26 @@ server-side.
families showed this roughly halves recall tool calls with no loss of
answer quality (Decision
`dec-adopt-a-bounded-status-briefing-fast-path-for-ef--kcw0rw39g3b2ym2h`).
2. **When a browse digest shows `[…truncated]` and you need the body:** run
2. **When you need a particular record or fact:** run `proof find` first,
then `proof get` the hit whose card fits. Do not page through `records`
to look for it.
3. **When a browse digest shows `[…truncated]` and you need the body:** run
`flatbread proof get <id>`, then Read/grep that digest (`artifact_path`)
for the full body. Reserve opening `.flatbread-proof/**/*.md` for rare
cases (e.g. digest byte-cap miss on an oversized record), not normal
zoom-in.
3. **During work:** apply the write gate above before any create or body
4. **During work:** apply the write gate above before any create or body
edit that adds claims. When outside material supports a 4/4 record, save
large content with `WriteBlob` if needed, then create a `WriteCitation`,
then create the Issue, Finding, Decision, Constraint, or Risk with
`cites: ["<cit-id>"]`. You cannot add a citation later, so create the
Citation first. Open Issues for real gaps or blockers, and use
`derives_from` on Decisions to link the Findings, Constraints, and Issues
they respond to.
4. **On commitment:** preview `AcceptDecision` with `dryRun: true`, inspect
5. **On commitment:** preview `AcceptDecision` with `dryRun: true`, inspect
its `rejectedIds`, then commit the acceptance. Use `ResolveIssue` with
`resolvedBy` citing the closing Decision/Findings. These lifecycle
transitions do not need a 4/4 score. Retract session noise with `Retract`
rather than deleting files.
5. Maintenance: `flatbread proof cache prune` deletes digests older than
6. Maintenance: `flatbread proof cache prune` deletes digests older than
24h / over the 100 MiB ceiling. Prune does not need a 4/4 score.
78 changes: 74 additions & 4 deletions .agents/skills/proof/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ survivor (score 4/4 if it adds claims) plus `Retract` on the rest.

`generation` is a durable, monotonic journal token — the input to strict reads.

## The 6 read queries
## The 7 read queries

All reads execute through Flatbread's query engine (in-process GraphQL over
the generated schema) and return a `ReadEnvelope`:
Expand All @@ -209,6 +209,8 @@ lists), one-hop related records, and an edge table. Body policy:
Blob payloads and Citation bodies (URLs / short notes).
- **`proof brief`:** full body of the head compaction only. Every other
record appears as one line.
- **`proof find`:** the hit cards (below) under `## Find hits`, and each hit's
record with the browse excerpt. The envelope also carries the cards inline.
- **`list` / `records` / `relations` / `blocking-decisions`:** body excerpt
capped at 600 chars / 12 lines (`[…truncated]`). **Blob bodies are omitted**
from these digests — use `proof get` for the payload. Citation bodies
Expand All @@ -231,7 +233,8 @@ order:
error; do not retry the same page.
3. If `cap_reasons` is not empty, narrow the query or fail closed. It can hold
several sorted, duplicate-free values from `primary_records`,
`displayed_edges`, and `bytes`.
`displayed_edges`, `bytes`, and `find_limit` (`proof find` matched more
records than it shows).
4. A hard cap alone leaves `page.has_more: false` and `next_cursor: null`.
Paging and hard caps can occur together, but caps never create a cursor.

Expand All @@ -247,13 +250,14 @@ flatbread proof records <effortId> [--kinds k1,k2] [--state s1,s2] [--status s1,
flatbread proof relations <effortId> <fromId> --relations r1,r2 [--limit n] [--cursor c] [consistency flags]
flatbread proof blocking-decisions <effortId> [consistency flags]
flatbread proof brief <effortId> [consistency flags]
flatbread proof find "<words>" [--effort <effortId> | --all] [--kinds k1,k2] [--as-of iso] [--retracted exclude|include|only] [--limit n] [consistency flags]
flatbread proof cache prune
flatbread proof serve [--socket .flatbread/proof/serve.sock]
```

- `proof serve` keeps the graph loaded in one process and answers the six
- `proof serve` keeps the graph loaded in one process and answers the seven
reads above (`list`, `records`, `get`, `relations`, `blocking-decisions`,
`brief`); it serves nothing that writes. Set `FLATBREAD_PROOF_SERVER` to its
`brief`, `find`); it keeps the `find` index with the graph and serves nothing that writes. Set `FLATBREAD_PROOF_SERVER` to its
socket and those read commands go to it, in any folder whose graph root
resolves to the same files. Each later read then takes a fraction of a
second instead of rebuilding the graph. A write bumps the journal
Expand Down Expand Up @@ -305,6 +309,72 @@ flatbread proof serve [--socket .flatbread/proof/serve.sock]
stale head, or at least 5 changes. Programs read these fields, not the
digest.

### `proof find`

`find` locates records by what they say. Quoted or not, every word after
`find` is part of the query, locally and through `proof serve`.

- **Scope:** `--effort <effortId>` searches one Effort. Without it, or with
`--all`, it searches every Effort. `--kinds` narrows to some of `issue`,
`finding`, `decision`, `constraint`, `risk`, `citation`, `blob` (default:
all seven; Efforts are never hits). `--retracted` works as on `records`:
retracted records are left out unless you pass `include` or `only`, and a
shown one is marked `RETRACTED`. Every version a card scores, quotes, or
shows passes this scope; other versions appear only on the chain line.
- **Ranking:** BM25 keyword scoring (k1 1.2, b 0.75) over the title (counted
twice), the full body, `kind`, `state`, `status`, `role`, `created_by`, and
Blob payloads. Rare words weigh more, measured over the whole graph. Words
are lowercased, apostrophes dropped, hyphenated words indexed joined and
split, and plurals, `-ed`, `-ing`, and a final `-e` folded (`added` meets
`add`, `using` meets `use`). Numbers with units
(`4.7 ms` is `4.7ms`), people (`created_by`), and code references (file
paths, `symbol()` names, commit SHAs, `#123`) count 1.5 times. Ties break by
id. The index is rebuilt from the record files; nothing derived is stored.
- **Chains:** all versions linked by `supersedes` / `superseded_by` form one
hit, scored by its best in-scope version. Versions are numbered `v1`,
`v2`, … in link order: each comes after the versions it supersedes, and
created time, then id, orders fork branches. The card shows the chain head,
or the newest in-scope version when the head is out of scope (for example,
retracted under the default `exclude`). If an older version matched, the
card says which (`matched: v1 <id>`).
- **As of:** `--as-of <date>` takes `YYYY-MM-DD` (UTC midnight) or a
date-time with `Z` or an offset, such as `2026-03-01T00:00:00Z`; other
forms are refused, so the answer does not depend on the machine's time
zone. Only versions created at or before that moment are scored, quoted,
and shown. The card shows the last such version in link order, and chains
with no such version drop out.
- **Grouping:** without `--effort`, Efforts are ranked by the summed score
of their best 3 hits (`efforts by match`), and hits are grouped under each
Effort. Unless the top Effort's sum is at least 1.5 times the next one's,
at most 3 hits come from one Effort at first; slots still empty then go to
the next hits by score.
- **Caps:** `--limit` 1 to 10, default 5. Each card holds at most 4 chain
versions (the head and the shown and matched versions always appear), 3
links per relation, and 6 code references. When five cards run past about
4,000 characters, the cards shrink in this order: link titles, the
`efforts by match` line, the middle of each chain, then link entries. A
relation with no entry left shows its count, as `cited_by +4`. When more
records matched than are shown, the envelope has `complete: false` and
`cap_reasons: ["find_limit"]`; there is no cursor, so narrow the query or
raise `--limit`.

Each card has these lines:

```text
1. <id> | <kind>[:<kind field>] | <standing> [(head)] | <created date> | score <n>
"<title>"
matched: v<n> <id> (only when an older version matched)
"…<160-character passage with the most query words>…" [chars <from>-<to> of <length>]
chain: v1 <id> <standing> <date> -> (n more) -> v4 <id> <standing> <date> (head)
out: <relation> <id> "<title, 60 chars>" (<standing>), … +n more; <relation> +n
in: <derived_by|cited_by|resolves|rejects|mitigates|evidence_for|blob_of> <id> "<title>" (<standing>)
code: <paths, symbol() names, SHAs, #numbers the chain's records name>
```

The card header reads `find "<words>" in <effortId|all Efforts> (<searched> records searched, <matched> matched, <shown> shown)`, then `matched: … | missing: …`. The envelope adds `cards` (that Markdown) and `find`:
`{"query","effort_id","searched","matched","shown","terms":{"matched","missing"},"efforts"?,"hits":[{"id","matched_id"?,"score"}]}`.
`find` shows only the top hits; use `records` to count.

### Consistency flags

- `--strict-min-generation <token>`: serve at or after that journal
Expand Down
1 change: 1 addition & 0 deletions packages/flatbread/bin/flatbread.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const SERVED_READS = new Set([
'relations',
'blocking-decisions',
'brief',
'find',
]);

/**
Expand Down
70 changes: 70 additions & 0 deletions packages/flatbread/src/cli/proof.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ import {
import {
blockingDecisions,
effortBrief,
effortFind,
effortRecords,
type FindEnvelope,
relations,
getRecord,
listEfforts,
Expand All @@ -39,6 +41,9 @@ export interface EffortCliOptions {
cursor?: string;
relations?: string[];
retracted?: RetractedMode;
effort?: string;
all?: boolean;
asOf?: string;
verify?: boolean;
dryRun?: boolean;
}
Expand Down Expand Up @@ -74,6 +79,15 @@ export function mapEffortCliOptions(
typeof options.retracted === 'string'
? (options.retracted as RetractedMode)
: undefined,
effort: typeof options.effort === 'string' ? options.effort : undefined,
all: options.all === true || options.all === 'true' ? true : undefined,
// The option parser turns `--as-of 2026` into a number; keep it as
// text so the date check refuses it, as the read server does.
asOf:
typeof options['as-of'] === 'string' ||
typeof options['as-of'] === 'number'
? String(options['as-of'])
: undefined,
verify: options.verify === true ? true : undefined,
dryRun:
options['dry-run'] === true || options.dryRun === true
Expand Down Expand Up @@ -258,6 +272,33 @@ export async function handleEffortBrief(
});
}

/**
* `proof find`: searches one Effort with `effort`, or every Effort when
* `effort` is left out or `all` is set.
*/
export async function handleEffortFind(
query: string,
options: EffortCliOptions = {}
): Promise<FindEnvelope> {
if (options.effort && options.all)
throw new ProofReadValidationError(
'PROOF_INVALID_ARGUMENT',
'Pass --effort <effortId> or --all, not both'
);
const cwd = options.cwd ?? process.cwd();
return effortFind(String(query ?? ''), {
cwd,
rootDir: await rootFor(cwd),
cacheRoot: resolve(cwd, '.flatbread/proof'),
consistency: consistency(options),
effort: options.effort,
kinds: options.kinds as PrimitiveKind[] | undefined,
asOf: options.asOf,
retracted: options.retracted,
limit: options.limit,
});
}

export interface EffortBootstrapReport {
readonly status: 'ready' | 'action_required';
readonly config_path: string | null;
Expand Down Expand Up @@ -538,6 +579,35 @@ export function registerProofCommands(prog: any): void {
).action(async (effortId: string, options: Record<string, unknown>) =>
printResult(handleEffortRecords(effortId, mapEffortCliOptions(options)))
);
consistencyOptions(
prog.command(
'proof find <words>',
'Find records by what they say, ranked; one card per supersession chain'
)
)
.option('--effort <effortId>', 'Search one Effort')
.option(
'--all',
'Search every Effort (the default without --effort)',
false
)
.option('--kinds <values>', 'Comma-separated primitive kinds')
.option('--as-of <iso>', 'Show the version current at this date')
.option(
'--retracted <mode>',
'exclude (default), include, or only retracted records'
)
.option('--limit <n>', 'Hits to show, 1 to 10 (default 5)')
.action(async (words: unknown, options: Record<string, unknown>) =>
printResult(
handleEffortFind(
// An unquoted query arrives as one word here and the rest in `_`.
// Join them, as the read server does, so every word is searched.
[words, ...(Array.isArray(options._) ? options._ : [])].join(' '),
mapEffortCliOptions(options)
)
)
);
paginationOptions(
consistencyOptions(
prog.command('proof list', 'List Efforts by lifecycle status')
Expand Down
Loading
Loading