killeros 2.1.22 → 2.1.24

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/CHANGELOG.md CHANGED
@@ -4,6 +4,29 @@ All notable changes to KillerOS are documented here.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [2.1.24] - 2026-09-05
8
+
9
+ ### Added
10
+
11
+ - Appended the settled model display name to version 4 worked-for receipts for single-model TUI runs.
12
+
13
+ ### Changed
14
+
15
+ - Rendered settled `Done` receipts without the `✓` marker, keeping `■ Stopped`, `× Failed`, and version 1 history unchanged.
16
+ - Raised the locked Pi development packages and minimum supported Pi peer version to 0.85.0, including `@earendil-works/pi-server`.
17
+
18
+ ## [2.1.23] - 2026-09-04
19
+
20
+ ### Changed
21
+
22
+ - Simplified `/goal` to five forms with proof-by-prose and silent file proof, removing configured completion checks.
23
+
24
+ ### Fixed
25
+
26
+ - Kept the automatic 20-turn goal pause silent as documented.
27
+ - Stripped sentence punctuation from unquoted inferred goal paths and picked token units after rounding so 999.6 shows as 1k and 999999 as 1M.
28
+ - Increased change-receipt Git timeouts to five seconds and stopped timeout failures from showing warning notifications.
29
+
7
30
  ## [2.1.22] - 2026-09-03
8
31
 
9
32
  ### Added
