sonichi / pending-questions
Ask the owner a decision you need, list what is waiting on him, close a question once he has answered, or run the reminder. Use when blocked on the owner's word, or asked "what are you waiting on me for?".
pending-questions
One CLI over the owner's pending questions, and the one store behind them: the
"Pending questions" database in the owner's room. Each question is a row carrying its
Host; a host reads and writes only its own rows, and the owner sets a row's Status
himself (code never writes it — its own marks live in the Recovery and Closed cells).
The room is PENDING_QUESTIONS_ROOM: env, then this manifest's config, then the host's<workspace>/state/pending-questions-room; unset, the owner's DM. Point every host of the
owner at one room shared by him and all his Sutandos. The collab service is the room
capability's own, unless PENDING_QUESTIONS_COLLAB_URL names one (env, then this manifest'sconfig, per skills/MANIFEST.md); the adapter's room_store(..., collab_url=) is the CLI
tier above both.
python3 skills/pending-questions/scripts/pq.py ask "<question>" [--context "<why / options>"] \
[--task-file <workspace>/tasks/<task>.txt] [--urgency live|durable] \
[--default-action "<what Approve does>" --reason "<why>"] [--option "Hold=wait"] \
[--priority High|Medium|Low]
python3 skills/pending-questions/scripts/pq.py list [--json]
python3 skills/pending-questions/scripts/pq.py reconcile
python3 skills/pending-questions/scripts/pq.py resolve <ask-id> [--answered]
python3 skills/pending-questions/scripts/pq.py remind [--force]**Trigger: any time you would write a question for the owner into terminal or summary text, file
it instead** with ask (or scripts/ask-owner.py; add --unless-open to file nothing when an
open row already has that question — it reports already open: [<id>]). The terminal and a compaction summary reach no one.
This skill's hooks/compact-carry.py keeps owner questions out of compaction summaries and, after
one, has the core file any never-asked question it carried; session-state.md shows only the
count (python3 src/pending_questions_reader.py summary).
askreconciles, queues the question to the owner (the task's conversation only for an
owner-tier task in his DM; otherwise his DM), saves it to the outbox, writes the row born
with its**Sent:**record, and once that exact row is confirmed complete commits the
store-history marker and THEN deletes the outbox file — a marker that cannot be written
keeps the file (history: FAILEDin the report) so the local evidence outlives the
failure. If the outbox cannot hold the question, NO row is written: the owner is still
asked, and the report says nothing holds it. Read the output: aFAILEDline is not an
ask. Then continue; never block.listis READ-ONLY — it writes no file, not even the history marker — and lists each ask
id in exactly one bucket, by precedence: a terminal or locally closed row is done; a
complete open row waits; an open row whose body has not landed is stood in for by its held
entry; a held entry with no row waits, marked(not yet in the room), or is done when
closed locally; a local close naming neither is `closed locally; its row is not in view
yet(neither open nor done,pending_closeincount`). When the room cannot be read it
printspending questions: UNKNOWN — room unreachable (…)and only what is held locally;
it never prints a zero it did not measure. Once a row of this workspace was ever confirmed
(written, replayed, ingested or observed by a reconcile), a room that cannot be reached is
an outage, not an empty outbox — whatever became of the one-time introduction message.reconcileis the explicit pass: outbox replay, local close records, stale marks, the
transitional legacy ingest, and the history backfill for rows that predate the marker.
Every step commits the history marker before it releases local evidence (an outbox entry,
a close record, a legacy file entry), and keeps that evidence when the commit fails.ask
andremindrun it;listdoes not.resolvecloses a row as Resolved (or Answered with--answered). It never reopens a
closed row. When the room cannot be written, the closure is recorded locally
(<outbox>/closed/<ask_id>.json) and applied by the nextreconcile— but only for a
question the outbox holds, or in outage mode (a row was confirmed here before); an unknown
id with neither is refused and changes no count. The replay retires a local close only
once its row is closed (or seen closed) AND complete — its body landed; a row the store
view does not show is ambiguous and an incomplete one is still being filed, so in both
cases the record stays andreconcilereports it.remindis the only way a reminder is sent (src/check-pending-questions.py --notify
with this skill's adapter). Nothing is scheduled, and no pass surfaces questions: the
owner is asked once, as questions come up, and reminded when he asks.
The outbox
<workspace>/state/pending-questions-outbox/<ask_id>.json holds a question while the room
is unreachable: one file per ask, written atomically before the room write, never edited,
deleted only when the complete row carrying that exact ask id is confirmed. Its record,
the ask-id grammar and the close record are this skill's (scripts/pending_questions_outbox.py
over core's generic record directory, src/local_record.py: an ask id outside[A-Za-z0-9][A-Za-z0-9._-]{0,119} is refused before any path is built, a file whose stem
does not match its inner id is skipped, and deletion stays inside the directory). reconcile
replays each entry through the normal add_row path (which resumes a row left incomplete), so
a replay is idempotent by ask id. Without a room capability or an owner room, every ask stays
in the outbox and list says why. Two markers under state/ are kept apart:pending-questions-store-history (a row of this workspace was confirmed; from then on no
store is an outage) and pending-questions-db-introduced (the owner was told once where the
database lives).
Who reads it
This skill's adapter, scripts/pending_questions_room_db.py, is the single reader and
writer (room_store, gather, waiting, count, reconcile_pass, resolve, ask_owner,remind); the manifest's pending_questions_store field declares it (skills/MANIFEST.md),
and the store, the outbox and its replay, the queue/routing of the owner message, the legacy
ingest and the reminder are this skill's (scripts/pending_questions_store.py,scripts/pending_questions_outbox.py, scripts/pending_questions_ask.py,scripts/pending_questions_compat.py, scripts/pending_questions_remind.py,scripts/pending_questions_ledger.py). Core — the dashboard, the morning briefing,
agent-api, friction-detector, session-handoff, obsidian-mirror, the reminder entry — reaches
it only through src/pending_questions_reader.py, injecting the adapter src/skill_roots.py
finds by that field across <repo>/skills and <workspace>/skills, and reads no file. Every
one of them shows "unknown" while the room cannot be read — and with no adapter installed,
since core has no store of its own; the briefing says only the count and where to open it.
Without this skill, scripts/ask-owner.py only queues the owner's DM and keeps one generic
record under <workspace>/state/ask-owner/, and says that nothing lists or closes it.
The room capability
The adapter looks for the room capability's scripts (room_collab_client.py,room_collab.py) in the room-commons skill first, then its room-collab alias, under<workspace>/skills/ then <repo>/skills/. Nothing else in the repo depends on either.
Migration from the per-host file
Existing
<workspace>/hosts/<host>/pending-questions.mdfiles are read-only history.
The one transitional reader,scripts/pending_questions_compat.ingest_legacy_file_entries
(called fromreconcile), copies each open entry into this host's row once — an entry
with an**Ask id:**line and a settled**Sent:**record (#5003 and this branch's
earlier heads), a##section without one (main's prose) keyed by a digest of its text,
or main's- **[label, ts]** …bullet — and marks it**Status:** moved — as row q-…in
place ONLY after the row is confirmed complete.Rolling upgrade. While an old head still writes the file, the new head's next
reconcileingests those entries; nothing is lost in the window. The ingest is removed
underdocs/migration-transition-window.md: ~30 days of zero source-side writes, checked
withpython3 skills/pending-questions/scripts/pending_questions_compat.py report(the
last mutation of each host's legacy file; the clock starts at the newest).Persisted schedules. The
pending-questionsentry is gone fromskills/schedule-crons/crons.example.json. An oldcrons.jsonentry that still runspython3 src/check-pending-questions.py(with no flag or the retired--reconcile-only)
now reconciles and lists; it sends nothing.
Testing against an unreachable room
PENDING_QUESTIONS_COLLAB_URL (declared in this skill's manifest.json config block,
empty by default; the env override wins) points the adapter's collab service at that URL
for a run, so an outage can be rehearsed from the CLI:
PENDING_QUESTIONS_COLLAB_URL=https://unreachable.test.invalid \
python3 skills/pending-questions/scripts/pq.py ask "outage rehearsal?" --urgency durable
PENDING_QUESTIONS_COLLAB_URL=https://unreachable.test.invalid \
python3 skills/pending-questions/scripts/pq.py list
python3 skills/pending-questions/scripts/pq.py reconcileask leaves the question in the outbox (recorded: OUTBOX …), list sayspending questions: UNKNOWN — room unreachable (…) with the held question, and thereconcile with the variable unset files it.