Skip to content

Commit 17e679b

Browse files
committed
add cline-inspired workflow features
1 parent 1070122 commit 17e679b

75 files changed

Lines changed: 3694 additions & 99 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ dotnet run --project src/SharpClaw.Code.Cli -- repl
6262

6363
# Run a one-shot prompt
6464
dotnet run --project src/SharpClaw.Code.Cli -- prompt "Summarize this workspace"
65+
dotnet run --project src/SharpClaw.Code.Cli -- --auto-approve shell --auto-approve-budget 2 prompt "Check git status and summarize"
6566

6667
# Inspect runtime health and status
6768
dotnet run --project src/SharpClaw.Code.Cli -- doctor
@@ -105,12 +106,13 @@ Parity-oriented commands now include:
105106
- `unshare` / `/unshare`
106107
- `compact` / `/compact`
107108
- `serve` / `/serve`
109+
- `worktree` / `/worktree`
108110
- `/sessions` as a friendlier alias over `/session list`
109111

110112
Primary workflow modes:
111113

112114
- `build`: normal coding-agent execution
113-
- `plan`: analysis-first mode that blocks mutating tools
115+
- `plan`: structured deep planning that blocks mutating tools and syncs planning-owned session todos
114116
- `spec`: generates Kiro-style spec artifacts under `docs/superpowers/specs/<date>-<slug>/`
115117

116118
## Core Capabilities
@@ -202,6 +204,8 @@ dotnet test SharpClawCode.sln --filter "FullyQualifiedName~ParityScenarioTests"
202204
| `--cwd <path>` | Working directory; defaults to the current directory |
203205
| `--model <id>` | Model id or alias; `provider/model` forms are supported where configured |
204206
| `--permission-mode <mode>` | `readOnly`, `workspaceWrite`, or `dangerFullAccess`; see [docs/permissions.md](docs/permissions.md) |
207+
| `--auto-approve <scopes>` | Auto-approve specific elevated scopes such as `shell`, `network`, or `promptRead` |
208+
| `--auto-approve-budget <n>` | Cap how many elevated operations may be auto-approved in the session |
205209
| `--output-format text\|json` | Human-readable or structured output |
206210
| `--primary-mode <mode>` | Workflow bias for prompts: `build`, `plan`, or `spec` |
207211
| `--session <id>` | Reuse a specific SharpClaw session id for prompt execution |
@@ -211,7 +215,7 @@ dotnet test SharpClawCode.sln --filter "FullyQualifiedName~ParityScenarioTests"
211215
| `--storage-root <path>` | External root for host-managed durable runtime state |
212216
| `--session-store fileSystem\|sqlite` | Select the embedded session/event storage backend |
213217

214-
Subcommands include `prompt`, `repl`, `doctor`, `status`, `session`, `index`, `memory`, `models`, `usage`, `cost`, `stats`, `connect`, `hooks`, `skills`, `agents`, `todo`, `share`, `unshare`, `compact`, `serve`, `commands`, `mcp`, `plugins`, `tool-packages`, `acp`, `bridge`, and `version`.
218+
Subcommands include `prompt`, `repl`, `doctor`, `status`, `session`, `index`, `memory`, `models`, `usage`, `cost`, `stats`, `connect`, `hooks`, `skills`, `agents`, `todo`, `share`, `unshare`, `compact`, `serve`, `commands`, `worktree`, `mcp`, `plugins`, `tool-packages`, `acp`, `bridge`, and `version`.
215219

216220
## Documentation Map
217221

docs/permissions.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,29 @@
1212

1313
Default: **`WorkspaceWrite`**.
1414

15+
## Bounded auto-approval
16+
17+
The CLI and REPL now support finer-grained approval control without switching all the way to **`DangerFullAccess`**.
18+
19+
- CLI:
20+
- `--auto-approve shell,network`
21+
- `--auto-approve-budget 3`
22+
- REPL:
23+
- `/approvals`
24+
- `/approvals set shell,promptRead 2`
25+
- `/approvals reset`
26+
27+
`ApprovalSettings` flow through `RuntimeCommandContext`, `RunPromptRequest`, `ToolExecutionContext`, and `PermissionEvaluationContext`.
28+
29+
Behavior:
30+
31+
- matching scopes are auto-approved only when the current rule/mode path would otherwise ask for approval
32+
- explicit deny rules still win
33+
- remembered approvals still short-circuit before budget consumption
34+
- when the configured auto-approve budget is exhausted, the engine falls back to the normal approval transport
35+
36+
The auto-approve budget is process-local and session-scoped, similar to remembered approvals.
37+
1538
## Policy engine
1639

