Questions

Status: current reference for the implemented queue-only question subsystem. The schema and guarded lifecycle described below are implemented; the final NOT in scope section records rejected or unbuilt ideas.

Design and implementation history (not operating instructions): docs/implemented-plans/questions-end-to-end.md.

Purpose

A question card is the agent borrowing authority it doesn't have — epistemic authority ("is this true / what do you prefer?") or decision authority ("what do I do with this item?"), usually co-occurring. Two things follow from that framing:

Every answer therefore has up to two products:

Never auto-answer. A question that goes unanswered ages out of the active view (see Aging below), but the system never synthesizes a response on the boxholder's behalf — see Prior art in the plan for why (Claude Code's AskUserQuestion timeout default and LangGraph's indefinite-wait interrupts were both considered; this system follows the latter).

Card shape

Schema: src/schemas/question.ts (QuestionSchema, type question).

---
status: pending              # pending | answered | dismissed | expired
memo: >                      # why you're asking — so the boxholder can
                              # answer without looking anything up
prompt: "…"                  # the actual question
input:
  type: select                # select | text | confirm
  options:                    # select only — at least 2, unique ids and
    - { id: finance, label: "Finance" }   # case-insensitively-unique labels
    - { id: _other, label: "None of these — write a directive" }
learning:                    # optional
  sink: guide                 # guide | briefing | personality
  ref: /_config/finance.guide.card   # optional; for sink briefing MUST be the
                                      # root briefing (only it compiles into
                                      # the box CLAUDE.md — see below)
  proposal: >
    Receipts photographed at intake belong in _content/finance/receipts/.
directive: "Move the held file into _content/finance/receipts/."
context:
  - { ref: /_content/inbox/triaged/_unsure/receipt.pdf }
asked-at: 2026-07-10T09:00:00-07:00   # set by the creating template
expires-after: P30D          # optional ISO-8601 duration override
---

Filled in by the answer/dismiss/expire transitions, never by the asker: answer: { text, selected? }, answered-at, answered-via (web | cli), dismissed-at, expired-at. A question's status is single — exactly the fields owned by its current status may be present; a superRefine (refineQuestionLifecycle in question.ts) rejects a card whose bookkeeping contradicts its status. Re-answering a dismissed/ expired question clears the stale dismissed-at/expired-at fields as part of the transition.

Input types and the option-id scheme

Where questions live

All question cards live in _bookkeeping/questions/. This is the only place the system looks for questions: getSystemState (src/core/state.ts) globs _bookkeeping/questions/**/*.question.card to build the pending-question list that feeds the header badge, the questions page, and the aging sweep. A question card written anywhere else (e.g. inside a capture session's .attach/ scope, which scan-import used to do) is invisible to all of that — no page, no badge, no notification, no aging. If a question needs to reference material that lives elsewhere (an attach-scope file, a held inbox item), that's what context: [{ref, text?}] is for — the card itself stays in _bookkeeping/questions/.

Lifecycle

pending ──answer──> answered
   │                    ^
   ├──dismiss──> dismissed ──answer──┘
   │                                 │
   └──(30d)──> expired ──answer──────┘

Before asking something new, agents are expected to check _bookkeeping/questions/ including answered/dismissed/expired cards, not just pending ones — see "The decision rule for agents" below.

Aging: nudge, then expire — never auto-answer

Implemented in src/core/question-aging.ts (ageQuestions), run from the same bbx finalize call site as the pending-question notification sweep (checkPendingQuestionsAndNotify, src/core/question-alert.ts) — both ride bbx finalize's existing per-wakeup cadence.

The answer round-trip

Answering, dismissing, and expiring all go through the same guarded transition helper: withQuestionTransition in src/core/commands/question-transition.ts.

Why a guard, not just withCardLock. withCardLock (src/lib/card-lock.ts) only serializes read-modify-write within one Node process; CLI answer, web answer, bbx finalize, and the scheduler are different processes. withQuestionTransition composes two locks:

  1. The cross-process file lock (src/lib/file-lock.ts, one lock file per question under .beebox/question-locks/) serializes across processes.
  2. withCardLock serializes overlapping in-process callers on top.

After both locks are held, the card is re-read and its status re-checked against the caller's allowedStatuses — so a transition that lost a race (another process already committed a different status) sees the winner's committed state and is rejected with a clear error, rather than clobbering it.

One commit, write-order matters. Inside the guard, the caller's plan returns a list of writes plus a commit message/trailers. applyAndCommit writes every file, then commits them all in one stageAndCommitPaths call. For answer, the plan lists the follow-up job file before the answered card — the card's status flip is the commit point, so the companion job must already exist on disk when it lands. A crash between the two writes leaves job + still-pending-question (harmless — answering again just creates a second job), never the unrecoverable answered-card-with-no-job state that existed before this guard.

Rollback on commit failure. The filesystem writes aren't atomic with the git commit. If stageAndCommitPaths fails, applyAndCommit unstages the paths and restores every touched file to its pre-write snapshot (or deletes it, if it didn't exist before) — so a failed transition leaves the card exactly as it was on disk and stays retryable, instead of stranding an answered-on-disk card that rejects retries.

Status guards per transition:

CommandAllowed fromSetsFollow-up job
answer (src/core/commands/answer.ts)pending, expired, dismissedanswered, answer, answered-at, answered-viaYes
dismiss (src/core/commands/dismiss.ts)pendingdismissed, dismissed-atNo
aging sweep's expire (src/core/question-aging.ts)pendingexpired, expired-atNo

Follow-up job: the two-product contract

Answering creates a question-followup-job card (src/schemas/question-followup-job.ts) carrying question-ref, directive, the resolved answer text, and (when present) the question's learning passthrough. Its instructions tell the executing agent to do, in order:

  1. Execute directive: using answer:.
  2. If learning: is present, record it in learning.sink as a source: user-stated belief — quote the answer, ref the question card (same evidence discipline as the retrospective's integrate step). A "no" is also learning: record the decline against the proposal rather than dropping it. For sink briefing, only the root briefing compiles into any agent's context (compileBriefings only compiles the root — directory briefings are an explicit TODO, src/core/docs-gen/compile.ts:74-101), so learning.ref must resolve to the root briefing even if it names something else; the job instructions say to resolve and note the substitution.
  3. If learning: is absent, still ask whether the answer generalizes — many answers are precedent even when the asker didn't declare a destination. Record it if it does; skip if it's genuinely a one-off (the common, correct outcome — not a failure).

This is where learning.sink/source: user-stated actually lands: the follow-up job is the only writer of the belief, and it always writes with that source tag, never a higher-confidence one — a question answer is a boxholder statement, which is exactly what user-stated means in the retro evidence model.

Surfacing

The decision rule for agents

Where an agent is deciding whether to ask, and how, is the operative question — not a separate policy from the lifecycle above. Stated in questionsSection (src/core/agent-guide/cards.ts), which is always-on agent-guide context:

NOT in scope

See docs/implemented-plans/questions-end-to-end.md for the full rationale, prior art, and failure-mode analysis behind these decisions.