kronk-cli 0.6.1 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -450,6 +450,88 @@ The profile it writes is the one documented under
450
450
  [Tip: use an `/AGENT` profile](#tip-use-an-agent-profile) — `context-window`, `nseq-max`,
451
451
  `preserve_thinking` and `max_tokens`, and deliberately no sampling parameters.
452
452
 
453
+ ### `kronk-cli rescue`
454
+
455
+ Everywhere else in this document, `kronk-cli` is something *you* talk to. `rescue` is the other
456
+ direction: a job queue so anything that can spawn a process — a script, another CLI, anything
457
+ external that wants to hand off a token-heavy job without sitting on it — can submit a
458
+ self-contained task to the local model, go do something else, and come back for the answer.
459
+
460
+ ```console
461
+ $ kronk-cli rescue "trace how the SSE parser handles multi-line data fields" --role explore
462
+ 9k2m1x-4d2-a1b2c3
463
+
464
+ $ kronk-cli rescue status 9k2m1x-4d2-a1b2c3
465
+ running (step 4)
466
+
467
+ $ kronk-cli rescue wait 9k2m1x-4d2-a1b2c3
468
+ Multi-line `data:` fields are joined with `\n` in src/sse.js:41-53 before the
469
+ frame is handed to the caller — see accumulateToolCalls's buffer.
470
+ $ echo $?
471
+ 0
472
+ ```
473
+
474
+ It is the same delegation `kronk-cli` already does internally — one self-contained task, its own
475
+ context, a report that stands alone — with the caller moved outside the process instead of inside
476
+ it. See [Sub-agents](#sub-agents) for the contract a rescue job runs under; it is the identical
477
+ one.
478
+
479
+ | Command | |
480
+ |---|---|
481
+ | `kronk-cli rescue "<prompt>" [--role explore\|code] [--model <id>] [-y\|-a] [--steps <n>] [--json]` | Submit. Runs detached; prints a job id and exits immediately — nothing here blocks |
482
+ | `kronk-cli rescue status <id> [--json]` | A snapshot: `queued`, `running (step n)`, `done`, `stopped`, `interrupted` or `failed` |
483
+ | `kronk-cli rescue wait <id> [--json]` | Block until the job reaches one of those last four states, then print its report |
484
+ | `kronk-cli rescue result <id> [--json]` | Print the report. Refuses (exit `4`) with the current state if the job has not finished yet — use `wait` instead of polling `result` in a loop |
485
+
486
+ **`--role`** is the same split `kronk-cli` gives its own sub-agents: `explore` can only read —
487
+ `read_file`, `list_dir`, `search` — and `code` can also write files and run commands. Default is
488
+ `explore`, the read-only one. **`--model`** is resolved exactly as `-m` resolves it everywhere
489
+ else in this program — exact id, else the closest substring, preferring a served `/AGENT`
490
+ profile — against what Kronk is serving *right now*; a name that matches nothing is refused at
491
+ submit time rather than silently falling back, because a caller that asked for a specific model
492
+ should hear about a bad name immediately, on the process it is still attached to, not from a job
493
+ that failed twenty minutes later. Leaving `--model` out defers the choice to the job itself, the
494
+ same default the session would otherwise use.
495
+
496
+ **Without `-y`/`-a`, a tool that needs approval is refused and reported, never auto-allowed.**
497
+ Nobody is at the prompt a rescue job would otherwise show, so there is no "ask anyway" — a refused
498
+ tool call comes back in the transcript exactly like a human said no, and the job carries on (or
499
+ does not) from there. This is deliberate and does not have a flag to turn it off: delegating a
500
+ job from outside the process must not become a way past the approval prompts that delegating from
501
+ inside it already cannot get past.
502
+
503
+ **One at a time.** Kronk holds one resident copy of the model, so a second `rescue` submitted
504
+ while one is running does not run alongside it — its `status` reports `queued` until the first
505
+ job finishes, exactly as the module comment on delegation in `src/subagent.js` already argues for
506
+ sub-agents. A runner that dies mid-job (killed, the machine restarting) does not wedge the queue
507
+ behind it: the next job to check finds the lock's process gone, marks the abandoned job
508
+ `interrupted`, and takes its turn.
509
+
510
+ **`--json`** on any of the four commands prints one object instead of prose:
511
+
512
+ ```json
513
+ { "id": "9k2m1x-4d2-a1b2c3", "state": "done", "role": "explore",
514
+ "model": "unsloth/Qwen3.6-35B-A3B-UD-Q4_K_M/AGENT", "steps": 3, "ms": 11421,
515
+ "report": "Multi-line `data:` fields are joined with…", "transcript": [ /* the full run */ ] }
516
+ ```
517
+
518
+ `wait`'s exit code is the one place a script gets the state without parsing anything:
519
+
520
+ | State | Meaning | `wait` exit code |
521
+ |---|---|---|
522
+ | `done` | Finished on its own terms | `0` |
523
+ | `stopped` | Hit its step cap before finishing | `1` |
524
+ | `interrupted` | Killed, or recovered from a runner that died mid-job | `2` |
525
+ | `failed` | The model call itself errored | `3` |
526
+
527
+ **Records outlive the process that submitted them, on purpose.** They are not sub-agent
528
+ transcripts (`src/tasklog.js`) and do not live in the same place or on the same clock — that
529
+ directory is swept on this session's own schedule specifically because nothing outside the
530
+ process is meant to still want it once the session ends, and a rescue job's whole point is that
531
+ something outside the process still does. A rescue record is kept indefinitely until `wait` or
532
+ `result` actually delivers it, and only then aged out, 24 hours later — `KRONK_RESCUE_DIR`
533
+ overrides where they live, same idea as `KRONK_TASK_LOG_DIR`.
534
+
453
535
  ---
454
536
 
455
537
  ## Startup: the model is loaded before you type
@@ -530,6 +612,70 @@ run (`kronk-cli "prompt"`) prints no banner and prints nothing here either.
530
612
 
531
613
  ---
532
614
 
615
+ ## Checking for updates
616
+
617
+ `kronk-cli --version` prints what is actually running:
618
+
619
+ ```console
620
+ $ kronk-cli --version
621
+ kronk-cli 0.7.0
622
+ kronk 1.32.7
623
+ ```
624
+
625
+ The second line is `kronk --version`, read through the same `runKronk` helper `setup` uses,
626
+ capped at a few seconds so a stuck binary cannot hang the command. It is omitted, not guessed
627
+ at, when `kronk` is not on `PATH` or does not answer in time — this has to work with **no server
628
+ running**, which is the ordinary case for asking a program what it is.
629
+
630
+ The startup banner adds a third line when a newer `kronk-cli` exists:
631
+
632
+ ```console
633
+ ██ kronk-cli · local agent, no network
634
+ model unsloth/Qwen3.6-35B-A3B-UD-Q4_K_M/AGENT
635
+ server http://localhost:11435/v1
636
+ update kronk-cli 0.7.0 → 0.8.1 · run: brew upgrade kronk-cli
637
+ /help for commands · Ctrl-C to interrupt · /exit to quit
638
+ ```
639
+
640
+ **It asks the package manager, never the network directly.** No `fetch`, no URL literal — the
641
+ version comes from whatever tool installed this copy, detected from the path `src/index.js`
642
+ itself resolved to:
643
+
644
+ | Route | How it's detected | What answers |
645
+ |---|---|---|
646
+ | Homebrew | a `Cellar/kronk-cli/` path | `brew outdated --json` |
647
+ | npm global install | a `node_modules/kronk-cli` path | `npm view kronk-cli version` |
648
+ | git checkout | a `.git` above the file | `git fetch --tags` + the newest tag |
649
+ | `packaging/install.sh` | a `lib/kronk-cli` path | nothing — see below |
650
+
651
+ The hint always matches the route: a Homebrew install is told `brew upgrade kronk-cli`, an npm
652
+ one `npm install -g kronk-cli@latest`, a checkout `git pull`. Suggesting the wrong one would send
653
+ someone to a manager that has never heard of this copy, which is worse than saying nothing.
654
+
655
+ The git checkout route is the one that makes a real network call, through `git fetch --tags`
656
+ rather than any request this program issues itself, and it honours the same off-switch as
657
+ everything else here.
658
+
659
+ **The install script has nothing to ask.** It unpacks a tarball into `$PREFIX/lib/kronk-cli` and,
660
+ unlike the other three routes, there is no registry behind it that knows the latest release. It
661
+ writes a small receipt (`$PREFIX/lib/kronk-cli/.install-receipt.json`, version and source) at
662
+ install time so this route is not silent forever; when that receipt is present the banner shows
663
+ a plain nudge — `update kronk-cli 0.7.0 · re-run the install script` — with no version
664
+ comparison, because there is genuinely nothing to compare against. A copy installed before the
665
+ receipt existed, or one someone assembled by hand, shows no update line at all, which is the
666
+ honest answer for a route with no manager behind it.
667
+
668
+ **Never on the path that slows startup down.** The check result is cached to disk with a
669
+ timestamp and reused for up to a day; a stale or missing cache still renders instantly (from
670
+ whatever was cached last, or nothing) while a fresh check for the *next* session runs
671
+ unawaited in the background. Nothing about this can delay, block, or fail the REPL — a package
672
+ manager that hangs, errors, or prints garbage simply produces no line.
673
+
674
+ `KRONK_UPDATE_CHECK=false` (or `"updateCheck": false` in the config file) turns all of this off:
675
+ no cache read, no subprocess, ever.
676
+
677
+ ---
678
+
533
679
  ## Performance
534
680
 
535
681
  The transcripts above are real sessions, kept to show what the UI looks like — not a benchmark
@@ -598,6 +744,7 @@ same way this run did.
598
744
  | `--no-subagents` | off | Remove the `task` tool, so the model cannot delegate — see [Sub-agents](#sub-agents) |
599
745
  | `--steps <n>` | unlimited | Cap tool calls per task. `0`, `off`, `none`, `inf`, `unlimited` all mean no cap |
600
746
  | `-h`, `--help` | — | Print all options and exit |
747
+ | `-v`, `--version` | — | Print the kronk-cli version, and Kronk's if it can be had without a network call, then exit |
601
748
  | `--` | — | End option parsing; everything after it is prompt text, dashes and all |
602
749
 
603
750
  Anything not consumed as an option becomes the prompt, except a token that looks like an
@@ -680,7 +827,7 @@ Two consequences worth knowing:
680
827
  | `/steps [n\|off]` | show or set the tool-call cap |
681
828
  | `/thinking` | show or hide the model's reasoning |
682
829
  | `/think` | turn reasoning off entirely — much faster |
683
- | `/agents` | the sub-agents `task` can delegate to, and what each may touch |
830
+ | `/agents` | the sub-agents `task` can delegate to, what each may touch, and the default model |
684
831
  | `/tasks [n]` | sub-agent runs this session; with `n`, that one in full |
685
832
  | `/mcp` | list attached MCP servers and their tools |
686
833
  | `/context` | how much of the context window is used |
@@ -713,14 +860,16 @@ Two consequences worth knowing:
713
860
  | `KRONK_DISTILL` | `true` | `false` disables tool-output distillation |
714
861
  | `KRONK_DISTILL_AT` | `8000` | Characters of output that trigger distillation |
715
862
  | `KRONK_SUBAGENTS` | `true` | `false` removes the `task` tool |
716
- | `KRONK_SUBAGENT_MODEL` | the main model | Model sub-agents run on — substring match, same as `KRONK_MODEL` |
863
+ | `KRONK_SUBAGENT_MODEL` | the main model | Default model for a task that names none itself — substring match, same as `KRONK_MODEL`. A task can pick its own from what Kronk is serving, see [Picking a model per task](#picking-a-model-per-task) |
717
864
  | `KRONK_SUBAGENT_STEPS` | `40` | Tool-call cap for one delegated task |
718
865
  | `KRONK_TASK_LOG` | `true` | `false` stops writing sub-agent transcripts, and `/tasks` says so |
719
866
  | `KRONK_TASK_LOG_DIR` | the temp dir | Where transcripts are written |
867
+ | `KRONK_RESCUE_DIR` | the temp dir | Where `kronk-cli rescue` keeps its job records — see [`kronk-cli rescue`](#kronk-cli-rescue) |
720
868
  | `KRONK_WARM` | `true` | `false` skips the boot-time model preload |
721
869
  | `KRONK_AUTO_COMPACT` | `true` | `false` disables automatic compaction |
722
870
  | `KRONK_COMPACT_AT` | `0.85` | Fraction of the window that triggers compaction |
723
871
  | `KRONK_THEME` | `auto` | `dark` or `light` pins the palette; `auto` reads the terminal's background |
872
+ | `KRONK_UPDATE_CHECK` | `true` | `false` never asks the package manager whether a newer kronk-cli exists — no subprocess, ever |
724
873
  | `NO_COLOR` | — | Any value disables colour |
725
874
  | `FORCE_COLOR` | — | `0` disables colour; any other value keeps it on through a pipe |
726
875
 
@@ -908,9 +1057,10 @@ Kronk reports the **effective** window for whichever model id you selected — t
908
1057
  Every limit is read from the selected model's profile, and re-read when the selection changes:
909
1058
  the window, the output cap (`sampling-parameters.max_tokens`), whether the chat template
910
1059
  understands `preserve_thinking`, and the model's native maximum. `/model` mid-session moves all
911
- of them, and a sub-agent running on `KRONK_SUBAGENT_MODEL` compacts against *its* window rather
912
- than the main model's. The one number that is not taken from the profile is a `KRONK_MAX_TOKENS`
913
- you set yourself — yours always wins.
1060
+ of them, and a sub-agent — whether it is running on `KRONK_SUBAGENT_MODEL` or on a model a task
1061
+ named for itself, see [Picking a model per task](#picking-a-model-per-task) — compacts against
1062
+ *its own* window rather than the main model's. The one number that is not taken from the profile
1063
+ is a `KRONK_MAX_TOKENS` you set yourself — yours always wins.
914
1064
 
915
1065
  Three places surface it:
916
1066
 
@@ -1238,30 +1388,53 @@ The model can hand one job to a **sub-agent**: a fresh agent with its own contex
1238
1388
  own tool loop, and no memory of your conversation. It runs the task, and the only thing that
1239
1389
  ever comes back is its report.
1240
1390
 
1391
+ While it runs, it draws in a box below the prompt that redraws in place:
1392
+
1241
1393
  ```console
1242
1394
  › which tools in this repo need approval, and where is that decided?
1243
1395
 
1244
1396
  1 ⚙ task explore: list every tool definition in src/, say which are gated and where
1245
- ┌ explore · list every tool definition in src/, say which are gated…
1246
- │ 1/40 ⚙ search /def\(/ in src
1247
- │ ✓ 9 lines
1248
- │ 2/40 ⚙ read src/tools.js
1249
- │ ✓ 587 lines
1250
- │ 3/40 ⚙ read src/subagent.js
1251
- │ ✓ 211 lines
1252
- │ 2104→380 tok · 58.1 tok/s · 1980 cached
1397
+ ┌─ explore ──────────────────────────────────────── 6/40 · 48s ─┐
1398
+ │ 4/40 ⚙ read src/tools.js │
1399
+ │ ✓ 587 lines │
1400
+ │ 5/40 ⚙ search /NEEDS_APPROVAL/ in src │
1401
+ │ ✓ 3 lines │
1402
+ │ 6/40 ⚙ read src/subagent.js │
1403
+ └─ esc to collapse · ctrl-c to stop ────────────────────────────┘
1404
+ ```
1405
+
1406
+ When it finishes the box is erased, and what is left in the scrollback is the answer and one
1407
+ line saying what the run cost:
1408
+
1409
+ ```console
1410
+ 1 ⚙ task explore: list every tool definition in src/, say which are gated and where
1253
1411
  Six built-in tools. write_file and bash are gated, by NEEDS_APPROVAL at
1254
1412
  src/tools.js:261. MCP tools are gated by name, mcpNeedsApproval at :270.
1255
1413
  read_file, list_dir, search, set_plan and task are not gated.
1256
- └ 3 steps · 11.4s · /tmp/kronk-cli/mfa1k2-8817/task-001.json
1414
+ └ 3 steps · 11.4s · /tasks 1 for the transcript
1257
1415
  ✓ 3 lines
1258
1416
 
1259
1417
  `write_file` and `bash`, from the `NEEDS_APPROVAL` set in src/tools.js:246…
1260
1418
  ```
1261
1419
 
1262
- `│` marks the sub-agent's own loop; the unprefixed lines are its report, which is what the main
1263
- agent receives as the tool result. `┌` and `└` fence one run, so two in a row are told apart in
1264
- scrollback, and the closing line says what it cost and where the rest of it went.
1420
+ The box is the run's only appearance on screen, and that is deliberate. A nine-step survey
1421
+ printed into the conversation leaves forty lines of someone else's work between the question
1422
+ you asked and the answer you got; scroll back ten minutes later and the conversation is mostly
1423
+ sub-agent. While the run is going a live view is the only sign the thing is alive, and it is
1424
+ where you notice one thrashing — but once it has ended, almost none of it is worth keeping.
1425
+
1426
+ Nothing is lost by erasing it. Everything the box shows is written to the run's transcript as
1427
+ it happens, so the box is a view of a file that outlives it, and `/tasks 1` below is how you
1428
+ read it back. Three keys and one fallback are worth knowing:
1429
+
1430
+ - **`esc` collapses the box immediately** and the run carries on. For when you have seen enough
1431
+ and want your screen back.
1432
+ - **`ctrl-c`** stops the run, as it stops anything else.
1433
+ - **An approval prompt wipes the box first.** A `code` sub-agent's `write_file` and `bash` stop
1434
+ for the same question the main agent's do, and that question gets the terminal to itself.
1435
+ - **On a pipe or a dumb terminal there is no box at all.** `kronk-cli "…" | grep`, a CI job and
1436
+ an `--auto` run nobody is watching each get the plain nested lines instead, in order, with no
1437
+ cursor control in them.
1265
1438
 
1266
1439
  Three files were read. None of them is in your conversation: the 800-odd lines they cost were
1267
1440
  spent in a context that no longer exists, and all that is left in your window is the four-line
@@ -1273,10 +1446,10 @@ already leaving it.
1273
1446
 
1274
1447
  ### Reading a run back
1275
1448
 
1276
- The report on screen is dimmed and capped at fourteen lines. That used to be the end of it: what
1277
- the sub-agent read, what it ran, and any line fifteen of its report went out of scope with its
1278
- context. Delegating is supposed to cost the *conversation* nothing — it was never supposed to
1279
- cost you the ability to check.
1449
+ The report on screen is dimmed and capped at fourteen lines, and the box it ran in is gone. That
1450
+ used to be the end of it: what the sub-agent read, what it ran, and any line fifteen of its
1451
+ report went out of scope with its context. Delegating is supposed to cost the *conversation*
1452
+ nothing — it was never supposed to cost you the ability to check.
1280
1453
 
1281
1454
  So each run is written to a file as it goes, and `/tasks` reads them back.
1282
1455
 
@@ -1402,6 +1575,38 @@ the delegation *and* the work.
1402
1575
  > works — *"use a sub-agent to survey the test suite first"* — and is usually how you will
1403
1576
  > drive this.
1404
1577
 
1578
+ ### Picking a model per task
1579
+
1580
+ `KRONK_SUBAGENT_MODEL` is a session-wide setting — every delegated task ran on it, or on the main
1581
+ model, and changing your mind meant leaving the session. But which model suits a task is a
1582
+ property of the *task*, not of the session: a survey that reads thirty files and answers one
1583
+ question wants the small fast model; reproducing a build failure and fixing it wants the big one.
1584
+
1585
+ So the `task` tool takes an optional `model`, and the delegating model picks it from what Kronk is
1586
+ actually serving — every chat model it has, with a bare id dropped when its own `/AGENT` profile
1587
+ is served beside it, offered as a fixed list rather than free
1588
+ text, so it can never ask for a model that does not exist. Leaving it out is unchanged:
1589
+ `KRONK_SUBAGENT_MODEL` if one is set, otherwise the main model.
1590
+
1591
+ ```console
1592
+ 1 ⚙ task explore: survey every call site of parseLimits · Qwen3.6-4B/AGENT
1593
+ ┌─ explore · Qwen3.6-4B/AGENT ──────────────────────────────── 3/40 · 9s ─┐
1594
+ ```
1595
+
1596
+ The model actually used is named on the opening line and in the box, because naming one that is
1597
+ not already resident is not free. Kronk holds one large model at a time, so a task that asks for
1598
+ a different one pays a real cold load — the same 10–30s spinner `kronk-cli` itself waits through
1599
+ at startup — and may evict the model the rest of the session is running on. `/tasks` records which
1600
+ model each run was on, so a delegation that took a minute longer than the others is explained by
1601
+ the list, not a mystery.
1602
+
1603
+ A task's own model gets *its* window and output cap, not the session's, read from that model's own
1604
+ limits the first time anything runs on it and kept for the rest of the session — the same
1605
+ argument [Context window](#context-window) already makes for `KRONK_SUBAGENT_MODEL`: a 32k
1606
+ sub-agent must not compact against the main model's 131k window. A model named that Kronk turns
1607
+ out not to be serving any more — unloaded mid-session, say — is not a failed delegation: the task
1608
+ still runs, on the same fallback as no `model` at all, with a note saying so in its report.
1609
+
1405
1610
  ## Autonomous mode
1406
1611
 
1407
1612
  `--auto`, or `/auto` in the REPL, swaps in a system prompt that tells the model to finish the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kronk-cli",
3
- "version": "0.6.1",
3
+ "version": "0.8.0",
4
4
  "description": "A Claude-Code-style terminal agent for local models served by Kronk.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
package/src/agent.js CHANGED
@@ -12,7 +12,7 @@ import { maybeDistill } from './distill.js';
12
12
  import {
13
13
  carryChecklist, clearPlan, openItems, outstandingLines, planLines, pushNudge,
14
14
  } from './plan.js';
15
- import { runTask, taskTools, TASK_TOOL } from './subagent.js';
15
+ import { runTask, taskTools } from './subagent.js';
16
16
 
17
17
  export const SYSTEM = `You are kronk-cli, a terse coding assistant running fully offline on the user's machine.
18
18
 
@@ -75,6 +75,9 @@ function endTurn(messages, { out = console.log, plan = true } = {}) {
75
75
  return messages;
76
76
  }
77
77
 
78
+ /** Stands in for `liveLine()` under a pane, which is drawing the same seconds itself. */
79
+ const QUIET_LIVE = { update() {}, done() {} };
80
+
78
81
  /**
79
82
  * Run one user turn to completion, looping while the model requests tools.
80
83
  *
@@ -89,6 +92,10 @@ function endTurn(messages, { out = console.log, plan = true } = {}) {
89
92
  export async function runTurn({
90
93
  messages, model, signal, approve, grant, mcp, auto = false, maxSteps = config.maxSteps,
91
94
  tools: toolset = TOOLS, plan: usePlan = true, out = console.log, stream = true, depth = 0,
95
+ // Print nothing that redraws the current line — the spinner and the live
96
+ // command line. Set by src/subagent.js when a pane is drawing the run, and
97
+ // the pane owns the bottom of the screen for as long as it is up.
98
+ quiet = false,
92
99
  // Called with the transcript so far after every round. Only src/subagent.js
93
100
  // passes one: a delegated run is the transcript nobody would otherwise keep,
94
101
  // and the round that ends it may be a throw or a step cap rather than a
@@ -125,7 +132,7 @@ export async function runTurn({
125
132
  : '(stopped: step cap reached)' });
126
133
  return endTurn(messages, { out, plan: usePlan });
127
134
  }
128
- let sp = spinner(depth ? 'sub-agent' : 'thinking');
135
+ let sp = quiet ? null : spinner(depth ? 'sub-agent' : 'thinking');
129
136
  let text = '';
130
137
  let reasoning = '';
131
138
  let calls = [];
@@ -278,7 +285,12 @@ export async function runTurn({
278
285
  // The same list the model was offered, not just the name it used: with
279
286
  // delegation off, or below the top level, a `task` call is a call to a
280
287
  // tool that does not exist and falls through to runTool saying so.
281
- const isTask = !isMcp && call.name === 'task' && tools.includes(TASK_TOOL);
288
+ //
289
+ // By name, not by identity — src/subagent.js rebuilds the task tool's
290
+ // definition object whenever the served-model list changes, so an
291
+ // object captured in `tools` at the top of this turn need not be the
292
+ // same reference `taskTools` would hand back if asked again now.
293
+ const isTask = !isMcp && call.name === 'task' && tools.some((t) => t.function.name === 'task');
282
294
  const label = isMcp
283
295
  ? `${c.magenta('⚙')} ${call.name} ${c.grey(JSON.stringify(args).slice(0, 120))}`
284
296
  : `${c.blue(`⚙ ${describe(call.name, args)}`)}`;
@@ -342,7 +354,7 @@ export async function runTurn({
342
354
  result = `error: the sub-agent failed — ${e.message}`;
343
355
  }
344
356
  } else {
345
- const live = liveLine();
357
+ const live = quiet ? QUIET_LIVE : liveLine();
346
358
  try {
347
359
  result = isMcp
348
360
  ? await mcp.call(call.name, args)
package/src/argv.js CHANGED
@@ -18,6 +18,7 @@
18
18
  */
19
19
  const SPECS = [
20
20
  { names: ['-h', '--help'], kind: 'flag', key: 'help' },
21
+ { names: ['-v', '--version'], kind: 'flag', key: 'version' },
21
22
  { names: ['-l', '--models', '--list'], kind: 'flag', key: 'models' },
22
23
  { names: ['--mcp-list'], kind: 'flag', key: 'mcpList' },
23
24
  { names: ['--no-context'], kind: 'flag', key: 'noContext' },
@@ -32,6 +33,12 @@ const SPECS = [
32
33
  { names: ['--steps'], kind: 'value', key: 'steps' },
33
34
  { names: ['--context'], kind: 'value', key: 'context' },
34
35
  { names: ['--mcp'], kind: 'optional', key: 'mcp' },
36
+ { names: ['--role'], kind: 'value', key: 'role' },
37
+ { names: ['--json'], kind: 'flag', key: 'json' },
38
+ // Internal: how `kronk-cli rescue` re-invokes itself as the detached
39
+ // worker for one job id (src/rescue.js). Not documented in --help — it is
40
+ // not a thing anyone types, only a thing the CLI passes to itself.
41
+ { names: ['--rescue-worker'], kind: 'value', key: 'rescueWorker' },
35
42
  ];
36
43
 
37
44
  const BY_NAME = new Map(SPECS.flatMap((s) => s.names.map((n) => [n, s])));
@@ -101,7 +108,7 @@ const unknown = (token) => {
101
108
  export function parseArgv(argv) {
102
109
  const out = {
103
110
  error: null,
104
- help: false, models: false, mcpList: false,
111
+ help: false, version: false, models: false, mcpList: false,
105
112
  noContext: false, noCompact: false, noWarm: false, noThink: false,
106
113
  noSubagents: false,
107
114
  auto: false, yes: false, dryRun: false,
@@ -109,6 +116,9 @@ export function parseArgv(argv) {
109
116
  context: null,
110
117
  steps: null,
111
118
  mcp: false, mcpNames: null,
119
+ role: null,
120
+ json: false,
121
+ rescueWorker: null,
112
122
  words: [],
113
123
  };
114
124
  const fail = (message) => ({ ...out, error: message });
package/src/boot.js CHANGED
@@ -2,10 +2,43 @@ import { config, DEFAULT_MODEL } from './config.js';
2
2
  import { listLoaded, warm } from './client.js';
3
3
  import { c, spinner } from './ui.js';
4
4
 
5
- /** Last resort when neither the flag nor DEFAULT_MODEL is being served. */
5
+ const PROFILE = '/AGENT';
6
+
7
+ /**
8
+ * Every model Kronk is serving that a delegated task may be offered: the chat
9
+ * models, minus the embedding and reranking ones, and minus a bare id whose
10
+ * own /AGENT profile is also being served — Kronk holds each profile as a
11
+ * separate resident copy of the same weights, so offering both would let a
12
+ * task load a second copy of a model that is already in the pool.
13
+ *
14
+ * Deliberately *not* `pickDefault`'s rule. That one collapses to "the /AGENT
15
+ * profiles, if there are any", which is right when choosing a single default
16
+ * — a tuned profile beats an untuned model — and wrong for building a menu,
17
+ * because it throws away every model that has no profile of its own. The
18
+ * small fast model a survey wants is exactly the one least likely to have had
19
+ * a profile written for it, so applying the default-picking rule here would
20
+ * drop the most useful entry in the list and leave a menu of nothing but
21
+ * large models — on a pool that holds one large model at a time, a menu where
22
+ * every choice costs an eviction.
23
+ */
24
+ export function chatModels(ids) {
25
+ const chat = ids.filter((id) => !/embedding|rerank/i.test(id));
26
+ const profiled = new Set(chat
27
+ .filter((id) => id.endsWith(PROFILE))
28
+ .map((id) => id.slice(0, -PROFILE.length)));
29
+ return chat.filter((id) => !profiled.has(id));
30
+ }
31
+
32
+ /**
33
+ * Last resort when neither the flag nor DEFAULT_MODEL is being served.
34
+ *
35
+ * Its own filter, unchanged: one model is being chosen on the user's behalf
36
+ * with nothing to go on, and there a tuned /AGENT profile wins over an
37
+ * untuned model outright rather than merely being listed beside it.
38
+ */
6
39
  export function pickDefault(ids) {
7
40
  const chat = ids.filter((id) => !/embedding|rerank/i.test(id));
8
- const agent = chat.filter((id) => id.endsWith('/AGENT'));
41
+ const agent = chat.filter((id) => id.endsWith(PROFILE));
9
42
  const pool = agent.length ? agent : chat;
10
43
  return pool.sort((a, b) => b.length - a.length)[0] ?? null;
11
44
  }
package/src/config.js CHANGED
@@ -77,6 +77,12 @@ export const config = {
77
77
  // one is not: nobody is watching a sub-agent, and its report is worth less
78
78
  // than the window it would spend earning it.
79
79
  subagents: (process.env.KRONK_SUBAGENTS ?? String(file.subagents ?? 'true')) !== 'false',
80
+ // The default model for a task that names none. A task's own `model`
81
+ // argument, when it has one, wins over this — see the `task` tool in
82
+ // src/subagent.js. Either way, the chosen model's window and output cap
83
+ // are resolved per model, not stored here: src/subagent.js keeps its own
84
+ // cache, because which limits apply now depends on the task, not the
85
+ // session.
80
86
  subagentModel: process.env.KRONK_SUBAGENT_MODEL ?? file.subagentModel ?? null,
81
87
  subagentSteps: Number(process.env.KRONK_SUBAGENT_STEPS ?? file.subagentSteps ?? 40),
82
88
  // Each sub-agent run is written to a file as it goes, so `/tasks` can show
@@ -86,9 +92,24 @@ export const config = {
86
92
  taskLog: (process.env.KRONK_TASK_LOG ?? String(file.taskLog ?? 'true')) !== 'false',
87
93
  // Same reason as modelConfigPath: so tests never go near the real one.
88
94
  taskLogDir: process.env.KRONK_TASK_LOG_DIR ?? file.taskLogDir ?? null,
95
+ // Where `kronk-cli rescue` keeps its job records — src/rescueStore.js.
96
+ // Deliberately not taskLogDir: that directory's lifetime is owned by
97
+ // src/tasklog.js's sweep() and cleanup(), and a rescue record has to
98
+ // survive both, so it lives somewhere neither of them has ever heard of.
99
+ rescueDir: process.env.KRONK_RESCUE_DIR ?? file.rescueDir ?? null,
89
100
  // Kronk's per-model runtime settings. `setup` is the only thing that writes
90
101
  // it; the override exists so tests never go near the real one.
91
102
  modelConfigPath: process.env.KRONK_MODEL_CONFIG ?? file.modelConfigPath ?? MODEL_CONFIG,
103
+ // The startup banner's "a newer kronk-cli exists" line asks the package
104
+ // manager that installed this copy, never the network directly — see
105
+ // src/update.js. Off entirely for anyone who wants a program that runs no
106
+ // subprocess it was not asked to run.
107
+ updateCheck: (process.env.KRONK_UPDATE_CHECK ?? String(file.updateCheck ?? 'true')) !== 'false',
108
+ // Same reason as modelConfigPath and taskLogDir: so tests never touch the
109
+ // real one, and a cold machine's first run does not have to guess where to
110
+ // put it.
111
+ updateCacheFile: process.env.KRONK_UPDATE_CACHE ?? file.updateCacheFile
112
+ ?? join(homedir(), '.kronk-cli-update-cache.json'),
92
113
  // Paths a previous session granted read-only to the sandbox, via `always` at
93
114
  // the credential prompt. Read-only is the whole point: KRONK_SANDBOX_ALLOW
94
115
  // grants writes as well, and these are credential stores.
@@ -101,9 +122,6 @@ export const config = {
101
122
  projectPrimer: null,
102
123
  contextWindow: null, // filled in at boot from the chosen model's profile
103
124
  nativeContext: null,
104
- // The sub-agent model's own limits, when one is configured. A sub-agent on
105
- // a 32k model must not compact against the main model's 131k window.
106
- subagentLimits: null,
107
125
  templatePreservesThinking: false, // filled in at boot from the model's template
108
126
  samplingOverride: null, // filled in at boot: params where the profile overrides the model's own
109
127
  rcPath: RC,