- Added support for caching the stable prefix of the system prompt for Anthropic models.
- Introduced a new option `cacheSystemPrefix` in SessionOptions to enable caching.
- Implemented a nudge escalation system that reminds users to update the TODO.md file, capped at three nudges.
- Created a new `scaffoldWorkflowFiles` function to generate TODO.md, ROADMAP.md, and docs/ directory when they are missing.
- Updated the App component to scaffold workflow files during initialization.
- Added hooks functionality to allow external scripts to modify tool input and manage approvals.
- Implemented tests for the new hooks functionality, ensuring proper approval and execution flow.
- contextPanel splits 'instructions' from 'trackers' (TODO.md/ROADMAP.md), so
/context no longer presents project trackers as standing orders, and its
empty-state mentions both instruction files and trackers
- README: docs table gets Project workflow, command list gets the new commands
- session: gitRoot memoized (one sync fs walk per session, not per prompt
build); workflowPolicy computed once per systemFor
- test: context panel grouping covered
10 workflow tests, 830+ suite green
- system prompt gains a Project workflow block when the repo tracks its own
progress (TODO.md/ROADMAP.md/docs): read the task list first and keep it
current, spec-first for non-trivial work, complete unit tests, verify before
declaring done
- TODO.md + ROADMAP.md loaded as 'Project tracker' instruction blocks, capped
tighter than AGENTS.md so the agent sees the shape without drowning in it
- one soft nudge per session when a turn writes files without touching the
task list; suppressed when todo_write was called; never a gate
- /workflow command + panel showing TODO/ROADMAP/docs presence, line/file
counts, nudge state
- config: workflow.enabled (default true), workflow.docsDir (default docs)
- prompt-cache version key extended with wf: so toggling the workflow busts
the cached system prompt
- docs: docs/workflow.md, configuration table row, ROADMAP + TODO entries
- tests: test/workflow.test.ts (9 tests)
830 tests pass, typecheck clean, build green, live headless demo verified
- /changes diffs the last turn's file snapshot (added/modified/deleted), reusing undo infra via SnapshotStack.peek()
- system prompt memoized behind version counters (notebook/memory/skills/plugins/tools/workspace); hit-rate in /cost, foundation for provider caching
- web_search: DuckDuckGo Lite, keyless, 5 results, SSRF-filtered, in the net set with ask permission
- /search <query>: full-text grep over saved sessions incl. tool-input JSON
- workspace file list re-walks at a turn boundary after writes
- maxSpendPerTurn: per-turn cap stops a runaway step with a notice
- /fork: branch the session at the last turn boundary, original untouched
821 tests pass, typecheck clean, build green
walk() at boot -> Session.workspaceFiles -> systemPrompt Environment
section + subagent system prompts. read_many_files still on-demand,
but the agent now sees the tree without a tool call.
walk({includeDirs}) yields src/ and src/ui/ with trailing slash, only
on the @ path. matchPaths ranks dirs before files for the same prefix
so @src/ surfaces the directory itself. Insertion keeps the slash.
File tools record before-write state via snapshot hook; Session
captures beforeFiles at turn start and afterFiles at turn end (cap
100 via SnapshotStack), and undo/redo restore files + messages
together. Bash not snapshotted (docs + notice).
task now accepts { description, prompt, kind } or
{ tasks: TaskSpec[], kind? } (up to 8) and fans out with
Promise.all. Each subagent keeps its own context window and
the progress panel receives start/step/result/end per id, so
independent searches overlap in wall time instead of queueing.
- src/subagent.ts: runOne extracted, createTaskTool uses union
schema (single | batch), batch validates worker channel once
then Promise.all, results joined as headings.
- docs/agents.md: delegation section notes tasks batch.
- 800 pass (added subagent-parallel.test.ts).
- mcpExpose=phi|direct|auto on RawConfig + MCPConfig; phi default keeps
direct as a measured option for small servers (2-tool cheaper direct)
- src/mcp.ts: buildMcpMetaTools + phi branch in connectMcp: lazy
list/inspect/call behind 3 fixed schemas instead of N per-tool schemas;
direct branch kept; bindMcpGuard wires permission+PluginHost guard for
MCP calls through the same gate as built-ins
- prompt mcpServers names-only under phi; under direct schemas travel
as before — 20-tool server ~2750 tok -> ~phi (names) until mcp_call
- session: isDirect mcpServerNames non-enumerable marker, activeTools
hides mcp_call from pre-wired rules when inside mcp_call, /tools
shows mcp_call and Enabled/Withheld reflects the 3 meta-names under phi
- permission: mcp_* as read (free), mcp_call mutating keyed by
server.tool, same top-level suppression + ask-to-approve as other
mutating tools
- cli: bindMcpGuard + /mcp list shows exposure + mcp_* listed;
headless denies line mentions mcp_list
- commit.ts: git_commit_message kept in git set via toolSetOf/
disabledToolNames overlay
Tests: 798 pass, 0 fail; tsc exit 0; mcp.test.ts covers phi stays
empty until mcp_call and direct still namespaced.
Wrap every builtin tool with withMeta({set, mutating}) at its definition
site (src/tool-utils.ts). TOOL_SETS and MUTATING_TOOLS are now derived
via setsFrom/mutatingNames rather than hand-lists, so a new write cannot
be added ungated by forgetting a parallel list. Permission defaults now
cover the 5 extra mutating line-edit tools. Test suite covers coverage,
derived-equality, and mutating consistency (test/tool-derive.test.ts).
Summarize pruned span: droppedSpan + summarizeDiscarded (6k excerpt,
3-6 lines, one call per compaction) with retained note already landed;
flip TODO Now checkboxes to [x].
Hot-reload: Session.updateSkills/updatePlugins with pendingSkills/
pendingHost deferred during a turn, drainPendingHotReload at turn
boundary, live skill tool closure, cli.tsx rebuilds host from
builtin+registry+external on /registry add/remove. Guard now uses
live pluginHost. Test mid-session skill callability and plugin
enforcement without restart.
Spec: .hermes/plans/todo-now-next.md
- Implement global memory layer for cross-project patterns.
- Improve memory entry scoring with recency and tokenization.
- Adjust MAX_TEXT limit from 400 to 800 for better context retention.
- Add TTL pruning for stale memory entries.
- Capture and summarize dropped content during compaction.
- Update tests to reflect changes in memory behavior and compaction logic.
Each tool line carries the arguments that identify the call - the paths a batch read is about to pull in, the files a patch touches - and its result line carries a one-line outcome. Worker approvals are labelled as subagent asks, and a subagent result attaches to its step in the panel.
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
verify: confirm a change works by running the artifact the way a user would, and report what was not verified. commit: stage deliberately, one commit one reason, match the repository style, and the refusals around amending, hooks, and pushing.
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
The tool guidance covers apply_patch and web_fetch, the approval line derives from the tools actually offered rather than a hardcoded edit-tool check, and plan and review gain approved web research when the net set is on.
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
worker holds the write tools and routes every write and command through the parent's approval gate: same permission rules, same guard, same prompt, flagged as coming from a subagent. Without an approval channel, as in headless runs, the kind is not offered at all rather than silently downgraded to read-only.
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
The fixed three-message tool window collapsed long transcripts to a handful of messages: a 405-message run kept two of 202 tool calls, and the model re-ran what it could no longer see. Pruning now drops reasoning first and keeps the widest recent tool tail that fits a ladder, the SDK carries that view into later steps, and the turn emits one compaction event instead of one per step.
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
One envelope carrying Add, Update, Move, and Delete file markers, validated in full before anything is written: a failure on the fourth file leaves the first three untouched. Permission rules match every path a patch touches, and the write guard and secret checks cover it alongside the other write tools.
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
URL to markdown, size-capped, with redirect and private-address checks so a public URL cannot be walked into the cloud metadata endpoint. It belongs to a net set that is off unless asked for: it is the one tool that leaves the machine.
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Approval was a list of tool names: `bash` needed it, `read_file` did not. That
fails in a specific way. `bash` covers `git status` and `rm -rf` equally, so a
user working through a batch presses `a` — always allow — on the first prompt and
every later command runs unasked, including the one they would have refused. The
gate was strongest when it mattered least and gone by the time it mattered.
Rules now match the *subject* of a call: the command for `bash`, the path for a
file tool, the pattern for a search.
"permission": {
"bash": { "*": "ask", "git *": "allow", "rm *": "deny" },
"edit_file": { "*": "deny", "src/generated/*": "allow" }
}
Plain last-match-wins, with no special case for deny. An earlier version made
deny win wherever it sat, on the theory that a refusal should be impossible to
undo by accident, and it made default-deny-with-exceptions unexpressible — which
is the shape a careful user actually writes, and the same shape as `*.env` denied
while `*.env.example` is allowed. Refusals that must never be configurable stay
in the guard plugin, which runs ahead of this and which --yolo cannot reach.
Three behaviours fall out of it:
- `always` grants the pattern the tool suggests, not the tool. Approving
`git status` runs `git log` unprompted and still asks about `npm publish`.
- `.env`, `.env.*`, and `.pem` are denied on read by default. Not gated, refused:
a secret that reaches the context is on the wire and in the session file, and
there is no taking it back. `.env.example` stays allowed.
- A call repeated identically three times in one turn asks even when allowed. A
model repeating itself is not making progress, and `bash: allow` is a statement
about which commands are safe rather than permission to loop.
The prompt now says which rule matched and what `always` would grant:
bash wants to run
git status --porcelain
y allow once | a always allow bash git * | n deny
A typo in a decision string is dropped at parse time, leaving the tool on its
default. Treating an unparseable value as `allow` would mean one misspelling
silently removing the gate.
Written after surveying Claude Code, Codex, opencode, and phi. Three of the four
had already moved to per-pattern rules; the credential deny and the repeat guard
come from opencode directly. ROADMAP records what was deliberately not taken and
why — OS sandboxing needs three platform implementations and is worse than
nothing if half-built, and opencode's own docs say LSP integration is often not a
net positive.
581 tests, up from 572. The engine is a pure function tested on its own, and the
loop is tested through a real Session: an allowed pattern never prompts, a denied
one never executes, and a `.env` read leaves no secret in the transcript.
The compaction bug, which is the important one:
beta.2 taught the pruner to drop any assistant part whose reasoning item it had
removed. That was right about the 400 and wrong about everything else. On a
reasoning model every tool call carries a provider itemId, so past the threshold
the model could no longer see what it had already run, and re-ran the same tools
until maxSteps ended the turn. Reproduced at 12 model calls for a job needing 4,
with nothing but the user message reaching the wire.
The dependency is not the part, it is the itemId. A part carrying one is
serialised as `{ type: 'item_reference', id }`, a pointer to an item stored
provider-side that depends on its reasoning item. Without the itemId the same
content goes out inline and carries no dependency at all. Verified against the
provider's own serialiser: `text` with an itemId becomes item_reference, the
identical part without one becomes output_text.
So `dropOrphanedItems` becomes `detachOrphanedItems`: strip the itemId, keep the
content. Compaction may shorten the history; it must not blank it. The new test
asserts behaviour rather than shape — the loop must end because the model chose
to, and every call after the first must still carry the earlier exchange. A shape
assertion passed the whole time the model was losing its memory.
Registry, via `/registry [list|search|add|remove|installed]`:
Skills and plugins are treated differently on purpose. A skill is prompt text, so
installing one puts a stranger's words into the system prompt of every future
session in this project; the install shows the body first and the origin is
recorded, so /skills always says where an instruction came from. A plugin is a
JSON manifest of deny rules, evaluated by compiled code identical for every
install. Loading TypeScript from a URL is declined outright: a plugin that can
block tool calls could otherwise lie about blocking them.
Validated before anything is written: https only (file: and data: rejected), name
matched against ^[a-z0-9][a-z0-9-]*$ so it cannot escape its directory, size
caps on index and body, every regex compiled, pattern length capped since it runs
on every tool call, and the body's own name checked against the index. Installed
skills rank below your own, so an install can never shadow a skill you wrote.
Interface:
- Context is a percentage of the compaction threshold, amber from two thirds and
red at 90. A turn about to lose history now says so beforehand.
- Aligned command menu and registry tables; /skills and /plugins name origins.
538 tests, up from 488. The registry is tested against a real local HTTP server,
and the guard is proven to refuse a .env write end to end rather than assumed to.
`bun build --compile --target=bun-windows-x64` rejects `--windows-title` unless
the host is Windows:
error: Using --windows-title is only available when compiling on Windows
build failed for windows-x64 (exit 1)
CI releases all five targets from one Ubuntu runner, so the flag failed the
release on the last of the five builds — after the other four had already been
produced. Locally on Windows it passed, which is why it shipped.
The metadata is now stamped only on a Windows host. The published binary goes
without it rather than the release failing.
Extracted `buildArgs()` so the host-dependent branch is testable: the previous
version could only be checked by running a five-platform build on two operating
systems.
Tools, six built-in to fourteen:
- read_many_files: up to 20 paths read concurrently, each with its own window.
An unreadable path is reported in its own block instead of throwing.
- multi_edit: several edits to one file, validated in memory first so a late
failure cannot leave the file half-written.
- list_dir: ignore-aware depth-limited tree.
- git_status/diff/log/show/blame: read-only, spawned with a fixed argv rather
than a shell string, which is what makes them safe to auto-approve.
toolSets gates them. core is always on; edit-plus and git are optional. A
disabled set reaches neither the wire nor the system prompt, since a prompt
naming an absent tool teaches calls that cannot succeed.
Interface:
- Reasoning streams to a collapsed panel, ctrl-r expands, dropped when the turn
ends: it is progress, not the answer.
- The tool in flight is named from tool-input-start, before its arguments finish
streaming, and cleared on its result.
- Prompts typed mid-turn queue and drain in order. esc clears the queue as well
as aborting.
- @ opens a path picker fed by the ignore-aware walker. Prefix matches rank
above substring matches, so @src/ means "under src/". The walk runs on the
first @, not at startup.
ctrl-c kills the command in flight and keeps the turn. The call throws rather
than returning, so the model cannot read a killed command as one that ran and
failed on its own terms. The kill takes the whole process tree: killing cmd /c
alone left the real command holding both pipes open, so the read never returned
and the interrupt did nothing for 19 seconds.
Two pruning fixes:
- A tool result whose tool call was pruned is now dropped with it. Pruning
counts messages, so the cut landed between an assistant tool-call and the tool
message answering it, producing 400 "No tool call found for function call
output with call_id ...". The reverse pairing is left alone: a call awaiting
its result is what a suspended approval looks like.
- ignore.ts called statFs without importing it, so walk() crashed on the first
symlink.
482 tests, up from 404. Docs synced across README, ROADMAP, TODO, and all of
docs/: tool sets, the new tools, ctrl-c semantics, the tool-start event, and the
two hand-maintained tool-name lists recorded as a known weakness.
Agentic coding CLI on Bun, Ink, and the AI SDK.
Core: streamText loop with SDK-level tool approval so a denied call provably never executes; endpoint fallback for OpenAI reasoning models; retry with backoff.
Tools: read/write/edit/glob/grep/bash, path-jailed, gitignore-aware, ripgrep with a JS fallback, binary rejection, live bash streaming.
Agents: five variants crossing thinking level with tool restriction; plan and review withhold mutating tools from the model.
Extensibility: frontmatter skills with on-demand bodies, plugin host with blocking hooks, MCP stdio and HTTP, read-only subagents.
State: durable per-project memory, session task lists, session persistence, compaction that repairs provider-item dependencies.
Distribution: five-platform cross-compiled binaries with checksums, install scripts, CI on three operating systems.
404 tests, typecheck clean.