Skip to content

ci: bump actions/setup-go from 5.6.0 to 7.0.0 - #1

Merged
treeol merged 1 commit into
masterfrom
dependabot/github_actions/actions/setup-go-7.0.0
Jul 22, 2026
Merged

ci: bump actions/setup-go from 5.6.0 to 7.0.0#1
treeol merged 1 commit into
masterfrom
dependabot/github_actions/actions/setup-go-7.0.0

Conversation

@dependabot

@dependabot dependabot Bot commented on behalf of github Jul 22, 2026

Copy link
Copy Markdown
Contributor

Bumps actions/setup-go from 5.6.0 to 7.0.0.

Release notes

Sourced from actions/setup-go's releases.

v7.0.0

What's Changed

New Contributors

Full Changelog: actions/setup-go@v6...v7.0.0

v6.5.0

What's Changed

Dependency update

New Contributors

Full Changelog: actions/setup-go@v6...v6.5.0

v6.4.0

What's Changed

Enhancement

Dependency update

Documentation update

New Contributors

Full Changelog: actions/setup-go@v6...v6.4.0

v6.3.0

What's Changed

Full Changelog: actions/setup-go@v6...v6.3.0

v6.2.0

What's Changed

... (truncated)

Commits

@dependabot @github

dependabot Bot commented on behalf of github Jul 22, 2026

Copy link
Copy Markdown
Contributor Author

Labels

The following labels could not be found: dependencies, github-actions. Please create them before Dependabot can add them to a pull request.

Please fix the above issues or remove invalid values from dependabot.yml.

treeol added a commit that referenced this pull request Jul 22, 2026
Add internal/safe package with Go(name, fn) — a goroutine launcher with
recover() + debug.Stack() logging. A panic in an unrecovered goroutine
crashes the entire process; safe.Go catches it and logs the name, panic
value, and full stack trace.

Applied to 4 Medium-priority goroutine sites (logic-bearing):
- internal/counsel/oracle.go:334 — oracle panel member call
- internal/agent/tool_handlers.go:586 — bg process reaper
- internal/browser/manager.go:131 — chromedp.Run browser launch
- internal/browser/manager.go:183 — browser ctx cancellation bridge

Low-priority cmd.Wait() reapers (exec.go, exec_ops.go, open.go) left as
direct go func() — near-zero panic risk, not worth the indirection.

Tests (3, race-clean):
- Panic is recovered and logged with name + stack trace
- Normal execution completes unaffected
- Multiple goroutines: only the panicking one is caught

Verified: build + vet + gofmt clean; 18 packages green; -race clean.

Trello: https://trello.com/c/eMPm7DUl
@treeol

treeol commented Jul 22, 2026

Copy link
Copy Markdown
Owner

@dependabot rebase

