Skip to content

[codex] Warp-inspired runtime extensions#11

Open
Telli wants to merge 1 commit into
mainfrom
codex/warp-runtime-extensions
Open

[codex] Warp-inspired runtime extensions#11
Telli wants to merge 1 commit into
mainfrom
codex/warp-runtime-extensions

Conversation

@Telli
Copy link
Copy Markdown
Contributor

@Telli Telli commented May 17, 2026

Summary

  • adds a new ExternalAgents module with built-in adapters for Claude Code, OpenCode, Gemini CLI, and Codex CLI
  • extends skills into session-aware skill packs with built-in packs, install/manage/run flows, and runtime events
  • adds WorkItems support for GitHub issue/PR import, manual JSON ingestion, session metadata, and summary export
  • adds static workbench/status surfaces for sessions, checkpoints, approvals, recent activity, and adapter health
  • updates docs and README coverage for external agents, skill packs, work items, and workbench commands

Validation

  • dotnet restore SharpClawCode.sln
  • dotnet build SharpClawCode.sln --no-restore
  • dotnet test SharpClawCode.sln --no-build
  • dotnet run --project src/SharpClaw.Code.Cli -- --output-format json external list

Intentionally Deferred

  • structured streaming for external CLIs
  • OAuth/cloud sync and automatic GitHub/Jira posting
  • live curses-style dashboard refresh
  • YAML skill-pack manifests

Summary by CodeRabbit

Release Notes

  • New Features

    • Added external agent adapter support for running CLI-based tools (Claude, OpenAI, Gemini, Codex)
    • Introduced skill packs for managing reusable skill bundles with install/enable/disable/run commands
    • Added work items integration for importing GitHub issues/PRs and exporting session summaries
    • Added workbench command for viewing session status, checkpoints, and system health
  • Documentation

    • Added documentation for external agents, skill packs, work items, and workbench features

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 17, 2026

📝 Walkthrough

Walkthrough

This PR introduces external agent CLI execution, skill pack management, work item integration with GitHub/generic JSON, and a workbench status surface. It adds two new projects (SharpClaw.Code.ExternalAgents and SharpClaw.Code.WorkItems), extends the protocol with new data models and events, creates service abstractions and implementations, wires new CLI commands and handlers, and updates runtime configuration merging to support the new features. Comprehensive tests and documentation accompany the changes.

Changes

External Agents CLI Adapter Framework

