mirror of
https://github.com/Twigpine/zero.git
synced 2026-11-11 23:40:28 -10:00
* 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]>
123 lines
3.3 KiB
Go
123 lines
3.3 KiB
Go
package cli
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strings"
|
|
|
|
"github.com/Gitlawb/zero/internal/agent"
|
|
"github.com/Gitlawb/zero/internal/sessions"
|
|
"github.com/Gitlawb/zero/internal/tools"
|
|
)
|
|
|
|
type execSessionRecorder struct {
|
|
prepared sessions.PreparedExec
|
|
err error
|
|
}
|
|
|
|
// captureCheckpoint snapshots the before-state of files a tool call will mutate.
|
|
// Best-effort: failures never affect the run. Returns the checkpoint event and
|
|
// true when one was recorded.
|
|
func (recorder *execSessionRecorder) captureCheckpoint(workspaceRoot string, call agent.ToolCall) (sessions.Event, bool) {
|
|
if recorder.prepared.Store == nil || recorder.prepared.Session.SessionID == "" {
|
|
return sessions.Event{}, false
|
|
}
|
|
var args map[string]any
|
|
if call.Arguments != "" {
|
|
_ = json.Unmarshal([]byte(call.Arguments), &args)
|
|
}
|
|
targets := tools.MutationTargets(workspaceRoot, call.Name, args)
|
|
if len(targets) == 0 {
|
|
return sessions.Event{}, false
|
|
}
|
|
event, err := recorder.prepared.Store.CaptureToolCheckpoint(recorder.prepared.Session.SessionID, workspaceRoot, call.Name, targets)
|
|
if err != nil || event.Type == "" {
|
|
return sessions.Event{}, false
|
|
}
|
|
return event, true
|
|
}
|
|
|
|
func shouldUseExecSession(options execOptions) bool {
|
|
return options.outputFormat == execOutputStreamJSON ||
|
|
options.resume != "" ||
|
|
options.resumeLatest ||
|
|
options.fork != "" ||
|
|
options.initSessionID != ""
|
|
}
|
|
|
|
func preflightExecSession(options execOptions) error {
|
|
if options.resume == "" && !options.resumeLatest && options.fork == "" {
|
|
return nil
|
|
}
|
|
if (options.resume != "" || options.resumeLatest) && options.fork != "" {
|
|
return execUsageError{"Use either --resume or --fork, not both."}
|
|
}
|
|
|
|
store := sessions.NewStore(sessions.StoreOptions{})
|
|
switch {
|
|
case options.fork != "":
|
|
session, err := store.Get(options.fork)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if session == nil {
|
|
return execUsageError{"Zero session not found: " + options.fork}
|
|
}
|
|
case options.resume != "":
|
|
session, err := store.Get(options.resume)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if session == nil {
|
|
return execUsageError{"Zero session not found: " + options.resume}
|
|
}
|
|
case options.resumeLatest:
|
|
latest, err := store.Latest()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if latest == nil {
|
|
return execUsageError{"No Zero sessions available to resume."}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func createSessionTitle(prompt string) string {
|
|
title := strings.Join(strings.Fields(prompt), " ")
|
|
if len(title) > 80 {
|
|
title = title[:80]
|
|
}
|
|
if title == "" {
|
|
return "Zero exec session"
|
|
}
|
|
return title
|
|
}
|
|
|
|
func execSessionTitle(options execOptions, prompt string) string {
|
|
if title := strings.TrimSpace(options.sessionTitle); title != "" {
|
|
return title
|
|
}
|
|
return createSessionTitle(prompt)
|
|
}
|
|
|
|
func specialistAgentName(sessionTitle string) string {
|
|
title := strings.TrimSpace(sessionTitle)
|
|
if title == "" {
|
|
return ""
|
|
}
|
|
if name, _, ok := strings.Cut(title, ":"); ok {
|
|
return strings.TrimSpace(name)
|
|
}
|
|
return title
|
|
}
|
|
|
|
func (recorder *execSessionRecorder) append(eventType sessions.EventType, payload any) {
|
|
if recorder.err != nil || recorder.prepared.Store == nil || recorder.prepared.Session.SessionID == "" {
|
|
return
|
|
}
|
|
_, recorder.err = recorder.prepared.Store.AppendEvent(recorder.prepared.Session.SessionID, sessions.AppendEventInput{
|
|
Type: eventType,
|
|
Payload: payload,
|
|
})
|
|
}
|