Skip to content

Report each hosted agent's slash commands for the composer picker - #1031

Merged
realtonyyoung merged 3 commits into
mainfrom
ai-2428-hosted-agent-slash-commands
Sep 18, 2026
Merged

realtonyyoung merged 3 commits into
mainfrom
ai-2428-hosted-agent-slash-commands

Conversation

@realtonyyoung

Copy link
Copy Markdown
Collaborator

Closes #747 — AI-2428

What & why

The daemon now reports each hosted agent's slash commands to the server for the composer's / picker. Commands are unknown at registration, so they ride a new best-effort ReportAgentCommands hub method rather than AgentRegistered: ACP vendors forward the available_commands_update they already receive (plus the session/new result), Codex calls skills/list, and Claude is probed headlessly with -p --input-format stream-json and an initialize control request.

Where to look

ClaudeCommandProbe is net new — KCAP_SKIP=1 keeps its SessionStart hooks from recording a phantom session, and an empty strict MCP config keeps it from spawning the worktree's MCP servers while still surfacing built-in and plugin commands. A buffered relay hands the orchestrator a list captured during the handshake the instant its callback attaches, so nothing races the attach. Codex skills/list's wire shape is not yet probe-confirmed, so its parser is defensive and fails safe to an empty list.

Verification

Daemon builds clean; AOT publish clean (no IL2026/IL3050); 12 new unit tests pass — probe parse against the real 2.1.273 initialize response, ACP/Codex extraction, and the relay's flush-on-attach timing.

🤖 Generated with Claude Code

Commands are unknown at registration, so they ride a new best-effort
ReportAgentCommands hub method rather than AgentRegistered: an ACP
available_commands_update, a Codex skills/list, or a headless Claude
initialize probe (KCAP_SKIP=1 keeps the probe from recording a session).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@linear-code

linear-code Bot commented Sep 18, 2026

Copy link
Copy Markdown

AI-2428

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Report hosted-agent slash commands to the composer picker

✨ Enhancement 🧪 Tests 🕐 40+ Minutes

Grey Divider

AI Description

• Collects hosted-agent commands from ACP, Codex skills, and headless Claude initialization.
• Buffers handshake discoveries and reports replacement command lists through a backward-compatible
 hub method.
• Adds parser and relay tests for payload shapes, failure handling, and attachment timing.
Diagram

graph TD
    ACP["ACP Runtime"] --> Relay["Command Relay"] --> Orchestrator["Agent Orchestrator"] --> Hub["Server Hub"] --> Picker["Composer Picker"]
    Codex["Codex Runtime"] --> Relay
    Claude["Claude Probe"] --> Orchestrator
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Include commands in agent registration
  • ➕ Avoids adding a separate hub method and report lifecycle.
  • ➕ Keeps initial agent metadata in one payload.
  • ➖ Commands are generally unavailable when registration occurs.
  • ➖ Would delay registration or require another registration-style update.
  • ➖ Cannot naturally represent later ACP command updates.
2. Discover commands server-side
  • ➕ Keeps vendor discovery logic outside the daemon.
  • ➕ Could centralize picker data management.
  • ➖ The server lacks direct access to local harness processes and ACP streams.
  • ➖ Would require new remote probing infrastructure and expose local environment details.
  • ➖ Duplicates context already available inside each daemon runtime.

Recommendation: Keep the PR's out-of-band, best-effort reporting design. Command availability is asynchronous and local to each harness, so a replaceable report cleanly handles handshake snapshots and later updates without delaying registration. The buffered relay appropriately closes the callback-attachment race.

Files changed (14) +560 / -0

Enhancement (10) +390 / -0
ClaudeCommandProbe.csAdd safe headless Claude command discovery +123/-0

Add safe headless Claude command discovery

• Introduces a bounded, best-effort Claude process probe that sends an initialize control request and parses built-in, plugin, and project commands. It suppresses session hooks and MCP startup while returning an empty list on all failures.

src/Capacitor.Cli.Core/Harness/Claude/ClaudeCommandProbe.cs

Models.csDefine hosted-agent command reporting contracts +14/-0

Define hosted-agent command reporting contracts

• Adds the shared command model and additive report payload used to serialize an agent ID with its latest slash-command list.

src/Capacitor.Cli.Core/Models.cs

AcpAvailableCommands.csExtract commands from ACP payloads +32/-0

Extract commands from ACP payloads

• Adds a defensive mapper for ACP availableCommands arrays, normalizing blank descriptions and omitting malformed nameless entries.

