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
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Developer documentation for Acolyte, a terminal-first AI coding agent. Reliable
## Core concepts

- [Architecture](./architecture.md) — system map of runtime flow and boundaries
- [Workspace](./workspace.md) — workspace root, sandbox, and profile behavior
- [Errors](./errors.md) — error contracts, runtime classes, and recovery boundaries
- [Lifecycle](./lifecycle.md) — one request through a bounded phase loop
- [TUI](./tui.md) — custom React reconciler for terminal rendering
Expand Down
2 changes: 2 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Every concept below is modeled as an explicit entity with typed contracts, its o
- **Lifecycle phases** — resolve, prepare, generate, evaluate, finalize as separate modules
- **Lifecycle state** — task-scoped internal retry/support state owned by the lifecycle
- **Effects** — lifecycle-owned side effects that run between generation and pure evaluation
- **Workspace sandbox** — canonical workspace-root boundary for tool filesystem access
- **Tools** — typed definitions with categories, schemas, and output contracts
- **Guards** — pre-tool policy units that inspect runtime state and decide allow or block
- **Evaluators** — post-generation policy units that decide accept or re-generate
Expand Down Expand Up @@ -68,6 +69,7 @@ lifecycle → guard → cache → toolkit → registry
- **cache:** per-task reuse layer for read-only and search tool results
- **toolkit:** domain tool definitions with guarded execution (`file-toolkit`, `code-toolkit`, `git-toolkit`, `shell-toolkit`, `web-toolkit`, `checklist-toolkit`)
- **registry:** toolkit registration and agent-facing tool surface
- **workspace sandbox:** canonical path boundary checks for tool file access and symlink-escape prevention
- **details:** see [Tooling](./tooling.md)

## Lifecycle flow
Expand Down
2 changes: 1 addition & 1 deletion docs/comparison.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ Most other agents rely on prompt instructions such as "please run the tests".

## Workspace detection

Acolyte auto-detects project tooling from workspace config files at lifecycle start. The detected profile includes ecosystem, package manager, lint command, format command, test command, and line width. Detection is cached per workspace and feeds into the lifecycle policy and agent instructions.
Acolyte auto-detects project tooling from workspace config files at lifecycle start. The detected profile includes ecosystem, package manager, lint command, format command, and test command. Detection is cached per workspace and feeds into the lifecycle policy and agent instructions.

| Project | Detection approach |
|---|---|
Expand Down
4 changes: 2 additions & 2 deletions docs/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ Shipped, user-visible capabilities.
- streaming progress output for tool activity with real-time token usage
- proactive token budgeting via tiktoken with system prompt reservation and priority-based allocation
- two-tier result cache for read-only and search tools with SQLite-backed cross-task persistence
- workspace profile detection for ecosystem, package manager, lint, format, test commands, and line width
- workspace profile detection for ecosystem, package manager, lint, format, and test commands
- automatic formatting of edited files via detected formatter
- automatic linting of edited files via detected linter
- ecosystem-aware scoped test runner (`test-run`) with auto-detected test command
Expand Down Expand Up @@ -61,7 +61,7 @@ Shipped, user-visible capabilities.

## Safety and control

- workspace and temp-root path guardrails
- workspace sandbox boundary enforcement for tool filesystem access
- cooperative interruption and queued message handling over RPC

## Diagnostics
Expand Down
2 changes: 1 addition & 1 deletion docs/glossary.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,4 @@ Naming conventions and core terms used across Acolyte code and docs.
| Tool Recovery | Structured recovery payload attached to a tool failure when the tool knows the corrective action |
| Toolkit | Group of domain tools exposed through adapters and composition |
| Workspace Command | Typed shell command descriptor used for lint, format, and test commands |
| Workspace Profile | Cached per-workspace detection result containing ecosystem, package manager, commands, and line width |
| Workspace Profile | Cached per-workspace detection result containing ecosystem, package manager, and commands |
2 changes: 1 addition & 1 deletion docs/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Built for safe autonomous execution of bounded tasks with the developer in contr
- Multi-provider support (OpenAI, Anthropic, Google)
- SQLite-backed cross-task tool cache
- Structured model-to-user handoff for blocked signals (`awaiting-input` state)
- Workspace profile detection for ecosystem, lint, format, test runner, package manager, and line width
- Workspace profile detection for ecosystem, lint, format, test runner, and package manager
- Format → lint evaluator chain from detected workspace commands
- Ecosystem-aware scoped test runner with auto-detected test command
- Pluggable ecosystem detectors for TypeScript, Python, Go, Rust
Expand Down
7 changes: 6 additions & 1 deletion docs/tooling.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ All tool calls run through guarded execution paths to ensure:
- consistent error shaping
- call recording for evaluators/debug

## Workspace sandbox