package/README.md CHANGED
@@ -5,7 +5,7 @@ A TypeScript extension for the [Pi coding agent](https://github.com/earendil-wor
5
5
  ## What you get
6
6
 
7
7
  - A custom TUI: startup card with version, model, provider, working directory, and Git branch; a dark theme with coral accents; a multiline editor with slash-command completion; a footer that tracks model, context, and goal state; settled task receipts with duration and token usage.
8
- - `/goal`: set an objective and Pi keeps working toward it across turns, compaction, reloads, and branch navigation. New goals default to 20 turns. Named checks and turn limits control completion and unattended work.
8
+ - `/goal`: set an objective and Pi keeps working toward it across turns, compaction, reloads, and branch navigation. New goals pause after 20 turns; `/goal resume` grants another 20.
9
9
  - `/init`: generates a root `AGENTS.md` from repository evidence, preserving compatible existing rules.
10
10
  - `/variants`: pick a reasoning level supported by the active model.
11
11
  - `/codex-fast`: toggles the `priority` service tier on Codex requests.
@@ -19,7 +19,7 @@ A TypeScript extension for the [Pi coding agent](https://github.com/earendil-wor
19
19
  ## Requirements
20
20
 
21
21
  - Node.js 22.19.0+
22
- - Pi 0.84.3 or later within the 0.x release line
22
+ - Pi 0.85.0 or later within the 0.x release line
23
23
  - An interactive TUI session for the custom header, editor, footer, `question`, and `/init`
24
24
 
25
25
  ## Install
@@ -34,19 +34,17 @@ Or from GitHub:
34
34
  pi install git:github.com/KyrosHendrix/pi-KillerOS
35
35
  ```
36
36
 
37
- Pin a release by appending its tag, for example `@v2.1.22`. Add `-l` to install only for the current project. Restart Pi after installing.
37
+ Pin a release by appending its tag, for example `@v2.1.24`. Add `-l` to install only for the current project. Restart Pi after installing.
38
38
 
39
39
  ## Commands
40
40
 
41
41
  ```text
42
42
  /init Generate root AGENTS.md from repository evidence
43
- /goal Open status, or set with /goal <objective>
44
- /goal start [--check name] [--turns count] -- <objective>
45
- /goal check <name|clear> Set or clear a named completion check
46
- /goal checks List configured completion-check names
47
- /goal limit <count|clear> Set a limit or clear it for unlimited turns
48
- /goal history [count] Show transitions and blocker evidence on this branch
49
- /goal edit|pause|resume|clear
43
+ /goal View the current goal
44
+ /goal <objective> Set an objective
45
+ /goal pause Stop automatic continuation
46
+ /goal resume Resume automatic continuation
47
+ /goal clear Remove the current goal
50
48
  /variants Reasoning-level selector (/variants high sets directly)
51
49
  /codex-fast Toggle Codex fast mode
52
50
  /notification Configure the completion sound
@@ -60,7 +58,7 @@ Pin a release by appending its tag, for example `@v2.1.22`. Add `-l` to install
60
58
  | Mode | What works |
61
59
  | --- | --- |
62
60
  | TUI | Everything |
63
- | RPC | Goals, proactive compaction; no TUI components, `/goal edit`, `/init`, sounds, title indicator |
61
+ | RPC | Goals, proactive compaction; no TUI components, `/init`, sounds, title indicator |
64
62
  | Print/JSON | No interactive questions, `/goal`, `/init`, or proactive compaction |
65
63
 
66
64
  ## Configuration
@@ -79,22 +77,19 @@ The packaged `killeros` theme activates on TUI start. Compaction triggers by def
79
77
 
80
78
  `handoffMaxTokens` caps the `/handoff` summary output at 8192 tokens by default; raise it when long sessions truncate the summary.
81
79
 
82
- Trusted projects can define up to 32 named goal checks in `.pi/killeros-hooks.json`:
80
+ State proof in the objective so the agent can verify it with its normal tools:
83
81
 
84
- ```json
85
- {
86
- "goalChecks": {
87
- "quality": {
88
- "command": "npm run check && npm test",
89
- "timeoutMs": 300000
90
- }
91
- }
92
- }
82
+ ```text
83
+ /goal Reduce p95 checkout latency below 120 ms, verified by the checkout benchmark, while keeping the correctness suite green
93
84
  ```
94
85
 
95
- Use `/goal checks` to list configured names. Use `/goal start -- <objective>` when an objective begins with `start`, `check`, `checks`, `limit`, `history`, `clear`, `edit`, `pause`, or `resume`. Bind a check with `/goal start --check quality -- <objective>` or `/goal check quality`. KillerOS stores the check name and definition hash, not the command. If the project changes the command or timeout, bind the check again before completing the goal.
86
+ A direct quoted file target binds silent file proof:
87
+
88
+ ```text
89
+ /goal Fix `killeros/footer.ts`, verified by npm test
90
+ ```
96
91
 
97
- New goals use a 20-turn limit unless `/goal start --turns <count> -- <objective>` supplies another value. Use `/goal limit <count>` to change the current goal or `/goal limit clear` to allow unlimited turns. Restored goals keep their persisted limit.
92
+ KillerOS captures the file baseline at goal start and only completes when the file is created or changed. New goals pause after 20 turns without warning. An explicit `/goal resume` on an exhausted goal grants another 20 turns; compaction recovery never grants turns. Restored goals keep their persisted limit.
98
93
 
99
94
  Completion sounds are off by default; change with `/notification` in TUI mode. The tab-title indicator requires a Nerd Font.
100
95
 
@@ -5,7 +5,7 @@ import path from "node:path";
5
5
  import { promisify } from "node:util";
6
6
  import { inflate } from "node:zlib";
7
7
 
8
- const GIT_TIMEOUT_MS = 1_000;
8
+ const GIT_TIMEOUT_MS = 5_000;
9
9
  const GIT_OUTPUT_LIMIT = 16 * 1024 * 1024;
10
10
  const SNAPSHOT_CONTENT_LIMIT = 128 * 1024 * 1024;
11
11
  const MAX_DIFF_OPERATIONS = 500_000;
@@ -70,7 +70,7 @@ const BUILTIN_COMMANDS: ReadonlyArray<{ name: string; description: string }> = [
70
70
  ];
71
71
 
72
72
  const COMMAND_SYNTAX_HINTS: Readonly<Record<string, string>> = {
73
- goal: "/goal [objective|start|check|checks|limit|history|clear|edit|pause|resume]",
73
+ goal: "/goal [objective|pause|resume|clear]",
74
74
  handoff: "/handoff [next-session focus]",
75
75
  variants: "/variants [level]",
76
76
  model: "/model [provider/model]",
@@ -40,12 +40,21 @@ export function formatTime(milliseconds: number): string {
40
40
 
41
41
  export function formatTokens(value: number): string {
42
42
  if (!Number.isFinite(value)) return "0";
43
- const amount = Math.max(0, value);
44
- if (amount < 1_000) return `${Math.round(amount)}`;
45
- if (amount >= 1_000_000) {
46
- const precision = amount >= 10_000_000 ? 0 : 1;
47
- return `${Number((amount / 1_000_000).toFixed(precision))}M`;
43
+ const rounded = Math.round(Math.max(0, value));
44
+ if (rounded < 1_000) return `${rounded}`;
45
+ if (rounded >= 1_000_000) {
46
+ const precision = rounded >= 10_000_000 ? 0 : 1;
47
+ return `${Number((rounded / 1_000_000).toFixed(precision))}M`;
48
48
  }
49
- const precision = amount >= 100_000 ? 0 : 1;
50
- return `${Number((amount / 1_000).toFixed(precision))}k`;
49
+ const precision = rounded >= 100_000 ? 0 : 1;
50
+ const inK = Number((rounded / 1_000).toFixed(precision));
51
+ if (inK >= 1_000) return `${Number((rounded / 1_000_000).toFixed(1))}M`;
52
+ return `${inK}k`;
53
+ }
54
+
55
+ /** Resolves a terminal-safe model display name, preferring the name over the id. */
56
+ export function modelDisplayName(model: { name?: string; id?: string }): string {
57
+ const name = safeTerminalText(model.name ?? "").replaceAll("\n", "").trim();
58
+ if (name) return name;
59
+ return safeTerminalText(model.id ?? "").replaceAll("\n", "").trim();
51
60
  }
@@ -3,7 +3,7 @@ import { watch } from "node:fs";
3
3
  import { type ExtensionAPI, type ExtensionContext, type Theme, type ThemeColor } from "@earendil-works/pi-coding-agent";
4
4
  import { truncateToWidth, visibleWidth, type TUI } from "@earendil-works/pi-tui";
5
5
  import { isCodexFastEnabled, subscribeCodexFast } from "./codex-fast-state.ts";
6
- import { formatCwd, formatTime, formatTokens, padRight } from "./display.ts";
6
+ import { formatCwd, formatTime, formatTokens, modelDisplayName, padRight } from "./display.ts";
7
7
  import { goalElapsedMilliseconds } from "./goal-state.ts";
8
8
  import type { GoalRuntime, GoalState } from "./runtime.ts";
9
9
  import { safeTerminalText } from "./safe-terminal-text.ts";
@@ -296,11 +296,6 @@ function formatProviderName(provider: string): string {
296
296
  .join(" ") || "Unknown provider";
297
297
  }
298
298
 
299
- function modelDisplayName(model: NonNullable<ExtensionContext["model"]>): string {
300
- const name = safeTerminalText(model.name ?? "").replaceAll("\n", "").trim();
301
- return name || safeTerminalText(model.id).replaceAll("\n", "").trim() || "Unknown model";
302
- }
303
-
304
299
  export function formatModel(
305
300
  model: ExtensionContext["model"],
306
301
  theme: Theme,
@@ -308,7 +303,7 @@ export function formatModel(
308
303
  showCodexFast = false,
309
304
  ): string {
310
305
  if (!model) return theme.fg("dim", "No model");
311
- const name = theme.fg("text", theme.bold(modelDisplayName(model)));
306
+ const name = theme.fg("text", theme.bold(modelDisplayName(model) || "Unknown model"));
312
307
  const fast = showCodexFast && model.provider === CODEX_PROVIDER
313
308
  ? theme.fg("accent", theme.bold("Fast"))
314
309
  : "";
@@ -1,120 +1,23 @@
1
- import { parseArgs } from "node:util";
2
- import { GOAL_CHECK_NAME_PATTERN, GOAL_MAX_TURNS, validateGoalObjective } from "./goal-state.ts";
3
-
4
- const GOAL_START_USAGE = "Usage: /goal start [--check <name>] [--turns <count>] -- <objective>";
5
- const GOAL_CHECK_USAGE = "Usage: /goal check <name|clear>";
6
- const GOAL_CHECKS_USAGE = "Usage: /goal checks";
7
- const GOAL_LIMIT_USAGE = "Usage: /goal limit <count|clear>";
8
- const GOAL_HISTORY_USAGE = "Usage: /goal history [count]";
9
- const RESERVED_OBJECTIVE_USAGE = "Objective begins with a reserved goal command. Use /goal start -- <objective>.";
10
-
11
- interface ControlledGoalStart {
12
- objective: string;
13
- completionCheckName?: string;
14
- maxTurns?: number;
15
- }
1
+ import { validateGoalObjective } from "./goal-state.ts";
16
2
 
17
3
  export type GoalCommand =
18
4
  | { kind: "status" }
19
5
  | { kind: "objective"; objective: string }
20
- | { kind: "start"; objective: string; completionCheckName?: string; maxTurns?: number }
21
- | { kind: "check"; value: { kind: "clear" } | { kind: "named"; name: string } }
22
- | { kind: "checks" }
23
- | { kind: "limit"; value: { kind: "clear" } | { kind: "count"; count: number } }
24
- | { kind: "history"; count: number }
25
- | { kind: "clear" }
26
- | { kind: "edit" }
27
6
  | { kind: "pause" }
28
7
  | { kind: "resume" }
8
+ | { kind: "clear" }
29
9
  | { kind: "invalid"; message: string };
30
10
 
31
- const RESERVED_GOAL_WORDS = ["start", "check", "checks", "limit", "history", "clear", "edit", "pause", "resume"] as const;
32
-
33
- function parseBoundedInteger(value: string, maximum: number): number | undefined {
34
- if (!/^[1-9][0-9]*$/u.test(value)) return undefined;
35
- const parsed = Number(value);
36
- return Number.isSafeInteger(parsed) && parsed <= maximum ? parsed : undefined;
37
- }
38
-
39
- function parseControlledGoalStart(input: string): ControlledGoalStart | undefined {
40
- const separator = input.startsWith("-- ") ? 0 : input.indexOf(" -- ");
41
- if (separator < 0) return undefined;
42
- const optionText = input.slice(0, separator).trim();
43
- const objective = validateGoalObjective(input.slice(separator + (separator === 0 ? 3 : 4)));
44
- if (!objective) return undefined;
45
- const tokens = optionText ? optionText.split(/\s+/u) : [];
46
- if (tokens.filter((token) => token === "--check" || token.startsWith("--check=")).length > 1
47
- || tokens.filter((token) => token === "--turns" || token.startsWith("--turns=")).length > 1) return undefined;
48
- try {
49
- const parsed = parseArgs({
50
- args: tokens,
51
- options: { check: { type: "string" }, turns: { type: "string" } },
52
- strict: true,
53
- allowPositionals: false,
54
- });
55
- const check = parsed.values.check;
56
- const turns = parsed.values.turns;
57
- if (check !== undefined && !GOAL_CHECK_NAME_PATTERN.test(check)) return undefined;
58
- const maxTurns = turns === undefined ? undefined : parseBoundedInteger(turns, GOAL_MAX_TURNS);
59
- if (turns !== undefined && maxTurns === undefined) return undefined;
60
- return {
61
- objective,
62
- ...(check === undefined ? {} : { completionCheckName: check }),
63
- ...(maxTurns === undefined ? {} : { maxTurns }),
64
- };
65
- } catch {
66
- return undefined;
67
- }
68
- }
69
-
70
11
  /** Parses one raw /goal argument string into a closed command variant. */
71
12
  export function parseGoalCommand(args: string): GoalCommand {
72
13
  const input = args.trim();
73
14
  if (!input) return { kind: "status" };
74
- const parts = input.split(/\s+/u);
75
- const rawFirstWord = parts[0] ?? "";
76
- const firstWord = RESERVED_GOAL_WORDS.find((word) => word === rawFirstWord.toLowerCase());
77
- if (!firstWord) {
78
- const objective = validateGoalObjective(input);
79
- return objective
80
- ? { kind: "objective", objective }
81
- : { kind: "invalid", message: "A goal objective may not exceed 4,000 characters" };
82
- }
83
-
84
- if (firstWord === "start") {
85
- const start = parseControlledGoalStart(input.slice(rawFirstWord.length).trimStart());
86
- if (start) return { kind: "start", ...start };
87
- return { kind: "invalid", message: rawFirstWord === firstWord ? GOAL_START_USAGE : RESERVED_OBJECTIVE_USAGE };
88
- }
89
- if (firstWord === "check") {
90
- const value = parts[1];
91
- if (parts.length === 2 && value?.toLowerCase() === "clear") return { kind: "check", value: { kind: "clear" } };
92
- if (parts.length === 2 && value && GOAL_CHECK_NAME_PATTERN.test(value)) return { kind: "check", value: { kind: "named", name: value } };
93
- return { kind: "invalid", message: rawFirstWord !== firstWord && parts.length > 2 ? RESERVED_OBJECTIVE_USAGE : GOAL_CHECK_USAGE };
94
- }
95
- if (firstWord === "checks") {
96
- return parts.length === 1
97
- ? { kind: "checks" }
98
- : { kind: "invalid", message: rawFirstWord === firstWord ? GOAL_CHECKS_USAGE : RESERVED_OBJECTIVE_USAGE };
99
- }
100
- if (firstWord === "limit") {
101
- const value = parts[1];
102
- if (parts.length === 2 && value?.toLowerCase() === "clear") return { kind: "limit", value: { kind: "clear" } };
103
- const count = parts.length === 2 && value ? parseBoundedInteger(value, GOAL_MAX_TURNS) : undefined;
104
- if (count !== undefined) return { kind: "limit", value: { kind: "count", count } };
105
- return { kind: "invalid", message: rawFirstWord !== firstWord && parts.length > 2 ? RESERVED_OBJECTIVE_USAGE : GOAL_LIMIT_USAGE };
106
- }
107
- if (firstWord === "history") {
108
- if (parts.length === 1) return { kind: "history", count: 20 };
109
- const count = parts.length === 2 && parts[1] ? parseBoundedInteger(parts[1], 50) : undefined;
110
- if (count !== undefined) return { kind: "history", count };
111
- return { kind: "invalid", message: rawFirstWord !== firstWord && parts.length > 2 ? RESERVED_OBJECTIVE_USAGE : GOAL_HISTORY_USAGE };
112
- }
113
- if (parts.length === 1) return { kind: firstWord };
114
- return {
115
- kind: "invalid",
116
- message: rawFirstWord === firstWord
117
- ? `Usage: /goal ${firstWord}`
118
- : RESERVED_OBJECTIVE_USAGE,
119
- };
15
+ const lowered = input.toLowerCase();
16
+ if (lowered === "pause") return { kind: "pause" };
17
+ if (lowered === "resume") return { kind: "resume" };
18
+ if (lowered === "clear") return { kind: "clear" };
19
+ const objective = validateGoalObjective(input);
20
+ return objective
21
+ ? { kind: "objective", objective }
22
+ : { kind: "invalid", message: "A goal objective may not exceed 4,000 characters" };
120
23
  }