src/Capacitor.Cli.Daemon/Acp/AcpAvailableCommands.cs

CodexAppServerHostedAgentRuntime.csPublish Codex skills after startup +25/-0

Publish Codex skills after startup

• Requests skills/list after starting the Codex thread and publishes successfully parsed skills through the buffered command relay. Unsupported methods and malformed responses remain non-fatal.

src/Capacitor.Cli.Daemon/Harness/Codex/CodexAppServerHostedAgentRuntime.cs

CodexSkills.csMap defensive Codex skill responses +49/-0

Map defensive Codex skill responses

• Adds fail-safe parsing for flat and grouped skills/list response variants, mapping valid skills to hosted-agent commands without argument hints.

src/Capacitor.Cli.Daemon/Harness/Codex/CodexSkills.cs

AcpHostedAgentRuntime.csForward ACP command snapshots and updates +22/-0

Forward ACP command snapshots and updates

• Captures commands from session/new results and available_commands_update notifications. Publishes them through a relay so handshake-time data survives late callback attachment.

src/Capacitor.Cli.Daemon/Services/AcpHostedAgentRuntime.cs

AgentOrchestrator.csStore, probe, and report agent commands +58/-0

Store, probe, and report agent commands

• Attaches runtime command callbacks after publication, launches the Claude probe for interactive sessions, updates local agent state, and sends command lists to the server without affecting agent execution on failure.

src/Capacitor.Cli.Daemon/Services/AgentOrchestrator.cs

HostedAgentCommandsRelay.csBuffer runtime commands until callback attachment +36/-0

Buffer runtime commands until callback attachment

• Adds a thread-safe latest-value relay that immediately flushes commands learned during startup when the orchestrator callback attaches.

src/Capacitor.Cli.Daemon/Services/HostedAgentCommandsRelay.cs

IHostedAgentRuntime.csExpose optional runtime command notifications +13/-0

Expose optional runtime command notifications

• Extends hosted runtimes with an optional command callback while preserving no-op defaults for PTY and other unaffected implementations.

src/Capacitor.Cli.Daemon/Services/IHostedAgentRuntime.cs

ServerConnection.csReport commands through the persistent hub +18/-0

Report commands through the persistent hub

• Adds the best-effort ReportAgentCommands hub send using an additive single-record payload. Failures from older servers are swallowed and logged for mixed-version compatibility.

src/Capacitor.Cli.Daemon/Services/ServerConnection.cs

Tests (4) +170 / -0
ClaudeCommandProbeTests.csTest Claude initialize-response parsing +42/-0

Test Claude initialize-response parsing

• Verifies extraction from a representative stream-json response, blank-field normalization, and safe empty results for absent or invalid responses.

test/Capacitor.Cli.Core.Tests.Unit/Harness/Claude/ClaudeCommandProbeTests.cs

AcpAvailableCommandsTests.csTest ACP command extraction +44/-0

Test ACP command extraction

• Covers command mapping, null argument hints, nameless-entry filtering, and absent or null command sources.

test/Capacitor.Cli.Daemon.Tests.Unit/Acp/AcpAvailableCommandsTests.cs

CodexSkillsTests.csTest defensive Codex skill mapping +40/-0

Test defensive Codex skill mapping

• Validates flat and grouped skill response variants and confirms unexpected or empty payloads fail safely.

test/Capacitor.Cli.Daemon.Tests.Unit/Harness/Codex/CodexSkillsTests.cs

HostedAgentCommandsRelayTests.csTest command relay timing behavior +44/-0

Test command relay timing behavior

• Confirms pre-attachment values flush immediately, post-attachment publications flow through, and null callbacks remain safe.

test/Capacitor.Cli.Daemon.Tests.Unit/Services/HostedAgentCommandsRelayTests.cs

@qodo-code-review

qodo-code-review Bot commented Sep 18, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (1) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Command discovery delays the first turn ✓ Resolved 🐞 Bug ➹ Performance
Description
StartAsync awaits the optional skills/list request before it dispatches the initial prompt. A
slow or nonresponsive request therefore consumes the launch timeout and can hold or fail the user's
first turn even though command discovery is explicitly nonessential.
Code

src/Capacitor.Cli.Daemon/Harness/Codex/CodexAppServerHostedAgentRuntime.cs[280]

+        await TryPublishSkillsAsync(linked.Token).ConfigureAwait(false);
Relevance

●●● Strong

Recent Codex startup findings accepted cancellation and startup-blocking fixes affecting
initial-turn dispatch.

