Commit Graph
9 Commits
Author SHA1 Message Date
KRATOS 58e209d021 Sound / completion notifier: terminal bell + OSC-9 on turn completion & awaiting-input (#149)
* 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).
2026-06-09 14:34:24 +05:30
Anandan b83367e9d3 Add spec mode review flow (#145) 2026-06-08 21:07:57 +05:30
KRATOSandGnanam 69810d5ab0 Mid-run model escalation: escalate_model tool + --allow-escalation (#146)
* 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]>
2026-06-08 20:50:32 +05:30
68facda077 Image (multimodal) input: exec --image, stream-json images, TUI /image across all 3 providers (#143)
* 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]>
2026-06-08 18:49:09 +05:30
KRATOSandKRATOS 07d8ffc524 CLI wiring: skills/zeroline commands, --mode presets, exec checkpoints, sandbox --effective, sessions rewind (#128)
* 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]>
2026-06-07 23:42:10 +05:30
Anandan ae6c9595db feat(specialist): add exec metadata flags (#104)
* Add specialist exec metadata flags

* Address exec metadata review
2026-06-07 10:40:16 +05:30
Vasanth T 43c256068a feat: make Go runtime the app path (#71)
* feat: make go runtime the app path

* fix: harden update semver parsing
2026-06-05 17:24:58 +05:30
KRATOS a58e6c4f57 feat: add Go worktree and verification backend (#70)
* feat: add Zero worktree isolation backend

* feat: add Zero verification runner backend

* feat: wire worktree and verification commands

* fix: address worktree verification review feedback

* test: tighten worktree base dir assertion

* fix: redact workflow CLI output paths

* fix: keep workflow diagnostics useful while redacted
2026-06-05 16:39:01 +05:30
Vasanth T a710b0e5f5 feat: add Go headless protocol sessions (#60)
* feat: add Go headless protocol sessions

* test: harden stream-json tool list filtering

* fix: resolve stream-json review followups

* chore: enable coderabbit review verdicts
2026-06-05 10:41:38 +05:30