Layer / File(s) Summary
External Agent Protocol Models and Events
src/SharpClaw.Code.Protocol/Models/ExternalAgentModels.cs, src/SharpClaw.Code.Protocol/Events/ExternalAgentEvents.cs, src/SharpClaw.Code.Protocol/Events/RuntimeEvent.cs
Enums for mode, health, and failure kind; records for adapter descriptors, status, run requests/results, configuration, and catalog reports; runtime event records for execution lifecycle (started, completed, failed).
External Agent Service Abstractions
src/SharpClaw.Code.ExternalAgents/Abstractions/*
Adapter, registry, service, executable resolver, and config provider interfaces defining execution and configuration contracts.
External Agent Adapters and Registry
src/SharpClaw.Code.ExternalAgents/Adapters/, src/SharpClaw.Code.ExternalAgents/Services/ExternalAgentRegistry.cs, src/SharpClaw.Code.ExternalAgents/Services/PathExternalAgentExecutableResolver.cs
Base adapter class with lifecycle/execution logic; four built-in adapters (Claude, OpenCode, Gemini, Codex); registry for adapter discovery and status probing.
External Agent Service Orchestration
src/SharpClaw.Code.ExternalAgents/Services/ExternalAgentService.cs
Coordinates external agent runs with session resolution, permission evaluation, event publishing, and metadata persistence.
External Agent CLI Handlers and Commands
src/SharpClaw.Code.Commands/Handlers/ExternalCommandHandler.cs, src/SharpClaw.Code.Commands/Handlers/AgentsCommandHandler.cs, src/SharpClaw.Code.Commands/Handlers/AgentStatusSlashCommandHandler.cs
Top-level external command, agents command extensions for external operations, and agent-status slash command.
External Agent DI, Configuration, and Runtime Integration
src/SharpClaw.Code.ExternalAgents/ExternalAgentsServiceCollectionExtensions.cs, src/SharpClaw.Code.Runtime/ExternalAgents/RuntimeExternalAgentConfigProvider.cs, src/SharpClaw.Code.Runtime/Composition/RuntimeServiceCollectionExtensions.cs, src/SharpClaw.Code.Runtime/Diagnostics/Checks/ExternalAgentHealthCheck.cs, src/SharpClaw.Code.Runtime/Configuration/SharpClawConfigService.cs, src/SharpClaw.Code.Cli/Composition/CliServiceCollectionExtensions.cs
Dependency injection wiring for external agents module; runtime config provider loading merged configs; health check integration; config merging for external agents section.

Skill Pack Management System

Layer / File(s) Summary
Skill Pack Protocol Models
src/SharpClaw.Code.Protocol/Models/SkillPackModels.cs
Records for commands, checklists, prompts, tool recommendations, manifests, installed packs, install requests, run requests, and inspection records.
Skill Pack Registry and Discovery
src/SharpClaw.Code.Skills/Abstractions/ISkillPackRegistry.cs, src/SharpClaw.Code.Skills/Services/SkillPackRegistry.cs
Interface and implementation for listing, resolving, installing, enabling, disabling, and running skill packs with built-in pack definitions.
Skill Pack CLI Command Handler
src/SharpClaw.Code.Commands/Handlers/SkillsCommandHandler.cs
Updated to support skill pack enable/disable/run operations, skill pack listing/showing, and manifest-based installation.
Skill Pack DI and Event Integration
src/SharpClaw.Code.Skills/SkillsServiceCollectionExtensions.cs, src/SharpClaw.Code.Protocol/Events/SkillAndWorkItemEvents.cs
Dependency injection for skill pack registry; skill pack installed and invoked event definitions.

Work Item Integration

Layer / File(s) Summary
Work Item Protocol Models
src/SharpClaw.Code.Protocol/Models/WorkItemModels.cs
Records for normalized work items, import/export requests and results, GitHub references, generic JSON fixtures, and work items configuration.
Work Item Provider and Service Abstractions
src/SharpClaw.Code.WorkItems/Abstractions/*
Interfaces for providers (import capability and async import), registry (provider resolution), and service (session-aware import/export).
Work Item Provider Implementations
src/SharpClaw.Code.WorkItems/Providers/GitHubWorkItemProvider.cs, src/SharpClaw.Code.WorkItems/Providers/GenericWorkItemProvider.cs, src/SharpClaw.Code.WorkItems/Services/WorkItemRegistry.cs
GitHub provider with REST API integration and optional token auth; generic JSON fixture provider; registry with provider name and fallback resolution.
Work Item Service and Session Integration
src/SharpClaw.Code.WorkItems/Services/WorkItemService.cs
Session-aware import (stores item JSON/title in metadata), export (renders markdown summary with completed turns), event publishing, and session creation/resolution.
Work Item CLI Handler and DI
src/SharpClaw.Code.Commands/Handlers/WorkCommandHandler.cs, src/SharpClaw.Code.WorkItems/WorkItemsServiceCollectionExtensions.cs
Import/show/export-summary CLI command handler; dependency injection for providers, registry, and service.

Workbench Status Surface

Layer / File(s) Summary
Workbench Status Report Model
src/SharpClaw.Code.Protocol/Operational/WorkbenchStatusReport.cs
Record capturing schema version, timestamp, workspace root, session/goal/mode metadata, checkpoint, activity history, external agent statuses, and diagnostic warnings.
Workbench Status Service Implementation
src/SharpClaw.Code.Runtime/Abstractions/IWorkbenchStatusService.cs, src/SharpClaw.Code.Runtime/Workflow/WorkbenchStatusService.cs
Service that aggregates session, checkpoint, summarized events, external agent health, and diagnostics into a status report; publishes workbench viewed event.
Workbench CLI Handlers and Checkpoint Integration
src/SharpClaw.Code.Commands/Handlers/WorkbenchCommandHandler.cs, src/SharpClaw.Code.Commands/Handlers/CheckpointsSlashCommandHandler.cs
Workbench command handler for status display; checkpoints slash command for retrieving latest checkpoint.
Workbench DI and Diagnostics Integration
src/SharpClaw.Code.Runtime/Composition/RuntimeServiceCollectionExtensions.cs
Dependency injection for workbench status service; external agent health check included in diagnostic suite.

Infrastructure, Configuration, and Project Structure

Layer / File(s) Summary
Solution and Project Files
SharpClawCode.sln, src/SharpClaw.Code.ExternalAgents/SharpClaw.Code.ExternalAgents.csproj, src/SharpClaw.Code.WorkItems/SharpClaw.Code.WorkItems.csproj, src/SharpClaw.Code.Commands/SharpClaw.Code.Commands.csproj, src/SharpClaw.Code.Runtime/SharpClaw.Code.Runtime.csproj, tests/SharpClaw.Code.UnitTests/SharpClaw.Code.UnitTests.csproj
Solution extended with ExternalAgents and WorkItems projects; project files updated with cross-project references and project GUIDs.
Configuration Merging and Metadata Keys
src/SharpClaw.Code.Protocol/Models/OpenCodeParityModels.cs, src/SharpClaw.Code.Protocol/Models/SharpClawWorkflowMetadataKeys.cs, src/SharpClaw.Code.Runtime/Configuration/SharpClawConfigService.cs
SharpClawConfigDocument extended with ExternalAgents, SkillPacks, and WorkItems config sections; metadata keys added for work items and external agents; config service updated to merge new sections with workspace-first precedence.
Protocol JSON Serialization and Runtime Events
src/SharpClaw.Code.Protocol/Serialization/ProtocolJsonContext.cs, src/SharpClaw.Code.Runtime/Export/SessionExportService.cs
Source-generated JSON context extended with metadata for all new event and model types; export service updated to summarize new runtime event types.
Runtime Service Collection and Configuration
src/SharpClaw.Code.Runtime/Composition/RuntimeServiceCollectionExtensions.cs, src/SharpClaw.Code.Runtime/ExternalAgents/RuntimeExternalAgentConfigProvider.cs
Runtime DI setup registers external agents, work items, and workbench modules; runtime config provider loads external agents config from merged SharpClawConfigService snapshot.
Documentation and Tests
README.md, docs/external-agents.md, docs/skills.md, docs/work-items.md, docs/workbench.md, tests/SharpClaw.Code.UnitTests/*
New documentation pages for external agents, skill packs, work items, and workbench features; unit tests for external agent adapters, skill pack registry, and GitHub work item provider parsing.

🎯 4 (Complex) | ⏱️ ~60 minutes

🐰 Hops with glee,
Three features bloom in tandem—
Agents, skills, and work!
Status glows on screen,
Adapters dance in the shell. 🎭✨

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/warp-runtime-extensions

@Telli Telli marked this pull request as ready for review May 18, 2026 01:03
Copilot AI review requested due to automatic review settings May 18, 2026 01:03
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds Warp-inspired runtime extensions: a new ExternalAgents module (Claude/OpenCode/Gemini/Codex CLI adapters), a session-aware skill-pack ecosystem with built-in packs and install/enable/disable/run flows, work-item import/export (GitHub + generic JSON) with session metadata persistence and Markdown/JSON summaries, and a static workbench/status surface. Protocol models, events, AOT serialization context, runtime DI, CLI/REPL commands, and docs are updated to expose these surfaces.

Changes:

  • New SharpClaw.Code.ExternalAgents and SharpClaw.Code.WorkItems projects with adapters, registries, services, permission-aware execution, and runtime events.
  • Skill packs (ISkillPackRegistry, manifest, install/enable/disable/run) layered alongside the legacy skill registry; SkillsCommandHandler extended with new subcommands.
  • WorkbenchStatusService plus workbench/work/external/agent-status/checkpoints CLI+slash handlers, runtime DI, diagnostics check, config schema, and docs.

Reviewed changes

Copilot reviewed 65 out of 65 changed files in this pull request and generated 12 comments.

Show a summary per file
File Description
src/SharpClaw.Code.ExternalAgents/** New module: adapter base, four built-in adapters, registry, permission-gated service, executable resolver, default config provider, DI
src/SharpClaw.Code.WorkItems/** New module: GitHub + generic providers, registry, session-aware import/export service, DI
src/SharpClaw.Code.Skills/Services/SkillPackRegistry.cs + Abstractions + DI Built-in packs, file-backed install/enable/disable, prompt template expansion
src/SharpClaw.Code.Protocol/** New event types, models (ExternalAgent/SkillPack/WorkItem/WorkbenchStatus), AOT serializer entries, polymorphic event mapping
src/SharpClaw.Code.Runtime/** Workbench status service + abstraction, external-agent config provider, health diagnostic check, config merge of new sections, DI wiring
src/SharpClaw.Code.Commands/Handlers/** New External, Work, Workbench, CheckpointsSlash, AgentStatusSlash handlers; Skills extended with pack commands; Agents extended with external subcommand
src/SharpClaw.Code.Cli/Composition/CliServiceCollectionExtensions.cs Registers new handlers and shares AgentsCommandHandler instance across both CLI and slash command surfaces
SharpClawCode.sln + new .csproj files New project references and solution entries
tests/SharpClaw.Code.UnitTests/** Adds tests for GitHubWorkItemProvider URL parsing, SkillPackRegistry listing/installing, and the Codex external-agent adapter command shaping
docs/{external-agents,skills,work-items,workbench}.md + README.md New documentation for the user-facing features
Comments suppressed due to low confidence (2)

src/SharpClaw.Code.Skills/Services/SkillPackRegistry.cs:122

  • EnableAsync / DisableAsync look up the pack directory using skillId as a literal directory name, but ResolveAsync/BuildPromptAsync allow the user to refer to a pack by either Manifest.Id or Manifest.Name (case-insensitive). As a result, a user can skills run a pack by name but then skills enable --id <Name> will return "not found" and a no-op. Consider resolving the pack via ResolveAsync first and acting on its Source directory.
    src/SharpClaw.Code.Commands/Handlers/WorkCommandHandler.cs:94
  • ExportSummaryAsync requires request.Provider (it is later persisted into WorkItemSummaryExportedEvent), but WorkCommandHandler.ExecuteExportSummaryAsync always hardcodes "github" for the provider regardless of which provider actually imported the work item. The exported event will therefore mis-attribute generic/Jira summaries as GitHub. Pull the provider from the session-stored WorkItem.Provider (already available via ReadWorkItem) instead of from the request.
    private async Task<int> ExecuteExportSummaryAsync(string format, CommandExecutionContext context, CancellationToken cancellationToken)
    {
        var result = await workItemService
            .ExportSummaryAsync(new WorkItemExportRequest("github", context.SessionId, null, format), context.WorkingDirectory, cancellationToken)
            .ConfigureAwait(false);

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +88 to +100
private async Task<(ConversationSession Session, bool Created)> ResolveSessionAsync(string workspace, string? sessionId, WorkItem workItem, CancellationToken cancellationToken)
{
ConversationSession? session = null;
if (!string.IsNullOrWhiteSpace(sessionId))
{
session = await sessionStore.GetByIdAsync(workspace, sessionId, cancellationToken).ConfigureAwait(false);
}

session ??= await sessionStore.GetLatestAsync(workspace, cancellationToken).ConfigureAwait(false);
if (session is not null)
{
return (session, false);
}
Comment on lines +29 to +35
using var client = new HttpClient();
client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("SharpClawCode", "1.0"));
var token = Environment.GetEnvironmentVariable("GITHUB_TOKEN");
if (!string.IsNullOrWhiteSpace(token))
{
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
}
ResolveMetadata(session, SharpClawWorkflowMetadataKeys.ActiveAgentId),
status.RuntimeState,
status.ApprovalSettings,
0,
Comment on lines +78 to +95
var targetDirectory = pathService.Combine(GetWorkspaceRoot(workspaceRoot, ensureExists: true), manifest.Id);
fileSystem.CreateDirectory(targetDirectory);
await fileSystem.WriteAllTextAsync(
pathService.Combine(targetDirectory, ManifestFileName),
JsonSerializer.Serialize(manifest, ProtocolJsonContext.Default.SkillPackManifest),
cancellationToken).ConfigureAwait(false);

if (request.EnableAfterInstall)
{
fileSystem.TryDeleteFile(pathService.Combine(targetDirectory, DisabledFileName));
}
else
{
await fileSystem.WriteAllTextAsync(pathService.Combine(targetDirectory, DisabledFileName), systemClock.UtcNow.ToString("O"), cancellationToken).ConfigureAwait(false);
}

return new SkillPack(manifest, targetDirectory, false, request.EnableAfterInstall, systemClock.UtcNow);
}
Comment on lines +115 to +149
private async Task<ConversationSession> ResolveSessionAsync(string workspace, ExternalAgentRunRequest request, CancellationToken cancellationToken)
{
ConversationSession? session = null;
if (!string.IsNullOrWhiteSpace(request.SessionId))
{
session = await sessionStore.GetByIdAsync(workspace, request.SessionId, cancellationToken).ConfigureAwait(false);
}

session ??= await sessionStore.GetLatestAsync(workspace, cancellationToken).ConfigureAwait(false);
if (session is not null)
{
return session;
}

var created = new ConversationSession(
CreateIdentifier("session"),
"External agent session",
SessionLifecycleState.Active,
request.PermissionMode,
OutputFormat.Text,
workspace,
workspace,
systemClock.UtcNow,
systemClock.UtcNow,
null,
null,
new Dictionary<string, string>());
await sessionStore.SaveAsync(workspace, created, cancellationToken).ConfigureAwait(false);
await PublishAsync(
workspace,
created.Id,
new SessionCreatedEvent(CreateIdentifier("event"), created.Id, null, systemClock.UtcNow, created),
cancellationToken).ConfigureAwait(false);
return created;
}
public string Provider => "generic";

/// <inheritdoc />
public bool CanImport(string idOrUrl) => idOrUrl.EndsWith(".json", StringComparison.OrdinalIgnoreCase) || idOrUrl.TrimStart().StartsWith('{');
Comment on lines +56 to +70
if (command.Arguments.Length >= 2 && string.Equals(command.Arguments[0], "import", StringComparison.OrdinalIgnoreCase))
{
return ExecuteImportAsync("github", command.Arguments[1], context, cancellationToken);
}

if (command.Arguments.Length == 0 || string.Equals(command.Arguments[0], "show", StringComparison.OrdinalIgnoreCase))
{
return ExecuteExportSummaryAsync("markdown", context, cancellationToken);
}

if (string.Equals(command.Arguments[0], "export-summary", StringComparison.OrdinalIgnoreCase))
{
return ExecuteExportSummaryAsync("markdown", context, cancellationToken);
}


/// <inheritdoc />
public Task<int> ExecuteAsync(SlashCommandParseResult command, CommandExecutionContext context, CancellationToken cancellationToken)
=> agentsCommandHandler.ExecuteAsync(new SlashCommandParseResult(true, "agents", ["external", "status"]), context, cancellationToken);
Comment on lines +29 to +43
var manifest = new SkillPackManifest(
"custom-review",
"Custom Review",
"1.0.0",
"Custom review pack",
"tests",
["review"],
null,
null,
null,
null,
null,
null,
"Review {{arguments}} in {{workspace}}.");
await File.WriteAllTextAsync(manifestPath, JsonSerializer.Serialize(manifest, ProtocolJsonContext.Default.SkillPackManifest));
Comment on lines +179 to +186
private static void Validate(SkillPackManifest manifest)
{
ArgumentException.ThrowIfNullOrWhiteSpace(manifest.Id);
ArgumentException.ThrowIfNullOrWhiteSpace(manifest.Name);
ArgumentException.ThrowIfNullOrWhiteSpace(manifest.Version);
ArgumentException.ThrowIfNullOrWhiteSpace(manifest.Description);
ArgumentException.ThrowIfNullOrWhiteSpace(manifest.EntryPointPrompt);
}
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 15

🧹 Nitpick comments (1)
src/SharpClaw.Code.WorkItems/Providers/GitHubWorkItemProvider.cs (1)

29-35: 🏗️ Heavy lift

Use IHttpClientFactory for HttpClient management

new HttpClient() on each call increases connection churn and can hurt reliability under repeated imports. The codebase already demonstrates the proper pattern using IHttpClientFactory in TelemetryServiceCollectionExtensions.cs—configure headers and timeouts centrally via dependency injection.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/SharpClaw.Code.WorkItems/Providers/GitHubWorkItemProvider.cs` around
lines 29 - 35, Replace the direct new HttpClient usage in GitHubWorkItemProvider
(where the code creates a client, sets UserAgent and Authorization from
GITHUB_TOKEN) with an injected IHttpClientFactory: add an IHttpClientFactory
dependency to the GitHubWorkItemProvider constructor and call
_httpClientFactory.CreateClient("GitHub") (or a suitable named/typed client)
instead of new HttpClient(); move default header and timeout configuration into
your DI setup (following the pattern in TelemetryServiceCollectionExtensions.cs)
so UserAgent, Authorization (if token present) and timeouts are applied
centrally, and remove per-call client creation to avoid connection churn.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/work-items.md`:
- Line 20: Replace the internal reference GenericWorkItemFixture with a concrete
JSON example in the docs: update the sentence that currently reads "The JSON
shape matches `GenericWorkItemFixture`..." to include the provided JSON payload
example (provider, id, title, description, url, status, labels, assignee,
metadata) so CLI users can copy/import directly; ensure the example is fenced as
JSON and matches the suggested keys/values exactly.

In `@src/SharpClaw.Code.Commands/Handlers/AgentsCommandHandler.cs`:
- Around line 208-211: ParseMode currently coerces any unknown string to
ExternalAgentMode.WorkspaceWrite; change it to validate explicitly and fail
fast: map "readOnly" and "read" (case-insensitive) to
ExternalAgentMode.ReadOnly, map "write" and "workspaceWrite" (case-insensitive)
to ExternalAgentMode.WorkspaceWrite, and for any other null/unknown value throw
an ArgumentException (or ArgumentOutOfRangeException) with a clear message;
update the ParseMode method to perform these checks and throw instead of
returning a default.

In `@src/SharpClaw.Code.Commands/Handlers/ExternalCommandHandler.cs`:
- Around line 107-110: The ParseMode method currently treats any unrecognized
string as ExternalAgentMode.WorkspaceWrite, which risks accidental mutating
behavior; update ParseMode to explicitly accept only known values ("readOnly" /
"read" -> ExternalAgentMode.ReadOnly, "writeOnly" / "write" / "workspaceWrite"
-> ExternalAgentMode.WorkspaceWrite) and throw an informative exception (e.g.,
ArgumentException) or return a Result/nullable to surface invalid '--mode'
inputs so callers fail fast instead of defaulting to write mode; reference the
ParseMode function and ExternalAgentMode enum when implementing this validation
and ensure the error message mentions the invalid value and expected modes.

In `@src/SharpClaw.Code.Commands/Handlers/WorkCommandHandler.cs`:
- Around line 56-59: The code in WorkCommandHandler calls ExecuteImportAsync and
ExecuteExportAsync with a hardcoded "github" provider, breaking non-GitHub
sessions; change those calls to use the current session/provider instead of the
literal string. Replace the "github" argument with the runtime provider (e.g.
context.Session?.Provider or the provider parsed from the command) when calling
ExecuteImportAsync and ExecuteExportAsync so import/export use the active
provider (keep a sensible fallback only if absolutely needed). Ensure you update
all occurrences (the ExecuteImportAsync and ExecuteExportAsync calls referenced
in this diff) to reference that provider variable.

In `@src/SharpClaw.Code.ExternalAgents/Adapters/ExternalAgentAdapterBase.cs`:
- Around line 60-64: The error message uses Descriptor.ExecutableName which can
be incorrect when ExternalAgentAdapterConfig.ExecutablePath overrides the
default; modify the executable resolution block in ExternalAgentAdapterBase
(where executableResolver.Resolve(ResolveExecutable(adapterConfig)) is called)
to capture the resolved executable identifier (e.g., store
ResolveExecutable(adapterConfig) into a local like resolvedExecutable or
resolvedPath) and use that value in the Failure(...) message instead of
Descriptor.ExecutableName so the missing-executable error reports the actual
configured/resolved executable.

In `@src/SharpClaw.Code.ExternalAgents/Services/ExternalAgentRegistry.cs`:
- Around line 27-30: The loop over orderedAdapters calling
adapter.GetStatusAsync(...) can let one adapter exception fail the whole report;
change the foreach in ExternalAgentRegistry so each adapter call is protected
with try/catch: for each adapter in orderedAdapters, call await
adapter.GetStatusAsync(workspaceRoot, cancellationToken).ConfigureAwait(false)
inside a try block and on success Add the result to statuses, and in the catch
create and add a fallback/failed status object (including adapter identifier and
exception message/stack) so one adapter failure doesn't break the entire catalog
report; ensure you still observe cancellationToken and do not swallow exceptions
globally.

In `@src/SharpClaw.Code.ExternalAgents/Services/ExternalAgentService.cs`:
- Around line 89-113: Wrap the call to adapter.RunAsync inside a try/catch that
preserves cancellation (catch only Exception and rethrow if cancellation
requested), and on any non-cancellation exception construct an
ExternalAgentRunResult representing a failure (set FailureKind to a
runtime/exception kind, include the exception message/stack in OutputText and
ExternalSessionId as appropriate, and set an exit code) then call
PublishFailedAsync (or publish an ExternalAgentRunFailedEvent analogous to the
successful path) with the workspace, session.Id, request.AdapterId and the
constructed result, await SaveSessionMetadataAsync only on success, and finally
return the failure result; update ExternalAgentService where adapter.RunAsync is
invoked to ensure all exceptions produce a consistent ExternalAgentRunResult and
emitted failed event instead of letting the exception escape.
- Around line 118-124: The code currently falls back to GetLatestAsync when a
requested SessionId is provided but not found; change the logic in
ExternalAgentService so that if request.SessionId is non-empty you call
sessionStore.GetByIdAsync(workspace, request.SessionId, cancellationToken) and
if that returns null immediately throw a clear exception (e.g.
InvalidOperationException or a NotFoundException) indicating the specific
SessionId was not found, and only call sessionStore.GetLatestAsync when
request.SessionId is null/empty; update the code around the session variable and
the calls to GetByIdAsync/GetLatestAsync accordingly.

In
`@src/SharpClaw.Code.ExternalAgents/Services/PathExternalAgentExecutableResolver.cs`:
- Around line 31-34: The resolver in PathExternalAgentExecutableResolver appends
every extension from the extensions list to executableNameOrPath
unconditionally, which produces invalid candidates when the input already
contains an extension (e.g., "foo.exe" -> "foo.exe.EXE"); update the resolution
logic to first check if Path.HasExtension(executableNameOrPath) or
Path.GetExtension(executableNameOrPath) matches a Windows extension and, if so,
only test the given name as-is (construct candidate = Path.Combine(directory,
executableNameOrPath) and call File.Exists), otherwise iterate extensions and
append them as currently done; ensure you still use the existing variables
(extensions, executableNameOrPath, candidate, directory, File.Exists) and
preserve current directory iteration behavior.

In `@src/SharpClaw.Code.Runtime/Workflow/WorkbenchStatusService.cs`:
- Around line 38-40: The code in WorkbenchStatusService.cs currently reads the
entire session event stream via eventStore.ReadAllAsync and then calls
TakeLast(8) into the variable events, which loads full history; change this to
fetch only the last 8 events from the store instead of reading all. Update the
callsite using a store API that returns the tail (e.g.,
eventStore.ReadLastAsync(workspace, session.Id, 8, cancellationToken)) or, if
such an API is not available, replace the ReadAllAsync + TakeLast logic with a
streaming/iterator read that iterates the stream in reverse or reads pages and
stops once 8 events are collected (so the variable events still ends up as the
last 8 events but without materializing the entire stream). Ensure you replace
usages of eventStore.ReadAllAsync(...).TakeLast(8) and keep cancellationToken
handling intact.

In `@src/SharpClaw.Code.Skills/Services/SkillPackRegistry.cs`:
- Around line 146-153: ResolveFromDirectoryAsync currently calls
JsonSerializer.Deserialize which can throw JsonException and it returns a
SkillPack without validating the manifest, causing ListAsync enumeration to fail
on a single malformed manifest; update ResolveFromDirectoryAsync to catch
JsonException (and other deserialization exceptions), log or swallow the error
and return null so the directory scan can continue, and ensure you call
Validate(manifest) (same as ResolveAsync) before constructing/returning the
SkillPack instance to reject invalid manifests early; reference
ResolveFromDirectoryAsync, JsonSerializer.Deserialize, Validate(manifest),
ResolveAsync and SkillPack when making the changes.
- Around line 78-79: The code in SkillPackRegistry.cs uses manifest.Id and
skillId directly with IPathService.Combine/GetWorkspaceRoot and
fileSystem.CreateDirectory, allowing path traversal or absolute paths; modify
the code to validate and sanitize these IDs before any path composition:
add/extend Validate() to reject empty or whitespace IDs, any path separator
characters (Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar), rooted
paths (Path.IsPathRooted), parent-segment sequences (".."), and any
Path.GetInvalidFileNameChars; alternatively normalize IDs by taking
Path.GetFileName(id) or using a dedicated SanitizeSkillPackId(string) helper and
call it everywhere manifest.Id or skillId are used (e.g., in GetWorkspaceRoot,
where paths are combined and in directory creation like
fileSystem.CreateDirectory(targetDirectory)) so that only safe, single-segment
directory names are allowed.

In `@src/SharpClaw.Code.WorkItems/Providers/GenericWorkItemProvider.cs`:
- Around line 27-28: The JSON deserialization in GenericWorkItemProvider
currently lets a JsonException bubble up instead of normalizing to the
provider's InvalidOperationException; wrap the call to
JsonSerializer.Deserialize(...) that assigns 'fixture' in
GenericWorkItemProvider in a try/catch that catches JsonException and rethrows a
new InvalidOperationException("Generic work-item fixture could not be parsed.",
innerException) so malformed JSON is reported consistently (preserve the
original exception as the inner exception).

In `@src/SharpClaw.Code.WorkItems/Services/WorkItemRegistry.cs`:
- Around line 13-15: Resolve currently falls back to CanImport even when a
non-empty provider argument was explicitly given, which can return an unintended
provider; update Resolve (method Resolve and use of orderedProviders) to first
attempt the case-insensitive provider match, and if provider is not null/empty
(use string.IsNullOrWhiteSpace or IsNullOrEmpty) and no match is found return
null immediately, otherwise proceed to the existing fallback that picks the
first orderedProviders item where CanImport(idOrUrl) is true.

In `@src/SharpClaw.Code.WorkItems/Services/WorkItemService.cs`:
- Around line 91-99: The current logic falls back to GetLatestAsync when a
requested sessionId is not found; change it so that when sessionId is provided
(non-empty) you call sessionStore.GetByIdAsync(workspace, sessionId, ...) and if
that returns null you fail immediately (throw an informative exception or return
an explicit error) instead of calling sessionStore.GetLatestAsync; only call
GetLatestAsync when sessionId is null/whitespace. Update the code around the
session variable and the calls to sessionStore.GetByIdAsync and GetLatestAsync
in the method (the block handling sessionId/session) to implement this behavior
and surface a clear error when the explicit session is missing.

---

Nitpick comments:
In `@src/SharpClaw.Code.WorkItems/Providers/GitHubWorkItemProvider.cs`:
- Around line 29-35: Replace the direct new HttpClient usage in
GitHubWorkItemProvider (where the code creates a client, sets UserAgent and
Authorization from GITHUB_TOKEN) with an injected IHttpClientFactory: add an
IHttpClientFactory dependency to the GitHubWorkItemProvider constructor and call
_httpClientFactory.CreateClient("GitHub") (or a suitable named/typed client)
instead of new HttpClient(); move default header and timeout configuration into
your DI setup (following the pattern in TelemetryServiceCollectionExtensions.cs)
so UserAgent, Authorization (if token present) and timeouts are applied
centrally, and remove per-call client creation to avoid connection churn.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c3a67092-172f-4c6a-8e67-40f657fe811b

📥 Commits

Reviewing files that changed from the base of the PR and between 2ec4dcb and 7094ec1.

📒 Files selected for processing (65)
  • README.md
  • SharpClawCode.sln
  • docs/external-agents.md
  • docs/skills.md
  • docs/work-items.md
  • docs/workbench.md
  • src/SharpClaw.Code.Cli/Composition/CliServiceCollectionExtensions.cs
  • src/SharpClaw.Code.Commands/Handlers/AgentStatusSlashCommandHandler.cs
  • src/SharpClaw.Code.Commands/Handlers/AgentsCommandHandler.cs
  • src/SharpClaw.Code.Commands/Handlers/CheckpointsSlashCommandHandler.cs
  • src/SharpClaw.Code.Commands/Handlers/ExternalCommandHandler.cs
  • src/SharpClaw.Code.Commands/Handlers/SkillsCommandHandler.cs
  • src/SharpClaw.Code.Commands/Handlers/WorkCommandHandler.cs
  • src/SharpClaw.Code.Commands/Handlers/WorkbenchCommandHandler.cs
  • src/SharpClaw.Code.Commands/SharpClaw.Code.Commands.csproj
  • src/SharpClaw.Code.ExternalAgents/Abstractions/IExternalAgentAdapter.cs
  • src/SharpClaw.Code.ExternalAgents/Abstractions/IExternalAgentConfigProvider.cs
  • src/SharpClaw.Code.ExternalAgents/Abstractions/IExternalAgentExecutableResolver.cs
  • src/SharpClaw.Code.ExternalAgents/Abstractions/IExternalAgentRegistry.cs
  • src/SharpClaw.Code.ExternalAgents/Abstractions/IExternalAgentService.cs
  • src/SharpClaw.Code.ExternalAgents/Adapters/BuiltInExternalAgentAdapters.cs
  • src/SharpClaw.Code.ExternalAgents/Adapters/ExternalAgentAdapterBase.cs
  • src/SharpClaw.Code.ExternalAgents/AssemblyMarker.cs
  • src/SharpClaw.Code.ExternalAgents/ExternalAgentsServiceCollectionExtensions.cs
  • src/SharpClaw.Code.ExternalAgents/Services/DefaultExternalAgentConfigProvider.cs
  • src/SharpClaw.Code.ExternalAgents/Services/ExternalAgentRegistry.cs
  • src/SharpClaw.Code.ExternalAgents/Services/ExternalAgentService.cs
  • src/SharpClaw.Code.ExternalAgents/Services/PathExternalAgentExecutableResolver.cs
  • src/SharpClaw.Code.ExternalAgents/SharpClaw.Code.ExternalAgents.csproj
  • src/SharpClaw.Code.Protocol/Events/ExternalAgentEvents.cs
  • src/SharpClaw.Code.Protocol/Events/RuntimeEvent.cs
  • src/SharpClaw.Code.Protocol/Events/SkillAndWorkItemEvents.cs
  • src/SharpClaw.Code.Protocol/Models/ExternalAgentModels.cs
  • src/SharpClaw.Code.Protocol/Models/OpenCodeParityModels.cs
  • src/SharpClaw.Code.Protocol/Models/SharpClawWorkflowMetadataKeys.cs
  • src/SharpClaw.Code.Protocol/Models/SkillPackModels.cs
  • src/SharpClaw.Code.Protocol/Models/WorkItemModels.cs
  • src/SharpClaw.Code.Protocol/Operational/WorkbenchStatusReport.cs
  • src/SharpClaw.Code.Protocol/Serialization/ProtocolJsonContext.cs
  • src/SharpClaw.Code.Runtime/Abstractions/IWorkbenchStatusService.cs
  • src/SharpClaw.Code.Runtime/Composition/RuntimeServiceCollectionExtensions.cs
  • src/SharpClaw.Code.Runtime/Configuration/SharpClawConfigService.cs
  • src/SharpClaw.Code.Runtime/Diagnostics/Checks/ExternalAgentHealthCheck.cs
  • src/SharpClaw.Code.Runtime/Diagnostics/OperationalDiagnosticsCoordinator.cs
  • src/SharpClaw.Code.Runtime/Export/SessionExportService.cs
  • src/SharpClaw.Code.Runtime/ExternalAgents/RuntimeExternalAgentConfigProvider.cs
  • src/SharpClaw.Code.Runtime/SharpClaw.Code.Runtime.csproj
  • src/SharpClaw.Code.Runtime/Workflow/WorkbenchStatusService.cs
  • src/SharpClaw.Code.Skills/Abstractions/ISkillPackRegistry.cs
  • src/SharpClaw.Code.Skills/Services/SkillPackRegistry.cs
  • src/SharpClaw.Code.Skills/SkillsServiceCollectionExtensions.cs
  • src/SharpClaw.Code.WorkItems/Abstractions/IWorkItemProvider.cs
  • src/SharpClaw.Code.WorkItems/Abstractions/IWorkItemRegistry.cs
  • src/SharpClaw.Code.WorkItems/Abstractions/IWorkItemService.cs
  • src/SharpClaw.Code.WorkItems/AssemblyMarker.cs
  • src/SharpClaw.Code.WorkItems/Providers/GenericWorkItemProvider.cs
  • src/SharpClaw.Code.WorkItems/Providers/GitHubWorkItemProvider.cs
  • src/SharpClaw.Code.WorkItems/Services/WorkItemRegistry.cs
  • src/SharpClaw.Code.WorkItems/Services/WorkItemService.cs
  • src/SharpClaw.Code.WorkItems/SharpClaw.Code.WorkItems.csproj
  • src/SharpClaw.Code.WorkItems/WorkItemsServiceCollectionExtensions.cs
  • tests/SharpClaw.Code.UnitTests/ExternalAgents/ExternalAgentAdapterTests.cs
  • tests/SharpClaw.Code.UnitTests/SharpClaw.Code.UnitTests.csproj
  • tests/SharpClaw.Code.UnitTests/Skills/SkillPackRegistryTests.cs
  • tests/SharpClaw.Code.UnitTests/WorkItems/GitHubWorkItemProviderTests.cs

Comment thread docs/work-items.md
sharpclaw work import ./task.json --provider generic
```

The JSON shape matches `GenericWorkItemFixture`: provider, id, title, description, url, status, labels, assignee, and metadata.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Replace internal fixture reference with a concrete JSON example.

GenericWorkItemFixture looks internal and is not useful to CLI users. Document the expected payload directly so users can import without reading source.

📄 Suggested doc update
-The JSON shape matches `GenericWorkItemFixture`: provider, id, title, description, url, status, labels, assignee, and metadata.
+Expected JSON fields: `provider`, `id`, `title`, `description`, `url`, `status`, `labels`, `assignee`, and `metadata`.
+
+Example:
+```json
+{
+  "provider": "generic",
+  "id": "task-123",
+  "title": "Investigate flaky test",
+  "description": "Repro and fix intermittent timeout in CI",
+  "url": "https://tracker.example.com/tasks/task-123",
+  "status": "open",
+  "labels": ["ci", "tests"],
+  "assignee": "alice",
+  "metadata": { "priority": "high" }
+}
+```
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
The JSON shape matches `GenericWorkItemFixture`: provider, id, title, description, url, status, labels, assignee, and metadata.
Expected JSON fields: `provider`, `id`, `title`, `description`, `url`, `status`, `labels`, `assignee`, and `metadata`.
Example:
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/work-items.md` at line 20, Replace the internal reference
GenericWorkItemFixture with a concrete JSON example in the docs: update the
sentence that currently reads "The JSON shape matches
`GenericWorkItemFixture`..." to include the provided JSON payload example
(provider, id, title, description, url, status, labels, assignee, metadata) so
CLI users can copy/import directly; ensure the example is fenced as JSON and
matches the suggested keys/values exactly.

Comment on lines +208 to +211
private static ExternalAgentMode ParseMode(string? value)
=> string.Equals(value, "readOnly", StringComparison.OrdinalIgnoreCase) || string.Equals(value, "read", StringComparison.OrdinalIgnoreCase)
? ExternalAgentMode.ReadOnly
: ExternalAgentMode.WorkspaceWrite;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Do not coerce invalid external mode values to WorkspaceWrite.

Unknown mode strings should fail fast; silently defaulting to write mode can trigger unintended workspace mutations.

Suggested fix
 private static ExternalAgentMode ParseMode(string? value)
-    => string.Equals(value, "readOnly", StringComparison.OrdinalIgnoreCase) || string.Equals(value, "read", StringComparison.OrdinalIgnoreCase)
-        ? ExternalAgentMode.ReadOnly
-        : ExternalAgentMode.WorkspaceWrite;
+{
+    if (string.Equals(value, "readOnly", StringComparison.OrdinalIgnoreCase) ||
+        string.Equals(value, "read", StringComparison.OrdinalIgnoreCase))
+    {
+        return ExternalAgentMode.ReadOnly;
+    }
+
+    if (string.Equals(value, "workspaceWrite", StringComparison.OrdinalIgnoreCase) ||
+        string.Equals(value, "write", StringComparison.OrdinalIgnoreCase))
+    {
+        return ExternalAgentMode.WorkspaceWrite;
+    }
+
+    throw new InvalidOperationException($"Unsupported external agent mode '{value}'.");
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/SharpClaw.Code.Commands/Handlers/AgentsCommandHandler.cs` around lines
208 - 211, ParseMode currently coerces any unknown string to
ExternalAgentMode.WorkspaceWrite; change it to validate explicitly and fail
fast: map "readOnly" and "read" (case-insensitive) to
ExternalAgentMode.ReadOnly, map "write" and "workspaceWrite" (case-insensitive)
to ExternalAgentMode.WorkspaceWrite, and for any other null/unknown value throw
an ArgumentException (or ArgumentOutOfRangeException) with a clear message;
update the ParseMode method to perform these checks and throw instead of
returning a default.

Comment on lines +107 to +110
private static ExternalAgentMode ParseMode(string? value)
=> string.Equals(value, "readOnly", StringComparison.OrdinalIgnoreCase) || string.Equals(value, "read", StringComparison.OrdinalIgnoreCase)
? ExternalAgentMode.ReadOnly
: ExternalAgentMode.WorkspaceWrite;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reject unknown --mode values instead of defaulting to mutating mode.

Invalid input currently falls back to WorkspaceWrite, which can unintentionally run with write permissions. Treat unknown modes as an error.

Suggested fix
 private static ExternalAgentMode ParseMode(string? value)
-    => string.Equals(value, "readOnly", StringComparison.OrdinalIgnoreCase) || string.Equals(value, "read", StringComparison.OrdinalIgnoreCase)
-        ? ExternalAgentMode.ReadOnly
-        : ExternalAgentMode.WorkspaceWrite;
+{
+    if (string.Equals(value, "readOnly", StringComparison.OrdinalIgnoreCase) ||
+        string.Equals(value, "read", StringComparison.OrdinalIgnoreCase))
+    {
+        return ExternalAgentMode.ReadOnly;
+    }
+
+    if (string.Equals(value, "workspaceWrite", StringComparison.OrdinalIgnoreCase) ||
+        string.Equals(value, "write", StringComparison.OrdinalIgnoreCase))
+    {
+        return ExternalAgentMode.WorkspaceWrite;
+    }
+
+    throw new InvalidOperationException($"Unsupported external agent mode '{value}'.");
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/SharpClaw.Code.Commands/Handlers/ExternalCommandHandler.cs` around lines
107 - 110, The ParseMode method currently treats any unrecognized string as
ExternalAgentMode.WorkspaceWrite, which risks accidental mutating behavior;
update ParseMode to explicitly accept only known values ("readOnly" / "read" ->
ExternalAgentMode.ReadOnly, "writeOnly" / "write" / "workspaceWrite" ->
ExternalAgentMode.WorkspaceWrite) and throw an informative exception (e.g.,
ArgumentException) or return a Result/nullable to surface invalid '--mode'
inputs so callers fail fast instead of defaulting to write mode; reference the
ParseMode function and ExternalAgentMode enum when implementing this validation
and ensure the error message mentions the invalid value and expected modes.

Comment on lines +56 to +59
if (command.Arguments.Length >= 2 && string.Equals(command.Arguments[0], "import", StringComparison.OrdinalIgnoreCase))
{
return ExecuteImportAsync("github", command.Arguments[1], context, cancellationToken);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Remove hardcoded github provider in import/export paths.

Line 58 and Line 93 force "github", which breaks provider parity for generic work items and can fail /work show / export-summary for non-GitHub sessions.

Suggested fix sketch
- private async Task<int> ExecuteExportSummaryAsync(string format, CommandExecutionContext context, CancellationToken cancellationToken)
+ private async Task<int> ExecuteExportSummaryAsync(string provider, string format, CommandExecutionContext context, CancellationToken cancellationToken)
 {
     var result = await workItemService
-        .ExportSummaryAsync(new WorkItemExportRequest("github", context.SessionId, null, format), context.WorkingDirectory, cancellationToken)
+        .ExportSummaryAsync(new WorkItemExportRequest(provider, context.SessionId, null, format), context.WorkingDirectory, cancellationToken)
         .ConfigureAwait(false);
- return ExecuteImportAsync("github", command.Arguments[1], context, cancellationToken);
+ // Parse provider in slash mode (default github) and pass through.
+ return ExecuteImportAsync(parsedProvider, parsedTarget, context, cancellationToken);

Also applies to: 90-94

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/SharpClaw.Code.Commands/Handlers/WorkCommandHandler.cs` around lines 56 -
59, The code in WorkCommandHandler calls ExecuteImportAsync and
ExecuteExportAsync with a hardcoded "github" provider, breaking non-GitHub
sessions; change those calls to use the current session/provider instead of the
literal string. Replace the "github" argument with the runtime provider (e.g.
context.Session?.Provider or the provider parsed from the command) when calling
ExecuteImportAsync and ExecuteExportAsync so import/export use the active
provider (keep a sensible fallback only if absolutely needed). Ensure you update
all occurrences (the ExecuteImportAsync and ExecuteExportAsync calls referenced
in this diff) to reference that provider variable.

Comment on lines +60 to +64
var executable = executableResolver.Resolve(ResolveExecutable(adapterConfig));
if (executable is null)
{
return Failure(request.AdapterId, ExternalAgentFailureKind.ExecutableMissing, $"Executable '{Descriptor.ExecutableName}' was not found.");
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use the resolved configured executable in missing-executable errors.

Current message can misreport the missing binary when ExternalAgentAdapterConfig.ExecutablePath overrides the default.

Suggested fix
-        var executable = executableResolver.Resolve(ResolveExecutable(adapterConfig));
+        var configuredExecutable = ResolveExecutable(adapterConfig);
+        var executable = executableResolver.Resolve(configuredExecutable);
         if (executable is null)
         {
-            return Failure(request.AdapterId, ExternalAgentFailureKind.ExecutableMissing, $"Executable '{Descriptor.ExecutableName}' was not found.");
+            return Failure(request.AdapterId, ExternalAgentFailureKind.ExecutableMissing, $"Executable '{configuredExecutable}' was not found.");
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
var executable = executableResolver.Resolve(ResolveExecutable(adapterConfig));
if (executable is null)
{
return Failure(request.AdapterId, ExternalAgentFailureKind.ExecutableMissing, $"Executable '{Descriptor.ExecutableName}' was not found.");
}
var configuredExecutable = ResolveExecutable(adapterConfig);
var executable = executableResolver.Resolve(configuredExecutable);
if (executable is null)
{
return Failure(request.AdapterId, ExternalAgentFailureKind.ExecutableMissing, $"Executable '{configuredExecutable}' was not found.");
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/SharpClaw.Code.ExternalAgents/Adapters/ExternalAgentAdapterBase.cs`
around lines 60 - 64, The error message uses Descriptor.ExecutableName which can
be incorrect when ExternalAgentAdapterConfig.ExecutablePath overrides the
default; modify the executable resolution block in ExternalAgentAdapterBase
(where executableResolver.Resolve(ResolveExecutable(adapterConfig)) is called)
to capture the resolved executable identifier (e.g., store
ResolveExecutable(adapterConfig) into a local like resolvedExecutable or
resolvedPath) and use that value in the Failure(...) message instead of
Descriptor.ExecutableName so the missing-executable error reports the actual
configured/resolved executable.

Comment on lines +78 to +79
var targetDirectory = pathService.Combine(GetWorkspaceRoot(workspaceRoot, ensureExists: true), manifest.Id);
fileSystem.CreateDirectory(targetDirectory);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether path services already enforce containment/sanitization and where user-provided IDs flow in.
fd -i "IPathService.cs" src
rg -n -C3 "interface IPathService|class .*PathService|: IPathService|GetFullPath\\(|Combine\\(" src
rg -n -C3 "InstallAsync\\(|EnableAsync\\(|DisableAsync\\(" src/SharpClaw.Code.Commands src/SharpClaw.Code.Skills

Repository: clawdotnet/SharpClawCode

Length of output: 50381


🏁 Script executed:

head -100 src/SharpClaw.Code.Infrastructure/Abstractions/IPathService.cs

Repository: clawdotnet/SharpClawCode

Length of output: 1699


🏁 Script executed:

sed -n '60,190p' src/SharpClaw.Code.Skills/Services/SkillPackRegistry.cs

Repository: clawdotnet/SharpClawCode

Length of output: 6295


🏁 Script executed:

rg -l "class.*PathService.*:.*IPathService" src

Repository: clawdotnet/SharpClawCode

Length of output: 126


🏁 Script executed:

cat src/SharpClaw.Code.Infrastructure/Services/PathService.cs

Repository: clawdotnet/SharpClawCode

Length of output: 2449


Harden skill-pack ID handling to prevent path traversal and arbitrary file writes.

SkillPackRegistry.cs composes filesystem paths from manifest.Id and skillId (lines 78, 101, 114) without validating they are safe directory names. The Validate() method (lines 179–186) only enforces non-empty values. IPathService.Combine() is a direct wrapper around Path.Combine(), which does not enforce containment—absolute paths or traversal sequences in the ID parameter override the base directory.

A crafted ID like ../../etc/passwd, /tmp/evil, or C:\Windows\System32 will escape the workspace skill-pack root, enabling arbitrary file creation, deletion, or overwrite operations outside the intended scope.

🔧 Suggested fix
+using System.IO;
 using System.Text.Json;
 using SharpClaw.Code.Infrastructure.Abstractions;
@@
-        var targetDirectory = pathService.Combine(GetWorkspaceRoot(workspaceRoot, ensureExists: true), manifest.Id);
+        var targetDirectory = GetScopedPackDirectory(workspaceRoot, manifest.Id, ensureExists: true);
@@
-        var directory = pathService.Combine(GetWorkspaceRoot(workspaceRoot, ensureExists: false), skillId);
+        var directory = GetScopedPackDirectory(workspaceRoot, skillId, ensureExists: false);
@@
-        var directory = pathService.Combine(GetWorkspaceRoot(workspaceRoot, ensureExists: false), skillId);
+        var directory = GetScopedPackDirectory(workspaceRoot, skillId, ensureExists: false);
@@
     private static void Validate(SkillPackManifest manifest)
     {
         ArgumentException.ThrowIfNullOrWhiteSpace(manifest.Id);
+        if (manifest.Id.IndexOfAny(['/', '\\']) >= 0 || manifest.Id.Contains("..", StringComparison.Ordinal))
+        {
+            throw new InvalidOperationException("Skill pack id must be a safe directory name.");
+        }
         ArgumentException.ThrowIfNullOrWhiteSpace(manifest.Name);
         ArgumentException.ThrowIfNullOrWhiteSpace(manifest.Version);
         ArgumentException.ThrowIfNullOrWhiteSpace(manifest.Description);
         ArgumentException.ThrowIfNullOrWhiteSpace(manifest.EntryPointPrompt);
     }
+
+    private string GetScopedPackDirectory(string workspaceRoot, string skillId, bool ensureExists)
+    {
+        ArgumentException.ThrowIfNullOrWhiteSpace(skillId);
+        if (skillId.IndexOfAny(['/', '\\']) >= 0 || skillId.Contains("..", StringComparison.Ordinal))
+        {
+            throw new InvalidOperationException("Skill pack id must be a safe directory name.");
+        }
+
+        var root = pathService.GetFullPath(GetWorkspaceRoot(workspaceRoot, ensureExists));
+        var candidate = pathService.GetFullPath(pathService.Combine(root, skillId));
+        var rootPrefix = root.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + Path.DirectorySeparatorChar;
+        if (!candidate.StartsWith(rootPrefix, StringComparison.OrdinalIgnoreCase))
+        {
+            throw new InvalidOperationException("Skill pack path escaped workspace scope.");
+        }
+
+        return candidate;
+    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/SharpClaw.Code.Skills/Services/SkillPackRegistry.cs` around lines 78 -
79, The code in SkillPackRegistry.cs uses manifest.Id and skillId directly with
IPathService.Combine/GetWorkspaceRoot and fileSystem.CreateDirectory, allowing
path traversal or absolute paths; modify the code to validate and sanitize these
IDs before any path composition: add/extend Validate() to reject empty or
whitespace IDs, any path separator characters (Path.DirectorySeparatorChar,
Path.AltDirectorySeparatorChar), rooted paths (Path.IsPathRooted),
parent-segment sequences (".."), and any Path.GetInvalidFileNameChars;
alternatively normalize IDs by taking Path.GetFileName(id) or using a dedicated
SanitizeSkillPackId(string) helper and call it everywhere manifest.Id or skillId
are used (e.g., in GetWorkspaceRoot, where paths are combined and in directory
creation like fileSystem.CreateDirectory(targetDirectory)) so that only safe,
single-segment directory names are allowed.

Comment on lines +146 to +153
var manifestText = await fileSystem.ReadAllTextIfExistsAsync(pathService.Combine(directory, ManifestFileName), cancellationToken).ConfigureAwait(false);
if (string.IsNullOrWhiteSpace(manifestText))
{
return null;
}

var manifest = JsonSerializer.Deserialize(manifestText, ProtocolJsonContext.Default.SkillPackManifest);
if (manifest is null)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify all SkillPackManifest deserialization paths and current exception handling coverage.
rg -n -C3 "JsonSerializer\\.Deserialize\\(.*SkillPackManifest" src
rg -n -C3 "catch \\(JsonException\\)|Validate\\(manifest\\)" src/SharpClaw.Code.Skills

Repository: clawdotnet/SharpClawCode

Length of output: 2607


🏁 Script executed:

sed -n '145,175p' src/SharpClaw.Code.Skills/Services/SkillPackRegistry.cs

Repository: clawdotnet/SharpClawCode

Length of output: 1266


🏁 Script executed:

rg -n "ResolveFromDirectoryAsync" src/SharpClaw.Code.Skills/Services/SkillPackRegistry.cs | head -20

Repository: clawdotnet/SharpClawCode

Length of output: 303


🏁 Script executed:

sed -n '40,60p' src/SharpClaw.Code.Skills/Services/SkillPackRegistry.cs

Repository: clawdotnet/SharpClawCode

Length of output: 874


🏁 Script executed:

sed -n '30,50p' src/SharpClaw.Code.Skills/Services/SkillPackRegistry.cs

Repository: clawdotnet/SharpClawCode

Length of output: 871


Add exception handling and validation to ResolveFromDirectoryAsync to prevent malformed manifests from breaking directory scans.

At line 152, JsonSerializer.Deserialize can throw JsonException uncaught. In ListAsync, this breaks enumeration of all remaining packs when a single manifest is malformed. Additionally, the method returns a SkillPack without calling Validate(manifest), unlike the ResolveAsync method which validates at line 76.

🛡️ Suggested fix
     private async Task<SkillPack?> ResolveFromDirectoryAsync(string directory, CancellationToken cancellationToken)
     {
         var manifestText = await fileSystem.ReadAllTextIfExistsAsync(pathService.Combine(directory, ManifestFileName), cancellationToken).ConfigureAwait(false);
         if (string.IsNullOrWhiteSpace(manifestText))
         {
             return null;
         }
 
-        var manifest = JsonSerializer.Deserialize(manifestText, ProtocolJsonContext.Default.SkillPackManifest);
-        if (manifest is null)
+        SkillPackManifest? manifest;
+        try
         {
-            return null;
+            manifest = JsonSerializer.Deserialize(manifestText, ProtocolJsonContext.Default.SkillPackManifest);
         }
+        catch (JsonException)
+        {
+            return null;
+        }
+        if (manifest is null)
+        {
+            return null;
+        }
+        try
+        {
+            Validate(manifest);
+        }
+        catch (ArgumentException)
+        {
+            return null;
+        }
+        catch (InvalidOperationException)
+        {
+            return null;
+        }
 
         return new SkillPack(manifest, directory, false, !fileSystem.FileExists(pathService.Combine(directory, DisabledFileName)));
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
var manifestText = await fileSystem.ReadAllTextIfExistsAsync(pathService.Combine(directory, ManifestFileName), cancellationToken).ConfigureAwait(false);
if (string.IsNullOrWhiteSpace(manifestText))
{
return null;
}
var manifest = JsonSerializer.Deserialize(manifestText, ProtocolJsonContext.Default.SkillPackManifest);
if (manifest is null)
var manifestText = await fileSystem.ReadAllTextIfExistsAsync(pathService.Combine(directory, ManifestFileName), cancellationToken).ConfigureAwait(false);
if (string.IsNullOrWhiteSpace(manifestText))
{
return null;
}
SkillPackManifest? manifest;
try
{
manifest = JsonSerializer.Deserialize(manifestText, ProtocolJsonContext.Default.SkillPackManifest);
}
catch (JsonException)
{
return null;
}
if (manifest is null)
{
return null;
}
try
{
Validate(manifest);
}
catch (ArgumentException)
{
return null;
}
catch (InvalidOperationException)
{
return null;
}
return new SkillPack(manifest, directory, false, !fileSystem.FileExists(pathService.Combine(directory, DisabledFileName)));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/SharpClaw.Code.Skills/Services/SkillPackRegistry.cs` around lines 146 -
153, ResolveFromDirectoryAsync currently calls JsonSerializer.Deserialize which
can throw JsonException and it returns a SkillPack without validating the
manifest, causing ListAsync enumeration to fail on a single malformed manifest;
update ResolveFromDirectoryAsync to catch JsonException (and other
deserialization exceptions), log or swallow the error and return null so the
directory scan can continue, and ensure you call Validate(manifest) (same as
ResolveAsync) before constructing/returning the SkillPack instance to reject
invalid manifests early; reference ResolveFromDirectoryAsync,
JsonSerializer.Deserialize, Validate(manifest), ResolveAsync and SkillPack when
making the changes.

Comment on lines +27 to +28
var fixture = JsonSerializer.Deserialize(json, ProtocolJsonContext.Default.GenericWorkItemFixture)
?? throw new InvalidOperationException("Generic work-item fixture could not be parsed.");
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

In .NET System.Text.Json, does JsonSerializer.Deserialize(string, JsonTypeInfo)throwJsonException for malformed JSON rather than returning null?

💡 Result:

Yes, JsonSerializer.Deserialize(string, JsonTypeInfo) throws a JsonException when the provided JSON is malformed [1][2]. According to official Microsoft documentation, the method is designed to parse JSON text into an instance of the specified type [1][2]. If the JSON is invalid, the JsonTypeInfo is not compatible with the JSON, or a value cannot be read from the reader, the method will throw a JsonException [1][2]. It does not return null for malformed JSON input [1][2].

Citations:


🏁 Script executed:

# First, check if the file exists and read the relevant section
find . -name "GenericWorkItemProvider.cs" -type f

Repository: clawdotnet/SharpClawCode

Length of output: 136


🏁 Script executed:

# Read the file around lines 27-28 to see the context
cat -n ./src/SharpClaw.Code.WorkItems/Providers/GenericWorkItemProvider.cs | head -50

Repository: clawdotnet/SharpClawCode

Length of output: 1723


Normalize malformed JSON failures to a consistent import exception.

At lines 27–28, malformed JSON throws JsonException directly instead of the provider's InvalidOperationException flow used for other failures (file not found at line 26, structure validation at line 28).

Proposed fix
-        var fixture = JsonSerializer.Deserialize(json, ProtocolJsonContext.Default.GenericWorkItemFixture)
-            ?? throw new InvalidOperationException("Generic work-item fixture could not be parsed.");
+        GenericWorkItemFixture fixture;
+        try
+        {
+            fixture = JsonSerializer.Deserialize(json, ProtocolJsonContext.Default.GenericWorkItemFixture)
+                ?? throw new InvalidOperationException("Generic work-item fixture could not be parsed.");
+        }
+        catch (JsonException ex)
+        {
+            throw new InvalidOperationException("Generic work-item fixture could not be parsed.", ex);
+        }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
var fixture = JsonSerializer.Deserialize(json, ProtocolJsonContext.Default.GenericWorkItemFixture)
?? throw new InvalidOperationException("Generic work-item fixture could not be parsed.");
GenericWorkItemFixture fixture;
try
{
fixture = JsonSerializer.Deserialize(json, ProtocolJsonContext.Default.GenericWorkItemFixture)
?? throw new InvalidOperationException("Generic work-item fixture could not be parsed.");
}
catch (JsonException ex)
{
throw new InvalidOperationException("Generic work-item fixture could not be parsed.", ex);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/SharpClaw.Code.WorkItems/Providers/GenericWorkItemProvider.cs` around
lines 27 - 28, The JSON deserialization in GenericWorkItemProvider currently
lets a JsonException bubble up instead of normalizing to the provider's
InvalidOperationException; wrap the call to JsonSerializer.Deserialize(...) that
assigns 'fixture' in GenericWorkItemProvider in a try/catch that catches
JsonException and rethrows a new InvalidOperationException("Generic work-item
fixture could not be parsed.", innerException) so malformed JSON is reported
consistently (preserve the original exception as the inner exception).

Comment on lines +13 to +15
public IWorkItemProvider? Resolve(string provider, string idOrUrl)
=> orderedProviders.FirstOrDefault(item => string.Equals(item.Provider, provider, StringComparison.OrdinalIgnoreCase))
?? orderedProviders.FirstOrDefault(item => item.CanImport(idOrUrl));
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Respect explicit provider selection before fallback.

At Line 13–15, Resolve falls back to CanImport even when a non-empty provider was explicitly supplied but not found. That can silently pick the wrong provider.

Suggested fix
 public IWorkItemProvider? Resolve(string provider, string idOrUrl)
-    => orderedProviders.FirstOrDefault(item => string.Equals(item.Provider, provider, StringComparison.OrdinalIgnoreCase))
-        ?? orderedProviders.FirstOrDefault(item => item.CanImport(idOrUrl));
+{
+    var direct = orderedProviders.FirstOrDefault(item =>
+        string.Equals(item.Provider, provider, StringComparison.OrdinalIgnoreCase));
+    if (direct is not null)
+    {
+        return direct;
+    }
+
+    if (!string.IsNullOrWhiteSpace(provider))
+    {
+        return null;
+    }
+
+    return orderedProviders.FirstOrDefault(item => item.CanImport(idOrUrl));
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/SharpClaw.Code.WorkItems/Services/WorkItemRegistry.cs` around lines 13 -
15, Resolve currently falls back to CanImport even when a non-empty provider
argument was explicitly given, which can return an unintended provider; update
Resolve (method Resolve and use of orderedProviders) to first attempt the
case-insensitive provider match, and if provider is not null/empty (use
string.IsNullOrWhiteSpace or IsNullOrEmpty) and no match is found return null
immediately, otherwise proceed to the existing fallback that picks the first
orderedProviders item where CanImport(idOrUrl) is true.

Comment on lines +91 to +99
if (!string.IsNullOrWhiteSpace(sessionId))
{
session = await sessionStore.GetByIdAsync(workspace, sessionId, cancellationToken).ConfigureAwait(false);
}

session ??= await sessionStore.GetLatestAsync(workspace, cancellationToken).ConfigureAwait(false);
if (session is not null)
{
return (session, false);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fail when an explicitly requested session does not exist.

If sessionId is provided but missing, code currently falls back to latest session, which can import into the wrong session.

Suggested fix
         if (!string.IsNullOrWhiteSpace(sessionId))
         {
             session = await sessionStore.GetByIdAsync(workspace, sessionId, cancellationToken).ConfigureAwait(false);
+            if (session is null)
+            {
+                throw new InvalidOperationException($"Session '{sessionId}' was not found.");
+            }
+
+            return (session, false);
         }
 
         session ??= await sessionStore.GetLatestAsync(workspace, cancellationToken).ConfigureAwait(false);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!string.IsNullOrWhiteSpace(sessionId))
{
session = await sessionStore.GetByIdAsync(workspace, sessionId, cancellationToken).ConfigureAwait(false);
}
session ??= await sessionStore.GetLatestAsync(workspace, cancellationToken).ConfigureAwait(false);
if (session is not null)
{
return (session, false);
if (!string.IsNullOrWhiteSpace(sessionId))
{
session = await sessionStore.GetByIdAsync(workspace, sessionId, cancellationToken).ConfigureAwait(false);
if (session is null)
{
throw new InvalidOperationException($"Session '{sessionId}' was not found.");
}
return (session, false);
}
session ??= await sessionStore.GetLatestAsync(workspace, cancellationToken).ConfigureAwait(false);
if (session is not null)
{
return (session, false);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/SharpClaw.Code.WorkItems/Services/WorkItemService.cs` around lines 91 -
99, The current logic falls back to GetLatestAsync when a requested sessionId is
not found; change it so that when sessionId is provided (non-empty) you call
sessionStore.GetByIdAsync(workspace, sessionId, ...) and if that returns null
you fail immediately (throw an informative exception or return an explicit
error) instead of calling sessionStore.GetLatestAsync; only call GetLatestAsync
when sessionId is null/whitespace. Update the code around the session variable
and the calls to sessionStore.GetByIdAsync and GetLatestAsync in the method (the
block handling sessionId/session) to implement this behavior and surface a clear
error when the explicit session is missing.

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.

2 participants