PR-#588
PR-#624

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The new awaited call sits directly between StartThreadAsync and the initial-prompt branch.
TryPublishSkillsAsync waits for a correlated JSON-RPC response, and that request completes only
after a response, cancellation, or connection termination.

src/Capacitor.Cli.Daemon/Harness/Codex/CodexAppServerHostedAgentRuntime.cs[277-294]
src/Capacitor.Cli.Daemon/Harness/Codex/CodexAppServerHostedAgentRuntime.cs[393-405]
src/Capacitor.Cli.Daemon/Harness/Codex/CodexAppServerConnection.cs[91-113]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Codex command discovery is awaited before the initial prompt, making an optional picker feature part of the launch-critical path.

## Fix Focus Areas
- src/Capacitor.Cli.Daemon/Harness/Codex/CodexAppServerHostedAgentRuntime.cs[277-294]
- src/Capacitor.Cli.Daemon/Harness/Codex/CodexAppServerHostedAgentRuntime.cs[393-406]

## Recommended Fix
Start command discovery only after startup and first-turn dispatch are no longer blocked by it. Run it as a lifetime-bound best-effort operation, observe and log its failures, and publish through the existing relay when it completes.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Pickers lose commands after reconnect ✓ Resolved 🐞 Bug ☼ Reliability
Description
ReportCommands sends each list once, while ReRegisterAgentsAsync never replays the value
retained in AgentInstance.Commands. If the initial send occurs during an outage or the server
restarts, Claude and Codex have no later producer, so their composer pickers remain empty for the
rest of the agent session.
Code

src/Capacitor.Cli.Daemon/Services/AgentOrchestrator.cs[R2829-2832]

+            if (_agents.TryGetValue(agentId, out var agent))
+                SetCommands(agent, commands);
+
+            _ = _server.ReportAgentCommandsAsync(agentId, commands);
Relevance

●● Moderate

Reconnect state-loss concerns are plausible, but no closely matching command replay precedent
appeared.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The discovered list is retained on the agent and sent once, but the reconnect path restores
registration, status, and terminal dimensions without restoring commands. The local status snapshot
also does not carry commands, so the status pulse cannot recover the server-side picker state.

src/Capacitor.Cli.Daemon/Services/AgentOrchestrator.cs[954-959]
src/Capacitor.Cli.Daemon/Services/AgentOrchestrator.cs[2827-2835]
src/Capacitor.Cli.Daemon/Services/AgentOrchestrator.cs[4824-4890]
src/Capacitor.Cli.Daemon/Services/AgentOrchestrator.LocalIpc.cs[59-86]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Hosted command lists are sent only when first discovered and are not restored after a server reconnect, despite being retained on each live agent.

## Fix Focus Areas
- src/Capacitor.Cli.Daemon/Services/AgentOrchestrator.cs[2827-2835]
- src/Capacitor.Cli.Daemon/Services/AgentOrchestrator.cs[4824-4890]

## Recommended Fix
During `ReRegisterAgentsAsync`, resend every non-null `agent.Commands` value after the agent has been successfully registered and its status restored. Await that send within the existing per-agent re-registration sequence so reconnect readiness cannot race command restoration, while preserving the method's existing best-effort isolation.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Commands never reach the composer ✗ Dismissed 🔗 Cross-repo conflict ≡ Correctness
Description
ServerConnection.ReportAgentCommandsAsync sends a ReportAgentCommands SignalR message whose
payload has no matching handler or public contract in the pinned kcap-server. Against that server
version the best-effort call is discarded, and no command collection reaches its agent read model or
composer-facing views.
Code

src/Capacitor.Cli.Daemon/Services/ServerConnection.cs[R933-935]

