Repository navigation
ink-agent v0.2.0
[0.2.0] - 2026-10-06
Added
-
guardian — the config-driven permission gate (#guardian). The
shipped gate example isexamples/extensions/guardian.mjsnow: rules are
plain text with*wildcards that span anything including newlines (or
regexfor full regular expressions), split into ordereddeny/ask
lists (denywins), with an optionalreasonshown to the human and
optionaltoolscoping —write/editrules match the resolved file
path, bash rules match the command. Anything unmatched runs; there is no
allowaction. A broken config keeps the last good rules (footer hint,
/guardian reload); every deny/ask outcome is audited to
~/.ink/guardian.log. The ask picker red-highlights the span the rule
matched (preview.warnSpans). An ask question left unanswered can now
time out (askTimeoutMs, a new optional host-side deadline on
api.confirm; the shipped template uses 10 minutes): the timeout counts
as declined, is audited astimeout, grants no session memory, and
reaches the model with its own wording, distinct from a manual decline.
While a deadline is set, the picker's question line carries the dim
countdown in parentheses right next to it (e.g.allow this bash command? (9:59), ticking down).
The template was rebuilt around portable, catastrophe-only rules
(whole-home and filesystem-root wipes, disk tools, the.sshfolder,
remote-destroying git / gh operations). Start from
examples/extensions/guardian.template.json; design and review log:
docs/guardian-design.md. -
Tool-name colors via extensions (#tool-name-colors). Tool names in the
TUI call header can carry a color now, through a new load-gated extension
registrationapi.registerToolColor(names, color)— one name, several, or
"*", with any of the 16 standard-16 tokens,"none"(bold-only), or an
absolute token:#rrggbb(truecolor) oransi256:N(0–255). Named
tokens follow the terminal theme; absolute tokens do not, by design.
api.suggestToolColor(names, color)is the author tier: a tool's own
extension suggests its default look, and any user registration outranks
any suggestion (resolution: user exact > user"*"> suggested exact >
suggested"*"). Nothing is colored by default; opt-in themes ship as
extensions (the owner's palette lives in
examples/extensions/tool-colors.mjs, which also doubles as the API
example). Exact names beat the wildcard, the first registration of a key
wins within its tier, hex is stored lowercased, and print/replay/legacy
surfaces are untouched. Colors never affect layout — spans are applied
after the row plan, so visible widths are unchanged; a colors-only theme
counts in the startup banner (— 8 colors), suggestions as their own
count (— 2 tools, 2 suggested colors). Design + three adversarial
review rounds + three amendments:docs/tool-name-colors-design.md.
Changed
-
Node support and CI coverage. The minimum Node version is now 22.19.0,
matching the pinned TUI dependency. CI checks that exact minimum and Node 24;
CI and release gates requirerg/fdinstead of silently skipping search tests. -
Ink rename (
ink-agent@0.2.0). The product is now Ink, an
open-source AI assistant and agent harness for the terminal. The sole
executable isink; there is noimpalias. Active configuration uses
~/.ink, project.ink, andINK_*, without automatic migration or
old-name fallback.ImpSettingsbecomesInkSettingsfor source consumers.
Ordinary session history can be preserved by a separately approved cutover;
historical child sessions remain inspectable but are not resumable. Saved
impVersionfields,.imp-machine-idleases, historical release records,
and actualFishInSalt/impURLs are retained. Publication uses new
INK_NPM_PUBLISH_ENABLED/INK_NPM_PACKAGEgates; dispatch is always
dry-run. Historical publishing capability needs separately approved
retirement, and the first Ink publication needs its own reviewed design. -
Task live view in its own header (#task-inline-live-rows). A running
taskcall's live overview (the└─ pending #N …line with the agent and
the elapsed time, plus itsN tool starts · last: …row) moved out of the
bottom activity region and into the transcript, directly under that call's
own● taskheader. Concurrent subagents therefore read top to bottom in launch order —
header, its own live line, its own result — instead of all headers stacked
above all overview rows. -
Confirm prompt surface, Phase 1 (#confirm-prompt). The guardian-style
approval ask is legible now: the transcript keeps one▪ confirm:record line
(the dim detail note is written only where no picker exists — readline,
no-host and print paths are byte-identical), the picker numbers its rows
(→ 1. Yes) on non-filterable pickers, digits1–9answer directly, and a
dim affordance line sits under the items
((↑/↓ move · enter select · esc cancel · 1-3 quick pick)). Filterable
pickers are untouched: digits stay query characters and no hint row is added.
Design + five adversarial review rounds:docs/confirm-prompt-design.md. -
Confirm prompt surface, Phase 2 (#confirm-prompt). Extensions can now say
it in two structured fields instead of prose:previewrenders the request in
the transcript's own call-header idiom (● bash rm -rf …, the risky fragment
alert-highlighted, never a completion suffix — the call has not run), and
rememberLabelnames what "don't ask again this session" will cover
(Yes, don't ask again this session (this command pattern)). The host
sanitizes both, so an extension can never inject terminal control sequences,
and shows the command exactly once on every surface — in the picker where one
exists, as one plain note line otherwise. Guardian passes both at its bash
gate and a memory label at its write gate, dropping thecommand:prefix
from its detail. Malformed values render nothing; hosts without a picker
ignore the styling and stay byte-stable. -
Confirm prompt surface, Phase 3 (#confirm-prompt). The approval moment now
says who is asking and reads as one piece. The host names the caller —
▪ confirm: guardian — allow this bash command?in the transcript, a faint
· guardianafter the picker title, andblocked by extension guardian:in
the block result — so extensions stop naming themselves (guardian drops its
[guardian]prefixes) without any new API surface: the label is host-derived
and cannot be spoofed. While a picker is open the activity region paints no
tool rows:tool_startprecedes the gate, so those rows used to claim
running Nsalthough nothing was executing, and repeated the gated command a
third time. Every picker now opens one blank row below the transcript, and the
line you decide on (the extension's reason) renders at normal weight instead of
faint — the(↑/↓ move …)affordance and the▪record lines stay faint.
Design + three adversarial review rounds + a mutation-verified implementation
check:docs/confirm-prompt-design.md§15. -
Confirm prompt surface, Phase 4 (#confirm-prompt). The picker title no
longer repeats the caller's name (allow this bash command?— the transcript's
▪ confirm: guardian — …record line already says who asks), and every picker
box now opens with a horizontal rule carrying that name in the middle:
──────────────── guardian ────────────────. The rule is host-drawn from the
same host-held name, so an extension cannot write a different one; the dashes
are faint and the label sits at normal weight. Unattributed pickers get plain
dashes, and the login dialog keeps its own frame. Design + two adversarial
review rounds + a mutation-verified implementation check:
docs/confirm-prompt-design.md§16. -
TUI tool-call duration (#tui-tool-elapsed). Completed tool calls in the
TUI transcript carry a marker — green✓for successes, red✗for
failures — with a dim wall-time suffix when the call took ≥1s
(● bash npm test ✓ 2.3s;1m03sminute form); sub-second calls show
the bare marker (amendments 2-3). Interruptions and replayed history carry
none; legacy and print rendering are unchanged. Design + adversarial
review:docs/tui-tool-elapsed-design.md. -
Loop health monitoring + uncapped children (#loop-health). Subagents no
longer stop at a 60-turn wall (CHILD_MAX_TURNSremoved; owner decisions
2026-09-29: no numeric valve, no prompt injection). A shared,
observation-only loop-health monitor now runs in both the subagent engine
and the main loop and detects three conditions from the existing event
stream: repeated identical tool-call turns, repeated failededit/write
attempts, and the child's
compaction-failure backstop. Child facts surface honestly (task-result
lines + ahealthfield on the TaskRecord); first fires render one dim
REPL note (▪ health: …;INK_HEALTH=0disables; thresholds overridable
viaINK_HEALTH_*env). Nothing is injected into any model context; print
mode gains no health output of its own (a fired signal's task-result text
change shows there only in the tool row's line count). Design + two-track
adversarial review (plus two folded post-merge review rounds):
docs/loop-health-design.md. -
Startup model resolution (#startup-model-resolution). A machine whose
only credential belongs to a non-anthropic family no longer demands a
manual/modelin every new session:- when the builtin startup default is unusable and exactly ONE credential
source exists (the shared-MOONSHOT_API_KEYpair counts once), Ink
resolves that family's curatedswitchHint(zai/glm-5.3, …) — one
▪note, never written to settings, seeded as a non-explicit session
model, print mode resolves silently; /loginauto-selects the family's switchHint when no usable model
exists (a usable model is never replaced);- an explicit
/modelswitch prints a one-time pointer:
/settings defaultModel <id>keeps it for new sessions; ink -c/-rre-resolves a stale restored model the same way (the
recordedsession_modelrow is intentionally left as-is);- copy: with a configured provider present,
no model available — run /loginbecomesno model selected — /modelon the banner/footer/title,
the runner note lists the configured families, and print-mode failure
text names-m <hint>/ the env-var family instead of the dead default.
- when the builtin startup default is unusable and exactly ONE credential
-
Fresh-install model honesty (#fresh-install-hint). On a machine
with no credentials anywhere, imp 0.1.0 displayed the hardcoded startup
default (claude-sonnet-4-5) as if it were in use — banner, footer, and
session seed — and only failed on the first message. Now:- the banner identity line, footer, and resumed-session lines render
no model available — run /login to connect one/no model — /login
instead of the unusable model id (the think segment is dropped while
unusable); - one startup
▪note teaches/login(all seven families listed) or
the env var — a configured-elsewhere machine gets the targeted
run /login <family>form; - print mode (
ink -p …) fails fast BEFORE any session/log write with a
family-targeted error (@fileargument errors still win the exit); - the dead default is never persisted: all three
seedModelsites
(startup,/new, resume restore) are gated on live usability, so
ink -ccan no longer resurrect it; sessions written by 0.1.0 resume
unchanged (no migration, nothing rewritten); /loginand/logoutrepaint the footer, so the model segment flips
immediately after a credential change;- the anthropic "No API key found" error now also teaches
/login anthropic.
- the banner identity line, footer, and resumed-session lines render
-
Tool live view under its own call line (#tool-inline-live-rows). A
running top-level tool call's live state (└─ running 3s) now renders in
the transcript, directly under that call's own● …row, instead of in the
bottom activity region — the command no longer appears twice while a call
runs. The activity region keeps only turn-level rows (⠋ working…,
compacting context…) and the existing picker suppression (norunning
claim while a gate is pending). A provider-synthesized tool_call id reused
within one run can no longer paint a running row onto the previous call's
settled entry. Display-only: the settled transcript, print mode, the legacy
shell, history, sessions, extensions, and replay are unchanged. Known
tradeoff (accepted, same astask): a long-running call's live row scrolls
with its header. Superseded before release by #call-closing-status
(below): the timer now closes the call's info row —taskcalls included —
and reads as a bareNs. -
Call closing status: one slot for the running timer and the completion
marker (#call-closing-status). A running call's timer (a bare dimNs)
— including ataskcall's, whose└─ pendingrow no longer carries
seconds — now renders at the end of the call info, closing the last row of
wrapped commands, multi-line commands, and longtaskprompts, and turns
into the completion marker (✓/✗, plus the time when ≥1s) in that same
place
when the call settles; the
separate non-task live row is gone, so a multi-line command is no longer
split by a status row and the marker no longer cuts words. Interrupted
calls and rows below the 8-column slot floor render no status. Display-only:
a call that settles on a single un-split row renders byte-identical to
before; print mode, the legacy shell, history, sessions, extensions, and
replay are unchanged.
Removed
- guardian v1 and the host classify seam. The rule-based example gate
(examples/extensions/guardian.mjs, configurable viaIMP_GUARDIAN_BLOCK,
audited to~/.imp/guardian.log), its tests (test/guardian.test.ts,
test/guardian-auto.test.ts,test/guardian-auto-host.test.ts,
test/classify-seam.test.ts,test/user-input-log.test.ts), and the
#guardian-auto-modehost machinery (api.classifyand its types, the
classify hostsrc/repl/classify.ts, the call-context and
user-input/gate-decision logs, theverifiedUserContextevent field,
LLMRequest.temperature) are gone; theExtensionApiis back to eleven
members. guardian needs none of it (on+confirm+setStatus+
registerCommand). The v1 design record stays at
docs/guardian-auto-mode-design.mdwith a superseded banner.
Fixed
-
A tool's result stays under its own call (#tool-result-follows-call). A
concurrency-safe chunk emits everytool_startbefore anytool_end(for
deterministic result ordering), so a result block used to be appended after
all headers — five paralleltaskcalls rendered as five headers followed by
five detached⎿blocks. Each result fold is now placed directly after its
own call's header, while the loop's emission order is untouched. Print mode
and the legacy shell are byte-identical. -
Per-call settle: real durations, live completion (#tool-settle). Every
call in a concurrent chunk used to report the batch's wall time (five tasks
all showing✓ 8.9s) and nothing updated until the slowest finished. Each
call is now measured at its own settle point, so its row shows its own
runtime, and its activity row,✓/✗marker and⎿result land as soon as
that call finishes — not when the chunk drains. The emitted
tool_settledevent is display-only: history, the session file, extension
hooks, print output and replay are unchanged.