Skip to content

feat: answer adopted-session questions via deny-feedback (Journey 2) - #21

Merged
PouyanJay merged 7 commits into
mainfrom
feat/adopted-sessions-questions
Jun 30, 2026
Merged

feat: answer adopted-session questions via deny-feedback (Journey 2)#21
PouyanJay merged 7 commits into
mainfrom
feat/adopted-sessions-questions

Conversation

@PouyanJay

Copy link
Copy Markdown
Owner

Journey 2 — answer adopted-session questions via deny-feedback (Phase 3, Tier 3)

Builds on Journey 1's adopted-sessions plumbing (PR #20). When an externally-started Claude Code session
(plain terminal / IDE sidebar) raises a multiple-choice AskUserQuestion, telecode now lets you answer it
remotely from the dashboard. The PreToolUse hook intercepts the question, forwards it to the browser, and
relays the human's pick back to the model as deny-feedback — framed as a relayed user answer so the model
adopts it. This is best-effort (AD-4), honestly labelled as such in the UI.

Spike-proven and re-verified end-to-end this pass in a real claude -p session: AskUserQuestion fired the
hook, the injected "chose: Postgres" was adopted, and the model replied "You chose Postgres".

What's in it

  • Protocolagent.question + question.answer wire messages, mirroring the empirically-captured
    AskUserQuestion tool_input shape (no allowsOther — "Other" is always implicitly offered as free text);
    a question transcript-entry kind for backfill.
  • Daemon — intercept AskUserQuestion before the gate → defensive questionsFromToolInput (fail-closed
    ask on unparseable input) → forward agent.question, block on pendingQuestions, return
    permissionDecision: deny carrying the pick (buildQuestionDenyReason). Fail-closed on no-answer / stop /
    interrupt / end (never auto-answers).
  • Relay — route agent.question like agent.permission_request (awaiting_input + cache + broadcast) and
    question.answer like permission.decision (running + forward), staying payload-blind (E2E ciphertext).
  • Web — reducer (question kind, 4 answer states, verification-gated), store/client answer(), and a new
    QuestionGate.svelte card (enterprise-ui: radio/checkbox + always-available "Other" free text; honest
    best-effort labeling; pending / answering / answered / closed states).
  • Refactor — the deferred Journey-1 AD-J1-1 one-export-per-file split of the adopt modules.

Tests & gates

  • 575 tests pass: protocol 83 · daemon 144 · relay 125 (real Postgres) · web 223. typecheck · eslint ·
    prettier · svelte-check (0/0) all green.
  • Daemon integration tests drive the full hook-bridge → socket → daemon → relay path, with parametrized
    variant coverage (single / multi / Other / multi-question), fail-closed paths, no-deadlock guards
    (stop + interrupt), and an explicit invariant-Professional README + structured docs (with dedicated E2E & pairing guides) #5 agent.question-is-ciphertext test.

Review

3-agent code review (typescript + test + clean-code) — all PASSED. Findings fixed in c8b6265 (notably a real
stopTurn deadlock on interrupt/end with a pending question, now fail-closed with a regression test).

Notes

  • Free-form questions (model ends its turn asking, no tool) remain a hard wall — no hook fires; the dedicated
    "answer on " UI state needs the Notification(idle) signal and lands in Journey 3 (AD-J2-5).
  • The daemon needs a @telecode/cli republish for adoption to work on the live deployment.

…ey 2, Task 1)

Add the adopted-session question messages for Phase 3 (answer Claude's
multiple-choice questions remotely via deny-feedback):

- agent.question (daemon -> web): mirrors the captured AskUserQuestion
  tool_input (questions[] with header, multiSelect, options[label,
  description]); description optional for version-drift safety; bounded
  caps. No allowsOther flag - Claude Code always implicitly allows
  'Other', so it is carried only on the answer as free text.
- question.answer (web -> daemon): one entry per question with
  selectedLabels[] and/or otherText; refined to reject an empty answer.

Both added to MESSAGE_TYPES and exported from the package entrypoint.
11 new protocol tests (82 total pass).
Resolve the deferred Journey-1 review finding by splitting the cohesive
multi-export adopt modules so each file has a single public export:

- hook-event.ts -> keep the input schema; move preToolUseOutput +
  HookPermissionDecision to pretooluse-output.ts (input vs output).
- transcript-mirror.ts -> keep the reader/factory; move the defensive
  JSONL parser transcriptEntriesFrom to transcript-parse.ts.
- hooks-install.ts -> split into telecode-hook-command.ts (recognizer),
  claude-settings.ts (settings.json read/write/strip substrate),
  hooks-install.ts (installHooks), hooks-uninstall.ts (uninstallHooks),
  hooks-status.ts (readHooksStatus). TELECODE_HOOK_EVENTS made private.