+    public virtual async Task ReportAgentCommandsAsync(string agentId, IReadOnlyList<HostedAgentCommand> commands) {
+        try {
+            await _hub.SendAsync("ReportAgentCommands", new ReportAgentCommandsArgs(agentId, commands), cancellationToken: _ct);
Relevance

●● Moderate

The cross-repository contract concern is consequential, but no matching historical precedent
established team acceptance.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The PR defines a client-only wire payload and sends it to a new hub target. The pinned server only
implements the analogous resolved-model report, while its agent storage and composer-facing DTO
contain no command collection, proving there is no end-to-end consumer for the new message.

src/Capacitor.Cli.Daemon/Services/ServerConnection.cs[926-939]
src/Capacitor.Cli.Core/Models.cs[2181-2193]
External repo: kurrent-io/kcap-server, src/Capacitor.Server/Sessions/CapacitorHub.cs [3436-3453]
External repo: kurrent-io/kcap-server, src/Capacitor.Server.Core/AgentInstanceInfo.cs [5-43]
External repo: kurrent-io/kcap-server, src/Capacitor.Server/Sessions/AgentStoreDataService.cs [2460-2510]
External repo: kurrent-io/kcap-server, src/Capacitor.Api.Web.Abstractions/Agents/AgentInstanceView.cs [8-35]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The CLI reports hosted-agent commands through a SignalR method that the pinned kcap-server does not implement, so the new composer data is discarded.

## Fix Focus Areas
- src/Capacitor.Cli.Daemon/Services/ServerConnection.cs[926-939]
- src/Capacitor.Cli.Core/Models.cs[2181-2193]
- /cross_repos/kcap-server/src/Capacitor.Server/Sessions/CapacitorHub.cs[3436-3453]
- /cross_repos/kcap-server/src/Capacitor.Server.Core/AgentInstanceInfo.cs[5-43]
- /cross_repos/kcap-server/src/Capacitor.Api.Web.Abstractions/Agents/AgentInstanceView.cs[8-35]

## Recommended Fix
Add the matching `ReportAgentCommands` payload types and hub handler to kcap-server, persist or retain the latest list for each agent, and expose it through the composer-facing model. Coordinate deployment or pin advancement so server support is available when this CLI feature is released.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

4. Cancelled launches can appear successful ⊘ Outdated 🐞 Bug ≡ Correctness
Description
TryPublishSkillsAsync catches OperationCanceledException from its launch-token-aware
RequestAsync call through the generic best-effort Exception handler and returns control to
StartAsync. If cancellation arrives while skills/list is pending after the thread starts and the
launch has no immediate initial prompt, no subsequent await or token check observes it, so startup
completes normally with the child still running; an initial turn is the only following path that
observes the token.
Code

src/Capacitor.Cli.Daemon/Harness/Codex/CodexAppServerHostedAgentRuntime.cs[R403-405]

+        } catch (Exception ex) {
+            _logger.LogDebug(ex, "codex app-server: skills/list failed; no slash commands surfaced.");
+        }
Relevance

●●● Strong

Recent Codex cancellation findings were accepted when startup cancellation could leave work running
or appear successful.

PR-#588
PR-#624

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The request implementation uses the launch token, explicitly cancels its pending task, and throws
OperationCanceledException when that token is cancelled, while the newly added broad catch
consumes every exception as a best-effort failure. After the helper returns, the startup path only
observes the token when it must immediately enqueue an initial turn, so an empty or deferred initial
prompt leaves no subsequent awaited operation or cancellation check.

src/Capacitor.Cli.Daemon/Harness/Codex/CodexAppServerHostedAgentRuntime.cs[280-294]
src/Capacitor.Cli.Daemon/Harness/Codex/CodexAppServerHostedAgentRuntime.cs[396-405]
src/Capacitor.Cli.Daemon/Harness/Codex/CodexAppServerConnection.cs[91-113]
src/Capacitor.Cli.Daemon/Harness/Codex/CodexAppServerHostedAgentRuntime.cs[277-292]
src/Capacitor.Cli.Daemon/Harness/Codex/CodexAppServerConnection.cs[97-125]
src/Capacitor.Cli.Daemon/Harness/Codex/CodexHostedAgentRuntimeFactory.cs[153-159]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The best-effort `skills/list` request swallows cancellation from the startup token, allowing a cancelled startup with no immediate initial prompt to return successfully while the child remains running.

## Fix Focus Areas
- src/Capacitor.Cli.Daemon/Harness/Codex/CodexAppServerHostedAgentRuntime.cs[396-406]
- src/Capacitor.Cli.Daemon/Harness/Codex/CodexAppServerHostedAgentRuntime.cs[280-294]

## Recommended Fix
Add a filtered `catch (OperationCanceledException) when (ct.IsCancellationRequested)` before the generic best-effort catch and rethrow launch-token cancellation. Continue swallowing ordinary RPC, protocol, and parsing failures so unsupported `skills/list` remains non-fatal, and add coverage for cancellation during `skills/list` both with and without an initial prompt.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Claude commands vanish after probe stalls ⊘ Outdated 🐞 Bug ☼ Reliability
Description
ClaudeCommandProbe.ProbeAsync redirects stderr but only drains stdout before waiting for the child
to exit. When verbose Claude diagnostics or hook output fills the stderr pipe, the child blocks
before closing stdout or exiting, so the probe reaches its timeout, kills the process, and returns
no commands.
Code

src/Capacitor.Cli.Core/Harness/Claude/ClaudeCommandProbe.cs[R64-65]

+            var stdout = await process.StandardOutput.ReadToEndAsync(linkedCts.Token);
+            await process.WaitForExitAsync(linkedCts.Token);
Relevance

●●● Strong

Recent Claude process reliability findings were accepted; concurrent stream draining is an
established safe pattern.

PR-#1016

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The probe redirects both output streams and enables verbose mode, but its completion path reads only
stdout before waiting for process exit. The existing Claude runner starts reads for both redirected
streams concurrently, demonstrating the repository's established safe process-draining pattern.

src/Capacitor.Cli.Core/Harness/Claude/ClaudeCommandProbe.cs[35-38]
src/Capacitor.Cli.Core/Harness/Claude/ClaudeCommandProbe.cs[45-49]
src/Capacitor.Cli.Core/Harness/Claude/ClaudeCommandProbe.cs[64-75]
src/Capacitor.Cli.Core/Harness/Claude/ClaudeCliRunner.cs[318-323]
src/Capacitor.Cli.Core/Harness/Claude/ClaudeCommandProbe.cs[32-49]
src/Capacitor.Cli.Core/Harness/Claude/ClaudeCommandProbe.cs[54-75]
src/Capacitor.Cli.Core/Harness/Claude/ClaudeCliRunner.cs[306-313]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`ClaudeCommandProbe` redirects both output streams but consumes only stdout, allowing verbose Claude diagnostics or hook output on stderr to block the child until the probe times out and discards otherwise valid commands.

## Fix Focus Areas
- src/Capacitor.Cli.Core/Harness/Claude/ClaudeCommandProbe.cs[32-47]
- src/Capacitor.Cli.Core/Harness/Claude/ClaudeCommandProbe.cs[57-75]
- src/Capacitor.Cli.Core/Harness/Claude/ClaudeCliRunner.cs[306-313]

## Recommended Fix
Start `StandardOutput.ReadToEndAsync` and `StandardError.ReadToEndAsync` before waiting for process exit, following `ClaudeCliRunner`'s established pattern, and await both reader tasks after the process exits. Continue parsing stdout; stderr must be drained concurrently and may be discarded or included as bounded context in failure logging.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. Relay test comments repeat the code ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
Publish_before_a_callback_attaches_is_flushed_on_attach labels relay.Publish(Sample) as
occurring before attachment and the callback assignment as attachment after start. The test name and
statement order already express both facts, so the comments add no behavior-critical constraint and
create duplicate prose to maintain.
Code

test/Capacitor.Cli.Daemon.Tests.Unit/Services/HostedAgentCommandsRelayTests.cs[R15-18]

+        relay.Publish(Sample); // captured during start, no callback yet
+
+        IReadOnlyList<HostedAgentCommand>? seen = null;
+        relay.Callback = c => seen = c; // orchestrator attaches after start
Relevance

●●● Strong

Recent precedents accept removing comments that restate test names, statements, or assertions.

PR-#879
PR-#718
PR-#688

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Rule 2762993 limits comments to non-obvious, behavior-critical constraints and explicitly rejects
comments that restate code. The cited inline comments paraphrase the adjacent publish and
callback-assignment statements.

Rule 2762993: Restrict comments to documenting non-obvious, behavior‑critical constraints
test/Capacitor.Cli.Daemon.Tests.Unit/Services/HostedAgentCommandsRelayTests.cs[15-18]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Two inline comments merely narrate the immediately adjacent relay test statements and repeat timing already expressed by the test name and statement order.

## Fix Focus Areas
- test/Capacitor.Cli.Daemon.Tests.Unit/Services/HostedAgentCommandsRelayTests.cs[15-18]

## Recommended Fix
Remove both inline comments while retaining the test statements and assertions unchanged.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View medium (4)
7. Two Codex comments become stale ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
CodexSkills and its test describe the wire shape as not yet probe-confirmed, which records a
time-dependent validation status rather than a lasting constraint. Once the response is probed, both
comments become false even if the defensive parser behavior remains unchanged.
Code

src/Capacitor.Cli.Daemon/Harness/Codex/CodexSkills.cs[R8-10]

+/// that shape is read, and it is deliberately defensive: the wire shape is not yet probe-confirmed
+/// here, so an unexpected or absent shape yields an empty list rather than throwing, and the picker
+/// simply shows nothing for Codex until the shape is verified.</summary>
Relevance

●●● Strong

Recent Codex comment findings were accepted when comments contained stale history or time-sensitive
implementation metadata.

PR-#690
PR-#622

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Rule 2897915 disallows time-sensitive metadata in comments. Both cited comments use `not yet
probe-confirmed`, tying their accuracy to an external validation event rather than current code
behavior.

Rule 2897915: Avoid time-sensitive or process-reference metadata in code comments
src/Capacitor.Cli.Daemon/Harness/Codex/CodexSkills.cs[8-10]
test/Capacitor.Cli.Daemon.Tests.Unit/Harness/Codex/CodexSkillsTests.cs[7-10]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Two Codex comments embed the temporary status that the response shape has not yet been probe-confirmed, so their truth depends on external validation history.

## Fix Focus Areas
- src/Capacitor.Cli.Daemon/Harness/Codex/CodexSkills.cs[8-10]
- test/Capacitor.Cli.Daemon.Tests.Unit/Harness/Codex/CodexSkillsTests.cs[7-10]

## Recommended Fix
Remove the time-dependent probe-status wording. Describe only the durable behavior: the parser accepts the supported candidate shapes defensively and returns an empty list for absent or unexpected data.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


8. Newer command updates can be overwritten 🐞 Bug ≡ Correctness
Description
HostedAgentCommandsRelay.Callback captures the pending _last list and callback under its lock
but invokes the callback after releasing the lock. If Publish replaces _last and delivers a
newer update before that delayed invocation, the older handshake snapshot arrives last and
unconditionally replaces AgentInstance.Commands while also starting a stale server report.
Code

src/Capacitor.Cli.Daemon/Services/HostedAgentCommandsRelay.cs[R17-24]

+            IReadOnlyList<HostedAgentCommand>? pending;
+            lock (_lock) {
+                _callback = value;
+                pending   = _last;
+            }
+
+            if (value is not null && pending is not null) value(pending);
+        }
Relevance

