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
25 changes: 19 additions & 6 deletions COMMANDS.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,11 @@ aether # no args = interactive REPL
`help`, `auth`, `models`, `model`, `switch`, `agent`, `agents`, `tier`, `effort`, `audit`, `doctor`, `settings`,
`voice`, `preview`, `clear`, `exit`, `mcp`, `autonomous-execution`, `subagent-driven-execution`, `self-review`, `recon`, `plan`, `research`, `project-review`,
`code-review`, `writing-skills`, `writing-plans`, `shell-profile`, `shell-result`, `shell-reset`, `terminal`, `terminal-attach`, `terminal-stop`, `terminal-status`, `queue`, `steer`,
`btw`, `pin`, `drop`, `snapshot`, `limit`, `audit-receipt`, `rollback`, `logs-view`, `goal`, `goals`, `memory`, `workflow`,
`workflow-templates`, `workflow-template`, `vault`, `vault-context`, `vault-search`, `vault-recent`, `vault-project`, `vault-tag`, `vault-tree`, `delegate`, `tree`, `broadcast`,
`gather`, `scaffold`, `port`, `test-drive`, `bench`, `purge`, `stage-diff`, `review`, `ship`, `revert`, `photogen`, `frame`,
`re-frame`, `videogen`, `sequence`, `animate`, `re-cut`, `output`, `storyboard`, `add`, `hud`, `agent-create`, `browser`, `ats`
`btw`, `pin`, `drop`, `context`, `snapshot`, `limit`, `audit-receipt`, `rollback`, `logs-view`, `goal`, `goals`, `memory`,
`workflow`, `workflow-templates`, `workflow-template`, `vault`, `vault-context`, `vault-search`, `vault-recent`, `vault-project`, `vault-tag`, `vault-tree`, `delegate`, `tree`,
`broadcast`, `gather`, `scaffold`, `port`, `test-drive`, `bench`, `purge`, `stage-diff`, `review`, `ship`, `revert`, `photogen`,
`frame`, `re-frame`, `videogen`, `sequence`, `animate`, `re-cut`, `output`, `storyboard`, `add`, `hud`, `agent-create`, `browser`,
`ats`
<!-- SLASH-COMMANDS:END -->

## Runtime capability requirements
Expand Down Expand Up @@ -618,9 +619,12 @@ Each starts an agent loop in the REPL.

| Command | Action |
|---|---|
| `/pin <path> [reason]` | Force a file into persistent context across loops. |
| `/pin <path> [reason]` | Select a file for bounded, fresh inclusion at coding-turn admission. |
| `/pin list` | List pinned files. |
| `/drop <path>` | Evict a file from context. |
| `/drop <path>` | Stop automatic inclusion on future turns; explicit file-reading tools remain available. |
| `/context` | Inspect the last admitted turn's rules, skills, file digests, bytes, bindings, and omissions without showing file bodies. |
| `/context next <task>` | Preview the context for a draft task in the current workspace without sending it. |
| `/context content <path>` | Explicitly preview up to 4096 bytes of a file included in the last admitted local turn. |
| `/snapshot` | Save session state to disk. |
| `/snapshot resume [id]` | Reload a snapshot (cloud first, else local; lists with no id). |
| `/snapshot list` | List saved snapshots. |
Expand All @@ -629,6 +633,15 @@ Each starts an agent loop in the REPL.
| `/rollback` | Discard uncommitted changes to tracked files (git-backed). Restores from the index, so files with staged changes come back to their staged state, not to the last commit. Untracked files are never touched. |
| `/logs-view`, `/logs` | Interactive session log browser. |

Pins are bound by project-relative path to the checkout where the task runs. The
host reads each file once at admission and includes complete UTF-8 files only,
up to 64 KiB per file, 32 pins, and the 512 KiB aggregate composed-context
ceiling shared with rules and skills. Missing, binary, unsafe, and over-budget
files are reported as omissions. `/context next` is a new preview; `/context`
shows the frozen last admission, so a later file edit does not rewrite its
digest. Server-executed cloud chat reports selected-file admission as
unsupported because the local host cannot inspect its server-side context.

