diff --git a/.claude/rules/production-cli-resolution.md b/.claude/rules/production-cli-resolution.md index fac41e0d15..997466e520 100644 --- a/.claude/rules/production-cli-resolution.md +++ b/.claude/rules/production-cli-resolution.md @@ -21,6 +21,7 @@ paths: |---|---| | `claude` | Claude harness chat/execution and reserved user-scope `ralphx` MCP registration cleanup | | `codex` | Codex harness chat/execution and capability probes | +| `tailscale` | remote host tailnet discovery and Serve exposure | | `gh` | GitHub auth, PR polling, PR/release operations | | `git` | repository state, diffs, worktrees, merge/cleanup | | `node` | bundled RalphX MCP servers | diff --git a/.claude/rules/remote-facade.md b/.claude/rules/remote-facade.md new file mode 100644 index 0000000000..1a4990f768 --- /dev/null +++ b/.claude/rules/remote-facade.md @@ -0,0 +1,29 @@ +--- +paths: + - "src-tauri/src/remote_server/**" + - "src-tauri/src/commands/remote_*.rs" + - "src-tauri/crates/ralphx-remote-protocol/**" + - "frontend/src/lib/remote/**" + - "frontend/src/api/*.ts" +--- + +> **Maintainer note:** This file optimizes for LLM context efficiency. Rules: (1) Tables > prose (2) One example max per concept (3) No redundant explanations (4) Use symbols: → = leads to, | = or, ❌/✅ = wrong/right (5) Before adding content, ask: "Can this be a single line?" If yes, make it one line. + +# Remote Facade + +Scoped rules for the `:3849` host/client surface. These were root `CLAUDE.md` principles 27-28; they moved here so they load only for the code they govern. + +| Rule | Detail | +|---|---| +| Command registration (NON-NEGOTIABLE) | Every `:3849`-reachable command is a hand-audited `remote_server/registry.rs` allowlist entry with a `capability_ledger.rs` class; never a passthrough, a `generate_handler!` edit, or a command fork. Details: `docs/architecture/remote-protocol.md` | +| Event fan-out (NON-NEGOTIABLE) | Remote events travel by classification-table delivery class — Durable rides the sequencer, Transient broadcasts with no seq and is never persisted, LocalOnly never leaves the host. ❌ Re-deriving remote event sources from `EventSink`/`AppState.events` | +| Twin naming | A spawn-free twin is named for what its closure does (persists an intent), not for what the host later does (starts an agent). | +| Absence is the signal | Client gates derive `unavailable` from a command's absence in the generated manifest — never from a hardcoded name list. | +| Spawn-distinct paths are distinct FUNCTIONS (NON-NEGOTIABLE) | When a local path may reach a spawn/recovery seam and its twin may not, express the difference as two functions sharing only work that names NEITHER seam. ❌ One body behind a `bool`/enum flag: the ledger detectors are call-graph based, so a shared body that names both seams puts the spawned side in the twin's closure whatever the flag does at runtime. Measured 2026-08-05 on `set_agent_conversation_muted`: the flag form ran 18 hops into `reconcile_reserved_claude_registration` and the corrective-transition sinks, failing `detector_c_floors_process_spawn_authority`, `batch13_detector_gap_is_measured_not_inherited`, and `no_registered_facade_target_reaches_a_corrective_transition`. Same reason a closure/bare-fn indirection must not be used to *hide* a launch — the gate is only worth what the graph can see. | +| Test the real envelope | Remote client tests assert the shapes the transport actually produces — `{outcome: "commandError", error: }` per `network-invoke.ts`, explicit nulls, real casing. ❌ Mock-convenient envelopes (`{outcome: "error", …}`) that no client path can emit; they pass while proving nothing. | + +## Client surfaces + +- **Remote Connection Journal** — `stores/remoteConnectionJournalStore.ts` is the per-environment connection diagnostics ring buffer; `lib/remote/environment-runtime.ts` is its single writer, the banner Details dialog its reader. Remote HTTP reads must lift the host's `REMOTE_COMMAND_UNAVAILABLE` envelope into `RemoteTransportError` (capability boundary, tolerated by the hydration barrier) — ❌ flattening it into generic HTTP errors. +- **Tauri Plugin Prefix Rule** — every `plugin:*` command (opener/dialog/fs/updater/process/global-shortcut/notification) routes to THIS device via the one prefix rule in `lib/remote/local-only-commands.ts`; their subject is the machine showing the UI. Host-targeted plugin calls need a reviewed row in `HOST_TARGETED_PLUGIN_COMMANDS` (empty today) and then a registration or ledger row. ❌ Per-call-site remote branching for plugin invokes; ❌ passing a host filesystem path to `openPath`/`revealItemInDir` — those degrade through host-affordance gating to `HostPathCopyButton`. +- **Syncing Presentation** — `lib/remote/supervisor-presentation.ts` owns the `syncing` projection (live socket mid-hydration → chip-only accent pulse in `EnvironmentSwitcher`, no banner) with K=2 barrier-failure / T=12s one-way escalation back to `reconnecting`; still read-only. ❌ New surfaces reading FSM state directly to infer "connection dropped". diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 95c13ad952..5c6893a530 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -62,6 +62,10 @@ jobs: - 'scripts/tests/test-ci-path-filters.py' - 'scripts/tests/test-claude-rule-utils.py' - 'scripts/tests/test-claude-rules-activation.py' + - 'scripts/check-raw-tauri-event-listen.mjs' + - 'scripts/tests/test-raw-tauri-event-listen-guard.sh' + - 'scripts/event-manifest-scanner/**' + - 'scripts/event-manifest.json' - 'scripts/build-prod-release.sh' - 'scripts/render-homebrew-cask.sh' - 'scripts/reconcile-homebrew-cask.sh' @@ -206,6 +210,10 @@ jobs: shell: bash run: bash scripts/tests/test-coverage-rust-shards.sh + - name: Validate raw Tauri event listen guard + shell: bash + run: bash scripts/tests/test-raw-tauri-event-listen-guard.sh + - name: Validate CI path filters shell: bash run: python3 scripts/tests/test-ci-path-filters.py @@ -252,6 +260,76 @@ jobs: - name: Check JavaScript and Rust Tauri package alignment run: node scripts/check-tauri-package-alignment.mjs . + # UNCONDITIONAL BY DESIGN — no `needs: changes`, no path filter. + # + # These guards enforce the remote command facade's P-11 invariant (zero + # unclassified commands) and the currency of the generated manifest/mirrors. + # A backend-only PR that registers or reclassifies a Tauri command, and a + # docs-only PR that hand-edits docs/generated/remote-commands.json, both + # violate these invariants without touching `frontend/**` or `src-tauri/**`. + # Gating this job would restore the "merge first, discover red on main" + # behaviour it exists to prevent. + remote-facade-guards: + name: Remote Facade Guards + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v6 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: frontend/package-lock.json + + # check-remote-transport-drift.mjs parses frontend TypeScript with the + # compiler resolved from frontend/package.json, so the frontend dev + # dependencies must be present. + - name: Install frontend dependencies + run: npm ci --no-audit --no-fund + working-directory: frontend + + - name: Guard self-test (drift detector) + run: node scripts/check-remote-transport-drift.mjs --self-test + + - name: Raw Tauri event listen guard + run: node scripts/check-raw-tauri-event-listen.mjs . + + - name: Remote transport drift (P-11 — zero unclassified) + run: node scripts/check-remote-transport-drift.mjs . + + - name: Local-only backend event mirror is current + run: node scripts/check-local-only-event-mirror.mjs . + + - name: Remote protocol vocabulary mirror is current + run: node scripts/check-remote-vocabulary-mirror.mjs . + + - name: Agent-control capability mirror is current + run: node scripts/check-agent-control-command-mirror.mjs . + + - name: Remote coverage census is current + run: node scripts/generate-remote-coverage-census.mjs --check + + event-manifest: + name: Event Manifest + needs: changes + if: needs.changes.outputs.run_automation == 'true' || needs.changes.outputs.run_rust == 'true' || needs.changes.outputs.run_frontend == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - name: Setup Rust + uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 + with: + toolchain: 1.91.0 + + - name: Test event manifest scanner + run: cargo test --manifest-path scripts/event-manifest-scanner/Cargo.toml --test scanner + + - name: Verify event manifest is current + run: cargo run --manifest-path scripts/event-manifest-scanner/Cargo.toml -- --check + rust-ipc-contracts: name: Rust IPC Contracts needs: changes @@ -315,6 +393,12 @@ jobs: env: CARGO_PROFILE_TEST_DEBUG: "1" RALPHX_TEST_MODE: "1" + # Archiving every test binary is the heaviest link step in CI. On this branch's Rust + # surface the job went silent mid-compile and the runner was reaped (exit 143) on every + # recent attempt, with 100+ GB of disk free — the signature of a memory kill on a 16 GB + # `ubuntu-latest`. Same value the Rust IPC Coverage job already uses; the 40 min budget + # absorbs the slower build (deaths were at ~7 min). Raise it if the runners grow. + CARGO_BUILD_JOBS: "1" steps: - uses: actions/checkout@v6 @@ -356,7 +440,7 @@ jobs: retention-days: 1 rust-lib-tests: - name: Rust Lib Tests (Shard ${{ matrix.shard }}/2) + name: Rust Lib Tests (Shard ${{ matrix.shard }}/3) needs: - changes - rust-lib-nextest-archive @@ -366,7 +450,7 @@ jobs: strategy: fail-fast: false matrix: - shard: [1, 2] + shard: [1, 2, 3] env: RALPHX_TEST_MODE: "1" steps: @@ -391,19 +475,19 @@ jobs: name: rust-lib-tests-nextest-archive path: . - - name: Run Rust lib tests shard ${{ matrix.shard }}/2 + - name: Run Rust lib tests shard ${{ matrix.shard }}/3 run: | cargo nextest run \ --archive-file rust-lib-tests.tar.zst \ --workspace-remap "${GITHUB_WORKSPACE}/src-tauri" \ --profile ci \ - --partition hash:${{ matrix.shard }}/2 + --partition hash:${{ matrix.shard }}/3 - name: Upload Rust lib test timings if: always() uses: actions/upload-artifact@v7 with: - name: rust-lib-tests-junit-${{ matrix.shard }}-of-2 + name: rust-lib-tests-junit-${{ matrix.shard }}-of-3 path: src-tauri/target/nextest/ci/junit.xml if-no-files-found: warn retention-days: 7 @@ -451,6 +535,11 @@ jobs: timeout-minutes: 30 env: RALPHX_TEST_MODE: "1" + # --all-targets --all-features peaks past the 16 GB on ubuntu-latest once the + # crate graph is this large: the job was reaped mid-compile with exit 143 + # ("runner has received a shutdown signal"), not a lint failure. Same cap, and + # same reason, as the coverage archive job. Raise if the runner grows. + CARGO_BUILD_JOBS: "1" steps: - uses: actions/checkout@v6 diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index bf0f1fc152..d791dbd065 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -149,8 +149,12 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 40 env: - # Coverage-instrumented lib builds exceed the hosted runner memory limit - # when Cargo compiles crates concurrently. + # Coverage-instrumented lib builds exceed the hosted runner memory limit when Cargo + # compiles crates concurrently. Observed on this branch, whose Rust surface is larger: + # the job went silent mid-compile and the runner was reaped (exit 143, "runner has + # received a shutdown signal") on 3 of 4 attempts at 4 parallel rustc processes, + # against the 16 GB on `ubuntu-latest`. Raise it back if the crate graph shrinks or + # the runner size grows. CARGO_BUILD_JOBS: "1" CARGO_TERM_COLOR: never RALPHX_TEST_MODE: "1" diff --git a/CLAUDE.md b/CLAUDE.md index 04a6e30dce..8432062ee0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,7 +15,7 @@ If one is missing, skip it and continue; do not fail work or run bare `sed`/`cat ## Project: RalphX Native Mac GUI for autonomous AI dev: Kanban, multi-agent orchestration, ideation chat. -Code quality: `.claude/rules/code-quality-standards.md` | State machine: `.claude/rules/task-state-machine.md` | Stateful review: `.claude/rules/stateful-workflow-review.md` | Big-PR checks: `.claude/rules/big-pr-review-checklist.md` | Git/merge: `.claude/rules/task-git-branching.md` | Merge recovery: `.claude/rules/merge-recovery-consistency.md` | Review modes: `.claude/rules/agent-workspace-review-modes.md` | Agents: `.claude/rules/task-execution-agents.md` | Delegation: `.claude/rules/delegation-topology.md` | Thinking: `.claude/rules/agent-thinking-capture.md` | Runtime roots: `.claude/rules/runtime-root-vs-target-project.md` | Prod CLI: `.claude/rules/production-cli-resolution.md` | CodeQL paths: `.claude/rules/codeql-path-safety.md` | Ideation verification: `.claude/rules/ideation-verification-architecture.md` | Blocker dedupe: `.claude/rules/followup-blocker-dedupe.md` | Agent types: `.claude/rules/agent-type-map.md` | Detail views: `.claude/rules/task-detail-views.md` | Frontend perf: `.claude/rules/frontend-interaction-performance.md` | Icons: `.claude/rules/icon-only-buttons.md` | Rust API: `.claude/rules/rust-stable-apis.md` | Rust tests: `.claude/rules/rust-test-execution.md` | WKWebView CSS: `.claude/rules/wkwebview-css-vars.md` | Release scripts: `.claude/rules/release-script-validation.md` | Assets: `.claude/rules/assets.md` | Tauri invoke: `.claude/rules/tauri-invoke-conventions.md` | PR body (READ): `.claude/rules/pr-descriptions.md` +Code quality: `.claude/rules/code-quality-standards.md` | State machine: `.claude/rules/task-state-machine.md` | Stateful review: `.claude/rules/stateful-workflow-review.md` | Big-PR checks: `.claude/rules/big-pr-review-checklist.md` | Git/merge: `.claude/rules/task-git-branching.md` | Merge recovery: `.claude/rules/merge-recovery-consistency.md` | Review modes: `.claude/rules/agent-workspace-review-modes.md` | Agents: `.claude/rules/task-execution-agents.md` | Delegation: `.claude/rules/delegation-topology.md` | Thinking: `.claude/rules/agent-thinking-capture.md` | Runtime roots: `.claude/rules/runtime-root-vs-target-project.md` | Prod CLI: `.claude/rules/production-cli-resolution.md` | CodeQL paths: `.claude/rules/codeql-path-safety.md` | Ideation verification: `.claude/rules/ideation-verification-architecture.md` | Blocker dedupe: `.claude/rules/followup-blocker-dedupe.md` | Agent types: `.claude/rules/agent-type-map.md` | Detail views: `.claude/rules/task-detail-views.md` | Frontend perf: `.claude/rules/frontend-interaction-performance.md` | Icons: `.claude/rules/icon-only-buttons.md` | Rust API: `.claude/rules/rust-stable-apis.md` | Rust tests: `.claude/rules/rust-test-execution.md` | WKWebView CSS: `.claude/rules/wkwebview-css-vars.md` | Release scripts: `.claude/rules/release-script-validation.md` | Remote facade: `.claude/rules/remote-facade.md` | Assets: `.claude/rules/assets.md` | Tauri invoke: `.claude/rules/tauri-invoke-conventions.md` | PR body (READ): `.claude/rules/pr-descriptions.md` CodeQL path safety applies to production and tests; use process-owned runtime roots, fixed entry lists, pure test builders, and suppress `rust/path-injection` only after containment validation. Production CLI resolution applies to installed app launches; all runtime subprocess binaries must go through the shared resolver surface. @@ -107,6 +107,7 @@ style={{ boxShadow: "none", outline: "none" }} - **Session Recovery** — Expired Claude session recovery with history preservation. Docs: `docs/features/session-recovery.md` - **Plan Verification** — Automated adversarial review loop for ideation plans. Docs: `docs/features/plan-verification.md` | Architecture: `.claude/rules/ideation-verification-architecture.md` - **Agent Personas** — Conversation-bound prompt-only behavior profiles for Project Agent conversations. Docs: `docs/features/agent-personas.md` +- **Remote Access** — Two-instance host/client mode over an authenticated `:3849` listener. Docs: `docs/features/remote-access.md` | Protocol: `docs/architecture/remote-protocol.md` ## Git Conventions ❌ git init/push/remotes | Prefixes: `docs:` | `feat:` | `fix:` | `chore:` diff --git a/config/ralphx.yaml b/config/ralphx.yaml index c79b696926..d731e3d6f4 100644 --- a/config/ralphx.yaml +++ b/config/ralphx.yaml @@ -67,6 +67,13 @@ ui: standalone_conversations: false # A20 escape hatch: skip --resume on the send after a persona switch. persona_switch_forces_fresh_provider_session: false + # Remote multi-environment client UI. + # Env override: RALPHX_UI_REMOTE_ENVIRONMENTS=true|false + # Shows the Remote Access + Connections settings panes, the environment + # switcher, and the connection banner. It does NOT expose this host — that + # is a separate DB-persisted toggle (RemoteHostSettings.enabled, default + # false) inside the Remote Access pane. + remote_environments: true # Compatibility runtime row for the PersonaBuilder-only extractor. Canonical # `agents/ralphx-persona-extractor/agent.yaml` remains authoritative. @@ -249,6 +256,8 @@ git: workspace_freshness_cache_ttl_ms: 2000 # Env: RALPHX_GIT_WORKSPACE_FRESHNESS_CACHE_TTL_MS # diff_commands.rs — short backend TTL for workspace review context/payload reuse workspace_review_cache_ttl_ms: 2000 # Env: RALPHX_GIT_WORKSPACE_REVIEW_CACHE_TTL_MS + # diff_commands.rs — long TTL for spawn-free paired-device workspace snapshots + remote_workspace_snapshot_ttl_ms: 86400000 # Env: RALPHX_GIT_REMOTE_WORKSPACE_SNAPSHOT_TTL_MS # agent_workspace_pr_description.rs — short backend TTL for precomputed publish PR descriptions workspace_pr_description_cache_ttl_ms: 300000 # Env: RALPHX_GIT_WORKSPACE_PR_DESCRIPTION_CACHE_TTL_MS # diff_commands.rs — short backend TTL for live GitHub PR annotation payload reuse diff --git a/docs/architecture/remote-protocol.md b/docs/architecture/remote-protocol.md new file mode 100644 index 0000000000..d449ae688b --- /dev/null +++ b/docs/architecture/remote-protocol.md @@ -0,0 +1,345 @@ +# RalphX Remote Protocol (v1) + +Developer-facing contract for the RalphX remote host surface — the wire a client environment +(desktop client mode today, the mobile app next) speaks to a RalphX host. + +This document is the **cross-spec contract**. A client written against it should need no +knowledge of RalphX internals. Where this document and older spec text disagree, this document +and `docs/handoffs/remote-mobile/spec-amendment-proposal.md` are authoritative — the amendments +record the places the implementation is right and the original spec prose is stale. + +Everything here ships dark behind the backend `remote_host` settings row and the frontend +`remoteEnvironments` flag. + +--- + +## 1. Transport and versioning + +| Property | Value | +|---|---| +| Default port | `3849` (`:3847` is the local-only backend and is never remote-reachable) | +| Bind policy | loopback, or a validated tailnet address — never `0.0.0.0` | +| Protocol version | `PROTOCOL_VERSION = 1`, advertised in the descriptor and in `hello` | +| Client floor | `MIN_CLIENT_PROTOCOL = 1`, an **independent** constant | + +`MIN_CLIENT_PROTOCOL` is deliberately *not* aliased to `PROTOCOL_VERSION`. Raising the floor +refuses every already-shipped client at the descriptor gate, so it is a deliberate +compatibility decision that must be argued on its own terms — never a side effect of bumping +`PROTOCOL_VERSION`. Evolution is additive (R-7): new frames, fields, and commands may appear at +version 1; removals or renames require a floor raise. + +### 1.1 Endpoint table + +| Method | Path | Auth | Purpose | +|---|---|---|---| +| GET | `/.well-known/ralphx/environment` | **pre-auth** | Descriptor: environment id, protocol version, min client protocol, server version, platform | +| POST | `/remote/v1/auth/pair` | **pre-auth** | Exchange a pairing code for a long-term device token | +| POST | `/remote/v1/auth/ws-ticket` | bearer | Mint a single-use WS ticket | +| POST | `/remote/v1/auth/revoke` | bearer, self-scoped | Device revokes itself; identity comes from the middleware, there is no device-id argument | +| GET | `/remote/v1/session` | bearer | Confirmed scope introspection for the calling device | +| POST | `/remote/v1/invoke` | bearer | The command facade | +| GET | `/remote/v1/events?ticket=…` | ticket | WebSocket event stream | +| GET | `/health` | bearer | Liveness | + +Two properties are load-bearing and CI-enforced: + +- **The pre-auth allowlist has exactly two entries** — descriptor and pairing. Everything else, + including `/health`, runs behind the bearer check. There is no zero-devices bootstrap + exception (A-2): a host with no paired devices still authenticates every other route. +- **`/remote/v1/events` is ticket-authenticated, not pre-auth.** A browser cannot attach an + `Authorization` header to a WebSocket upgrade, so the client mints a single-use, device-bound + ticket over the bearer-authenticated route first. It is tracked in a separate allowlist so the + "exactly two pre-auth routes" guarantee stays literally true. + +A curated set of read-only `:3847` fetch routes is remounted on `:3849` behind the same auth and +scope middleware, reusing the same handler functions. The remount list is a checked-in +allowlist; route-set equality is asserted in CI. Named-denied sinks (`/api/permission/resolve`, +`/api/question/resolve`, `/api/add_task_note`) stay unmounted. + +--- + +## 2. Authentication and authorization + +### 2.1 Pairing + +The host mints a short-lived pairing code. The client posts the code plus a device name, its +client version, and the scopes it requests. The host **intersects** the request with the code's +grant and issues a long-term device token (`rxd_live_…`). + +Pairing codes are single-use: the second exchange of the same code is refused. + +### 2.2 Scopes + +| Scope | Meaning | +|---|---| +| `ui:read` | Read the workspace | +| `ui:operate` | Global pause/stop brakes, attachment handling, and low-risk edits | +| `ui:agent` | Start, resume, restart, or steer an agent | +| `ui:elevated` | **Reserved, not implemented.** Placeholder for the deferred terminal/PTY surface | + +The default pairing grant is `ui:read + ui:operate`. **`ui:agent` is not grantable at pairing +time** — the pairing-grant validator refuses it. It is a separate, off-by-default, per-device +toggle. See §6 and `docs/features/remote-access.md` for why that separation exists. + +### 2.3 Capability classes + +Every registered command carries a hand-audited risk class. The class mechanically determines +the scope required (`scope_for_class`); the scope is never declared independently, so the two +cannot drift. + +| Class | Required scope | Meaning | +|---|---|---| +| `Read` | `ui:read` | No downstream authority | +| `Operate` | `ui:operate` | Brakes and low-risk mutations | +| `PathScoped` | `ui:operate` | Operate, plus a path-containment predicate | +| `AgentControl` | `ui:agent` | Can start or steer an agent, directly or by seeding state a background loop consumes | +| `Elevated` | `ui:elevated` | Spawns processes / touches credentials — unreachable in v1 | +| `Denied` | — | **Unregistrable.** Registering one fails compilation | + +Classes are backed by an eleven-member capability vocabulary: `spawnsProcess`, +`writesArbitraryPath`, `mutatesWorkingDirectory`, `configuresFutureProcessAuthority`, +`touchesCredentials`, `ptyControl`, `agentControl`, `seedsSpawnTriggeringState`, +`mutatesAgentConsumedContent`, `hostManagement`, `deletesEntity`. A capability that the declared +class does not permit is a **compile error**, not a lint. + +Two audit rules matter to a client author: + +- The classification traces *downstream* authority, not immediate action. A command that merely + writes a database row is `AgentControl` if a background loop turns that row into a spawn. +- The default-tier brakes are deliberately narrow: `pause_execution` and `stop_execution` stay + `ui:operate` because they set the process-wide pause gate before any task transition. + `deny_permission_request` also stays `ui:operate` and server-pins `decision = "deny"`, so a + client that sends `"allow"` still denies. Per-task `block_task`, `pause_task`, and `stop_task`, + plus bulk `pause_tasks_in_group` and `cancel_tasks_in_group`, require `ui:agent`: agent-active + exits can run Git side effects, and `block_task` can free capacity and ask the scheduler to + launch queued work. + +### 2.4 The manifest + +`docs/generated/remote-commands.json` is the generated, diff-checked manifest: every registered +command with its risk class and capability set, plus six audit tables (loop inventory, state +surface, content surface, `WorkerTaskView` allowlist, exemption table, declared memberships). +Staleness fails CI. + +Two documented reductions from the originally specified schema: `scope` is omitted because it is +mechanically derivable from `riskClass` and duplicating it would invite drift; `argNames` is +omitted because the wire argument surface is guarded by the frontend AST census (P-11) instead. +A consumer that needs `argNames` should request it as a schema addition rather than infer it. + +### 2.5 Revocation + +Revocation is durable-first and takes effect on **live** sessions, not just future requests: the +existing WebSocket closes with `error{revoked}` within the heartbeat window, and the next bearer +request 401s. A client must treat both as the same event. + +--- + +## 3. The command facade + +``` +POST /remote/v1/invoke +Authorization: Bearer rxd_live_… +{ "requestId": "", "cmd": "list_tasks", "args": { … } } +``` + +The facade is an **allowlist**, never a passthrough. Commands are registered one at a time in +`remote_server/registry.rs` against the existing local Tauri command functions — there are no +handler forks and no `generate_handler!` edits, so remote and local dispatch cannot diverge. +Serialization is byte-identical to local Tauri IPC across argument shapes and error paths. + +`requestId` is client-minted and binds mutation dedup: replaying the **same** id returns the +cached outcome instead of executing twice. A client retrying a mutation must reuse the id; a +client starting new work must mint a new one. + +### 3.1 Error taxonomy — exactly ten codes + +| Code | HTTP | Retryable | Meaning | +|---|---|---|---| +| `REMOTE_UNAUTHORIZED` | 401 | no | Missing, revoked, or invalid credential | +| `REMOTE_FORBIDDEN` | 403 | no | Authenticated, but the device lacks the required scope | +| `REMOTE_COMMAND_UNAVAILABLE` | 404 | no | Not in the allowlist | +| `REMOTE_INVALID_ARGUMENTS` | 400 | no | Argument shape rejected — an identical resend cannot succeed | +| `REMOTE_VERSION_MISMATCH` | — | no | Client below `MIN_CLIENT_PROTOCOL` | +| `REMOTE_REQUEST_IN_PROGRESS` | — | yes | Same `requestId` still executing | +| `REMOTE_REQUEST_ID_REUSED` | — | no | `requestId` reused for different arguments | +| `REMOTE_UNREACHABLE` | — | yes | Transport failure | +| `REMOTE_TIMEOUT_UNKNOWN` | — | **unknown** | Outcome indeterminate — retry only with the same `requestId` | +| `REMOTE_INTERNAL_ERROR` | 500 | yes | Host-side failure, distinct from transport failure | + +`REMOTE_TIMEOUT_UNKNOWN` is the only code where the client must not assume the mutation did *or* +did not happen. Resolve it by replaying the same `requestId`. + +Four causes move a client environment to `blocked` rather than `reconnecting`: 401/403, version +mismatch, malformed descriptor, invalid arguments. + +--- + +## 4. The event stream + +### 4.1 Frames + +Server → client: + +| Frame | Fields | Purpose | +|---|---|---| +| `hello` | `protocolVersion`, `environmentId`, `streamEpoch`, `serverVersion`, `maxSeq`, `heartbeatSecs` | First frame after upgrade | +| `event` | `seq` (**optional**), `name`, `payload` | An application event | +| `replayDone` | `throughSeq` | Cursor replay finished | +| `reset` | `reason` | Warm resume is impossible; cold hydrate | +| `heartbeat` | `t` | Liveness probe | +| `error` | `code`, `message` | Terminal error for this session | + +Client → server: `subscribe{afterSeq, streamEpoch}`, `cursorAck{seq}`, `heartbeatAck{t}`. + +Heartbeat cadence is 20 s and the session closes after two unacked beats, so a dead host or a +half-open socket is detected in roughly 40 s. + +### 4.2 Delivery classes + +Every event name is classified in a checked-in table with one of three deliveries: + +- **Durable** — sequenced, persisted to `remote_event_log`, replayable by cursor. +- **Transient** — broadcast only, **bypassing the sequencer entirely**. Carries **no `seq`**. +- **LocalOnly** — never leaves the host. + +The `seq: null` on a transient frame is a contract, not an accident: a transient frame must +never advance a client's resume cursor. High-volume streaming (`agent:chunk`, +`agent:usage_updated`) is Transient and is **never** written to `remote_event_log`. Resume for +those surfaces is owned by snapshots and fetch routes, not by replay — a client that reconnects +mid-turn repairs the missing chunk text by re-fetching the message, not by asking for chunks +again. + +`agent_terminal:event` is the single `excluded_from_v1` class. No PTY route is mounted, no +terminal command is registered, and the terminal drawer is hidden for remote environments. +Re-enabling it requires the deferred `ui:elevated` scope. + +### 4.3 The `H` barrier and the canonical resume rule + +`streamEpoch` identifies one contiguous run of the durable log. It resets per host boot and rolls +live under overload. **A cursor is only meaningful within its epoch.** + +Cold hydrate: + +1. Connect; read `H = hello.maxSeq`. +2. Fetch snapshots (the REST/fetch surface) for the state you need. +3. `subscribe{afterSeq: H, streamEpoch}`. +4. Apply events from `H+1` onward. + +Taking `H` **before** fetching snapshots is what makes this safe: any event that lands during the +fetch is above `H` and therefore replayed, so nothing is lost. Events at or below `H` are already +reflected in the snapshot, so nothing is double-applied. + +Warm resume: `subscribe{afterSeq: lastCommittedSeq, streamEpoch}`, apply the replay, then treat +`replayDone` as the point where live delivery resumes. + +The client must **cold hydrate**, not splice, whenever it receives `reset`: + +| Reason | Cause | +|---|---| +| `cursor_pruned` | Retention advanced past the cursor — the missed rows no longer exist | +| `epoch_changed` | The epoch rolled; the old cursor addresses a different log | +| `after_seq_gt_max` | The client's cursor is ahead of the host (host rollback / different host) | +| `read_error` | The host could not read the durable range — **fail closed**, never treated as "no rows" | +| `revoked` | Credential revoked | +| `host_disabled` | Host mode turned off | + +`read_error` deserves emphasis: an empty replay and a failed replay are different, and a client +that conflates them silently drops events. + +### 4.4 Sequencer, leases, and retention + +A single-actor sequencer allocates seqs, micro-batches commits, then publishes. Publication +happens strictly **after** the commit, so a frame a client has seen is always durable. + +Under overload the sequencer **rolls the epoch live** rather than blocking emitters. Connected +clients get `reset{epoch_changed}` and cold hydrate. Blocking the application to preserve a +remote client's cursor is never the right trade. + +Retention is `max(50 000 rows, 7 days)`. A subscribed client holds a **retention lease** at its +acked cursor; the pruner deletes only rows at or below the minimum live lease cursor. A client +that stops acking has its lease TTL-expire, after which the pruner is free to advance past it — +and its next interaction yields `reset{cursor_pruned}`. + +`cursorAck` means **committed**, not merely received. A background observer that counts events +without projecting them must therefore **not** ack: acking would mint a resume cursor with no +commit behind it. Background environments let the lease expire and absorb the resulting +`cursor_pruned` as ignorable, because reactivation is always a full cold hydrate anyway. + +### 4.5 RS-EXT-1 — the canonical forwarder source + +Any external push forwarder (mobile push, webhooks) **must** tap the classified capture / +sequencer broadcast stream. It must never re-derive event sources from `EventSink` or +`AppState.events`. + +The classified stream is the only place where the delivery classification, the v1 exclusions, and +the durable ordering have all been applied. A forwarder that reads the raw sink bypasses all +three and will eventually forward a `LocalOnly` event, a terminal byte, or an unordered +duplicate. + +--- + +## 5. Client-side model + +### 5.1 Supervisor FSM + +One supervisor per environment owns the connection lifecycle: + +``` +idle → connecting → connected → degraded → reconnecting → connected + ↓ ↓ + blocked ←──────────────────┘ +``` + +`blocked` is terminal-until-user-action and is entered only for the four causes in §3.1. It is +deliberately distinct from `reconnecting`: retrying a 403 forever is a bug, not resilience. + +### 5.2 Environment isolation + +Environments are isolated by construction: per-environment QueryClient, per-environment event +bus, per-environment cursor. Two invariants are enforced by tests: + +- **An inactive environment never advances its warm cursor and never mutates its cache.** Any + observation it does (badge counts) is observation only; reactivation is always a full + `H`-barrier cold hydrate. +- **A background environment issues health-only operations** (descriptor probe, heartbeat) + through the Rust proxy. It may not issue arbitrary invokes. + +The local environment is never affected by remote flapping. + +### 5.3 Mobile non-preclusion + +Nothing in this protocol assumes a Tauri client. The facade is plain HTTP + JSON, the stream is a +plain WebSocket, and no path rides `window.__TAURI_INTERNALS__`. A mobile client consumes §1.1, +§2, §3.1, and §4 verbatim. + +--- + +## 6. Security boundary + +- `:3847` (the local backend) is **loopback-only and byte-identical** to its non-remote form. No + `:3847` trust-header handler is reachable on `:3849`: presenting `X-RalphX-Tauri-MCP: 1` to the + remote listener yields 401, and trust headers are stripped at the remote router's edge. +- CORS on `:3849` admits only the shipped app origins. +- The command facade is an allowlist; the fetch remount is an allowlist; the pre-auth surface is + two routes. All three are asserted as *equalities* in CI, so an accidental addition fails. +- `ui:agent` is the real trust boundary. See `docs/features/remote-access.md` §"What you are + actually granting" — a stolen `ui:agent` token lets the holder run code on the host machine. + +--- + +## 7. Where the code lives + +| Concern | File | +|---|---| +| Router, routes, allowlists | `src-tauri/src/remote_server/mod.rs` | +| Pairing, tokens, revocation | `src-tauri/src/remote_server/auth.rs`, `auth_endpoints.rs` | +| Command allowlist | `src-tauri/src/remote_server/registry.rs` | +| Risk classes and capabilities | `src-tauri/src/remote_server/capability_ledger.rs` | +| Event classification + capture | `src-tauri/src/remote_server/capture.rs`, `crates/ralphx-remote-protocol` | +| Sequencer, epoch, publication | `src-tauri/src/remote_server/sequencer.rs` | +| Leases, prune, retention | `src-tauri/src/remote_server/retention.rs` | +| WS sessions, heartbeat, replay | `src-tauri/src/remote_server/ws.rs` | +| Fetch remount allowlist | `src-tauri/src/remote_server/fetch_remount.rs` | +| Two-instance test fixture | `src-tauri/src/remote_server/harness.rs` (`test-utils`) | +| E2E suite | `src-tauri/tests/remote_e2e.rs` | +| Client transport / bus / supervisor | `frontend/src/lib/remote/**` | diff --git a/docs/architecture/remote-r8-appstate.md b/docs/architecture/remote-r8-appstate.md new file mode 100644 index 0000000000..ca9a46b95b --- /dev/null +++ b/docs/architecture/remote-r8-appstate.md @@ -0,0 +1,91 @@ +# R-8 — AppState identity behind the remote fetch remount + +The remote listener (`:3849`) serves a curated subset of the UI's own `/api` routes so a paired +device can read the same data the local app reads. R-8 is the guarantee that it reads that data +from the **same memory**, not from a look-alike copy. + +## The problem R-8 closes + +RalphX already runs two `AppState` graphs: + +| Graph | Built by | Consumed by | +|---|---|---| +| Tauri-managed | app setup | every `#[tauri::command]`, and therefore the remote **invoke** facade | +| `:3847` HTTP | `build_http_app_state` | every `/api` handler | + +`build_http_app_state` deliberately Arc-shares the authority-bearing fields between them. If the +fetch remount had constructed a **third** graph, a remote client could see one answer through +`/invoke` and a different answer through a proxied fetch — the same class of divergence the +big-PR checklist calls DRIFT. Slice B therefore reuses the `:3847` Arc rather than rebuilding. + +## Mechanism + +`SharedHttpAppState` (`src-tauri/src/remote_server/fetch_remount.rs`) is a newtype over the +`Arc` and `Arc` that `:3847` is about to serve from. It is registered +as Tauri-managed state at the `server_boot` seam — the one place holding both Arcs and the +`AppHandle` — and read back when the remote listener builds its router. + +``` +server_boot ──manage──▶ Arc ──try_state──▶ RemoteRouterState.remount + │ + ▼ + remount_router → HttpServerState +``` + +**Fail-closed.** If the newtype is not managed, `remount_router` is never called: the `/api` +routes are not mounted at all and answer with the listener's normal +`REMOTE_COMMAND_UNAVAILABLE` 404. There is no fallback that builds a fresh `AppState`. +Pinned by `without_shared_state_no_api_route_is_mounted`. + +## Shared vs fresh + +Two questions matter, and they are different: + +1. **Does the remount share with `:3847`?** Yes, totally — it is the same `Arc`, so every field + is shared by construction. `the_shared_state_hands_out_the_same_arcs_it_was_built_from` and + `every_resolution_of_the_shared_state_yields_the_same_arcs` assert `Arc::ptr_eq`. +2. **Does `:3847` share with the Tauri-managed graph?** Field by field, per the table below. + +### Fields `build_http_app_state` Arc-shares with the managed `AppState` + +`db` (inner connection) · `question_state` · `permission_state` · `message_queue` · +`queued_message_repo` · `interactive_process_registry` · `github_service` · +`pr_poller_registry` · `events` · `internal_event_bus` · `app_paths` · `window_focus_state` · +`notification_service_cache` · `agent_capability_gate` · `streaming_state_cache` · +`webhook_publisher` · `session_merge_locks` · `startup_coordinator` (via +`share_startup_coordinator`) · `plan_verification_locks` and `plan_verification_admissions` +(via `share_plan_verification_runtime`). + +`the_r8_shared_field_enumeration_matches_build_http_app_state` pins this list against the live +source, so a field quietly dropped from the sharing list fails a test instead of surfacing as a +remote client reading different authority state than the local UI. + +### Fields that are fresh — and why no mounted route cares + +Every **repository** not named above is a fresh struct in the `:3847` clone, but each one is +constructed over the **shared `db` connection**. Their reads are therefore identical to the +managed graph's; freshness of the struct is not freshness of the data. All eight mounted routes +read exclusively through repositories (`ideation_session_repo`, `artifact_repo`, +`agent_task_repo`, `agent_workflow_repo`, `agent_run_repo`) plus `message_queue`, which is +shared outright. + +`DelegationService` is fresh and stays fresh. It backs `/api/internal/*` only, which this slice +never mounts. `no_mounted_route_touches_delegation_service` scans the eight mounted handler +**bodies** (not their whole files — `agent_workflows.rs` also holds the workflow runner, which +legitimately uses the delegation service) and fails if any of them reads it. + +### Routes dropped because they read genuinely fresh in-memory state + +| Route | Fresh field | Resolution | +|---|---|---| +| `GET /api/conversations/:id/active-state` | `running_agent_registry` | **Dropped.** Its `isActive` would diverge from the facade's view. Promoting the field would change `:3847` behavior, which is outside slice B's scope. | +| `GET /api/ideation/sessions/:id/child-status` | `running_agent_registry` | **Dropped**, same reason. | + +Recording them here rather than silently omitting them is the point: v1 may be a subset, but the +subset must be explained. + +## Related + +- Allowlist, denied sinks and the scope gate: `src-tauri/src/remote_server/fetch_remount.rs` +- Tests: `src-tauri/src/remote_server/fetch_remount_tests.rs` +- Workflow separation this must not be confused with: `.claude/rules/agent-workspace-review-modes.md` diff --git a/docs/features/remote-access-setup.md b/docs/features/remote-access-setup.md new file mode 100644 index 0000000000..8bec06bcfe --- /dev/null +++ b/docs/features/remote-access-setup.md @@ -0,0 +1,411 @@ +# Remote Access — End-to-End Setup Guide + +Step-by-step for getting one RalphX (the **host**) reachable from another (the **client**), +including the Tailscale account setup both sides need. + +For what Remote Access *is* and what a paired device is allowed to do, read +[`remote-access.md`](./remote-access.md) first — this guide is the mechanical walkthrough. + +--- + +## What you end up with + +Your work Mac keeps running agents, holding repositories, and spawning processes. A second +machine connects to it over your private tailnet and gives you a window into it. Nothing moves +off the host, and nothing is exposed to the public internet. + +## Before you start + +| Requirement | Why | +|---|---| +| Two machines, both running RalphX | There is no separate "server build" — any RalphX can be host, client, or both | +| A Tailscale account | Remote Access is **tailnet-only**. There is no LAN or public mode. | +| Tailscale installed on **both** machines | The host binds a tailnet address; the client dials one | +| Physical access to the host's screen at pairing time | Pairing is deliberately face-to-face — you read a code off the host | + +**Time:** ~15 minutes, most of it Tailscale. + +--- + +## Part 1 — Tailscale + +### The short version + +**Both machines need Tailscale installed, running, and signed into the same account.** Tailscale +is the network underneath — without it on both ends there is no route between them, whichever +machine is hosting. + +On **each** machine, once: + +```bash +brew install tailscale # CLI + daemon +sudo brew services start tailscale # run it now, and at every boot +tailscale up # prints an auth URL — open it and sign in +tailscale status # confirm: you should see a 100.x.y.z address +``` + +That is the whole setup. You authenticate **once per machine** — the node key is stored, so it +survives reboots and RalphX restarts. You do not log in again each session. + +Then, on the host only, disable key expiry (see §1.5) so it does not silently drop off the +tailnet in six months. + +### Who needs what + +| | Host (serves) | Client (connects) | +|---|---|---| +| Tailscale installed + daemon running | Yes | Yes | +| Signed into the same tailnet | Yes | Yes | +| RalphX invokes the `tailscale` CLI | **Yes** — `status`, and `serve` in Serve mode, so the binary must be resolvable by RalphX (§1.2.2) | No — it only makes HTTP requests to the host's tailnet address | +| MagicDNS + HTTPS Certificates | Only for Serve mode (§1.4) | No | +| Key expiry disabled | Recommended (§1.5) | Optional | + +The rest of Part 1 is the detail behind those four commands. Skim it if the short version worked. + +### 1.1 Create the tailnet + +1. Go to [tailscale.com](https://tailscale.com) and sign up. The free Personal plan is enough. + You authenticate with an existing identity provider (Google, GitHub, Microsoft, Apple…) — the + account you pick becomes the owner of your tailnet. +2. You now have a *tailnet*: a private network only your devices can join. + +### 1.2 Install on the host + +Pick **one** of these. They are not equivalent — see the CLI note below. + +| Install method | What you get | CLI on `PATH`? | +|---|---|---| +| `brew install tailscale` (formula) | CLI + `tailscaled` daemon, no GUI | **Yes** — `/opt/homebrew/bin/tailscale` | +| `brew install --cask tailscale` | The macOS GUI app | No — see §1.2.2 | +| [tailscale.com/download](https://tailscale.com/download) | The macOS GUI app | No — see §1.2.2 | + +### 1.2.1 Start the daemon (Homebrew formula only) + +The formula installs the CLI **and** the `tailscaled` daemon, but does not start it. The GUI +variants run their daemon for you; the formula does not. Until you start it, every CLI command +fails with `failed to connect to local Tailscale service; is Tailscale running?`: + +```bash +sudo brew services start tailscale # installs a LaunchDaemon; root is required for the TUN interface +brew services list | grep tailscale # should now read "started" +``` + +Skip this if you installed a GUI variant. + +### 1.2.2 macOS GUI variants: making the CLI reachable + +**Skip this entire section if you used `brew install tailscale`** — the formula puts `tailscale` +at `/opt/homebrew/bin/tailscale`, which is already on your `PATH` *and* is one of the fixed paths +RalphX checks. Nothing to do. + +This applies only to the **cask / direct-download GUI app**, where the app bundle *is* the CLI — +the same binary switches to CLI mode when run from a terminal — and is not on your `PATH`. Add it: + +```bash +# ~/.zshrc +export PATH="/Applications/Tailscale.app/Contents/MacOS:$PATH" +``` + +> **Use a `PATH` export, not a shell alias.** A common suggestion is +> `alias tailscale="/Applications/Tailscale.app/Contents/MacOS/Tailscale"`. That works when *you* +> type it, but **RalphX will not find it** — aliases exist only inside an interactive shell and +> are invisible to a spawned process. RalphX resolves the binary by looking on `PATH`, then at +> fixed locations (`/Applications/Tailscale.app/Contents/MacOS/tailscale`, +> `/opt/homebrew/bin/tailscale`, `/usr/local/bin/tailscale`), then via a login-shell +> `command -v`. A `PATH` export in your shell profile is found by that last step; an alias never is. + +Verify RalphX's view, not just your shell's: + +```bash +zsh -lic 'command -v tailscale' # this is roughly what RalphX does +``` + +If that prints a path, RalphX will find it. If it prints nothing, Serve mode will report +`cliUnavailable` (§2.3). + +### 1.2.3 Log in from the CLI + +Signing in through the GUI works, but the CLI is faster and is the only option on a headless or +`tailscaled`-only host. + +```bash +tailscale up +``` + +This prints an authentication URL. Open it, sign in, and the machine joins your tailnet. Useful +variants: + +```bash +tailscale up --hostname=work-mac # name the device explicitly +tailscale login --qr # render the login URL as a QR code +tailscale login # re-authenticate / switch accounts +``` + +For an unattended or scripted host, mint an auth key in the admin console +(**Settings → Keys**) and skip the browser entirely: + +```bash +tailscale up --auth-key=tskey-auth-xxxxxxxxxxxx +``` + +Treat an auth key like a password — anyone holding it can add a device to your tailnet. + +### 1.2.4 Confirm it worked + +```bash +tailscale status # lists your devices; yours should show a 100.x.y.z address +tailscale ip -4 # this machine's tailnet IPv4 — the address clients dial in Tailnet direct +``` + +The machine should also now appear in your +[admin console](https://login.tailscale.com/admin/machines). + +To disconnect later: `tailscale logout` (expires the auth; the next `tailscale up` re-prompts). + +### 1.3 Install on the client + +Same four commands, **same Tailscale account**. Two devices on different tailnets cannot see each +other, and the failure looks like a RalphX problem rather than a network one. + +The client is simpler than the host: RalphX never shells out to the `tailscale` CLI on this side, +it just makes HTTP requests to the host's tailnet address. So the daemon has to be running and +signed in, but you do not need the binary to be resolvable *by RalphX* the way §1.2.2 describes — +that requirement is host-only. + +Verify from the client that it can reach the host: + +```bash +tailscale status # host should be listed +ping # e.g. ping 100.101.102.103 +``` + +### 1.4 Enable MagicDNS + HTTPS — only if you want *Tailscale Serve* + +RalphX offers two exposure modes. **Tailscale Serve** gives you TLS terminated at the tailnet +edge and a friendly hostname; it requires two tailnet-level features that are **off by default**: + +1. Admin console → **DNS** → enable **MagicDNS**. +2. Same page → enable **HTTPS Certificates**. + +Skip this section if you plan to use *Tailnet direct* — that mode needs neither. + +> RalphX runs `tailscale serve --bg --https=443 http://127.0.0.1:` on your behalf when you +> pick Serve mode, and releases it with `tailscale serve --https=443 off` when you turn host mode +> off. Without HTTPS Certificates enabled, that command fails and RalphX reports the listener as +> degraded. + +### 1.5 Keep it running — disable key expiry on the host + +Tailscale node keys **expire after 180 days by default** (configurable 1–180). When a key expires +the device drops off the tailnet and "connections to/from the given endpoint stop working" — which +for a RalphX host means every paired client silently loses it until someone re-authenticates at +the host's keyboard. + +For a machine that exists to be connected to, turn that off: + +**[Admin console](https://login.tailscale.com/admin/machines) → Machines → your host's ⋯ menu → +Disable key expiry.** + +There is no CLI equivalent — it is admin-console only. Tailscale explicitly endorses this for +"trusted servers, subnet routers, or remote IoT devices that are hard to reach": you trade +periodic reauthentication for the device staying reachable. A RalphX host is exactly that case. + +The client matters less — if its key expires you are sitting in front of it and can just run +`tailscale up` again — but there is no harm in disabling it there too. + +> This is separate from RalphX's own pairing tokens, which never expire on a schedule and are +> revoked per-device from the Remote Access pane. Tailscale controls whether the machines can +> reach each other; RalphX controls whether a paired device is allowed to do anything. + +--- + +## Part 2 — Host setup + +### 2.1 Make the panes visible + +The Remote Access and Connections panes are behind a client-owned feature flag. + +`config/ralphx.yaml` ships with it enabled: + +```yaml +ui: + feature_flags: + remote_environments: true +``` + +Or override per launch without editing the file: + +```bash +RALPHX_UI_REMOTE_ENVIRONMENTS=true npm run tauri dev +``` + +**Restart RalphX after changing this.** The config is read once per process (a `OnceLock`) and +the frontend fetches the flags once at boot — a window reload is not enough. + +### 2.2 Enable host mode + +**Settings → Integrations → Remote Access → Enable remote access.** + +This is a separate switch from the feature flag, persisted in the database and defaulting to +**off**. The flag reveals the pane; this toggle starts the listener. + +### 2.3 Choose the exposure mode + +Both modes are tailnet-only and both carry all traffic inside WireGuard. The listener **refuses +to bind** a wildcard or LAN address — the bind address must sit inside `100.64.0.0/10`. + +> Do not port-forward `3849` to the internet. If you need access from outside your tailnet, put +> it behind something that terminates TLS and authenticates. + +#### Which one should I pick? + +**Start with Tailnet direct.** It has fewer moving parts and no tailnet-level prerequisites, so +if pairing fails you know the problem is RalphX and not Tailscale. Move to Serve once it works +and you want the nicer hostname or a real TLS certificate. + +| | **Tailnet direct** | **Tailscale Serve** | +|---|---|---| +| **Pick it when** | Getting started; debugging; two machines you control | You want a stable hostname, a real cert, or a browser/mobile client that insists on HTTPS | +| **Tailnet setup needed** | None | MagicDNS **and** HTTPS Certificates (§1.4) | +| **Client-facing address** | `http://100.x.y.z:3849` | `https://..ts.net` (port 443) | +| **What RalphX binds** | The tailnet IP, port `3849` | Loopback `127.0.0.1:` only | +| **Reachable on the tailnet by** | Anything on your tailnet that can route to `:3849` | The Serve proxy | +| **TLS** | None — plaintext HTTP inside the WireGuard tunnel | TLS terminated at the tailnet edge, on top of WireGuard | +| **Network hops** | Client → host listener | Client → `tailscaled` proxy → host listener (one extra hop) | +| **Moving parts at startup** | Bind a socket | Bind a socket, resolve the `tailscale` CLI, acquire a Serve mapping, provision/renew a cert | +| **External process invoked** | None | `tailscale serve --bg --https=443 http://127.0.0.1:` | +| **Teardown obligation** | Close the socket | Release the mapping (`tailscale serve --https=443 off`) — RalphX does this on disable | +| **Ways it can fail** | Address not in `100.64.0.0/10`; port in use | The four below, plus everything Tailnet direct can hit | + +#### Reading the fine print + +**"TLS: none" is not the same as unencrypted.** Both modes ride inside WireGuard, so nothing is +in the clear on the wire either way. Serve adds a *second* layer plus a certificate a browser +will accept. That matters for defense in depth and for clients that refuse plain HTTP — not for +whether a passive observer can read your traffic. + +**Bind surface is the sharper security difference.** In Serve mode RalphX binds loopback only, so +the listener is not directly addressable from the tailnet at all — every request arrives through +the Serve proxy. In Tailnet direct, `:3849` is reachable by anything on your tailnet that can +route to it. Both are still behind RalphX's own bearer-token auth, so this is a matter of layers, +not of one being open. + +**No measured performance comparison exists.** Serve adds one proxy hop and TLS termination, so +it cannot be faster — but nobody has benchmarked the difference, and neither mode has been shown +to be a bottleneck. Do not pick on performance grounds; pick on setup cost and whether you need a +certificate. + +#### When Serve fails, it fails in one of four ways + +RalphX reports a typed reason rather than prose, so the pane can tell you what to actually do: + +| Kind | Meaning | Fix | +|---|---|---| +| `cliUnavailable` | The `tailscale` binary could not be resolved | Install Tailscale, or put it on `PATH` (§1.2.2). If `tailscale` works in your terminal but RalphX still reports this, you almost certainly have a shell *alias* rather than a `PATH` entry — check with `zsh -lic 'command -v tailscale'` | +| `launchFailed` | The binary exists but would not start | Check permissions and that the Tailscale app is running | +| `timeout` | The command hung | Check `tailscale status`; the daemon may be wedged | +| `commandFailed` | The command ran and was refused | Usually MagicDNS/HTTPS Certificates not enabled, or not logged in (§1.4) | + +If acquiring the Serve mapping fails, RalphX **releases any mapping an earlier run left behind** +rather than staying silently tailnet-reachable. A degraded Serve listener is not a half-open one. + +### 2.4 Generate a pairing code + +**Remote Access → *Pair a device* → Generate pairing code.** + +You get a short code, a copyable URL, and a QR code. Keep this screen up — you need it on the +client in the next step. + +- A pairing code works **exactly once** and expires on its own. +- A used code cannot be reused, even by you — generate a new one if you fumble it. +- After six failed attempts, pairing is rate-limited. + +--- + +## Part 3 — Client setup + +### 3.1 Make the panes visible + +Same as §2.1 — the flag is **per device**, so enabling it on the host does not enable it on the +client. Set it and restart. + +### 3.2 Add the environment + +**Settings → Integrations → Connections → Add environment**, then enter the pairing code (or +scan the QR). + +On success the host mints a long-term token for this device and lists it by name. + +- The token is stored in the client's system Keychain and is **never shown in the interface** + after pairing. +- Each device gets its own token; revoking one does not touch the others. + +### 3.3 Switch to it + +The environment switcher in the top bar now lists your Local environment plus the host you +paired with. Switching does not disturb the environment you left, and a host going offline never +affects Local. + +--- + +## Part 4 — Granting agent control (optional) + +A freshly paired device is a **viewer with brakes**: it can see everything and stop everything, +but it cannot start work. + +To let it steer agents, go to the host's **Remote Access → device list** and enable agent control +for that specific device. A confirmation dialog spells out what you are granting — read it. It +covers code execution on the host machine, and withdrawal disconnects live sessions immediately. + +This is per-device and reversible at any time. + +--- + +## Verifying it works + +| Check | Where | Expected | +|---|---|---| +| Host listener is up | Host → Remote Access pane | Status shows running, with the address clients should use | +| Device is paired | Host → device list | Client listed by name, with paired date and last-seen | +| Client is connected | Client → environment switcher | Host environment present and reporting connected | +| Traffic is tailnet-only | `tailscale status` on either machine | The peer connection is listed | + +--- + +## Turning it off + +| Goal | Action | +|---|---| +| Disconnect one device | Host → Remote Access → device list → revoke. Its next request is refused and its live sessions are killed. | +| Stop serving entirely | Host → Remote Access → disable. Releases the Tailscale Serve mapping if one was acquired. | +| Hide the panes again | Set `remote_environments: false` and restart | + +--- + +## Troubleshooting + +| Symptom | Likely cause | +|---|---| +| Remote Access / Connections panes missing | Flag off, or RalphX not restarted after changing it | +| Pane visible, but nothing serves | Host mode toggle (§2.2) is separate from the flag — enable it too | +| Listener degraded in Serve mode | MagicDNS or HTTPS Certificates not enabled (§1.4), or the `tailscale` CLI is not resolvable | +| Bind refused | The address is outside `100.64.0.0/10`. Tailscale may not be connected. | +| Pairing code rejected | Already used, or expired. Generate a new one. | +| Client cannot see the host at all | Different tailnet accounts, or Tailscale disconnected on either end | +| Version mismatch on pairing | Host and client protocol versions must match — update both to the same RalphX build | + +--- + +## Known limitations in this build + +Be aware before you invest time: + +- **Projects do not load remotely.** The project-listing commands shell out to git and are + deliberately not exposed on the remote facade, so a remote environment currently lands on the + no-projects Welcome screen. This is a known gap, not a misconfiguration. +- **Tool-call detail is unavailable remotely.** Transcripts load, but expanding a tool call in one + fails — there is no spawn-free variant of that read yet. +- **Some Agents surfaces are host-only**, including starting a conversation and the queued-message + views. Affordances that are unavailable remotely are shown as such rather than failing silently. +- **Remote send is delivered in-band.** A message sent remotely is refused, actionably, when no + run is live — there is no background queue draining it. diff --git a/docs/features/remote-access.md b/docs/features/remote-access.md new file mode 100644 index 0000000000..c45e50d300 --- /dev/null +++ b/docs/features/remote-access.md @@ -0,0 +1,201 @@ +# Remote Access + +Use RalphX on one machine to watch and steer work running on another. + +Your Mac keeps doing the work — running agents, holding your repositories, spawning processes. +A second RalphX (or, later, the mobile app) connects to it over your network and gives you a +window into it. Nothing moves off the host. + +> **Status:** Remote Access ships dark. The host side is behind the `remote_host` setting and the +> client side behind the `remoteEnvironments` flag. Both default to off. + +--- + +## The two modes + +RalphX does not have a "server build" and a "client build". Any RalphX can be either, or both. + +**Host mode** — this machine accepts connections. It opens a second, authenticated listener +(default port `3849`) alongside the local backend. Your agents, worktrees, and settings stay +exactly where they are. + +**Client mode** — this machine connects to one or more hosts. Each host you pair with becomes an +*environment* you can switch between, alongside your own Local environment. + +Environments are fully isolated from each other. Switching environments does not disturb the one +you left, and a remote host going offline never affects your Local environment. + +--- + +## Turning on host mode + +1. **Settings → Integrations → Remote Access → Enable remote access.** +2. **Choose how it is exposed.** + - *Tailscale Serve* — TLS terminated at the tailnet edge, reached via your MagicDNS name. + - *Tailnet direct* — plain HTTP on your tailnet IP, inside WireGuard. + + Both options are tailnet-only. There is no "expose to the whole internet" option, and the + listener refuses to bind a wildcard or LAN address — the bind address must sit inside + `100.64.0.0/10`. If you want access from outside your tailnet, put it behind something that + terminates TLS and authenticates — do not port-forward to `3849`. +3. The pane shows the listener's status, the address clients should use, and every paired device. + +--- + +## Pairing a device + +Pairing is deliberately a face-to-face gesture: you must be able to see the host's screen. + +1. On the **host**: Settings → Integrations → Remote Access → *Pair a device* → **Generate + pairing code**. A short pairing code appears, with a copyable URL and a QR code. +2. On the **client**: Settings → Integrations → Connections → **Add environment**, then enter the + code (or scan the QR). +3. The host mints a long-term token for that device and lists it by name. + +Things worth knowing: + +- **A pairing code works exactly once**, and it expires on its own. If you fumble it, generate a + new one — a used code cannot be reused, even by you. +- **The token never appears in the RalphX interface** after pairing. It is stored in the client's + system Keychain and is not readable from the app. +- Each device gets its own token. Revoking one does not touch the others. + +--- + +## What you are actually granting + +This is the part worth reading slowly. + +### The default: a viewer with brakes + +A freshly paired device gets `ui:read` and `ui:operate`. In plain terms, it can: + +- **See everything** — tasks, plans, conversations, diffs, agent output, notifications. +- **Stop everything safely** — deny a permission request, or use the global pause/stop controls. +- **Make small edits** — change a task's category or priority, create tasks in the Backlog. +- **Handle attachments** — upload and retrieve device-scoped task attachments. + +That is a **viewer with brakes**. It can see your work and it can halt your work. It cannot +*start* work. + +The split is intentional. Stopping something is safe: the worst case is that an agent idles. +Starting something is not. So the brakes are handed out by default and the accelerator is not. + +Per-task block, pause, and stop controls are part of the agent-control grant, as are bulk group +pause and cancel. They are not pure brakes in the execution engine: leaving an agent-active task +can run execution-exit Git work, and `block_task` frees capacity and asks the scheduler to start +queued work. The safe default-tier brakes are the **global** pause and stop controls, which set +the process-wide pause gate before transitioning any task, so no replacement agent can launch. + +### The upgrade: "Allow remote agent control" + +`ui:agent` is a separate, **off-by-default, per-device** toggle on the host. Turning it on lets +that device start agent runs, send chat messages that steer an agent, resume runs, and write the +kinds of records a background loop turns into a spawn. It also enables per-task block/pause/stop +and bulk group pause/cancel because those operations can trigger agent-active exit behavior. + +**Anyone who steals a `ui:agent` token can run arbitrary code on your Mac.** + +That is not a worst-case reading of the grant. It is what the grant *is*. An agent run executes +commands, writes files, and installs things in your working directory, under your user account, +with your credentials on disk. Handing a device `ui:agent` is handing it the ability to execute +code on the host machine. Treat that token exactly as you would treat SSH access. + +Concretely, before you enable it: + +- Grant it per device, never as a habit. Enable it on the phone you actually carry; leave it off + on the laptop in the drawer. +- Only over a network you control. A tailnet is a reasonable trust boundary. A café's Wi-Fi with + a forwarded port is not. +- Revoke it the moment a device is lost, sold, or handed to someone else. +- If you would not give the device an SSH key to this machine, do not give it `ui:agent`. + +There is no configuration that softens this. The grant is powerful because agent control is +powerful. + +### What no device can do + +Some things are refused regardless of grant, because there is no safe version of them over a +network: + +- **Terminal / shell access.** No PTY is exposed, no terminal command is reachable, and the + terminal drawer is hidden for remote environments. Not "restricted" — absent. +- **Credential and authentication setup.** Git and GitHub auth configuration is not reachable. +- **Arbitrary path writes** and process spawns outside the agent surface. + +These are refused by construction: the commands are not on the remote allowlist at all, and the +build fails if someone tries to add one under an insufficient permission level. + +--- + +## Managing and revoking devices + +The host's *Paired devices* list shows each device's name, its token prefix, when it was paired, +when it was last seen, and how many live sessions it holds. Each row has a **Revoke** action, and +each has its own agent-control toggle. + +**Revocation is immediate and applies to live sessions.** The device's open connection is closed +within the heartbeat window — it does not keep streaming until it happens to reconnect — and its +next request is refused. Revoking is durable-first, so it survives a host restart even if the +client never hears about it. + +Removing an environment on the client side asks the host to revoke first, then deletes the local +token and the environment row. If the host is unreachable, the local removal still completes; +revoke the device from the host when you next can. + +--- + +## Living with a remote connection + +**Going offline.** If the host stops answering, the client shows a disconnected state and keeps +retrying with a backoff. Dead hosts are detected within about a minute. Your Local environment is +untouched. + +**Coming back.** On reconnect, the client resumes from where it left off when it safely can, and +otherwise re-fetches from scratch. You may briefly see a loading state — that is the client +choosing correctness over a fast-looking-but-wrong transcript. + +**Missed events.** RalphX keeps a bounded event history. If you are away long enough for it to +age out, the client refetches rather than showing you a transcript with an invisible hole. + +**Things that never replay.** Live typing (streaming agent output) and permission prompts are not +stored for replay. They are recovered by re-reading the actual message and re-asking for the +pending prompt list, so a prompt raised while you were disconnected still appears when you +return — it is never silently lost. + +**Background environments.** Environments you are not looking at can still surface a notification +count, but they do not sync in the background. Switching to one always loads it fresh. + +--- + +## What stays on the host + +Everything that matters: + +- Your repositories, worktrees, and every file an agent touches. +- Agent processes and their output. +- Credentials — git, GitHub, provider API keys. None are readable over the remote surface. +- The database, the event history, and all settings. + +The client holds one thing: the device token for each environment it has paired, in the system +Keychain. + +--- + +## Troubleshooting + +| Symptom | Likely cause | +|---|---| +| "Cannot reach host" | Host mode off, machine asleep, or not on the same tailnet | +| Pairing code rejected | Already used, or expired — generate a new one | +| An action is greyed out | The device lacks the scope; check its toggles on the host | +| "Update required" | Client older than the host's supported floor | +| Stuck reconnecting | Check the host's Remote Access pane for listener errors | +| Terminal missing | Expected — terminal is not available remotely | + +--- + +## For developers + +The wire protocol, error taxonomy, capability classes, and event-stream semantics are documented +in [`docs/architecture/remote-protocol.md`](../architecture/remote-protocol.md). diff --git a/docs/generated/remote-commands.json b/docs/generated/remote-commands.json new file mode 100644 index 0000000000..5495dfecfd --- /dev/null +++ b/docs/generated/remote-commands.json @@ -0,0 +1,12111 @@ +{ + "agent_consumed_content_surface": { + "detected_writers": [ + { + "surfaces": [ + "artifacts" + ], + "writer": "add_artifact_relation" + }, + { + "surfaces": [ + "review-feedback" + ], + "writer": "approve_review" + }, + { + "surfaces": [ + "task-steps" + ], + "writer": "complete_step" + }, + { + "surfaces": [ + "artifacts" + ], + "writer": "create_artifact" + }, + { + "surfaces": [ + "task-steps" + ], + "writer": "create_task" + }, + { + "surfaces": [ + "task-steps" + ], + "writer": "create_task_step" + }, + { + "surfaces": [ + "task-steps" + ], + "writer": "fail_step" + }, + { + "surfaces": [ + "review-feedback" + ], + "writer": "mark_issue_addressed" + }, + { + "surfaces": [ + "review-feedback" + ], + "writer": "mark_issue_in_progress" + }, + { + "surfaces": [ + "review-feedback" + ], + "writer": "reject_fix_task" + }, + { + "surfaces": [ + "review-feedback" + ], + "writer": "reject_review" + }, + { + "surfaces": [ + "review-feedback" + ], + "writer": "reopen_issue" + }, + { + "surfaces": [ + "review-feedback" + ], + "writer": "request_changes" + }, + { + "surfaces": [ + "review-feedback" + ], + "writer": "request_task_changes_from_reviewing" + }, + { + "surfaces": [ + "task-steps" + ], + "writer": "skip_step" + }, + { + "surfaces": [ + "task-steps" + ], + "writer": "start_step" + }, + { + "surfaces": [ + "artifacts" + ], + "writer": "update_artifact" + }, + { + "surfaces": [ + "task-steps" + ], + "writer": "update_task_step" + }, + { + "surfaces": [ + "review-feedback" + ], + "writer": "verify_issue" + } + ], + "exemptions": [ + { + "command": "create_task", + "reason": "born-backlog-only: `CreateTaskInput` carries no status field and every construction path runs `Task::new_with_category`, so the task and the steps created with it land in `InternalStatus::Backlog`. No loop or worker reads them until a separate AgentControl-class action arms the task, which is the same rationale the registry records for registering `create_task` at Operate" + } + ], + "reads": [ + { + "grantedTo": [ + "ralphx-execution-worker", + "ralphx-execution-coder" + ], + "reads": "ticket attachments", + "tool": "fetch_ticket_attachment" + }, + { + "grantedTo": [ + "ralphx-execution-worker", + "ralphx-execution-coder" + ], + "reads": "agent_tasks", + "tool": "get_agent_task" + }, + { + "grantedTo": [ + "ralphx-execution-worker", + "ralphx-execution-coder", + "ralphx-execution-reviewer" + ], + "reads": "artifacts/artifact_versions/artifact_relations", + "tool": "get_artifact" + }, + { + "grantedTo": [ + "ralphx-execution-worker", + "ralphx-execution-coder", + "ralphx-execution-reviewer" + ], + "reads": "artifacts/artifact_versions/artifact_relations", + "tool": "get_artifact_version" + }, + { + "grantedTo": [ + "ralphx-execution-reviewer" + ], + "reads": "review_notes/task_issues", + "tool": "get_issue_progress" + }, + { + "grantedTo": [ + "ralphx-execution-worker", + "ralphx-execution-coder", + "ralphx-execution-reviewer", + "ralphx-execution-merger" + ], + "reads": "memory_entries", + "tool": "get_memories_for_paths" + }, + { + "grantedTo": [ + "ralphx-execution-worker", + "ralphx-execution-coder", + "ralphx-execution-reviewer", + "ralphx-execution-merger" + ], + "reads": "memory_entries", + "tool": "get_memory" + }, + { + "grantedTo": [ + "ralphx-execution-merger" + ], + "reads": "merge target/branch state", + "tool": "get_merge_target" + }, + { + "grantedTo": [ + "ralphx-execution-worker", + "ralphx-execution-coder", + "ralphx-execution-reviewer", + "ralphx-execution-merger" + ], + "reads": "project_analysis", + "tool": "get_project_analysis" + }, + { + "grantedTo": [ + "ralphx-execution-worker", + "ralphx-execution-coder", + "ralphx-execution-reviewer" + ], + "reads": "artifacts/artifact_versions/artifact_relations", + "tool": "get_related_artifacts" + }, + { + "grantedTo": [ + "ralphx-execution-worker", + "ralphx-execution-coder", + "ralphx-execution-reviewer" + ], + "reads": "review_notes/task_issues", + "tool": "get_review_notes" + }, + { + "grantedTo": [ + "ralphx-execution-worker", + "ralphx-execution-coder" + ], + "reads": "task_steps", + "tool": "get_step_context" + }, + { + "grantedTo": [ + "ralphx-execution-worker", + "ralphx-execution-coder", + "ralphx-execution-reviewer" + ], + "reads": "task_steps", + "tool": "get_step_progress" + }, + { + "grantedTo": [ + "ralphx-execution-worker" + ], + "reads": "task_steps", + "tool": "get_sub_steps" + }, + { + "grantedTo": [ + "ralphx-execution-worker", + "ralphx-execution-coder", + "ralphx-execution-reviewer", + "ralphx-execution-merger" + ], + "reads": "worker TaskContext/prompt projection", + "tool": "get_task_context" + }, + { + "grantedTo": [ + "ralphx-execution-reviewer" + ], + "reads": "task/worktree diff", + "tool": "get_task_diff" + }, + { + "grantedTo": [ + "ralphx-execution-reviewer" + ], + "reads": "task/worktree diff", + "tool": "get_task_diff_stat" + }, + { + "grantedTo": [ + "ralphx-execution-worker", + "ralphx-execution-coder", + "ralphx-execution-reviewer" + ], + "reads": "review_notes/task_issues", + "tool": "get_task_issues" + }, + { + "grantedTo": [ + "ralphx-execution-worker", + "ralphx-execution-coder", + "ralphx-execution-reviewer" + ], + "reads": "task_steps", + "tool": "get_task_steps" + }, + { + "grantedTo": [ + "ralphx-execution-reviewer" + ], + "reads": "validation_runs", + "tool": "get_task_validation_summary" + }, + { + "grantedTo": [ + "ralphx-execution-worker", + "ralphx-execution-coder" + ], + "reads": "agent_tasks", + "tool": "list_agent_tasks" + }, + { + "grantedTo": [ + "ralphx-execution-worker", + "ralphx-execution-coder" + ], + "reads": "ticket attachments", + "tool": "list_ticket_attachments" + }, + { + "grantedTo": [ + "ralphx-execution-worker", + "ralphx-execution-coder", + "ralphx-execution-reviewer", + "ralphx-execution-merger" + ], + "reads": "memory_entries", + "tool": "search_memories" + }, + { + "grantedTo": [ + "ralphx-execution-worker", + "ralphx-execution-coder", + "ralphx-execution-reviewer" + ], + "reads": "artifacts/artifact_versions/artifact_relations", + "tool": "search_project_artifacts" + } + ], + "writers": [ + { + "surface": "tauri-command", + "writer": "create_task_step", + "writes": "task_steps" + }, + { + "surface": "tauri-command", + "writer": "update_task_step", + "writes": "task_steps" + }, + { + "surface": "tauri-command", + "writer": "create_artifact", + "writes": "artifacts (any kind)" + }, + { + "surface": "tauri-command", + "writer": "update_artifact", + "writes": "artifacts (any kind)" + }, + { + "surface": "tauri-command", + "writer": "add_artifact_relation", + "writes": "artifact_relations" + }, + { + "surface": "tauri-command", + "writer": "update_task_proposal", + "writes": "task proposals" + }, + { + "surface": "tauri-command", + "writer": "approve_review", + "writes": "review feedback" + }, + { + "surface": "tauri-command", + "writer": "reject_review", + "writes": "review feedback" + }, + { + "surface": "tauri-command", + "writer": "request_changes", + "writes": "review feedback" + }, + { + "surface": "tauri-command", + "writer": "reject_fix_task", + "writes": "review notes/fix feedback" + }, + { + "surface": "tauri-command", + "writer": "approve_task_for_review", + "writes": "review notes" + }, + { + "surface": "tauri-command", + "writer": "request_task_changes_for_review", + "writes": "review notes/feedback" + }, + { + "surface": "tauri-command", + "writer": "request_task_changes_from_reviewing", + "writes": "review notes/feedback" + }, + { + "conditional": "note", + "surface": "tauri-command", + "writer": "move_task", + "writes": "task restart note" + }, + { + "conditional": "title,description — discharged by update_task_authz", + "surface": "tauri-command", + "writer": "update_task", + "writes": "task title/description" + }, + { + "surface": "http-handler", + "writer": "add_task_note", + "writes": "task.description" + }, + { + "surface": "tauri-command", + "writer": "start_step", + "writes": "task_steps" + }, + { + "surface": "tauri-command", + "writer": "complete_step", + "writes": "task_steps" + }, + { + "surface": "tauri-command", + "writer": "skip_step", + "writes": "task_steps" + }, + { + "surface": "tauri-command", + "writer": "fail_step", + "writes": "task_steps" + }, + { + "surface": "tauri-command", + "writer": "verify_issue", + "writes": "review_notes/task_issues" + }, + { + "surface": "tauri-command", + "writer": "reopen_issue", + "writes": "review_notes/task_issues" + }, + { + "surface": "tauri-command", + "writer": "mark_issue_in_progress", + "writes": "review_notes/task_issues" + }, + { + "surface": "tauri-command", + "writer": "mark_issue_addressed", + "writes": "review_notes/task_issues" + } + ] + }, + "agent_control_floor": [ + "retry_startup", + "update_notification_settings", + "remote_fetch", + "complete_atlassian_oauth_local_callback", + "exchange_atlassian_oauth_code", + "validate_atlassian_integration", + "search_atlassian_resources", + "resolve_atlassian_resource_urls", + "assign_agent_conversation_jira_issue", + "refresh_agent_conversation_jira_issue", + "assign_agent_conversation_jira_issue_to_me", + "resume_automation", + "finalize_automation", + "resume_automation_run", + "create_persona_draft", + "update_persona_draft", + "update_persona", + "approve_persona", + "reseed_persona_draft", + "approve_persona_as_new", + "archive_persona", + "delete_persona_draft", + "assign_agent_conversation_linear_issue", + "refresh_agent_conversation_linear_issue", + "answer_user_question", + "inject_task", + "move_task", + "unblock_task", + "resume_tasks_in_group", + "pause_execution_plan", + "resume_execution_plan", + "stop_execution_plan", + "resume_task", + "retry_branch_update", + "create_project", + "reanalyze_project", + "resume_deferred_git_startup", + "update_github_pr_enabled", + "copy_agent_conversation_plan", + "import_agent_conversation_plan", + "activate_agent_task_pipeline", + "activate_agent_plan_direct_implementation", + "start_agent_task_pipeline", + "approve_fix_task", + "reject_fix_task", + "approve_task_for_review", + "request_task_changes_for_review", + "request_task_changes_from_reviewing", + "re_review_task_from_escalated", + "update_review_settings", + "resume_execution", + "restart_task", + "recover_task_execution", + "resolve_recovery_prompt", + "set_max_concurrent", + "update_execution_settings", + "update_global_execution_settings", + "save_metrics_config", + "create_ideation_session", + "create_cross_project_session", + "archive_task_proposal", + "apply_proposals_to_kanban", + "restart_ideation_implementation", + "send_orchestrator_message", + "set_tasks_feature_enabled", + "get_mcp_catalog", + "refresh_mcp_catalog", + "get_managed_provider_cli_status", + "install_or_update_managed_provider_cli", + "auto_update_managed_provider_clis", + "import_ideation_session", + "search_linear_issues", + "validate_linear_integration", + "validate_clickup_integration", + "list_clickup_workspaces", + "search_clickup_tasks", + "assign_agent_conversation_granola_note", + "get_granola_note_detail", + "list_granola_notes", + "refresh_agent_conversation_granola_note", + "validate_granola_integration_settings", + "resolve_user_question", + "request_remote_queued_message_send", + "request_remote_automation_run", + "request_remote_automation_draft", + "request_remote_agent_conversation_start", + "request_remote_agent_conversation_message", + "request_remote_agent_conversation_mode_switch", + "request_remote_conversation_archive", + "request_remote_conversation_fork", + "request_remote_execution_resume", + "request_remote_task_resume", + "request_remote_task_restart", + "request_remote_group_resume", + "request_remote_recovery_prompt_resolution", + "request_remote_plan_approval", + "request_remote_ideation_finalize_decision", + "start_agent_conversation", + "abort_seeded_agent_conversation", + "switch_agent_conversation_mode", + "send_agent_message", + "send_queued_agent_message_now", + "list_agent_sidebar_conversations", + "set_agent_conversation_muted", + "get_agent_conversation", + "get_agent_conversation_messages_page", + "get_agent_conversation_timeline_page", + "get_agent_conversation_workspace", + "recheck_pr_health", + "retry_pr_autofix_override", + "stop_pr_autofix_for_failure", + "set_agent_conversation_workspace_auto_publish", + "set_agent_conversation_workspace_pr_supervision", + "set_agent_conversation_workspace_review_automation", + "list_agent_conversation_workspaces_by_project", + "get_agent_conversation_workspace_freshness", + "reconcile_agent_conversation_workspace_publication", + "update_agent_conversation_workspace_from_base", + "publish_agent_conversation_workspace", + "commit_agent_conversation_workspace_locally", + "close_agent_workspace_pr", + "reopen_agent_workspace_pr", + "resolve_merge_conflict", + "retry_merge", + "update_api_key_projects", + "update_api_key_permissions", + "list_ticketing_columns", + "refresh_ticketing_status_catalog", + "start_ralphx_work_from_ticket", + "transition_ticket_status", + "assign_ticket", + "clear_ticket_assignee", + "add_ticket_comment", + "set_ticket_labels" + ], + "authority_reducing_exemptions": [ + { + "command": "pause_execution", + "direction": "authority-reducing", + "kind": "command", + "rationale": "commands/execution_commands/lifecycle.rs sets the pause flag and transitions agent-active tasks only to Paused; commands/execution_commands/state.rs can_start_task returns false on is_paused before reading any quota", + "scope": "ui:operate" + }, + { + "command": "stop_execution", + "direction": "authority-reducing", + "kind": "command", + "rationale": "commands/execution_commands/lifecycle.rs sets the pause flag and transitions agent-active tasks only to Stopped; the only production caller of ExecutionState::resume is resume_execution, which re-syncs the quota first", + "scope": "ui:operate" + }, + { + "command": "request_remote_agent_stop", + "direction": "authority-reducing", + "kind": "command", + "rationale": "commands/remote_agent_stop_commands.rs persists one conversation-scoped stop intent and nothing else; the only reader is application/startup_background.rs::drain_one_remote_agent_stop, which calls ChatService::stop_agent and can therefore only END an agent run — there is no path from the row to a start, resume, or content write, and the row names no process", + "scope": "ui:operate" + }, + { + "command": "cancel_remote_queued_agent_message", + "direction": "authority-reducing", + "kind": "command", + "rationale": "commands/remote_queue_commands.rs validates an active Project conversation and only removes the named queued turn from durable storage and memory; it cannot create content, start, resume, steer, or dispatch an agent", + "scope": "ui:operate" + }, + { + "command": "deny_permission_request", + "direction": "authority-reducing", + "kind": "command", + "rationale": "denies a live tool call", + "scope": "ui:operate" + }, + { + "direction": "authority-reducing", + "kind": "transition-target", + "rationale": "domain/state_machine/transition_handler/mod.rs on_exit stops pollers for Cancelled; on_enter_states/mod.rs has no Cancelled entry action", + "scope": "transition-target", + "target": "Cancelled" + }, + { + "direction": "authority-reducing", + "kind": "transition-target", + "rationale": "domain/state_machine/transition_handler/on_enter_states/mod.rs has no Archived entry action and application reconciliation does not scan Archived tasks", + "scope": "transition-target", + "target": "Archived" + } + ], + "background_loop_inventory": [ + { + "authorityBearing": false, + "enclosingFunction": "application/agent_conversation_workspace.rs:::::run_or_defer_agent_conversation_workspace_setup", + "file": "application/agent_conversation_workspace.rs", + "id": "application/agent_conversation_workspace.rs::application/agent_conversation_workspace.rs:::::run_or_defer_agent_conversation_workspace_setup@98fe58e8270b1358", + "kind": "tokio::spawn", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": false, + "enclosingFunction": "application/agent_terminal.rs::AgentTerminalService::spawn_event_pump", + "file": "application/agent_terminal.rs", + "id": "application/agent_terminal.rs::application/agent_terminal.rs::AgentTerminalService::spawn_event_pump@beabd5f078a42f70", + "kind": "async_runtime::spawn", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": false, + "enclosingFunction": "application/agent_terminal.rs::PortablePtyProcessFactory::spawn", + "file": "application/agent_terminal.rs", + "id": "application/agent_terminal.rs::application/agent_terminal.rs::PortablePtyProcessFactory::spawn@f283e93d56935210", + "kind": "thread::spawn", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": true, + "enclosingFunction": "application/agent_workspace_external_pr_reconciliation.rs:::::schedule_agent_workspace_external_pr_reconciliation_with_lazy_deps", + "file": "application/agent_workspace_external_pr_reconciliation.rs", + "id": "application/agent_workspace_external_pr_reconciliation.rs::application/agent_workspace_external_pr_reconciliation.rs:::::schedule_agent_workspace_external_pr_reconciliation_with_lazy_deps@f00e635e44918aa6", + "kind": "tokio::spawn", + "readSurface": [], + "sinksReached": [ + "execute_entry_actions", + "send_message", + "transition_task", + "transition_task_corrective_with_exit", + "try_schedule_ready_tasks", + "write_message" + ] + }, + { + "authorityBearing": true, + "enclosingFunction": "application/agent_workspace_pr_supervision_recovery.rs:::::schedule_agent_workspace_pr_supervision_recovery_with_lazy_deps", + "file": "application/agent_workspace_pr_supervision_recovery.rs", + "id": "application/agent_workspace_pr_supervision_recovery.rs::application/agent_workspace_pr_supervision_recovery.rs:::::schedule_agent_workspace_pr_supervision_recovery_with_lazy_deps@e2996e37b5161290", + "kind": "tokio::spawn", + "readSurface": [], + "sinksReached": [ + "execute_entry_actions", + "send_message", + "transition_task", + "transition_task_corrective_with_exit", + "try_schedule_ready_tasks" + ] + }, + { + "authorityBearing": true, + "enclosingFunction": "application/agent_workspace_pr_supervision_recovery.rs:::::schedule_agent_workspace_durable_repair_reconciliation", + "file": "application/agent_workspace_pr_supervision_recovery.rs", + "id": "application/agent_workspace_pr_supervision_recovery.rs::application/agent_workspace_pr_supervision_recovery.rs:::::schedule_agent_workspace_durable_repair_reconciliation@f48509ef2fce95d5", + "kind": "tokio::spawn", + "readSurface": [], + "sinksReached": [ + "execute_entry_actions", + "send_message", + "transition_task", + "transition_task_corrective_with_exit", + "try_schedule_ready_tasks" + ] + }, + { + "authorityBearing": false, + "enclosingFunction": "application/agent_workspace_publish_lease.rs:::::spawn_publish_operation_lease_heartbeat_for_operation", + "file": "application/agent_workspace_publish_lease.rs", + "id": "application/agent_workspace_publish_lease.rs::application/agent_workspace_publish_lease.rs:::::spawn_publish_operation_lease_heartbeat_for_operation@f3bbfb85280204e7", + "kind": "tokio::spawn", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": false, + "enclosingFunction": "application/agent_workspace_review.rs:::::spawn_workspace_review_waiter", + "file": "application/agent_workspace_review.rs", + "id": "application/agent_workspace_review.rs::application/agent_workspace_review.rs:::::spawn_workspace_review_waiter@d26b9d0eeeb8d71c", + "kind": "tokio::spawn", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": true, + "enclosingFunction": "application/app_setup.rs:::::spawn_tasks_disabled_startup_reconciliation", + "file": "application/app_setup.rs", + "id": "application/app_setup.rs::application/app_setup.rs:::::spawn_tasks_disabled_startup_reconciliation@1af6913939164cf8", + "kind": "async_runtime::spawn", + "readSurface": [], + "sinksReached": [ + "apply_corrective_transition", + "transition_task_with_metadata", + "try_schedule_ready_tasks", + "write_message" + ] + }, + { + "authorityBearing": true, + "enclosingFunction": "application/app_setup.rs:::::launch_startup_attempt", + "file": "application/app_setup.rs", + "id": "application/app_setup.rs::application/app_setup.rs:::::launch_startup_attempt@5fc4b3f848826903", + "kind": "async_runtime::spawn", + "readSurface": [], + "sinksReached": [ + "apply_corrective_transition", + "execute_entry_actions", + "send_message", + "spawn_ready_task_scheduler_if_needed", + "transition_task", + "transition_task_corrective", + "transition_task_corrective_with_exit", + "transition_task_with_metadata", + "try_schedule_ready_tasks", + "write_message" + ] + }, + { + "authorityBearing": false, + "enclosingFunction": "application/app_setup.rs:::::launch_startup_attempt", + "file": "application/app_setup.rs", + "id": "application/app_setup.rs::application/app_setup.rs:::::launch_startup_attempt@d038ae2e22f815b6", + "kind": "spawn_blocking", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": false, + "enclosingFunction": "application/atlassian_integration_service.rs:::::spawn_oauth_callback_listener", + "file": "application/atlassian_integration_service.rs", + "id": "application/atlassian_integration_service.rs::application/atlassian_integration_service.rs:::::spawn_oauth_callback_listener@48c52bbda98df691", + "kind": "tokio::spawn", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": false, + "enclosingFunction": "application/automation/scheduler.rs:::::spawn_automation_judge_task", + "file": "application/automation/scheduler.rs", + "id": "application/automation/scheduler.rs::application/automation/scheduler.rs:::::spawn_automation_judge_task@4ec9a7f10e387111", + "kind": "async_runtime::spawn", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": true, + "enclosingFunction": "application/automation/scheduler.rs:::::spawn_automation_plan_judge_task", + "file": "application/automation/scheduler.rs", + "id": "application/automation/scheduler.rs::application/automation/scheduler.rs:::::spawn_automation_plan_judge_task@4234cda95c7e16e5", + "kind": "async_runtime::spawn", + "readSurface": [], + "sinksReached": [ + "send_message" + ] + }, + { + "authorityBearing": false, + "enclosingFunction": "application/chat_service/chat_service_merge.rs:::::complete_merge_and_schedule", + "file": "application/chat_service/chat_service_merge.rs", + "id": "application/chat_service/chat_service_merge.rs::application/chat_service/chat_service_merge.rs:::::complete_merge_and_schedule@8de33965b9c5cb44", + "kind": "tokio::spawn", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": true, + "enclosingFunction": "application/chat_service/chat_service_merge.rs:::::complete_merge_and_schedule", + "file": "application/chat_service/chat_service_merge.rs", + "id": "application/chat_service/chat_service_merge.rs::application/chat_service/chat_service_merge.rs:::::complete_merge_and_schedule@fe9f5f19b8585062", + "kind": "tokio::spawn", + "readSurface": [], + "sinksReached": [ + "try_schedule_ready_tasks" + ] + }, + { + "authorityBearing": false, + "enclosingFunction": "application/chat_service/chat_service_merge.rs:::::spawn_merge_completion_watcher", + "file": "application/chat_service/chat_service_merge.rs", + "id": "application/chat_service/chat_service_merge.rs::application/chat_service/chat_service_merge.rs:::::spawn_merge_completion_watcher@a492a96b8b5aebed", + "kind": "tokio::spawn", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": false, + "enclosingFunction": "application/chat_service/chat_service_runtime_handoff.rs:::::activate_runtime_handoff_watchdog", + "file": "application/chat_service/chat_service_runtime_handoff.rs", + "id": "application/chat_service/chat_service_runtime_handoff.rs::application/chat_service/chat_service_runtime_handoff.rs:::::activate_runtime_handoff_watchdog@12f01816b3f2b1dc", + "kind": "tokio::spawn", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": true, + "enclosingFunction": "application/chat_service/chat_service_send_background.rs:::::spawn_send_message_background", + "file": "application/chat_service/chat_service_send_background.rs", + "id": "application/chat_service/chat_service_send_background.rs::application/chat_service/chat_service_send_background.rs:::::spawn_send_message_background@d4c8882a5ca83ac6", + "kind": "tokio::spawn", + "readSurface": [], + "sinksReached": [ + "apply_corrective_transition", + "execute_entry_actions", + "send_message", + "transition_task", + "transition_task_corrective_with_exit", + "try_schedule_ready_tasks", + "write_message" + ] + }, + { + "authorityBearing": true, + "enclosingFunction": "application/chat_service/chat_service_send_background.rs:::::spawn_send_message_background", + "file": "application/chat_service/chat_service_send_background.rs", + "id": "application/chat_service/chat_service_send_background.rs::application/chat_service/chat_service_send_background.rs:::::spawn_send_message_background@58d1d53a0125385c", + "kind": "tokio::spawn", + "readSurface": [], + "sinksReached": [ + "send_message" + ] + }, + { + "authorityBearing": false, + "enclosingFunction": "application/chat_service/chat_service_streaming.rs:::::process_stream_background", + "file": "application/chat_service/chat_service_streaming.rs", + "id": "application/chat_service/chat_service_streaming.rs::application/chat_service/chat_service_streaming.rs:::::process_stream_background@8f61252966eb76fc", + "kind": "tokio::spawn", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": false, + "enclosingFunction": "application/chat_service/chat_service_streaming.rs:::::process_codex_stream_background", + "file": "application/chat_service/chat_service_streaming.rs", + "id": "application/chat_service/chat_service_streaming.rs::application/chat_service/chat_service_streaming.rs:::::process_codex_stream_background@8f61252966eb76fc", + "kind": "tokio::spawn", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": false, + "enclosingFunction": "application/chat_service/chat_service_streaming.rs:::::detach_codex_completed_process_cleanup", + "file": "application/chat_service/chat_service_streaming.rs", + "id": "application/chat_service/chat_service_streaming.rs::application/chat_service/chat_service_streaming.rs:::::detach_codex_completed_process_cleanup@f8154e156bbf1929", + "kind": "tokio::spawn", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": false, + "enclosingFunction": "application/chat_service/launch_reservation.rs::LaunchReservationGuard::new", + "file": "application/chat_service/launch_reservation.rs", + "id": "application/chat_service/launch_reservation.rs::application/chat_service/launch_reservation.rs::LaunchReservationGuard::new@eb4d1acdcacd6079", + "kind": "tokio::spawn", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": false, + "enclosingFunction": "application/desktop_notification.rs:::::send_actionable", + "file": "application/desktop_notification.rs", + "id": "application/desktop_notification.rs::application/desktop_notification.rs:::::send_actionable@460f4228198e486d", + "kind": "method::spawn", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": false, + "enclosingFunction": "application/harness_runtime_registry.rs:::::probe_standard_harnesses_with", + "file": "application/harness_runtime_registry.rs", + "id": "application/harness_runtime_registry.rs::application/harness_runtime_registry.rs:::::probe_standard_harnesses_with@59d10e2433c6a7d0", + "kind": "method::spawn", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": false, + "enclosingFunction": "application/mcp_policy_service.rs::McpPolicyService::resolve_claude_cleanup_cli", + "file": "application/mcp_policy_service.rs", + "id": "application/mcp_policy_service.rs::application/mcp_policy_service.rs::McpPolicyService::resolve_claude_cleanup_cli@273379f9de704f96", + "kind": "spawn_blocking", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": false, + "enclosingFunction": "application/memory_orchestration.rs:::::trigger_memory_pipelines", + "file": "application/memory_orchestration.rs", + "id": "application/memory_orchestration.rs::application/memory_orchestration.rs:::::trigger_memory_pipelines@e5ba04d0a436c18e", + "kind": "tokio::spawn", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": false, + "enclosingFunction": "application/memory_orchestration.rs:::::trigger_memory_pipelines", + "file": "application/memory_orchestration.rs", + "id": "application/memory_orchestration.rs::application/memory_orchestration.rs:::::trigger_memory_pipelines@7d88c4f92e305916", + "kind": "tokio::spawn", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": false, + "enclosingFunction": "application/notification_service.rs::DesktopNotificationCoalescer::enqueue", + "file": "application/notification_service.rs", + "id": "application/notification_service.rs::application/notification_service.rs::DesktopNotificationCoalescer::enqueue@7cd7d7202cf7752b", + "kind": "tokio::spawn", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": false, + "enclosingFunction": "application/plan_complexity_assessment.rs:::::spawn_plan_complexity_assessor_after_approval", + "file": "application/plan_complexity_assessment.rs", + "id": "application/plan_complexity_assessment.rs::application/plan_complexity_assessment.rs:::::spawn_plan_complexity_assessor_after_approval@55958ae5147d554e", + "kind": "tokio::spawn", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": false, + "enclosingFunction": "application/plan_complexity_assessment.rs:::::spawn_plan_complexity_assessor_from_app_handle", + "file": "application/plan_complexity_assessment.rs", + "id": "application/plan_complexity_assessment.rs::application/plan_complexity_assessment.rs:::::spawn_plan_complexity_assessor_from_app_handle@4085b3dfc61cecc6", + "kind": "tokio::spawn", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": false, + "enclosingFunction": "application/pr_startup_recovery.rs:::::recover_missing_draft_prs", + "file": "application/pr_startup_recovery.rs", + "id": "application/pr_startup_recovery.rs::application/pr_startup_recovery.rs:::::recover_missing_draft_prs@59280b2016c90ef8", + "kind": "async_runtime::spawn", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": true, + "enclosingFunction": "application/ready_task_scheduler.rs:::::spawn_ready_task_scheduler_if_needed", + "file": "application/ready_task_scheduler.rs", + "id": "application/ready_task_scheduler.rs::application/ready_task_scheduler.rs:::::spawn_ready_task_scheduler_if_needed@57e1eb6d86c1770f", + "kind": "tokio::spawn", + "readSurface": [ + "ready-task" + ], + "sinksReached": [ + "try_schedule_ready_tasks" + ] + }, + { + "authorityBearing": false, + "enclosingFunction": "application/remote_event_relay.rs::RemoteEventRelay::connect", + "file": "application/remote_event_relay.rs", + "id": "application/remote_event_relay.rs::application/remote_event_relay.rs::RemoteEventRelay::connect@ea05994e664c8c1f", + "kind": "tokio::spawn", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": true, + "enclosingFunction": "application/server_boot.rs:::::start_server_boot", + "file": "application/server_boot.rs", + "id": "application/server_boot.rs::application/server_boot.rs:::::start_server_boot@5ea4e57295850417", + "kind": "async_runtime::spawn", + "readSurface": [], + "sinksReached": [ + "write_message" + ] + }, + { + "authorityBearing": true, + "enclosingFunction": "application/services/pr_merge_poller.rs::PrPollerRegistry::start_agent_workspace_polling_with_optional_repair_repo", + "file": "application/services/pr_merge_poller.rs", + "id": "application/services/pr_merge_poller.rs::application/services/pr_merge_poller.rs::PrPollerRegistry::start_agent_workspace_polling_with_optional_repair_repo@9463291f555a3c9d", + "kind": "tokio::spawn", + "readSurface": [], + "sinksReached": [ + "execute_entry_actions", + "send_message", + "transition_task", + "transition_task_corrective_with_exit", + "try_schedule_ready_tasks" + ] + }, + { + "authorityBearing": true, + "enclosingFunction": "application/services/pr_merge_poller.rs::PrPollerRegistry::start_polling", + "file": "application/services/pr_merge_poller.rs", + "id": "application/services/pr_merge_poller.rs::application/services/pr_merge_poller.rs::PrPollerRegistry::start_polling@d0919ee006ecaf5d", + "kind": "tokio::spawn", + "readSurface": [], + "sinksReached": [ + "execute_entry_actions", + "send_message", + "transition_task", + "transition_task_corrective_with_exit", + "try_schedule_ready_tasks" + ] + }, + { + "authorityBearing": false, + "enclosingFunction": "application/services/pr_merge_poller.rs::PrPollerRegistry::stop_polling", + "file": "application/services/pr_merge_poller.rs", + "id": "application/services/pr_merge_poller.rs::application/services/pr_merge_poller.rs::PrPollerRegistry::stop_polling@af695266fb3e0867", + "kind": "tokio::spawn", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": false, + "enclosingFunction": "application/session_namer_agent.rs:::::spawn_session_namer_agent", + "file": "application/session_namer_agent.rs", + "id": "application/session_namer_agent.rs::application/session_namer_agent.rs:::::spawn_session_namer_agent@8f4be8f1cf009c4f", + "kind": "tokio::spawn", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": false, + "enclosingFunction": "application/shutdown.rs::ExitWatchdog::arm_with", + "file": "application/shutdown.rs", + "id": "application/shutdown.rs::application/shutdown.rs::ExitWatchdog::arm_with@443d5cc12f6f5989", + "kind": "thread::spawn", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": true, + "enclosingFunction": "application/startup_background.rs:::::spawn_watchdog", + "file": "application/startup_background.rs", + "id": "application/startup_background.rs::application/startup_background.rs:::::spawn_watchdog@8c28974ee8ca859d", + "kind": "async_runtime::spawn", + "readSurface": [ + "pending-review-freshness" + ], + "sinksReached": [ + "try_schedule_ready_tasks" + ] + }, + { + "authorityBearing": true, + "enclosingFunction": "application/startup_background.rs:::::spawn_automation_scheduler", + "file": "application/startup_background.rs", + "id": "application/startup_background.rs::application/startup_background.rs:::::spawn_automation_scheduler@c034c5fc2b8fe7b8", + "kind": "async_runtime::spawn", + "readSurface": [ + "automation-active" + ], + "sinksReached": [ + "execute_entry_actions", + "send_message", + "transition_task", + "transition_task_corrective_with_exit", + "try_schedule_ready_tasks", + "write_message" + ] + }, + { + "authorityBearing": true, + "enclosingFunction": "application/startup_background.rs:::::spawn_cleanup_loops", + "file": "application/startup_background.rs", + "id": "application/startup_background.rs::application/startup_background.rs:::::spawn_cleanup_loops@37241f00ac06a1e1", + "kind": "async_runtime::spawn", + "readSurface": [], + "sinksReached": [ + "try_schedule_ready_tasks" + ] + }, + { + "authorityBearing": false, + "enclosingFunction": "application/startup_background.rs:::::spawn_cleanup_loops", + "file": "application/startup_background.rs", + "id": "application/startup_background.rs::application/startup_background.rs:::::spawn_cleanup_loops@4d9ee87be9aa0564", + "kind": "async_runtime::spawn", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": true, + "enclosingFunction": "application/startup_background.rs:::::spawn_agent_workspace_bridge_dispatcher", + "file": "application/startup_background.rs", + "id": "application/startup_background.rs::application/startup_background.rs:::::spawn_agent_workspace_bridge_dispatcher@ce779acefa5432", + "kind": "async_runtime::spawn", + "readSurface": [ + "workspace-bridge", + "external-event-cursor" + ], + "sinksReached": [ + "send_message" + ] + }, + { + "authorityBearing": true, + "enclosingFunction": "application/startup_background.rs:::::spawn_remote_conversation_start_dispatcher", + "file": "application/startup_background.rs", + "id": "application/startup_background.rs::application/startup_background.rs:::::spawn_remote_conversation_start_dispatcher@2212793b1dbfb5d0", + "kind": "async_runtime::spawn", + "readSurface": [ + "remote-conversation-start" + ], + "sinksReached": [ + "execute_entry_actions", + "send_message", + "transition_task", + "transition_task_corrective_with_exit", + "try_schedule_ready_tasks", + "write_message" + ] + }, + { + "authorityBearing": true, + "enclosingFunction": "application/startup_background.rs:::::spawn_remote_conversation_message_dispatcher", + "file": "application/startup_background.rs", + "id": "application/startup_background.rs::application/startup_background.rs:::::spawn_remote_conversation_message_dispatcher@c959b62d91939d1", + "kind": "async_runtime::spawn", + "readSurface": [ + "remote-conversation-message" + ], + "sinksReached": [ + "send_message" + ] + }, + { + "authorityBearing": false, + "enclosingFunction": "application/startup_background.rs:::::spawn_remote_agent_stop_dispatcher", + "file": "application/startup_background.rs", + "id": "application/startup_background.rs::application/startup_background.rs:::::spawn_remote_agent_stop_dispatcher@4d7fc0892abbe19d", + "kind": "async_runtime::spawn", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": true, + "enclosingFunction": "application/startup_background.rs:::::spawn_remote_resume_dispatchers", + "file": "application/startup_background.rs", + "id": "application/startup_background.rs::application/startup_background.rs:::::spawn_remote_resume_dispatchers@ced3a7ce75eb6466", + "kind": "async_runtime::spawn", + "readSurface": [ + "remote-conversation-lifecycle", + "remote-execution-resume", + "remote-task-action", + "remote-plan-approval", + "remote-finalize-decision", + "remote-queued-send", + "remote-automation-run", + "remote-automation-draft" + ], + "sinksReached": [ + "apply_corrective_transition", + "execute_entry_actions", + "send_message", + "spawn_ready_task_scheduler_if_needed", + "transition_task", + "transition_task_corrective_with_exit", + "transition_task_with_metadata", + "try_schedule_ready_tasks", + "write_message" + ] + }, + { + "authorityBearing": true, + "enclosingFunction": "application/startup_background.rs:::::spawn_remote_conversation_mode_switch_dispatcher", + "file": "application/startup_background.rs", + "id": "application/startup_background.rs::application/startup_background.rs:::::spawn_remote_conversation_mode_switch_dispatcher@af26b90d13c69a8c", + "kind": "async_runtime::spawn", + "readSurface": [ + "remote-conversation-mode-switch" + ], + "sinksReached": [ + "execute_entry_actions", + "send_message", + "transition_task", + "transition_task_corrective_with_exit", + "try_schedule_ready_tasks" + ] + }, + { + "authorityBearing": false, + "enclosingFunction": "application/startup_cleanup.rs:::::run_startup_cleanup", + "file": "application/startup_cleanup.rs", + "id": "application/startup_cleanup.rs::application/startup_cleanup.rs:::::run_startup_cleanup@b23727dfe32db57e", + "kind": "async_runtime::spawn", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": false, + "enclosingFunction": "application/startup_cleanup.rs:::::run_startup_cleanup", + "file": "application/startup_cleanup.rs", + "id": "application/startup_cleanup.rs::application/startup_cleanup.rs:::::run_startup_cleanup@ac5266eb7ef74c2", + "kind": "async_runtime::spawn", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": true, + "enclosingFunction": "application/startup_jobs.rs::StartupJobRunner::spawn_post_ready_safety_net", + "file": "application/startup_jobs.rs", + "id": "application/startup_jobs.rs::application/startup_jobs.rs::StartupJobRunner::spawn_post_ready_safety_net@820a72b82039af04", + "kind": "async_runtime::spawn", + "readSurface": [], + "sinksReached": [ + "execute_entry_actions", + "send_message", + "transition_task", + "transition_task_corrective", + "transition_task_corrective_with_exit", + "transition_task_with_metadata", + "try_schedule_ready_tasks", + "write_message" + ] + }, + { + "authorityBearing": false, + "enclosingFunction": "application/startup_jobs.rs::StartupJobRunner::run_inner", + "file": "application/startup_jobs.rs", + "id": "application/startup_jobs.rs::application/startup_jobs.rs::StartupJobRunner::run_inner@6a4da428113a9f43", + "kind": "async_runtime::spawn", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": false, + "enclosingFunction": "application/startup_jobs.rs::StartupJobRunner::run_inner", + "file": "application/startup_jobs.rs", + "id": "application/startup_jobs.rs::application/startup_jobs.rs::StartupJobRunner::run_inner@c38775e416773b52", + "kind": "tokio::spawn", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": false, + "enclosingFunction": "application/startup_jobs.rs::StartupJobRunner::resume_pending_cleanup", + "file": "application/startup_jobs.rs", + "id": "application/startup_jobs.rs::application/startup_jobs.rs::StartupJobRunner::resume_pending_cleanup@39565a10f03dbcdc", + "kind": "tokio::spawn", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": true, + "enclosingFunction": "application/startup_pipeline.rs:::::spawn_delegation_park_deadline_sweep", + "file": "application/startup_pipeline.rs", + "id": "application/startup_pipeline.rs::application/startup_pipeline.rs:::::spawn_delegation_park_deadline_sweep@c42aace6a39532e2", + "kind": "async_runtime::spawn", + "readSurface": [], + "sinksReached": [ + "send_message" + ] + }, + { + "authorityBearing": false, + "enclosingFunction": "application/startup_pipeline.rs:::::run_startup_pipeline", + "file": "application/startup_pipeline.rs", + "id": "application/startup_pipeline.rs::application/startup_pipeline.rs:::::run_startup_pipeline@3747a8fa9db45d7a", + "kind": "spawn_blocking", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": false, + "enclosingFunction": "application/startup_pipeline.rs:::::run_startup_pipeline", + "file": "application/startup_pipeline.rs", + "id": "application/startup_pipeline.rs::application/startup_pipeline.rs:::::run_startup_pipeline@3b3bf6744b4afcb8", + "kind": "async_runtime::spawn", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": false, + "enclosingFunction": "application/startup_pipeline.rs:::::run_startup_pipeline", + "file": "application/startup_pipeline.rs", + "id": "application/startup_pipeline.rs::application/startup_pipeline.rs:::::run_startup_pipeline@51c2951c6eed71b0", + "kind": "async_runtime::spawn", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": true, + "enclosingFunction": "application/startup_pipeline.rs:::::run_startup_pipeline", + "file": "application/startup_pipeline.rs", + "id": "application/startup_pipeline.rs::application/startup_pipeline.rs:::::run_startup_pipeline@e80cc739fa2ab5dd", + "kind": "async_runtime::spawn", + "readSurface": [], + "sinksReached": [ + "execute_entry_actions", + "send_message", + "transition_task", + "transition_task_corrective_with_exit", + "try_schedule_ready_tasks" + ] + }, + { + "authorityBearing": true, + "enclosingFunction": "application/startup_pipeline.rs:::::run_startup_pipeline", + "file": "application/startup_pipeline.rs", + "id": "application/startup_pipeline.rs::application/startup_pipeline.rs:::::run_startup_pipeline@574295ff86f1f8ec", + "kind": "async_runtime::spawn", + "readSurface": [], + "sinksReached": [ + "execute_entry_actions", + "send_message", + "transition_task", + "transition_task_corrective", + "transition_task_corrective_with_exit", + "transition_task_with_metadata", + "try_schedule_ready_tasks", + "write_message" + ] + }, + { + "authorityBearing": true, + "enclosingFunction": "application/startup_pipeline_launch.rs:::::launch_startup_pipeline_from_handle", + "file": "application/startup_pipeline_launch.rs", + "id": "application/startup_pipeline_launch.rs::application/startup_pipeline_launch.rs:::::launch_startup_pipeline_from_handle@20818f1b5ff08f6f", + "kind": "async_runtime::spawn", + "readSurface": [], + "sinksReached": [ + "apply_corrective_transition", + "execute_entry_actions", + "send_message", + "spawn_ready_task_scheduler_if_needed", + "transition_task", + "transition_task_corrective", + "transition_task_corrective_with_exit", + "transition_task_with_metadata", + "try_schedule_ready_tasks", + "write_message" + ] + }, + { + "authorityBearing": true, + "enclosingFunction": "application/task_scheduler_service/mod.rs::TaskSchedulerService::try_schedule_ready_tasks", + "file": "application/task_scheduler_service/mod.rs", + "id": "application/task_scheduler_service/mod.rs::application/task_scheduler_service/mod.rs::TaskSchedulerService::try_schedule_ready_tasks@d9815866a2059bea", + "kind": "tokio::spawn", + "readSurface": [], + "sinksReached": [ + "try_schedule_ready_tasks" + ] + }, + { + "authorityBearing": false, + "enclosingFunction": "application/tasks_feature_toggle_service.rs::TasksFeatureToggleService::reconcile_missing_assessments", + "file": "application/tasks_feature_toggle_service.rs", + "id": "application/tasks_feature_toggle_service.rs::application/tasks_feature_toggle_service.rs::TasksFeatureToggleService::reconcile_missing_assessments@bf6483028506d209", + "kind": "async_runtime::spawn", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": false, + "enclosingFunction": "application/throttled_emitter.rs::ThrottledEmitter::new", + "file": "application/throttled_emitter.rs", + "id": "application/throttled_emitter.rs::application/throttled_emitter.rs::ThrottledEmitter::new@727141e7f83b927c", + "kind": "thread::spawn", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": false, + "enclosingFunction": "commands/agent_composer_commands/project_entries.rs:::::search_agent_composer_entries", + "file": "commands/agent_composer_commands/project_entries.rs", + "id": "commands/agent_composer_commands/project_entries.rs::commands/agent_composer_commands/project_entries.rs:::::search_agent_composer_entries@83e87f60377a9417", + "kind": "spawn_blocking", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": false, + "enclosingFunction": "commands/agent_composer_commands/skills.rs:::::list_agent_composer_skills", + "file": "commands/agent_composer_commands/skills.rs", + "id": "commands/agent_composer_commands/skills.rs::commands/agent_composer_commands/skills.rs:::::list_agent_composer_skills@434d1ab79ffd0c60", + "kind": "spawn_blocking", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": true, + "enclosingFunction": "commands/agent_workspace_auto_publish.rs:::::install_agent_workspace_auto_publish_non_completion_sources", + "file": "commands/agent_workspace_auto_publish.rs", + "id": "commands/agent_workspace_auto_publish.rs::commands/agent_workspace_auto_publish.rs:::::install_agent_workspace_auto_publish_non_completion_sources@df1d3f2407db9b84", + "kind": "listen_any", + "readSurface": [], + "sinksReached": [ + "execute_entry_actions", + "send_message", + "transition_task", + "transition_task_corrective_with_exit", + "try_schedule_ready_tasks" + ] + }, + { + "authorityBearing": true, + "enclosingFunction": "commands/agent_workspace_auto_publish.rs:::::spawn_pr_supervision_recovery_from_completion_payload", + "file": "commands/agent_workspace_auto_publish.rs", + "id": "commands/agent_workspace_auto_publish.rs::commands/agent_workspace_auto_publish.rs:::::spawn_pr_supervision_recovery_from_completion_payload@fd751fcb1dd83895", + "kind": "async_runtime::spawn", + "readSurface": [], + "sinksReached": [ + "execute_entry_actions", + "send_message", + "transition_task", + "transition_task_corrective_with_exit", + "try_schedule_ready_tasks" + ] + }, + { + "authorityBearing": true, + "enclosingFunction": "commands/agent_workspace_auto_publish.rs:::::spawn_auto_publish_existing_pr", + "file": "commands/agent_workspace_auto_publish.rs", + "id": "commands/agent_workspace_auto_publish.rs::commands/agent_workspace_auto_publish.rs:::::spawn_auto_publish_existing_pr@a8d5e7104647d8a8", + "kind": "async_runtime::spawn", + "readSurface": [], + "sinksReached": [ + "execute_entry_actions", + "send_message", + "transition_task", + "transition_task_corrective_with_exit", + "try_schedule_ready_tasks" + ] + }, + { + "authorityBearing": true, + "enclosingFunction": "commands/agent_workspace_auto_publish.rs:::::start_agent_workspace_auto_publish_freshness_scan", + "file": "commands/agent_workspace_auto_publish.rs", + "id": "commands/agent_workspace_auto_publish.rs::commands/agent_workspace_auto_publish.rs:::::start_agent_workspace_auto_publish_freshness_scan@3a8d62e625ea5914", + "kind": "async_runtime::spawn", + "readSurface": [ + "workspace-auto-publish" + ], + "sinksReached": [ + "execute_entry_actions", + "send_message", + "transition_task", + "transition_task_corrective_with_exit", + "try_schedule_ready_tasks" + ] + }, + { + "authorityBearing": true, + "enclosingFunction": "commands/agent_workspace_auto_review.rs:::::spawn_auto_review_from_completion_payload", + "file": "commands/agent_workspace_auto_review.rs", + "id": "commands/agent_workspace_auto_review.rs::commands/agent_workspace_auto_review.rs:::::spawn_auto_review_from_completion_payload@2080f33f14a291dc", + "kind": "async_runtime::spawn", + "readSurface": [], + "sinksReached": [ + "send_message" + ] + }, + { + "authorityBearing": true, + "enclosingFunction": "commands/agent_workspace_auto_review.rs:::::spawn_auto_review_for_workspace", + "file": "commands/agent_workspace_auto_review.rs", + "id": "commands/agent_workspace_auto_review.rs::commands/agent_workspace_auto_review.rs:::::spawn_auto_review_for_workspace@a952be79d060c28f", + "kind": "async_runtime::spawn", + "readSurface": [ + "workspace-auto-review" + ], + "sinksReached": [ + "send_message" + ] + }, + { + "authorityBearing": true, + "enclosingFunction": "commands/agent_workspace_auto_review.rs:::::spawn_auto_review_after_workspace_change", + "file": "commands/agent_workspace_auto_review.rs", + "id": "commands/agent_workspace_auto_review.rs::commands/agent_workspace_auto_review.rs:::::spawn_auto_review_after_workspace_change@c220b792375b363c", + "kind": "async_runtime::spawn", + "readSurface": [], + "sinksReached": [ + "send_message" + ] + }, + { + "authorityBearing": false, + "enclosingFunction": "commands/agent_workspace_completion_dispatch.rs:::::listen_for_tauri_completion", + "file": "commands/agent_workspace_completion_dispatch.rs", + "id": "commands/agent_workspace_completion_dispatch.rs::commands/agent_workspace_completion_dispatch.rs:::::listen_for_tauri_completion@60afc40b1bc3f83a", + "kind": "listen_any", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": false, + "enclosingFunction": "commands/agent_workspace_completion_dispatch.rs:::::spawn_bus_completion_dispatch", + "file": "commands/agent_workspace_completion_dispatch.rs", + "id": "commands/agent_workspace_completion_dispatch.rs::commands/agent_workspace_completion_dispatch.rs:::::spawn_bus_completion_dispatch@f0f007830c625948", + "kind": "async_runtime::spawn", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": true, + "enclosingFunction": "commands/agent_workspace_repair_reconciliation_scan.rs:::::start_agent_workspace_repair_reconciliation_scan", + "file": "commands/agent_workspace_repair_reconciliation_scan.rs", + "id": "commands/agent_workspace_repair_reconciliation_scan.rs::commands/agent_workspace_repair_reconciliation_scan.rs:::::start_agent_workspace_repair_reconciliation_scan@7d96f5df9cf8fefb", + "kind": "async_runtime::spawn", + "readSurface": [], + "sinksReached": [ + "execute_entry_actions", + "send_message", + "transition_task", + "transition_task_corrective_with_exit", + "try_schedule_ready_tasks" + ] + }, + { + "authorityBearing": false, + "enclosingFunction": "commands/diff_commands.rs:::::get_agent_conversation_workspace_change_summary_for_state", + "file": "commands/diff_commands.rs", + "id": "commands/diff_commands.rs::commands/diff_commands.rs:::::get_agent_conversation_workspace_change_summary_for_state@12968b3b82dcf471", + "kind": "spawn_blocking", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": false, + "enclosingFunction": "commands/diff_commands.rs:::::get_agent_conversation_workspace_repair_change_summary_for_state", + "file": "commands/diff_commands.rs", + "id": "commands/diff_commands.rs::commands/diff_commands.rs:::::get_agent_conversation_workspace_repair_change_summary_for_state@e2588773af98a069", + "kind": "spawn_blocking", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": false, + "enclosingFunction": "commands/diff_commands.rs:::::get_agent_workspace_review_for_context", + "file": "commands/diff_commands.rs", + "id": "commands/diff_commands.rs::commands/diff_commands.rs:::::get_agent_workspace_review_for_context@aadd5e01730ac2e8", + "kind": "spawn_blocking", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": false, + "enclosingFunction": "commands/diff_commands.rs:::::get_agent_conversation_workspace_staged_file_changes_for_state", + "file": "commands/diff_commands.rs", + "id": "commands/diff_commands.rs::commands/diff_commands.rs:::::get_agent_conversation_workspace_staged_file_changes_for_state@69a0ae9dd523b936", + "kind": "spawn_blocking", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": false, + "enclosingFunction": "commands/diff_commands.rs:::::get_agent_conversation_workspace_unstaged_file_changes_for_state", + "file": "commands/diff_commands.rs", + "id": "commands/diff_commands.rs::commands/diff_commands.rs:::::get_agent_conversation_workspace_unstaged_file_changes_for_state@660112e8884af9d7", + "kind": "spawn_blocking", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": false, + "enclosingFunction": "commands/diff_commands.rs:::::get_agent_conversation_workspace_staged_file_diff_for_state", + "file": "commands/diff_commands.rs", + "id": "commands/diff_commands.rs::commands/diff_commands.rs:::::get_agent_conversation_workspace_staged_file_diff_for_state@ccba17139d9550d1", + "kind": "spawn_blocking", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": false, + "enclosingFunction": "commands/diff_commands.rs:::::get_agent_conversation_workspace_unstaged_file_diff_for_state", + "file": "commands/diff_commands.rs", + "id": "commands/diff_commands.rs::commands/diff_commands.rs:::::get_agent_conversation_workspace_unstaged_file_diff_for_state@eb1e79af2d20dabe", + "kind": "spawn_blocking", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": false, + "enclosingFunction": "commands/diff_commands.rs:::::get_agent_conversation_workspace_repair_staged_file_changes_for_state", + "file": "commands/diff_commands.rs", + "id": "commands/diff_commands.rs::commands/diff_commands.rs:::::get_agent_conversation_workspace_repair_staged_file_changes_for_state@69a0ae9dd523b936", + "kind": "spawn_blocking", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": false, + "enclosingFunction": "commands/diff_commands.rs:::::get_agent_conversation_workspace_repair_unstaged_file_changes_for_state", + "file": "commands/diff_commands.rs", + "id": "commands/diff_commands.rs::commands/diff_commands.rs:::::get_agent_conversation_workspace_repair_unstaged_file_changes_for_state@660112e8884af9d7", + "kind": "spawn_blocking", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": false, + "enclosingFunction": "commands/diff_commands.rs:::::get_agent_conversation_workspace_repair_staged_file_diff_for_state", + "file": "commands/diff_commands.rs", + "id": "commands/diff_commands.rs::commands/diff_commands.rs:::::get_agent_conversation_workspace_repair_staged_file_diff_for_state@ccba17139d9550d1", + "kind": "spawn_blocking", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": false, + "enclosingFunction": "commands/diff_commands.rs:::::get_agent_conversation_workspace_repair_unstaged_file_diff_for_state", + "file": "commands/diff_commands.rs", + "id": "commands/diff_commands.rs::commands/diff_commands.rs:::::get_agent_conversation_workspace_repair_unstaged_file_diff_for_state@eb1e79af2d20dabe", + "kind": "spawn_blocking", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": false, + "enclosingFunction": "commands/diff_commands.rs:::::get_agent_conversation_workspace_repair_conflict_file_diff_for_state", + "file": "commands/diff_commands.rs", + "id": "commands/diff_commands.rs::commands/diff_commands.rs:::::get_agent_conversation_workspace_repair_conflict_file_diff_for_state@4228250d84cfbfed", + "kind": "spawn_blocking", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": false, + "enclosingFunction": "commands/diff_commands.rs:::::get_agent_conversation_workspace_cumulative_file_changes_for_state", + "file": "commands/diff_commands.rs", + "id": "commands/diff_commands.rs::commands/diff_commands.rs:::::get_agent_conversation_workspace_cumulative_file_changes_for_state@f2607936528bf8d2", + "kind": "spawn_blocking", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": false, + "enclosingFunction": "commands/diff_commands.rs:::::get_agent_conversation_workspace_cumulative_file_diff_for_state", + "file": "commands/diff_commands.rs", + "id": "commands/diff_commands.rs::commands/diff_commands.rs:::::get_agent_conversation_workspace_cumulative_file_diff_for_state@303a9ded7f443b78", + "kind": "spawn_blocking", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": false, + "enclosingFunction": "commands/diff_commands.rs:::::get_agent_conversation_workspace_file_diff_page_for_state", + "file": "commands/diff_commands.rs", + "id": "commands/diff_commands.rs::commands/diff_commands.rs:::::get_agent_conversation_workspace_file_diff_page_for_state@14caa5cc82434f62", + "kind": "spawn_blocking", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": false, + "enclosingFunction": "commands/diff_commands.rs:::::get_agent_conversation_workspace_file_content_range_for_state", + "file": "commands/diff_commands.rs", + "id": "commands/diff_commands.rs::commands/diff_commands.rs:::::get_agent_conversation_workspace_file_content_range_for_state@8b4c500c430d0d4b", + "kind": "spawn_blocking", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": true, + "enclosingFunction": "commands/execution_commands/settings.rs:::::update_execution_settings", + "file": "commands/execution_commands/settings.rs", + "id": "commands/execution_commands/settings.rs::commands/execution_commands/settings.rs:::::update_execution_settings@ed80dfce434dab24", + "kind": "tokio::spawn", + "readSurface": [], + "sinksReached": [ + "send_message" + ] + }, + { + "authorityBearing": true, + "enclosingFunction": "commands/git_commands.rs:::::retry_merge_inner", + "file": "commands/git_commands.rs", + "id": "commands/git_commands.rs::commands/git_commands.rs:::::retry_merge_inner@de73d6494a605890", + "kind": "tokio::spawn", + "readSurface": [], + "sinksReached": [ + "transition_task" + ] + }, + { + "authorityBearing": false, + "enclosingFunction": "commands/mcp_policy_commands.rs:::::resolve_codex_catalog_cli_path", + "file": "commands/mcp_policy_commands.rs", + "id": "commands/mcp_policy_commands.rs::commands/mcp_policy_commands.rs:::::resolve_codex_catalog_cli_path@eab37640616400ce", + "kind": "spawn_blocking", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": false, + "enclosingFunction": "commands/project_commands.rs:::::spawn_project_analyzer", + "file": "commands/project_commands.rs", + "id": "commands/project_commands.rs::commands/project_commands.rs:::::spawn_project_analyzer@ad18ac0f2f7e9186", + "kind": "tokio::spawn", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": false, + "enclosingFunction": "commands/project_commands.rs:::::run_gh_web_login_command", + "file": "commands/project_commands.rs", + "id": "commands/project_commands.rs::commands/project_commands.rs:::::run_gh_web_login_command@54c99a518fbaeda4", + "kind": "tokio::spawn", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": false, + "enclosingFunction": "commands/project_commands.rs:::::run_gh_web_login_command", + "file": "commands/project_commands.rs", + "id": "commands/project_commands.rs::commands/project_commands.rs:::::run_gh_web_login_command@dcfe8ed8ccfae301", + "kind": "tokio::spawn", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": false, + "enclosingFunction": "commands/provider_cli_management_commands.rs:::::resolve_user_managed_codex_cli", + "file": "commands/provider_cli_management_commands.rs", + "id": "commands/provider_cli_management_commands.rs::commands/provider_cli_management_commands.rs:::::resolve_user_managed_codex_cli@e42cd7c298f67a", + "kind": "spawn_blocking", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": true, + "enclosingFunction": "commands/unified_chat_commands/mod.rs:::::spawn_deferred_agent_workspace_repair_message", + "file": "commands/unified_chat_commands/mod.rs", + "id": "commands/unified_chat_commands/mod.rs::commands/unified_chat_commands/mod.rs:::::spawn_deferred_agent_workspace_repair_message@20a7b50b03f88805", + "kind": "async_runtime::spawn", + "readSurface": [], + "sinksReached": [ + "send_message" + ] + }, + { + "authorityBearing": false, + "enclosingFunction": "commands/workspace_open_commands/mod.rs:::::warm_workspace_open_target_cache", + "file": "commands/workspace_open_commands/mod.rs", + "id": "commands/workspace_open_commands/mod.rs::commands/workspace_open_commands/mod.rs:::::warm_workspace_open_target_cache@57e0acfe73eecbdc", + "kind": "method::spawn", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": false, + "enclosingFunction": "domain/state_machine/transition_handler/cleanup_helpers.rs:::::os_thread_timeout", + "file": "domain/state_machine/transition_handler/cleanup_helpers.rs", + "id": "domain/state_machine/transition_handler/cleanup_helpers.rs::domain/state_machine/transition_handler/cleanup_helpers.rs:::::os_thread_timeout@b10ae79e820d6863", + "kind": "thread::spawn", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": true, + "enclosingFunction": "domain/state_machine/transition_handler/exit_actions.rs:::::spawn_deferred_merge_retry", + "file": "domain/state_machine/transition_handler/exit_actions.rs", + "id": "domain/state_machine/transition_handler/exit_actions.rs::domain/state_machine/transition_handler/exit_actions.rs:::::spawn_deferred_merge_retry@f709ea313a93796", + "kind": "tokio::spawn", + "readSurface": [], + "sinksReached": [ + "execute_entry_actions" + ] + }, + { + "authorityBearing": false, + "enclosingFunction": "domain/state_machine/transition_handler/merge_orchestrator.rs::super::TransitionHandler::check_already_merged", + "file": "domain/state_machine/transition_handler/merge_orchestrator.rs", + "id": "domain/state_machine/transition_handler/merge_orchestrator.rs::domain/state_machine/transition_handler/merge_orchestrator.rs::super::TransitionHandler::check_already_merged@477f1c0037d54120", + "kind": "tokio::spawn", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": false, + "enclosingFunction": "domain/state_machine/transition_handler/merge_orchestrator.rs::super::TransitionHandler::check_already_merged", + "file": "domain/state_machine/transition_handler/merge_orchestrator.rs", + "id": "domain/state_machine/transition_handler/merge_orchestrator.rs::domain/state_machine/transition_handler/merge_orchestrator.rs::super::TransitionHandler::check_already_merged@2d1c8510b2d00db2", + "kind": "tokio::spawn", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": false, + "enclosingFunction": "domain/state_machine/transition_handler/merge_orchestrator.rs::super::TransitionHandler::recover_deleted_source_branch", + "file": "domain/state_machine/transition_handler/merge_orchestrator.rs", + "id": "domain/state_machine/transition_handler/merge_orchestrator.rs::domain/state_machine/transition_handler/merge_orchestrator.rs::super::TransitionHandler::recover_deleted_source_branch@477f1c0037d54120", + "kind": "tokio::spawn", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": false, + "enclosingFunction": "domain/state_machine/transition_handler/merge_orchestrator.rs::super::TransitionHandler::recover_deleted_source_branch", + "file": "domain/state_machine/transition_handler/merge_orchestrator.rs", + "id": "domain/state_machine/transition_handler/merge_orchestrator.rs::domain/state_machine/transition_handler/merge_orchestrator.rs::super::TransitionHandler::recover_deleted_source_branch@2d1c8510b2d00db2", + "kind": "tokio::spawn", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": false, + "enclosingFunction": "domain/state_machine/transition_handler/merge_outcome_handler.rs::super::TransitionHandler::handle_outcome_success", + "file": "domain/state_machine/transition_handler/merge_outcome_handler.rs", + "id": "domain/state_machine/transition_handler/merge_outcome_handler.rs::domain/state_machine/transition_handler/merge_outcome_handler.rs::super::TransitionHandler::handle_outcome_success@fe7eb0b101aa2b57", + "kind": "tokio::spawn", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": false, + "enclosingFunction": "domain/state_machine/transition_handler/mod.rs::TransitionHandler::handle_transition", + "file": "domain/state_machine/transition_handler/mod.rs", + "id": "domain/state_machine/transition_handler/mod.rs::domain/state_machine/transition_handler/mod.rs::TransitionHandler::handle_transition@fcbd19f0afd1895", + "kind": "tokio::spawn", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": true, + "enclosingFunction": "domain/state_machine/transition_handler/on_enter_states/mod.rs::super::TransitionHandler::on_enter_dispatch", + "file": "domain/state_machine/transition_handler/on_enter_states/mod.rs", + "id": "domain/state_machine/transition_handler/on_enter_states/mod.rs::domain/state_machine/transition_handler/on_enter_states/mod.rs::super::TransitionHandler::on_enter_dispatch@d2cb0ef9904cf26f", + "kind": "tokio::spawn", + "readSurface": [], + "sinksReached": [ + "try_schedule_ready_tasks" + ] + }, + { + "authorityBearing": true, + "enclosingFunction": "domain/state_machine/transition_handler/on_enter_states/outcomes.rs::TransitionHandler::enter_merged_state", + "file": "domain/state_machine/transition_handler/on_enter_states/outcomes.rs", + "id": "domain/state_machine/transition_handler/on_enter_states/outcomes.rs::domain/state_machine/transition_handler/on_enter_states/outcomes.rs::TransitionHandler::enter_merged_state@fe9f5f19b8585062", + "kind": "tokio::spawn", + "readSurface": [], + "sinksReached": [ + "try_schedule_ready_tasks" + ] + }, + { + "authorityBearing": true, + "enclosingFunction": "domain/state_machine/transition_handler/on_enter_states/outcomes.rs::TransitionHandler::enter_merged_state", + "file": "domain/state_machine/transition_handler/on_enter_states/outcomes.rs", + "id": "domain/state_machine/transition_handler/on_enter_states/outcomes.rs::domain/state_machine/transition_handler/on_enter_states/outcomes.rs::TransitionHandler::enter_merged_state@13dc4a9048aabaa1", + "kind": "tokio::spawn", + "readSurface": [], + "sinksReached": [ + "execute_entry_actions" + ] + }, + { + "authorityBearing": true, + "enclosingFunction": "domain/state_machine/transition_handler/side_effects/transitions.rs::TransitionHandler::post_merge_cleanup", + "file": "domain/state_machine/transition_handler/side_effects/transitions.rs", + "id": "domain/state_machine/transition_handler/side_effects/transitions.rs::domain/state_machine/transition_handler/side_effects/transitions.rs::TransitionHandler::post_merge_cleanup@fe9f5f19b8585062", + "kind": "tokio::spawn", + "readSurface": [], + "sinksReached": [ + "try_schedule_ready_tasks" + ] + }, + { + "authorityBearing": true, + "enclosingFunction": "http_server/handlers/agent_workflows.rs:::::spawn_workflow_run", + "file": "http_server/handlers/agent_workflows.rs", + "id": "http_server/handlers/agent_workflows.rs::http_server/handlers/agent_workflows.rs:::::spawn_workflow_run@145b75766759fe7e", + "kind": "tokio::spawn", + "readSurface": [], + "sinksReached": [ + "send_message" + ] + }, + { + "authorityBearing": false, + "enclosingFunction": "http_server/handlers/api_keys.rs:::::validate_api_key", + "file": "http_server/handlers/api_keys.rs", + "id": "http_server/handlers/api_keys.rs::http_server/handlers/api_keys.rs:::::validate_api_key@6502ff212f0c9f9e", + "kind": "tokio::spawn", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": false, + "enclosingFunction": "http_server/handlers/api_keys.rs:::::validate_key", + "file": "http_server/handlers/api_keys.rs", + "id": "http_server/handlers/api_keys.rs::http_server/handlers/api_keys.rs:::::validate_key@9eab6a94e4b8a81", + "kind": "tokio::spawn", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": true, + "enclosingFunction": "http_server/handlers/coordination/native_delegation.rs:::::settle_delegation_from_run", + "file": "http_server/handlers/coordination/native_delegation.rs", + "id": "http_server/handlers/coordination/native_delegation.rs::http_server/handlers/coordination/native_delegation.rs:::::settle_delegation_from_run@bf518596cb4a8aa5", + "kind": "tokio::spawn", + "readSurface": [], + "sinksReached": [ + "send_message" + ] + }, + { + "authorityBearing": true, + "enclosingFunction": "http_server/handlers/coordination/native_delegation.rs:::::settle_delegation_from_run", + "file": "http_server/handlers/coordination/native_delegation.rs", + "id": "http_server/handlers/coordination/native_delegation.rs::http_server/handlers/coordination/native_delegation.rs:::::settle_delegation_from_run@2587d494b7cce8c2", + "kind": "tokio::spawn", + "readSurface": [], + "sinksReached": [ + "send_message" + ] + }, + { + "authorityBearing": true, + "enclosingFunction": "http_server/handlers/coordination/native_delegation.rs:::::start_delegate_impl_with_parent_run", + "file": "http_server/handlers/coordination/native_delegation.rs", + "id": "http_server/handlers/coordination/native_delegation.rs::http_server/handlers/coordination/native_delegation.rs:::::start_delegate_impl_with_parent_run@bc6f55eab957ba94", + "kind": "tokio::spawn", + "readSurface": [], + "sinksReached": [ + "send_message" + ] + }, + { + "authorityBearing": false, + "enclosingFunction": "http_server/handlers/external/ideation_runtime/messaging.rs:::::ideation_message_http", + "file": "http_server/handlers/external/ideation_runtime/messaging.rs", + "id": "http_server/handlers/external/ideation_runtime/messaging.rs::http_server/handlers/external/ideation_runtime/messaging.rs:::::ideation_message_http@bd681b5e09bf611a", + "kind": "tokio::spawn", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": false, + "enclosingFunction": "http_server/handlers/external/ideation_start/start.rs:::::start_ideation_http", + "file": "http_server/handlers/external/ideation_start/start.rs", + "id": "http_server/handlers/external/ideation_start/start.rs::http_server/handlers/external/ideation_start/start.rs:::::start_ideation_http@c1608db41e6d2b91", + "kind": "tokio::spawn", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": false, + "enclosingFunction": "http_server/handlers/ideation/proposals.rs:::::finalize_proposals", + "file": "http_server/handlers/ideation/proposals.rs", + "id": "http_server/handlers/ideation/proposals.rs::http_server/handlers/ideation/proposals.rs:::::finalize_proposals@f013f2149c17d6b9", + "kind": "tokio::spawn", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": false, + "enclosingFunction": "http_server/handlers/ideation/verification/lifecycle.rs:::::mark_verification_infra_failure", + "file": "http_server/handlers/ideation/verification/lifecycle.rs", + "id": "http_server/handlers/ideation/verification/lifecycle.rs::http_server/handlers/ideation/verification/lifecycle.rs:::::mark_verification_infra_failure@456ad8b7ee94da4e", + "kind": "async_runtime::spawn", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": true, + "enclosingFunction": "http_server/handlers/ideation/verification/update.rs:::::post_verification_status", + "file": "http_server/handlers/ideation/verification/update.rs", + "id": "http_server/handlers/ideation/verification/update.rs::http_server/handlers/ideation/verification/update.rs:::::post_verification_status@2d962709c1852760", + "kind": "async_runtime::spawn", + "readSurface": [], + "sinksReached": [ + "send_message", + "write_message" + ] + }, + { + "authorityBearing": true, + "enclosingFunction": "http_server/handlers/managed_team/messaging.rs:::::send_managed_team_message", + "file": "http_server/handlers/managed_team/messaging.rs", + "id": "http_server/handlers/managed_team/messaging.rs::http_server/handlers/managed_team/messaging.rs:::::send_managed_team_message@3ebdff8b8d3d7c20", + "kind": "tokio::spawn", + "readSurface": [], + "sinksReached": [ + "send_message" + ] + }, + { + "authorityBearing": false, + "enclosingFunction": "http_server/handlers/reviews/complete.rs:::::complete_review", + "file": "http_server/handlers/reviews/complete.rs", + "id": "http_server/handlers/reviews/complete.rs::http_server/handlers/reviews/complete.rs:::::complete_review@50c1711424ada62c", + "kind": "tokio::spawn", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": false, + "enclosingFunction": "infrastructure/agents/claude/mod.rs::SpawnableCommand::spawn", + "file": "infrastructure/agents/claude/mod.rs", + "id": "infrastructure/agents/claude/mod.rs::infrastructure/agents/claude/mod.rs::SpawnableCommand::spawn@f589f668db5f09e", + "kind": "tokio::spawn", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": false, + "enclosingFunction": "infrastructure/agents/mock/mock_client.rs::MockAgenticClient::stream_response", + "file": "infrastructure/agents/mock/mock_client.rs", + "id": "infrastructure/agents/mock/mock_client.rs::infrastructure/agents/mock/mock_client.rs::MockAgenticClient::stream_response@6c0348ea2278446a", + "kind": "tokio::spawn", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": false, + "enclosingFunction": "infrastructure/external_mcp_supervisor.rs::ExternalMcpSupervisor::start", + "file": "infrastructure/external_mcp_supervisor.rs", + "id": "infrastructure/external_mcp_supervisor.rs::infrastructure/external_mcp_supervisor.rs::ExternalMcpSupervisor::start@916b09f3970dc3d1", + "kind": "tokio::spawn", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": false, + "enclosingFunction": "infrastructure/external_mcp_supervisor.rs::ExternalMcpSupervisor::run_supervisor_with_panic_guard", + "file": "infrastructure/external_mcp_supervisor.rs", + "id": "infrastructure/external_mcp_supervisor.rs::infrastructure/external_mcp_supervisor.rs::ExternalMcpSupervisor::run_supervisor_with_panic_guard@53028497fb458f18", + "kind": "tokio::spawn", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": false, + "enclosingFunction": "infrastructure/external_mcp_supervisor.rs::ExternalMcpSupervisor::attach_io_handles", + "file": "infrastructure/external_mcp_supervisor.rs", + "id": "infrastructure/external_mcp_supervisor.rs::infrastructure/external_mcp_supervisor.rs::ExternalMcpSupervisor::attach_io_handles@fd5d894aaab899c0", + "kind": "tokio::spawn", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": false, + "enclosingFunction": "infrastructure/external_mcp_supervisor.rs::ExternalMcpSupervisor::attach_io_handles", + "file": "infrastructure/external_mcp_supervisor.rs", + "id": "infrastructure/external_mcp_supervisor.rs::infrastructure/external_mcp_supervisor.rs::ExternalMcpSupervisor::attach_io_handles@fb002282e52f47bd", + "kind": "tokio::spawn", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": false, + "enclosingFunction": "infrastructure/secret_store.rs::MacosKeychainSecretStore::put_secret", + "file": "infrastructure/secret_store.rs", + "id": "infrastructure/secret_store.rs::infrastructure/secret_store.rs::MacosKeychainSecretStore::put_secret@9f07f610441c928a", + "kind": "spawn_blocking", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": false, + "enclosingFunction": "infrastructure/secret_store.rs::MacosKeychainSecretStore::get_secret", + "file": "infrastructure/secret_store.rs", + "id": "infrastructure/secret_store.rs::infrastructure/secret_store.rs::MacosKeychainSecretStore::get_secret@48e713a2ef009462", + "kind": "spawn_blocking", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": false, + "enclosingFunction": "infrastructure/secret_store.rs::MacosKeychainSecretStore::delete_secret", + "file": "infrastructure/secret_store.rs", + "id": "infrastructure/secret_store.rs::infrastructure/secret_store.rs::MacosKeychainSecretStore::delete_secret@2e567ab42a39b70f", + "kind": "spawn_blocking", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": false, + "enclosingFunction": "infrastructure/services/gh_cli_github_service.rs::GhCliGithubService::collect_output", + "file": "infrastructure/services/gh_cli_github_service.rs", + "id": "infrastructure/services/gh_cli_github_service.rs::infrastructure/services/gh_cli_github_service.rs::GhCliGithubService::collect_output@7e41f77d141c8976", + "kind": "tokio::spawn", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": false, + "enclosingFunction": "infrastructure/services/gh_cli_github_service.rs::GhCliGithubService::collect_output", + "file": "infrastructure/services/gh_cli_github_service.rs", + "id": "infrastructure/services/gh_cli_github_service.rs::infrastructure/services/gh_cli_github_service.rs::GhCliGithubService::collect_output@e654a8518f0b53b3", + "kind": "tokio::spawn", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": false, + "enclosingFunction": "infrastructure/services/gh_cli_github_service.rs::GhCliGithubService::run_git_process", + "file": "infrastructure/services/gh_cli_github_service.rs", + "id": "infrastructure/services/gh_cli_github_service.rs::infrastructure/services/gh_cli_github_service.rs::GhCliGithubService::run_git_process@cddeca28c8cdb1f", + "kind": "tokio::spawn", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": false, + "enclosingFunction": "infrastructure/services/gh_cli_github_service.rs::GhCliGithubService::delete_remote_branch", + "file": "infrastructure/services/gh_cli_github_service.rs", + "id": "infrastructure/services/gh_cli_github_service.rs::infrastructure/services/gh_cli_github_service.rs::GhCliGithubService::delete_remote_branch@cddeca28c8cdb1f", + "kind": "tokio::spawn", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": false, + "enclosingFunction": "infrastructure/sqlite/db_connection.rs::DbConnection::run", + "file": "infrastructure/sqlite/db_connection.rs", + "id": "infrastructure/sqlite/db_connection.rs::infrastructure/sqlite/db_connection.rs::DbConnection::run@227eb61b93933277", + "kind": "spawn_blocking", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": false, + "enclosingFunction": "infrastructure/sqlite/db_connection.rs::DbConnection::run_transaction", + "file": "infrastructure/sqlite/db_connection.rs", + "id": "infrastructure/sqlite/db_connection.rs::infrastructure/sqlite/db_connection.rs::DbConnection::run_transaction@6c38774c7797f505", + "kind": "spawn_blocking", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": true, + "enclosingFunction": "infrastructure/webhook_publisher.rs::WebhookPublisher::publish", + "file": "infrastructure/webhook_publisher.rs", + "id": "infrastructure/webhook_publisher.rs::infrastructure/webhook_publisher.rs::WebhookPublisher::publish@1b0527619cc918c9", + "kind": "tokio::spawn", + "readSurface": [], + "sinksReached": [ + "write_message" + ] + }, + { + "authorityBearing": false, + "enclosingFunction": "remote_server/capture.rs::TauriRegistrar::listen", + "file": "remote_server/capture.rs", + "id": "remote_server/capture.rs::remote_server/capture.rs::TauriRegistrar::listen@82976e4e1c578dbe", + "kind": "listen_any", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": false, + "enclosingFunction": "remote_server/invoke.rs:::::invoke_handler", + "file": "remote_server/invoke.rs", + "id": "remote_server/invoke.rs::remote_server/invoke.rs:::::invoke_handler@14f7bca1671b7209", + "kind": "tokio::spawn", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": false, + "enclosingFunction": "remote_server/mod.rs:::::start_listener_with_runtime", + "file": "remote_server/mod.rs", + "id": "remote_server/mod.rs::remote_server/mod.rs:::::start_listener_with_runtime@be5782b8beb913b0", + "kind": "async_runtime::spawn", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": false, + "enclosingFunction": "remote_server/retention.rs:::::spawn_pruner", + "file": "remote_server/retention.rs", + "id": "remote_server/retention.rs::remote_server/retention.rs:::::spawn_pruner@a6866fa8f9aee649", + "kind": "tokio::spawn", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": false, + "enclosingFunction": "remote_server/sequencer.rs::RemoteSequencer::start", + "file": "remote_server/sequencer.rs", + "id": "remote_server/sequencer.rs::remote_server/sequencer.rs::RemoteSequencer::start@d4b8ecd0cec0470a", + "kind": "tokio::spawn", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": false, + "enclosingFunction": "remote_server/sequencer.rs::RemoteSequencer::start", + "file": "remote_server/sequencer.rs", + "id": "remote_server/sequencer.rs::remote_server/sequencer.rs::RemoteSequencer::start@dbb327c115f4ccf", + "kind": "tokio::spawn", + "readSurface": [], + "sinksReached": [] + }, + { + "authorityBearing": false, + "enclosingFunction": "remote_server/transport_spike.rs:::::start_cors_probe_listener_with_address", + "file": "remote_server/transport_spike.rs", + "id": "remote_server/transport_spike.rs::remote_server/transport_spike.rs:::::start_cors_probe_listener_with_address@b1c8c2e4049b1957", + "kind": "async_runtime::spawn", + "readSurface": [], + "sinksReached": [] + } + ], + "conditional_capabilities": [ + { + "capability": "mutatesAgentConsumedContent", + "command": "update_task", + "condition": "conditional: title,description — discharged by update_task_authz" + } + ], + "coverage": { + "agentConsumedContent": "complete", + "detectorA": "complete", + "detectorB": "complete" + }, + "declared_memberships": [ + { + "command": "approve_permission_request", + "reason": "authorizes-live-tool-call" + }, + { + "command": "resolve_user_question", + "reason": "steering-question" + }, + { + "command": "send_remote_chat_message", + "reason": "steers-live-agent-turn" + }, + { + "command": "update_qa_settings", + "reason": "arms-auto-qa" + }, + { + "command": "set_active_project", + "reason": "arms-scheduler-quota" + }, + { + "command": "update_ideation_settings", + "reason": "arms-auto-plan-verification" + }, + { + "command": "update_agent_lane_settings", + "reason": "arms-agent-spawn-harness" + }, + { + "command": "update_mcp_server_override", + "reason": "configures-future-agent-tool-authority" + }, + { + "command": "clear_mcp_server_override", + "reason": "configures-future-agent-tool-authority" + }, + { + "command": "update_mcp_tool_override", + "reason": "configures-future-agent-tool-authority" + }, + { + "command": "clear_mcp_tool_override", + "reason": "configures-future-agent-tool-authority" + }, + { + "command": "update_ui_feature_flags", + "reason": "configures-future-agent-capability-gates" + }, + { + "command": "restart_automation", + "reason": "arms-automation-scheduler" + }, + { + "command": "retry_automation_plan_judge", + "reason": "arms-automation-scheduler" + }, + { + "command": "skip_automation_judge", + "reason": "arms-automation-scheduler" + }, + { + "command": "update_workspace_review_runtime_settings", + "reason": "configures-future-agent-runtime" + }, + { + "command": "upsert_custom_agent_model", + "reason": "configures-future-agent-runtime" + }, + { + "command": "request_remote_agent_conversation_message", + "reason": "seeds-agent-turn-for-idle-conversation" + }, + { + "command": "request_remote_agent_conversation_mode_switch", + "reason": "prepares-workspace-for-later-agent-run" + }, + { + "command": "resolve_remote_user_question", + "reason": "steering-question" + }, + { + "command": "request_remote_execution_resume", + "reason": "resumes-execution-through-host-dispatcher" + }, + { + "command": "request_remote_task_resume", + "reason": "resumes-task-through-host-dispatcher" + }, + { + "command": "request_remote_task_restart", + "reason": "restarts-task-through-host-dispatcher" + }, + { + "command": "request_remote_group_resume", + "reason": "resumes-task-group-through-host-dispatcher" + }, + { + "command": "request_remote_recovery_prompt_resolution", + "reason": "resolves-recovery-through-host-dispatcher" + } + ], + "facade_ops": [ + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "health_check", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::health::health_check" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "list_tasks", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::task_commands::query::list_tasks" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_task", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::task_commands::query::get_task" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "search_tasks", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::task_commands::query::search_tasks" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_valid_transitions", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::task_commands::query::get_valid_transitions" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "list_pending_permission_gates", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::permission_commands::list_pending_permission_gates" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "list_pending_question_gates", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::question_commands::list_pending_question_gates" + }, + { + "argumentSensitive": false, + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "resolve_remote_user_question", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::remote_question_commands::resolve_remote_user_question" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_archived_count", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::task_commands::query::get_archived_count" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_tasks_awaiting_review", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::task_commands::query::get_tasks_awaiting_review" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_session_task_history_availability", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::task_commands::query::get_session_task_history_availability" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_task_state_transitions", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::task_commands::query::get_task_state_transitions" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_task_dependency_graph", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::task_commands::query::get_task_dependency_graph" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_task_timeline_events", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::task_commands::query::get_task_timeline_events" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_task_agent_workspace", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::task_commands::query::get_task_agent_workspace" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_task_steps", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::task_step_commands::get_task_steps" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_step_progress", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::task_step_commands::get_step_progress" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_execution_settings", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::execution_commands::get_execution_settings" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_global_execution_settings", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::execution_commands::get_global_execution_settings" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_active_project", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::execution_commands::get_active_project" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_agent_conversation_stats", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::conversation_stats_commands::get_agent_conversation_stats" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_project_chat_usage_stats", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::conversation_stats_commands::get_project_chat_usage_stats" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_task_chat_usage_stats", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::conversation_stats_commands::get_task_chat_usage_stats" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_insights_chat_usage_stats", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::conversation_stats_commands::get_insights_chat_usage_stats" + }, + { + "argumentSensitive": true, + "capabilities": [], + "class": "operate", + "command": "update_task", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::task_commands::mutation::update_task" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "operate", + "command": "create_task", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::task_commands::mutation::create_task" + }, + { + "argumentSensitive": false, + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "pause_task", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::task_commands::mutation::pause_task" + }, + { + "argumentSensitive": false, + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "block_task", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::task_commands::mutation::block_task" + }, + { + "argumentSensitive": false, + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "stop_task", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::task_commands::mutation::stop_task" + }, + { + "argumentSensitive": false, + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "pause_tasks_in_group", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::task_commands::mutation::pause_tasks_in_group" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "operate", + "command": "pause_execution", + "pins": [], + "scopeConfined": true, + "target": "crate::commands::execution_commands::pause_execution" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "operate", + "command": "stop_execution", + "pins": [], + "scopeConfined": true, + "target": "crate::commands::execution_commands::stop_execution" + }, + { + "argumentSensitive": false, + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "cancel_tasks_in_group", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::task_commands::mutation::cancel_tasks_in_group" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "operate", + "command": "deny_permission_request", + "pins": [ + { + "field": "decision", + "param": "args", + "value": "deny" + } + ], + "scopeConfined": false, + "target": "crate::commands::permission_commands::resolve_permission_request" + }, + { + "argumentSensitive": false, + "capabilities": [ + "agentControl", + "mutatesAgentConsumedContent" + ], + "class": "agentControl", + "command": "move_task", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::task_commands::mutation::move_task" + }, + { + "argumentSensitive": false, + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "unblock_task", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::task_commands::mutation::unblock_task" + }, + { + "argumentSensitive": false, + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "answer_user_question", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::task_commands::mutation::answer_user_question" + }, + { + "argumentSensitive": false, + "capabilities": [ + "mutatesAgentConsumedContent" + ], + "class": "agentControl", + "command": "approve_task_for_review", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::review_commands::approve_task_for_review" + }, + { + "argumentSensitive": false, + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "reanalyze_project", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::project_commands::reanalyze_project" + }, + { + "argumentSensitive": false, + "capabilities": [ + "seedsSpawnTriggeringState" + ], + "class": "agentControl", + "command": "inject_task", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::task_commands::mutation::inject_task" + }, + { + "argumentSensitive": false, + "capabilities": [ + "seedsSpawnTriggeringState" + ], + "class": "agentControl", + "command": "resume_automation", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::automation_commands::resume_automation" + }, + { + "argumentSensitive": false, + "capabilities": [ + "seedsSpawnTriggeringState" + ], + "class": "agentControl", + "command": "finalize_automation", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::automation_commands::finalize_automation" + }, + { + "argumentSensitive": false, + "capabilities": [ + "mutatesAgentConsumedContent" + ], + "class": "agentControl", + "command": "send_remote_chat_message", + "pins": [ + { + "field": "role", + "param": "input", + "value": "user" + } + ], + "scopeConfined": false, + "target": "crate::commands::remote_chat_commands::send_remote_chat_message" + }, + { + "argumentSensitive": false, + "capabilities": [ + "mutatesAgentConsumedContent", + "seedsSpawnTriggeringState" + ], + "class": "agentControl", + "command": "request_remote_agent_conversation_start", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::remote_conversation_start_commands::request_remote_agent_conversation_start" + }, + { + "argumentSensitive": false, + "capabilities": [ + "mutatesAgentConsumedContent", + "seedsSpawnTriggeringState" + ], + "class": "agentControl", + "command": "request_remote_agent_conversation_message", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::remote_conversation_message_commands::request_remote_agent_conversation_message" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_remote_conversation_message_request", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::remote_conversation_message_commands::get_remote_conversation_message_request" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_remote_conversation_start_request", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::remote_conversation_start_commands::get_remote_conversation_start_request" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "operate", + "command": "request_remote_agent_stop", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::remote_agent_stop_commands::request_remote_agent_stop" + }, + { + "argumentSensitive": false, + "capabilities": [ + "seedsSpawnTriggeringState" + ], + "class": "agentControl", + "command": "request_remote_agent_conversation_mode_switch", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::remote_conversation_mode_switch_commands::request_remote_agent_conversation_mode_switch" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_remote_conversation_mode_switch_request", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::remote_conversation_mode_switch_commands::get_remote_conversation_mode_switch_request" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "agentControl", + "command": "set_remote_agent_conversation_muted", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::remote_conversation_lifecycle_commands::set_remote_agent_conversation_muted" + }, + { + "argumentSensitive": false, + "capabilities": [ + "mutatesAgentConsumedContent" + ], + "class": "agentControl", + "command": "switch_remote_agent_conversation_persona", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::remote_conversation_lifecycle_commands::switch_remote_agent_conversation_persona" + }, + { + "argumentSensitive": false, + "capabilities": [ + "seedsSpawnTriggeringState" + ], + "class": "agentControl", + "command": "request_remote_conversation_archive", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::remote_conversation_lifecycle_commands::request_remote_conversation_archive" + }, + { + "argumentSensitive": false, + "capabilities": [ + "mutatesAgentConsumedContent", + "seedsSpawnTriggeringState" + ], + "class": "agentControl", + "command": "request_remote_conversation_fork", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::remote_conversation_lifecycle_commands::request_remote_conversation_fork" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_remote_conversation_lifecycle_request", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::remote_conversation_lifecycle_commands::get_remote_conversation_lifecycle_request" + }, + { + "argumentSensitive": false, + "capabilities": [ + "seedsSpawnTriggeringState" + ], + "class": "agentControl", + "command": "request_remote_execution_resume", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::remote_resume_commands::request_remote_execution_resume" + }, + { + "argumentSensitive": false, + "capabilities": [ + "seedsSpawnTriggeringState" + ], + "class": "agentControl", + "command": "request_remote_task_resume", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::remote_resume_commands::request_remote_task_resume" + }, + { + "argumentSensitive": false, + "capabilities": [ + "seedsSpawnTriggeringState" + ], + "class": "agentControl", + "command": "request_remote_task_restart", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::remote_resume_commands::request_remote_task_restart" + }, + { + "argumentSensitive": false, + "capabilities": [ + "seedsSpawnTriggeringState" + ], + "class": "agentControl", + "command": "request_remote_group_resume", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::remote_resume_commands::request_remote_group_resume" + }, + { + "argumentSensitive": false, + "capabilities": [ + "seedsSpawnTriggeringState" + ], + "class": "agentControl", + "command": "request_remote_recovery_prompt_resolution", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::remote_resume_commands::request_remote_recovery_prompt_resolution" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_remote_execution_resume_request", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::remote_resume_commands::get_remote_execution_resume_request" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_remote_task_action_request", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::remote_resume_commands::get_remote_task_action_request" + }, + { + "argumentSensitive": false, + "capabilities": [ + "seedsSpawnTriggeringState" + ], + "class": "agentControl", + "command": "request_remote_plan_approval", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::remote_plan_commands::request_remote_plan_approval" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_remote_plan_approval_request", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::remote_plan_commands::get_remote_plan_approval_request" + }, + { + "argumentSensitive": false, + "capabilities": [ + "mutatesAgentConsumedContent" + ], + "class": "agentControl", + "command": "request_remote_plan_artifact_edit", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::remote_plan_commands::request_remote_plan_artifact_edit" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_remote_plan_edit_request", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::remote_plan_commands::get_remote_plan_edit_request" + }, + { + "argumentSensitive": false, + "capabilities": [ + "seedsSpawnTriggeringState" + ], + "class": "agentControl", + "command": "request_remote_ideation_finalize_decision", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::remote_ideation_commands::request_remote_ideation_finalize_decision" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_remote_ideation_finalize_request", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::remote_ideation_commands::get_remote_ideation_finalize_request" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_remote_agent_stop_request", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::remote_agent_stop_commands::get_remote_agent_stop_request" + }, + { + "argumentSensitive": false, + "capabilities": [ + "mutatesAgentConsumedContent" + ], + "class": "agentControl", + "command": "request_task_changes_for_review", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::review_commands::request_task_changes_for_review" + }, + { + "argumentSensitive": false, + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "re_review_task_from_escalated", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::review_commands::re_review_task_from_escalated" + }, + { + "argumentSensitive": false, + "capabilities": [ + "agentControl", + "seedsSpawnTriggeringState" + ], + "class": "agentControl", + "command": "update_review_settings", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::review_commands::update_review_settings" + }, + { + "argumentSensitive": false, + "capabilities": [ + "agentControl", + "mutatesAgentConsumedContent" + ], + "class": "agentControl", + "command": "reopen_issue", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::review_commands::reopen_issue" + }, + { + "argumentSensitive": false, + "capabilities": [ + "agentControl", + "mutatesAgentConsumedContent" + ], + "class": "agentControl", + "command": "verify_issue", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::review_commands::verify_issue" + }, + { + "argumentSensitive": false, + "capabilities": [ + "agentControl", + "mutatesAgentConsumedContent" + ], + "class": "agentControl", + "command": "mark_issue_in_progress", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::review_commands::mark_issue_in_progress" + }, + { + "argumentSensitive": false, + "capabilities": [ + "agentControl", + "mutatesAgentConsumedContent" + ], + "class": "agentControl", + "command": "mark_issue_addressed", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::review_commands::mark_issue_addressed" + }, + { + "argumentSensitive": false, + "capabilities": [ + "mutatesAgentConsumedContent" + ], + "class": "agentControl", + "command": "approve_review", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::review_commands::approve_review" + }, + { + "argumentSensitive": false, + "capabilities": [ + "mutatesAgentConsumedContent" + ], + "class": "agentControl", + "command": "reject_review", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::review_commands::reject_review" + }, + { + "argumentSensitive": false, + "capabilities": [ + "mutatesAgentConsumedContent" + ], + "class": "agentControl", + "command": "request_changes", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::review_commands::request_changes" + }, + { + "argumentSensitive": false, + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "retry_qa", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::qa_commands::retry_qa" + }, + { + "argumentSensitive": false, + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "update_qa_settings", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::qa_commands::update_qa_settings" + }, + { + "argumentSensitive": false, + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "clear_active_plan", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::plan_commands::clear_active_plan" + }, + { + "argumentSensitive": false, + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "seed_builtin_workflows", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::workflow_commands::seed_builtin_workflows" + }, + { + "argumentSensitive": false, + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "start_research", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::research_commands::start_research" + }, + { + "argumentSensitive": false, + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "set_active_project", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::execution_commands::set_active_project" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_remote_agent_conversation", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::remote_transcript_commands::get_remote_agent_conversation" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_remote_agent_conversation_workspace", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::remote_transcript_commands::get_remote_agent_conversation_workspace" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_remote_agent_conversation_messages_page", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::remote_transcript_commands::get_remote_agent_conversation_messages_page" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_remote_agent_conversation_timeline_page", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::remote_transcript_commands::get_remote_agent_conversation_timeline_page" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_agent_message_tool_call_detail", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::unified_chat_commands::get_agent_message_tool_call_detail" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_agent_timeline_item_tool_call_detail", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::unified_chat_commands::get_agent_timeline_item_tool_call_detail" + }, + { + "argumentSensitive": false, + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "update_remote_execution_settings", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::remote_execution_settings_commands::update_remote_execution_settings" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_remote_execution_status", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::remote_execution_status_commands::get_remote_execution_status" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_remote_mcp_catalog", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::remote_mcp_policy_commands::get_remote_mcp_catalog" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "list_remote_message_attachments", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::remote_attachment_commands::list_remote_message_attachments" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_remote_agent_conversation_workspace_change_summary", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::remote_diff_commands::get_remote_agent_conversation_workspace_change_summary" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_remote_agent_conversation_workspace_review", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::remote_diff_commands::get_remote_agent_conversation_workspace_review" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_remote_agent_conversation_workspace_file_diff", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::remote_diff_commands::get_remote_agent_conversation_workspace_file_diff" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_remote_agent_conversation_workspace_commit_file_diff", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::remote_diff_commands::get_remote_agent_conversation_workspace_commit_file_diff" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_remote_agent_conversation_workspace_cumulative_file_diff", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::remote_diff_commands::get_remote_agent_conversation_workspace_cumulative_file_diff" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_remote_agent_conversation_workspace_file_diff_page", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::remote_diff_commands::get_remote_agent_conversation_workspace_file_diff_page" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "list_remote_projects", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::remote_workspace_commands::list_remote_projects" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_remote_project", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::remote_workspace_commands::get_remote_project" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_remote_provider_readiness", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::remote_workspace_commands::get_remote_provider_readiness" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "list_remote_agent_providers", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::remote_workspace_commands::list_remote_agent_providers" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "list_remote_agent_conversations", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::remote_transcript_commands::list_remote_agent_conversations" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "list_remote_agent_conversations_page", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::remote_transcript_commands::list_remote_agent_conversations_page" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "list_remote_queued_agent_messages", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::remote_queue_commands::list_remote_queued_agent_messages" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "operate", + "command": "cancel_remote_queued_agent_message", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::remote_queue_commands::cancel_remote_queued_agent_message" + }, + { + "argumentSensitive": false, + "capabilities": [ + "seedsSpawnTriggeringState" + ], + "class": "agentControl", + "command": "request_remote_queued_message_send", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::remote_queue_commands::request_remote_queued_message_send" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_remote_queued_message_send_request", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::remote_queue_commands::get_remote_queued_message_send_request" + }, + { + "argumentSensitive": false, + "capabilities": [ + "seedsSpawnTriggeringState" + ], + "class": "agentControl", + "command": "request_remote_automation_run", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::remote_automation_commands::request_remote_automation_run" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_remote_automation_run_request", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::remote_automation_commands::get_remote_automation_run_request" + }, + { + "argumentSensitive": false, + "capabilities": [ + "mutatesAgentConsumedContent", + "seedsSpawnTriggeringState" + ], + "class": "agentControl", + "command": "request_remote_automation_draft", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::remote_automation_commands::request_remote_automation_draft" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_remote_automation_draft_request", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::remote_automation_commands::get_remote_automation_draft_request" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "list_remote_agent_sidebar_conversations", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::remote_transcript_commands::list_remote_agent_sidebar_conversations" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_agent_conversation_summary", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::unified_chat_commands::get_agent_conversation_summary" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_agent_conversation_runtime_index", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::unified_chat_commands::get_agent_conversation_runtime_index" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_agent_run_attribution", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::unified_chat_commands::get_agent_run_attribution" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_agent_run_attributions", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::unified_chat_commands::get_agent_run_attributions" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "list_agent_conversation_workspace_publication_events", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::unified_chat_commands::list_agent_conversation_workspace_publication_events" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_bulk_workspace_publication_states", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::agent_sidebar_commands::get_bulk_workspace_publication_states" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "list_agent_models", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::agent_model_commands::list_agent_models" + }, + { + "argumentSensitive": false, + "capabilities": [ + "mutatesAgentConsumedContent" + ], + "class": "agentControl", + "command": "create_task_step", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::task_step_commands::create_task_step" + }, + { + "argumentSensitive": false, + "capabilities": [ + "mutatesAgentConsumedContent" + ], + "class": "agentControl", + "command": "update_task_step", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::task_step_commands::update_task_step" + }, + { + "argumentSensitive": false, + "capabilities": [ + "agentControl", + "mutatesAgentConsumedContent" + ], + "class": "agentControl", + "command": "start_step", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::task_step_commands::start_step" + }, + { + "argumentSensitive": false, + "capabilities": [ + "agentControl", + "mutatesAgentConsumedContent" + ], + "class": "agentControl", + "command": "complete_step", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::task_step_commands::complete_step" + }, + { + "argumentSensitive": false, + "capabilities": [ + "agentControl", + "mutatesAgentConsumedContent" + ], + "class": "agentControl", + "command": "skip_step", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::task_step_commands::skip_step" + }, + { + "argumentSensitive": false, + "capabilities": [ + "agentControl", + "mutatesAgentConsumedContent" + ], + "class": "agentControl", + "command": "fail_step", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::task_step_commands::fail_step" + }, + { + "argumentSensitive": false, + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "reorder_task_steps", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::task_step_commands::reorder_task_steps" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "list_conversation_folder_references", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::conversation_folder_reference_commands::list_conversation_folder_references" + }, + { + "argumentSensitive": false, + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "remove_conversation_folder_reference", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::conversation_folder_reference_commands::remove_conversation_folder_reference" + }, + { + "argumentSensitive": false, + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "abort_seeded_agent_conversation", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::unified_chat_commands::abort_seeded_agent_conversation" + }, + { + "argumentSensitive": false, + "capabilities": [ + "mutatesAgentConsumedContent" + ], + "class": "agentControl", + "command": "create_artifact", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::artifact_commands::create_artifact" + }, + { + "argumentSensitive": false, + "capabilities": [ + "mutatesAgentConsumedContent" + ], + "class": "agentControl", + "command": "update_artifact", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::artifact_commands::update_artifact" + }, + { + "argumentSensitive": false, + "capabilities": [ + "mutatesAgentConsumedContent" + ], + "class": "agentControl", + "command": "add_artifact_relation", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::artifact_commands::add_artifact_relation" + }, + { + "argumentSensitive": false, + "capabilities": [ + "mutatesAgentConsumedContent" + ], + "class": "agentControl", + "command": "update_task_proposal", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::ideation_commands::update_task_proposal" + }, + { + "argumentSensitive": false, + "capabilities": [ + "mutatesAgentConsumedContent" + ], + "class": "agentControl", + "command": "archive_task_proposal", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::ideation_commands::archive_task_proposal" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "list_ticketing_providers", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::ticketing_commands::list_ticketing_providers" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "list_ticketing_containers", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::ticketing_commands::list_ticketing_containers" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "agentControl", + "command": "list_ticketing_columns", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::ticketing_commands::list_ticketing_columns" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "list_ticketing_status_catalog", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::ticketing_commands::list_ticketing_status_catalog" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "agentControl", + "command": "refresh_ticketing_status_catalog", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::ticketing_commands::refresh_ticketing_status_catalog" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "agentControl", + "command": "update_ticketing_status_presentation", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::ticketing_commands::update_ticketing_status_presentation" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "list_tickets", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::ticketing_commands::list_tickets" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "list_ticket_filter_options", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::ticketing_commands::list_ticket_filter_options" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_ticket_detail", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::ticketing_commands::get_ticket_detail" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "list_ticket_transitions", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::ticketing_commands::list_ticket_transitions" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_ticket_associations", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::ticketing_commands::get_ticket_associations" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_conversation_ticket", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::ticketing_commands::get_conversation_ticket" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "refresh_tickets", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::ticketing_commands::refresh_tickets" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "agentControl", + "command": "transition_ticket_status", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::ticketing_commands::transition_ticket_status" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "agentControl", + "command": "assign_ticket", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::ticketing_commands::assign_ticket" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "agentControl", + "command": "clear_ticket_assignee", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::ticketing_commands::clear_ticket_assignee" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "agentControl", + "command": "add_ticket_comment", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::ticketing_commands::add_ticket_comment" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "agentControl", + "command": "set_ticket_labels", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::ticketing_commands::set_ticket_labels" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "list_ticket_labels", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::ticketing_commands::list_ticket_labels" + }, + { + "argumentSensitive": false, + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "approve_permission_request", + "pins": [ + { + "field": "decision", + "param": "args", + "value": "allow" + } + ], + "scopeConfined": false, + "target": "crate::commands::permission_commands::resolve_permission_request" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_pending_reviews", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::review_commands::get_pending_reviews" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_review_by_id", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::review_commands::get_review_by_id" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_reviews_by_task_id", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::review_commands::get_reviews_by_task_id" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_task_state_history", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::review_commands::get_task_state_history" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_fix_task_attempts", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::review_commands::get_fix_task_attempts" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_task_issues", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::review_commands::get_task_issues" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_issue_progress", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::review_commands::get_issue_progress" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_review_settings", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::review_commands::get_review_settings" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_qa_settings", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::qa_commands::get_qa_settings" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_task_qa", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::qa_commands::get_task_qa" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_qa_results", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::qa_commands::get_qa_results" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_merge_pipeline", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::merge_pipeline_commands::get_merge_pipeline" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_merge_progress", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::merge_pipeline_commands::get_merge_progress" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_merge_phase_list", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::merge_pipeline_commands::get_merge_phase_list" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_active_plan", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::plan_commands::get_active_plan" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_active_execution_plan", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::plan_commands::get_active_execution_plan" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "list_plan_selector_candidates", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::plan_commands::list_plan_selector_candidates" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_methodologies", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::methodology_commands::get_methodologies" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_active_methodology", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::methodology_commands::get_active_methodology" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_workflows", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::workflow_commands::get_workflows" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_workflow", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::workflow_commands::get_workflow" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_builtin_workflows", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::workflow_commands::get_builtin_workflows" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_active_workflow_columns", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::workflow_commands::get_active_workflow_columns" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "search_agent_composer_plan_references", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::agent_composer_commands::search_agent_composer_plan_references" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_ideation_session", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::ideation_commands::get_ideation_session" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_ideation_session_with_data", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::ideation_commands::get_ideation_session_with_data" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_ideation_agent_workspace", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::ideation_commands::get_ideation_agent_workspace" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "list_ideation_sessions", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::ideation_commands::list_ideation_sessions" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_session_group_counts", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::ideation_commands::get_session_group_counts" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "list_sessions_by_group", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::ideation_commands::list_sessions_by_group" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_child_sessions", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::ideation_commands::get_child_sessions" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_latest_child_session_id", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::ideation_commands::get_latest_child_session_id" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_task_proposal", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::ideation_commands::get_task_proposal" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "list_session_proposals", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::ideation_commands::list_session_proposals" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_proposal_dependencies", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::ideation_commands::get_proposal_dependencies" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_proposal_dependents", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::ideation_commands::get_proposal_dependents" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_task_blockers", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::ideation_commands::get_task_blockers" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_blocked_tasks", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::ideation_commands::get_blocked_tasks" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_tasks_disable_impact", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::ideation_commands::get_tasks_disable_impact" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_ideation_settings", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::ideation_commands::get_ideation_settings" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_ideation_effort_settings", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::ideation_commands::get_ideation_effort_settings" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_ideation_model_settings", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::ideation_commands::get_ideation_model_settings" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_agent_lane_settings", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::ideation_commands::get_agent_lane_settings" + }, + { + "argumentSensitive": false, + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "update_ideation_session_title", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::ideation_commands::update_ideation_session_title" + }, + { + "argumentSensitive": false, + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "reorder_proposals", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::ideation_commands::reorder_proposals" + }, + { + "argumentSensitive": false, + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "assess_proposal_priority", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::ideation_commands::assess_proposal_priority" + }, + { + "argumentSensitive": false, + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "assess_all_priorities", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::ideation_commands::assess_all_priorities" + }, + { + "argumentSensitive": false, + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "remove_proposal_dependency", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::ideation_commands::remove_proposal_dependency" + }, + { + "argumentSensitive": false, + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "update_ideation_settings", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::ideation_commands::update_ideation_settings" + }, + { + "argumentSensitive": false, + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "update_ideation_effort_settings", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::ideation_commands::update_ideation_effort_settings" + }, + { + "argumentSensitive": false, + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "update_ideation_model_settings", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::ideation_commands::update_ideation_model_settings" + }, + { + "argumentSensitive": false, + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "update_agent_lane_settings", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::ideation_commands::update_agent_lane_settings" + }, + { + "argumentSensitive": false, + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "create_workflow", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::workflow_commands::create_workflow" + }, + { + "argumentSensitive": false, + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "update_workflow", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::workflow_commands::update_workflow" + }, + { + "argumentSensitive": false, + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "set_default_workflow", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::workflow_commands::set_default_workflow" + }, + { + "argumentSensitive": false, + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "activate_methodology", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::methodology_commands::activate_methodology" + }, + { + "argumentSensitive": false, + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "deactivate_methodology", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::methodology_commands::deactivate_methodology" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "list_task_activity_events", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::activity_commands::list_task_activity_events" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "list_session_activity_events", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::activity_commands::list_session_activity_events" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "list_all_activity_events", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::activity_commands::list_all_activity_events" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "count_task_activity_events", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::activity_commands::count_task_activity_events" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "count_session_activity_events", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::activity_commands::count_session_activity_events" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_project_stats", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::metrics_commands::get_project_stats" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_insights_stats", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::metrics_commands::get_insights_stats" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_project_trends", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::metrics_commands::get_project_trends" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_insights_trends", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::metrics_commands::get_insights_trends" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_project_pr_insights", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::metrics_commands::get_project_pr_insights" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_insights_pr_insights", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::metrics_commands::get_insights_pr_insights" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_metrics_config", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::metrics_commands::get_metrics_config" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_task_metrics", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::metrics_commands::get_task_metrics" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_research_presets", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::research_commands::get_research_presets" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_research_process", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::research_commands::get_research_process" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_research_processes", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::research_commands::get_research_processes" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "list_automations", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::automation_commands::list_automations" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_automation", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::automation_commands::get_automation" + }, + { + "argumentSensitive": false, + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "save_metrics_config", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::metrics_commands::save_metrics_config" + }, + { + "argumentSensitive": false, + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "pause_research", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::research_commands::pause_research" + }, + { + "argumentSensitive": false, + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "resume_research", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::research_commands::resume_research" + }, + { + "argumentSensitive": false, + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "stop_research", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::research_commands::stop_research" + }, + { + "argumentSensitive": false, + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "pause_automation", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::automation_commands::pause_automation" + }, + { + "argumentSensitive": false, + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "stop_automation", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::automation_commands::stop_automation" + }, + { + "argumentSensitive": false, + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "cancel_automation_run", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::automation_commands::cancel_automation_run" + }, + { + "argumentSensitive": false, + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "update_automation_settings", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::automation_commands::update_automation_settings" + }, + { + "argumentSensitive": false, + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "update_automation_config", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::automation_commands::update_automation_config" + }, + { + "argumentSensitive": false, + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "restart_automation", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::automation_commands::restart_automation" + }, + { + "argumentSensitive": false, + "capabilities": [ + "seedsSpawnTriggeringState" + ], + "class": "agentControl", + "command": "resume_automation_run", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::automation_commands::resume_automation_run" + }, + { + "argumentSensitive": false, + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "retry_automation_plan_judge", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::automation_commands::retry_automation_plan_judge" + }, + { + "argumentSensitive": false, + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "skip_automation_judge", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::automation_commands::skip_automation_judge" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_artifacts", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::artifact_commands::get_artifacts" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_artifact", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::artifact_commands::get_artifact" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_artifact_at_version", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::artifact_commands::get_artifact_at_version" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_artifacts_by_bucket", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::artifact_commands::get_artifacts_by_bucket" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_artifacts_by_task", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::artifact_commands::get_artifacts_by_task" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_artifact_version_history", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::artifact_commands::get_artifact_version_history" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_buckets", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::artifact_commands::get_buckets" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_system_buckets", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::artifact_commands::get_system_buckets" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_artifact_relations", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::artifact_commands::get_artifact_relations" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_task_context", + "pins": [], + "scopeConfined": false, + "target": "crate::remote_server::task_projection::get_task_context" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_artifact_full", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::task_context_commands::get_artifact_full" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_artifact_version", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::task_context_commands::get_artifact_version" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_related_artifacts", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::task_context_commands::get_related_artifacts" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "search_artifacts", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::task_context_commands::search_artifacts" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_notification_settings", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::notification_commands::get_notification_settings" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_unread_notification_count", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::notification_commands::get_unread_notification_count" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "list_attention_items", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::notification_commands::list_attention_items" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "list_notifications", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::notification_commands::list_notifications" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_current_release_notes", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::release_notes_commands::get_current_release_notes" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_release_notes_for_version", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::release_notes_commands::get_release_notes_for_version" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_last_seen_release_notes_version", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::release_notes_commands::get_last_seen_release_notes_version" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "list_release_notes_versions", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::release_notes_commands::list_release_notes_versions" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "list_personas", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::persona_commands::list_personas" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_persona", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::persona_commands::get_persona" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "list_persona_usage", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::persona_commands::list_persona_usage" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "preview_persona_overlay", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::persona_commands::preview_persona_overlay" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_ui_feature_flags", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::ui_commands::get_ui_feature_flags" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_update_channel", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::update_channel_commands::get_update_channel" + }, + { + "argumentSensitive": false, + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "set_update_channel", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::update_channel_commands::set_update_channel" + }, + { + "argumentSensitive": false, + "capabilities": [ + "mutatesAgentConsumedContent" + ], + "class": "agentControl", + "command": "archive_artifact", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::artifact_commands::archive_artifact" + }, + { + "argumentSensitive": false, + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "create_bucket", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::artifact_commands::create_bucket" + }, + { + "argumentSensitive": false, + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "mark_notification_read", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::notification_commands::mark_notification_read" + }, + { + "argumentSensitive": false, + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "mark_all_notifications_read", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::notification_commands::mark_all_notifications_read" + }, + { + "argumentSensitive": false, + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "set_dock_badge_count", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::notification_commands::set_dock_badge_count" + }, + { + "argumentSensitive": false, + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "update_notification_settings", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::notification_commands::update_notification_settings" + }, + { + "argumentSensitive": false, + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "mark_release_notes_seen", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::release_notes_commands::mark_release_notes_seen" + }, + { + "argumentSensitive": false, + "capabilities": [ + "mutatesAgentConsumedContent" + ], + "class": "agentControl", + "command": "create_persona_draft", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::persona_commands::create_persona_draft" + }, + { + "argumentSensitive": false, + "capabilities": [ + "mutatesAgentConsumedContent" + ], + "class": "agentControl", + "command": "update_persona_draft", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::persona_commands::update_persona_draft" + }, + { + "argumentSensitive": false, + "capabilities": [ + "mutatesAgentConsumedContent" + ], + "class": "agentControl", + "command": "update_persona", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::persona_commands::update_persona" + }, + { + "argumentSensitive": false, + "capabilities": [ + "mutatesAgentConsumedContent" + ], + "class": "agentControl", + "command": "approve_persona", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::persona_commands::approve_persona" + }, + { + "argumentSensitive": false, + "capabilities": [ + "mutatesAgentConsumedContent" + ], + "class": "agentControl", + "command": "approve_persona_as_new", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::persona_commands::approve_persona_as_new" + }, + { + "argumentSensitive": false, + "capabilities": [ + "mutatesAgentConsumedContent" + ], + "class": "agentControl", + "command": "reseed_persona_draft", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::persona_commands::reseed_persona_draft" + }, + { + "argumentSensitive": false, + "capabilities": [ + "mutatesAgentConsumedContent" + ], + "class": "agentControl", + "command": "archive_persona", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::persona_commands::archive_persona" + }, + { + "argumentSensitive": false, + "capabilities": [ + "mutatesAgentConsumedContent" + ], + "class": "agentControl", + "command": "unarchive_persona", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::persona_commands::unarchive_persona" + }, + { + "argumentSensitive": false, + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "update_ui_feature_flags", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::ui_commands::update_ui_feature_flags" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_start_composer_role_default", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::manual_role_default_commands::get_start_composer_role_default" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_agent_conversation_role_default", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::manual_role_default_commands::get_agent_conversation_role_default" + }, + { + "argumentSensitive": false, + "capabilities": [], + "class": "read", + "command": "get_workspace_review_runtime_settings", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::workspace_review_settings_commands::get_workspace_review_runtime_settings" + }, + { + "argumentSensitive": false, + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "archive_task", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::task_commands::mutation::archive_task" + }, + { + "argumentSensitive": false, + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "restore_task", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::task_commands::mutation::restore_task" + }, + { + "argumentSensitive": false, + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "create_agent_conversation", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::unified_chat_commands::create_agent_conversation" + }, + { + "argumentSensitive": false, + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "restore_agent_conversation", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::unified_chat_commands::restore_agent_conversation" + }, + { + "argumentSensitive": false, + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "update_agent_conversation_title", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::unified_chat_commands::update_agent_conversation_title" + }, + { + "argumentSensitive": false, + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "update_workspace_review_runtime_settings", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::workspace_review_settings_commands::update_workspace_review_runtime_settings" + }, + { + "argumentSensitive": false, + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "upsert_custom_agent_model", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::agent_model_commands::upsert_custom_agent_model" + }, + { + "argumentSensitive": false, + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "approve_fix_task", + "pins": [], + "scopeConfined": false, + "target": "crate::commands::review_commands::approve_fix_task" + } + ], + "ledger": [ + { + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "greet", + "module": "root", + "reason": "conservative-module-default: may steer or arm autonomous work", + "registered": false, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "health_check", + "module": "health", + "reason": "pure health read", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "hostManagement" + ], + "class": "elevated", + "command": "get_startup_status", + "module": "startup_commands", + "reason": "startup and log-management authority", + "registered": false, + "v1Resolution": "v1-deferred" + }, + { + "capabilities": [ + "hostManagement" + ], + "class": "elevated", + "command": "get_startup_diagnostics", + "module": "startup_commands", + "reason": "startup and log-management authority", + "registered": false, + "v1Resolution": "v1-deferred" + }, + { + "capabilities": [ + "hostManagement" + ], + "class": "elevated", + "command": "retry_startup", + "module": "startup_commands", + "reason": "startup and log-management authority", + "registered": false, + "v1Resolution": "v1-deferred" + }, + { + "capabilities": [ + "hostManagement" + ], + "class": "elevated", + "command": "open_startup_logs", + "module": "startup_commands", + "reason": "startup and log-management authority", + "registered": false, + "v1Resolution": "v1-deferred" + }, + { + "capabilities": [ + "hostManagement" + ], + "class": "elevated", + "command": "report_startup_frontend_milestone", + "module": "startup_commands", + "reason": "startup and log-management authority", + "registered": false, + "v1Resolution": "v1-deferred" + }, + { + "capabilities": [], + "class": "read", + "command": "list_attention_items", + "module": "notification_commands", + "reason": "batch-13 audit: AttentionService::list_attention_items, documented and confirmed fail-closed — an unloadable authoritative source errors rather than shipping a partial list as complete", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "set_dock_badge_count", + "module": "notification_commands", + "reason": "batch-13 audit: mirrors a frontend-owned count onto the macOS Dock tile through run_on_main_thread, whose error propagates. Persists nothing and reads no domain state; classified AgentControl because it is a host-visible side effect, and NOT HostManagement — every HOST row in this ledger sits at a class v1 does not grant, and a cosmetic badge is not that authority", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_notification_settings", + "module": "notification_commands", + "reason": "batch-13 audit: one notification_settings_repo.get_settings, error mapped", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "update_notification_settings", + "module": "notification_commands", + "reason": "batch-13 audit: applies only the Some(..) fields to NotificationSettings and writes one update_settings; both repository calls propagate. Detector (b) FIRES on it and the row deliberately does NOT claim SeedsSpawnTriggeringState: the flag is a MARKER collision, measured as entry=workspace-auto-review with write_markers_matched=[\"update_settings\"] and armed_matched=[\"require_workspace_review\"]. The notification settings repository method merely SHARES a bare name with the workspace-review write marker. Claiming the tag would have PASSED seeds_spawn_triggering_state_tags_track_detector_b_evidence, which only enforces tag -> evidence, while being false", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "list_notifications", + "module": "notification_commands", + "reason": "batch-13 audit: one cursor-paginated notification_repo.list; the `limit.unwrap_or(50)` defaults an absent ARGUMENT, never a swallowed Err", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "mark_notification_read", + "module": "notification_commands", + "reason": "batch-13 audit: notification_service().mark_read, error propagated with `?`", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "mark_all_notifications_read", + "module": "notification_commands", + "reason": "batch-13 audit: notification_service().mark_all_read over an optional project scope, error propagated with `?`", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_unread_notification_count", + "module": "notification_commands", + "reason": "batch-13 audit: one notification_repo.unread_count, error mapped", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "debug_send_test_notification", + "module": "notification_commands", + "reason": "conservative-module-default: may steer or arm autonomous work", + "registered": false, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "hostManagement" + ], + "class": "elevated", + "command": "preview_remote_environment", + "module": "remote_environment_commands", + "reason": "remote environment authority", + "registered": false, + "v1Resolution": "v1-deferred" + }, + { + "capabilities": [ + "hostManagement" + ], + "class": "elevated", + "command": "pair_remote_environment", + "module": "remote_environment_commands", + "reason": "remote environment authority", + "registered": false, + "v1Resolution": "v1-deferred" + }, + { + "capabilities": [ + "hostManagement" + ], + "class": "elevated", + "command": "list_remote_environments", + "module": "remote_environment_commands", + "reason": "remote environment authority", + "registered": false, + "v1Resolution": "v1-deferred" + }, + { + "capabilities": [ + "hostManagement" + ], + "class": "elevated", + "command": "remove_remote_environment", + "module": "remote_environment_commands", + "reason": "remote environment authority", + "registered": false, + "v1Resolution": "v1-deferred" + }, + { + "capabilities": [ + "hostManagement" + ], + "class": "elevated", + "command": "get_active_environment", + "module": "remote_environment_commands", + "reason": "remote environment authority", + "registered": false, + "v1Resolution": "v1-deferred" + }, + { + "capabilities": [ + "hostManagement" + ], + "class": "elevated", + "command": "set_active_environment", + "module": "remote_environment_commands", + "reason": "remote environment authority", + "registered": false, + "v1Resolution": "v1-deferred" + }, + { + "capabilities": [ + "hostManagement" + ], + "class": "elevated", + "command": "remote_connect", + "module": "remote_environment_commands", + "reason": "remote environment authority", + "registered": false, + "v1Resolution": "v1-deferred" + }, + { + "capabilities": [ + "hostManagement" + ], + "class": "elevated", + "command": "remote_disconnect", + "module": "remote_environment_commands", + "reason": "remote environment authority", + "registered": false, + "v1Resolution": "v1-deferred" + }, + { + "capabilities": [ + "hostManagement" + ], + "class": "elevated", + "command": "remote_stream_send", + "module": "remote_environment_commands", + "reason": "remote environment authority", + "registered": false, + "v1Resolution": "v1-deferred" + }, + { + "capabilities": [ + "hostManagement" + ], + "class": "elevated", + "command": "remote_invoke", + "module": "remote_environment_commands", + "reason": "remote environment authority", + "registered": false, + "v1Resolution": "v1-deferred" + }, + { + "capabilities": [ + "hostManagement" + ], + "class": "elevated", + "command": "remote_fetch", + "module": "remote_environment_commands", + "reason": "remote environment authority", + "registered": false, + "v1Resolution": "v1-deferred" + }, + { + "capabilities": [ + "hostManagement" + ], + "class": "elevated", + "command": "start_remote_listener", + "module": "remote_host_commands", + "reason": "remote listener authority", + "registered": false, + "v1Resolution": "v1-deferred" + }, + { + "capabilities": [ + "hostManagement" + ], + "class": "elevated", + "command": "stop_remote_listener", + "module": "remote_host_commands", + "reason": "remote listener authority", + "registered": false, + "v1Resolution": "v1-deferred" + }, + { + "capabilities": [ + "hostManagement" + ], + "class": "elevated", + "command": "set_remote_exposure_mode", + "module": "remote_host_commands", + "reason": "remote listener authority", + "registered": false, + "v1Resolution": "v1-deferred" + }, + { + "capabilities": [ + "hostManagement" + ], + "class": "elevated", + "command": "get_remote_listener_status", + "module": "remote_host_commands", + "reason": "remote listener authority", + "registered": false, + "v1Resolution": "v1-deferred" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "list_remote_advertised_endpoints", + "module": "remote_host_commands", + "reason": "resolves the Tailscale CLI to enumerate advertised endpoints", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "hostManagement" + ], + "class": "elevated", + "command": "generate_remote_pairing_code", + "module": "remote_device_commands", + "reason": "remote host/device authority", + "registered": false, + "v1Resolution": "v1-deferred" + }, + { + "capabilities": [ + "hostManagement" + ], + "class": "elevated", + "command": "list_remote_pairing_codes", + "module": "remote_device_commands", + "reason": "remote host/device authority", + "registered": false, + "v1Resolution": "v1-deferred" + }, + { + "capabilities": [], + "class": "read", + "command": "list_remote_audit_entries", + "module": "remote_device_commands", + "reason": "remote audit read; AppHandle-ineligible until PR 3.1", + "registered": false, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "hostManagement" + ], + "class": "elevated", + "command": "revoke_remote_pairing_code", + "module": "remote_device_commands", + "reason": "remote host/device authority", + "registered": false, + "v1Resolution": "v1-deferred" + }, + { + "capabilities": [ + "hostManagement" + ], + "class": "elevated", + "command": "list_remote_devices", + "module": "remote_device_commands", + "reason": "remote host/device authority", + "registered": false, + "v1Resolution": "v1-deferred" + }, + { + "capabilities": [ + "hostManagement" + ], + "class": "elevated", + "command": "set_remote_device_agent_control", + "module": "remote_device_commands", + "reason": "remote host/device authority", + "registered": false, + "v1Resolution": "v1-deferred" + }, + { + "capabilities": [ + "hostManagement" + ], + "class": "elevated", + "command": "revoke_remote_device", + "module": "remote_device_commands", + "reason": "remote host/device authority", + "registered": false, + "v1Resolution": "v1-deferred" + }, + { + "capabilities": [ + "hostManagement" + ], + "class": "elevated", + "command": "list_remote_sessions", + "module": "remote_device_commands", + "reason": "remote host/device authority", + "registered": false, + "v1Resolution": "v1-deferred" + }, + { + "capabilities": [ + "hostManagement" + ], + "class": "elevated", + "command": "disconnect_remote_session", + "module": "remote_device_commands", + "reason": "remote host/device authority", + "registered": false, + "v1Resolution": "v1-deferred" + }, + { + "capabilities": [ + "hostManagement" + ], + "class": "elevated", + "command": "debug_start_remote_transport_cors_probe", + "module": "remote_transport_spike_commands", + "reason": "debug remote transport authority", + "registered": false, + "v1Resolution": "v1-deferred" + }, + { + "capabilities": [ + "hostManagement" + ], + "class": "elevated", + "command": "debug_stop_remote_transport_cors_probe", + "module": "remote_transport_spike_commands", + "reason": "debug remote transport authority", + "registered": false, + "v1Resolution": "v1-deferred" + }, + { + "capabilities": [ + "hostManagement" + ], + "class": "elevated", + "command": "debug_run_desktop_proxy_stub", + "module": "remote_transport_spike_commands", + "reason": "debug remote transport authority", + "registered": false, + "v1Resolution": "v1-deferred" + }, + { + "capabilities": [], + "class": "read", + "command": "get_current_release_notes", + "module": "release_notes_commands", + "reason": "batch-13 audit: reads the packaged version and resolves the notes file through sanitize_release_notes_version, which rejects `..`, separators and non-ASCII. An unreadable candidate yields source: Missing — an EXPLICIT tri-state the caller can see, not a fabricated body", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_last_seen_release_notes_version", + "module": "release_notes_commands", + "reason": "batch-13 audit: one app_state_repo.get projecting a single field, error mapped", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "mark_release_notes_seen", + "module": "release_notes_commands", + "reason": "batch-13 audit: sanitizes the version through the same containment check the read half uses, then one app_state_repo.set_last_seen_release_notes_version", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "list_release_notes_versions", + "module": "release_notes_commands", + "reason": "batch-13 audit: FIXED THEN REGISTERED. The reader was `std::fs::read_dir(path).ok()`, so a permissions or I/O failure produced an empty version list indistinguishable from a genuine empty directory. collect_versions_from_dirs now returns io::Result: an ABSENT root is still skipped (one of the two candidates is always absent by construction) while any other error propagates", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_release_notes_for_version", + "module": "release_notes_commands", + "reason": "batch-13 audit: same path as get_current_release_notes with a caller-supplied version, through the same containment check", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "touchesCredentials" + ], + "class": "denied", + "command": "get_atlassian_integration_settings", + "module": "atlassian_commands", + "reason": "integration credential surface", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "touchesCredentials" + ], + "class": "denied", + "command": "save_atlassian_integration_settings", + "module": "atlassian_commands", + "reason": "integration credential surface", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "touchesCredentials" + ], + "class": "denied", + "command": "build_atlassian_oauth_authorization_url", + "module": "atlassian_commands", + "reason": "integration credential surface", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "touchesCredentials" + ], + "class": "denied", + "command": "start_atlassian_oauth_local_callback", + "module": "atlassian_commands", + "reason": "integration credential surface", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "touchesCredentials" + ], + "class": "denied", + "command": "complete_atlassian_oauth_local_callback", + "module": "atlassian_commands", + "reason": "integration credential surface", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "touchesCredentials" + ], + "class": "denied", + "command": "exchange_atlassian_oauth_code", + "module": "atlassian_commands", + "reason": "integration credential surface", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "touchesCredentials" + ], + "class": "denied", + "command": "validate_atlassian_integration", + "module": "atlassian_commands", + "reason": "integration credential surface", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "touchesCredentials" + ], + "class": "denied", + "command": "disconnect_atlassian_integration", + "module": "atlassian_commands", + "reason": "integration credential surface", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "touchesCredentials" + ], + "class": "denied", + "command": "search_atlassian_resources", + "module": "atlassian_commands", + "reason": "integration credential surface", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "touchesCredentials" + ], + "class": "denied", + "command": "resolve_atlassian_resource_urls", + "module": "atlassian_commands", + "reason": "integration credential surface", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "touchesCredentials" + ], + "class": "denied", + "command": "get_agent_conversation_jira_issue", + "module": "atlassian_commands", + "reason": "integration credential surface", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "touchesCredentials" + ], + "class": "denied", + "command": "assign_agent_conversation_jira_issue", + "module": "atlassian_commands", + "reason": "integration credential surface", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "touchesCredentials" + ], + "class": "denied", + "command": "refresh_agent_conversation_jira_issue", + "module": "atlassian_commands", + "reason": "integration credential surface", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "touchesCredentials" + ], + "class": "denied", + "command": "assign_agent_conversation_jira_issue_to_me", + "module": "atlassian_commands", + "reason": "integration credential surface", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "touchesCredentials" + ], + "class": "denied", + "command": "clear_agent_conversation_jira_issue", + "module": "atlassian_commands", + "reason": "integration credential surface", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [], + "class": "read", + "command": "list_automations", + "module": "automation_commands", + "reason": "batch-12 audit: `AutomationService::list_automations`; the service is constructed from AppState Arc clones, reaches no launch resolver, and this path performs no write", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_automation", + "module": "automation_commands", + "reason": "batch-12 audit: detail read plus `automation_detail_response_for_state`, whose usage, pipeline and run hydrators all propagate with `?` — no `.ok()` anywhere on the path", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "create_automation_draft", + "module": "automation_commands", + "reason": "detector-c, hand-traced: create_automation_draft_for_state calls prepare_agent_conversation_workspace_with_setup_mode_and_defaults, which reaches GitService::ref_exists -> run_status -> build_git_command. Setup mode is Deferred, so no worktree is materialised, but the ref probe is an unconditional git launch", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "update_automation_settings", + "module": "automation_commands", + "reason": "batch-12 audit: one settings patch; plan_approval_mode and pr_merge_mode are both `parse()`-validated and rejected on a miss. These knobs govern whether a LATER run auto-approves a plan or auto-merges a PR, but they seed no scanned surface value on their own — the run has to already exist and reach that gate", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "update_automation_config", + "module": "automation_commands", + "reason": "batch-12 audit: direct spawn-free automation setup patch using the same validated settings-then-config service flow as the setup HTTP route. The twin requires an exact expected_updated_at match before either write, so stale remote clients fail closed without mutating the row; these fields only configure a later run and do not arm the automation scheduler", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "pause_automation", + "module": "automation_commands", + "reason": "batch-12 audit: one CAS status write to Paused. Authority-reducing — it removes the Active value the automation scheduler scans for", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "seedsSpawnTriggeringState" + ], + "class": "agentControl", + "command": "resume_automation", + "module": "automation_commands", + "reason": "detector-b: restores Active automation consumed by the automation scheduler", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "seedsSpawnTriggeringState" + ], + "class": "agentControl", + "command": "finalize_automation", + "module": "automation_commands", + "reason": "detector-b: completes automation arming state consumed by the automation scheduler", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "stop_automation", + "module": "automation_commands", + "reason": "batch-12 audit: CAS write to Stopped. The Active write in this body is a ROLLBACK restoring the pre-call value after a failed follow-up, not a fresh arming", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "restart_automation", + "module": "automation_commands", + "reason": "batch-12 audit: CAS Stopped->Active. Declared arms-automation-scheduler: Active is the armed value of the automation-active surface, but that surface's only write marker is `reopen_run_corrective`, which this path does not carry, so detector (b) is silent on a write that genuinely re-arms the scheduler", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "trigger_automation_run_now", + "module": "automation_commands", + "reason": "detector-c, hand-traced: dispatch_automation_run_now_action -> spawn_automation_judge_task -> AutomationJudgeTask::invoke_and_parse_judge -> invoke_automation_utility_agent -> CodexCliClient::spawn_agent, resolving the Codex CLI and, through build_codex_internal_mcp_overrides, the node binary. A real agent spawn", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "retry_automation_judge", + "module": "automation_commands", + "reason": "detector-c, hand-traced: the SAME dispatch_automation_run_now_action chain as trigger_automation_run_now, genuinely shared rather than inferred, so the identical Codex spawn. Its plan-judge sibling retry_automation_plan_judge does NOT share it and is registered as an arming write instead", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "retry_automation_plan_judge", + "module": "automation_commands", + "reason": "batch-12 audit: does NOT spawn inline — unlike its `retry_automation_judge` twin it never reaches dispatch_automation_run_now_action, which is why detector (c) is correctly silent. It instead un-pauses the automation to Active and resets plan_judge_state Failed->None on a run AwaitingPlanApproval, leaving exactly the state the scheduler dispatches a fresh plan judge from. Declared arms-automation-scheduler", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "skip_automation_judge", + "module": "automation_commands", + "reason": "batch-12 audit: advances a run past its judge and, when the automation was paused for a failed judge, flips Paused->Active. Skipping the judge is the point: it removes the gate AND restores the scanned Active value. Declared arms-automation-scheduler", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "cancel_automation_run", + "module": "automation_commands", + "reason": "batch-12 audit: ownership-checked run cancel via CAS. The trailing sync_goal_items_for_closed_run_without_successor returns unit and absorbs its own repo errors, but it is a derived goal-item projection running AFTER the cancel is durable and returned — it cannot make a failed cancel look successful", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "deletesEntity" + ], + "class": "denied", + "command": "delete_automation_run", + "module": "automation_commands", + "reason": "deletes a durable entity", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "seedsSpawnTriggeringState" + ], + "class": "agentControl", + "command": "resume_automation_run", + "module": "automation_commands", + "reason": "batch-12 audit: `reopen_automation_run` re-opens a closed run. Detector (a) AND (b) both fire here — it carries the surface's `reopen_run_corrective` marker — so it is the ONE arming member of this batch that earns SeedsSpawnTriggeringState, which `seeds_spawn_triggering_state_tags_track_detector_b_evidence` defines as detector-(b) evidence. Its three detector-silent siblings take AGENT plus a declared membership instead, the same split batches 10 and 11 used", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "deletesEntity" + ], + "class": "denied", + "command": "delete_automation", + "module": "automation_commands", + "reason": "deletes a durable entity", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [], + "class": "read", + "command": "list_personas", + "module": "persona_commands", + "reason": "batch-13 audit: PersonaService::list_personas behind the feature flag, error mapped. 29 closure nodes", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_persona", + "module": "persona_commands", + "reason": "batch-13 audit: PersonaService::get_persona, error mapped", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "mutatesAgentConsumedContent" + ], + "class": "agentControl", + "command": "create_persona_draft", + "module": "persona_commands", + "reason": "batch-13 audit: composes persona content (content, or description+body, else ERROR) and writes a draft through PersonaService::create_draft with map_err(to_string)?; the follow-up emit_draft_updated runs after the write. Persona bodies are injected into agent prompts, hence MutatesAgentConsumedContent", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "mutatesAgentConsumedContent" + ], + "class": "agentControl", + "command": "update_persona_draft", + "module": "persona_commands", + "reason": "batch-13 audit: PersonaService::update_draft carrying an optional expected_content_hash — an OPTIMISTIC-CONCURRENCY check, propagated not swallowed", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "mutatesAgentConsumedContent" + ], + "class": "agentControl", + "command": "update_persona", + "module": "persona_commands", + "reason": "batch-13 audit: re-reads the existing persona for its slug, recomposes content, then PersonaService::update_persona; every hop map_err(to_string)?", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "mutatesAgentConsumedContent" + ], + "class": "agentControl", + "command": "approve_persona", + "module": "persona_commands", + "reason": "batch-13 audit: reads the draft's source_persona_id, approves through PersonaService::approve_persona, and emits persona:draft_applied only when the approved id MATCHES the recorded source. Promotes a draft to the content live conversations overlay", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "mutatesAgentConsumedContent" + ], + "class": "agentControl", + "command": "reseed_persona_draft", + "module": "persona_commands", + "reason": "batch-13 audit: PersonaService::reseed_persona_draft resets a draft to its source, error mapped", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "mutatesAgentConsumedContent" + ], + "class": "agentControl", + "command": "approve_persona_as_new", + "module": "persona_commands", + "reason": "batch-13 audit: PersonaService::approve_persona_as_new with an optional new slug, error mapped. Forks rather than overwrites; same content authority", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "mutatesAgentConsumedContent" + ], + "class": "agentControl", + "command": "archive_persona", + "module": "persona_commands", + "reason": "batch-13 audit: PersonaService::archive_persona, error mapped. Withdraws a persona from overlay resolution", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "mutatesAgentConsumedContent" + ], + "class": "agentControl", + "command": "unarchive_persona", + "module": "persona_commands", + "reason": "batch-13 audit: PersonaService::unarchive_persona, error mapped. The measured CONTRAST that proves the sibling collision is an artifact: identical shape to archive_persona but 120 closure nodes and a=FALSE, exactly the get_metrics_config (23) vs save_metrics_config (1200) asymmetry batch 12 pinned", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "list_persona_usage", + "module": "persona_commands", + "reason": "batch-13 audit: PersonaService::list_persona_usage, error mapped", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "preview_persona_overlay", + "module": "persona_commands", + "reason": "batch-13 audit: resolves the overlay a conversation WOULD receive and returns the rendered block on the direct command response only; the empty-id guard errors first and the chat-service error propagates. Renders, persists nothing", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "deletesEntity" + ], + "class": "denied", + "command": "delete_persona_draft", + "module": "persona_commands", + "reason": "deletes a durable entity", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "touchesCredentials" + ], + "class": "denied", + "command": "get_agent_conversation_linear_issue", + "module": "linear_commands", + "reason": "integration credential surface", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "touchesCredentials" + ], + "class": "denied", + "command": "assign_agent_conversation_linear_issue", + "module": "linear_commands", + "reason": "integration credential surface", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "touchesCredentials" + ], + "class": "denied", + "command": "refresh_agent_conversation_linear_issue", + "module": "linear_commands", + "reason": "integration credential surface", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "touchesCredentials" + ], + "class": "denied", + "command": "clear_agent_conversation_linear_issue", + "module": "linear_commands", + "reason": "integration credential surface", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [], + "class": "read", + "command": "list_tasks", + "module": "task_commands", + "reason": "task read", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_task", + "module": "task_commands", + "reason": "task read", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "operate", + "command": "create_task", + "module": "task_commands", + "reason": "Backlog-only by construction: CreateTaskInput carries no status field and Task::new_with_category sets InternalStatus::Backlog, so a created task cannot be born in a spawn-triggering state", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "operate", + "command": "update_task", + "module": "task_commands", + "reason": "inert fields only at this class: category is a closed enum and priority an i32; title/description carry a conditional MutatesAgentConsumedContent discharged by update_task_authz, and internal_status is rejected by validate_update_task_input", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "answer_user_question", + "module": "task_commands", + "reason": "conservative-module-default: may steer or arm autonomous work", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "seedsSpawnTriggeringState" + ], + "class": "agentControl", + "command": "inject_task", + "module": "task_commands", + "reason": "detector-b: seeds internal_status=Ready consumed by the ready-task scheduler", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "agentControl", + "mutatesAgentConsumedContent" + ], + "class": "agentControl", + "command": "move_task", + "module": "task_commands", + "reason": "detector-a plus content-surface: restart note is worker-consumed", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "archive_task", + "module": "task_commands", + "reason": "batch-14 audit: writes tasks.archived_at and updated_at through a pure db.run SQL path behind authorize_task_mutation. No transition service, no entry/exit actions, no scheduler, no registry, no git — hand-traced, not inferred from silence. DISARMS scheduling (get_oldest_ready_tasks filters archived_at IS NULL). Recorded standing wart, unchanged by this batch: archiving does not kill an already-running agent for the task, so an Executing task can go invisible to the reconciler", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "restore_task", + "module": "task_commands", + "reason": "batch-14 audit: the exact inverse of archive_task and the same pure-SQL body. It is a real ARM despite launching nothing: clearing archived_at on a task already in Ready re-admits it to get_oldest_ready_tasks, so the next scheduler tick may spawn for it. It does NOT claim SeedsSpawnTriggeringState — detector (b) does not flag it, and that capability is defined as detector-(b) evidence, so the tag would be false even though the arming is real", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "block_task", + "module": "task_commands", + "reason": "exiting an agent-active state decrements capacity and calls try_schedule_ready_tasks through the attached scheduler, which can launch queued work", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "unblock_task", + "module": "task_commands", + "reason": "authority-restoring transition", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "denied", + "command": "cleanup_task", + "module": "task_commands", + "reason": "destructive task cleanup", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "denied", + "command": "cleanup_tasks_in_group", + "module": "task_commands", + "reason": "destructive task cleanup across a whole group", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "cancel_tasks_in_group", + "module": "task_commands", + "reason": "bulk-terminalizes an attacker-chosen group and execution exits reach auto-commit, which invokes Git", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "pause_tasks_in_group", + "module": "task_commands", + "reason": "bulk-pauses an attacker-chosen group; leaving Executing/ReExecuting reaches the normal exit auto-commit path and can invoke Git", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "resume_tasks_in_group", + "module": "task_commands", + "reason": "detector-c-MISS (M2+M3), hand-traced: mutation.rs:2143/:2161 transition each task back to its PRE-PAUSE status and run execute_entry_actions for it, so the restored status is Executing/Reviewing/Merging and the on_enter spine spawns the agent; request_remote_group_resume is the spawn-free intent twin and the host dispatcher alone calls this seam", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "auditRefusal": { + "batch": "3 refused, audited and classified 10", + "finding": "task_commands/mutation.rs:2242 swallows each per-task archive error with tracing::warn! and continues the loop, so the command returns Ok(BulkArchiveResponse { archived_count }) with a count silently short of the group and no way for the caller to learn which tasks survived. Compounded by the standing authority-OBSCURING finding: archive writes only archived_at, there is no InternalStatus::Archived, and get_by_status filters `archived_at IS NULL`, so a partially-failed sweep leaves Executing tasks holding their agent process while invisible to the reconciler; fix by propagating the per-task error", + "reason": "fail-open-until-fixed" + }, + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "archive_tasks_in_group", + "module": "task_commands", + "reason": "conservative-module-default: may steer or arm autonomous work", + "registered": false, + "v1Resolution": "v1-audit-refused" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "pause_execution_plan", + "module": "task_commands", + "reason": "detector-c-MISS (M1+M2+M3), hand-traced: only AGENT_ACTIVE tasks are touched, so on_exit from Executing ALWAYS runs auto_commit_on_execution_done (git has_uncommitted_changes + commit_all), and stop_task_runtime_contexts reaches kill_process -> Command::new(resolve_pkill_cli_path())", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "resume_execution_plan", + "module": "task_commands", + "reason": "detector-c-MISS (M2+M3), hand-traced, three ways: transition_task into a gated AGENT_ACTIVE restore status, execute_entry_actions, and an explicit scheduler.try_schedule_ready_tasks()", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "stop_execution_plan", + "module": "task_commands", + "reason": "detector-c-MISS (M1+M2+M3), hand-traced: the same on_exit auto-commit and stop_task_runtime_contexts kill path as pause_execution_plan, via transition_to_stopped_with_context", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "pause_task", + "module": "task_commands", + "reason": "leaving Executing/ReExecuting reaches the normal exit auto-commit path and can invoke Git", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "resume_task", + "module": "task_commands", + "reason": "detector-c: publish_post_merge_branch_update -> run_authorized_mutation -> build_git_command; request_remote_task_resume is the spawn-free intent twin and spawn_remote_resume_dispatchers alone calls this host seam", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "retry_branch_update", + "module": "task_commands", + "reason": "detector-c: execute_programmatic_branch_update -> run_authorized_mutation -> build_git_command; hand-tracing adds two more independent launches (the post-merge publish push, and entry actions into UpdatingTaskBranch/UpdatingPlanBranch which start the branch-update resolver agent)", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "stop_task", + "module": "task_commands", + "reason": "leaving Executing/ReExecuting reaches the normal exit auto-commit path and can invoke Git", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_archived_count", + "module": "task_commands", + "reason": "archived-task count: `task_repo.get_archived_count`, a scalar count read", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "search_tasks", + "module": "task_commands", + "reason": "task read", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_valid_transitions", + "module": "task_commands", + "reason": "state-machine metadata read", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_tasks_awaiting_review", + "module": "task_commands", + "reason": "review-queue read: `task_repo.list_paginated` filtered to the four review statuses; selects rows and starts no review", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_session_task_history_availability", + "module": "task_commands", + "reason": "session history availability: `task_repo.count_tasks` rendered as a bool plus a count", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_task_state_transitions", + "module": "task_commands", + "reason": "status-history read: `task_repo.get_status_history` mapped to a response; reads transitions already taken and requests none", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_task_dependency_graph", + "module": "task_commands", + "reason": "dependency-graph read: in-process traversal over `task_repo` rows; writes no edge and schedules nothing", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_task_timeline_events", + "module": "task_commands", + "reason": "timeline read: derives events from `task_repo` rows in process", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "mutatesAgentConsumedContent" + ], + "class": "agentControl", + "command": "create_task_step", + "module": "task_step_commands", + "reason": "content-surface: creates worker-consumed task step", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_task_steps", + "module": "task_step_commands", + "reason": "task-step read: `task_step_repo.get_by_task` mapped to responses", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "mutatesAgentConsumedContent" + ], + "class": "agentControl", + "command": "update_task_step", + "module": "task_step_commands", + "reason": "content-surface: updates worker-consumed task step", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "reorder_task_steps", + "module": "task_step_commands", + "reason": "reorders a task's steps in one transaction scoped `WHERE id = ?2 AND task_id = ?3`, so a foreign step id is a no-op rather than a cross-task write; propagates every repository error. Not a content writer — it moves sort_order and no step body — so it does not carry MutatesAgentConsumedContent the way its four status siblings do", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_step_progress", + "module": "task_step_commands", + "reason": "step-progress read: `task_step_repo.get_by_task` summarised in process", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "agentControl", + "mutatesAgentConsumedContent" + ], + "class": "agentControl", + "command": "start_step", + "module": "task_step_commands", + "reason": "detector-d: writes worker-consumed task step status", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "agentControl", + "mutatesAgentConsumedContent" + ], + "class": "agentControl", + "command": "complete_step", + "module": "task_step_commands", + "reason": "detector-d: writes worker-consumed task step status", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "agentControl", + "mutatesAgentConsumedContent" + ], + "class": "agentControl", + "command": "skip_step", + "module": "task_step_commands", + "reason": "detector-d: writes worker-consumed task step status", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "agentControl", + "mutatesAgentConsumedContent" + ], + "class": "agentControl", + "command": "fail_step", + "module": "task_step_commands", + "reason": "detector-d: writes worker-consumed task step status", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "list_projects", + "module": "project_commands", + "reason": "project git/gh and deferred shell authority", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "get_project", + "module": "project_commands", + "reason": "project git/gh and deferred shell authority", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "create_project", + "module": "project_commands", + "reason": "project git/gh and deferred shell authority", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "update_project", + "module": "project_commands", + "reason": "project git/gh and deferred shell authority", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "archive_project", + "module": "project_commands", + "reason": "project git/gh and deferred shell authority", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "read_pr_template", + "module": "project_commands", + "reason": "project git/gh and deferred shell authority", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "write_pr_template", + "module": "project_commands", + "reason": "project git/gh and deferred shell authority", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "denied", + "command": "get_git_branches", + "module": "project_commands", + "reason": "spawns git over project-controlled state", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "get_git_current_branch", + "module": "project_commands", + "reason": "project git/gh and deferred shell authority", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "get_git_default_branch", + "module": "project_commands", + "reason": "project git/gh and deferred shell authority", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "search_github_pull_requests", + "module": "project_commands", + "reason": "project git/gh and deferred shell authority", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "reanalyze_project", + "module": "project_commands", + "reason": "spawns the project-analyzer agent", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "denied", + "command": "update_custom_analysis", + "module": "project_commands", + "reason": "executes the canonical deferred shell-authority shape", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "get_git_remote_url", + "module": "project_commands", + "reason": "project git/gh and deferred shell authority", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "get_git_auth_diagnostics", + "module": "project_commands", + "reason": "project git/gh and deferred shell authority", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "denied", + "command": "switch_git_origin_to_ssh", + "module": "project_commands", + "reason": "changes repository origin authentication", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "denied", + "command": "setup_gh_git_auth", + "module": "project_commands", + "reason": "configures git credential authority", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "denied", + "command": "login_gh_with_browser", + "module": "project_commands", + "reason": "starts interactive GitHub authentication", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "resume_deferred_git_startup", + "module": "project_commands", + "reason": "project git/gh and deferred shell authority", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "check_gh_auth", + "module": "project_commands", + "reason": "project git/gh and deferred shell authority", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "update_github_pr_enabled", + "module": "project_commands", + "reason": "project git/gh and deferred shell authority", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "list_agent_profiles", + "module": "agent_profile_commands", + "reason": "conservative-module-default: may steer or arm autonomous work", + "registered": false, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "get_agent_profile", + "module": "agent_profile_commands", + "reason": "conservative-module-default: may steer or arm autonomous work", + "registered": false, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "get_agent_profiles_by_role", + "module": "agent_profile_commands", + "reason": "conservative-module-default: may steer or arm autonomous work", + "registered": false, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "get_builtin_agent_profiles", + "module": "agent_profile_commands", + "reason": "conservative-module-default: may steer or arm autonomous work", + "registered": false, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "get_custom_agent_profiles", + "module": "agent_profile_commands", + "reason": "conservative-module-default: may steer or arm autonomous work", + "registered": false, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "seed_builtin_profiles", + "module": "agent_profile_commands", + "reason": "conservative-module-default: may steer or arm autonomous work", + "registered": false, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "list_agent_models", + "module": "agent_model_commands", + "reason": "built-in and custom model registry merge; propagates read errors", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "upsert_custom_agent_model", + "module": "agent_model_commands", + "reason": "content-surface, declared membership configures-future-agent-runtime: writes an agent_model_registry row consumed by normalize_agent_runtime_selection to pick the harness CLI's model and effort. Same bounded-deferred-authority idiom as update_workspace_review_runtime_settings. Registered only AFTER fixing the fail-open this batch found in its return path: the repo read `.ok()`-swallowed the created_at/updated_at columns and fell through to a fabricated Utc::now(), so a column error was rendered as a real timestamp", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "deletesEntity" + ], + "class": "denied", + "command": "delete_custom_agent_model", + "module": "agent_model_commands", + "reason": "deletes a durable entity", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "search_agent_composer_entries", + "module": "agent_composer_commands", + "reason": "detector-c: indexes project entries via Command::new(resolve_git_cli_path())", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [], + "class": "read", + "command": "search_agent_composer_plan_references", + "module": "agent_composer_commands", + "reason": "plan-reference search: ideation sessions plus artifact resolution, ranked and truncated to a capped limit; the resolver fail-open that once dropped sessions silently was already removed, so a resolver outage now errors instead of shipping a short list that looks complete", + "registered": true, + "v1Resolution": "registerable" + }, + { + "auditRefusal": { + "batch": "4, re-audited 8", + "finding": "agent_composer_commands/skills.rs:766 swallows the Codex config read, so losing it reports DISABLED skills as enabled — a fail-open that changes the answer, not just its completeness (also :299/:318/:442/:589)", + "reason": "fail-open-until-fixed" + }, + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "list_agent_composer_skills", + "module": "agent_composer_commands", + "reason": "conservative-module-default: may steer or arm autonomous work", + "registered": false, + "v1Resolution": "v1-audit-refused" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "copy_agent_conversation_plan", + "module": "agent_plan_commands", + "reason": "detector-c, hand-traced: seed_agent_conversation_plan prepares the plan workspace and reaches GitService::get_current_branch, and on a conversation with no workspace yet also create_worktree plus the project's pre-execution shell setup. The probe's codex/node tokens are artifacts — this path never spawns an agent — but the git launch is real", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "import_agent_conversation_plan", + "module": "agent_plan_commands", + "reason": "detector-c, hand-traced: the same seed_agent_conversation_plan helper as copy_agent_conversation_plan, genuinely shared, so the same git worktree launch. codex/node artifacts likewise", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "activate_agent_task_pipeline", + "module": "agent_plan_commands", + "reason": "detector-c, hand-traced and NARROW: the command's own work is DB-only. The launch is reached through agent_workspace_response_for_state's stale-publish repair, which runs `git rev-parse --is-inside-work-tree` when the conversation has a stranded PR-fix review handoff. Refused because the process-launch floor is absolute, not because the reach is broad — recorded this way so a future seam split can be argued against the real path", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "activate_agent_plan_direct_implementation", + "module": "agent_plan_commands", + "reason": "detector-c, hand-traced and NARROW: same incidental publish-repair probe as activate_agent_task_pipeline. This command flips the mode inline in SQL and does NOT inherit the worktree-creation edge the copy/import pair carries", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "start_agent_task_pipeline", + "module": "agent_plan_commands", + "reason": "detector-c, hand-traced: delegates to apply_supervised_proposals_core and inherits all three of apply_proposals_to_kanban's sinks — the repository capability probe, base-branch creation, and the session-namer agent spawn", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [], + "class": "read", + "command": "get_qa_settings", + "module": "qa_commands", + "reason": "QA settings read: clones the in-memory `AppState::qa_settings` behind a read guard; the WRITE half (`update_qa_settings`) arms auto-QA and stays AgentControl", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "update_qa_settings", + "module": "qa_commands", + "reason": "arms-auto-qa: applies only the Some(..) fields of the input to the in-memory AppState::qa_settings write guard, so it can enable auto-QA. Detector-silent by construction — the surface is a RwLock, not a repository — which is why it also carries an explicit DECLARED_MEMBERSHIPS row. The READ half (`get_qa_settings`) is already registered at Read", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_task_qa", + "module": "qa_commands", + "reason": "per-task QA record: `task_qa_repo.get_by_task_id` mapped to a response", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_qa_results", + "module": "qa_commands", + "reason": "QA test results: `task_qa_repo.get_by_task_id` projected to its `test_results`; retries nothing and resets no result", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "retry_qa", + "module": "qa_commands", + "reason": "reads task_qa_repo.get_by_task_id and writes a fresh all-Pending QAResults through update_results; every error propagates with `?`. Its sibling `skip_qa` is REFUSED — skip writes a verdict that does not mean what its name promises, while retry writes the unambiguous Pending reset", + "registered": true, + "v1Resolution": "registerable" + }, + { + "auditRefusal": { + "batch": "7 refused, audited and classified 10", + "finding": "the command promises a QA bypass and does not deliver one: it writes every step as QAStepResult::skipped, but QAResults::from_results derives Passed only when passed_steps == total_steps and skipped steps increment skipped_steps, so overall_status resolves to Pending, not Passed — contradicting the body's own `// Mark all steps as passed (skipped behavior)` comment. A caller is told the skip succeeded while the verdict it wanted was never written; fix by deciding the intended verdict and making from_results express it", + "reason": "fail-open-until-fixed" + }, + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "skip_qa", + "module": "qa_commands", + "reason": "conservative-module-default: may steer or arm autonomous work", + "registered": false, + "v1Resolution": "v1-audit-refused" + }, + { + "capabilities": [], + "class": "read", + "command": "get_pending_reviews", + "module": "review_commands", + "reason": "pending-review enumeration: `review_repo.get_pending` mapped to responses; selects rows and starts no review", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_review_by_id", + "module": "review_commands", + "reason": "single-review read: `review_repo.get_by_id`, an Option-returning row read whose repository error propagates rather than reading as absent", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_reviews_by_task_id", + "module": "review_commands", + "reason": "per-task review list: `review_repo.get_by_task_id` mapped to responses", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_task_state_history", + "module": "review_commands", + "reason": "review-note history: `review_repo.get_notes_by_task_id`; reads notes already written and writes none", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "mutatesAgentConsumedContent" + ], + "class": "agentControl", + "command": "approve_review", + "module": "review_commands", + "reason": "content-surface: writes worker-consumed review feedback", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "mutatesAgentConsumedContent" + ], + "class": "agentControl", + "command": "request_changes", + "module": "review_commands", + "reason": "content-surface: writes worker-consumed review feedback", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "mutatesAgentConsumedContent" + ], + "class": "agentControl", + "command": "reject_review", + "module": "review_commands", + "reason": "content-surface: writes worker-consumed review feedback", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "approve_fix_task", + "module": "review_commands", + "reason": "batch-14: the STANDING HELD half, released. Batch 10 audited the body clean (a Blocked guard, then Blocked->Ready in the registered unblock_task shape, every error propagated) and withheld it ONLY on a pair argument — no remote way to reject. That argument does not survive the current registry: block_task and stop_task are both registered, so the remote brake exists; its exact scheduler-construction shape is already registered as approve_task_for_review; and the in-band Ready->Executing spawn is MODELLED rather than hidden, because TRANSITION_SINKS cuts traversal at transition_task and SCHEDULER_SINKS names try_schedule_ready_tasks, so the hit is classified by its target. Pinned corrective- free: unlike its partner it reaches no corrective sink", + "registered": true, + "v1Resolution": "registerable" + }, + { + "auditRefusal": { + "batch": "10 refused, classified 14", + "finding": "review_commands.rs:273 calls transition_task_corrective(fix_task, Failed) and :297 calls it again for the original task (Backlog) once max attempts are exceeded. Corrective jumps are the repair-path-only state-machine escape and `no_registered_facade_target_reaches_a_corrective_transition` forbids a registered target from reaching one at ANY scope, so this is a hard invariant rather than a scope call. Held unclassified since batch 10 for want of an honest code; classified here rather than mis-filed. The rest of the body is clean, so the finding is precisely the corrective reach — fix by routing the rejection through a mediator that pins its own target, as move_task does", + "reason": "reaches-corrective-transition" + }, + "capabilities": [ + "mutatesAgentConsumedContent" + ], + "class": "agentControl", + "command": "reject_fix_task", + "module": "review_commands", + "reason": "content-surface: writes worker-consumed fix feedback", + "registered": false, + "v1Resolution": "v1-audit-refused" + }, + { + "capabilities": [], + "class": "read", + "command": "get_fix_task_attempts", + "module": "review_commands", + "reason": "fix-attempt count: `review_repo.count_fix_actions` rendered as a scalar", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "mutatesAgentConsumedContent" + ], + "class": "agentControl", + "command": "approve_task_for_review", + "module": "review_commands", + "reason": "content-surface: writes worker-consumed review note", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "mutatesAgentConsumedContent" + ], + "class": "agentControl", + "command": "request_task_changes_for_review", + "module": "review_commands", + "reason": "content-surface: writes worker-consumed review feedback", + "registered": true, + "v1Resolution": "registerable" + }, + { + "auditRefusal": { + "batch": "audited and classified 10", + "finding": "the idempotency-flag write degrades destructively: review_commands.rs:675 reads the task's metadata with parse_metadata(&task).unwrap_or_else(|| json!({})) and :683 re-serialises it with unwrap_or_else(|_| r#\"{\\\"request_changes_initiated\\\":true}\"#), so an unparseable or unserialisable blob is REPLACED by a stub and every other metadata field is dropped — while the command returns Ok. Its sibling request_task_changes_for_review reaches the same RevisionNeeded transition with no such write and is registered instead; fix by propagating both serde errors", + "reason": "fail-open-until-fixed" + }, + "capabilities": [ + "mutatesAgentConsumedContent" + ], + "class": "agentControl", + "command": "request_task_changes_from_reviewing", + "module": "review_commands", + "reason": "content-surface: writes worker-consumed review feedback", + "registered": false, + "v1Resolution": "v1-audit-refused" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "re_review_task_from_escalated", + "module": "review_commands", + "reason": "detector-a/b: guards internal_status == Escalated, optionally restores a stale worktree path (errors propagated with `?`), then transitions to PendingReview, which dispatches the AI reviewer. A user-initiated gate decision of precisely the shape `ui:agent` exists for", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_task_issues", + "module": "review_commands", + "reason": "issue list: `review_issue_repo.get_open_by_task_id`/`get_by_task_id` selected by a status filter; both halves propagate their repository error", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_issue_progress", + "module": "review_commands", + "reason": "issue progress summary: `review_issue_repo.get_summary`, an aggregate read", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "agentControl", + "mutatesAgentConsumedContent" + ], + "class": "agentControl", + "command": "verify_issue", + "module": "review_commands", + "reason": "detector-d: writes worker-consumed review issue state", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "agentControl", + "mutatesAgentConsumedContent" + ], + "class": "agentControl", + "command": "reopen_issue", + "module": "review_commands", + "reason": "detector-d: writes worker-consumed review issue state", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "agentControl", + "mutatesAgentConsumedContent" + ], + "class": "agentControl", + "command": "mark_issue_in_progress", + "module": "review_commands", + "reason": "detector-d: writes worker-consumed review issue state", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "agentControl", + "mutatesAgentConsumedContent" + ], + "class": "agentControl", + "command": "mark_issue_addressed", + "module": "review_commands", + "reason": "detector-d: writes worker-consumed review issue state", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_review_settings", + "module": "review_commands", + "reason": "review policy read: `review_settings_repo.get_settings`; the WRITE half (`update_review_settings`) seeds spawn-triggering state and stays AgentControl", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "agentControl", + "seedsSpawnTriggeringState" + ], + "class": "agentControl", + "command": "update_review_settings", + "module": "review_commands", + "reason": "detector-b: arms require_workspace_review consumed by the auto-review spawner", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "get_task_validation_summary", + "module": "validation_commands", + "reason": "resolves the git CLI through `GitService::get_head_sha` to stamp the validation summary with the current HEAD sha", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [], + "class": "read", + "command": "get_workspace_review_runtime_settings", + "module": "workspace_review_settings_commands", + "reason": "batch-14 audit: two fetch_many branches, both propagating with `?`. An empty vec genuinely means `no rows`, never `the read failed`. Recorded semantic caveat, not a fail-open: project_id=None returns GLOBAL rows only and does not merge project scope; effective resolution is a separate concern owned by resolve_effective", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "update_workspace_review_runtime_settings", + "module": "workspace_review_settings_commands", + "reason": "content-surface, declared membership configures-future-agent-runtime: upserts model and effort for the Workspace Review background agent, read back by resolve_explicit_workspace_review_runtime_settings. This is batch 13's update_agent_lane_settings idiom exactly — BOUNDED deferred authority over which model runs, not over the sandbox/approval envelope — so it stays registerable with the finding declared rather than becoming a deferral by notation. Fail-closed: a missing post-upsert re-read is an error, not a default", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "get_execution_status", + "module": "execution_commands", + "reason": "detector-c: resolves the process-inspection CLI (resolve_tasklist_cli_path) to report live execution status; get_remote_execution_status is the spawn-free read twin", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [], + "class": "operate", + "command": "pause_execution", + "module": "execution_commands", + "reason": "authority-reducing: gates scheduling and transitions agent-active tasks only to Paused", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "resume_execution", + "module": "execution_commands", + "reason": "detector-c after the Wave B1 shared-seam extraction; THREE independent launch chains. (1) execute_entry_actions -> TransitionHandler::on_enter -> enter_executing_state -> send_task_execution_message; (2) try_schedule_ready_tasks transitions a Ready task to Executing into the same spine; (3) four paused-queue relaunchers reach ChatService::send_message. All terminate at ChatHarnessLaunchPlan::spawn -> Command::new(cli_path) (claude/mod.rs:664, codex/mod.rs:1156), which names no resolver. It ALSO fails open at lifecycle.rs:273: the task is transitioned into an agent-active status at :258, then `if let Ok(Some(..))` collapses read error and absence, so entry actions never run, restoring_count is never incremented, and the capacity guard admits MORE tasks than the cap while the command returns success; request_remote_execution_resume is the spawn-free intent twin and the host dispatcher alone calls this seam", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [], + "class": "operate", + "command": "stop_execution", + "module": "execution_commands", + "reason": "authority-reducing: gates scheduling and transitions agent-active tasks only to Stopped", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "restart_task", + "module": "execution_commands", + "reason": "detector-c: validate_resume -> GitService::branch_exists -> run_status -> build_git_command; request_remote_task_restart is the spawn-free intent twin and spawn_remote_resume_dispatchers alone calls this host seam", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "recover_task_execution", + "module": "execution_commands", + "reason": "detector-c: recover_execution_stop -> apply_recovery_decision -> reconcile_merge_auto_complete -> try_complete_stale_rebase -> build_git_command, and separately is_ipr_process_alive -> is_process_alive", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "resolve_recovery_prompt", + "module": "execution_commands", + "reason": "detector-c: apply_user_recovery_action -> apply_failed_user_recovery_action -> GitService::delete_branch -> build_git_command", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "set_max_concurrent", + "module": "execution_commands", + "reason": "conservative-module-default: may steer or arm autonomous work", + "registered": false, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_execution_settings", + "module": "execution_commands", + "reason": "execution-settings read: `execution_settings_repo.get_settings`; reads the quota and changes none", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "update_execution_settings", + "module": "execution_commands", + "reason": "detector-c-MISS (M2+M3), hand-traced: settings.rs:147 tokio::spawn -> PendingSessionDrainService::try_drain_pending_for_project -> send_message -> agent spawn, plus the scheduler kick at :113. It also fails open at settings.rs:88, where `.map(..).unwrap_or(input.project_ideation_max)` on a Result makes a failed read look like `value unchanged`, so a capacity raise is silently dropped", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "set_active_project", + "module": "execution_commands", + "reason": "arms-scheduler-quota: calls sync_quota_from_project, which writes the runtime ExecutionState max_concurrent and project_ideation_max atomics that can_start_task reads. Deliberately NOT declared SeedsSpawnTriggeringState: unlike its siblings set_max_concurrent and update_execution_settings, this command never calls schedule_ready_tasks_for_project, so it raises the ceiling without itself dispatching anything. Detector-silent, hence the DECLARED_MEMBERSHIPS row", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_active_project", + "module": "execution_commands", + "reason": "active-project read: clones the in-memory `ActiveProjectState` id; the WRITE half (`set_active_project`) syncs the scheduler quota and stays AgentControl", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_global_execution_settings", + "module": "execution_commands", + "reason": "global execution-settings read: `global_execution_settings_repo.get_settings`", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "update_global_execution_settings", + "module": "execution_commands", + "reason": "detector-c-MISS (M2+M3), hand-traced: resume_paused_workspace_queues_with_chat_service reaches send_message, and :353 kicks the ready-task scheduler", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "get_running_processes", + "module": "execution_commands", + "reason": "detector-c: resolves the process-inspection CLI (resolve_tasklist_cli_path)", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [], + "class": "read", + "command": "get_merge_pipeline", + "module": "merge_pipeline_commands", + "reason": "merge-pipeline projection: batched `project_repo`/`task_repo`/`plan_branch_repo`/`agent_conversation_workspace_repo` reads bucketed by `InternalStatus`; every repository error propagates and no merge is started, deferred or resolved", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_merge_progress", + "module": "merge_pipeline_commands", + "reason": "merge-progress hydration: clones accumulated events out of the in-memory `MERGE_PROGRESS_STORE`. The empty default is absence of emitted events, not a swallowed error — the store read cannot fail", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_merge_phase_list", + "module": "merge_pipeline_commands", + "reason": "merge phase-list hydration: clones the stored phase list out of the in-memory `MERGE_PHASE_LIST_STORE`; returns `None` when nothing was emitted", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_agent_conversation_stats", + "module": "conversation_stats_commands", + "reason": "aggregates conversation/message/run repository reads into usage totals; propagates read errors", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_insights_chat_usage_stats", + "module": "conversation_stats_commands", + "reason": "project-or-all-projects usage aggregation over repository reads; propagates read errors", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_project_chat_usage_stats", + "module": "conversation_stats_commands", + "reason": "project-scoped usage aggregation over repository reads; propagates read errors", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_task_chat_usage_stats", + "module": "conversation_stats_commands", + "reason": "task-scoped usage aggregation over repository reads; propagates read errors", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "denied", + "command": "build_agent_issue_report", + "module": "agent_issue_report_commands", + "reason": "spawns diagnostic report tooling", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "submit_agent_issue_report", + "module": "agent_issue_report_commands", + "reason": "report construction may spawn diagnostics", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [], + "class": "read", + "command": "get_insights_stats", + "module": "metrics_commands", + "reason": "batch-12 audit: cross-project aggregate query; the only `unwrap_or` defaults an absent timezone/week-start ARGUMENT, never a swallowed Err", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_project_stats", + "module": "metrics_commands", + "reason": "batch-12 audit: project-scoped twin of get_insights_stats, same shape", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_insights_pr_insights", + "module": "metrics_commands", + "reason": "batch-12 audit: PR aggregate read; no write", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_project_pr_insights", + "module": "metrics_commands", + "reason": "batch-12 audit: project-scoped twin of get_insights_pr_insights", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_insights_trends", + "module": "metrics_commands", + "reason": "batch-12 audit: bucketed trend query; no write", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_project_trends", + "module": "metrics_commands", + "reason": "batch-12 audit: project-scoped twin of get_insights_trends", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_metrics_config", + "module": "metrics_commands", + "reason": "batch-12 audit: single `project_metrics_config` row read; 23-node closure, detectors silent, and unlike its writing sibling it never touches the colliding `execute` name", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "save_metrics_config", + "module": "metrics_commands", + "reason": "batch-12 audit: ONE `project_metrics_config` upsert inside a single `db.run`, error propagated. Detector (a) fires on it and the hit is an attribution artifact, not a finding: the bare name `execute` from `conn.execute(..)` resolves to `AgentWorkflowRunner::execute`. Kept at AgentControl regardless — it is a write, and a write is not dropped to Read on a detector verdict in either direction", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "get_column_metrics", + "module": "metrics_commands", + "reason": "conservative-module-default: may steer or arm autonomous work", + "registered": false, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_task_metrics", + "module": "metrics_commands", + "reason": "batch-12 audit: per-task metric read; no write", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "create_ideation_session", + "module": "ideation_commands", + "reason": "detector-c, hand-traced and UNCONDITIONAL: prepare_ideation_analysis_state calls GitService::get_current_branch as the fourth statement of the impl, before any branching, followed immediately by resolve_project_default_branch", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "auditRefusal": { + "batch": "audited and classified 11", + "finding": "plan_reference_import.rs:239 swallows artifact_repo.add_relation after the cloned plan artifact at :233 has already been persisted, so the imported plan loses its derived_from provenance edge while the command returns Ok — and provenance is the whole point of a cross-project import. The durable external_events row is discarded the same way at ideation_commands_cross_project.rs:242", + "reason": "fail-open-until-fixed" + }, + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "create_cross_project_session", + "module": "ideation_commands", + "reason": "conservative-module-default: may steer or arm autonomous work", + "registered": false, + "v1Resolution": "v1-audit-refused" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "migrate_proposals", + "module": "ideation_commands", + "reason": "conservative-module-default: may steer or arm autonomous work", + "registered": false, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_ideation_session", + "module": "ideation_commands", + "reason": "batch-11 audit: one `ideation_session_repo` read plus in-memory title hydration; `agent_planning_session_titles` mutates the returned struct, never the repository", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_ideation_session_with_data", + "module": "ideation_commands", + "reason": "batch-11 audit: same single-session read widened with proposals/dependencies; no write", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_ideation_agent_workspace", + "module": "ideation_commands", + "reason": "batch-11 audit: resolves the linked workspace through `resolve_agent_workspace_target_for_ideation_session` — three repository reads and a pure title helper. NOT the `agent_workspace_response_for_state` hydrator, which is the detector-(c) funnel that forecloses the agent-conversation twins", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "list_ideation_sessions", + "module": "ideation_commands", + "reason": "batch-11 audit: project-scoped session list; no write", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_session_group_counts", + "module": "ideation_commands", + "reason": "batch-11 audit: aggregate count query; no write", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "list_sessions_by_group", + "module": "ideation_commands", + "reason": "batch-11 audit: paged group list; the `group` argument is checked against an allowlist and rejected on miss, so it cannot widen the query", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "archive_ideation_session", + "module": "ideation_commands", + "reason": "detector-c, hand-traced: TaskCleanupService::cleanup_tasks walks and deletes worktrees and branches, then delete_feature_branch runs `git branch -D` for the session's active plan branch", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "reopen_ideation_session", + "module": "ideation_commands", + "reason": "detector-c, hand-traced: SessionReopenService::reopen runs the same cleanup_tasks worktree/branch walk and then delete_feature_branch. Reached through a different helper from create/archive — all three are independently confirmed, none inherited", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "update_ideation_session_title", + "module": "ideation_commands", + "reason": "batch-11 audit: single `ideation_sessions` title write, read back before return; the only discard is the post-commit `app.emit`", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "spawn_session_namer", + "module": "ideation_commands", + "reason": "detector-c, hand-traced: client.spawn_agent resolves the Codex CLI and the node binary for MCP wiring, and the caller selects the harness through the provider_harness argument", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [], + "class": "read", + "command": "get_child_sessions", + "module": "ideation_commands", + "reason": "batch-11 audit: child-session read with purpose filter; no write", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_latest_child_session_id", + "module": "ideation_commands", + "reason": "batch-11 audit: single-id read; the purpose parse is `.transpose()?`, not a default", + "registered": true, + "v1Resolution": "registerable" + }, + { + "auditRefusal": { + "batch": "audited and classified 11", + "finding": "ideation_commands_proposals.rs:38 coerces an unparsable client priority to Priority::Medium instead of rejecting it, and :42/:45/:48 turn a failed serde_json::to_string into an empty string — including affected_paths, the exact value validate_affected_paths_json is later supposed to check, so a serialization failure silently produces an empty path set that passes validation. helpers.rs:462 additionally swallows set_dependencies_acknowledged after the proposal INSERT has already committed", + "reason": "fail-open-until-fixed" + }, + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "create_task_proposal", + "module": "ideation_commands", + "reason": "conservative-module-default: may steer or arm autonomous work", + "registered": false, + "v1Resolution": "v1-audit-refused" + }, + { + "capabilities": [], + "class": "read", + "command": "get_task_proposal", + "module": "ideation_commands", + "reason": "batch-11 audit: one proposal read; no write", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "list_session_proposals", + "module": "ideation_commands", + "reason": "batch-11 audit: session-scoped proposal list; no write", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "mutatesAgentConsumedContent" + ], + "class": "agentControl", + "command": "update_task_proposal", + "module": "ideation_commands", + "reason": "content-surface: updates worker-consumed task proposal", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "mutatesAgentConsumedContent" + ], + "class": "agentControl", + "command": "archive_task_proposal", + "module": "ideation_commands", + "reason": "content-surface: archives a worker-consumed task proposal", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "toggle_proposal_selection", + "module": "ideation_commands", + "reason": "conservative-module-default: may steer or arm autonomous work", + "registered": false, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "set_proposal_selection", + "module": "ideation_commands", + "reason": "conservative-module-default: may steer or arm autonomous work", + "registered": false, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "reorder_proposals", + "module": "ideation_commands", + "reason": "batch-11 audit: the reorder is ONE `UPDATE .. SET sort_order = CASE ..` statement, so there is no half-reordered mid-loop state; failure propagates", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "assess_proposal_priority", + "module": "ideation_commands", + "reason": "batch-11 audit: pure in-process scoring (dependency/critical-path/keyword factors) then one `update_priority` write. Reaches no LLM, harness or process", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "assess_all_priorities", + "module": "ideation_commands", + "reason": "batch-11 audit: same scoring, looped; both fallible calls inside the loop use `?`, so a mid-loop failure returns `Err` rather than a short list as success", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "remove_proposal_dependency", + "module": "ideation_commands", + "reason": "batch-11 audit: one `proposal_dependencies` delete, error propagated", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_proposal_dependencies", + "module": "ideation_commands", + "reason": "batch-11 audit: dependency edge read; no write", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_proposal_dependents", + "module": "ideation_commands", + "reason": "batch-11 audit: reverse dependency edge read; no write", + "registered": true, + "v1Resolution": "registerable" + }, + { + "auditRefusal": { + "batch": "audited and classified 11", + "finding": "ideation_commands_dependencies.rs:117 downgrades the set_dependencies_acknowledged write to a tracing::warn! and returns Ok(DependencyGraphResponse), so the accept-gate flag the command exists to set can silently stay unset. The command is not the pure read its name implies — viewing the graph IS the write, and the write is the part that can vanish", + "reason": "fail-open-until-fixed" + }, + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "analyze_dependencies", + "module": "ideation_commands", + "reason": "conservative-module-default: may steer or arm autonomous work", + "registered": false, + "v1Resolution": "v1-audit-refused" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "apply_proposals_to_kanban", + "module": "ideation_commands", + "reason": "detector-c, hand-traced, three independent sinks: the github_pr_enabled capability probe (ensure_git_worktree), base-branch creation directly controlled by the caller's base_branch_override, and the session-namer agent spawn resolving codex and node", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "restart_ideation_implementation", + "module": "ideation_commands", + "reason": "detector-c, hand-traced: the capability probe, then GitService list/delete_worktree, fetch_origin_branch_strict, and a reset_hard + clean_working_tree pair on the restarted worktree — the destructive end of the range", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [], + "class": "read", + "command": "get_task_blockers", + "module": "ideation_commands", + "reason": "batch-11 audit: blocker read; no write", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_blocked_tasks", + "module": "ideation_commands", + "reason": "batch-11 audit: blocked-task read; no write", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "send_chat_message", + "module": "ideation_commands", + "reason": "conservative-module-default: may steer or arm autonomous work", + "registered": false, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "get_session_messages", + "module": "ideation_commands", + "reason": "conservative-module-default: may steer or arm autonomous work", + "registered": false, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "get_recent_session_messages", + "module": "ideation_commands", + "reason": "conservative-module-default: may steer or arm autonomous work", + "registered": false, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "get_project_messages", + "module": "ideation_commands", + "reason": "conservative-module-default: may steer or arm autonomous work", + "registered": false, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "get_task_messages", + "module": "ideation_commands", + "reason": "conservative-module-default: may steer or arm autonomous work", + "registered": false, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "deletesEntity" + ], + "class": "denied", + "command": "delete_chat_message", + "module": "ideation_commands", + "reason": "deletes a durable entity", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "deletesEntity" + ], + "class": "denied", + "command": "delete_session_messages", + "module": "ideation_commands", + "reason": "deletes a durable entity", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "count_session_messages", + "module": "ideation_commands", + "reason": "conservative-module-default: may steer or arm autonomous work", + "registered": false, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "send_orchestrator_message", + "module": "ideation_commands", + "reason": "conservative-module-default: may steer or arm autonomous work", + "registered": false, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "is_orchestrator_available", + "module": "ideation_commands", + "reason": "conservative-module-default: may steer or arm autonomous work", + "registered": false, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_ideation_settings", + "module": "ideation_commands", + "reason": "batch-11 audit: settings read; the repo maps only `QueryReturnedNoRows` to the default and propagates every other error", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "update_ideation_settings", + "module": "ideation_commands", + "reason": "batch-11 audit: one UPDATE plus a read-back inside a single `db.run`. Declared arms-auto-plan-verification: `auto_verify_draft_plans` written here is the gate `plan_verification_service` reads before launching the verification agent, and no detector models it", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_tasks_disable_impact", + "module": "ideation_commands", + "reason": "batch-11 audit: `TasksFeatureToggleService::get_disable_impact` aggregates counts and returns them; it is the read half of the toggle pair and never emits or persists. Its writing sibling `set_tasks_feature_enabled` is a detector-(c) refusal below", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "set_tasks_feature_enabled", + "module": "ideation_commands", + "reason": "detector-c, hand-traced: toggling the feature ON fans out up to EIGHT Codex agent spawns via reconcile_missing_assessments -> spawn_plan_complexity_assessor, gated only on the caller's `enabled` argument and the prior Disabled state. Its read sibling get_tasks_disable_impact is registered; the writer is foreclosed at every v1 scope", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [], + "class": "read", + "command": "get_agent_lane_settings", + "module": "ideation_commands", + "reason": "batch-11 audit: lane settings read; both branches propagate with `map_err(..)?`", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "get_agent_harness_availability", + "module": "ideation_commands", + "reason": "detector-c: get_harness_availability_for_lanes -> refreshed_provider_aware_runtime_probes -> refresh_supported_harnesses -> refresh_harness_runtime_probe_with_force -> cached_harness_runtime_refresh_probe -> resolved_harness_binary_path -> find_claude_cli. The #976 probe cache keys rows by the resolved binary path, so even the cache-hit branch resolves CLI paths; a READ by intent that reaches the launch floor, and the floor does not care which", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "get_ideation_harness_availability", + "module": "ideation_commands", + "reason": "detector-c: the SAME get_harness_availability_for_lanes chain as get_agent_harness_availability, over IDEATION_LANES instead of AGENT_LANES — one shared helper, two ledger rows, so re-auditing the helper clears both", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "update_agent_lane_settings", + "module": "ideation_commands", + "reason": "batch-11 audit: one lane upsert. Declared arms-agent-spawn-harness: `resolve_agent_spawn_settings` reads this row on the live spawn path to pick the harness, model and effort an agent is actually launched with, and no detector models it", + "registered": true, + "v1Resolution": "registerable" + }, + { + "auditRefusal": { + "batch": "14", + "finding": "manual_role_default_commands.rs:539-546 turns a resolution Err into `effective: None` PLUS an assumed AgentHarnessKind::Claude provider, and control_options at :558 then computes capability and speed availability AGAINST that fabricated default. The caller receives a plausible enabled/disabled control set derived from an error rather than from configuration — an outage changes the ANSWER, not just its completeness. Its two sibling reads take a different path and are registered; fix by propagating the resolution error", + "reason": "fail-open-until-fixed" + }, + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "get_manual_role_defaults", + "module": "manual_role_default_commands", + "reason": "conservative-module-default: may steer or arm autonomous work", + "registered": false, + "v1Resolution": "v1-audit-refused" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "get_effective_manual_role_default", + "module": "manual_role_default_commands", + "reason": "conservative-module-default: may steer or arm autonomous work", + "registered": false, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_start_composer_role_default", + "module": "manual_role_default_commands", + "reason": "batch-14 audit: resolves the backend-owned role default for a NEW conversation and returns it. No write, no launch; every repository error propagates. Deliberately NOT sharing get_manual_role_defaults' catalog_entry fallback, which is why that sibling is refused and this one is registered", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_agent_conversation_role_default", + "module": "manual_role_default_commands", + "reason": "batch-14 audit: the same resolve_composer_role_default read as get_start_composer_role_default, keyed by an existing conversation. Same clean error handling", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "reset_agent_conversation_role_default", + "module": "manual_role_default_commands", + "reason": "detector-c-MISS (M1+M2+M3), hand-traced: service.stop_agent -> running_agent_registry stop -> stop_if_owned -> (self.kill)(pid) -> kill_process -> Command::new(resolve_pkill_cli_path()). The kill is reached through a function VALUE held in a struct field, which is M1 in a shape batch 13 had not seen. It also constructs a full AppChatService unconditionally before knowing whether any agent is live", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "configuresFutureProcessAuthority" + ], + "class": "elevated", + "command": "update_manual_role_default", + "module": "manual_role_default_commands", + "reason": "writes the whole tuple a later spawn consumes through resolve_manual_role_spawn_settings -> ResolvedAgentSpawnSettings: harness, model, effort, service_tier, coordination_mode, persona_id, and critically approval_policy and sandbox_mode. The last two are the spawned agent's security envelope rather than a preference, which is what separates this from the bounded lane/MCP writes batch 13 registered with a declared membership", + "registered": false, + "v1Resolution": "v1-deferred" + }, + { + "capabilities": [ + "configuresFutureProcessAuthority" + ], + "class": "elevated", + "command": "clear_manual_role_default", + "module": "manual_role_default_commands", + "reason": "the destructive half of update_manual_role_default and the more dangerous one: it performs NO validation at all, deleting the row so resolution falls through to project YAML, global, legacy lane, then provider default. A hardened approval_policy/sandbox_mode can therefore be silently downgraded to whatever the fallback layer says, with nothing in the response indicating the demotion", + "registered": false, + "v1Resolution": "v1-deferred" + }, + { + "capabilities": [ + "configuresFutureProcessAuthority" + ], + "class": "denied", + "command": "get_agent_provider_settings", + "module": "harness_provider_commands", + "reason": "configures future provider process authority", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "configuresFutureProcessAuthority" + ], + "class": "denied", + "command": "update_agent_provider_settings", + "module": "harness_provider_commands", + "reason": "configures future provider process authority", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "get_mcp_catalog", + "module": "mcp_policy_commands", + "reason": "detector-c: build_catalog -> discover_provider_catalog -> resolve_codex_catalog_cli_path -> resolve_codex_cli, and the Codex branch then runs discover_native_mcp_servers_via_app_server against that CLI path. A READ by intent that launches the Codex app-server to answer; the floor is absolute and does not care which. Remote readers use the spawn-free get_remote_mcp_catalog snapshot twin", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "refresh_mcp_catalog", + "module": "mcp_policy_commands", + "reason": "detector-c: the SAME build_catalog chain as get_mcp_catalog, reached with an explicit provider rather than an optional one. Remote readers use the spawn-free get_remote_mcp_catalog snapshot twin; refresh remains host-local", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "retry_legacy_mcp_registration_repair", + "module": "mcp_policy_commands", + "reason": "detector-c: ensure_mutation_ready -> resolve_provider_management_eligibility -> refresh_harness_runtime_probe -> resolved_harness_binary_path -> find_claude_cli — the #976 probe-cache path made this command detector-visible, so the row moved from the hand-traced miss to a measured refusal. The ORIGINAL launch it was refused for is still invisible: it runs `claude mcp remove ralphx -s user` through tokio::process::Command::new at infrastructure/agents/claude/mcp_registration_repair.rs, via retry_reserved_claude_registration_repair -> reconcile_reserved_claude_registration -> {resolve_claude_cleanup_cli, remove_reserved_user_registration}, and two mechanisms still hide that path: resolve_claude_cleanup_cli reaches find_claude_cli only by passing it to spawn_blocking as a bare function VALUE, which creates no call edge, and remove_reserved_user_registration spawns an already-resolved path, naming no resolver for the sink model to match. batch13_detector_gap_is_measured_not_inherited pins both mechanisms as still-open root-level gaps. Remote MCP settings use the spawn-free get_remote_mcp_catalog snapshot twin and leave repair unavailable", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "update_mcp_server_override", + "module": "mcp_policy_commands", + "reason": "detector-c: ensure_mutation_ready -> resolve_provider_management_eligibility -> refresh_harness_runtime_probe -> cached_harness_runtime_refresh_probe -> resolved_harness_binary_path -> find_claude_cli. The eligibility guard that runs BEFORE the write resolves the harness binary path even on the cache-hit branch, so the floor forecloses the row at every v1 scope. Declared configures-future-agent-tool-authority — the membership states what the command DOES and survives the class correction", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "clear_mcp_server_override", + "module": "mcp_policy_commands", + "reason": "detector-c: the SAME ensure_mutation_ready -> resolve_provider_management_eligibility -> refresh_harness_runtime_probe chain as update_mcp_server_override. Declared configures-future-agent-tool-authority, as its update half is", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "update_mcp_tool_override", + "module": "mcp_policy_commands", + "reason": "detector-c: the SAME ensure_mutation_ready -> resolve_provider_management_eligibility -> refresh_harness_runtime_probe chain as update_mcp_server_override. Declared configures-future-agent-tool-authority, as its server-scoped sibling is", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "clear_mcp_tool_override", + "module": "mcp_policy_commands", + "reason": "detector-c: the SAME ensure_mutation_ready -> resolve_provider_management_eligibility -> refresh_harness_runtime_probe chain as update_mcp_server_override. Declared configures-future-agent-tool-authority", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "denied", + "command": "get_managed_provider_cli_status", + "module": "provider_cli_management_commands", + "reason": "provider CLI installer surface", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "denied", + "command": "install_or_update_managed_provider_cli", + "module": "provider_cli_management_commands", + "reason": "provider CLI installer surface", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "denied", + "command": "auto_update_managed_provider_clis", + "module": "provider_cli_management_commands", + "reason": "provider CLI installer surface", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [], + "class": "read", + "command": "get_ideation_effort_settings", + "module": "ideation_commands", + "reason": "batch-11 audit: effort read; the `unwrap_or_else` fallbacks fire on an absent row, never on a swallowed `Err`", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "update_ideation_effort_settings", + "module": "ideation_commands", + "reason": "batch-11 audit: read-merge-upsert, every step `?`; effort changes HOW a spawned agent runs, not WHETHER a scheduler launches one", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_ideation_model_settings", + "module": "ideation_commands", + "reason": "batch-11 audit: model read; same absent-row-not-error fallback shape as the effort read", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "update_ideation_model_settings", + "module": "ideation_commands", + "reason": "batch-11 audit: validated then one upsert; both project/global branches `?`", + "registered": true, + "v1Resolution": "registerable" + }, + { + "auditRefusal": { + "batch": "audited and classified 11", + "finding": "session_export_service.rs:291/295/299 decode proposal steps, acceptance_criteria and priority_factors with serde_json::from_str(..).ok(), so a column that fails to parse is exported as absent rather than as an error — the artifact silently loses the fields a re-import would rebuild tasks from. Compounded at :411, where a detected cycle replaces the whole plan version history with Ok(vec![])", + "reason": "fail-open-until-fixed" + }, + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "export_ideation_session", + "module": "ideation_commands", + "reason": "conservative-module-default: may steer or arm autonomous work", + "registered": false, + "v1Resolution": "v1-audit-refused" + }, + { + "auditRefusal": { + "batch": "audited and classified 11", + "finding": "session_export_service.rs:671/675/679 re-serialize proposal steps, acceptance_criteria and priority_factors with .ok() INSIDE the committed import transaction, so those columns land NULL while ImportedSession.proposal_count still reports the proposal as fully imported. The input validation front end is strong (size cap, schema-version pin, cycle and bounds checks); the loss is entirely on the write side", + "reason": "fail-open-until-fixed" + }, + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "import_ideation_session", + "module": "ideation_commands", + "reason": "conservative-module-default: may steer or arm autonomous work", + "registered": false, + "v1Resolution": "v1-audit-refused" + }, + { + "capabilities": [], + "class": "read", + "command": "get_workflows", + "module": "workflow_commands", + "reason": "workflow list: `workflow_repo.get_all` mapped to responses", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_workflow", + "module": "workflow_commands", + "reason": "single workflow read: `workflow_repo.get_by_id`, Option-returning with the repository error propagated rather than read as absent", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "create_workflow", + "module": "workflow_commands", + "reason": "batch-11 audit: builds the workflow from input and creates it; every step propagates. Touches no task and no transition service", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "update_workflow", + "module": "workflow_commands", + "reason": "batch-11 audit: get-or-404 then one update; propagates. Replacing the column set can leave a task's `internal_status` unmapped by any column, but that is a board projection, not a task write — the module reaches neither `task_repo` nor a transition service", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "set_default_workflow", + "module": "workflow_commands", + "reason": "batch-11 audit: clear-then-set default. NOT a fail-open — the error is propagated, not swallowed — but the pair runs under `db.run` (no BEGIN), so a failed second write leaves ZERO defaults. Recorded as a product bug, not a refusal: the local UI reaches the identical path, so refusing it would not fix it", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_active_workflow_columns", + "module": "workflow_commands", + "reason": "active column set: `workflow_repo.get_default` with its error propagated, falling back to the built-in RalphX columns only when no default is SET; `seed_builtin_workflows` is the write half and stays AgentControl", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_builtin_workflows", + "module": "workflow_commands", + "reason": "built-in workflow schemas: constructs three in-process constants and touches no state at all — the command takes no `AppState`", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "seed_builtin_workflows", + "module": "workflow_commands", + "reason": "idempotent built-in workflow seed: for each of the three builtins, creates it only when workflow_repo.get_by_id returns None, so re-running is a no-op returning Ok(0) and a customised builtin is never overwritten; every error propagates with `?`", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "touchesCredentials" + ], + "class": "denied", + "command": "get_linear_integration_settings", + "module": "linear_commands", + "reason": "integration credential surface", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "touchesCredentials" + ], + "class": "denied", + "command": "get_linear_webhook_config", + "module": "linear_commands", + "reason": "integration credential surface", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "touchesCredentials" + ], + "class": "denied", + "command": "save_linear_integration_settings", + "module": "linear_commands", + "reason": "integration credential surface", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "touchesCredentials" + ], + "class": "denied", + "command": "save_linear_webhook_signing_secret", + "module": "linear_commands", + "reason": "integration credential surface", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "touchesCredentials" + ], + "class": "denied", + "command": "search_linear_issues", + "module": "linear_commands", + "reason": "integration credential surface", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "touchesCredentials" + ], + "class": "denied", + "command": "validate_linear_integration", + "module": "linear_commands", + "reason": "integration credential surface", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "touchesCredentials" + ], + "class": "denied", + "command": "disconnect_linear_integration", + "module": "linear_commands", + "reason": "integration credential surface", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "touchesCredentials" + ], + "class": "denied", + "command": "get_clickup_integration_settings", + "module": "clickup_commands", + "reason": "integration credential surface", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "touchesCredentials" + ], + "class": "denied", + "command": "save_clickup_integration_settings", + "module": "clickup_commands", + "reason": "integration credential surface", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "touchesCredentials" + ], + "class": "denied", + "command": "validate_clickup_integration", + "module": "clickup_commands", + "reason": "integration credential surface", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "touchesCredentials" + ], + "class": "denied", + "command": "disconnect_clickup_integration", + "module": "clickup_commands", + "reason": "integration credential surface", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "touchesCredentials" + ], + "class": "denied", + "command": "list_clickup_workspaces", + "module": "clickup_commands", + "reason": "integration credential surface", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "touchesCredentials" + ], + "class": "denied", + "command": "search_clickup_tasks", + "module": "clickup_commands", + "reason": "integration credential surface", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "touchesCredentials" + ], + "class": "denied", + "command": "assign_agent_conversation_granola_note", + "module": "granola_commands", + "reason": "integration credential surface", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "touchesCredentials" + ], + "class": "denied", + "command": "clear_agent_conversation_granola_note", + "module": "granola_commands", + "reason": "integration credential surface", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "touchesCredentials" + ], + "class": "denied", + "command": "get_agent_conversation_granola_note", + "module": "granola_commands", + "reason": "integration credential surface", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "touchesCredentials" + ], + "class": "denied", + "command": "get_granola_integration_settings", + "module": "granola_commands", + "reason": "integration credential surface", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "touchesCredentials" + ], + "class": "denied", + "command": "get_granola_note_detail", + "module": "granola_commands", + "reason": "integration credential surface", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "touchesCredentials" + ], + "class": "denied", + "command": "list_granola_notes", + "module": "granola_commands", + "reason": "integration credential surface", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "touchesCredentials" + ], + "class": "denied", + "command": "refresh_agent_conversation_granola_note", + "module": "granola_commands", + "reason": "integration credential surface", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "touchesCredentials" + ], + "class": "denied", + "command": "save_granola_integration_settings", + "module": "granola_commands", + "reason": "integration credential surface", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "touchesCredentials" + ], + "class": "denied", + "command": "validate_granola_integration_settings", + "module": "granola_commands", + "reason": "integration credential surface", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [], + "class": "read", + "command": "get_artifacts", + "module": "artifact_commands", + "reason": "batch-13 audit: a single artifact_repo.get_by_type behind a parsed filter, error via map_err. The `artifact_type: None` branch returns Ok(vec![]) rather than all artifacts — recorded as a product bug, not a fail-open: it is an unimplemented filter path, not a swallowed host error", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_artifact", + "module": "artifact_commands", + "reason": "batch-13 audit: one artifact_repo.get_by_id; Option is preserved into the response and the repository error propagates through map_err", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_artifact_at_version", + "module": "artifact_commands", + "reason": "batch-13 audit: one artifact_repo.get_by_id_at_version, Option preserved, error mapped", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "mutatesAgentConsumedContent" + ], + "class": "agentControl", + "command": "create_artifact", + "module": "artifact_commands", + "reason": "content-surface: creates worker-consumed artifact of any kind", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "mutatesAgentConsumedContent" + ], + "class": "agentControl", + "command": "update_artifact", + "module": "artifact_commands", + "reason": "content-surface: updates worker-consumed artifact of any kind", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "mutatesAgentConsumedContent" + ], + "class": "agentControl", + "command": "archive_artifact", + "module": "artifact_commands", + "reason": "batch-13 audit: artifact_repo.archive sets archived_at and the repository error propagates with `?`; the follow-up app.emit is `.ok()`-discarded but runs AFTER the write is durable and cannot make a failed archive look successful. Carries MutatesAgentConsumedContent for the same reason create_artifact/update_artifact do — hiding an artifact changes what agents subsequently read", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_artifacts_by_bucket", + "module": "artifact_commands", + "reason": "batch-13 audit: one artifact_repo.get_by_bucket, error mapped", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_artifacts_by_task", + "module": "artifact_commands", + "reason": "batch-13 audit: one artifact_repo.get_by_task, error mapped", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "get_team_artifacts_by_session", + "module": "artifact_commands", + "reason": "conservative-module-default: may steer or arm autonomous work", + "registered": false, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_artifact_version_history", + "module": "artifact_commands", + "reason": "batch-13 audit: one artifact_repo.get_version_history, error mapped", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_buckets", + "module": "artifact_commands", + "reason": "batch-13 audit: one artifact_bucket_repo.get_all, error mapped", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "create_bucket", + "module": "artifact_commands", + "reason": "batch-13 audit: builds an ArtifactBucket from validated accepted-types (an unknown type ERRORS) plus writer/reader lists, then one artifact_bucket_repo.create. Creates a CONTAINER, not agent-consumed content, so it does not take MutatesAgentConsumedContent", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_system_buckets", + "module": "artifact_commands", + "reason": "batch-13 audit: takes NO AppState — a pure function over the compiled-in system bucket table, in the `get_research_presets` shape batch 12 registered", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "mutatesAgentConsumedContent" + ], + "class": "agentControl", + "command": "add_artifact_relation", + "module": "artifact_commands", + "reason": "content-surface: changes worker-consumed artifact relations", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_artifact_relations", + "module": "artifact_commands", + "reason": "batch-13 audit: one artifact_repo.get_relations, error mapped", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "start_research", + "module": "research_commands", + "reason": "durable row write only: builds a ResearchProcess, marks it Running and persists it via process_repo.create with errors propagated. Recorded honestly — no spawn is reached, transitively or otherwise, and no production consumer scans for Running ResearchProcess rows, so this arms nothing today; it is registered as a guarded write, NOT as a research launcher", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "pause_research", + "module": "research_commands", + "reason": "batch-12 audit: guarded Running->Paused entity mutation then one `process_repo` update; a wrong-status caller gets Err, not a silent no-op", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "resume_research", + "module": "research_commands", + "reason": "batch-12 audit: guarded Paused->Running write. Canonicalized with the already registered `start_research`, which reaches the SAME Running value: no production consumer scans for Running ResearchProcess rows — the only reader is startup_cleanup's fail_all_active — so this arms nothing and carries no SeedsSpawnTriggeringState. If a research executor is ever wired, BOTH rows move", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "stop_research", + "module": "research_commands", + "reason": "batch-12 audit: terminal-guarded write recording a user stop; authority-reducing", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_research_processes", + "module": "research_commands", + "reason": "batch-12 audit: list read with an optional status filter that is `parse()?`-rejected on a miss rather than defaulted, so a bad filter cannot silently widen the query", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_research_process", + "module": "research_commands", + "reason": "batch-12 audit: single `process_repo` read; no write", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_research_presets", + "module": "research_commands", + "reason": "batch-12 audit: pure function over ResearchDepthPreset::all(); takes no AppState at all and cannot read or write anything", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_methodologies", + "module": "methodology_commands", + "reason": "methodology list: `methodology_repo.get_all` mapped to responses", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_active_methodology", + "module": "methodology_commands", + "reason": "active methodology read: `methodology_repo.get_active`; the ACTIVATE half writes the active row and stays AgentControl", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "activate_methodology", + "module": "methodology_commands", + "reason": "batch-11 audit: deactivate-previous then activate; every await `?`. Same non-atomic-toggle product bug as `set_default_workflow`, same reasoning", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "deactivate_methodology", + "module": "methodology_commands", + "reason": "batch-11 audit: single-row deactivate with a not-active guard; no partial window", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "deletesEntity" + ], + "class": "denied", + "command": "seed_test_data", + "module": "test_data_commands", + "reason": "test-data mutation is never remotely operable", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "deletesEntity" + ], + "class": "denied", + "command": "seed_visual_audit_data", + "module": "test_data_commands", + "reason": "test-data mutation is never remotely operable", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "deletesEntity" + ], + "class": "denied", + "command": "clear_test_data", + "module": "test_data_commands", + "reason": "test-data mutation is never remotely operable", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "auditRefusal": { + "batch": "1.5 split, classified 10", + "finding": "the facade already registers this exact fn twice — approve_permission_request (AgentControl) and deny_permission_request (Operate) both target it with the decision field server-pinned. Registering the raw name would move branch selection from the command name to a client-supplied argument and collapse the Operate/AgentControl split that pinning exists to enforce", + "reason": "seam-resolved-via-remote-twin" + }, + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "resolve_permission_request", + "module": "permission_commands", + "reason": "approve branch authorizes-live-tool-call; deny branch is authority-reducing", + "registered": false, + "v1Resolution": "v1-audit-refused" + }, + { + "auditRefusal": { + "batch": "1-2", + "finding": "returns Ok(vec![]) when the repository read fails, so an outage is indistinguishable from `no gates are open`; fix by propagating the error", + "reason": "fail-open-until-fixed" + }, + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "get_pending_permissions", + "module": "permission_commands", + "reason": "conservative-module-default: may steer or arm autonomous work", + "registered": false, + "v1Resolution": "v1-audit-refused" + }, + { + "capabilities": [], + "class": "read", + "command": "list_pending_permission_gates", + "module": "permission_commands", + "reason": "pending permission-gate enumeration: fail-closed read of the pending repository plus the in-memory gate map; resolves no gate and arms no scheduling", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "resolve_user_question", + "module": "question_commands", + "reason": "detector-c: steering-question — answering a live gate resumes the agent turn, so the closure resolves resolve_git_cli_path, resolve_node_cli_path and find_codex_cli_candidates; the registered resolve_remote_user_question twin omits handle_accepted_plan_mode_proposal/create_chat_service/kick_runtime_handoff and refuses Plan-mode acceptance fail-closed", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "auditRefusal": { + "batch": "1-2", + "finding": "same Ok(vec![]) shape as get_pending_permissions; fix by propagating the error", + "reason": "fail-open-until-fixed" + }, + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "get_pending_questions", + "module": "question_commands", + "reason": "conservative-module-default: may steer or arm autonomous work", + "registered": false, + "v1Resolution": "v1-audit-refused" + }, + { + "capabilities": [], + "class": "read", + "command": "list_pending_question_gates", + "module": "question_commands", + "reason": "pending question-gate enumeration: fail-closed read of the pending repository plus the in-memory gate map; answers no question and arms no scheduling", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "resolve_remote_user_question", + "module": "remote_question_commands", + "reason": "declared membership: steering-question; spawn-free answer twin omits handle_accepted_plan_mode_proposal, create_chat_service, and kick_runtime_handoff, and refuses Plan-mode acceptance fail-closed before committing the claim", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "list_remote_message_attachments", + "module": "remote_attachment_commands", + "reason": "audited metadata-only attachment repository read; propagates read errors and projects filename, MIME type, size, and identity while omitting the host file path", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "list_remote_queued_agent_messages", + "module": "remote_queue_commands", + "reason": "spawn-free AppState-only queue read; validates a live Project conversation, propagates durable repository errors, merges durable-first, and filters hidden recovery rows", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "operate", + "command": "cancel_remote_queued_agent_message", + "module": "remote_queue_commands", + "reason": "authority-reducing queue removal after live Project-conversation validation; durable-first deletion prevents restart resurrection and cannot add or dispatch agent-consumed content", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "seedsSpawnTriggeringState" + ], + "class": "agentControl", + "command": "request_remote_queued_message_send", + "module": "remote_queue_commands", + "reason": "seeds-spawn-triggering-state: persists an id-only queued SEND-NOW intent; the host dispatcher alone resolves the payload and executes the kill-and-launch seam", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_remote_queued_message_send_request", + "module": "remote_queue_commands", + "reason": "pure repository read of one queued SEND-NOW intent; propagates missing and read failures distinctly", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "seedsSpawnTriggeringState" + ], + "class": "agentControl", + "command": "request_remote_automation_run", + "module": "remote_automation_commands", + "reason": "seeds-spawn-triggering-state, declared membership runs-automation-now-through-host-dispatcher: persists a validated run-now or retry-judge intent; spawn_remote_resume_dispatchers is the sole dispatcher", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_remote_automation_run_request", + "module": "remote_automation_commands", + "reason": "pure repository read of one automation-run intent; propagates read errors", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "mutatesAgentConsumedContent", + "seedsSpawnTriggeringState" + ], + "class": "agentControl", + "command": "request_remote_automation_draft", + "module": "remote_automation_commands", + "reason": "seeds-spawn-triggering-state and content-surface: persists a validated automation-draft intent with a pre-allocated automation id; spawn_remote_resume_dispatchers is the sole dispatcher", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_remote_automation_draft_request", + "module": "remote_automation_commands", + "reason": "pure repository read of one automation-draft intent; propagates read errors", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "mutatesAgentConsumedContent" + ], + "class": "agentControl", + "command": "send_remote_chat_message", + "module": "remote_chat_commands", + "reason": "content-surface, declared membership steers-live-agent-turn: queues a role-pinned user turn for a run that is already live; detector-silent on (a), (b) and (c) — it arms no scheduler, resolves no CLI path, and refuses when no live run would drain the row, so a message can never be persisted as sent yet delivered to nobody. The role is pinned to \"user\" at dispatch, so a remote client cannot forge an orchestrator speaker label", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_remote_agent_conversation", + "module": "remote_transcript_commands", + "reason": "pure repository read of a conversation and its messages; no wake, no spawn; propagates read errors", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_remote_agent_conversation_workspace", + "module": "remote_transcript_commands", + "reason": "recovery-free persisted workspace read via agent_workspace_response_without_repair_recovery_for_state; blanks host paths and propagates read errors", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_remote_agent_conversation_messages_page", + "module": "remote_transcript_commands", + "reason": "pure repository read of a message page; no wake, no spawn; propagates read errors", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_remote_agent_conversation_timeline_page", + "module": "remote_transcript_commands", + "reason": "pure repository read of a timeline page; no wake, no spawn; propagates read errors", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "list_remote_agent_conversations", + "module": "remote_transcript_commands", + "reason": "pure repository read of a context's conversation metadata; no spawn carrier; propagates read errors", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "update_remote_execution_settings", + "module": "remote_execution_settings_commands", + "reason": "persists execution settings and syncs the in-process caps; reaches neither the scheduler kick nor the ideation drain, but a raised cap seeds work a later scheduling pass can launch", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_remote_execution_status", + "module": "remote_execution_status_commands", + "reason": "spawn-free status derivation from DB halt mode plus in-memory registry/atomics; propagates read errors, performs no process inspection, and makes no runtime writes", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_remote_mcp_catalog", + "module": "remote_mcp_policy_commands", + "reason": "pure repository read of one coherent host-built MCP catalog snapshot; no provider readiness or catalog discovery carrier", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_remote_agent_conversation_workspace_change_summary", + "module": "remote_diff_commands", + "reason": "snapshot-only in-memory read of a host-captured workspace change summary; no DiffService, GitService, or CLI resolver carrier", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_remote_agent_conversation_workspace_commit_file_diff", + "module": "remote_diff_commands", + "reason": "snapshot-only in-memory read of one host-captured commit file diff; no DiffService, GitService, or CLI resolver carrier", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_remote_agent_conversation_workspace_cumulative_file_diff", + "module": "remote_diff_commands", + "reason": "snapshot-only in-memory read of one host-captured cumulative file diff; no DiffService, GitService, or CLI resolver carrier", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_remote_agent_conversation_workspace_file_diff", + "module": "remote_diff_commands", + "reason": "snapshot-only in-memory read of one host-captured workspace file diff; no DiffService, GitService, or CLI resolver carrier", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_remote_agent_conversation_workspace_file_diff_page", + "module": "remote_diff_commands", + "reason": "snapshot-only in-memory read keyed by host-captured file page range and ref scope; no DiffService, GitService, or CLI resolver carrier", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_remote_agent_conversation_workspace_review", + "module": "remote_diff_commands", + "reason": "snapshot-only in-memory read of host-captured workspace changes and commits; no DiffService, GitService, or CLI resolver carrier", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "list_remote_projects", + "module": "remote_workspace_commands", + "reason": "pure repository reads of project rows plus stored repository-capability snapshots, including stored remote URLs; performs no live inspection and propagates read errors", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_remote_project", + "module": "remote_workspace_commands", + "reason": "pure repository reads of one project row plus its stored repository-capability snapshot, including stored remote URLs, through the same projection as `list_remote_projects`; performs no live inspection and propagates read errors", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_remote_provider_readiness", + "module": "remote_workspace_commands", + "reason": "pure repository read reduced to two scalars; no CLI probe, no provider identity, model, path, or credential surface", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "list_remote_agent_providers", + "module": "remote_workspace_commands", + "reason": "pure repository read projecting stored provider enablement, default flag, and default model/effort names; no CLI probe, no path, no credential, no process-configuration surface", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "mutatesAgentConsumedContent", + "seedsSpawnTriggeringState" + ], + "class": "agentControl", + "command": "request_remote_agent_conversation_start", + "module": "remote_conversation_start_commands", + "reason": "seeds-spawn-triggering-state: persists a host-validated known-mode start intent a host loop later spawns; validates mode/provider/model/project fail-closed and rejects unknown models rather than passing them to CLI argv; resolves no CLI path and arms no scheduler in-band", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_remote_conversation_start_request", + "module": "remote_conversation_start_commands", + "reason": "pure repository read of one start-intent row; no spawn carrier; propagates read errors", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "mutatesAgentConsumedContent", + "seedsSpawnTriggeringState" + ], + "class": "agentControl", + "command": "request_remote_agent_conversation_message", + "module": "remote_conversation_message_commands", + "reason": "seeds-spawn-triggering-state, declared membership seeds-agent-turn-for-idle-conversation: persists a continuation intent a host loop later sends through the provider-session resume seam; validates conversation ownership, archival, run liveness, provider and model fail-closed and rejects unknown models rather than passing them to CLI argv; has no role field to forge; resolves no CLI path and arms no scheduler in-band", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_remote_conversation_message_request", + "module": "remote_conversation_message_commands", + "reason": "pure repository read of one message-intent row; no spawn carrier; propagates read errors", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "operate", + "command": "request_remote_agent_stop", + "module": "remote_agent_stop_commands", + "reason": "authority-reducing brake: persists a conversation-scoped stop intent a host-owned dispatcher drains; names no pid/run/process, resolves no CLI path, arms nothing, and dedupes per conversation so a second tap joins the in-flight brake", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_remote_agent_stop_request", + "module": "remote_agent_stop_commands", + "reason": "pure repository read of one stop-intent row; no spawn carrier; propagates read errors", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "seedsSpawnTriggeringState" + ], + "class": "agentControl", + "command": "request_remote_agent_conversation_mode_switch", + "module": "remote_conversation_mode_switch_commands", + "reason": "seeds-spawn-triggering-state, declared membership prepares-workspace-for-later-agent-run: persists a target-mode intent a host loop later applies through `switch_agent_conversation_mode_for_state`, whose REJECT policy keeps the process-terminating stop path out of the dispatcher; validates conversation ownership, archival, mode validity and run liveness fail-closed; carries no base/branch/runtime-override field to aim workspace preparation with; resolves no CLI path and arms no scheduler in-band", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_remote_conversation_mode_switch_request", + "module": "remote_conversation_mode_switch_commands", + "reason": "pure repository read of one mode-switch-intent row; no spawn carrier; propagates read errors", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "agentControl", + "command": "set_remote_agent_conversation_muted", + "module": "remote_conversation_lifecycle_commands", + "reason": "spawn-free direct twin: writes mute metadata after projecting workspace state through the recovery-free response builder", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "mutatesAgentConsumedContent" + ], + "class": "agentControl", + "command": "switch_remote_agent_conversation_persona", + "module": "remote_conversation_lifecycle_commands", + "reason": "spawn-free direct twin: rejects running agents and changes prompt content consumed by the next turn", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "seedsSpawnTriggeringState" + ], + "class": "agentControl", + "command": "request_remote_conversation_archive", + "module": "remote_conversation_lifecycle_commands", + "reason": "spawn-free twin persists a host archive lifecycle intent", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "mutatesAgentConsumedContent", + "seedsSpawnTriggeringState" + ], + "class": "agentControl", + "command": "request_remote_conversation_fork", + "module": "remote_conversation_lifecycle_commands", + "reason": "spawn-free twin preallocates a child id and persists a host fork lifecycle intent", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_remote_conversation_lifecycle_request", + "module": "remote_conversation_lifecycle_commands", + "reason": "pure lifecycle intent repository read", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "seedsSpawnTriggeringState" + ], + "class": "agentControl", + "command": "request_remote_execution_resume", + "module": "remote_resume_commands", + "reason": "seeds-spawn-triggering-state, declared membership resumes-execution-through-host-dispatcher: persists a validated execution-resume intent; spawn_remote_resume_dispatchers is the sole spawner", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "seedsSpawnTriggeringState" + ], + "class": "agentControl", + "command": "request_remote_task_resume", + "module": "remote_resume_commands", + "reason": "seeds-spawn-triggering-state, declared membership resumes-task-through-host-dispatcher: persists a validated paused-task intent; spawn_remote_resume_dispatchers is the sole spawner", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "seedsSpawnTriggeringState" + ], + "class": "agentControl", + "command": "request_remote_task_restart", + "module": "remote_resume_commands", + "reason": "seeds-spawn-triggering-state, declared membership restarts-task-through-host-dispatcher: persists a validated stopped-or-failed task intent; spawn_remote_resume_dispatchers is the sole spawner", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "seedsSpawnTriggeringState" + ], + "class": "agentControl", + "command": "request_remote_group_resume", + "module": "remote_resume_commands", + "reason": "seeds-spawn-triggering-state, declared membership resumes-task-group-through-host-dispatcher: persists a validated group intent; spawn_remote_resume_dispatchers is the sole spawner", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "seedsSpawnTriggeringState" + ], + "class": "agentControl", + "command": "request_remote_recovery_prompt_resolution", + "module": "remote_resume_commands", + "reason": "seeds-spawn-triggering-state, declared membership resolves-recovery-through-host-dispatcher: persists a status-and-live-marker validated recovery intent; Restart may execute entry actions and Failed+Restart deletes worktree/branch before resetting retry authority; spawn_remote_resume_dispatchers is the sole dispatcher", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_remote_execution_resume_request", + "module": "remote_resume_commands", + "reason": "pure repository read of one execution-resume intent; propagates read errors", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_remote_task_action_request", + "module": "remote_resume_commands", + "reason": "pure repository read of one task-action intent; propagates read errors", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "seedsSpawnTriggeringState" + ], + "class": "agentControl", + "command": "request_remote_plan_approval", + "module": "remote_plan_commands", + "reason": "seeds-spawn-triggering-state, declared membership approves-plan-through-host-dispatcher: persists a validated plan-approval intent; host approval can fan a Codex complexity assessor when tasks_enabled; spawn_remote_resume_dispatchers is the sole dispatcher", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_remote_plan_approval_request", + "module": "remote_plan_commands", + "reason": "pure repository read of one plan-approval intent; propagates read errors", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "mutatesAgentConsumedContent" + ], + "class": "agentControl", + "command": "request_remote_plan_artifact_edit", + "module": "remote_plan_commands", + "reason": "content-surface: persists a host-applied plan edit intent; expected_version is checked at request and claim to prevent silent clobber, while caller session and agent mutation provenance are host-forced absent", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_remote_plan_edit_request", + "module": "remote_plan_commands", + "reason": "pure repository read of one plan-edit intent; propagates missing and read failures distinctly", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "seedsSpawnTriggeringState" + ], + "class": "agentControl", + "command": "request_remote_ideation_finalize_decision", + "module": "remote_ideation_commands", + "reason": "seeds-spawn-triggering-state, declared membership records accept-or-reject finalize intent; host accept creates tasks and arms the ready scheduler; spawn_remote_resume_dispatchers is the sole dispatcher", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_remote_ideation_finalize_request", + "module": "remote_ideation_commands", + "reason": "pure repository read of one finalize-decision intent; propagates read errors", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "list_remote_agent_conversations_page", + "module": "remote_transcript_commands", + "reason": "pure repository read of a conversation-list page; no spawn carrier; propagates read errors", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "list_remote_agent_sidebar_conversations", + "module": "remote_transcript_commands", + "reason": "Agents-sidebar inbox read over conversation/workspace/run repositories, hydrated through the recovery-free workspace seam so it schedules NO PR-supervision recovery and reaches no CLI resolver; the host worktree_path is blanked at the facade; propagates read errors. The local list_agent_sidebar_conversations stays host-denied because it DOES schedule recovery", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_task_context", + "module": "task_context_commands", + "reason": "batch-13 audit: TaskContextService::get_task_context over five repositories; the error match discriminates NotFound from other AppError variants and both propagate. No write", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_artifact_full", + "module": "task_context_commands", + "reason": "batch-13 audit: artifact_repo.get_by_id with the repository error mapped and the absent row turned into an explicit NOT-FOUND message, never an empty success", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_artifact_version", + "module": "task_context_commands", + "reason": "batch-13 audit: artifact_repo.get_by_id_at_version, same explicit-absence shape", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_related_artifacts", + "module": "task_context_commands", + "reason": "batch-13 audit: one artifact_repo.get_related, error mapped", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "search_artifacts", + "module": "task_context_commands", + "reason": "batch-13 audit: fans out get_by_type over the requested types and filters in memory; the type parse propagates with `?`, so an unknown type ERRORS. Its HTTP namesake in http_server/handlers/worker.rs silently skips unparsable types — the Tauri command audited here is the fail-closed half", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_task_agent_workspace", + "module": "task_commands", + "reason": "workspace-association read: joins the task's plan-branch and agent-conversation workspace rows; resolves no CLI and touches no filesystem", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "start_agent_conversation", + "module": "unified_chat_commands", + "reason": "detector-c: conversation start reaches the same three CLI resolvers as the send path", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "abort_seeded_agent_conversation", + "module": "unified_chat_commands", + "reason": "cancels a NEVER-STARTED seeded conversation and the resources minted while preparing its first send. Guarded fail-closed: it refuses with SeededAgentConversationAlreadyStarted if the conversation has any message, any run, or any provider/claude session id, so it can never reach a conversation with history; every step propagates by `?` and it launches nothing", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "fork_agent_conversation", + "module": "unified_chat_commands", + "reason": "detector-c-MISS (M2), hand-traced: prepare_agent_conversation_workspace_* runs GitService get_current_branch/ensure_local_branch_from_origin_if_missing/ get_branch_sha/create_worktree, then backgrounds run_pre_execution_setup, which executes the project's setup commands through a shell AFTER the command has returned", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "switch_agent_conversation_mode", + "module": "unified_chat_commands", + "reason": "detector-c: the running-agent-stopping mode switch prepares the conversation workspace (GitService::ref_exists) and reaches the publish path's inspect_repository_capability -> ensure_git_worktree", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "switch_agent_conversation_persona", + "module": "unified_chat_commands", + "reason": "detector-c-MISS (M1+M2+M3), hand-traced: constructs the chat service and calls stop_agent, reaching kill_process -> Command::new(resolve_pkill_cli_path()) plus a raw SIGTERM that names no binary at all; switch_remote_agent_conversation_persona is the spawn-free twin", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "send_agent_message", + "module": "unified_chat_commands", + "reason": "detector-c: the send path resolves the git, node and Codex CLIs to run the agent turn", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "queue_agent_message", + "module": "unified_chat_commands", + "reason": "conservative-module-default: may steer or arm autonomous work", + "registered": false, + "v1Resolution": "registerable" + }, + { + "auditRefusal": { + "batch": "8, resolved B3a", + "finding": "Wave B3a split the corrected unified_chat_commands/mod.rs:4516 carrier seam; list_remote_queued_agent_messages is the registered spawn-free answer through list_queued_agent_messages_for_state, while the local command deliberately keeps its ChatService path, so registering both names would duplicate one query", + "reason": "seam-resolved-via-remote-twin" + }, + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "get_queued_agent_messages", + "module": "unified_chat_commands", + "reason": "conservative-module-default: may steer or arm autonomous work", + "registered": false, + "v1Resolution": "v1-audit-refused" + }, + { + "capabilities": [ + "deletesEntity" + ], + "class": "denied", + "command": "delete_queued_agent_message", + "module": "unified_chat_commands", + "reason": "deletes a durable entity", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "send_queued_agent_message_now", + "module": "unified_chat_commands", + "reason": "detector-c-MISS (M2+M3), hand-traced, and the highest-authority command in the batch: send_queued_message_with_policy(ManualNow) first stop_agent's the in-flight provider (pkill + SIGTERM) and then send_message's a fresh turn, i.e. launch_plan.spawn(). It does not re-timestamp a row; it interrupts one agent process and starts another", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "auditRefusal": { + "batch": "5, re-affirmed 8", + "finding": "batch 5 split the seam; list_remote_agent_conversations is the registered answer and the local twin deliberately stays off the facade, so registering this name would put two facade paths on one query for no new capability", + "reason": "seam-resolved-via-remote-twin" + }, + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "list_agent_conversations", + "module": "unified_chat_commands", + "reason": "conservative-module-default: may steer or arm autonomous work", + "registered": false, + "v1Resolution": "v1-audit-refused" + }, + { + "auditRefusal": { + "batch": "5, re-affirmed 8", + "finding": "same split seam; list_remote_agent_conversations_page is the registered answer", + "reason": "seam-resolved-via-remote-twin" + }, + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "list_agent_conversations_page", + "module": "unified_chat_commands", + "reason": "conservative-module-default: may steer or arm autonomous work", + "registered": false, + "v1Resolution": "v1-audit-refused" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "list_agent_sidebar_conversations", + "module": "agent_sidebar_commands", + "reason": "detector-c: the sidebar list reaches the same hydrator and its three CLI resolvers", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "set_agent_conversation_muted", + "module": "agent_conversation_mute_commands", + "reason": "detector-c, hand-traced: the mute=true path calls agent_workspace_response_for_state, which schedules PR supervision recovery; recover_agent_workspace_pr_supervision can reach GitService::get_head_sha and can resume agent repair/publication work before the mute metadata row is written; set_remote_agent_conversation_muted is the spawn-free twin", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [], + "class": "read", + "command": "get_bulk_workspace_publication_states", + "module": "agent_sidebar_commands", + "reason": "publication state enum and label per conversation; propagates read errors", + "registered": true, + "v1Resolution": "registerable" + }, + { + "auditRefusal": { + "batch": "4, classified 10", + "finding": "batch 4 split the seam; both this command and the registered get_remote_agent_conversation delegate to the SAME get_agent_conversation_for_app_state seam and return the same payload, so the only difference is that the local name first calls wake_agent_workspace_for_bridge_events — whose error it discards with tracing::warn! and reads anyway. Registering it would put two facade paths on one query while dragging the wake's steer sink onto the facade for no payload", + "reason": "seam-resolved-via-remote-twin" + }, + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "get_agent_conversation", + "module": "unified_chat_commands", + "reason": "conservative-module-default: may steer or arm autonomous work", + "registered": false, + "v1Resolution": "v1-audit-refused" + }, + { + "capabilities": [], + "class": "read", + "command": "get_agent_conversation_summary", + "module": "unified_chat_commands", + "reason": "conversation metadata without messages; propagates read errors", + "registered": true, + "v1Resolution": "registerable" + }, + { + "auditRefusal": { + "batch": "4, classified 10", + "finding": "same split seam and the same discarded wake; get_remote_agent_conversation_messages_page is the registered answer and applies identical limit clamping (unwrap_or(40).clamp(1, 200))", + "reason": "seam-resolved-via-remote-twin" + }, + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "get_agent_conversation_messages_page", + "module": "unified_chat_commands", + "reason": "conservative-module-default: may steer or arm autonomous work", + "registered": false, + "v1Resolution": "v1-audit-refused" + }, + { + "auditRefusal": { + "batch": "4, classified 10", + "finding": "same split seam and the same discarded wake; get_remote_agent_conversation_timeline_page is the registered answer", + "reason": "seam-resolved-via-remote-twin" + }, + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "get_agent_conversation_timeline_page", + "module": "unified_chat_commands", + "reason": "conservative-module-default: may steer or arm autonomous work", + "registered": false, + "v1Resolution": "v1-audit-refused" + }, + { + "capabilities": [], + "class": "read", + "command": "get_agent_message_tool_call_detail", + "module": "unified_chat_commands", + "reason": "WP3 audit: `chat_message_repo` read plus delegated-run reconciliation, all errors propagated (`load_delegated_tool_runtime_snapshot` now returns `AppResult`); no AppHandle/ExecutionState/ChatService, no repository write", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_agent_timeline_item_tool_call_detail", + "module": "unified_chat_commands", + "reason": "WP3 audit: `chat_timeline_repo` read plus the same propagating delegated-run reconciliation; no AppHandle/ExecutionState/ChatService, no repository write", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "get_agent_conversation_workspace", + "module": "unified_chat_commands", + "reason": "detector-c: the workspace hydrator reaches resolve_git_cli_path, resolve_node_cli_path and find_codex_cli_candidates; it also arms, but the process launch is what forecloses every v1 scope; get_remote_agent_conversation_workspace is the registered recovery-free twin", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "denied", + "command": "recheck_pr_health", + "module": "unified_chat_commands", + "reason": "re-polls a workspace pull request's health through the host's gh CLI", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "denied", + "command": "retry_pr_autofix_override", + "module": "unified_chat_commands", + "reason": "overrides a held PR autofix and relaunches the autofix agent on the host checkout", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "denied", + "command": "retry_agent_workspace_publication_effect", + "module": "unified_chat_commands", + "reason": "clears a publication-effect hold and re-runs the reconciler, which republishes through git and gh", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "denied", + "command": "stop_pr_autofix_for_failure", + "module": "unified_chat_commands", + "reason": "stops the held PR autofix generation and leaves auto-merge disabled", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "spawnsProcess", + "seedsSpawnTriggeringState" + ], + "class": "elevated", + "command": "set_agent_conversation_workspace_auto_publish", + "module": "unified_chat_commands", + "reason": "detector-c: resolve_agent_workspace_pr_automation_target -> ensure_linked_plan_branch_agent_worktree -> GitService::get_current_branch. It ALSO arms auto_publish_enabled for the auto-publish freshness scan (detector b, tag retained), but the process launch is what forecloses it at every v1 scope", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "set_agent_conversation_workspace_pr_supervision", + "module": "unified_chat_commands", + "reason": "detector-c: the same resolve_agent_workspace_pr_automation_target worktree path as set_agent_conversation_workspace_auto_publish, genuinely shared", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "spawnsProcess", + "seedsSpawnTriggeringState" + ], + "class": "elevated", + "command": "set_agent_conversation_workspace_review_automation", + "module": "unified_chat_commands", + "reason": "detector-c: agent_workspace_response_for_state -> repair recovery -> CLI path resolution; ALSO detector-b: arms the Auto Review & Fix override consumed by the auto-review spawner (tag retained)", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "list_agent_conversation_workspaces_by_project", + "module": "unified_chat_commands", + "reason": "detector-c: same workspace hydrator, same three CLI resolvers", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [], + "class": "read", + "command": "list_agent_conversation_workspace_publication_events", + "module": "unified_chat_commands", + "reason": "workspace publication event history; propagates read errors", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "get_agent_conversation_workspace_freshness", + "module": "unified_chat_commands", + "reason": "detector-c: compares base against remote via resolve_git_cli_path", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "reconcile_agent_conversation_workspace_publication", + "module": "unified_chat_commands", + "reason": "detector-c: schedule_pr_supervision_recovery_for_conversation_id -> recover_agent_workspace_pr_supervision -> GitService::get_head_sha", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "denied", + "command": "update_agent_conversation_workspace_from_base", + "module": "unified_chat_commands", + "reason": "rewrites an agent workspace checkout from its base branch", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "denied", + "command": "publish_agent_conversation_workspace", + "module": "unified_chat_commands", + "reason": "publishes an agent conversation workspace", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "commit_agent_conversation_workspace_locally", + "module": "unified_chat_commands", + "reason": "detector-c: commit_agent_workspace_locally_unlocked -> GitService::get_head_sha; the command's whole purpose is a local git commit", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "precompute_agent_conversation_workspace_pr_description", + "module": "unified_chat_commands", + "reason": "detector-c, two sinks: draft_agent_workspace_pr_metadata_decision_unlocked runs run_git_text for the diff AND reaches CodexCliClient::spawn_agent -> resolve_codex_cli to draft the description with an agent", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "denied", + "command": "close_agent_workspace_pr", + "module": "unified_chat_commands", + "reason": "closes the remote pull request an agent workspace published", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "denied", + "command": "reopen_agent_workspace_pr", + "module": "unified_chat_commands", + "reason": "reopens the pull request through the host's gh credential and rebuilds the local branch and worktree from origin", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "create_agent_conversation", + "module": "unified_chat_commands", + "reason": "batch-14 audit: inserts the conversation row and, for standalone/persona-builder modes, creates a private workspace directory through standalone_workspace, whose fs calls are containment-checked. Hand-traced clear of all four workspace helper families that make its siblings spawn — it never reaches prepare_agent_conversation_workspace_*, so no worktree and no setup shell", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "update_agent_conversation_coordination_mode", + "module": "unified_chat_commands", + "reason": "detector-c-MISS (M2), hand-traced and CONDITIONAL: selecting CodexNativeUltra reaches codex_ultra_support_for_model -> probe_harness -> probe_codex_cli, which runs up to six `codex` subprocesses (--version, --help, exec --help, features list, debug models) per candidate path on the first uncached call. Solo/Team/Workflow never reach it. Conditional launches still foreclose: the caller picks the mode", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "update_agent_conversation_title", + "module": "unified_chat_commands", + "reason": "batch-14 audit: writes the conversation title and the linked ideation-session title. Recorded and deliberately NOT treated as a blocking fail-open: unified_chat_commands/mod.rs:11652 uses `.ok()?` on the message read used to normalise a Jira key, so a repo error degrades to `no key found`. The command still returns the title it actually stored, so no caller is told a write succeeded that did not — it loses a cosmetic normalisation, not an authority answer", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "archive_agent_conversation", + "module": "unified_chat_commands", + "reason": "detector-c: archive_agent_conversation_for_state -> terminalize_agent_workspace_after_pr -> cleanup_force_owned_terminal_artifacts -> GitService::branch_exists; archiving walks and deletes worktrees and branches", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "restore_agent_conversation", + "module": "unified_chat_commands", + "reason": "batch-14 audit: one restore() un-archiving the conversation row, errors propagated, no launch and no fail-open. The narrowest write in the batch", + "registered": true, + "v1Resolution": "registerable" + }, + { + "auditRefusal": { + "batch": "8", + "finding": "takes execution_state and tauri::AppHandle and calls create_chat_service (unified_chat_commands/mod.rs:9657) purely to reach one getter, handing a read-scoped caller a constructed steer surface", + "reason": "constructs-spawn-capable-service" + }, + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "get_agent_run_status_unified", + "module": "unified_chat_commands", + "reason": "conservative-module-default: may steer or arm autonomous work", + "registered": false, + "v1Resolution": "v1-audit-refused" + }, + { + "capabilities": [], + "class": "read", + "command": "get_agent_run_attribution", + "module": "unified_chat_commands", + "reason": "bounded agent_run_repo lookup of one persisted attribution row; no spawn, no writes", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_agent_run_attributions", + "module": "unified_chat_commands", + "reason": "batched (max 100) agent_run_repo lookup of persisted attribution rows; no spawn, no writes", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "is_chat_service_available", + "module": "unified_chat_commands", + "reason": "detector-c: the harness capability probe resolves the Codex CLI (find_codex_cli_candidates)", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "stop_agent", + "module": "unified_chat_commands", + "reason": "detector-c-MISS (M1+M2), hand-traced: AppChatService::stop_agent drops the InteractiveProcess (EOF to the child's stdin), then running_agent_registry stop -> kill_process -> Command::new(resolve_pkill_cli_path()).args([\"-TERM\", \"-P\", pid]) plus nix SIGTERM. It terminates the agent child AND its whole child tree including the MCP node servers — not in-memory state", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "is_agent_running", + "module": "unified_chat_commands", + "reason": "detector-c: the read-only registry cleanup path resolves the process-kill CLIs (resolve_pkill/taskkill/tasklist) to reap dead entries", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "get_agent_running_states", + "module": "unified_chat_commands", + "reason": "detector-c: same read-only registry cleanup path, same process-kill CLI resolvers", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "get_agent_conversation_runtime_statuses", + "module": "unified_chat_commands", + "reason": "detector-c: inherits the running-states registry cleanup path and its process-kill CLI resolvers", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [], + "class": "read", + "command": "get_agent_conversation_runtime_index", + "module": "unified_chat_commands", + "reason": "runtime lifecycle index via the non-mutating direct_agent_running_state_for_context path; propagates read errors", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "ptyControl" + ], + "class": "denied", + "command": "open_agent_terminal", + "module": "agent_terminal_commands", + "reason": "terminal PTY control", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "ptyControl" + ], + "class": "denied", + "command": "write_agent_terminal", + "module": "agent_terminal_commands", + "reason": "terminal PTY control", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "ptyControl" + ], + "class": "denied", + "command": "resize_agent_terminal", + "module": "agent_terminal_commands", + "reason": "terminal PTY control", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "ptyControl" + ], + "class": "denied", + "command": "clear_agent_terminal", + "module": "agent_terminal_commands", + "reason": "terminal PTY control", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "ptyControl" + ], + "class": "denied", + "command": "restart_agent_terminal", + "module": "agent_terminal_commands", + "reason": "terminal PTY control", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "ptyControl" + ], + "class": "denied", + "command": "close_agent_terminal", + "module": "agent_terminal_commands", + "reason": "terminal PTY control", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "writesArbitraryPath" + ], + "class": "denied", + "command": "upload_chat_attachment", + "module": "chat_attachment_commands", + "reason": "attachment filesystem surface", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "writesArbitraryPath" + ], + "class": "denied", + "command": "link_attachments_to_message", + "module": "chat_attachment_commands", + "reason": "attachment filesystem surface", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "writesArbitraryPath" + ], + "class": "denied", + "command": "list_conversation_attachments", + "module": "chat_attachment_commands", + "reason": "attachment filesystem surface", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "writesArbitraryPath" + ], + "class": "denied", + "command": "list_message_attachments", + "module": "chat_attachment_commands", + "reason": "attachment filesystem surface", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "deletesEntity" + ], + "class": "denied", + "command": "delete_chat_attachment", + "module": "chat_attachment_commands", + "reason": "deletes a durable entity", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "configuresFutureProcessAuthority" + ], + "class": "elevated", + "command": "add_conversation_folder_reference", + "module": "conversation_folder_reference_commands", + "reason": "the stored folder_path is read at spawn time by resolve_mcp_filesystem_read_roots_with_folder_references and appended to the MCP filesystem roots enforced for every subsequently spawned agent conversation. Containment is real but stops short: validate_registration_path rejects relative, ParentDir, root, symlink, non-directory and app-data paths, and re-validates on read — but there is NO project-root or allowlist confinement, so any absolute non-root directory (~/.ssh, /etc, another user's project) is accepted. A remote caller could hand a later agent read access to arbitrary host directories. Deferred, not denied: an allowlist confinement unlocks it", + "registered": false, + "v1Resolution": "v1-deferred" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "remove_conversation_folder_reference", + "module": "conversation_folder_reference_commands", + "reason": "soft-deletes ONE folder reference, scoped by both folder_reference_id and conversation_id, and fails closed with NotFound on a missing row; takes no path and reaches no spawn sink. AgentControl because the reference list is read at spawn time to build the MCP filesystem roots, so removing one narrows a future agent's reach — the authority-REDUCING half of the pair whose adding half stays deferred on the missing project-root allowlist", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "list_conversation_folder_references", + "module": "conversation_folder_reference_commands", + "reason": "pure `SELECT ... WHERE removed_at IS NULL` over the folder-reference repository; takes no path, writes nothing, propagates read errors. Discloses the stored host folder_path, on the same owner ruling that lets `list_remote_projects` carry working_directory: the paired device is the user's own machine holding ui:read on their own host", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "list_task_activity_events", + "module": "activity_commands", + "reason": "batch-12 audit: one cursor-paginated `activity_event_repo` read; the limit is clamped to 100 host-side and every error is `map_err(..)?`", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "list_session_activity_events", + "module": "activity_commands", + "reason": "batch-12 audit: the same paginated read keyed by session; no write", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "list_all_activity_events", + "module": "activity_commands", + "reason": "batch-12 audit: unscoped paginated read. Widest reader in the block, but still a read — the filter is the caller's own narrowing and omitting it is already the default", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "count_task_activity_events", + "module": "activity_commands", + "reason": "batch-12 audit: aggregate count; no write", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "count_session_activity_events", + "module": "activity_commands", + "reason": "batch-12 audit: aggregate count; no write", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "denied", + "command": "get_task_file_changes", + "module": "diff_commands", + "reason": "spawns git for task file changes", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "denied", + "command": "get_file_diff", + "module": "diff_commands", + "reason": "spawns git for an arbitrary file diff", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "get_agent_conversation_workspace_review", + "module": "diff_commands", + "reason": "diff getters may spawn git", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "get_agent_conversation_workspace_change_summary", + "module": "diff_commands", + "reason": "diff getters may spawn git", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "get_agent_conversation_workspace_repair_change_summary", + "module": "diff_commands", + "reason": "diff getters may spawn git", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "get_agent_conversation_workspace_pr_annotations", + "module": "diff_commands", + "reason": "diff getters may spawn git", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "get_agent_conversation_workspace_review_hunk_annotations", + "module": "diff_commands", + "reason": "diff getters may spawn git", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "get_agent_conversation_workspace_file_changes", + "module": "diff_commands", + "reason": "diff getters may spawn git", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "get_agent_conversation_workspace_file_diff", + "module": "diff_commands", + "reason": "diff getters may spawn git", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "get_agent_conversation_workspace_commits", + "module": "diff_commands", + "reason": "diff getters may spawn git", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "get_agent_conversation_workspace_commit_file_changes", + "module": "diff_commands", + "reason": "diff getters may spawn git", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "get_agent_conversation_workspace_commit_file_diff", + "module": "diff_commands", + "reason": "diff getters may spawn git", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "get_agent_conversation_workspace_staged_file_changes", + "module": "diff_commands", + "reason": "diff getters may spawn git", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "get_agent_conversation_workspace_unstaged_file_changes", + "module": "diff_commands", + "reason": "diff getters may spawn git", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "get_agent_conversation_workspace_staged_file_diff", + "module": "diff_commands", + "reason": "diff getters may spawn git", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "get_agent_conversation_workspace_unstaged_file_diff", + "module": "diff_commands", + "reason": "diff getters may spawn git", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "get_agent_conversation_workspace_repair_staged_file_changes", + "module": "diff_commands", + "reason": "diff getters may spawn git", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "get_agent_conversation_workspace_repair_unstaged_file_changes", + "module": "diff_commands", + "reason": "diff getters may spawn git", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "get_agent_conversation_workspace_repair_staged_file_diff", + "module": "diff_commands", + "reason": "diff getters may spawn git", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "get_agent_conversation_workspace_repair_unstaged_file_diff", + "module": "diff_commands", + "reason": "diff getters may spawn git", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "get_agent_conversation_workspace_repair_conflict_file_diff", + "module": "diff_commands", + "reason": "diff getters may spawn git", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "get_agent_conversation_workspace_cumulative_file_changes", + "module": "diff_commands", + "reason": "diff getters may spawn git", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "get_agent_conversation_workspace_cumulative_file_diff", + "module": "diff_commands", + "reason": "diff getters may spawn git", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "get_agent_conversation_workspace_file_diff_page", + "module": "diff_commands", + "reason": "diff getters may spawn git", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "get_agent_conversation_workspace_file_content_range", + "module": "diff_commands", + "reason": "diff getters may spawn git", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "get_commit_file_changes", + "module": "diff_commands", + "reason": "diff getters may spawn git", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "get_commit_file_diff", + "module": "diff_commands", + "reason": "diff getters may spawn git", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "detect_merge_conflicts", + "module": "diff_commands", + "reason": "diff getters may spawn git", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "get_conflict_file_diff", + "module": "diff_commands", + "reason": "diff getters may spawn git", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "get_task_commits", + "module": "git_commands", + "reason": "git process and worktree authority", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "get_task_diff_stats", + "module": "git_commands", + "reason": "git process and worktree authority", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "denied", + "command": "resolve_merge_conflict", + "module": "git_commands", + "reason": "destructive merge-conflict resolution", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "retry_merge", + "module": "git_commands", + "reason": "git process and worktree authority", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "denied", + "command": "cleanup_task_branch", + "module": "git_commands", + "reason": "destructive task branch cleanup", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "denied", + "command": "change_project_git_mode", + "module": "git_commands", + "reason": "changes project git authority", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "get_github_branch_overview", + "module": "github_commands", + "reason": "GitHub CLI/network process authority", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "get_github_connection_status", + "module": "github_commands", + "reason": "GitHub CLI/network process authority", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "get_pull_request_detail", + "module": "github_commands", + "reason": "GitHub CLI/network process authority", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "configuresFutureProcessAuthority" + ], + "class": "elevated", + "command": "get_repository_settings", + "module": "repository_settings_commands", + "reason": "configures repository process authority", + "registered": false, + "v1Resolution": "v1-deferred" + }, + { + "capabilities": [ + "configuresFutureProcessAuthority" + ], + "class": "elevated", + "command": "update_repository_settings", + "module": "repository_settings_commands", + "reason": "configures repository process authority", + "registered": false, + "v1Resolution": "v1-deferred" + }, + { + "capabilities": [ + "writesArbitraryPath" + ], + "class": "denied", + "command": "get_database_maintenance_stats", + "module": "database_maintenance_commands", + "reason": "host database file maintenance (stats read + compaction marker) operates on this Mac's SQLite files", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "writesArbitraryPath" + ], + "class": "denied", + "command": "set_database_compaction_pending", + "module": "database_maintenance_commands", + "reason": "host database file maintenance (stats read + compaction marker) operates on this Mac's SQLite files", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [], + "class": "read", + "command": "get_update_channel", + "module": "update_channel_commands", + "reason": "batch-13 audit: one app_state_repo.get projecting update_channel, error mapped", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "set_update_channel", + "module": "update_channel_commands", + "reason": "batch-13 audit: one app_state_repo.set_update_channel with the error propagated — a single audited repository write. The owner settled the authority question and accepted that a paired device may move the host's release train; the host will auto-update and restart, terminating running agents. The accepted authority is AgentControl, and HostManagement is deliberately absent because class_permits rejects it at this class", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "get_plan_branch", + "module": "plan_branch_commands", + "reason": "branch operations spawn git", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "get_plan_branch_by_task_id", + "module": "plan_branch_commands", + "reason": "branch operations spawn git", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "get_project_plan_branches", + "module": "plan_branch_commands", + "reason": "branch operations spawn git", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "enable_feature_branch", + "module": "plan_branch_commands", + "reason": "branch operations spawn git", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [], + "class": "read", + "command": "get_active_plan", + "module": "plan_commands", + "reason": "active-plan read: `active_plan_repo.get` rendered as an Option; selects no plan and records no selection", + "registered": true, + "v1Resolution": "registerable" + }, + { + "auditRefusal": { + "batch": "7", + "finding": "swallows TWO errors — the execution-plan lookup is `if let Ok(Some(ep))` and the follow-up set_execution_plan_id write is discarded with `let _ =` — so a partial write returns Ok(()) while the execution-plan id the Kanban/Graph filters and the scheduler read silently did not move", + "reason": "fail-open-until-fixed" + }, + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "set_active_plan", + "module": "plan_commands", + "reason": "conservative-module-default: may steer or arm autonomous work", + "registered": false, + "v1Resolution": "v1-audit-refused" + }, + { + "capabilities": [], + "class": "read", + "command": "get_active_execution_plan", + "module": "plan_commands", + "reason": "active execution-plan id read: `active_plan_repo.get_execution_plan_id`", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "clear_active_plan", + "module": "plan_commands", + "reason": "authority-reducing plan clear: a single active_plan_repo.clear whose error propagates through map_err. Registered where its WRITE sibling `set_active_plan` is refused, and the asymmetry is the whole finding — set_active_plan additionally derives an execution_plan_id behind `if let Ok(Some(ep))` and discards the follow-up write with `let _ =`; clear touches execution_plan_id not at all", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "list_plan_selector_candidates", + "module": "plan_commands", + "reason": "plan selector candidates: `ideation_session_repo.get_by_project` filtered to Accepted, joined per session with `task_repo.get_by_ideation_session` and scored in process; every repository error propagates", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "touchesCredentials" + ], + "class": "denied", + "command": "list_api_keys", + "module": "api_key_commands", + "reason": "credential surface", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "touchesCredentials" + ], + "class": "denied", + "command": "create_api_key", + "module": "api_key_commands", + "reason": "credential surface", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "touchesCredentials" + ], + "class": "denied", + "command": "revoke_api_key", + "module": "api_key_commands", + "reason": "credential surface", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "touchesCredentials" + ], + "class": "denied", + "command": "rotate_api_key", + "module": "api_key_commands", + "reason": "credential surface", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "touchesCredentials" + ], + "class": "denied", + "command": "update_api_key_projects", + "module": "api_key_commands", + "reason": "credential surface", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "touchesCredentials" + ], + "class": "denied", + "command": "update_api_key_permissions", + "module": "api_key_commands", + "reason": "credential surface", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "touchesCredentials" + ], + "class": "denied", + "command": "get_api_key_audit_log", + "module": "api_key_commands", + "reason": "credential surface", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "get_agent_health", + "module": "diagnostic_commands", + "reason": "diagnostics may spawn provider CLIs", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "denied", + "command": "get_codex_cli_diagnostics", + "module": "diagnostic_commands", + "reason": "spawns the Codex CLI for diagnostics", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "hostManagement" + ], + "class": "elevated", + "command": "log_frontend_error", + "module": "diagnostic_commands", + "reason": "host-only log sink: each call truncates its three fields before tracing::error!, but the caller can invoke it without a count bound and thereby drive unbounded writes through the host tracing/file-log pipeline", + "registered": false, + "v1Resolution": "v1-deferred" + }, + { + "capabilities": [], + "class": "read", + "command": "get_ui_feature_flags", + "module": "ui_commands", + "reason": "batch-13 audit: projects the OnceLock runtime config plus the agent-capability snapshot. Infallible and write-free", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "agentControl" + ], + "class": "agentControl", + "command": "update_ui_feature_flags", + "module": "ui_commands", + "reason": "batch-13 audit: persists the agent-personas override and the team/workflows/autopilot capability gates, each repository error propagated, then republishes the snapshot. Declared configures-future-agent-capability-gates: the personas override changes injected prompt content and the capability gates change which agent modes exist. Registerable at AgentControl for the same reason the MCP override rows are — see update_mcp_server_override on why the literal ConfiguresFutureProcessAuthority reading is unrepresentable here. Recorded product bug: the two writes are not atomic — a failed capability write leaves the personas override already applied to both the repository and the process-global", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "list_ticketing_providers", + "module": "ticketing_commands", + "reason": "audited local settings-repository reads; response summaries omit token_secret_ref and no provider call is made", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "list_ticketing_containers", + "module": "ticketing_commands", + "reason": "audited outbound provider container call spends the host credential; the credential is resolved host-side and does not cross the wire", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "agentControl", + "command": "list_ticketing_columns", + "module": "ticketing_commands", + "reason": "syncs provider statuses into the local ticketing status catalog, changing local catalog rows", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "list_ticketing_status_catalog", + "module": "ticketing_commands", + "reason": "audited local status-catalog repository read; no sync, provider call, credential reference, or write", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "agentControl", + "command": "refresh_ticketing_status_catalog", + "module": "ticketing_commands", + "reason": "fetches provider statuses and changes the local ticketing status catalog", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "agentControl", + "command": "update_ticketing_status_presentation", + "module": "ticketing_commands", + "reason": "changes display order, color, visibility, or terminal presentation in the local ticketing status catalog without an outbound provider write", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "list_tickets", + "module": "ticketing_commands", + "reason": "audited outbound provider ticket-list call spends the host credential; the credential is resolved host-side and does not cross the wire", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "list_ticket_filter_options", + "module": "ticketing_commands", + "reason": "audited outbound provider filter-options call spends the host credential; the credential is resolved host-side and does not cross the wire", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_ticket_detail", + "module": "ticketing_commands", + "reason": "audited outbound provider ticket-detail call spends the host credential; the credential is resolved host-side and does not cross the wire", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "list_ticket_transitions", + "module": "ticketing_commands", + "reason": "audited outbound provider transition-list call spends the host credential; the credential is resolved host-side and does not cross the wire", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_ticket_associations", + "module": "ticketing_commands", + "reason": "audited local link-repository and persisted PR-branch-summary reads; no provider call or credential reference", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "get_conversation_ticket", + "module": "ticketing_commands", + "reason": "audited local conversation-link repository reads; response contains ticket identity only and no credential reference", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "elevated", + "command": "start_ralphx_work_from_ticket", + "module": "ticketing_commands", + "reason": "detector-c, hand-traced: AgentConversationStartService::start reaches the agent conversation process launch chain", + "registered": false, + "v1Resolution": "host-denied-spawns-process" + }, + { + "capabilities": [], + "class": "read", + "command": "refresh_tickets", + "module": "ticketing_commands", + "reason": "audited capability-free clock response; validates provider and returns now_string without state, network, or credential access", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "agentControl", + "command": "transition_ticket_status", + "module": "ticketing_commands", + "reason": "changes the ticket workflow status on the provider", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "agentControl", + "command": "assign_ticket", + "module": "ticketing_commands", + "reason": "changes the ticket assignee on the provider to the credential owner", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "agentControl", + "command": "clear_ticket_assignee", + "module": "ticketing_commands", + "reason": "clears the ticket assignee on the provider", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "agentControl", + "command": "add_ticket_comment", + "module": "ticketing_commands", + "reason": "adds a comment to the ticket on the provider", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "agentControl", + "command": "set_ticket_labels", + "module": "ticketing_commands", + "reason": "replaces the ticket labels or tags on the provider", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [], + "class": "read", + "command": "list_ticket_labels", + "module": "ticketing_commands", + "reason": "audited outbound provider label-list call spends the host credential; the credential is resolved host-side and does not cross the wire", + "registered": true, + "v1Resolution": "registerable" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "denied", + "command": "list_workspace_open_targets", + "module": "workspace_open_commands", + "reason": "opens workspace in an external process", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "denied", + "command": "open_agent_conversation_workspace", + "module": "workspace_open_commands", + "reason": "opens workspace in an external process", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "spawnsProcess" + ], + "class": "denied", + "command": "open_agent_conversation_workspace_path", + "module": "workspace_open_commands", + "reason": "opens workspace in an external process", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "touchesCredentials" + ], + "class": "denied", + "command": "get_external_mcp_config", + "module": "external_mcp_commands", + "reason": "external MCP credential surface", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "touchesCredentials" + ], + "class": "denied", + "command": "get_external_mcp_readiness", + "module": "external_mcp_commands", + "reason": "external MCP credential surface", + "registered": false, + "v1Resolution": "host-denied" + }, + { + "capabilities": [ + "touchesCredentials" + ], + "class": "denied", + "command": "update_external_mcp_config", + "module": "external_mcp_commands", + "reason": "external MCP credential surface", + "registered": false, + "v1Resolution": "host-denied" + } + ], + "schemaVersion": 2, + "scope_confinements": [ + { + "argument": "projectId", + "command": "pause_execution", + "reason": "null projectId sweeps every project via project_repo.get_all(); confines the task-transition sweep, NOT the global pause flag — discharged by require_explicit_project_scope" + }, + { + "argument": "projectId", + "command": "stop_execution", + "reason": "null projectId sweeps every project via project_repo.get_all(); confines the task-transition sweep, NOT the global pause flag — discharged by require_explicit_project_scope" + } + ], + "spawn_triggering_state_surface": [ + { + "armedValue": "Ready", + "id": "ready-task", + "readByLoops": [ + "application/ready_task_scheduler.rs::application/ready_task_scheduler.rs:::::spawn_ready_task_scheduler_if_needed@57e1eb6d86c1770f" + ], + "surface": "tasks.internal_status", + "writers": [ + "inject_task", + "move_task", + "restart_task", + "resume_deferred_git_startup" + ] + }, + { + "armedValue": "PendingReview with no fresh/running reviewer", + "id": "pending-review-freshness", + "readByLoops": [ + "application/startup_background.rs::application/startup_background.rs:::::spawn_watchdog@8c28974ee8ca859d" + ], + "surface": "tasks.internal_status + task_status_history.entered_at + agent_runs.status", + "writers": [ + "activate_agent_plan_direct_implementation", + "activate_agent_task_pipeline", + "approve_task_for_review", + "close_agent_workspace_pr", + "commit_agent_conversation_workspace_locally", + "copy_agent_conversation_plan", + "get_agent_conversation_workspace", + "get_agent_conversation_workspace_freshness", + "import_agent_conversation_plan", + "list_agent_conversation_workspaces_by_project", + "list_agent_sidebar_conversations", + "publish_agent_conversation_workspace", + "re_review_task_from_escalated", + "reconcile_agent_conversation_workspace_publication", + "recover_task_execution", + "reject_fix_task", + "reopen_agent_workspace_pr", + "request_task_changes_for_review", + "request_task_changes_from_reviewing", + "resolve_recovery_prompt", + "resolve_user_question", + "resume_deferred_git_startup", + "retry_merge", + "retry_pr_autofix_override", + "set_agent_conversation_muted", + "set_agent_conversation_workspace_auto_publish", + "set_agent_conversation_workspace_pr_supervision", + "set_agent_conversation_workspace_review_automation", + "start_agent_conversation", + "start_ralphx_work_from_ticket", + "stop_pr_autofix_for_failure", + "switch_agent_conversation_mode", + "update_agent_conversation_workspace_from_base" + ] + }, + { + "armedValue": "Active", + "id": "automation-active", + "readByLoops": [ + "application/startup_background.rs::application/startup_background.rs:::::spawn_automation_scheduler@c034c5fc2b8fe7b8" + ], + "surface": "automations.status", + "writers": [ + "finalize_automation", + "resume_automation", + "resume_automation_run" + ] + }, + { + "armedValue": "linked active plan/edit workspace", + "id": "workspace-bridge", + "readByLoops": [ + "application/startup_background.rs::application/startup_background.rs:::::spawn_agent_workspace_bridge_dispatcher@ce779acefa5432" + ], + "surface": "agent_conversation_workspaces.linked_ideation_session_id/status/mode", + "writers": [ + "activate_agent_plan_direct_implementation", + "activate_agent_task_pipeline", + "apply_proposals_to_kanban", + "close_agent_workspace_pr", + "commit_agent_conversation_workspace_locally", + "copy_agent_conversation_plan", + "get_agent_conversation_workspace", + "get_agent_conversation_workspace_freshness", + "import_agent_conversation_plan", + "list_agent_conversation_workspaces_by_project", + "list_agent_sidebar_conversations", + "publish_agent_conversation_workspace", + "recheck_pr_health", + "reconcile_agent_conversation_workspace_publication", + "reopen_agent_workspace_pr", + "resolve_user_question", + "resume_deferred_git_startup", + "retry_pr_autofix_override", + "send_agent_message", + "set_agent_conversation_muted", + "set_agent_conversation_workspace_auto_publish", + "set_agent_conversation_workspace_pr_supervision", + "set_agent_conversation_workspace_review_automation", + "start_agent_conversation", + "start_agent_task_pipeline", + "start_ralphx_work_from_ticket", + "stop_pr_autofix_for_failure", + "switch_agent_conversation_mode", + "update_agent_conversation_workspace_from_base" + ] + }, + { + "armedValue": "unconsumed row", + "id": "external-event-cursor", + "readByLoops": [ + "application/startup_background.rs::application/startup_background.rs:::::spawn_agent_workspace_bridge_dispatcher@ce779acefa5432" + ], + "surface": "external_events rows/cursor", + "writers": [ + "create_cross_project_session", + "create_ideation_session", + "import_ideation_session", + "move_task", + "restart_task", + "resume_deferred_git_startup", + "set_tasks_feature_enabled" + ] + }, + { + "armedValue": "enabled and publishable/needs_agent", + "id": "workspace-auto-publish", + "readByLoops": [ + "commands/agent_workspace_auto_publish.rs::commands/agent_workspace_auto_publish.rs:::::start_agent_workspace_auto_publish_freshness_scan@3a8d62e625ea5914" + ], + "surface": "agent_conversation_workspaces.auto_publish_enabled/publication_push_status", + "writers": [ + "set_agent_conversation_workspace_auto_publish" + ] + }, + { + "armedValue": "true", + "id": "workspace-auto-review", + "readByLoops": [ + "commands/agent_workspace_auto_review.rs::commands/agent_workspace_auto_review.rs:::::spawn_auto_review_for_workspace@a952be79d060c28f" + ], + "surface": "review_settings.require_workspace_review", + "writers": [ + "update_notification_settings", + "update_review_settings" + ] + }, + { + "armedValue": "Pending", + "id": "remote-conversation-message", + "readByLoops": [ + "application/startup_background.rs::application/startup_background.rs:::::spawn_remote_conversation_message_dispatcher@c959b62d91939d1" + ], + "surface": "remote_conversation_message_requests.status", + "writers": [ + "request_remote_agent_conversation_message" + ] + }, + { + "armedValue": "Pending", + "id": "remote-conversation-mode-switch", + "readByLoops": [ + "application/startup_background.rs::application/startup_background.rs:::::spawn_remote_conversation_mode_switch_dispatcher@af26b90d13c69a8c" + ], + "surface": "remote_conversation_mode_switch_requests.status", + "writers": [ + "request_remote_agent_conversation_mode_switch" + ] + }, + { + "armedValue": "Pending", + "id": "remote-conversation-start", + "readByLoops": [ + "application/startup_background.rs::application/startup_background.rs:::::spawn_remote_conversation_start_dispatcher@2212793b1dbfb5d0" + ], + "surface": "remote_conversation_start_requests.status", + "writers": [ + "request_remote_agent_conversation_start" + ] + }, + { + "armedValue": "Pending", + "id": "remote-conversation-lifecycle", + "readByLoops": [ + "application/startup_background.rs::application/startup_background.rs:::::spawn_remote_resume_dispatchers@ced3a7ce75eb6466" + ], + "surface": "remote_conversation_lifecycle_requests.status", + "writers": [ + "request_remote_conversation_archive", + "request_remote_conversation_fork" + ] + }, + { + "armedValue": "Pending", + "id": "remote-execution-resume", + "readByLoops": [ + "application/startup_background.rs::application/startup_background.rs:::::spawn_remote_resume_dispatchers@ced3a7ce75eb6466" + ], + "surface": "remote_resume_requests.status where family=execution", + "writers": [ + "request_remote_execution_resume" + ] + }, + { + "armedValue": "Pending", + "id": "remote-task-action", + "readByLoops": [ + "application/startup_background.rs::application/startup_background.rs:::::spawn_remote_resume_dispatchers@ced3a7ce75eb6466" + ], + "surface": "remote_resume_requests.status where family=task", + "writers": [ + "request_remote_group_resume", + "request_remote_recovery_prompt_resolution", + "request_remote_task_restart", + "request_remote_task_resume" + ] + }, + { + "armedValue": "Pending", + "id": "remote-plan-approval", + "readByLoops": [ + "application/startup_background.rs::application/startup_background.rs:::::spawn_remote_resume_dispatchers@ced3a7ce75eb6466" + ], + "surface": "remote_plan_approval_requests.status", + "writers": [ + "request_remote_plan_approval" + ] + }, + { + "armedValue": "Pending", + "id": "remote-finalize-decision", + "readByLoops": [ + "application/startup_background.rs::application/startup_background.rs:::::spawn_remote_resume_dispatchers@ced3a7ce75eb6466" + ], + "surface": "remote_finalize_decision_requests.status", + "writers": [ + "request_remote_ideation_finalize_decision" + ] + }, + { + "armedValue": "Pending", + "id": "remote-queued-send", + "readByLoops": [ + "application/startup_background.rs::application/startup_background.rs:::::spawn_remote_resume_dispatchers@ced3a7ce75eb6466" + ], + "surface": "remote_queued_send_requests.status", + "writers": [ + "request_remote_queued_message_send" + ] + }, + { + "armedValue": "Pending", + "id": "remote-automation-run", + "readByLoops": [ + "application/startup_background.rs::application/startup_background.rs:::::spawn_remote_resume_dispatchers@ced3a7ce75eb6466" + ], + "surface": "remote_automation_run_requests.status", + "writers": [ + "request_remote_automation_run" + ] + }, + { + "armedValue": "Pending", + "id": "remote-automation-draft", + "readByLoops": [ + "application/startup_background.rs::application/startup_background.rs:::::spawn_remote_resume_dispatchers@ced3a7ce75eb6466" + ], + "surface": "remote_automation_draft_requests.status", + "writers": [ + "request_remote_automation_draft" + ] + } + ], + "worker_task_view_allowlist": [ + "description", + "id", + "ideation_session_id", + "internal_status", + "project_id", + "title" + ] +} diff --git a/docs/generated/remote-coverage-census.json b/docs/generated/remote-coverage-census.json new file mode 100644 index 0000000000..25dc37c40c --- /dev/null +++ b/docs/generated/remote-coverage-census.json @@ -0,0 +1,438 @@ +{ + "schemaVersion": 1, + "generatedBy": "scripts/generate-remote-coverage-census.mjs", + "purpose": "PR 3.1 work manifest: every P-11-unclassified invoke command name, grouped into module-scoped work batches with a recommended registration order. A census and a plan — it registers nothing and decides no final class.", + "scan": { + "invokedCommands": 621, + "dynamicExpressions": 0, + "seamBypasses": 0, + "manifestClassified": 268, + "unclassified": 0, + "pluginCommandNames": 51, + "pluginPackages": 7, + "pluginHostTargetedExceptions": 0, + "pluginLocal": 51, + "line": "PASS: remote transport drift — 621 invoke command name(s), 0 dynamic, 0 seam bypasses; 268 manifest-classified; 0 unclassified (P-11 COMPLETE — permanent zero).\n P-11 census: all 621 names have a reviewed disposition — 301 remote-registered, 33 reason-coded local-only, 51 plugin-local (prefix rule), 236 manifest-classified only, 0 unclassified, 0 suppressions.\n Tauri plugin surface: 51 plugin: command name(s) across 7 imported @tauri-apps/plugin-* package(s), 0 reviewed host-targeted exception(s)." + }, + "totals": { + "invokedCommandNames": 621, + "remoteRegistered": 300, + "localOnlyRows": 34, + "pluginCommandNames": 51, + "pluginPackages": 7, + "pluginLocal": 51, + "pluginHostTargetedExceptions": 0, + "ledgerRows": 610, + "unclassified": 0, + "assignedToBatches": 0, + "batches": 14 + }, + "dispositionModel": { + "registerCandidate": { + "label": "register-candidate", + "rule": "ledgered AgentControl (or lower) with no SpawnsProcess capability — eligible for a hand-audited `remote_commands!` entry under `ui:agent`" + }, + "hostDeniedClass": { + "label": "host-denied (class: denied)", + "rule": "`class_permits` returns false for Denied at any capability set — registering it fails compilation. Resolves for P-11 through the manifest, never through a local-only reason (phase doc key point 6)" + }, + "hostDeniedSpawn": { + "label": "host-denied (SpawnsProcess)", + "rule": "carries `SpawnsProcess`; `class_permits(AgentControl, [SpawnsProcess])` is false and Elevated is a v1 non-goal, so it is not exposable on the v1 facade at any scope (`remote_server/registry.rs` detector-(c) note)" + }, + "v1DeferredElevated": { + "label": "v1-deferred (Elevated)", + "rule": "ledgered Elevated without SpawnsProcess — reachable only under `ui:elevated`, which §1 excludes from v1; deferred, not denied" + }, + "v1AuditRefused": { + "label": "v1-audit-refused (per-command finding)", + "rule": "the class/capability pair would admit a v1 scope, but a recorded audit found a property of the command AS IT STANDS that no v1 scope can accommodate — fail-open, spawn-capable machinery built to serve a read, an unrenderable transport shape, or a registered remote twin that already answers the query. Never used for arming/steering/write refusals: the facade serves 17 `agentControl` ops (Wave F2 added `set_update_channel`), so those stay register-candidates" + }, + "orphan": { + "label": "orphan invoke (no local handler)", + "rule": "invoked by the frontend but absent from `generate_handler!` and from the ledger — it cannot be registered remotely because it does not exist locally either" + } + }, + "dispositionTotals": {}, + "batches": [ + { + "id": "B0", + "order": 1, + "title": "P-11 third-disposition mechanism (prerequisite, no registrations)", + "rationale": "LANDED (PR 3.1-b batch B0). The drift scan used to admit two answers — remote-registered, or client-local with a reason. 162 of the then-419 gap names were neither and never will be: host commands the facade denies (Denied class, SpawnsProcess) or defers (Elevated), and writing them into `local-only-commands.ts` would have put a false statement in a file whose whole value is that its reasons are true. `ralphx_remote_protocol::v1_resolution` now derives the verdict from the ledger row, `capability_ledger_tests` renders it as `v1Resolution` on every manifest row, and the scan reads it as a third classification source. The ratchet moved 419 → 257 with zero registrations. Every later batch's delta is now measurable.", + "retiredBy": null, + "retiredNote": null, + "work": [ + "DONE — `v1_resolution(class, capabilities)` in `ralphx-remote-protocol` derives one of `registerable` / `host-denied` / `host-denied-spawns-process` / `v1-deferred`. The ledger row is the authority; nothing downstream re-derives `class_permits`.", + "DONE — the `Elevated`/v1-deferred disposition rides the SAME manifest path under a distinct reason code, not a side list and not `local-only-commands.ts` (key point 6). CI shrinks it as Elevated rows are reclassified.", + "DONE — 9 new scan self-test cases (26 → 35): each refusal class classifies, a registerable name does not, a name absent from every source stays unclassified, an unknown resolution literal throws, a registered-and-refused row throws, and an absent/shapeless/field-less manifest classifies nothing.", + "DONE — the ratchet held: the baseline shrank 419 → 257 and is still delete-on-zero.", + "NOTE — `host-only-ux` needed no separate annotation list: all 162 manifest-resolvable names carry a Denied or Elevated ledger row already, so the census's taxonomy covers the set with no side file." + ], + "gate": "MET — scan self-test 26 → 35 cases; the PASS line reports 190 manifest-classified and the unclassified count fell 419 → 257, exactly the 162-name manifest-resolved set, with zero registrations.", + "commandCount": 0, + "modules": [], + "dispositionCounts": {}, + "registerCandidates": 0, + "nonRegistering": 0, + "commandsByModule": {}, + "commands": [] + }, + { + "id": "B1", + "order": 2, + "title": "Task core — lifecycle, steps, execution, gates", + "rationale": "The 1.5-A surface already registered the neighbouring commands (`move_task`, `unblock_task`, `answer_user_question`, the brakes), so the injection table, the `authz:` predicate shape and the P-4 parity rows for these argument shapes are proven on this exact module family. Lowest parity risk, highest reuse — the right batch to shake out the per-batch harness before it meets 41-command modules.", + "retiredBy": null, + "retiredNote": null, + "work": [ + "Hand-audit each command's downstream authority (detector (a) transitions, detector (b) spawn-triggering state writes, content-surface writes) and assign class + capability set in `capability_ledger.rs`.", + "P-4 parity rows FIRST (flat args, struct-wrapped, camelCase, `Option`, error path) per C-11.", + "Confirm the brakes in these modules stay `ui:operate` (A-14) and that no arming transition lands below the `AgentControl` floor.", + "DONE (PR 3.1-b batch 10): `question_commands` and `permission_commands` are fully classified and no longer appear in this batch's module list. `resolve_user_question` was corrected in place to `Elevated`/`SpawnsProcess` (`host-denied-spawns-process`) after it was measured reaching `resolve_git_cli_path`, `resolve_node_cli_path` and `find_codex_cli_candidates` while sitting at `AgentControl` — an authority-INCREASING correction that preserved its `steering-question` declared membership. `resolve_permission_request` is `seam-resolved-via-remote-twin`: the facade already registers that exact fn twice, as `approve_permission_request` (AgentControl) and `deny_permission_request` (Operate), with the decision field server-pinned, so registering the raw name would move branch selection to a client-supplied argument." + ], + "gate": "P-17 suite green; P-17b generated scope entries exist for every new AgentControl member; C-9 dual-lens review recorded.", + "commandCount": 0, + "modules": [], + "dispositionCounts": {}, + "registerCandidates": 0, + "nonRegistering": 0, + "commandsByModule": {}, + "commands": [] + }, + { + "id": "B2", + "order": 3, + "title": "Chat + agent conversation surface (unblocks PR 3.2)", + "rationale": "PR 3.2's whole premise is that chat send paths answer `REMOTE_FORBIDDEN` without `ui:agent` rather than `REMOTE_COMMAND_UNAVAILABLE` — which requires them registered. 2.6 shipped the honest interim (composer renders UNAVAILABLE remotely) and its product note says it 'flips with no client change when 3.1 registers them'. This is the batch that flips it, so it must land before 3.2 starts. It is also the highest-risk batch: `send_message` is a detector-(a) steer sink and the module contains the workspace-publish `git push` surface that stays denied.", + "retiredBy": null, + "retiredNote": null, + "work": [ + "Split the module by authority: the send/steer commands register as `AgentControl`; the publish/PR surface (`publish_agent_conversation_workspace`, `update_agent_conversation_workspace_from_base`, `close_agent_workspace_pr`) stays denied, and `set_agent_conversation_workspace_auto_publish` is an already-proven detector-(c) rejection.", + "Verify per command that the process-launch sink sits BEYOND the steer-sink cut (`chat_service.send_message`) rather than inside the command's own closure — the cut is what makes chat send registerable while `resume_task` is not. Any command whose own closure resolves a CLI path is a detector-(c) rejection, not a registration.", + "P-4 rows must cover `SendAgentMessageInput`'s optional/override fields (the `runtimeOverride` vs legacy-field rejection is an error-path parity row).", + "DONE (PR 3.1-b batch 3): `conversation_stats_commands` — all four usage-aggregate reads registered at `ui:read`, so the module no longer appears in this batch's module list. Batch 3's `probe_b2_module_batch_audit` also published detector output for every remaining B2 member; start from it rather than re-deriving. Its headline finding: `get_agent_conversation`, `get_agent_conversation_messages_page` and `get_agent_conversation_timeline_page` — the three transcript reads PR 3.2 needs — all fire detector (a), so they are NOT free reads and need their own hand-trace.", + "DONE (PR 3.1-b batch 9): `agent_sidebar_commands` — `list_agent_sidebar_conversations` resolved as `host-denied-spawns-process`, so the module no longer appears in this batch's module list. Batch 9 also closed eight more B2 members by manifest classification rather than registration: `send_agent_message`, `start_agent_conversation`, `get_agent_conversation_workspace`, `list_agent_conversation_workspaces_by_project`, `get_agent_conversation_workspace_freshness`, `is_chat_service_available`, `is_agent_running`, `get_agent_running_states` and `get_agent_conversation_runtime_statuses` all measurably resolve a CLI path in their OWN closure — which is precisely the detector-(c) rejection this batch's work list predicted, now recorded in the ledger instead of only in a pin. `agent_composer_commands` is also fully retired: batch 8 registered `search_agent_composer_plan_references` at `ui:read` and batch 9 resolved `search_agent_composer_entries` (`host-denied-spawns-process`) and `list_agent_composer_skills` (`v1-audit-refused`, fail-open that reports DISABLED skills as enabled).", + "READ FIRST — `send_agent_message` and `start_agent_conversation` are ledgered `Elevated`/`SpawnsProcess` as of batch 9, so the split-by-authority plan above no longer applies to them unmodified. PR 3.2's premise (chat send answers `REMOTE_FORBIDDEN` rather than `REMOTE_COMMAND_UNAVAILABLE`) needs the process-launch sink moved BEYOND the command's own closure first — the `list_remote_*`/`get_remote_*` seam split is the proven shape for that. Registering them as they stand would fail `detector_c_floors_process_spawn_authority`." + ], + "gate": "P-17 green; C-9 dual-lens review recorded; the five 2.6-surfaced ops resolve per this census's `resolvedItems.unregisteredUiAgentOps`.", + "commandCount": 0, + "modules": [], + "dispositionCounts": {}, + "registerCandidates": 0, + "nonRegistering": 0, + "commandsByModule": {}, + "commands": [] + }, + { + "id": "B3", + "order": 4, + "title": "Review, QA, merge pipeline, validation", + "rationale": "Approval/review commands write the agent-consumed content surface (`MutatesAgentConsumedContent` already appears on 6 of them), which is exactly the capability whose floor P-17d enforces. Grouping them keeps that audit in one review rather than spread across batches.", + "retiredBy": null, + "retiredNote": null, + "work": [ + "Confirm every content-surface writer keeps `MutatesAgentConsumedContent` and lands at or above the AgentControl floor.", + "Check the merge-pipeline members against the destructive-git deny list (`cleanup_task_branch`, `resolve_merge_conflict` are Denied and must not ride in on module similarity).", + "DONE (PR 3.1-b batch 7): `merge_pipeline_commands` — all three hydration/projection reads registered at `ui:read`; `validation_commands` — `get_task_validation_summary` resolved as `host-denied-spawns-process`. Neither module appears in this batch's module list any more. Batch 7 also registered the `review_commands`/`qa_commands` read cluster (11 rows) and published `probe_b3_module_batch_audit`; start from its detector output rather than re-deriving.", + "READ FIRST — batch 7's audit-graph fix changes what a clean probe means. `resolve_dispatch` used to drop every call inside a `commands/` file whose name matched a registered command, which deleted the command→same-named-service delegation edge and made detectors (a)/(b)/(c) vacuously silent for 92 command names. Verdicts taken before that fix are not evidence. `get_task_validation_summary` is the worked example: clean on all three detectors, and shelling out to `git rev-parse HEAD` the whole time.", + "OPEN — a second scanner-scope gap is recorded but NOT fixed: `load_production_sources` walks `src-tauri/src` only, so entity methods defined in the `ralphx-domain` crate are invisible and every call to one falls into the resolver's all-same-name fallback. That is what makes `reopen_issue` read as a detector-(c) spawner when its body is a repository read plus an update. It is refused rather than registered, and deliberately NOT ledgered `SpawnsProcess`, so it stays in the gap until the crate scope is widened.", + "DONE (PR 3.1-b batch 10): `qa_commands` is fully classified and no longer appears in this batch's module list. `retry_qa` and `update_qa_settings` registered at `ui:agent` — the latter with a declared `arms-auto-qa` membership, because it arms through an in-memory `RwLock` that no detector watches — and `skip_qa` is `v1-audit-refused`: it writes every step as `QAStepResult::skipped`, but `QAResults::from_results` then derives `Pending` rather than `Passed`, contradicting the body's own comment. That discrepancy is a live product bug, not only a facade finding." + ], + "gate": "P-17d floor diff clean; C-9 review recorded.", + "commandCount": 0, + "modules": [], + "dispositionCounts": {}, + "registerCandidates": 0, + "nonRegistering": 0, + "commandsByModule": {}, + "commands": [] + }, + { + "id": "B4", + "order": 5, + "title": "Ideation, plans, methodology, workflow", + "rationale": "The largest single module in the gap (42). It is also where the known detector-(c) rejection `apply_proposals_to_kanban` lives, so the batch must be sized to absorb a mid-batch reclassification without stalling the others.", + "retiredBy": "B4", + "retiredNote": null, + "work": [ + "Expect a non-empty detector-(c) rejection subset; record each rejection in the manifest disposition rather than downgrading the class.", + "CORRECTED (Wave D1): `archive_task_proposal` honestly names the archive-only body, inherits the ideation agent default, and is registered at `ui:agent`; the old `delete_` prefix floor no longer misclassifies it.", + "DONE (PR 3.1-b batch 11): the B4 remainder is dispositioned — 19 reads registered at `ui:read`, 14 writers at `ui:agent`, 7 `v1-audit-refused`, 12 `host-denied-spawns-process`. `agent_plan_commands`, `methodology_commands` and `workflow_commands` are fully classified and no longer appear in this batch's module list.", + "READ FIRST — batch 11 hand-traced all twelve detector-(c) hits instead of accepting the probe boolean, and correctly established that all twelve reach a real `Command::new`, so the floor excluded none. `activate_agent_task_pipeline` and `activate_agent_plan_direct_implementation` reach it ONLY through the stale-publish repair probe and are recorded as NARROW, which batch 12 re-confirmed by reconstructing the edge chain.", + "CORRECTION (PR 3.1-b batch 12) — batch 11 also recorded TWO scanner errors as fact, and NEITHER reproduces. `resolve_manual_role_spawn_settings` and `find_node_cli_path`/`ensure_resolved_node_bin_in_path`/`resolved_node_bin_dir` are all launch-free by the engine's own measurement, so the engine agreed with the hand trace all along. The `codex`/`node` tokens batch 11 called artifacts riding on a git command are REAL, and arrive through `CodexCliClient::spawn_agent -> build_codex_internal_mcp_overrides -> find_node_binary`. Do not inherit the artifact claim. The genuine over-attribution is a third mechanism: callees resolve by BARE NAME, so `conn.execute(..)` binds to `AgentWorkflowRunner::execute`. It is pinned by `batch12_detector_attribution_limits_are_measured_not_assumed` and deliberately not fixed — narrowing resolution removes edges, and edges are what the floor is measured from.", + "OPEN — the highest-value fail-open fix in the gap is `ideation_harness_availability.rs:344/:360`: `.ok().flatten()` plus an infallible resolver makes a lane-settings DB error indistinguishable from 'no row configured', so a lane configured to an unavailable Codex reports the Claude default as `available: true`. One propagation fix clears BOTH `get_agent_harness_availability` and `get_ideation_harness_availability`." + ], + "gate": "P-17 green; C-9 review recorded; rejected members appear as manifest dispositions, never as local-only rows.", + "commandCount": 0, + "modules": [], + "dispositionCounts": {}, + "registerCandidates": 0, + "nonRegistering": 0, + "commandsByModule": {}, + "commands": [] + }, + { + "id": "B5", + "order": 6, + "title": "Automation, research, metrics, activity", + "rationale": "Automation run/restart are two of the five 2.6-surfaced ops; the rest are read-shaped commands that were swept to the conservative module default and are the cheapest reclassification wins in the gap.", + "retiredBy": "B5", + "retiredNote": null, + "work": [ + "Re-audit the conservative-module-default rows: a genuinely inert read here may drop to `Read`/`Operate`, but only with sink evidence — the floor may not be undershot.", + "DONE (PR 3.1-b batch 12): all 33 B5 ratchet members are dispositioned — 18 reads registered at `ui:read`, 12 writers at `ui:agent` (4 of them arming, 1 carrying `SeedsSpawnTriggeringState` and 3 carrying `DECLARED_MEMBERSHIPS` rows), 3 `host-denied-spawns-process`. `activity_commands`, `automation_commands`, `metrics_commands` and `research_commands` are fully classified.", + "RESOLVED — the plan asked whether `trigger_automation_run_now` / `restart_automation` have arming targets visible to detector (a). They do not, and the two commands are NOT alike. `trigger_automation_run_now` reaches a real Codex spawn (`dispatch_automation_run_now_action -> spawn_automation_judge_task -> invoke_automation_utility_agent -> CodexCliClient::spawn_agent`) and is refused at the floor with `retry_automation_judge`, which shares that chain. `restart_automation` spawns nothing; it flips `automations.status` to Active, the armed value `spawn_automation_scheduler` scans, and detector (b) misses it because that surface's sole write marker is `reopen_run_corrective`. It is registered at `ui:agent` with a `DECLARED_MEMBERSHIPS` row — NOT with `SeedsSpawnTriggeringState`, which `seeds_spawn_triggering_state_tags_track_detector_b_evidence` defines as detector-(b) evidence — as are `retry_automation_plan_judge` and `skip_automation_judge`. Only `resume_automation_run`, which the detector does flag, earns the capability.", + "NOTE for successors — the four automation arming writes were NOT bought by widening the `automation-active` write-marker list. Markers are matched against every command's closure, so a broader marker moves the floor for members batches 7-11 already dispositioned. Declare the membership instead." + ], + "gate": "P-17 green; C-9 review recorded.", + "commandCount": 0, + "modules": [], + "dispositionCounts": {}, + "registerCandidates": 0, + "nonRegistering": 0, + "commandsByModule": {}, + "commands": [] + }, + { + "id": "B6", + "order": 7, + "title": "Personas, role defaults, MCP policy, review settings", + "rationale": "Configuration-of-future-authority shapes cluster here: a persona/role/policy write does not act now but changes what a later spawn is allowed to do. This is the `update_custom_analysis` family of risk (§3.3 backstop-1 residual), so it gets one focused dual-lens review instead of being sprinkled across batches.", + "retiredBy": null, + "retiredNote": null, + "work": [ + "For each command ask the deferred-authority question explicitly: does this write change what a FUTURE agent process may do? If yes it is at least `AgentControl` with `ConfiguresFutureProcessAuthority`, regardless of how inert the immediate action looks.", + "`delete_persona`-shaped members stay Denied (deletesEntity).", + "DONE (PR 3.1-b batch 13): `persona_commands` (12) and `mcp_policy_commands` (7) are fully classified — 8 reads at `ui:read`, 12 writers at `ui:agent` (8 carrying `MutatesAgentConsumedContent`, 4 carrying `DECLARED_MEMBERSHIPS`), 3 `host-denied-spawns-process`.", + "RESOLVED, and successors must not re-litigate it — the deferred-authority lens above says such a write is 'at least AgentControl with ConfiguresFutureProcessAuthority'. That reading is UNREPRESENTABLE: `class_permits` admits `ConfiguresFutureProcessAuthority` only under `Elevated`, which v1 grants no scope for, so declaring it converts an audited-clean bounded write into a deferral by notation rather than by finding. The idiom that records the same finding at a registerable class is AgentControl plus a `DECLARED_MEMBERSHIPS` row, which is what `update_agent_lane_settings` already carries for picking the harness a live agent is launched with — strictly more deferred authority than an MCP server/tool override. Batch 13 used declarations `configures-future-agent-tool-authority` and `configures-future-agent-capability-gates`.", + "READ FIRST — `get_mcp_catalog` and `refresh_mcp_catalog` are REFUSED at the floor. They are reads by intent, but `build_catalog -> discover_provider_catalog -> resolve_codex_catalog_cli_path` launches the Codex app-server to answer. `retry_legacy_mcp_registration_repair` is ALSO refused, and detector (c) does NOT see it: it runs `claude mcp remove ralphx -s user` through `tokio::process::Command::new`, hidden by a `spawn_blocking(bare_fn)` call shape that creates no edge plus a spawn on an already-resolved path that names no resolver. Pinned by `batch13_detector_gap_is_measured_not_inherited`." + ], + "gate": "P-17 green; C-9 review recorded with the deferred-authority lens explicitly exercised.", + "commandCount": 0, + "modules": [], + "dispositionCounts": {}, + "registerCandidates": 0, + "nonRegistering": 0, + "commandsByModule": {}, + "commands": [] + }, + { + "id": "B7", + "order": 8, + "title": "Artifacts, task context, notifications, app chrome", + "rationale": "The tail. Mixed reads and small writes; also the batch that must decide which names are genuinely CLIENT-LOCAL (updater channel, window/dock chrome) and therefore belong in `local-only-commands.ts` with an honest reason — the only batch expected to add local-only rows.", + "retiredBy": "B7", + "retiredNote": null, + "work": [ + "Split client-local from host-owned per command: `update_channel_commands` and parts of `ui_commands` are plausible `local-only` rows; artifacts and task context are host state and must register or be manifest-disposed.", + "`get_task_context` and the prompt-builder reads are content-surface members (ledger-soundness round found 5 dropped worker content reads) — re-check the surface enumeration before assigning.", + "Every local-only row gets an honest client-local reason; 'hard to classify' is never valid.", + "DONE (PR 3.1-b batch 13): all 33 B7 ratchet members are dispositioned — 24 reads at `ui:read`, 8 writers at `ui:agent`, 1 `v1-deferred`. Zero local-only rows were added, which answers the batch's own open question: the client-local split it anticipated did not survive contact with the commands.", + "RESOLVED — the batch was expected to move `update_channel_commands` and parts of `ui_commands` to `local-only-commands.ts`. Neither is client-local. `get_update_channel`/`set_update_channel` read and write `app_state_repo`, which is HOST state, and `get_ui_feature_flags` projects the host runtime config plus the agent-capability snapshot. Wave F2 records the owner-approved authority decision: `set_update_channel` is an audited `AgentControl` repository write, and a paired device with `ui:agent` may move the host release train despite the accepted auto-update/restart risk.", + "RESOLVED — the plan flagged 5 dropped worker content reads. They are the `task_context_commands` Tauri commands (`get_task_context`, `get_artifact_full`, `get_artifact_version`, `get_related_artifacts`, `search_artifacts`), all registered at `ui:read`. Note their HTTP namesakes in `http_server/handlers/worker.rs` are DIFFERENT functions: the axum `search_artifacts` silently skips unparsable artifact types, while the Tauri command propagates the parse error. Do not reason about one from the other.", + "NOTE — batch 12 measured this block detector-silent and batch 13 re-measured rather than inheriting, which is how it found that detector (b)'s flag on `update_notification_settings` is a bare-name MARKER collision (`update_settings` vs the workspace-auto-review write marker), not a spawn-triggering write. It is registered WITHOUT `SeedsSpawnTriggeringState`; claiming the tag would have passed the evidence test while being false." + ], + "gate": "P-17 green; every new local-only row has a reason; C-9 review recorded.", + "commandCount": 0, + "modules": [], + "dispositionCounts": {}, + "registerCandidates": 0, + "nonRegistering": 0, + "commandsByModule": {}, + "commands": [] + }, + { + "id": "D1", + "order": 9, + "title": "Credential + integration surface (disposition only, no registrations)", + "rationale": "Every member is `TouchesCredentials` or `ConfiguresFutureProcessAuthority`. API-key management is compile-denied from the facade (§4.3) and the integration-settings saves are the round-3 module deny list. Nothing here registers in v1; the entire batch is manifest disposition, so it is pure throughput once B0 lands — 72 names retired with zero registration risk.", + "retiredBy": "B0", + "retiredNote": null, + "work": [ + "Confirm each ledger row already carries the denying capability; add missing rows rather than adding local-only reasons.", + "The ticketing reads are Elevated-not-Denied (they read a credentialed provider): decide once, for the whole module, whether v1 defers them or the reads split from the writes in a later phase. Record the decision in the ledger reason." + ], + "gate": "Manifest regenerated and diff-clean; unclassified count drops by exactly this batch's size; zero new local-only rows.", + "commandCount": 0, + "modules": [], + "dispositionCounts": {}, + "registerCandidates": 0, + "nonRegistering": 0, + "commandsByModule": {}, + "commands": [] + }, + { + "id": "D2", + "order": 10, + "title": "Process-launch getters and git/gh surface (disposition only)", + "rationale": "The 'getter that shells out' family plus the destructive-git and installer surfaces. `SpawnsProcess` is not exposable at any v1 scope, so these are dispositions, not registrations. `get_project`/`list_projects` are carved out into R1 because they are the one case where the spawn is removable rather than inherent.", + "retiredBy": "B0", + "retiredNote": null, + "work": [ + "Verify each row carries `SpawnsProcess` (detector (c) is the floor: a Read/Operate row reaching a launch sink fails CI).", + "`get_task_file_changes` / `get_file_diff` / `get_codex_cli_diagnostics` are the named getter-spawns — they stay denied even though they read like reads." + ], + "gate": "Manifest diff-clean; detector-(c) floor test green; unclassified count drops by exactly this batch's size.", + "commandCount": 0, + "modules": [], + "dispositionCounts": {}, + "registerCandidates": 0, + "nonRegistering": 0, + "commandsByModule": {}, + "commands": [] + }, + { + "id": "R1", + "order": 11, + "title": "`get_project` / `list_projects` — spawn-free read path", + "rationale": "The only commands in the gap whose process authority is INCIDENTAL. Both are pure repository reads; the single spawning field is `repository_capability`, computed per project by shelling out to git in `project_response()`. Removing that inline shell-out makes the highest-traffic read on the whole remote surface registerable as `Read`. See `resolvedItems.projectGetters` for the proposed path and the rejected alternatives.", + "retiredBy": "B0", + "retiredNote": "NOT closed, though: leaving the ratchet is a bookkeeping fact, not an answer. Both names are manifest-classified `host-denied-spawns-process` because the getter shells out TODAY; §5.1's open question is whether to remove the spawn so they can be registered, and that owner call still stands. If it is answered yes, these rows change class and re-enter as registration work.", + "work": [ + "Land the cache-backed capability read (option A in `resolvedItems.projectGetters`) as its own change with its own tests — NOT inside a registration batch.", + "Only after the shell-out is gone: re-run detector (c), drop the `SpawnsProcess` capability, reclassify to `Read`, register both.", + "If option A is rejected by the owner, both names fall back into D2 as v1-deferred dispositions and the frontend project list stays a fetch-route question (3.1 open question 4)." + ], + "gate": "Detector (c) reports no launch sink in either closure; P-4 parity rows for both; the manifest shows class `read` with an empty capability set.", + "commandCount": 0, + "modules": [], + "dispositionCounts": {}, + "registerCandidates": 0, + "nonRegistering": 0, + "commandsByModule": {}, + "commands": [] + }, + { + "id": "D3", + "order": 12, + "title": "Host chrome, terminal, repository settings, test data (disposition only)", + "rationale": "Terminal is the phase doc's worked example of the third disposition: its invoke names resolve for P-11 through the module-`Denied` (`PtyControl`) rows, NEVER through a client-local reason. Test data is hard-denied outright (total-data-loss blast radius). Startup/repository settings are `HostManagement`/`ConfiguresFutureProcessAuthority` — v1-deferred.", + "retiredBy": "B0", + "retiredNote": null, + "work": [ + "Assert the terminal names resolve through the manifest path introduced in B0; a `local-only` row for any of them is a defect, not a shortcut.", + "Keep `report_startup_frontend_milestone` honest: it is a client-originated report about the LOCAL app boot — check whether it is genuinely client-local (local-only row) rather than host-deferred." + ], + "gate": "Manifest diff-clean; a planted local-only row for a terminal command fails CI.", + "commandCount": 0, + "modules": [], + "dispositionCounts": {}, + "registerCandidates": 0, + "nonRegistering": 0, + "commandsByModule": {}, + "commands": [] + }, + { + "id": "A1", + "order": 13, + "title": "Chat attachments — disposition + remote rendering (deferred from 2.6/review-4)", + "rationale": "2.6-a shipped the honest interim: under a remote environment `getImagePreviewSrc()` returns `null`, because `convertFileSrc` mints an `asset://` URL for a path on the CLIENT's filesystem while attachment content lives on the HOST. Wave E6 adds the metadata-only `list_remote_message_attachments` read twin and deliberately omits `filePath`, so paired clients can render filename/MIME/size cards with the existing host-content affordance. Upload, delete, and attachment bytes remain unavailable remotely.", + "retiredBy": "B0", + "retiredNote": "NOT closed: the attachment names leave the ratchet, but remote attachment RENDERING is a fetch route, not an invoke command, and §5.3's `ChatAttachmentGallery.tsx` gap plus the 1.5-C endpoint dependency are untouched by B0.", + "work": [ + "Blocked on 1.5-C: `/remote/v1/attachments/{id}` does not exist on this base (no `attachments` route in `remote_server/`). Do not start A1 until the 1.5 lane lands it.", + "Branch preview-source resolution on env kind in BOTH renderers — `MessageAttachments.tsx:115` and `ChatAttachmentGallery.tsx:97` (2.6 only hardened the first; the gallery still calls `convertFileSrc` unconditionally, which is a live gap this census surfaces).", + "Route the remote branch through the scoped endpoint under `ui:read` with a binary-safe body and 2.7's response-header envelope; never through JSON `/invoke` (C-16).", + "Resolve open question 4 explicitly: extending the §3.5 fetch-route remount allowlist rides 3.1, or it is a separate change against P-1's checked-in allowlist. Record the call.", + "Register the path-free `list_remote_message_attachments` metadata twin; keep upload/delete and byte fetch outside this slice." + ], + "gate": "A remote attachment renders through the scoped endpoint; a local one still uses `convertFileSrc`; P-1 route-allowlist equality still holds; the three commands are manifest-disposed with zero local-only rows.", + "commandCount": 0, + "modules": [], + "dispositionCounts": {}, + "registerCandidates": 0, + "nonRegistering": 0, + "commandsByModule": {}, + "commands": [] + }, + { + "id": "X1", + "order": 14, + "title": "Orphan invokes — no local handler exists", + "rationale": "RESOLVED in PR 3.1-b. Five gap names were absent from `src-tauri/src/commands/registry.rs` `generate_handler!` AND from the ledger (which is exhaustive over it), so every call rejected at runtime with no remote environment involved. The reachability audit found none of the five was wired to any component or event handler — the wrappers were dead code kept alive only by their own unit tests — so all five were resolved by DELETING the call site rather than by minting host authority the product does not use. This batch is now empty and stays here as the record of that call.", + "retiredBy": null, + "retiredNote": null, + "work": [ + "`add_proposal_dependency` — deleted at both call sites (`api/ideation.ts` `dependencies.add`, `api/proposal.ts` `addProposalDependency`) plus the `addDependency` mutation in `hooks/useDependencyGraph.ts`. Its owning hook `useDependencyMutations` had no consumer at all: the UI reads the dependency graph and never writes edges, so there was no product asymmetry to fix by adding the missing command.", + "`create_child_session` / `get_parent_session_context` — deleted from `api/ideation.ts` (zero callers, zero tests). The capability is not lost: both are live HTTP routes (`POST /api/create_child_session`, `GET /api/parent_session_context/:session_id`), which is how the backend actually reaches them.", + "`delete_project` — deleted from `api/projects.ts`; `projectsApi.archive` is the live removal path.", + "`delete_task` — deleted from `api/tasks.ts` and `hooks/useTaskMutation.ts`; it was already `@deprecated Use cleanupTask instead`, and every component destructures `cleanupTaskMutation`.", + "Regression guard: `frontend/src/api/orphan-invokes.test.ts` asserts each wrapper stays absent while its surviving sibling (`remove`, `getChildren`, `archive`, `cleanupTask`) stays present, so the test cannot pass by the namespace disappearing." + ], + "gate": "Each of the five is deleted at the call site with a regression test; the P-11 scan sees zero orphans.", + "commandCount": 0, + "modules": [], + "dispositionCounts": {}, + "registerCandidates": 0, + "nonRegistering": 0, + "commandsByModule": {}, + "commands": [] + } + ], + "commands": [], + "resolvedItems": { + "projectGetters": { + "commands": [ + "get_project", + "list_projects" + ], + "batch": "R1", + "status": "proposal — needs an owner call before 3.1-b starts R1", + "finding": "Both are pure repository reads (`project_commands.rs:211-240`): `project_repo.get_all()` / `get_by_id()`, then `project_response()` per row. The ONLY process authority is one response field — `repository_capability`, produced by `inspect_repository_capability()` (`infrastructure/git_auth.rs`), which runs `git remote get-url origin` and `git remote get-url --push origin` through `resolve_git_cli_path()` with a 5s deadline, once PER PROJECT. That is what makes a getter a `SpawnsProcess`/Elevated command, and it is incidental to the read, not inherent to it.", + "proposal": "Option A — cache the capability, do not compute it in the getter. Persist the inspected `repository_capability` (plus `inspected_at`) alongside the project row, write it from the paths that already have process authority and already shell out (project create/update, `change_project_git_mode`, `setup_gh_git_auth`, `switch_git_origin_to_ssh`, `reanalyze_project`) plus one background refresh whose loop root is declared in the manifest's `background_loop_inventory`, and have `project_response()` READ the cached value. `list_projects`/`get_project` then hold no launch sink in their closure, detector (c) goes quiet, the `SpawnsProcess` capability drops, and both classify as `Read` — registerable on the v1 facade at `ui:read`, with zero `generate_handler!` edits and zero command-fn forks (A-7). The response shape is unchanged, so P-4 parity and every existing caller are untouched; only the freshness semantics change, and a stale-capability value is strictly safer than the current InspectionFailed-on-timeout behaviour (`inspect_repository_capability` already returns `InspectionFailed{message}` rather than erroring, so consumers already handle a non-authoritative value).", + "rejectedAlternatives": [ + "A response projection that omits `repository_capability` for remote callers — that is a command-fn fork, which A-7 forbids, and it would break P-4 byte-identity between local IPC and remote dispatch (the whole point of the parity suite).", + "A pinned facade op — pins fix ARGUMENTS (`approve_permission_request` / `deny_permission_request`), not response shape; there is no pin that removes a field.", + "Registering as Elevated — `ui:elevated` is a §1 v1 non-goal; this would ship a scope nothing can hold.", + "Serving the project list over a remounted fetch route instead — `http_server/handlers/projects.rs` computes the SAME capability inline, so the spawn moves rather than disappears, and it opens 3.1 open question 4 unnecessarily." + ], + "ifRejected": "Both names fall back to D2 as v1-deferred dispositions. That is not cost-free: the project list is the entry point of nearly every remote screen, so a remote client would have to hydrate projects through a fetch route (open question 4) or run with no project list at all." + }, + "unregisteredUiAgentOps": { + "commands": [ + "send_agent_message", + "start_agent_conversation", + "skip_step", + "trigger_automation_run_now", + "restart_automation" + ], + "batches": { + "send_agent_message": "B2", + "start_agent_conversation": "B2", + "skip_step": "B1", + "trigger_automation_run_now": "B5", + "restart_automation": "B5" + }, + "resolutions": { + "send_agent_message": "DEMOTED (batch 9) — `host-denied-spawns-process`. Detector (c) fires on its OWN closure, which is already cut at the `send_message` steer sink: it still reaches `resolve_git_cli_path`, `resolve_node_cli_path` and `find_codex_cli_candidates` by another route. Registering it would fail `detector_c_floors_process_spawn_authority`.", + "start_agent_conversation": "DEMOTED (batch 9) — `host-denied-spawns-process`. Same three resolvers reached from its own cut closure.", + "skip_step": "register (`ui:agent`), pending detector-(c) confirmation", + "trigger_automation_run_now": "register (`ui:agent`), pending detector-(c) confirmation", + "restart_automation": "register (`ui:agent`), pending detector-(c) confirmation" + }, + "status": "PARTIALLY RESOLVED (PR 3.1-b batch 9) — the detector-(c) confirmation this section made mandatory was run. `skip_step`, `trigger_automation_run_now` and `restart_automation` remain registration candidates; `send_agent_message` and `start_agent_conversation` came back POSITIVE and are now manifest-classified `host-denied-spawns-process`. The evidence bullet below claiming the provider launch sits outside chat send's own closure is therefore WRONG and is retained only as the record of what the static read predicted.", + "briefingCorrection": "The 3.1-a brief states three of these five are detector-(c)-rejected. That does not match the code: the detector-(c) trio is `resume_task`, `apply_proposals_to_kanban`, `set_agent_conversation_workspace_auto_publish` (`remote_server/registry.rs` NOT-registered note; `frontend/src/lib/remote/agent-gate.test.ts:114-124` uses exactly those three as the unavailable-by-ABSENCE fixture). None of the five 2.6-surfaced ops appears in that set. The two lists were conflated — they are different trios, and 2.6's tracker note lists the five as ops that 'flip with no client change when 3.1 registers them', i.e. registration is the intended resolution.", + "evidence": [ + "2.6 tracker product note: 'with `ui:agent` granted, chat send / start composer / skip_step / automation run+restart render UNAVAILABLE remotely — send_agent_message etc. are unregistered in 1.5-A's 27-op surface. Honest against this build; flips with no client change when 3.1 registers them.'", + "Phase 3 doc, PR 3.2 key point 4: 'Chat send paths (`start_agent_conversation`, `send_agent_message` + variants, …) are `AgentControl` — a device without `ui:agent` gets `REMOTE_FORBIDDEN`'. `REMOTE_FORBIDDEN` (not `REMOTE_COMMAND_UNAVAILABLE`) is only reachable for a REGISTERED command, so 3.2 requires these registered.", + "All five are ledgered `class: agentControl`, `capabilities: [agentControl]`, reason `conservative-module-default` — none carries `SpawnsProcess`.", + "`send_agent_message` reaches `chat_service.send_message` (`unified_chat_commands/mod.rs`), which is a detector-(a) STEER sink. `all_cut_sinks()` CUTS the closure at steer sinks, so the provider process launch beyond it is outside the command's own closure — which is precisely why chat send is registerable while `resume_task` (whose closure resolves a CLI path directly) is not." + ], + "obligation": "This is a static read of the call graph, not a detector run. 3.1-b must confirm each of the five against the live P-17 detector-(c) output as the first step of its batch, and demote any that come back positive to a manifest disposition — the class is decided by the detector, never by this census.", + "clientImpact": "No client change is needed: `agent-gate.ts` derives availability from ABSENCE in `facade_ops`, so each op flips from `unavailable` to `gated`/`enabled` the moment its registration lands in the regenerated manifest." + }, + "remoteAttachmentRendering": { + "batch": "A1", + "status": "scoped into batch A1; BLOCKED on the 1.5-C endpoint", + "finding": "Deferred here from 2.6-a and the review-4 round. Current behaviour is the honest interim, not a bug: `getImagePreviewSrc()` (`frontend/src/components/Chat/MessageAttachments.tsx:99-116`) returns `null` whenever the active environment is remote, so every host attachment renders as a placeholder card instead of a broken image — `convertFileSrc` would mint an `asset://` URL for a path on the CLIENT's disk while `attachment.filePath` names a file on the HOST.", + "blockers": [ + "`/remote/v1/attachments/{id}` does not exist on this base — there is no attachments route in `src-tauri/src/remote_server/`. It is 1.5-C's deliverable (live in the `rme-pr-1-5` lane). A1 cannot start until it lands.", + "2.7's response-header envelope and a binary-safe body are prerequisites; binary must never travel through JSON `/invoke` (C-16)." + ], + "newGapFoundByThisCensus": "2.6 hardened only ONE of the two renderers. `ChatAttachmentGallery.tsx:97` still calls `convertFileSrc(attachment.filePath)` with no env-kind branch, so the gallery surface renders broken images under a remote environment where `MessageAttachments` renders placeholders. A1 must fix both, and the 2.6 negative test (`host-affordance-gating.test.tsx`, which asserts `convertFileSrc` was NOT called) should be extended to cover the gallery.", + "openQuestion": "Phase-3 open question 4 applies verbatim: attachment rendering is a FETCH route, not an invoke command, and the source does not say whether extending the §3.5 remount allowlist rides 3.1 or requires a separate change against P-1's checked-in allowlist. A1 must record the call before it writes a route.", + "commandSide": "The local attachment commands remain ledgered dispositions: upload/delete retain their filesystem authority and `list_message_attachments` remains denied by its mixed-authority module default. Wave E6 registers the commands-resident, path-free `list_remote_message_attachments` twin at `read`; it serves metadata only, while bytes remain a later binary-envelope slice." + } + } +} diff --git a/docs/generated/remote-coverage-census.md b/docs/generated/remote-coverage-census.md new file mode 100644 index 0000000000..754856c8a2 --- /dev/null +++ b/docs/generated/remote-coverage-census.md @@ -0,0 +1,395 @@ +# PR 3.1 — Facade coverage census (**P-11 COMPLETE**) + +> GENERATED — do not edit by hand. Regenerate: `node scripts/generate-remote-coverage-census.mjs`. Staleness gate: `--check`. +> This is the PR 3.1-a planning artifact. It registers nothing. Every class here is the ledger's CURRENT value; the per-command hand audit (§3.3) and the P-17 detector run own the final one. + +> **P-11 is COMPLETE.** All 621 production invoke command names carry a reviewed disposition: remote-registered, reason-coded local-only, plugin-local by the `plugin:` prefix rule, or manifest-classified (host-denied / v1-deferred / v1-audit-refused). **0 unclassified, 0 dynamic expressions, 0 suppressions.** +> The inventory spans two source sets: `frontend/src`, plus the 7 `@tauri-apps/plugin-*` packages it imports. The Vite alias redirects `@tauri-apps/api/core` for the whole module graph, node_modules included, so those packages' own 51 `plugin:` command names ride the same transport — see §7. +> The ratchet baseline `scripts/remote-transport-drift-baseline.json` is now a PERMANENT ZERO — `check-remote-transport-drift.mjs` fails if it is non-empty and refuses `--update-baseline` when unclassified names exist, so the list cannot quietly regrow. The work-batch sections below are kept as the audit record of how the 499 were resolved. + +## 1. Scan state + +``` +PASS: remote transport drift — 621 invoke command name(s), 0 dynamic, 0 seam bypasses; 268 manifest-classified; 0 unclassified (P-11 COMPLETE — permanent zero). + P-11 census: all 621 names have a reviewed disposition — 301 remote-registered, 33 reason-coded local-only, 51 plugin-local (prefix rule), 236 manifest-classified only, 0 unclassified, 0 suppressions. + Tauri plugin surface: 51 plugin: command name(s) across 7 imported @tauri-apps/plugin-* package(s), 0 reviewed host-targeted exception(s). +``` + +| Measure | Count | Source | +|---|---|---| +| Invoke command names on the transport | 621 | drift scan (AST over `frontend/src` + imported `@tauri-apps/plugin-*`) | +| Dynamic / unresolvable expressions | 0 | drift scan — must stay 0 | +| Transport seam bypasses | 0 | drift scan — must stay 0 | +| Remote-registered (`remote_commands!`) | 300 | `docs/generated/remote-commands.json` | +| Reason-coded local-only rows | 34 | `frontend/src/lib/remote/local-only-commands.ts` | +| `plugin:` names classified by the prefix rule | 51 | `PLUGIN_COMMAND_PREFIX` in `local-only-commands.ts` | +| `plugin:` host-targeted exceptions | 0 | `HOST_TARGETED_PLUGIN_COMMANDS` — reviewed, currently empty | +| Ledger rows (exhaustive over `generate_handler!`) | 610 | `docs/generated/remote-commands.json` | +| Manifest-classified (host-denied / v1-deferred) | 268 | `v1Resolution` in `docs/generated/remote-commands.json` | +| **Unclassified — the 3.1 gap** | **0** | `scripts/remote-transport-drift-baseline.json` | + +## 2. What the gap is made of + +Routing each name mechanically through the ledger splits it into very different kinds of work. B0 has already retired the three non-registerable dispositions from the gap, so they read 0 here — their members now resolve through the manifest and no longer sit in the baseline: + +| Disposition | Count | Rule | +|---|---|---| +| register-candidate | 0 | ledgered AgentControl (or lower) with no SpawnsProcess capability — eligible for a hand-audited `remote_commands!` entry under `ui:agent` | +| host-denied (class: denied) | 0 | `class_permits` returns false for Denied at any capability set — registering it fails compilation. Resolves for P-11 through the manifest, never through a local-only reason (phase doc key point 6) | +| host-denied (SpawnsProcess) | 0 | carries `SpawnsProcess`; `class_permits(AgentControl, [SpawnsProcess])` is false and Elevated is a v1 non-goal, so it is not exposable on the v1 facade at any scope (`remote_server/registry.rs` detector-(c) note) | +| v1-deferred (Elevated) | 0 | ledgered Elevated without SpawnsProcess — reachable only under `ui:elevated`, which §1 excludes from v1; deferred, not denied | +| v1-audit-refused (per-command finding) | 0 | the class/capability pair would admit a v1 scope, but a recorded audit found a property of the command AS IT STANDS that no v1 scope can accommodate — fail-open, spawn-capable machinery built to serve a read, an unrenderable transport shape, or a registered remote twin that already answers the query. Never used for arming/steering/write refusals: the facade serves 17 `agentControl` ops (Wave F2 added `set_update_channel`), so those stay register-candidates | +| orphan invoke (no local handler) | 0 | invoked by the frontend but absent from `generate_handler!` and from the ledger — it cannot be registered remotely because it does not exist locally either | + +**268 invoked names now resolve through the manifest** — host-side commands the facade denies or defers, classified by their ledger row's `v1Resolution` rather than by a registration or a client-local reason (phase-doc key point 6). B0 landed that mechanism and the gap fell 419 → 0 with zero registrations. **The baseline is now empty**: batches B1–B7, D1–D3, R1, A1 and the 3.1-b registration batches resolved every remaining name. + +**0 names are registration candidates** — the gap is closed. The rejection subset this section predicted was real and large: detector (c) refused ledgered-`AgentControl` commands whose process authority the manifest could not see (`resume_task`, `apply_proposals_to_kanban`, `set_agent_conversation_workspace_auto_publish`), and PR 3.1-b batch 14 additionally hand-traced THIRTEEN launches detector (c) could not see at all — `Command::new()` names no resolver, which is how every agent launch in the codebase is written. Detector silence was never sufficient evidence to register. + +## 3. Recommended batch order + +| # | Batch | Title | Cmds | Register-candidates | Not registering | Modules | +|---|---|---|---|---|---|---| +| 1 | `B0` | P-11 third-disposition mechanism (prerequisite, no registrations) | 0 | 0 | 0 | 0 | +| 2 | `B1` | Task core — lifecycle, steps, execution, gates | 0 | 0 | 0 | 0 | +| 3 | `B2` | Chat + agent conversation surface (unblocks PR 3.2) | 0 | 0 | 0 | 0 | +| 4 | `B3` | Review, QA, merge pipeline, validation | 0 | 0 | 0 | 0 | +| 5 | `B4` | Ideation, plans, methodology, workflow | 0 | 0 | 0 | 0 | +| 6 | `B5` | Automation, research, metrics, activity | 0 | 0 | 0 | 0 | +| 7 | `B6` | Personas, role defaults, MCP policy, review settings | 0 | 0 | 0 | 0 | +| 8 | `B7` | Artifacts, task context, notifications, app chrome | 0 | 0 | 0 | 0 | +| 9 | `D1` | Credential + integration surface (disposition only, no registrations) | 0 | 0 | 0 | 0 | +| 10 | `D2` | Process-launch getters and git/gh surface (disposition only) | 0 | 0 | 0 | 0 | +| 11 | `R1` | `get_project` / `list_projects` — spawn-free read path | 0 | 0 | 0 | 0 | +| 12 | `D3` | Host chrome, terminal, repository settings, test data (disposition only) | 0 | 0 | 0 | 0 | +| 13 | `A1` | Chat attachments — disposition + remote rendering (deferred from 2.6/review-4) | 0 | 0 | 0 | 0 | +| 14 | `X1` | Orphan invokes — no local handler exists | 0 | 0 | 0 | 0 | + +Ordering logic: **B0 first** (nothing is measurable without the third disposition) → **B1** (smallest parity risk, reuses 1.5-A's proven injection shapes) → **B2** (unblocks PR 3.2, which cannot start until chat send answers `REMOTE_FORBIDDEN` instead of `REMOTE_COMMAND_UNAVAILABLE`) → **B3–B7** registration batches by falling audit risk → **D1/D2/D3** disposition-only batches, which retire large blocks with zero registration risk and can run in parallel with any registration batch once B0 lands → **R1** (a code change, not a registration, and gated on an owner call) → **A1** (blocked on 1.5-C) → **X1** (live defects, independent of remote work). + +## 4. Batches + +### 1. `B0` — P-11 third-disposition mechanism (prerequisite, no registrations) + +**Commands:** 0 · **Register-candidates:** 0 · **Risk classes:** — + +**Why here:** LANDED (PR 3.1-b batch B0). The drift scan used to admit two answers — remote-registered, or client-local with a reason. 162 of the then-419 gap names were neither and never will be: host commands the facade denies (Denied class, SpawnsProcess) or defers (Elevated), and writing them into `local-only-commands.ts` would have put a false statement in a file whose whole value is that its reasons are true. `ralphx_remote_protocol::v1_resolution` now derives the verdict from the ledger row, `capability_ledger_tests` renders it as `v1Resolution` on every manifest row, and the scan reads it as a third classification source. The ratchet moved 419 → 257 with zero registrations. Every later batch's delta is now measurable. + +**Work:** + +- DONE — `v1_resolution(class, capabilities)` in `ralphx-remote-protocol` derives one of `registerable` / `host-denied` / `host-denied-spawns-process` / `v1-deferred`. The ledger row is the authority; nothing downstream re-derives `class_permits`. +- DONE — the `Elevated`/v1-deferred disposition rides the SAME manifest path under a distinct reason code, not a side list and not `local-only-commands.ts` (key point 6). CI shrinks it as Elevated rows are reclassified. +- DONE — 9 new scan self-test cases (26 → 35): each refusal class classifies, a registerable name does not, a name absent from every source stays unclassified, an unknown resolution literal throws, a registered-and-refused row throws, and an absent/shapeless/field-less manifest classifies nothing. +- DONE — the ratchet held: the baseline shrank 419 → 257 and is still delete-on-zero. +- NOTE — `host-only-ux` needed no separate annotation list: all 162 manifest-resolvable names carry a Denied or Elevated ledger row already, so the census's taxonomy covers the set with no side file. + +**Gate:** MET — scan self-test 26 → 35 cases; the PASS line reports 190 manifest-classified and the unclassified count fell 419 → 257, exactly the 162-name manifest-resolved set, with zero registrations. + +### 2. `B1` — Task core — lifecycle, steps, execution, gates + +**Commands:** 0 · **Register-candidates:** 0 · **Risk classes:** — + +**Why here:** The 1.5-A surface already registered the neighbouring commands (`move_task`, `unblock_task`, `answer_user_question`, the brakes), so the injection table, the `authz:` predicate shape and the P-4 parity rows for these argument shapes are proven on this exact module family. Lowest parity risk, highest reuse — the right batch to shake out the per-batch harness before it meets 41-command modules. + +**Work:** + +- Hand-audit each command's downstream authority (detector (a) transitions, detector (b) spawn-triggering state writes, content-surface writes) and assign class + capability set in `capability_ledger.rs`. +- P-4 parity rows FIRST (flat args, struct-wrapped, camelCase, `Option`, error path) per C-11. +- Confirm the brakes in these modules stay `ui:operate` (A-14) and that no arming transition lands below the `AgentControl` floor. +- DONE (PR 3.1-b batch 10): `question_commands` and `permission_commands` are fully classified and no longer appear in this batch's module list. `resolve_user_question` was corrected in place to `Elevated`/`SpawnsProcess` (`host-denied-spawns-process`) after it was measured reaching `resolve_git_cli_path`, `resolve_node_cli_path` and `find_codex_cli_candidates` while sitting at `AgentControl` — an authority-INCREASING correction that preserved its `steering-question` declared membership. `resolve_permission_request` is `seam-resolved-via-remote-twin`: the facade already registers that exact fn twice, as `approve_permission_request` (AgentControl) and `deny_permission_request` (Operate), with the decision field server-pinned, so registering the raw name would move branch selection to a client-supplied argument. + +**Gate:** P-17 suite green; P-17b generated scope entries exist for every new AgentControl member; C-9 dual-lens review recorded. + +### 3. `B2` — Chat + agent conversation surface (unblocks PR 3.2) + +**Commands:** 0 · **Register-candidates:** 0 · **Risk classes:** — + +**Why here:** PR 3.2's whole premise is that chat send paths answer `REMOTE_FORBIDDEN` without `ui:agent` rather than `REMOTE_COMMAND_UNAVAILABLE` — which requires them registered. 2.6 shipped the honest interim (composer renders UNAVAILABLE remotely) and its product note says it 'flips with no client change when 3.1 registers them'. This is the batch that flips it, so it must land before 3.2 starts. It is also the highest-risk batch: `send_message` is a detector-(a) steer sink and the module contains the workspace-publish `git push` surface that stays denied. + +**Work:** + +- Split the module by authority: the send/steer commands register as `AgentControl`; the publish/PR surface (`publish_agent_conversation_workspace`, `update_agent_conversation_workspace_from_base`, `close_agent_workspace_pr`) stays denied, and `set_agent_conversation_workspace_auto_publish` is an already-proven detector-(c) rejection. +- Verify per command that the process-launch sink sits BEYOND the steer-sink cut (`chat_service.send_message`) rather than inside the command's own closure — the cut is what makes chat send registerable while `resume_task` is not. Any command whose own closure resolves a CLI path is a detector-(c) rejection, not a registration. +- P-4 rows must cover `SendAgentMessageInput`'s optional/override fields (the `runtimeOverride` vs legacy-field rejection is an error-path parity row). +- DONE (PR 3.1-b batch 3): `conversation_stats_commands` — all four usage-aggregate reads registered at `ui:read`, so the module no longer appears in this batch's module list. Batch 3's `probe_b2_module_batch_audit` also published detector output for every remaining B2 member; start from it rather than re-deriving. Its headline finding: `get_agent_conversation`, `get_agent_conversation_messages_page` and `get_agent_conversation_timeline_page` — the three transcript reads PR 3.2 needs — all fire detector (a), so they are NOT free reads and need their own hand-trace. +- DONE (PR 3.1-b batch 9): `agent_sidebar_commands` — `list_agent_sidebar_conversations` resolved as `host-denied-spawns-process`, so the module no longer appears in this batch's module list. Batch 9 also closed eight more B2 members by manifest classification rather than registration: `send_agent_message`, `start_agent_conversation`, `get_agent_conversation_workspace`, `list_agent_conversation_workspaces_by_project`, `get_agent_conversation_workspace_freshness`, `is_chat_service_available`, `is_agent_running`, `get_agent_running_states` and `get_agent_conversation_runtime_statuses` all measurably resolve a CLI path in their OWN closure — which is precisely the detector-(c) rejection this batch's work list predicted, now recorded in the ledger instead of only in a pin. `agent_composer_commands` is also fully retired: batch 8 registered `search_agent_composer_plan_references` at `ui:read` and batch 9 resolved `search_agent_composer_entries` (`host-denied-spawns-process`) and `list_agent_composer_skills` (`v1-audit-refused`, fail-open that reports DISABLED skills as enabled). +- READ FIRST — `send_agent_message` and `start_agent_conversation` are ledgered `Elevated`/`SpawnsProcess` as of batch 9, so the split-by-authority plan above no longer applies to them unmodified. PR 3.2's premise (chat send answers `REMOTE_FORBIDDEN` rather than `REMOTE_COMMAND_UNAVAILABLE`) needs the process-launch sink moved BEYOND the command's own closure first — the `list_remote_*`/`get_remote_*` seam split is the proven shape for that. Registering them as they stand would fail `detector_c_floors_process_spawn_authority`. + +**Gate:** P-17 green; C-9 dual-lens review recorded; the five 2.6-surfaced ops resolve per this census's `resolvedItems.unregisteredUiAgentOps`. + +### 4. `B3` — Review, QA, merge pipeline, validation + +**Commands:** 0 · **Register-candidates:** 0 · **Risk classes:** — + +**Why here:** Approval/review commands write the agent-consumed content surface (`MutatesAgentConsumedContent` already appears on 6 of them), which is exactly the capability whose floor P-17d enforces. Grouping them keeps that audit in one review rather than spread across batches. + +**Work:** + +- Confirm every content-surface writer keeps `MutatesAgentConsumedContent` and lands at or above the AgentControl floor. +- Check the merge-pipeline members against the destructive-git deny list (`cleanup_task_branch`, `resolve_merge_conflict` are Denied and must not ride in on module similarity). +- DONE (PR 3.1-b batch 7): `merge_pipeline_commands` — all three hydration/projection reads registered at `ui:read`; `validation_commands` — `get_task_validation_summary` resolved as `host-denied-spawns-process`. Neither module appears in this batch's module list any more. Batch 7 also registered the `review_commands`/`qa_commands` read cluster (11 rows) and published `probe_b3_module_batch_audit`; start from its detector output rather than re-deriving. +- READ FIRST — batch 7's audit-graph fix changes what a clean probe means. `resolve_dispatch` used to drop every call inside a `commands/` file whose name matched a registered command, which deleted the command→same-named-service delegation edge and made detectors (a)/(b)/(c) vacuously silent for 92 command names. Verdicts taken before that fix are not evidence. `get_task_validation_summary` is the worked example: clean on all three detectors, and shelling out to `git rev-parse HEAD` the whole time. +- OPEN — a second scanner-scope gap is recorded but NOT fixed: `load_production_sources` walks `src-tauri/src` only, so entity methods defined in the `ralphx-domain` crate are invisible and every call to one falls into the resolver's all-same-name fallback. That is what makes `reopen_issue` read as a detector-(c) spawner when its body is a repository read plus an update. It is refused rather than registered, and deliberately NOT ledgered `SpawnsProcess`, so it stays in the gap until the crate scope is widened. +- DONE (PR 3.1-b batch 10): `qa_commands` is fully classified and no longer appears in this batch's module list. `retry_qa` and `update_qa_settings` registered at `ui:agent` — the latter with a declared `arms-auto-qa` membership, because it arms through an in-memory `RwLock` that no detector watches — and `skip_qa` is `v1-audit-refused`: it writes every step as `QAStepResult::skipped`, but `QAResults::from_results` then derives `Pending` rather than `Passed`, contradicting the body's own comment. That discrepancy is a live product bug, not only a facade finding. + +**Gate:** P-17d floor diff clean; C-9 review recorded. + +### 5. `B4` — Ideation, plans, methodology, workflow + +**Commands:** 0 · **Register-candidates:** 0 · **Risk classes:** — + +**Retired by `B4`.** Every member left the P-11 ratchet as manifest-classified, so this batch has no registration work. Disposition-only from the start — the manifest classification IS the disposition. + +**Why here:** The largest single module in the gap (42). It is also where the known detector-(c) rejection `apply_proposals_to_kanban` lives, so the batch must be sized to absorb a mid-batch reclassification without stalling the others. + +**Work:** + +- Expect a non-empty detector-(c) rejection subset; record each rejection in the manifest disposition rather than downgrading the class. +- CORRECTED (Wave D1): `archive_task_proposal` honestly names the archive-only body, inherits the ideation agent default, and is registered at `ui:agent`; the old `delete_` prefix floor no longer misclassifies it. +- DONE (PR 3.1-b batch 11): the B4 remainder is dispositioned — 19 reads registered at `ui:read`, 14 writers at `ui:agent`, 7 `v1-audit-refused`, 12 `host-denied-spawns-process`. `agent_plan_commands`, `methodology_commands` and `workflow_commands` are fully classified and no longer appear in this batch's module list. +- READ FIRST — batch 11 hand-traced all twelve detector-(c) hits instead of accepting the probe boolean, and correctly established that all twelve reach a real `Command::new`, so the floor excluded none. `activate_agent_task_pipeline` and `activate_agent_plan_direct_implementation` reach it ONLY through the stale-publish repair probe and are recorded as NARROW, which batch 12 re-confirmed by reconstructing the edge chain. +- CORRECTION (PR 3.1-b batch 12) — batch 11 also recorded TWO scanner errors as fact, and NEITHER reproduces. `resolve_manual_role_spawn_settings` and `find_node_cli_path`/`ensure_resolved_node_bin_in_path`/`resolved_node_bin_dir` are all launch-free by the engine's own measurement, so the engine agreed with the hand trace all along. The `codex`/`node` tokens batch 11 called artifacts riding on a git command are REAL, and arrive through `CodexCliClient::spawn_agent -> build_codex_internal_mcp_overrides -> find_node_binary`. Do not inherit the artifact claim. The genuine over-attribution is a third mechanism: callees resolve by BARE NAME, so `conn.execute(..)` binds to `AgentWorkflowRunner::execute`. It is pinned by `batch12_detector_attribution_limits_are_measured_not_assumed` and deliberately not fixed — narrowing resolution removes edges, and edges are what the floor is measured from. +- OPEN — the highest-value fail-open fix in the gap is `ideation_harness_availability.rs:344/:360`: `.ok().flatten()` plus an infallible resolver makes a lane-settings DB error indistinguishable from 'no row configured', so a lane configured to an unavailable Codex reports the Claude default as `available: true`. One propagation fix clears BOTH `get_agent_harness_availability` and `get_ideation_harness_availability`. + +**Gate:** P-17 green; C-9 review recorded; rejected members appear as manifest dispositions, never as local-only rows. + +### 6. `B5` — Automation, research, metrics, activity + +**Commands:** 0 · **Register-candidates:** 0 · **Risk classes:** — + +**Retired by `B5`.** Every member left the P-11 ratchet as manifest-classified, so this batch has no registration work. Disposition-only from the start — the manifest classification IS the disposition. + +**Why here:** Automation run/restart are two of the five 2.6-surfaced ops; the rest are read-shaped commands that were swept to the conservative module default and are the cheapest reclassification wins in the gap. + +**Work:** + +- Re-audit the conservative-module-default rows: a genuinely inert read here may drop to `Read`/`Operate`, but only with sink evidence — the floor may not be undershot. +- DONE (PR 3.1-b batch 12): all 33 B5 ratchet members are dispositioned — 18 reads registered at `ui:read`, 12 writers at `ui:agent` (4 of them arming, 1 carrying `SeedsSpawnTriggeringState` and 3 carrying `DECLARED_MEMBERSHIPS` rows), 3 `host-denied-spawns-process`. `activity_commands`, `automation_commands`, `metrics_commands` and `research_commands` are fully classified. +- RESOLVED — the plan asked whether `trigger_automation_run_now` / `restart_automation` have arming targets visible to detector (a). They do not, and the two commands are NOT alike. `trigger_automation_run_now` reaches a real Codex spawn (`dispatch_automation_run_now_action -> spawn_automation_judge_task -> invoke_automation_utility_agent -> CodexCliClient::spawn_agent`) and is refused at the floor with `retry_automation_judge`, which shares that chain. `restart_automation` spawns nothing; it flips `automations.status` to Active, the armed value `spawn_automation_scheduler` scans, and detector (b) misses it because that surface's sole write marker is `reopen_run_corrective`. It is registered at `ui:agent` with a `DECLARED_MEMBERSHIPS` row — NOT with `SeedsSpawnTriggeringState`, which `seeds_spawn_triggering_state_tags_track_detector_b_evidence` defines as detector-(b) evidence — as are `retry_automation_plan_judge` and `skip_automation_judge`. Only `resume_automation_run`, which the detector does flag, earns the capability. +- NOTE for successors — the four automation arming writes were NOT bought by widening the `automation-active` write-marker list. Markers are matched against every command's closure, so a broader marker moves the floor for members batches 7-11 already dispositioned. Declare the membership instead. + +**Gate:** P-17 green; C-9 review recorded. + +### 7. `B6` — Personas, role defaults, MCP policy, review settings + +**Commands:** 0 · **Register-candidates:** 0 · **Risk classes:** — + +**Why here:** Configuration-of-future-authority shapes cluster here: a persona/role/policy write does not act now but changes what a later spawn is allowed to do. This is the `update_custom_analysis` family of risk (§3.3 backstop-1 residual), so it gets one focused dual-lens review instead of being sprinkled across batches. + +**Work:** + +- For each command ask the deferred-authority question explicitly: does this write change what a FUTURE agent process may do? If yes it is at least `AgentControl` with `ConfiguresFutureProcessAuthority`, regardless of how inert the immediate action looks. +- `delete_persona`-shaped members stay Denied (deletesEntity). +- DONE (PR 3.1-b batch 13): `persona_commands` (12) and `mcp_policy_commands` (7) are fully classified — 8 reads at `ui:read`, 12 writers at `ui:agent` (8 carrying `MutatesAgentConsumedContent`, 4 carrying `DECLARED_MEMBERSHIPS`), 3 `host-denied-spawns-process`. +- RESOLVED, and successors must not re-litigate it — the deferred-authority lens above says such a write is 'at least AgentControl with ConfiguresFutureProcessAuthority'. That reading is UNREPRESENTABLE: `class_permits` admits `ConfiguresFutureProcessAuthority` only under `Elevated`, which v1 grants no scope for, so declaring it converts an audited-clean bounded write into a deferral by notation rather than by finding. The idiom that records the same finding at a registerable class is AgentControl plus a `DECLARED_MEMBERSHIPS` row, which is what `update_agent_lane_settings` already carries for picking the harness a live agent is launched with — strictly more deferred authority than an MCP server/tool override. Batch 13 used declarations `configures-future-agent-tool-authority` and `configures-future-agent-capability-gates`. +- READ FIRST — `get_mcp_catalog` and `refresh_mcp_catalog` are REFUSED at the floor. They are reads by intent, but `build_catalog -> discover_provider_catalog -> resolve_codex_catalog_cli_path` launches the Codex app-server to answer. `retry_legacy_mcp_registration_repair` is ALSO refused, and detector (c) does NOT see it: it runs `claude mcp remove ralphx -s user` through `tokio::process::Command::new`, hidden by a `spawn_blocking(bare_fn)` call shape that creates no edge plus a spawn on an already-resolved path that names no resolver. Pinned by `batch13_detector_gap_is_measured_not_inherited`. + +**Gate:** P-17 green; C-9 review recorded with the deferred-authority lens explicitly exercised. + +### 8. `B7` — Artifacts, task context, notifications, app chrome + +**Commands:** 0 · **Register-candidates:** 0 · **Risk classes:** — + +**Retired by `B7`.** Every member left the P-11 ratchet as manifest-classified, so this batch has no registration work. Disposition-only from the start — the manifest classification IS the disposition. + +**Why here:** The tail. Mixed reads and small writes; also the batch that must decide which names are genuinely CLIENT-LOCAL (updater channel, window/dock chrome) and therefore belong in `local-only-commands.ts` with an honest reason — the only batch expected to add local-only rows. + +**Work:** + +- Split client-local from host-owned per command: `update_channel_commands` and parts of `ui_commands` are plausible `local-only` rows; artifacts and task context are host state and must register or be manifest-disposed. +- `get_task_context` and the prompt-builder reads are content-surface members (ledger-soundness round found 5 dropped worker content reads) — re-check the surface enumeration before assigning. +- Every local-only row gets an honest client-local reason; 'hard to classify' is never valid. +- DONE (PR 3.1-b batch 13): all 33 B7 ratchet members are dispositioned — 24 reads at `ui:read`, 8 writers at `ui:agent`, 1 `v1-deferred`. Zero local-only rows were added, which answers the batch's own open question: the client-local split it anticipated did not survive contact with the commands. +- RESOLVED — the batch was expected to move `update_channel_commands` and parts of `ui_commands` to `local-only-commands.ts`. Neither is client-local. `get_update_channel`/`set_update_channel` read and write `app_state_repo`, which is HOST state, and `get_ui_feature_flags` projects the host runtime config plus the agent-capability snapshot. Wave F2 records the owner-approved authority decision: `set_update_channel` is an audited `AgentControl` repository write, and a paired device with `ui:agent` may move the host release train despite the accepted auto-update/restart risk. +- RESOLVED — the plan flagged 5 dropped worker content reads. They are the `task_context_commands` Tauri commands (`get_task_context`, `get_artifact_full`, `get_artifact_version`, `get_related_artifacts`, `search_artifacts`), all registered at `ui:read`. Note their HTTP namesakes in `http_server/handlers/worker.rs` are DIFFERENT functions: the axum `search_artifacts` silently skips unparsable artifact types, while the Tauri command propagates the parse error. Do not reason about one from the other. +- NOTE — batch 12 measured this block detector-silent and batch 13 re-measured rather than inheriting, which is how it found that detector (b)'s flag on `update_notification_settings` is a bare-name MARKER collision (`update_settings` vs the workspace-auto-review write marker), not a spawn-triggering write. It is registered WITHOUT `SeedsSpawnTriggeringState`; claiming the tag would have passed the evidence test while being false. + +**Gate:** P-17 green; every new local-only row has a reason; C-9 review recorded. + +### 9. `D1` — Credential + integration surface (disposition only, no registrations) + +**Commands:** 0 · **Register-candidates:** 0 · **Risk classes:** — + +**Retired by `B0`.** Every member left the P-11 ratchet as manifest-classified, so this batch has no registration work. Disposition-only from the start — the manifest classification IS the disposition. + +**Why here:** Every member is `TouchesCredentials` or `ConfiguresFutureProcessAuthority`. API-key management is compile-denied from the facade (§4.3) and the integration-settings saves are the round-3 module deny list. Nothing here registers in v1; the entire batch is manifest disposition, so it is pure throughput once B0 lands — 72 names retired with zero registration risk. + +**Work:** + +- Confirm each ledger row already carries the denying capability; add missing rows rather than adding local-only reasons. +- The ticketing reads are Elevated-not-Denied (they read a credentialed provider): decide once, for the whole module, whether v1 defers them or the reads split from the writes in a later phase. Record the decision in the ledger reason. + +**Gate:** Manifest regenerated and diff-clean; unclassified count drops by exactly this batch's size; zero new local-only rows. + +### 10. `D2` — Process-launch getters and git/gh surface (disposition only) + +**Commands:** 0 · **Register-candidates:** 0 · **Risk classes:** — + +**Retired by `B0`.** Every member left the P-11 ratchet as manifest-classified, so this batch has no registration work. Disposition-only from the start — the manifest classification IS the disposition. + +**Why here:** The 'getter that shells out' family plus the destructive-git and installer surfaces. `SpawnsProcess` is not exposable at any v1 scope, so these are dispositions, not registrations. `get_project`/`list_projects` are carved out into R1 because they are the one case where the spawn is removable rather than inherent. + +**Work:** + +- Verify each row carries `SpawnsProcess` (detector (c) is the floor: a Read/Operate row reaching a launch sink fails CI). +- `get_task_file_changes` / `get_file_diff` / `get_codex_cli_diagnostics` are the named getter-spawns — they stay denied even though they read like reads. + +**Gate:** Manifest diff-clean; detector-(c) floor test green; unclassified count drops by exactly this batch's size. + +### 11. `R1` — `get_project` / `list_projects` — spawn-free read path + +**Commands:** 0 · **Register-candidates:** 0 · **Risk classes:** — + +**Retired by `B0`.** Every member left the P-11 ratchet as manifest-classified, so this batch has no registration work. NOT closed, though: leaving the ratchet is a bookkeeping fact, not an answer. Both names are manifest-classified `host-denied-spawns-process` because the getter shells out TODAY; §5.1's open question is whether to remove the spawn so they can be registered, and that owner call still stands. If it is answered yes, these rows change class and re-enter as registration work. + +**Why here:** The only commands in the gap whose process authority is INCIDENTAL. Both are pure repository reads; the single spawning field is `repository_capability`, computed per project by shelling out to git in `project_response()`. Removing that inline shell-out makes the highest-traffic read on the whole remote surface registerable as `Read`. See `resolvedItems.projectGetters` for the proposed path and the rejected alternatives. + +**Work:** + +- Land the cache-backed capability read (option A in `resolvedItems.projectGetters`) as its own change with its own tests — NOT inside a registration batch. +- Only after the shell-out is gone: re-run detector (c), drop the `SpawnsProcess` capability, reclassify to `Read`, register both. +- If option A is rejected by the owner, both names fall back into D2 as v1-deferred dispositions and the frontend project list stays a fetch-route question (3.1 open question 4). + +**Gate:** Detector (c) reports no launch sink in either closure; P-4 parity rows for both; the manifest shows class `read` with an empty capability set. + +### 12. `D3` — Host chrome, terminal, repository settings, test data (disposition only) + +**Commands:** 0 · **Register-candidates:** 0 · **Risk classes:** — + +**Retired by `B0`.** Every member left the P-11 ratchet as manifest-classified, so this batch has no registration work. Disposition-only from the start — the manifest classification IS the disposition. + +**Why here:** Terminal is the phase doc's worked example of the third disposition: its invoke names resolve for P-11 through the module-`Denied` (`PtyControl`) rows, NEVER through a client-local reason. Test data is hard-denied outright (total-data-loss blast radius). Startup/repository settings are `HostManagement`/`ConfiguresFutureProcessAuthority` — v1-deferred. + +**Work:** + +- Assert the terminal names resolve through the manifest path introduced in B0; a `local-only` row for any of them is a defect, not a shortcut. +- Keep `report_startup_frontend_milestone` honest: it is a client-originated report about the LOCAL app boot — check whether it is genuinely client-local (local-only row) rather than host-deferred. + +**Gate:** Manifest diff-clean; a planted local-only row for a terminal command fails CI. + +### 13. `A1` — Chat attachments — disposition + remote rendering (deferred from 2.6/review-4) + +**Commands:** 0 · **Register-candidates:** 0 · **Risk classes:** — + +**Retired by `B0`.** Every member left the P-11 ratchet as manifest-classified, so this batch has no registration work. NOT closed: the attachment names leave the ratchet, but remote attachment RENDERING is a fetch route, not an invoke command, and §5.3's `ChatAttachmentGallery.tsx` gap plus the 1.5-C endpoint dependency are untouched by B0. + +**Why here:** 2.6-a shipped the honest interim: under a remote environment `getImagePreviewSrc()` returns `null`, because `convertFileSrc` mints an `asset://` URL for a path on the CLIENT's filesystem while attachment content lives on the HOST. Wave E6 adds the metadata-only `list_remote_message_attachments` read twin and deliberately omits `filePath`, so paired clients can render filename/MIME/size cards with the existing host-content affordance. Upload, delete, and attachment bytes remain unavailable remotely. + +**Work:** + +- Blocked on 1.5-C: `/remote/v1/attachments/{id}` does not exist on this base (no `attachments` route in `remote_server/`). Do not start A1 until the 1.5 lane lands it. +- Branch preview-source resolution on env kind in BOTH renderers — `MessageAttachments.tsx:115` and `ChatAttachmentGallery.tsx:97` (2.6 only hardened the first; the gallery still calls `convertFileSrc` unconditionally, which is a live gap this census surfaces). +- Route the remote branch through the scoped endpoint under `ui:read` with a binary-safe body and 2.7's response-header envelope; never through JSON `/invoke` (C-16). +- Resolve open question 4 explicitly: extending the §3.5 fetch-route remount allowlist rides 3.1, or it is a separate change against P-1's checked-in allowlist. Record the call. +- Register the path-free `list_remote_message_attachments` metadata twin; keep upload/delete and byte fetch outside this slice. + +**Gate:** A remote attachment renders through the scoped endpoint; a local one still uses `convertFileSrc`; P-1 route-allowlist equality still holds; the three commands are manifest-disposed with zero local-only rows. + +### 14. `X1` — Orphan invokes — no local handler exists + +**Commands:** 0 · **Register-candidates:** 0 · **Risk classes:** — + +**Why here:** RESOLVED in PR 3.1-b. Five gap names were absent from `src-tauri/src/commands/registry.rs` `generate_handler!` AND from the ledger (which is exhaustive over it), so every call rejected at runtime with no remote environment involved. The reachability audit found none of the five was wired to any component or event handler — the wrappers were dead code kept alive only by their own unit tests — so all five were resolved by DELETING the call site rather than by minting host authority the product does not use. This batch is now empty and stays here as the record of that call. + +**Work:** + +- `add_proposal_dependency` — deleted at both call sites (`api/ideation.ts` `dependencies.add`, `api/proposal.ts` `addProposalDependency`) plus the `addDependency` mutation in `hooks/useDependencyGraph.ts`. Its owning hook `useDependencyMutations` had no consumer at all: the UI reads the dependency graph and never writes edges, so there was no product asymmetry to fix by adding the missing command. +- `create_child_session` / `get_parent_session_context` — deleted from `api/ideation.ts` (zero callers, zero tests). The capability is not lost: both are live HTTP routes (`POST /api/create_child_session`, `GET /api/parent_session_context/:session_id`), which is how the backend actually reaches them. +- `delete_project` — deleted from `api/projects.ts`; `projectsApi.archive` is the live removal path. +- `delete_task` — deleted from `api/tasks.ts` and `hooks/useTaskMutation.ts`; it was already `@deprecated Use cleanupTask instead`, and every component destructures `cleanupTaskMutation`. +- Regression guard: `frontend/src/api/orphan-invokes.test.ts` asserts each wrapper stays absent while its surviving sibling (`remove`, `getChildren`, `archive`, `cleanupTask`) stays present, so the test cannot pass by the namespace disappearing. + +**Gate:** Each of the five is deleted at the call site with a regression test; the P-11 scan sees zero orphans. + +## 5. Resolved items + +### 5.1 `get_project` / `list_projects` — the getter that shells out + +**Status:** proposal — needs an owner call before 3.1-b starts R1 · **Batch:** `R1` + +**Finding.** Both are pure repository reads (`project_commands.rs:211-240`): `project_repo.get_all()` / `get_by_id()`, then `project_response()` per row. The ONLY process authority is one response field — `repository_capability`, produced by `inspect_repository_capability()` (`infrastructure/git_auth.rs`), which runs `git remote get-url origin` and `git remote get-url --push origin` through `resolve_git_cli_path()` with a 5s deadline, once PER PROJECT. That is what makes a getter a `SpawnsProcess`/Elevated command, and it is incidental to the read, not inherent to it. + +**Proposal.** Option A — cache the capability, do not compute it in the getter. Persist the inspected `repository_capability` (plus `inspected_at`) alongside the project row, write it from the paths that already have process authority and already shell out (project create/update, `change_project_git_mode`, `setup_gh_git_auth`, `switch_git_origin_to_ssh`, `reanalyze_project`) plus one background refresh whose loop root is declared in the manifest's `background_loop_inventory`, and have `project_response()` READ the cached value. `list_projects`/`get_project` then hold no launch sink in their closure, detector (c) goes quiet, the `SpawnsProcess` capability drops, and both classify as `Read` — registerable on the v1 facade at `ui:read`, with zero `generate_handler!` edits and zero command-fn forks (A-7). The response shape is unchanged, so P-4 parity and every existing caller are untouched; only the freshness semantics change, and a stale-capability value is strictly safer than the current InspectionFailed-on-timeout behaviour (`inspect_repository_capability` already returns `InspectionFailed{message}` rather than erroring, so consumers already handle a non-authoritative value). + +**Rejected alternatives:** + +- A response projection that omits `repository_capability` for remote callers — that is a command-fn fork, which A-7 forbids, and it would break P-4 byte-identity between local IPC and remote dispatch (the whole point of the parity suite). +- A pinned facade op — pins fix ARGUMENTS (`approve_permission_request` / `deny_permission_request`), not response shape; there is no pin that removes a field. +- Registering as Elevated — `ui:elevated` is a §1 v1 non-goal; this would ship a scope nothing can hold. +- Serving the project list over a remounted fetch route instead — `http_server/handlers/projects.rs` computes the SAME capability inline, so the spawn moves rather than disappears, and it opens 3.1 open question 4 unnecessarily. + +**If the owner rejects option A.** Both names fall back to D2 as v1-deferred dispositions. That is not cost-free: the project list is the entry point of nearly every remote screen, so a remote client would have to hydrate projects through a fetch route (open question 4) or run with no project list at all. + +### 5.2 The five 2.6-surfaced unregistered `ui:agent` ops + +**Status:** PARTIALLY RESOLVED (PR 3.1-b batch 9) — the detector-(c) confirmation this section made mandatory was run. `skip_step`, `trigger_automation_run_now` and `restart_automation` remain registration candidates; `send_agent_message` and `start_agent_conversation` came back POSITIVE and are now manifest-classified `host-denied-spawns-process`. The evidence bullet below claiming the provider launch sits outside chat send's own closure is therefore WRONG and is retained only as the record of what the static read predicted. + +| Command | Ledger class | Capabilities | Batch | Resolution | +|---|---|---|---|---| +| `send_agent_message` | elevated | spawnsProcess | `B2` | DEMOTED (batch 9) — `host-denied-spawns-process`. Detector (c) fires on its OWN closure, which is already cut at the `send_message` steer sink: it still reaches `resolve_git_cli_path`, `resolve_node_cli_path` and `find_codex_cli_candidates` by another route. Registering it would fail `detector_c_floors_process_spawn_authority`. | +| `start_agent_conversation` | elevated | spawnsProcess | `B2` | DEMOTED (batch 9) — `host-denied-spawns-process`. Same three resolvers reached from its own cut closure. | +| `skip_step` | agentControl | agentControl, mutatesAgentConsumedContent | `B1` | register (`ui:agent`), pending detector-(c) confirmation | +| `trigger_automation_run_now` | elevated | spawnsProcess | `B5` | register (`ui:agent`), pending detector-(c) confirmation | +| `restart_automation` | agentControl | agentControl | `B5` | register (`ui:agent`), pending detector-(c) confirmation | + +**Briefing correction.** The 3.1-a brief states three of these five are detector-(c)-rejected. That does not match the code: the detector-(c) trio is `resume_task`, `apply_proposals_to_kanban`, `set_agent_conversation_workspace_auto_publish` (`remote_server/registry.rs` NOT-registered note; `frontend/src/lib/remote/agent-gate.test.ts:114-124` uses exactly those three as the unavailable-by-ABSENCE fixture). None of the five 2.6-surfaced ops appears in that set. The two lists were conflated — they are different trios, and 2.6's tracker note lists the five as ops that 'flip with no client change when 3.1 registers them', i.e. registration is the intended resolution. + +**Evidence:** + +- 2.6 tracker product note: 'with `ui:agent` granted, chat send / start composer / skip_step / automation run+restart render UNAVAILABLE remotely — send_agent_message etc. are unregistered in 1.5-A's 27-op surface. Honest against this build; flips with no client change when 3.1 registers them.' +- Phase 3 doc, PR 3.2 key point 4: 'Chat send paths (`start_agent_conversation`, `send_agent_message` + variants, …) are `AgentControl` — a device without `ui:agent` gets `REMOTE_FORBIDDEN`'. `REMOTE_FORBIDDEN` (not `REMOTE_COMMAND_UNAVAILABLE`) is only reachable for a REGISTERED command, so 3.2 requires these registered. +- All five are ledgered `class: agentControl`, `capabilities: [agentControl]`, reason `conservative-module-default` — none carries `SpawnsProcess`. +- `send_agent_message` reaches `chat_service.send_message` (`unified_chat_commands/mod.rs`), which is a detector-(a) STEER sink. `all_cut_sinks()` CUTS the closure at steer sinks, so the provider process launch beyond it is outside the command's own closure — which is precisely why chat send is registerable while `resume_task` (whose closure resolves a CLI path directly) is not. + +**Obligation on 3.1-b.** This is a static read of the call graph, not a detector run. 3.1-b must confirm each of the five against the live P-17 detector-(c) output as the first step of its batch, and demote any that come back positive to a manifest disposition — the class is decided by the detector, never by this census. + +**Client impact.** No client change is needed: `agent-gate.ts` derives availability from ABSENCE in `facade_ops`, so each op flips from `unavailable` to `gated`/`enabled` the moment its registration lands in the regenerated manifest. + +### 5.3 Remote attachment rendering + +**Status:** scoped into batch A1; BLOCKED on the 1.5-C endpoint · **Batch:** `A1` + +**Finding.** Deferred here from 2.6-a and the review-4 round. Current behaviour is the honest interim, not a bug: `getImagePreviewSrc()` (`frontend/src/components/Chat/MessageAttachments.tsx:99-116`) returns `null` whenever the active environment is remote, so every host attachment renders as a placeholder card instead of a broken image — `convertFileSrc` would mint an `asset://` URL for a path on the CLIENT's disk while `attachment.filePath` names a file on the HOST. + +**Blockers:** + +- `/remote/v1/attachments/{id}` does not exist on this base — there is no attachments route in `src-tauri/src/remote_server/`. It is 1.5-C's deliverable (live in the `rme-pr-1-5` lane). A1 cannot start until it lands. +- 2.7's response-header envelope and a binary-safe body are prerequisites; binary must never travel through JSON `/invoke` (C-16). + +**New gap this census found.** 2.6 hardened only ONE of the two renderers. `ChatAttachmentGallery.tsx:97` still calls `convertFileSrc(attachment.filePath)` with no env-kind branch, so the gallery surface renders broken images under a remote environment where `MessageAttachments` renders placeholders. A1 must fix both, and the 2.6 negative test (`host-affordance-gating.test.tsx`, which asserts `convertFileSrc` was NOT called) should be extended to cover the gallery. + +**Open question.** Phase-3 open question 4 applies verbatim: attachment rendering is a FETCH route, not an invoke command, and the source does not say whether extending the §3.5 remount allowlist rides 3.1 or requires a separate change against P-1's checked-in allowlist. A1 must record the call before it writes a route. + +**Command side.** The local attachment commands remain ledgered dispositions: upload/delete retain their filesystem authority and `list_message_attachments` remains denied by its mixed-authority module default. Wave E6 registers the commands-resident, path-free `list_remote_message_attachments` twin at `read`; it serves metadata only, while bytes remain a later binary-envelope slice. + +## 6. Reconciliation + +| Check | Result | +|---|---| +| Drift scan passes | yes (this file is not emitted otherwise) | +| Scan unclassified count == baseline size | 0 == 0 | +| Every gap command in exactly one batch | 0 / 0 | +| Disposition totals sum to the gap | 0 == 0 | +| Batch plan claims no empty module and pins no absent command | enforced by the generator | +| Every discovered `plugin:` name is dispositioned | 51 prefix-rule + 0 exception(s) == 51 | + +## 7. The Tauri plugin surface + +**51 `plugin:` command names across 7 packages — all routed to the LOCAL device by one prefix rule.** + +`frontend/vite.config.ts` aliases `@tauri-apps/api/core` for the whole module graph, node_modules included, so every `@tauri-apps/plugin-*` package invokes through `src/lib/remote/invoke.ts` exactly like an app call site does. None of these names can ever be registered on the host facade — the facade is exhaustive over `generate_handler!`, which plugin commands bypass by construction — so before the prefix rule every one of them travelled to the host and answered `REMOTE_COMMAND_UNAVAILABLE`. + +That was not merely unavailable, it was aimed at the wrong machine: `plugin:opener|open_url` opened the host operator's browser, `plugin:updater|check` asked the host whether *this* app binary had an update, global shortcuts bound the host's keyboard, and `plugin:notification|is_permission_granted` rejected — silently short-circuiting a settings write the facade *does* register. Each plugin's subject is the device showing the UI, so the whole namespace is `run-locally`. + +Earlier revisions of this census could not see any of it: the drift scan walked `frontend/src` only, and these invoke literals live in node_modules. The headline "0 unclassified" was therefore blind over the namespace rather than true of it. The scan now parses each imported package's shipped ESM bundle with the same AST machinery, folds the names into the same inventory, and classifies them through the prefix rule read out of `local-only-commands.ts` — so the claim covers them. + +| Property | Value | Why it is falsifiable | +|---|---|---| +| Classification | `plugin:` prefix → `run-locally` | One rule, not 77 call-site edits and not 51 table rows — a plugin added tomorrow inherits it | +| Host-targeted exceptions | 0 (`HOST_TARGETED_PLUGIN_COMMANDS`) | Reviewed and empty, not absent. An excepted name leaves local-only classification and must earn a registration or a ledger row, or the scan reports it unclassified | +| Missing prefix rule | fails closed | `parsePluginPrefixRule` returns `null` and classifies nothing, so every plugin name goes unclassified and CI goes red | +| Dynamic name inside a plugin package | hard failure | An unenumerable name is the blindness itself; a dependency upgrade that introduces one fails the scan instead of under-reporting | +| Uninstalled imported plugin package | hard failure | An uncomputable census must not pass as an empty one | + +Host-path affordances are handled at a different seam and are unaffected: `openPath` / `revealItemInDir` against host-side workspace paths are suppressed by host-affordance gating (`lib/remote/host-affordances.ts`) and degrade to `HostPathCopyButton`, so the prefix rule never opens a host path on the client. + +Machine-readable companion for 3.1-b/c: [`remote-coverage-census.json`](./remote-coverage-census.json) — same batches, plus per-command `{batch, module, ledgerClass, capabilities, disposition}` rows. diff --git a/docs/handoffs/remote-coverage-implementation-handoff.md b/docs/handoffs/remote-coverage-implementation-handoff.md new file mode 100644 index 0000000000..1a13087d06 --- /dev/null +++ b/docs/handoffs/remote-coverage-implementation-handoff.md @@ -0,0 +1,114 @@ +# Remote Coverage — Implementation Handoff + +**Source:** `docs/reviews/remote-coverage-adversarial-review.md` (2026-08-01, branch `feat/remote-multi-env`) +**Status:** ready to implement · phases ordered top priority → lowest · each phase is independently shippable + +This handoff turns the review's 17 confirmed findings + 10 cross-cutting findings into an ordered build plan. It deviates from the report's own priority list in four places, each explained inline under **Direction change**. The one-sentence thesis: *the remote feature's read plane is done; the write plane fails not because gates are missing as a concept but because the existing gate machinery is pointed at the wrong ops, never consumed, or bypassed by whole surfaces — so the cheapest highest-leverage work is making the machinery **verifiable**, then sweeping surfaces onto it.* + +## Phase map + +| Phase | Theme | Size | Ships user-visible value | +|---|---|---|---| +| 0 | Guardrails: make gate wiring falsifiable | ~½ day | No (but multiplies every later phase) | +| 1 | Restore the safety contract (brakes + questions + queue) | 1–2 days | Yes — permission/question gates work remotely | +| 2 | Close the Tauri-plugin side door | ~1 day | Yes — links open on the right machine, notifications toggle works | +| 3 | Unblind liveness, kill the poll storms | 2–3 days | Yes — running state + Stop button + halt banner on the client | +| 4 | Honest gating sweep (automations, task details, plan, chat tail) | 3–5 days | Yes — enabled-but-doomed buttons become honest disabled states | +| 5 | Truthful attribution (ticketing / GitHub copy about the host) | 2–3 days | Yes — the UI stops lying about the host's configuration | +| 6 | Product decisions + protocol extensions (design-first) | 1–2 wks | Yes — plan approval remotely, unknown-outcome safety | +| 7 | Sweep the unswept domains | ongoing | Audit output feeding phases 4–6 | + +--- + +## Phase 0 — Guardrails first + +**Direction change #1 (vs. report priority list):** the report puts fixes first and tests inside each fix. Do the guards *before* any fix. Two confirmed criticals ("Run now" gated by the wrong op; PlanEditor's gate resolving an op its save path never calls) lived in files the wiring guard **certified as correctly gated** — the guard only asserts a file imports `useAgentGate`, never that the resolved op matches the invoked command. Every gate added in phases 1–5 lands under the same blind guard unless we fix it now. + +Work items: + +1. **Op↔callsite consistency test.** New static test alongside `frontend/src/components/remote/agent-gate-surfaces.test.tsx`: for every `AGENT_GATED_AFFORDANCES` row, (a) at least one production file calls `useAgentGate("")` — kills dead rows (`automationRunNow`, `automationRestart`, `folderReferenceRemove` are dead today); (b) in each file that resolves an affordance, the command names it invokes include the row's op (or a declared alias) — kills wrong-op gating. AST-lite (regex over source, same style as the existing guard) is acceptable; perfect resolution is not required, an allowlist for indirection is. +2. **Never-invoke-the-raw-twins test.** The facade splits `resolve_permission_request` → `approve_/deny_permission_request` and denies `resolve_user_question` in favor of `answer_user_question`. Add a test asserting the raw names never appear in a production `invoke(` in `frontend/src` (they may appear in `LOCAL_ONLY_COMMANDS`-style declarations). This turns Phase 1's fix into a ratchet. +3. **Extend the wiring-guard file list** to `frontend/src/components/agents/task-details/detail-views/*` (the ungated fork the Agents pane actually renders — critic critical #2). The guard will go red; that red is Phase 4's worklist, so mark the new entries as `todo`-style expected failures or land the list extension in the same PR as Phase 4's first slice — your call, but the list must not silently omit the fork again. + +Exit: new tests exist; the two wrong-op findings are reproduced by a failing test before any fix lands. + +## Phase 1 — Restore the safety contract + +The core promise — "viewer with brakes" — is broken at every brake. All fixes are frontend rerouting onto ops the host **already registers**; no host changes. + +1. **Permission gates** (`frontend/src/api/permission.ts:46`): route approve → `approve_permission_request`, deny → `deny_permission_request` under a remote environment (local keeps `resolve_permission_request`). Deny is `operate`-class — it must work on a *default* pairing; approve is `agentControl`. The gate rows (`permissionApprove`) already point at the pinned ops — after this fix they'll finally describe reality. +2. **Question answers** (`frontend/src/hooks/useAskUserQuestion.ts:279`, `frontend/src/api/ask-user-question.ts:67`): the `requestId` branch routes to `resolve_user_question` (Elevated/SpawnsProcess — unreachable at every scope). Route to the registered `answer_user_question` remotely. Then fix the failure handling: a transport refusal must **not** render "Agent session expired" and must **not** clear the question banner over a still-blocked agent — that's a false-terminal write from a non-authoritative error (stateful-workflow rule: fail closed on reads). +3. **Queued-message delete/edit fail closed** (`frontend/src/hooks/useChatActions.ts:485-496, :557-596`): both swallow the host failure after (or while) mutating local state; edit then re-sends unconditionally → the agent receives both turns. Under remote: attempt the host op first, keep local state on failure, surface the error (the `handleSendQueuedMessageNow` path already does this correctly — mirror it). `delete_queued_agent_message` is ledger-denied, so remotely these become *gated* affordances (add rows) until/unless a spawn-free queue twin is registered (Phase 6 candidate). +4. Tests: production-entry-path tests per `stateful-workflow-review.md` — assert the pinned ops are invoked remotely, assert absence of the bad effects (banner cleared, local queue mutated on failure). + +Exit: on a paired client — deny works with default scopes, approve/answer work with `ui:agent`, a failed queue edit leaves exactly one truthful queued chip. Phase 0's ratchet tests keep it that way. + +## Phase 2 — Close the Tauri-plugin side door + +**Direction change #2:** the report ranks this #3; it goes ahead of the gating sweep because it is *actively wrong today* (not merely dishonest): "Open in browser" opens on the **host** Mac, `plugin:updater|check` asks the host about updates, global shortcuts try to bind on the host, notifications permission probe rejects and silently short-circuits a registered settings write. Small, contained, high blast radius. + +1. **Routing policy** (`frontend/src/lib/remote/local-only-commands.ts:31`): add a `plugin:` rule. Default `plugin:*` → **local** (the plugins operate on *this* device: opener, dialog, fs pickers, updater, process, global-shortcut, notification), with an explicit reviewed exception list if any plugin call must target the host (none identified by the review). One prefix rule beats 77 per-import fixes. +2. **Census visibility** (P-11 drift scan + `docs/generated/remote-coverage-census.md`): teach the scan that `plugin:` names exist and are classified by the prefix rule, so the census's "0 unclassified" claim becomes true again instead of blind. +3. **Notifications toggle** (`frontend/src/components/settings/NotificationSettingsPanel.tsx:150-192`): with the routing fixed the permission probe runs locally; also add the missing `.catch` on the mount-time probe and stop `void`-discarding the toggle promise so failures surface. +4. Sanity pass over the 29 `openUrl` sites: after the fix they open on the client, which is correct for URLs (PR links, docs, OAuth). Anything that opens a host *filesystem path* must instead use the existing `HostPathCopyButton` degradation pattern. + +Exit: with a remote environment active, links open locally, the updater checks the client, the notifications toggle persists; a census test enumerates `plugin:` routing. + +## Phase 3 — Unblind liveness, kill the poll storms + +**Direction change #3:** the report offers "wire the index *or* register a status twin." Do **not** start with new host registrations. The verifier's own evidence names the in-repo seam that already solves this exact problem — `pending-gate-reconcile.ts`, built for "backend-memory state the event log cannot replay." Mirror it with the **already-registered** `get_agent_conversation_runtime_index` (`registry.rs:1791`, carries `lifecycle: running|waiting|queued` per conversation). Zero new host surface, rule-27 audit avoided entirely. + +1. **Runtime-index reconcile on connect**: on every `goLive`/reconnect (same hook point as `requestPendingGateReconcile`), fetch the runtime index and write run-liveness into the chat/sidebar stores. This fixes the cold-hydrate blindness (`subscribe{afterSeq: H}` never replays pre-connect `agent:run_started`) that hides typing indicators AND the Stop button (`shouldShowStop` requires `generating`, `AgentComposerSurface.tsx:470`) — restoring the third brake without touching the host. +2. **Transport-aware polling**: `useAgentConversationRuntimeStatus` (5s, polls on error *by design* — wrong for a capability boundary), `useAgentSidebarRunningStates` (5s + swallowed errors), `useChatRecovery` (1.5s `is_agent_running`) — under a remote environment, replace their command with the runtime index or suspend them; `isRemotelyAvailable()` (`agent-gate.ts:294`) exists for exactly this and is consumed nowhere. This ends the permanent `REMOTE_COMMAND_UNAVAILABLE` poll storm eating the per-device pacing budget (8 slots / 10 rps). +3. **Execution status** (critic #6): `get_execution_status` is denied (resolves process-inspection CLI) so every consumer defaults to "running, nothing queued, may start" and the halt banner can never render — a remote user's prompts queue invisibly. The write side already has a spawn-free twin (`update_remote_execution_settings`); register the matching **read** twin (`get_remote_execution_status` from DB state, no process inspection) — this one *does* need a rule-27 hand-audited registry entry + ledger class + denial-test update, and is worth it. Fail closed in the meantime: consumers must treat an unavailable read as "unknown", never as `canStartTask: true`. +4. Tests: cold-hydrate scenario (run live on host → client connects → status shows running, Stop renders); poll-storm regression (no repeating invokes of unregistered commands under remote). + +Exit: a client that connects mid-run sees the run, can stop it, and issues zero doomed polls; a stopped host scheduler shows the halt banner remotely. + +## Phase 4 — Honest gating sweep + +Mechanical once Phases 0–3 exist; the pattern is always the same: add/point the affordance row, consume it via `useAgentGate` (which folds in read-only mode — this is deliberately the *only* way surfaces get degraded-connection behavior, per critic #8), render the existing `AGENT_CONTROL_DISABLED_HINT` / `REMOTE_UNAVAILABLE_HINT` copy, and let Phase 0's guard verify op↔callsite. Slices, in order: + +1. **Automations** (2 confirmed criticals): fix the "Run now" wrong-op gate (`AgentsAutomationPanel.tsx:654/1113/1424` → consume `automationRunNow`); gate the ungated judge-retry beside it; sweep the Automations page's 12 call sites (`AutomationDetailView/Header/RunsTab/RunTimelineItem`) with rows for pause/stop/cancel-run/resume-run/skip-judge/plan-judge-retry/settings-edit and unavailable rows for run-now/judge-retry/delete-automation/delete-run/setup-edit. Fix the two brake-comment lies: pause/stop are host-classified `AgentControl`, so either re-class them host-side to `operate` (they reduce authority — defensible, needs the rule-27 audit) **or** gate them as agentControl and delete the "brakes boundary" comments; pick one, don't leave the contradiction. Fix the notification resume action's false "no longer resumable" copy (`notificationNavigation.ts:62`). +2. **Agents task-detail fork** (critic critical #2): port the twins' `useAgentGate` wiring into all 13 `components/agents/task-details/detail-views/*` files (`taskApprove`, `taskUnblock`, merge rows), add rows + gates for `retry_merge`/`resolve_merge_conflict` in **both** copies, and skip the optimistic `pending_merge` cache write when the gate isn't enabled. Then flip Phase 0's guard entries from expected-fail to enforced. (Do *not* attempt to unfork the views in this phase — the fork is a deliberate pattern per `task-detail-views.md`; unforking is a separate refactor decision.) +3. **Plan approval / ideation acceptance** (critic critical #3): gate `handleApprovePlanFromQuestion` (`AgentsActiveConversationPanel.tsx:1890-1926`), PlanEditor save (point its gate at what it actually calls — or better, port the save to `update_artifact` which *is* registered and is what its current gate claims), and `accept-finalize`/`reject-finalize` with honest unavailable copy. This phase makes them honest; **making them work remotely is Phase 6** and is flagged there as the highest-value product decision in this document. +4. **Conversation tail**: hide the mode picker on remote active conversations (the start composer already does exactly this — `AgentsStartComposer.tsx:659-667`; it's a straight inconsistency); rows for fork/archive/mute/persona-switch; gate attachments (`enableAttachments`) until a remote upload path exists — note the current path serializes whole files onto a JSON invoke the host then refuses; consume `folderReferenceRemove` at the chip's × (`AgentComposerSurface.tsx:2069-2081`). +5. **"New automation" silent rewrite** (`AgentsStartComposer.tsx:663-667` + `App.tsx:871`): a remote user clicking New automation lands in a plain chat composer with no explanation — show the unavailable hint instead of silently degrading. + +Exit: zero enabled controls on a paired client whose click can only produce `REMOTE_COMMAND_UNAVAILABLE`/`REMOTE_FORBIDDEN`; Phase 0 guards enforce it structurally. + +## Phase 5 — Truthful attribution + +The misattribution family: reads about the *host* fail and the UI converts absence into confident wrong claims about whichever machine the user is thinking of. + +1. **GitHub settings** (`GitHubIntegrationSettingsPanel.tsx:24-58`, `IntegrationsHubSection.tsx:137-152`): branch on `useIsRemoteEnvironment` (the pattern already exists at `HarnessProvidersSection.tsx:668`) and render "checked on the host — not available remotely" instead of "gh missing / Install the GitHub CLI". +2. **GitAuthRepairPanel** (`GitAuthRepairPanel.tsx:139-166`) + `useGitAuthStartupNotification`: suppress the transport-error → "git problem" inversion under remote; also fix the always-refetch query economics (`useGithubSettings.ts:60-70` staleTime 0 + focus refetch on a command that can never succeed remotely — same anti-pattern Phase 3 kills for liveness). +3. **PR Mode toggle** (`RepositorySettingsSection.tsx:277-335`): the remote project twin deliberately drops `repository_capability`, and the client renders that as OFF+disabled+"could not inspect". Either (a) have the twin carry a coarse, path-free capability kind (host-side change, small; recommended), or (b) render a distinct "managed on the host" state. Never render a durable host setting as its opposite. +4. **Ticketing**: providers pane says "Not configured" about a host that is configured, linked-ticket chips silently vanish, and the dashboard nav strands a switched user. Same recipe: remote-aware copy ("configured on the host — ticketing runs host-side"), an explicit absent-remotely chip state, and nav gating (`LeftNavRail.tsx:123-132` already gates ticketing by provider presence — make the provider read's unavailability collapse the entry rather than error the view). GitHub dashboard nav (`item.view === "github"` unconditional) gets the same treatment. +5. **PR review deep-link** (confirmed critical): the ActionRequired notification lands a remote user in a conversation with no Review tab and no explanation. Since the sidebar twin already ships per-row workspace metadata, render a lightweight read-only "PR #N — review runs on the host" notice on the conversation (hide-with-explanation), rather than nothing. PR-detail body: use `remoteErrorBannerProps` (`agent-gate.ts:340-352`, built for this, unused here). + +Exit: no surface makes a false claim about the host's configuration; every capability boundary in these surfaces names itself as one. + +## Phase 6 — Product decisions + protocol extensions (design-first) + +Things that need a decision or a host-side surface, not just wiring. Each wants its own short design note + rule-27/28 audit before code. + +1. **Make plan approval work remotely** — *the* product gap. Approving a plan from the couch is the reason remote exists; today the mandatory confirmation gate dies on an unmounted POST route. Recommended shape: spawn-free **invoke** ops (`approve_remote_plan_artifact`, ideation accept/reject twins) with `agentControl` class + dedup, per the established twin pattern — not remount-POST expansion (fetch stays read-only by construction, keep it that way). +2. **Unknown-outcome reconciliation as a seam, not call sites** (critic #6-adjacent, confirmed): `requestId` is minted per *call*, so a post-timeout re-click is a genuine second mutation; `reconcileUnknownOutcome` has 2 consumers out of ~90 mutating ops. Design: a per-facade-op reconcile registry consulted by `networkInvoke` on `REMOTE_TIMEOUT_UNKNOWN`/`REMOTE_REQUEST_IN_PROGRESS` (refetch the affected entity, block the re-click until reconciled), plus intent-level request ids for the worst offenders (`inject_task`, `create_task`, review transitions). This is transport-layer work; do it once, every op inherits it. +3. **Queue management twins** (unblocks Phase 1's gated delete/edit): spawn-free `get/delete/update_queued_agent_message` reads/writes from DB state. +4. **Execution control** — the board can pause but never resume remotely (whole-scheduler asymmetry, unswept domain #1). Decide the remote execution-control story deliberately: which of resume/restart/recover get spawn-free intent twins (the conversation-start dispatcher pattern generalizes) vs. stay host-only with honest gates. +5. **Global execution settings twin** (`update_global_execution_settings`) — the per-project sibling already has one; also fix the optimistic-keep-on-reject + unmount-flush toast (`GlobalExecutionSection.tsx:57-94`). +6. **Publish/Commit&PR**: keep host-only in v1 (verifier downgraded this to hide-with-explanation), but render the explanation; remote publish is a separate future design. + +## Phase 7 — Sweep the unswept + +The report's "Not swept" list is a queue of future reviews, highest value first: **diff/workspace-changes viewer** (29/29 commands denied — likely the automations story again), **Settings tree half-saves**, **projects twins field-drop audit** (what else besides `repository_capability` vanished?), **retention/cursor-resume behavior when a client's lease expired**, **host-path leakage in task/merge/artifact/automation DTOs**, **mid-session scope widening** (requires reconnect today; decide if that's acceptable and document it), **env-switch query routing race** (`invoke` resolves the transport env at call time while caches are env-keyed — audit refetch-after-switch). Re-run the adversarial workflow per domain as each is picked up. + +--- + +## Standing rules for every phase + +- **Extend the owning seams** (repo rule 0): gates go through `agent-gate.ts` rows + `useAgentGate`; new host surface goes through hand-audited `registry.rs` + `capability_ledger.rs` entries (rule 27); events through the classification table (rule 28); reads that mirror host memory go through the reconcile-on-connect pattern (`pending-gate-reconcile.ts`). No parallel gating systems, no `generate_handler!` shortcuts. +- **Fail closed** (stateful-workflow-review): an unavailable read is "unknown", never a default that authorizes progress (`canStartTask ?? true` is the anti-pattern, twice found). +- **TDD, focused validation**: every fix lands with a production-entry-path test asserting the *absence* of the bad effect; Rust changes trigger the post-test `cargo clean` rule. +- **Copy discipline**: capability boundaries use the two existing hint constants; never invent new "not available" phrasings per surface. diff --git a/docs/handoffs/remote-host-auth-stall.md b/docs/handoffs/remote-host-auth-stall.md new file mode 100644 index 0000000000..5f127b54d8 --- /dev/null +++ b/docs/handoffs/remote-host-auth-stall.md @@ -0,0 +1,143 @@ +# Remote host: every authenticated route hangs — host-side handoff + +**Status:** diagnosed from the client side, cause not yet confirmed on the host. +**Symptom (client):** a paired client shows `Connecting to "100.95.136.117:3849"… Setting up +this environment for the first time.` forever, and panes surface +`` `get_execution_settings` did not answer within 30000ms ``. +**Date:** 2026-07-31. Host: `reefs-mac-studio` (`100.95.136.117:3849`), RalphX 0.85.1. + +--- + +## 1. What is measured, not guessed + +All probes below are `curl` from the client Mac against the live host. The tailnet peer is +`active` and TCP connects in **0.02s**, so this is not a network problem. + +| Route | Auth | Touches SQLite? | Result | +|---|---|---|---| +| `/.well-known/ralphx/environment` | pre-auth | **no** | **200**, repeatably | +| `/health` | bearer | yes (`resolve_device`) | **hangs** — no response in 25s | +| `/remote/v1/session` | bearer | yes | **hangs** | +| `/remote/v1/auth/ws-ticket` | bearer | yes | **hangs** | +| `/health` **without** a bearer | pre-auth reject | **yes** (`record_audit`) | **hangs** | + +That last row is the decisive one. Yesterday an unauthenticated `/health` returned +`401 REMOTE_UNAUTHORIZED` instantly; today it hangs. The no-bearer path never looks up a +device — it goes straight to `reject(...)`, which **writes an audit row** before responding +(`remote_server/auth.rs:598-601` → `record_audit` → `remote_audit_log` INSERT). + +So the split is not authenticated vs unauthenticated. It is: + +> **Every request that touches the host's SQLite hangs. Every request that does not, answers.** + +The descriptor handler reads no database, which is why it is the only thing still working. + +## 2. Where it blocks + +`authenticate_remote_request` (`remote_server/auth.rs:483+`) has exactly two DB sinks, and +both observed hangs land on one of them: + +| Path | Sink | File | +|---|---|---| +| Bearer presented | `resolve_device` → `devices.lookup_by_token_hash` | `auth.rs:328` | +| No/!valid bearer | `reject` → `record_audit` → `audit.record` INSERT | `auth.rs:598`, `sqlite_remote_access_repo.rs:635` | + +Ruled out by evidence, so do not start here: + +- **Not the rate limiter.** `acquire_device_slot` returns `None` and rejects fast with + `TooManyConcurrentRequests`; it cannot block. A saturated limiter would produce fast 403s, + not silence. +- **Not the listener or router.** The descriptor is served by the same router, same port, + same middleware stack minus the auth layer. +- **Not the client.** Same result from `curl` with no RalphX client involved. + +## 3. Prime suspect: the DB is wedged + +`DbConnection` is a `Mutex` (single or small pool, +`infrastructure/sqlite/db_connection.rs:42-55`), and `run_transaction` uses `BEGIN IMMEDIATE`. +One long-held write transaction therefore stalls **every** other caller, which matches the +symptom exactly: not slow, but indefinitely silent. + +Two writers this branch added run on a timer against that same database and are the first +things to look at: + +1. **The retention pruner** — `retention::spawn_pruner`, every `PRUNE_INTERVAL` (5 min, + `retention.rs:34`), deleting from `remote_event_log`. On a host that has accumulated a + large log, a single unbounded `DELETE` inside one transaction is exactly the shape that + parks everything else for minutes at a time. +2. **The durable sequencer** — micro-batched commits into `remote_event_log` + (`sequencer.rs`), continuously while the host is emitting events. + +Neither is proven. They are the two new periodic writers on the contended connection, so +they are where to look first. + +## 4. Diagnostics to run ON the host + +In rough order of cheapness: + +1. **App log.** Grep the current launch log for `database is locked`, `SQLITE_BUSY`, + `run_transaction`, `Remote audit log write failed`, and + `Remote durable sequencer`. `record_audit` logs `Remote audit log write failed` on error — + if the DB were erroring rather than blocking, that line would be present. Its **absence** + alongside the hang is itself evidence of a block rather than a failure. +2. **Is the process healthy?** Confirm the running binary is the app and not a stale process + holding `:3849` while a rebuild is in flight — TCP answering while HTTP does not is also + consistent with that. +3. **Log-table size.** `sqlite3 ralphx.db "SELECT count(*) FROM remote_event_log;"` and + `"SELECT count(*) FROM remote_audit_log;"`. A very large `remote_event_log` supports the + pruner hypothesis. (Run this from a shell — if the DB is genuinely wedged this command + will itself block, which is a positive result, not a failed command.) +4. **Wedged vs slow.** `sqlite3 ralphx.db ".timeout 2000" "SELECT 1;"` — if that cannot get + through, the lock is held by the app process. +5. **Restart the host app.** If the DB was wedged, this clears it and the client reconnects + on its next ladder tick with no client-side action. **This is also the immediate unblock** + if you just need the pairing working again — but capture the log first, because a restart + destroys the evidence. + +## 5. Candidate fixes, once confirmed + +Do not apply these blind; confirm §4 first. + +| If | Then | +|---|---| +| The pruner holds a long transaction | Bound the delete — chunked `DELETE … LIMIT n` per tick, or a batched loop that releases the transaction between chunks. Retention is a background reclaim; it has no business blocking the auth path. | +| The sequencer's commit batching starves readers | Give the auth reads their own pooled connection, or shorten the commit batch window. Note WAL allows concurrent readers — a reader stalling implies a writer holding an exclusive lock, so check `BEGIN IMMEDIATE` scopes. | +| Neither reproduces under load | Add a `busy_timeout` at connection setup so a contended DB fails fast and typed instead of hanging forever. A request that returns `REMOTE_INTERNAL_ERROR` in 2s is strictly better than one that never returns: the client supervisor can classify it, the user sees a real error, and the 15s connect budget stops being consumed by silence. | + +## 6. The client-side hardening this exposes (separate lane, not host work) + +Worth recording because the client behaved poorly against a silent host: + +- The supervisor spends its **entire 15s connect budget** on a host that accepts TCP and + then says nothing, because step 1 (descriptor) *succeeds* and only step 2 hangs. It never + reaches `blocked`, so the UI says "Connecting… first time" indefinitely with no hint that + the host is unhealthy rather than absent. +- A host that answers the descriptor but stalls every authenticated route is a distinguishable + state and deserves distinct copy — "the host is not responding" rather than "connecting". +- `REMOTE_TIMEOUT_UNKNOWN` surfaced raw to the user (`get_execution_settings did not answer + within 30000ms. The outcome is UNKNOWN — reconcile by refetching, do not re-send.`). That + is protocol-accurate and user-hostile; it should render as a plain "the host stopped + responding" with a retry affordance. + +--- + +## Appendix — reproducing the measurement from any client + +```bash +HOST=http://100.95.136.117:3849 +TOKEN=$(security find-generic-password -s com.ralphx.app \ + -a "remote-env::token" -w) + +# Answers (no DB): +curl -s -m 10 -o /dev/null -w "descriptor %{http_code} %{time_total}s\n" \ + "$HOST/.well-known/ralphx/environment" + +# Hangs (DB read): +curl -s -m 10 -o /dev/null -w "health %{http_code} %{time_total}s\n" \ + -H "Authorization: Bearer $TOKEN" "$HOST/health" + +# Hangs (DB write on the reject path) — the decisive probe: +curl -s -m 10 -o /dev/null -w "no-auth %{http_code} %{time_total}s\n" "$HOST/health" +``` + +A healthy host answers all three: `200`, `200`, and `401` respectively. diff --git a/docs/handoffs/remote-mobile/chat-send-spawn-free-design.md b/docs/handoffs/remote-mobile/chat-send-spawn-free-design.md new file mode 100644 index 0000000000..7fe74419c6 --- /dev/null +++ b/docs/handoffs/remote-mobile/chat-send-spawn-free-design.md @@ -0,0 +1,256 @@ +# Chat send on the remote facade — a spawn-free seam + +**Status:** design only, no implementation. Written for Fable/owner review. +**Lane:** PR 3.1-b batch 2 · **Base:** `feat/rme-31b-batch2` @ `2774540e4` +**Unblocks:** the single most user-visible remote gap — a paired `ui:agent` device cannot +send a chat message, because `send_agent_message` is unregistered and the client reads +`REMOTE_COMMAND_UNAVAILABLE` as "this host does not support chat". + +--- + +## 1. Why this is not just "register it at ui:agent" + +The obvious move — ledger `send_agent_message` as `AgentControl`, register it, require +`ui:agent` — is already what the ledger says, and it is still not registrable. The blocker is +not the risk class. It is that **the command spawns a provider process**, and the facade has a +standing structural rule that no registered command may: + +> `detector (c)`: reaching a `tool_paths` resolver IS spawning a process; `SpawnsProcess` is +> expressible only under `Elevated` (`capability_ledger_tests::detector_c_floors_process_spawn_authority`). + +`detector_c_floors_process_spawn_authority` asserts `registered_spawners.is_empty()`. So the +gate that stops chat send is a *capability* gate, not a scope gate, and no amount of scope +tightening opens it. Something has to change about the command's reachable sinks. + +That framing matters for the owner decision: this is not "do we trust remote users with +chat", it is "where does the process start, and who is standing there when it does". + +--- + +## 2. What the audit engine actually reports + +Run reproducibly via the checked-in probe: + +``` +cargo test --features test-utils --lib \ + remote_server::capability_ledger_tests::probe_chat_send_trio_sink_paths \ + -- --ignored --nocapture +``` + +Detectors: (a) transitively reaches an agent spawn/steer sink, target-sensitive; (b) writes +persisted state a registered background loop consumes to spawn/steer; (c) resolves a CLI path. + +| Command | Module | a | b | c | Class today | +|---|---|:-:|:-:|:-:|---| +| `send_agent_message` | `unified_chat_commands` | ✅ | ✅ | ✅ | AgentControl | +| `start_agent_conversation` | `unified_chat_commands` | ✅ | ✅ | ✅ | AgentControl | +| `create_agent_conversation` | `unified_chat_commands` | — | — | — | AgentControl *(module default)* | +| `send_chat_message` | `ideation_commands` | — | — | — | AgentControl *(module default)* | +| `resume_automation` | `automation_commands` | ✅ | ✅ | — | AgentControl `SeedsSpawnTriggeringState` | +| `finalize_automation` | `automation_commands` | — | ✅ | — | AgentControl `SeedsSpawnTriggeringState` | +| `stop_automation` | `automation_commands` | — | — | — | AgentControl *(module default)* | +| `pause_automation` | `automation_commands` | — | — | — | AgentControl *(module default)* | + +**Exact sink paths** (the `PROBE-TRIO` lines, not paraphrased): + +``` +send_agent_message STEER -> ["send_message"] +send_agent_message LAUNCH -> ["crate::infrastructure::tool_paths::find_codex_cli_candidates", + "crate::infrastructure::tool_paths::resolve_node_cli_path", + "find_codex_cli_candidates", "resolve_git_cli_path", + "resolve_node_cli_path"] + +start_agent_conversation STEER -> ["send_message", "write_message"] +start_agent_conversation SCHEDULER -> ["execute_entry_actions", "try_schedule_ready_tasks"] +start_agent_conversation TRANSITION -> ["transition_task", "transition_task_corrective_with_exit"] +start_agent_conversation LAUNCH -> (same five resolvers) + +resume_automation STEER -> ["send_message"] +``` + +Two readings worth stating plainly: + +- `send_agent_message` carries **strictly less** authority than `start_agent_conversation`. It + reaches steer + launch, but NOT the scheduler and NOT the task transition sinks. The two are + routinely discussed as one problem; they are not one problem, and the cheaper half is the one + users actually want. +- `start_agent_conversation` reaching `transition_task` and `try_schedule_ready_tasks` means + starting a conversation can move a *task* through the execution state machine. Any remote + exposure of it is remote control of the Kanban pipeline, not just of a chat pane. + +--- + +## 3. The finding that shapes the design: the seam already exists + +The spawn-free seam does not have to be invented. **RalphX already ships one, on the ideation +chat surface**, and the probe found it by accident: + +`ideation_commands::send_chat_message` is detector-silent on all three because its entire body +is validate-then-persist — `chat_message_repo.create(message)` is the last statement +(`ideation_commands/ideation_commands_chat.rs`). It starts no process, emits no event, and +steers nothing. Agent invocation on that surface is a separate concern from message +persistence. + +`unified_chat_commands::create_agent_conversation` is likewise detector-silent: creating the +conversation row is already separable from starting it. + +So the codebase's own answer to "can a chat send be spawn-free" is **yes, and here is the +established pattern** — persistence-only send is not a novel architecture this PR would +introduce. That materially lowers the risk of the proposal below, and it should be the first +thing a reviewer checks, because it converts the question from "design a new seam" into +"extend an existing one to a second surface". + +### 3.1 …but `send_chat_message` must NOT simply be registered + +It is detector-silent and it is still **not** a safe registration as written. It takes a +client-supplied `role` and will persist a message as `Orchestrator`, `Worker`, `Reviewer`, or +`Merger`: + +```rust +let role: MessageRole = input.role.parse()...; +match role { + MessageRole::User => ChatMessage::user_in_session(...), + MessageRole::Orchestrator => ChatMessage::orchestrator_in_session(...), + ... +} +``` + +Those rows are agent-consumed transcript context. A remote client that can write a message +attributed to the *orchestrator* can put words in the agent's own mouth — prompt injection with +a forged speaker label, which is worse than plain content injection because the transcript's +role field is exactly what downstream prompt assembly trusts to distinguish instruction from +user input. + +This is a `MutatesAgentConsumedContent` surface with a **role-spoofing** amplifier, and it is +the concrete reason the seam needs a server pin rather than a bare registration. The facade +already has the mechanism: `PinnedField` (`registry.rs`), which `deny_permission_request` uses +to pin `decision = "deny"` so a client sending `"allow"` still denies. + +**Proposal A (cheap, high value, low risk):** register `send_chat_message` with +`pins: [PinnedField { param: "input", field: "role", value: "user" }]`. A remote client then +cannot author anything but a user turn, by construction, on the wire path — pins are read from +`spec.pins` at dispatch time, so the declaration cannot drift from behaviour. + +--- + +## 4. Per-op analysis and proposal + +### 4.1 `send_agent_message` — the one users want + +Reaches steer + launch because the chat service's `send_message` will **start a provider +process if no live session is attached**. The send and the spawn are fused in one command. + +Three candidate seams, in increasing cost: + +**Option 1 — Queue-then-authorize (recommended for design review).** +Split the command at the fusion point: + +- `enqueue_agent_message` — persists the user turn plus a "pending dispatch" marker, exactly + the `send_chat_message` shape. Detector-silent by construction. Registrable at `ui:agent` + with a pinned role. +- Dispatch to the provider happens on the HOST side, driven by the existing chat service, not + by the remote request. + +The honest problem, and the reason this is a *design* question rather than a task: **what +drives the dispatch?** If a background loop drains the queue, detector (b) will flag +`enqueue_agent_message` as a spawn-triggering writer the moment that loop is registered in the +loop inventory — and correctly so, because the remote client's write would then cause a spawn. +The capability would become `SeedsSpawnTriggeringState`, which IS expressible under +`AgentControl` (`inject_task` and `resume_automation` already carry it). That is a *coherent* +outcome, not a defeat: it means remote chat send is ledgered as "seeds state a scheduler +consumes", the same class as injecting a task, and it never resolves a CLI path on the request +path. Detector (c) stays silent, which is the gate that currently blocks registration. + +So Option 1's real claim is: **move the process launch off the request path**, accept +`SeedsSpawnTriggeringState`, and let the existing floor machinery classify it honestly. + +**Option 2 — Live-session-only send.** Register a narrowed command that refuses when no live +provider session exists (`REMOTE_*` error rather than starting one). Sending to an already-running +conversation is then genuinely spawn-free. Cheaper than Option 1 and needs no queue, but the +UX is conditional in a way remote users will find arbitrary ("send works, except when it +doesn't"), and it needs a fail-closed liveness read — if the "is there a session" check errors +and is treated as "no session", the user gets a confusing refusal; if treated as "yes", the +command spawns and the gate is defeated. That check is the whole security surface. + +**Option 3 — Elevated registration.** Ledger it `Elevated` + `SpawnsProcess` and relax +`detector_c_floors_process_spawn_authority` for it. **Not recommended.** It converts the +facade's strongest mechanical invariant into a per-command judgement call, and the first +exception is the one that makes the second cheap. It also puts chat send behind `ui:elevated`, +which is not the scope the product wants for the default remote pairing. + +### 4.2 `start_agent_conversation` — recommend NOT registering in 3.1 + +It reaches scheduler AND transition AND launch. Registering it is remote control of the task +state machine under a chat-shaped name. The user-facing need ("I want to chat from my phone") +is served by sending into a conversation the host already started; **starting** one remotely is +a separate product decision that should not ride a registration sweep. + +If it is wanted later, `create_agent_conversation` (detector-silent) is the registrable half — +create remotely, start on the host — which is the same split as Option 1. + +### 4.3 The 2.6-surfaced automation ops + +`stop_automation` and `pause_automation` are detector-silent and are **authority-reducing** in +the exact sense the ledger already recognises (`pause_task`, `block_task`, `stop_task`, +`deny_permission_request` all carry `AuthorityReducingExemption` down to `Operate`). + +**Proposal B:** audit both for the exemption and, if they hold, ledger them `Operate` with an +`AUTHORITY_REDUCING_EXEMPTIONS` row and register at `ui:operate`. This extends the product's +promised "viewer with brakes" boundary to automation, which today can be started from a phone's +view of the board but not stopped. That asymmetry is the worst possible one to ship. + +`resume_automation` and `finalize_automation` stay `AgentControl` — both already carry +`SeedsSpawnTriggeringState` from a real detector (b) hit, and `resume_automation` additionally +reaches `send_message`. Restoring authority is not the mirror of reducing it. + +--- + +## 5. Recommended sequencing + +| # | Change | Class / scope | Cost | Risk | +|---|---|---|---|---| +| 1 | `send_chat_message` + pinned `role: "user"` | AgentControl / `ui:agent` | low | low — detector-silent, pin is proven machinery | +| 2 | `stop_automation`, `pause_automation` if the brake audit holds | Operate / `ui:operate` | low | low — established exemption shape | +| 3 | `send_agent_message` via Option 1 queue seam | AgentControl `SeedsSpawnTriggeringState` / `ui:agent` | high | medium — new dispatch driver, needs loop-inventory row | +| 4 | `start_agent_conversation` | — | — | defer past 3.1 | + +Items 1 and 2 are registration-shaped and could land in a batch-3 sweep. Item 3 is a feature +with a state machine and belongs in its own PR. + +--- + +## 6. Owner decisions required + +1. **Is remote chat send allowed to cause a provider process to start at all?** If yes, + Option 1 is the design and `SeedsSpawnTriggeringState` is the honest capability. If no, only + Option 2 (live-session-only) is available and the UX is conditional. Everything else follows + from this answer. +2. **Should `detector_c_floors_process_spawn_authority` ever admit an exception?** The + recommendation is a firm no, and to treat any command that needs one as a redesign signal. + Worth an explicit ruling, because Option 3 will keep being proposed as the cheap path. +3. **Is remote *starting* of conversations/tasks in scope for the remote product at all**, or + is remote confined to participating in work the host began? §4.2 and the deferral of + `start_agent_conversation` assume the latter. +4. **`ui:agent` vs a new scope for content authorship.** Every proposal here writes + agent-consumed content. The scope set has no "may write transcript content" distinct from + "may steer an agent". If the owner wants a device that reads and brakes but cannot author + prompt text, that is a new scope, and deciding it before item 1 lands is much cheaper than + after. +5. **Does the pinned-role restriction break a real client need?** Item 1 assumes no legitimate + remote client authors non-user roles. If the mobile client is ever meant to replay + orchestrator turns, the pin is wrong and the design needs a different guard. + +--- + +## 7. What this design does NOT claim + +- No claim that Option 1's queue is free of the false-success class. A queued message that is + persisted but never dispatched shows the user a sent message that no agent ever saw. The + dispatch driver needs its own terminal/failure surfacing, and that is a substantial part of + item 3's cost — it is exactly the "authority before effects" rule in + `stateful-workflow-review.md` and should be reviewed under that lens, not this one. +- No claim that the detector silence of `send_chat_message` / `create_agent_conversation` makes + them safe. Detectors are a floor. §3.1 is a live example of a detector-silent command that is + unsafe to register as written, and it was found by hand-tracing, not by the engine. +- No audit of the remaining `unified_chat_commands` / `automation_commands` members. Only the + commands listed in §2 were probed. diff --git a/docs/handoffs/remote-mobile/full-host-control-reassessment.md b/docs/handoffs/remote-mobile/full-host-control-reassessment.md new file mode 100644 index 0000000000..1f6425636b --- /dev/null +++ b/docs/handoffs/remote-mobile/full-host-control-reassessment.md @@ -0,0 +1,100 @@ +# Remote Multi-Env — Full Host-Control Reassessment (2026-08-01) + +Scope: verification of the 2026-07-31 gap report against branch tip `bb84dd53c`, plus the phase plan for lifting v1 deferrals so a paired client can manage the host — conversations, artifacts, reviews, options — with near-local fidelity under `ui:agent`. Evidence gathered by three independent code sweeps over this worktree; all file:line refs verified at `bb84dd53c`. + +Authority note: the gists circulated with the report (`ce3bf9c4…`, `5964a766…`) are the **provider-connections** spec/plan, not the remote spec. The authoritative remote spec is `.artifacts/specs/remote-multi-env/source-spec.md` (round 6) + `tracker.md` + `docs/handoffs/remote-mobile/spec-amendment-proposal.md`. + +--- + +## 1. Verdict on the reported A-list gaps + +All five confirmed, but three had wrong root causes. Corrections matter because they change the fix shape. + +### Gap 1 — Continue an idle conversation: CONFIRMED, intentional (Option 2) + +- `send_remote_chat_message` refuses without a live run at `src-tauri/src/commands/remote_chat_commands.rs:123-135` (`REMOTE_CHAT_SEND_NOT_STEERABLE`). Deliberate: the queued row is drained only inside a live run (`chat_service_queue.rs:987`), so a no-run send would be a persisted-but-never-delivered false success. This is design §4.1 **Option 2** shipped as designed; **Option 1** (durable queue + host dispatch driver, `SeedsSpawnTriggeringState`) is the unbuilt continuation seam, pre-acknowledged in the tracker ("future PR if the UX reads as arbitrary" — it does). +- The spawn-free start path mints **new conversations only**: `RequestRemoteAgentConversationStartInput` has no `conversation_id` field (`remote_conversation_start_commands.rs:71-79`); the dispatcher calls `AgentConversationStartService::start`, which treats a supplied id as a seeded *draft*, never a resume target (`start.rs:442`). +- The real continuation seam is provider-session resume inside `ChatService::send_message` (`chat_service_context.rs:2791-2843`, `--resume `), reachable only via `send_agent_message` — ledgered `host-denied-spawns-process`. + +### Gap 2 — Stop a running agent: CONFIRMED, including "silent" + +- `stop_agent` is unregistered because its implementation resolves `pkill` (`capability_ledger.rs:2956-2963`) — blocked by the detector-(c) process floor, not by risk class, even though it is authority-*reducing* (the spec's own exemption category; `stop_execution`/`pause_execution`/`deny_permission_request` all carry `AUTHORITY_REDUCING_EXEMPTIONS` rows). +- It is missing from the agent-gate/inert affordance maps, so the Stop button renders **enabled** remotely; the host answers `REMOTE_COMMAND_UNAVAILABLE`, and the failure is swallowed twice: `useChatActions.ts:448-459` (`logger.warn` only) and `AgentComposerSurface.tsx:2239-2243` (`void onStop?.()`). +- The chat-send design doc never audited `stop_agent` (§7 disclaims it) — this is an unexamined gap, not deferred work. + +### Gap 3 — Host-produced attachments: CONFIRMED, worse than reported + +- Not a scoping bug: host-agent files never get a `remote_attachments` row at all — the only writer is the client `upload_handler` (`attachments.rs:120-305`). The fetch route is additionally device-scoped in SQL (`sqlite_remote_request_dedup_repo.rs:248`). +- **No frontend code calls `/remote/v1/attachments/{id}` at all** — the placeholder ("Stored on the host", `host-affordances.ts:31`) cannot ever fill by construction. `MessageAttachments.tsx:92-94` still says the endpoint work is "DEFERRED TO 3.1"; 3.1 closed without it (tracker's orphaned-scope ledger owes it explicitly). +- Fix = (a) host-side ingress minting rows for host-produced files, (b) an authorization model beyond `device_id = uploader` (host-owned vs device-owned), (c) client GET + blob URL rendering. No spawn machinery involved. + +### Gap 4 — Expand a tool call: CONFIRMED unregistered; "never audited" REFUTED + +- `get_agent_message_tool_call_detail` / `get_agent_timeline_item_tool_call_detail` are `v1-audit-refused / fail-open-until-fixed`: `load_delegated_tool_runtime_snapshot` applies `.ok().flatten()` to five repo reads (`unified_chat_commands/mod.rs:2496-2536`), so an outage serves a **stale persisted tool result as current**. +- Handlers are pure DB reads (`mod.rs:10015`, `:10056` — no AppHandle/ExecutionState/ChatService). Fix = propagate the five errors, reclassify from the conservative module default to `read`, register at `ui:read` (registered sibling `get_remote_agent_conversation` already carries transcript text at that tier). + +### Gap 5 — Open a workspace review: CONFIRMED; blocker is the fetch remount, not invoke + +- The event forwards fine and both artifact halves are already fetchable (`get_artifact` is registered). The break: the artifact ids come only from `GET /api/agent-workspaces/:id/workspace-review-context` via `backendFetch`, and that route is not in the 8-row `REMOUNT_ALLOWLIST` (`fetch_remount.rs:92-141`). Ids stay null → both `get_artifact` queries stay disabled → opened tab, no artifact. +- Fix = one `RemountRoute` (GET, `RiskClass::Read`) + handler arm; audit the `?refresh_target=true` variant (`chat.ts:4148`) for read-onlyness first. Same story for `pr-review-context` (`AgentsArtifactPane.tsx:979`). + +--- + +## 2. The bigger reframe the report missed + +The tracker's UX lens (record 16) is blunter than the gap report: **"the remote product is substantially non-functional today even though the security model is sound."** Two items sit *ahead of* the A-list: + +- **UX-1 (front door):** `list_projects`/`get_project` are spawn-blocked and CI-pinned unregistered (`spawning_project_getters_are_elevated_and_not_registered`); census batch R1 (spawn-free project read) is a code change gated on an owner call. ⚠️ The dogfood session reported project/task reads working — reconcile whether a twin landed post-census or the dogfood host used a different path before treating UX-1 as open. +- **UX-2:** the purpose-built remote transcript twins (`get_remote_agent_conversation*`, 6 registered commands) have **zero frontend callers** — the UI still invokes the unregistered local names. Registration without affordance repointing ships dead capability (chat-send lane finding #1). + +Also open from the tracker follow-up ledger (owner sign-off owed): A2 `get_artifacts(None)` fail-open, A3/L2 transcript trio swallows 5 delegated-tool reads while its ledger claims propagation, A-new `UpdateTaskInput.internal_status` silently dropped on the registered Operate path, B-new1 attachment orphan-blob quota leak, D1/M3 drift/mirror CI gates skipped by backend-/docs-only PRs. + +--- + +## 3. What "full host control" actually requires — inventory triage + +Manifest state at tip: 555 ledger rows; **226 registered** (130 read / 93 agentControl / 5 operate — tracker counts are stale); 114 `host-denied-spawns-process`; 98 `host-denied`; 58 `v1-deferred`; 34 `v1-audit-refused`; **25 `registerable` but unregistered**. + +Standing owner rulings (2026-07-28) that bound this phase: detector-(c) process floor is absolute ("no exceptions ever") — every unlock below is a redesign, never a relaxation; "permitting more = register more surface under `ui:agent`, never soften the tier boundary"; every spawn-free steering command needs an explicit `DECLARED_MEMBERSHIPS` row (P-17b generates from detector output and cannot see them). + +| Bucket | Content | Cost | +|---|---|---| +| **A — free now** | 25 `registerable` rows: `queue_agent_message`, `get_effective_manual_role_default`, `set_max_concurrent`, 6 agent-profile ops, 11 ideation ops (incl. `send_chat_message`, `send_orchestrator_message`), `get_team_artifacts_by_session`… | Registration + affordance repoint only | +| **B — one shared fix** | `transport-shape-deferred` rows (~10, bodies audit clean): all 5 task-step ops, `reorder_task_steps`, folder-reference reads, `abort_seeded_agent_conversation` | Make `AppError` serializable (or add a rendering dispatch arm). Highest ratio in the inventory | +| **C — small named bug fixes** | fail-open/corrective rows with fixes already written in the manifest: the 2 tool-call-detail reads, `request_task_changes_from_reviewing` (propagate 2 serde errors), `reject_fix_task` (mediator pins target), `get_manual_role_defaults`, `get_pending_permissions`/`questions`, `set_active_plan`… | Per-command TDD fix + reclassify + register | +| **D — intent-dispatcher / seam splits** | The control plane: continue, stop, queued-send-now, persona/mode switch, auto-publish & PR-supervision flags, muted, execution settings, resume family (UX-4), workspace-hydrator reads, 29 `diff_commands` + 4 `plan_branch_commands` (blanket "may spawn git", never hand-traced) | Pattern is proven twice (`send_remote_chat_message`, `request_remote_agent_conversation_start`); details §4 | +| **E — policy-only unlocks** | `update_/clear_manual_role_default` (a variant refusing `approval_policy`/`sandbox_mode` writes is the honest split), `repository_settings` (already "flagged-not-flipped for phase review"), `add_conversation_folder_reference` (manifest names the unlock: project-root allowlist confinement) | Owner decision + narrowed variant | +| **F — stays denied** | terminal/PTY, api-key/external-MCP/credential ops, git-origin/gh-auth, destructive cleanup/merge-conflict ops, `deletesEntity` rows, installer surface, `workspace_open` (opens on the wrong machine), attachments-as-written (`writesArbitraryPath`; the bounded endpoint in §1-Gap 3 is the replacement) | — | + +--- + +## 4. Proposed lanes (order matters) + +**Lane 0 — front door + dead capability (prereq).** Resolve the UX-1 project-read question (owner call on census R1); wire UX-2 (repoint UI to the registered transcript twins); Bucket A registrations with affordance repoints; Bucket B `AppError` serialization. + +**Lane 1 — Continue + Stop (converts "watch" to "manage").** +- *Continue:* clone the intent pattern (`remote_conversation_start_requests` shape: entity + CAS-claim repo with distinctive method names for detector-(b), migration, 2s dispatcher with stale-lease sweep in the always-run startup prefix, `SPAWN_TRIGGERING_STATE_SURFACE` row, `DECLARED_MEMBERSHIPS` row, pinned `role:"user"`). Critical difference: the dispatcher terminal call must be `ChatService::send_message` (hits the `--resume` seam at `chat_service_context.rs:2791-2843`), **not** `AgentConversationStartService::start` (fresh-run semantics). Client: invert today's liveness check — live run ⇒ existing queued path; idle ⇒ persist continue-intent + poll. Must solve the design doc §7 hazard: a persisted-never-dispatched message needs terminal/failure surfacing. +- *Stop:* two coherent options — (i) stop-intent row drained by a host loop (request path never resolves a CLI), or (ii) refactor termination off `pkill` and take an `AUTHORITY_REDUCING_EXEMPTIONS` row at `ui:operate` like `stop_execution` (spec-aligned: stop is authority-reducing and arguably shouldn't need `ui:agent` at all). Either way: fix both silent-swallow sites (`useChatActions.ts:452-459`, `void onStop?.()` at `AgentComposerSurface.tsx:2241`) and add the affordance to the agent-gate map so unavailable renders as a hint, not an enabled button. + +**Lane 2 — render fidelity (all spawn-free).** Workspace-review + PR-review context remounts (Gap 5); tool-call detail fail-open fix + registration (Gap 4); host-produced attachment ingress + authorization + client fetch (Gap 3, the largest of the three — needs a small design). + +**Lane 3 — diff/branch audit lane (unlocks the review/PR domain).** Hand-trace the 29 `diff_commands` + 4 `plan_branch_commands` past the blanket module reason; split persisted/cached-served reads (registerable) from genuinely-spawning ones (host-side snapshot job read remotely, per the twin/projection precedent). Biggest single unlock, biggest unaudited surface — budget it as its own lane. + +**Lane 4 — options & workspace automation.** Seam-split the DB-only flag writes from their incidental spawn probes: `set_agent_conversation_workspace_auto_publish`/`_pr_supervision` (split flag write from `resolve_agent_workspace_pr_automation_target`), `set_agent_conversation_muted`, `update_execution_settings` (write-then-host-drains), `update_agent_conversation_coordination_mode` (non-Ultra variant is spawn-free today), `activate_agent_task_pipeline`/`_plan_direct_implementation` (manifest pre-argues the seam split). Resume family (UX-4) rides the same pattern. + +**Lane 5 — Bucket E policy decisions + Bucket C fix batch.** + +--- + +## 5. Owner decisions needed before/while building + +1. **Q3 supersession:** the 2026-07-28 ruling said remote *starting* stays host-only in v1; Parts B+C then shipped spawn-free remote start. Record the supersession (or revert) — the tracker ruling and the shipped code currently contradict. +2. **Stop tier:** `ui:operate` via authority-reducing exemption (spec-consistent) vs `ui:agent` via intent row. Recommend (ii)+(i) hybrid: intent row, ledgered authority-reducing, registered at `ui:operate`. +3. **Auto-merge from a phone:** the report's open flag — remote devices can flip `auto_commit`/PR auto-merge under `ui:agent`. Confirm intended, or pin those two behind an explicit host-side toggle. +4. **Role defaults:** accept the narrowed variant (no `approval_policy`/`sandbox_mode` writes) or keep `v1-deferred`. +5. **UX-1/R1:** authorize the spawn-free project-read code change (if still open post-dogfood). +6. **Attachment authorization model** for host-produced files (host-owned rows readable by any paired `ui:read` device vs per-conversation scoping). + +## 6. Deploy note + +None of this is observable until the Mac Studio host runs a build ≥ `bb84dd53c` (which also carries the dispatcher-placement fix `bb84dd53c` — the start dispatcher previously sat behind the recovery pipeline and could be disabled with it). diff --git a/docs/handoffs/remote-mobile/full-remote-management-implementation-spec.md b/docs/handoffs/remote-mobile/full-remote-management-implementation-spec.md new file mode 100644 index 0000000000..ec3098ba0d --- /dev/null +++ b/docs/handoffs/remote-mobile/full-remote-management-implementation-spec.md @@ -0,0 +1,51 @@ +# Remote Multi-Env — Full Remote Management: Cleanup + Implementation Spec (2026-08-01) + +Goal: a paired `ui:agent` client manages the host with near-local fidelity — conversations (start/continue/steer/stop), artifacts, workspace reviews + Review PR, workspace automation flags, and options. Read-only/one-shot behaviours shipped as v1 stopgaps are removed. Companion analysis: `full-host-control-reassessment.md` (same directory) — all file:line evidence lives there. + +## Owner decisions (resolved 2026-08-01, this spec) + +| # | Decision | Resolution | +|---|---|---| +| 1 | Q3 (remote start host-only) | **Superseded** by shipped Parts B+C. Record in tracker; spawn-free start is sanctioned. | +| 2 | Stop tier | Intent-row redesign, ledgered authority-reducing, registered at **`ui:operate`** (brakes stay on the default pairing). | +| 3 | Auto-merge/auto-commit from remote | **Permitted** under `ui:agent` (full-management goal). No extra host toggle. | +| 4 | Role defaults | Narrowed variant registered: refuses `approval_policy`/`sandbox_mode` writes; those two stay host-local. | +| 5 | UX-1/R1 project reads | **Authorized**: spawn-free project read path (twin or seam split), registered `ui:read`. | +| 6 | Host-produced attachments | Host-owned rows readable by **any paired device at `ui:read`**, conversation-scoped ids. | + +## Invariants (NON-NEGOTIABLE, every work package) + +1. **Detector-(c) process floor is absolute.** No command that resolves a CLI path or spawns is ever registered. Unlocks are redesigns: intent rows drained by host-owned dispatchers, seam splits isolating DB-only writes, projected/persisted reads (twin pattern). +2. Never soften the tier boundary: reads → `ui:read`; brakes/inert → `ui:operate`; arming/steering/content writes → `ui:agent`. Pins (`role`, `mode`, field-absence) over trust. +3. Every spawn-free steering command gets a `DECLARED_MEMBERSHIPS` row (P-17b is detector-generated and blind to them) and, when it arms a loop, a `SPAWN_TRIGGERING_STATE_SURFACE` row in `authority_audit.rs`. +4. Fail closed. No `.ok().flatten()`/`unwrap_or_default()` on reads that gate rendering or authority. Registering a command with a known fail-open requires fixing it first. +5. New intent surfaces follow the proven shape: entity + status enum with terminal failure states, CAS `claim_pending_*` repo method with **distinctive method names** (detector-b ties mechanically), migration (forward-only, after `v20260801120000`), dispatcher in the **always-run startup prefix** (`startup_pipeline.rs` before the recovery early-return), stale-lease sweep, revalidation before spawn, terminal/failure surfacing to the client (design doc §7 hazard: persisted-but-never-dispatched must become a visible failure). +6. TDD; focused tests only (rule 8); `cd src-tauri && cargo clean` after any Rust test run in your worktree (rule 8.5). Regenerate `docs/generated/remote-commands.json` + frontend generated mirrors after ledger/registry changes. +7. Stays denied forever (out of scope, do not touch): `agent_terminal_commands`, `api_key_commands`, `external_mcp_commands`, gh/git credential + origin ops, `resolve_merge_conflict`/`cleanup_task*`, installer surface, `workspace_open_commands`, `deletesEntity` rows, `update_custom_analysis`. + +## Work packages + +### Wave 1 (parallel worktrees off `feat/remote-multi-env`) + +**WP1 — Conversation continuation (Option 1).** +Remove one-shot behaviour. New intent surface `remote_conversation_message_requests` (or `kind` column reuse — implementer's call, document it): client `request_remote_agent_conversation_message` (AgentControl, pinned `role:"user"`, caps `[MutatesAgentConsumedContent, SeedsSpawnTriggeringState]`) + `get_remote_conversation_message_request` poll (Read). Dispatcher terminal call is `ChatService::send_message` (provider-session resume seam, `chat_service_context.rs:2791-2843`) — **not** `AgentConversationStartService::start`. Client: `sendMessage` remote branch becomes live-run ⇒ existing `send_remote_chat_message` path; idle ⇒ intent + poll; remove/replace the `REMOTE_CHAT_SEND_NOT_STEERABLE` dead-end UX. Also carry the UX-5 fix: composer options (model/effort) travel in the intent instead of being silently dropped. + +**WP2 — Stop + brake surfacing.** +`request_remote_agent_stop` intent row (statuses incl. `NoLiveRun` terminal), dispatcher calls host-local stop (the pkill path stays host-owned). Ledger: authority-reducing, register at `ui:operate` with `AUTHORITY_REDUCING_EXEMPTIONS` row. Client: repoint stop affordance for remote envs; **fix both swallow sites** (`useChatActions.ts` catch → surfaced error/toast state; `AgentComposerSurface.tsx` `void onStop?.()` → awaited with failure surfacing — check local UX parity); add gate-map entries so unavailable ops render hints, not enabled buttons. Include `send_queued_agent_message_now`'s stop half only if trivial; else record deferred. + +**WP3 — Render fidelity: remounts + tool-call detail.** +(a) Add `GET /api/agent-workspaces/:conversation_id/workspace-review-context` and `.../pr-review-context` to `REMOUNT_ALLOWLIST` + handler arms; audit `?refresh_target=true` for read-onlyness first — if it mutates, strip the param on the remote path and document. (b) Fix the five `.ok().flatten()` fail-opens in `load_delegated_tool_runtime_snapshot`, reclassify both tool-call-detail commands to `read`, register at `ui:read`. Also fix the A3/L2 ledger-claim mismatch (transcript trio's five swallowed delegated-tool reads) if it is the same seam. + +**WP4 — Registration sweep + dead-capability wiring.** +(a) `AppError` serialization (or fallible dispatch arm) → register the ~10 clean `transport-shape-deferred` rows (5 task-step ops, `reorder_task_steps`, folder-reference reads, `abort_seeded_agent_conversation`). (b) Register the 25 `registerable` rows with frontend affordance repoints (registration without repoint = dead flag). (c) UX-2: repoint the UI transcript reads to the six registered `get_remote_*` twins for remote environments. (d) UX-1: spawn-free `list_projects`/`get_project` (twin or seam split past the spawning hydrator), registered `ui:read`; update the CI pin test to assert the *spawning* getters stay unregistered while the twins are. (e) Bucket C small fixes as reachable: `get_pending_permissions`/`get_pending_questions` fail-open, `get_manual_role_defaults` fabricated default, `set_active_plan`. + +### Wave 2 (after Wave 1 merges) + +**WP5 — Diff/branch audit lane.** Hand-trace 29 `diff_commands` + 4 `plan_branch_commands`; register the persisted/cache-served reads; host-side snapshot job + remote read for the genuinely-spawning ones. Unlocks review/PR fidelity. +**WP6 — Options & automation seam splits.** Auto-publish / PR-supervision flag writes split from `resolve_agent_workspace_pr_automation_target`; `set_agent_conversation_muted`; `update_execution_settings` (write-then-host-drains); non-Ultra `update_agent_conversation_coordination_mode`; resume family (UX-4) via intent rows; `activate_agent_task_pipeline`/`_plan_direct_implementation` seam splits. +**WP7 — Host-produced attachments.** Ingress minting host-owned `remote_attachments` rows (conversation-scoped), authorization per decision 6, client GET + blob rendering replacing the "Stored on the host" placeholder; startup orphan-blob sweep (B-new1). +**WP8 — Policy unlocks + review fixes.** Narrowed role-default variant (decision 4); `repository_settings` flip per 1.3 flagged-not-flipped review; folder-reference allowlist confinement; `request_task_changes_from_reviewing` (propagate serde errors) + `reject_fix_task` (mediator pins corrective target). + +## Merge protocol + +Each WP: own worktree + branch `feat/rme-wp-` off `feat/remote-multi-env`. Orchestrator reviews the diff, re-runs the WP's stated gates, merges into `feat/remote-multi-env` sequentially, regenerates `remote-commands.json` + frontend mirrors after each merge (the generated files are expected conflict points — resolution is always "regenerate", never hand-merge). Tracker updated per WP (Q3 supersession recorded in WP1's merge). diff --git a/docs/handoffs/remote-mobile/mobile-client-techstack-architecture.md b/docs/handoffs/remote-mobile/mobile-client-techstack-architecture.md new file mode 100644 index 0000000000..2c66f16ee5 --- /dev/null +++ b/docs/handoffs/remote-mobile/mobile-client-techstack-architecture.md @@ -0,0 +1,511 @@ +# RalphX Mobile — React Native Client: Tech Stack, Architecture & Full UX/UI Spec (2026-08-01, rev 2) + +The mobile app is a **remote control for RalphX hosts**: it executes nothing locally, syncs the way the desktop client syncs (snapshot-hydrate + live WS), and inherits every capability the remote facade registers. Rev 2 commits to **React Native** and specs the complete screen set. Protocol ground truth: `.artifacts/specs/remote-multi-env/source-spec.md`; capability roadmap: `full-remote-management-implementation-spec.md`. + +--- + +## 1. Tech stack (React Native, committed) + +| Layer | Choice | Notes | +|---|---|---| +| Framework | React Native + Expo (dev client, EAS builds) | OTA updates for UI iterations; native modules allowed | +| Navigation | Expo Router (stack per tab, native gestures) | Deep links: `ralphx://pair?...#code=...`, `ralphx://env//...` | +| Styling | NativeWind (Tailwind syntax) | Tokens mirrored from the desktop design system; accent `#ff6b35`; system font (SF Pro / Roboto); dark-first | +| Data | TanStack Query, keys `(envId, cmd, argsHash)` | Event-driven invalidation identical to the desktop remote environment | +| State | Zustand stores: environments, supervisors, gates, composer drafts | Ports of the desktop TS stores | +| Transport | `fetch` + `WebSocket` implementing `NetworkInvoke` / `NetworkEventBus` | The only platform-specific layer; everything above rides shared packages | +| Shared packages | `@ralphx/remote-protocol` (zod schemas, 10-code error taxonomy, generated capability manifest), `@ralphx/remote-client` (supervisor, epoch/cursor, intent-poll helpers), `@ralphx/api` (typed command wrappers) | Extracted from `frontend/src/lib/remote` + `frontend/src/api`; consumed by desktop and mobile | +| Secure storage | `expo-secure-store` (iOS Keychain / Android Keystore) | Device token never in JS-accessible plain storage; supervisor requests it per connect | +| QR | `expo-camera` scanner | Pairing payload: one-time code in URL hash fragment | +| Lists | FlashList | Transcript + kanban virtualization | +| Media | Streamed download of `/remote/v1/attachments/:id` to file URI | Blob-in-memory only under ~2 MB | + +**Sync model (unchanged from desktop):** invoke plane (`POST /remote/v1/invoke`), event plane (`GET /remote/v1/events?ticket=…` with `(streamEpoch, cursor)` resume; new epoch ⇒ cold-hydrate), allowlisted fetch remounts, intent rows + poll for spawn-adjacent mutations (start / continue / stop). Offline = read-only cache + banner; no mutation outbox. Background suspension is modeled as an ordinary disconnect; foreground triggers the reconnect path. + +**Authority rules (non-negotiable, enforced in the shared client layer):** +- Writable affordances require confirmed scopes AND `presentation === connected`. +- The scope set gates rendering: `ui:read` viewer, `+ui:operate` brakes/inert edits, `+ui:agent` full control. Never render an enabled control the token can't use — render the locked variant with the reason. +- Every mutation carries `requestId` idempotency. All errors render from the 10-code `REMOTE_*` taxonomy — never a generic toast for a typed refusal. +- `emit()` on the event bus is local-only. The client never writes to the WS. + +--- + +## 2. Navigation architecture + +``` +Root +├─ (no environments) → S1 Welcome → S2 Pairing +└─ Tab bar (per active environment, switcher in header) + ├─ ⌂ Projects S3 → S4 Kanban → S5 Task + ├─ ✦ Agents S6 → S7 Conversation (→ S8 Tool detail, S9 Gates, + │ S10 Workspace → S11 Review) + ├─ ◇ Plans S12 Ideation sessions → session detail + ├─ ▤ Activity S13 Inbox/Notifications (+ Automations) + └─ ⚙ Settings S14 Environments & devices → S15 Env settings, S16 App +``` + +Global chrome on every screen: +- **Header**: environment pill (name + status dot: ● green connected / ◐ amber reconnecting / ○ gray offline) — tap opens the environment switcher sheet. Scope chip when not full control: `[viewer]` or `[operate]`. +- **Offline banner** (persistent, under header, when disconnected): `○ Offline — showing cached data · Retry`. All mutating controls become locked variants. +- **Intent-pending affordance**: any intent-row action (start/continue/stop) shows an inline spinner on the control plus a status line fed by the poll; terminal failure surfaces as an inline error card with the `REMOTE_*` code's human string and a Retry. + +Design language: dark-first (`#111114` canvas, `#1b1b20` surfaces, 1px `#2a2a30` borders), accent `#ff6b35` reserved for primary actions + live-run indicators, status colors: running amber pulse, done green, failed red, paused gray. Text hierarchy via weight not color. Touch targets ≥ 44pt. Every icon-only button has an accessible label. + +--- + +## 3. Screens, one by one + +Legend for the specs: **Manages** = what the screen owns; **Data** = commands/events feeding it; **Scopes** = what each tier can do here. Mockups are ~phone width; `▸` = navigates, `⋯` = overflow menu. + +### S1 — Welcome (first run / zero environments) + +``` +┌──────────────────────────────────┐ +│ │ +│ ◆ RalphX │ +│ │ +│ Control your agents from │ +│ anywhere on your tailnet. │ +│ │ +│ 1. Open RalphX on your Mac │ +│ 2. Settings → Remote Access │ +│ 3. Show pairing code │ +│ │ +│ ┌────────────────────────────┐ │ +│ │ ▣ Scan QR code │ │ ← primary, #ff6b35 +│ └────────────────────────────┘ │ +│ Enter code manually │ ← text link +│ │ +│ Requires Tailscale on this │ +│ phone. Open Tailscale ↗ │ +└──────────────────────────────────┘ +``` + +**Manages**: entry into pairing; Tailscale prerequisite check (best-effort probe of the host URL scheme — if unreachable, inline hint "Can't reach the tailnet — is Tailscale connected?"). +**Data**: none (pre-auth). +**States**: default; camera-permission-denied (fall back to manual entry with explainer). + +### S2 — Pairing + +``` + Scan Manual +┌──────────────────────────────────┐ +│ ‹ Back │ +│ ┌────────────────────────────┐ │ +│ │ │ │ +│ │ [ camera view ] │ │ +│ │ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ┐ │ │ +│ │ QR reticle │ │ +│ │ └ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │ │ +│ └────────────────────────────┘ │ +│ Point at the pairing code on │ +│ your Mac. │ +│ Enter code instead │ +└──────────────────────────────────┘ + ↓ on scan / submit +┌──────────────────────────────────┐ +│ Pairing with │ +│ ● studio.tailnet.ts.net │ +│ │ +│ This device will be able to: │ +│ ✓ View projects, tasks, chats │ +│ ✓ Stop, pause & deny (brakes) │ +│ ✗ Start or steer agents │ +│ (enable later on the host) │ +│ │ +│ Device name [ Adrian's iPhone ]│ +│ ┌────────────────────────────┐ │ +│ │ Pair device │ │ +│ └────────────────────────────┘ │ +└──────────────────────────────────┘ +``` + +**Manages**: one-time code → device-token exchange; device naming; scope preview (rendered from the pairing payload — clearly separates default `read+operate` from host-granted `ui:agent`). +**Data**: `POST /remote/v1/auth/pair`; token → SecureStore; environment row created; supervisor starts. +**States**: scanning; exchanging (spinner on button); success (auto-navigate to S3); failures — invalid/expired code, version mismatch, unreachable host — each a distinct full-width error card with the taxonomy string and "Try again". +**Scopes**: n/a (creates them). + +### S3 — Projects (environment home) + +``` +┌──────────────────────────────────┐ +│ ● Studio ▾ [operate]│ +│ Projects │ +│ ┌──────────────────────────────┐ │ +│ │ ralphx.app ▸ │ │ +│ │ 4 running · 2 awaiting review│ │ +│ ├──────────────────────────────┤ │ +│ │ themefy-web ▸ │ │ +│ │ idle · 12 open tasks │ │ +│ ├──────────────────────────────┤ │ +│ │ internal-tools ▸ │ │ +│ │ idle │ │ +│ └──────────────────────────────┘ │ +│ │ +│ Projects are managed on the │ +│ host. This list is read-only. │ +└──────────────────────────────────┘ +``` + +**Manages**: project selection; per-project live summary (running agents, review-waiting counts). Explicitly does NOT create projects (spec non-goal — footer states it). +**Data**: spawn-free project reads (`list_remote_projects` twin), task/agent count reads; invalidated by `task:*` / `agent:*` events. +**States**: loading skeleton rows; empty ("No projects on this host yet — add them on the Mac"); offline (cached list, dimmed counts + "as of 12:41"). +**Scopes**: identical at all tiers (pure read). + +### S4 — Kanban (project board) + +``` +┌──────────────────────────────────┐ +│ ‹ ralphx.app ⌕ ⋯ │ +│ Plan: v0.90 remote ▾ │ ← active-plan filter +│ ◄ Backlog │ Ready │ Running ► ● │ ← swipeable columns +│ ┌──────────────────────────────┐ │ +│ │ Fix FK baseline diff ▸ │ │ +│ │ #482 · fix · ▲ high │ │ +│ ├──────────────────────────────┤ │ +│ │ Remote stop lane ▸ │ │ +│ │ #495 · feat · ⟳ worker 12m │ │ ← amber pulse when running +│ ├──────────────────────────────┤ │ +│ │ + Add task (Backlog) │ │ ← operate: backlog-only create +│ └──────────────────────────────┘ │ +│ long-press card: │ +│ ┌ Pause ─ Block ─ Move ▸ ┐ │ ← Move/resume locked at operate +└──────────────────────────────────┘ +``` + +**Manages**: column browsing (swipe between state-machine columns, running counts per column); task cards (title, id, type, priority, live run indicator); backlog task creation; brakes via long-press. +**Data**: task list reads + `task:*` events; `create_task` (Backlog pinned at operate), `pause_task`, `block_task`, `move_task`/`resume_task` (`ui:agent`). +**States**: per-column skeletons; empty column illustrations; offline = read-only cached board. +**Scopes**: read = browse; operate = + create-backlog, pause, block; agent = + move, resume, restart, approve (menu entries appear; locked variants show `⌂ agent control required` hint otherwise). + +### S5 — Task detail + +``` +┌──────────────────────────────────┐ +│ ‹ Board #495 ⋯ │ +│ Remote stop lane │ +│ ⟳ Running · worker · 12m │ +│ [ Overview | Agent | Steps | │ +│ Activity | Review ] │ +│ ────────────────────────────────│ +│ Overview │ +│ Priority ▲ High (editable) │ +│ Category feat (editable) │ +│ Branch feat/rme-wp2-stop │ +│ Descr. Implement stop via … │ ← read-only at operate +│ ────────────────────────────────│ +│ ┌ ■ Stop run ┐ ┌ ⏸ Pause ┐ │ ← brakes row, always visible +│ └────────────┘ └─────────┘ │ +│ ┌────────── Resume ───────────┐ │ ← agent-gated primary +└──────────────────────────────────┘ +``` + +**Manages**: full task lifecycle view. Tabs: **Overview** (metadata; `category`/`priority` editable at operate — the only inert edits; title/description editable only at agent tier because they're agent-consumed), **Agent** (see below), **Steps** (step list + status; start/complete/skip at agent), **Activity** (full-timestamp state history), **Review** (links into S11 when review artifacts exist). + +**Agent tab — the task↔conversation bridge** (parity gap on desktop too; mobile specs it first): + +``` +│ Agent │ +│ ⟳ worker · claude · turn 4 │ +│ ┌──────────────────────────────┐ │ +│ │ Latest activity │ │ +│ │ ✓ step 2/5 validate entity │ │ +│ │ 🔧 cargo test (running…) │ │ +│ │ ◉ Question pending ▸ │ │ → S9 gate sheet +│ ├──────────────────────────────┤ │ +│ │ ▸ Open conversation │ │ → S7 (linked agent +│ │ ▸ Open workspace │ │ conversation/workspace) +│ ├──────────────────────────────┤ │ +│ │ Task chat (3 messages) ▸ │ │ ← task-scoped messages +│ └──────────────────────────────┘ │ +``` + +Shows who is working the task (role, provider, run age), a compact live-activity feed (recent steps/tool events, not a full transcript — the full transcript lives in S7 via "Open conversation"), pending gates for THIS task's run, and the task-scoped chat thread. **Data**: run/status reads + `agent:*`/`step:*` events scoped to the task's current run; task→conversation/workspace resolution read (needs a small facade read if no registered command maps task → owning conversation — flag for WP4/Wave 2); `get_task_messages` (registerable, WP4). When no agent has ever run the task, the tab collapses to "No agent activity yet". +**Data**: task read + state history + steps + `task:*`/`step:*` events; brakes `stop_task`/`pause_task` (operate); `update_task`, step ops, `resume_task` intent (agent). +**States**: running (live header pulse), paused, blocked (banner with unblock — agent-gated), failed (terminal banner: "Failed steps are terminal on the host"), offline. +**Scopes**: as annotated; every locked control renders with the lock reason, never hidden — the user should learn what flipping `ui:agent` on the host unlocks. + +### S6 — Agents (conversations + inbox) + +``` +┌──────────────────────────────────┐ +│ ● Studio ▾ [agent] ⌕ │ +│ Agents ◉ 2 need you │ ← inbox strip, accent +│ ┌──────────────────────────────┐ │ +│ │ ◉ Permission: run `cargo …` │ │ +│ │ ralphx.app · builder ▸ │ │ +│ │ ◉ Question: pick base branch │ │ +│ │ themefy-web · planner ▸ │ │ +│ └──────────────────────────────┘ │ +│ Conversations │ +│ ┌──────────────────────────────┐ │ +│ │ ⟳ WP2 stop lane ▸ │ │ +│ │ opus · running · 2m ago │ │ +│ ├──────────────────────────────┤ │ +│ │ ○ Release notes draft ▸ │ │ +│ │ idle · yesterday │ │ +│ ├──────────────────────────────┤ │ +│ │ ✓ FK migration fix ▸ │ │ +│ │ done · Tue │ │ +│ └──────────────────────────────┘ │ +│ ┌──────┐ │ +│ │ ✚ New│ │ ← agent-gated FAB +└──────────────────────────────────┘ +``` + +**Manages**: the Agents inbox (pending permission requests + questions, surfaced above everything — these are the highest-urgency mobile moments) and the conversation list (host sidebar order preserved; status glyph, provider, recency). +**Data**: sidebar/list remote twins (paginated), pending-gate reads (fail-closed), `agent:*` + gate events; `✚ New` → S7 composer in start mode (start intent). +**States**: skeletons; empty ("No conversations yet"); inbox empty state collapses the strip; offline. +**Scopes**: read = list + transcripts; operate = + deny permission, answer question **No** (deny-shaped answers only if backend classifies so — otherwise question answering is agent); agent = + new conversation, approvals. + +### S7 — Conversation + +``` +┌──────────────────────────────────┐ +│ ‹ Agents WP2 stop lane ⋯ │ +│ ⟳ running · opus · turn 6 │ ← run status bar (live) +│ ┌──────────────────────────────┐ │ +│ │ You 12:03 │ │ +│ │ Implement the stop intent… │ │ +│ │──────────────────────────────│ │ +│ │ ✦ Agent 12:04 │ │ +│ │ I'll start with the entity… │ │ +│ │ ▸ 🔧 Edit remote_stop.rs │ │ ← tool chip, tap → S8 +│ │ ▸ 🔧 cargo test (4 files) ✓ │ │ +│ │ ▸ 🖼 screenshot.png │ │ ← attachment, tap → viewer +│ │──────────────────────────────│ │ +│ │ ◉ Permission needed │ │ ← inline gate card → S9 +│ │ Run `git push origin…` │ │ +│ │ [ Deny ] [ Approve ] │ │ +│ └──────────────────────────────┘ │ +│ ┌──────────────────────────────┐ │ +│ │ Message… ⏎ │ │ +│ └──────────────────────────────┘ │ +│ opus ▾ · high ▾ ■ Stop │ ← composer options + stop +└──────────────────────────────────┘ +``` + +**Manages**: the core loop. Virtualized transcript (chrome + placeholders paint first, hydration after — same first-paint rule as desktop); live streaming blocks; tool chips (truncated preview, tap expands); attachments; inline gate cards; composer with model/effort options (these travel with the send — UX-5); **Stop** always visible while running. +**Data**: transcript twins (paged) + `agent:*` stream events; send: live run ⇒ `send_remote_chat_message`, idle ⇒ continue-intent + poll (seamless to the user — one send affordance, the client picks the path); stop ⇒ stop-intent + poll; gates via registered approve/deny + answer commands. +**States**: streaming (token-level append); idle (composer hint "Sending will wake this agent"); **send-pending** (message renders with ◐ "delivering…" until the intent terminalizes — a persisted-never-dispatched intent MUST surface as a red failure state on the bubble with Retry, never a ghost sent message); stop-pending (Stop → spinner → run status flips or inline failure); attachment placeholders fill from the binary route (until host-produced ingress ships: "Stored on the host" chip + copy-path); offline (composer disabled with reason). +**Scopes**: read = watch live; operate = Stop + Deny; agent = send/steer/approve/answer/new. + +### S8 — Tool-call detail (sheet) + +``` +┌──────────────────────────────────┐ +│ ── drag handle ── │ +│ 🔧 Edit remote_stop.rs ✕ │ +│ status ✓ completed · 1.2s │ +│ Arguments │ +│ ┌──────────────────────────────┐ │ +│ │ { "path": "src-tauri/…", │ │ +│ │ "old_string": "…" } │ │ ← mono, scrollable +│ └──────────────────────────────┘ │ +│ Result │ +│ ┌──────────────────────────────┐ │ +│ │ Applied 1 edit. 34 lines… │ │ +│ └──────────────────────────────┘ │ +│ Copy result │ +└──────────────────────────────────┘ +``` + +**Manages**: full untruncated arguments/result for one tool call. +**Data**: `get_agent_message_tool_call_detail` / `get_agent_timeline_item_tool_call_detail` (registered `ui:read` after WP3). Fetch error ⇒ error card in the sheet (fail-closed — never silently show the truncated preview as if complete). +**States**: loading, loaded, error+retry. + +### S9 — Gates (approval / question sheets) + +``` +┌──────────────────────────────────┐ +│ ◉ Permission request │ +│ WP2 stop lane · builder │ +│ │ +│ The agent wants to run: │ +│ ┌──────────────────────────────┐ │ +│ │ git push origin feat/rme-… │ │ +│ └──────────────────────────────┘ │ +│ ┌──── Deny ────┐ ┌─ Approve ──┐ │ +│ └──────────────┘ └────────────┘ │ +│ Deny is available to every │ +│ paired device. │ +├──────────────────────────────────┤ +│ ? Question │ +│ Which base branch? │ +│ ○ main │ +│ ● feat/remote-multi-env │ +│ [ or type an answer… ] │ +│ ┌──────────── Send ───────────┐ │ +└──────────────────────────────────┘ +``` + +**Manages**: the two mid-run gates. Approve/deny permission (deny at operate, approve at agent); structured or free-text question answers (agent — free-text steers a live run). +**Data**: pending-gate reads (fail-closed — a read error renders "Couldn't verify pending gates", never an empty happy state); registered `approve_`/`deny_permission_request` (pinned decisions), `answer_user_question`/`resolve_user_question` seam. CAS/stale handling: acting on an already-resolved gate renders "Already handled on the host" (idempotent, calm). +**Entry points**: inbox strip (S6), inline cards (S7), push notification (P3). + +### S10 — Workspace (per-conversation publish/PR surface) + +``` +┌──────────────────────────────────┐ +│ ‹ Conversation Workspace │ +│ branch feat/rme-wp2-stop │ +│ base feat/remote-multi-env ✓ │ +│ ┌──────────────────────────────┐ │ +│ │ Changes 14 files +812 −96 ▸│ │ ← diff summary (Wave-2 lane) +│ ├──────────────────────────────┤ │ +│ │ Review ● blocking: 2 ▸ │ │ → S11 +│ ├──────────────────────────────┤ │ +│ │ PR #961 open · checks ⟳ ▸ │ │ ← PR card (read + close) +│ └──────────────────────────────┘ │ +│ Automation │ +│ Auto-publish [on ▣] │ ← agent-gated toggles +│ PR supervision [on ▣] │ +│ Auto-merge [off □] │ +│ ┌────────── Publish ──────────┐ │ ← agent-gated primary +└──────────────────────────────────┘ +``` + +**Manages**: workspace state for one conversation: branch/base freshness, change summary, review status, linked PR lifecycle, automation toggles, publish. +**Data**: workspace reads (projected twins per Wave-2), publication events, PR monitor state; toggles = seam-split flag writes (`SeedsSpawnTriggeringState` @ agent); `publish_agent_conversation_workspace`, `close_agent_workspace_pr` (agent). Diff drill-down ships with the Wave-2 diff lane; until then the row shows counts only with "Full diff on the host". +**States**: clean/dirty/stale-base (banner: "Base moved — update from base runs on the host"); PR terminal (merged/closed collapses actions — durable-state-authoritative, no stale controls); offline. +**Scopes**: read = all state; operate = nothing extra; agent = toggles, publish, close PR. + +### S11 — Review artifact viewer + +``` +┌──────────────────────────────────┐ +│ ‹ Workspace Review v3 │ +│ ● Blocking · 2 requested changes │ +│ [ Overview | Requested changes ] │ +│ ┌──────────────────────────────┐ │ +│ │ ## Summary │ │ +│ │ The stop-intent dispatcher │ │ +│ │ correctly claims… │ │ +│ │ │ │ +│ │ ▸ src-tauri/…/stop.rs:141 │ │ ← hunk annotation link +│ │ claim/CAS race note… │ │ +│ └──────────────────────────────┘ │ +│ ┌ Request fixes ┐ ┌ Approve ─┐ │ ← agent-gated actions +└──────────────────────────────────┘ +``` + +**Manages**: the versioned Overview + Requested Changes artifact pair (markdown render, hunk annotations as expandable cards; file:line links copy the ref — no local file to open). Version picker in title. For **Review PR** conversations the same screen renders the PR-review artifact for the reviewed head, with propose-Approve/Request-Changes/Comment actions that queue for explicit user submission semantics identical to desktop. +**Data**: review-context remount (WP3) → artifact ids → `get_artifact`/`get_artifact_at_version`; review events invalidate. Actions: fixer routing / gate completion commands (agent). +**States**: no-review-yet empty state; context-fetch error (typed, retry); artifact version superseded banner ("A newer review exists — v4"). + +### S12 — Plans (ideation) + +``` +┌──────────────────────────────────┐ +│ ● Studio ▾ Plans │ +│ ┌──────────────────────────────┐ │ +│ │ ◇ Remote full management ▸ │ │ +│ │ verifying · round 2 │ │ +│ ├──────────────────────────────┤ │ +│ │ ◇ Provider connections ▸ │ │ +│ │ converged · 6 proposals │ │ +│ └──────────────────────────────┘ │ +│ session detail ▾ │ +│ [ Chat | Plan | Proposals ] │ +│ Proposals │ +│ ▣ P1 Continue lane │ ← selection toggles (agent) +│ ▣ P2 Stop lane │ +│ □ P3 Attachments │ +│ ┌── Apply to kanban (host) ───┐ │ ← locked: runs on host, v1 +└──────────────────────────────────┘ +``` + +**Manages**: ideation sessions list; per-session tabs: **Chat** (same composer pattern as S7 via `send_chat_message`/`send_orchestrator_message`), **Plan** (rendered plan doc + verification status read), **Proposals** (selection toggles). **Apply to kanban stays host-locked in v1** (spawn-heavy) — the button renders locked with "Runs on the host" until its Wave-2+ seam exists. +**Data**: ideation session/message/proposal reads (paged), `set_proposal_selection`/`toggle_proposal_selection`, session chat sends (agent); verification status reads. +**Scopes**: read = browse everything; agent = chat + proposal selection. + +### S13 — Activity (inbox, notifications, automations) + +``` +┌──────────────────────────────────┐ +│ ● Studio ▾ Activity │ +│ [ Needs you | All | Automations ]│ +│ Needs you (2) │ +│ ┌──────────────────────────────┐ │ +│ │ ◉ Permission · cargo publish │ │ +│ │ ◉ Question · base branch │ │ +│ └──────────────────────────────┘ │ +│ All │ +│ │ ✓ Review passed · WP1 · 12:22│ │ +│ │ ⇧ PR #961 opened · 12:10 │ │ +│ │ ■ Run stopped · WP3 · 11:58 │ │ +│ Automations │ +│ │ ⟳ nightly-triage · running │ │ +│ │ [ ⏸ Pause ] [ ■ Stop ] │ │ ← brakes at operate +└──────────────────────────────────┘ +``` + +**Manages**: unified notification feed (badge source for the tab), gate fast-path ("Needs you"), automation runs with brakes. Full timestamps on every row. +**Data**: notification reads (badge fail-closed: unread-count read error shows `!`, not 0), notification events; automation list/run reads, `stop_automation`/`pause_automation` (operate, authority-reducing). +**States**: empty ("All caught up"); offline (cached feed). + +### S14 — Environments & devices + +``` +┌──────────────────────────────────┐ +│ Settings │ +│ Environments │ +│ ┌──────────────────────────────┐ │ +│ │ ● Studio ▸ │ │ +│ │ agent control · connected │ │ +│ ├──────────────────────────────┤ │ +│ │ ○ MacBook ▸ │ │ +│ │ viewer · unreachable │ │ +│ └──────────────────────────────┘ │ +│ ┌──── ✚ Pair new environment ──┐ │ +│ App │ +│ Appearance Dark ▾ │ +│ Notifications On ▾ │ +│ About · protocol v1 │ +└──────────────────────────────────┘ +``` + +**Manages**: environment roster (connection state, granted tier), entry to pairing (S2) and per-environment settings (S15); app-local prefs. +**Data**: local environment store; descriptor probe on pull-to-refresh. + +### S15 — Environment settings + +``` +┌──────────────────────────────────┐ +│ ‹ Settings Studio │ +│ Connection │ +│ Host studio.tailnet.ts.net │ +│ Status ● connected · 34ms │ +│ Protocol v1 · epoch 8f2c │ +│ This device │ +│ Name Adrian's iPhone │ +│ Scopes read · operate · agent │ +│ ┌──── Remove this device ────┐ │ ← destructive, confirm sheet: +│ └────────────────────────────┘ │ revoke → keychain → row +│ Host options (ui:agent) │ +│ Default provider claude ▾ │ +│ Role defaults ▸ │ ← narrowed editor: model/effort +│ Review runtime settings ▸ │ only; approval/sandbox rows +│ Execution settings ▸ │ shown read-only "host-managed" +└──────────────────────────────────┘ +``` + +**Manages**: per-environment connection diagnostics; device self-management (rename, remove = revoke-first flow); host option editors backed by the registered settings commands. The narrowed role-default editor **shows** `approval_policy`/`sandbox_mode` as read-only "host-managed" rows — visible honesty about the security envelope staying host-local. +**Data**: session/descriptor reads, `POST /remote/v1/auth/revoke`, registered `update_*_settings` commands (agent). +**States**: connected/degraded (latency + last-event age); revoke failure (host unreachable) still deletes locally with "host copy revokes on next contact" note. + +--- + +## 4. Cross-screen patterns (the contract every screen obeys) + +1. **Locked ≠ hidden.** Scope-gated controls render disabled with the reason ("Enable agent control for this device on the host"). Discovery of the grant path is a product feature. +2. **Brakes are never gated behind agent.** Stop/pause/block/deny appear at `ui:operate` on every surface that shows a live run. +3. **Intent lifecycle is visible.** Pending → spinner on the initiating control + status line; terminal failure → inline error card with taxonomy string + Retry; never a silent revert. +4. **Fail-closed rendering.** A read error is an error state; it never renders as "zero items" when zero would calm the user (gates, badges, review status). +5. **Stale-action calm.** CAS/already-resolved refusals render "Already handled on the host" — expected in a two-writer world (desktop + phone), not an error tone. +6. **First paint wins.** Every heavy surface (transcript, board, diff) paints chrome + placeholders synchronously; hydration follows a paint boundary. +7. **Cached truth is labeled.** Offline views show "as of