●● Moderate

The concurrency ordering concern is specific and plausible, but no close relay or stale-callback
precedent appeared.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Callback attachment releases the relay lock after capturing the pending value, while Publish can
independently replace _last and invoke the same callback outside the lock, allowing the two
deliveries to reorder. Each invocation immediately replaces AgentInstance.Commands and starts a
separate server report, with no downstream version or ordering guard to prevent the late stale
snapshot from becoming externally observable.

src/Capacitor.Cli.Daemon/Services/HostedAgentCommandsRelay.cs[17-24]
src/Capacitor.Cli.Daemon/Services/HostedAgentCommandsRelay.cs[27-35]
src/Capacitor.Cli.Daemon/Services/AgentOrchestrator.cs[2827-2833]
src/Capacitor.Cli.Daemon/Services/HostedAgentCommandsRelay.cs[14-34]
src/Capacitor.Cli.Daemon/Services/AgentOrchestrator.cs[2553-2557]
src/Capacitor.Cli.Daemon/Services/AgentOrchestrator.cs[2827-2834]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The relay can deliver callback snapshots in reverse order because callback attachment captures a buffered value under the state lock but invokes it outside the lock without a version check. A concurrent publication can therefore deliver a newer command list before the older attach-time handshake snapshot, allowing the older list to overwrite local and server state.

