diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..7ca383a
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,35 @@
+name: CI
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ branches: [main]
+
+permissions:
+ contents: read
+
+jobs:
+ build-and-test:
+ strategy:
+ matrix:
+ os: [ubuntu-latest, windows-latest, macos-latest]
+ runs-on: ${{ matrix.os }}
+ steps:
+ - uses: actions/checkout@v4
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: '10.0.x'
+ - name: Restore
+ run: dotnet restore SharpClawCode.sln
+ - name: Build
+ run: dotnet build SharpClawCode.sln --no-restore --configuration Release
+ - name: Test
+ run: dotnet test SharpClawCode.sln --no-build --configuration Release --collect:"XPlat Code Coverage" --results-directory ./coverage
+ - name: Upload coverage
+ if: matrix.os == 'ubuntu-latest'
+ uses: actions/upload-artifact@v4
+ with:
+ name: coverage-report
+ path: ./coverage/**/coverage.cobertura.xml
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..d83e7f3
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,29 @@
+name: Release
+
+on:
+ push:
+ tags: ['v*']
+
+permissions:
+ contents: read
+ packages: write
+
+jobs:
+ publish:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: '10.0.x'
+ - name: Restore
+ run: dotnet restore SharpClawCode.sln
+ - name: Build
+ run: dotnet build SharpClawCode.sln --no-restore --configuration Release
+ - name: Test
+ run: dotnet test SharpClawCode.sln --no-build --configuration Release
+ - name: Pack
+ run: dotnet pack SharpClawCode.sln --no-build --configuration Release --output ./nupkg
+ - name: Push to NuGet
+ run: dotnet nuget push ./nupkg/*.nupkg --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate
diff --git a/Directory.Build.props b/Directory.Build.props
index 8883f05..34f6fc1 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -10,5 +10,12 @@
true
false
1591;$(NoWarn)
+ clawdotnet
+ clawdotnet
+ MIT
+ https://github.com/clawdotnet/SharpClawCode
+ https://github.com/clawdotnet/SharpClawCode
+ git
+ Copyright (c) 2025 clawdotnet
diff --git a/README.md b/README.md
index 209f549..4a74792 100644
--- a/README.md
+++ b/README.md
@@ -7,9 +7,9 @@
SharpClaw Code is a C# and .NET-native coding agent runtime for teams building AI developer tools, agentic CLIs, and MCP-enabled workflows.
-It combines durable sessions, permission-aware tool execution, provider abstraction, structured telemetry, and an automation-friendly command-line surface in a codebase designed for production-quality .NET systems, not toy demos.
+It combines durable sessions, permission-aware tool execution, provider abstraction, structured telemetry, and an automation-friendly command-line surface in a runtime shaped for real .NET systems: explicit, testable, and operationally legible.
-## What SharpClaw Code Is
+## What It Is
SharpClaw Code is an open-source runtime for building and operating coding-agent experiences in the .NET ecosystem.
@@ -27,7 +27,7 @@ It is designed for:
- **Durable runtime model**: sessions, checkpoints, append-only event logs, export/import flows, and recovery-aware orchestration
- **Safety by default**: permission modes, approval gates, workspace-boundary enforcement, and mediated tool execution
- **Extensible surface**: providers, MCP servers, plugins, skills, ACP hosting, and runtime commands integrate through explicit seams
-- **Good fit for automation**: JSON-friendly command output, stable operational commands, and a clean CLI-first workflow
+- **Automation-ready interface**: stable JSON output, operational commands, and a clean CLI-first workflow that works for both humans and scripts
## Quick Start
@@ -71,6 +71,23 @@ dotnet run --project src/SharpClaw.Code.Cli -- --output-format json doctor
Built-in REPL slash commands include `/help`, `/status`, `/doctor`, `/session`, `/commands`, `/mode`, `/editor`, `/export`, `/undo`, `/redo`, and `/version`. Use `/help` to see the active command set, including discovered workspace custom commands.
+Parity-oriented commands now include:
+
+- `models` / `/models`
+- `usage` / `/usage`
+- `cost` / `/cost`
+- `stats` / `/stats`
+- `connect` / `/connect`
+- `hooks` / `/hooks`
+- `skills` / `/skills`
+- `agents` / `/agents`
+- `todo` / `/todo`
+- `share` / `/share`
+- `unshare` / `/unshare`
+- `compact` / `/compact`
+- `serve` / `/serve`
+- `/sessions` as a friendlier alias over `/session list`
+
Primary workflow modes:
- `build`: normal coding-agent execution
@@ -89,6 +106,10 @@ Primary workflow modes:
| Structured telemetry | Emit runtime events and usage signals that support diagnostics, replay, and automation |
| JSON-friendly CLI | Use the same runtime through human-readable terminal flows or machine-readable command output |
| Spec workflow mode | Turn prompts into structured requirements, technical design, and task documents for feature proposals |
+| Embedded local server | Expose prompt, session, status, doctor, and share endpoints for editor or automation clients |
+| Config + agent catalog | Layer user/workspace JSONC config with typed agent defaults, tool allowlists, and runtime hooks |
+| Session sharing | Create self-hosted share links and durable sanitized share snapshots under `.sharpclaw/` |
+| Diagnostics context | Surface configured diagnostics sources into prompt context, status, and machine-readable output |
## Good Fit For
@@ -96,7 +117,7 @@ Primary workflow modes:
- running a **local or hosted coding-agent CLI**
- creating a **.NET MCP client/runtime**
- adding **session persistence and auditability** to agent workflows
-- experimenting with **Agent Framework-backed orchestration** without pushing core runtime logic into the framework layer
+- building on **Microsoft Agent Framework** without pushing your application core into framework-specific code
## Solution Layout
@@ -145,8 +166,9 @@ dotnet test SharpClawCode.sln --filter "FullyQualifiedName~ParityScenarioTests"
| `--output-format text\|json` | Human-readable or structured output |
| `--primary-mode ` | Workflow bias for prompts: `build`, `plan`, or `spec` |
| `--session ` | Reuse a specific SharpClaw session id for prompt execution |
+| `--agent ` | Select the active agent for prompt execution |
-Subcommands include `prompt`, `repl`, `doctor`, `status`, `session`, `commands`, `mcp`, `plugins`, `acp`, `bridge`, and `version`.
+Subcommands include `prompt`, `repl`, `doctor`, `status`, `session`, `models`, `usage`, `cost`, `stats`, `connect`, `hooks`, `skills`, `agents`, `todo`, `share`, `unshare`, `compact`, `serve`, `commands`, `mcp`, `plugins`, `acp`, `bridge`, and `version`.
## Documentation Map
@@ -167,7 +189,17 @@ Subcommands include `prompt`, `repl`, `doctor`, `status`, `session`, `commands`,
## Configuration
-SharpClaw Code uses the standard .NET configuration stack (`appsettings.json`, environment variables, CLI args). Key configuration sections:
+SharpClaw Code uses both the standard .NET configuration stack (`appsettings.json`, environment variables, CLI args) and layered SharpClaw JSONC config files:
+
+- user config: `~/.config/sharpclaw/config.jsonc` on Unix-like systems
+- Windows user config: `%AppData%\\SharpClaw\\config.jsonc`
+- workspace config: `/sharpclaw.jsonc`
+
+Precedence is:
+
+`CLI args > workspace sharpclaw.jsonc > user config.jsonc > appsettings/environment defaults`
+
+Key runtime configuration sections:
| Section | Purpose |
|---|---|
@@ -177,13 +209,25 @@ SharpClaw Code uses the standard .NET configuration stack (`appsettings.json`, e
| `SharpClaw:Web` | Web search provider name, endpoint template, user agent |
| `SharpClaw:Telemetry` | Runtime event ring buffer capacity |
+Key `sharpclaw.jsonc` capabilities:
+
+- `shareMode`: `manual`, `auto`, or `disabled`
+- `server`: host, port, and optional public base URL for share links
+- `defaultAgentId`: default prompt agent
+- `agents`: typed agent catalog entries with model defaults, tool allowlists, and instruction appendices
+- `lspServers`: configured diagnostics sources
+- `hooks`: lifecycle hooks for turn/tool/share/server events
+- `connectLinks`: browser entry points for provider or external auth flows
+
All options are validated at startup via `IValidateOptions` implementations.
## Current Scope
- The shared tooling layer is permission-aware across the runtime.
-- The current Agent Framework bridge is focused on provider-backed runs rather than a full tool-calling loop inside the framework path.
-- Operational commands support stable JSON output via `--output-format json`, which makes them useful in scripts and automation.
+- The current runtime includes multi-turn provider-backed tool execution, session-backed prompt replay, and durable conversation history.
+- Agent-driven tool calls flow through the same approval and allowlist enforcement path used by direct tool execution, including caller-aware interactive approval behavior.
+- Operational commands support stable JSON output via `--output-format json`, which makes them suitable for scripts, editors, and automation.
+- The embedded server exposes local JSON and SSE endpoints for prompts, sessions, sharing, status, and doctor flows.
## Contributing
diff --git a/docs/MONETIZATION.md b/docs/MONETIZATION.md
new file mode 100644
index 0000000..ba442e6
--- /dev/null
+++ b/docs/MONETIZATION.md
@@ -0,0 +1,424 @@
+# SharpClaw Code — Monetization Strategy
+
+## Document Info
+
+| Field | Value |
+|-------|-------|
+| Status | Draft |
+| Author | telli |
+| Date | 2026-04-10 |
+| Model | Open Core |
+
+---
+
+## 1. Strategy Summary
+
+SharpClaw Code follows an **open-core model** with phased revenue introduction:
+
+| Phase | Focus | Revenue | Timeline |
+|-------|-------|---------|----------|
+| **Phase 1** | Adoption & category ownership | $0 (investment phase) | Months 0-12 |
+| **Phase 2** | Enterprise & ISV monetization | Enterprise licenses + support | Months 6-18 |
+| **Phase 3** | Consumer & ecosystem monetization | Pro tier + marketplace | Months 12-24 |
+
+The core runtime remains MIT-licensed permanently. Revenue comes from proprietary extensions, managed services, and ecosystem fees that serve segments willing to pay for capabilities the open-source core intentionally doesn't include.
+
+---
+
+## 2. What Stays Free (Forever)
+
+The open-source core includes everything needed to build and run a coding agent:
+
+| Capability | Package |
+|------------|---------|
+| All protocol contracts and DTOs | `SharpClaw.Code.Protocol` |
+| Full runtime orchestration | `SharpClaw.Code.Runtime` |
+| Anthropic and OpenAI-compatible providers | `SharpClaw.Code.Providers.*` |
+| Built-in tools (read, write, edit, grep, glob, bash) | `SharpClaw.Code.Tools` |
+| Permission policy engine and approval gates | `SharpClaw.Code.Permissions` |
+| Session persistence (file-backed) | `SharpClaw.Code.Sessions` |
+| MCP client integration | `SharpClaw.Code.Mcp` |
+| Plugin system | `SharpClaw.Code.Plugins` |
+| Structured telemetry and event publishing | `SharpClaw.Code.Telemetry` |
+| CLI with REPL, slash commands, JSON output | `SharpClaw.Code.Cli` |
+| Spec workflow mode | `SharpClaw.Code.Runtime` |
+| All documentation and examples | `docs/` |
+
+**Principle:** A single developer or small team should never hit a paywall for core agent functionality. The free tier must be genuinely useful, not a crippled demo.
+
+---
+
+## 3. Phase 1 — Investment Phase (Months 0-12)
+
+### Revenue: $0
+
+### Goal: Category Ownership
+
+All effort goes into adoption, community, and establishing SharpClaw as the default .NET agent runtime.
+
+### Investment Activities
+
+| Activity | Purpose | Cost |
+|----------|---------|------|
+| NuGet package publishing | Frictionless adoption | CI/CD time |
+| Documentation + tutorials | Reduce time-to-value | Author time |
+| Conference talks / blog posts | .NET community visibility | Travel + time |
+| Discord community | Developer engagement | Moderation time |
+| GitHub Sponsors | Signal legitimacy, collect early support | $0 cost |
+| "Built with SharpClaw" showcase | Social proof | Curation time |
+
+### Early Revenue Signals (Not Revenue)
+
+- **GitHub Sponsors:** Accept individual and corporate sponsorships. Not a business model, but validates willingness to pay and builds a mailing list of engaged users.
+- **Consulting:** Offer paid architecture reviews for teams adopting SharpClaw. This generates revenue, but more importantly surfaces enterprise requirements for Phase 2.
+- **Training workshops:** Paid half-day workshops ("Building Production Agents with SharpClaw") at .NET conferences. Revenue is modest but builds authority.
+
+**Target:** $5K-$15K in consulting/workshop revenue. Primary purpose is learning, not profit.
+
+---
+
+## 4. Phase 2 — Enterprise & ISV Monetization (Months 6-18)
+
+### Revenue Target: $10K-$50K MRR by month 18
+
+### 4.1 Tier Structure
+
+#### Free (Open Source)
+
+Everything in the MIT-licensed core. No limits, no telemetry, no registration required.
+
+#### Team — $500/month (per organization)
+
+For teams embedding SharpClaw in internal tools or products with <50 users.
+
+| Feature | Description |
+|---------|-------------|
+| **Priority support** | 48-hour response SLA via dedicated channel |
+| **Office hours** | Monthly group call with maintainers |
+| **Early access** | Pre-release builds and roadmap input |
+| **Logo rights** | "Powered by SharpClaw" badge for marketing |
+
+**Why teams pay:** Support SLA and early access. These teams have adopted the open-source runtime and need confidence they won't get stuck.
+
+#### Enterprise — $2,500/month (per organization)
+
+For organizations running SharpClaw in production with compliance, scale, or multi-team requirements.
+
+| Feature | Description |
+|---------|-------------|
+| Everything in Team | — |
+| **Multi-tenant session store** | Pluggable session backends (Azure CosmosDB, SQL Server, PostgreSQL) with tenant isolation |
+| **Enterprise SSO integration** | Microsoft Entra ID, SAML, and OIDC for approval workflows — tie permission approvals to corporate identity |
+| **Audit log export** | Compliance-ready export of all tool executions, approvals, and provider calls |
+| **Advanced telemetry sinks** | Azure Monitor, OpenTelemetry, Datadog, and Splunk exporters |
+| **Session encryption at rest** | AES-256 encryption for session snapshots and event logs |
+| **Role-based access control** | Define who can approve dangerous operations, manage MCP servers, install plugins |
+| **Dedicated support** | 8-hour response SLA, named account engineer |
+| **Custom SLA** | Uptime and response guarantees |
+
+**Why enterprises pay:** Compliance (audit logs, encryption, SSO), scale (multi-tenant), and support guarantees. These are table-stakes for enterprise procurement.
+
+#### ISV / OEM — Custom pricing
+
+For companies embedding SharpClaw as the runtime inside a commercial product.
+
+| Feature | Description |
+|---------|-------------|
+| Everything in Enterprise | — |
+| **White-label rights** | Remove SharpClaw branding from end-user surfaces |
+| **Embedded runtime SDK** | Optimized for hosting inside ASP.NET Core, worker services, or desktop apps |
+| **Usage metering API** | Per-tenant token and tool usage tracking for ISV billing |
+| **Custom provider integration** | Assistance building proprietary model provider adapters |
+| **Roadmap influence** | Direct input on feature prioritization |
+| **Indemnification** | IP indemnity for the proprietary components |
+
+**Pricing model:** Base platform fee ($5K-$15K/month) + per-seat or per-usage component negotiated per deal.
+
+### 4.2 What's Proprietary vs. Open Source
+
+The boundary is drawn at a clear principle: **the open-source core runs a single-user, single-workspace agent with full functionality. Proprietary features serve multi-user, multi-tenant, compliance, and enterprise-scale needs.**
+
+| Capability | Open Source | Proprietary |
+|------------|:-----------:|:-----------:|
+| Runtime orchestration | x | |
+| File-backed session storage | x | |
+| SQL/cloud session backends | | x |
+| Permission policy engine | x | |
+| SSO-backed approval workflows | | x |
+| Structured telemetry (ring buffer) | x | |
+| OpenTelemetry/Datadog/Splunk sinks | | x |
+| Event log (NDJSON) | x | |
+| Compliance audit export | | x |
+| Session encryption at rest | | x |
+| Single-workspace MCP | x | |
+| Multi-tenant MCP orchestration | | x |
+| Plugin system | x | |
+| Plugin marketplace hosting | | x |
+| CLI + REPL | x | |
+| Admin REST API | | x |
+| Role-based access control | | x |
+| Usage metering | | x |
+
+### 4.3 Packaging the Proprietary Extensions
+
+Proprietary features ship as separate NuGet packages under a commercial license:
+
+```
+SharpClaw.Code.Enterprise.Sessions.CosmosDb
+SharpClaw.Code.Enterprise.Sessions.SqlServer
+SharpClaw.Code.Enterprise.Telemetry.AzureMonitor
+SharpClaw.Code.Enterprise.Telemetry.OpenTelemetry
+SharpClaw.Code.Enterprise.Telemetry.Datadog
+SharpClaw.Code.Enterprise.Auth.EntraId
+SharpClaw.Code.Enterprise.Auth.Oidc
+SharpClaw.Code.Enterprise.Audit
+SharpClaw.Code.Enterprise.Encryption
+SharpClaw.Code.Enterprise.Admin
+```
+
+These packages depend on the open-source core and plug in via the existing DI extension pattern (`services.AddSharpClawEnterpriseSessions(configuration)`). No fork, no separate build — enterprise customers add packages and configure.
+
+**License enforcement:** Package-level license key validation at startup. No runtime phone-home; offline validation with periodic renewal.
+
+---
+
+## 5. Phase 3 — Consumer & Ecosystem Monetization (Months 12-24)
+
+### Revenue Target: $50K-$150K MRR by month 24
+
+### 5.1 SharpClaw Pro — $20/month (individual)
+
+A personal tier for developers using SharpClaw as their daily coding agent.
+
+| Feature | Description |
+|---------|-------------|
+| **IDE extensions** | VS Code and Rider extensions with rich integration |
+| **Cross-session memory** | Persistent project knowledge that survives session boundaries |
+| **Workspace indexing** | Semantic code search, symbol navigation, dependency graph |
+| **Priority model routing** | Automatic provider selection optimized for cost/quality/speed |
+| **Session sync** | Sync sessions across machines via cloud storage |
+| **Custom slash commands** | Visual editor for creating and sharing custom commands |
+| **Pro badge** | Community recognition in Discord and GitHub |
+
+**Why individuals pay:** The free CLI is fully functional. Pro adds convenience and power-user features that save time daily. The $20 price point is impulse-buy territory for professional developers.
+
+### 5.2 Plugin Marketplace
+
+A curated registry where third parties publish and optionally sell SharpClaw plugins.
+
+| Revenue Stream | Model |
+|----------------|-------|
+| **Free plugins** | Listed for free; drives ecosystem growth |
+| **Paid plugins** | 70/30 revenue split (developer/SharpClaw) |
+| **Verified publisher** | $99/year badge for trust signal |
+| **Featured placement** | $500/month for homepage visibility |
+
+**Marketplace economics:** The marketplace is a flywheel — more plugins attract more users, which attract more plugin developers. The 30% take rate is standard (Apple, Stripe, etc.) and funds curation, security review, and infrastructure.
+
+### 5.3 Managed Cloud (Optional)
+
+A hosted SharpClaw runtime for teams that don't want to self-host.
+
+| Tier | Price | Includes |
+|------|-------|----------|
+| **Starter** | $99/month | 5 seats, 100K tokens/month, file-backed sessions |
+| **Growth** | $499/month | 25 seats, 1M tokens/month, SQL-backed sessions, SSO |
+| **Scale** | $1,999/month | Unlimited seats, 10M tokens/month, full enterprise features |
+
+**Build-or-buy decision:** The managed cloud is the highest-effort revenue stream. It requires infrastructure, ops, and support investment. Consider partnering with a hosting provider (Azure, Railway) rather than building from scratch. Evaluate at month 12 based on demand signals.
+
+---
+
+## 6. Pricing Philosophy
+
+### Principles
+
+1. **Free must be genuinely useful.** A developer should be able to build and ship a real product on the free tier. If the free tier feels crippled, adoption dies.
+
+2. **Paid tiers solve real problems the free tier can't.** Enterprise features (compliance, multi-tenancy, SSO) are genuinely different requirements, not artificial limitations.
+
+3. **Price on value, not cost.** The Enterprise tier costs $2,500/month but saves an enterprise team months of building session backends, audit infrastructure, and SSO integration.
+
+4. **No per-seat pricing on the core runtime.** Per-seat pricing on an open-source runtime feels extractive. Charge per organization, not per developer.
+
+5. **Annual discounts.** 20% discount for annual commitment (Team: $4,800/year, Enterprise: $24,000/year). Improves cash flow predictability.
+
+### Competitive Pricing Context
+
+| Competitor | Pricing | SharpClaw Comparison |
+|------------|---------|---------------------|
+| GitHub Copilot Business | $19/user/month | SharpClaw Pro at $20/month is comparable but includes the full runtime |
+| Cursor Business | $40/user/month | SharpClaw is cheaper and open-source at the core |
+| Semantic Kernel | Free (framework only) | SharpClaw builds on Agent Framework with production runtime; complementary, not competing |
+| LangChain / LangSmith | Free core + $39/user for platform | Similar open-core model; SharpClaw's .NET/Agent Framework foundation is differentiated |
+
+### Microsoft Partnership Value
+
+The monetization strategy is designed to be **friendly to Microsoft's ecosystem interests**:
+
+- **Free tier grows Agent Framework adoption.** Every SharpClaw user is a `Microsoft.Agents.AI` NuGet consumer. Microsoft benefits from SharpClaw's success.
+- **Enterprise tier drives Azure alignment.** Multi-tenant session backends (CosmosDB, SQL Server), Azure Monitor telemetry sinks, and Entra ID SSO integration all drive Azure consumption.
+- **No competition with Microsoft's own monetization.** SharpClaw monetizes the runtime/operational layer. Microsoft monetizes Azure infrastructure and AI model APIs. The incentives are aligned.
+- **Co-marketing reduces CAC.** If Microsoft features SharpClaw in Agent Framework docs or conference talks, customer acquisition cost for all tiers drops significantly.
+
+---
+
+## 7. Revenue Projections
+
+### Conservative Scenario
+
+| Month | Phase | Free Users | Team Orgs | Enterprise Orgs | Pro Users | MRR |
+|-------|-------|-----------|-----------|-----------------|-----------|-----|
+| 6 | 1 | 500 | 0 | 0 | 0 | $0 |
+| 12 | 1-2 | 2,000 | 5 | 1 | 0 | $5,000 |
+| 18 | 2 | 5,000 | 15 | 5 | 100 | $22,000 |
+| 24 | 2-3 | 10,000 | 25 | 10 | 500 | $47,500 |
+
+### Optimistic Scenario
+
+| Month | Phase | Free Users | Team Orgs | Enterprise Orgs | Pro Users | MRR |
+|-------|-------|-----------|-----------|-----------------|-----------|-----|
+| 6 | 1 | 1,500 | 0 | 0 | 0 | $0 |
+| 12 | 1-2 | 5,000 | 10 | 3 | 0 | $12,500 |
+| 18 | 2 | 15,000 | 30 | 10 | 500 | $50,000 |
+| 24 | 2-3 | 30,000 | 50 | 20 | 2,000 | $115,000 |
+
+### Revenue Mix at Month 24 (Conservative)
+
+```
+Team: $12,500 (26%)
+Enterprise: $25,000 (53%)
+Pro: $10,000 (21%)
+─────────────────────────
+Total MRR: $47,500
+ARR: $570,000
+```
+
+---
+
+## 8. Go-to-Market Channels
+
+### Phase 1 (Adoption)
+
+| Channel | Action | Expected Impact |
+|---------|--------|-----------------|
+| **NuGet** | Publish packages with clear README and getting-started | Primary discovery channel for .NET developers |
+| **GitHub** | Optimize repo (README, badges, examples, issue templates) | Social proof and contribution funnel |
+| **Microsoft partnership** | Engage Agent Framework team; offer SharpClaw as reference implementation | Ecosystem credibility, docs listing, co-marketing |
+| **.NET blogs** | "Building a production coding agent on Microsoft Agent Framework" | Direct reach to target persona, friendly to Microsoft |
+| **Conference talks** | .NET Conf, NDC, Build — joint sessions with Agent Framework team if possible | Authority positioning |
+| **Discord** | Developer community with channels for help, showcase, RFC | Engagement and retention |
+| **Twitter/X + Bluesky** | Regular updates, demos, Agent Framework integration highlights | Awareness |
+| **YouTube** | "Build an agent in 15 minutes with Agent Framework + SharpClaw" | Long-tail discovery |
+
+### Phase 2 (Enterprise)
+
+| Channel | Action |
+|---------|--------|
+| **Direct outreach** | Identify companies using Semantic Kernel for agent work; offer migration assistance |
+| **Case studies** | Publish 2-3 production deployment stories |
+| **Partner program** | .NET consultancies who recommend SharpClaw to enterprise clients |
+| **Enterprise landing page** | Separate page with compliance, security, and ROI messaging |
+
+### Phase 3 (Consumer)
+
+| Channel | Action |
+|---------|--------|
+| **VS Code Marketplace** | IDE extension as primary distribution |
+| **Product Hunt launch** | Consumer awareness burst |
+| **Developer influencers** | Sponsored reviews and walkthroughs |
+| **Plugin marketplace** | Self-reinforcing ecosystem growth |
+
+---
+
+## 9. Cost Structure
+
+### Phase 1 (Months 0-12)
+
+| Item | Monthly Cost | Notes |
+|------|-------------|-------|
+| GitHub Actions CI/CD | $0-50 | Free tier covers most OSS needs |
+| NuGet hosting | $0 | Free for public packages |
+| Domain + hosting (docs site) | $20 | Static site on Cloudflare/Vercel |
+| Discord (Nitro for branding) | $10 | Optional |
+| Conference travel (amortized) | $500 | 2-3 conferences per year |
+| **Total** | **~$580/month** | |
+
+### Phase 2 (Months 6-18)
+
+| Item | Monthly Cost | Notes |
+|------|-------------|-------|
+| Phase 1 costs | $580 | Continuing |
+| License server infrastructure | $100 | Simple key validation service |
+| Enterprise package CI/CD | $200 | Private build pipelines |
+| Support tooling (Intercom/Linear) | $200 | Customer communication |
+| Part-time support engineer | $3,000 | Contractor or part-time hire |
+| **Total** | **~$4,080/month** | |
+
+### Breakeven
+
+At conservative projections, MRR exceeds costs by **month 14-15** (Enterprise tier covers the burn).
+
+---
+
+## 10. Key Risks to Monetization
+
+| Risk | Mitigation |
+|------|------------|
+| Enterprise features built by community (defeating proprietary value) | Keep proprietary features integration-heavy (Azure backends, Entra ID, audit export) — hard to replicate without infrastructure |
+| Microsoft builds competing production layer into Agent Framework | Stay close to the team, contribute upstream, position as community complement. If they build it, pivot to hosting/tooling layer. The incentive alignment (SharpClaw drives Agent Framework adoption) makes this unlikely |
+| .NET agent market doesn't materialize | Phase 1 positioning as production runtime layer doesn't require a large agent-specific market |
+| Free tier is too good, nobody upgrades | Monitor conversion at Phase 2 launch; adjust boundary if needed, but err on the side of generous free tier |
+| Price resistance at $2,500/month for Enterprise | Offer quarterly billing and proof-of-value pilots (30-day trial with migration assistance) |
+| Microsoft relationship doesn't materialize | Product stands alone regardless; Microsoft alignment is accelerant, not dependency |
+
+---
+
+## 11. Microsoft Partnership Strategy
+
+SharpClaw's success is accelerated by — but not dependent on — a strong relationship with the Microsoft Agent Framework team. The strategy is to make SharpClaw so useful to Microsoft's ecosystem goals that partnership is a natural outcome.
+
+### Why Microsoft Benefits
+
+| Microsoft Goal | How SharpClaw Helps |
+|---------------|---------------------|
+| Agent Framework adoption | Every SharpClaw user downloads `Microsoft.Agents.AI` from NuGet |
+| Azure consumption | Enterprise tier drives CosmosDB, Azure Monitor, Entra ID, and Azure OpenAI usage |
+| .NET ecosystem competitiveness | SharpClaw demonstrates .NET is a first-class platform for AI agents (vs. Python/TypeScript narrative) |
+| Agent Framework credibility | A production-grade, open-source project validates the framework for real workloads |
+| Community engagement | SharpClaw's contributor community feeds Agent Framework issue reports, feature requests, and real-world usage patterns |
+
+### Partnership Playbook
+
+| Timeline | Action | Ask |
+|----------|--------|-----|
+| **Month 1** | File well-crafted Agent Framework issues from real SharpClaw usage | Build visibility with the team |
+| **Month 2** | Publish blog post: "Building a Production Agent Runtime on Microsoft Agent Framework" | Ask to be retweeted / shared by .NET team accounts |
+| **Month 3** | Submit PR to Agent Framework docs: "Community Projects" section featuring SharpClaw | Get listed in official docs |
+| **Month 4** | Propose .NET Conf community talk: "From Agent Framework to Production" | Conference visibility |
+| **Month 6** | Request meeting with Agent Framework PM to share usage data and feature requests | Establish direct relationship |
+| **Month 8** | Propose joint blog post or case study | Co-marketing |
+| **Month 12** | Explore Microsoft for Startups or ISV partnership program | Formal partnership, potential Azure credits |
+
+### What Not to Do
+
+- Don't position against Semantic Kernel or AutoGen — they serve different use cases
+- Don't ask for special treatment or early access before proving value
+- Don't depend on Microsoft for distribution — NuGet and GitHub are the primary channels
+- Don't build on unstable Agent Framework APIs — pin to released versions
+- Don't gate features behind Microsoft-specific infrastructure — Azure backends are one option, not the only option
+
+---
+
+## 12. Decision Log
+
+| Decision | Rationale | Date |
+|----------|-----------|------|
+| MIT license for core | Already shipped; builds trust; maximizes adoption | 2026-04-10 |
+| Open-core model | Best fit for MIT base + enterprise upsell | 2026-04-10 |
+| No per-seat pricing on core | Feels extractive for OSS runtime; charge per org instead | 2026-04-10 |
+| Phase 1 = $0 revenue | Category ownership > early revenue; consulting covers costs | 2026-04-10 |
+| Enterprise features as separate NuGet packages | Clean separation; no fork; existing DI patterns | 2026-04-10 |
+| Managed cloud as Phase 3 optional | High effort; evaluate based on demand signals at month 12 | 2026-04-10 |
+| Position as Agent Framework complement, not competitor | Microsoft partnership accelerates all phases; aligned incentives (SharpClaw drives AF adoption, enterprise tier drives Azure consumption) | 2026-04-10 |
+| Azure-first enterprise backends (CosmosDB, Entra ID, Azure Monitor) | Aligns with Microsoft ecosystem; enterprise .NET teams are already on Azure | 2026-04-10 |
diff --git a/docs/PRD.md b/docs/PRD.md
new file mode 100644
index 0000000..875e315
--- /dev/null
+++ b/docs/PRD.md
@@ -0,0 +1,323 @@
+# SharpClaw Code — Product Requirements Document
+
+## Document Info
+
+| Field | Value |
+|-------|-------|
+| Status | Draft |
+| Author | telli |
+| Date | 2026-04-10 |
+| Version | 1.0 |
+
+---
+
+## 1. Vision
+
+SharpClaw Code is the production-grade, open-source .NET runtime for building and operating AI coding agents. Built on Microsoft Agent Framework, it adds the durability, permission, and operational layers that turn agent prototypes into shippable products.
+
+**One-liner:** The production runtime that makes Microsoft Agent Framework real for coding agent workloads.
+
+---
+
+## 2. Strategic Positioning
+
+### Phase 1 — The Production .NET Agent Runtime (Months 0-12)
+
+**Beachhead:** .NET developers adopting Microsoft Agent Framework who need production-grade runtime capabilities beyond what the framework provides out of the box.
+
+**Positioning:** "SharpClaw Code is the production runtime built on Microsoft Agent Framework. The framework gives you agent abstractions and provider integration — SharpClaw adds durable sessions, permission enforcement, MCP lifecycle management, structured telemetry, and an operational CLI surface. Together, they're what you need to ship a real coding agent."
+
+**Relationship to the Microsoft ecosystem:**
+
+SharpClaw Code is a **complement to Microsoft Agent Framework**, not a competitor. The relationship is analogous to how ASP.NET Core provides the web framework and tools like MassTransit or Wolverine add production messaging patterns on top.
+
+| Layer | Microsoft Provides | SharpClaw Adds |
+|-------|-------------------|----------------|
+| Agent abstractions | Agent kernel, activity protocol | Coding-agent-specific orchestration (turns, context assembly) |
+| Provider integration | Multi-provider interfaces | Provider resilience, auth preflight, streaming adapters |
+| Tool execution | Tool invocation primitives | Permission policy engine, approval gates, workspace boundaries |
+| Session state | In-memory by default | Durable snapshots, append-only event logs, checkpoints, undo/redo |
+| MCP | — | First-class registration, supervision, health checks, lifecycle state |
+| Plugin system | — | Manifest-based discovery, trust levels, out-of-process execution |
+| Telemetry | Standard .NET logging | Structured event-first ring buffer, JSON export, usage tracking |
+| CLI surface | — | Full REPL, slash commands, spec mode, JSON output |
+| Testing | — | Deterministic mock provider, parity harness, named scenarios |
+
+**Why Microsoft should care:** SharpClaw is one of the most complete open-source projects built on Agent Framework. It demonstrates that the framework is production-viable for complex workloads, drives NuGet downloads of `Microsoft.Agents.AI`, and provides a reference architecture that other teams can learn from.
+
+**Co-marketing opportunities:**
+- Featured in Microsoft Agent Framework documentation as a reference implementation
+- Joint blog posts: "Building a Production Coding Agent with Microsoft Agent Framework and SharpClaw"
+- .NET Conf / Build talk: "From Agent Framework to Production — Lessons from SharpClaw Code"
+- Listed in the Agent Framework ecosystem / community projects page
+- Collaboration on Agent Framework feature requests informed by real-world SharpClaw usage
+
+### Phase 2 — Build Your Own Coding Agent (Months 6-18)
+
+**Expand to:** Startups and ISVs building coding agent products who need an embeddable runtime on the Microsoft stack.
+
+**Positioning:** "Ship your AI coding assistant in weeks, not months. SharpClaw Code brings Microsoft Agent Framework to production — you provide the experience."
+
+**New capabilities required:**
+- Embeddable runtime SDK (no CLI dependency)
+- Multi-tenant session isolation
+- Custom tool SDK with packaging and distribution
+- White-label provider configuration
+- Webhooks and event streaming for external integrations
+
+**Microsoft alignment:** Position SharpClaw as the go-to path for ISVs adopting Agent Framework. Enterprise customers already on Azure and .NET get a runtime that fits their existing stack, identity, and compliance infrastructure.
+
+### Phase 3 — The Open-Source Coding Agent for .NET Teams (Months 12-24)
+
+**Expand to:** Individual .NET developers who want a local coding agent built on familiar Microsoft technologies.
+
+**Positioning:** "A coding agent built on the Microsoft stack, for the Microsoft stack. Open-source, runs locally, respects your workspace."
+
+**New capabilities required:**
+- Polished interactive CLI experience (auto-complete, rich rendering)
+- IDE integrations (VS Code extension, Rider plugin)
+- Local model support (Ollama, llama.cpp via OpenAI-compatible provider)
+- Workspace indexing and semantic code search
+- Conversation memory across sessions
+
+**Microsoft alignment:** The consumer agent demonstrates that Agent Framework powers real end-user experiences, not just enterprise backends. Opportunity for joint promotion as "the open-source coding agent for the .NET ecosystem."
+
+---
+
+## 3. Target Users
+
+### Phase 1 Users
+
+| Persona | Description | Pain Point |
+|---------|-------------|------------|
+| **Platform Engineer** | Building internal AI developer tools at a mid-to-large .NET shop | Agent Framework provides abstractions but not sessions, permissions, or audit — building that from scratch |
+| **.NET Tech Lead** | Evaluating how to take Agent Framework to production | Needs durability, testing, observability — production characteristics beyond the framework primitives |
+| **DevTools Startup Founder** | Building an AI-powered code review / generation tool on the Microsoft stack | Needs a runtime on top of Agent Framework so they can focus on their product, not infrastructure |
+| **Microsoft Agent Framework Team** | Growing the Agent Framework ecosystem | Needs visible, high-quality projects that demonstrate the framework's production viability |
+
+### Phase 2 Users
+
+| Persona | Description | Pain Point |
+|---------|-------------|------------|
+| **ISV Product Manager** | Shipping a coding agent as part of a larger product | Needs embeddable runtime with multi-tenancy, not a CLI tool |
+| **Enterprise Architect** | Standardizing agent infrastructure across teams | Needs governance, audit trails, and permission enforcement at scale |
+
+### Phase 3 Users
+
+| Persona | Description | Pain Point |
+|---------|-------------|------------|
+| **.NET Developer** | Wants a local coding agent that works well with C# / .NET projects | Existing agents (Claude Code, Cursor) are JS/Python-centric; poor .NET experience |
+| **Open-Source Contributor** | Wants to build and extend a coding agent they can understand and modify | Closed-source agents can't be customized; Python agents aren't their stack |
+
+---
+
+## 4. Phase 1 Requirements
+
+### 4.1 Core Runtime (Exists)
+
+The following are implemented and tested:
+
+- [x] Durable sessions with append-only event logs and JSON snapshots
+- [x] Permission policy engine with workspace boundary enforcement
+- [x] Anthropic and OpenAI-compatible provider abstraction
+- [x] MCP server registration, supervision, and lifecycle management
+- [x] Plugin discovery, manifest validation, and trust-based execution
+- [x] Structured telemetry with ring buffer and JSON export
+- [x] CLI with REPL, slash commands, and JSON output mode
+- [x] Checkpoint-based undo/redo with mutation tracking
+- [x] Spec workflow mode for structured requirements generation
+- [x] Cross-platform support with Windows-safe behavior
+
+### 4.2 Phase 1 Gaps (Must Build)
+
+#### 4.2.1 Tool-Calling Loop in Agent Framework Bridge
+
+**Priority:** P0
+**Why:** The current agent bridge streams provider responses but does not execute tools within the agent loop. This is the single biggest functional gap — without it, SharpClaw Code is a streaming wrapper, not a coding agent.
+
+**Requirements:**
+- Agent receives tool-use requests from the provider response
+- Agent dispatches tool calls through the existing IToolExecutor (inheriting permission checks)
+- Tool results are fed back to the provider for the next turn iteration
+- Multi-turn tool loops terminate on provider completion or configurable max iterations
+- Each tool call is recorded as a runtime event (ToolStartedEvent, ToolCompletedEvent)
+
+#### 4.2.2 Conversation History
+
+**Priority:** P0
+**Why:** Multi-turn conversations require prior context. Currently each prompt is stateless within the provider call.
+
+**Requirements:**
+- Session-scoped conversation history assembled from persisted events
+- Configurable context window management (truncation, summarization)
+- System prompt injection from workspace context (CLAUDE.md equivalent)
+- History survives session resume
+
+#### 4.2.3 NuGet Package Distribution
+
+**Priority:** P1
+**Why:** Adoption requires `dotnet add package`, not `git clone`.
+
+**Requirements:**
+- Publish core packages to NuGet.org:
+ - `SharpClaw.Code.Protocol` — contracts only, zero dependencies
+ - `SharpClaw.Code.Runtime` — full runtime with DI extensions
+ - `SharpClaw.Code.Providers.Anthropic` — Anthropic provider
+ - `SharpClaw.Code.Providers.OpenAi` — OpenAI-compatible provider
+ - `SharpClaw.Code.Tools` — built-in tools and tool SDK
+ - `SharpClaw.Code.Mcp` — MCP client integration
+- Stable API surface with semantic versioning
+- XML documentation included in packages
+
+#### 4.2.4 Documentation and Getting Started
+
+**Priority:** P1
+**Why:** Framework adoption lives or dies on docs.
+
+**Requirements:**
+- Getting started guide: "Build your first agent in 15 minutes"
+- Integration guide: "Using SharpClaw with Microsoft Agent Framework"
+- Architecture deep-dive for contributors
+- API reference generated from XML docs
+- Example projects:
+ - Minimal console agent
+ - Web API agent with session persistence
+ - MCP-enabled agent with custom tools
+
+#### 4.2.5 CI/CD Pipeline
+
+**Priority:** P1
+**Why:** No CI currently exists. Contributors need confidence their PRs don't break things.
+
+**Requirements:**
+- GitHub Actions workflow: build + test on push/PR
+- Matrix: ubuntu-latest, windows-latest, macos-latest
+- NuGet package publishing on release tags
+- Code coverage reporting
+
+#### 4.2.6 Provider Resilience
+
+**Priority:** P2
+**Why:** Production workloads need retry logic, rate limiting, and graceful degradation.
+
+**Requirements:**
+- Configurable retry with exponential backoff for transient HTTP failures
+- Rate limit detection and backoff (429 handling)
+- Request timeout configuration per provider
+- Circuit breaker pattern for repeated failures
+- Fallback provider chain (try Anthropic, fall back to OpenAI)
+
+#### 4.2.7 Observability
+
+**Priority:** P2
+**Why:** Production deployments need more than a ring buffer.
+
+**Requirements:**
+- OpenTelemetry activity/span integration for distributed tracing
+- Structured log correlation IDs across turn execution
+- Metrics: token usage, tool execution duration, provider latency
+- Optional NDJSON trace file sink for offline analysis
+
+### 4.3 Phase 1 Non-Goals
+
+- IDE integrations (Phase 3)
+- Multi-tenant session isolation (Phase 2)
+- Local model hosting (Phase 3)
+- Marketplace or plugin store (Phase 2)
+- Billing or usage metering (Phase 2)
+- GUI or web dashboard (Phase 2+)
+
+---
+
+## 5. Phase 2 Requirements (Summary)
+
+| Capability | Description |
+|------------|-------------|
+| Embeddable Runtime SDK | Host SharpClaw runtime in ASP.NET Core, worker services, or custom hosts without the CLI |
+| Multi-Tenant Sessions | Workspace and session isolation per tenant with configurable storage backends |
+| Custom Tool SDK | Package, version, and distribute custom tools as NuGet packages with manifest metadata |
+| Event Streaming | Webhooks or message bus integration for real-time event forwarding |
+| Admin API | REST API for session management, provider configuration, and runtime health |
+| Usage Metering | Token and tool usage tracking with per-tenant attribution |
+| SSO / Auth Integration | Enterprise identity provider support for approval workflows |
+
+---
+
+## 6. Phase 3 Requirements (Summary)
+
+| Capability | Description |
+|------------|-------------|
+| Polished CLI UX | Auto-complete, syntax highlighting, rich diff rendering, progress indicators |
+| IDE Extensions | VS Code and JetBrains Rider extensions using ACP protocol |
+| Local Models | Ollama and llama.cpp support via OpenAI-compatible provider |
+| Workspace Indexing | Semantic code search, symbol navigation, dependency graph |
+| Cross-Session Memory | Persistent memory that survives session boundaries (project knowledge, user preferences) |
+| Community Plugin Registry | Discoverable, installable plugins from a public registry |
+
+---
+
+## 7. Success Metrics
+
+### Phase 1 (Months 0-12)
+
+| Metric | Target | Rationale |
+|--------|--------|-----------|
+| GitHub stars | 2,000 | Validates .NET community interest |
+| NuGet downloads (monthly) | 5,000 | Measures actual adoption |
+| Contributors (unique) | 25 | Healthy contributor ecosystem |
+| Discord / community members | 500 | Engaged developer community |
+| Production deployments (known) | 10 | Real-world validation |
+| Documentation pages | 30+ | Comprehensive getting-started and reference |
+
+### Phase 2 (Months 6-18)
+
+| Metric | Target |
+|--------|--------|
+| Companies using embedded SDK | 5 |
+| Enterprise pilot conversations | 10 |
+| Monthly recurring revenue | $10K (from enterprise licenses) |
+
+### Phase 3 (Months 12-24)
+
+| Metric | Target |
+|--------|--------|
+| Monthly active CLI users | 5,000 |
+| IDE extension installs | 2,000 |
+| Community plugins published | 20 |
+
+---
+
+## 8. Technical Constraints
+
+- **.NET 10 / C# 13** — non-negotiable; this is a .NET-native product
+- **System.Text.Json only** — no Newtonsoft.Json; source-generated serialization
+- **Cross-platform** — must work on Windows, macOS, and Linux
+- **MIT license for core** — open-core model; proprietary extensions for enterprise
+- **Protocol-first** — all contracts go through SharpClaw.Code.Protocol; no leaking provider types
+- **Async end-to-end** — no sync-over-async or fire-and-forget patterns
+- **Permission-aware by default** — tools must route through the policy engine
+
+---
+
+## 9. Risks
+
+| Risk | Likelihood | Impact | Mitigation |
+|------|-----------|--------|------------|
+| .NET coding agent market is too small | Medium | High | Phase 1 positions as production runtime layer (broader market), not just coding agent |
+| Microsoft builds competing runtime layer into Agent Framework | Low-Medium | High | Stay close to the team; contribute upstream; position as community-driven complement rather than competitor. If Microsoft builds it, pivot to tooling/hosting layer on top |
+| Agent Framework breaks API compatibility | Medium | Medium | Abstraction layer (AgentFrameworkBridge) already isolates framework types; pin to stable versions |
+| Provider APIs change frequently | High | Low | Abstraction layer already isolates provider details |
+| Contributor burnout (small team) | Medium | High | Keep scope tight per phase; automate CI/CD; accept contributions early |
+| Enterprise sales cycle too long for Phase 2 | Medium | Medium | Offer self-serve enterprise tier alongside sales-led motion |
+| Microsoft relationship doesn't materialize | Medium | Medium | Product stands alone regardless; Microsoft alignment is accelerant, not dependency |
+
+---
+
+## 10. Open Questions
+
+1. Should the CLI be distributed as a `dotnet tool` (global install) or standalone binary?
+2. What's the naming/branding strategy for paid tiers? ("SharpClaw Pro"? "SharpClaw Enterprise"?)
+3. Should Phase 2 include a hosted/managed offering, or stay self-hosted only?
+4. How deep should the Agent Framework integration go — should SharpClaw contribute features upstream, or keep the runtime layer cleanly separated?
+5. Is there an opportunity to join Microsoft's Agent Framework partner/early-adopter program?
+6. Should SharpClaw target inclusion in .NET project templates (e.g., `dotnet new sharpclaw-agent`)?
diff --git a/docs/agent-framework-integration.md b/docs/agent-framework-integration.md
new file mode 100644
index 0000000..29376c3
--- /dev/null
+++ b/docs/agent-framework-integration.md
@@ -0,0 +1,480 @@
+# Microsoft Agent Framework Integration
+
+SharpClaw Code is built **on top of** the [Microsoft Agent Framework](https://github.com/microsoft/agents) (`Microsoft.Agents.AI` NuGet package). This guide explains how SharpClaw leverages the framework and what production capabilities SharpClaw adds.
+
+## Overview
+
+**Microsoft Agent Framework** provides:
+- Abstract agent interfaces (`AIAgent`, `AgentSession`, `AgentResponse`)
+- Session lifecycle management
+- Chat message and tool-calling abstractions
+- A foundation for building multi-turn agent systems
+
+**SharpClaw Code** complements the framework by adding:
+- Production-grade agent orchestration for coding tasks
+- Provider abstraction layer with auth preflight and streaming adapters
+- Permission-aware tool execution with approval gates
+- Durable session snapshots and NDJSON event logs
+- MCP (Model Context Protocol) server supervision
+- Plugin system with trust levels and out-of-process execution
+- Structured telemetry with ring buffer and usage tracking
+- REPL and CLI with spec mode
+
+## Quick comparison
+
+| Layer | Agent Framework Provides | SharpClaw Adds |
+|-------|--------------------------|---|
+| **Agent abstractions** | `AIAgent`, `AgentSession`, `AgentResponse` | Coding-agent orchestration, turns, context assembly |
+| **Provider integration** | Multi-provider interfaces | Resilience, auth preflight, streaming adapters, tool-use extraction |
+| **Tool execution** | — | Permission-aware tools, approval gates, workspace boundaries |
+| **Sessions** | In-memory | Durable snapshots, NDJSON event logs, checkpoints, undo/redo |
+| **MCP support** | — | Server registration, supervision, health checks |
+| **Plugins** | — | Manifest discovery, trust levels, out-of-process execution |
+| **Telemetry** | Standard logging | Structured events, ring buffer, usage tracking |
+| **CLI & REPL** | — | REPL, slash commands, JSON output, spec mode |
+
+## Architecture
+
+The integration is layered. Each layer builds on the one below:
+
+```
+Microsoft Agent Framework (AIAgent, AgentSession, AgentResponse)
+ ↓
+SharpClawFrameworkAgent (implements AIAgent)
+ ↓
+AgentFrameworkBridge (orchestration layer)
+ ↓
+ProviderBackedAgentKernel (provider + tool-calling loop)
+ ↓
+IModelProvider (Anthropic, OpenAI-compatible)
+```
+
+### Layer 1: SharpClawFrameworkAgent
+
+**File:** `src/SharpClaw.Code.Agents/Internal/SharpClawFrameworkAgent.cs`
+
+A concrete implementation of `AIAgent` that adapts SharpClaw's agent model to the framework:
+
+```csharp
+internal sealed class SharpClawFrameworkAgent(
+ string agentId,
+ string name,
+ string description,
+ Func, AgentSession, AgentRunOptions, CancellationToken, Task> runAsync)
+ : AIAgent
+```
+
+Responsibilities:
+- Provides framework-required properties (`Id`, `Name`, `Description`)
+- Creates and deserializes `AgentSession` instances (backed by `SharpClawAgentSession`)
+- Delegates core execution to a caller-provided delegate
+- Implements streaming semantics by converting `AgentResponse` to `AgentResponseUpdate` sequences
+
+The session state is serialized/deserialized via `StateBag`, allowing framework-level session persistence.
+
+### Layer 2: AgentFrameworkBridge
+
+**File:** `src/SharpClaw.Code.Agents/Services/AgentFrameworkBridge.cs`
+
+The orchestration layer that:
+
+1. **Translates context:** Converts `AgentFrameworkRequest` (SharpClaw's agent input model) into:
+ - Tool registry entries → `ProviderToolDefinition` list
+ - `ToolExecutionContext` (permissions, workspace bounds, mutation recorder)
+ - Framework session and run options
+
+2. **Instantiates the framework agent:** Creates a `SharpClawFrameworkAgent` with a delegate that calls `ProviderBackedAgentKernel`
+
+3. **Orchestrates execution:**
+ ```csharp
+ var frameworkAgent = new SharpClawFrameworkAgent(
+ request.AgentId,
+ request.Name,
+ request.Description,
+ async (messages, session, runOptions, ct) =>
+ {
+ providerResult = await providerBackedAgentKernel.ExecuteAsync(
+ request,
+ toolExecutionContext,
+ providerTools,
+ ct).ConfigureAwait(false);
+ return new AgentResponse(new ChatMessage(ChatRole.Assistant, providerResult.Output));
+ });
+
+ response = await frameworkAgent.RunAsync(request.Context.Prompt, session, cancellationToken: cancellationToken);
+ ```
+
+4. **Returns an `AgentRunResult`** with:
+ - Output text
+ - Token usage metrics
+ - Provider request/response details
+ - Tool results and runtime events
+ - `AgentSpawnedEvent` and `AgentCompletedEvent` for session telemetry
+
+### Layer 3: ProviderBackedAgentKernel
+
+**File:** `src/SharpClaw.Code.Agents/Internal/ProviderBackedAgentKernel.cs`
+
+The core execution engine for streaming provider responses and driving the tool-calling loop.
+
+Key responsibilities:
+
+1. **Auth preflight:** Checks `IAuthFlowService` to verify the provider is authenticated before making calls
+2. **Provider resolution:** Uses `IModelProviderResolver` to get the configured `IModelProvider`
+3. **Message assembly:** Builds the conversation thread from:
+ - System prompt
+ - Prior turn history (multi-turn context)
+ - Current user prompt
+4. **Tool-calling loop:**
+ - Calls `provider.StartStreamAsync()` to get a streaming provider event sequence
+ - Extracts `ProviderEvent` items (text chunks, tool-use invocations, usage stats)
+ - On tool-use events, constructs `ContentBlock` entries with tool name, ID, and input JSON
+ - Dispatches each tool via `ToolCallDispatcher` (which runs through the permission engine)
+ - Feeds tool results back to the provider in the next iteration
+ - Repeats until max iterations or no tool calls remain
+
+5. **Error handling:**
+ - Missing provider → `ProviderExecutionException` with `ProviderFailureKind.MissingProvider`
+ - Auth check failure → `ProviderFailureKind.AuthenticationUnavailable`
+ - Stream error → `ProviderFailureKind.StreamFailed`
+ - Placeholder response when stream is empty
+
+**Loop Configuration:** Controlled by `AgentLoopOptions`:
+- `MaxToolIterations` — maximum rounds (default 25)
+- `MaxTokensPerRequest` — per-iteration token budget
+
+### Layer 4: IModelProvider
+
+**File:** `src/SharpClaw.Code.Providers/Abstractions/IModelProvider.cs`
+
+SharpClaw's abstraction over model providers:
+
+```csharp
+public interface IModelProvider
+{
+ string ProviderName { get; }
+ Task GetAuthStatusAsync(CancellationToken cancellationToken);
+ Task StartStreamAsync(ProviderRequest request, CancellationToken cancellationToken);
+}
+```
+
+**Registered implementations:**
+- `AnthropicProvider` — Anthropic Claude models via HTTP
+- `OpenAiCompatibleProvider` — OpenAI-compatible endpoints (LM Studio, Ollama, etc.)
+
+Both stream `ProviderEvent` sequences containing:
+- Text chunks
+- Tool-use invocations (`ToolUseId`, `ToolName`, `ToolInputJson`)
+- Terminal usage metrics
+
+## Integration entry points
+
+### 1. Extending SharpClaw Agent Types
+
+SharpClaw provides a base class for custom agents:
+
+**File:** `src/SharpClaw.Code.Agents/Agents/SharpClawAgentBase.cs`
+
+```csharp
+public abstract class SharpClawAgentBase(IAgentFrameworkBridge agentFrameworkBridge) : ISharpClawAgent
+{
+ public abstract string AgentId { get; }
+ public abstract string AgentKind { get; }
+ protected abstract string Name { get; }
+ protected abstract string Description { get; }
+ protected abstract string Instructions { get; }
+
+ public virtual Task RunAsync(AgentRunContext context, CancellationToken cancellationToken)
+ => agentFrameworkBridge.RunAsync(
+ new AgentFrameworkRequest(
+ AgentId,
+ AgentKind,
+ Name,
+ Description,
+ Instructions,
+ context),
+ cancellationToken);
+}
+```
+
+**To add a custom agent:**
+
+1. Inherit from `SharpClawAgentBase`
+2. Provide concrete implementations of `AgentId`, `AgentKind`, `Name`, `Description`, `Instructions`
+3. Optionally override `RunAsync` to customize behavior before/after framework execution
+4. Register in DI:
+ ```csharp
+ services.AddSingleton();
+ services.AddSingleton(sp => sp.GetRequiredService());
+ ```
+
+**Example:** `PrimaryCodingAgent` (default agent for prompts):
+```csharp
+public sealed class PrimaryCodingAgent(IAgentFrameworkBridge agentFrameworkBridge)
+ : SharpClawAgentBase(agentFrameworkBridge)
+{
+ public override string AgentId => "primary-coding-agent";
+ public override string AgentKind => "primaryCoding";
+ protected override string Name => "Primary Coding Agent";
+ protected override string Description => "Handles the default coding workflow for prompt execution.";
+ protected override string Instructions => "You are SharpClaw Code's primary coding agent. ...";
+}
+```
+
+### 2. Adding a Custom Model Provider
+
+Implement `IModelProvider` to integrate a new model source:
+
+```csharp
+public sealed class YourModelProvider : IModelProvider
+{
+ public string ProviderName => "your-provider";
+
+ public async Task GetAuthStatusAsync(CancellationToken cancellationToken)
+ {
+ // Check if credentials are available (API key, token, etc.)
+ return new AuthStatus(IsAuthenticated: _hasCredentials);
+ }
+
+ public async Task StartStreamAsync(
+ ProviderRequest request,
+ CancellationToken cancellationToken)
+ {
+ // Stream model responses as ProviderEvent sequences
+ return new ProviderStreamHandle(
+ Events: StreamEventsAsync(request, cancellationToken));
+ }
+
+ private async IAsyncEnumerable StreamEventsAsync(
+ ProviderRequest request,
+ [EnumeratorCancellation] CancellationToken cancellationToken)
+ {
+ // Emit text chunks as ProviderEvent with IsTerminal=false
+ // Emit tool-use events with ToolUseId, ToolName, ToolInputJson
+ // Emit usage metrics in the final ProviderEvent with IsTerminal=true
+ }
+}
+```
+
+**Registration:**
+
+```csharp
+public static void AddYourProvider(this IServiceCollection services)
+{
+ services.AddSingleton();
+ // Configure options if needed
+ services.Configure(configuration.GetSection("Your:Provider"));
+}
+```
+
+**Provider catalog:** Update `ProviderCatalogOptions` to register aliases:
+```json
+{
+ "SharpClaw:Providers:Catalog": {
+ "DefaultProvider": "your-provider",
+ "ModelAliases": {
+ "default": "your-provider/latest-model"
+ }
+ }
+}
+```
+
+### 3. Adding Custom Tools
+
+Custom tools integrate via the registry and are automatically available to agents:
+
+**File:** `src/SharpClaw.Code.Tools/Abstractions/ISharpClawTool.cs`
+
+```csharp
+public interface ISharpClawTool
+{
+ ToolDefinition Definition { get; }
+ PluginToolSource? PluginSource { get; }
+ Task ExecuteAsync(ToolExecutionContext context, ToolExecutionRequest request, CancellationToken cancellationToken);
+}
+```
+
+**Implementation:** Extend `SharpClawToolBase` for the common pattern:
+
+```csharp
+public sealed class YourCustomTool(IPathService pathService) : SharpClawToolBase
+{
+ public override ToolDefinition Definition { get; } = new(
+ Name: "your-tool",
+ Description: "Does something useful",
+ ApprovalScope: ApprovalScope.ToolExecution,
+ IsDestructive: false,
+ RequiresApproval: false,
+ InputTypeName: "YourToolArguments",
+ InputDescription: "JSON object with tool parameters.",
+ Tags: ["custom"]);
+
+ public override async Task ExecuteAsync(
+ ToolExecutionContext context,
+ ToolExecutionRequest request,
+ CancellationToken cancellationToken)
+ {
+ var arguments = DeserializeArguments(request);
+ // Perform work respecting context.WorkspaceRoot, context.PermissionMode, etc.
+ return CreateSuccessResult(context, request, "output text", null);
+ }
+}
+```
+
+**DI Registration:**
+
+```csharp
+services.AddSingleton();
+```
+
+**Tool calling:** The agent kernel automatically:
+1. Includes tool schemas in the initial provider request
+2. Extracts tool-use events from the provider stream
+3. Dispatches via `ToolCallDispatcher` (which consults the permission engine)
+4. Collects results and feeds them back to the provider for continued reasoning
+
+See [tools.md](tools.md) for full details on tool execution, permissions, and plugin integration.
+
+## Tool-calling flow within the framework
+
+The `ProviderBackedAgentKernel` drives a multi-iteration loop that respects the framework's abstractions:
+
+1. **Iteration N:** Call `provider.StartStreamAsync()` with conversation history + tool schemas
+2. **Stream processing:** Collect text and tool-use events
+3. **Build assistant message:** Add text block and tool-use content blocks to conversation
+4. **Tool dispatch:** Call `ToolCallDispatcher` for each tool-use event
+5. **Build user message:** Add tool result content blocks
+6. **Continue:** Append both messages and loop back to step 1
+7. **Exit:** When iteration returns no tool-use events, break and return accumulated text
+
+This pattern keeps the framework session state synchronized with the multi-turn conversation and tool results.
+
+## Configuration and instantiation
+
+### Runtime integration
+
+The `ConversationRuntime` owns agent execution via the `DefaultTurnRunner`:
+
+```
+Prompt input
+ ↓
+ConversationRuntime.RunPromptAsync
+ ↓
+DefaultTurnRunner.RunAsync (assembles context)
+ ↓
+PrimaryCodingAgent.RunAsync (framework bridge)
+ ↓
+AgentFrameworkBridge.RunAsync
+ ↓
+ProviderBackedAgentKernel.ExecuteAsync (tool-calling loop)
+ ↓
+IModelProvider.StartStreamAsync (streaming)
+```
+
+### Service registration
+
+The agents module registers via `AgentsServiceCollectionExtensions`:
+
+```csharp
+public static IServiceCollection AddSharpClawAgents(
+ this IServiceCollection services,
+ IConfiguration configuration)
+{
+ // Register bridge, kernel, concrete agents, etc.
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ // ... other agents
+
+ services.Configure(configuration.GetSection("SharpClaw:AgentLoop"));
+ return services;
+}
+```
+
+The CLI host calls:
+```csharp
+services.AddSharpClawRuntime(configuration); // includes agents
+```
+
+## Key architectural decisions
+
+### Why a bridge?
+
+The `AgentFrameworkBridge` isolates **SharpClaw's agent orchestration** (context assembly, tool dispatch, permission checks) from **Microsoft Agent Framework's abstractions** (session, message, response). This allows:
+
+- **Version independence:** Framework updates don't force refactoring across SharpClaw
+- **Testing:** Bridge can be tested with mock providers and kernels
+- **Clarity:** Clear contract between layers; framework details are hidden from callers
+
+### Why ProviderBackedAgentKernel?
+
+Separates **provider streaming** and **tool-calling logic** from **framework integration**:
+
+- **Streaming:** Handles partial chunks, tool-use extraction, usage metrics
+- **Tool calling:** Drives the multi-iteration loop, permission checks, result collection
+- **Auth checks:** Runs preflight before expensive provider calls
+- **Error handling:** Classifies failures and maps to `ProviderFailureKind`
+
+This kernel can be tested independently or used in non-framework contexts (e.g., batch processing).
+
+### Why IModelProvider over framework providers?
+
+SharpClaw's `IModelProvider` is:
+
+- **Simpler:** One async method to stream events
+- **Resilient:** Built-in auth preflight and preflight normalization
+- **Pluggable:** Easy to add new endpoints (Anthropic, OpenAI-compatible, custom)
+- **Streaming-first:** Designed for partial updates and tool-calling loops
+
+The framework provides `IChatCompletionService` and `IEmbeddingService` abstractions; SharpClaw adds `IModelProvider` for agent-specific streaming requirements.
+
+## Testing
+
+### Unit testing the bridge
+
+Test with a mock provider:
+
+```csharp
+var mockProvider = new MockModelProvider();
+services.AddSingleton(mockProvider);
+
+var bridge = new AgentFrameworkBridge(/* deps */);
+var result = await bridge.RunAsync(request, cancellationToken);
+
+Assert.NotNull(result.Output);
+Assert.Equal(request.AgentId, result.AgentId);
+```
+
+### Integration testing
+
+Use the `SharpClaw.Code.MockProvider` test fixture:
+
+```csharp
+var host = TestHostBuilder.BuildWithMockProvider();
+var runtime = host.Services.GetRequiredService();
+
+var result = await runtime.RunPromptAsync(
+ sessionId: "test-session",
+ prompt: "What is 2 + 2?",
+ cancellationToken);
+
+Assert.Contains("4", result.Output);
+```
+
+See [testing.md](testing.md) for full test patterns.
+
+## Further reading
+
+- [Architecture](architecture.md) — Solution structure and overall data flow
+- [Providers](providers.md) — Provider interface, registration, and catalog
+- [Tools](tools.md) — Tool registry, execution, permissions, and plugins
+- [Sessions](sessions.md) — Session snapshots, event logs, and checkpoints
+- [MCP](mcp.md) — Model Context Protocol server registration and supervision
+- [Testing](testing.md) — Test patterns and fixtures
+
+## Microsoft Agent Framework links
+
+- [Microsoft Agent Framework GitHub](https://github.com/microsoft/agents)
+- [AIAgent interface documentation](https://github.com/microsoft/agents/blob/main/dotnet/src/Microsoft.Agents.Core/AIAgent.cs)
+- [Agent Framework samples](https://github.com/microsoft/agents/tree/main/dotnet/samples)
diff --git a/docs/architecture.md b/docs/architecture.md
index 3507212..180086c 100644
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -40,7 +40,7 @@ Test projects: **UnitTests**, **IntegrationTests**, **MockProvider**, **ParityHa
4. **`SharpClawAgentBase`** delegates to **`AgentFrameworkBridge.RunAsync`**, which drives **`ProviderBackedAgentKernel`** (streaming `IModelProvider`, auth checks, **`ProviderExecutionException`** on hard failures).
5. Turn completion updates session, checkpoints as implemented in **`ConversationRuntime`**, publishes events via **`IRuntimeEventPublisher`**.
-**Note:** `AgentRunContext` carries **`IToolExecutor`**, but the current **`AgentFrameworkBridge`** path does not attach SharpClaw tools to the Microsoft Agent Framework chat loop; **`AgentRunResult.ToolResults`** is empty in that bridge. Tools are still fully usable via **`IToolExecutor`** (tests and parity harness call it directly).
+**Note:** `AgentRunContext` carries **`IToolExecutor`**, and the current **`AgentFrameworkBridge`** path advertises the resolved tool set to the provider, executes tool calls through the permission-aware executor, and records tool results in the agent run result. Prompt references and tool approvals respect the caller's normalized interactivity mode.
### Operational commands
diff --git a/docs/getting-started.md b/docs/getting-started.md
new file mode 100644
index 0000000..6fa98af
--- /dev/null
+++ b/docs/getting-started.md
@@ -0,0 +1,320 @@
+# Getting Started with SharpClaw Code
+
+Run a .NET-native coding agent in 15 minutes.
+
+## Prerequisites
+
+- [.NET SDK 10](https://dotnet.microsoft.com/download/dotnet/10.0) or later
+- A terminal or command prompt
+- A text editor (optional, for configuration)
+
+## Clone and Build
+
+Clone the repository and build the solution:
+
+```bash
+git clone https://github.com/clawdotnet/SharpClawCode.git
+cd SharpClawCode
+dotnet build SharpClawCode.sln
+```
+
+Run the test suite to verify your build:
+
+```bash
+dotnet test SharpClawCode.sln
+```
+
+All tests should pass. If they don't, check that you have .NET 10 SDK installed:
+
+```bash
+dotnet --version
+```
+
+## Run the CLI
+
+The CLI is in `src/SharpClaw.Code.Cli`. Start the interactive REPL:
+
+```bash
+dotnet run --project src/SharpClaw.Code.Cli
+```
+
+You'll see a prompt and command-line interface. This is the REPL.
+
+## Interactive REPL
+
+The REPL is your primary interface for chatting with the agent.
+
+### Slash Commands
+
+Type `/` to see available commands:
+
+- `/help` – Show all available commands
+- `/status` – Display current session and workspace state
+- `/doctor` – Check runtime health and provider configuration
+- `/session` – View or manage the current session
+- `/mode` – Switch workflow mode (build, plan, spec)
+- `/editor` – Open current conversation in $EDITOR
+- `/export` – Export session history as JSON
+- `/undo` – Undo the last turn
+- `/redo` – Redo the last undone turn
+- `/version` – Show SharpClaw version
+- `/commands` – List custom workspace commands
+- `/exit` – Exit the REPL
+
+### Workflow Modes
+
+The runtime supports three primary modes:
+
+| Mode | Purpose |
+|------|---------|
+| `build` | Normal coding-agent execution; all tools enabled |
+| `plan` | Analysis-first mode; planning tools only, no file/shell mutations |
+| `spec` | Generate structured spec artifacts in `docs/superpowers/specs/` |
+
+Switch modes in the REPL with `/mode build`, `/mode plan`, or `/mode spec`.
+
+## Your First Prompt
+
+Run a one-shot prompt without entering the REPL:
+
+```bash
+dotnet run --project src/SharpClaw.Code.Cli -- prompt "List all .cs files in this workspace"
+```
+
+The agent will execute and print the result to stdout.
+
+### Output Formats
+
+Emit JSON instead of human-readable output:
+
+```bash
+dotnet run --project src/SharpClaw.Code.Cli -- --output-format json prompt "Summarize the README"
+```
+
+Supported formats: `text` (default), `json`, `markdown`.
+
+## Configuration
+
+### API Keys (Environment Variables)
+
+Set provider API keys before running the CLI:
+
+```bash
+# .NET configuration uses double-underscore for nested keys in env vars
+export SharpClaw__Providers__Anthropic__ApiKey=sk-ant-...
+dotnet run --project src/SharpClaw.Code.Cli
+```
+
+Supported environment variables (using .NET configuration path format):
+
+- `SharpClaw__Providers__Anthropic__ApiKey` – Anthropic API key
+- `SharpClaw__Providers__OpenAiCompatible__ApiKey` – OpenAI-compatible API key
+- `SharpClaw__Providers__Catalog__DefaultProvider` – Default provider name
+
+### Configuration File
+
+Alternatively, configure providers in `appsettings.json`:
+
+```json
+{
+ "SharpClaw": {
+ "Providers": {
+ "Catalog": {
+ "DefaultProvider": "Anthropic"
+ },
+ "Anthropic": {
+ "ApiKey": "sk-ant-...",
+ "DefaultModel": "claude-sonnet-4-5"
+ },
+ "OpenAiCompatible": {
+ "ApiKey": "sk-...",
+ "DefaultModel": "gpt-4-turbo"
+ }
+ }
+ }
+}
+```
+
+The runtime loads from standard .NET configuration sources:
+1. Environment variables (highest priority, double-underscore path format)
+2. `appsettings.{Environment}.json`
+3. `appsettings.json` (default)
+4. Command-line arguments
+
+## Embed in Your Own App
+
+Use SharpClaw as a library in your .NET application.
+
+### 1. Install the NuGet Package
+
+```bash
+dotnet add package SharpClaw.Code.Runtime
+```
+
+### 2. Register the Runtime
+
+In your application startup, add SharpClaw to the dependency injection container:
+
+```csharp
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using SharpClaw.Code.Runtime;
+using SharpClaw.Code.Protocol.Commands;
+using SharpClaw.Code.Protocol.Enums;
+
+var builder = Host.CreateApplicationBuilder(args);
+
+// Add SharpClaw runtime
+builder.Services.AddSharpClawRuntime(builder.Configuration);
+
+var host = builder.Build();
+```
+
+### 3. Execute a Prompt
+
+```csharp
+using var host = builder.Build();
+await host.StartAsync();
+
+var runtime = host.Services.GetRequiredService();
+
+var request = new RunPromptRequest(
+ Prompt: "Analyze the current workspace",
+ SessionId: null, // new session
+ WorkingDirectory: Environment.CurrentDirectory,
+ PermissionMode: PermissionMode.Auto,
+ OutputFormat: OutputFormat.Markdown,
+ Metadata: new Dictionary
+ {
+ { "user-id", "developer-1" }
+ }
+);
+
+var result = await runtime.RunPromptAsync(request, CancellationToken.None);
+
+Console.WriteLine(result.FinalOutput);
+Console.WriteLine($"Session: {result.Session.Id}");
+```
+
+### 4. Reuse Sessions
+
+Sessions are durable. Resume an existing session by passing `SessionId`:
+
+```csharp
+var latestSession = await runtime.GetLatestSessionAsync(
+ workspacePath: Environment.CurrentDirectory,
+ cancellationToken: CancellationToken.None
+);
+
+var request = new RunPromptRequest(
+ Prompt: "Continue from before",
+ SessionId: latestSession?.Id, // Resume this session
+ WorkingDirectory: Environment.CurrentDirectory,
+ PermissionMode: PermissionMode.Auto,
+ OutputFormat: OutputFormat.Markdown,
+ Metadata: null
+);
+
+var result = await runtime.RunPromptAsync(request, CancellationToken.None);
+```
+
+### Minimal Example
+
+Complete console app:
+
+```csharp
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using SharpClaw.Code.Runtime;
+using SharpClaw.Code.Protocol.Commands;
+using SharpClaw.Code.Protocol.Enums;
+
+var builder = Host.CreateApplicationBuilder(args);
+builder.Services.AddSharpClawRuntime(builder.Configuration);
+
+var host = builder.Build();
+await host.StartAsync();
+
+try
+{
+ var runtime = host.Services.GetRequiredService();
+ var result = await runtime.RunPromptAsync(
+ new RunPromptRequest(
+ "What is in this directory?",
+ SessionId: null,
+ WorkingDirectory: Environment.CurrentDirectory,
+ PermissionMode: PermissionMode.Auto,
+ OutputFormat: OutputFormat.Markdown,
+ Metadata: null
+ ),
+ CancellationToken.None
+ );
+
+ Console.WriteLine(result.FinalOutput);
+}
+finally
+{
+ await host.StopAsync();
+}
+```
+
+## Next Steps
+
+Learn more about SharpClaw:
+
+- **[Architecture](architecture.md)** – Design, layers, and runtime model
+- **[Sessions](sessions.md)** – Durable state, history, checkpoints, and recovery
+- **[Tools](tools.md)** – Available tools and integration patterns
+- **[Providers](providers.md)** – Provider abstraction, Anthropic, OpenAI, and custom backends
+- **[MCP Support](mcp.md)** – Model Context Protocol servers and lifecycle
+- **[Agents](agents.md)** – Agent Framework integration and configuration
+- **[Runtime Concepts](runtime.md)** – Execution model, turns, events, and telemetry
+- **[Permissions](permissions.md)** – Permission modes and approval gates
+- **[Testing](testing.md)** – Unit and integration testing strategies
+- **[Plugins](plugins.md)** – Extending SharpClaw with custom plugins
+
+## Troubleshooting
+
+### Agent doesn't respond or times out
+
+Check that your API key is set:
+
+```bash
+dotnet run --project src/SharpClaw.Code.Cli -- doctor
+```
+
+Look for your provider (Anthropic or OpenAI) in the output. If it shows "not configured", set `SHARPCLAW_ANTHROPIC_API_KEY` or configure `appsettings.json`.
+
+### Build fails with .NET version error
+
+Ensure you have .NET 10:
+
+```bash
+dotnet --version
+```
+
+If you have an older version, [install .NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0).
+
+### REPL commands not available
+
+Update to the latest main branch:
+
+```bash
+git pull origin main
+dotnet build SharpClawCode.sln
+```
+
+### Tests fail
+
+Run with verbose output:
+
+```bash
+dotnet test SharpClawCode.sln --verbosity detailed
+```
+
+Check that all prerequisites are installed and your internet connection is stable (tests may fetch test fixtures or run integration tests).
+
+## Questions?
+
+- Open an issue: [github.com/clawdotnet/SharpClawCode/issues](https://github.com/clawdotnet/SharpClawCode/issues)
+- Read the [README](../README.md) for a full feature overview
diff --git a/docs/runtime.md b/docs/runtime.md
index 191c523..d0a5a93 100644
--- a/docs/runtime.md
+++ b/docs/runtime.md
@@ -3,7 +3,7 @@
The **runtime** layer is centered on **`SharpClaw.Code.Runtime`** and especially **`ConversationRuntime`**, which implements:
- Session surface on **`IConversationRuntime`** — create/get session, **`RunPromptAsync`**
-- **`IRuntimeCommandService`** — **`ExecutePromptAsync`**, **`GetStatusAsync`**, **`RunDoctorAsync`**, **`InspectSessionAsync`**
+- **`IRuntimeCommandService`** — prompt execution plus status, doctor, session inspection, share/unshare, and compaction commands
Registration: `RuntimeServiceCollectionExtensions.AddSharpClawRuntime`.
@@ -15,6 +15,13 @@ Registration: `RuntimeServiceCollectionExtensions.AddSharpClawRuntime`.
2. Maps **`RunPromptRequest`** + session into **`AgentRunContext`** (session/turn ids, working directory, permission mode, output format, **`IToolExecutor`**, metadata).
3. Invokes **`PrimaryCodingAgent.RunAsync`**.
+Before the agent runs, **`ConversationRuntime`** also layers in:
+
+- merged SharpClaw JSONC config (`ISharpClawConfigService`)
+- resolved agent defaults (`IAgentCatalogService`)
+- persisted active-agent metadata from the session, when present
+- auto-share policy checks (`ShareMode.Auto`)
+
The agent stack is described in [agents.md](agents.md).
## Lifecycle and state
@@ -24,7 +31,9 @@ The agent stack is described in [agents.md](agents.md).
## Context assembly
-**`PromptContextAssembler`** pulls workspace/session-aware data (skills registry, memory hooks, git context as wired today) into the prompt path before the agent runs.
+**`PromptContextAssembler`** pulls workspace/session-aware data (skills registry, todo state, memory hooks, git context as wired today) into the prompt path before the agent runs.
+
+It also includes a compact diagnostics summary from **`IWorkspaceDiagnosticsService`**, which currently surfaces configured diagnostics sources and build-derived findings for .NET workspaces.
When the effective **`PrimaryMode`** is **`Spec`**, the assembler appends a structured output contract that requires the model to return machine-readable requirements, design, and task content.
@@ -50,6 +59,41 @@ Each spec-mode prompt creates a fresh folder. If the same slug already exists, t
Used by **`GetStatusAsync`**, **`RunDoctorAsync`**, and **`InspectSessionAsync`** to build **Protocol** reports (`DoctorReport`, `RuntimeStatusReport`, `SessionInspectionReport`).
+`RuntimeStatusReport` now also carries:
+
+- configured diagnostics source count
+- current diagnostic error count
+- current diagnostic warning count
+
+## Config, agents, and hooks
+
+The parity layer adds several runtime-owned services:
+
+- **`ISharpClawConfigService`** — loads user/workspace `config.jsonc` + `sharpclaw.jsonc` and merges them by precedence
+- **`IAgentCatalogService`** — overlays configured specialist agents on top of built-in agents
+- **`IConversationCompactionService`** — creates durable session summaries stored in session metadata
+- **`IShareSessionService`** — creates and removes self-hosted share snapshots
+- **`IHookDispatcher`** — executes configured hook processes for turn/tool/share/server events and exposes hook inspection/testing
+- **`ITodoService`** — persists session and workspace todo items under session metadata and `.sharpclaw/tasks.json`
+- **`IWorkspaceInsightsService`** — reconstructs durable usage, cost, and execution stats from persisted event logs
+
+These services are intentionally small and runtime-owned rather than separate orchestration subsystems.
+
+## Embedded server
+
+**`IWorkspaceHttpServer`** / **`WorkspaceHttpServer`** expose a minimal local HTTP surface for editor and automation clients:
+
+- `POST /v1/prompt`
+- `GET /v1/sessions`
+- `GET /v1/sessions/{id}`
+- `POST /v1/share/{sessionId}`
+- `DELETE /v1/share/{sessionId}`
+- `GET /v1/status`
+- `GET /v1/doctor`
+- `GET /s/{shareId}`
+
+Prompt requests can return JSON or replay the completed runtime event stream as SSE.
+
## Hosted service
**`RuntimeCoordinatorHostedServiceAdapter`** is registered as **`IHostedService`** and currently logs start/stop only (placeholder for future lifecycle coordination).
@@ -62,3 +106,7 @@ Used by **`GetStatusAsync`**, **`RunDoctorAsync`**, and **`InspectSessionAsync`*
| `DefaultTurnRunner` | `src/SharpClaw.Code.Runtime/Turns/DefaultTurnRunner.cs` |
| `OperationalDiagnosticsCoordinator` | `src/SharpClaw.Code.Runtime/Diagnostics/OperationalDiagnosticsCoordinator.cs` |
| `IRuntimeCommandService` | `src/SharpClaw.Code.Runtime/Abstractions/IRuntimeCommandService.cs` |
+| `SharpClawConfigService` | `src/SharpClaw.Code.Runtime/Configuration/SharpClawConfigService.cs` |
+| `ShareSessionService` | `src/SharpClaw.Code.Runtime/Workflow/ShareSessionService.cs` |
+| `ConversationCompactionService` | `src/SharpClaw.Code.Runtime/Workflow/ConversationCompactionService.cs` |
+| `WorkspaceHttpServer` | `src/SharpClaw.Code.Runtime/Server/WorkspaceHttpServer.cs` |
diff --git a/docs/superpowers/plans/2026-04-10-phase1-gaps.md b/docs/superpowers/plans/2026-04-10-phase1-gaps.md
new file mode 100644
index 0000000..c8b602f
--- /dev/null
+++ b/docs/superpowers/plans/2026-04-10-phase1-gaps.md
@@ -0,0 +1,1156 @@
+# Phase 1 Gaps Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Close all 7 Phase 1 gaps from the PRD: tool-calling loop (P0), conversation history (P0), NuGet packages (P1), documentation (P1), CI/CD (P1), provider resilience (P2), and observability (P2).
+
+**Architecture:** The plan builds bottom-up — Protocol models first, then provider/adapter changes, then agent loop, then runtime/history integration. Infrastructure tasks (CI, NuGet, docs) are independent and can run in parallel with the P2 work.
+
+**Tech Stack:** .NET 10, C# 13, System.Text.Json, Microsoft Agent Framework, Anthropic SDK, Microsoft.Extensions.AI, OpenTelemetry, GitHub Actions
+
+---
+
+## File Map
+
+### Protocol Layer (new/modified models)
+
+| File | Action | Responsibility |
+|------|--------|----------------|
+| `src/SharpClaw.Code.Protocol/Models/ChatMessage.cs` | Create | Role + content blocks model for conversation history |
+| `src/SharpClaw.Code.Protocol/Models/ContentBlock.cs` | Create | Discriminated content: text, tool-use, tool-result |
+| `src/SharpClaw.Code.Protocol/Models/ToolDefinition.cs` | Modify | Add `InputSchema` property for provider tool definitions |
+| `src/SharpClaw.Code.Protocol/Models/ProviderRequest.cs` | Modify | Add `Messages`, `Tools`, `MaxTokens` fields |
+| `src/SharpClaw.Code.Protocol/Models/ProviderEvent.cs` | Modify | Add `BlockType`, `ToolUseId`, `ToolName`, `ToolInputJson` fields |
+| `src/SharpClaw.Code.Protocol/Serialization/ProtocolJsonContext.cs` | Modify | Register new types for source-generated serialization |
+
+### Provider Layer (tool-use streaming)
+
+| File | Action | Responsibility |
+|------|--------|----------------|
+| `src/SharpClaw.Code.Providers/Internal/ProviderStreamEventFactory.cs` | Modify | Add `ToolUse` and `ToolResult` event factory methods |
+| `src/SharpClaw.Code.Providers/Internal/AnthropicSdkStreamAdapter.cs` | Modify | Extract `tool_use` blocks from Anthropic stream |
+| `src/SharpClaw.Code.Providers/Internal/OpenAiMeaiStreamAdapter.cs` | Modify | Extract `tool_calls` from OpenAI stream |
+| `src/SharpClaw.Code.Providers/Internal/AnthropicMessageBuilder.cs` | Create | Map `ChatMessage[]` + tool defs to Anthropic SDK params |
+| `src/SharpClaw.Code.Providers/Internal/OpenAiMessageBuilder.cs` | Create | Map `ChatMessage[]` + tool defs to MEAI ChatMessage list |
+| `src/SharpClaw.Code.Providers/AnthropicProvider.cs` | Modify | Accept messages array and tool definitions |
+| `src/SharpClaw.Code.Providers/OpenAiCompatibleProvider.cs` | Modify | Accept messages array and tool definitions |
+
+### Agent Layer (tool-calling loop)
+
+| File | Action | Responsibility |
+|------|--------|----------------|
+| `src/SharpClaw.Code.Agents/Internal/ProviderBackedAgentKernel.cs` | Modify | Implement tool-calling loop: stream → detect tool-use → execute → resume |
+| `src/SharpClaw.Code.Agents/Internal/ToolCallDispatcher.cs` | Create | Bridges tool-use events to IToolExecutor, builds tool-result content blocks |
+| `src/SharpClaw.Code.Agents/Configuration/AgentLoopOptions.cs` | Create | `MaxToolIterations`, `MaxTokensPerTurn` |
+| `src/SharpClaw.Code.Agents/Services/AgentFrameworkBridge.cs` | Modify | Pass tool definitions and messages to kernel |
+
+### Runtime Layer (conversation history)
+
+| File | Action | Responsibility |
+|------|--------|----------------|
+| `src/SharpClaw.Code.Runtime/Context/ConversationHistoryAssembler.cs` | Create | Builds `ChatMessage[]` from session event log |
+| `src/SharpClaw.Code.Runtime/Context/PromptContextAssembler.cs` | Modify | Include conversation history in assembled context |
+| `src/SharpClaw.Code.Runtime/Context/ContextWindowManager.cs` | Create | Token-aware truncation of message history |
+| `src/SharpClaw.Code.Runtime/Turns/DefaultTurnRunner.cs` | Modify | Pass messages + tool definitions to agent |
+
+### CI/CD
+
+| File | Action | Responsibility |
+|------|--------|----------------|
+| `.github/workflows/ci.yml` | Create | Build + test on push/PR, cross-platform matrix |
+| `.github/workflows/release.yml` | Create | NuGet publish on release tags |
+
+### NuGet Packaging
+
+| File | Action | Responsibility |
+|------|--------|----------------|
+| `Directory.Build.props` | Modify | Add NuGet metadata (Authors, PackageLicenseExpression, etc.) |
+| Per-project `.csproj` files | Modify | Add `PackageId`, `Description`, `PackageTags` |
+
+### Provider Resilience
+
+| File | Action | Responsibility |
+|------|--------|----------------|
+| `src/SharpClaw.Code.Providers/Resilience/RetryHandler.cs` | Create | Exponential backoff with jitter for transient failures |
+| `src/SharpClaw.Code.Providers/Resilience/RateLimitHandler.cs` | Create | 429 detection and Retry-After backoff |
+| `src/SharpClaw.Code.Providers/Resilience/CircuitBreakerHandler.cs` | Create | Half-open/open/closed circuit breaker |
+| `src/SharpClaw.Code.Providers/Configuration/ProviderResilienceOptions.cs` | Create | Options for retry, timeout, circuit breaker |
+| `src/SharpClaw.Code.Providers/Services/ResilientProviderDecorator.cs` | Create | Decorates IModelProvider with resilience pipeline |
+
+### Observability
+
+| File | Action | Responsibility |
+|------|--------|----------------|
+| `src/SharpClaw.Code.Telemetry/Diagnostics/SharpClawActivitySource.cs` | Create | OpenTelemetry ActivitySource for spans |
+| `src/SharpClaw.Code.Telemetry/Diagnostics/TurnActivityScope.cs` | Create | Wraps turn execution in an Activity span |
+| `src/SharpClaw.Code.Telemetry/Diagnostics/ProviderActivityScope.cs` | Create | Wraps provider calls in an Activity span |
+| `src/SharpClaw.Code.Telemetry/Metrics/SharpClawMeterSource.cs` | Create | Counters and histograms for tokens, duration, tools |
+| `src/SharpClaw.Code.Telemetry/Export/NdjsonTraceFileSink.cs` | Create | Optional NDJSON file sink for offline analysis |
+
+### Documentation & Examples
+
+| File | Action | Responsibility |
+|------|--------|----------------|
+| `docs/getting-started.md` | Create | 15-minute guide |
+| `docs/agent-framework-integration.md` | Create | How SharpClaw builds on Microsoft Agent Framework |
+| `examples/MinimalConsoleAgent/` | Create | Simplest possible agent |
+| `examples/WebApiAgent/` | Create | ASP.NET Core hosted agent with session persistence |
+| `examples/McpToolAgent/` | Create | Agent with custom MCP tools |
+
+### Tests
+
+| File | Action | Responsibility |
+|------|--------|----------------|
+| `tests/SharpClaw.Code.UnitTests/Protocol/ChatMessageSerializationTests.cs` | Create | Roundtrip for new message models |
+| `tests/SharpClaw.Code.UnitTests/Providers/ToolUseStreamAdapterTests.cs` | Create | Verify tool-use block extraction |
+| `tests/SharpClaw.Code.UnitTests/Agents/ToolCallDispatcherTests.cs` | Create | Verify tool dispatch and result building |
+| `tests/SharpClaw.Code.UnitTests/Runtime/ConversationHistoryAssemblerTests.cs` | Create | History assembly from events |
+| `tests/SharpClaw.Code.UnitTests/Runtime/ContextWindowManagerTests.cs` | Create | Truncation logic |
+| `tests/SharpClaw.Code.MockProvider/DeterministicMockModelProvider.cs` | Modify | Add tool-use scenario |
+| `tests/SharpClaw.Code.ParityHarness/ParityScenarioTests.cs` | Modify | Add tool-calling loop scenario |
+| `tests/SharpClaw.Code.UnitTests/Providers/ResilienceTests.cs` | Create | Retry, circuit breaker, rate limit tests |
+
+---
+
+## P0 — Tool-Calling Loop + Conversation History
+
+These are tightly coupled: the tool-calling loop requires conversation messages to feed tool results back, and conversation history requires the message model to persist multi-turn exchanges. Build together, bottom-up.
+
+### Task 1: Protocol — ChatMessage and ContentBlock models
+
+**Files:**
+- Create: `src/SharpClaw.Code.Protocol/Models/ChatMessage.cs`
+- Create: `src/SharpClaw.Code.Protocol/Models/ContentBlock.cs`
+- Modify: `src/SharpClaw.Code.Protocol/Serialization/ProtocolJsonContext.cs`
+- Test: `tests/SharpClaw.Code.UnitTests/Protocol/ChatMessageSerializationTests.cs`
+
+- [ ] **Step 1: Write failing test for ChatMessage JSON roundtrip**
+
+```csharp
+// tests/SharpClaw.Code.UnitTests/Protocol/ChatMessageSerializationTests.cs
+using System.Text.Json;
+using FluentAssertions;
+using SharpClaw.Code.Protocol.Models;
+using SharpClaw.Code.Protocol.Serialization;
+
+namespace SharpClaw.Code.UnitTests.Protocol;
+
+public sealed class ChatMessageSerializationTests
+{
+ [Fact]
+ public void ChatMessage_with_text_block_roundtrips()
+ {
+ var message = new ChatMessage("user", [new ContentBlock(ContentBlockKind.Text, "Hello", null, null, null, null)]);
+ var json = JsonSerializer.Serialize(message, ProtocolJsonContext.Default.ChatMessage);
+ var deserialized = JsonSerializer.Deserialize(json, ProtocolJsonContext.Default.ChatMessage);
+
+ deserialized.Should().NotBeNull();
+ deserialized!.Role.Should().Be("user");
+ deserialized.Content.Should().ContainSingle();
+ deserialized.Content[0].Kind.Should().Be(ContentBlockKind.Text);
+ deserialized.Content[0].Text.Should().Be("Hello");
+ }
+
+ [Fact]
+ public void ChatMessage_with_tool_use_block_roundtrips()
+ {
+ var message = new ChatMessage("assistant", [
+ new ContentBlock(ContentBlockKind.ToolUse, null, "call-1", "read_file", "{\"path\":\"a.cs\"}", null)
+ ]);
+ var json = JsonSerializer.Serialize(message, ProtocolJsonContext.Default.ChatMessage);
+ var deserialized = JsonSerializer.Deserialize(json, ProtocolJsonContext.Default.ChatMessage);
+
+ deserialized!.Content[0].Kind.Should().Be(ContentBlockKind.ToolUse);
+ deserialized.Content[0].ToolUseId.Should().Be("call-1");
+ deserialized.Content[0].ToolName.Should().Be("read_file");
+ deserialized.Content[0].ToolInputJson.Should().Be("{\"path\":\"a.cs\"}");
+ }
+
+ [Fact]
+ public void ChatMessage_with_tool_result_block_roundtrips()
+ {
+ var message = new ChatMessage("user", [
+ new ContentBlock(ContentBlockKind.ToolResult, "file contents here", "call-1", null, null, null)
+ ]);
+ var json = JsonSerializer.Serialize(message, ProtocolJsonContext.Default.ChatMessage);
+ var deserialized = JsonSerializer.Deserialize(json, ProtocolJsonContext.Default.ChatMessage);
+
+ deserialized!.Content[0].Kind.Should().Be(ContentBlockKind.ToolResult);
+ deserialized.Content[0].ToolUseId.Should().Be("call-1");
+ deserialized.Content[0].Text.Should().Be("file contents here");
+ }
+}
+```
+
+- [ ] **Step 2: Run test to verify it fails**
+
+Run: `dotnet test SharpClawCode.sln --filter "FullyQualifiedName~ChatMessageSerializationTests" -v minimal`
+Expected: Build failure — `ChatMessage`, `ContentBlock`, `ContentBlockKind` do not exist.
+
+- [ ] **Step 3: Create ContentBlock and ChatMessage models**
+
+```csharp
+// src/SharpClaw.Code.Protocol/Models/ContentBlock.cs
+using System.Text.Json.Serialization;
+
+namespace SharpClaw.Code.Protocol.Models;
+
+///
+/// Describes the kind of content within a message block.
+///
+[JsonConverter(typeof(JsonStringEnumConverter))]
+public enum ContentBlockKind
+{
+ /// Plain text content.
+ [JsonStringEnumMemberName("text")]
+ Text,
+
+ /// A request from the model to invoke a tool.
+ [JsonStringEnumMemberName("tool_use")]
+ ToolUse,
+
+ /// The result of a tool invocation, returned to the model.
+ [JsonStringEnumMemberName("tool_result")]
+ ToolResult,
+}
+
+///
+/// A single content block within a .
+///
+/// The block type discriminator.
+/// Text content (for and ).
+/// The tool invocation identifier (for and ).
+/// The tool name (for ).
+/// The tool input as a JSON string (for ).
+/// Whether the tool result represents an error (for ).
+public sealed record ContentBlock(
+ ContentBlockKind Kind,
+ string? Text,
+ string? ToolUseId,
+ string? ToolName,
+ string? ToolInputJson,
+ bool? IsError);
+```
+
+```csharp
+// src/SharpClaw.Code.Protocol/Models/ChatMessage.cs
+namespace SharpClaw.Code.Protocol.Models;
+
+///
+/// A single message in a conversation history, containing one or more content blocks.
+///
+/// The message role: "user", "assistant", or "system".
+/// The content blocks within this message.
+public sealed record ChatMessage(
+ string Role,
+ IReadOnlyList Content);
+```
+
+- [ ] **Step 4: Register types in ProtocolJsonContext**
+
+Add to the `[JsonSerializable]` attributes in `src/SharpClaw.Code.Protocol/Serialization/ProtocolJsonContext.cs`:
+
+```csharp
+[JsonSerializable(typeof(ChatMessage))]
+[JsonSerializable(typeof(ChatMessage[]))]
+[JsonSerializable(typeof(ContentBlock))]
+[JsonSerializable(typeof(ContentBlock[]))]
+[JsonSerializable(typeof(ContentBlockKind))]
+```
+
+- [ ] **Step 5: Run tests to verify they pass**
+
+Run: `dotnet test SharpClawCode.sln --filter "FullyQualifiedName~ChatMessageSerializationTests" -v minimal`
+Expected: All 3 tests PASS.
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add src/SharpClaw.Code.Protocol/Models/ChatMessage.cs \
+ src/SharpClaw.Code.Protocol/Models/ContentBlock.cs \
+ src/SharpClaw.Code.Protocol/Serialization/ProtocolJsonContext.cs \
+ tests/SharpClaw.Code.UnitTests/Protocol/ChatMessageSerializationTests.cs
+git commit -m "feat(protocol): add ChatMessage and ContentBlock models for conversation history"
+```
+
+---
+
+### Task 2: Protocol — Extend ProviderRequest and ProviderEvent
+
+**Files:**
+- Modify: `src/SharpClaw.Code.Protocol/Models/ProviderRequest.cs`
+- Modify: `src/SharpClaw.Code.Protocol/Models/ProviderEvent.cs`
+- Modify: `src/SharpClaw.Code.Protocol/Models/ToolDefinition.cs`
+
+- [ ] **Step 1: Read current ProviderRequest**
+
+Read: `src/SharpClaw.Code.Protocol/Models/ProviderRequest.cs`
+
+- [ ] **Step 2: Add Messages, Tools, and MaxTokens fields to ProviderRequest**
+
+Add these parameters to the existing `ProviderRequest` record:
+
+```csharp
+IReadOnlyList? Messages,
+IReadOnlyList? Tools,
+int? MaxTokens
+```
+
+Keep `Prompt` and `SystemPrompt` for backward compatibility. When `Messages` is non-null, providers use it; when null, they fall back to constructing a single-user-message from `Prompt`.
+
+- [ ] **Step 3: Add tool-use fields to ProviderEvent**
+
+Add to `ProviderEvent` record:
+
+```csharp
+string? BlockType, // "text", "tool_use", "tool_result"
+string? ToolUseId, // tool call identifier
+string? ToolName, // tool name for tool_use events
+string? ToolInputJson // tool input JSON for tool_use events
+```
+
+- [ ] **Step 4: Add InputSchema to ToolDefinition**
+
+Read current `ToolDefinition` and add:
+
+```csharp
+string? InputSchemaJson // JSON Schema for tool input parameters
+```
+
+- [ ] **Step 5: Build to verify compilation**
+
+Run: `dotnet build SharpClawCode.sln`
+Expected: Build may fail in callers that construct these records positionally. Fix by adding default parameter values or updating call sites.
+
+- [ ] **Step 6: Fix all compilation errors from record changes**
+
+Update all call sites that construct `ProviderRequest`, `ProviderEvent`, and `ToolDefinition` with the new parameters. New fields should default to `null` where not yet used.
+
+- [ ] **Step 7: Run all tests**
+
+Run: `dotnet test SharpClawCode.sln`
+Expected: All existing tests PASS.
+
+- [ ] **Step 8: Commit**
+
+```bash
+git add -A
+git commit -m "feat(protocol): extend ProviderRequest with Messages/Tools, ProviderEvent with tool-use fields"
+```
+
+---
+
+### Task 3: Provider Adapters — Anthropic tool-use extraction
+
+**Files:**
+- Modify: `src/SharpClaw.Code.Providers/Internal/ProviderStreamEventFactory.cs`
+- Modify: `src/SharpClaw.Code.Providers/Internal/AnthropicSdkStreamAdapter.cs`
+- Create: `src/SharpClaw.Code.Providers/Internal/AnthropicMessageBuilder.cs`
+- Modify: `src/SharpClaw.Code.Providers/AnthropicProvider.cs`
+- Test: `tests/SharpClaw.Code.UnitTests/Providers/ToolUseStreamAdapterTests.cs`
+
+- [ ] **Step 1: Write failing test for ProviderStreamEventFactory.ToolUse**
+
+```csharp
+// tests/SharpClaw.Code.UnitTests/Providers/ToolUseStreamAdapterTests.cs
+using FluentAssertions;
+using SharpClaw.Code.Infrastructure.Services;
+using SharpClaw.Code.Providers.Internal;
+
+namespace SharpClaw.Code.UnitTests.Providers;
+
+public sealed class ToolUseStreamAdapterTests
+{
+ [Fact]
+ public void ToolUse_event_contains_tool_metadata()
+ {
+ var clock = new SystemClock();
+ var ev = ProviderStreamEventFactory.ToolUse("req-1", clock, "call-1", "read_file", "{\"path\":\"a.cs\"}");
+
+ ev.Kind.Should().Be("tool_use");
+ ev.BlockType.Should().Be("tool_use");
+ ev.ToolUseId.Should().Be("call-1");
+ ev.ToolName.Should().Be("read_file");
+ ev.ToolInputJson.Should().Be("{\"path\":\"a.cs\"}");
+ ev.IsTerminal.Should().BeFalse();
+ }
+}
+```
+
+- [ ] **Step 2: Run test to verify it fails**
+
+Expected: `ProviderStreamEventFactory.ToolUse` does not exist.
+
+- [ ] **Step 3: Add ToolUse factory method to ProviderStreamEventFactory**
+
+```csharp
+///
+/// Creates a non-terminal event representing a tool-use request from the model.
+///
+public static ProviderEvent ToolUse(string requestId, ISystemClock clock, string toolUseId, string toolName, string toolInputJson)
+ => new(
+ Id: $"provider-event-{Guid.NewGuid():N}",
+ RequestId: requestId,
+ Kind: "tool_use",
+ CreatedAtUtc: clock.UtcNow,
+ Content: null,
+ IsTerminal: false,
+ Usage: null,
+ BlockType: "tool_use",
+ ToolUseId: toolUseId,
+ ToolName: toolName,
+ ToolInputJson: toolInputJson);
+```
+
+- [ ] **Step 4: Run test to verify it passes**
+
+- [ ] **Step 5: Create AnthropicMessageBuilder**
+
+This class maps `ChatMessage[]` and `ToolDefinition[]` to Anthropic SDK `MessageCreateParams`:
+
+```csharp
+// src/SharpClaw.Code.Providers/Internal/AnthropicMessageBuilder.cs
+// - Converts ChatMessage[] to MessageParam[] (mapping roles and content blocks)
+// - Converts ToolDefinition[] to Anthropic Tool[] (mapping name, description, input schema)
+// - Handles tool_result content blocks as ToolResultBlockParam
+```
+
+- [ ] **Step 6: Update AnthropicSdkStreamAdapter to extract tool_use blocks**
+
+In the stream consumption loop, handle `TryPickContentBlockStart` for tool-use blocks. Accumulate tool input JSON from deltas. On `ContentBlockStop`, yield a `ProviderStreamEventFactory.ToolUse(...)` event.
+
+- [ ] **Step 7: Update AnthropicProvider.StartStreamAsync to accept Messages and Tools**
+
+When `request.Messages` is non-null, use `AnthropicMessageBuilder` to construct params instead of building a single-message request.
+
+- [ ] **Step 8: Run all tests**
+
+Run: `dotnet test SharpClawCode.sln`
+Expected: All tests PASS including new tool-use adapter tests.
+
+- [ ] **Step 9: Commit**
+
+```bash
+git add -A
+git commit -m "feat(providers): Anthropic adapter extracts tool-use blocks and accepts message history"
+```
+
+---
+
+### Task 4: Provider Adapters — OpenAI tool-use extraction
+
+**Files:**
+- Modify: `src/SharpClaw.Code.Providers/Internal/OpenAiMeaiStreamAdapter.cs`
+- Create: `src/SharpClaw.Code.Providers/Internal/OpenAiMessageBuilder.cs`
+- Modify: `src/SharpClaw.Code.Providers/OpenAiCompatibleProvider.cs`
+- Test: `tests/SharpClaw.Code.UnitTests/Providers/ToolUseStreamAdapterTests.cs` (add tests)
+
+- [ ] **Step 1: Write failing test for OpenAI tool-call extraction**
+
+Add to `ToolUseStreamAdapterTests`:
+
+```csharp
+[Fact]
+public async Task OpenAi_adapter_yields_tool_use_from_function_call()
+{
+ // Create a mock IAsyncEnumerable that contains a FunctionCallContent
+ // Verify the adapter emits a ProviderEvent with Kind="tool_use"
+}
+```
+
+- [ ] **Step 2: Create OpenAiMessageBuilder**
+
+Maps `ChatMessage[]` and `ToolDefinition[]` to `Microsoft.Extensions.AI.ChatMessage` list and `ChatOptions.Tools`.
+
+- [ ] **Step 3: Update OpenAiMeaiStreamAdapter to extract function calls**
+
+When a `ChatResponseUpdate` contains `FunctionCallContent` in its `Contents`, yield a `ToolUse` event.
+
+- [ ] **Step 4: Update OpenAiCompatibleProvider.StreamEventsAsync to accept Messages and Tools**
+
+When `request.Messages` is non-null, use `OpenAiMessageBuilder`.
+
+- [ ] **Step 5: Run all tests**
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add -A
+git commit -m "feat(providers): OpenAI adapter extracts tool calls and accepts message history"
+```
+
+---
+
+### Task 5: Agent — ToolCallDispatcher
+
+**Files:**
+- Create: `src/SharpClaw.Code.Agents/Internal/ToolCallDispatcher.cs`
+- Test: `tests/SharpClaw.Code.UnitTests/Agents/ToolCallDispatcherTests.cs`
+
+- [ ] **Step 1: Write failing test for ToolCallDispatcher**
+
+```csharp
+// tests/SharpClaw.Code.UnitTests/Agents/ToolCallDispatcherTests.cs
+using FluentAssertions;
+using SharpClaw.Code.Agents.Internal;
+using SharpClaw.Code.Protocol.Models;
+
+namespace SharpClaw.Code.UnitTests.Agents;
+
+public sealed class ToolCallDispatcherTests
+{
+ [Fact]
+ public async Task DispatchAsync_executes_tool_and_returns_result_block()
+ {
+ // Arrange: create a mock IToolExecutor that returns a success ToolResult
+ // Create a ToolCallDispatcher with the mock executor
+ // Create a ProviderEvent with Kind="tool_use", ToolName="read_file"
+ //
+ // Act: call dispatcher.DispatchAsync(event, context, cancellationToken)
+ //
+ // Assert: result is a ContentBlock with Kind=ToolResult, ToolUseId matches, Text contains output
+ }
+
+ [Fact]
+ public async Task DispatchAsync_returns_error_block_on_tool_failure()
+ {
+ // Arrange: mock IToolExecutor that returns Succeeded=false
+ // Act + Assert: result ContentBlock has IsError=true and contains error message
+ }
+}
+```
+
+- [ ] **Step 2: Implement ToolCallDispatcher**
+
+```csharp
+// src/SharpClaw.Code.Agents/Internal/ToolCallDispatcher.cs
+// Responsibility:
+// - Takes a ProviderEvent with Kind="tool_use"
+// - Builds a ToolExecutionRequest from the event's ToolName and ToolInputJson
+// - Calls IToolExecutor.ExecuteAsync(...)
+// - Publishes ToolStartedEvent and ToolCompletedEvent via IRuntimeEventPublisher
+// - Returns a ContentBlock(ToolResult, output, toolUseId, ...) for feeding back to the provider
+```
+
+- [ ] **Step 3: Run tests**
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add -A
+git commit -m "feat(agents): add ToolCallDispatcher to bridge tool-use events to IToolExecutor"
+```
+
+---
+
+### Task 6: Agent — Tool-Calling Loop in ProviderBackedAgentKernel
+
+**Files:**
+- Modify: `src/SharpClaw.Code.Agents/Internal/ProviderBackedAgentKernel.cs`
+- Create: `src/SharpClaw.Code.Agents/Configuration/AgentLoopOptions.cs`
+- Modify: `src/SharpClaw.Code.Agents/Services/AgentFrameworkBridge.cs`
+- Modify: `tests/SharpClaw.Code.MockProvider/DeterministicMockModelProvider.cs` (add tool-use scenario)
+- Modify: `tests/SharpClaw.Code.ParityHarness/ParityScenarioTests.cs` (add tool-calling test)
+
+- [ ] **Step 1: Create AgentLoopOptions**
+
+```csharp
+// src/SharpClaw.Code.Agents/Configuration/AgentLoopOptions.cs
+namespace SharpClaw.Code.Agents.Configuration;
+
+///
+/// Configuration for the agent tool-calling loop.
+///
+public sealed class AgentLoopOptions
+{
+ /// Maximum number of tool-calling iterations before forcing termination.
+ public int MaxToolIterations { get; set; } = 25;
+
+ /// Maximum tokens per provider request within a turn.
+ public int MaxTokensPerRequest { get; set; } = 16_384;
+}
+```
+
+- [ ] **Step 2: Implement the tool-calling loop in ProviderBackedAgentKernel**
+
+Modify `ExecuteAsync` to:
+
+1. Build initial `ChatMessage[]` from the request (system prompt + user message + history)
+2. Collect available `ToolDefinition[]` from the tool registry
+3. **Loop:**
+ a. Call `provider.StartStreamAsync(request)` with messages and tools
+ b. Consume stream events, accumulating text deltas and collecting tool-use events
+ c. If no tool-use events: break (model is done)
+ d. If tool-use events present:
+ - Build an assistant message with the tool-use content blocks
+ - For each tool-use: call `ToolCallDispatcher.DispatchAsync(...)` to execute
+ - Build a user message with tool-result content blocks
+ - Append both messages to the conversation
+ - Increment iteration counter; if >= `MaxToolIterations`, break with warning
+ e. Continue loop
+4. Return the accumulated text output and collected events
+
+- [ ] **Step 3: Update AgentFrameworkBridge to pass tool definitions**
+
+The bridge should:
+- Resolve available tools via `IToolRegistry`
+- Map `ToolDefinition` instances to include `InputSchemaJson` for provider consumption
+- Pass tools and any existing conversation messages to the kernel
+
+- [ ] **Step 4: Add tool-use scenario to DeterministicMockModelProvider**
+
+Add a new scenario `tool_call_roundtrip` that:
+1. First call: yields a tool-use event for `read_file` with `{"path":"test.txt"}`
+2. Second call (after receiving tool result): yields text delta "File content is: {result}" + completion
+
+- [ ] **Step 5: Add parity test for tool-calling loop**
+
+Add to `ParityScenarioTests`:
+
+```csharp
+[Fact]
+public async Task Tool_call_roundtrip_executes_tool_and_resumes()
+{
+ // Uses tool_call_roundtrip scenario
+ // Verifies: tool-use event emitted, tool executed, result fed back, final text output assembled
+}
+```
+
+- [ ] **Step 6: Run all tests**
+
+Run: `dotnet test SharpClawCode.sln`
+Expected: All tests PASS including new tool-calling parity test.
+
+- [ ] **Step 7: Commit**
+
+```bash
+git add -A
+git commit -m "feat(agents): implement tool-calling loop in ProviderBackedAgentKernel"
+```
+
+---
+
+### Task 7: Runtime — Conversation History Assembly
+
+**Files:**
+- Create: `src/SharpClaw.Code.Runtime/Context/ConversationHistoryAssembler.cs`
+- Create: `src/SharpClaw.Code.Runtime/Context/ContextWindowManager.cs`
+- Modify: `src/SharpClaw.Code.Runtime/Context/PromptContextAssembler.cs`
+- Modify: `src/SharpClaw.Code.Runtime/Turns/DefaultTurnRunner.cs`
+- Test: `tests/SharpClaw.Code.UnitTests/Runtime/ConversationHistoryAssemblerTests.cs`
+- Test: `tests/SharpClaw.Code.UnitTests/Runtime/ContextWindowManagerTests.cs`
+
+- [ ] **Step 1: Write failing test for ConversationHistoryAssembler**
+
+```csharp
+// tests/SharpClaw.Code.UnitTests/Runtime/ConversationHistoryAssemblerTests.cs
+using FluentAssertions;
+using SharpClaw.Code.Protocol.Events;
+using SharpClaw.Code.Protocol.Models;
+using SharpClaw.Code.Runtime.Context;
+
+namespace SharpClaw.Code.UnitTests.Runtime;
+
+public sealed class ConversationHistoryAssemblerTests
+{
+ [Fact]
+ public void Assembles_user_assistant_pairs_from_turn_events()
+ {
+ // Given: a list of RuntimeEvents containing TurnStartedEvent (with Input)
+ // and TurnCompletedEvent (with Output) for 2 completed turns
+ // When: ConversationHistoryAssembler.Assemble(events)
+ // Then: returns ChatMessage[] with [user, assistant, user, assistant] pattern
+ }
+
+ [Fact]
+ public void Includes_tool_use_and_tool_result_from_events()
+ {
+ // Given: events containing ToolStartedEvent and ToolCompletedEvent within a turn
+ // When: assembled
+ // Then: assistant message contains tool_use blocks, user message contains tool_result blocks
+ }
+
+ [Fact]
+ public void Skips_incomplete_turns()
+ {
+ // Given: a TurnStartedEvent with no matching TurnCompletedEvent
+ // When: assembled
+ // Then: incomplete turn is excluded from history
+ }
+}
+```
+
+- [ ] **Step 2: Implement ConversationHistoryAssembler**
+
+```csharp
+// src/SharpClaw.Code.Runtime/Context/ConversationHistoryAssembler.cs
+// Responsibility:
+// - Takes IReadOnlyList (from IEventStore.ReadAllAsync)
+// - Groups events by TurnId
+// - For each completed turn: builds user message (from input) and assistant message (from output + tool blocks)
+// - Returns ChatMessage[]
+```
+
+- [ ] **Step 3: Write failing test for ContextWindowManager**
+
+```csharp
+// tests/SharpClaw.Code.UnitTests/Runtime/ContextWindowManagerTests.cs
+public sealed class ContextWindowManagerTests
+{
+ [Fact]
+ public void Truncates_oldest_messages_when_over_token_budget()
+ {
+ // Given: 10 messages totaling ~5000 tokens, budget of 2000
+ // When: ContextWindowManager.Truncate(messages, maxTokens: 2000)
+ // Then: returns the most recent messages that fit within budget
+ // system message (if present) is always retained
+ }
+}
+```
+
+- [ ] **Step 4: Implement ContextWindowManager**
+
+```csharp
+// src/SharpClaw.Code.Runtime/Context/ContextWindowManager.cs
+// Responsibility:
+// - Takes ChatMessage[] and a max token budget
+// - Estimates token count per message (simple word-based heuristic: words * 1.3)
+// - Always keeps system messages
+// - Drops oldest non-system messages until total fits within budget
+// - Returns truncated ChatMessage[]
+```
+
+- [ ] **Step 5: Integrate into PromptContextAssembler and DefaultTurnRunner**
+
+Update `PromptContextAssembler` to:
+- Call `IEventStore.ReadAllAsync` for the current session
+- Pass events to `ConversationHistoryAssembler.Assemble()`
+- Apply `ContextWindowManager.Truncate()` with configured max tokens
+- Return messages as part of the assembled context
+
+Update `DefaultTurnRunner` to:
+- Pass the assembled `ChatMessage[]` through to the agent's `AgentRunContext`
+
+- [ ] **Step 6: Run all tests**
+
+Run: `dotnet test SharpClawCode.sln`
+Expected: All tests PASS.
+
+- [ ] **Step 7: Commit**
+
+```bash
+git add -A
+git commit -m "feat(runtime): assemble conversation history from session events with context window management"
+```
+
+---
+
+## P1 — CI/CD, NuGet Packages, Documentation
+
+These are independent of each other and of the P0 work. Can be parallelized.
+
+### Task 8: CI/CD Pipeline
+
+**Files:**
+- Create: `.github/workflows/ci.yml`
+- Create: `.github/workflows/release.yml`
+
+- [ ] **Step 1: Create CI workflow**
+
+```yaml
+# .github/workflows/ci.yml
+name: CI
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ branches: [main]
+
+permissions:
+ contents: read
+
+jobs:
+ build-and-test:
+ strategy:
+ matrix:
+ os: [ubuntu-latest, windows-latest, macos-latest]
+ runs-on: ${{ matrix.os }}
+ steps:
+ - uses: actions/checkout@v4
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: '10.0.x'
+ - name: Restore
+ run: dotnet restore SharpClawCode.sln
+ - name: Build
+ run: dotnet build SharpClawCode.sln --no-restore --configuration Release
+ - name: Test
+ run: dotnet test SharpClawCode.sln --no-build --configuration Release --collect:"XPlat Code Coverage" --results-directory ./coverage
+ - name: Upload coverage
+ if: matrix.os == 'ubuntu-latest'
+ uses: actions/upload-artifact@v4
+ with:
+ name: coverage-report
+ path: ./coverage/**/coverage.cobertura.xml
+```
+
+- [ ] **Step 2: Create release workflow**
+
+```yaml
+# .github/workflows/release.yml
+name: Release
+
+on:
+ push:
+ tags: ['v*']
+
+permissions:
+ contents: read
+ packages: write
+
+jobs:
+ publish:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: '10.0.x'
+ - name: Restore
+ run: dotnet restore SharpClawCode.sln
+ - name: Build
+ run: dotnet build SharpClawCode.sln --no-restore --configuration Release
+ - name: Test
+ run: dotnet test SharpClawCode.sln --no-build --configuration Release
+ - name: Pack
+ run: dotnet pack SharpClawCode.sln --no-build --configuration Release --output ./nupkg
+ - name: Push to NuGet
+ run: dotnet nuget push ./nupkg/*.nupkg --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate
+```
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add .github/workflows/ci.yml .github/workflows/release.yml
+git commit -m "ci: add GitHub Actions workflows for CI and NuGet release"
+```
+
+---
+
+### Task 9: NuGet Package Metadata
+
+**Files:**
+- Modify: `Directory.Build.props`
+- Modify: Per-project `.csproj` files that should be packaged
+
+- [ ] **Step 1: Add shared NuGet metadata to Directory.Build.props**
+
+```xml
+
+clawdotnet
+clawdotnet
+MIT
+https://github.com/clawdotnet/SharpClawCode
+https://github.com/clawdotnet/SharpClawCode
+git
+README.md
+icon.png
+Copyright (c) 2025 clawdotnet
+```
+
+- [ ] **Step 2: Add per-project PackageId and Description to each publishable csproj**
+
+For each of these projects, add `` and ``:
+
+- `SharpClaw.Code.Protocol` — "Core contracts and models for the SharpClaw Code agent runtime."
+- `SharpClaw.Code.Runtime` — "Production runtime orchestration for SharpClaw Code agents."
+- `SharpClaw.Code.Providers` — "Anthropic and OpenAI-compatible provider integration for SharpClaw Code."
+- `SharpClaw.Code.Tools` — "Built-in tools and tool execution framework for SharpClaw Code."
+- `SharpClaw.Code.Mcp` — "Model Context Protocol client integration for SharpClaw Code."
+- `SharpClaw.Code.Sessions` — "Durable session persistence for SharpClaw Code."
+- `SharpClaw.Code.Permissions` — "Permission policy engine for SharpClaw Code tool execution."
+- `SharpClaw.Code.Telemetry` — "Structured telemetry and event publishing for SharpClaw Code."
+- `SharpClaw.Code.Agents` — "Microsoft Agent Framework integration for SharpClaw Code."
+- `SharpClaw.Code.Infrastructure` — "Shared infrastructure services for SharpClaw Code."
+
+Mark test projects with `false` (most already have this).
+
+- [ ] **Step 3: Verify pack works**
+
+Run: `dotnet pack SharpClawCode.sln --configuration Release --output ./nupkg`
+Expected: `.nupkg` files created for each publishable project.
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add Directory.Build.props src/**/*.csproj
+git commit -m "build: add NuGet package metadata for publishing"
+```
+
+---
+
+### Task 10: Documentation — Getting Started Guide
+
+**Files:**
+- Create: `docs/getting-started.md`
+
+- [ ] **Step 1: Write the getting started guide**
+
+Structure:
+1. Prerequisites (.NET 10 SDK)
+2. Install via NuGet (`dotnet add package SharpClaw.Code.Runtime`)
+3. Minimal console agent (15 lines: create host, add runtime, run prompt)
+4. Add a custom tool
+5. Enable session persistence
+6. Next steps (links to architecture, tools, providers docs)
+
+The guide should be self-contained and copy-pasteable.
+
+- [ ] **Step 2: Commit**
+
+```bash
+git add docs/getting-started.md
+git commit -m "docs: add getting started guide"
+```
+
+---
+
+### Task 11: Documentation — Agent Framework Integration Guide
+
+**Files:**
+- Create: `docs/agent-framework-integration.md`
+
+- [ ] **Step 1: Write the integration guide**
+
+Structure:
+1. How SharpClaw builds on Microsoft Agent Framework
+2. Architecture diagram: Agent Framework → SharpClaw Runtime → Providers/Tools/Sessions
+3. Key integration points: `ProviderBackedAgentKernel`, `AgentFrameworkBridge`, `SharpClawFrameworkAgent`
+4. How to extend: adding a custom agent type, custom provider, custom tools
+5. Comparison table: what Agent Framework provides vs. what SharpClaw adds
+
+- [ ] **Step 2: Commit**
+
+```bash
+git add docs/agent-framework-integration.md
+git commit -m "docs: add Microsoft Agent Framework integration guide"
+```
+
+---
+
+### Task 12: Example Projects
+
+**Files:**
+- Create: `examples/MinimalConsoleAgent/MinimalConsoleAgent.csproj`
+- Create: `examples/MinimalConsoleAgent/Program.cs`
+- Create: `examples/WebApiAgent/WebApiAgent.csproj`
+- Create: `examples/WebApiAgent/Program.cs`
+- Create: `examples/McpToolAgent/McpToolAgent.csproj`
+- Create: `examples/McpToolAgent/Program.cs`
+
+- [ ] **Step 1: Create MinimalConsoleAgent**
+
+A ~30-line console app that:
+- Creates a host with `AddSharpClawRuntime(configuration)`
+- Reads a prompt from args or stdin
+- Calls `IConversationRuntime.RunPromptAsync()`
+- Prints the output
+
+- [ ] **Step 2: Create WebApiAgent**
+
+An ASP.NET Core minimal API that:
+- Exposes `POST /chat` accepting `{ "prompt": "..." }`
+- Returns `{ "response": "...", "sessionId": "..." }`
+- Persists sessions across requests
+
+- [ ] **Step 3: Create McpToolAgent**
+
+A console app that:
+- Registers a custom MCP server
+- Registers a custom built-in tool
+- Runs a prompt that exercises both
+
+- [ ] **Step 4: Verify examples build**
+
+Run: `dotnet build examples/MinimalConsoleAgent/MinimalConsoleAgent.csproj` (and others)
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add examples/
+git commit -m "docs: add example projects (minimal console, web API, MCP tools)"
+```
+
+---
+
+## P2 — Provider Resilience and Observability
+
+### Task 13: Provider Resilience
+
+**Files:**
+- Create: `src/SharpClaw.Code.Providers/Resilience/RetryHandler.cs`
+- Create: `src/SharpClaw.Code.Providers/Resilience/RateLimitHandler.cs`
+- Create: `src/SharpClaw.Code.Providers/Resilience/CircuitBreakerHandler.cs`
+- Create: `src/SharpClaw.Code.Providers/Configuration/ProviderResilienceOptions.cs`
+- Create: `src/SharpClaw.Code.Providers/Services/ResilientProviderDecorator.cs`
+- Modify: `src/SharpClaw.Code.Providers/ProvidersServiceCollectionExtensions.cs`
+- Test: `tests/SharpClaw.Code.UnitTests/Providers/ResilienceTests.cs`
+
+- [ ] **Step 1: Create ProviderResilienceOptions**
+
+```csharp
+public sealed class ProviderResilienceOptions
+{
+ public int MaxRetries { get; set; } = 3;
+ public TimeSpan InitialRetryDelay { get; set; } = TimeSpan.FromMilliseconds(500);
+ public TimeSpan MaxRetryDelay { get; set; } = TimeSpan.FromSeconds(30);
+ public TimeSpan RequestTimeout { get; set; } = TimeSpan.FromMinutes(5);
+ public int CircuitBreakerFailureThreshold { get; set; } = 5;
+ public TimeSpan CircuitBreakerBreakDuration { get; set; } = TimeSpan.FromSeconds(30);
+}
+```
+
+- [ ] **Step 2: Write failing tests for RetryHandler**
+
+```csharp
+// tests/SharpClaw.Code.UnitTests/Providers/ResilienceTests.cs
+public sealed class ResilienceTests
+{
+ [Fact]
+ public async Task RetryHandler_retries_on_transient_failure()
+ {
+ // Mock provider that fails twice then succeeds
+ // Verify: 3 total attempts, final result is success
+ }
+
+ [Fact]
+ public async Task RetryHandler_respects_429_retry_after_header()
+ {
+ // Mock provider that returns 429 with Retry-After: 1
+ // Verify: waits at least 1 second before retry
+ }
+
+ [Fact]
+ public async Task CircuitBreaker_opens_after_threshold_failures()
+ {
+ // Mock provider that always fails
+ // Call 5 times (threshold)
+ // 6th call should fail immediately without calling provider
+ }
+}
+```
+
+- [ ] **Step 3: Implement RetryHandler**
+
+Exponential backoff with jitter. Catches `HttpRequestException` with 5xx status codes and `TaskCanceledException` (timeout). Respects `Retry-After` header for 429s.
+
+- [ ] **Step 4: Implement CircuitBreakerHandler**
+
+Three states: Closed (normal), Open (reject fast), Half-Open (allow one probe). Transitions based on failure count and time elapsed.
+
+- [ ] **Step 5: Implement ResilientProviderDecorator**
+
+Wraps `IModelProvider` with retry → circuit breaker → timeout pipeline. Registered as a decorator in DI.
+
+- [ ] **Step 6: Register in ProvidersServiceCollectionExtensions**
+
+Wrap each `IModelProvider` registration with `ResilientProviderDecorator` when `ProviderResilienceOptions` is configured.
+
+- [ ] **Step 7: Run all tests**
+
+- [ ] **Step 8: Commit**
+
+```bash
+git add -A
+git commit -m "feat(providers): add retry, rate-limit, and circuit-breaker resilience"
+```
+
+---
+
+### Task 14: Observability — OpenTelemetry Integration
+
+**Files:**
+- Create: `src/SharpClaw.Code.Telemetry/Diagnostics/SharpClawActivitySource.cs`
+- Create: `src/SharpClaw.Code.Telemetry/Diagnostics/TurnActivityScope.cs`
+- Create: `src/SharpClaw.Code.Telemetry/Diagnostics/ProviderActivityScope.cs`
+- Create: `src/SharpClaw.Code.Telemetry/Metrics/SharpClawMeterSource.cs`
+- Create: `src/SharpClaw.Code.Telemetry/Export/NdjsonTraceFileSink.cs`
+- Modify: `src/SharpClaw.Code.Runtime/Turns/DefaultTurnRunner.cs`
+- Modify: `src/SharpClaw.Code.Agents/Internal/ProviderBackedAgentKernel.cs`
+
+- [ ] **Step 1: Create SharpClawActivitySource**
+
+```csharp
+// src/SharpClaw.Code.Telemetry/Diagnostics/SharpClawActivitySource.cs
+using System.Diagnostics;
+
+namespace SharpClaw.Code.Telemetry.Diagnostics;
+
+///
+/// Central ActivitySource for OpenTelemetry distributed tracing.
+///
+public static class SharpClawActivitySource
+{
+ public const string SourceName = "SharpClaw.Code";
+ public static readonly ActivitySource Instance = new(SourceName, "1.0.0");
+}
+```
+
+- [ ] **Step 2: Create TurnActivityScope and ProviderActivityScope**
+
+Thin wrappers that start an `Activity` with appropriate tags (session ID, turn ID, provider name, model, token counts).
+
+- [ ] **Step 3: Create SharpClawMeterSource**
+
+```csharp
+// Counters: sharpclaw.tokens.input, sharpclaw.tokens.output
+// Histograms: sharpclaw.turn.duration_ms, sharpclaw.provider.duration_ms, sharpclaw.tool.duration_ms
+```
+
+- [ ] **Step 4: Integrate spans into DefaultTurnRunner and ProviderBackedAgentKernel**
+
+Wrap turn execution and provider calls in Activity scopes. Record token usage in meters.
+
+- [ ] **Step 5: Create NdjsonTraceFileSink**
+
+Optional file-based exporter that writes Activity spans as NDJSON for offline analysis.
+
+- [ ] **Step 6: Run all tests**
+
+- [ ] **Step 7: Commit**
+
+```bash
+git add -A
+git commit -m "feat(telemetry): add OpenTelemetry spans, metrics, and NDJSON trace sink"
+```
+
+---
+
+## Execution Order Summary
+
+```
+P0 (sequential, bottom-up):
+ Task 1: Protocol models (ChatMessage, ContentBlock)
+ Task 2: Protocol extensions (ProviderRequest, ProviderEvent)
+ Task 3: Anthropic tool-use adapter
+ Task 4: OpenAI tool-use adapter
+ Task 5: ToolCallDispatcher
+ Task 6: Tool-calling loop in agent kernel
+ Task 7: Conversation history assembly
+
+P1 (parallel with each other, parallel with P0 after Task 2):
+ Task 8: CI/CD pipeline
+ Task 9: NuGet package metadata
+ Task 10: Getting started guide
+ Task 11: Agent Framework integration guide
+ Task 12: Example projects
+
+P2 (after P0, parallel with each other):
+ Task 13: Provider resilience
+ Task 14: Observability
+```
+
+**Estimated total: 14 tasks, ~40-60 hours of implementation work.**
diff --git a/examples/McpToolAgent/EchoTool.cs b/examples/McpToolAgent/EchoTool.cs
new file mode 100644
index 0000000..2477e8e
--- /dev/null
+++ b/examples/McpToolAgent/EchoTool.cs
@@ -0,0 +1,52 @@
+using SharpClaw.Code.Protocol.Enums;
+using SharpClaw.Code.Protocol.Models;
+using SharpClaw.Code.Tools.BuiltIn;
+using SharpClaw.Code.Tools.Models;
+
+namespace McpToolAgent;
+
+///
+/// A simple echo tool that returns its input unchanged.
+/// Demonstrates the minimal pattern for implementing a custom SharpClaw tool.
+///
+public sealed class EchoTool : SharpClawToolBase
+{
+ ///
+ /// The stable tool name used by the agent to invoke this tool.
+ ///
+ public const string ToolName = "echo";
+
+ ///
+ public override ToolDefinition Definition { get; } = new(
+ Name: ToolName,
+ Description: "Returns the supplied message unchanged. Useful for testing tool dispatch.",
+ ApprovalScope: ApprovalScope.None,
+ IsDestructive: false,
+ RequiresApproval: false,
+ InputTypeName: nameof(EchoToolArguments),
+ InputDescription: "JSON object with a single 'message' string field.",
+ Tags: ["echo", "test", "example"]);
+
+ ///
+ public override Task ExecuteAsync(
+ ToolExecutionContext context,
+ ToolExecutionRequest request,
+ CancellationToken cancellationToken)
+ {
+ var arguments = DeserializeArguments(request);
+ var payload = new EchoToolResult(arguments.Message);
+ return Task.FromResult(CreateSuccessResult(context, request, arguments.Message, payload));
+ }
+}
+
+///
+/// Arguments accepted by .
+///
+/// The message to echo back.
+public sealed record EchoToolArguments(string Message);
+
+///
+/// Structured result produced by .
+///
+/// The echoed message.
+public sealed record EchoToolResult(string Message);
diff --git a/examples/McpToolAgent/McpToolAgent.csproj b/examples/McpToolAgent/McpToolAgent.csproj
new file mode 100644
index 0000000..bbc30f8
--- /dev/null
+++ b/examples/McpToolAgent/McpToolAgent.csproj
@@ -0,0 +1,17 @@
+
+
+
+ Exe
+ Custom tool agent example for SharpClaw Code.
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/McpToolAgent/Program.cs b/examples/McpToolAgent/Program.cs
new file mode 100644
index 0000000..38d9e7a
--- /dev/null
+++ b/examples/McpToolAgent/Program.cs
@@ -0,0 +1,42 @@
+using McpToolAgent;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using SharpClaw.Code.Protocol.Commands;
+using SharpClaw.Code.Protocol.Enums;
+using SharpClaw.Code.Runtime.Abstractions;
+using SharpClaw.Code.Runtime.Composition;
+using SharpClaw.Code.Tools.Abstractions;
+
+var builder = Host.CreateApplicationBuilder(args);
+builder.Services.AddSharpClawRuntime(builder.Configuration);
+
+// Register the custom echo tool so the agent can invoke it during turns.
+builder.Services.AddSingleton();
+builder.Services.AddSingleton(sp => sp.GetRequiredService());
+
+using var host = builder.Build();
+await host.StartAsync();
+
+var runtime = host.Services.GetRequiredService();
+
+var workspacePath = Directory.GetCurrentDirectory();
+var session = await runtime.CreateSessionAsync(
+ workspacePath,
+ PermissionMode.ReadOnly,
+ OutputFormat.Text,
+ CancellationToken.None);
+
+// Ask the agent to use the echo tool.
+var request = new RunPromptRequest(
+ Prompt: "Use the echo tool to echo the message: Hello from SharpClaw!",
+ SessionId: session.Id,
+ WorkingDirectory: workspacePath,
+ PermissionMode: PermissionMode.ReadOnly,
+ OutputFormat: OutputFormat.Text,
+ Metadata: null);
+
+var result = await runtime.RunPromptAsync(request, CancellationToken.None);
+
+Console.WriteLine(result.FinalOutput ?? "(no output)");
+
+await host.StopAsync();
diff --git a/examples/McpToolAgent/appsettings.json b/examples/McpToolAgent/appsettings.json
new file mode 100644
index 0000000..0cff851
--- /dev/null
+++ b/examples/McpToolAgent/appsettings.json
@@ -0,0 +1,19 @@
+{
+ "SharpClaw": {
+ "Providers": {
+ "Catalog": {
+ "DefaultProvider": "Anthropic"
+ },
+ "Anthropic": {
+ "ApiKey": "",
+ "DefaultModel": "claude-sonnet-4-5"
+ }
+ }
+ },
+ "Logging": {
+ "LogLevel": {
+ "Default": "Warning",
+ "SharpClaw": "Information"
+ }
+ }
+}
diff --git a/examples/MinimalConsoleAgent/MinimalConsoleAgent.csproj b/examples/MinimalConsoleAgent/MinimalConsoleAgent.csproj
new file mode 100644
index 0000000..09f2de4
--- /dev/null
+++ b/examples/MinimalConsoleAgent/MinimalConsoleAgent.csproj
@@ -0,0 +1,16 @@
+
+
+
+ Exe
+ Minimal console agent example for SharpClaw Code.
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/MinimalConsoleAgent/Program.cs b/examples/MinimalConsoleAgent/Program.cs
new file mode 100644
index 0000000..8d53747
--- /dev/null
+++ b/examples/MinimalConsoleAgent/Program.cs
@@ -0,0 +1,44 @@
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using SharpClaw.Code.Protocol.Commands;
+using SharpClaw.Code.Protocol.Enums;
+using SharpClaw.Code.Runtime.Abstractions;
+using SharpClaw.Code.Runtime.Composition;
+
+if (args.Length == 0)
+{
+ Console.Error.WriteLine("Usage: MinimalConsoleAgent ");
+ return 1;
+}
+
+var prompt = string.Join(' ', args);
+
+var builder = Host.CreateApplicationBuilder(args);
+builder.Services.AddSharpClawRuntime(builder.Configuration);
+
+using var host = builder.Build();
+await host.StartAsync();
+
+var runtime = host.Services.GetRequiredService();
+
+var workspacePath = Directory.GetCurrentDirectory();
+var session = await runtime.CreateSessionAsync(
+ workspacePath,
+ PermissionMode.ReadOnly,
+ OutputFormat.Text,
+ CancellationToken.None);
+
+var request = new RunPromptRequest(
+ Prompt: prompt,
+ SessionId: session.Id,
+ WorkingDirectory: workspacePath,
+ PermissionMode: PermissionMode.ReadOnly,
+ OutputFormat: OutputFormat.Text,
+ Metadata: null);
+
+var result = await runtime.RunPromptAsync(request, CancellationToken.None);
+
+Console.WriteLine(result.FinalOutput ?? "(no output)");
+
+await host.StopAsync();
+return 0;
diff --git a/examples/MinimalConsoleAgent/appsettings.json b/examples/MinimalConsoleAgent/appsettings.json
new file mode 100644
index 0000000..0cff851
--- /dev/null
+++ b/examples/MinimalConsoleAgent/appsettings.json
@@ -0,0 +1,19 @@
+{
+ "SharpClaw": {
+ "Providers": {
+ "Catalog": {
+ "DefaultProvider": "Anthropic"
+ },
+ "Anthropic": {
+ "ApiKey": "",
+ "DefaultModel": "claude-sonnet-4-5"
+ }
+ }
+ },
+ "Logging": {
+ "LogLevel": {
+ "Default": "Warning",
+ "SharpClaw": "Information"
+ }
+ }
+}
diff --git a/examples/WebApiAgent/Program.cs b/examples/WebApiAgent/Program.cs
new file mode 100644
index 0000000..f83023a
--- /dev/null
+++ b/examples/WebApiAgent/Program.cs
@@ -0,0 +1,46 @@
+using SharpClaw.Code.Protocol.Commands;
+using SharpClaw.Code.Protocol.Enums;
+using SharpClaw.Code.Runtime.Abstractions;
+using SharpClaw.Code.Runtime.Composition;
+
+var builder = WebApplication.CreateBuilder(args);
+builder.Services.AddSharpClawRuntime(builder.Configuration);
+
+var app = builder.Build();
+
+app.MapPost("/chat", async (ChatRequest body, IConversationRuntime runtime, CancellationToken ct) =>
+{
+ var workspacePath = Directory.GetCurrentDirectory();
+
+ string sessionId;
+ if (!string.IsNullOrWhiteSpace(body.SessionId))
+ {
+ sessionId = body.SessionId;
+ }
+ else
+ {
+ var session = await runtime.CreateSessionAsync(
+ workspacePath,
+ PermissionMode.ReadOnly,
+ OutputFormat.Text,
+ ct);
+ sessionId = session.Id;
+ }
+
+ var request = new RunPromptRequest(
+ Prompt: body.Prompt,
+ SessionId: sessionId,
+ WorkingDirectory: workspacePath,
+ PermissionMode: PermissionMode.ReadOnly,
+ OutputFormat: OutputFormat.Text,
+ Metadata: null);
+
+ var result = await runtime.RunPromptAsync(request, ct);
+
+ return Results.Ok(new ChatResponse(result.FinalOutput ?? string.Empty, sessionId));
+});
+
+app.Run();
+
+record ChatRequest(string Prompt, string? SessionId);
+record ChatResponse(string Output, string SessionId);
diff --git a/examples/WebApiAgent/WebApiAgent.csproj b/examples/WebApiAgent/WebApiAgent.csproj
new file mode 100644
index 0000000..243d3a3
--- /dev/null
+++ b/examples/WebApiAgent/WebApiAgent.csproj
@@ -0,0 +1,11 @@
+
+
+
+ ASP.NET Core minimal API agent example for SharpClaw Code.
+
+
+
+
+
+
+
diff --git a/examples/WebApiAgent/appsettings.json b/examples/WebApiAgent/appsettings.json
new file mode 100644
index 0000000..93325ed
--- /dev/null
+++ b/examples/WebApiAgent/appsettings.json
@@ -0,0 +1,21 @@
+{
+ "SharpClaw": {
+ "Providers": {
+ "Catalog": {
+ "DefaultProvider": "Anthropic"
+ },
+ "Anthropic": {
+ "ApiKey": "",
+ "DefaultModel": "claude-sonnet-4-5"
+ }
+ }
+ },
+ "Logging": {
+ "LogLevel": {
+ "Default": "Warning",
+ "SharpClaw": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*"
+}
diff --git a/src/SharpClaw.Code.Acp/AcpStdioHost.cs b/src/SharpClaw.Code.Acp/AcpStdioHost.cs
index 749b0d3..652ee6d 100644
--- a/src/SharpClaw.Code.Acp/AcpStdioHost.cs
+++ b/src/SharpClaw.Code.Acp/AcpStdioHost.cs
@@ -190,7 +190,8 @@ private async Task HandleSessionPromptAsync(
WorkingDirectory: workspace,
PermissionMode: PermissionMode.WorkspaceWrite,
OutputFormat: OutputFormat.Json,
- Metadata: new Dictionary { ["acp"] = "true" }),
+ Metadata: new Dictionary { ["acp"] = "true" },
+ IsInteractive: false),
cancellationToken)
.ConfigureAwait(false);
diff --git a/src/SharpClaw.Code.Acp/SharpClaw.Code.Acp.csproj b/src/SharpClaw.Code.Acp/SharpClaw.Code.Acp.csproj
index 344d324..3b86ce5 100644
--- a/src/SharpClaw.Code.Acp/SharpClaw.Code.Acp.csproj
+++ b/src/SharpClaw.Code.Acp/SharpClaw.Code.Acp.csproj
@@ -4,6 +4,7 @@
net10.0
enable
enable
+ ACP stdio host for editor and protocol bridge scenarios.
diff --git a/src/SharpClaw.Code.Agents/Agents/SharpClawAgentBase.cs b/src/SharpClaw.Code.Agents/Agents/SharpClawAgentBase.cs
index aa91de5..c83e440 100644
--- a/src/SharpClaw.Code.Agents/Agents/SharpClawAgentBase.cs
+++ b/src/SharpClaw.Code.Agents/Agents/SharpClawAgentBase.cs
@@ -1,5 +1,6 @@
using SharpClaw.Code.Agents.Abstractions;
using SharpClaw.Code.Agents.Models;
+using SharpClaw.Code.Protocol.Models;
namespace SharpClaw.Code.Agents.Agents;
@@ -31,13 +32,23 @@ public abstract class SharpClawAgentBase(IAgentFrameworkBridge agentFrameworkBri
///
public virtual Task RunAsync(AgentRunContext context, CancellationToken cancellationToken)
- => agentFrameworkBridge.RunAsync(
+ {
+ var instructions = Instructions;
+ if (context.Metadata is not null
+ && context.Metadata.TryGetValue(SharpClawWorkflowMetadataKeys.AgentInstructionAppendix, out var appendix)
+ && !string.IsNullOrWhiteSpace(appendix))
+ {
+ instructions = $"{instructions}{Environment.NewLine}{Environment.NewLine}{appendix.Trim()}";
+ }
+
+ return agentFrameworkBridge.RunAsync(
new AgentFrameworkRequest(
AgentId,
AgentKind,
Name,
Description,
- Instructions,
+ instructions,
context),
cancellationToken);
+ }
}
diff --git a/src/SharpClaw.Code.Agents/AgentsServiceCollectionExtensions.cs b/src/SharpClaw.Code.Agents/AgentsServiceCollectionExtensions.cs
index f62a742..629b9b0 100644
--- a/src/SharpClaw.Code.Agents/AgentsServiceCollectionExtensions.cs
+++ b/src/SharpClaw.Code.Agents/AgentsServiceCollectionExtensions.cs
@@ -1,6 +1,7 @@
using Microsoft.Extensions.DependencyInjection;
using SharpClaw.Code.Agents.Abstractions;
using SharpClaw.Code.Agents.Agents;
+using SharpClaw.Code.Agents.Configuration;
using SharpClaw.Code.Agents.Internal;
using SharpClaw.Code.Agents.Services;
@@ -18,6 +19,8 @@ public static class AgentsServiceCollectionExtensions
/// The updated service collection.
public static IServiceCollection AddSharpClawAgents(this IServiceCollection services)
{
+ services.AddOptions();
+ services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
diff --git a/src/SharpClaw.Code.Agents/Configuration/AgentLoopOptions.cs b/src/SharpClaw.Code.Agents/Configuration/AgentLoopOptions.cs
new file mode 100644
index 0000000..6fedd5d
--- /dev/null
+++ b/src/SharpClaw.Code.Agents/Configuration/AgentLoopOptions.cs
@@ -0,0 +1,17 @@
+namespace SharpClaw.Code.Agents.Configuration;
+
+///
+/// Configures the tool-calling loop executed by .
+///
+public sealed class AgentLoopOptions
+{
+ ///
+ /// The maximum number of tool-calling iterations before the loop is forcefully terminated.
+ ///
+ public int MaxToolIterations { get; set; } = 25;
+
+ ///
+ /// The maximum number of tokens to request per provider call.
+ ///
+ public int MaxTokensPerRequest { get; set; } = 16_384;
+}
diff --git a/src/SharpClaw.Code.Agents/Internal/ProviderBackedAgentKernel.cs b/src/SharpClaw.Code.Agents/Internal/ProviderBackedAgentKernel.cs
index eedc14c..5363022 100644
--- a/src/SharpClaw.Code.Agents/Internal/ProviderBackedAgentKernel.cs
+++ b/src/SharpClaw.Code.Agents/Internal/ProviderBackedAgentKernel.cs
@@ -1,29 +1,49 @@
+using System.Diagnostics;
using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using SharpClaw.Code.Agents.Configuration;
using SharpClaw.Code.Agents.Models;
+using SharpClaw.Code.Protocol.Events;
+using SharpClaw.Code.Protocol.Models;
using SharpClaw.Code.Providers.Abstractions;
using SharpClaw.Code.Providers.Models;
using SharpClaw.Code.Protocol.Enums;
-using SharpClaw.Code.Protocol.Models;
+using SharpClaw.Code.Telemetry.Diagnostics;
+using SharpClaw.Code.Telemetry.Metrics;
+using SharpClaw.Code.Tools.Models;
namespace SharpClaw.Code.Agents.Internal;
///
-/// Executes the provider-backed core of a SharpClaw agent run.
+/// Executes the provider-backed core of a SharpClaw agent run,
+/// including a multi-iteration tool-calling loop.
///
public sealed class ProviderBackedAgentKernel(
IProviderRequestPreflight providerRequestPreflight,
IModelProviderResolver providerResolver,
IAuthFlowService authFlowService,
+ ToolCallDispatcher toolCallDispatcher,
+ IOptions loopOptions,
ILogger logger)
{
- internal async Task ExecuteAsync(AgentFrameworkRequest request, CancellationToken cancellationToken)
+ internal async Task ExecuteAsync(
+ AgentFrameworkRequest request,
+ ToolExecutionContext? toolExecutionContext,
+ IReadOnlyList? availableTools,
+ CancellationToken cancellationToken)
{
+ var options = loopOptions.Value;
var requestedModel = request.Context.Model;
var requestedProvider = request.Context.Metadata is not null && request.Context.Metadata.TryGetValue("provider", out var metadataProvider)
? metadataProvider
: string.Empty;
- var providerRequest = providerRequestPreflight.Prepare(new ProviderRequest(
+ var baseMetadata = request.Context.Metadata is null
+ ? null
+ : new Dictionary(request.Context.Metadata, StringComparer.Ordinal);
+
+ // Run a single preflight to resolve the effective provider name for auth/resolution
+ var resolvedRequest = providerRequestPreflight.Prepare(new ProviderRequest(
Id: $"provider-request-{Guid.NewGuid():N}",
SessionId: request.Context.SessionId,
TurnId: request.Context.TurnId,
@@ -33,28 +53,29 @@ internal async Task ExecuteAsync(AgentFrameworkRequest
SystemPrompt: request.Instructions,
OutputFormat: request.Context.OutputFormat,
Temperature: 0.1m,
- Metadata: request.Context.Metadata is null
- ? null
- : new Dictionary(request.Context.Metadata, StringComparer.Ordinal)));
+ Metadata: baseMetadata));
+
+ var resolvedProviderName = resolvedRequest.ProviderName;
try
{
+ // --- Auth check ---
AuthStatus authStatus;
try
{
- authStatus = await authFlowService.GetStatusAsync(providerRequest.ProviderName, cancellationToken).ConfigureAwait(false);
+ authStatus = await authFlowService.GetStatusAsync(resolvedProviderName, cancellationToken).ConfigureAwait(false);
}
catch (InvalidOperationException)
{
- throw CreateMissingProviderException(providerRequest.ProviderName, requestedModel, "auth status lookup");
+ throw CreateMissingProviderException(resolvedProviderName, requestedModel, "auth status lookup");
}
catch (Exception exception)
{
throw new ProviderExecutionException(
- providerRequest.ProviderName,
+ resolvedProviderName,
requestedModel,
ProviderFailureKind.AuthenticationUnavailable,
- $"Provider '{providerRequest.ProviderName}' authentication probe failed.",
+ $"Provider '{resolvedProviderName}' authentication probe failed.",
exception);
}
@@ -62,43 +83,184 @@ internal async Task ExecuteAsync(AgentFrameworkRequest
{
logger.LogWarning(
"Provider {ProviderName} is not authenticated for session {SessionId}.",
- providerRequest.ProviderName,
+ resolvedProviderName,
request.Context.SessionId);
throw new ProviderExecutionException(
- providerRequest.ProviderName,
- providerRequest.Model,
+ resolvedProviderName,
+ requestedModel,
ProviderFailureKind.AuthenticationUnavailable,
- $"Provider '{providerRequest.ProviderName}' is not authenticated.");
+ $"Provider '{resolvedProviderName}' is not authenticated.");
}
+ // --- Resolve provider ---
IModelProvider provider;
try
{
- provider = providerResolver.Resolve(providerRequest.ProviderName);
+ provider = providerResolver.Resolve(resolvedProviderName);
}
catch (InvalidOperationException)
{
- throw CreateMissingProviderException(providerRequest.ProviderName, requestedModel, "provider resolution");
+ throw CreateMissingProviderException(resolvedProviderName, requestedModel, "provider resolution");
}
- var stream = await provider.StartStreamAsync(providerRequest, cancellationToken).ConfigureAwait(false);
- var providerEvents = new List();
+ // --- Build initial conversation messages ---
+ // Do not add request.Instructions as a shared "system" chat message here.
+ // Provider adapters apply system instructions via ProviderRequest.SystemPrompt
+ // so providers that do not support a native "system" role (e.g. Anthropic)
+ // do not receive duplicated or remapped instruction turns.
+ var messages = new List();
+
+ // Prepend prior-turn conversation history for multi-turn context.
+ if (request.Context.ConversationHistory is { Count: > 0 } history)
+ {
+ messages.AddRange(history);
+ }
+
+ messages.Add(new ChatMessage("user", [new ContentBlock(ContentBlockKind.Text, request.Context.Prompt, null, null, null, null)]));
+
+ // --- Tool-calling loop ---
+ var allProviderEvents = new List();
+ var allToolResults = new List();
+ var allToolEvents = new List();
var outputSegments = new List();
UsageSnapshot? terminalUsage = null;
+ ProviderRequest? lastProviderRequest = null;
- await foreach (var providerEvent in stream.Events.WithCancellation(cancellationToken))
+ var iteration = 0;
+ for (; iteration < options.MaxToolIterations; iteration++)
{
- providerEvents.Add(providerEvent);
+ UsageSnapshot? iterationUsage = null;
+
+ var providerRequest = providerRequestPreflight.Prepare(new ProviderRequest(
+ Id: $"provider-request-{Guid.NewGuid():N}",
+ SessionId: request.Context.SessionId,
+ TurnId: request.Context.TurnId,
+ ProviderName: resolvedProviderName,
+ Model: requestedModel,
+ Prompt: request.Context.Prompt,
+ SystemPrompt: request.Instructions,
+ OutputFormat: request.Context.OutputFormat,
+ Temperature: 0.1m,
+ Metadata: baseMetadata,
+ Messages: messages,
+ Tools: availableTools,
+ MaxTokens: options.MaxTokensPerRequest));
+
+ lastProviderRequest = providerRequest;
+
+ var iterationTextSegments = new List();
+ var toolUseEvents = new List();
+
+ using var providerScope = new ProviderActivityScope(resolvedProviderName, requestedModel, providerRequest.Id);
+ var providerSw = Stopwatch.StartNew();
+ try
+ {
+ var stream = await provider.StartStreamAsync(providerRequest, cancellationToken).ConfigureAwait(false);
+
+ await foreach (var providerEvent in stream.Events.WithCancellation(cancellationToken))
+ {
+ allProviderEvents.Add(providerEvent);
+
+ if (!providerEvent.IsTerminal && !string.IsNullOrWhiteSpace(providerEvent.Content))
+ {
+ iterationTextSegments.Add(providerEvent.Content);
+ }
+
+ if (!string.IsNullOrEmpty(providerEvent.ToolUseId) && !string.IsNullOrEmpty(providerEvent.ToolName))
+ {
+ toolUseEvents.Add(providerEvent);
+ }
+
+ if (providerEvent.IsTerminal && providerEvent.Usage is not null)
+ {
+ iterationUsage = providerEvent.Usage;
+ terminalUsage = providerEvent.Usage;
+ }
+ }
+
+ providerSw.Stop();
+ providerScope.SetCompleted(iterationUsage?.InputTokens, iterationUsage?.OutputTokens);
+ SharpClawMeterSource.ProviderDuration.Record(providerSw.Elapsed.TotalMilliseconds);
+ }
+ catch (Exception ex)
+ {
+ providerSw.Stop();
+ providerScope.SetError(ex.Message);
+ throw;
+ }
+
+ // If no tool-use events, accumulate text and break
+ if (toolUseEvents.Count == 0)
+ {
+ outputSegments.AddRange(iterationTextSegments);
+ break;
+ }
- if (!providerEvent.IsTerminal && !string.IsNullOrWhiteSpace(providerEvent.Content))
+ // Build assistant message with text + tool-use content blocks
+ var assistantBlocks = new List();
+ var iterationText = string.Concat(iterationTextSegments);
+ if (!string.IsNullOrEmpty(iterationText))
{
- outputSegments.Add(providerEvent.Content);
+ assistantBlocks.Add(new ContentBlock(ContentBlockKind.Text, iterationText, null, null, null, null));
}
- if (providerEvent.IsTerminal && providerEvent.Usage is not null)
+ foreach (var toolUseEvent in toolUseEvents)
{
- terminalUsage = providerEvent.Usage;
+ assistantBlocks.Add(new ContentBlock(
+ ContentBlockKind.ToolUse,
+ null,
+ toolUseEvent.ToolUseId,
+ toolUseEvent.ToolName,
+ toolUseEvent.ToolInputJson,
+ null));
+ }
+
+ messages.Add(new ChatMessage("assistant", assistantBlocks));
+
+ // Dispatch each tool call and collect results
+ var toolResultBlocks = new List();
+ foreach (var toolUseEvent in toolUseEvents)
+ {
+ if (toolExecutionContext is null)
+ {
+ // No tool execution context means we cannot dispatch tools
+ toolResultBlocks.Add(new ContentBlock(
+ ContentBlockKind.ToolResult,
+ "Tool execution is not available in this context.",
+ toolUseEvent.ToolUseId,
+ null,
+ null,
+ true));
+ continue;
+ }
+
+ var (resultBlock, toolResult, events) = await toolCallDispatcher.DispatchAsync(
+ toolUseEvent,
+ toolExecutionContext,
+ cancellationToken).ConfigureAwait(false);
+
+ toolResultBlocks.Add(resultBlock);
+ allToolResults.Add(toolResult);
+ allToolEvents.AddRange(events);
}
+
+ messages.Add(new ChatMessage("user", toolResultBlocks));
+
+ // Accumulate partial text from tool-calling iterations
+ if (!string.IsNullOrEmpty(iterationText))
+ {
+ outputSegments.Add(iterationText);
+ }
+ }
+
+ // Detect if the loop was exhausted (provider kept requesting tools every iteration).
+ if (iteration >= options.MaxToolIterations)
+ {
+ logger.LogWarning(
+ "Tool-calling loop reached maximum iterations ({MaxIterations}) for session {SessionId}; output may be incomplete.",
+ options.MaxToolIterations,
+ request.Context.SessionId);
+ outputSegments.Add($"\n\n[Tool-calling loop reached the maximum of {options.MaxToolIterations} iterations. Output may be incomplete.]");
}
var output = string.Concat(outputSegments);
@@ -106,9 +268,9 @@ internal async Task ExecuteAsync(AgentFrameworkRequest
{
logger.LogWarning(
"Provider {ProviderName} returned no stream content for session {SessionId}; returning placeholder response.",
- providerRequest.ProviderName,
+ resolvedProviderName,
request.Context.SessionId);
- return CreatePlaceholderResult(request, providerRequest.Model, $"Provider '{providerRequest.ProviderName}' returned no content; using placeholder response.");
+ return CreatePlaceholderResult(request, requestedModel, $"Provider '{resolvedProviderName}' returned no content; using placeholder response.");
}
var usage = terminalUsage ?? new UsageSnapshot(
@@ -121,9 +283,11 @@ internal async Task ExecuteAsync(AgentFrameworkRequest
return new ProviderInvocationResult(
Output: output,
Usage: usage,
- Summary: $"Streamed provider response from {providerRequest.ProviderName}/{providerRequest.Model}.",
- ProviderRequest: providerRequest,
- ProviderEvents: providerEvents);
+ Summary: $"Streamed provider response from {resolvedProviderName}/{requestedModel}.",
+ ProviderRequest: lastProviderRequest,
+ ProviderEvents: allProviderEvents,
+ ToolResults: allToolResults.Count > 0 ? allToolResults : null,
+ ToolEvents: allToolEvents.Count > 0 ? allToolEvents : null);
}
catch (OperationCanceledException)
{
@@ -140,14 +304,20 @@ internal async Task ExecuteAsync(AgentFrameworkRequest
catch (Exception exception)
{
throw new ProviderExecutionException(
- providerRequest.ProviderName,
- providerRequest.Model,
+ resolvedProviderName,
+ requestedModel,
ProviderFailureKind.StreamFailed,
- $"Provider '{providerRequest.ProviderName}' failed during execution.",
+ $"Provider '{resolvedProviderName}' failed during execution.",
exception);
}
}
+ ///
+ /// Backward-compatible overload for callers that do not need tool calling.
+ ///
+ internal Task ExecuteAsync(AgentFrameworkRequest request, CancellationToken cancellationToken)
+ => ExecuteAsync(request, toolExecutionContext: null, availableTools: null, cancellationToken);
+
private static ProviderInvocationResult CreatePlaceholderResult(AgentFrameworkRequest request, string model, string summary)
{
var output = $"Session {request.Context.SessionId} turn {request.Context.TurnId}: placeholder response for '{request.Context.Prompt}' using model '{model}'.";
diff --git a/src/SharpClaw.Code.Agents/Internal/ProviderInvocationResult.cs b/src/SharpClaw.Code.Agents/Internal/ProviderInvocationResult.cs
index 65ff1f2..3e0e157 100644
--- a/src/SharpClaw.Code.Agents/Internal/ProviderInvocationResult.cs
+++ b/src/SharpClaw.Code.Agents/Internal/ProviderInvocationResult.cs
@@ -1,3 +1,4 @@
+using SharpClaw.Code.Protocol.Events;
using SharpClaw.Code.Protocol.Models;
namespace SharpClaw.Code.Agents.Internal;
@@ -7,4 +8,6 @@ internal sealed record ProviderInvocationResult(
UsageSnapshot Usage,
string Summary,
ProviderRequest? ProviderRequest,
- IReadOnlyList? ProviderEvents);
+ IReadOnlyList? ProviderEvents,
+ IReadOnlyList? ToolResults = null,
+ IReadOnlyList? ToolEvents = null);
diff --git a/src/SharpClaw.Code.Agents/Internal/ToolCallDispatcher.cs b/src/SharpClaw.Code.Agents/Internal/ToolCallDispatcher.cs
new file mode 100644
index 0000000..f2b3d6b
--- /dev/null
+++ b/src/SharpClaw.Code.Agents/Internal/ToolCallDispatcher.cs
@@ -0,0 +1,69 @@
+using SharpClaw.Code.Protocol.Events;
+using SharpClaw.Code.Protocol.Models;
+using SharpClaw.Code.Tools.Abstractions;
+using SharpClaw.Code.Tools.Models;
+
+namespace SharpClaw.Code.Agents.Internal;
+
+///
+/// Dispatches tool-use requests from provider events through the permission-aware tool executor
+/// and returns content blocks for the provider conversation.
+///
+public sealed class ToolCallDispatcher(
+ IToolExecutor toolExecutor)
+{
+ ///
+ /// Executes a tool call and returns a tool-result content block.
+ ///
+ public async Task<(ContentBlock ResultBlock, ToolResult ToolResult, List Events)> DispatchAsync(
+ ProviderEvent toolUseEvent,
+ ToolExecutionContext context,
+ CancellationToken cancellationToken)
+ {
+ if (string.IsNullOrWhiteSpace(toolUseEvent.ToolName))
+ {
+ return (new ContentBlock(ContentBlockKind.ToolResult, "Tool call missing required tool name.", toolUseEvent.ToolUseId, null, null, true),
+ new ToolResult("unknown", "unknown", false, Protocol.Enums.OutputFormat.Text, null, "Tool call missing required tool name.", 1, null, null), []);
+ }
+
+ if (string.IsNullOrWhiteSpace(toolUseEvent.ToolUseId))
+ {
+ return (new ContentBlock(ContentBlockKind.ToolResult, "Tool call missing required tool use ID.", null, null, null, true),
+ new ToolResult("unknown", toolUseEvent.ToolName, false, Protocol.Enums.OutputFormat.Text, null, "Tool call missing required tool use ID.", 1, null, null), []);
+ }
+
+ var toolName = toolUseEvent.ToolName;
+ var toolInputJson = toolUseEvent.ToolInputJson ?? "{}";
+ var toolUseId = toolUseEvent.ToolUseId;
+
+ // Execute the tool — ToolExecutor already publishes ToolStartedEvent and ToolCompletedEvent
+ // via IRuntimeEventPublisher, so we do NOT re-publish here to avoid duplicates.
+ var envelope = await toolExecutor.ExecuteAsync(toolName, toolInputJson, context, cancellationToken);
+
+ // Convert ToolResult to ContentBlock
+ ContentBlock resultBlock;
+ if (envelope.Result.Succeeded)
+ {
+ resultBlock = new ContentBlock(
+ ContentBlockKind.ToolResult,
+ envelope.Result.Output,
+ toolUseId,
+ null,
+ null,
+ null);
+ }
+ else
+ {
+ resultBlock = new ContentBlock(
+ ContentBlockKind.ToolResult,
+ envelope.Result.ErrorMessage ?? "Tool execution failed",
+ toolUseId,
+ null,
+ null,
+ true);
+ }
+
+ // Events are already published by ToolExecutor; return empty list to avoid duplicates.
+ return (resultBlock, envelope.Result, []);
+ }
+}
diff --git a/src/SharpClaw.Code.Agents/Models/AgentRunContext.cs b/src/SharpClaw.Code.Agents/Models/AgentRunContext.cs
index 09b01a1..c1955b0 100644
--- a/src/SharpClaw.Code.Agents/Models/AgentRunContext.cs
+++ b/src/SharpClaw.Code.Agents/Models/AgentRunContext.cs
@@ -20,6 +20,11 @@ namespace SharpClaw.Code.Agents.Models;
/// The bounded delegated task contract, if any.
/// Active build vs plan workflow mode for tool permission behavior.
/// Optional mutation recorder forwarded to tool executions.
+///
+/// Prior-turn messages assembled from session events. When non-empty these are prepended
+/// to the provider request so the model has multi-turn context.
+///
+/// Whether tool approvals can interact with the caller.
public sealed record AgentRunContext(
string SessionId,
string TurnId,
@@ -33,4 +38,6 @@ public sealed record AgentRunContext(
string? ParentAgentId = null,
DelegatedTaskContract? DelegatedTask = null,
PrimaryMode PrimaryMode = PrimaryMode.Build,
- IToolMutationRecorder? ToolMutationRecorder = null);
+ IToolMutationRecorder? ToolMutationRecorder = null,
+ IReadOnlyList? ConversationHistory = null,
+ bool IsInteractive = true);
diff --git a/src/SharpClaw.Code.Agents/Services/AgentFrameworkBridge.cs b/src/SharpClaw.Code.Agents/Services/AgentFrameworkBridge.cs
index 90a0709..8bb9073 100644
--- a/src/SharpClaw.Code.Agents/Services/AgentFrameworkBridge.cs
+++ b/src/SharpClaw.Code.Agents/Services/AgentFrameworkBridge.cs
@@ -1,12 +1,20 @@
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Logging;
+using System.Text.Json;
using SharpClaw.Code.Agents.Abstractions;
using SharpClaw.Code.Infrastructure.Abstractions;
using SharpClaw.Code.Agents.Internal;
using SharpClaw.Code.Agents.Models;
+using SharpClaw.Code.Permissions.Models;
+using SharpClaw.Code.Protocol.Enums;
using SharpClaw.Code.Providers.Models;
using SharpClaw.Code.Protocol.Events;
+using SharpClaw.Code.Protocol.Models;
+using SharpClaw.Code.Protocol.Serialization;
+using SharpClaw.Code.Tools.Abstractions;
+using SharpClaw.Code.Tools.Models;
+using ChatMessage = Microsoft.Extensions.AI.ChatMessage;
namespace SharpClaw.Code.Agents.Services;
@@ -15,6 +23,7 @@ namespace SharpClaw.Code.Agents.Services;
///
public sealed class AgentFrameworkBridge(
ProviderBackedAgentKernel providerBackedAgentKernel,
+ IToolRegistry toolRegistry,
ISystemClock systemClock,
ILogger logger) : IAgentFrameworkBridge
{
@@ -22,6 +31,35 @@ public sealed class AgentFrameworkBridge(
public async Task RunAsync(AgentFrameworkRequest request, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
+ var allowedTools = ResolveAllowedTools(request.Context.Metadata);
+
+ // Build tool execution context from agent run context
+ var toolExecutionContext = new ToolExecutionContext(
+ SessionId: request.Context.SessionId,
+ TurnId: request.Context.TurnId,
+ WorkspaceRoot: request.Context.WorkingDirectory,
+ WorkingDirectory: request.Context.WorkingDirectory,
+ PermissionMode: request.Context.PermissionMode,
+ OutputFormat: request.Context.OutputFormat,
+ EnvironmentVariables: null,
+ AllowedTools: allowedTools,
+ AllowDangerousBypass: false,
+ IsInteractive: request.Context.IsInteractive,
+ SourceKind: PermissionRequestSourceKind.Runtime,
+ SourceName: null,
+ TrustedPluginNames: null,
+ TrustedMcpServerNames: null,
+ PrimaryMode: request.Context.PrimaryMode,
+ MutationRecorder: request.Context.ToolMutationRecorder);
+
+ // Map tool definitions from the registry to provider tool definitions
+ var registryTools = await toolRegistry.ListAsync(
+ request.Context.WorkingDirectory,
+ cancellationToken).ConfigureAwait(false);
+
+ var providerTools = FilterAdvertisedTools(registryTools, allowedTools)
+ .Select(t => new ProviderToolDefinition(t.Name, t.Description, t.InputSchemaJson))
+ .ToList();
ProviderInvocationResult? providerResult = null;
var frameworkAgent = new SharpClawFrameworkAgent(
@@ -30,7 +68,11 @@ public async Task RunAsync(AgentFrameworkRequest request, Cancel
request.Description,
async (messages, session, runOptions, ct) =>
{
- providerResult = await providerBackedAgentKernel.ExecuteAsync(request, ct).ConfigureAwait(false);
+ providerResult = await providerBackedAgentKernel.ExecuteAsync(
+ request,
+ toolExecutionContext,
+ providerTools,
+ ct).ConfigureAwait(false);
return new AgentResponse(new ChatMessage(ChatRole.Assistant, providerResult.Output));
});
@@ -58,7 +100,8 @@ public async Task RunAsync(AgentFrameworkRequest request, Cancel
ProviderEvents: null);
logger.LogInformation("Completed framework-backed agent run for {AgentId}.", request.AgentId);
- var events = new RuntimeEvent[]
+
+ var events = new List
{
new AgentSpawnedEvent(
EventId: $"event-{Guid.NewGuid():N}",
@@ -79,6 +122,12 @@ public async Task RunAsync(AgentFrameworkRequest request, Cancel
Usage: resolvedProviderResult.Usage)
};
+ // Include tool-related events from the kernel
+ if (resolvedProviderResult.ToolEvents is { Count: > 0 } toolEvents)
+ {
+ events.AddRange(toolEvents);
+ }
+
return new AgentRunResult(
AgentId: request.AgentId,
AgentKind: request.AgentKind,
@@ -87,7 +136,38 @@ public async Task RunAsync(AgentFrameworkRequest request, Cancel
Summary: resolvedProviderResult.Summary,
ProviderRequest: resolvedProviderResult.ProviderRequest,
ProviderEvents: resolvedProviderResult.ProviderEvents,
- ToolResults: [],
+ ToolResults: resolvedProviderResult.ToolResults ?? [],
Events: events);
}
+
+ private static IReadOnlyCollection? ResolveAllowedTools(IReadOnlyDictionary? metadata)
+ {
+ if (metadata is null
+ || !metadata.TryGetValue(SharpClawWorkflowMetadataKeys.AgentAllowedToolsJson, out var payload)
+ || string.IsNullOrWhiteSpace(payload))
+ {
+ return null;
+ }
+
+ try
+ {
+ return JsonSerializer.Deserialize(payload, ProtocolJsonContext.Default.StringArray);
+ }
+ catch (JsonException)
+ {
+ return null;
+ }
+ }
+
+ private static IEnumerable FilterAdvertisedTools(
+ IReadOnlyList registryTools,
+ IReadOnlyCollection? allowedTools)
+ {
+ if (allowedTools is null || allowedTools.Count == 0)
+ {
+ return registryTools;
+ }
+
+ return registryTools.Where(tool => allowedTools.Contains(tool.Name, StringComparer.OrdinalIgnoreCase));
+ }
}
diff --git a/src/SharpClaw.Code.Agents/SharpClaw.Code.Agents.csproj b/src/SharpClaw.Code.Agents/SharpClaw.Code.Agents.csproj
index a89f4db..c687b4d 100644
--- a/src/SharpClaw.Code.Agents/SharpClaw.Code.Agents.csproj
+++ b/src/SharpClaw.Code.Agents/SharpClaw.Code.Agents.csproj
@@ -3,6 +3,7 @@
+
@@ -16,10 +17,15 @@
+
+
+
+
net10.0
enable
enable
+ Microsoft Agent Framework integration and agent orchestration for SharpClaw Code.
diff --git a/src/SharpClaw.Code.Cli/Composition/CliServiceCollectionExtensions.cs b/src/SharpClaw.Code.Cli/Composition/CliServiceCollectionExtensions.cs
index 528d919..631b7d7 100644
--- a/src/SharpClaw.Code.Cli/Composition/CliServiceCollectionExtensions.cs
+++ b/src/SharpClaw.Code.Cli/Composition/CliServiceCollectionExtensions.cs
@@ -26,12 +26,26 @@ public static IServiceCollection AddSharpClawCli(this IServiceCollection service
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
+ services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
- services.AddSingleton();
+ services.AddSingleton(serviceProvider => serviceProvider.GetRequiredService());
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
@@ -41,7 +55,21 @@ public static IServiceCollection AddSharpClawCli(this IServiceCollection service
services.AddSingleton();
services.AddSingleton();
- services.AddSingleton();
+ services.AddSingleton(serviceProvider => serviceProvider.GetRequiredService());
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
diff --git a/src/SharpClaw.Code.Cli/SharpClaw.Code.Cli.csproj b/src/SharpClaw.Code.Cli/SharpClaw.Code.Cli.csproj
index e2b73ce..1521160 100644
--- a/src/SharpClaw.Code.Cli/SharpClaw.Code.Cli.csproj
+++ b/src/SharpClaw.Code.Cli/SharpClaw.Code.Cli.csproj
@@ -2,6 +2,7 @@
Exe
+ Command-line interface and REPL for SharpClaw Code.
diff --git a/src/SharpClaw.Code.Commands/CliCommandFactory.cs b/src/SharpClaw.Code.Commands/CliCommandFactory.cs
index 8f5d69d..6239a93 100644
--- a/src/SharpClaw.Code.Commands/CliCommandFactory.cs
+++ b/src/SharpClaw.Code.Commands/CliCommandFactory.cs
@@ -110,5 +110,6 @@ private static RuntimeCommandContext ToRuntimeContext(CommandExecutionContext co
context.PermissionMode,
context.OutputFormat,
context.PrimaryMode,
- context.SessionId);
+ context.SessionId,
+ context.AgentId);
}
diff --git a/src/SharpClaw.Code.Commands/Handlers/AgentsCommandHandler.cs b/src/SharpClaw.Code.Commands/Handlers/AgentsCommandHandler.cs
new file mode 100644
index 0000000..c8aa72e
--- /dev/null
+++ b/src/SharpClaw.Code.Commands/Handlers/AgentsCommandHandler.cs
@@ -0,0 +1,99 @@
+using System.CommandLine;
+using System.Text.Json;
+using SharpClaw.Code.Commands.Models;
+using SharpClaw.Code.Commands.Options;
+using SharpClaw.Code.Protocol.Commands;
+using SharpClaw.Code.Protocol.Models;
+using SharpClaw.Code.Protocol.Serialization;
+using SharpClaw.Code.Runtime.Abstractions;
+
+namespace SharpClaw.Code.Commands;
+
+///
+/// Lists the effective agent catalog and manages the REPL agent override.
+///
+public sealed class AgentsCommandHandler(
+ IAgentCatalogService agentCatalogService,
+ ReplInteractionState replInteractionState,
+ OutputRendererDispatcher outputRendererDispatcher) : ICommandHandler, ISlashCommandHandler
+{
+ ///
+ public string Name => "agents";
+
+ ///
+ public string Description => "Lists agents and selects the active REPL agent override.";
+
+ ///
+ public string CommandName => Name;
+
+ ///
+ public Command BuildCommand(GlobalCliOptions globalOptions)
+ {
+ var command = new Command(Name, Description);
+
+ var list = new Command("list", "Lists the effective agent catalog.");
+ list.SetAction((parseResult, cancellationToken) => ExecuteListAsync(globalOptions.Resolve(parseResult), cancellationToken));
+ command.Subcommands.Add(list);
+
+ var use = new Command("use", "Sets the current process REPL agent override.");
+ var idOption = new Option("--id") { Required = true, Description = "Agent id to activate." };
+ use.Options.Add(idOption);
+ use.SetAction(async (parseResult, cancellationToken) =>
+ {
+ var context = globalOptions.Resolve(parseResult);
+ var id = parseResult.GetValue(idOption) ?? throw new InvalidOperationException("--id is required.");
+ return await ExecuteUseAsync(id, context, cancellationToken).ConfigureAwait(false);
+ });
+ command.Subcommands.Add(use);
+
+ command.SetAction((parseResult, cancellationToken) => ExecuteListAsync(globalOptions.Resolve(parseResult), cancellationToken));
+ return command;
+ }
+
+ ///
+ public Task ExecuteAsync(SlashCommandParseResult command, CommandExecutionContext context, CancellationToken cancellationToken)
+ {
+ if (command.Arguments.Length >= 2 && string.Equals(command.Arguments[0], "use", StringComparison.OrdinalIgnoreCase))
+ {
+ return ExecuteUseAsync(command.Arguments[1], context, cancellationToken);
+ }
+
+ return ExecuteListAsync(context, cancellationToken);
+ }
+
+ private async Task ExecuteListAsync(CommandExecutionContext context, CancellationToken cancellationToken)
+ {
+ var agents = await agentCatalogService.ListAsync(context.WorkingDirectory, cancellationToken).ConfigureAwait(false);
+ var message = replInteractionState.AgentIdOverride is null
+ ? $"{agents.Count} agent(s)."
+ : $"{agents.Count} agent(s). Active REPL agent override: {replInteractionState.AgentIdOverride}.";
+ var result = new CommandResult(
+ true,
+ 0,
+ context.OutputFormat,
+ message,
+ JsonSerializer.Serialize(agents.ToList(), ProtocolJsonContext.Default.ListAgentCatalogEntry));
+ await outputRendererDispatcher.RenderCommandResultAsync(result, context.OutputFormat, cancellationToken).ConfigureAwait(false);
+ return 0;
+ }
+
+ private async Task ExecuteUseAsync(string agentId, CommandExecutionContext context, CancellationToken cancellationToken)
+ {
+ var agents = await agentCatalogService.ListAsync(context.WorkingDirectory, cancellationToken).ConfigureAwait(false);
+ if (!agents.Any(agent => string.Equals(agent.Id, agentId, StringComparison.OrdinalIgnoreCase)))
+ {
+ await outputRendererDispatcher.RenderCommandResultAsync(
+ new CommandResult(false, 1, context.OutputFormat, $"Unknown agent '{agentId}'.", null),
+ context.OutputFormat,
+ cancellationToken).ConfigureAwait(false);
+ return 1;
+ }
+
+ replInteractionState.AgentIdOverride = agentId;
+ await outputRendererDispatcher.RenderCommandResultAsync(
+ new CommandResult(true, 0, context.OutputFormat, $"Active REPL agent set to {agentId}.", null),
+ context.OutputFormat,
+ cancellationToken).ConfigureAwait(false);
+ return 0;
+ }
+}
diff --git a/src/SharpClaw.Code.Commands/Handlers/CompactCommandHandler.cs b/src/SharpClaw.Code.Commands/Handlers/CompactCommandHandler.cs
new file mode 100644
index 0000000..83a042a
--- /dev/null
+++ b/src/SharpClaw.Code.Commands/Handlers/CompactCommandHandler.cs
@@ -0,0 +1,59 @@
+using System.CommandLine;
+using SharpClaw.Code.Commands.Models;
+using SharpClaw.Code.Commands.Options;
+using SharpClaw.Code.Runtime.Abstractions;
+
+namespace SharpClaw.Code.Commands;
+
+///
+/// Compacts a session into a durable summary and refreshed title.
+///
+public sealed class CompactCommandHandler(
+ IRuntimeCommandService runtimeCommandService,
+ OutputRendererDispatcher outputRendererDispatcher) : ICommandHandler, ISlashCommandHandler
+{
+ ///
+ public string Name => "compact";
+
+ ///
+ public string Description => "Compacts a session into a reusable summary.";
+
+ ///
+ public string CommandName => Name;
+
+ ///
+ public Command BuildCommand(GlobalCliOptions globalOptions)
+ {
+ var command = new Command(Name, Description);
+ var idOption = new Option("--id") { Description = "Session id; latest when omitted." };
+ command.Options.Add(idOption);
+ command.SetAction(async (parseResult, cancellationToken) =>
+ {
+ var context = globalOptions.Resolve(parseResult);
+ var id = parseResult.GetValue(idOption);
+ var result = await runtimeCommandService.CompactSessionAsync(id, ToRuntimeContext(context), cancellationToken).ConfigureAwait(false);
+ await outputRendererDispatcher.RenderCommandResultAsync(result, context.OutputFormat, cancellationToken).ConfigureAwait(false);
+ return result.ExitCode;
+ });
+ return command;
+ }
+
+ ///
+ public async Task ExecuteAsync(SlashCommandParseResult command, CommandExecutionContext context, CancellationToken cancellationToken)
+ {
+ var id = command.Arguments.Length > 0 ? command.Arguments[0] : null;
+ var result = await runtimeCommandService.CompactSessionAsync(id, ToRuntimeContext(context), cancellationToken).ConfigureAwait(false);
+ await outputRendererDispatcher.RenderCommandResultAsync(result, context.OutputFormat, cancellationToken).ConfigureAwait(false);
+ return result.ExitCode;
+ }
+
+ private static RuntimeCommandContext ToRuntimeContext(CommandExecutionContext context)
+ => new(
+ context.WorkingDirectory,
+ context.Model,
+ context.PermissionMode,
+ context.OutputFormat,
+ context.PrimaryMode,
+ context.SessionId,
+ context.AgentId);
+}
diff --git a/src/SharpClaw.Code.Commands/Handlers/ConnectCommandHandler.cs b/src/SharpClaw.Code.Commands/Handlers/ConnectCommandHandler.cs
new file mode 100644
index 0000000..ed1429f
--- /dev/null
+++ b/src/SharpClaw.Code.Commands/Handlers/ConnectCommandHandler.cs
@@ -0,0 +1,146 @@
+using System.CommandLine;
+using System.Diagnostics;
+using System.Text.Json;
+using SharpClaw.Code.Commands.Models;
+using SharpClaw.Code.Commands.Options;
+using SharpClaw.Code.Infrastructure.Abstractions;
+using SharpClaw.Code.Providers.Abstractions;
+using SharpClaw.Code.Protocol.Commands;
+using SharpClaw.Code.Protocol.Models;
+using SharpClaw.Code.Protocol.Serialization;
+using SharpClaw.Code.Runtime.Abstractions;
+
+namespace SharpClaw.Code.Commands;
+
+///
+/// Lists and opens browser-based provider or external connection entry points.
+///
+public sealed class ConnectCommandHandler(
+ ISharpClawConfigService sharpClawConfigService,
+ IEnumerable modelProviders,
+ IAuthFlowService authFlowService,
+ IPathService pathService,
+ OutputRendererDispatcher outputRendererDispatcher) : ICommandHandler, ISlashCommandHandler
+{
+ ///
+ public string Name => "connect";
+
+ ///
+ public string Description => "Lists connection targets and opens configured browser flows.";
+
+ ///
+ public string CommandName => Name;
+
+ ///
+ public Command BuildCommand(GlobalCliOptions globalOptions)
+ {
+ var command = new Command(Name, Description);
+
+ var list = new Command("list", "Lists configured connection targets.");
+ list.SetAction((parseResult, cancellationToken) => ExecuteListAsync(globalOptions.Resolve(parseResult), cancellationToken));
+ command.Subcommands.Add(list);
+
+ var open = new Command("open", "Opens a configured browser connection target.");
+ var targetOption = new Option("--target") { Required = true, Description = "Target id to open." };
+ open.Options.Add(targetOption);
+ open.SetAction(async (parseResult, cancellationToken) =>
+ {
+ var context = globalOptions.Resolve(parseResult);
+ var target = parseResult.GetValue(targetOption) ?? throw new InvalidOperationException("--target is required.");
+ return await ExecuteOpenAsync(target, context, cancellationToken).ConfigureAwait(false);
+ });
+ command.Subcommands.Add(open);
+
+ command.SetAction((parseResult, cancellationToken) => ExecuteListAsync(globalOptions.Resolve(parseResult), cancellationToken));
+ return command;
+ }
+
+ ///
+ public Task ExecuteAsync(SlashCommandParseResult command, CommandExecutionContext context, CancellationToken cancellationToken)
+ {
+ if (command.Arguments.Length >= 2 && string.Equals(command.Arguments[0], "open", StringComparison.OrdinalIgnoreCase))
+ {
+ return ExecuteOpenAsync(command.Arguments[1], context, cancellationToken);
+ }
+
+ return ExecuteListAsync(context, cancellationToken);
+ }
+
+ private async Task ExecuteListAsync(CommandExecutionContext context, CancellationToken cancellationToken)
+ {
+ var statuses = await BuildStatusesAsync(context.WorkingDirectory, cancellationToken).ConfigureAwait(false);
+ var result = new CommandResult(
+ true,
+ 0,
+ context.OutputFormat,
+ $"{statuses.Count} connection target(s).",
+ JsonSerializer.Serialize(statuses, ProtocolJsonContext.Default.ListConnectTargetStatus));
+ await outputRendererDispatcher.RenderCommandResultAsync(result, context.OutputFormat, cancellationToken).ConfigureAwait(false);
+ return 0;
+ }
+
+ private async Task ExecuteOpenAsync(string target, CommandExecutionContext context, CancellationToken cancellationToken)
+ {
+ var statuses = await BuildStatusesAsync(context.WorkingDirectory, cancellationToken).ConfigureAwait(false);
+ var status = statuses.FirstOrDefault(item => string.Equals(item.Target, target, StringComparison.OrdinalIgnoreCase));
+ if (status is null || string.IsNullOrWhiteSpace(status.ConnectUrl))
+ {
+ await outputRendererDispatcher.RenderCommandResultAsync(
+ new CommandResult(false, 1, context.OutputFormat, $"No browser-connectable target '{target}' was found.", null),
+ context.OutputFormat,
+ cancellationToken).ConfigureAwait(false);
+ return 1;
+ }
+
+ OpenBrowser(status.ConnectUrl!);
+ await outputRendererDispatcher.RenderCommandResultAsync(
+ new CommandResult(true, 0, context.OutputFormat, $"Opened {status.ConnectUrl}.", null),
+ context.OutputFormat,
+ cancellationToken).ConfigureAwait(false);
+ return 0;
+ }
+
+ private async Task> BuildStatusesAsync(string workspacePath, CancellationToken cancellationToken)
+ {
+ var workspaceRoot = pathService.GetFullPath(workspacePath);
+ var config = await sharpClawConfigService.GetConfigAsync(workspaceRoot, cancellationToken).ConfigureAwait(false);
+ var results = new List();
+
+ foreach (var provider in modelProviders.OrderBy(static provider => provider.ProviderName, StringComparer.OrdinalIgnoreCase))
+ {
+ var auth = await authFlowService.GetStatusAsync(provider.ProviderName, cancellationToken).ConfigureAwait(false);
+ var configuredUrl = config.Document.ConnectLinks?.FirstOrDefault(link => string.Equals(link.Target, provider.ProviderName, StringComparison.OrdinalIgnoreCase))?.Url;
+ var detail = auth.IsAuthenticated
+ ? string.IsNullOrWhiteSpace(auth.SubjectId)
+ ? "authenticated"
+ : $"authenticated as {auth.SubjectId}"
+ : auth.ExpiresAtUtc is { } expiresAt && expiresAt <= DateTimeOffset.UtcNow
+ ? "authentication expired"
+ : "not authenticated";
+ results.Add(new ConnectTargetStatus(provider.ProviderName, provider.ProviderName, "provider", auth.IsAuthenticated, configuredUrl, auth.ExpiresAtUtc, detail));
+ }
+
+ foreach (var link in config.Document.ConnectLinks ?? [])
+ {
+ if (results.Any(existing => string.Equals(existing.Target, link.Target, StringComparison.OrdinalIgnoreCase)))
+ {
+ continue;
+ }
+
+ results.Add(new ConnectTargetStatus(link.Target, link.DisplayName, "external", false, link.Url, null, "manual browser flow"));
+ }
+
+ return results.OrderBy(static item => item.Target, StringComparer.OrdinalIgnoreCase).ToList();
+ }
+
+ private static void OpenBrowser(string url)
+ {
+ var startInfo = OperatingSystem.IsWindows()
+ ? new ProcessStartInfo("cmd", $"/c start \"\" \"{url}\"") { CreateNoWindow = true }
+ : OperatingSystem.IsMacOS()
+ ? new ProcessStartInfo("open", url)
+ : new ProcessStartInfo("xdg-open", url);
+ startInfo.UseShellExecute = false;
+ Process.Start(startInfo);
+ }
+}
diff --git a/src/SharpClaw.Code.Commands/Handlers/CostCommandHandler.cs b/src/SharpClaw.Code.Commands/Handlers/CostCommandHandler.cs
new file mode 100644
index 0000000..3c5a2cb
--- /dev/null
+++ b/src/SharpClaw.Code.Commands/Handlers/CostCommandHandler.cs
@@ -0,0 +1,57 @@
+using System.CommandLine;
+using System.Globalization;
+using System.Text.Json;
+using SharpClaw.Code.Commands.Models;
+using SharpClaw.Code.Commands.Options;
+using SharpClaw.Code.Protocol.Commands;
+using SharpClaw.Code.Protocol.Serialization;
+using SharpClaw.Code.Runtime.Abstractions;
+
+namespace SharpClaw.Code.Commands;
+
+///
+/// Surfaces estimated workspace cost based on persisted usage snapshots.
+///
+public sealed class CostCommandHandler(
+ IWorkspaceInsightsService workspaceInsightsService,
+ OutputRendererDispatcher outputRendererDispatcher) : ICommandHandler, ISlashCommandHandler
+{
+ ///
+ public string Name => "cost";
+
+ ///
+ public string Description => "Shows estimated usage cost for the current workspace.";
+
+ ///
+ public string CommandName => Name;
+
+ ///
+ public Command BuildCommand(GlobalCliOptions globalOptions)
+ {
+ var command = new Command(Name, Description);
+ command.SetAction((parseResult, cancellationToken) => ExecuteAsync(globalOptions.Resolve(parseResult), cancellationToken));
+ return command;
+ }
+
+ ///
+ public Task ExecuteAsync(SlashCommandParseResult command, CommandExecutionContext context, CancellationToken cancellationToken)
+ => ExecuteAsync(context, cancellationToken);
+
+ private async Task ExecuteAsync(CommandExecutionContext context, CancellationToken cancellationToken)
+ {
+ var report = await workspaceInsightsService
+ .BuildCostReportAsync(context.WorkingDirectory, context.SessionId, cancellationToken)
+ .ConfigureAwait(false);
+ var total = report.WorkspaceEstimatedCostUsd.HasValue
+ ? report.WorkspaceEstimatedCostUsd.Value.ToString("0.0000", CultureInfo.InvariantCulture)
+ : "n/a";
+ var result = new CommandResult(
+ true,
+ 0,
+ context.OutputFormat,
+ $"Workspace estimated cost: ${total} across {report.Sessions.Count} session(s).",
+ JsonSerializer.Serialize(report, ProtocolJsonContext.Default.WorkspaceCostReport));
+ await outputRendererDispatcher.RenderCommandResultAsync(result, context.OutputFormat, cancellationToken).ConfigureAwait(false);
+ return 0;
+ }
+}
diff --git a/src/SharpClaw.Code.Commands/Handlers/HooksCommandHandler.cs b/src/SharpClaw.Code.Commands/Handlers/HooksCommandHandler.cs
new file mode 100644
index 0000000..44321b4
--- /dev/null
+++ b/src/SharpClaw.Code.Commands/Handlers/HooksCommandHandler.cs
@@ -0,0 +1,128 @@
+using System.CommandLine;
+using System.Text.Json;
+using SharpClaw.Code.Commands.Models;
+using SharpClaw.Code.Commands.Options;
+using SharpClaw.Code.Protocol.Commands;
+using SharpClaw.Code.Protocol.Serialization;
+using SharpClaw.Code.Runtime.Abstractions;
+
+namespace SharpClaw.Code.Commands;
+
+///
+/// Lists and tests configured runtime hooks.
+///
+public sealed class HooksCommandHandler(
+ IHookDispatcher hookDispatcher,
+ OutputRendererDispatcher outputRendererDispatcher) : ICommandHandler, ISlashCommandHandler
+{
+ ///
+ public string Name => "hooks";
+
+ ///
+ public string Description => "Lists, inspects, and tests configured runtime hooks.";
+
+ ///
+ public string CommandName => Name;
+
+ ///
+ public Command BuildCommand(GlobalCliOptions globalOptions)
+ {
+ var command = new Command(Name, Description);
+ command.Subcommands.Add(BuildListCommand(globalOptions));
+ command.Subcommands.Add(BuildShowCommand(globalOptions));
+ command.Subcommands.Add(BuildTestCommand(globalOptions));
+ command.SetAction((parseResult, cancellationToken) => ExecuteListAsync(globalOptions.Resolve(parseResult), cancellationToken));
+ return command;
+ }
+
+ ///
+ public Task ExecuteAsync(SlashCommandParseResult command, CommandExecutionContext context, CancellationToken cancellationToken)
+ => command.Arguments.Length switch
+ {
+ 0 => ExecuteListAsync(context, cancellationToken),
+ _ when string.Equals(command.Arguments[0], "show", StringComparison.OrdinalIgnoreCase) && command.Arguments.Length >= 2
+ => ExecuteShowAsync(command.Arguments[1], context, cancellationToken),
+ _ when string.Equals(command.Arguments[0], "test", StringComparison.OrdinalIgnoreCase) && command.Arguments.Length >= 2
+ => ExecuteTestAsync(command.Arguments[1], context, cancellationToken),
+ _ when string.Equals(command.Arguments[0], "list", StringComparison.OrdinalIgnoreCase)
+ => ExecuteListAsync(context, cancellationToken),
+ _ => ExecuteListAsync(context, cancellationToken)
+ };
+
+ private Command BuildListCommand(GlobalCliOptions globalOptions)
+ {
+ var command = new Command("list", "Lists configured hooks.");
+ command.SetAction((parseResult, cancellationToken) => ExecuteListAsync(globalOptions.Resolve(parseResult), cancellationToken));
+ return command;
+ }
+
+ private Command BuildShowCommand(GlobalCliOptions globalOptions)
+ {
+ var command = new Command("show", "Shows one configured hook.");
+ var nameOption = new Option("--name") { Required = true, Description = "Hook name." };
+ command.Options.Add(nameOption);
+ command.SetAction((parseResult, cancellationToken) => ExecuteShowAsync(parseResult.GetValue(nameOption)!, globalOptions.Resolve(parseResult), cancellationToken));
+ return command;
+ }
+
+ private Command BuildTestCommand(GlobalCliOptions globalOptions)
+ {
+ var command = new Command("test", "Executes one configured hook with a synthetic payload.");
+ var nameOption = new Option("--name") { Required = true, Description = "Hook name." };
+ command.Options.Add(nameOption);
+ command.SetAction((parseResult, cancellationToken) => ExecuteTestAsync(parseResult.GetValue(nameOption)!, globalOptions.Resolve(parseResult), cancellationToken));
+ return command;
+ }
+
+ private async Task ExecuteListAsync(CommandExecutionContext context, CancellationToken cancellationToken)
+ {
+ var hooks = await hookDispatcher.ListAsync(context.WorkingDirectory, cancellationToken).ConfigureAwait(false);
+ var result = new CommandResult(
+ true,
+ 0,
+ context.OutputFormat,
+ hooks.Count == 0 ? "No hooks configured." : $"{hooks.Count} hook(s).",
+ JsonSerializer.Serialize(hooks.ToList(), ProtocolJsonContext.Default.ListHookStatusRecord));
+ await outputRendererDispatcher.RenderCommandResultAsync(result, context.OutputFormat, cancellationToken).ConfigureAwait(false);
+ return 0;
+ }
+
+ private async Task ExecuteShowAsync(string name, CommandExecutionContext context, CancellationToken cancellationToken)
+ {
+ var hooks = await hookDispatcher.ListAsync(context.WorkingDirectory, cancellationToken).ConfigureAwait(false);
+ var hook = hooks.FirstOrDefault(item => string.Equals(item.Name, name, StringComparison.OrdinalIgnoreCase));
+ if (hook is null)
+ {
+ await outputRendererDispatcher.RenderCommandResultAsync(
+ new CommandResult(false, 1, context.OutputFormat, $"Hook '{name}' was not found.", null),
+ context.OutputFormat,
+ cancellationToken).ConfigureAwait(false);
+ return 1;
+ }
+
+ await outputRendererDispatcher.RenderCommandResultAsync(
+ new CommandResult(true, 0, context.OutputFormat, $"{hook.Name} ({hook.Trigger})", JsonSerializer.Serialize(hook, ProtocolJsonContext.Default.HookStatusRecord)),
+ context.OutputFormat,
+ cancellationToken).ConfigureAwait(false);
+ return 0;
+ }
+
+ private async Task ExecuteTestAsync(string name, CommandExecutionContext context, CancellationToken cancellationToken)
+ {
+ var payloadJson = JsonSerializer.Serialize(
+ new Dictionary
+ {
+ ["kind"] = "hook-test",
+ ["workspaceRoot"] = context.WorkingDirectory,
+ ["requestedAtUtc"] = DateTimeOffset.UtcNow.ToString("O"),
+ },
+ ProtocolJsonContext.Default.DictionaryStringString);
+ var test = await hookDispatcher.TestAsync(context.WorkingDirectory, name, payloadJson, cancellationToken).ConfigureAwait(false);
+ var exitCode = test.Succeeded ? 0 : 1;
+ await outputRendererDispatcher.RenderCommandResultAsync(
+ new CommandResult(test.Succeeded, exitCode, context.OutputFormat, test.Message, JsonSerializer.Serialize(test, ProtocolJsonContext.Default.HookTestResult)),
+ context.OutputFormat,
+ cancellationToken).ConfigureAwait(false);
+ return exitCode;
+ }
+}
diff --git a/src/SharpClaw.Code.Commands/Handlers/ModelsCommandHandler.cs b/src/SharpClaw.Code.Commands/Handlers/ModelsCommandHandler.cs
new file mode 100644
index 0000000..5cdc014
--- /dev/null
+++ b/src/SharpClaw.Code.Commands/Handlers/ModelsCommandHandler.cs
@@ -0,0 +1,83 @@
+using System.CommandLine;
+using System.Text.Json;
+using Microsoft.Extensions.Options;
+using SharpClaw.Code.Commands.Models;
+using SharpClaw.Code.Commands.Options;
+using SharpClaw.Code.Providers.Abstractions;
+using SharpClaw.Code.Providers.Configuration;
+using SharpClaw.Code.Protocol.Commands;
+using SharpClaw.Code.Protocol.Models;
+using SharpClaw.Code.Protocol.Serialization;
+
+namespace SharpClaw.Code.Commands;
+
+///
+/// Lists the configured provider/model surface available to SharpClaw.
+///
+public sealed class ModelsCommandHandler(
+ IEnumerable modelProviders,
+ IAuthFlowService authFlowService,
+ IOptions providerCatalogOptions,
+ IOptions anthropicOptions,
+ IOptions openAiCompatibleOptions,
+ OutputRendererDispatcher outputRendererDispatcher) : ICommandHandler, ISlashCommandHandler
+{
+ ///
+ public string Name => "models";
+
+ ///
+ public string Description => "Lists provider defaults, aliases, and authentication status.";
+
+ ///
+ public string CommandName => Name;
+
+ ///
+ public Command BuildCommand(GlobalCliOptions globalOptions)
+ {
+ var command = new Command(Name, Description);
+ command.SetAction((parseResult, cancellationToken) => ExecuteAsync(globalOptions.Resolve(parseResult), cancellationToken));
+ return command;
+ }
+
+ ///
+ public Task ExecuteAsync(SlashCommandParseResult command, CommandExecutionContext context, CancellationToken cancellationToken)
+ => ExecuteAsync(context, cancellationToken);
+
+ private async Task ExecuteAsync(CommandExecutionContext context, CancellationToken cancellationToken)
+ {
+ var entries = new List();
+ var aliasesByProvider = providerCatalogOptions.Value.ModelAliases
+ .GroupBy(static pair => pair.Value.ProviderName, StringComparer.OrdinalIgnoreCase)
+ .ToDictionary(
+ static group => group.Key,
+ static group => group.Select(pair => pair.Key).OrderBy(static alias => alias, StringComparer.OrdinalIgnoreCase).ToArray(),
+ StringComparer.OrdinalIgnoreCase);
+
+ foreach (var provider in modelProviders.OrderBy(static provider => provider.ProviderName, StringComparer.OrdinalIgnoreCase))
+ {
+ var auth = await authFlowService.GetStatusAsync(provider.ProviderName, cancellationToken).ConfigureAwait(false);
+ entries.Add(
+ new ProviderModelCatalogEntry(
+ provider.ProviderName,
+ ResolveDefaultModel(provider.ProviderName),
+ aliasesByProvider.TryGetValue(provider.ProviderName, out var aliases) ? aliases : [],
+ auth));
+ }
+
+ var result = new CommandResult(
+ true,
+ 0,
+ context.OutputFormat,
+ $"{entries.Count} provider model surface(s).",
+ JsonSerializer.Serialize(entries, ProtocolJsonContext.Default.ListProviderModelCatalogEntry));
+ await outputRendererDispatcher.RenderCommandResultAsync(result, context.OutputFormat, cancellationToken).ConfigureAwait(false);
+ return 0;
+ }
+
+ private string ResolveDefaultModel(string providerName)
+ => string.Equals(providerName, anthropicOptions.Value.ProviderName, StringComparison.OrdinalIgnoreCase)
+ ? anthropicOptions.Value.DefaultModel
+ : string.Equals(providerName, openAiCompatibleOptions.Value.ProviderName, StringComparison.OrdinalIgnoreCase)
+ ? openAiCompatibleOptions.Value.DefaultModel
+ : "default";
+}
diff --git a/src/SharpClaw.Code.Commands/Handlers/PluginsCommandHandler.cs b/src/SharpClaw.Code.Commands/Handlers/PluginsCommandHandler.cs
index 422bed8..0ba977c 100644
--- a/src/SharpClaw.Code.Commands/Handlers/PluginsCommandHandler.cs
+++ b/src/SharpClaw.Code.Commands/Handlers/PluginsCommandHandler.cs
@@ -14,6 +14,7 @@ namespace SharpClaw.Code.Commands;
///
public sealed class PluginsCommandHandler(
IPluginManager pluginManager,
+ IPluginManifestImportService pluginManifestImportService,
OutputRendererDispatcher outputRendererDispatcher) : ICommandHandler
{
///
@@ -29,6 +30,7 @@ public Command BuildCommand(GlobalCliOptions globalOptions)
command.Subcommands.Add(BuildListCommand(globalOptions));
command.Subcommands.Add(BuildInstallCommand(globalOptions, isUpdate: false));
command.Subcommands.Add(BuildInstallCommand(globalOptions, isUpdate: true));
+ command.Subcommands.Add(BuildImportCommand(globalOptions));
command.Subcommands.Add(BuildEnableCommand(globalOptions));
command.Subcommands.Add(BuildDisableCommand(globalOptions));
command.Subcommands.Add(BuildUninstallCommand(globalOptions));
@@ -126,6 +128,53 @@ private Command BuildUninstallCommand(GlobalCliOptions globalOptions)
return command;
}
+ private Command BuildImportCommand(GlobalCliOptions globalOptions)
+ {
+ var command = new Command("import", "Imports an external manifest into the local SharpClaw plugin format.");
+ var manifestOption = new Option("--manifest")
+ {
+ Required = true,
+ Description = "The path to the external or SharpClaw plugin manifest JSON file."
+ };
+ var formatOption = new Option("--format")
+ {
+ Description = "Manifest format hint: auto, sharpclaw, or external."
+ };
+ var updateOption = new Option("--update")
+ {
+ Description = "Updates the plugin when already installed."
+ };
+
+ command.Options.Add(manifestOption);
+ command.Options.Add(formatOption);
+ command.Options.Add(updateOption);
+ command.SetAction(async (parseResult, cancellationToken) =>
+ {
+ var context = globalOptions.Resolve(parseResult);
+ var manifestPath = parseResult.GetValue(manifestOption) ?? throw new InvalidOperationException("The --manifest option is required.");
+ var format = parseResult.GetValue(formatOption);
+ var update = parseResult.GetValue(updateOption);
+ var (request, importResult) = await pluginManifestImportService.ImportAsync(manifestPath, format, cancellationToken).ConfigureAwait(false);
+ var plugin = update
+ ? await pluginManager.UpdateAsync(context.WorkingDirectory, request, cancellationToken).ConfigureAwait(false)
+ : await pluginManager.InstallAsync(context.WorkingDirectory, request, cancellationToken).ConfigureAwait(false);
+
+ var message = importResult.Warnings.Length == 0
+ ? $"Imported plugin '{plugin.Descriptor.Id}' from {importResult.SourceFormat} manifest."
+ : $"Imported plugin '{plugin.Descriptor.Id}' from {importResult.SourceFormat} manifest with {importResult.Warnings.Length} warning(s).";
+ var result = new CommandResult(
+ true,
+ 0,
+ context.OutputFormat,
+ message,
+ JsonSerializer.Serialize(importResult, ProtocolJsonContext.Default.ImportedPluginManifestResult));
+ await outputRendererDispatcher.RenderCommandResultAsync(result, context.OutputFormat, cancellationToken).ConfigureAwait(false);
+ return 0;
+ });
+
+ return command;
+ }
+
private Command BuildStateCommand(
string name,
string description,
diff --git a/src/SharpClaw.Code.Commands/Handlers/PromptCommandHandler.cs b/src/SharpClaw.Code.Commands/Handlers/PromptCommandHandler.cs
index d30f7d3..024d9c2 100644
--- a/src/SharpClaw.Code.Commands/Handlers/PromptCommandHandler.cs
+++ b/src/SharpClaw.Code.Commands/Handlers/PromptCommandHandler.cs
@@ -61,7 +61,8 @@ private static RuntimeCommandContext ToRuntimeContext(CommandExecutionContext co
context.PermissionMode,
context.OutputFormat,
context.PrimaryMode,
- context.SessionId);
+ context.SessionId,
+ context.AgentId);
private static CommandResult CreateProviderFailureResult(ProviderExecutionException exception, OutputFormat outputFormat)
=> new(
diff --git a/src/SharpClaw.Code.Commands/Handlers/ServeCommandHandler.cs b/src/SharpClaw.Code.Commands/Handlers/ServeCommandHandler.cs
new file mode 100644
index 0000000..5b17bf3
--- /dev/null
+++ b/src/SharpClaw.Code.Commands/Handlers/ServeCommandHandler.cs
@@ -0,0 +1,82 @@
+using System.CommandLine;
+using SharpClaw.Code.Commands.Models;
+using SharpClaw.Code.Commands.Options;
+using SharpClaw.Code.Protocol.Commands;
+using SharpClaw.Code.Runtime.Abstractions;
+
+namespace SharpClaw.Code.Commands;
+
+///
+/// Hosts the embedded SharpClaw HTTP server for local editor and automation clients.
+///
+public sealed class ServeCommandHandler(
+ IWorkspaceHttpServer workspaceHttpServer,
+ OutputRendererDispatcher outputRendererDispatcher) : ICommandHandler, ISlashCommandHandler
+{
+ ///
+ public string Name => "serve";
+
+ ///
+ public string Description => "Runs the embedded SharpClaw HTTP server.";
+
+ ///
+ public string CommandName => Name;
+
+ ///
+ public Command BuildCommand(GlobalCliOptions globalOptions)
+ {
+ var command = new Command(Name, Description);
+ var hostOption = new Option("--host") { Description = "Optional bind host override." };
+ var portOption = new Option("--port") { Description = "Optional bind port override." };
+ command.Options.Add(hostOption);
+ command.Options.Add(portOption);
+ command.SetAction(async (parseResult, cancellationToken) =>
+ {
+ var context = globalOptions.Resolve(parseResult);
+ var host = parseResult.GetValue(hostOption);
+ var port = parseResult.GetValue(portOption);
+ await outputRendererDispatcher.RenderCommandResultAsync(
+ new CommandResult(true, 0, context.OutputFormat, "Starting embedded SharpClaw server. Press Ctrl+C to stop.", null),
+ context.OutputFormat,
+ cancellationToken).ConfigureAwait(false);
+ await workspaceHttpServer
+ .RunAsync(context.WorkingDirectory, host, port, ToRuntimeContext(context), cancellationToken)
+ .ConfigureAwait(false);
+ return 0;
+ });
+ return command;
+ }
+
+ ///
+ public async Task ExecuteAsync(SlashCommandParseResult command, CommandExecutionContext context, CancellationToken cancellationToken)
+ {
+ string? host = null;
+ int? port = null;
+ if (command.Arguments.Length >= 1)
+ {
+ host = command.Arguments[0];
+ }
+
+ if (command.Arguments.Length >= 2 && int.TryParse(command.Arguments[1], out var parsedPort))
+ {
+ port = parsedPort;
+ }
+
+ await outputRendererDispatcher.RenderCommandResultAsync(
+ new CommandResult(true, 0, context.OutputFormat, "Starting embedded SharpClaw server. Press Ctrl+C to stop.", null),
+ context.OutputFormat,
+ cancellationToken).ConfigureAwait(false);
+ await workspaceHttpServer.RunAsync(context.WorkingDirectory, host, port, ToRuntimeContext(context), cancellationToken).ConfigureAwait(false);
+ return 0;
+ }
+
+ private static RuntimeCommandContext ToRuntimeContext(CommandExecutionContext context)
+ => new(
+ context.WorkingDirectory,
+ context.Model,
+ context.PermissionMode,
+ context.OutputFormat,
+ context.PrimaryMode,
+ context.SessionId,
+ context.AgentId);
+}
diff --git a/src/SharpClaw.Code.Commands/Handlers/SessionsSlashCommandHandler.cs b/src/SharpClaw.Code.Commands/Handlers/SessionsSlashCommandHandler.cs
new file mode 100644
index 0000000..e0e264f
--- /dev/null
+++ b/src/SharpClaw.Code.Commands/Handlers/SessionsSlashCommandHandler.cs
@@ -0,0 +1,24 @@
+using SharpClaw.Code.Commands.Models;
+
+namespace SharpClaw.Code.Commands;
+
+///
+/// Provides a friendlier plural alias over the session slash command.
+///
+public sealed class SessionsSlashCommandHandler(SessionCommandHandler sessionCommandHandler) : ISlashCommandHandler
+{
+ ///
+ public string CommandName => "sessions";
+
+ ///
+ public string Description => "Alias for /session list and /session show.";
+
+ ///
+ public Task ExecuteAsync(SlashCommandParseResult command, CommandExecutionContext context, CancellationToken cancellationToken)
+ {
+ var translated = command.Arguments.Length == 0
+ ? new SlashCommandParseResult(true, "session", ["list"])
+ : new SlashCommandParseResult(true, "session", command.Arguments);
+ return sessionCommandHandler.ExecuteAsync(translated, context, cancellationToken);
+ }
+}
diff --git a/src/SharpClaw.Code.Commands/Handlers/ShareCommandHandler.cs b/src/SharpClaw.Code.Commands/Handlers/ShareCommandHandler.cs
new file mode 100644
index 0000000..0b8afe0
--- /dev/null
+++ b/src/SharpClaw.Code.Commands/Handlers/ShareCommandHandler.cs
@@ -0,0 +1,59 @@
+using System.CommandLine;
+using SharpClaw.Code.Commands.Models;
+using SharpClaw.Code.Commands.Options;
+using SharpClaw.Code.Runtime.Abstractions;
+
+namespace SharpClaw.Code.Commands;
+
+///
+/// Creates or refreshes a self-hosted share snapshot for a session.
+///
+public sealed class ShareCommandHandler(
+ IRuntimeCommandService runtimeCommandService,
+ OutputRendererDispatcher outputRendererDispatcher) : ICommandHandler, ISlashCommandHandler
+{
+ ///
+ public string Name => "share";
+
+ ///
+ public string Description => "Creates or refreshes a self-hosted share link for a session.";
+
+ ///
+ public string CommandName => Name;
+
+ ///
+ public Command BuildCommand(GlobalCliOptions globalOptions)
+ {
+ var command = new Command(Name, Description);
+ var idOption = new Option("--id") { Description = "Session id; latest when omitted." };
+ command.Options.Add(idOption);
+ command.SetAction(async (parseResult, cancellationToken) =>
+ {
+ var context = globalOptions.Resolve(parseResult);
+ var id = parseResult.GetValue(idOption);
+ var result = await runtimeCommandService.ShareSessionAsync(id, ToRuntimeContext(context), cancellationToken).ConfigureAwait(false);
+ await outputRendererDispatcher.RenderCommandResultAsync(result, context.OutputFormat, cancellationToken).ConfigureAwait(false);
+ return result.ExitCode;
+ });
+ return command;
+ }
+
+ ///
+ public async Task ExecuteAsync(SlashCommandParseResult command, CommandExecutionContext context, CancellationToken cancellationToken)
+ {
+ var id = command.Arguments.Length > 0 ? command.Arguments[0] : null;
+ var result = await runtimeCommandService.ShareSessionAsync(id, ToRuntimeContext(context), cancellationToken).ConfigureAwait(false);
+ await outputRendererDispatcher.RenderCommandResultAsync(result, context.OutputFormat, cancellationToken).ConfigureAwait(false);
+ return result.ExitCode;
+ }
+
+ private static RuntimeCommandContext ToRuntimeContext(CommandExecutionContext context)
+ => new(
+ context.WorkingDirectory,
+ context.Model,
+ context.PermissionMode,
+ context.OutputFormat,
+ context.PrimaryMode,
+ context.SessionId,
+ context.AgentId);
+}
diff --git a/src/SharpClaw.Code.Commands/Handlers/SkillsCommandHandler.cs b/src/SharpClaw.Code.Commands/Handlers/SkillsCommandHandler.cs
new file mode 100644
index 0000000..4894bd3
--- /dev/null
+++ b/src/SharpClaw.Code.Commands/Handlers/SkillsCommandHandler.cs
@@ -0,0 +1,156 @@
+using System.CommandLine;
+using System.Text.Json;
+using SharpClaw.Code.Commands.Models;
+using SharpClaw.Code.Commands.Options;
+using SharpClaw.Code.Infrastructure.Abstractions;
+using SharpClaw.Code.Protocol.Commands;
+using SharpClaw.Code.Protocol.Models;
+using SharpClaw.Code.Protocol.Serialization;
+using SharpClaw.Code.Skills.Abstractions;
+using SharpClaw.Code.Skills.Models;
+
+namespace SharpClaw.Code.Commands;
+
+///
+/// Manages workspace-local skills under .sharpclaw/skills.
+///
+public sealed class SkillsCommandHandler(
+ ISkillRegistry skillRegistry,
+ IFileSystem fileSystem,
+ OutputRendererDispatcher outputRendererDispatcher) : ICommandHandler, ISlashCommandHandler
+{
+ ///
+ public string Name => "skills";
+
+ ///
+ public string Description => "Lists, installs, inspects, and removes local skills.";
+
+ ///
+ public string CommandName => Name;
+
+ ///
+ public Command BuildCommand(GlobalCliOptions globalOptions)
+ {
+ var command = new Command(Name, Description);
+ command.Subcommands.Add(BuildListCommand(globalOptions));
+ command.Subcommands.Add(BuildShowCommand(globalOptions));
+ command.Subcommands.Add(BuildInstallCommand(globalOptions));
+ command.Subcommands.Add(BuildUninstallCommand(globalOptions));
+ command.SetAction((parseResult, cancellationToken) => ExecuteListAsync(globalOptions.Resolve(parseResult), cancellationToken));
+ return command;
+ }
+
+ ///
+ public Task ExecuteAsync(SlashCommandParseResult command, CommandExecutionContext context, CancellationToken cancellationToken)
+ => command.Arguments.Length switch
+ {
+ 0 => ExecuteListAsync(context, cancellationToken),
+ _ when string.Equals(command.Arguments[0], "list", StringComparison.OrdinalIgnoreCase)
+ => ExecuteListAsync(context, cancellationToken),
+ _ when string.Equals(command.Arguments[0], "show", StringComparison.OrdinalIgnoreCase) && command.Arguments.Length >= 2
+ => ExecuteShowAsync(command.Arguments[1], context, cancellationToken),
+ _ when string.Equals(command.Arguments[0], "install", StringComparison.OrdinalIgnoreCase) && command.Arguments.Length >= 2
+ => ExecuteInstallAsync(command.Arguments[1], context, cancellationToken),
+ _ when string.Equals(command.Arguments[0], "uninstall", StringComparison.OrdinalIgnoreCase) && command.Arguments.Length >= 2
+ => ExecuteUninstallAsync(command.Arguments[1], context, cancellationToken),
+ _ => ExecuteListAsync(context, cancellationToken)
+ };
+
+ private Command BuildListCommand(GlobalCliOptions globalOptions)
+ {
+ var command = new Command("list", "Lists installed skills.");
+ command.SetAction((parseResult, cancellationToken) => ExecuteListAsync(globalOptions.Resolve(parseResult), cancellationToken));
+ return command;
+ }
+
+ private Command BuildShowCommand(GlobalCliOptions globalOptions)
+ {
+ var command = new Command("show", "Shows a skill manifest and prompt.");
+ var idOption = new Option("--id") { Required = true, Description = "Skill id or name." };
+ command.Options.Add(idOption);
+ command.SetAction((parseResult, cancellationToken) => ExecuteShowAsync(parseResult.GetValue(idOption)!, globalOptions.Resolve(parseResult), cancellationToken));
+ return command;
+ }
+
+ private Command BuildInstallCommand(GlobalCliOptions globalOptions)
+ {
+ var command = new Command("install", "Installs a skill from a JSON manifest.");
+ var manifestOption = new Option("--manifest") { Required = true, Description = "Path to a serialized SkillInstallRequest JSON document." };
+ command.Options.Add(manifestOption);
+ command.SetAction((parseResult, cancellationToken) => ExecuteInstallAsync(parseResult.GetValue(manifestOption)!, globalOptions.Resolve(parseResult), cancellationToken));
+ return command;
+ }
+
+ private Command BuildUninstallCommand(GlobalCliOptions globalOptions)
+ {
+ var command = new Command("uninstall", "Removes an installed skill.");
+ var idOption = new Option("--id") { Required = true, Description = "Skill id." };
+ command.Options.Add(idOption);
+ command.SetAction((parseResult, cancellationToken) => ExecuteUninstallAsync(parseResult.GetValue(idOption)!, globalOptions.Resolve(parseResult), cancellationToken));
+ return command;
+ }
+
+ private async Task ExecuteListAsync(CommandExecutionContext context, CancellationToken cancellationToken)
+ {
+ var skills = await skillRegistry.ListAsync(context.WorkingDirectory, cancellationToken).ConfigureAwait(false);
+ var result = new CommandResult(
+ true,
+ 0,
+ context.OutputFormat,
+ skills.Count == 0 ? "No skills installed." : $"{skills.Count} skill(s).",
+ JsonSerializer.Serialize(skills.ToList(), ProtocolJsonContext.Default.ListSkillDefinition));
+ await outputRendererDispatcher.RenderCommandResultAsync(result, context.OutputFormat, cancellationToken).ConfigureAwait(false);
+ return 0;
+ }
+
+ private async Task ExecuteShowAsync(string id, CommandExecutionContext context, CancellationToken cancellationToken)
+ {
+ var skill = await skillRegistry.ResolveAsync(context.WorkingDirectory, id, cancellationToken).ConfigureAwait(false);
+ if (skill is null)
+ {
+ await outputRendererDispatcher.RenderCommandResultAsync(
+ new CommandResult(false, 1, context.OutputFormat, $"Skill '{id}' was not found.", null),
+ context.OutputFormat,
+ cancellationToken).ConfigureAwait(false);
+ return 1;
+ }
+
+ var payload = new SkillInspectionRecord(skill.Definition, skill.PromptTemplate, skill.Metadata);
+ await outputRendererDispatcher.RenderCommandResultAsync(
+ new CommandResult(true, 0, context.OutputFormat, $"{skill.Definition.Id}: {skill.Definition.Description}", JsonSerializer.Serialize(payload, ProtocolJsonContext.Default.SkillInspectionRecord)),
+ context.OutputFormat,
+ cancellationToken).ConfigureAwait(false);
+ return 0;
+ }
+
+ private async Task ExecuteInstallAsync(string manifestPath, CommandExecutionContext context, CancellationToken cancellationToken)
+ {
+ var manifestJson = await fileSystem.ReadAllTextIfExistsAsync(manifestPath, cancellationToken).ConfigureAwait(false)
+ ?? throw new InvalidOperationException($"Skill manifest '{manifestPath}' was not found.");
+ var request = JsonSerializer.Deserialize(manifestJson, new JsonSerializerOptions(JsonSerializerDefaults.Web))
+ ?? throw new InvalidOperationException($"Skill manifest '{manifestPath}' could not be parsed.");
+ var installed = await skillRegistry.InstallAsync(context.WorkingDirectory, request, cancellationToken).ConfigureAwait(false);
+ var payload = new SkillInspectionRecord(installed.Definition, installed.PromptTemplate, installed.Metadata);
+ await outputRendererDispatcher.RenderCommandResultAsync(
+ new CommandResult(true, 0, context.OutputFormat, $"Installed skill '{installed.Definition.Id}'.", JsonSerializer.Serialize(payload, ProtocolJsonContext.Default.SkillInspectionRecord)),
+ context.OutputFormat,
+ cancellationToken).ConfigureAwait(false);
+ return 0;
+ }
+
+ private async Task ExecuteUninstallAsync(string id, CommandExecutionContext context, CancellationToken cancellationToken)
+ {
+ var removed = await skillRegistry.UninstallAsync(context.WorkingDirectory, id, cancellationToken).ConfigureAwait(false);
+ var exitCode = removed ? 0 : 1;
+ await outputRendererDispatcher.RenderCommandResultAsync(
+ new CommandResult(
+ removed,
+ exitCode,
+ context.OutputFormat,
+ removed ? $"Removed skill '{id}'." : $"Skill '{id}' was not found.",
+ JsonSerializer.Serialize(new Dictionary { ["id"] = id }, ProtocolJsonContext.Default.DictionaryStringString)),
+ context.OutputFormat,
+ cancellationToken).ConfigureAwait(false);
+ return exitCode;
+ }
+}
diff --git a/src/SharpClaw.Code.Commands/Handlers/StatsCommandHandler.cs b/src/SharpClaw.Code.Commands/Handlers/StatsCommandHandler.cs
new file mode 100644
index 0000000..0b924fe
--- /dev/null
+++ b/src/SharpClaw.Code.Commands/Handlers/StatsCommandHandler.cs
@@ -0,0 +1,53 @@
+using System.CommandLine;
+using System.Text.Json;
+using SharpClaw.Code.Commands.Models;
+using SharpClaw.Code.Commands.Options;
+using SharpClaw.Code.Protocol.Commands;
+using SharpClaw.Code.Protocol.Serialization;
+using SharpClaw.Code.Runtime.Abstractions;
+
+namespace SharpClaw.Code.Commands;
+
+///
+/// Surfaces durable workspace execution counts.
+///
+public sealed class StatsCommandHandler(
+ IWorkspaceInsightsService workspaceInsightsService,
+ OutputRendererDispatcher outputRendererDispatcher) : ICommandHandler, ISlashCommandHandler
+{
+ ///
+ public string Name => "stats";
+
+ ///
+ public string Description => "Shows turn, tool, provider, share, and todo counts.";
+
+ ///
+ public string CommandName => Name;
+
+ ///
+ public Command BuildCommand(GlobalCliOptions globalOptions)
+ {
+ var command = new Command(Name, Description);
+ command.SetAction((parseResult, cancellationToken) => ExecuteAsync(globalOptions.Resolve(parseResult), cancellationToken));
+ return command;
+ }
+
+ ///
+ public Task ExecuteAsync(SlashCommandParseResult command, CommandExecutionContext context, CancellationToken cancellationToken)
+ => ExecuteAsync(context, cancellationToken);
+
+ private async Task ExecuteAsync(CommandExecutionContext context, CancellationToken cancellationToken)
+ {
+ var report = await workspaceInsightsService
+ .BuildStatsReportAsync(context.WorkingDirectory, context.SessionId, cancellationToken)
+ .ConfigureAwait(false);
+ var result = new CommandResult(
+ true,
+ 0,
+ context.OutputFormat,
+ $"{report.SessionCount} session(s), {report.TurnCompletedCount} completed turn(s), {report.ToolExecutionCount} tool execution(s), {report.ActiveTodoCount} active todo(s).",
+ JsonSerializer.Serialize(report, ProtocolJsonContext.Default.WorkspaceStatsReport));
+ await outputRendererDispatcher.RenderCommandResultAsync(result, context.OutputFormat, cancellationToken).ConfigureAwait(false);
+ return 0;
+ }
+}
diff --git a/src/SharpClaw.Code.Commands/Handlers/TodoCommandHandler.cs b/src/SharpClaw.Code.Commands/Handlers/TodoCommandHandler.cs
new file mode 100644
index 0000000..4035678
--- /dev/null
+++ b/src/SharpClaw.Code.Commands/Handlers/TodoCommandHandler.cs
@@ -0,0 +1,300 @@
+using System.CommandLine;
+using System.Text.Json;
+using SharpClaw.Code.Commands.Models;
+using SharpClaw.Code.Commands.Options;
+using SharpClaw.Code.Protocol.Commands;
+using SharpClaw.Code.Protocol.Models;
+using SharpClaw.Code.Protocol.Serialization;
+using SharpClaw.Code.Runtime.Abstractions;
+using SharpClaw.Code.Sessions.Abstractions;
+
+namespace SharpClaw.Code.Commands;
+
+///
+/// Manages durable session and workspace todo items.
+///
+public sealed class TodoCommandHandler(
+ ITodoService todoService,
+ ISessionCoordinator sessionCoordinator,
+ ISessionStore sessionStore,
+ OutputRendererDispatcher outputRendererDispatcher) : ICommandHandler, ISlashCommandHandler
+{
+ ///
+ public string Name => "todo";
+
+ ///
+ public string Description => "Lists and mutates session or workspace todo items.";
+
+ ///
+ public string CommandName => Name;
+
+ ///
+ public Command BuildCommand(GlobalCliOptions globalOptions)
+ {
+ var command = new Command(Name, Description);
+ command.Subcommands.Add(BuildListCommand(globalOptions));
+ command.Subcommands.Add(BuildAddCommand(globalOptions));
+ command.Subcommands.Add(BuildUpdateCommand(globalOptions));
+ command.Subcommands.Add(BuildDoneCommand(globalOptions));
+ command.Subcommands.Add(BuildRemoveCommand(globalOptions));
+ command.SetAction((parseResult, cancellationToken) => ExecuteListAsync(null, null, globalOptions.Resolve(parseResult), cancellationToken));
+ return command;
+ }
+
+ ///
+ public async Task ExecuteAsync(SlashCommandParseResult command, CommandExecutionContext context, CancellationToken cancellationToken)
+ {
+ if (command.Arguments.Length == 0 || string.Equals(command.Arguments[0], "list", StringComparison.OrdinalIgnoreCase))
+ {
+ var scopeText = command.Arguments.Length >= 2 ? command.Arguments[1] : null;
+ var sessionId = command.Arguments.Length >= 3 ? command.Arguments[2] : null;
+ return await ExecuteListAsync(scopeText, sessionId, context, cancellationToken).ConfigureAwait(false);
+ }
+
+ if (string.Equals(command.Arguments[0], "add", StringComparison.OrdinalIgnoreCase) && command.Arguments.Length >= 3)
+ {
+ var scope = ParseScope(command.Arguments[1]);
+ var title = string.Join(' ', command.Arguments.Skip(2));
+ return await ExecuteAddAsync(scope, title, null, null, context, cancellationToken).ConfigureAwait(false);
+ }
+
+ if (string.Equals(command.Arguments[0], "update", StringComparison.OrdinalIgnoreCase) && command.Arguments.Length >= 4)
+ {
+ var scope = ParseScope(command.Arguments[1]);
+ var id = command.Arguments[2];
+ var status = TryParseStatus(command.Arguments[3]);
+ var title = command.Arguments.Length > 4 ? string.Join(' ', command.Arguments.Skip(4)) : null;
+ return await ExecuteUpdateAsync(scope, id, title, status, null, null, context, cancellationToken).ConfigureAwait(false);
+ }
+
+ if (string.Equals(command.Arguments[0], "done", StringComparison.OrdinalIgnoreCase) && command.Arguments.Length >= 3)
+ {
+ var scope = ParseScope(command.Arguments[1]);
+ return await ExecuteUpdateAsync(scope, command.Arguments[2], null, TodoStatus.Done, null, null, context, cancellationToken).ConfigureAwait(false);
+ }
+
+ if (string.Equals(command.Arguments[0], "remove", StringComparison.OrdinalIgnoreCase) && command.Arguments.Length >= 3)
+ {
+ var scope = ParseScope(command.Arguments[1]);
+ return await ExecuteRemoveAsync(scope, command.Arguments[2], null, context, cancellationToken).ConfigureAwait(false);
+ }
+
+ await outputRendererDispatcher.RenderCommandResultAsync(
+ new CommandResult(false, 1, context.OutputFormat, "Unsupported todo syntax.", null),
+ context.OutputFormat,
+ cancellationToken).ConfigureAwait(false);
+ return 1;
+ }
+
+ private Command BuildListCommand(GlobalCliOptions globalOptions)
+ {
+ var command = new Command("list", "Lists workspace and session todos.");
+ var scopeOption = new Option("--scope") { Description = "Scope: session, workspace, or all." };
+ var sessionOption = new Option("--session") { Description = "Session id when listing session todos." };
+ command.Options.Add(scopeOption);
+ command.Options.Add(sessionOption);
+ command.SetAction((parseResult, cancellationToken) => ExecuteListAsync(parseResult.GetValue(scopeOption), parseResult.GetValue(sessionOption), globalOptions.Resolve(parseResult), cancellationToken));
+ return command;
+ }
+
+ private Command BuildAddCommand(GlobalCliOptions globalOptions)
+ {
+ var command = new Command("add", "Adds a todo item.");
+ var scopeOption = new Option("--scope") { Required = true, Description = "Scope: session or workspace." };
+ var titleOption = new Option("--title") { Required = true, Description = "Todo title." };
+ var sessionOption = new Option("--session") { Description = "Session id for session-scoped todos." };
+ var ownerOption = new Option("--owner-agent") { Description = "Optional owner agent id." };
+ command.Options.Add(scopeOption);
+ command.Options.Add(titleOption);
+ command.Options.Add(sessionOption);
+ command.Options.Add(ownerOption);
+ command.SetAction((parseResult, cancellationToken) => ExecuteAddAsync(ParseScope(parseResult.GetValue(scopeOption)!), parseResult.GetValue(titleOption)!, parseResult.GetValue(sessionOption), parseResult.GetValue(ownerOption), globalOptions.Resolve(parseResult), cancellationToken));
+ return command;
+ }
+
+ private Command BuildUpdateCommand(GlobalCliOptions globalOptions)
+ {
+ var command = new Command("update", "Updates a todo item.");
+ var scopeOption = new Option("--scope") { Required = true, Description = "Scope: session or workspace." };
+ var idOption = new Option("--id") { Required = true, Description = "Todo id." };
+ var titleOption = new Option("--title") { Description = "Optional replacement title." };
+ var statusOption = new Option("--status") { Description = "Optional status: open, inProgress, blocked, done." };
+ var sessionOption = new Option("--session") { Description = "Session id for session-scoped todos." };
+ var ownerOption = new Option("--owner-agent") { Description = "Optional owner agent id." };
+ command.Options.Add(scopeOption);
+ command.Options.Add(idOption);
+ command.Options.Add(titleOption);
+ command.Options.Add(statusOption);
+ command.Options.Add(sessionOption);
+ command.Options.Add(ownerOption);
+ command.SetAction((parseResult, cancellationToken) => ExecuteUpdateAsync(
+ ParseScope(parseResult.GetValue(scopeOption)!),
+ parseResult.GetValue(idOption)!,
+ parseResult.GetValue(titleOption),
+ TryParseStatus(parseResult.GetValue(statusOption)),
+ parseResult.GetValue(sessionOption),
+ parseResult.GetValue(ownerOption),
+ globalOptions.Resolve(parseResult),
+ cancellationToken));
+ return command;
+ }
+
+ private Command BuildDoneCommand(GlobalCliOptions globalOptions)
+ {
+ var command = new Command("done", "Marks a todo as done.");
+ var scopeOption = new Option("--scope") { Required = true, Description = "Scope: session or workspace." };
+ var idOption = new Option("--id") { Required = true, Description = "Todo id." };
+ var sessionOption = new Option("--session") { Description = "Session id for session-scoped todos." };
+ command.Options.Add(scopeOption);
+ command.Options.Add(idOption);
+ command.Options.Add(sessionOption);
+ command.SetAction((parseResult, cancellationToken) => ExecuteUpdateAsync(
+ ParseScope(parseResult.GetValue(scopeOption)!),
+ parseResult.GetValue(idOption)!,
+ null,
+ TodoStatus.Done,
+ parseResult.GetValue(sessionOption),
+ null,
+ globalOptions.Resolve(parseResult),
+ cancellationToken));
+ return command;
+ }
+
+ private Command BuildRemoveCommand(GlobalCliOptions globalOptions)
+ {
+ var command = new Command("remove", "Removes a todo item.");
+ var scopeOption = new Option("--scope") { Required = true, Description = "Scope: session or workspace." };
+ var idOption = new Option("--id") { Required = true, Description = "Todo id." };
+ var sessionOption = new Option("--session") { Description = "Session id for session-scoped todos." };
+ command.Options.Add(scopeOption);
+ command.Options.Add(idOption);
+ command.Options.Add(sessionOption);
+ command.SetAction((parseResult, cancellationToken) => ExecuteRemoveAsync(ParseScope(parseResult.GetValue(scopeOption)!), parseResult.GetValue(idOption)!, parseResult.GetValue(sessionOption), globalOptions.Resolve(parseResult), cancellationToken));
+ return command;
+ }
+
+ private async Task ExecuteListAsync(string? scopeText, string? explicitSessionId, CommandExecutionContext context, CancellationToken cancellationToken)
+ {
+ var scope = ParseOptionalScope(scopeText);
+ var sessionId = explicitSessionId;
+ if (scope is null or TodoScope.Session)
+ {
+ sessionId = await ResolveSessionIdAsync(context.WorkingDirectory, explicitSessionId ?? context.SessionId, cancellationToken).ConfigureAwait(false);
+ }
+
+ var snapshot = await todoService.GetSnapshotAsync(context.WorkingDirectory, sessionId, cancellationToken).ConfigureAwait(false);
+ var payload = scope switch
+ {
+ TodoScope.Session => snapshot with { WorkspaceTodos = [] },
+ TodoScope.Workspace => snapshot with { SessionTodos = [] },
+ _ => snapshot
+ };
+ var count = payload.SessionTodos.Count + payload.WorkspaceTodos.Count;
+ await outputRendererDispatcher.RenderCommandResultAsync(
+ new CommandResult(true, 0, context.OutputFormat, $"{count} todo item(s).", JsonSerializer.Serialize(payload, ProtocolJsonContext.Default.TodoSnapshot)),
+ context.OutputFormat,
+ cancellationToken).ConfigureAwait(false);
+ return 0;
+ }
+
+ private async Task ExecuteAddAsync(
+ TodoScope scope,
+ string title,
+ string? explicitSessionId,
+ string? ownerAgentId,
+ CommandExecutionContext context,
+ CancellationToken cancellationToken)
+ {
+ var sessionId = scope == TodoScope.Session
+ ? await ResolveSessionIdAsync(context.WorkingDirectory, explicitSessionId ?? context.SessionId, cancellationToken).ConfigureAwait(false)
+ : explicitSessionId ?? context.SessionId;
+ var todo = await todoService.AddAsync(context.WorkingDirectory, scope, title, sessionId, ownerAgentId, cancellationToken).ConfigureAwait(false);
+ await outputRendererDispatcher.RenderCommandResultAsync(
+ new CommandResult(true, 0, context.OutputFormat, $"Added todo '{todo.Id}'.", JsonSerializer.Serialize(todo, ProtocolJsonContext.Default.TodoItem)),
+ context.OutputFormat,
+ cancellationToken).ConfigureAwait(false);
+ return 0;
+ }
+
+ private async Task ExecuteUpdateAsync(
+ TodoScope scope,
+ string id,
+ string? title,
+ TodoStatus? status,
+ string? explicitSessionId,
+ string? ownerAgentId,
+ CommandExecutionContext context,
+ CancellationToken cancellationToken)
+ {
+ var sessionId = scope == TodoScope.Session
+ ? await ResolveSessionIdAsync(context.WorkingDirectory, explicitSessionId ?? context.SessionId, cancellationToken).ConfigureAwait(false)
+ : explicitSessionId ?? context.SessionId;
+ var todo = await todoService.UpdateAsync(context.WorkingDirectory, scope, id, sessionId, title, status, ownerAgentId, cancellationToken).ConfigureAwait(false);
+ await outputRendererDispatcher.RenderCommandResultAsync(
+ new CommandResult(true, 0, context.OutputFormat, $"Updated todo '{todo.Id}'.", JsonSerializer.Serialize(todo, ProtocolJsonContext.Default.TodoItem)),
+ context.OutputFormat,
+ cancellationToken).ConfigureAwait(false);
+ return 0;
+ }
+
+ private async Task ExecuteRemoveAsync(
+ TodoScope scope,
+ string id,
+ string? explicitSessionId,
+ CommandExecutionContext context,
+ CancellationToken cancellationToken)
+ {
+ var sessionId = scope == TodoScope.Session
+ ? await ResolveSessionIdAsync(context.WorkingDirectory, explicitSessionId ?? context.SessionId, cancellationToken).ConfigureAwait(false)
+ : explicitSessionId ?? context.SessionId;
+ var removed = await todoService.RemoveAsync(context.WorkingDirectory, scope, id, sessionId, cancellationToken).ConfigureAwait(false);
+ var exitCode = removed ? 0 : 1;
+ await outputRendererDispatcher.RenderCommandResultAsync(
+ new CommandResult(removed, exitCode, context.OutputFormat, removed ? $"Removed todo '{id}'." : $"Todo '{id}' was not found.", JsonSerializer.Serialize(new Dictionary { ["id"] = id }, ProtocolJsonContext.Default.DictionaryStringString)),
+ context.OutputFormat,
+ cancellationToken).ConfigureAwait(false);
+ return exitCode;
+ }
+
+ private async Task ResolveSessionIdAsync(string workspaceRoot, string? preferredSessionId, CancellationToken cancellationToken)
+ {
+ if (!string.IsNullOrWhiteSpace(preferredSessionId))
+ {
+ return preferredSessionId;
+ }
+
+ var attached = await sessionCoordinator.GetAttachedSessionIdAsync(workspaceRoot, cancellationToken).ConfigureAwait(false);
+ if (!string.IsNullOrWhiteSpace(attached))
+ {
+ return attached;
+ }
+
+ var latest = await sessionStore.GetLatestAsync(workspaceRoot, cancellationToken).ConfigureAwait(false);
+ return latest?.Id;
+ }
+
+ private static TodoScope ParseScope(string scope)
+ => scope.Trim().ToLowerInvariant() switch
+ {
+ "session" => TodoScope.Session,
+ "workspace" => TodoScope.Workspace,
+ _ => throw new InvalidOperationException($"Unsupported todo scope '{scope}'.")
+ };
+
+ private static TodoScope? ParseOptionalScope(string? scope)
+ => string.IsNullOrWhiteSpace(scope)
+ ? null
+ : ParseScope(scope);
+
+ private static TodoStatus? TryParseStatus(string? status)
+ => string.IsNullOrWhiteSpace(status)
+ ? null
+ : status.Trim().ToLowerInvariant() switch
+ {
+ "open" => TodoStatus.Open,
+ "inprogress" or "in-progress" => TodoStatus.InProgress,
+ "blocked" => TodoStatus.Blocked,
+ "done" => TodoStatus.Done,
+ _ => throw new InvalidOperationException($"Unsupported todo status '{status}'.")
+ };
+}
diff --git a/src/SharpClaw.Code.Commands/Handlers/UnshareCommandHandler.cs b/src/SharpClaw.Code.Commands/Handlers/UnshareCommandHandler.cs
new file mode 100644
index 0000000..ec32c45
--- /dev/null
+++ b/src/SharpClaw.Code.Commands/Handlers/UnshareCommandHandler.cs
@@ -0,0 +1,59 @@
+using System.CommandLine;
+using SharpClaw.Code.Commands.Models;
+using SharpClaw.Code.Commands.Options;
+using SharpClaw.Code.Runtime.Abstractions;
+
+namespace SharpClaw.Code.Commands;
+
+///
+/// Removes a self-hosted share snapshot for a session.
+///
+public sealed class UnshareCommandHandler(
+ IRuntimeCommandService runtimeCommandService,
+ OutputRendererDispatcher outputRendererDispatcher) : ICommandHandler, ISlashCommandHandler
+{
+ ///
+ public string Name => "unshare";
+
+ ///
+ public string Description => "Removes a self-hosted share link for a session.";
+
+ ///
+ public string CommandName => Name;
+
+ ///
+ public Command BuildCommand(GlobalCliOptions globalOptions)
+ {
+ var command = new Command(Name, Description);
+ var idOption = new Option("--id") { Description = "Session id; latest when omitted." };
+ command.Options.Add(idOption);
+ command.SetAction(async (parseResult, cancellationToken) =>
+ {
+ var context = globalOptions.Resolve(parseResult);
+ var id = parseResult.GetValue(idOption);
+ var result = await runtimeCommandService.UnshareSessionAsync(id, ToRuntimeContext(context), cancellationToken).ConfigureAwait(false);
+ await outputRendererDispatcher.RenderCommandResultAsync(result, context.OutputFormat, cancellationToken).ConfigureAwait(false);
+ return result.ExitCode;
+ });
+ return command;
+ }
+
+ ///
+ public async Task ExecuteAsync(SlashCommandParseResult command, CommandExecutionContext context, CancellationToken cancellationToken)
+ {
+ var id = command.Arguments.Length > 0 ? command.Arguments[0] : null;
+ var result = await runtimeCommandService.UnshareSessionAsync(id, ToRuntimeContext(context), cancellationToken).ConfigureAwait(false);
+ await outputRendererDispatcher.RenderCommandResultAsync(result, context.OutputFormat, cancellationToken).ConfigureAwait(false);
+ return result.ExitCode;
+ }
+
+ private static RuntimeCommandContext ToRuntimeContext(CommandExecutionContext context)
+ => new(
+ context.WorkingDirectory,
+ context.Model,
+ context.PermissionMode,
+ context.OutputFormat,
+ context.PrimaryMode,
+ context.SessionId,
+ context.AgentId);
+}
diff --git a/src/SharpClaw.Code.Commands/Handlers/UsageCommandHandler.cs b/src/SharpClaw.Code.Commands/Handlers/UsageCommandHandler.cs
new file mode 100644
index 0000000..f100794
--- /dev/null
+++ b/src/SharpClaw.Code.Commands/Handlers/UsageCommandHandler.cs
@@ -0,0 +1,54 @@
+using System.CommandLine;
+using System.Text.Json;
+using SharpClaw.Code.Commands.Models;
+using SharpClaw.Code.Commands.Options;
+using SharpClaw.Code.Protocol.Commands;
+using SharpClaw.Code.Protocol.Models;
+using SharpClaw.Code.Protocol.Serialization;
+using SharpClaw.Code.Runtime.Abstractions;
+
+namespace SharpClaw.Code.Commands;
+
+///
+/// Surfaces durable usage totals for the current workspace.
+///
+public sealed class UsageCommandHandler(
+ IWorkspaceInsightsService workspaceInsightsService,
+ OutputRendererDispatcher outputRendererDispatcher) : ICommandHandler, ISlashCommandHandler
+{
+ ///
+ public string Name => "usage";
+
+ ///
+ public string Description => "Shows session and workspace token usage totals.";
+
+ ///
+ public string CommandName => Name;
+
+ ///
+ public Command BuildCommand(GlobalCliOptions globalOptions)
+ {
+ var command = new Command(Name, Description);
+ command.SetAction((parseResult, cancellationToken) => ExecuteAsync(globalOptions.Resolve(parseResult), cancellationToken));
+ return command;
+ }
+
+ ///
+ public Task ExecuteAsync(SlashCommandParseResult command, CommandExecutionContext context, CancellationToken cancellationToken)
+ => ExecuteAsync(context, cancellationToken);
+
+ private async Task ExecuteAsync(CommandExecutionContext context, CancellationToken cancellationToken)
+ {
+ var report = await workspaceInsightsService
+ .BuildUsageReportAsync(context.WorkingDirectory, context.SessionId, cancellationToken)
+ .ConfigureAwait(false);
+ var result = new CommandResult(
+ true,
+ 0,
+ context.OutputFormat,
+ $"Workspace total: {report.WorkspaceTotal.TotalTokens} tokens across {report.Sessions.Count} session(s).",
+ JsonSerializer.Serialize(report, ProtocolJsonContext.Default.WorkspaceUsageReport));
+ await outputRendererDispatcher.RenderCommandResultAsync(result, context.OutputFormat, cancellationToken).ConfigureAwait(false);
+ return 0;
+ }
+}
diff --git a/src/SharpClaw.Code.Commands/Models/CommandExecutionContext.cs b/src/SharpClaw.Code.Commands/Models/CommandExecutionContext.cs
index ff3fba0..2e00c4f 100644
--- a/src/SharpClaw.Code.Commands/Models/CommandExecutionContext.cs
+++ b/src/SharpClaw.Code.Commands/Models/CommandExecutionContext.cs
@@ -11,10 +11,12 @@ namespace SharpClaw.Code.Commands.Models;
/// The requested output format.
/// Build, plan, or spec workflow from global CLI options.
/// Optional explicit session id for prompts and session-scoped commands.
+/// Optional explicit agent id for prompt execution.
public sealed record CommandExecutionContext(
string WorkingDirectory,
string? Model,
PermissionMode PermissionMode,
OutputFormat OutputFormat,
PrimaryMode PrimaryMode,
- string? SessionId = null);
+ string? SessionId = null,
+ string? AgentId = null);
diff --git a/src/SharpClaw.Code.Commands/Options/GlobalCliOptions.cs b/src/SharpClaw.Code.Commands/Options/GlobalCliOptions.cs
index 1308c67..4e2e96d 100644
--- a/src/SharpClaw.Code.Commands/Options/GlobalCliOptions.cs
+++ b/src/SharpClaw.Code.Commands/Options/GlobalCliOptions.cs
@@ -53,6 +53,12 @@ public GlobalCliOptions()
Description = "Targets a specific SharpClaw session id for prompts.",
Recursive = true
};
+
+ AgentOption = new Option("--agent")
+ {
+ Description = "Selects the effective agent id for prompt execution.",
+ Recursive = true
+ };
}
///
@@ -85,10 +91,15 @@ public GlobalCliOptions()
///
public Option SessionOption { get; }
+ ///
+ /// Gets the optional agent id option.
+ ///
+ public Option AgentOption { get; }
+
///
/// Gets all global options.
///
- public IEnumerable