Skip to content

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

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_ones wins. The spec default is 0; among the builtins, flake ships 3 because a test that failed once is the case most often misread as a real defect, bug ships 1 because a defect found while doing something else deserves one independent reproduction before it interrupts anyone, and ci, feature, and memory ship 0.
  • Untyped, or typed with a slug this machine does not have registered — the configured bead.task_triage.min_plus_ones is the fallback. It ships as 1.

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_by thresholds 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 to claimed and 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 the bead_claim_checks reconciler. A bead can therefore turn claimed a 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_progress and 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 open with 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 the bead_claim_checks job 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 by sase doctor instead. 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_checks job — registered under the waits routine — 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 one waits interval. A held claim is recorded in the agent's bead_claim.json artifact file, so an agent that already holds its claim costs the job nothing: it is filtered out without opening a bead store. sase doctor reports the residue in either direction — a claim with no resolvable owner, and a live pre-launch agent whose bead is still open.

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)
  1. 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.

  1. 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.

  1. Triage it. The default AXE checks routine scans enabled non-home projects every five minutes and creates one priority TaskTriage gate for each task whose stored status is ready and that has accumulated at least its effective +1 bar — its task type's own triage.min_plus_ones (0 for ci, feature, and memory; 1 for bug; 3 for flake), or bead.task_triage.min_plus_ones for an untyped or unregistered type. A sub-threshold task is withheld from triage — it stays stored as ready and stays visible to sase bead ready and this triage guide's other commands, only the gate is withheld — and a TaskTriage gate 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 by sase 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 runs sase bead work <task-id> --yes-to-all; Close requires feedback and closes the bead with resolution=canceled and that feedback as the reason. The detached launch survives sase's TUI, CLI, Telegram, or mobile client exit and appears in sase proc list and sase's TUI Procs tab. Snooze requires a wake time, accepts an optional +N wake 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.

  1. 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.

  1. Route the worker model. A task's explicit model wins. Otherwise a stored size selects the corresponding @xsmall, @small, @medium, @large, or @xlarge alias directly; a legacy task without size metadata uses @small. As with epic phases, large and xlarge task 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 ↺N badge sits next to the +N corroboration badge on sase bead show, sase bead list, sase bead ready, sase bead blocked, sase bead search rows, 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 close badge next to its normal +N corroboration badge; the same post-close marker appears in sase's TUI, generated bead pages, and task-triage previews.
  • sase bead show --format full renders a PREVIOUSLY CLOSED section — placed where RESOLUTION sits, above DESCRIPTION — with one entry per record, newest first, and the +1 EVIDENCE entry 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) include close_history on the issue object. Each plus_one_evidence entry carries observed_since when provenance was provided, a derived recorded_after_current_close boolean for post-close evidence, and a derived reopened_bead boolean for the +1 entry that actually reopened the task.
  • sase bead search indexes archived close reasons, resolutions, and timestamps, so a reason recorded before a reopen is still findable.
  • sase's TUI beads pane shows the ↺N badge on list rows, a "Previously closed" property and a ## Previously Closed body section in the detail pane, and a has:reopened filter label.
  • Generated bead pages render a ## Previously Closed section and a **↺ Reopened:** primary fact.
  • The TaskTriage gate 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 ↺N badge 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 ready if it is a task with stored status ready and all dependencies are closed.
  • Blocked if it has at least one dependency with status open, claimed, ready, or in_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 +N wake threshold and reason, and moves the task to snoozed. The next reconciliation replaces the settled triage gate with a snoozed BeadSnooze gate.

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.ext for 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, use sase 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-private sidecar 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>--attachments sidecar 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 init asks before creating a missing remote. Declining does not create a git store on that machine; repos.sidecar.builtin.attachments-private.disabled: true prevents sidecar setup and hides the role even when a clone already exists. Use -L/--local-only for 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-only keeps an object on this machine only, badged ⚠ only on <machine>. It is an explicit, visible choice, and sase bead attachment push promotes 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 in sase 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_at for 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.jsonl on the per-mutation path; sase bead export -o <path> regenerates it on demand (default: the store's issues.jsonl), and sase bead doctor --fix-projection regenerates the local untracked copy. Legacy stores without events/ keep writing the tracked projection. beads.db is touched only as the cooperative write lock.
  • Reads prefer events/manifest.json plus events/streams/*.jsonl, falling back to legacy issues.jsonl only 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-directory mtime/inode plus 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 doctor prints a cache status line (location, size, generation, last sweep, serve/tail/rebuild outcome counts, last refresh and why), and sase bead doctor --verify-cache compares 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_removed events. 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_at or erase a close reason. Reopening or otherwise moving out of closed ends 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_appended events 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/--remove record their own events and stamp an edited_at/edited_by marker rather than mutating the original record in place; a retraction stays visible in sase bead history. Legacy issue_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 (#2 and 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 a manifest.json with restore commands (git fetch <bundle> 'refs/*:refs/sase/rescued/<stamp>/*', git apply --index <patch>) — plus a recovery ref under refs/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/beads for a split clone root and sase/repos/plans/beads for a combined sidecar — in addition to <repo_root>/beads and 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 --levels truncated 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 design plan 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 with sase bead doctor --fix-issue-prefix)
  • Artifact references with unknown namespaces, missing targets, or ambiguous targets
  • claimed beads whose assignee resolves to no agent artifact (reported only; run sase bead open <id> to clear them)
  • open beads owned by a live agent that has not started work yet (reported only; it means the bead_claim_checks job 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:

  1. Force-reuses the task ID as the deterministic agent name after showing or confirming any destructive cleanup.
  2. Selects the bead's stored model or its size-derived phase-worker alias; missing legacy size normalizes to small.
  3. Renders one VCS-aware prompt ending in #bd/work_task:<task-id>, plus #plan for large/xlarge tasks.
  4. Sets status=in_progress and assignee=<task-id> in one checkpoint commit, publishes it, then launches the worker.
  5. 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:

  1. Validates the file against the epic plan schema and reports the complete diagnostics on failure.
  2. Resolves the project's SDD and bead stores, initializing the bead store when needed.
  3. Archives the plan under the resolved {YYYYMM}/ plans directory and commits it.
  4. Resumes the linked epic when the archived plan already has a valid bead_id.
  5. Otherwise creates the epic plan bead from the plan's title, goal, top-level model, optional parent_bead, and optional Patch metadata; creates phase beads with their authored sizes in phases[] order; wires every depends_on edge; and commits the new bead_id link.
  6. 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:

  1. Validates that <epic_id> resolves to an issue of type plan with tier=epic. If the plan is already marked is_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.
  2. 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 a workflow_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-run performs 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.

  1. Flips the epic plan bead's is_ready_to_work flag to True when it was not already ready.
  2. 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.
  3. Associates each rendered worker with exactly one bead in its %id: the first phase uses its full agent name plus bead=<phase-id> beside the separate clan declaration, later phases combine their suffix, clan=<epic-id>, and bead=<phase-id>, and the land agent combines land, the clan, and bead=<epic-id>.
  4. Renders a single ----separated multi-prompt. Each per-phase agent is named <epic_id>.<N> and references the work_phase_bead macro; a final land agent named <epic_id>.land references the land_epic macro. Every segment joins clan <epic_id> and assigns that whole clan to tribe @epic with 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 explicit for_epic=false keeps 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, and medium phases implement directly with %model:@xsmall, %model:@small, and %model:@medium, respectively. Only large and xlarge phases append #plan after their work reference and use %model:@large and %model:@xlarge. Each size alias resolves directly to its configured target — there is no intermediate hop through a second alias. A stored phase model always wins over the size-derived alias without changing whether the phase receives #plan, and a missing legacy size behaves as small. The land agent emits %model:<value> when the epic plan bead has a stored model. Without one, it emits the configured llm_provider.epic_lander_model (shipped default @large) below bead.big_epic_phase_threshold and the configured llm_provider.big_epic_lander_model (shipped default @xlarge) at or above the threshold (default 5), 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 under llm_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 authored tier selects 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 through parent_bead.
  5. 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 to in_progress. It commits readiness, assignments, and the complete graph as one chore(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.
  6. 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 (with clan= on join segments), so re-running sase bead work after 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-push was 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_id link. A failure in this later plans-store push is a warning, not a launch failure.
  • --no-push skips 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.