## Fix Focus Areas
- src/Capacitor.Cli.Daemon/Services/HostedAgentCommandsRelay.cs[14-34]
- test/Capacitor.Cli.Daemon.Tests.Unit/Services/HostedAgentCommandsRelayTests.cs[12-43]

## Recommended Fix
Associate each published list with a monotonically increasing version and coordinate callback attachment and publication so delivery order follows publication order without executing arbitrary user callbacks while holding the relay lock. Deliver a buffered attach-time snapshot only if it is still current, and add a deterministic concurrency test that pauses attachment after it captures the buffered snapshot, publishes a newer list, and verifies that the older list can never be delivered last.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


9. Probe cleanup bypasses safe tree killing ⊘ Outdated 📘 Rule violation ≡ Correctness
Description
ClaudeCommandProbe.ProbeAsync terminates its failed child with `process.Kill(entireProcessTree:
true) rather than the daemon's ProcessTree.Kill` abstraction. Cancellation, timeout, or any other
probe exception reaches this cleanup path without the required child-first and single-signal
guarantees.
Code

src/Capacitor.Cli.Core/Harness/Claude/ClaudeCommandProbe.cs[R71-73]

+            try { process.Kill(entireProcessTree: true); } catch {
+                /* best-effort */
+            }
Relevance

