Canonical per-case memory-store directory contract. Every motion that reads or writes case state (M2 case templates, M5 bl consult/bl run/bl case, M6 bl defend, M7 bl clean) binds against this file. Promoted from DESIGN.md §7.2 and expanded with a writer-owner table the motion-map needs before parallel worktrees start fighting over hypothesis.md edits.
One workspace contains one bl-case memory store (see DESIGN.md §7.2). Inside that store lives:
- One workspace roster file (
INDEX.md) that lists every case + pointers to closed briefs. - One per-case subtree under
bl-case/CASE-<YYYY>-<NNNN>/containing hypothesis state, evidence pointers, agent-proposed steps, wrapper-written results, remediation actions, and case closure artifacts.
M2 owns the templates written on bl case --new. M5 owns the case lifecycle (open → active → close). M6 writes action proposals into actions/pending/; the wrapper promotes them to actions/applied/ on operator approval. Every write is auditable — the memory store's memver_ versioning provides a 30-day immutable audit trail per Managed Agents platform contract.
Reproduces DESIGN.md §7.2 lines 368–392 verbatim. This tree is canonical; additions require a §3 writer-owner row + a DESIGN.md §7.2 patch (or a waiver with rationale inline — see §7 change control).
bl-case/
├── INDEX.md # workspace case roster + pointers to closed briefs
├── CASE-<YYYY>-<NNNN>/
│ ├── hypothesis.md # current hypothesis + confidence + reasoning
│ ├── history/<ISO-ts>.md # each hypothesis revision immutable
│ ├── evidence/ # pointer-style, one per observation
│ │ ├── evid-<id>.md # {source, sha256, summary, file_id?}
│ │ └── obs-<id>-<kind>.json # raw observation output (JSONL usually)
│ ├── attribution.md # kill chain (upload/exec/persist/lateral/exfil)
│ ├── ip-clusters.md # IP cluster analysis per skills/ioc-aggregation
│ ├── url-patterns.md # URL evasion → generalized regex
│ ├── file-patterns.md # magic bytes + naming → yara synthesis
│ ├── open-questions.md # unresolved; gates case close
│ ├── pending/ # agent-emitted proposed steps
│ │ └── s-<id>.json
│ ├── results/ # wrapper-written step results
│ │ └── s-<id>.json
│ ├── actions/
│ │ ├── pending/<act-id>.json # awaiting operator approval
│ │ ├── applied/<act-id>.json # applied; carries retire-hint
│ │ └── retired/<act-id>.json # closed; no longer active
│ ├── defense-hits.md # running log of blocks that fired
│ ├── closed.md # present iff case closed: brief file_ids + retirement schedule
│ └── STEP_COUNTER # wrapper-managed step-id allocator (see §3 note)
Preamble. Every path below is labeled with {writer, when, cap, lifecycle} where:
- writer ∈
{curator, wrapper, operator}— the only actor permitted to create or modify the path. Cross-writes (e.g., the wrapper editinghypothesis.md) are policy violations and are logged. - when ∈
{on-open, on-step-emit, on-step-run, on-action-apply, on-close, on-hypothesis-revision, on-evidence-ingest, allocated-on-demand}— the event that triggers the write. - cap — approximate per-file size cap. The Managed Agents platform enforces 100 KB per file; the numbers here are target steady-state footprint.
- lifecycle ∈
{mutable, append-only, immutable-after-write, immutable-after-close, monotonic-non-decreasing}— how the path evolves over the case's life.
Note on STEP_COUNTER: STEP_COUNTER is referenced by schemas/step.md §step_id as the wrapper's step-id allocator state file — the curator requests a fresh s-NNNN from the counter before emitting a report_step. The path is NOT listed in DESIGN.md §7.2 tree at spec authoring time (4ec1c23); this doc adds it to the canonical per-case layout. A later DESIGN.md §7.2 edit to unify is welcomed and does not require re-issuing this doc (§7 change control permits it).
| Path | Writer | When | Cap | Lifecycle |
|---|---|---|---|---|
bl-case/INDEX.md |
wrapper | on-open + on-close | 100 KB workspace-wide | append-mostly; closed-case lines may be edited to update brief file_id |
bl-case/CASE-<id>/hypothesis.md |
curator | on-open + on-hypothesis-revision | 50 KB | mutable; previous values archived to history/ before in-place update |
bl-case/CASE-<id>/history/<ISO-ts>.md |
curator | on-hypothesis-revision (before mutating hypothesis.md) | 20 KB | append-only; immutable after write |
bl-case/CASE-<id>/evidence/evid-<id>.md |
curator | on-evidence-ingest | 10 KB | immutable-after-write; carries {source, sha256, summary, file_id?} |
bl-case/CASE-<id>/evidence/obs-<id>-<kind>.json |
wrapper | on-evidence-ingest (paired with evid-.md) | 50 KB | immutable-after-write; JSONL raw observation |
bl-case/CASE-<id>/attribution.md |
curator | on-hypothesis-revision (when kill chain advances) | 40 KB | mutable; typically edited ≤10 times per case |
bl-case/CASE-<id>/ip-clusters.md |
curator | on-evidence-ingest (after IP aggregation) | 30 KB | mutable; grows as evidence compounds |
bl-case/CASE-<id>/url-patterns.md |
curator | on-evidence-ingest (after URL-pattern generalization) | 20 KB | mutable |
bl-case/CASE-<id>/file-patterns.md |
curator | on-evidence-ingest (after magic/yara synthesis) | 20 KB | mutable |
bl-case/CASE-<id>/open-questions.md |
curator | on-hypothesis-revision (gates case-close) | 15 KB | mutable; must be empty (or explicit "none") for case-close |
bl-case/CASE-<id>/pending/s-<id>.json |
curator (via report_step) |
on-step-emit | 10 KB | mutable until bl run; then the wrapper moves the file to results/ and clears pending/ |
bl-case/CASE-<id>/results/s-<id>.json |
wrapper | on-step-run | 50 KB | immutable-after-write |
bl-case/CASE-<id>/actions/pending/<act-id>.json |
curator (via synthesize_defense) |
on-step-emit (from synthesize_defense tool) |
40 KB | mutable until operator bl run --yes; then wrapper moves to applied/ |
bl-case/CASE-<id>/actions/applied/<act-id>.json |
wrapper | on-action-apply | 40 KB | immutable-after-write; carries applied_at, backup_path, retire_hint |
bl-case/CASE-<id>/actions/retired/<act-id>.json |
wrapper | on-retire (manual removal, case-close, or retire_hint trigger) | 40 KB | immutable-after-close |
bl-case/CASE-<id>/defense-hits.md |
wrapper | on-evidence-ingest (when a new block-hit record ingests for an applied action) | 30 KB | append-only |
bl-case/CASE-<id>/closed.md |
wrapper | on-close | 20 KB | present-iff-closed; immutable-after-close; carries brief file_ids + retirement schedule |
bl-case/CASE-<id>/STEP_COUNTER |
wrapper | allocated-on-demand (pre-step-emit); incremented at each allocation | 16 bytes | mutable; monotonic non-decreasing |
Implications for parallel work:
- M2 templates (case-open skeleton): wrapper writes every path except
hypothesis.mdandhistory/,attribution.md,ip-clusters.md,url-patterns.md,file-patterns.md,open-questions.md(which are curator-written on first revision). Templates exist as empty-or-placeholder skeletons. - M5
bl case --new: wrapper-only writes (INDEX.md append + directory skeleton). No curator interaction untilbl consult --attachelevates the case into an active session. - M6 + M7 never write to
hypothesis.mdorhistory/— only toactions/pending/(fordefendproposals) andbackups/(out-of-tree, under/var/lib/bl/). - Curator never writes to
results/oractions/applied/— those are wrapper-authored, wrapper-immutable.
Managed Agents platform caps every memory-store file at 100 KB (see docs/internal/managed-agents.md §memory_stores.file_size). The caps in §3 are steady-state targets — if any file approaches 80 KB, the wrapper emits a warning and the curator's open-questions.md gets a new entry to compress, split, or promote-to-Files.
Per-case steady-state estimate (assuming ~50 evidence ingests, ~20 step emits, ~10 action applies):
| Bucket | Files | Total size |
|---|---|---|
| hypothesis + history (10 revisions) | 11 | ~250 KB |
| evidence (50 obs) | 100 | ~3 MB (obs--.json is the largest) |
| pointer-kind files (attribution, ip-clusters, url-patterns, file-patterns, open-questions) | 5 | ~125 KB |
| pending + results (20 steps) | 40 | ~1.2 MB |
| actions (10 applies) | 30 | ~1.2 MB |
| defense-hits | 1 | ~30 KB |
| STEP_COUNTER | 1 | 16 bytes |
| Total per case | ~190 files | ~6 MB |
Memory stores tolerate this footprint (platform spec documents no aggregate cap per store). Workspace-wide with ~20 concurrent cases = ~120 MB, well within Managed Agents limits.
CASE-OPEN ─► ACTIVE ─► CLOSED
│ │ │
│ │ └─► INDEX.md line mutated to carry brief file_id
│ │ closed.md written (immutable-after-close)
│ │ actions/retired/ sweep (per retire_hint)
│ │
│ ├─► step loop (curator report_step → pending → bl run → results)
│ ├─► action loop (curator synthesize_defense → actions/pending → bl run --yes → actions/applied)
│ ├─► hypothesis revision (history/<ISO-ts>.md written before hypothesis.md mutated)
│ └─► evidence ingest (evid-<id>.md + obs-<id>-<kind>.json written)
│
└─► INDEX.md line appended
empty skeleton templates materialized (per M2)
STEP_COUNTER initialized to 0
hypothesis.md placeholder written ("investigation open, no hypothesis yet")
Blocking conditions:
bl case closerequiresopen-questions.mdto be empty (or contain the literalnone). Wrapper rejects with exit 68 otherwise.bl case closerequires everypending/s-<id>.jsonto have a pairedresults/s-<id>.json(no un-run steps).bl case closerequires everyactions/applied/<act-id>.jsonto have aretire_hintfield (even if the hint ismanual).- Case cannot be re-opened after close — operator runs
bl case --new --split-from <closed-id>to create a linked successor case.
Workspace roster. One line per case. Wrapper-maintained. Shape:
| Case | Opened | Status | Hypothesis (30-char preview) | Closed brief (file_id) |
|------|--------|--------|------------------------------|-------------------------|
| CASE-2026-0007 | 2026-04-24T14:00Z | active | polyshell staging on host-2 | — |
| CASE-2026-0006 | 2026-04-23T18:00Z | closed | magecart skimmer, host-5 | file_011C... |
Update events:
bl case --new: append one row;Status: active; brieffile_idcolumn is—.bl case close: mutate the case's row —Status: closed+ populate brieffile_id.bl case reopen: not supported; see §5.
bl-case/CASE-<id>/history/<ISO-ts>.md is append-only. Each file captures one hypothesis revision. Filename = the ISO-8601 timestamp of the revision (millisecond precision permitted for ordering tight-loop revisions).
Shape:
# Hypothesis revision — <ISO-ts>
## Prior
<copy of the prior hypothesis.md contents>
## New
<copy of the new hypothesis.md contents>
## Trigger
<one paragraph — the evidence, open question, or external input that drove the revision>
## Open questions
<any new unresolved lines the revision introduces; forwarded to open-questions.md>
Curator writes the history file BEFORE mutating hypothesis.md. If the write fails (memory store quota, network), the mutation is aborted — consistency > performance.
Never edit a history file in place. Even typo fixes get a new revision. git blame — or in this case memver_ audit — is the only source of truth for when a claim was made.
Enforced separation:
pending/s-<id>.jsonis curator-written (viareport_stepcustom tool — seeDESIGN.md §12.1.1). Wrapper reads it forbl run, never modifies it.results/s-<id>.jsonis wrapper-written afterbl runexecutes. Curator reads it for evidence-revision; never modifies it.- When
bl runcompletes, the wrapper movespending/s-<id>.json→results/s-<id>.jsonwith the result payload appended. This is the only directory the wrapper mutates on behalf of a curator-authored step.
Cross-writes are forbidden. The wrapper's system sanity check runs on every boot: find bl-case -path '*/pending/*' -newer <bl-case/CASE-*/STEP_COUNTER> should return nothing — if it does, something wrote to pending/ outside the curator's step-emit path.
Three states, linear transitions:
actions/pending/<act-id>.json— written by the curator viasynthesize_defense(DESIGN.md §12.2). Wrapper runs kind-specific FP-gate (modsec:apachectl -t; firewall: ASN safelist check; sig: FP-corpus scan) before promotion.actions/applied/<act-id>.json— wrapper writes afterbl run --yessuccessfully applies. Carriesapplied_at,backup_path,retire_hint(conditions under which the wrapper should retire the action — e.g., "retire if no defense-hits in 14 days").actions/retired/<act-id>.json— wrapper writes when one of: (a) operator runsbl defend <kind> --remove <id>; (b)retire_hintfires; (c) case closes. The retired JSON preserves the full apply payload + retirement reason +retired_at.
Retired actions stay in the store for the memstore's 30-day audit window. After that they age out of memver_ but the local ledger (/var/lib/bl/ledger/<case>.jsonl) preserves the full history indefinitely.
bl-case/ is remote (memory store). /var/lib/bl/ledger/<case-id>.jsonl is local (append-only). Dual-write per DESIGN.md §13.4.
Dual-write protects against:
- Memory corrupted / workspace wiped: local ledger preserves the full action history; operator reconstructs via
bl case log --from-ledger. - Host wiped: remote memstore preserves hypothesis + evidence + actions; new host's
/var/lib/bl/boots empty and re-reads from memstore on nextbl consult.
The ledger is NOT a cache — it is authoritative for local state after a memstore wipe. bl case log --audit prints the ledger in a regulator-friendly format regardless of memstore reachability.
M13 (Skills primitive realignment) migrated per-case evidence blobs from the bl-case
memstore to the Anthropic Files API. The memstore retains hypothesis + steps + working
memory only; raw evidence and summaries live in mounted Files.
| Kind | Path in Files API mount | Description |
|---|---|---|
| Raw observation bundle | /case/<id>/raw/<source>.<ext> |
JSONL observation output (was bl-case/<id>/evidence/obs-<id>-<kind>.json) |
| Summary | /case/<id>/summary/<source>.md |
curator-authored evidence summary (was bl-case/<id>/evidence/evid-<id>.md) |
<source> is a kebab-slug derived from the observation command (e.g., apache-log, mtime-cluster-fs, htaccess). <ext> is jsonl for structured observation output, json for single-record envelopes.
| Surface | Holds | Mount point | Access |
|---|---|---|---|
| Files API | Raw evidence bundles + closed-case briefs + shell samples | /case/<id>/raw/ + /case/<id>/summary/ |
hot-attachable mid-session via sessions.resources.add |
bl-case memstore |
Hypothesis + steps (pending + results) + actions + open-questions + attribution | bl-case/CASE-<id>/ key prefix |
always-on read_write |
Key invariant: the Files API is the blob store; the memstore is the reasoning scratchpad.
Evidence is uploaded to Files on observation (bl observe) and attached to the curator
session at bl consult. After bl case close, the per-case Files are moved to
files_pending_deletion[] in state.json and deleted by bl setup --gc once no live
sessions hold them.
See also: DESIGN.md §3.4 (Primitives map), docs/managed-agents.md §11 (Path C primitives map).
Additions to the tree: new path → §3 writer-owner row (required) + §2 tree entry (or waiver inline) + M2 template update if on-open-materialized + any consuming handler (M5/M6/M7/setup) updated. Reviewer flags missing targets as MUST-FIX.
Renames: not recommended; remote memstore carries historical paths for 30 days under old names, and the curator's context window persists prior paths in-session. If a rename is required, run it as a two-commit dance: (1) emit under both old and new paths for one memory-store versioning cycle; (2) stop emitting under old path. Never rename with a hard cutover.
Removals: only if no M2/M5/M6/M7 handler writes the path. Grep bl_*_*() function bodies across bl for every reference before removing.
Unification with DESIGN.md §7.2: welcomed at any time; the §7.2 tree block can grow or add entries that this file already covers. No re-issue of this doc is required. The STEP_COUNTER row in §3 is the canonical forward-reference; a DESIGN.md §7.2 edit to add it closes that loop.