profclaw 2.3.0 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +5 -2
  3. package/config/pricing.yml +26 -2
  4. package/dist/agents/command-sandbox.d.ts +93 -0
  5. package/dist/agents/command-sandbox.js +235 -0
  6. package/dist/agents/executor-runner.d.ts +135 -0
  7. package/dist/agents/executor-runner.js +303 -0
  8. package/dist/agents/executor.d.ts +2 -0
  9. package/dist/agents/executor.js +35 -2
  10. package/dist/agents/output-truncator.d.ts +59 -0
  11. package/dist/agents/output-truncator.js +128 -0
  12. package/dist/agents/receipt/receipt-cli.d.ts +43 -0
  13. package/dist/agents/receipt/receipt-cli.js +82 -0
  14. package/dist/agents/receipt/receipt-types.d.ts +136 -0
  15. package/dist/agents/receipt/receipt-types.js +19 -0
  16. package/dist/agents/receipt/receipt-verify.d.ts +19 -0
  17. package/dist/agents/receipt/receipt-verify.js +176 -0
  18. package/dist/agents/receipt/receipt-view.d.ts +29 -0
  19. package/dist/agents/receipt/receipt-view.js +227 -0
  20. package/dist/agents/receipt/receipt-writer.d.ts +47 -0
  21. package/dist/agents/receipt/receipt-writer.js +186 -0
  22. package/dist/agents/result-store.d.ts +6 -0
  23. package/dist/agents/result-store.js +43 -2
  24. package/dist/agents/session-diff.d.ts +6 -0
  25. package/dist/agents/session-diff.js +10 -19
  26. package/dist/agents/tool-selector.d.ts +123 -0
  27. package/dist/agents/tool-selector.js +321 -0
  28. package/dist/agents/verified-run-git.d.ts +49 -0
  29. package/dist/agents/verified-run-git.js +82 -0
  30. package/dist/agents/verified-run.d.ts +149 -0
  31. package/dist/agents/verified-run.js +402 -0
  32. package/dist/agents/verifier.d.ts +64 -0
  33. package/dist/agents/verifier.js +186 -0
  34. package/dist/agents/worktree-tools.d.ts +47 -0
  35. package/dist/agents/worktree-tools.js +246 -0
  36. package/dist/chat/agentic-executor.js +14 -6
  37. package/dist/chat/execution/sandbox.d.ts +8 -0
  38. package/dist/chat/execution/sandbox.js +25 -1
  39. package/dist/chat/execution/tools/tts-speak.d.ts +2 -2
  40. package/dist/chat/meta-tool-handlers.d.ts +18 -0
  41. package/dist/chat/meta-tool-handlers.js +51 -0
  42. package/dist/chat/system-prompts.js +6 -1
  43. package/dist/chat/tool-handler.d.ts +2 -0
  44. package/dist/chat/tool-handler.js +47 -3
  45. package/dist/cli/commands/receipt.d.ts +7 -0
  46. package/dist/cli/commands/receipt.js +90 -0
  47. package/dist/cli/commands/run.d.ts +28 -0
  48. package/dist/cli/commands/run.js +204 -0
  49. package/dist/cli/index.js +4 -0
  50. package/dist/costs/token-tracker.d.ts +10 -1
  51. package/dist/costs/token-tracker.js +37 -5
  52. package/dist/providers/ai-sdk.js +33 -37
  53. package/dist/providers/core/models.js +59 -17
  54. package/dist/providers/core/types.d.ts +4 -0
  55. package/dist/providers/prompt-cache.d.ts +68 -0
  56. package/dist/providers/prompt-cache.js +165 -0
  57. package/dist/providers/smart-router.js +11 -3
  58. package/dist/routes/chat.js +22 -5
  59. package/dist/server.js +3 -0
  60. package/package.json +1 -1
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Tool Selector
3
+ *
4
+ * Sends only the tool schemas that are likely needed on a given turn instead
5
+ * of every registered tool. Selection is a cheap, deterministic heuristic (no
6
+ * LLM call): a small core set, plus groups whose keywords appear in the user
7
+ * message or whose tools were used recently. The model can request more groups
8
+ * with the `load_tools` meta-tool.
9
+ *
10
+ * Cache stability: within a session the set of loaded groups only grows, and
11
+ * the output is always sorted by tool name, so the tool prefix of the prompt
12
+ * stays byte-identical between turns unless a new group is genuinely needed.
13
+ *
14
+ * Env:
15
+ * PROFCLAW_TOOL_SELECTION off/0/false/no disables (default on)
16
+ * PROFCLAW_TOOL_SELECTION_MIN_TOOLS skip selection when the tool count is at
17
+ * or below this number (default 12)
18
+ * PROFCLAW_TOOL_SELECTION_KEEP_UNGROUPED
19
+ * keep tools that match no group, such as
20
+ * MCP or plugin tools (default true)
21
+ */
22
+ import { z } from 'zod';
23
+ export interface SelectableTool {
24
+ name: string;
25
+ description: string;
26
+ parameters: unknown;
27
+ }
28
+ export interface ToolGroup {
29
+ name: string;
30
+ description: string;
31
+ /** Exact tool names, or prefixes ending in `*` */
32
+ tools: string[];
33
+ /** Matched against the lowercased user message */
34
+ keywords: RegExp;
35
+ }
36
+ export declare const LOAD_TOOLS_NAME = "load_tools";
37
+ export declare const FETCH_RESULT_NAME = "fetch_result";
38
+ /** Tools that are always sent. Keep this small. */
39
+ export declare const CORE_TOOL_NAMES: readonly string[];
40
+ export declare const TOOL_GROUPS: readonly ToolGroup[];
41
+ export declare function isToolSelectionEnabled(): boolean;
42
+ /** Group name a tool belongs to, or undefined for core/ungrouped tools. */
43
+ export declare function groupOfTool(name: string): string | undefined;
44
+ export declare class ToolSelectorSession {
45
+ /** Groups loaded so far. Only ever grows, which keeps the prefix stable. */
46
+ private readonly loaded;
47
+ getLoadedGroups(): string[];
48
+ /** Mark groups loaded. Returns the names that were valid. */
49
+ loadGroups(names: string[]): string[];
50
+ /** Keep the group of a tool that was just executed loaded. */
51
+ noteToolUse(toolName: string): void;
52
+ /** Add groups whose keywords match the user message. */
53
+ noteMessage(message: string): string[];
54
+ /**
55
+ * Select tools for this turn. Output is sorted by name and, apart from
56
+ * newly loaded groups, identical across calls in the same session.
57
+ */
58
+ select<T extends SelectableTool>(all: readonly T[], message?: string): T[];
59
+ }
60
+ export declare function getToolSelectorSession(sessionId: string): ToolSelectorSession;
61
+ export declare function resetToolSelectorSessions(): void;
62
+ declare const LoadToolsSchema: z.ZodObject<{
63
+ groups: z.ZodArray<z.ZodString, "many">;
64
+ invoke: z.ZodOptional<z.ZodObject<{
65
+ tool: z.ZodString;
66
+ args: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
67
+ }, "strip", z.ZodTypeAny, {
68
+ tool: string;
69
+ args?: Record<string, unknown> | undefined;
70
+ }, {
71
+ tool: string;
72
+ args?: Record<string, unknown> | undefined;
73
+ }>>;
74
+ }, "strip", z.ZodTypeAny, {
75
+ groups: string[];
76
+ invoke?: {
77
+ tool: string;
78
+ args?: Record<string, unknown> | undefined;
79
+ } | undefined;
80
+ }, {
81
+ groups: string[];
82
+ invoke?: {
83
+ tool: string;
84
+ args?: Record<string, unknown> | undefined;
85
+ } | undefined;
86
+ }>;
87
+ declare const FetchResultSchema: z.ZodObject<{
88
+ result_id: z.ZodString;
89
+ offset: z.ZodOptional<z.ZodNumber>;
90
+ length: z.ZodOptional<z.ZodNumber>;
91
+ }, "strip", z.ZodTypeAny, {
92
+ result_id: string;
93
+ length?: number | undefined;
94
+ offset?: number | undefined;
95
+ }, {
96
+ result_id: string;
97
+ length?: number | undefined;
98
+ offset?: number | undefined;
99
+ }>;
100
+ export type LoadToolsArgs = z.infer<typeof LoadToolsSchema>;
101
+ export type FetchResultArgs = z.infer<typeof FetchResultSchema>;
102
+ export declare function parseLoadToolsArgs(args: unknown): LoadToolsArgs | null;
103
+ export declare function parseFetchResultArgs(args: unknown): FetchResultArgs | null;
104
+ /** Stable, compact meta-tool definitions. Description text must not vary. */
105
+ export declare function getMetaTools(includeFetchResult: boolean): SelectableTool[];
106
+ /** Rough tokens for one tool schema as sent to the provider (about 4 chars per token). */
107
+ export declare function estimateToolTokens(tool: SelectableTool): number;
108
+ export declare function estimateToolsTokens(tools: readonly SelectableTool[]): number;
109
+ /** Compact JSON schema for a tool, used in load_tools results. */
110
+ export declare function describeTool(tool: SelectableTool): {
111
+ name: string;
112
+ description: string;
113
+ parameters: unknown;
114
+ };
115
+ /** Tools belonging to a named group, sorted by name. */
116
+ export declare function toolsInGroup<T extends SelectableTool>(all: readonly T[], groupName: string): T[];
117
+ /**
118
+ * Select tools for one agentic request and append the meta-tools. When
119
+ * selection is disabled the input list is returned unchanged.
120
+ */
121
+ export declare function selectToolsForRequest<T extends SelectableTool>(tools: readonly T[], sessionId: string, message: string): Array<T | SelectableTool>;
122
+ export {};
123
+ //# sourceMappingURL=tool-selector.d.ts.map
@@ -0,0 +1,321 @@
1
+ /**
2
+ * Tool Selector
3
+ *
4
+ * Sends only the tool schemas that are likely needed on a given turn instead
5
+ * of every registered tool. Selection is a cheap, deterministic heuristic (no
6
+ * LLM call): a small core set, plus groups whose keywords appear in the user
7
+ * message or whose tools were used recently. The model can request more groups
8
+ * with the `load_tools` meta-tool.
9
+ *
10
+ * Cache stability: within a session the set of loaded groups only grows, and
11
+ * the output is always sorted by tool name, so the tool prefix of the prompt
12
+ * stays byte-identical between turns unless a new group is genuinely needed.
13
+ *
14
+ * Env:
15
+ * PROFCLAW_TOOL_SELECTION off/0/false/no disables (default on)
16
+ * PROFCLAW_TOOL_SELECTION_MIN_TOOLS skip selection when the tool count is at
17
+ * or below this number (default 12)
18
+ * PROFCLAW_TOOL_SELECTION_KEEP_UNGROUPED
19
+ * keep tools that match no group, such as
20
+ * MCP or plugin tools (default true)
21
+ */
22
+ import { z } from 'zod';
23
+ import { zodToJsonSchema } from 'zod-to-json-schema';
24
+ export const LOAD_TOOLS_NAME = 'load_tools';
25
+ export const FETCH_RESULT_NAME = 'fetch_result';
26
+ /** Tools that are always sent. Keep this small. */
27
+ export const CORE_TOOL_NAMES = [
28
+ 'read_file',
29
+ 'write_file',
30
+ 'edit_file',
31
+ 'search_files',
32
+ 'grep',
33
+ 'exec',
34
+ 'complete_task',
35
+ ];
36
+ // Groups
37
+ export const TOOL_GROUPS = [
38
+ {
39
+ name: 'git',
40
+ description: 'git status/diff/log/branch/stash/remote/commit, PRs',
41
+ tools: ['git_*', 'create_pr', 'github_pr'],
42
+ keywords: /\b(git|commit|branch|stash|rebase|merge|diff|pull request|pr|github|checkout|push)\b/,
43
+ },
44
+ {
45
+ name: 'dev',
46
+ description: 'run tests, lint, build, typecheck, format, REPL, project info',
47
+ tools: ['test_run', 'lint', 'build', 'format', 'typecheck', 'project_info', 'repl_execute'],
48
+ keywords: /\b(test|tests|lint|build|compile|typecheck|type check|tsc|format|prettier|eslint|repl|ci)\b/,
49
+ },
50
+ {
51
+ name: 'files_extra',
52
+ description: 'directory tree, multi-file patch, patch apply, image analysis',
53
+ tools: ['directory_tree', 'multi_patch', 'patch_apply', 'image_analyze'],
54
+ keywords: /\b(tree|directory|folder|patch|refactor|rename|image|screenshot|picture|photo)\b/,
55
+ },
56
+ {
57
+ name: 'web',
58
+ description: 'web search, fetch a URL, understand a link',
59
+ tools: ['web_fetch', 'web_search', 'link_understand'],
60
+ keywords: /(https?:\/\/|\b(web|website|url|link|search online|google|internet|news|latest|docs?|documentation|browse)\b)/,
61
+ },
62
+ {
63
+ name: 'browser',
64
+ description: 'browser automation: navigate, click, fill, screenshot',
65
+ tools: ['browser_*'],
66
+ keywords: /\b(browser|playwright|click|navigate|scrape|webpage|web page|page)\b/,
67
+ },
68
+ {
69
+ name: 'memory',
70
+ description: 'search and read long-term memory',
71
+ tools: ['memory_*'],
72
+ keywords: /\b(memory|memories|remember|recall|earlier|previous(ly)?|last time|we discussed)\b/,
73
+ },
74
+ {
75
+ name: 'tasks',
76
+ description: 'tickets, projects, todos, task decomposition',
77
+ tools: [
78
+ 'create_ticket', 'update_ticket', 'get_ticket', 'list_tickets',
79
+ 'create_project', 'list_projects', 'todo_read', 'todo_write', 'decompose_task',
80
+ ],
81
+ keywords: /\b(ticket|tickets|project|projects|todo|todos|task list|backlog|issue|decompose|plan)\b/,
82
+ },
83
+ {
84
+ name: 'sessions',
85
+ description: 'sessions, sub-agents, agent messaging',
86
+ tools: [
87
+ 'sessions_*', 'list_sessions', 'spawn_session', 'send_message', 'receive_messages',
88
+ 'agents_list', 'session_status', 'subagent_orchestrate',
89
+ ],
90
+ keywords: /\b(session|sessions|agent|agents|sub-?agent|spawn|delegate|orchestrate|parallel)\b/,
91
+ },
92
+ {
93
+ name: 'cron',
94
+ description: 'scheduled jobs: create, list, pause, trigger, delete',
95
+ tools: ['cron_*'],
96
+ keywords: /\b(cron|schedule|scheduled|recurring|every (day|hour|week|minute)|daily|hourly|timer)\b/,
97
+ },
98
+ {
99
+ name: 'feeds',
100
+ description: 'RSS/feed subscriptions and digests',
101
+ tools: ['feed_*'],
102
+ keywords: /\b(feed|feeds|rss|atom|digest|subscribe|newsletter)\b/,
103
+ },
104
+ {
105
+ name: 'messaging',
106
+ description: 'Slack, Discord, Telegram, notifications, clipboard',
107
+ tools: ['slack_actions', 'discord_actions', 'telegram_actions', 'notify', 'clipboard_*'],
108
+ keywords: /\b(slack|discord|telegram|notify|notification|clipboard|copy to|paste)\b/,
109
+ },
110
+ {
111
+ name: 'media',
112
+ description: 'image generation, text to speech, canvas',
113
+ tools: ['openai_image_gen', 'tts_speak', 'canvas_*'],
114
+ keywords: /\b(generate an? image|image generation|draw|illustrat\w*|tts|speak|speech|voice|audio|canvas|render)\b/,
115
+ },
116
+ {
117
+ name: 'system',
118
+ description: 'system info, env vars, processes, which, path info, db maintenance',
119
+ tools: ['system_info', 'env', 'env_vars', 'process_list', 'path_info', 'which', 'db_maintenance'],
120
+ keywords: /\b(system|env|environment|process|processes|cpu|memory usage|disk|port|installed|which|path|maintenance|vacuum)\b/,
121
+ },
122
+ ];
123
+ // Config
124
+ function isOff(value) {
125
+ return ['off', '0', 'false', 'no'].includes((value ?? '').toLowerCase());
126
+ }
127
+ export function isToolSelectionEnabled() {
128
+ return !isOff(process.env.PROFCLAW_TOOL_SELECTION);
129
+ }
130
+ function minToolsThreshold() {
131
+ const parsed = Number.parseInt(process.env.PROFCLAW_TOOL_SELECTION_MIN_TOOLS ?? '', 10);
132
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : 12;
133
+ }
134
+ function keepUngrouped() {
135
+ return !isOff(process.env.PROFCLAW_TOOL_SELECTION_KEEP_UNGROUPED);
136
+ }
137
+ // Matching helpers
138
+ function matchesPattern(name, pattern) {
139
+ return pattern.endsWith('*') ? name.startsWith(pattern.slice(0, -1)) : name === pattern;
140
+ }
141
+ /** Group name a tool belongs to, or undefined for core/ungrouped tools. */
142
+ export function groupOfTool(name) {
143
+ for (const group of TOOL_GROUPS) {
144
+ if (group.tools.some((p) => matchesPattern(name, p)))
145
+ return group.name;
146
+ }
147
+ return undefined;
148
+ }
149
+ function isCore(name) {
150
+ return CORE_TOOL_NAMES.includes(name) || name === LOAD_TOOLS_NAME || name === FETCH_RESULT_NAME;
151
+ }
152
+ // Session state
153
+ export class ToolSelectorSession {
154
+ /** Groups loaded so far. Only ever grows, which keeps the prefix stable. */
155
+ loaded = new Set();
156
+ getLoadedGroups() {
157
+ return Array.from(this.loaded).sort();
158
+ }
159
+ /** Mark groups loaded. Returns the names that were valid. */
160
+ loadGroups(names) {
161
+ const valid = [];
162
+ for (const raw of names) {
163
+ const name = raw.trim().toLowerCase();
164
+ if (TOOL_GROUPS.some((g) => g.name === name)) {
165
+ this.loaded.add(name);
166
+ valid.push(name);
167
+ }
168
+ }
169
+ return valid;
170
+ }
171
+ /** Keep the group of a tool that was just executed loaded. */
172
+ noteToolUse(toolName) {
173
+ const group = groupOfTool(toolName);
174
+ if (group)
175
+ this.loaded.add(group);
176
+ }
177
+ /** Add groups whose keywords match the user message. */
178
+ noteMessage(message) {
179
+ const text = message.toLowerCase();
180
+ const hit = [];
181
+ for (const group of TOOL_GROUPS) {
182
+ if (group.keywords.test(text)) {
183
+ this.loaded.add(group.name);
184
+ hit.push(group.name);
185
+ }
186
+ }
187
+ return hit;
188
+ }
189
+ /**
190
+ * Select tools for this turn. Output is sorted by name and, apart from
191
+ * newly loaded groups, identical across calls in the same session.
192
+ */
193
+ select(all, message = '') {
194
+ if (!isToolSelectionEnabled() || all.length <= minToolsThreshold()) {
195
+ return [...all].sort(byName);
196
+ }
197
+ if (message)
198
+ this.noteMessage(message);
199
+ const keepOther = keepUngrouped();
200
+ const picked = all.filter((tool) => {
201
+ if (isCore(tool.name))
202
+ return true;
203
+ const group = groupOfTool(tool.name);
204
+ if (group === undefined)
205
+ return keepOther;
206
+ return this.loaded.has(group);
207
+ });
208
+ return picked.sort(byName);
209
+ }
210
+ }
211
+ function byName(a, b) {
212
+ return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;
213
+ }
214
+ // Session registry (keyed by conversation)
215
+ const MAX_SESSIONS = 500;
216
+ const sessions = new Map();
217
+ export function getToolSelectorSession(sessionId) {
218
+ let session = sessions.get(sessionId);
219
+ if (!session) {
220
+ if (sessions.size >= MAX_SESSIONS) {
221
+ const oldest = sessions.keys().next().value;
222
+ if (oldest !== undefined)
223
+ sessions.delete(oldest);
224
+ }
225
+ session = new ToolSelectorSession();
226
+ sessions.set(sessionId, session);
227
+ }
228
+ return session;
229
+ }
230
+ export function resetToolSelectorSessions() {
231
+ sessions.clear();
232
+ }
233
+ // Meta tools
234
+ const LoadToolsSchema = z.object({
235
+ groups: z.array(z.string()).describe('Group names to load'),
236
+ invoke: z
237
+ .object({ tool: z.string(), args: z.record(z.string(), z.unknown()).optional() })
238
+ .optional()
239
+ .describe('Optionally call one of the loaded tools right now'),
240
+ });
241
+ const FetchResultSchema = z.object({
242
+ result_id: z.string(),
243
+ offset: z.number().int().min(0).optional(),
244
+ length: z.number().int().min(1).optional(),
245
+ });
246
+ export function parseLoadToolsArgs(args) {
247
+ const parsed = LoadToolsSchema.safeParse(args);
248
+ return parsed.success ? parsed.data : null;
249
+ }
250
+ export function parseFetchResultArgs(args) {
251
+ const parsed = FetchResultSchema.safeParse(args);
252
+ return parsed.success ? parsed.data : null;
253
+ }
254
+ /** Stable, compact meta-tool definitions. Description text must not vary. */
255
+ export function getMetaTools(includeFetchResult) {
256
+ const groupList = TOOL_GROUPS.map((g) => `${g.name} (${g.description})`).join('; ');
257
+ const tools = [
258
+ {
259
+ name: LOAD_TOOLS_NAME,
260
+ description: `Load more tool groups when you need a tool you do not have. Groups: ${groupList}. ` +
261
+ 'Returns the tool schemas; they are available from the next turn, or use invoke to call one now.',
262
+ parameters: LoadToolsSchema,
263
+ },
264
+ ];
265
+ if (includeFetchResult) {
266
+ tools.push({
267
+ name: FETCH_RESULT_NAME,
268
+ description: 'Read a char range of a truncated tool result by its result_id.',
269
+ parameters: FetchResultSchema,
270
+ });
271
+ }
272
+ return tools;
273
+ }
274
+ // Token estimate
275
+ /** Rough tokens for one tool schema as sent to the provider (about 4 chars per token). */
276
+ export function estimateToolTokens(tool) {
277
+ let schema = '{}';
278
+ try {
279
+ schema = JSON.stringify(zodToJsonSchema(tool.parameters, { $refStrategy: 'none' }));
280
+ }
281
+ catch {
282
+ // fall through with an empty schema
283
+ }
284
+ return Math.ceil((tool.name.length + tool.description.length + schema.length) / 4);
285
+ }
286
+ export function estimateToolsTokens(tools) {
287
+ return tools.reduce((sum, t) => sum + estimateToolTokens(t), 0);
288
+ }
289
+ /** Compact JSON schema for a tool, used in load_tools results. */
290
+ export function describeTool(tool) {
291
+ let parameters = {};
292
+ try {
293
+ parameters = zodToJsonSchema(tool.parameters, { $refStrategy: 'none' });
294
+ }
295
+ catch {
296
+ // keep empty schema
297
+ }
298
+ return { name: tool.name, description: tool.description, parameters };
299
+ }
300
+ /** Tools belonging to a named group, sorted by name. */
301
+ export function toolsInGroup(all, groupName) {
302
+ const group = TOOL_GROUPS.find((g) => g.name === groupName);
303
+ if (!group)
304
+ return [];
305
+ return all.filter((t) => group.tools.some((p) => matchesPattern(t.name, p))).sort(byName);
306
+ }
307
+ // Request-level entry point
308
+ /**
309
+ * Select tools for one agentic request and append the meta-tools. When
310
+ * selection is disabled the input list is returned unchanged.
311
+ */
312
+ export function selectToolsForRequest(tools, sessionId, message) {
313
+ if (!isToolSelectionEnabled() || tools.length <= minToolsThreshold()) {
314
+ return [...tools];
315
+ }
316
+ const session = getToolSelectorSession(sessionId);
317
+ const selected = session.select(tools, message);
318
+ const withMeta = [...selected, ...getMetaTools(true)];
319
+ return withMeta.sort(byName);
320
+ }
321
+ //# sourceMappingURL=tool-selector.js.map
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Git-backed workspace for verified runs.
3
+ *
4
+ * Isolation comes from WorktreeManager. Checkpoints are commits on the run
5
+ * branch: restoring is `git reset --hard <sha>` plus `git clean -fd`, so a
6
+ * failed attempt can be discarded without touching the user's main tree.
7
+ * All git calls use execFile (no shell), and nothing here pushes.
8
+ */
9
+ import type { FileChangeStatus } from './session-diff.js';
10
+ export interface WorkspaceChange {
11
+ /** Absolute path in the workspace */
12
+ path: string;
13
+ /** Path relative to the workspace root */
14
+ relPath: string;
15
+ status: FileChangeStatus;
16
+ /** Content at the base commit, null for created files */
17
+ original: string | null;
18
+ }
19
+ /** Everything the run loop needs from an isolated working copy. */
20
+ export interface RunWorkspace {
21
+ readonly path: string;
22
+ readonly branch: string;
23
+ /** Create the isolated copy and return a ref for its pristine state. */
24
+ prepare(): Promise<string>;
25
+ /** Persist the current state and return a ref that restore() accepts. */
26
+ checkpoint(label: string): Promise<string>;
27
+ /** Discard everything since the given ref. */
28
+ restore(ref: string): Promise<void>;
29
+ /** Files differing from the pristine state. */
30
+ changedFiles(): Promise<WorkspaceChange[]>;
31
+ /** Remove the isolated copy (the branch is kept). */
32
+ dispose(): Promise<void>;
33
+ }
34
+ export declare class GitRunWorkspace implements RunWorkspace {
35
+ private readonly projectRoot;
36
+ private readonly runId;
37
+ path: string;
38
+ branch: string;
39
+ private baseRef;
40
+ private readonly manager;
41
+ constructor(projectRoot: string, runId: string, branchName?: string);
42
+ private git;
43
+ prepare(): Promise<string>;
44
+ checkpoint(label: string): Promise<string>;
45
+ restore(ref: string): Promise<void>;
46
+ changedFiles(): Promise<WorkspaceChange[]>;
47
+ dispose(): Promise<void>;
48
+ }
49
+ //# sourceMappingURL=verified-run-git.d.ts.map
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Git-backed workspace for verified runs.
3
+ *
4
+ * Isolation comes from WorktreeManager. Checkpoints are commits on the run
5
+ * branch: restoring is `git reset --hard <sha>` plus `git clean -fd`, so a
6
+ * failed attempt can be discarded without touching the user's main tree.
7
+ * All git calls use execFile (no shell), and nothing here pushes.
8
+ */
9
+ import { execFile as execFileCb } from 'node:child_process';
10
+ import { promisify } from 'node:util';
11
+ import { join } from 'node:path';
12
+ import { WorktreeManager } from './worktree-manager.js';
13
+ const execFile = promisify(execFileCb);
14
+ const IDENTITY = ['-c', 'user.name=profClaw', '-c', 'user.email=bot@profclaw.ai', '-c', 'commit.gpgsign=false'];
15
+ export class GitRunWorkspace {
16
+ projectRoot;
17
+ runId;
18
+ path = '';
19
+ branch;
20
+ baseRef = '';
21
+ manager;
22
+ constructor(projectRoot, runId, branchName) {
23
+ this.projectRoot = projectRoot;
24
+ this.runId = runId;
25
+ this.manager = new WorktreeManager(projectRoot);
26
+ this.branch = branchName ?? `profclaw/run-${runId}`;
27
+ }
28
+ async git(args, cwd) {
29
+ const { stdout } = await execFile('git', args, {
30
+ cwd: cwd ?? (this.path || this.projectRoot),
31
+ maxBuffer: 16 * 1024 * 1024,
32
+ });
33
+ return stdout;
34
+ }
35
+ async prepare() {
36
+ const info = await this.manager.create({
37
+ taskId: this.runId,
38
+ branchName: this.branch,
39
+ });
40
+ this.path = info.path;
41
+ this.branch = info.branch;
42
+ this.baseRef = (await this.git(['rev-parse', 'HEAD'])).trim();
43
+ return this.baseRef;
44
+ }
45
+ async checkpoint(label) {
46
+ await this.git(['add', '-A']);
47
+ await this.git([...IDENTITY, 'commit', '--allow-empty', '--no-verify', '-m', `profclaw run: ${label}`]);
48
+ return (await this.git(['rev-parse', 'HEAD'])).trim();
49
+ }
50
+ async restore(ref) {
51
+ await this.git(['reset', '--hard', ref]);
52
+ await this.git(['clean', '-fd']);
53
+ }
54
+ async changedFiles() {
55
+ // Include uncommitted work by staging it into the index first (no commit).
56
+ await this.git(['add', '-A']);
57
+ const raw = await this.git(['diff', '--cached', '--name-status', '--no-renames', this.baseRef]);
58
+ const changes = [];
59
+ for (const line of raw.split('\n')) {
60
+ if (!line.trim())
61
+ continue;
62
+ const [code, ...rest] = line.split('\t');
63
+ const relPath = rest.join('\t');
64
+ const status = code === 'A' ? 'created' : code === 'D' ? 'deleted' : 'modified';
65
+ let original = null;
66
+ if (status !== 'created') {
67
+ try {
68
+ original = await this.git(['show', `${this.baseRef}:${relPath}`]);
69
+ }
70
+ catch {
71
+ original = null;
72
+ }
73
+ }
74
+ changes.push({ path: join(this.path, relPath), relPath, status, original });
75
+ }
76
+ return changes;
77
+ }
78
+ async dispose() {
79
+ await this.manager.remove(this.runId);
80
+ }
81
+ }
82
+ //# sourceMappingURL=verified-run-git.js.map