### Goals & workflows

| Command | Action |
Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,15 @@ Type `/` at an idle raw terminal prompt to browse described commands. Use
arrows or Tab to choose, Enter to insert editable command text, then Enter
again to run it; Escape restores your earlier draft.

Use `/pin src/guide.md` to attach that file's complete, bounded UTF-8 content
to each admitted coding turn. The file is read from the execution checkout, so
a worktree turn uses its own copy. `/context` shows what the last turn actually
included, with digests and omissions but no file bodies; `/context next <task>`
previews a draft, and `/context content src/guide.md` explicitly previews local
admitted content. `/drop src/guide.md` stops automatic inclusion on later turns.
Server-executed cloud chat reports pin delivery as unsupported when the local
host cannot inspect it. [Limits and details](COMMANDS.md#context--limits).

## Everyday commands

| Goal | Command |
Expand Down
8 changes: 7 additions & 1 deletion docs/generated/commands.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!-- GENERATED FILE: run `npm run docs:generate`; do not edit by hand. -->
<!-- manifest-digest: sha256:f047dc15eff1c35784add44adb5ab9699d07d9e865d06fcc757b2f9b9718d721 -->
<!-- manifest-digest: sha256:15c107ce5164a95e2383d4e4b82c4f8dee285d5942b384498ee7dedbb9b934d6 -->
# Generated command reference

This reference is generated from the validated, versioned command manifest. Availability is evaluated at runtime; a listed command may still require authentication, a hosted capability, or local tooling.
Expand Down Expand Up @@ -561,6 +561,12 @@ evict file from context

Permission: `unknown` · Availability: `runtime-dependent` · Telemetry: `slash.drop`

#### `/context [next <task> | content <path>]`

inspect admitted and next\-task selected context

Permission: `read-only` · Availability: `runtime-dependent` · Telemetry: `slash.context`

#### `/snapshot [resume <id>]`

save session state / reload a snapshot
Expand Down
13 changes: 11 additions & 2 deletions src/commands/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,8 @@ export async function runTurn(
const opened = openRunSession({
projectRoot: ctx.flags.cwd,
prompt,
selectedPins: getRegistry().selectedPins(),
selectedFileTransport: backend === "cloud" ? "unsupported" : "host",
...(skillOpts.capability ? { capability: skillOpts.capability } : {}),
allowIncompleteInstructionDiscovery: backend === "cloud",
...(skillOpts.explicitSkill ? { explicitSkill: skillOpts.explicitSkill } : {}),
Expand Down Expand Up @@ -528,6 +530,7 @@ export async function runTurn(
lastTurnHeader = null;
}
const brief = run.brief(prompt);
getRegistry().lastAdmitted = run.admittedContext();

if (backend === "local") {
// Aether meters nothing on a local brain, so the session is unmetered
Expand Down Expand Up @@ -1968,7 +1971,10 @@ export async function repl(ctx: AppContext, skillOpts: TurnSkillOptions = {}): P
if (setupOwnsInput) buf.endRecoveryScope();
if (setupOwnsInput) process.stdout.write("\x1b[?2004l");
try {
const res = await handleSlash(composerCtx, t, process.stdout, slashAbort.signal);
const res = await handleSlash(composerCtx, t, process.stdout, slashAbort.signal, {
...(skillOpts.explicitSkill ? { explicitSkill: skillOpts.explicitSkill } : {}),
...(skillOpts.noSkills ? { noSkills: true } : {}),
});
if (res.exit) {
discardQueue("session ended"); // entries held after a failure are listed, not lost silently
cleanup();
Expand Down Expand Up @@ -2403,7 +2409,10 @@ export async function replLines(ctx: AppContext, skillOpts: TurnSkillOptions = {
if (t.startsWith("/")) {
inflight = new AbortController();
try {
const res = await handleSlash(ctx, t, process.stdout, inflight.signal);
const res = await handleSlash(ctx, t, process.stdout, inflight.signal, {
...(skillOpts.explicitSkill ? { explicitSkill: skillOpts.explicitSkill } : {}),
...(skillOpts.noSkills ? { noSkills: true } : {}),
});
if (res.exit) break;
if (res.restart) {
applyRestart(ctx.flags, res.restart);
Expand Down
3 changes: 3 additions & 0 deletions src/commands/code.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ import { turnOutcomeRecord } from "./chat.js";
import { openRcCodingObserver, type RcCodingObserver } from "./rc_observation.js";
import { publishCodingVerification } from "./rc_verification.js";
import { promptInputLabel, type PromptInput } from "./prompt_file.js";
import { getRegistry } from "../core/context_registry.js";
import { refuseRunCapability, type RunCapability } from "../core/run_capability.js";
import {
TRANSIENT_READ_AUTO_RETRIES,
Expand Down Expand Up @@ -921,6 +922,7 @@ export async function cmdCode(
const opened = openRunSession({
projectRoot: cwd,
prompt: task || label,
selectedPins: getRegistry().selectedPins(),
...(opts.capability ? { capability: opts.capability } : {}),
...(opts.skill ? { explicitSkill: opts.skill } : {}),
...(opts.noSkills ? { noSkills: true } : {}),
Expand Down Expand Up @@ -1092,6 +1094,7 @@ export async function cmdCode(
model: localSelection?.tag ?? (resolvedHostedModel || undefined),
testCmd: opts.capability === "planning" ? undefined : opts.testCmd,
};
getRegistry().lastAdmitted = run.admittedContext();

// One correlation identity owns the production run. A brain `done` event is
// advisory; the lifecycle remains completing until host verification below.
Expand Down
40 changes: 40 additions & 0 deletions src/commands/command_manifest_data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4524,6 +4524,46 @@ export const COMMAND_MANIFEST_SOURCE: readonly CommandManifestEntry[] = [
"note": null
}
},
{
"key": "slash:context",
"surface": "slash",
"name": "context",
"aliases": [],
"compatibilityAliases": [],
"deprecatedAliases": [],
"args": "[next <task> | content <path>]",
"summary": "inspect admitted and next-task selected context",
"detailedHelp": "/context [next <task> | content <project-relative path>]\nShows the last admitted run's rules, skills, pinned files, digests, bytes and omissions. /context next <task> previews the next task without sending it. /context content <path> explicitly previews admitted local file content (up to 4096 bytes).",
"section": "Context & Limits",
"hidden": false,
"permissionClass": "read-only",
"availability": {
"state": "runtime-dependent",
"capabilityRequirements": []
},
"telemetryName": "slash.context",
"acceptedGlobalFlags": [],
"ownedFlags": {},
"handler": {
"id": "handler:slash:context",
"kind": "host",
"module": "src/commands/slash.ts",
"symbol": "handleSlash"
},
"docs": {
"kind": "manifest",
"module": "src/commands/command_manifest_data.ts",
"symbol": "COMMAND_MANIFEST_SOURCE",
"target": "context",
"usage": "/context [next <task> | content <path>]",
"visible": true,
"disposition": "generated"
},
"release": {
"disposition": "changed",
"note": "Adds an admitted-context inspector and an unsent next-task preview."
}
},
{
"key": "slash:snapshot",
"surface": "slash",
Expand Down
6 changes: 5 additions & 1 deletion src/commands/slash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ import { normalizeOllamaHost } from "../core/ollama.js";
import { listInstalledOllamaModels, OllamaModelsError } from "../core/ollama_models.js";
import { runLogsViewer } from "../ui/logs_viewer.js";

import { pinSlash, dropSlash, snapshotSlash, limitSlash, auditReceiptSlash, purgeSlash } from "./slash_context.js";
import { pinSlash, dropSlash, contextSlash, snapshotSlash, limitSlash, auditReceiptSlash, purgeSlash, type ContextInspectorOptions } from "./slash_context.js";
import { rollbackSlash, revertSlash, stageDiffSlash } from "./slash_git_tools.js";
import { reviewSlash } from "./review.js";
import { shipSlash } from "./ship.js";
Expand Down Expand Up @@ -175,6 +175,7 @@ export async function handleSlash(
line: string,
out: Writable,
signal?: AbortSignal,
contextOptions: ContextInspectorOptions = {},
): Promise<SlashResult> {
const { cmd, arg } = splitSlashCommand(line);

Expand Down Expand Up @@ -430,6 +431,9 @@ export async function handleSlash(
case "drop":
await dropSlash(ctx, out, arg);
break;
case "context":
await contextSlash(ctx, out, arg, { ...contextOptions, backend: await activeBackend(ctx) });
break;
case "snapshot":
await snapshotSlash(ctx, out, arg);
break;
Expand Down
100 changes: 94 additions & 6 deletions src/commands/slash_context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,93 @@ import {
import { readCustodyLog, shortCustodyHash } from "../core/custody.js";
import { fetchTrail } from "../core/audit.js";
import type { AuditEntry } from "../core/audit.js";
import { openRunSession } from "../core/skills/run_session.js";
import { sanitizeForTransport } from "../core/skills/context_packet.js";
import { applyPromptMode } from "./prompt_modes.js";
import type { AdmittedContext } from "../core/selected_context.js";

export interface ContextInspectorOptions {
backend?: "local" | "cloud";
explicitSkill?: string;
noSkills?: boolean;
}

function showContext(out: Writable, heading: string, admitted: AdmittedContext): void {
const d = admitted.descriptor;
out.write(`${heading}\n`);
out.write(` mode: ${d.capability} · route: ${d.transport}\n`);
out.write(` execution workspace: ${JSON.stringify(d.executionRoot)}\n`);
out.write(` pin origin: ${d.originRoot ? JSON.stringify(d.originRoot) : "none"}\n`);
out.write(` assembled context: ${d.contextBytes}/${d.contextLimitBytes} bytes\n`);
out.write(` rules: ${d.rules.length ? "" : "none"}\n`);
for (const rule of d.rules) out.write(` ${JSON.stringify(rule.path)} · ${rule.digest} · ${rule.status}\n`);
out.write(` skills: ${d.skills.length ? "" : "none"}\n`);
for (const skill of d.skills) out.write(` ${skill.id} · ${skill.digest} · ${skill.invocation}\n`);
out.write(` selected files: ${d.files.length ? "" : "none"}\n`);
for (const file of d.files) {
const binding = file.executionPath ? ` · bound to ${JSON.stringify(file.executionPath)}` : "";
const digest = file.digest ? ` · ${file.digest}` : "";
out.write(` ${JSON.stringify(file.path)} · ${file.status} · ${file.includedBytes}/${file.sourceBytes ?? "?"} bytes${digest}${binding}\n`);
out.write(` ${file.range ?? file.reason}\n`);
}
}

/** Metadata by default. Content requires an explicit local preview command. */
export async function contextSlash(ctx: AppContext, out: Writable, arg: string, options: ContextInspectorOptions = {}): Promise<void> {
const registry = getRegistry();
const command = arg.trim();
if (!command) {
if (registry.lastAdmitted) showContext(out, "Last admitted turn (frozen at admission):", registry.lastAdmitted);
else out.write("Last admitted turn: none in this console.\n");
out.write(`Next-draft preview: not resolved without a task (${registry.pins.length} pin${registry.pins.length === 1 ? "" : "s"} configured). Use /context next <task>.\n`);
out.write("Use /context content <project-relative path> to preview an admitted local file.\n");
return;
}
if (command === "next" || command === "content") {
out.write(command === "next" ? "usage: /context next <task>\n" : "usage: /context content <project-relative path>\n");
return;
}
if (command.startsWith("content ")) {
const path = command.slice("content ".length).trim().replace(/\\/g, "/");
const admitted = registry.lastAdmitted;
if (!admitted) { out.write("No admitted turn is available for a local content preview.\n"); return; }
const file = admitted.descriptor.files.find((entry) => entry.path === path && entry.status === "included");
const content = admitted.contents.get(path);
if (!file || content === undefined) { out.write(`No admitted local file content for ${JSON.stringify(path)}.\n`); return; }
const bytes = Buffer.from(content, "utf8");
const limit = 4096;
const preview = sanitizeForTransport(bytes.subarray(0, limit).toString("utf8"));
out.write(`Local content preview: ${JSON.stringify(path)} · ${file.digest} · first ${Math.min(bytes.length, limit)}/${bytes.length} UTF-8 bytes\n`);
out.write(preview + (preview.endsWith("\n") ? "" : "\n"));
if (bytes.length > limit) out.write("[preview clipped; admitted file was included in full]\n");
return;
}
if (command.startsWith("next ")) {
const rawTask = command.slice("next ".length);
if (!rawTask.trim()) { out.write("usage: /context next <task>\n"); return; }
const mode = applyPromptMode(rawTask);
if (mode.error) { out.write(mode.error + "\n"); return; }
if (mode.capability === "planning" && options.backend === "cloud") {
out.write("Next-draft preview refused: /plan cannot run on server-executed cloud chat. Use aether agent --planning.\n");
return;
}
const prompt = mode.handled ? mode.prompt! : rawTask;
const opened = openRunSession({
projectRoot: ctx.flags.cwd,
prompt,
selectedPins: registry.selectedPins(),
selectedFileTransport: options.backend === "cloud" ? "unsupported" : "host",
...(mode.capability ? { capability: mode.capability } : {}),
...(options.explicitSkill ? { explicitSkill: options.explicitSkill } : {}),
...(options.noSkills ? { noSkills: true } : {}),
allowIncompleteInstructionDiscovery: options.backend === "cloud",
});
if (!opened.ok) { for (const line of opened.lines) out.write(line + "\n"); return; }
showContext(out, "Next-draft preview (not admitted or sent):", opened.run.admittedContext());
return;
}
out.write("usage: /context [next <task> | content <project-relative path>]\n");
}

// ── /pin ──────────────────────────────────────

Expand All @@ -39,9 +126,9 @@ export async function pinSlash(ctx: AppContext, out: Writable, arg: string, _lin
const resolved = confineToWorkspace(ctx.flags.cwd, pth);
const label = pth.split("/").pop() || pth;

const entry = getRegistry().pin(resolved, label, reason);
const entry = getRegistry().pin(resolved, label, reason, ctx.flags.cwd);
out.write(`${theme.cyan("📌 pinned")} ${theme.bold(entry.label)} ${theme.dim(entry.path)} (${entry.reason})\n`);
out.write(theme.dim(" This file will persist in context across /recon and /autonomous-execution loops.\n"));
out.write(theme.dim(" Its bounded contents will be read from the task's execution checkout at turn admission; /context shows what was included.\n"));
syncAfter(ctx);
}

Expand All @@ -68,15 +155,16 @@ export async function dropSlash(ctx: AppContext, out: Writable, arg: string): Pr
}

const pth = arg.trim();
const resolved = confineToWorkspace(ctx.flags.cwd, pth);
const wasPinned = getRegistry().isPinned(resolved);
getRegistry().drop(resolved);
const registry = getRegistry();
const resolved = confineToWorkspace(registry.originWorkspace ?? ctx.flags.cwd, pth);
const wasPinned = registry.isPinned(resolved);
registry.drop(resolved);

if (wasPinned) {
out.write(`${theme.cyan("🗑 dropped")} ${theme.bold(pth)} — removed from pinned context\n`);
} else {
out.write(`${theme.cyan("🗑 evicted")} ${theme.dim(pth)}\n`);
out.write(theme.dim(" (wasn't pinned, but will be excluded from future context loads)\n"));
out.write(theme.dim(" (recorded in the drop list; explicit file-reading tools remain available)\n"));
}
syncAfter(ctx);
}
Expand Down
Loading
Loading