* notify: dep-free terminal bell / OSC-9 completion notifier
* config: notify.mode + notify.focusMode (validated, default off)
* exec: --notify/--no-notify + completion notify on stderr
* tui: focus reporting + completion/awaiting-input notifications
* notify: fire on exec --use-spec completion; headless ignores focus; gate focus-reporting on enabled
* notify: validate --notify value, skip notify on empty ask_user, deterministic resolver tests
Addresses #149 review (CodeRabbit + @Vasanthdev2004):
- parseExecArgs rejects an invalid --notify value (only off/bell/notify/both),
so a CLI typo errors early instead of silently running with no notification.
- TUI OnAskUser fires AwaitingInput only when len(request.Questions) > 0 — a
request with no questions auto-resolves without prompting, so the bell/desktop
notification was a false positive.
- notify resolver tests pass Env: map[string]string{} so Resolve does not fall
back to os.Getenv (deterministic, not ambient-env dependent).
* modelregistry: add UpgradeTargetID and Registry.UpgradeTarget escalation lookup
* modelregistry: seed escalation chains in catalog decoration
* modelregistry: verification gate for escalation upgrade map
* agent: add ModelSwitcher option and ToolResult.RequestedModel field
* agent: lift escalate_to_model meta into ToolResult.RequestedModel
* agent: perform at-most-one mid-run model switch in the turn loop
* tools: add SideEffectNone for control-only tools
* tools: add escalate_model control tool (metadata + args)
* cli: parse --allow-escalation exec flag
* cli: document --allow-escalation in exec help
* cli: register escalate_model tool under --allow-escalation
* cli: wire model switcher for exec escalation
* cli: guard byte-identical exec run without escalation flag
* cli: attribute post-switch usage to the escalated model
* tools: accept an explicit empty escalate_model reason
reason is an optional, informational-only arg, but the previous arg
parsing rejected an explicit reason:"" (non-empty-string check), failing
the whole escalation. Switch to stringArgWithEmpty so an absent OR empty
reason is accepted while a non-string value is still rejected. Add a test
covering both the empty and absent forms.
* agent: test (nil,nil) escalation switcher keeps original provider
When a ModelSwitcher returns (nil, nil) — no error, no replacement — the
loop's `else if newProvider != nil` guard already keeps the active
provider, so the run continues on the original model. Add an agent-layer
test that proves this (it panics if the nil guard is dropped), closing the
gap the adversarial review flagged.
Also note the deferred compaction limitation near the switch: the
compactor's context-window budget is fixed at run start and not updated on
a mid-run switch (needs a ModelSwitcher contract change to fix).
* cli: gate usage model attribution on the escalation flag and harden tests
FIX A (back-compat): the OnUsage closure wrote a "model" key into every
persisted EventUsage payload unconditionally, diverging a non-escalation
run from the byte-identical origin/main baseline. The model can only
change mid-run under --allow-escalation, so include the "model" key only
then. A new flag-OFF test asserts the persisted usage payload carries no
"model" key (mutation-proven).
Test hardening for the CLI escalation boundary:
- Wire test: each newProvider build returns a DISTINCT provider with its
own turn counter; assert the source provider served exactly the
escalation turn and the escalated provider served exactly the
post-switch answer. Fails if `provider = newProvider` is dropped.
- Mixed usage attribution: emit usage pre- and post-switch and assert the
first attributes to the original model, the second to the escalated
target (the loop's ordering guarantee).
- Switch-error path: a failed rebuild is non-fatal and post-error usage
stays attributed to the original model (currentModel unchanged).
- Top-tier decline end-to-end: a flag-on run that escalates while already
top-tier performs no switch (newProvider built exactly once) and
succeeds.
* modelregistry: validate UpgradeTargetID resolves at registry construction
* cli: update currentModel only when the switched provider is non-nil
* sandbox: make 'none' a first-class side effect (control-only tools)
---------
Co-authored-by: Gnanam <[email protected]>
* zeroruntime: add ImageBlock type and NormalizeImageMediaType
* zeroruntime: add optional Images field to Message
* zeroruntime: add SeedMessagesWithImages, delegate SeedMessages with nil
* Emit Anthropic image source blocks for user image turns
* Add inlineData part type to Gemini request schema
* Serialize user image attachments as Gemini inlineData parts
* test(openai): pin text-only chat request serialization before content refactor
* feat(openai): allow structured multimodal content parts in chat request
* feat(openai): map image attachments to image_url content parts
* test(openai): assert image content parts serialize over the request body
* modelregistry: add SupportsVision capability gate
* Add shared image-file loader package
* cli: parse repeatable exec --image flag into imagePaths
* cli: resolve exec image attachments via shared loader with sniff and size cap
* agent: thread image attachments through Options into the seeded user turn
* cli: resolve and vision-gate exec image attachments into the agent run
* cli: document exec --image flag in help output
* streamjson: add InputImage type and InputEvent.Images field
* streamjson: whitelist images field on message events only
* streamjson: allow empty content on message events that carry images
* streamjson: add ResolveImages to decode, normalize and cap input images
* streamjson: cover image parse-and-resolve round-trip and text-only invariance
* Add /image command definition to TUI
* Add synchronous vision check for TUI model
* Implement /image attach, clear, and non-vision refusal in TUI
* Render pending image chips above the input in both skins
* Thread pending images into the agent turn and clear on submit
* Wire stream-json images through to the agent run
Stream-json message images were parsed and validated but never reached
agent.Options.Images: streamjson.ResolveImages had no production caller,
so images sent over stream-json input were silently dropped.
resolveExecPrompt now also returns the resolved stream-json images
(nil for text input and for image-free stream-json input). The exec
runner merges them with --image attachments before the vision gate, so
both input sources flow through the same drop+warn and the same
agent.Options.Images wiring.
Adds an exec-level end-to-end test that feeds a base64 image on a
stream-json message event via stdin on a vision model and asserts the
image reaches the agent run (the provider request's user turn).
* Re-check TUI vision support at submit, not only at /image attach
A user could attach an image on a vision model and then switch to a
non-vision model via /model before submitting; the images were threaded
unconditionally into the agent turn, reaching a model that rejects them.
The commandPrompt submit path now re-runs the vision check against the
current effective model. When the active model can't accept images, the
pending images are dropped (not threaded) and an inline notice mirroring
the headless drop+warn wording is appended. Pending image state is still
cleared after submit either way.
Adds a test that attaches on a vision model, switches the model id to a
non-vision one, submits, and asserts the run received no images, an
inline notice was appended, and pending state was cleared.
* Bound memory with an os.Stat size pre-check in imageinput.LoadFile
LoadFile read the whole file into memory before checking the size cap,
so a multi-gigabyte file allocated a huge buffer only to be discarded.
Add an os.Stat size pre-check before os.ReadFile: a file whose size
exceeds MaxImageBytes is rejected with the same 10 MiB-limit error
before any read; a missing file surfaces the same not-found error. The
existing post-read len(data) check is kept as a TOCTOU backstop in case
the file grows between Stat and ReadFile.
* OpenAI: only the user role emits image content-parts
The Anthropic and Gemini providers only attach images in their user
branches, but OpenAI funnels every role through a single mapMessage,
which would emit image content-parts for any role carrying Images.
Guard the content-parts path to the user role: a non-user message that
happens to carry Images keeps the plain string/nil content path. Pins
the invariant with TestMapMessageNonUserRolesNeverCarryImages, asserting
assistant/tool/system messages with Images set still serialize plain
content (no content-parts array).
* Deep-copy image buffers so attachments are never aliased
Add a shared CloneImageBlocks helper in zeroruntime that deep-copies a
slice of ImageBlock including each Data byte slice (returning nil for
nil/empty input). Use it in SeedMessagesWithImages so a seeded user turn
owns an independent copy of the caller's image bytes, and in the agent
loop's copyMessages so image bytes are deep-copied alongside ToolCalls
instead of staying aliased across history/request/result copies.
Tests assert that mutating the caller's original Data bytes after seeding
leaves the seeded message unchanged, and that copyMessages produces
independent image bytes.
* Accept image-only stream-json turns and bound base64 image size
Resolve stream-json images before the prompt in the exec input path and
tolerate an empty prompt when images are present, so an image-only
message event (empty content + one image) proceeds with prompt="" and the
image reaches the agent run instead of being rejected. A turn carrying
neither text nor images still errors.
In ResolveImages, reject an oversized payload from its ENCODED length
(DecodedLen) BEFORE the base64 decode so a huge blob never allocates a
decode buffer just to be capped afterward; the post-decode length check
stays as a backstop.
Tests cover the image-only exec turn reaching the agent and the
pre-decode oversize rejection (asserting the size gate fires before any
decode).
* Bound the image read in imageinput.LoadFile with a LimitReader
Replace os.ReadFile with os.Open + io.ReadAll over an
io.LimitReader(f, MaxImageBytes+1). The os.Stat pre-check is only a
fast-path hint (a non-regular file reports a misleading size and the file
can grow between stat and read); the LimitReader is the real bound, so at
most one byte past the cap is ever buffered and an oversized or unbounded
source can never allocate a multi-gigabyte buffer. The file is closed via
defer and the existing size message is reused.
* Fix RenderChat frame-height off-by-one with image chips
cmdRegion emits TWO rows when ImageChips is set (a chip row above the
input line), but the body/overlay height accounting assumed a one-row
command region, overflowing the fixed frame by one line. Compute the
fixed-row count from the actual command-region height (1 or 2 rows) so
the composed frame stays exactly Height rows in both cases.
A RenderChat test asserts the rendered frame line-count equals the
requested height both with and without ImageChips set.
* Reject an empty inline --image= value
Route the inline --image= form through requiredInlineFlagValue, the same
empty-rejection the other inline flags use, so --image= errors with
"--image requires a value" instead of appending an empty image path. A
parse test covers the empty inline value.
* imageinput: reject non-regular files (FIFO/dir) before opening
---------
Co-authored-by: KRATOS <[email protected]>
Co-authored-by: Gnanam <[email protected]>
* CLI wiring: skills/zeroline commands, --mode presets, exec checkpoints, sandbox --effective, sessions rewind
Final module of the runtime-core split. Wires the new runtime features into the cli, PRESERVING main's merged specialist cli (the source branch predates specialist and drops it; this keeps it via a 3-way merge from the common base, adding the reskin features as ours/theirs-only changes).
- skills command (internal/cli/skills.go) listing internal/skills; zeroline-skin TUI launcher (internal/cli/zeroline.go) via runInteractiveTUIWithSkin.
- exec: --mode presets (smart/deep/fast/large/precise) via applyExecMode; model-registry alias resolution + deprecation/effort notices; ContextWindow sizing (modelContextWindow); before-mutation checkpoint recording in OnToolCall; enhanced tool-result output (ChangedFiles/Redacted/Display); permission-mode validation fix.
- sandbox policy --effective (resolved guards); sessions rewind (executes the rewind plan); interactive TUI defaults to Ask mode (advertises write/edit/bash + gates via prompts).
- internal/streamjson: EventCheckpoint/EventRestore + CheckpointInfo for structured checkpoint output.
Specialist cli preserved (specialist.go + flags + runtime + tests all intact). build/vet/-race/full-suite + GOOS=windows build green; no new deps.
* Address #128 review: make sessions rewind reachable + honor --exclude-target, mode/notice/redaction fixes
- sessions: add "rewind" to the parser whitelist (isSessionsCommand) — it was wired in dispatch + help but parseSessionsArgs rejected it as 'unknown sessions command', so it was unreachable. Regression test TestRunSessionsRewindIsReachableAndHonorsExcludeTarget.
- sessions rewind: honor --exclude-target — ApplyRewind keeps THROUGH a sequence, so when KeepTarget is false apply through TargetSequence-1, matching rewind-plan (previously rewind always applied TO the target, disagreeing with the plan). The test asserts --exclude-target keeps fewer events than the default.
- sessions rewind: redact the session id in the success + not-found messages (consistent with every other human-readable path).
- exec_parse: reject empty inline --mode= via requiredInlineFlagValue (was a silent no-op running defaults).
- exec: notice writes (model-deprecation + reasoning-effort advisories) now treat write failures as exitCrash, consistent with the rest of the command.
build/vet/-race/full-suite + GOOS=windows build green.
---------
Co-authored-by: KRATOS <[email protected]>