Pure refactor: importers (daemon.ts, main.ts) and tests updated; the
hooks test is renamed hooks-settings.test.ts (it covers the whole
lifecycle). All 127 daemon tests pass; typecheck/lint/format clean.
…rney 2, Task 3)

Intercept AskUserQuestion at the PreToolUse hook for an adopted session
and answer it remotely (Phase 3, Tier 3):

- handleHookEvent routes tool_name === 'AskUserQuestion' to a question
  path BEFORE the permission gate: parse the tool_input defensively
  (questionsFromToolInput), forward a structured agent.question to the
  browser, block on the new pendingQuestions map until question.answer,
  then return permissionDecision:deny carrying the user's pick framed as
  a relayed user answer (buildQuestionDenyReason, AD-4/AD-J2-3).
- New handleFrame case for question.answer resolves the pending question,
  records the answer on the question transcript entry, and resumes the
  session (with the same settled-gate reconciliation as permissions).
- Fail-closed (AD-2/AD-J2-4): unparseable input or no remote answer
  (daemon stopping) returns 'ask' so Claude Code defers to its local
  picker; stop() releases pending questions (J1 deadlock guard).
- Protocol: add a 'question' transcript entry kind so a question is
  recorded for backfill (pending vs answered).

New pure helpers are one-export modules with unit tests. Daemon
integration tests cover the round-trip, fail-closed, and no-deadlock.
83 protocol + 137 daemon tests pass.
Route the Journey-2 question messages the same way as the permission
gate, staying payload-blind (correct under E2E ciphertext):

- agent.question (daemon -> browser): persist awaiting_input BEFORE the
  broadcast (alongside agent.permission_request), fire the awaiting-input
  web push, and add it to the ciphertext cache so a reopening browser
  replays the pending question instantly.
- question.answer (browser -> daemon): flip the row back to running
  before forwarding it opaquely to the daemon (alongside
  permission.decision / user.message).

Real relay + real Postgres integration tests: awaiting_input on
question, running on answer, and cache replay on reopen. Relay suite
green (125 pass).
…ey 2, Task 5)

Surface an adopted session's AskUserQuestion and let the operator answer
it remotely (Phase 3, best-effort):

- session reducer: a new 'question' transcript entry + AnswerState
  (pending/answering/answered/closed); applyEnvelope('agent.question')
  parks at awaiting_input; session.history backfills questions
  (answered when answers present, else pending); pendingQuestion() +
  markAnswering() (verification-gated like markDeciding); in-flight
  answers confirm on the next frame; a still-open question closes when
  the session ends (no dead, clickable picker).
- relay-client + store: an answer() path mirroring decide(), sealed
  under the session key when E2E.
- QuestionGate.svelte (enterprise-ui): renders each question as a
  radio (single) / checkbox (multi) group with an always-available
  'Other' free-text field; honest best-effort labeling; pending /
  answering / answered / closed states; semantic fieldset/legend +
  labels, focus-visible rings, role=alert errors, reduced motion.
  Wired into the Transcript next to PermissionGate.

223 web tests pass; svelte-check + eslint clean.
…, Task 6)

Final journey task — parametrize the adopted-question deny-feedback path
over the answer shapes Claude Code produces (single-select, multi-select,
'Other' free text, multiple questions in one call), each asserting the
relayed pick lands in the deny reason. Plus an explicit invariant-#5
test that agent.question reaches the relay as ciphertext, never the
cleartext questions. 142 daemon tests pass.
From the 3-agent review (typescript + test + clean-code), all PASSED:

- BLOCKING (clean-code): drop the dead `OTHER` constant in
  QuestionGate.svelte (the 'Other' option uses the otherOn flag).
- Important (typescript): stopTurn now releases pendingQuestions for the
  session (resolve null -> hook returns 'ask'), so interrupt/end no
  longer deadlocks the hook when an AskUserQuestion is pending (adopted
  sessions have no AbortController). Regression test added.
- Important (typescript): extract stripTelecodeHooks to its own
  one-export module (strip-telecode-hooks.ts); claude-settings.ts is now
  the schema + read/write layer.
- Important (clean-code): hooks-uninstall.ts JSDoc reformatted multi-line.
- Important (test): guard the it.each variant loop (expected non-empty).
- Cleanups: drop the unused `questions` field from the pendingQuestions
  map (YAGNI); relay integration test closes sockets in afterEach
  (no leak on assertion failure) + parses payloads with the wire schema
  instead of `as` casts; deny-reason test covers a combined
  selectedLabels + otherText answer.

In-place mutation of the question transcript entry in the question.answer
handler is kept for parity with the (single-threaded, documented)
permission handler rather than introducing an asymmetric helper.

144 daemon + 223 web + 125 relay + 83 protocol tests pass; all gates green.
@PouyanJay
PouyanJay merged commit 25742a0 into main Jun 30, 2026
2 checks passed
@PouyanJay
PouyanJay deleted the feat/adopted-sessions-questions branch June 30, 2026 20:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant