one-code-extension 0.1.4 → 0.3.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.
- package/LICENSE +20 -0
- package/README.md +200 -172
- package/agents/explore.md +9 -6
- package/agents/general-purpose.md +1 -1
- package/agents/plan.md +6 -4
- package/docs/guide/README.md +52 -0
- package/docs/guide/bring-your-claude-code-setup.md +126 -0
- package/docs/guide/configuration.md +171 -0
- package/docs/guide/differences-from-claude-code.md +150 -0
- package/docs/guide/doctor.md +86 -0
- package/docs/guide/hooks.md +109 -0
- package/docs/guide/installation.md +124 -0
- package/docs/guide/permissions-modes-and-auto-mode.md +450 -0
- package/docs/guide/providers-and-models.md +194 -0
- package/docs/guide/reference.md +224 -0
- package/docs/guide/sessions-and-context.md +190 -0
- package/docs/guide/skills-plugins-and-mcp.md +176 -0
- package/docs/guide/subagents-and-workflows.md +193 -0
- package/docs/guide/tasks-and-background-work.md +111 -0
- package/docs/guide/terminal-interface.md +155 -0
- package/docs/guide/tools.md +175 -0
- package/docs/guide/troubleshooting.md +175 -0
- package/extensions/ask-user/index.ts +65 -71
- package/extensions/ask-user/questions.ts +15 -28
- package/extensions/ask-user/widget.ts +533 -0
- package/extensions/auto-mode/actions.ts +9 -26
- package/extensions/auto-mode/classifier-prompt.ts +127 -0
- package/extensions/auto-mode/classifier.ts +305 -227
- package/extensions/auto-mode/config.ts +279 -77
- package/extensions/auto-mode/defaults.ts +17 -60
- package/extensions/auto-mode/instructions.ts +2 -1
- package/extensions/auto-mode/model-picker.ts +53 -31
- package/extensions/auto-mode/model-select.ts +153 -82
- package/extensions/auto-mode/paths.ts +13 -1
- package/extensions/auto-mode/pause.ts +8 -0
- package/extensions/auto-mode/prompt.ts +205 -238
- package/extensions/auto-mode/recoverability.ts +229 -0
- package/extensions/auto-mode/rules.ts +92 -75
- package/extensions/auto-mode/safety-floor.ts +43 -12
- package/extensions/auto-mode/sensitive.ts +11 -0
- package/extensions/auto-mode/setup-run.ts +245 -0
- package/extensions/auto-mode/setup.ts +235 -0
- package/extensions/auto-mode/shell-analysis.ts +332 -26
- package/extensions/auto-mode/transcript.ts +102 -0
- package/extensions/background/index.ts +256 -43
- package/extensions/background/monitor-batch.ts +58 -0
- package/extensions/background/registry.ts +9 -0
- package/extensions/background/wakeup.ts +45 -0
- package/extensions/bash/background.ts +47 -22
- package/extensions/bash/guards.ts +229 -0
- package/extensions/bash/index.ts +81 -19
- package/extensions/branding/index.ts +44 -18
- package/extensions/branding/startup.ts +23 -8
- package/extensions/claude-compat/index.ts +17 -2
- package/extensions/claude-context/index.ts +41 -12
- package/extensions/clear/index.ts +27 -0
- package/extensions/compaction/fit.ts +121 -0
- package/extensions/compaction/index.ts +176 -71
- package/extensions/compaction/prompt.ts +21 -1
- package/extensions/context-budget/budget.ts +61 -0
- package/extensions/context-budget/index.ts +66 -0
- package/extensions/context-management/index.ts +40 -16
- package/extensions/doctor/build.ts +226 -0
- package/extensions/doctor/cli.ts +99 -0
- package/extensions/doctor/compat.ts +532 -0
- package/extensions/doctor/dependencies.ts +186 -0
- package/extensions/doctor/fix-prompt.ts +125 -0
- package/extensions/doctor/index.ts +308 -0
- package/extensions/doctor/models.ts +231 -0
- package/extensions/doctor/presets.ts +208 -0
- package/extensions/doctor/report.ts +189 -0
- package/extensions/doctor/update-lookup.ts +46 -0
- package/extensions/doctor/viewer.ts +124 -0
- package/extensions/effort/index.ts +41 -4
- package/extensions/effort/slider.ts +33 -2
- package/extensions/file-tracker/index.ts +143 -20
- package/extensions/file-tracker/replay.ts +46 -0
- package/extensions/file-tracker/tracker.ts +51 -4
- package/extensions/footer/footer-line.ts +207 -0
- package/extensions/footer/index.ts +152 -0
- package/extensions/footer/pr.ts +53 -0
- package/extensions/hooks/executor.ts +57 -20
- package/extensions/hooks/formatter-notice.ts +76 -0
- package/extensions/hooks/index.ts +314 -73
- package/extensions/hooks/matcher.ts +35 -1
- package/extensions/hooks/plugin-hooks.ts +3 -3
- package/extensions/hooks/protocol.ts +58 -0
- package/extensions/hooks/subagent-bridge.ts +77 -0
- package/extensions/hooks/trust.ts +15 -4
- package/extensions/init/index.ts +26 -0
- package/extensions/init/prompt.ts +40 -0
- package/extensions/interrupted/index.ts +35 -0
- package/extensions/interrupted/line.ts +9 -0
- package/extensions/lib/abort.ts +15 -0
- package/extensions/lib/agent-loader.ts +183 -0
- package/extensions/lib/anthropic-payload.ts +15 -0
- package/extensions/lib/atomic-write.ts +56 -0
- package/extensions/lib/bridge-watch.ts +20 -0
- package/extensions/lib/cache-retention.ts +60 -0
- package/extensions/lib/capability-index.ts +466 -0
- package/extensions/lib/claude-context.ts +318 -27
- package/extensions/lib/claude-settings.ts +64 -0
- package/extensions/lib/consent-preview.ts +30 -0
- package/extensions/lib/deferred.ts +144 -13
- package/extensions/lib/favorites.ts +43 -0
- package/extensions/lib/fetch-timeout.ts +15 -0
- package/extensions/lib/frontmatter.ts +31 -0
- package/extensions/lib/git.ts +44 -2
- package/extensions/lib/hook-gate.ts +90 -0
- package/extensions/lib/interrupt.ts +94 -0
- package/extensions/lib/mcp-overrides.ts +61 -0
- package/extensions/lib/mcp-share.ts +25 -0
- package/extensions/lib/mcp-status.ts +34 -0
- package/extensions/lib/memory.ts +41 -6
- package/extensions/lib/model-facts.generated.json +2480 -0
- package/extensions/lib/model-facts.ts +162 -0
- package/extensions/lib/model-policy.ts +145 -189
- package/extensions/lib/model-tier.ts +480 -64
- package/extensions/lib/model-unusable.ts +37 -0
- package/extensions/lib/notifications.ts +320 -1
- package/extensions/lib/one-code-settings.ts +63 -0
- package/extensions/lib/original-command.ts +71 -0
- package/extensions/lib/package-version.ts +31 -0
- package/extensions/lib/paths.ts +39 -4
- package/extensions/lib/permission-gate.ts +192 -0
- package/extensions/lib/persisted-output.ts +18 -0
- package/extensions/lib/pi-ai-estimate.ts +160 -0
- package/extensions/lib/pi-version.ts +1 -1
- package/extensions/lib/plan-mode-channels.ts +11 -0
- package/extensions/lib/plugin-overrides.ts +24 -0
- package/extensions/lib/plugin-root.ts +44 -0
- package/extensions/lib/plugins.ts +233 -54
- package/extensions/lib/prefix-warm-gate.ts +157 -0
- package/extensions/lib/process-tree.ts +56 -0
- package/extensions/lib/reminders.ts +321 -68
- package/extensions/lib/scratchpad.ts +11 -8
- package/extensions/lib/session-lifecycle.ts +28 -0
- package/extensions/lib/settings-channels.ts +14 -0
- package/extensions/lib/shell-tasks.ts +54 -0
- package/extensions/lib/skill-overrides.ts +86 -0
- package/extensions/lib/skill-scan.ts +110 -0
- package/extensions/lib/text-width.ts +267 -0
- package/extensions/lib/tui-render.ts +281 -54
- package/extensions/lib/usage-bus.ts +75 -0
- package/extensions/lib/usage-tracker.ts +67 -0
- package/extensions/lib/worktree-isolation.ts +170 -0
- package/extensions/lsp/client.ts +69 -12
- package/extensions/lsp/format.ts +12 -2
- package/extensions/lsp/index.ts +273 -84
- package/extensions/lsp/install-hints.ts +43 -0
- package/extensions/lsp/keep-alive.ts +19 -0
- package/extensions/lsp/plugin-servers.ts +331 -0
- package/extensions/lsp/watcher.ts +157 -0
- package/extensions/mcp/announce.ts +99 -0
- package/extensions/mcp/client.ts +181 -22
- package/extensions/mcp/config.ts +55 -24
- package/extensions/mcp/index.ts +586 -86
- package/extensions/mcp/oauth/browser.ts +26 -0
- package/extensions/mcp/oauth/callback.ts +104 -0
- package/extensions/mcp/oauth/flow.ts +68 -0
- package/extensions/mcp/oauth/provider.ts +147 -0
- package/extensions/mcp/oauth/store.ts +86 -0
- package/extensions/mcp/panel/keys.ts +34 -0
- package/extensions/mcp/panel/model.ts +62 -0
- package/extensions/mcp/panel/render.ts +141 -0
- package/extensions/mcp/panel/state.ts +108 -0
- package/extensions/mcp/schema.ts +83 -6
- package/extensions/mcp/trust.ts +340 -0
- package/extensions/memory/entries.ts +163 -0
- package/extensions/memory/index.ts +119 -2
- package/extensions/memory/open-external.ts +97 -0
- package/extensions/memory/panel.ts +124 -0
- package/extensions/model-default/index.ts +74 -0
- package/extensions/notebook/index.ts +36 -18
- package/extensions/notebook/notebook.ts +110 -17
- package/extensions/permissions/index.ts +1012 -247
- package/extensions/permissions/matcher.ts +488 -57
- package/extensions/permissions/modes.ts +55 -7
- package/extensions/permissions/project-trust.ts +81 -0
- package/extensions/permissions/protected-paths.ts +16 -3
- package/extensions/permissions/rule-prose.ts +79 -0
- package/extensions/permissions/session-grant.ts +91 -0
- package/extensions/permissions/settings.ts +112 -33
- package/extensions/permissions/subagent-gate.ts +57 -0
- package/extensions/plan-mode/index.ts +143 -33
- package/extensions/plan-mode/reminder.ts +16 -19
- package/extensions/plan-mode/viewer.ts +40 -31
- package/extensions/plugins/counts.ts +68 -0
- package/extensions/plugins/index.ts +496 -32
- package/extensions/plugins/install/install.ts +167 -0
- package/extensions/plugins/install/paths.ts +23 -0
- package/extensions/plugins/install/registry.ts +72 -0
- package/extensions/plugins/marketplace/git.ts +103 -0
- package/extensions/plugins/marketplace/official.ts +36 -0
- package/extensions/plugins/marketplace/parse.ts +94 -0
- package/extensions/plugins/marketplace/registry.ts +60 -0
- package/extensions/plugins/marketplace/sync.ts +146 -0
- package/extensions/plugins/marketplace/types.ts +128 -0
- package/extensions/plugins/panel/keys.ts +52 -0
- package/extensions/plugins/panel/render.ts +315 -0
- package/extensions/plugins/panel/rows.ts +230 -0
- package/extensions/plugins/panel/state.ts +333 -0
- package/extensions/recap/index.ts +215 -0
- package/extensions/recap/prompt.ts +44 -0
- package/extensions/recap/scheduler.ts +87 -0
- package/extensions/search-tools/index.ts +0 -0
- package/extensions/search-tools/tools.ts +7 -5
- package/extensions/skill/index.ts +360 -34
- package/extensions/skill/invoke.ts +185 -0
- package/extensions/skill/listing.ts +21 -0
- package/extensions/skill/panel/keys.ts +41 -0
- package/extensions/skill/panel/render.ts +91 -0
- package/extensions/skill/panel/state.ts +115 -0
- package/extensions/spinner/index.ts +110 -0
- package/extensions/spinner/line.ts +51 -0
- package/extensions/spinner/verbs.ts +201 -0
- package/extensions/subagents/agents.ts +88 -44
- package/extensions/subagents/default-model.ts +62 -32
- package/extensions/subagents/delegation-steer.ts +23 -0
- package/extensions/subagents/hand-back-review.ts +67 -0
- package/extensions/subagents/index.ts +1545 -346
- package/extensions/subagents/live-runs.ts +293 -0
- package/extensions/subagents/model-select.ts +252 -82
- package/extensions/subagents/outcome.ts +90 -0
- package/extensions/subagents/panel-keys.ts +41 -0
- package/extensions/subagents/panel-render.ts +357 -0
- package/extensions/subagents/panel-widget.ts +330 -0
- package/extensions/subagents/pending-claim.ts +45 -0
- package/extensions/subagents/prose.ts +105 -0
- package/extensions/subagents/runner.ts +674 -0
- package/extensions/subagents/runs.ts +68 -1
- package/extensions/subagents/send-to-main-tool.ts +55 -0
- package/extensions/subagents/session-turns.ts +138 -0
- package/extensions/subagents/shell-panel.ts +198 -0
- package/extensions/subagents/usage.ts +16 -4
- package/extensions/subagents/worktree.ts +13 -0
- package/extensions/system-prompt/environment.ts +5 -3
- package/extensions/system-prompt/git-status.ts +93 -0
- package/extensions/system-prompt/index.ts +23 -1
- package/extensions/system-prompt/template.ts +34 -4
- package/extensions/system-prompt/tiers/common.ts +31 -6
- package/extensions/system-prompt/tiers/low.ts +13 -3
- package/extensions/system-prompt/tiers/mid.ts +21 -8
- package/extensions/system-reminder/index.ts +53 -9
- package/extensions/tasks/index.ts +65 -16
- package/extensions/tasks/store.ts +76 -3
- package/extensions/tool-search/announce.ts +42 -0
- package/extensions/tool-search/index.ts +136 -13
- package/extensions/turn-duration/index.ts +60 -0
- package/extensions/turn-duration/line.ts +22 -0
- package/extensions/turn-duration/span.ts +41 -0
- package/extensions/turn-duration/verbs.ts +24 -0
- package/extensions/web/backends.ts +421 -0
- package/extensions/web/index.ts +178 -21
- package/extensions/web-fetch/index.ts +74 -18
- package/extensions/web-fetch/summarize.ts +1 -1
- package/extensions/workflow/agent-session.ts +182 -112
- package/extensions/workflow/globals.ts +129 -106
- package/extensions/workflow/index.ts +343 -28
- package/extensions/workflow/records.ts +118 -0
- package/extensions/workflow/run-manager.ts +80 -31
- package/extensions/workflow/saved-workflows.ts +2 -1
- package/extensions/workflow/script-worker.mjs +110 -0
- package/extensions/workflow/types.ts +74 -8
- package/extensions/workflow/viewer.ts +562 -0
- package/extensions/workflow/vm-runtime.ts +118 -22
- package/extensions/workflow/widget.ts +126 -18
- package/extensions/worktree/guards.ts +240 -0
- package/extensions/worktree/index.ts +30 -5
- package/extensions/worktree/rewrite.ts +32 -16
- package/package.json +20 -10
- package/skills/code-review/SKILL.md +165 -0
- package/skills/fewer-permission-prompts/SKILL.md +61 -0
- package/skills/security-review/SKILL.md +178 -0
- package/skills/simplify/SKILL.md +70 -0
- package/themes/{one-code-light.json → onecode-light.json} +6 -1
- package/themes/{one-code.json → onecode.json} +6 -1
- package/types/app.d.ts +8 -1
- package/types/community-packages.d.ts +45 -2
- package/docs/decisions/auto-mode.md +0 -526
- package/docs/decisions/branding.md +0 -127
- package/docs/decisions/code-review-remediation.md +0 -106
- package/docs/decisions/compaction.md +0 -140
- package/docs/decisions/distribution.md +0 -168
- package/docs/decisions/hooks.md +0 -64
- package/docs/decisions/lsp.md +0 -58
- package/docs/decisions/mcp.md +0 -104
- package/docs/decisions/memory-state.md +0 -170
- package/docs/decisions/model-policy.md +0 -126
- package/docs/decisions/modes.md +0 -178
- package/docs/decisions/skills-plugins.md +0 -56
- package/docs/decisions/subagents-workflows.md +0 -225
- package/docs/decisions/system-prompt.md +0 -98
- package/docs/decisions/tools.md +0 -407
- package/docs/decisions/tui.md +0 -217
- package/docs/decisions.md +0 -148
- package/extensions/subagents/child.ts +0 -414
- package/extensions/subagents/rpc-turns.ts +0 -123
- package/extensions/todo/index.ts +0 -133
- package/extensions/workflow/permission-gate.ts +0 -71
|
@@ -19,8 +19,23 @@ export interface AgentRunRecord {
|
|
|
19
19
|
/** Resolved session file, once known. */
|
|
20
20
|
sessionFile?: string;
|
|
21
21
|
cwd: string;
|
|
22
|
+
/** True when `cwd` is an isolation worktree, so a resume re-applies the git-isolation guard. */
|
|
23
|
+
worktree?: boolean;
|
|
22
24
|
model?: string;
|
|
23
25
|
thinking?: string;
|
|
26
|
+
/**
|
|
27
|
+
* Nesting level: 0 = spawned by main, 1 = spawned by a child's own Agent
|
|
28
|
+
* tool. Set once at creation and read by every spawn-tool handout (the
|
|
29
|
+
* MAX_SPAWN_DEPTH gate), so a RESUMED grandchild stays capped too. Absent
|
|
30
|
+
* on records from before nesting existed (treated as 0).
|
|
31
|
+
*/
|
|
32
|
+
depth?: number;
|
|
33
|
+
/**
|
|
34
|
+
* The run completed inline in a one-shot session (`-p` / `--mode json`): its
|
|
35
|
+
* report was returned in the tool result and no background task was ever
|
|
36
|
+
* registered, so `task_output` does not know its id.
|
|
37
|
+
*/
|
|
38
|
+
inline?: boolean;
|
|
24
39
|
}
|
|
25
40
|
|
|
26
41
|
/** `<agent>-<n>` with the lowest n not already taken. */
|
|
@@ -32,6 +47,37 @@ export function nextRunName(existing: Iterable<string>, agent: string): string {
|
|
|
32
47
|
}
|
|
33
48
|
}
|
|
34
49
|
|
|
50
|
+
/**
|
|
51
|
+
* The name a new run gets: the requested one when free, else a fresh
|
|
52
|
+
* `<agent>-<n>`. A run name is an identifier for the whole session, so a reused
|
|
53
|
+
* one is replaced rather than shadowing the older run (which would vanish from
|
|
54
|
+
* list_agents while still running). `note` is the model-facing explanation
|
|
55
|
+
* when the request was overridden; the caller prepends it to the spawn result.
|
|
56
|
+
* Shared by the main Agent tool and the nested spawn tool.
|
|
57
|
+
*/
|
|
58
|
+
export function resolveRunName(
|
|
59
|
+
registry: Pick<RunRegistry, "names" | "resolve" | "reserve">,
|
|
60
|
+
agent: string,
|
|
61
|
+
requested: string | undefined,
|
|
62
|
+
): { name: string; note?: string } {
|
|
63
|
+
const taken = new Set(registry.names());
|
|
64
|
+
const wanted = requested?.trim();
|
|
65
|
+
// Reserved synchronously: pi runs a message's tool calls in parallel, and the
|
|
66
|
+
// record is only added after async work (worktree creation, model checks), so
|
|
67
|
+
// two same-turn spawns would otherwise both pick "explore-1".
|
|
68
|
+
if (wanted && !taken.has(wanted)) {
|
|
69
|
+
registry.reserve(wanted);
|
|
70
|
+
return { name: wanted };
|
|
71
|
+
}
|
|
72
|
+
const name = nextRunName(taken, agent);
|
|
73
|
+
registry.reserve(name);
|
|
74
|
+
if (!wanted) return { name };
|
|
75
|
+
return {
|
|
76
|
+
name,
|
|
77
|
+
note: `Note: the name "${wanted}" is already used by task ${registry.resolve(wanted)?.taskId ?? "?"} in this session; this run is named "${name}".`,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
35
81
|
/** Newest .jsonl under `dir` (recursive) — the child session pi created there. */
|
|
36
82
|
export function findSessionFile(dir: string): string | undefined {
|
|
37
83
|
let newest: { path: string; mtime: number } | undefined;
|
|
@@ -63,15 +109,36 @@ export function findSessionFile(dir: string): string | undefined {
|
|
|
63
109
|
export class RunRegistry {
|
|
64
110
|
private byName = new Map<string, AgentRunRecord>();
|
|
65
111
|
private byId = new Map<string, AgentRunRecord>();
|
|
112
|
+
/** Names picked for runs whose record has not been added yet (see resolveRunName). */
|
|
113
|
+
private reserved = new Set<string>();
|
|
114
|
+
|
|
115
|
+
/** Drop every record — a new session (/clear) starts with no runs to address. */
|
|
116
|
+
clear(): void {
|
|
117
|
+
this.byName.clear();
|
|
118
|
+
this.byId.clear();
|
|
119
|
+
this.reserved.clear();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Hold a name until its record is added (a spawn that fails validation leaves it held — harmless). */
|
|
123
|
+
reserve(name: string): void {
|
|
124
|
+
this.reserved.add(name);
|
|
125
|
+
}
|
|
66
126
|
|
|
67
127
|
/** Latest wins per name, matching Claude Code's semantics. */
|
|
68
128
|
add(record: AgentRunRecord): void {
|
|
129
|
+
this.reserved.delete(record.name);
|
|
69
130
|
this.byName.set(record.name, record);
|
|
70
131
|
this.byId.set(record.taskId, record);
|
|
71
132
|
}
|
|
72
133
|
|
|
134
|
+
/** Every name in use: recorded runs plus names reserved for runs being spawned. */
|
|
73
135
|
names(): string[] {
|
|
74
|
-
return [...this.byName.keys()];
|
|
136
|
+
return [...new Set([...this.byName.keys(), ...this.reserved])];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Every spawned run, latest-per-name (what list_agents enumerates). */
|
|
140
|
+
list(): AgentRunRecord[] {
|
|
141
|
+
return [...this.byName.values()];
|
|
75
142
|
}
|
|
76
143
|
|
|
77
144
|
/** Resolve by name, exact task id, or unique task-id prefix (3+ chars). */
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The child-only `send_message` tool, injected as a custom tool into an
|
|
3
|
+
* in-process subagent session. From inside a subagent, `to: "main"` reports
|
|
4
|
+
* progress/findings/questions to the main conversation mid-run — the subagent's
|
|
5
|
+
* plain text output is not visible to the main conversation until it finishes.
|
|
6
|
+
*
|
|
7
|
+
* In-process this needs no IPC or event-stream parsing (unlike the spawned RPC
|
|
8
|
+
* child, where the parent re-parsed the child's tool_execution_end): the tool's
|
|
9
|
+
* execute() calls the parent's `onMessage` callback directly. The main
|
|
10
|
+
* conversation's own `send_message`/`SendMessage` tool (agent→agent addressing)
|
|
11
|
+
* is a different tool registered on the parent session.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
15
|
+
import { Type } from "typebox";
|
|
16
|
+
|
|
17
|
+
export function sendToMainTool(onMessage: (message: string, summary?: string) => void): ToolDefinition {
|
|
18
|
+
return {
|
|
19
|
+
name: "SendMessage",
|
|
20
|
+
label: "Send Message",
|
|
21
|
+
description:
|
|
22
|
+
'Report progress, findings, or a question to the main conversation mid-run (to: "main"). Your plain text output is NOT visible to the main conversation until you finish, so use this to surface something before then.',
|
|
23
|
+
parameters: Type.Object({
|
|
24
|
+
to: Type.Optional(Type.String({ description: 'Recipient — only "main" is valid from inside a subagent' })),
|
|
25
|
+
message: Type.String({ description: "Plain text message for the main conversation" }),
|
|
26
|
+
summary: Type.Optional(Type.String({ description: "5-10 word preview shown in the UI" })),
|
|
27
|
+
}) as never,
|
|
28
|
+
async execute(_toolCallId: string, params: unknown) {
|
|
29
|
+
const p = (params ?? {}) as { to?: unknown; message?: unknown; summary?: unknown };
|
|
30
|
+
// Fail loud (docs/decisions/tools.md): a wrong recipient or a missing
|
|
31
|
+
// message returns isError with the fix named, never a silent success.
|
|
32
|
+
if (p.to !== undefined && p.to !== "main") {
|
|
33
|
+
return {
|
|
34
|
+
content: [{ type: "text" as const, text: `From inside a subagent the only recipient is "main" — got ${JSON.stringify(p.to)}. Omit \`to\` or pass "main".` }],
|
|
35
|
+
details: {},
|
|
36
|
+
isError: true,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
const message = typeof p.message === "string" ? p.message.trim() : "";
|
|
40
|
+
if (!message) {
|
|
41
|
+
return {
|
|
42
|
+
content: [{ type: "text" as const, text: "`message` is required: pass the plain text to deliver to the main conversation." }],
|
|
43
|
+
details: {},
|
|
44
|
+
isError: true,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
const summary = typeof p.summary === "string" ? p.summary : undefined;
|
|
48
|
+
onMessage(message, summary);
|
|
49
|
+
return {
|
|
50
|
+
content: [{ type: "text" as const, text: "Message delivered to the main conversation." }],
|
|
51
|
+
details: { toMain: true, message, summary },
|
|
52
|
+
};
|
|
53
|
+
},
|
|
54
|
+
} as ToolDefinition;
|
|
55
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-process turn tracking for a resident subagent session — the successor to
|
|
3
|
+
* rpc-turns.ts's `RpcTurnTracker`, which parsed a `pi --mode rpc` child's JSONL
|
|
4
|
+
* wire. Here it consumes `AgentSession.subscribe` events directly (no JSON
|
|
5
|
+
* parsing, no process).
|
|
6
|
+
*
|
|
7
|
+
* Turn-end fires on `agent_settled`, not `agent_end`: `agent_end` can be
|
|
8
|
+
* followed by an internal retry/auto-compaction/continue loop before the run is
|
|
9
|
+
* really done, whereas `agent_settled` is emitted exactly once the session
|
|
10
|
+
* becomes idle (see pi's AgentSession `_emitAgentSettled`). Busy/idle is read
|
|
11
|
+
* from the live `AgentSession` getters, so this only accumulates per-turn stats.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { recordAction, type ChildAction } from "../auto-mode/actions.ts";
|
|
15
|
+
import { type ChildOutcome, OUTPUT_CAP } from "./outcome.ts";
|
|
16
|
+
import { addUsage, emptyUsage, type UsageTotals } from "./usage.ts";
|
|
17
|
+
|
|
18
|
+
/** The subset of AgentSession events this tracker reads. */
|
|
19
|
+
interface TrackedEvent {
|
|
20
|
+
type?: string;
|
|
21
|
+
toolName?: string;
|
|
22
|
+
args?: unknown;
|
|
23
|
+
message?: { role?: string; content?: unknown; usage?: unknown; stopReason?: string; errorMessage?: string };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class SessionTurnTracker {
|
|
27
|
+
toolCalls = 0;
|
|
28
|
+
/** What the child did this turn, for auto mode's return review. */
|
|
29
|
+
actions: ChildAction[] = [];
|
|
30
|
+
readonly usage: UsageTotals = emptyUsage();
|
|
31
|
+
/** Final assistant text of the turn in progress (or the last finished one). */
|
|
32
|
+
turnText = "";
|
|
33
|
+
/** Set when the turn's last assistant message ended with a provider error; cleared by a later success. */
|
|
34
|
+
providerError: string | undefined;
|
|
35
|
+
/**
|
|
36
|
+
* Set when the turn was cut short, so its partial text is never reported as
|
|
37
|
+
* a completion (SUBAGENT-REVIEW M3). The runner's `markAborted` is the
|
|
38
|
+
* authoritative setter: every `session.abort()` the runner issues marks the
|
|
39
|
+
* tracker first, and an abort that lands during a tool call leaves no
|
|
40
|
+
* aborted assistant message to observe. The `stopReason: "aborted"` branch
|
|
41
|
+
* in `process` is belt-and-braces for an abort the runner did not initiate.
|
|
42
|
+
*/
|
|
43
|
+
aborted: string | undefined;
|
|
44
|
+
/** Every turn's final text joined, for task_output on a resident agent. */
|
|
45
|
+
transcript = "";
|
|
46
|
+
|
|
47
|
+
/** Call when a new turn is started (a prompt sent while idle). */
|
|
48
|
+
beginTurn(): void {
|
|
49
|
+
this.turnText = "";
|
|
50
|
+
this.providerError = undefined;
|
|
51
|
+
this.aborted = undefined;
|
|
52
|
+
// Per turn, not per session: the review that reads this judges the turn just finished.
|
|
53
|
+
this.actions = [];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** The runner cut this turn short (kill, wall-clock cap, cancelled signal); `reason` is the suffix the report carries. */
|
|
57
|
+
markAborted(reason = "terminated before the turn finished"): void {
|
|
58
|
+
// First reason wins: a wall-clock cap followed by a shutdown kill in the same
|
|
59
|
+
// abort-settling window keeps the more specific explanation.
|
|
60
|
+
this.aborted ??= reason;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Feed one subscribed event. Returns true when a turn just settled. */
|
|
64
|
+
process(event: TrackedEvent): boolean {
|
|
65
|
+
switch (event.type) {
|
|
66
|
+
case "tool_execution_start": {
|
|
67
|
+
this.toolCalls++;
|
|
68
|
+
if (event.toolName) recordAction(this.actions, event.toolName, event.args);
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
case "message_end": {
|
|
72
|
+
if (event.message?.role !== "assistant") return false;
|
|
73
|
+
addUsage(this.usage, event.message.usage);
|
|
74
|
+
this.providerError =
|
|
75
|
+
event.message.stopReason === "error" ? event.message.errorMessage || "unknown provider error" : undefined;
|
|
76
|
+
if (event.message.stopReason === "aborted") this.aborted ??= "terminated before the turn finished";
|
|
77
|
+
const blocks = Array.isArray(event.message.content) ? event.message.content : [];
|
|
78
|
+
const text = blocks
|
|
79
|
+
.filter((b): b is { type: string; text: string } => (b as { type?: string }).type === "text")
|
|
80
|
+
.map((b) => b.text)
|
|
81
|
+
.join("");
|
|
82
|
+
if (text.trim()) this.turnText = text;
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
case "agent_settled": {
|
|
86
|
+
if (this.turnText.trim()) {
|
|
87
|
+
this.transcript = this.transcript ? `${this.transcript}\n\n---\n\n${this.turnText}` : this.turnText;
|
|
88
|
+
}
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
default:
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** The outcome of the turn that just settled. */
|
|
97
|
+
turnOutcome(): ChildOutcome {
|
|
98
|
+
return finishOutcome(this.turnText, this.providerError, this.toolCalls, this.usage, this.actions, this.aborted);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Shape a run/turn's collected state into a ChildOutcome, matching the spawned
|
|
104
|
+
* child's messages. `aborted` (a reason) wins over everything: an aborted turn's
|
|
105
|
+
* partial text is reported as terminated and failed, never as the answer.
|
|
106
|
+
*/
|
|
107
|
+
export function finishOutcome(
|
|
108
|
+
rawOutput: string,
|
|
109
|
+
providerError: string | undefined,
|
|
110
|
+
toolCalls: number,
|
|
111
|
+
usage: UsageTotals,
|
|
112
|
+
actions: ChildAction[],
|
|
113
|
+
aborted?: string,
|
|
114
|
+
): ChildOutcome {
|
|
115
|
+
const output = rawOutput.slice(0, OUTPUT_CAP);
|
|
116
|
+
if (aborted) {
|
|
117
|
+
return {
|
|
118
|
+
output: output.trim() ? `${output}\n\n[${aborted}]` : `Subagent ${aborted}.`,
|
|
119
|
+
toolCalls,
|
|
120
|
+
usage,
|
|
121
|
+
actions,
|
|
122
|
+
failed: true,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
if (providerError) {
|
|
126
|
+
return {
|
|
127
|
+
output: output.trim()
|
|
128
|
+
? `${output}\n\n[The subagent's last request ended with a provider error: ${providerError}]`
|
|
129
|
+
: `Subagent failed with a provider error (its model could not be called — an auth/billing/rate-limit problem, not a task failure): ${providerError}`,
|
|
130
|
+
toolCalls,
|
|
131
|
+
usage,
|
|
132
|
+
actions,
|
|
133
|
+
failed: true,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
if (output.trim()) return { output, toolCalls, usage, actions };
|
|
137
|
+
return { output: "Subagent produced no output.", toolCalls, usage, actions, failed: true };
|
|
138
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure state + rendering for the background-shells side of the below-editor
|
|
3
|
+
* panel — Claude Code's shell manager. Three stages, entered with the FIRST
|
|
4
|
+
* down-arrow (a second ↓ moves on to the agent rows):
|
|
5
|
+
*
|
|
6
|
+
* chip "2 shells · ↓ to manage" → focused: "[2 shells] · Enter to view tasks"
|
|
7
|
+
* list the Background panel: selectable command rows, x stops, Esc backs out
|
|
8
|
+
* details one shell: Status/Runtime/Command + a live bordered output box
|
|
9
|
+
*
|
|
10
|
+
* Task objects arrive over TASK_REGISTER_CHANNEL (lib/shell-tasks.ts) and stay
|
|
11
|
+
* live — output()/status are read at render time. Key wiring lives in
|
|
12
|
+
* index.ts; this file owns layout and the focus reducer only.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { BackgroundTask } from "../background/registry.ts";
|
|
16
|
+
import { countNoun, cutPlainText as cut, formatDuration, padPlainText } from "../lib/tui-render.ts";
|
|
17
|
+
import type { StripKey } from "./panel-keys.ts";
|
|
18
|
+
import type { Paint } from "./panel-render.ts";
|
|
19
|
+
|
|
20
|
+
export interface ShellPaint extends Paint {
|
|
21
|
+
inverse(text: string): string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type ShellFocus =
|
|
25
|
+
| { stage: "chip" }
|
|
26
|
+
| { stage: "list"; selectedId: string }
|
|
27
|
+
| { stage: "details"; selectedId: string };
|
|
28
|
+
|
|
29
|
+
/** A finished shell stays listed this long, so its terminal status is seen. */
|
|
30
|
+
export const SHELL_LIST_LINGER_MS = 60_000;
|
|
31
|
+
|
|
32
|
+
/** Output-box height in the details view (content rows, borders excluded). */
|
|
33
|
+
export const OUTPUT_BOX_ROWS = 10;
|
|
34
|
+
|
|
35
|
+
/** Most list rows shown at once; the selection slides a window over the rest. */
|
|
36
|
+
export const MAX_SHELL_LIST_ROWS = 8;
|
|
37
|
+
|
|
38
|
+
/** Only this much of the spool is split per frame — the box shows 10 lines. */
|
|
39
|
+
const OUTPUT_TAIL_CHARS = 8192;
|
|
40
|
+
|
|
41
|
+
/** The list rows: running shells first, newest-first within each group. */
|
|
42
|
+
export function shellRows(tasks: BackgroundTask[], now: number): BackgroundTask[] {
|
|
43
|
+
return tasks
|
|
44
|
+
.filter((t) => t.status === "running" || (t.finishedAt ?? now) > now - SHELL_LIST_LINGER_MS)
|
|
45
|
+
.sort((a, b) => Number(b.status === "running") - Number(a.status === "running") || b.startedAt - a.startedAt);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Re-anchor a focus whose selected shell left the rows (lingered out or never
|
|
50
|
+
* existed): fall back to the first row, or to the chip when the list emptied.
|
|
51
|
+
*/
|
|
52
|
+
export function anchorShellFocus(focus: ShellFocus, ids: string[]): ShellFocus {
|
|
53
|
+
if (focus.stage === "chip" || ids.includes(focus.selectedId)) return focus;
|
|
54
|
+
return ids.length ? { ...focus, selectedId: ids[0] } : { stage: "chip" };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface ShellTransition {
|
|
58
|
+
/** The next focus; undefined closes the shell section entirely. */
|
|
59
|
+
focus: ShellFocus | undefined;
|
|
60
|
+
/** stopSelected: stop the selected task; toAgents: hand ↓ on to the agent
|
|
61
|
+
* rows; passthrough: unhandled byte — drop focus and let the editor have it. */
|
|
62
|
+
effect?: "stopSelected" | "toAgents" | "passthrough";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The focus reducer — Claude Code's shell-manager keys. `ids` is the current
|
|
67
|
+
* row order (shellRows), `focus` is already anchored against it.
|
|
68
|
+
*/
|
|
69
|
+
export function reduceShellKey(focus: ShellFocus, key: StripKey | undefined, ids: string[]): ShellTransition {
|
|
70
|
+
const index = focus.stage === "chip" ? -1 : ids.indexOf(focus.selectedId);
|
|
71
|
+
switch (focus.stage) {
|
|
72
|
+
case "chip":
|
|
73
|
+
if (key === "down") return { focus: undefined, effect: "toAgents" };
|
|
74
|
+
if (key === "open") return { focus: ids.length ? { stage: "list", selectedId: ids[0] } : focus };
|
|
75
|
+
if (key === "up" || key === "leave") return { focus: undefined };
|
|
76
|
+
return { focus: undefined, effect: "passthrough" };
|
|
77
|
+
case "list":
|
|
78
|
+
if (key === "up") return { focus: index > 0 ? { stage: "list", selectedId: ids[index - 1] } : focus };
|
|
79
|
+
if (key === "down")
|
|
80
|
+
return { focus: index < ids.length - 1 ? { stage: "list", selectedId: ids[index + 1] } : focus };
|
|
81
|
+
if (key === "open") return { focus: { stage: "details", selectedId: focus.selectedId } };
|
|
82
|
+
if (key === "stop") return { focus, effect: "stopSelected" };
|
|
83
|
+
if (key === "leave" || key === "left") return { focus: { stage: "chip" } };
|
|
84
|
+
if (key === "pageUp" || key === "pageDown") return { focus };
|
|
85
|
+
return { focus: undefined, effect: "passthrough" };
|
|
86
|
+
case "details":
|
|
87
|
+
if (key === "left") return { focus: { stage: "list", selectedId: focus.selectedId } };
|
|
88
|
+
if (key === "leave" || key === "open" || key === "space") return { focus: undefined };
|
|
89
|
+
if (key === "stop") return { focus, effect: "stopSelected" };
|
|
90
|
+
if (key === "up" || key === "down" || key === "pageUp" || key === "pageDown") return { focus };
|
|
91
|
+
return { focus: undefined, effect: "passthrough" };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface ShellSectionInput {
|
|
96
|
+
/** shellRows()-ordered. */
|
|
97
|
+
rows: BackgroundTask[];
|
|
98
|
+
runningCount: number;
|
|
99
|
+
focus: ShellFocus | undefined;
|
|
100
|
+
width: number;
|
|
101
|
+
now: number;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Whether the section renders at all (drives widget visibility and ↓ entry).
|
|
106
|
+
* Keyed on the ROW count, not the running count, so a finished shell's chip
|
|
107
|
+
* stays reachable for the linger window — otherwise its outcome would only be
|
|
108
|
+
* visible if the panel happened to be open when it finished.
|
|
109
|
+
*/
|
|
110
|
+
export function shellSectionVisible(rowCount: number, focus: ShellFocus | undefined): boolean {
|
|
111
|
+
return rowCount > 0 || focus !== undefined;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const STATUS_COLOR: Record<string, string> = { running: "accent", failed: "error", stopped: "dim" };
|
|
115
|
+
|
|
116
|
+
/** The section's lines at the current stage. Caller checks visibility first. */
|
|
117
|
+
export function renderShellSection(input: ShellSectionInput, paint: ShellPaint): string[] {
|
|
118
|
+
const width = Math.max(20, input.width);
|
|
119
|
+
const focus = input.focus;
|
|
120
|
+
// The label counts ROWS (running + lingering finished), matching what the
|
|
121
|
+
// panel actually shows — visibility keys on rows too, so a finished shell's
|
|
122
|
+
// chip must not read "0 shells" while it is still openable.
|
|
123
|
+
if (!focus) return [paint.fg("dim", cut(`${countNoun(input.rows.length, "shell")} · ↓ to manage`, width))];
|
|
124
|
+
if (focus.stage === "chip") {
|
|
125
|
+
// Cut the plain parts BEFORE painting — cutting painted text breaks escapes.
|
|
126
|
+
const label = cut(` ${countNoun(input.rows.length, "shell")} `, width);
|
|
127
|
+
const hint = cut("· Enter to view tasks", Math.max(0, width - label.length - 1));
|
|
128
|
+
return [`${paint.inverse(label)}${hint ? ` ${paint.fg("dim", hint)}` : ""}`];
|
|
129
|
+
}
|
|
130
|
+
if (focus.stage === "list") return renderShellList(input, focus.selectedId, paint, width);
|
|
131
|
+
return renderShellDetails(input, focus.selectedId, paint, width);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function renderShellList(input: ShellSectionInput, selectedId: string, paint: ShellPaint, width: number): string[] {
|
|
135
|
+
const out: string[] = [];
|
|
136
|
+
out.push(paint.fg("accent", paint.bold("Background")));
|
|
137
|
+
out.push(cut(countNoun(input.runningCount, "active shell"), width));
|
|
138
|
+
out.push("");
|
|
139
|
+
// A window of MAX_SHELL_LIST_ROWS slid to keep the selection visible, so a
|
|
140
|
+
// big shell fan-out cannot grow the panel past the terminal.
|
|
141
|
+
const selectedIndex = Math.max(0, input.rows.findIndex((t) => t.id === selectedId));
|
|
142
|
+
const start = Math.min(Math.max(0, selectedIndex - MAX_SHELL_LIST_ROWS + 1), Math.max(0, input.rows.length - MAX_SHELL_LIST_ROWS));
|
|
143
|
+
const shown = input.rows.slice(start, start + MAX_SHELL_LIST_ROWS);
|
|
144
|
+
if (start > 0) out.push(paint.fg("dim", ` +${start} more above`));
|
|
145
|
+
for (const task of shown) {
|
|
146
|
+
const marker = task.id === selectedId ? "❯ " : " ";
|
|
147
|
+
const status = ` (${task.status})`;
|
|
148
|
+
const command = cut(task.command ?? task.description, Math.max(1, width - marker.length - status.length));
|
|
149
|
+
out.push(`${marker}${command}${paint.fg(STATUS_COLOR[task.status] ?? "dim", status)}`);
|
|
150
|
+
}
|
|
151
|
+
const below = input.rows.length - start - shown.length;
|
|
152
|
+
if (below > 0) out.push(paint.fg("dim", ` +${below} more below`));
|
|
153
|
+
if (!input.rows.length) out.push(paint.fg("dim", " no shells"));
|
|
154
|
+
out.push("");
|
|
155
|
+
out.push(paint.fg("dim", cut("↑/↓ to select · Enter to view · x to stop · Esc to close", width)));
|
|
156
|
+
return out;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function renderShellDetails(input: ShellSectionInput, selectedId: string, paint: ShellPaint, width: number): string[] {
|
|
160
|
+
const task = input.rows.find((t) => t.id === selectedId);
|
|
161
|
+
const out: string[] = [];
|
|
162
|
+
out.push(paint.fg("accent", paint.bold("Shell details")));
|
|
163
|
+
out.push("");
|
|
164
|
+
if (!task) {
|
|
165
|
+
out.push(paint.fg("dim", "shell gone"));
|
|
166
|
+
return out;
|
|
167
|
+
}
|
|
168
|
+
const field = (label: string, value: string) => `${paint.bold(padPlainText(label, 9))} ${value}`;
|
|
169
|
+
out.push(field("Status:", paint.fg(STATUS_COLOR[task.status] ?? "dim", task.status)));
|
|
170
|
+
out.push(field("Runtime:", formatDuration(task.startedAt, task.finishedAt, input.now)));
|
|
171
|
+
out.push(field("Command:", cut(task.command ?? task.description, Math.max(1, width - 10))));
|
|
172
|
+
out.push("");
|
|
173
|
+
out.push(paint.bold("Output:"));
|
|
174
|
+
|
|
175
|
+
let output = "";
|
|
176
|
+
try {
|
|
177
|
+
output = task.output();
|
|
178
|
+
} catch {
|
|
179
|
+
output = "(output unavailable)";
|
|
180
|
+
}
|
|
181
|
+
// Bound the split to a tail slice — re-splitting a 200KB spool every ticker
|
|
182
|
+
// frame to keep 10 lines is O(buffer) work for nothing. A mid-line cut only
|
|
183
|
+
// matters when the tail holds fewer lines than the box, and those are
|
|
184
|
+
// width-cut anyway.
|
|
185
|
+
const tail = output.length > OUTPUT_TAIL_CHARS ? output.slice(-OUTPUT_TAIL_CHARS) : output;
|
|
186
|
+
const lines = tail ? tail.replace(/\r\n/g, "\n").replace(/\n$/, "").split("\n") : [];
|
|
187
|
+
const shown = lines.slice(-OUTPUT_BOX_ROWS);
|
|
188
|
+
const inner = Math.max(1, width - 4);
|
|
189
|
+
out.push(`┌${"─".repeat(Math.max(0, width - 2))}┐`);
|
|
190
|
+
for (let i = 0; i < OUTPUT_BOX_ROWS; i++) {
|
|
191
|
+
out.push(`│ ${padPlainText(cut(shown[i] ?? "", inner), inner)} │`);
|
|
192
|
+
}
|
|
193
|
+
out.push(`└${"─".repeat(Math.max(0, width - 2))}┘`);
|
|
194
|
+
out.push(paint.fg("dim", `Showing ${countNoun(shown.length, "line")}`));
|
|
195
|
+
out.push("");
|
|
196
|
+
out.push(paint.fg("dim", cut("← to go back · Esc/Enter/Space to close · x to stop", width)));
|
|
197
|
+
return out;
|
|
198
|
+
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Token/cost accounting for
|
|
2
|
+
* Token/cost accounting for in-process subagent runs.
|
|
3
3
|
*
|
|
4
|
-
* A child
|
|
5
|
-
*
|
|
6
|
-
* run's totals
|
|
4
|
+
* A child session emits one `message_end` per assistant message, each carrying
|
|
5
|
+
* a pi-ai `Usage` for that API call (SessionTurnTracker feeds them in); summing
|
|
6
|
+
* them gives the run's totals, across every turn of a resident.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
export interface UsageTotals {
|
|
@@ -19,6 +19,18 @@ export function emptyUsage(): UsageTotals {
|
|
|
19
19
|
return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0, cost: 0 };
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
/** Field-wise sum of two totals (pure — neither input is mutated). */
|
|
23
|
+
export function sumUsage(a: UsageTotals, b: UsageTotals): UsageTotals {
|
|
24
|
+
return {
|
|
25
|
+
input: a.input + b.input,
|
|
26
|
+
output: a.output + b.output,
|
|
27
|
+
cacheRead: a.cacheRead + b.cacheRead,
|
|
28
|
+
cacheWrite: a.cacheWrite + b.cacheWrite,
|
|
29
|
+
total: a.total + b.total,
|
|
30
|
+
cost: a.cost + b.cost,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
22
34
|
/** Add one assistant message's `usage` (untrusted JSONL) into `totals`. */
|
|
23
35
|
export function addUsage(totals: UsageTotals, usage: unknown): void {
|
|
24
36
|
if (!usage || typeof usage !== "object") return;
|
|
@@ -11,6 +11,7 @@ import { mkdtempSync } from "node:fs";
|
|
|
11
11
|
import { tmpdir } from "node:os";
|
|
12
12
|
import { join } from "node:path";
|
|
13
13
|
import { promisify } from "node:util";
|
|
14
|
+
import { registerWorktreeIsolation, releaseWorktreeIsolation } from "../lib/worktree-isolation.ts";
|
|
14
15
|
|
|
15
16
|
const run = promisify(execFile);
|
|
16
17
|
|
|
@@ -39,6 +40,15 @@ export async function createWorktree(cwd: string, label: string): Promise<Worktr
|
|
|
39
40
|
const path = join(dir, "tree");
|
|
40
41
|
const branch = `cc-subagent/${safeLabel}-${Date.now().toString(36)}`;
|
|
41
42
|
await git(["worktree", "add", "-b", branch, path, "HEAD"], cwd);
|
|
43
|
+
// Register for the child permission gate's git-isolation guard. The shared
|
|
44
|
+
// checkout's ROOT, not cwd — the spawn may run from a subdirectory.
|
|
45
|
+
let sharedRoot = cwd;
|
|
46
|
+
try {
|
|
47
|
+
sharedRoot = await git(["rev-parse", "--show-toplevel"], cwd);
|
|
48
|
+
} catch {
|
|
49
|
+
// Keep cwd as the best available anchor.
|
|
50
|
+
}
|
|
51
|
+
registerWorktreeIsolation(path, sharedRoot);
|
|
42
52
|
return { path, branch };
|
|
43
53
|
}
|
|
44
54
|
|
|
@@ -65,5 +75,8 @@ export async function cleanupWorktree(cwd: string, worktree: Worktree): Promise<
|
|
|
65
75
|
// Leave it behind rather than failing the run.
|
|
66
76
|
return false;
|
|
67
77
|
}
|
|
78
|
+
// Release only when the worktree is actually gone — a kept worktree can
|
|
79
|
+
// still host a resumed session (SendMessage), which must stay guarded.
|
|
80
|
+
releaseWorktreeIsolation(worktree.path);
|
|
68
81
|
return true;
|
|
69
82
|
}
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Environment facts for the system prompt's # Environment section.
|
|
3
3
|
* Collected once per (cwd, model) and cached so the prompt stays byte-stable
|
|
4
|
-
* across turns — required for provider prompt caching to pay off.
|
|
4
|
+
* across turns — required for provider prompt caching to pay off. Nothing
|
|
5
|
+
* time-valued belongs here: the date rides Claude Code's `# currentDate` line
|
|
6
|
+
* in the first-message reminder (claude-context), never the system prompt
|
|
7
|
+
* (Claude Code's own prompt has no date line; a cached one went stale at
|
|
8
|
+
* midnight — CACHE-REVIEW-2026-09-04 L2).
|
|
5
9
|
*/
|
|
6
10
|
|
|
7
11
|
import os from "node:os";
|
|
@@ -14,7 +18,6 @@ export interface EnvironmentInfo {
|
|
|
14
18
|
platform: string;
|
|
15
19
|
osVersion: string;
|
|
16
20
|
shell: string;
|
|
17
|
-
date: string;
|
|
18
21
|
modelLine: string;
|
|
19
22
|
/** Per-project auto-memory directory; the memory extension guarantees it exists. */
|
|
20
23
|
memoryDir: string;
|
|
@@ -28,7 +31,6 @@ export function collectEnvironment(cwd: string, modelLine: string): EnvironmentI
|
|
|
28
31
|
platform: process.platform,
|
|
29
32
|
osVersion: `${os.type()} ${os.release()}`,
|
|
30
33
|
shell: process.env.SHELL ? (process.env.SHELL.split("/").pop() ?? "unknown") : "unknown",
|
|
31
|
-
date: new Date().toISOString().slice(0, 10),
|
|
32
34
|
modelLine,
|
|
33
35
|
memoryDir: projectMemoryDir(cwd, os.homedir()),
|
|
34
36
|
};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code's `gitStatus:` block — appended to the very end of the system
|
|
3
|
+
* prompt in a git repo, identical across model tiers, and a one-time snapshot:
|
|
4
|
+
* CC states it is "the git status at the start of the conversation" and "will not
|
|
5
|
+
* update during the conversation", so it is computed once per session and frozen.
|
|
6
|
+
*
|
|
7
|
+
* Format and commands reverse-engineered from real CC captures
|
|
8
|
+
* (git-cc-sonnet.json / git-cc-haiku.json): `git branch --show-current`, the main
|
|
9
|
+
* branch from `origin/HEAD` (falling back to a local main/master), `git config
|
|
10
|
+
* user.name`, `git status --porcelain`, and `git log -5 --format='%h %s'`. Both
|
|
11
|
+
* the status and the log are whole-output `.trim()`ed — which is why a leading
|
|
12
|
+
* `??`/` M` porcelain line renders flush-left in the capture.
|
|
13
|
+
*
|
|
14
|
+
* The module is pure apart from `defaultRunner`; `collectGitStatus` takes an
|
|
15
|
+
* injectable runner so the git commands can be faked in unit tests.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { execFileSync } from "node:child_process";
|
|
19
|
+
|
|
20
|
+
export interface GitSnapshot {
|
|
21
|
+
branch: string;
|
|
22
|
+
mainBranch: string;
|
|
23
|
+
user: string;
|
|
24
|
+
/** `git status --porcelain`, trimmed. */
|
|
25
|
+
status: string;
|
|
26
|
+
/** `git log -5 --format='%h %s'`, trimmed. */
|
|
27
|
+
commits: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const HEADER =
|
|
31
|
+
"gitStatus: This is the git status at the start of the conversation. " +
|
|
32
|
+
"Note that this status is a snapshot in time, and will not update during the conversation.";
|
|
33
|
+
|
|
34
|
+
/** Assemble CC's block from a snapshot. Pure; byte-exact against the captures. */
|
|
35
|
+
export function formatGitStatus(s: GitSnapshot): string {
|
|
36
|
+
return [
|
|
37
|
+
HEADER,
|
|
38
|
+
"",
|
|
39
|
+
`Current branch: ${s.branch}`,
|
|
40
|
+
"",
|
|
41
|
+
`Main branch (you will usually use this for PRs): ${s.mainBranch}`,
|
|
42
|
+
"",
|
|
43
|
+
`Git user: ${s.user}`,
|
|
44
|
+
"",
|
|
45
|
+
"Status:",
|
|
46
|
+
s.status,
|
|
47
|
+
"",
|
|
48
|
+
"Recent commits:",
|
|
49
|
+
s.commits,
|
|
50
|
+
].join("\n");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Runs one git command in `cwd`, returning trimmed stdout or null on any failure. */
|
|
54
|
+
export type GitRunner = (args: string[]) => string | null;
|
|
55
|
+
|
|
56
|
+
function defaultRunner(cwd: string): GitRunner {
|
|
57
|
+
return (args) => {
|
|
58
|
+
try {
|
|
59
|
+
return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
60
|
+
} catch {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** The main branch: `origin/HEAD` if set, else a local `main`/`master`, else the current branch. */
|
|
67
|
+
function resolveMainBranch(run: GitRunner, branch: string): string {
|
|
68
|
+
const originHead = run(["symbolic-ref", "--short", "refs/remotes/origin/HEAD"]);
|
|
69
|
+
if (originHead) return originHead.replace(/^origin\//, "");
|
|
70
|
+
for (const candidate of ["main", "master"]) {
|
|
71
|
+
if (run(["rev-parse", "--verify", "--quiet", candidate])) return candidate;
|
|
72
|
+
}
|
|
73
|
+
return branch;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* CC's `gitStatus:` block for `cwd`, or null when `cwd` is not inside a git work
|
|
78
|
+
* tree. Individual command failures degrade to empty fields rather than dropping
|
|
79
|
+
* the whole block, matching CC's per-field resilience.
|
|
80
|
+
*/
|
|
81
|
+
export function collectGitStatus(cwd: string, runner?: GitRunner): string | null {
|
|
82
|
+
const run = runner ?? defaultRunner(cwd);
|
|
83
|
+
if (run(["rev-parse", "--is-inside-work-tree"]) !== "true") return null;
|
|
84
|
+
|
|
85
|
+
const branch = run(["branch", "--show-current"]) ?? "";
|
|
86
|
+
return formatGitStatus({
|
|
87
|
+
branch,
|
|
88
|
+
mainBranch: resolveMainBranch(run, branch),
|
|
89
|
+
user: run(["config", "user.name"]) ?? "",
|
|
90
|
+
status: run(["status", "--porcelain"]) ?? "",
|
|
91
|
+
commits: run(["log", "-5", "--format=%h %s"]) ?? "",
|
|
92
|
+
});
|
|
93
|
+
}
|