Bead Issue Tracking¶
Bead is a lightweight, git-native issue tracking system built into sase. It uses
Rust-backed event storage, query/reduction, and mutation logic through the required
sase_core_rs extension, with generated JSONL compatibility projections for older
tooling (inspired by Fossil). Issues are organized into
plan-like containers, executable child phases, and standalone task beads for discovered
follow-up work. Plan beads can represent ordinary plans or executable epics through
their tier metadata; task beads capture independent work that does not need an epic
DAG.
Table of Contents¶
- Quick Start
- Bead ID Arguments
- Data Model
- Issue Types
- Task Types
- Status Lifecycle
- Flag Bead Lifecycle
- Bead Claim Lifecycle
- Standalone Task Workflow
- Snoozing a Task Bead
- Task Corroboration (+1)
- Close History
- Dependencies
- Discovered Follow-Up Capture and Triage
- External Issue Mirroring
- Artifact References
- Attachments
- Creation Reason
- Creation Time Presentation
- Storage
- Directory Structure
- Event Log + Compatibility Projections
- Sealed Segments (Gated Design)
- Sync Mechanism
- Bead Pages
- CLI Commands
- Rust Backend
- Current Checkout Source Of Truth
- sase's TUI Integration
Quick Start¶
PLANS_ROOT=$(sase repo path plans)
sase bead init # Initialize beads in current project
sase bead create -t "New feature" --type "plan(${PLANS_ROOT}/202605/feature.md)" --tier plan \
-w "Track the feature plan before implementation starts"
sase bead create -t "Epic" --type "plan(${PLANS_ROOT}/202605/epic.md)" --tier epic \
-w "Break the feature into phased work"
sase bead create -t "Sub-task" --type "phase(beads-001)" --size small \
-w "The epic plan calls for this phase" # Create a sized epic phase
sase bead create -t "Fix flaky test" --type 'task(flake)' --size small \
-w "CI flakes on the retry path" \
-f node_id=tests/foo.py::test_bar -f evidence='failed then passed' # Create a typed draft task
sase bead +1 beads-002 --note "Independent reproduction" # Corroborate an existing task
sase bead update beads-002 --status=ready # Offer the task for human triage
sase bead list # List open, claimed, ready, snoozed, and in-progress issues
sase bead list --status=open # List open issues
sase bead list --status=ready --type=task # List tasks awaiting triage
sase bead list --status=closed # List closed issues
sase bead search auth # Search issues in every status
sase bead ready # Show unblocked ready task beads
sase bead show beads-001 # View issue details
sase bead read beads-001 -r "Need the scope" # Audited agent read with a reason
sase bead ref add beads-001 research:202607/report.md # Attach durable context
sase bead ref list beads-001 --resolve # List references and resolution state
sase bead ref rm beads-001 research:202607/report.md # Detach a reference
sase bead update beads-001.1 --status=in_progress # Set status; assignee stays unchanged
sase bead note beads-001.1 "Verified with just check" # Append an attributed note
sase bead open beads-001.1 # Reopen an issue
sase bead close beads-001.1 # Close an issue
sase bead dep add beads-001.2 beads-001.1 # Add dependency
sase bead dep list beads-001.2 --format full # Inspect dependency provenance
sase bead dep tree beads-001.2 # Follow the blocking chain
sase bead dep rm beads-001.2 beads-001.1 # Remove a wrong dependency
sase bead blocked # Show blocked issues
sase bead sync # Export and stage JSONL in git
sase bead pages refresh # Preview regenerated bead pages
sase bead pages refresh --write # Regenerate, commit, and push bead pages
sase bead pages url beads-001.1 # Print the hosted page URL when available
sase bead stats # Project statistics
sase bead doctor # Health check
sase bead doctor --fix-plan-archive # Backfill recoverable missing plan archives
sase bead doctor --fix-issue-prefix # Reset a leaked ProjectSpec-key issue prefix
sase bead doctor --fix-projection # Repair issues.jsonl from canonical events
sase bead work "$PLANS_ROOT/202605/epic.md" --dry-run # Preview bead creation and launch waves
sase bead work "$PLANS_ROOT/202605/epic.md" --yes # Create, link, and launch an epic plan
sase bead work "$PLANS_ROOT/202605/epic.md" --wait 'sase-s7.2,bead=sase-64.3' --yes
sase bead work epic-a epic-b -c 3 --yes # Capacity 3 for every new phase and land agent
sase bead work "$PLANS_ROOT/202605/epic.md" -C feature_epic --yes
sase bead work beads-001 # Launch agents for an epic plan bead
sase bead work beads-002 --dry-run # Preview one standalone task worker
sase bead work beads-002 --yes # Launch one standalone task worker
sase bead work "$PLANS_ROOT/202605/epic.md" beads-002 --yes # Run targets in order, stopping at first error
Bead ID Arguments¶
Every sase bead command argument that names an existing bead accepts either the full
ID or its shorthand suffix after the final dash. For example, sase bead show 001
resolves to beads-001, and dotted descendants work the same way:
sase bead close 001.2 resolves to beads-001.2. Output, events, dependencies,
reference ownership, commit summaries, agent names, generated page paths, and JSON
payloads always use the canonical full ID.
Full IDs are still accepted unchanged, including IDs whose project prefix contains dashes. If a shorthand suffix matches more than one bead, SASE rejects the command and lists the candidate full IDs instead of choosing one arbitrarily.
When a full ID is not present in the current store, existing-bead operands fall back to
enabled SASE projects. SASE checks the current store first, and a local hit never reads
the project registry. After a local miss it looks for the exact full ID in each enabled
project's canonical bead store; project names, aliases, and stored prefixes are hints,
not proof that an ID exists. Shorthand suffixes remain local-only unless
sase bead show -P/--project pins a project. Every command that takes an existing bead
ID routes this way: +1, apply-status, close (including --phases), create (the
parent in phase(<parent>) or plan(<file>,<parent>)), dep, epic-symbols,
history, note, open, pages refresh --bead, pages url, ref, rm, show,
snooze, update, and work. Commands with no bead selector, such as list,
search, ready, blocked, and stats, keep their current-project scope, as do the
store-wide forms of dep list, dep tree, and ref list.
A routed command settles on one owning store before it does any work. Reads, writes, the
store commit, publication, and refresh all use that store, and project-specific context
follows the owner: close settles the owner's triage gates and checks the owner's
Justfile for --epic-symbol entries, epic-symbols <id> lists the owner's Justfile,
ref list --resolve resolves from the owner's workspace, create records the plan
reference relative to the owner's SDD store, and work launches in the owner's project
context. Free-form inputs stay relative to you: @<path> values and relative plan paths
resolve from the directory where you ran the command. A routed command never creates a
bead store in your current directory.
Every bead operand is resolved before anything is written. A batch that spans more than
one store, including a local shorthand next to another project's full ID, fails with
multiple stores match the requested bead targets before any event is recorded. For
example, sase bead close bob-cli-1e --note done can be run from another enabled
project, while sase bead update bob-cli-1e sase-2 -s ready is refused if those IDs
belong to different stores. A full ID that no readable store holds reports
issue not found: <id>, and an ID that exists in more than one enabled project's store
is reported as ambiguous with the candidate projects. If the likely owner (by name,
alias, or stored prefix) has no materialized or readable store on this machine, the
error names that project; an unrelated project with an unavailable store never blocks a
valid target. A routed mutation must also produce its store commit: if the owner's
auto-commit fails or creates nothing, the command fails with a
routed bead mutation ... could not be committed for publication diagnostic.
The active checkout belongs to the enabled project whose WORKSPACE_DIR, or a numbered
_<N> sibling of it, contains the current directory. Another project whose directory
key merely matches the workspace name, such as one auto-created for a #git:<name>
reference, cannot take over bead-store resolution. sase doctor warns
(project.name_collisions) when a project's PROJECT_NAME or alias case-insensitively
matches another project's directory key, PROJECT_NAME, or alias.
Data Model¶
Issue Types¶
| Type | Description | ID Format |
|---|---|---|
| Plan | Plan-like container with a tier; may be a child epic | {prefix}-{counter} or {parent_id}.{N} |
| Phase | Sized executable child within an epic/plan bead | {parent_id}.{N} |
| Task | Independent, explicitly sized work item with no parent or tier | {prefix}-{counter} |
Plans are groupings that can optionally link to an SDD file via the design field.
Phases always belong to a parent plan and use hierarchical IDs (e.g., beads-001.1,
beads-001.2). Task beads are top-level, carry neither a parent nor a tier, and require
a size when newly created. A SASE feature-flag removal bead is a task bead of type
flag — see Flag Bead Lifecycle — not a fourth issue type. An
epic proposed by a phase or land agent becomes a child plan bead beneath the bead
responsible for that agent. For example, phase beads-001.2 can own child epic
beads-001.2.1; an epic proposed by the land agent can become the next direct child
such as beads-001.3.
Task beads are deliberately flat: the task creation form takes no plan path or parent ID, and a task cannot carry a plan tier or Patch metadata. Use a task for independent follow-up work that one worker can own. Use an epic and phase beads when the work needs a validated plan, dependency waves, or a final land agent.
New task beads also require a task type from the project's catalog:
-T 'task(<slug>)' plus any required -f/--field values. Bare -T task is an error
that lists the agent-creatable slugs.
The generic sase bead update <task-id> --design <path> command currently accepts
design metadata on a task, even though task creation does not. That metadata does not
give the task a parent or make task launch plan-backed: sase bead work still sends the
task description and notes to one worker. sase's TUI Plans pane shows the stored
reference but does not load its document into the task detail.
Plan beads carry a tier. The paths below are relative to the effective plans root. Use
sase repo path plans or SASE_SDD_PLANS_DIR to locate it without depending on the
storage layout.
| Tier | Plans-root path | Behavior |
|---|---|---|
plan |
{YYYYMM}/*.md |
Normal non-epic implementation plan (tier: tale) |
epic |
{YYYYMM}/*.md |
Executable multi-phase plan (tier: epic) |
Epics use the plan syntax:
sase bead create --title "Epic" --type "plan(${SASE_SDD_PLANS_DIR}/202605/epic.md)" --tier epic \
-w "Planning the feature breakdown"
Task Types¶
task_type is an orthogonal flavor of discovered work. It is valid only on a task bead
and is distinct from the bead's issue type (plan, phase, task). New tasks require
a catalog slug; existing untyped legacy tasks stay readable and render as a dim
untyped chip. Feature-flag removal beads are task beads of type flag; list them with
-T flag, not --type flag.
sase bead create -T 'task(flake)' -t "Fix flaky retry" -z medium \
-w "CI flakes on the retry path three times this week" \
-f node_id=tests/foo.py::test_bar -f evidence=@evidence.md
sase bead task-type # agent-creatable catalog
sase bead task-type show flake # fields, template, triage, provenance
sase bead list --task-type flake
sase bead list --task-type untyped # legacy beads with no type
sase bead list -T flag # feature-flag removal beads
-T 'task(<slug>)' selects the type. Repeatable -f/--field k=v supplies declared
fields; a value of @<path> is read from that file so long prose does not have to
survive the shell. Duplicate field keys are an error. The stored description stays
free text: the type's body template is rendered below it at display time, never merged
in. sase bead update has no --task-type; the type is immutable once set — close the
bead and recreate it to change the flavor.
When this machine does not have the providing plugin, sase bead show still succeeds
and prints the raw key/value pairs under a
Task type: <slug> (not installed on this machine) header. Creating a type that only
the committed snapshot knows names the package and the sase plugin install command.
The catalog is assembled from three sources, first wins: builtin specs,
sase_task_types plugin hooks, then bead.task_types project config. A
use: <plugin>@<slug> config entry deep-merges its sibling keys onto that slug; an
entry without use: defines a new slug and may not shadow a builtin. sase memory init
writes the committed catalog to sase/task_types.json and generates the short
sase/memory/task_types.md note from it for SASE-managed project repositories only, so
generated agent instructions stay a function of committed files rather than whichever
optional plugins happen to be installed. See
Required plugins and
Task-type plugins.
Suppress a builtin for a project or the whole machine with use: <plugin>@<slug> plus
agent_creatable: false; existing beads of that type keep rendering normally. The
use: prefix always names the type's original provider (builtin for a builtin), no
matter how many layers have already overridden the slug. A machine-global entry in
~/.config/sase/sase.yml applies to every project on the machine, and a project's own
sase/sase.yml entry wins over it.
Every CLI, sase's TUI, bead-page, and gate-preview surface routes the colored type chip
through one presentation module. Builtins use hand-tuned glyphs (⨯ bug, ⚙ ci, ✦
feature, ≈ flake, ▤ memory); a type that declares none gets a stable hash-derived
color.
A type also carries its own triage bar. triage.min_plus_ones is the number of
independent +1 reports a ready bead of that type needs before
it earns a TaskTriage gate. A bead resolves its bar one of two ways:
- Typed, and this machine has the type registered — the type's own
triage.min_plus_oneswins. The spec default is0; among the builtins,flakeships3because a test that failed once is the case most often misread as a real defect,bugships1because a defect found while doing something else deserves one independent reproduction before it interrupts anyone, andci,feature, andmemoryship0. - Untyped, or typed with a slug this machine does not have registered — the
configured
bead.task_triage.min_plus_onesis the fallback. It ships as1.
Those two defaults differ on purpose, so do not read the spec default of 0 as "the
default bar is zero": a legacy untyped bead needs one +1 out of the box, while a typed
ci bead needs none. sase bead task-type show <slug> prints the effective bar under
TRIAGE.
Status Lifecycle¶
| Status | Icon | Description |
|---|---|---|
open |
○ |
Not started; for task beads, still a draft that is not offered for triage |
claimed |
◎ |
Reserved by a live agent that has not started work |
ready |
◇ |
Task bead explicitly offered for triage; invalid for plan, phase, and flag task beads |
snoozed |
◈ |
Task bead deferred to a wake time (or +1 target); invalid for plan, phase, and flag task beads |
in_progress |
◐ |
Being worked on, or preassigned by an epic/task launch checkpoint |
closed |
✓ |
Completed, canceled, or superseded |
Status can transition between values via sase bead update --status=<status>. A task
normally moves open → ready when its draft is proposed, ready → open when retracted,
and ready → in_progress when launched. Only task beads may carry ready; set it after
the title, description, notes, dependencies, and any desired size or model are ready for
human review. Moving a bead to closed is rejected while any descendant remains open,
claimed, ready, snoozed, or in progress. Close those descendants deliberately first;
update --status=closed never cascades. sase bead open <id> reopens the bead and
every closed ancestor above it, archiving their resolution, close reason, and close
timestamp into close history instead of discarding them, so a closed
parent never sits above reopened work but the reason it was closed is not lost either.
claimed is machine-managed by the agent runner (see
Bead Claim Lifecycle); do not set it by hand. snoozed cannot
be set through update --status either — snoozing requires a wake time, so
sase bead update -s snoozed is refused with a pointer to sase bead snooze, the same
way update --status=closed points at sase bead close. See
Snoozing a Task Bead for the full workflow.
Every new close records a typed resolution: done, canceled, or superseded.
Normal closes default to done; close_reason remains optional free text for the human
explanation. Closing an already-closed bead is a verified no-op: the command succeeds
when any supplied reason or resolution agrees with the recorded close, and fails without
writing when the request conflicts. Historical closed beads are not backfilled, so their
resolution remains unset and human-readable detail views show (unrecorded).
Flag Bead Lifecycle¶
A flag bead is not a fourth issue type and not the epic or task that introduced a
feature. It is a top-level task bead of type flag: the dedicated removal dossier for
one SASE feature flag, linked from the code-owned registry. Feature flags are a
sase-project concern only.
Two kinds exist, and the registry default is derived from the kind:
beta— default off. The behavior is unproven; a user opts in.sunset— default on. The behavior is already the default; the flag keeps the old branch reachable while callers migrate.
Removing a flag deletes the Off branch and makes the On branch unconditional.
The bead carries seven required fields. sase flag new supplies key, kind,
remove_by_date (today + 90 days), and remove_by_release (current minor + 2). The
author writes the three a removal agent cannot reconstruct: --when-enabled,
--when-disabled (the branch deleted at removal), and --remove-when (the qualitative
gate). The typed body block renders those fields. -d/--description seeds the registry
entry's one-line help.
Create feature flags with sase flag new <key>, not by hand-editing the registry and
not with sase bead create or /sase_new_task. The command creates the typed task
bead, prints the registry entry to paste, and shows the both-states test checklist.
sase bead create -T 'task(flag)' is refused because the type is not agent-creatable.
Flag task beads stay open until someone is working them or they close; they do not use
ready or snoozed. A flag becomes due only when both the date and release threshold
have passed. Due-ness never flips the stored status and never changes the boolean value.
Extend both thresholds with sase bead update <id> -b YYYY-MM-DD/release.
AXE's bead-gate reconciler raises one FlagTriage gate for a live due flag task bead.
Its decisions are:
- Remove deletes the Off branch, makes the On branch unconditional, removes the registry entry, and closes the flag bead in the same change.
- Extend pushes both
remove_bythresholds out and records why the flag is still temporary. - Keep records that the behavior is permanent. It was never a feature flag; make it a config field and close the bead.
- Close abandons the removal. Use this when the flag was already removed or is intentionally orphaned; registry/bead integrity checks catch an inconsistent survivor.
Bead Claim Lifecycle¶
An agent launched with %id(<name>, bead=<id>) reserves its bead before it starts
working, so a bead is never silently owned by a process that nothing else can see:
open ──claim──▶ claimed ──promote──▶ in_progress ──close──▶ closed
▲ │
└────release─────┘ (claim owner died before launching)
- Claim. When a bead-carrying agent enters a wait phase (dependency
%wait, runner-slot, or duration waits), the runner sets the bead toclaimedand assigns it to the agent name. Claims are written to the project's canonical bead store, committed locally, and then published synchronously on a best-effort basis so other hosts can see the claim: the runner runs the managed sync worker for that store right after the commit lands. Publication never rolls a claim back — a missing git repo or missing remote is a silent local-only outcome, and a real sync failure only prints a warning with the managed-sync log path while the local commit stands. Claiming is advisory: it never blocks or fails an agent launch, and a straight-through launch with no waits skips it entirely. Because it is advisory, the claim is acquired best-effort: the runner retries a bounded number of times (refreshing the canonical store once when the bead is not there yet, which is normal right after an epic graph is published), and whatever it fails to acquire is picked up by thebead_claim_checksreconciler. A bead can therefore turnclaimeda few seconds after its agent starts waiting rather than instantly. - Promote. Immediately before model execution the runner performs the existing
just-in-time claim, which sets
status=in_progressand assigns the runner name. Promotion is what makes the claim permanent; from that point the claim is never released automatically. In managed standalone SDD stores the promotion must produce a local commit and is published the same best-effort way before model execution; in in-tree stores the agent commits the promotion along with its implementation instead. - Release. If the owning agent dies before it ever promoted its claim, the bead
returns to
openwith an empty assignee. The runner shutdown path releases the claim on ordinary kills (except when a retry handoff is pending, which keeps the claim), and thebead_claim_checksjob is the backstop for SIGKILL, crashes, and reboots. It releases a claim only when the owning agent is dead, never promoted, and resolvable to its artifact; anything else is left untouched and reported bysase doctorinstead. A committed release is published the same best-effort way as a claim, so a freed bead does not stay claimed on other hosts. - Reconcile. The
bead_claim_checksjob — registered under thewaitsroutine — runs in both directions. Next to the release pass above, an acquire pass claims a bead on behalf of a live agent that is waiting without a claim, which is what makes a lost or delayed claim self-healing within onewaitsinterval. A held claim is recorded in the agent'sbead_claim.jsonartifact file, so an agent that already holds its claim costs the job nothing: it is filtered out without opening a bead store.sase doctorreports the residue in either direction — a claim with no resolvable owner, and a live pre-launch agent whose bead is stillopen.
Claim and release are compare-and-swap operations: a claim succeeds only from open
(re-claiming your own claim is a no-op), and a release succeeds only when the bead is
still claimed by the releasing agent. Both decline silently rather than overwriting
someone else's state, so all three layers are safe to run concurrently.
The diagram above describes an ordinary bead-carrying agent. sase bead work uses a
stronger batch checkpoint: before spawning any epic worker, it sets every scheduled
phase to in_progress with its deterministic worker as assignee and does the same for
the epic and land worker. The later runner-side wait claim and launch promotion become
idempotent no-ops. Scheduling still ignores bead status and decides from agent liveness
(artifacts and PID checks), so a retry can schedule preassigned work without creating a
duplicate name.
Task-bead launches use the same strong checkpoint principle for one worker:
sase bead work <task-id> assigns the task to the deterministic agent name <task-id>,
sets it to in_progress, commits that state, synchronizes it unless --no-push was
requested, and only then spawns the worker. A task launched through this path therefore
does not pass through the advisory claimed state.
Standalone Task Workflow¶
Task beads separate collecting follow-up work from deciding whether to run it:
open (draft) ──mark ready──▶ ready (triage) ──launch──▶ in_progress ──close──▶ closed
│
└──close with reason──▶ closed (canceled)
- Invoke
/sase_new_task. It first checks for semantic duplicates and causally related in-progress epics. Only when neither exists does it create and refine a draft:
sase bead create -T 'task(bug)' -t "Remove the compatibility shim" \
-w "The new parser has shipped and the old path is still imported" \
-d "Verify callers and remove the old path." \
-z medium -f location=src/compat.py -f repro='old path still imported'
sase bead note <task-id> "Found while landing sase-123"
sase bead dep add <task-id> <blocking-bead-id>
New task beads start open. While they are open, edit their title, description,
notes, references, dependencies, optional model, and required size without creating a
triage notification.
- Offer the task for review:
sase bead update <task-id> --status ready
sase bead ready
sase bead ready lists only task beads whose stored status is ready and whose
dependencies are all closed. A task may remain stored as ready while blocked; it
becomes visible to this command when the last blocker closes. The scheduled triage
scan currently behaves differently, as described next.
- Triage it. The default AXE
checksroutine scans enabled non-home projects every five minutes and creates one priorityTaskTriagegate for each task whose stored status isreadyand that has accumulated at least its effective+1bar — its task type's owntriage.min_plus_ones(0forci,feature, andmemory;1forbug;3forflake), orbead.task_triage.min_plus_onesfor an untyped or unregistered type. A sub-threshold task is withheld from triage — it stays stored asreadyand stays visible tosase bead readyand this triage guide's other commands, only the gate is withheld — and aTaskTriagegate already raised for a task that later falls below the bar is canceled and its notification dismissed. This scan currently does not apply the dependency filter used bysase bead ready, so a blocked ready task can still receive a gate. The reviewed preview contains the task's title, description, and notes. Launch accepts optional feedback and submits one global unattributed proc that runssase bead work <task-id> --yes-to-all; Close requires feedback and closes the bead withresolution=canceledand that feedback as the reason. The detached launch survives sase's TUI, CLI, Telegram, or mobile client exit and appears insase proc listand sase's TUI Procs tab. Snooze requires a wake time, accepts an optional+Nwake threshold and reason, and defers the task until either condition is met.
Only one pending gate is kept per task. If the task leaves stored status ready, AXE
cancels the pending gate. If a request is answered, canceled, or missing while the
task is still ready, the next scan creates a new generation-specific request. The
same happens if the task leaves ready and returns later. After the bead mutation
commits, sase bead close makes a best-effort attempt to cancel a just-closed task's
pending TaskTriage or BeadSnooze gate. A cancellation failure does not undo or
fail the close; the next five-minute scan remains the backstop. Choosing Launch
in that gate answers it through the normal gate workflow, and a successful launch
submission from sase's TUI Beads pane explicitly cancels the matching gate. A direct
sase bead work command changes the task's status but does not settle an existing
gate itself; the scheduled scan remains its cleanup backstop.
- Work it. You can bypass scheduled triage and launch directly:
sase bead work <task-id> --dry-run
sase bead work <task-id> --yes
A direct launch accepts open, ready, or recoverable in_progress task beads. It
rejects claimed and closed tasks, but currently does not reject a task with
active dependency blockers. An in_progress task assigned to a live agent is an
idempotent success with no second launch. If the assignee is stale, SASE previews
and, after the required cleanup confirmation, force-reuses the deterministic
task-agent name. --yes skips only the launch prompt; use --yes-to-all when
stale-agent cleanup must also be non-interactive.
Before spawning, SASE commits the in_progress status and <task-id> assignee and
applies the same target synchronization safety as bead-ID epic launches. A checkpoint
failure, or a dispatch failure before any worker is spawned, restores the task's
prior status and assignee. A partial dispatch failure terminates the partial launch
but preserves the in_progress assignment for recovery. The worker receives the task
ID, description, and notes through the work_task_bead macro and is instructed to
close the task with verification evidence. A successful sase stitch create commit
or PR from the task worker's primary-repo workspace can close the assigned bead only
when invoked with -B close; intermediate commits use -B keep — see
Explicit Bead Action.
- Route the worker model. A task's explicit
modelwins. Otherwise a stored size selects the corresponding@xsmall,@small,@medium,@large, or@xlargealias directly; a legacy task without size metadata uses@small. As with epic phases,largeandxlargetask prompts add#plan, while smaller tasks implement directly.
Snoozing a Task Bead¶
An open or ready task bead can be deferred instead of triaged immediately:
sase bead snooze <task-id> -u 3d
sase bead snooze <task-id> -u 2h -r "waiting on the upstream fix"
sase bead snooze <task-id> -u 7d -p 2
sase bead snooze <task-id> --cancel
-u/--until (a duration such as 30m, 2h, 1h30m, 3d, or an absolute ISO-8601
timestamp) is required unless --cancel is given; a non-positive duration or a past
absolute time is rejected. -p/--plus-ones adds a second wake condition: the bead also
wakes when that many additional +1 reports arrive, whichever wake condition is
reached first. -r/--reason is optional free text. --cancel returns the bead to
ready immediately and clears the snooze record; the same happens automatically once a
wake condition fires.
Every successful snooze (including a re-snooze) appends one attributed note recording
the wake time, the deferral length, any +1 target, and the reason, in the same store
mutation that sets status: snoozed. This is what preserves the "why and until when"
after a wake clears the snooze record — sase bead show only renders the SNOOZE block
while the bead is still snoozed, so the note is the only place a past deferral's
conditions survive. For example:
[2026-08-07T13:21:54Z · bryanbugyi34@gmail.com] Snoozed until 2026-08-10T09:21:53-04:00 (in 3d). Reason: waiting on the upstream fix
--cancel appends no note of its own; it only clears the snooze record.
Snoozing always snoozes the bead's own notification in the same step — there is no
separate scheduler, and the bead's row stays visible in the notification panel's
Snoozed tab (see Tabs and Ordering in the
notifications doc) for the whole deferral. When the wake time arrives, the notification
resurfaces as a BeadSnooze gate with three options: Close (primary; empty feedback
uses a preset "stale, no new evidence" reason, any feedback text replaces it), Ready
(returns the bead to ready, where the ordinary TaskTriage gate takes over), and
Snooze (re-snoozes with one required duration line using the same
"<wake-time> [+<N>]" vocabulary as the -u/-p flags combined into one expression,
for example 3d, 2026-08-09T09:00:00-04:00, or 3d +2; optional feedback remains the
deferral reason). Reaching the +1 target instead of the wake time promotes the bead
straight to ready with a preset note ("Reopened by +1 threshold: ...") and cancels
the pending BeadSnooze gate in favor of a fresh TaskTriage gate — the two gate kinds
are mutually exclusive, and a task bead never holds more than one pending gate at a
time.
A ready task can also be snoozed directly from its TaskTriage gate's Snooze
option, without a separate CLI call; it uses the same required duration line and keeps
optional feedback separate as the reason — the most common time to defer a task is
exactly when the triage gate is already in front of you.
status:snoozed and -status:snoozed work as query and filter tokens like any other
status. The default bead-list filter (-status:closed) does not hide snoozed beads:
a snoozed task is still live work the user chose to defer, not a black hole.
sase bead list --status snoozed / sase bead search --status snoozed filter to just
those beads.
Beads-pane query property values also accept * wildcards: id:sase-16n.* lists an
epic's phases (see Wildcards). sase bead search is not
a property query: its argument stays a literal substring (or --regex pattern).
Task Corroboration (+1)¶
sase bead +1 records one additional independently attributed report of the same
actionable task. It is evidence, not a generic vote: duplicates share the same
underlying defect/root cause or desired remediation, rather than merely a subsystem or
similar symptom.
sase bead +1 <task-id> --note "<independent reproduction and impact>"
sase bead +1 <task-id> --note "<independent evidence>" --ref <artifact-ref>
The note is required, artifact refs are repeatable, and --author supports explicit
attribution. Each reporter counts at most once, and the task creator does not count as
an additional reporter; a retry is an unchanged no-op that points the reporter to
sase bead note for supplementary evidence. The evidence entries—not a mutable
counter—derive the visible total and machine-readable plus_one_count.
Adding new evidence to an open draft atomically promotes it to ready. A closed task
reopens and promotes only when the reporter's observation window starts strictly after
the current close. SASE CLI callers derive that window from the current agent's
agent_meta.json run_started_at; human callers and --verified-after-close use the
current instant. Non-CLI callers that omit observed_since preserve the legacy
closed-task reopen behavior.
If the report was already in flight before the close, the evidence is still recorded but
the close remains standing. The command prints that the reopen was withheld, appends a
durable note naming the standing close, and surfaces the entry as post-close evidence.
Use --verified-after-close only for an actual reproduction on a tree that already
contains the close. A claimed, ready, in_progress, or snoozed task keeps its
existing status behavior. The same mutation attaches normalized artifact refs, and
plan/phase targets are rejected without writing.
Close History¶
A bead remembers how it was closed even after a later reopen undoes that close. The
current close, when a bead is closed right now, stays exactly where it has always
lived: the flat closed_at, close_reason, and resolution fields. close_history is
strictly the past — an append-only, oldest-first list of close episodes that have since
been undone. This applies to every bead type, including flag-typed task beads, not just
ordinary tasks: non-task beads are reopened by sase bead open and by epic work
preclaims, and "why was this closed before?" is the same useful question there too.
Each record captures one undone close:
| Field | Description |
|---|---|
closed_at |
When the archived close happened |
close_reason |
The free-text reason recorded on that close, if any |
resolution |
done, canceled, or superseded, if recorded |
reopened_at |
When the close was undone |
reopened_via |
plus_one, open, update, or epic_preclaim |
reopened_by |
Who reopened it, populated only for plus_one (see below) |
A record is created whenever a closed bead leaves closed: a qualifying fresh
sase bead +1 on a closed task (plus_one), sase bead open (open),
sase bead update --status moving a bead away from closed (update), and an epic
work preclaim relaunching a previously-closed phase or epic (epic_preclaim). Stale
post-close +1 evidence is retained on the still-closed task and does not archive the
current close into history. Reopening a bead that was never closed adds no record. A
bead closed, reopened, and closed again accumulates one record per undone close;
sase bead show renders them newest first.
reopened_by is populated only for plus_one reopens, because add_task_plus_one is
the one reopen path whose event actor is genuinely the agent that ran the command —
sase bead close/open/update's mutations currently attribute their events to the
bead's creator rather than the acting agent, which is a separate, known defect.
Recording a reopened_by from those paths would confidently print the wrong name, so it
is left unpopulated there instead.
Because the canonical event log already recorded every close and reopen, existing stores
recover close reasons that a reopen previously destroyed automatically, the next
time their events are reduced (for example, on the next mutation, or with
sase bead doctor --fix-projection) — no backfill script is needed.
Every surface that shows a bead's status also shows its reopen history:
- The
↺Nbadge sits next to the+Ncorroboration badge onsase bead show,sase bead list,sase bead ready,sase bead blocked,sase bead searchrows, the sase's TUI beads pane, and the generated bead page lineage roster. - A closed task with stale post-close evidence shows a
+1 after closebadge next to its normal+Ncorroboration badge; the same post-close marker appears in sase's TUI, generated bead pages, and task-triage previews. sase bead show --format fullrenders aPREVIOUSLY CLOSEDsection — placed whereRESOLUTIONsits, aboveDESCRIPTION— with one entry per record, newest first, and the+1 EVIDENCEentry that reopened the bead marked with↺ reopened this task.sase bead show --format json(and other JSON-emitting bead commands sharing the same issue schema) includeclose_historyon the issue object. Eachplus_one_evidenceentry carriesobserved_sincewhen provenance was provided, a derivedrecorded_after_current_closeboolean for post-close evidence, and a derivedreopened_beadboolean for the+1entry that actually reopened the task.sase bead searchindexes archived close reasons, resolutions, and timestamps, so a reason recorded before a reopen is still findable.- sase's TUI beads pane shows the
↺Nbadge on list rows, a "Previously closed" property and a## Previously Closedbody section in the detail pane, and ahas:reopenedfilter label. - Generated bead pages render a
## Previously Closedsection and a**↺ Reopened:**primary fact. - The
TaskTriagegate preview — the highest-value surface, since Launch is its default decision — renders one> [!WARNING]callout per record above the description, newest first, and adds the↺Nbadge to its notification note. A typed task's preview also includes a**Task type:**fact in the metadata block above## Description, for example**Task type:** ≈ \flake``.
Dependencies¶
Dependencies are one-way relationships: issue A depends on issue B. Every edge records the source issue, the target issue, when the edge was added, and who added it. An issue is:
- Unblocked if all its dependencies are
closed. - Shown by
sase bead readyif it is a task with stored statusreadyand all dependencies areclosed. - Blocked if it has at least one dependency with status
open,claimed,ready, orin_progress.
sase bead dep list prints the forward DEPENDS ON view, the reverse BLOCKS view, or
both, including the edge's provenance in --format full. sase bead dep tree walks the
same graph when a one-level detail view is not enough. Removing a dependency appends a
dependency_removed event rather than editing or erasing the original add event, so
history keeps both the mistake and its correction.
Discovered Follow-Up Capture and Triage¶
Unless a prompt forbids bead creation, agents should run /sase_new_task for useful
work discovered outside their current scope. The skill searches every task status and
all in-progress epic plans before allowing a new task:
sase bead create -T 'task(flake)' -t "Fix flaky integration test" \
-w "The retry test flakes under parallel pytest" \
-d "Discovered while landing sase-xy." \
--size small -f node_id=tests/retry.py::test_retry -f evidence='failed then passed'
sase bead update <task-id> -s ready
For a semantic duplicate, the skill uses sase bead +1 and does not create a task. When
an in-progress epic credibly caused the issue—not merely shares its topic—the skill
records a DISCOVERED ISSUE: note on that epic and does not create a task. Both records
are made when both cases apply. Only a genuinely distinct issue becomes a sized draft.
Duplicate detection is not text search alone: the skill also sweeps
sase bead list --type task --since 1w --status all before drafting, because a
duplicate filed hours ago by another agent often shares no term with your query. If the
closest match is a closed task whose close reason declares it retired and forbids +1
(a "retired umbrella"), the skill does not corroborate or reopen it — it routes the
reporter to a new, node-specific task bead instead, then records a typed related
artifact link back to the retired umbrella.
The task stays open while its title, description, size, model, references, and
dependencies are drafted. Marking it ready proposes it to the project owner. The
bead_task_triage job scans enabled projects every five minutes and raises one
human-only TaskTriage gate per ready task bead that has accumulated at least its
effective +1 bar — its task type's own triage.min_plus_ones,
or bead.task_triage.min_plus_ones for an untyped or
unregistered type. A sub-threshold task is withheld from triage without any change to
its stored status, and a gate already raised for a task that later falls below the bar
is canceled and its notification dismissed. The compact [bead] <bead-id> — <title>
notification lands in the Beads panel. Every gate whose subject is a typed task bead
also carries a type chip (glyph + slug) and a second note with the compact typed facts
line. The Markdown preview's metadata block includes a Task type fact such as
**Task type:** ≈ flake next to Size and References. The filing agent travels
with the gate into its Markdown preview when that attribution is known. The job records
pending gates in lane state so later ticks do not repeat the notification, cancels a
pending gate if the bead leaves ready or falls below the +1 bar, defers re-gating
while that task bead's detached launch is still in flight, and uses a new deterministic
generation if the same task becomes ready again or its pending gate needs a
presentation-contract refresh.
The hourly bead_stale_cleanup job is the other half of that bar. Sub-threshold ready
task beads stay ready and stay visible here; they are not closed automatically. Once
at least bead.task_triage.stale_cleanup_min_beads of them
have been below the bar for bead.task_triage.stale_after_days
days, one BeadStaleCleanup gate offers the oldest 50 (naming any remainder) so the
reviewer can close a selected subset as canceled. The job keeps a single pending gate
and cancels it when the backlog drops below the bar. See
Stale Task Cleanup Notification and
the housekeeping lane.
The gate offers three decisions:
- Launch (default) submits an unattributed proc that runs
sase bead work <task-id> --yes-to-all. Optional feedback is appended to the worker prompt. - Close requires feedback and closes the task with that reason and
resolution=canceled. - Snooze requires a wake time, accepts an optional
+Nwake threshold and reason, and moves the task tosnoozed. The next reconciliation replaces the settled triage gate with a snoozedBeadSnoozegate.
Epic phase workers follow a stricter capture rule: they do not create beads. Instead, a
phase worker appends PROPOSED FOLLOW-UP: <one-line summary — detail> to its own bead
with sase bead note. The epic land agent collects those notes, files the worthwhile
proposals as task beads, marks them ready, and records why it declined any others.
External Issue Mirroring¶
The external_issue_mirror AXE job (see
external_mirror lane) keeps every enabled
project's issue tracker mirrored into task beads: each pass diffs the tracker against
local beads on external_ref and creates an explicitly small, open task bead —
never ready — for every uncovered issue, so a first-pass backlog never floods the
TaskTriage gate queue. Run
sase bead sync-external [--project P] [--dry-run] [--full] to trigger or preview the
same pass manually.
Flag-typed task beads are deliberately excluded from external mirroring. Feature-flag
removal is internal SASE hygiene owned by the registry, the flag task bead, and the
FlagTriage gate; it should not become GitHub issue noise.
A mirrored bead carries external_ref (the mirror's idempotency key, project-key
qualified) and a matching bug:<display-name>#<n> entry in refs (the human-facing,
searchable spelling); both normalize to the same identity. A bead that only carries the
bug: ref is a reference, not a mirror, and its status stays human-owned. When the
upstream issue behind external_ref closes or reopens, the mirror closes or reopens the
mirrored bead and appends one attributed note. It appends the note without changing
status when the bead is reference-only, claimed or in progress, has unclosed
descendants, or already matches the upstream state. Disappearances are also note-only
because there is no safe status target. After this reconciliation, the Beads pane's
drift badge means the link is still unreconciled: guard-skipped, reference-only, or
title drift. The external_mirror.issues.filters
surface (empty by default) excludes tracker issues from mirroring by author, label,
title, or state; filters gate creation only, so clearing one re-examines the issues it
previously dropped without deleting a bead that already exists.
Artifact References¶
Every bead can carry a refs list: an ordered, deduplicated set of canonical artifact
references. This is distinct from design. design points to the one plan that
produced the bead; refs can point to many supporting artifacts such as research
reports, explicit files, related beads, agents, Patches, stitches, or configured
document roles.
Reference entries are stored without the prompt-time @ sigil:
research:202607/artifact_capture_and_retention/artifact_capture_and_retention.md
file:default:0123456789abcdef01234567
bead:sase-b7
Write commands parse and normalize references before storing them, deduplicate repeated
entries while preserving first-write order, and do not require the reference to resolve
on the current machine. That matches the durable, cross-machine purpose of the field: a
reference may be valid even when this checkout does not have the sidecar, artifact row,
or agent history needed to resolve it locally. sase bead doctor performs the
resolution audit and reports references with unknown namespaces, missing targets, or
ambiguous targets.
Use typed artifact links, not free-text RELATED: notes, when one bead should carry
relationship context to another:
sase artifact link add bead:<task-id> related bead:<other-bead-id> "<why>"
Use sase bead dep add only when the relationship blocks work scheduling.
Attachments¶
Inline @<path> references in bead note text attach content-addressed file snapshots,
and sase bead attach attaches files without prose. Rendering needs no opt-in: a note
written with attachments renders on every machine.
@in bead notes. Inside note text,@<path>attaches a snapshot of that file. The bead keeps the exact bytes on every machine, even after the file is gone. Accepted forms:@./shot.png,@~/logs/crash.log,@/tmp/trace.json,@docs/plan.md,@"name with spaces.png", or a bare@name.extfor a known file type. Write@@where you need a literal@that would otherwise start a reference.me@host,@large, and@research:…citations never need escaping. A note argument that is only@<file>still reads the note's text from that file, and any@<path>inside that text then attaches. To attach files without prose, usesase bead attach.
Resolution uses the invocation cwd, including text loaded from a whole-argument @file,
with ~ expanded. Path.resolve() follows symlinks, then the resolved regular file is
ingested. Sensitive paths use the core policy plus bead.attachments.sensitive_patterns
unless -S/--allow-sensitive. Directories get the existing tar czf hint. FIFOs,
devices, and sockets are refused. Every problem is collected and nothing is written.
| Rule | Behavior |
|---|---|
| Boundary | @ is significant only at the start of the text or after whitespace, ", ', (, [, {, ,, =, plus <. |
| Escape | Boundary @@ becomes a literal @. The next character is not a boundary. |
| Reuse | @attachment:<name> references an attachment already on this bead, or one attached earlier in the same text by its assigned name. An unknown name is an error. |
| Citation | @<kind>:<arg> for a registered or known document kind stays literal text, as today. |
| Quoted path | @"…" uses the existing quoted-candidate rules (\", \\, never crosses a newline). |
| Path-shaped | @/…, @~/…, @./…, @../…. Also a token containing / whose segments match [A-Za-z0-9._+~-]+. Also a bare stem.ext where ext is in the curated core extension table. Trailing . , ; : ! ? ) ] } > are trimmed. |
| Anything else | Literal (@large, @dataclass, @pytest.mark.asyncio, bare @). |
Every note-bearing verb accepts attachments:
sase bead note <id> <text> [-S/--allow-sensitive] [-L/--local-only] [-K/--private] [-W/--public] [-y/--yes]
sase bead close <id> -n <text> [-S/--allow-sensitive] [-L/--local-only] [-K/--private] [-W/--public] [-y/--yes]
sase bead update <id> -n <text> [-S/--allow-sensitive] [-L/--local-only] [-K/--private] [-W/--public] [-y/--yes]
sase bead +1 <id> -n <text> [-S/--allow-sensitive] [-L/--local-only] [-K/--private] [-W/--public] [-y/--yes]
sase bead attach <id> <file|->... [-a/--author NAME] [-n/--note TEXT] [-N/--name NAME] [-S/--allow-sensitive] [-L/--local-only] [-K/--private] [-W/--public] [-y/--yes]
sase bead attachment list [<id>] [-j/--json]
sase bead attachment open <id> [<name>]
sase bead attachment path <id> <name>
sase bead attachment publish <id> <name> [-y/--yes]
sase bead attachment push [<id>]
sase bead attachment purge <id> <name> -r WHY [-y/--yes]
sase bead attachment prune [-y/--yes]
sase bead attachment unpublish <id> <name> [-y/--yes]
Storage tiers and privacy¶
Bytes never enter the bead store: note events carry only location-free descriptors
(name, sha256, size_bytes, mime_type, image dimensions, machine origin), which
are as public as the bead itself. Bytes live in a local content-addressed cache first,
then in shared stores picked deterministically by size:
- The git tier holds objects up to
bead.attachments.git_max_bytes(default 50 MiB) in a<project>--attachments-privatesidecar repo, one per project. Its local copy is a bare partial clone at~/.sase/projects/<project_key>/repos/attachments-private. Public attachments live in a<project>--attachmentssidecar repo with the same bare layout at~/.sase/projects/<project_key>/repos/attachments. SASE requests private visibility when creating the remote, but does not check the visibility of an existing remote; verify that before using it for attachment bytes. The role is hidden from agent instructions, and descriptors contain no local file paths.sase repo initasks before creating a missing remote. Declining does not create a git store on that machine;repos.sidecar.builtin.attachments-private.disabled: trueprevents sidecar setup and hides the role even when a clone already exists. Use-L/--local-onlyfor an individual attachment that must stay on this machine. See Repository initialization. - The optional rclone large tier (
bead.attachments.large_store) takes larger objects; see below. -L/--local-onlykeeps an object on this machine only, badged⚠ only on <machine>. It is an explicit, visible choice, andsase bead attachment pushpromotes local-only objects later without editing any note.
Uploads never run under the bead lock: the event is appended first, then bytes upload
pre-publication, so other machines never see a note before its bytes. A failed upload
lands in a durable outbox, badged ⇡ pending upload, and drains on the next push, bead
sync, or worker launch. bead.attachments.require_upload: true uploads before the event
instead and aborts with nothing written when the upload fails. An explicit
-L/--local-only still keeps the bytes local in that mode.
Attachment visibility¶
Every attachment gets a SASE audience decision: 🌐 public means readable by anyone who
can read the bead store — treat it as irreversible publication — and 🔒 private stays
on the private attachments sidecar. Use -K/--private to force private and
-W/--public to request public (a human confirms on a TTY, or passes -y/--yes).
Agents can only narrow; only humans widen. Files over
bead.attachments.public_max_bytes (default 25 MiB) never go public. Private protects
bytes, not names or prose: filenames and note text stay as public as the bead itself.
SASE decides in the Rust core from an ordered rule table, and uncertainty resolves to
private. In order: a private bead store forces private; sensitive paths and known-secret
values never go public; an explicit -K, -W, or -L is honored subject to the
widening rules below; files over the public cap stay private; secret-scan hits, private
provenance (owner-only files, ignored files, private or unknown remotes, personal and
config zones), opaque types, and unverified media stay private; tracked files already
public on a public remote, and scan-clean workspace text or self-produced media, go
public; everything else — including stdin — stays private.
Widening a policy-private result with -W/--public refuses for agents (the error names
the exact sase bead attachment publish command to offer through /sase_gate), asks a
human to confirm on a TTY (-y skips the prompt), and always refuses sensitive paths,
known-secret values, over-cap sizes, and private bead stores.
From the Beads pane¶
Select a bead in the Beads pane and press N to open the add-note editor. Earlier notes
remain in place. Add @<path> to capture a file snapshot on save: Tab completes
paths, pasting a single existing file path inserts an attachment reference, and @@
inserts a literal @. Relative paths use the selected bead project's workspace when
available, otherwise the TUI's working directory. Press Ctrl+S or Add note to
save.
Before saving, use Ctrl+T to cycle the audience request through automatic
(Attachments: automatic (SASE decides)), private (🔒 private (narrows freely)), and
public (🌐 public (confirms on save)). Automatic applies the audience policy above;
private requests private storage; public requests publication for the note's new
attachments. Existing attachments keep their audience. This key is configurable as
beads_toggle_note_audience.
Saving with public selected opens Publish attachments. Choose Publish to confirm
or cancel to return to the editor. Confirmation does not override policy: an attachment
that cannot be made public rejects the whole note and reopens the editor with an error.
The dialog currently says policy-blocked files stay private, but the save path refuses
the public request instead. Select automatic or private and save again to avoid
requesting publication; sensitive-path restrictions still apply. Publication is
irreversible. The audience cycle belongs to this add-note modal; outside it, the
Beads-pane action reports Open a bead note (N) first to toggle attachment audience.
The pane's note body lists each attachment with the audience chip from its stored
descriptor (🌐 for public, 🔒 for private or for a descriptor that has no visibility
field). The pane does not probe the content store, so availability badges and thumbnails
stay in sase bead show and the attachment viewer. On save, SASE prepares attachment
uploads before appending the note, then registers uploads for the post-commit outbox,
using the same path as sase bead note. Notes without attachments skip these steps. The
audience policy applies to both interfaces, and agents cannot widen an audience.
Old readers treat every attachment as private: a pre-visibility descriptor (no
visibility field) is always private, which is safe for mixed fleets.
sase bead attachment publish <id> <name> [-y/--yes] widens one note attachment to
public. It is human-only: inside an agent run it is refused unless it runs as an
approved gate option (the /sase_gate path that offers the publish command). It fetches
the bytes, rescans with the current scanner rules, refuses a private bead store,
over-cap sizes, and stored sensitive_path or known-secret-value metadata, previews
exactly what becomes public, warns that publication is irreversible, uploads to the
public <project>--attachments sidecar, and appends NoteEdited with
visibility: public and the note text unchanged. +1 evidence is refused: its manifest
is immutable.
sase bead attachment unpublish <id> <name> [-y/--yes] narrows one note attachment to
private, so agents may run it. It ensures a private copy exists (uploading to the
private store, or keeping the bytes local with a ⚠ only on <origin> warning), removes
the object from the public tree with a chore(attachments): withdraw <sha> commit (no
tombstone, so the private copy stays readable), and appends NoteEdited with
visibility: private. It prints the caveat that forks, clones, caches, and GitHub's
retention of unreachable objects persist, plus the git filter-repo runbook for the
attachments repo only.
Viewing¶
sase bead show draws image previews on a TTY; read, JSON, and piped show never
draw and print absolute, extension-preserving view paths instead. Previews degrade
without ever failing the command: kitty inline pixels, then subpixel cell thumbnails (≤
10 rows), then text cards (≤ 5 dim lines), then chips. Control the mode with
show -i/--images auto|cells|kitty|never or the permanent bead.show.images config
(default auto). sase bead attachment open <id> [<name>] opens one attachment in the
terminal viewer (fetches from the shared store when needed, like path; with no name it
opens the only available attachment). Attachment chips are labeled pager links into the
existing viewer, and attachment:<bead-id>/<name> refs resolve wherever artifact refs
do.
Troubleshooting badges¶
Prose always renders; a preview or fetch failure never fails show/read. Every
attachment line carries an audience badge: 🌐 means public (readable by anyone who can
read the bead store) and 🔒 means private (or a pre-visibility descriptor, which is
always private). Each attachment also carries one availability badge: no badge means
cached, ⇣ not downloaded means above the bead.attachments.auto_fetch_max_bytes cap
(25 MiB; -d/--download lifts it for one invocation, attachment path and
attachment open always fetch), ⇡ pending upload means the bytes have not reached the
shared store yet, ⚠ only on <machine> means local-only, ⧉ on <machine> means
local-only and larger than bead.attachments.git_max_bytes, so it can only be fetched
from that machine (see the %dispatch:<machine> hint), 🔒 no access (<repo>) means
the shared store denied the fetch (no grant on that machine),
⛔ blocked by secret scanning means the push was rejected and the object stays local,
✕ unavailable offline means no reachable copy, (purged) means the bytes were purged
behind a tombstone, and ‼ digest mismatch means the cached bytes failed verification
and were quarantined.
Mixed-fleet upgrades¶
Attachment manifests ride on optional event fields, so old readers ignore them: upgrade
every machine to a build with the attachment wire before writing notes with attachments.
Otherwise an older writer regenerates issues.jsonl without manifests (the event store
stays authoritative); repair the projection with sase bead doctor --fix-projection.
Large-object store (rclone)¶
Objects above bead.attachments.git_max_bytes (default 50 MiB) need the optional rclone
large tier: set bead.attachments.large_store to
{remote: "<rclone remote:path>", max_bytes: 2147483648} (default cap 2 GiB). Placement
is deterministic by size — at exactly git_max_bytes the object still takes the git
tier; one byte over moves to the large tier. Without a tier that accepts the size, the
command fails before writing unless -L/--local-only is given. Uploads stage to a
.partial-<uuid> name, move into place, then verify the remote size (and SHA-256 when
the backend reports one).
Objects at or above bead.attachments.background_upload_min_bytes (default 64 MiB)
upload in the background: the command queues them in the durable outbox, launches a
detached drain worker, and echoes
⇡ uploading in background (1.8 GiB) — sase bead attachment push. The outbox is the
crash recovery — entries left by a dead worker drain on the next push, bead sync, or
worker launch. sase bead attachment push drains every tier with live per-object
progress; require_upload: true stays synchronous instead.
Transfers above 8 MiB draw a Rich progress bar on stderr on a TTY; agents and pipes get only the final echo line.
Remote setup is per machine, in that machine's rclone.conf (RCLONE_CONFIG overrides
the path when set):
# Example: SFTP to a machine you control (private to that host).
[attachments-sftp]
type = sftp
host = attachments.example
user = sase
key_file = ~/.ssh/id_ed25519
shell_type = unix
# Project config (checked in): points at that remote.
bead:
attachments:
large_store: { remote: "attachments-sftp:attachments", max_bytes: 2147483648 }
# Alternative: Cloudflare R2 (S3-compatible, needs credentials).
[r2-attachments]
type = s3
provider = Cloudflare
access_key_id = <R2 token key>
secret_access_key = <R2 token secret>
endpoint = https://<account-id>.r2.cloudflarestorage.com
A local directory path also works as the remote (the acceptance tests use one).
sase doctor -C project.attachment_store (alias attachments.store) warns when the git
sidecar cannot be fetched or the upload outbox still has queued uploads
(sase bead attachment push drains them). When a large tier is configured, the same
check warns if rclone is missing or that remote is unreachable. No current project
skips the check. No attachments-private clone and no large tier also skips it, and
attachments then stay on this machine unless bead.attachments.require_upload: true
rejects the write.
Text viewing rules: prose replaces each @attachment:<name> token with [name]
(filenames stripped of control and bidi characters). The note label gains 📎 N when
that note has attachments. Under the prose, an ATTACHMENTS block lists
name · mime · dims · size · sha256:<12 hex> and, when cached, the absolute view path;
unavailable objects render ✕ unavailable offline and no path. show and read both
use this plain form. Compact list/search rows gain a 📎N suffix only when the total
attachment count is non-zero. Public JSON (issue_to_wire_dict) adds availability
and, when cached, local_path onto each attachment object; the store codec never
carries those keys. JSON contains no bytes and no escape codes. sase bead history full
output joins @attachment: tokens to the bead's current manifests, printing
name (not on the current bead) for a token that is gone.
Creation Reason¶
Every new bead stores a filing reason: why the bead exists. The title names the work,
and the description holds scope and evidence. Close text from sase bead close -r and
the audited reason on sase bead read -r are separate fields.
sase bead create requires -w/--reason on plan, phase, and task beads. The value is
trimmed, must be non-blank, and may be at most 2000 characters. A value of @<path> is
read from that file, the same way --description works. A missing, blank, or over-long
reason is rejected before the store changes.
The reason is fixed when the bead is filed. sase bead update has no flag for it, and
the Beads pane editor does not offer the field.
Beads filed before this field existed store an empty reason. Human detail output omits the section for those beads, and JSON omits the key.
sase bead show and sase bead read print a CREATION REASON section immediately
before DESCRIPTION when the reason is non-empty. That prose wraps with --wrap and
takes the same rich highlighting as the description. --format json includes
issue.creation_reason only when the stored reason is non-empty.
sase bead search indexes the same text as the
creation_reason field. A hit that matches only that field still lists the bead, and
JSON reports creation_reason in matched_fields. Compact search output does not print
a creation_reason: snippet line for that hit.
In sase's TUI, the Beads pane detail shows a Creation reason property and a
**Creation reason:** line in the preview. n opens the create modal for a standalone
task in the selected project; its "Why this bead was filed" field is required and uses
the same 2000-character limit (Ctrl+S creates, Esc cancels). On the Agents tab, a
bead the selected agent created shows the reason as a why: line under that bead in
SASE CONTEXT / ARTIFACTS. A truncated index copy adds … full reason in bead detail.
An assigned bead the agent did not create has no filing-reason line.
Some flows file beads without asking you for -w. Approving an epic records
Approved epic plan <plan-ref> requests this work on the epic bead and
Epic plan <plan-ref> defines phase <id> as one work unit on each phase.
sase flag new records Flag '<key>' needs a removal-tracked bead. External-issue
mirroring records Mirror upstream <ref> as a tracked bead.
The TaskTriage gate preview does not include the filing reason. Open the bead with
sase bead show or the Beads pane to read it.
Creation Time Presentation¶
Every bead's created_at is TEXT NOT NULL in the store schema and is populated on
every bead, always in aware-UTC ISO form (2026-04-28T01:34:17Z). Every surface that
renders a bead also renders when it was created, through the single shared module
src/sase/bead_time_presentation.py, so the glyph, accent color, wording, and timezone
agree everywhere.
Two glyphs carry the vocabulary, both rendered in the muted teal accent #5FAFAF,
deliberately distinct from the bead identity colors (gold ids, blue phase, purple task)
so creation time reads as provenance metadata rather than competing with the title:
| Glyph | Meaning |
|---|---|
⧖ |
created |
✎ |
last updated |
Three density tiers, selected per surface:
- Full — a labeled row on detail surfaces:
⧖ Created 2026-04-28 01:34:17 EDT · 3mo ago - Compact — a glyph-prefixed cell on single-line rows:
⧖ 3mo - Data — the raw stored ISO string, unformatted, on JSON/wire surfaces.
The live-vs-persisted rule governs which form a surface may use: a relative age may
appear only on surfaces that are re-rendered on every read (sase's TUI panes, the BEAD
lane, CLI terminal output). Any surface whose bytes are persisted, hashed, or
reconstructed for validation — the TaskTriage gate preview, bead pages, JSON, the mobile
wire — renders the absolute timestamp only (relative=False), because a relative age
would make those bytes drift as the bead ages and break byte-stability or gate
validation.
Unparseable or empty values render an honest unknown placeholder rather than a
fabricated time; elapsed time is clamped at zero so clock skew renders now instead of
a negative age.
Any new bead-rendering surface must format creation and update time through
sase.bead_time_presentation rather than formatting a timestamp itself. Known,
deliberate exceptions where a bead-rendering surface does not show its own creation
time:
- The dependency-edge
added <ts> by <who>line insase bead dep list/sase bead dep tree— the dependency edge's own timestamp, not the bead's; the bead's creation time appears in the same row as a separate⧖cell. - The artifact-reference completion menu's shared age column, which renders
updated_atfor both bead and agent rows; repointing it would change agent-row semantics for no gain, so the bead's creation age instead rides in the glyph-labeled detail string. - The bead pages Mermaid lineage graph node labels, which stay minimal
(
id: title [status]) for diagram readability; the full instant is one click away in the same page's identity block.
tests/test_bead_time_surface_coverage.py enumerates every covered surface and asserts
each renders a creation time for a fixture bead, so a future surface cannot silently
regress this contract.
Storage¶
Directory Structure¶
When the workspace provider declares in-tree storage, as the built-in bare_git
provider does:
sdd/beads/
config.json # Configuration (issue prefix, counter, owner)
events/
manifest.json # Event-store schema and migration metadata
streams/
<root-id>.jsonl # Canonical append-only event stream
issues.jsonl # On-demand export (gitignored; regenerate with `sase bead export`)
beads.db # Mutation flock (gitignored; never a cache)
Providerless local storage and legacy single-sidecar storage use .sase/sdd/beads/ with
the same structure. Local storage uses the primary workspace; every sidecar layout uses
the active workspace clone and records provider/remote metadata in the primary
workspace's .sase/sdd-store.json.
Split sidecar storage puts bead state in its own <owner>/<repo>--beads repository,
materialized on demand and checked out at <workspace>/sase/repos/beads. That
repository keeps the store at its root rather than under a beads/ subdirectory, so
config.json, metadata.json, issues.jsonl, and events/ sit beside the generated
README.md, assets/, .gitignore, and generated pages/. A split project that has
not been migrated yet still keeps bead state at beads/ in the root of its auto-cloned
--plans repository; the .sase/sdd-store.json record decides which, and only a record
that names a beads sidecar (schema version 3) resolves to the dedicated repository.
See SDD Storage for the record format and the adoption transaction
that performs the move.
Isolating bead state this way gives it its own git history, its own cooperative write lock, and its own repository-health preflight, so hot bead writes no longer serialize behind plan writes and a wedged bead rebase cannot block plan commits or epic approval.
Normal bead commands read and write one store for the active checkout. The exception is
a full bead ID that the active store does not hold: it routes to the owning enabled
project's store, as described in Bead ID Arguments. In in-tree
mode, canonical bead state lives in the current checkout's sdd/beads/events/** event
store plus sdd/beads/config.json. Providerless local commands route to the primary
workspace's .sase/sdd/beads/ store. Sidecar-policy commands first materialize the
provider store, then route to the active workspace clone so an agent in workspace #N
writes its matching .sase/sdd/ checkout, its sase/repos/beads/ clone, or its
sase/repos/plans/beads/ directory. If the event store is absent, reads fall back to
legacy issues.jsonl. Numbered sibling workspaces and legacy stores are not merged into
normal sase bead reads.
sase bead clones the beads sidecar on demand. When the store record names one and
sase/repos/beads is missing or its origin does not match the recorded remote, the
command materializes the clone before serving the request—reads included, since a read
cannot be served from a clone that does not exist. If the clone cannot be made usable,
the command fails with an error naming the repository and its remote. Projects whose
record has no beads sidecar clone nothing extra.
Event Log + Compatibility Projections¶
Rust owns the bead storage/query/mutation path. The append-only event streams are the
canonical git-portable state. issues.jsonl is an on-demand export regenerated from
those streams (never committed), and beads.db is only the mutation flock, never a
cache.
- Writes append canonical Rust events. Event stores never rewrite
issues.jsonlon the per-mutation path;sase bead export -o <path>regenerates it on demand (default: the store'sissues.jsonl), andsase bead doctor --fix-projectionregenerates the local untracked copy. Legacy stores withoutevents/keep writing the tracked projection.beads.dbis touched only as the cooperative write lock. - Reads prefer
events/manifest.jsonplusevents/streams/*.jsonl, falling back to legacyissues.jsonlonly when no event store is present. - Read model. Hot reads are served from a versioned SQLite read model under the
clone's git dir (
<git-dir>/sase/bead-read-model/<key>.sqlite, WAL). A constant-cost freshness token (streams-directorymtime/inodeplus manifest and config signatures) decides whether the cache serves; a full stat sweep runs whenever the token changes, on every write, at least every 60 s, and on demand. Any schema, reducer, or crate version mismatch drops and rebuilds the cache, and any cache error falls back to plain replay. When the sweep finds changes, the cache applies only the appended bytes after each stream's stored length instead of rebuilding, as long as every changed stream is a pure append, no stream vanished, the manifest only grew by stream additions, the parsed config is unchanged, and every new event sorts after the stored merge frontier; any precondition failure rebuilds from a full replay.sase bead doctorprints a cache status line (location, size, generation, last sweep, serve/tail/rebuild outcome counts, last refresh and why), andsase bead doctor --verify-cachecompares the cache against a forced full replay and reports drift. - History replays those same streams in projection order;
sase bead history <id>makes every recorded field revision readable without changing canonical state. - Fresh clones read directly from the tracked event streams and can rebuild the compatibility mirrors on demand.
- Dependency removals are recorded as
dependency_removedevents. During merged replay, a remove sorts after an add with the same timestamp, so add-then-remove deterministically leaves the edge absent. - Closed intervals start with the first close after a bead becomes closed. Later
close events in the same interval are kept in the log but ignored by the projection,
so duplicate closes cannot move
closed_ator erase a close reason. Reopening or otherwise moving out ofclosedends the interval and archives its close metadata as a close-history record instead of discarding it. - Notes are a structured, append-only log, not a free-text blob.
note_appendedevents store only the new entry text; the reducer builds a timestamped, attributed record from the event's own metadata, so concurrent note appends merge as separate records instead of replacing each other.sase bead note --edit/--removerecord their own events and stamp anedited_at/edited_bymarker rather than mutating the original record in place; a retraction stays visible insase bead history. Legacyissue_updated { notes }events (written before this log existed) remain immutable whole-field replacements in the stream; on replay their blob is parsed back into individually timestamped and attributed records instead of one opaque string.
The .gitignore excludes the local beads.db* flock files and the issues.jsonl
on-demand export. The event store and config.json are tracked in git.
Sealed Segments (Gated Design)¶
"Archived" is logical today: the read model never re-parses unchanged closed history.
The physical sealed archive below is not built. It waits behind measured triggers,
and sase bead doctor reports each trigger as OK with its current value or WARN
with a pointer back here:
- Hot stream files warn above 10,000 files in
events/streams/. - Full stat sweep warns above 50 ms for the read-model sweep pass.
- Store working tree warns above ~250 MiB.
The thresholds are core constants (SEAL_WATCH_HOT_STREAM_FILES_WARN,
SEAL_WATCH_SWEEP_MS_WARN, SEAL_WATCH_TREE_BYTES_WARN), computed in Rust and rendered
by doctor, so every frontend reports the same numbers.
When a trigger fires, the gated build seals fully closed root lineages that have been
quiet for at least 30 days and have no live dependents, claims, or pending outbox
entries. Sealed streams move verbatim with git mv into events/sealed/<YYYY-MM>/,
recorded in a SHA-256 manifest plus a sealed index. Thawing replays an overlay stream
while the reducer de-duplicates by event_id; the integrity guard and conflict resolver
compare hot plus sealed; every seal commit carries a parity proof proving the sealed
read matches the pre-seal replay. Compressed bundles (zstd) exist only as off-repo
backups, never as canonical state.
Two triggers are not measurable and stay judgment calls: clone complaints that partial clones cannot fix, and parity proving itself becoming infeasible.
Sync Mechanism¶
sase bead sync stages the bead state in the owning git repo, including events/** and
config.json (issues.jsonl is git-ignored and never staged). For legacy stores
without events/ it first regenerates the tracked projection. The export contains one
JSON object per line, sorted by issue ID for clean diffs; regenerate it any time with
sase bead export.
When both stores exist, the event store wins. Manual edits to issues.jsonl do not
change command output unless the event store is absent.
Publication Verification¶
Agents work in numbered workspace clones that are eventually discarded. A bead mutation that is committed but never pushed therefore does not merely arrive late — it is destroyed. Committing is not sufficient on its own, because the configured push policy can be asynchronous, queued, or aimed at a different checkout than the one holding the commit.
So every bead CLI mutation that creates a commit runs a verification pass after the
normal push policy: it resolves the bead store's own git root, and if that root has a
tracking upstream and still carries unpushed canonical bead commits, it forces one
synchronous push against the checkout holding them and re-verifies. If commits remain
unpublished the command fails with an operator diagnostic naming the unpublished
commit count, the store path and repository, the latest managed sync log, and a literal
git -C <repo> push remediation:
ERROR: <mutation> was committed locally but NOT published.
unpublished bead commit(s): 1
bead store: …/sase/repos/plans/beads
store repository: …/sase/repos/plans
latest managed sync log: …
This mutation exists only in this checkout. It is invisible to everyone else and is destroyed if this workspace is
evicted.
Remediation: git -C … push
A store with nothing of its own to publish to is reported as not applicable and never
fails the command: no git root, no tracking upstream, an in-tree
layout, or a store this checkout may only read (one discovered through a checkout-local
.sase/sdd-store.json record, which refuses mutations outright). --no-push skips the
check along with the push it verifies. The check is also defensive about itself: a
verification that raises is logged and ignored rather than converting an otherwise
healthy mutation into a failure.
Because verifying a close by reading the local store cannot distinguish a published
close from one that will die with the workspace, agent-facing instructions point at the
close command's own exit status and this diagnostic rather than at a follow-up
sase bead show.
Two other surfaces enforce the same invariant:
- The commit finalizer commits leftover bead state as a safety net, then runs the
same verification against the store it committed to. Unpublished state fails the run
through
finalizer_result.json, carrying the full diagnostic instead of reporting success. The failure is raised at the finalizer's return points, so the agent's own commit passes still run first — aborting earlier would strand uncommitted code in the workspace in order to report a bead problem. - Launch-time workspace preparation rescues and evicts a numbered workspace (
#2and above) whose sidecar bead-store clones hold unpublished canonical commits. It publishes synchronously first (one attempt per store per launch, with a bounded wait for a busy sync worker); anything still unpublished is rescued to the durable rescue store outside the workspace — a git bundle of the local-only commits, a binary worktree patch, and amanifest.jsonwith restore commands (git fetch <bundle> 'refs/*:refs/sase/rescued/<stamp>/*',git apply --index <patch>) — plus a recovery ref underrefs/sase/recovery/in the store's own repository, and exactly one inbox notification. Eviction then proceeds; a launch never fails because of leftover state. The guard understands the sidecar layouts —sase/repos/beadsfor a split clone root andsase/repos/plans/beadsfor a combined sidecar — in addition to<repo_root>/beadsand in-tree stores. Ordinary (non-launch) workspace preparation warns and proceeds instead of rescuing; only a store whose recovery ref could not be written stops it too.
Duplicate Bead IDs¶
Two clones minting from their own next_counter can allocate the same bead ID and each
write their own events/streams/<id>.jsonl. A naive merge of that add/add conflict
produces a stream with two issue_created events, which the reducer rejects —
historically wedging the shared store because every sync retry failed the same way.
Conflict resolution relocates one of the two beads instead of failing. The resolver
allocates the relocation ID from a store-wide pool built from config.json's
next_counter plus every stream ID already in the store, so the ID a collision lands on
depends only on the store's contents and not on which clone happens to resolve the
conflict first. Both beads survive under distinct IDs, and the resolver reports
relocated duplicate beads: <old> -> <new> — in its own resolution message and in the
managed sync log under ~/.sase/bead_push_logs/sync-*.log. If a bead you expect is
missing after a concurrent-mint conflict, search those logs for that line: the bead
still exists, under the new ID.
Bead Pages¶
Projects with a hosted beads sidecar can publish one Markdown page per bead. Pages live
in the --beads repository under pages/<root>/, where <root> is the bead ID segment
before the first dot. The root bead renders as pages/<root>/README.md; descendants
render as pages/<root>/<bead-id>.md.
An artifact reference such as @bead:sase-9z addresses that generated page directly.
The payload is the exact bead ID, with no prefix-less shorthand and no #L, #page=,
or #t= fragment support. Addressing is lexical and offline: SASE derives the page path
from the ID without reading issues.jsonl, then reports the page missing if it has not
yet been published. Run sase bead pages refresh --write to publish or repair bead
pages before sharing durable @bead: refs.
Pages are generated projections, not hand-maintained state. They are rebuilt from the
canonical bead event store plus the primary repository's commit history, and they link
to the bead's plan, artifact references, parent and child beads, dependencies,
associated agents, and commits. Current commits use a structured SASE_BEAD=<id> footer
tag instead of a subject-line parenthetical; historical commits with trailing
(<bead-id>) subjects are still recognized when the ID exists in the store. Published
agent and session pages in the agents sidecar link back to the bead they worked, so the
bead↔agent relationship is navigable in both directions.
Each page's identity block also renders the bead's creator as **Created by:** <name>,
between **Owner:** and **Assignee:**. It links to the creator's hosted
agents-sidecar page when one resolves and otherwise renders as inline code with no link;
a bead with no recorded creator omits the fact entirely.
sase bead pages refresh # dry run; writes nothing
sase bead pages refresh --write # write changed pages and commit one beads-sidecar batch
sase bead pages refresh --bead beads-1 # refresh one lineage
sase bead pages refresh --json # machine-readable report
sase bead pages url beads-1.2 # print the hosted URL for one bead
Per-commit publication refreshes the committed bead's lineage after a create_commit or
create_pull_request workflow that carries SASE_BEAD=. The shared pages/README.md
roster is owned by sase bead pages refresh, so regular commits avoid rewriting a file
every active agent could touch. sase bead show <id> prints a PAGE section when the
local sidecar remote and branch resolve to a hosted URL; --format json includes
page_url in the same case. Its CREATED BY section similarly links agent-created
beads to the corresponding hosted agents-sidecar page. An epic agent clan's summary
panel also shows its epic bead's hosted page URL when one resolves; run
sase plan links refresh to repair a plan whose BEAD bullet predates hosted links.
Epic clan summaries place the label and complete URL on one logical line, with no
SASE-authored break or whitespace inside the address. A panel too narrow for the
composed row moves the whole address to the next row flush-left, so terminal URL
matchers and copy/paste always see the complete target.
CLI Commands¶
With no subcommand, sase bead defaults to sase bead list with default options. Use
the explicit sase bead list form when passing list filters.
sase bead apply-status <id> <status>¶
Set one bead's status through the normal bead-store mutation path. This is the
proc-friendly form of sase bead update <id> --status <status>: the full or shorthand
ID routes like any other existing-bead operand, and when the command runs as a durable
proc it writes its typed result to the proc's result sidecar.
| Flag | Description |
|---|---|
-Q, --request-path |
Versioned operation request sidecar; defaults to $SASE_PROC_REQUEST_PATH when set |
-R, --result-path |
Typed result sidecar; defaults to $SASE_PROC_RESULT_PATH when set |
sase bead attach <id> <file|->...¶
Attach snapshots of files to a bead as a new attributed note. The bead keeps the exact
bytes on every machine, even after the file is gone. Use - to read one attachment from
stdin (with -N/--name).
sase bead attach sase-ab ./shot.png
sase bead attach sase-ab -n "Crash trace" ./trace.json
sase bead attach sase-ab -N trace.json - < /tmp/trace.json
sase bead attach sase-ab ./a.png ./b.png
The stored note is the prose (when present), a blank line, and the @attachment:<name>
tokens.
| Flag | Description |
|---|---|
-a, --author |
Author recorded on the entry (default: current agent, else store owner) |
-n, --note TEXT |
Optional prose stored above the attachment tokens; @<path> references inside it attach too |
-N, --name NAME |
Attachment name; valid only with one file, and required when the file is - (stdin) |
-S, --allow-sensitive |
Attach files from sensitive paths |
sase bead attachment list [<id>] / open / path / push¶
List content-addressed attachment snapshots, open one in the terminal viewer, or print
the absolute local view path for one attachment. With no child subcommand,
sase bead attachment delegates to sase bead attachment list. List never fetches;
open and path fetch from the shared store when needed, even above the auto-fetch cap.
sase bead attachment list sase-ab
sase bead attachment list sase-ab --json
sase bead attachment path sase-ab shot.png
list text output shows one descriptor per attachment plus the cached view path or an
unavailable marker. path materializes the extension-preserving local view and prints
its absolute path, or fails with a clear unavailable error when no shared copy exists.
open opens one attachment in the terminal viewer, fetching from the shared store when
needed (with no name, it opens the only available attachment or lists candidates).
push drains the upload outbox and promotes local-only objects to the shared stores
without authoring notes.
sase bead attachment open sase-ab shot.png
sase bead attachment push
sase bead attachment push sase-ab
| Subcommand | Flag | Description |
|---|---|---|
list |
-j, --json |
Emit machine-readable attachment data |
sase bead attachment purge <id> <name> -r WHY / sase bead attachment prune¶
Remove one attachment's bytes everywhere while leaving every bead event untouched
(purge), or evict store-confirmed cached objects to fit the local cache budget
(prune). purge writes a tombstone to each configured shared store, deletes the local
object and its views, and records a local tombstone, so notes keep their text and render
(purged) while fetches refuse the digest. It previews every note that references the
digest, confirms on a TTY (or -y/--yes), and prints the manual git filter-repo
history-erasure procedure, which is never automated. prune prints a dry-run plan by
default and evicts with -y/--yes; pending-upload and local-only objects are never
evicted. sase bead doctor reports token/manifest mismatches, dangling descriptors,
pending uploads, local-only objects, tombstoned references, digest mismatches, and
orphans older than 7 days; sase bead doctor --fix-attachments removes orphans,
re-drains the outbox, and quarantines corrupt objects.
sase bead attachment purge sase-ab shot.png -r 'contains a secret' -y
sase bead attachment prune
sase bead attachment prune -y
sase bead doctor --fix-attachments
sase bead blocked¶
Show all issues that have at least one active (non-closed) blocker. Rows use the same plain status/size gutter as compact list rows, including the collapsed size column when no blocked bead has a stored size.
sase bead +1 <task-id>¶
Corroborate an existing task with independently attributed evidence. --note is
required; a value of @<path> is read from that file. --ref is repeatable and
--author overrides normal current-agent attribution. Each reporter counts once, the
creator does not count, and repeat reporters are unchanged no-ops. New evidence promotes
open tasks to ready atomically. Closed tasks promote only when the report's
observed_since window starts strictly after the current close, archiving close
metadata into close history and clearing the assignee; missing provenance preserves the
legacy reopen behavior. Stale post-close evidence is recorded with observed_since,
shown with the post-close evidence marker and +1 after close badge, and leaves the
close standing. --verified-after-close asserts that the defect was reproduced on a
tree already containing the close and uses the current instant as the observation-window
start. Other active statuses are preserved, and plan or phase beads are rejected. See
Task Corroboration (+1) and Close History.
sase bead close <id> [<id2> ...]¶
Close one or more issues. (A worker's final sase stitch create -B close commit or PR
also closes its assigned in-progress bead — see
Standalone Task Workflow — so an explicit close is usually
only needed for canceling, superseding, or a non-commit resolution.) Every requested
bead is checked before the first write, so a batch either closes completely or leaves
the store untouched. A bead with any non-closed descendant is rejected and names the
unfinished work; phase agents should continue to close only their assigned phase bead,
not the parent epic. Closing does not touch a bead's existing
close history; a new record is only archived there the next time the
bead reopens.
Closing a bead that is already closed exits successfully and reports Already closed
without appending another close event, changing issues.jsonl, or creating a store
commit. If the repeat close includes a note, the note is still appended and committed as
a note-only mutation. If the repeat close supplies an explicit --resolution or
--reason that disagrees with the recorded close, the command exits non-zero before
writing and points at sase bead open or sase bead note as the appropriate remedy.
For an epic plan bead, --phases (-p) closes phase beads by their numeric bead-ID
suffix: for example, sase bead close sase-at -p 1-3,5 closes sase-at.1, sase-at.2,
sase-at.3, and sase-at.5. The epic target itself may be full or shorthand. The
option accepts comma-separated numbers and inclusive ranges, may be repeated, and
requires exactly one epic ID. It never closes the epic itself. A plan-tier, untiered, or
phase target is rejected without writing to the store.
--force is the explicit exception for canceling or superseding an unfinished tree. It
requires a non-empty reason and an explicit canceled or superseded resolution;
--force --resolution done is rejected. A forced close recursively closes the
unfinished descendants with the same non-done resolution, gives each one a close reason
naming the forcing parent, and records the swept descendant IDs in that parent's close
event.
--note appends one attributed note to every explicitly listed bead before any close
events, in the same mutation, so completion evidence and the close land in one commit
and one push instead of two. The note entry is stored as a note_appended event,
matching sase bead note; forced closes apply it only to the listed beads, never to the
swept descendants. Keep sase bead note for mid-work progress notes and for adding
evidence to a bead that is already closed.
Closing a plan or phase bead that still has Justfile --epic-symbol entries keyed to it
(or to a descendant suffix) is rejected before any store write. The error lists the
exact flags. They go stale the instant the bead closes and turn unrelated agents'
just check red, so resolve each symbol or re-key the line to a still-open bead first.
List the entries with sase bead epic-symbols <id>. Already-closed no-ops skip this
check. --force does not bypass it: leftover entries still break the lint gate.
Closing a flag task bead is rejected the same way while the working tree's
src/sase/feature_flags/registry.py still defines a flag naming it
(check_feature_flags rule 7 would turn every workspace red). The registry is parsed
statically, not imported, and the error names each surviving flag key. In the same
change, delete the flag's Off branch, make its On branch unconditional, and remove the
registry entry, or keep the bead open. A tree with no registry file skips the check. The
flag-triage gate's deliberate Close action is unaffected.
Closing a delegated child plan/epic also closes its parent phase automatically once every child of that phase is closed. This upward cascade continues only through phase parents and never auto-closes a parent plan/epic; the parent land agent retains that responsibility. Removing a child epic does not trigger the cascade, so its phase stays open and can be scheduled again on retry.
When a close changes the store, SASE commits it and then publishes it according to
sdd.push_after_commit (default: async), followed by the
publication check that turns an unpublished mutation into a
failure instead of a false success. Use --no-push to keep the commit local while
batching bead mutations, then publish the batch with a later sase bead sync.
| Flag | Description |
|---|---|
-f, --force |
Sweep unfinished descendants; requires a reason and canceled or superseded |
-P, --no-push |
Commit the close locally but skip the post-commit push |
-n, --note |
Append this attributed note to each listed issue before closing it. A value of @<path> is read from that file. |
-S, --allow-sensitive |
Attach files from sensitive paths |
-p, --phases |
Close numbered phases of one epic; accepts comma-separated numbers and ranges |
-r, --reason |
Optional close reason text; required with --force. A value of @<path> is read from that file. |
-R, --resolution |
canceled, done, or superseded; real closes default to done; repeat closes compare only when supplied |
sase bead create¶
Create a new issue. Every create requires -w/--reason; see
Creation Reason.
| Flag | Required | Description |
|---|---|---|
-t, --title |
yes | Issue title |
-w, --reason |
yes | Why this bead was filed. Trimmed, non-blank, at most 2000 characters. @<path> reads it from that file. Distinct from the title and from -d/--description. |
-T, --type |
yes | Bead type: task(<slug>), plan(<file>), plan(<file>,<parent>), or phase(<parent_id>); parent IDs may be full or shorthand. New tasks require a catalog slug; list them with sase bead task-type. Feature flags use sase flag new, not this grammar. |
-f, --field |
no | Task-type field value as k=v (repeatable). A value of @<path> is read from that file. Valid only with -T 'task(<slug>)' |
-d, --description |
no | Issue description; @<path> reads it from that file and @@ escapes a literal leading @ |
-a, --assignee |
no | Assignee name |
-x, --external-ref |
no | Project-qualified external issue identity, e.g. bug:sase#42; accepts a bare number, #N, <project>#N, bug:<project>#N, or a github.com/<owner>/<repo>/issues\|pull/<n> URL, and normalizes to bug:<project-key>#<issue-id>. Must be unique across beads. |
-r, --tier |
no | Plan-bead tier: plan or epic |
-c, --patch / --changespec |
no | Attach a Patch name to a plan bead; --changespec is legacy-compatible |
-b, --bug-id |
no | Bug ID for the attached Patch; requires --patch or --changespec |
-m, --model |
no | Model used when this bead is launched. Provider-qualified (e.g. codex/gpt-6.1-sol) or a configured local alias (e.g. #pro). On epic plan beads this becomes the land-agent model; on phase/task beads it is the worker model. |
-R, --ref |
no | Artifact reference to attach to the bead; repeatable and stored canonically |
-z, --size |
task: yes; phase: no | Phase/task size: xsmall, small, medium, large, or xlarge. It controls model routing and whether large/xlarge work receives a plan-first handoff. Legacy sizeless tasks remain readable. |
Patch metadata is valid only on plan beads. It is used by the epic-approval and
sase bead work flows to keep plan beads linked to the Patch they are intended to
produce.
New beads are attributed to the acting SASE agent (from SASE_AGENT_NAME or
agent_meta.json), falling back to the store owner when no agent identity resolves. A
phase bead always inherits its creator from its parent epic instead of being
attributed independently. A plan bead prefers the proposed_by value
sase plan propose stamps onto the plan file at proposal time, and falls back to the
acting agent when the plan carries none. See sase bead show
for how the resolved creator is displayed.
sase bead dep¶
Inspect and manage dependency edges. With no child subcommand, sase bead dep delegates
to sase bead dep list and prints the same central delegation notice used by other
default-list verbs.
sase bead dep
sase bead dep add <issue> <depends_on>
sase bead dep list [<id>]
sase bead dep rm <issue> <depends_on> [<depends_on2> ...]
sase bead dep tree [<id>]
sase bead dep add 001.2 001.1
dep add makes <issue> depend on <depends_on>. The issue becomes blocked if the
dependency is not yet closed.
dep list prints dependency edges with their blocking state and recorded provenance. A
scoped read, such as sase bead dep list beads-001.2, includes every bead status by
default because closed dependencies are usually what you need to see when explaining
readiness. A store-wide read defaults to open, claimed, ready, snoozed, and
in_progress, matching sase bead list. The -s/--status filter currently accepts
only open, claimed, in_progress, and closed. The current dep list --help and
dep tree --help descriptions still omit ready and snoozed from their stated
store-wide defaults; the five-status list here reflects the runtime behavior.
dep tree walks the dependency graph as a deterministic tree. --direction out follows
what the root waits on, --direction in follows what is waiting on the root, and
--direction both renders both trees. Store-wide trees use the same active-status
default as store-wide dep list; scoped trees include every status by default.
Tree output marks graph states explicitly:
⇡ (shown above)means a shared subtree was already expanded, as in a fan-in diamond.↻ (cycle)means a dependency cycle was detected and that branch stopped.(+N more, use --levels 0)means--levelstruncated descendants.? <id> (not found)means an edge points at an unresolved bead ID.
dep rm removes one or more existing dependency edges from <issue> in one
all-or-nothing mutation. The command records dependency_removed events and then
reports whether the source bead is ready or still blocked.
| Subcommand | Flag | Values | Description |
|---|---|---|---|
list |
-c, --color |
auto, always, never |
Color mode for text output |
list |
-d, --direction |
both, in, out |
Edges to show; defaults to both |
list |
-f, --format |
compact, full, json |
Output format; defaults to compact |
list |
-n, --limit |
non-negative integer | Maximum root beads to print; 0 means unlimited |
list |
-s, --status |
open, claimed, in_progress, closed |
Filter by endpoint/status root (repeatable) |
tree |
-c, --color |
auto, always, never |
Color mode for text output |
tree |
-d, --direction |
both, in, out |
Direction to walk; defaults to out |
tree |
-f, --format |
compact, full, json |
Output format; defaults to compact |
tree |
-L, --levels |
non-negative integer | Maximum levels to descend; 0 means unlimited |
tree |
-s, --status |
open, claimed, in_progress, closed |
Filter by bead status (repeatable) |
sase bead ref¶
Inspect and manage artifact references attached to beads. With no child subcommand,
sase bead ref delegates to sase bead ref list.
sase bead ref add <id> <ref> [<ref2> ...]
sase bead ref list [<id>]
sase bead ref list <id> --resolve
sase bead ref rm <id> <ref> [<ref2> ...]
ref add normalizes every supplied reference, appends entries that are not already
present, and reports a no-op when the bead already carries all of them. ref rm removes
the supplied normalized references and leaves absent entries alone. Both commands record
per-reference events, so concurrent agents attaching different references do not replace
each other's entries.
ref list prints stored canonical references. With --resolve, it also reports where
each reference resolves from the current workspace (or, for a bead routed to another
enabled project, from that project's primary workspace), or that it resolves nowhere.
The optional bead ID scopes the listing to one bead; without it, the command lists beads
in the current store that carry references. Add --json for a stable machine-readable
response.
| Subcommand | Flag | Description |
|---|---|---|
list |
-j, --json |
Emit machine-readable reference data |
list |
-r, --resolve |
Resolve references against the current workspace |
sase bead doctor¶
Run health checks on the beads database. Checks for:
- Missing
config.json, event store, legacy projection, or compatibility cache - Projection drift between canonical events and
issues.jsonl - Redundant close events, including how many landed in the recent diagnostic window
- Invalid events or unreduced orphan phase records
- Uncommitted bead-state changes
- Orphan children (phase or nested-plan beads whose parent is missing)
- Plan-archive sidecar drift: bead-linked plans missing from the sidecar, orphaned legacy plan-link indexes, and canonical plans found only in local plan roots
- Legacy or unresolved
designplan references - Issue prefix leaked as the project's ProjectSpec directory key instead of its
PROJECT_NAME(reported; automatically repaired before the next top-level bead is minted, or repair on demand withsase bead doctor --fix-issue-prefix) - Artifact references with unknown namespaces, missing targets, or ambiguous targets
claimedbeads whose assignee resolves to no agent artifact (reported only; runsase bead open <id>to clear them)openbeads owned by a live agent that has not started work yet (reported only; it means thebead_claim_checksjob is not running or is failing, since it should have claimed them)
If bead commands fail before opening a store, run sase core health first. It verifies
that the required sase_core_rs extension is importable and exposes the representative
bead CLI binding used by the fast path.
--fix-projection previews rows where issues.jsonl differs from replaying the
canonical event streams, then rewrites the projection from the streams after
confirmation. The repair refuses unexpected diffs: row additions or removals, status
changes, fields outside closed_at, close_reason, close_history, and updated_at,
or any closed_at move later. close_history is allowed because the first repair after
upgrading to a sase-core release with close history legitimately materializes archived
records for beads whose close reasons were destroyed by a reopen before sase-core
started archiving them — see Close History. A successful repair
regenerates the local git-ignored export and commits nothing; a second clean run writes
nothing. Use --yes for non-interactive repair after reviewing the preview through an
external approval gate.
--fix-issue-prefix previews and, after confirmation, resets a store's issue prefix on
demand when it was leaked as the project's ProjectSpec directory key (e.g.
gh_bobs-org__bob-cli) instead of its PROJECT_NAME (e.g. bob-cli). The same repair
also runs automatically before the next top-level bead is minted. A deliberately
customized prefix is never flagged. The repair is forward-only: existing bead IDs keep
the old prefix, and only new top-level beads use the corrected one.
--fix-plan-archive previews recoverable plan-sidecar gaps and separates them from
entries that cannot be repaired safely on this machine. A candidate is recoverable only
when its canonical source exists in a resolved local plan root and passes committed-plan
validation. For a bead-owned gap, source frontmatter may omit bead_id or name the
expected bead; naming a different bead makes the candidate unrecoverable. Local-only
canonical plans are reported as drift but are explicitly “not necessarily approved”;
review the preview before applying.
After confirmation, the repair rechecks the complete finding set to catch races, adds a
missing bead_id to a bead-owned local source when required, archives the plan without
overwriting an existing canonical copy, and commits the plans sidecar as
Backfill missing plan archives. When that sidecar has a push remote, success also
requires the commit to be published. Missing, conflicting, or invalid source content is
reported as unrecoverable and is never fabricated. Use --yes only after reviewing the
same preview in automation.
| Flag | Description |
|---|---|
-A, --fix-plan-archive |
Backfill recoverable missing canonical plans into the plans sidecar |
-F, --fix-design-refs |
Repair recoverable legacy design references after confirmation |
-I, --fix-issue-prefix |
Repair a leaked ProjectSpec-key issue prefix to the project name after confirmation |
-P, --fix-projection |
Rewrite issues.jsonl from canonical event streams after confirmation |
-y, --yes |
Apply the requested doctor repair without an interactive confirmation |
sase bead epic-symbols [<id>]¶
List --epic-symbol whitelist entries from the working tree's Justfile (for a full ID
owned by another enabled project, that project's primary-workspace Justfile). With no
ID, every entry is printed. With an ID, only entries keyed to that bead or a descendant
suffix (sase-64 matches sase-64 and sase-64.2) are shown. Land and phase agents
should run this before closing: leftover entries go stale the instant the bead closes,
and sase bead close refuses while any remain.
| Flag | Description |
|---|---|
-c, --color |
Color output: auto, always, or never (default: auto) |
-f, --format |
compact (default) or json |
sase bead history [<id>]¶
Replay one bead's canonical event stream as an ordered, field-level timeline. Compact
output prints the timestamp, actor, operation, and changed field names for each event.
Full output prints every prior and new value, including earlier note revisions that
later updates replaced. JSON emits one envelope with issue_id, schema_version, and
entries. A duplicate close that the reducer treated as inert is labeled as redundant
instead of rendering as an empty change row.
--lost-notes is a historical repair for stores that predate the append-only note log:
it reports notes snapshots whose nonblank text no longer appears in the current notes,
the pattern only the old whole-field sase bead update --notes replacement could
produce. Since notes are structured, append-only records now — appending, editing, and
retracting each keep their own event and never overwrite another record's bytes — new
data cannot lose a note this way; the check exists to find and fix damage already done
to older stores. With no positional ID it scans the whole store; with an ID it checks
only that bead. Findings are sorted by bead ID. Add --restore to preview
provenance-tagged appends, prompt once, and restore every finding through the same
atomic append mutation used by sase bead note. Restoration is idempotent: restored
text is retained by later append snapshots, so a second scan reports nothing.
Non-interactive restoration declines safely unless --yes is supplied, and --restore
without --lost-notes is a usage error.
| Flag | Values | Description |
|---|---|---|
-F, --field |
field name | Restrict to events changing the field; repeatable |
-f, --format |
compact, full, json |
Output format; defaults to compact |
-n, --limit |
non-negative integer | Newest entries to print; omitted or 0 is unlimited |
-l, --lost-notes |
boolean | Report beads whose current notes dropped an earlier revision |
-R, --restore |
boolean | With --lost-notes, re-append findings after one confirmation |
-y, --yes |
boolean | With --restore, skip the confirmation prompt |
sase bead init¶
Initialize the bead store for the current project. In effective in-tree SDD mode this is
sdd/beads/; local and legacy separate-repo modes use .sase/sdd/beads/. Split sidecar
mode uses the root of the --beads repository once the store record names that sidecar,
and beads/ in the --plans repository until then.
A newly initialized store's default issue_prefix is the project's PROJECT_NAME
display name (falling back to the internal ProjectSpec key, then the git remote's repo
name, then the directory name) rather than the raw ProjectSpec key. Stores created
before this change that already leaked the key are forward-repaired automatically before
their next top-level bead is minted. Use sase bead doctor --fix-issue-prefix to repair
one on demand before creating another bead (see
sase bead doctor).
sase bead list¶
List issues with optional filtering. Without --status, the command lists open,
claimed, ready, snoozed, and in_progress issues; pass --status=closed when you
need closed history. When the default active query is empty and no explicit --status
was given, the command falls back to listing closed beads. --status, --type, and
--tier are repeatable.
Compact rows lead with aligned, colored type, task-type, and size indicators ahead of the ID:
{type_glyph}<pad> {task_type_glyph}<pad> {status_glyph} {size_token}<pad> {id} · {title}{ ← parent_id}{ ⧖ age}
▸ ◐ sase-bv · Attribute beads to the agent that created them ⧖ 1d
◆ ≈ ◐ S sase-bt · Fix xdist flake in artifact modal copy shortcut ⧖ 1d
↳ ◐ M sase-bv.3 · Record the creator on every bead creation path ← sase-bv ⧖ 1d
The fixed first column is the bead type. The second column is the task-type glyph for
task beads (≈ flake, ⨯ bug, · untyped, and so on) and is blank-padded for non-task
beads so columns stay aligned. Status follows, then the stored phase/task size. The size
column appears only when at least one listed bead has a stored size, stays blank for
unsized rows, and collapses entirely for listings where no bead is sized. Type,
task-type, and size color are controlled by the same -c, --color option as the status
and ID styles, and the icons and size tokens remain distinct without color. Tier (plan
vs. epic) stays out of this column; it remains visible through --tier,
--format full, and --format json.
| Type | Icon | Description |
|---|---|---|
plan |
▸ |
Plan-like container with a tier; may be a child epic |
phase |
↳ |
Sized executable child within an epic/plan bead |
task |
◆ |
Independent work item; new tasks require a size |
| Size Token | Stored Size |
|---|---|
XS |
xsmall |
S |
small |
M |
medium |
L |
large |
XL |
xlarge |
Summary line¶
Non-empty compact and full listings end with a blank line and one summary line:
{shown}[ {status-adjective}] {type-noun}[ · {type counts}][ · {status counts}][ · {hidden}]
Every count in the summary describes the rows printed above it, except the explicit
hidden clause, which counts matching beads not printed because a limit was active. If
all printed rows share one type, the head folds that type into plan/plans,
phase/phases, or task/tasks and omits the type-count group. If all printed rows
share one status, the head folds that status into open, claimed, ready, snoozed,
in-progress, or closed and omits the status-count group. Mixed listings use the
neutral bead/beads noun and include the needed groups:
6 beads · ▸ 3 ↳ 3 · ○ 5 ◐ 1
2 open beads · ▸ 1 ↳ 1 · 4 hidden
20 closed plans · 5 hidden (--limit 0 shows all)
The hidden clause is N hidden when --limit was explicit. When SASE applied the
default closed-list limit, it is N hidden (--limit 0 shows all). With color enabled,
summary glyphs reuse the row type/status accents, folded type and status words use their
matching accents, counts stay plain, and the hidden clause is dim. --format json does
not print the prose line; its envelope carries the same printed-row counts in by_type
and by_status, including zero buckets for every known type and status.
| Flag | Values | Description |
|---|---|---|
-c, --color |
auto, always, never |
Color mode for compact output |
-f, --format |
compact, json, full |
Output format; defaults to compact |
-n, --limit |
integer | Maximum beads to print; closed listings default to the newest 20, 0 means unlimited |
-S, --since |
DATE |
Only beads created at or after DATE |
-s, --status |
all, open, claimed, ready, snoozed, in_progress, closed |
Filter by status (repeatable) |
-T, --task-type |
catalog slug or untyped |
Filter by task type (repeatable); untyped selects legacy beads |
-r, --tier |
plan, epic |
Filter by plan-bead tier |
-t, --type |
plan, phase, task |
Filter by issue type (repeatable). Flag beads are tasks; use -T flag |
-u, --until |
DATE |
Only beads created at or before DATE |
Active (open/claimed/ready/snoozed/in_progress) listings are unlimited by
default. Whenever the final status scope includes closed and --limit is omitted,
only the newest 20 beads print; pass --limit 0 for the full closed history. When
--since or --until is present, the newest-20 closed default does not apply; an
explicit --limit still wins.
Creation-date filters accept Nh/Nd/Nw, today, yesterday, YYYY-MM-DD, and
YYYY-MM-DDTHH:MM; day-granular --until bounds include the full named day. These CLI
flags bound bead creation time, unlike sase's TUI since: and until: filter tokens,
which bound last activity. A bead with no usable creation timestamp is omitted whenever
a creation-date bound is present.
sase bead note <id> [<text>]¶
Append, edit, or retract one entry in an issue's note log. A bead's notes are a list of
records, not one free-text field: each record carries its own timestamp, author,
text, and (once edited) edited_at/edited_by. sase bead show renders one dated,
attributed block per record, addressed by a 1-based ordinal (#1, #2, …) that shifts
whenever an earlier record is removed — re-read show after any edit or removal before
addressing another one by ordinal.
With no --edit/--remove, text is required and the mutation records a
note_appended event containing only the new entry text; the reducer builds the
timestamped, attributed record from the event's own metadata. The mutation runs
atomically in the Rust bead store, and concurrent note writers merge as separate records
rather than replacing each other. --edit N rewrites record #N's text and stamps
edited_at/edited_by, keeping the original timestamp/author; sase bead show
renders the edit marker alongside the record. --remove N retracts record #N; the
rendered log drops it, but sase bead history still replays the original. --edit and
--remove resolve the ordinal to the record's stable ID before writing any event,
reject an out-of-range ordinal without writing, and are mutually exclusive; --edit
requires text, --remove forbids it.
For the flattened text projection every legacy consumer expects (search indexes, bead
pages, --field notes), see notes_text under
sase bead show.
| Flag | Description |
|---|---|
-a, --author |
Author recorded on a new entry; defaults to current agent, then store owner |
-S, --allow-sensitive |
Attach files from sensitive paths |
-e, --edit |
Rewrite note #N (mutually exclusive with --remove); N is the ordinal from show |
-x, --remove |
Retract note #N (mutually exclusive with --edit); sase bead history keeps it |
sase bead onboard¶
Display a quick-start guide with common command examples, including required task size,
/sase_new_task agent policy, and task corroboration.
sase bead open <id>¶
Reopen an issue with an issue_opened event. Every closed ancestor above it is reopened
in the same mutation, and the command prints the ancestor IDs it changed. Resolutions,
close reasons, and close timestamps are archived into each reopened bead's close history
instead of being discarded — see Close History; the full history also
remains available in sase bead history --format full.
sase bead ready¶
Show task beads whose explicit status is ready and whose dependencies are all
closed. Epic work does not appear: phase beads are preassigned at epic launch rather
than entering a derived ready queue. When no rows qualify, the command prints
No ready task beads (epic work is preassigned at launch). A ready task with an active
blocker remains stored as ready but is omitted until the blocker closes. Rows use the
same plain status/size gutter as compact list rows, including the collapsed size column
when no ready bead has a stored size.
sase bead resolve-conflicts¶
Resolve merge or rebase conflicts in generated bead state from the current store. Only
events/manifest.json, config.json (when only next_counter conflicts), and
events/streams/*.jsonl conflicts are merged automatically; any other conflict is left
for you. See Duplicate Bead IDs for how add/add stream conflicts
are relocated.
sase bead rm <id> [<id2> ...]¶
Remove one or more issues and recursively cascade-delete the union of all their descendants, including phases nested beneath child epics. Every requested ID is validated before anything is removed, so a missing ID leaves the store unchanged. Overlapping or repeated selections remove and print each issue only once. This is irreversible.
sase bead search <query>¶
Find beads whose indexed text fields contain a case-insensitive literal substring by
default. Pass -e/--regex to interpret the query as a regular expression; regex
search is case-insensitive unless the pattern starts with (?-i). Regex matching is
unanchored, and ^/$ anchor the whole field unless the pattern uses (?m). A pattern
that starts with - needs the -- separator, such as
sase bead search --regex -- '-x'. Current indexed fields include ID, title,
description, the filing reason (creation_reason), notes, design/plan path, artifact
references, external issue reference, owner, assignee, model, phase/task size, Patch
name/bug ID, status, type, and tier; timestamps are not searched. Compact output prints
a field-labeled snippet for matches outside the title and description when the renderer
has that field's text. A match that hits only creation_reason still returns the bead,
and --format json lists creation_reason in matched_fields, but the compact snippet
line for that field is omitted. Unlike sase bead list, search includes open,
claimed, ready, snoozed, in_progress, and closed beads by default, so it is
the quickest way to recover older context.
Compact output prints each matching bead with the same type/status/size gutter as
sase bead list, followed by a short snippet. For multi-line fields such as
descriptions or notes, the snippet uses the line that matched the query when possible
instead of always showing the first line. JSON output exposes the exact matched_fields
list for each result.
sase bead search auth
sase bead search '^sase-g' --regex
sase bead search auth --format json
sase bead search auth --format full --limit 3
sase bead search auth --status open --type phase
sase bead search auth --type plan --tier epic
| Flag | Values | Description |
|---|---|---|
-c, --color |
auto, always, never |
Color mode for compact output |
-f, --format |
compact, json, full |
Output format; defaults to compact |
-n, --limit |
non-negative integer | Maximum results; omitted or 0 means unlimited |
-e, --regex |
flag | Interpret the query as a regular expression |
-s, --status |
open, claimed, ready, snoozed, in_progress, closed |
Filter by status (repeatable) |
-T, --task-type |
catalog slug or untyped |
Filter by task type (repeatable); untyped selects legacy |
-r, --tier |
plan, epic |
Filter by plan-bead tier (repeatable) |
-t, --type |
plan, phase, task |
Filter by issue type (repeatable). Flag beads are tasks; use -T flag |
sase bead read <id> [<id2> ...]¶
Read one or more beads with output identical to
sase bead show. Agents consulting beads to do work must use
this command; show is the unaudited human command.
sase bead read sase-64 -r "Need the epic scope"
sase bead read sase-64 sase-65 -r "Need both designs" --format json
-r/--reason is required and must be non-empty. Before printing, the command appends
one audited, agent-attributed read row per resolved bead to artifact_reads.jsonl with
ref bead:<full-id> (shorthand input still stores the full ID) and the trimmed reason —
the same row shape as sase artifact read bead:<id>. .. expansion audits each
expanded bead once, duplicates collapse after resolution so each bead is audited once
per invocation, and unresolved IDs audit only the beads that did resolve. An
audit-append failure prints the error, prints no bead output, and exits 1.
Inside a SASE agent run with an identity, the command also queues one
agent:<name> -read-> bead:<id> graph-edge row per bead; outside an agent run it prints
the one-line "not recorded as a graph edge" note on stderr instead. Link queueing is
best-effort: a failure prints Error: could not record read link: ... but the beads
still print.
sase bead show <id> [<id2> ...]¶
Display complete details for one or more issues including status, type, task type, tier,
parent lineage, dependencies, blockers, typed artifact links, the filing reason when one
was stored, description, the rendered task-type body block, notes, Patch metadata,
model, linked plan path, artifact references, external issue reference, creator, and the
hosted page URL when one resolves locally. See Creation Reason. Full
IDs and shorthand suffixes are accepted. Multiple IDs render in the order given, and
duplicates collapse after resolution, so a full ID and its shorthand render one block.
If one ID in a batch is missing, the beads that resolved still print first; after output
or the pager exits, stderr gets one Error: issue not found: <id> line per miss and the
command exits 1.
Run inside a SASE agent run with an identity (and without SASE_BEAD_SKIP_VIEW_LOG=1),
show refuses before printing anything: it exits 2, prints nothing to stdout, records
no rows, and names the matching sase bead read ... -r "<why>" command on stderr.
Automation running inside agents (symvision, flag checks, commit hooks) sets
SASE_BEAD_SKIP_VIEW_LOG=1, so its show probes bypass the guard. viewed rows
written to ~/.sase/projects/<project>/bead_views.jsonl before this guard still surface
as the weaker viewed verb in sase bead touched and the
Agents-tab Beads: rows; the log is no longer written and is not synced, so another
machine's agent views are not visible here. Use
sase bead read when the consultation should be audited.
An ID ending in .. expands to that bead plus its direct children in one argv token:
sase bead show sase-tt.. is exactly
sase bead show sase-tt sase-tt.1 sase-tt.2 ... sase-tt.8. Expansion is not recursive —
only the target's direct children (phase beads and child epics, one level) are included,
sorted by the integer suffix after the final dot when it parses as digits, with any
unnumbered children kept after those in the order the store returned. A target with no
children is not an error; it renders alone. Expansion happens before the existing
positional dedup, so sase bead show tt.3 tt.. renders sase-tt.3, then sase-tt,
then its remaining children — sase-tt.3 is not repeated. A malformed token such as
.. or tt... reports
Error: invalid ID expansion: '<token>' (expected <epic-id>.., for example sase-tt..)
and exits 1. A stem that does not resolve reports Error: issue not found: <stem> and
exits 1, the same as any other missing ID, while beads that did resolve still print.
Full IDs follow the same cross-project contract described in
Bead ID Arguments: show first asks the current project's bead
store, so a local bead always wins when the same full ID also exists elsewhere. If the
local store misses and the ID has a full project prefix, SASE looks for the exact ID in
enabled projects' canonical bead stores. The fallback is read-only and only runs after a
local miss, so an all-local invocation does not scan the project registry. Shorthand
suffixes such as 1e remain local-only because they carry no prefix to route on.
Use -P/--project PROJECT to pin the entire invocation to one enabled project by
canonical key, display name, or alias. With a pinned project, shorthand suffixes resolve
inside that project's store and prefix routing is skipped:
sase bead show bob-cli-1e # works from another enabled project
sase bead show 1e --project bob-cli
If no enabled project's store holds a full ID, the error remains
Error: issue not found: <id>. If the exact ID exists in more than one enabled
project's store, show reports an ambiguous bead ID with the candidate projects; pin
one with -P/--project. Two projects that merely share a prefix are not ambiguous when
only one of them holds the ID. If the project that looks like the owner (by name, alias,
or stored prefix) has no materialized or readable bead store on this machine, show
names that project and says its store is not available.
show never creates a bead store in the current directory. From a directory with no
store, full IDs still route, but a shorthand suffix needs -P/--project; without it the
command reports Error: no local bead store is available. A multi-ID batch may mix
projects: each bead renders with its own project's plan paths and page URL, and an
<epic-id>.. expansion reads the children from the epic's own store. Aggregate read
commands (list, search, ready, blocked, and stats) stay project-local.
With --format full, a multi-bead batch prints one detail block per bead. Each block is
preceded by a left-aligned ordinal divider such as ── 1/3 ───, styled with the same
additive palette as the rest of the detail view. Single-ID output has no divider and
keeps the same bytes as before. --format compact prints one aligned compact table in
argv order. --format json preserves the existing single-bead envelope for one ID; two
or more IDs emit an array of those envelopes, omitting any misses from the array while
using exit 1 to report that the array is incomplete. An expansion token (<epic-id>..)
always emits the array form too, even when the epic has exactly one child or none, so a
script never has to inspect the store to learn which shape it is parsing.
A typed task prints a Task type row and appends the spec's body template below the
description. When the type is unknown on this machine, the raw field pairs print under a
(not installed on this machine) header instead. An EXTERNAL section (Ref: <value>)
appears only when the bead has an external reference set via -x/--external-ref. The
CREATED BY block localizes an agent's durable global name and links to its hosted
agents-sidecar page when that URL resolves. A human-created bead shows the creator's
email without a link. sase bead list --format full and
sase bead search --format full share the same CREATED BY block but never resolve or
print the hosted-agent link — only sase bead show does. Compact sase bead list/
sase bead search rows never show the creator at all. Closed beads include their
resolution, close reason, and close timestamp; legacy closures without a resolution show
(unrecorded). Phase and task detail views always print a size: they use the stored
value when present and small (default) when it is absent. Legacy sizeless task
launches use the same @small fallback. Any bead's children are grouped as phases (with
status and size) and child epics (with tier and status), including child epics owned by
a phase bead. Nested beads show their complete lineage back to the root plan. A
claimed bead also prints Claimed by: <assignee> (agent has not started working yet).
A task bead of type flag additionally prints a FLAG section with the registry key,
both remove_by thresholds, and derived due state, plus the typed body block for the
three authored prose fields.
Detail resolution — the target issue plus its ancestors, children, dependencies, and
blockers — comes from a single Rust-side store read instead of the three independent
reductions earlier versions performed. Combined with a narrowed CLI parser (only the
bead subcommand tree is built, not every sase subcommand) and memoized
repo-inventory lookups (the creator-URL and artifact-reference-context resolvers no
longer each re-probe git remote and re-merge sidecar config from scratch), this keeps
sase bead show fast regardless of store size or how many other beads reference the
target — including beads with refs, which previously paid the repo-inventory cost
twice.
Typed artifact relationships appear after parent/children/dependency blocks and before
description and notes. They are not scheduling edges: DEPENDS ON and BLOCKS stay
backed only by sase bead dep. Full output shows every link that touches the bead,
including links stored on another bead, document-sidecar rows, and aggregate-only
automatic citations or audited reads. Each row is described from the displayed bead's
perspective using the relation registry: outgoing directed edges keep their authored
slug, incoming directed edges use the registered inverse, and undirected related edges
stay symmetric.
LINKS (2)
→ implements · plan:202608/link-aware-bead-show.md
Lands the approved CLI design.
manual · added 2026-08-22T14:10:00Z by alice.athena.worker
↔ related · bead:sase-a1
Shares the same rendering contract.
migrated · added 2026-08-20T09:00:00Z by alice
REFERENCED BY (1)
← cited-by · agent:alice.athena.reviewer
Prompt citation of bead:sase-b2.
prompt citation · 3 uses · added 2026-08-22T15:00:00Z by alice.athena.reviewer
LINKS holds manual, migrated, and any other non-automatic origin. REFERENCED BY
holds prompt_ref and read origins with friendly labels (prompt citation,
audited read) and accumulated use counts. Unknown legacy origin values print as raw
labels. Identity and provenance rows stay atomic; link reasons wrap with the same
--wrap budget as description prose. Missing optional sidecars mean there are no
external rows. A present but malformed or unsupported link index fails loudly and prints
a hint to rerun with --no-links. Stored issue.refs remain a separate REFS section.
full is the default detail block. compact prints the same single row as
sase bead list and never expands artifact links. json emits a single-bead envelope
with issue, ancestors, children, depends_on, blocks, plan, and an additive
artifact_links array, plus page_url when a hosted page URL resolves and
created_by_url when the creator's hosted agent page resolves; every relationship
reference includes a resolved flag and fixed null-valued fields for unresolved IDs.
Each artifact_links row includes both stored endpoints, the stored and
perspective-aware relations, direction (outgoing, incoming, or symmetric),
counterpart ref, reason, origin, actor, timestamp, and uses. The existing issue.links
outbound storage projection remains unchanged by default. --no-links omits both human
link sections, the top-level artifact_links array, and issue.links.
issue.notes is a breaking change from earlier releases: it is the list of structured
note records (id, timestamp, author, text, edited_at, edited_by), not a
single string. issue.notes_text is new and carries the flattened
[<timestamp> · <author>] <text> projection every prior consumer of issue.notes
actually wanted; a caller that parsed the old string field should switch to
notes_text. sase bead search --format json reports the same flattened text under its
own per-result notes key in matched_fields, unrelated to this detail-envelope shape.
The issue.size key is always present and is null for a bead with no stored size. The
issue.external_ref key is always present and is an empty string for a bead with no
external reference set. issue.creation_reason is included only when the stored filing
reason is non-empty. sase bead list --format json and sase bead search --format json
use this same issue object, so they follow the same rules for size, external_ref,
and creation_reason.
--format full renders a semantically colored, syntax-highlighted detail block
controlled by -s/--style. Styling is purely additive ANSI: stripping SGR escapes from
any styled output reproduces the exact plain bytes, so piping to a non-TTY (as every
agent does) is unaffected. --color decides whether ANSI may be emitted; --style
decides how much styling to apply once that gate is open:
--style |
Meaning |
|---|---|
auto |
Resolve to rich when color is enabled, else plain. Default. |
plain |
No ANSI at all, regardless of --color. |
rich |
Semantic palette plus markdown/code syntax highlighting inside CREATION REASON, DESCRIPTION, NOTES, and evidence. |
--style has no effect on --format json, which is never styled. For
--format compact, plain forces no ANSI while auto/rich enable the compact row's
semantic colors when the color gate is open.
sase bead show sase-64 --style rich --color always
-p/--pager controls paging for long terminal output:
--pager |
Meaning |
|---|---|
auto |
Default. Page only on a real terminal when output is taller than the terminal. |
always |
Page on a real terminal whenever the SASE pager can run, even when output fits. |
never |
Write directly to stdout. |
auto refuses to page when TERM is unset or dumb, stdout is redirected,
SASE_AGENT is set, or the output fits the terminal. The SASE_AGENT guard prevents
launched agents with a TTY from blocking on an interactive pager. When output pages,
sase bead show --format full runs the SASE Pager in-process with one
structured section per bead. Direct output still uses the same flattened bytes, so shell
pipelines remain plain stdout.
sase bead show sase-64 --pager always
CREATION REASON, DESCRIPTION, NOTES, and task +1 EVIDENCE notes wrap at the
configured markdown.print_width total columns by default (88 unless you configure
otherwise); NOTES appears only when the bead has notes. The budget includes the
rendered indent: with --wrap 60, no line in a description block exceeds 60 columns
unless it contains a single token that is longer than the budget. Wrapping is
break-only: short lines are emitted byte-for-byte, existing line breaks are not reflowed
into longer paragraphs, and --wrap none or --wrap 0 disables wrapping. --wrap auto
uses the current terminal width, floored at 20 columns.
The wrapper never splits URLs, inline code spans, Markdown links, autolinks, or ordinary non-whitespace tokens. Fenced code blocks, indented code, tables, tab-bearing lines, structured relationship rows, plan paths, refs, and the title row are left unwrapped.
sase bead show sase-64 --wrap auto
| Flag | Values | Description |
|---|---|---|
-c, --color |
auto, always, never |
Color mode; now applies to --format full too |
-f, --format |
compact, json, full |
Output format; defaults to full |
-N, --no-links |
flag | Skip artifact-link resolution; omit LINKS, REFERENCED BY, and JSON link data |
-p, --pager |
auto, always, never |
Page long terminal output; defaults to auto |
-P, --project |
project key, name, or alias | Resolve every ID against one enabled project's bead store |
-s, --style |
auto, plain, rich |
Styling level for --format full; defaults to auto |
-w, --wrap |
integer >= 20, auto, none, 0 |
Prose wrap width for full output; defaults to markdown.print_width (88) |
sase bead snooze <id> [<id2> ...]¶
Defer one or more open or ready task beads. See
Snoozing a Task Bead for the full workflow. Multiple IDs apply
the same wake time, +1 target, and reason atomically, matching sase bead update's
batch semantics.
| Flag | Description |
|---|---|
-u, --until TIME |
Wake time: a duration (30m, 2h, 1h30m, 3d) or absolute ISO-8601 timestamp; required unless --cancel |
-p, --plus-ones COUNT |
Also wake when this many additional +1 reports arrive |
-r, --reason TEXT |
Why this task is being deferred; embedded in the note this snooze appends. A value of @<path> is read from that file. |
-c, --cancel |
Wake these beads now, returning them to ready |
sase bead stats¶
Show project statistics: total, open, claimed, ready, snoozed, in-progress, and closed counts, plus plan, phase, task, flag, and due-flag counts.
sase bead sync¶
Regenerate the compatibility projection from the canonical event store and stage bead state in git. It does not create a commit; the staged event/projection files are included in the next normal project or SDD commit.
| Flag | Description |
|---|---|
-s, --status |
Check whether bead state has unstaged changes |
sase bead sync-external¶
Run one external tracker mirror pass — the same reconciliation path the
external_issue_mirror AXE job runs every fifteen minutes. See
External Issue Mirroring.
| Flag | Description |
|---|---|
-f, --full |
Force a full repair scan instead of the steady-state short-circuit |
-n, --dry-run |
Show exact planned creations and status transitions without mutating anything |
-p, --project |
Only mirror one project by display name, alias, or canonical key |
sase bead task-type¶
Inspect the effective task-type catalog assembled from builtins, plugins, and project
config. With no subcommand, sase bead task-type delegates to
sase bead task-type list.
sase bead task-type
sase bead task-type list
sase bead task-type list -a
sase bead task-type list -j
sase bead task-type show flake
sase bead task-type show flake -j
list prints a colored table of slug, label, summary, source, and whether agents may
file the type. Agent-uncreatable types such as github are hidden unless -a/--all is
passed. show <slug> prints the label, when_to_use, every field with type,
requirement, role, help, and validators, the body template, the triage threshold, and
provenance. Reads are not audited.
| Subcommand | Flag | Description |
|---|---|---|
list |
-a, --all |
Include agent-uncreatable types |
list |
-j, --json |
Machine-readable catalog |
show |
-j, --json |
Machine-readable spec, fields, template, and provenance |
sase bead touched <agent>¶
List the beads one agent touched, newest touch first, one row per bead with verb chips,
title, and relative age. Durable index rows merge with the legacy, no-longer-written
sase bead show view log (viewed) and audited bead: reads (read), matching the
Agents-tab Beads: sub-section row for row for touched beads. read comes from
sase bead read / sase artifact read bead: with reasons; agents are now refused at
show, so viewed rows predate that guard. Automation running inside agents
(symvision, flag checks, commit hooks) sets SASE_BEAD_SKIP_VIEW_LOG=1 and bypasses the
guard. The CLI lists touched beads only; the panel also marks assigned but untouched
beads own. A bead with read reasons prints one extra indented line with the newest
reason (↳ <reason>); JSON rows carry read_reasons.
sase bead touched bbugyi200.athena.0oa
sase bead touched 0oa -l 10
sase bead touched 0oa -v noted -v closed
sase bead touched 0oa -v viewed
sase bead touched 0oa -v read -j
| Flag | Description |
|---|---|
-j, --json |
Machine-readable rows with actors and read_reasons |
-l, --limit N |
Maximum beads to print; 0 means unlimited |
-v, --verb VERB |
Only show beads with this verb (repeatable) |
When the agent has no touched beads, or none match the -v filter, the command prints
No beads touched by <agent>. (-j prints an empty touches list instead).
Each row leads with one glyph for its strongest verb, shared with the Agents-tab
Beads: rows. The table lists glyphs from strongest to weakest, so a bead that was both
viewed and removed shows ◇:
| Glyph | Verb(s) |
|---|---|
✚ |
created |
✓ |
closed |
↻ |
reopened |
✎ |
updated, noted, ready, snoozed, dep, linked, ref, +1 |
← |
read |
◇ |
viewed (also an own-only panel row) |
⌫ |
removed |
Durable verbs come from a derived per-project touch index
(~/.sase/projects/<project>/agent_bead_touches.json) that is rebuilt from the bead
event streams after sase bead sync, by the periodic artifact-link backfill job, and
after bead mutations that run through SASE's Python mutation path (for example
sase bead create, sase bead close, note-style updates, and bead actions taken from
sase's TUI or gates). Mutations that the Rust fast path handles directly (typically
plain sase bead update, +1, or snooze) do not refresh it. Reading the index never
refreshes it, so rows reflect the last refresh; a missing index lists no durable verbs,
though read and viewed rows still appear. Run sase doctor -C beads.touch_index to
check it: a fresh or not-yet-built index reports OK, a stale index warns and lists the
changed and vanished streams, and an unreadable or wrong-schema index warns that the
next refresh rebuilds it. sase bead sync is a reliable way to refresh it on demand.
sase bead update <id> [<id2> ...]¶
Update one or more fields on one or more issues. Every listed bead receives the same field changes in a single all-or-nothing store mutation: every ID is resolved and every resulting issue is validated before anything is written, so an unknown ID or an invalid field value leaves every named bead untouched. Duplicate IDs, including a shorthand alongside its resolved full form, collapse to a single update. A single-ID invocation is unaffected — same syntax, output line, and commit message as before.
| Flag | Description |
|---|---|
-s, --status |
Change status |
-t, --title |
Change title |
-d, --description |
Change description |
-n, --note |
Append this attributed note to each listed issue. A value of @<path> is read from that file. |
-S, --allow-sensitive |
Attach files from sensitive paths |
-D, --design |
Change plan path |
-a, --assignee |
Change assignee |
-x, --external-ref |
Set the external issue identity (mutually exclusive with -X); see sase bead create for accepted forms. Must be unique across beads. |
-X, --clear-external-ref |
Clear the external issue identity (mutually exclusive with -x) |
-r, --tier |
Change plan tier |
-m, --model |
Change the launch model. Pass an empty string to clear. |
-b, --remove-by |
Extend one flag task bead's removal thresholds as <YYYY-MM-DD>/<release>. Takes exactly one flag-typed task bead ID. |
-z, --size |
Change a phase or task bead's xsmall, small, medium, large, or xlarge size. |
task_type is immutable: sase bead update has no --task-type. An attempt is
rejected with a message pointing at close-and-recreate. The filing reason is immutable
the same way: update has no flag for it. See Creation Reason.
sase bead update --notes was removed: it used to replace the whole note field,
destroying every earlier note. Notes are append-only now, and --note appends the same
attributed record to every named bead inside the same mutation — a batch form of
sase bead note. --notes still parses (hidden from --help) so passing it exits
non-zero with a message pointing at sase bead note (single bead) or
sase bead update --note (batch) instead of failing as an unrecognized argument. Use
sase bead note --edit/--remove to correct or retract an individual record.
Beads whose requested fields already hold the requested values are quiet no-ops: they
are reported as Unchanged, excluded from the commit, and an all-no-op batch writes
nothing. --status closed keeps the descendant guard that prefers sase bead close,
evaluated against the whole batch: a descendant that is itself being closed by the same
invocation counts as closed, so argument order does not matter, but a descendant left
out of the batch still rejects the whole update.
sase bead work <target> [<target> ...]¶
Create or resume one or more epics from validated Markdown plans, launch existing
epic-tier plan beads, launch standalone task beads, or run an ordered mix of those
targets. Each target is treated as a plan file when it ends in .md, contains a path
separator, or names an existing file; other targets are bead IDs whose type selects the
epic or task path. Epic modes run one agent per non-closed, non-delegated phase plus a
final land agent; a retry with authored phases that are all already closed launches only
the land agent. Task mode runs exactly one deterministic worker; see
Standalone Task Workflow for its full lifecycle.
Multiple targets use shell-&& style sequencing: SASE finishes each target before
starting the next, stops at the first error, and does not validate or launch later
targets after that error. Successful earlier targets keep their output and side effects;
there is no batch rollback. A declined launch confirmation still counts the same way it
does for a one-target command: the current target returns success after printing
Aborted., so the next target may run.
Flags are command-wide and are applied independently to every processed target.
Target-local validation is unchanged, so plan-only flags such as --parent,
--artifacts-dir, and --cl-name work for earlier plan files and then stop the
sequence if a later bead-ID target reaches the same incompatible flag. --json prints
one complete result object per processed target. A one-target JSON run is unchanged; a
multi-target JSON run is newline-delimited JSON, one object per line, including the
first failing target's error object when the command stops.
-c/--capacity N is an epic-only invocation control: every selected phase and land
segment emits %queue(capacity=N), except that a segment whose macro claims more weight
gets a budget equal to that weight (ceil(weight)). N is that launch's own admission
budget and must be at least 1; 1 means run alone. Omission preserves default queue
behavior. The option applies to epic bead IDs and epic Markdown plan targets. An
explicit capacity on a standalone task target is an actionable error: earlier successful
targets stand and processing stops. When a segment is raised above N, the work-plan
summary prints Capacity: requested N · <agent> raised to M (queue weight W). The raise
follows the queue_capacity_budget flag, which is on by default; with the flag off,
every segment gets plain N. Before it marks the epic ready, preclaims beads, or spawns
anything, SASE expands each phase and land macro to check the combined queue fields: a
macro that sets its own %queue(capacity=...) conflicts with --capacity, and that
target fails (under --dry-run too) without changing any bead or agent state.
-C/--cl-name NAME retains the existing completion-notification behavior and plan-file
restriction.
-w/--wait SPEC holds launched epic phases until every named dependency finishes.
SPEC is a comma-separated list of agent names and bead=<id> entries; time=,
runners=, priority=, unit=, and proc= are rejected. A parse error exits 2 before
any bead or file mutation. The option applies to plan-file targets and existing epic
beads. Extra waits are appended only to segments whose intra-epic waits_on is empty —
the current root wave, and the land segment when it does not wait on phases — after
those segments' existing wait lines and before their #<macro> line. Dependent segments
inherit the wait transitively and do not repeat it. A bead=<id> entry may name a full
ID from another enabled project; the waiting segment stays parked until that bead's
owning store shows it closed, including while the ID is ambiguous or its store is
unavailable. Shorthand bead= entries keep the launching project's scope.
A full epic or task bead ID from another enabled project launches in that project's
context: SASE reads and checkpoints the owner's bead store, resolves the owner project's
bead-work macros, and prefixes each segment with the owner's VCS workflow and project
name. --dry-run reads the owner's store without materializing it. In plan-file mode, a
parent_bead or --parent that belongs to another enabled project archives the plan
into that project's SDD store and creates the epic in its bead store.
For a task bead, sase bead work <task-id> accepts ready (normal), open (manual
launch), or recoverable in_progress state. It does not launch a duplicate when the
assigned agent is still alive, and it rejects closed tasks. --dry-run prints the
single worker prompt without changing the bead or agent registry. A real launch:
- Force-reuses the task ID as the deterministic agent name after showing or confirming any destructive cleanup.
- Selects the bead's stored model or its size-derived phase-worker alias; missing legacy size normalizes to small.
- Renders one VCS-aware prompt ending in
#bd/work_task:<task-id>, plus#planfor large/xlarge tasks. - Sets
status=in_progressandassignee=<task-id>in one checkpoint commit, publishes it, then launches the worker. - Restores the prior task state if dispatch fails before any runner starts; a live runner keeps the checkpoint.
A launch that can only run on a hard-disabled provider is refused with a
provider-and-expiry diagnostic before any runner starts — the same fail-closed guard
sase run and sase's TUI use. A soft disable does not refuse the launch. See
Provider routing.
The TaskTriage gate's default Launch branch submits this command as an unattributed
proc with --yes-to-all; optional gate feedback is appended to the worker prompt.
Plan-file mode is the canonical epic-approval entry point. It:
- Validates the file against the epic plan schema and reports the complete diagnostics on failure.
- Resolves the project's SDD and bead stores, initializing the bead store when needed.
- Archives the plan under the resolved
{YYYYMM}/plans directory and commits it. - Resumes the linked epic when the archived plan already has a valid
bead_id. - Otherwise creates the epic plan bead from the plan's
title,goal, top-levelmodel, optionalparent_bead, and optional Patch metadata; creates phase beads with their authored sizes inphases[]order; wires everydepends_onedge; and commits the newbead_idlink. - Invokes the existing bead-ID launch path.
A missing phase description becomes a deterministic pointer to the plan and phase ID. A
linked bead_id that no longer exists fails with instructions to remove the stale link
or restore the bead store. Failures before the launch checkpoint is committed remove the
newly-created epic and children and restore the plan link. A publication failure after
the checkpoint preserves the linked, preassigned epic as the safe retry point even
though no runner spawned. If dispatch fails with no runner spawned, plan-file mode
removes a newly created graph and restores the plan link; for an epic that already
existed, it instead restores that epic's prior readiness, assignments, and statuses.
Once a runner has spawned, the linked epic and checkpoint are preserved for recovery and
partial runners are terminated. Every plan-file failure after archiving prints the exact
sase bead work ... --yes command to resume.
When an epic-tier plan is proposed from bead work, sase plan propose automatically
stamps parent_bead from the phase agent's SASE_PHASE_BEAD_ID, from the land agent's
SASE_EPIC_BEAD_ID, or from the task worker's SASE_BEAD_ID. Plan-file mode resolves
that bead and creates the new epic beneath it, yielding recursive IDs such as
beads-001.2.1 or sase-iq.1; an unresolved parent fails with a remedy instead of
silently creating a top-level epic. A task bead that spawned a child epic cannot be
closed until that child epic closes. --parent <bead-id> overrides the authored
association, while --parent top-level explicitly creates an unparented epic. The
override applies only to plan-file targets.
--dry-run plan-file mode validates and resolves the stores, previews the archive
destination, parented epic ID, authored beads, routed models, and dependency waves, and
does not write files, create beads, reserve names, or launch agents. --json prints one
stable object for scripting; successful human output always ends with a grep-friendly
Epic: <id> line used by approval hosts.
Once an epic bead exists, the shared launch path:
- Validates that
<epic_id>resolves to an issue of typeplanwithtier=epic. If the plan is already markedis_ready_to_work, the command treats the run as a retry and schedules any remaining non-closed phases. If the epic has authored phase children but every one is already closed, the retry schedules zero phase agents and still launches or recovers<epic_id>.land; an epic with no authored phase children remains invalid. A phase that owns a non-closed child plan/epic is delegated work already in flight and is skipped until that child closes or is removed; retries therefore do not launch a duplicate phase agent. - On a confirmed launch, force-reuses the deterministic bead-work names —
<epic_id>.<N>(for each open phase),<epic_id>.land(for the land agent), and the legacy<epic_id>land-agent name. It wipes any prior owner of those names that is a completed, failed, dismissed, or planned reservation, or a live worker still waiting to start (waiting owners are stopped). A live owner that is already running is preserved: its slot is not relaunched. This also covers owners that hold the name only as aworkflow_name. If the forced-reuse cleanup cannot complete (a wipe fails or a name is still reserved afterward), the command aborts before mutating any bead state. A waiting phase, land, or task worker is not considered reusable merely because a signal was sent: SASE records explicit kill intent, waits for the old process group to stop, escalates when needed, and only then removes artifacts, releases workspaces, preclaims beads, or launches the replacement. If two waiting turns share the same selected name, every matching turn must be confirmed stopped before the new turn can reuse that name; an unresolved or still-running duplicate blocks the launch.--dry-runperforms no cleanup; it only warns which live agents a real launch would force-reuse.
The itemized preview labels each existing agent with the action cleanup would take —
BLOCKED, PRESERVE, KILL, REMOVE, or RELEASE. Rows are grouped in that
order, then sorted by agent name, and each prints as
ACTION (current-state) agent-name bead=<expected-bead> reason, with the bead=
part omitted when the slot has no expected bead. BLOCKED marks a slot whose
existing owner cannot be safely classified, usually a conflicting bead association,
an unknown live state, or a second still-running turn that holds the same name;
session and clan members reached without their own bead ids are accepted rather than
treated as conflicts. Every blocker is listed instead of aborting on the first one,
and a real launch still stops before any wipe, bead mutation, or spawn. --dry-run
renders the BLOCKED rows, reports how many blockers would abort a real launch, and
still exits 0.
- Flips the epic plan bead's
is_ready_to_workflag toTruewhen it was not already ready. - Builds a Kahn-wave schedule from the epic's schedulable open phase children, respecting dependencies and excluding delegated phases with an open child plan/epic. When every remaining phase is delegated, or when no non-closed phase remains on a retry, only the land agent is launched and remains parked behind the phase beads that have not closed yet.
- Associates each rendered worker with exactly one bead in its
%id: the first phase uses its full agent name plusbead=<phase-id>beside the separate clan declaration, later phases combine their suffix,clan=<epic-id>, andbead=<phase-id>, and the land agent combinesland, the clan, andbead=<epic-id>. - Renders a single
----separated multi-prompt. Each per-phase agent is named<epic_id>.<N>and references thework_phase_beadmacro; a final land agent named<epic_id>.landreferences theland_epicmacro. Every segment joins clan<epic_id>and assigns that whole clan to tribe@epicwith the single%clan(<epic_id>, tribe=epic)directive. Each phase dependency becomes both a%w(<blocker-agent>, for_epic=false)wait on the blocker phase-agent name and a%w(bead=<blocker-phase-id>)closure wait. The explicitfor_epic=falsekeeps intra-epic sequencing agent-only under the flipped follow default. The land agent likewise waits on every launched phase agent and on every authored phase bead, including already-closed or currently delegated phases. Requiring both conditions prevents a phase that delegated to a child epic from releasing dependents merely because its original agent finished; the child epic must land and close the parent phase first. A failed or killed phase keeps dependents and the land agent parked until its agent name is retried successfully and its bead closes.xsmall,small, andmediumphases implement directly with%model:@xsmall,%model:@small, and%model:@medium, respectively. Onlylargeandxlargephases append#planafter their work reference and use%model:@largeand%model:@xlarge. Each size alias resolves directly to its configured target — there is no intermediate hop through a second alias. A stored phasemodelalways wins over the size-derived alias without changing whether the phase receives#plan, and a missing legacy size behaves assmall. The land agent emits%model:<value>when the epic plan bead has a storedmodel. Without one, it emits the configuredllm_provider.epic_lander_model(shipped default@large) belowbead.big_epic_phase_thresholdand the configuredllm_provider.big_epic_lander_model(shipped default@xlarge) at or above the threshold (default5), using the total authored phase count even when resumed work has already-closed phases. These two fields are independent, directly-configured scalars with their own shipped defaults; neither chains through the other or through any other alias. Builtin size aliases can be configured underllm_provider.model_aliases.builtin. Each phase segment and the final land-epic segment carries%auto:tale, so submitted tale implementation and landing plans are approved and archived automatically, while a nested epic plan waits for human approval. An agent may author a tale or an epic as needed; the plan's authoredtierselects the corresponding follow-up path. The land agent prefers a tale for remaining work that one agent can finish. Because nothing resumes the landing after a tale's coder finishes, the land agent triages follow-ups first and writes the epic's closeout into the tale as its final step, while a child epic instead hands the resumed landing to its own land agent throughparent_bead. - Before spawning any runner, batch-preassigns every scheduled phase bead to its
rendered worker and the epic bead to
<epic_id>.land, setting all of them toin_progress. It commits readiness, assignments, and the complete graph as onechore(beads): checkpoint approved epic graph <id>checkpoint. A retry whose graph is already committed may have no new checkpoint commit. Before dispatch, SASE applies the target-specific synchronization rules below. - Dispatches the rendered multi-prompt. Runner-side waiting claims and launch
promotions see their preassignment and become no-ops. Each segment uses a force-reuse
%id(!<agent_name>, bead=<bead-id>)form (withclan=on join segments), so re-runningsase bead workafter a killed or failed run wipes stale name owners before relaunch. The schedule is status-blind and uses agent liveness, which makes the checkpoint safe to retry.
When a phase agent authors an epic-tier implementation plan, that plan parks for human
review under %auto:tale; once approved, the child epic is created beneath the phase
and the phase remains open while delegated work runs. A planner whose phase already
holds an earlier agent's unfinished increment authors a child epic whose phases each fit
one coding agent instead of another single-agent tale. Landing the child epic triggers
the upward close cascade described above, which closes the phase and lets its bead-gated
dependents proceed. Until then, parent-epic retries skip that delegated phase. The land
agent now genuinely requires every phase bead to close; if a phase crashes before
closure, retry or close that phase explicitly rather than expecting landing to sweep it
up.
| Flag | Description |
|---|---|
-a, --artifacts-dir |
Planner artifacts directory to back-fill after an approved epic launch; plan-file targets only |
-c, --capacity |
Epic-only per-launch capacity budget (at least 1); a heavier macro weight floors the segment; omit for default queue behavior; 1 runs alone |
-C, --cl-name |
Patch name for the approved epic completion notification; plan-file targets only |
-n, --dry-run |
Preview the epic graph or task prompt, model routing, and cleanup without mutation |
-j, --json |
Print one machine-readable result object; also implies --yes-to-all |
-P, --no-push |
Skip checkpoint synchronization; a remote-backed detached store stops before spawning |
-p, --parent |
Override a plan file's parent_bead; use top-level for an unparented epic; plan-file targets only |
-y, --yes |
Skip only the launch confirmation prompt |
-w, --wait |
Comma-separated agent names and bead=<id> entries the launched epic phases wait for; plan-file targets and epic beads |
-Y, --yes-to-all |
Skip both the destructive-cleanup and launch confirmation prompts |
Progress, timing, and admission are separate from the dependency schedule. Kahn waves
and %w waits decide order; they do not wait for an LLM or a runner slot merely to
register the remaining deterministic names. Capacity admission is when a spawned child
actually acquires a runner. A fast CLI return means the requested names are reserved and
the children are registered, not that every child has already been admitted to a
provider. already_running retries return before cleanup, bead writes, publication, or
reservations.
Human stderr may print throttled launch_timing target=... stage=... lines while a
launch is in progress (current target/stage and completed/total owners when known).
Those lines are progress, not part of the command's JSON contract: --json still prints
one complete result object per processed target (newline-delimited for several targets)
and is otherwise byte-stable. Enable the same structured stage records at info level
with SASE_BEAD_WORK_TIMING=1 (bead work) or SASE_AGENT_LAUNCH_TIMING=1 (generic
agent launches). Durable stage and summary events also append to
~/.sase/logs/tui_launch_timing.jsonl (overridable with SASE_TUI_LAUNCH_TIMING_PATH).
--dry-run does not create reservations or mutate agent/bead state.
The work macros are resolved by MacroTag (tag-based lookup), so a project-local or
user-defined work_phase_bead, work_task_bead, or land_epic macro overrides the
built-in. For epic-tier work, every phase and land segment carries %auto:tale, so
spawned agents auto-approve submitted tale plans and follow the path selected by the
authored tier, while a nested epic plan parks for human review, without a
human-in-the-loop checkpoint between dependency waves.
When the epic plan bead is attached to Patch metadata (--patch, legacy --changespec,
or --bug-id), sase bead work preserves the current project's VCS context in the
generated prompt. The first phase segment targets the project reference and adds a #pr
reference for the Patch, while later phase and land segments target the Patch ref
directly. For non-Patch epics launched from a known SASE workspace, each segment is
still prefixed with the project's project tag (for example
+sase, which expands to #gh:gh_sase-org__sase); a bead routed to another enabled
project uses that project's tag. If the current directory is not associated with a SASE
project, the prompts are left unprefixed and run in the caller's normal launch context.
If checkpoint creation fails before it commits, the command restores every phase/epic
status and assignee it changed, and restores is_ready_to_work only when this attempt
set it. A detached-store publication failure after the local checkpoint commit stops
before spawning and preserves that checkpoint as the safe retry point; rerun without
--no-push after fixing the remote. For an existing epic, an agent-dispatch failure
before any runner spawns restores the prior assignments and commits the recovery.
Plan-file mode additionally removes a graph created by that invocation and restores its
plan link. A partial-spawn failure SIGTERMs the children it did start and preserves the
preassigned checkpoint for recovery. An epic that was already ready remains ready.
Successful launches do not add a post-launch bead commit: the pre-spawn graph checkpoint
is the complete launch-owned state. The accepted bead.push_after_commit configuration
field is not consulted by this current path. The exact synchronization sequence depends
on the target:
- For a bead-ID target, SASE runs the managed sync worker synchronously after the
checkpoint unless
--no-pushwas passed. A store with no Git remote makes that sync a local no-op. Any reported sync or push error stops the launch before dispatch, including for an in-tree Git store. A remote-backed detached store has the additional requirement that the checkpoint was actually pushed. - For a plan-file target, SASE synchronously publishes a remote-backed detached bead
graph before dispatch. After a successful dispatch it makes a best-effort synchronous
push of the plans store, which publishes the archived plan and its
bead_idlink. A failure in this later plans-store push is a warning, not a launch failure. --no-pushskips these synchronization steps. It is usable only when workers can see the local checkpoint directly; a remote-backed detached bead store exits nonzero before any agent is spawned.
Rust Backend¶
The bead data model, event reducer, JSONL/config codecs, compatibility-cache refresh,
mutation transactions, ID allocation, deterministic work-plan DAG, and common CLI output
planning are implemented in sase-core and exposed through sase_core_rs. Python keeps
the host logic that belongs in the application layer: locating the active bead store,
relativizing plan paths, resolving VCS context and macros for sase bead work,
prompting the user, launching agents, rolling back failed launches, and incrementing
telemetry counters.
Common sase bead commands dispatch through an early CLI fast path before the full
top-level parser is built. Help text and host-coupled commands still fall through to the
normal Python parser/handlers where needed.
Use these checks when changing bead internals:
sase core health -j
pytest tests/test_bead tests/test_core_facade/test_bead_read.py tests/test_core_facade/test_bead_mutation.py
just rust-check
just bead-perf-smoke
Current Checkout Source Of Truth¶
In in-tree mode, sase bead reads and mutations of beads that the current checkout
holds use that checkout's sdd/beads/events/** event store and sdd/beads/config.json,
with issues.jsonl used only as a fallback when events are absent. Running the command
in myproject/ reads that checkout's bead state; running it in myproject_2/ reads
myproject_2/sdd/beads/. The CLI does not merge sibling workspace stores, and duplicate
IDs in another checkout do not override the active checkout's records.
ID allocation also uses only the active store's config.json and canonical event state.
If a sibling checkout has not pulled or merged the latest bead state, it may allocate
IDs based on its local state; sync bead changes through the normal VCS workflow when
several agents are coordinating on the same project.
Cross-project helper surfaces, such as mobile/editor bead pickers, may inspect one canonical store per known project. So may CLI commands, which fall back to those stores for a full bead ID after a local miss (see Bead ID Arguments). None of them merge numbered sibling workspaces or legacy bead stores for the same project.
sase's TUI Integration¶
Plan File Linking¶
When creating a plan bead with --type plan(PATH), the file path is stored in the
design field. sase's TUI can navigate from a bead to its linked SDD file.
For SDD-generated epics, PATH should be the shared plan reference emitted by the plan
approval flow: sdd/plans/... in in-tree mode, .sase/sdd/plans/... in local and
legacy separate-repo modes, or <YYYYMM>/... in the split --plans repository. SASE
resolves those references against the effective SDD root when launching bead work. For
manual commands and prompts, SASE_SDD_PLANS_DIR or sase repo path plans is less
ambiguous than guessing which relative prefix applies.
Task Bead Surfaces¶
sase's TUI Artifacts → Plans pane renders standalone task beads in their own section
with an orchid ◆ type marker and mint ◇ ready state. The detail view labels the type
as task. The s action only changes status; it cycles a task through
open → ready → in_progress → closed → open (claimed → ready) but does not launch a
worker when it reaches in_progress. The e action edits its title and description.
The filing reason stays as filed. n creates a task and requires that reason in the
modal. w launches an open or ready task immediately, and asks before launching an epic
that has phase beads. A phase row says to launch the epic instead. Closed beads, blocked
epics, and epics with no phases are refused with a toast. The same launch is also
available from a TaskTriage notification or sase bead work <task-id>.
Generated bead pages and the mobile bead bridge expose the same literal type and status.
Default non-closed mobile listings include ready tasks. sase's TUI task detail exposes
stored metadata and each dependency's status, but it does not show a reverse blocker
list. When task design metadata is present, the pane shows its plan reference without
loading the linked document. Its shared phase/task presentation also shows the small
fallback for a task with no stored size.
Plan Approval Flow¶
The plan approval popup in sase's TUI includes normal approval and E (Epic) actions.
Normal approval saves to the resolved SDD plans/ directory with tier: tale. Every
epic approval surface behaves the same way — sase's TUI,
sase plan approve --kind epic, Telegram, and bare gate responses all hand
sase bead work <plan-file> --yes-to-all to a detached supervisor that runs it from the
project's primary workspace, then record that the host owns the launch in the planner
response. Epic Custom Approval exposes an optional Capacity control (c) beside Wait:
blank (Default) means omission; an explicit value must be at least 1, and 1 runs
alone. The durable approve result retains that integer so launch argv can emit
--capacity N; tale, reject, and feedback actions never submit it.
The preferred handoff is a monitor turn under the planner's own agent
session, labeled Epic launch · <plan>. The monitor turn reads EPIC APPROVED while
sase bead work runs. After any terminal outcome its configured label is
EPIC CREATED, including when the monitor failed, timed out, was stopped, or was lost;
the monitor's state, bucket, exit code, and output—not that label—show whether the
command succeeded. A successful launch separately attempts to back-fill the created epic
ID; when that metadata lands, the planner row moves to EPIC CREATED, and otherwise the
planner remains EPIC APPROVED. No follow-up agent is recorded because sase bead work
launches the phase agents itself. The monitor takes a zero workspace claim rather than
the planner's, since the launch runs in the primary workspace. When the planner's agent
session cannot be resolved, the same command is submitted as one deduplicated global
detached proc instead.
Either handoff is durable and unowned by any interactive session: it survives the
approving process, and normal command success or failure emits the epic-completion
notification. For a monitor handoff, that notification is held until the monitor turn
itself settles, so opening it shows the settled monitor rather than a running one. If
the process that settles the monitor dies before sending it, monitor reconciliation or
the epic_launch_flush AXE job publishes it later, at most once. Inspect a monitor
through sase monitor list / sase monitor show <id> --follow, and the proc fallback
through every default sase proc list and Procs-tab scope,
sase proc show <id> --follow, and sase proc kill. The approval passes
--artifacts-dir, --capacity N when the durable result set an explicit capacity, and
--cl-name when a Patch is involved, so a successful launch attempts to back-fill the
epic ID and committed plan path into planner metadata.
There is no planner-side subprocess fallback and no foreground path. An absent or
unresolvable planner agent session selects the detached-proc fallback; other
monitor-start errors do not. If the host cannot resolve the primary workspace, finds the
approved-epic plans store unusable, or cannot submit the selected handoff, approval
fails loudly and reports the sase bead work <plan> --yes-to-all resume command rather
than launching invisibly. Immediately after a successful handoff, the planner publishes
its prompt archive entry, finishes as EPIC APPROVED, and does not race the command for
ownership of the epic plan file.