Tool filesystem access is scoped to the workspace root. See [Workspace](./workspace.md) for enforcement details.

## File discovery

`collectWorkspaceFiles` determines what files are in scope for `file-find` and `file-search`. Three exclusion layers apply in order:
Expand Down Expand Up @@ -73,7 +77,8 @@ Internal implementations may share compilers, rule objects, or AST helpers, but
- `src/code-toolkit.ts` — Code manipulation for scanning and editing source files.
- `src/git-toolkit.ts` — Git operations (status, diff, log, show, add, commit).
- `src/tool-registry.ts` — Tool registration and agent-facing surface.
- `src/tool-guards.ts` — Pre-execution guards including limits and path validation.
- `src/tool-guards.ts` — Pre-execution guards including limits and redundancy controls.
- `src/workspace-sandbox.ts` — Canonical workspace sandbox boundary and path enforcement.
- `src/tool-cache.ts` — Per-task result caching with stable key generation.

## Further reading
Expand Down
62 changes: 62 additions & 0 deletions docs/workspace.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Workspace

Defines how Acolyte resolves, scopes, and enforces workspace behavior.

## Workspace root

Each request runs with one workspace root directory.

If no workspace is provided, Acolyte uses the current working directory.
When a workspace path is provided, it must exist and be a directory.

## Detection

Workspace behavior has two detection layers:

1. Root detection:
The runtime resolves the active workspace from request input (`workspace`) or defaults to process CWD.

2. Profile detection:
The workspace detector infers ecosystem and commands (format/lint/test, package manager) from project files and caches the result per workspace for reuse.

## Sandbox

Tool filesystem access is scoped to the workspace root.

Access inside the workspace is allowed. Access outside the workspace is denied.
This rule is enforced across tool entry paths, including CLI tool mode (`acolyte tool ...`).

Path checks are fail-closed and use resolved-path validation (`realpath`) so symlink escapes are blocked. For paths that do not exist yet, validation resolves the nearest existing parent and enforces the same boundary.

For `shell-run`, Acolyte executes argv (`cmd` + `args`) without shell evaluation. The command path and path-like arguments are validated against the workspace sandbox, and execution runs with a restricted environment allowlist. This is command-level enforcement, not kernel-level process isolation, and it does not constrain filesystem access performed internally by the executed binary.

No special temp-root exception exists in sandbox enforcement.

## Sandbox violations

Boundary violations are returned as structured tool errors:

- `code`: `E_SANDBOX_VIOLATION`
- `kind`: `sandbox_violation`

## Workspace profile

Acolyte detects and stores a workspace profile with:

- ecosystem
- package manager
- format command
- lint command
- test command

The profile is used by lifecycle effects and tooling behavior, including format/lint runs on edited files and scoped test execution through detected test commands.

Profile detection is implemented by workspace detector modules and exposed via `resolveWorkspaceProfile`.

## Observability

Workspace and sandbox behavior is visible in lifecycle debug/trace events:

- `lifecycle.workspace.profile`
- `lifecycle.workspace.sandbox`
- `lifecycle.sandbox.violation`
3 changes: 2 additions & 1 deletion scripts/benchmark.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ interface Project {

const PROJECTS: Project[] = [
{ name: "acolyte", url: "https://github.com/cniska/acolyte.git", lang: "typescript" },
{ name: "aider", url: "https://github.com/Aider-AI/aider.git", lang: "python" },
{ name: "opencode", url: "https://github.com/anomalyco/opencode.git", lang: "typescript" },
{ name: "crush", url: "https://github.com/charmbracelet/crush.git", lang: "go" },
{ name: "aider", url: "https://github.com/Aider-AI/aider.git", lang: "python" },
{ name: "pi", url: "https://github.com/badlogic/pi-mono.git", lang: "typescript" },
{ name: "goose", url: "https://github.com/block/goose.git", lang: "rust" },
{ name: "openhands", url: "https://github.com/All-Hands-AI/OpenHands.git", lang: "python" },
Expand Down
4 changes: 2 additions & 2 deletions src/agent-instructions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ describe("createInstructions", () => {
["file-create", "full content"],
["file-find", "name/path pattern"],
["file-search", "text/regex"],
["shell-run", "explicit user commands", "known repo commands"],
["do not use shell", "file read/search/edit fallbacks"],
["shell-run", "user explicitly asked", "known repository commands"],
["do not use it for file read/search/edit fallbacks"],
]);
});