●● Moderate

The rule is specific, but no closely matching accepted or rejected termination precedent appeared.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Rule 3209096 requires daemon process-tree termination to use ProcessTree.Kill. The probe is
launched by AgentOrchestrator in the daemon flow, and its exception handler instead invokes the
framework's recursive Process.Kill directly.

Rule 3209096: Process tree termination must use ProcessTree.Kill with SIGKILL-only, child-first, and single-signal semantics
src/Capacitor.Cli.Core/Harness/Claude/ClaudeCommandProbe.cs[71-73]
src/Capacitor.Cli.Daemon/Services/AgentOrchestrator.cs[2838-2846]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The Claude command probe directly invokes `Process.Kill(bool)` to terminate a process tree, bypassing the centralized daemon process-tree termination semantics.

## Fix Focus Areas
- src/Capacitor.Cli.Core/Harness/Claude/ClaudeCommandProbe.cs[71-73]

## Recommended Fix
Route probe termination through `ProcessTree.Kill`. If necessary to respect project dependencies, relocate the process-owning probe into the daemon harness assembly or inject a daemon-owned termination operation, then update its call site and tests.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


10. Three parsers bypass shape helpers ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
ClaudeCommandProbe, AcpAvailableCommands, and CodexSkills directly inspect
JsonElement.ValueKind five times instead of using the existing IsObject extension. These
parallel shape-checking styles make future JSON handling changes easy to apply inconsistently across
the new command parsers.
Code

src/Capacitor.Cli.Core/Harness/Claude/ClaudeCommandProbe.cs[105]

+                if (entry.ValueKind != JsonValueKind.Object) continue;
Relevance

●● Moderate

Recent JSON-helper findings were accepted, but comparable direct checks were also rejected; evidence
is mixed.

PR-#1016
PR-#622
PR-#910

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Rule 2270023 prohibits direct JsonElement.ValueKind checks when an equivalent extension exists.
The cited parser lines directly compare or pattern-match JsonValueKind.Object, while the shared
extension file defines IsObject.

Rule 2270023: Use JsonElementExtensions helpers instead of direct JsonValueKind comparisons
src/Capacitor.Cli.Core/Harness/Claude/ClaudeCommandProbe.cs[105-105]
src/Capacitor.Cli.Daemon/Acp/AcpAvailableCommands.cs[12-18]
src/Capacitor.Cli.Daemon/Harness/Codex/CodexSkills.cs[22-38]
src/Capacitor.Models.Transcripts/JsonElementExtensions.cs[5-13]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new command parsers directly compare or pattern-match `JsonElement.ValueKind` even though the shared JSON extensions provide `IsObject` for these checks.

## Fix Focus Areas
- src/Capacitor.Cli.Core/Harness/Claude/ClaudeCommandProbe.cs[105-105]
- src/Capacitor.Cli.Daemon/Acp/AcpAvailableCommands.cs[12-18]
- src/Capacitor.Cli.Daemon/Harness/Codex/CodexSkills.cs[22-38]

## Recommended Fix
Replace each direct object-kind comparison with `IsObject`. Rewrite the nullable source pattern in `AcpAvailableCommands.Extract` as a null check followed by `obj.IsObject`, preserving the current empty-list behavior.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

11. Two command records share one file 📘 Rule violation ⚙ Maintainability
Description
Models.cs adds the primary top-level records HostedAgentCommand and ReportAgentCommandsArgs
even though its filename matches neither type. Their definitions join an unrelated aggregate model
file, so later readers cannot locate either record through the repository's one-type-per-file
convention.
Code

src/Capacitor.Cli.Core/Models.cs[2184]

+public record HostedAgentCommand(string Name, string? Description, string? ArgumentHint);
Relevance

● Weak

Recent one-type-per-file findings were explicitly rejected, including similarly documented DTO/model
files.

PR-#875
PR-#976
PR-#873

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Rule 3162234 requires one primary type per file and a filename matching that type. The cited
additions introduce two separate public top-level records in Models.cs.

Rule 3162234: One primary type per file, with only narrow documented exceptions
src/Capacitor.Cli.Core/Models.cs[2181-2193]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Two new top-level command model types are added to `Models.cs`, leaving multiple primary types in a file whose name matches neither new record.

## Fix Focus Areas
- src/Capacitor.Cli.Core/Models.cs[2181-2193]

