diff --git a/src/agent/Terminal.zig b/src/agent/Terminal.zig index 09ae7e493..0a552e5e2 100644 --- a/src/agent/Terminal.zig +++ b/src/agent/Terminal.zig @@ -1419,6 +1419,7 @@ pub fn printSlashParseError(self: *Terminal, err: Schema.ParseError, name: []con error.UnknownField => "unknown field (typo?)", error.DuplicateField => "the same field was supplied twice (check for case-variants like Selector vs selector)", error.PositionalNotAllowed => "positional only works for commands with one required field. Use key=value", + error.PositionalMustComeFirst => "the positional value must come before key=value pairs", error.UnterminatedQuote => "unterminated quote", error.UnsupportedEscape => "backslash escapes aren't supported in quoted values; use the other quote style or `'''…'''`", error.InvalidValue => "invalid value (check argument type)", diff --git a/src/agent/settings.zig b/src/agent/settings.zig index 5041f9001..93f1add12 100644 --- a/src/agent/settings.zig +++ b/src/agent/settings.zig @@ -162,8 +162,9 @@ pub fn resolveModelName(opts: Config.Agent, resolved: ?ResolvedProvider, remembe /// Precedence: explicit `--effort` flag > remembered `.lp-agent.zon` value > /// mode default. The interactive REPL defaults to `.low` so turns stay snappy; -/// one-shot `--task` and script runs default to `.medium`, where answer -/// quality matters more than per-turn latency. +/// one-shot `--task` defaults to `.medium`, where answer quality matters more +/// than per-turn latency. (Script runs never call the LLM, so the resolved +/// effort is unused there.) pub fn resolveEffort(opts: Config.Agent, remembered: ?Remembered, will_repl: bool) Config.Effort { if (opts.effort) |e| return e; if (remembered) |r| if (r.effort) |e| return e; diff --git a/src/script/Schema.zig b/src/script/Schema.zig index 3aeebc097..b447696e3 100644 --- a/src/script/Schema.zig +++ b/src/script/Schema.zig @@ -78,6 +78,7 @@ pub const ParseError = error{ DuplicateField, MalformedKv, PositionalNotAllowed, + PositionalMustComeFirst, UnterminatedQuote, UnsupportedEscape, InvalidValue, @@ -224,7 +225,13 @@ pub fn parseValueDiag(self: Schema, arena: std.mem.Allocator, rest_raw: []const list.appendAssumeCapacity(.{ .key = self.required[0], .value = stripQuotes(tokens[0]) }); } for (tokens[kv_start..]) |tok| { - const eq = std.mem.indexOfScalar(u8, tok, '=') orelse return error.MalformedKv; + const eq = std.mem.indexOfScalar(u8, tok, '=') orelse { + // `/extract save=x '{…}'` — the value would have bound fine as a + // leading positional, so point at the ordering instead of the + // generic kv complaint. + if (self.required.len == 1 and !leading_positional) return error.PositionalMustComeFirst; + return error.MalformedKv; + }; if (eq == 0 or eq == tok.len - 1) return error.MalformedKv; const key = tok[0..eq]; // Reject typos like `checke=false` that would otherwise be silently diff --git a/src/script/command.zig b/src/script/command.zig index 4f52e5a96..e76d67409 100644 --- a/src/script/command.zig +++ b/src/script/command.zig @@ -384,6 +384,14 @@ test "parse: /click rejects positional (zero required fields)" { try testing.expectString("Login", cmd.tool_call.args.?.object.get("selector").?.string); } +test "parse: /extract rejects positional after key=value" { + var arena: std.heap.ArenaAllocator = .init(testing.allocator); + defer arena.deinit(); + try testing.expectError(error.PositionalMustComeFirst, Command.parse(arena.allocator(), "/extract save=front '{\"karma\":\"#karma\"}'")); + const cmd = try Command.parse(arena.allocator(), "/extract '{\"karma\":\"#karma\"}' save=front"); + try testing.expectString("front", cmd.tool_call.args.?.object.get("save").?.string); +} + test "parse: /scroll y=200" { var arena: std.heap.ArenaAllocator = .init(testing.allocator); defer arena.deinit();