Bumps [actions/setup-go](https://github.com/actions/setup-go) from 5.6.0 to 7.0.0.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](actions/setup-go@40f1582...b7ad1da)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
@dependabot
dependabot Bot force-pushed the dependabot/github_actions/actions/setup-go-7.0.0 branch from 799a0a2 to cec4921 Compare July 22, 2026 14:06
@treeol
treeol merged commit ba89c2f into master Jul 22, 2026
2 checks passed
@dependabot
dependabot Bot deleted the dependabot/github_actions/actions/setup-go-7.0.0 branch July 22, 2026 14:19
treeol added a commit that referenced this pull request Aug 26, 2026
* docs(card148): add wakild foundation design doc, Mashura-gated plan, D10 inventory

Card #148 (https://trello.com/c/Ba4YYGXM) planning artifacts on
feature/wakild-daemon:

- docs/design/wakild-foundation.md — the wakild daemon foundation design doc,
  verbatim from the user (command/event split, Connect API, tenancy from day
  one, SQLite+kvr storage split, P0-P5 phases).
- docs/cards/card-148-wakild-impl-plan.md — FINAL implementation plan, gated
  by Mashura (3 panels). Key decisions: P0 is daemon-shaped (non-blocking
  SubmitInput), two event classes, store-pluggable sequencer, tenancy from P0,
  sync->async approval shim in P0, Service split, P1 replay = projection not
  event sourcing.
- docs/cards/card-148-d10-inventory.md — P0 chunk 1: full inventory of every
  direct *agent.App access from internal/tui and cmd/wakil (~130 accesses,
  7 output channels, 27 sendEvent sites, 1 channel-bearing message).

No code changes in this commit.

* feat(core/event): add transport-free domain event model (card #148 P0 chunk 2)

First code of P0. internal/core/event is the domain event vocabulary every
client will consume; it lives under internal/core so that core never imports
api/gen or internal/server (foundation doc §2.1).

Implements plan decisions D2/D4/D6:
- Two event classes (durable replayable vs ephemeral live-only), derived from
  Kind; MessageCommitted is the durable counterpart of MessageDelta.
- Tenancy from day one: TenantID/SessionID on every envelope, embedded
  tnt_local/usr_local principals; no empty tenant passes.
- Events are data-only: single envelope + typed payload, no channels/callbacks.
  Kind/payload pairing enforced against an exact reflect.Type registry (not
  name strings, so same-named types from other packages or proto can't match).
- Draft vs committed lifecycle: ValidateDraft (Seq==0) vs ValidateCommitted
  (durable Seq>=1, ephemeral Seq==0), so the appender has a clean pre/post
  contract before the store exists.
- Typed prefixed IDs (tnt_/usr_/wsp_/ses_/trn_/tcl_/apr_/sub_) with Validate
  methods, called from the envelope AND payloads; checkID panics on unknown
  kind (programmer error) instead of silently passing.

Mashura (3 panels) reviewed this chunk; folded in: draft/committed split,
ephemeral Seq==0 enforcement, reflect.Type registry, typed-nil pointer
rejection, payload ID/enum validation, checkID unknown-key guard, registry
completeness test.

Verified: go test + go test -race pass, go build ./... clean, go vet clean,
gofmt clean.

* feat(core): add session service contracts + typed UUIDv7 id generation (card #148 P0 chunk 3)

Introduces the transport-free core contract surface (plan D3/D4/D7) that
chunks 4-8 and P1/P2 build on, plus prefixed UUIDv7 id generation.

internal/core (package core, service.go):
- Three behavior-first interfaces (D7): SessionService (commands),
  EventReader (Subscribe/ListEvents), SessionReader (queries/snapshot).
  Bootstrap (config, proxy.Client, executor, sinks) stays OFF the interfaces.
- Principal identity model (D4): typed tenant/user + role + scopes +
  auth_method; EmbeddedPrincipal() returns a fresh value (no mutable global).
- Session state machine with an explicit transition table + CanTransitionTo.
- Store contracts (D3): EventAppender (durable-append + sequence assignment,
  atomic, commit->notify point; rejects ephemeral drafts) and EventLog
  (cursor-addressable durable read). Sequencer is NOT exposed as a seam --
  assignment folds into Append so contiguous sequences can't be violated.
- ApprovalOutcome enum (deny/allow_once/allow_reads_once) instead of a
  representable-ambiguous bool pair; decision carries session correlation.
- SubmitInput contract pinned: FIFO enqueue (bounded), TurnAck = acceptance
  not completion, error-session re-drive via SubmitInput, non-blocking.
- Subscription lifecycle contract (handoff, dedup-by-seq, Next->io.EOF after
  Close, backpressure) and a fuller error vocabulary (state transition,
  approval not-found/already-resolved, subscription closed).

internal/core/id:
- Generator producing event-prefixed UUIDv7 ids (tnt_/usr_/wsp_/ses_/trn_/
  tcl_/apr_/sub_); NewFromReader for deterministic tests; package-level
  helpers on crypto/rand. Promotes github.com/google/uuid v1.6.0 to a direct
  dependency.

Verified: go build ./... + go vet clean; go test -race ./internal/core/...
green; full go test -count=1 ./... green across all packages; gofmt clean on
tracked files (CI gate). Mashura review (3 panels) folded in: Sequencer seam
removed, ephemeral-append rejection, approval enum, state-transition table,
EmbeddedPrincipal as function, consistent explicit principal args, submit
enqueue-vs-busy resolution, subscription lifecycle contract, not-found vs
not-authorized resolution, honest SessionSnapshot framing. Known seams noted
in docs, not implemented: tenant-isolation is service-layer (plan D4/P1 note),
pagination/retention, idempotency enforcement (RequestID reserved), P1
session-creation fields (additive).

Refs: https://trello.com/c/Ba4YYGXM

* feat(core): add in-memory session host (card #148 P0 chunk 4)

Implements deliverable 3 of the P0 plan and all three service interfaces
(SessionService, EventReader, SessionReader) over the D3 store contracts.

- One executor goroutine per session with a bounded FIFO input queue;
  SubmitInput is genuinely non-blocking and returns TurnAck{TurnID}.
- Turn finalization is a single lock-protected linearization point
  (finishTurn), so a close or interrupt racing a turn's return can never be
  lost or park the session in a stuck state.
- Interrupt/CloseSession cancel via an internal context and emit
  TurnCompleted{cancelled} / SessionClosed — never a silent abort.
- Durable event log (MemLog) with contiguous, atomic append-as-one-step
  sequencing; the executor is the single commit->notify point.
- Subscribe registers-before-replays, stages replay history in an unbounded
  seq-ordered pending queue, and merges concurrent live commits without
  order inversion or loss. Slow subscribers are disconnected with
  ErrSubscriptionGap (never block the executor; durables never silently lost).
- Turn output is committed as MessageCommitted; acked-but-never-run inputs are
  always explicitly abandoned with TurnCompleted{cancelled} (never silent).
- Crash-recovery stub RecoverRunning: recovered sessions enter error state
  with a SessionError{daemon_restart} marker.
- Tenant isolation (no existence leak) and role gating on every method.

Verified: build/vet clean; go test -race -count=5 on sessionhost green;
full go test -count=1 ./... green across all 27 packages; gofmt clean.

Mashura review (card #148 chunk 4): two panels converged on blocking defects
(finalization race, replay/live reordering + lossy catch-up, discarded turn
output, subscription detach leak); all folded in.

Trello: https://trello.com/c/Ba4YYGXM

* docs(card148): add Mashura-reviewed chunk-5 plan (event-emission seam + agent adapter)

Card #148 P0, workflow step 4: plan for chunk 5 (deliverables 4 + 6).
Reviewed by three panels (gpt-5.6-sol, claude-fable-5, glm-5.2); feedback
folded in: serialized append->notify (emitMu), turn fencing + late-emission
contract, internal_error classification, ctx-aware approval confirmer with
full outcome fidelity, TurnInput.UserID resolver identity, authoritative
message text = TurnOutcome.Text, tool/subagent durable events deferred.

* feat(core): add turn-scoped event emitter + agent-loop adapter (card #148 P0 chunk 5)

Implements plan deliverables 4 + 6: the command/event boundary's outbound
half. A turn can now emit intermediate domain events (not just its flat
text/error result), and a real *agent.App turn drives an in-memory host
session end-to-end through SessionService/EventReader only.

sessionhost:
- TurnInput.Emit: turn-scoped Emitter (durable Emit + ephemeral Notify),
  concurrent-safe, fenced at finalization.
- s.emitMu serializes durable append->notify across ALL producers (executor
  terminals + worker-emitted events) so subscribers observe exact increasing
  Seq order.
- Fence check runs INSIDE emitMu against finishTurn's terminal emit, so a
  turn-emitted durable event can never append after its TurnCompleted
  (terminal ordering).
- Host-owned kind allowlist; ErrEmitFailed + ErrInternal classify emit/store
  failures as SessionError{internal_error}, not backend_failure.
- TurnInput.UserID carries the submitter principal for audit identity.

internal/wiring (new):
- HostTurnFunc: installs agent Out/OnReasoning/Confirm callbacks, runs the
  SendOutcome -> WaitForAsyncCompletion -> Resume loop, restores callbacks
  panic-safe. Bound to ONE host session (single-App, single-session rejected
  loudly). MessageDelta/ReasoningDelta emissions; authoritative text returns
  TurnOutcome.Text.
- hostConfirmer (D5 shim): ApprovalRequested -> ApprovalResolved with full
  approve/decline/allow-reads fidelity; resolver runs in a goroutine raced
  against ctx cancellation (stuck resolver cannot hang Interrupt/Close);
  emit failures latch into the turn error (no orphaned/duplicate approvals).

Docs: MessageDelta presentation-streaming caveat; ApprovalResolved.Resolver
now populated in P0; SubmitInput.ReadAction doc corrected; plan fence
wording corrected (linearizes before terminal append).

Reviewed by three panels (gpt-5.6-sol, claude-fable-5, glm-5.2): fold-ins
from both plan review and implementation review are applied.

Verified: go build/vet clean; go test -race full suite green (28 pkg);
go list structural gate clean (internal/core imports no bubbletea/server/
agent/tui/api-gen; wiring is the sole bridge). Trello: card #148.

Ref: https://trello.com/c/Ba4YYGXM

* docs(card148): chunk 6 plan — interim TUI→App control seam (deliverable 5, step 1/2)

* feat(card148): chunk 6 — interim TUI→App control seam (deliverable 5, step 1/2)

Introduce Control (user/session commands) and StateApply (round-trip runtime
results) interfaces in internal/agent, implemented by *App, and route all 20
TUI mutation sites through them. Fix ResumeSessionMsg + AppendSystemMessage to
take convMu (pre-existing lock bypass). Add a heuristic structural guard test
(narrowed: field writes rooted at m.app + seam-method calls through m.app +
enumerated pass-*App set) plus a negative test proving the guard is not
vacuous, and fake-Control/StateApply routing tests proving the seam is used.
D12 + deliverable-5 completion remain red (TUI still holds *agent.App; the
turn-driving re-route is the next chunk).

* docs(card148): chunk 7 plan — headless turn-driving re-route (Mashura-gated)

* feat(card148): chunk 7 — headless single-task re-route through session host (deliverable 5 step 2, exit gate #2 partial)

* docs(card148): chunk 7b plan — TUI re-route off *agent.App (Mashura-gated v2, gate #1)

* feat(card148): chunk 7b1 — agent-free facade contract + appOwners release path (Mashura-gated)

7b1: server-side foundations for closing Gate #1 (remove *agent.App from
internal/tui). No TUI cut yet — Gate #1 stays red; the TUI keeps driving
*agent.App until 7b3.

sessionclient (NEW package, internal/core/sessionclient):
- Agent-free facade interface + neutral DTO inventory (D26)
- Imports only event, proxy, core — never internal/agent
- go list -deps verified: zero internal/agent in transitive graph
- DTOs: Consent, ContextLimit, Backend, OpID, SessionSummary, SessionScope,
  ApprovalChoice, ApprovalRequest, ClientSnapshot, WorkflowSnapshot,
  CommandResult, RotateRequest, RepoStateMutator, CompletionSource
- CommandResult replaces agent.HandleTUICommand's (handled, quit, cmd)
  return — no agent.Cmd/Msg leak; Validate() rejects contradictory states
- ClientSnapshot: immutable version-stamped view-model (D26), not live getters
- Full Facade interface: SessionService + EventReader + TUI-specific ops +
  client-initiated mutations (D26 grounding #11) + side questions (D29) +
  slash-command dispatch (D23) + session listing + lifecycle

wiring/hostturn.go — appOwners release path (7b1):
- appOwners map: struct{} → *hostTurn (tracks the owner)
- HostTurnHandle: bundles TurnFunc + Release() + App()
- NewHostTurnHandle: factory entry point returning the handle
- HostTurnFunc: kept as thin wrapper (headless backward compat)
- Release(): idempotent, rejects active turns (ErrTurnActive)
- run(): atomically sets turnActive; rejects second concurrent turn and
  released-before-start (defense-in-depth + concurrency fix from Mashura review)

Tests:
- sessionclient: structural agent-free guard (go list -deps), ContextLimit
  parity, CommandResult.Validate
- wiring/factory_test.go: claim/release/reclaim lifecycle, idempotent release,
  active-turn rejection, released-turn rejection, concurrent claim/release
- All green under -race (29 packages)

Mashura review op-12 (gpt-5.6-sol) findings addressed:
- Critical: run() now rejects a second concurrent turn atomically
- doc.go: 'event and proxy only' → 'event, proxy, and core'
- TestPackageIsAgentFree: real go list -deps guard (not a placebo)
- CommandResult.Validate + RotateRequest.Validate added
- Rotation rationale clarified: fresh App pointer; Release is cleanup signal

* feat(card148): chunk 7b2 — session-scoped emitter + async approval + new event kinds (Mashura-gated)

Event package (D24/D28/D29):
- 10 new event kinds: user_message_committed (durable, replay truth),
  conversation_compacted, workflow_turn_started, workflow_final_review,
  async_job_started/completed, side_question_completed (all durable),
  tok_rate, async_job_progress, side_question_progress, learn_nudge
  (all ephemeral)
- OpID type with op_ prefix, validation, constructor
- TurnCompleted gains Warn + WorkflowWillContinue fields (D28)
- All new payloads with Validate() methods
- Registry + completeness + validation tests updated

Session host (D24/D25):
- SessionEmitter interface: session-scoped emitter fenced at session
  close (not turn close) — legal for detached work after turn completion
- sessionEmitter concrete impl: rejects host-reserved, turn-scoped,
  and ephemeral kinds on Emit; accepts ephemeral on Notify
- turnScopedKinds allowlist: approvals, tool calls, subagent events
  rejected by session emitter to preserve terminal turn ordering
- UserMessageCommitted emitted from executor goroutine (handleInput)
  not SubmitInput — guarantees FIFO ordering invariant
- UserMessageCommitted in hostReservedKinds (host-owned)
- ParkApproval hook on TurnInput: parks session in awaiting_approval,
  blocks turn goroutine on decision channel raced with ctx
- Real RespondToApproval: validates pending approval, resolves via
  buffered channel, idempotent same-outcome duplicates
- Cancel-during-approval: ctx cancellation → forced decline before
  emitter fence
- Resolver identity: ApprovalResolved.Resolver records who actually
  answered (principal.UserID), not just the turn submitter

Wiring adapter (D25):
- WithAsyncApproval() option: TUI uses async park+resolve path;
  headless keeps sync inline resolver (parity unchanged)
- app.OnTokRate wired to SessionEmit.Notify(KindTokRate)
- app.EventSink wired to session-scoped surface (projection TODO 7b3)
- ApprovalResolved emit failure now fails closed (return false, not
  just log) — safety invariant

Tests (14 new, 2 updated):
- Session emitter: legal-after-turn, fenced-at-close, rejects-host-reserved,
  rejects-ephemeral-emit, rejects-turn-scoped-kinds, notify-accepts-ephemeral
- Async approval: round-trip, cancel-during-approval, not-found,
  already-resolved, wrong-id, concurrent-park-and-resolve (race)
- Headless sync parity: unchanged
- Event: OpID validation, new payload validation, updated kind class test
- Updated TestSubscribeReplayOverlap for UserMessageCommitted event

Pre-existing flaky test TestCloseSessionEmitsSessionClosedAndIsIdempotent
confirmed flaky before and after changes (state-before-event race in test).

Mashura review (op-14, gpt-5.6-sol): 5 critical findings addressed
(host-reserved UserMessageCommitted, executor-goroutine ordering,
resolver identity, fail-closed audit, turn-scoped kind rejection);
D28 workflow emission and D29 EventSink projection deferred to 7b3
per plan scope.

* fix(card148): 7b2 prerequisite fixes — detached callback ownership, approval resolver identity, SetAllowReads ordering, event projection scaffold (Mashura-gated)

Three 7b2 bugs identified by Mashura review (op-17, 3 panels) fixed:

1. Detached event delivery broken (hostturn.go): hostTurn.run saved/restored
   app.EventSink and app.OnTokRate per-turn. Between turns, the sink reverted
   to main.go's globalProg.Send — detached async jobs/side questions lost the
   session-scoped emitter. Fix: session-scoped callbacks (EventSink, OnTokRate)
   are now installed permanently on the first turn and NOT restored. Only
   turn-scoped callbacks (Out, Confirm, OnReasoning) are restored per-turn.
   The hostTurn struct gains a sessionEmit field that persists across turns.

2. Approval resolver identity (hostturn.go): forced decline on cancel/interrupt
   used in.UserID (submitter) instead of a system principal. D25 says
   cancellation records the system/interrupt principal. Fix: add
   event.SystemUserID and use it for forced declines.

3. SetAllowReads before durable emit (hostturn.go): app.SetAllowReads(true)
   was called before ApprovalResolved was durably emitted. If the append
   failed, consent was mutated but the turn failed. Fix: move consent mutation
   AFTER successful durable emit.

4. Event projection scaffold (projection.go): the app.EventSink was a no-op
   (TODO(7b3)). Added projectAgentEvent with the full mapping structure
   (skeleton — projection implementations land in 7b3 m2). This is the legal
   emit path for all agent message types through the session-scoped emitter.

TestCallbackRestore updated: OnTokRate/EventSink are now session-scoped
(permanent, not restored). Only Out/Confirm/OnReasoning are turn-scoped.

Refs: card-148-chunk7b-plan.md D24/D25, Mashura op-17 (gpt-5.6-sol,
claude-fable-5, glm-5.2)

* feat(card148): 7b3 m1 — neutral format package, ConversationManager interface, snapshot fixes, async command mechanism, mapping matrix

m1 (Contracts & Lifecycle):
- internal/core/format: extracted ShortID, Truncate, DerefStr, Indent,
  Yellow, StrPtr, TranscriptSize from internal/agent so agent-free
  packages (sessionclient, wiring, TUI) can use them without importing
  internal/agent. Includes agent-free structural guard test.
- ClientSnapshot: added OutputMode field (was missing), added slice
  immutability documentation and test.
- CommandResult: added OpID field for async commands (/handoff,
  /remember, /recall) that need event-based completion notification.
- ConversationManager interface: agent-free, sits above facade, handles
  /new, /resume, /handoff rotation. Documents detached-job cancel policy.
- mapping_matrix.go: complete command/message mapping matrix documenting
  every agent.Msg type → event.Kind or CommandResult or ClientSnapshot
  field, plus slash-command → CommandResult mapping.
- Structural tests: OpID validation, OutputMode field, slice cloning,
  ConversationManager interface shape.

All tests pass: go build ./..., go vet, agent, TUI, cmd/wakil, core.

* feat(card148): 7b3 m2 — complete agent-message→domain-event projection

m2 (Host Capabilities & Projection):
- Complete projectAgentEvent in internal/wiring/projection.go:
  - SubagentStartMsg → KindSubagentSpawned (durable)
  - SubagentActiveMsg → KindSubagentProgress (ephemeral, [active] marker)
  - SubagentChunkMsg → KindSubagentProgress (ephemeral)
  - SubagentFinishedMsg → KindSubagentProgress (ephemeral, [finished:status])
  - SubagentDoneMsg → KindSubagentCompleted (durable, status from Err)
  - AsyncJobStartMsg → KindAsyncJobStarted (durable)
  - AsyncJobChunkMsg → KindAsyncJobProgress (ephemeral)
  - AsyncJobDoneMsg → KindAsyncJobCompleted (durable, status from Err)
  - SideQuestionChunkMsg → KindSideQuestionProgress (ephemeral)
  - SideQuestionDoneMsg → KindSideQuestionCompleted (durable, status from Err)
  - AgentDoneMsg.LearnNudge → KindLearnNudge (ephemeral, if non-empty)
  - ToolStartMsg/ToolResultMsg: dropped (turn-scoped path handles these)
  - SysNoteMsg/CompactedMsg/BackendCtxLimitMsg/ModelListUpdatedMsg/
    MCPReconnectedMsg/TokRateMsg: dropped (snapshot fields or client-local)

- ID mapping helpers: subagentIDFromChatID, opIDFromString, toolCallIDFromString
  (deterministic prefix-stripping: proxy ID body reused with domain prefix)

- 33 projection tests covering every message type, nil/closed emitter,
  ID helpers, error status derivation, dropped messages, unknown types.

All tests pass: go build, go vet, wiring package.

* feat(card148): 7b3 m3 — wiring facade, ConversationManager, event pump

m3 (Wiring Implementation & Event Pump):
- wiringFacade: implements sessionclient.Facade by bridging *agent.App +
  *sessionhost.Host. Delegates SessionService/EventReader to host, constructs
  ClientSnapshot from App fields, routes mutations to App methods.
- conversationManager: implements sessionclient.ConversationManager. Creates
  fresh *agent.App for each conversation, wires to host, handles /new, /resume,
  /handoff. Detached-job cancel policy on close.
- EventPump: goroutine driving EventSubscription.Next, delivers events to TUI
  callback. Handles subscription gap recovery (resubscribe from lastSeq),
  pump cancellation, rotation drain via Done() channel.
- ClientSnapshot.Costs changed to *proxy.CostTracker (CostTracker has a mutex,
  cannot be copied by value).
- interpretAgentMsg: translates agent.Msg from HandleTUICommand into
  CommandResult fields (Notice, Rotate, Submit, Compacted).
- Conversion helpers: toClientContextLimit, toAgentContextLimit, toClientBackends,
  toClientWorkflow.
- Tests: event pump delivery, idempotent stop, ctx cancel, interface satisfaction.

All tests pass: go build, go vet, core, wiring, tui, cmd/wakil.

* fix(card148): 7b3 m3 — Mashura review fixes (gap recovery, SaveRepoState, event pump ownership)

Fixes from Mashura op-20 review:
1. EventPump gap recovery: use errors.Is instead of == (wrapped errors)
2. EventPump lastSeq: accept initialSeq in constructor so gap recovery starts
   from the subscription's cursor, not zero (prevents full-history replay)
3. EventPump EOF: use errors.Is(err, io.EOF) for terminal error
4. SaveRepoState: initialize mutator from current repo state before callback
   so unset fields preserve existing values (no accidental zeroing)
5. wiringFacade: own the EventPump (not just subscription); Close stops the
   pump and drains it, not just the subscription

All tests pass: go build, go vet, wiring package.

* feat(card148): 7b3 m4a — workflow continuation via host enqueue hook, session notes, learn-nudge parity

Mashura review panel (3 panels unanimous) chose Option A: the adapter runs
HandleWorkflowTransition after a successful turn and enqueues the continuation
through a host-provided TurnInput.EnqueueInput closure — the TUI is passive.

- sessionhost: TurnInput.EnqueueInput hook; enqueueTurn (SubmitInput-equivalent
  acceptance semantics); finishTurn sets TurnCompleted.WorkflowWillContinue
  when a completed turn has queued work following it
- wiring/hostturn: post-turn HandleWorkflowTransition + enqueue + durable
  workflow_turn_started audit marker; TakeLearnNudge parity (agent accessor
  replicating the old RunTurn nudge computation) delivered as ephemeral
  learn_nudge
- event: KindSessionNote (ephemeral) + SessionNote payload — in-turn progress
  notes (wfProgNote, handoff progress, policy notices) projected instead of
  dropped
- projection: SysNoteMsg → ephemeral session_note
- tests: workflow continuation (2 turns + audit marker + WorkflowWillContinue),
  no-workflow control, session-note projection

* feat(card148): 7b3 m4a — side-question registry, SetWorkflow conversion, snapshot versioning

- facade: unique domain OpIDs for side questions (id.NewOpID generator added
  to core/id) with a facade-side OpID→CancelFunc registry; CancelSideQuestion
  looks up + cancels + removes; Close cancels all registered side questions
  (detached-job policy). Replaces the constant 'op_sq' pseudo-ID and no-op
  cancel stub.
- facade: SetWorkflow converts WorkflowSnapshot → workflow.WorkflowState
  (phase-name→enum mapping, workflowPhaseFromName); nil still clears.
- facade: snapshot revision counter — every facade-mediated mutation bumps
  Version so clients detect staleness; Snapshot.Title from Session.Label.
- tests: unique OpIDs + registry lifecycle, SetWorkflow round-trip through
  Snapshot, version increments across mutators

* feat(card148): 7b3 m4a — real HandoffConversation via RunHandoffPipeline, hashed workspace IDs

- agent: RunHandoffPipeline exported seam over performHandoff steps 1–4
  (validation, old-session save, recency-split summary generation,
  session-history indexing with fallback chain, durable handoff record);
  HandoffResult carries what the wiring layer needs (payload, continuation
  prompt, note, chat IDs)
- wiring: HandoffConversation now runs the real pipeline — saves old session,
  generates summary, creates new conversation seeded with the pinned handoff
  context (untrusted-delimiter framing), clears pending images, and enqueues
  the continuation turn via host SubmitInput when proceeding (host-enqueues
  policy; auto-start failure degrades to a startup note, not a failed rotation)
- wiring: workspaceIDFromConfig derives wsp_<sha256[:8]> of the effective
  workdir — stable per workspace, valid ID grammar (the raw-path stub failed
  validation for empty workdirs)
- tests: handoff seeding (pinned context, no image leak, new ChatID), empty-
  conversation guard

* feat(card148): 7b3 m4a — subagent event enrichment, InfoSnapshot DTO, SessionSummary.Turns

- event payloads: SubagentSpawned carries Backend/Model/ToolNames;
  SubagentCompleted carries Err/CostUSD/FilesChanged/Grounding labels/
  CtxSize/HardMaxBytes/UsedBackend; SubagentProgress carries structured
  early-finished fields (Finished/FinishedStatus/FinishedCostUSD/
  FinishedFilesN) instead of a '[finished:status]' text marker;
  AsyncJobCompleted carries Err. Client tab/info-panel parity with the old
  Subagent*Msg/AsyncJobDoneMsg fields.
- projection: maps the new fields through.
- sessionclient: InfoSnapshot DTO (narrow, immutable, defensive copies) —
  endpoint/identity, model+backend selection, prompt/config bits, context
  gauge, transcript stats, workflow label, MCP servers, SearXNG tools,
  grounding entries, costs. Facade.Info() interface method; SessionSummary.
  Turns() mirrors agent.SessionTurns for the resume picker.
- wiring: Info() implementation (mashuraPanelLabel moved from the TUI so the
  label is computed wiring-side); endpoints for completion.
- tests: Info snapshot content + copy semantics.

* fix(card148): 7b3 m4a — DispatchCommand handoff deferral + learn-text mapping

- DispatchCommand intercepts /handoff BEFORE agent.HandleTUICommand: arg
  validation + quick-fail emptiness guards run at dispatch (fast), and the
  result carries Rotate{Type:handoff, Proceed} WITHOUT executing the
  summarizer pipeline — HandoffConversation runs it exactly once. Fixes the
  double-pipeline (dispatch executing performHandoff AND the manager running
  RunHandoffPipeline) and the event-loop blocking (120s summarizer) from the
  synchronous cmd() call.
- Documented calling contract: slow commands (/handoff /remember /recall
  /compact) execute synchronously inside DispatchCommand; the caller (TUI)
  wraps the call in a worker goroutine — the AdaptCmd pattern.
- LearnTurnMsg → Submit 'learn this for next time' (the literal the old TUI's
  LearnTurnMsg handler submitted via RunTurn), NOT '/learn' (infinite
  redispatch). WFFinalReviewMsg → Submit 'continue' with rationale (the
  adapter re-runs HandleFinalReview at turn end in verify state).
- tests: handoff deferral (no pipeline execution, proceed/stop/usage
  variants), empty quick-fail, learn literal.

* feat(card148): 7b3 m4b-prep — tool-call events on turn emitter, facade-owned event pump, BootstrapTUI

- hostturn: turnEmit field — the EventSink closure routes ToolStartMsg/
  ToolResultMsg (turn-scoped kinds; the session emitter rejects them by
  design) to the live turn's emitter, stamped with the turn ID; everything
  else projects on the session surface. turnEmit set/cleared per turn.
- projection: ToolStartMsg → tool_call_started (ArgDigest = primary arg),
  ToolResultMsg → tool_call_completed (Result preview) — previously dropped
  with a wrong assumption ('the host emits them' — tools run inside the
  agent loop; nothing emitted them). The TUI running-tool status line now
  has a live wiring path.
- facade: Subscribe starts the facade-owned event pump (deliver callback =
  tea.Program.Send); StartEventPump begins delivery when the caller is
  ready. Facade interface updated accordingly.
- wiring: BootstrapTUI — manager + first conversation (fresh or --resume),
  subscription armed, cleanup closure; the m4c main.go entry point.
- tests: tool events through a real turn (ordering: start < done < turn
  completed; TurnID stamped), projection unit tests, BootstrapTUI fresh +
  missing-resume.

* feat(card148): 7b3 m4b-prep — wiring-side fixes for the TUI cut (Mashura op-32 review)

Wiring-side blockers and review findings fixed before the TUI cutover:

- WithAsyncApproval enabled on manager-built conversations (B1): the
  sync confirmer with nil resolver declined every approval — an
  interactive TUI could never approve a tool. E2E test proves the park/
  respond/complete cycle.
- hostTurn EventSink closure bugs (op-32): captured the FIRST turn's
  TurnID forever (later turns' tool events stamped wrong) and read
  turnEmit without the mutex (data race). Both fields now written/read
  under ht.mu as a consistent pair.
- interpretAgentMsg gaps (B2): BatchMsg recursion (/backend, /model —
  note + ctx-limit/model-list side effects now applied facade-side per
  D24 query-state), MCPReconnectedMsg applies rebuilt tools,
  ClipboardImageRequest sentinel → CommandResult.ClipboardImage,
  OpenResumePickerMsg → ResumePicker (was a bogus Rotate).
- /new, /reset, /resume intercepted in DispatchCommand (B3): the agent
  path mutated the OLD App (NewConversation + finalizeSessionHistory).
  Rotation now classifies only; the manager owns finalize-on-rotation
  (FinalizeSessionHistory exported) and old-App freshness.
- Facade.Close: CloseSession first (cancels in-flight turn → unblocks a
  PARKED approval — was a permanent goroutine leak on rotation), drain
  the pump (bounded), retry Release while the turn winds down.
- bumpVersion AFTER mutation (was before — new version + stale data
  window defeated the staleness check).
- BootstrapTUI subscribes at the durable HEAD, not seq 0 (replayed
  ApprovalRequested would pop dead confirm gates; replay duplicates
  hydrated snapshot state).
- InfoSnapshot: InfoPanelOpen + OracleLabel 'no key' env fallback moved
  wiring-side.
- ConversationManager.NewConversation gains a current-Facade param
  (rotation context for finalize).
- TUI: tui_msgs.go + session_view.go migrated off agent format utils to
  internal/core/format (first two of nine files; stage 1 of the cut).

* feat(card148): 7b3 m4b stage 2 — TUI read paths through facade Snapshot/Info

Model gains facade/manager/principal fields (wiring path), with the App
kept for the legacy path until stage 3 (one read source per file:
snapshot()/info() accessors return ok=false when facade is nil and
callers fall back).

Migrated files (wiring path reads facade; legacy path unchanged):
- info_panel.go: infoMainSegments/infoSubSegments/infoToolsSegments/
  infoGroundingSegments/costSegments/billedSegment/toggleInfoPanel —
  every App-internal read (Exec.Describe/Cwd, Cfg.Image/Oracle/SearXng,
  Client.BaseURL/ChatID/Grounding, MCP.Servers, Workflow.SidebarLabel,
  Costs) now comes from InfoSnapshot on the wiring path. OracleLabel
  'no key' fallback + InfoPanelOpen are wiring-side (committed earlier).
- tui_view.go: ctxSegment (limit/usage/transcript stats) and
  headerStatusInput (workflow label, backends, model/submodel, consent,
  rawTools) read Info()/Snapshot()/facade.Consent().
- complete.go: compSources() model method (backends/models/endpoints/
  mentionBase via snapshot+Info); fetchSessionShortIDs via
  facade.ListSessions; computeCompletion/computeSlashCompletion take an
  injected session fetcher (stays a pure function).
- resume_picker.go: SessionSummary DTO, facade.ListSessions reload,
  Turns() for row rendering; Enter routes through beginRotation on the
  wiring path (rotation scaffolding: rotationRequest/rotationMsg/
  beginRotation — build-new-first, old facade closed after replacement
  exists, per the op-32 review); legacy path keeps agent resume.
- NewTUIModelWithFacade: wiring-path constructor (nil-App base model,
  hydrates from snapshot.Conv + Info().InfoPanelOpen).

tui_msgs.go + session_view.go were already migrated in stage 1 (a80666e).

* feat(card148): 7b3 m4b stage 3 — wiring-path runtime cut (events, approval gate, send, rotation)

The TUI now runs end-to-end on the wiring path when facade-backed:

- tui_events.go: handleEventMsg switch on event.Kind — the D2 mapping.
  TurnStarted (state machine for host-initiated turns), MessageDelta/
  ReasoningDelta (stream accumulation, reasoning collapse), ToolCall-
  Started/Completed (status-line indicators; ArgDigest=command),
  ApprovalRequested/Resolved (async gate), TurnCompleted (finishWiring-
  Turn: flush, classification by Outcome, deferred /auto grants, queue
  flush gated on Outcome+WorkflowWillContinue, chime, clearWiringTurn-
  State incl. pendApproval safety clear), SessionError, Subagent*
  Spawned/Progress(Finished)/Completed, AsyncJob*, SideQuestion*,
  SessionNote, LearnNudge, Workflow* (passive audit notes), Compacted,
  UserMessage/MessageCommitted (replay truth — no live handling).
  Session guard: cached m.sessionID drops stale-pump events.
- tui_event_tabs.go: tab lifecycle (spawn/complete/job) keyed by domain
  IDs (sub_*/op_*), extracted as methods.
- Confirm gate: pendApproval{approvalID} + y/a/n/esc/ctrl+c → facade.
  RespondToApproval (non-blocking buffered chan); ctrl+c also Interrupt;
  already-resolved races tolerated.
- Send path: wiring branch submits via facade.SubmitInput (no RunTurn/
  startTurn); optimistic streaming state confirmed by TurnStarted;
  image chips reconcile against snapshot.PendingImages; mentions via
  Info().MentionBase. flushQueuedPrompt mirrors it.
- Slash dispatch: /-prefixed input → facade.DispatchCommand in a Cmd
  goroutine → commandResultMsg applied on the event loop (Notice/
  Submit/Rotate→beginRotation/ResumePicker/ClipboardImage/Compacted).
  Plain text no longer swallowed (slash-prefix guard).
- Rotation: beginRotation Cmd (manager op → close OLD facade after the
  replacement exists) → applyRotation swaps refs, rebuilds view state,
  THEN subscribes at the durable head + starts the pump (after the swap
  — events before it would hit the session guard). SetProgramSend
  installs tea.Program.Send without a package global in main.
- cancelTurn → facade.Interrupt; startSideQuestion → facade; Init
  startup note via local startupNoteMsg; mid-turn /auto consent via
  facade.
- E2E tests (fake facade): turn lifecycle, approval gate incl. ctrl+c,
  session guard, send path, queue-flush-vs-workflow-continuation.

* feat(card148): 7b3 m4c — main.go bootstrap reroute through the session host

cmd/wakil/main.go now runs the TUI on the wiring path (Gate #1, TUI
half): wiring.BootstrapTUI builds the ConversationManager + first
conversation (fresh or --resume), runs the TUI startup steps, and
subscribes the event stream; tui.NewTUIModelWithFacade carries the
facade/manager/principal; tui.SetProgramSend installs prog.Send and
rt.StartEventPump begins delivery. globalProg and the
app.EventSink=globalProg.Send line are gone — the pump is the only
runtime event path.

BootstrapTUI gains BootstrapTUIOpts carrying what main.go did inline:
--attach-image (pending images), RestoreRepoState (fresh conversations
only + ctx re-resolve, same caveat about literal restored strings),
counsel mode/max defaults, and staging/memory startup-note composition.
--resume/--resume-id resolution (workspace-scoped most-recent for bare
--resume) moved into main.go's bootstrap prelude; the manager restores
the transcript.

main.go keeps exactly two agent imports: PrintSessions (--list-sessions
short-circuit) and ShortID (diag log path) + LoadSessionScoped for bare
--resume resolution — all three move behind wiring/agent wrappers in
m4d's guard pass.

* feat(card148): 7b3 m4d — hard cut complete: TUI production code drops internal/agent (Gate #1)

The legacy App-backed path is deleted; the facade+event path is the only
runtime path. Local TUI messages (dotTickMsg, armTickMsg, subTabCloseMsg,
copiedMsg, clipboardImageMsg) moved from the deleted handleAgentMsg
switch into handleEventMsg's local-message section.

Removed: tui_agent_msgs.go (the 30-case agent-msg switch), adapter.go
(AdaptCmd — no agent.Cmds remain), tuiModel's app/control/apply/pendConf
fields, NewTUIModel(app) (newBaseModel + NewTUIModelWithFacade only),
startTurn (submit path owns the state flip), legacy branches in
info_panel/tui_view/complete/resume_picker (Snapshot/Info are the only
read sources), startSideQuestion/cancelTurn legacy paths, the legacy
converters.

Tests (~30 files) migrated to the event switch: evt(kind,payload,sid)
feeds, fakeFacade-backed models, domain-ID tab identities (sub_*/op_*),
rotationMsg for rotation semantics, TurnCompleted outcomes for the
AgentDoneMsg cases. Test helpers: fakeFacade (extended: consent mutators,
SetInfoPanelOpen, StartSideQuestion), newWiringModel, rotatedFake,
wiringTestInfo. Deleted: adapter_test, control_routing_test (seam
deleted), control_seam_test (seam deleted). Agent-package behavior tests
(cost recording, ctx-limit resolution, ProgWriter) keep their agent
imports — they test agent functions and are exempt from the guard.

Guard: TestNoAgentImport asserts go list -deps of internal/tui contains
no internal/agent package — Gate #1's TUI half is now compiler+test
enforced. main.go's residual agent uses (PrintSessions, ShortID,
LoadSessionScoped for bare --resume) are the remaining cmd-side gap.

Full suite + -race green: internal/... cmd/...

* fix(card148): 7b3 m4 — subscribe first-boot event stream after program construction

BootstrapTUI was called with deliver=nil (prog.Send does not exist at
construction time), and Subscribe is gated on deliver != nil — so on first
boot the facade never subscribed, StartEventPump was a no-op (no pump), and
turn events never reached the TUI. The host still ran every submitted turn
(billing the request), but MessageDelta/TurnCompleted were undelivered: the
optimistic streaming state never cleared and the answer never rendered
(the 'stuck streaming' live-test finding).

Fix: TUIRuntime.SubscribeLive subscribes at the durable head once the
tea.Program exists (mirroring the rotation path's lazy subscribe);
main.go binds prog.Send as the deliver callback between SetProgramSend
and StartEventPump. Regression test TestBootstrapTUISubscribeLive proves
turn events flow through a nil-deliver bootstrap + manual subscribe.

* feat(card148): 7b3 m4e — main.go drops internal/agent via wiring session wrappers (Gate #1 cmd half)

main.go was the last production file importing internal/agent. Three thin
package-level wrappers in internal/wiring/sessions.go (PrintSessions,
ResolveRecentSession, ShortID) now carry those call sites:

- --list-sessions and the session-log ShortID calls delegate verbatim
- bare --resume resolves through ResolveRecentSession, fixing a
  variable-shadowing bug from the half-applied rewire where the resolved
  id was silently clobbered by the outer empty id
- the cmd guard is tightened: internal/agent is banned in ALL non-test
  cmd/wakil files including main.go; internal/tui stays main.go-only
- guard proven non-vacuous (synthetic agent import in main.go fails the
  test); wrapper tests pin the scoped/all/error contracts main.go relies on

go build, go vet, and tests for cmd/wakil + internal/wiring all green.

* test(card148): P0 exit-gate certification tests — concurrent seq, dual-subscriber order, replay projection

Closes impl-plan §3 gates 4, 5 and 9:

- Gate 4: TestExitGateConcurrentSeqUniqueAndIncreasing — 8 concurrent
  SubmitInput producers + 8 detached workers emitting through the session
  emitter inside a turn; durable log must be unique + strictly increasing.
  Backpressure rejections (queue full) are counted, not failed.
- Gate 5: TestExitGateTwoSubscribersSameOrder — two subscribers from
  cursor 0 see the identical durable order, equal to ListEvents order.
- Gate 9 (D9): TestExitGateReplayReconstructsProjection — replay from
  ListEvents(0) reconstructs the user+assistant transcript and approval
  terminal state identically to a live subscriber (6-entry transcript,
  approval resolved "approved").

Also deflakes TestCloseSessionEmitsSessionClosedAndIsIdempotent: it
previously read the log right after observing state=closed, racing the
documented P0 window where state flips before SessionClosed is appended
(host.go package doc). The test now also waits for the event itself.

Gate 3 coverage mapped (no new tests needed): all six enumerated paths
have existing host/integration tests.

All green under -race (full sessionhost suite, -count=1, plus targeted
stability loop).

Ref: trello.com/c/Ba4YYGXM (card #148, P0 exit gate)

* feat(card148): 7c — headless --plan re-routed through the session host (Gate #2 fully green)

`wakil run --plan` no longer drives agent.App directly. The legacy workflow
loop (workflow_legacy.go, deleted) is replaced by host-driven turns with an
after-turn resolver in the adapter:

- New HostTurnFunc option WithPlanAutoAdvance: the resolver applies the legacy
  headless auto-advance policy when HandleWorkflowTransition pauses the
  workflow — present→implement, review force-skip (+ legacy warning record),
  implement step advance, resolver-owned final review on no-marker crossing.
- Exactly-one-of invariant (Mashura op-34/35): every completed plan turn
  yields one of {terminal WorkflowOutcome event, one queued continuation,
  error}; enqueue rejection is terminal in plan mode (never silently idle).
- Decline control latch on hostTurn (last-wins, legacy parity): a declined
  approval — tool OR oracle confirm, including one latched DURING the
  transition — terminates the workflow before another turn is enqueued.
  Cancellation declines (reason "cancelled") are excluded.
- New durable KindWorkflowOutcome (+payload/validation) and ephemeral
  KindWorkflowWarning; KindWorkflowFinalReview is finally emitted (plan mode
  only — TUI event stream unchanged).
- runPlanTask/runPlanSession/consumeWorkflowEvents in wiring: first submit is
  "continue" (byte parity), terminal records byte-identical to legacy
  (pass/declined/gaps/verify_failed/backend_failure+resume_id), no tokens
  record (legacy parity), consumer never reads app state.
- Fail-closed fixes from review: NewApprovalID failure now records
  emitErr (internal error) instead of a silent decline.

Tests: 6 new plan-host integration tests (full chain→gaps, backend failure
with resume_id, mid-workflow decline, verify_failed, PASS with live oracle
reviewing, decline reason capture) + the 115 existing cmd/wakil workflow
tests now exercising the host path (oracle default/no-oracle, confirmer
policy). All green under -race; full build + vet clean.

Plan: docs/cards/card-148-chunk7c-plan.md (v2, Mashura-gated: op-34 plan
review with 3 panels, op-35 implementation review; all blockers folded in).

Ref: trello.com/c/Ba4YYGXM (card #148, P0 — Gate #2 closed)

* feat(card148): P1a-P1e — SQLite event log + store-backed sequencer (D3)

P1a — Event payload codec (internal/core/event/codec.go):
- MarshalPayload/UnmarshalPayload using payloadTypes registry for type dispatch
- JSON encoding ("json-v1") as P1 interim; P2 will add proto
- UnmarshalPayload returns VALUE (not pointer) matching MemLog representation
- Rejects: nil, typed-nil, type mismatch, NaN/Inf, malformed JSON, invalid
  decoded payload
- 30-kind round-trip test + 7 edge-case rejection tests

P1b — Migrations (internal/store/migrations/):
- 001_init.sql: sessions + events tables with composite FK
  (tenant_id, session_id) → sessions(tenant_id, id), CHECK constraints
  (seq > 0, encoding IN json-v1, last_seq >= 0), UNIQUE(tenant_id, id)
- migrate.go: forward-only, per-migration transactions, version tracked in
  schema_migrations (owned by runner, not SQL file), idempotent
- 6 migration tests: fresh DB, idempotent, constraints, reopen, load, missing dir

P1c — SQLiteStore (internal/core/sessionhost/sqlstore/):
- Implements sessionhost.Store (EventAppender + EventLog — NO interface change)
- SessionCreated append: atomic session row + event (seq=1) in one transaction
- Non-SessionCreated append: tenant-qualified UPDATE + INSERT in one tx;
  unknown session → ErrSessionNotFound; tenant mismatch → ErrSessionNotFound
  (no existence leak)
- Read: cursor-exclusive, ORDER BY seq ASC, limit<=0 omits LIMIT clause,
  eager payload decoding via UnmarshalPayload, encoding dispatch
- LastSeq: 0 for nonexistent session
- modernc.org/sqlite, SetMaxOpenConns(1), WAL, foreign_keys=ON, busy_timeout=5000
- 15 unit tests including: concurrent seq uniqueness (-race), reopen durability,
  PRAGMA verification after reopen, pointer payload, duplicate SessionCreated

P1d — Shared store contract harness (internal/core/sessionhost/storetest/):
- RunContract: 8 subtests (append, ascending seq, LastSeq, concurrent, cursor,
  ephemeral rejection, replay reconstruction) run identically against both
  MemLog and SQLiteStore
- MemLog contract test + SQLiteStore contract test (with reopen durability)
- exit_gate_test.go UNCHANGED (still uses MemLog)

P1e — Cross-tenant isolation test (internal/core/sessionhost/cross_tenant_test.go):
- Verifies host-level authz: tenant B → all of ListEvents, GetSession,
  Subscribe, SessionSnapshot, SubmitInput, CloseSession, Interrupt,
  RespondToApproval return ErrSessionNotFound (not ErrNotAuthorized, not empty
  — per core/service.go "no existence leak" contract)
- ListSessions does not leak cross-tenant sessions
- Runs against MemLog (tests P0 host authz, not store-level behavior)

Plan: docs/cards/card-148-p1-chunk-plan.md (v2.1, 2 Mashūra review rounds:
op-40 → 11 blocking findings → v2; op-41 → 5 remaining → v2.1 addendum)

Verification: go build ./... ✅, go vet ./... ✅,
go test -race ./... 31/31 packages ok ✅,
exit_gate_test.go unchanged ✅

Refs: Trello card Ba4YYGXM

* feat(card148): P1f — wire SQLiteStore into daemon composition (TUI + headless)

Production now uses SQLiteStore instead of MemLog for the session-host event
log (card #148 D3). Both TUI and headless paths open a workspace-keyed
SQLite database at <wakil-data>/sessionhost/<short-key>/sessionhost.db.

Wiring:
- agent.SessionHostDBPath: workspace-keyed DB path (same pattern as
  MemoryDBPath/SessionHistoryDBPath)
- conversation_manager.go: NewConversationManager opens SQLiteStore at init,
  injects via WithStore for every newConversation. Best-effort: falls back
  to MemLog on open failure (logged to stderr).
- headless.go: headlessStoreOpts() helper opens SQLiteStore for both
  runSingleTask and runPlanSession paths. Same best-effort fallback.

Tests (sqlstore_restart_test.go):
- TestHostRestartRecovery: create session + turn → close host+store → reopen
  → verify events persist (SessionCreated, UserMessageCommitted, TurnStarted,
  MessageCommitted, TurnCompleted, SessionClosed). Second host creates a
  new session in the same store; independent seq counters, old session
  unchanged.
- TestHostWithSQLiteStore_SubmitAndConsume: full turn cycle through host
  with SQLiteStore; verify MessageCommitted persisted with correct text.

Verification: go build/vet ✅, go test -race ./... 31/31 ok ✅

Refs: Trello card Ba4YYGXM

* feat(card148): P2a — proto schema + buf toolchain (wakil.v1alpha1)

- api/proto/wakil/v1alpha1/: 5 proto files (event, session, event_service,
  session_service, system) defining the P2 wire contract
- Event oneof: all 33 event kinds (field numbers 10-40, reserved 41-50)
- Services: SessionService (8 RPCs incl DeleteSession), EventService (3 RPCs
  incl GetSessionSnapshot), SystemService (GetServerInfo, Health)
- buf.yaml (lint: STANDARD + PACKAGE_DIRECTORY_MATCH + PACKAGE_VERSION_SUFFIX,
  except RPC_REQUEST_RESPONSE_UNIQUE + RPC_RESPONSE_STANDARD_NAME for shared
  Session type), buf.gen.yaml (local protoc-gen-go + protoc-gen-connect-go)
- api/gen/ committed (reproducible: buf generate && git diff --exit-code clean)
- go.mod: +connectrpc.com/connect v1.20.0, +google.golang.org/protobuf v1.36.12
- P2 chunk plan at docs/cards/card-148-p2-chunk-plan.md (Mashura-reviewed,
  revised per 3-panel feedback: D5 already implemented, TurnFunc transport-
  free, principal server-side, fail-closed store, SessionSnapshot RPC added)

buf lint: pass | buf generate: no drift | go build ./...: pass | go vet: pass | go test -race ./...: 33/33 ok

* feat(card148): P2b — DeleteSession on SessionService + SessionSnapshot wire path

- DeleteSession added to core.SessionService interface (service.go:70 says P2)
- Host.DeleteSession: soft-delete — closes active session, marks deleted,
  excludes from GetSession/ListSessions; events remain for audit
- lookup() returns ErrSessionNotFound for deleted sessions (all methods reject)
- Double-delete = ErrSessionNotFound; cross-tenant = ErrSessionNotFound;
  viewer = ErrNotAuthorized
- SessionSnapshot already on SessionReader — no core change needed,
  proto GetSessionSnapshot RPC maps directly
- 5 new tests: excludes from queries, rejects operations, closes active,
  cross-tenant, viewer-not-allowed
- Full -race suite: 33/33 ok

* feat(card148): P2c — Connect server adapter (core↔proto bridge)

- internal/server/connect/: 7 files bridging proto wire contract to core
- event_conv.go: 33-kind core.Event ↔ proto Event converter (all payloads
  round-trip verified)
- session_handler.go: SessionService 8 RPCs (Create/Get/List/Delete/Submit/
  Approval/Interrupt/Close)
- event_handler.go: EventService 3 RPCs (StreamEvents server-streaming,
  ListEvents, GetSessionSnapshot)
- system_handler.go: SystemService (GetServerInfo, Health)
- errors.go: 10 core sentinels → Connect codes (NotFound, FailedPrecondition,
  ResourceExhausted, PermissionDenied, etc.)
- principal.go: server-side EmbeddedPrincipal (no client-supplied identity)
- server.go: Server bundling all 3 handlers, http.Handler mount
- converter_test.go: 33-kind round-trip test (all pass)

proto: added MessageCommitted (field 41) to oneof (was missing from initial
schema). buf lint + generate: clean. go build + vet: pass.

* feat(card148): P2d — wakild binary + Unix-socket transport

Daemon binary that serves the Connect API over a Unix socket:
- cmd/wakild/main.go: flag parsing (--socket, --ephemeral,
  --shutdown-timeout), config loading, workspace ID derivation,
  serve-until-signal lifecycle.
- cmd/wakild/server.go: fail-closed SQLiteStore open (unless --ephemeral),
  executor + App + HostTurnHandle with WithAsyncApproval, sessionhost.New
  with store, Connect server, Unix-socket listener (0600 permissions,
  stale-socket detection/unlink, in-use refusal, parent dir 0700),
  graceful shutdown (http.Server.Shutdown → host.Close → resource cleanup).
- cmd/wakild/signal.go: SIGTERM/SIGINT → context cancellation.
- 16 tests: socket lifecycle (stale/in-use/permissions/parent-dir/
  connect-accept), flag parsing, Health RPC + GetServerInfo RPC over
  real Unix socket (ephemeral + non-ephemeral), nil-safe shutdown.

P2d simplification: one App drives one session (HostTurnFunc's
single-App binding). The daemon serves one active session at a time.
Per-session factory (multiple Apps/hosts) is a P2e concern.

* feat(card148): P2e — remote client + TUI --daemon mode

Remote client architecture (internal/remote/):
- dialer.go: Unix-socket HTTP client for Connect-go service clients
  (Session, Event, System). Verifies socket exists and is connectable
  before building clients. Disables keepalive pooling for clean
  reconnects across daemon restarts.
- pump.go: RemoteEventPump consumes the StreamEvents server-stream RPC,
  converts proto→domain events via protoconv, deduplicates durable events
  by seq, and reconnects from lastSeq+1 on stream break. Stop cancels
  the stream context; Done channel for rotation drain.
- facade.go: RemoteFacade implements sessionclient.Facade by calling
  Connect RPCs for all SessionService + EventReader surfaces. TUI-
  specific surfaces (Snapshot, Consent, Info) are limited to what the
  daemon exposes — the conversation is projected from events, not read
  from agent.App. Slash-command dispatch handles /quit, /new, /resume
  client-side; the rest passes through as regular input.
- manager.go: RemoteConversationManager implements
  sessionclient.ConversationManager (New/Resume/Handoff/Close).
- bootstrap.go: BootstrapRemote mirrors wiring.BootstrapTUI — dials
  daemon, checks health, creates/resumes first conversation, subscribes
  event stream. Returns TUIRuntime (Facade, Manager, Principal).

Shared proto conversion (internal/protoconv/):
- event_conv.go + payload.go: 32-kind proto↔domain event oneof conversion
  extracted from internal/server/connect/event_conv.go. Used by both
  the server adapter (domain→proto for RPC responses) and the remote
  client (proto→domain for inbound events). Avoids duplication and
  drift. SessionToProto/SessionFromProto handle the Session message.
- internal/server/connect/event_conv.go refactored to delegate to
  protoconv (432 lines → 14).

Config + main.go wiring:
- --daemon flag: selects remote daemon mode (TUI dials wakild over
  Unix socket instead of embedding the agent loop).
- --socket flag: overrides the default socket path
  (/wakild.sock or ~/.local/share/wakil/wakild.sock).
- cmd/wakil/daemon_mode.go: RunDaemonMode wires the remote bootstrap
  into the TUI — mirrors main.go's embedded path but uses remote.Bootstrap
  Remote instead of wiring.BootstrapTUI.

Tests: 6 tests pass with -race — dialer (socket exists, missing, non-
socket), default socket path, event round-trip conversion, pump dedup
logic, facade slash-command dispatch. Full suite passes (all 29
internal/ packages green).

* feat(card148): P2f — integration tests + buf breaking CI

End-to-end daemon↔client integration tests (cmd/wakild/integration_e2e_test.go):
- 9 tests exercising the full wire path over Unix socket: CreateSession,
  SubmitInput + ListEvents (verifies 7-event sequence), SessionSnapshot,
  CloseSession, Interrupt (verifies cancelled outcome), ResumeSession,
  DeleteSession, StreamEvents (real-time pump delivery), NewConversation
  (rotation). All pass with -race.

buf breaking CI (.github/workflows/ci.yml):
- New proto-breaking job: buf lint + buf breaking against base branch.
  Uses bufbuild/buf-action@v1, fetch-depth: 0 for full git history.
  PRs compare against origin/master; pushes compare against HEAD~1.
  buf.yaml already configured with WIRE_JSON + WIRE breaking rules.

Fix: merge daemon_mode.go into main.go (P2e regression).
- daemon_mode.go imported internal/tui, violating the headless seam test
  (TestHeadlessNoAgentImport: internal/tui is main.go-only). Moved
  RunDaemonMode → runDaemonMode into main.go, deleted daemon_mode.go.

* fix(card148): client workspace ID must be hashed, not raw path

runDaemonMode was passing cfg.WorkDir directly as the workspace ID
(e.g. "/home/valon/coding/wakil") instead of the hashed form
("wsp_<16-hex>") that the daemon expects. CreateSession rejected it
with invalid_argument because the raw path doesn't match the
WorkspaceID.Validate() format.

Fix: export wiring.WorkspaceIDFromConfig (was unexported) and use it
in both runDaemonMode (client) and cmd/wakild/main.go (daemon),
removing the duplicate copy. This ensures both sides derive the same
"wsp_"-prefixed SHA-256 hash of the effective workdir.

* feat(card148): P3 — read-only web UI

Add a static web console served by wakild via --http-addr flag.
The browser speaks the same Connect API (HTTP/JSON) as the TUI:

- web/embed.go: //go:embed for static assets
- web/index.html: SPA shell (sessions list + live viewer)
- web/app.js: vanilla JS RPC client (protojson camelCase, oneof
  payload access, 500ms polling for live events, renders all 32
  event kinds incl. tool calls and subagent tree)
- web/styles.css: Tokyo Night dark theme

Daemon changes:
- cmd/wakild/main.go: --http-addr flag (TCP, empty = disabled)
- cmd/wakild/server.go: dual-listener (Unix socket + optional TCP),
  webHandler combining Connect RPC + static files
- internal/server/connect/server.go: HandlerWithStatic method

Tests (6): static files, GetServerInfo, Health, ListSessions,
SessionSnapshot (with tool-call verification = exit gate),
ListEvents cursor pagination.

Exit gate: a running session is live-trackable in the browser,
including tool-calls and subagent tree.

go test -race ./... 36/36 packages green.
buf lint + buf breaking clean. gofmt + go vet clean.

* feat(card148): P4a — auth + tenancy schema (migration 002)

New migration 002_auth_tenancy.sql creates the control-plane tables for
auth and multi-tenancy per design doc §4.3:

- tenants (id, slug, display_name, status)
- users (id, email, display_name, auth_subject, password_hash, status)
  with partial UNIQUE index on auth_subject (non-NULL values only)
- memberships (tenant_id, user_id, role) with composite PK
- api_tokens (id, tenant_id, user_id, name, token_hash UNIQUE, scopes,
  expires_at, last_used_at, revoked_at) — composite FK to memberships
- join_tokens (id, tenant_id, user_id nullable, role, token_hash UNIQUE,
  created_by, expires_at, used_at) — composite FK on (tenant_id, created_by)
  to memberships; nullable user_id for create-on-exchange tokens

Indexes on all FK columns. Explicit ON DELETE clauses (CASCADE/SET NULL/
RESTRICT). Default tenant (tnt_local), user (usr_local), and owner
membership seeded via plain INSERT (tables are new within this migration).

sessions.tenant_id stays app-layer validated (accepted gap: SQLite cannot
add FKs to existing tables without recreation).

Tests (11 total, all pass with -race):
- Updated: TestApplyFreshDB (checks all 8 tables, version >= 2),
  TestApplyIdempotent (dynamic count), TestApplyReopen (pragmas on reopen),
  TestLoadMigrations (checks versions 1+2)
- New: TestApplyAuthTenancyTables (bootstrap + foreign_key_check),
  TestAuthTenancyConstraints (CHECK on role/status/expires_at),
  TestAuthTenancyFKs (all FK paths incl. composite FKs),
  TestAuthTenancyUnique (email/slug/token_hash/auth_subject),
  TestUpgradeFromV1 (data preservation across reopen + bootstrap)

Mashūra-reviewed: added UNIQUE on token_hash, composite FK on
join_tokens.created_by, partial unique on auth_subject, ON DELETE clauses,
FK indexes, fixed bootstrap to plain INSERT, expanded test coverage.

Verified: go build, go test -race, go vet, gofmt, buf lint, buf breaking —
all clean.

Trello: https://trello.com/c/Ba4YYGXM/148

* feat(card148): P4b — SO_PEERCRED local auth + fail-closed TCP

P4b implements Unix-socket peer-credential authentication (SO_PEERCRED) and
closes the TCP authentication bypass that existed since P3.

## What changed

### SO_PEERCRED + principal resolution
- New internal/auth/peercred package: platform-conditional extraction of
  Unix-socket peer credentials. Linux uses GetsockoptUcred (SO_PEERCRED);
  other platforms return unsupported (fail-closed).
- New internal/auth package: PrincipalResolver interface + LocalResolver
  that maps the daemon owner UID (os.Geteuid) to the seeded local owner
  principal (tnt_local/usr_local/owner/AuthLocal). Rejects all other UIDs
  including root. Fail-closed: no credentials -> ErrUnauthenticated.
- http.Server.ConnContext hook captures peer creds at connection-accept
  time and stores them in the request context. The resolver reads them
  per-request.
- All 11 Connect handler methods (8 SessionHandler + 3 EventHandler) now
  call resolvePrincipal(ctx, resolver) instead of the old localPrincipal()
  stub. SystemHandler (Health/GetServerInfo) remains intentionally
  unauthenticated — it exposes no session or tenant data.

### TCP security hole closed
- TCP listener now serves ONLY static files (webStaticHandler). Connect
  RPC handlers are NOT mounted on TCP. Before P4b, the TCP path served
  Connect RPC with EmbeddedPrincipal (owner) — an authentication bypass.
- HandlerWithStatic (Connect + static on one mux) deleted — it was a
  loaded footgun that could reopen the hole.
- serve() fixed: TCP server errors are logged, not returned to the
  daemon main loop (only Unix-socket errors stop the daemon).

### Store tenant predicates
- EventLog interface unchanged (Read/LastSeq have no tenant_id parameter).
  The service-layer lookup() is the tenant isolation gate, as documented
  in the EventLog contract. Cross-tenant tests (cross_tenant_test.go)
  verify all read/write paths reject cross-tenant access with
  ErrSessionNotFound. Mashura confirmed this is the correct stance;
  adding SQL predicates without a tenant input would be security theater.

### Error handling
- resolvePrincipal distinguishes auth.ErrUnauthenticated (CodeUnauthenticated)
  from other resolver errors (CodeInternal via mapError fallthrough).
  This prepares the seam for P4c DB-backed resolvers.
- errUnauthenticated added to errors.go map.
- Nil resolver guard: NewServer panics at construction, not at first request.
- ConnContext logs peercred extraction failures (fail-closed but visible).

## Mashura review
- Plan reviewed by 3 panels (gpt-5.6-sol, claude-fable-5, glm-5.2).
- Implementation reviewed by 3 panels.
- Key feedback folded in:
  * TCP security hole (critical blocker) — Connect removed from TCP
  * os.Geteuid() not os.Getuid() (real vs effective UID)
  * resolvePrincipal error distinction (auth vs internal)
  * HandlerWithStatic deletion (loaded footgun)
  * Stale comment fixes (listenUnix, tcpSrv field, serve(), role-refresh)
  * Embedded resolver kept in production package with explicit test-only
    doc (Go internal package visibility requires this for cmd/wakild tests)

## Tests
- internal/auth/peercred: Unix-socket + TCP extraction tests
- internal/auth: LocalResolver tests (owner UID, wrong UID, no creds, root)
- cmd/wakild/p4b_test: TCP does NOT serve Connect RPC, static files still
  served, Unix socket RPC works
- cmd/wakild/p4b_integration_test: real SO_PEERCRED path (success on Linux,
  UID mismatch rejection, no-credentials rejection over TCP)
- Existing cross-tenant tests (cross_tenant_test.go) verify service-layer
  tenant isolation for all read/write paths
- Web integration tests updated: RPCs now go through Unix socket, TCP
  serves only static files

Verified: go build ./..., go test -race (cmd/wakild, server/connect, auth,
sessionhost), go vet, gofmt — all clean.

Refs: Trello card #148 (https://trello.com/c/Ba4YYGXM/148)

* P4c: join token system + session cookies

Implements the join token onboarding flow and browser session cookies
for the wakild daemon. Admin (local owner via SO_PEERCRED) issues
one-time-use join tokens; clients exchange them for opaque server-side
session cookies stored in SQLite.

New packages:
- internal/auth/tokenstore: DB queries for join_tokens + web_sessions
- internal/auth/jointoken: token generation (256-bit CSPRNG, jnt_ prefix),
  issuance with role-based authorization, atomic one-transaction exchange
- internal/auth/tokenresolver: WebSessionResolver (cookie -> principal),
  distinguishes ErrCredentialAbsent (try next) from ErrInvalidCredential
  (hard fail, no fallthrough)

New proto:
- auth.proto + auth_service.proto: AuthService with CreateJoinToken,
  ListJoinTokens, RevokeJoinToken, ExchangeJoinToken (public),
  WhoAmI, Logout. No Login/Refresh (OIDC/P4e scope).

Migration 003:
- web_sessions table (opaque token, SHA-256 hashed, sliding + absolute
  expiry, FK to memberships ON DELETE CASCADE)
- join_tokens recreation: fix ON DELETE SET NULL -> CASCADE (security:
  SET NULL converts bound tokens to create-user-on-exchange tokens),
  add revoked_at column

Security (Mashura-reviewed, 3 panels):
- 256-bit CSPRNG token entropy, SHA-256 hash at rest
- One-time-use via conditional UPDATE (used_at IS NULL AND expires_at > now
  AND revoked_at IS NULL) in a single transaction
- auth_subject NEVER from exchange request (OIDC only, P4e)
- Only owners can issue owner-role tokens
- Cookie: HttpOnly, SameSite=Strict, Path=/
- Origin validation middleware (CSRF defense-in-depth)
- Generic errors (no token enumeration: expired vs used vs revoked)
- Server-side sessions: immediate revocation, current role read from
  memberships at resolve time (not cached in session row)

Daemon wiring:
- TCP server now mounts Conne…
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