## Recommended Fix
Move `HostedAgentCommand` into `HostedAgentCommand.cs` and `ReportAgentCommandsArgs` into `ReportAgentCommandsArgs.cs`, retaining the appropriate project namespace and existing public API.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
✅ Compliance rules (platform): 64 rules
✅ Cross-repo context — repo relationships
  Explored: repo: kurrent-io/kcap-server (sha: 2e863aa9)
Review mode: 🧠 Deep: This is a broad, behavior-changing cross-component feature spanning process probing, ACP/Codex protocol parsing, concurrency-sensitive callback relaying, orchestration, and server reporting, with many independent sites where subtle defects could be missed in one pass.

Grey Divider

Tip of the day
💡 Did you know, you can type 'qodo, fix this' on a finding and the fix lands right on your PR

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread src/Capacitor.Cli.Core/Harness/Claude/ClaudeCommandProbe.cs Outdated
Comment thread src/Capacitor.Cli.Core/Harness/Claude/ClaudeCommandProbe.cs Outdated
Comment thread src/Capacitor.Cli.Daemon/Harness/Codex/CodexSkills.cs Outdated
Comment thread test/Capacitor.Cli.Daemon.Tests.Unit/Services/HostedAgentCommandsRelayTests.cs Outdated
Comment thread src/Capacitor.Cli.Daemon/Services/AgentOrchestrator.cs
Comment thread src/Capacitor.Cli.Daemon/Harness/Codex/CodexAppServerHostedAgentRuntime.cs Outdated
Comment thread src/Capacitor.Cli.Core/Harness/Claude/ClaudeCommandProbe.cs Outdated
Comment on lines +17 to +24
IReadOnlyList<HostedAgentCommand>? pending;
lock (_lock) {
_callback = value;
pending = _last;
}

if (value is not null && pending is not null) value(pending);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

10. Newer command updates can be overwritten 🐞 Bug ≡ Correctness

HostedAgentCommandsRelay.Callback captures the pending _last list and callback under its lock
but invokes the callback after releasing the lock. If Publish replaces _last and delivers a
newer update before that delayed invocation, the older handshake snapshot arrives last and
unconditionally replaces AgentInstance.Commands while also starting a stale server report.
Agent Prompt
## Issue description
The relay can deliver callback snapshots in reverse order because callback attachment captures a buffered value under the state lock but invokes it outside the lock without a version check. A concurrent publication can therefore deliver a newer command list before the older attach-time handshake snapshot, allowing the older list to overwrite local and server state.

## Fix Focus Areas
- src/Capacitor.Cli.Daemon/Services/HostedAgentCommandsRelay.cs[14-34]
- test/Capacitor.Cli.Daemon.Tests.Unit/Services/HostedAgentCommandsRelayTests.cs[12-43]

## Recommended Fix
Associate each published list with a monotonically increasing version and coordinate callback attachment and publication so delivery order follows publication order without executing arbitrary user callbacks while holding the relay lock. Deliver a buffered attach-time snapshot only if it is still current, and add a deterministic concurrency test that pauses attachment after it captures the buffered snapshot, publishes a newer list, and verifies that the older list can never be delivered last.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread src/Capacitor.Cli.Daemon/Services/ServerConnection.cs
realtonyyoung and others added 2 commits September 18, 2026 14:10
The server drops a command report for an agent it has not registered yet, so
attach the callback (which flushes the handshake snapshot) and probe Claude
only after AgentRegistered is acked. Resend the cached list on re-registration
too — a same-slot re-register replaces the server entry with a null-Commands
record, and a server restart wiped it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Claude probe moves into the daemon assembly so it stops its child through
ProcessTree.Kill, and it drains stderr so a full stderr pipe can't deadlock it.
Codex skills/list runs off the launch-critical path so a slow request can't
delay the first turn. The relay versions each list so a late attach-time
snapshot can't overwrite a newer one.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@realtonyyoung

Copy link
Copy Markdown
Collaborator Author

Qodo disposition (the "Code Review by Qodo" summary above predates the fix commits): all 10 inline findings are addressed. Fixed in later commits — #1 skills/list off the launch-critical path, #2 reconnect replay, #4 Codex comment reworded, #5 relay test comments trimmed, #6 cancelled-launch (resolved by #1), #7 stderr drain, #8 IsObject, #9 probe moved into the daemon assembly to use ProcessTree.Kill, #10 relay versioned against out-of-order delivery. #3 (cross-repo: pinned server has no handler) dismissed — that is the intended server-first rollout with a best-effort send. CI green.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Offer the harness's slash commands in hosted-agent chat

1 participant