1740
**`PermissionPolicyEngine`** evaluates **`ToolExecutionRequest`** with **`PermissionEvaluationContext`** by running an ordered list of **`IPermissionRule`** instances:
@@ -57,6 +80,15 @@ Authenticated approvals are tenant-bound. If the runtime host context carries `T
5780

5881
When a rule returns **`RequireApproval`** with **`CanRememberApproval`**, an approved outcome may be **`Store`**d and reused via **`TryGet`**. In embedded-host flows, the remembered approval remains scoped to the current session and tenant context.
5982

83+
### Auto-approve budget tracking
84+
85+
**`IAutoApprovalBudgetTracker`** (**`AutoApprovalBudgetTracker`**) tracks how many elevated operations have been auto-approved for the current session/tenant key.
86+
87+
When `ApprovalSettings.AutoApproveBudget` is set:
88+
89+
- the first matching operations consume the budget and are auto-approved
90+
- later matching operations are no longer auto-approved and go through the normal approval path
91+
6092
## Tool execution context
6193

6294
**`ToolExecutionContext`** (`src/SharpClaw.Code.Tools/Models/ToolExecutionContext.cs`) carries **`IsInteractive`** (default **true** on the record). Parity tests set **`interactive: true/false`** to exercise approval vs deny paths.

docs/runtime.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ Prompt references are resolved before provider execution. Outside-workspace file
5353

5454
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.
5555

56+
When the effective **`PrimaryMode`** is **`Plan`**, the assembler now appends a deep-planning JSON contract that requires the model to return summary, assumptions, risks, next action, and task data.
57+
5658
Conversation history is rebuilt from persisted session events and truncated by token budget before being attached to the next provider request. Assistant history prefers the persisted final turn output and only falls back to streamed provider deltas when needed.
5759

5860
Cross-session memory is sourced from:
@@ -77,6 +79,17 @@ The runtime injects only compact recall text and index freshness metadata. Detai
7779

7880
Each spec-mode prompt creates a fresh folder. If the same slug already exists, the runtime appends `-2`, `-3`, and so on instead of overwriting an existing spec set.
7981

82+
## Plan workflow
83+
84+
**`IPlanWorkflowService`** handles the post-processing path for **`plan`** mode:
85+
86+
- parses the model response as structured JSON
87+
- persists the latest deep-plan summary and next action into session metadata
88+
- synchronizes planning-owned session todos through **`ITodoService`**
89+
- returns a structured **`PlanExecutionResult`** on the turn result contract
90+
91+
Planning-managed todos are isolated by owner id (`deep-planning`) so manual session todos remain untouched.
92+
8093
## Operational diagnostics
8194

8295
**`OperationalDiagnosticsCoordinator`** runs injectable **`IOperationalCheck`** implementations:
@@ -102,6 +115,7 @@ The parity layer adds several runtime-owned services:
102115
- **`IShareSessionService`** — creates and removes self-hosted share snapshots
103116
- **`IHookDispatcher`** — executes configured hook processes for turn/tool/share/server events and exposes hook inspection/testing
104117
- **`ITodoService`** — persists session and workspace todo items under session metadata and `.sharpclaw/tasks.json`
118+
- deep plan mode also uses `ITodoService.SyncManagedSessionTodosAsync(...)` to reconcile planning-owned session tasks
105119
- **`IWorkspaceInsightsService`** — reconstructs durable usage, cost, and execution stats from persisted event logs
106120

107121
These services are intentionally small and runtime-owned rather than separate orchestration subsystems.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
using SharpClaw.Code.Agents.Models;
2+
using SharpClaw.Code.Protocol.Models;
3+
using SharpClaw.Code.Tools.Models;
4+
5+
namespace SharpClaw.Code.Agents.Abstractions;
6+
7+
/// <summary>
8+
/// Executes bounded delegated subagent tasks on behalf of a parent agent tool call.
9+
/// </summary>
10+
public interface ISubAgentOrchestrator
11+
{
12+
/// <summary>
13+
/// Executes the supplied delegated tasks using the bounded subagent worker.
14+
/// </summary>
15+
/// <param name="request">The delegated task batch.</param>
16+
/// <param name="context">The parent tool execution context.</param>
17+
/// <param name="cancellationToken">The cancellation token.</param>
18+
/// <returns>The batch execution result and emitted runtime events.</returns>
19+
Task<SubAgentBatchExecutionResult> ExecuteAsync(
20+
SubAgentBatchRequest request,
21+
ToolExecutionContext context,
22+
CancellationToken cancellationToken);
23+
}

src/SharpClaw.Code.Agents/AgentsServiceCollectionExtensions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ public static class AgentsServiceCollectionExtensions
2020
public static IServiceCollection AddSharpClawAgents(this IServiceCollection services)
2121
{
2222
services.AddOptions<AgentLoopOptions>();
23+
services.AddSingleton<ISubAgentOrchestrator, SubAgentOrchestrator>();
2324
services.AddSingleton<ToolCallDispatcher>();
2425
services.AddSingleton<ProviderBackedAgentKernel>();
2526
services.AddSingleton<IAgentFrameworkBridge, AgentFrameworkBridge>();
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
using System.Text.Json;
2+
using Microsoft.Extensions.DependencyInjection;
3+
using Microsoft.Extensions.Logging;
4+
using SharpClaw.Code.Agents.Abstractions;
5+
using SharpClaw.Code.Agents.Agents;
6+
using SharpClaw.Code.Agents.Models;
7+
using SharpClaw.Code.Protocol.Enums;
8+
using SharpClaw.Code.Protocol.Events;
9+
using SharpClaw.Code.Protocol.Models;
10+
using SharpClaw.Code.Protocol.Serialization;
11+
using SharpClaw.Code.Tools.Abstractions;
12+
using SharpClaw.Code.Tools.Models;
13+
14+
namespace SharpClaw.Code.Agents.Internal;
15+
16+
/// <summary>
17+
/// Executes delegated subagent tasks as bounded read-only child runs.
18+
/// </summary>
19+
public sealed class SubAgentOrchestrator(
20+
IServiceProvider serviceProvider,
21+
IToolExecutor toolExecutor,
22+
ILogger<SubAgentOrchestrator> logger) : ISubAgentOrchestrator
23+
{
24+
/// <inheritdoc />
25+
public async Task<SubAgentBatchExecutionResult> ExecuteAsync(
26+
SubAgentBatchRequest request,
27+
ToolExecutionContext context,
28+
CancellationToken cancellationToken)
29+
{
30+
ArgumentNullException.ThrowIfNull(request);
31+
ArgumentNullException.ThrowIfNull(context);
32+
33+
if (request.Tasks is not { Length: > 0 })
34+
{
35+
throw new InvalidOperationException("The subagent request must include at least one task.");
36+
}
37+
38+
if (request.Tasks.Length > SubAgentToolContract.MaxTasks)
39+
{
40+
throw new InvalidOperationException($"The subagent request exceeds the limit of {SubAgentToolContract.MaxTasks} tasks.");
41+
}
42+
43+
var runs = request.Tasks
44+
.Select((task, index) => ExecuteSingleAsync(task, index, context, cancellationToken))
45+
.ToArray();
46+
var completedRuns = await Task.WhenAll(runs).ConfigureAwait(false);
47+
48+
var taskResults = completedRuns.Select(static run => run.TaskResult).ToArray();
49+
var events = completedRuns.SelectMany(static run => run.Events).ToArray();
50+
var result = new SubAgentBatchResult(
51+
Tasks: taskResults,
52+
CompletedCount: taskResults.Count(static task => task.Succeeded),
53+
FailedCount: taskResults.Count(static task => !task.Succeeded));
54+
55+
return new SubAgentBatchExecutionResult(result, events);
56+
}
57+
58+
private async Task<SingleTaskExecutionResult> ExecuteSingleAsync(
59+
SubAgentTaskRequest task,
60+
int index,
61+
ToolExecutionContext parentContext,
62+
CancellationToken cancellationToken)
63+
{
64+
ArgumentNullException.ThrowIfNull(task);
65+
var goal = task.Goal?.Trim();
66+
var expectedOutput = task.ExpectedOutput?.Trim();
67+
if (string.IsNullOrWhiteSpace(goal) || string.IsNullOrWhiteSpace(expectedOutput))
68+
{
69+
throw new InvalidOperationException("Each subagent task requires both goal and expectedOutput.");
70+
}
71+
72+
var taskId = $"subtask-{index + 1:D2}-{Guid.NewGuid():N}";
73+
var delegatedTask = new DelegatedTaskContract(
74+
taskId,
75+
goal,
76+
expectedOutput,
77+
NormalizeConstraints(task.Constraints));
78+
79+
try
80+
{
81+
var subAgentWorker = serviceProvider.GetRequiredService<SubAgentWorker>();
82+
var result = await subAgentWorker.RunAsync(
83+
new AgentRunContext(
84+
SessionId: parentContext.SessionId,
85+
TurnId: parentContext.TurnId,
86+
Prompt: goal,
87+
WorkingDirectory: parentContext.WorkingDirectory,
88+
Model: string.IsNullOrWhiteSpace(parentContext.Model) ? "default" : parentContext.Model!,
89+
PermissionMode: PermissionMode.ReadOnly,
90+
OutputFormat: OutputFormat.Text,
91+
ToolExecutor: toolExecutor,
92+
Metadata: BuildChildMetadata(parentContext),
93+
ParentAgentId: parentContext.AgentId,
94+
DelegatedTask: delegatedTask,
95+
PrimaryMode: PrimaryMode.Plan,
96+
ToolMutationRecorder: null,
97+
ConversationHistory: null,
98+
IsInteractive: false,
99+
ApprovalSettings: ApprovalSettings.Empty),
100+
cancellationToken).ConfigureAwait(false);
101+
102+
return new SingleTaskExecutionResult(
103+
new SubAgentTaskResult(
104+
TaskId: taskId,
105+
Goal: goal,
106+
ExpectedOutput: expectedOutput,
107+
Succeeded: true,
108+
Output: string.IsNullOrWhiteSpace(result.Output) ? "(no output)" : result.Output.Trim(),
109+
ErrorMessage: null,
110+
AgentId: result.AgentId),
111+
result.Events ?? []);
112+
}
113+
catch (OperationCanceledException)
114+
{
115+
throw;
116+
}
117+
catch (Exception exception)
118+
{
119+
logger.LogWarning(
120+
exception,
121+
"Delegated subagent task {TaskId} failed for session {SessionId}, turn {TurnId}.",
122+
taskId,
123+
parentContext.SessionId,
124+
parentContext.TurnId);
125+
126+
return new SingleTaskExecutionResult(
127+
new SubAgentTaskResult(
128+
TaskId: taskId,
129+
Goal: goal,
130+
ExpectedOutput: expectedOutput,
131+
Succeeded: false,
132+
Output: null,
133+
ErrorMessage: exception.Message,
134+
AgentId: SubAgentWorker.SubAgentId),
135+
[]);
136+
}
137+
}
138+
139+
private static string[] NormalizeConstraints(string[]? constraints)
140+
=> constraints?
141+
.Where(static value => !string.IsNullOrWhiteSpace(value))
142+
.Select(static value => value.Trim())
143+
.Distinct(StringComparer.Ordinal)
144+
.ToArray()
145+
?? [];
146+
147+
private static Dictionary<string, string> BuildChildMetadata(ToolExecutionContext parentContext)
148+
{
149+
var metadata = new Dictionary<string, string>(StringComparer.Ordinal);
150+
if (parentContext.Metadata is not null
151+
&& parentContext.Metadata.TryGetValue("provider", out var provider)
152+
&& !string.IsNullOrWhiteSpace(provider))
153+
{
154+
metadata["provider"] = provider;
155+
}
156+
157+
metadata[SharpClawWorkflowMetadataKeys.AgentAllowedToolsJson] = JsonSerializer.Serialize(
158+
SubAgentToolContract.AllowedReadOnlyTools,
159+
ProtocolJsonContext.Default.StringArray);
160+
return metadata;
161+
}
162+
163+
private sealed record SingleTaskExecutionResult(
164+
SubAgentTaskResult TaskResult,
165+
IReadOnlyList<RuntimeEvent> Events);
166+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
using SharpClaw.Code.Protocol.Models;
2+
using SharpClaw.Code.Tools.BuiltIn;
3+
4+
namespace SharpClaw.Code.Agents.Internal;
5+
6+
internal static class SubAgentToolContract
7+
{
8+
public const string ToolName = "use_subagents";
9+
public const int MaxTasks = 3;
10+
11+
public static readonly string[] AllowedReadOnlyTools =
12+
[
13+
ReadFileTool.ToolName,
14+
GlobSearchTool.ToolName,
15+
GrepSearchTool.ToolName,
16+
WorkspaceSearchTool.ToolName,
17+
SymbolSearchTool.ToolName,
18+
ToolSearchTool.ToolName,
19+
];
20+
21+
public static readonly ProviderToolDefinition Definition = new(
22+
ToolName,
23+
"Delegate up to 3 bounded read-only repository investigation tasks to subagents. Use this for parallel codebase research, not for edits or shell commands.",
24+
"""
25+
{
26+
"type": "object",
27+
"additionalProperties": false,
28+
"properties": {
29+
"tasks": {
30+
"type": "array",
31+
"minItems": 1,
32+
"maxItems": 3,
33+
"items": {
34+
"type": "object",
35+
"additionalProperties": false,
36+
"properties": {
37+
"goal": { "type": "string" },
38+
"expectedOutput": { "type": "string" },
39+
"constraints": {
40+
"type": "array",
41+
"items": { "type": "string" }
42+
}
43+
},
44+
"required": ["goal", "expectedOutput"]
45+
}
46+
}
47+
},
48+
"required": ["tasks"]
49+
}
50+
""");
51+
}

0 commit comments

Comments
 (0)