Expand Down
4 changes: 2 additions & 2 deletions src/chat-message-handler-stream.int.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ describe("chat message handler stream behavior", () => {
type: "tool-call",
toolCallId: "call_1",
toolName: "shell-run",
args: { command: "echo hi" },
args: { cmd: "echo", args: ["hi"] },
});
options.onEvent({
type: "tool-output",
Expand Down Expand Up @@ -553,7 +553,7 @@ describe("chat message handler stream behavior", () => {
type: "tool-call",
toolCallId: "call_1",
toolName: "shell-run",
args: { command: "echo hi" },
args: { cmd: "echo", args: ["hi"] },
});
options.onEvent({
type: "tool-output",
Expand Down
2 changes: 1 addition & 1 deletion src/cli-command-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ const COMMAND_REGISTRY: Record<string, CliCommand> = {
command: "tool",
usage: "acolyte tool <tool-id> [args...]",
description: t("cli.help.desc.tool"),
examples: ['acolyte tool file-find "src/**/*.ts"', 'acolyte tool shell-run "bun run verify"'],
examples: ['acolyte tool file-find "src/**/*.ts"', "acolyte tool shell-run bun run verify"],
},
handler: (args) =>
toolMode(args, {
Expand Down
2 changes: 1 addition & 1 deletion src/cli-tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ function coerceInput(toolId: string, rest: string[]): unknown {
if (typeof parsed === "object" && parsed !== null) return parsed;
}
const joined = rest.join(" ");
if (toolId === "shell-run") return { command: joined };
if (toolId === "shell-run") return { cmd: rest[0], args: rest.slice(1) };
if (toolId === "file-find") return { patterns: [joined] };
if (toolId === "file-search") return { patterns: [joined] };
if (toolId === "file-read") return { paths: [{ path: joined }] };
Expand Down
22 changes: 20 additions & 2 deletions src/cli-trace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const traceEventSchema = z.enum([
"chat.request.started",
"chat.request.completed",
"lifecycle.workspace.profile",
"lifecycle.workspace.sandbox",
"lifecycle.start",
"lifecycle.prepare",
"lifecycle.generate.start",
Expand All @@ -37,6 +38,7 @@ const traceEventSchema = z.enum([
"lifecycle.tool.result",
"lifecycle.tool.error",
"lifecycle.tool.output",
"lifecycle.sandbox.violation",
"lifecycle.guard",
"lifecycle.signal.accepted",
"lifecycle.skill.context",
Expand Down Expand Up @@ -77,18 +79,20 @@ const EVENT_FIELDS: Record<TraceEvent, FieldSpec[]> = {
"test_command",
"line_width",
],
"lifecycle.workspace.sandbox": ["sandbox_root"],
"lifecycle.start": ["model"],
"lifecycle.prepare": ["model", "history_messages"],
"lifecycle.generate.start": ["model"],
"lifecycle.generate.done": ["model", "tool_calls", "text_chars"],
"lifecycle.generate.error": ["model", "error"],
"lifecycle.error": ["source", "kind", "code", "category", "tool"],
"lifecycle.yield": ["generation_attempt"],
"lifecycle.tool.call": ["tool", "path", "paths", "pattern", "command"],
"lifecycle.tool.call": ["tool", "path", "paths", "pattern", "cmd", "args"],
"lifecycle.tool.cache": ["tool", "hit", "hits", "misses", "size"],
"lifecycle.tool.result": ["tool", "duration_ms", "is_error"],
"lifecycle.tool.error": ["tool", "error"],
"lifecycle.tool.output": ["tool"],
"lifecycle.sandbox.violation": ["tool", "path", "paths", "cmd", "args"],
"lifecycle.guard": ["guard", "tool", "action", "detail"],
"lifecycle.signal.accepted": ["signal"],
"lifecycle.skill.context": ["skill_name", "instruction_chars"],
Expand Down Expand Up @@ -177,13 +181,27 @@ function parsePaths(raw: string): string[] {
}
}

function parseArgs(raw: string): string[] {
try {
const parsed = JSON.parse(raw) as unknown;
if (!Array.isArray(parsed)) return [];
return parsed.filter((entry): entry is string => typeof entry === "string");
} catch {
return [];
}
}

function truncate(value: string, max: number): string {
return value.length > max ? `${value.slice(0, max - 1)}…` : value;
}

function extractToolArg(fields: Record<string, string>): string {
if (fields.path) return fields.path;
if (fields.command) return truncate(fields.command, 40);
if (fields.cmd) {
const args = fields.args ? parseArgs(fields.args) : [];
const rendered = [fields.cmd, ...args].join(" ").trim();
return truncate(rendered, 40);
}
if (fields.pattern) return `"${fields.pattern}"`;
if (fields.paths) return parsePaths(fields.paths).join(", ");
return "";
Expand Down
2 changes: 1 addition & 1 deletion src/cli-visual.int.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -450,7 +450,7 @@ describe("cli visual regression", () => {

Examples:
acolyte tool file-find "src/**/*.ts"
acolyte tool shell-run "bun run verify"
acolyte tool shell-run bun run verify
`),
},
{
Expand Down
Loading
Loading