Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ Sessions are stored as `<cwd>/.polycode/sessions/<id>.json` and saved after each
| `/context` | Token breakdown vs the model window. |
| `/review` (`/cranky`) | Spawn a depth-1 reviewer child on the local git diff. |
| `/explore <q>` | Spawn a read-only explore child. |
| `/team <task>` | Parallel explore (code + verify), then implement in a git worktree, then review. `/worktree apply` to copy back. |
| `/dashboard` (`/agents`, `Ctrl+\`) | Child agents. Enter/`p` peek live output · `a` attach into the transcript · `x` kill. |
| `/loop [5m] <prompt>` | Recurring prompt (min 15s). `/loop` lists, `/loop stop [id]` cancels. Skips a tick if a turn is already running. |
| `/personas` | Persona overlays from `.polycode/personas/*.md`. Pass `persona` on `task`. |
| `/skills` | List loaded skills (bundled forme, matsumura-style, deep-research). |
| `/mcp` | MCP servers from `.polycode/mcp.json` (fail-soft connect). Notes deferred schemas. |
| `/plugins` | Loaded plugin bundles (`.polycode/plugins/<name>`). |
Expand All @@ -67,8 +71,8 @@ Sessions are stored as `<cwd>/.polycode/sessions/<id>.json` and saved after each
| `/rewind` (`/undo`) | Restore files from the last mutating-tool checkpoint and drop that turn. |
| `/settings` (or `/login`) | Open the API-key Settings screen (paste / import-env / open-page / validate / agentic). |
| `/clear` | Clear the transcript. |
| `/help` | List commands. |
| `/exit` (or `/quit`) | Quit. |
| `/help` | Command overlay (also `/`). Tab completes; ↑↓ history. |
| `/exit` (or `/quit`) | Quit. Ctrl+C interrupts a turn; twice quits. |

## Permission prompt

Expand All @@ -79,6 +83,8 @@ class, and the call. Answer with:
- `a` — allow this tool for the rest of the session (sticky; `plan` mode still overrides)
- `n` / `Esc` — deny

While a turn is running: **Esc** interrupts (kills foreground children). **Ctrl+B** detaches running children and frees the composer (`/dashboard` to peek/attach).

A denied call is reported back to the model so it can adapt.

## Server endpoints (hosted mode)
Expand Down
21 changes: 19 additions & 2 deletions docs/html/cli.html
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,22 @@ <h2>Slash commands (in the TUI)</h2>
<td>Spawn a read-only explore child.</td>
</tr>
<tr>
<td><code>/team &lt;task&gt;</code></td>
<td>Parallel explore (code + verify), then implement in a git worktree, then review. <code>/worktree apply</code> to copy back.</td>
</tr>
<tr>
<td><code>/dashboard</code> (<code>/agents</code>, <code>Ctrl+\</code>)</td>
<td>Child agents. Enter/<code>p</code> peek live output · <code>a</code> attach into the transcript · <code>x</code> kill.</td>
</tr>
<tr>
<td><code>/loop [5m] &lt;prompt&gt;</code></td>
<td>Recurring prompt (min 15s). <code>/loop</code> lists, <code>/loop stop [id]</code> cancels. Skips a tick if a turn is already running.</td>
</tr>
<tr>
<td><code>/personas</code></td>
<td>Persona overlays from <code>.polycode/personas/*.md</code>. Pass <code>persona</code> on <code>task</code>.</td>
</tr>
<tr>
<td><code>/skills</code></td>
<td>List loaded skills (bundled forme, matsumura-style, deep-research).</td>
</tr>
Expand Down Expand Up @@ -282,11 +298,11 @@ <h2>Slash commands (in the TUI)</h2>
</tr>
<tr>
<td><code>/help</code></td>
<td>List commands.</td>
<td>Command overlay (also <code>/</code>). Tab completes; ↑↓ history.</td>
</tr>
<tr>
<td><code>/exit</code> (or <code>/quit</code>)</td>
<td>Quit.</td>
<td>Quit. Ctrl+C interrupts a turn; twice quits.</td>
</tr>
</tbody></table>
<h2>Permission prompt</h2>
Expand All @@ -297,6 +313,7 @@ <h2>Permission prompt</h2>
<li><code>a</code> — allow this tool for the rest of the session (sticky; <code>plan</code> mode still overrides)</li>
<li><code>n</code> / <code>Esc</code> — deny</li>
</ul>
<p>While a turn is running: <strong>Esc</strong> interrupts (kills foreground children). <strong>Ctrl+B</strong> detaches running children and frees the composer (<code>/dashboard</code> to peek/attach).</p>
<p>A denied call is reported back to the model so it can adapt.</p>
<h2>Server endpoints (hosted mode)</h2>
<table>
Expand Down
3 changes: 3 additions & 0 deletions examples/personas/terse.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Terse

Reply in at most 5 bullets. No preamble. Cite file paths.
9 changes: 6 additions & 3 deletions packages/cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
mergeHookSets,
registerExtraChildren,
listExtraChildren,
loadPersonas,
CHILD_TYPES,
type CompactConfig,
type GenerateRequest,
Expand Down Expand Up @@ -40,7 +41,7 @@ import {
import { loadWorkflows } from "@polycode/workflows";
import { loadPlugins } from "@polycode/plugins";
import { createLspTool } from "@polycode/lsp";
import { hydrateEnv, getKey, ENV_VAR, type ProviderId } from "@polycode/secrets";
import { hydrateEnv, loadDotEnvFiles, envFileCandidates, getKey, ENV_VAR, type ProviderId } from "@polycode/secrets";
import { tools as allTools } from "@polycode/tools";
import { connectMcpServers, createMcpSearchTool, loadMcpConfig, type McpServerStatus } from "@polycode/mcp";
import { expandSkill, loadSkills, skillPromptBlock } from "@polycode/skills";
Expand Down Expand Up @@ -85,7 +86,7 @@ const DEFAULT_CONFIG: AppConfig = {
long: { provider: "google", model: "gemini-2.5-pro" },
},
system:
"You are polycode, a terminal coding agent. Be concise. Use tools to inspect and edit the project.",
"You are polycode, a terminal coding agent. Be concise. Use tools to inspect and edit the project. When work splits, call several task tools in one turn so they run in parallel: explore/researcher for read-only surveys, isolation=worktree for implementers. background=true returns an id (task_wait collects). resume_from continues a finished child. persona= applies .polycode/personas. Depth 1. Summarize child returns; do not paste raw dumps.",
};

function loadConfig(): AppConfig {
Expand Down Expand Up @@ -124,7 +125,7 @@ async function main(): Promise<void> {
allowPositionals: true,
});

// Pull keys from the OS keychain / file store into the env the SDK reads.
loadDotEnvFiles(envFileCandidates(process.cwd()));
hydrateEnv();

const cfg = loadConfig();
Expand Down Expand Up @@ -363,6 +364,8 @@ async function main(): Promise<void> {
route,
autoRoute: router.strategy === "model",
validate: (p) => validateKey(cfg, p),
specForProvider: (p) => ({ provider: p, model: modelFor(cfg, p as ProviderId) }),
personas: loadPersonas(cwd),
onAgentic: (p) =>
`agentic provisioning for ${p} is not wired yet — coming soon (MCP/tool flow). Use paste / import-env / open-page for now.`,
initialMessages,
Expand Down
74 changes: 74 additions & 0 deletions packages/core/src/agent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,80 @@ describe("Agent", () => {
expect(toolMessage?.content.map((p) => "name" in p ? p.name : "")).toEqual(["slow", "fast"]);
});

it("runs consecutive explore task children concurrently", async () => {
let active = 0;
let maxActive = 0;
const task: ToolSpec = {
name: "task",
description: "task",
parameters: {},
permission: "mutating",
parallelSafe: true,
async run() {
active++;
maxActive = Math.max(maxActive, active);
await new Promise((r) => setTimeout(r, 25));
active--;
return { output: "ok" };
},
};
const call = (id: string, type: string) =>
({
type: "tool_call" as const,
call: {
type: "tool_call" as const,
id,
name: "task",
input: { description: id, prompt: "p", subagent_type: type },
},
});
const provider = new ScriptedProvider([
[call("1", "explore"), call("2", "explore"), { type: "stop", reason: "tool_use" }],
[{ type: "stop", reason: "end_turn" }],
]);
const agent = new Agent(provider, [task], new PermissionEngine("ask", async () => "deny"), { sandbox });
agent.pushUser("fan out");
await collect(agent);
expect(maxActive).toBe(2);
});

it("auto-isolates two writable task children onto worktrees", async () => {
const seen: unknown[] = [];
const task: ToolSpec = {
name: "task",
description: "task",
parameters: {},
permission: "mutating",
parallelSafe: true,
async run(input) {
seen.push(input);
return { output: "ok" };
},
};
const call = (id: string) =>
({
type: "tool_call" as const,
call: {
type: "tool_call" as const,
id,
name: "task",
input: { description: id, prompt: "edit", subagent_type: "general" },
},
});
const provider = new ScriptedProvider([
[call("1"), call("2"), { type: "stop", reason: "tool_use" }],
[{ type: "stop", reason: "end_turn" }],
]);
const agent = new Agent(provider, [task], new PermissionEngine("yolo", async () => "once"), {
sandbox,
openWorktree: async () => ({ sandbox, path: "/tmp/wt" }),
});
agent.pushUser("two writers");
await collect(agent);
expect(seen).toHaveLength(2);
expect(seen.every((s) => (s as { isolation?: string }).isolation === "worktree")).toBe(true);
});

it("stops a runaway tool loop at maxSteps", async () => {
const call = { type: "tool_call", call: { type: "tool_call", id: "1", name: "read", input: {} } } as const;
const provider = new ScriptedProvider([[call, { type: "stop", reason: "tool_use" }]]);
Expand Down
Loading
Loading