killeros 2.0.16 → 2.0.17

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,22 @@ All notable changes to KillerOS are documented here.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [2.0.17] - 2026-08-23
8
+
9
+ ### Added
10
+
11
+ - Added per-task token usage to settled TUI receipts for ordinary requests and individual goal turns.
12
+
13
+ ### Changed
14
+
15
+ - Limited push CI to `main` and `dev`; feature branches run through pull request CI without duplicate push runs.
16
+ - Removed obsolete Pi tool API casts, shared caught-error formatting, and expanded the non-repeating startup tip and editor suggestion banks.
17
+ - Clarified that the README's pinned Git tag is an example rather than the current package version.
18
+
19
+ ### Fixed
20
+
21
+ - Limited retired-feature repository checks to tracked and non-ignored files so ignored private notes cannot fail the suite.
22
+
7
23
  ## [2.0.16] - 2026-08-23
8
24
 
9
25
  ### Fixed
package/Killeros.ts CHANGED
@@ -48,10 +48,10 @@ export default function Killeros(pi: ExtensionAPI, options: KillerosOptions = {}
48
48
  registerCodexFastMode(pi);
49
49
  registerInitCommand(pi, initRuntime, goalRuntime);
50
50
  registerLifecycleHooks(pi);
51
+ registerWorkedFor(pi);
51
52
  const goalCompaction = registerGoalSettlement(pi, goalRuntime, initRuntime);
52
53
  registerAutoCompaction(pi, { goal: goalCompaction });
53
54
  registerInitSettlement(pi, initRuntime);
54
55
  registerRequestActivity(pi);
55
56
  registerCompletionNotifications(pi, options.completionNotifications);
56
- registerWorkedFor(pi);
57
57
  }
package/README.md CHANGED
@@ -4,7 +4,7 @@ A TypeScript extension for the [Pi coding agent](https://github.com/earendil-wor
4
4
 
5
5
  ## What you get
6
6
 
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.
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
8
  - `/goal`: set an objective and Pi keeps working toward it across turns, compaction, reloads, and branch navigation. Pause, resume, edit, or clear it anytime.
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.
@@ -34,7 +34,7 @@ Or from GitHub:
34
34
  pi install git:github.com/KyrosHendrix/pi-KillerOS
35
35
  ```
36
36
 
37
- Pin a version with `@v2.0.16`, add `-l` to install only for the current project. Restart Pi after installing.
37
+ Pin a release by appending its tag, for example `@v2.0.17`. Add `-l` to install only for the current project. Restart Pi after installing.
38
38
 
39
39
  ## Commands
40
40
 
@@ -6,6 +6,7 @@ import {
6
6
  type ExtensionAPI,
7
7
  type ExtensionContext,
8
8
  } from "@earendil-works/pi-coding-agent";
9
+ import { errorMessage } from "./errors.ts";
9
10
  import { createKillerosSettingsStore } from "./settings.ts";
10
11
 
11
12
  export const DEFAULT_AUTO_COMPACTION_PERCENT_REMAINING = 15;
@@ -71,6 +72,7 @@ function reserveTokens(settings: Pick<CompactionSettings, "reserveTokens">): num
71
72
  : 0;
72
73
  }
73
74
 
75
+ /** Triggers at the stricter of the user's percentage and Pi's token reserve. */
74
76
  export function shouldTriggerAutoCompaction(
75
77
  usage: Pick<ContextUsage, "tokens" | "contextWindow"> | undefined,
76
78
  preference: AutoCompactionPreference,
@@ -90,10 +92,6 @@ export function shouldTriggerAutoCompaction(
90
92
  return remainingTokens <= threshold;
91
93
  }
92
94
 
93
- function errorMessage(error: unknown): string {
94
- return error instanceof Error ? error.message : String(error);
95
- }
96
-
97
95
  function supportedMode(ctx: ExtensionContext): boolean {
98
96
  return ctx.mode === "tui" || ctx.mode === "rpc";
99
97
  }
@@ -1,6 +1,10 @@
1
1
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
 
3
+ /** Converts unknown caught values into text suitable for user-facing errors. */
4
+ export function errorMessage(error: unknown): string {
5
+ return error instanceof Error ? error.message : String(error);
6
+ }
7
+
3
8
  export function reportError(ctx: ExtensionContext, area: string, error: unknown): void {
4
- const message = error instanceof Error ? error.message : String(error);
5
- ctx.ui.notify(`${area}: ${message}`, "error");
9
+ ctx.ui.notify(`${area}: ${errorMessage(error)}`, "error");
6
10
  }
package/killeros/goals.ts CHANGED
@@ -5,7 +5,6 @@ import path from "node:path";
5
5
  import { type ExtensionAPI, type ExtensionCommandContext, type ExtensionContext, type ThemeColor } from "@earendil-works/pi-coding-agent";
6
6
  import { Text } from "@earendil-works/pi-tui";
7
7
  import { Type } from "typebox";
8
- import { MAX_NODE_TIMER_MS } from "./limits.ts";
9
8
  import { BoundedText } from "./bounded-text.ts";
10
9
  import { formatTime, formatTokens } from "./display.ts";
11
10
  import { reportError } from "./errors.ts";
@@ -131,6 +130,7 @@ function captureGoalFileBaseline(filePath: string): GoalFileBaseline {
131
130
  }
132
131
  }
133
132
 
133
+ /** Captures one explicit absolute output path so goal completion can verify its creation or modification. */
134
134
  function inferGoalVerification(objective: string): GoalFileVerification | undefined {
135
135
  const destination = /\b(?:create|write|save|generate)\b[^\r\n]{0,160}?\b(?:file|document|markdown|report|spreadsheet|presentation|image)\b\s+(?:to|at|as|destination(?:\s+is)?|output(?:\s+(?:to|at))?)\b\s*(?:`([^`\r\n]+)`|"([^"\r\n]+)"|'([^'\r\n]+)'|([A-Za-z]:\\[^\s,;]+|\/[^\s,;]+))/giu;
136
136
  const paths = [...objective.matchAll(destination)]
@@ -283,12 +283,10 @@ function sumGoalTokens(ctx: ExtensionContext): number {
283
283
  }
284
284
 
285
285
  function setGoalUpdateToolActive(pi: ExtensionAPI, active: boolean): void {
286
- const api = pi as ExtensionAPI & { getActiveTools?: () => string[]; setActiveTools?: (names: string[]) => void };
287
- if (!api.getActiveTools || !api.setActiveTools) return;
288
- const activeTools = api.getActiveTools();
286
+ const activeTools = pi.getActiveTools();
289
287
  const isActive = activeTools.includes(GOAL_UPDATE_TOOL);
290
288
  if (active === isActive) return;
291
- api.setActiveTools(active
289
+ pi.setActiveTools(active
292
290
  ? [...activeTools, GOAL_UPDATE_TOOL]
293
291
  : activeTools.filter((name) => name !== GOAL_UPDATE_TOOL));
294
292
  }
@@ -495,6 +493,7 @@ function beginGoalTurn(
495
493
  return next;
496
494
  }
497
495
 
496
+ /** Starts one goal turn only after Pi is idle and all competing workflow gates are clear. */
498
497
  function scheduleGoalContinuation(
499
498
  pi: ExtensionAPI,
500
499
  runtime: GoalRuntime,
@@ -146,6 +146,7 @@ async function pathExists(filePath: string): Promise<boolean> {
146
146
  }
147
147
  }
148
148
 
149
+ /** Installs generated guidance atomically and preserves any target changed after baseline capture. */
149
150
  export async function installInitAgentsFile(
150
151
  targetPath: string,
151
152
  content: string,
package/killeros/init.ts CHANGED
@@ -47,16 +47,14 @@ After a successful write, read generated AGENTS.md once through killeros_init_re
47
47
  `.trim();
48
48
 
49
49
  function setInitTools(pi: ExtensionAPI, initState: InitRuntime, active: boolean): void {
50
- const runtime = pi as ExtensionAPI & { getActiveTools?: () => string[]; setActiveTools?: (names: string[]) => void };
51
- if (!runtime.getActiveTools || !runtime.setActiveTools) return;
52
50
  if (active) {
53
- initState.activeTools ??= runtime.getActiveTools().filter((name) => !INIT_SCOPED_TOOLS.includes(name as (typeof INIT_SCOPED_TOOLS)[number]));
54
- runtime.setActiveTools([...INIT_SCOPED_TOOLS]);
51
+ initState.activeTools ??= pi.getActiveTools().filter((name) => !INIT_SCOPED_TOOLS.includes(name as (typeof INIT_SCOPED_TOOLS)[number]));
52
+ pi.setActiveTools([...INIT_SCOPED_TOOLS]);
55
53
  } else if (initState.activeTools) {
56
- runtime.setActiveTools(initState.activeTools);
54
+ pi.setActiveTools(initState.activeTools);
57
55
  initState.activeTools = undefined;
58
56
  } else {
59
- runtime.setActiveTools(runtime.getActiveTools().filter((name) => !INIT_SCOPED_TOOLS.includes(name as (typeof INIT_SCOPED_TOOLS)[number])));
57
+ pi.setActiveTools(pi.getActiveTools().filter((name) => !INIT_SCOPED_TOOLS.includes(name as (typeof INIT_SCOPED_TOOLS)[number])));
60
58
  }
61
59
  }
62
60
 
@@ -4,6 +4,7 @@ import {
4
4
  type ExtensionAPI,
5
5
  type ExtensionContext,
6
6
  } from "@earendil-works/pi-coding-agent";
7
+ import { errorMessage } from "./errors.ts";
7
8
  import { createKillerosSettingsStore } from "./settings.ts";
8
9
 
9
10
  export interface NotificationPreferenceStore {
@@ -40,10 +41,6 @@ export function formatNotificationTitle(
40
41
  return enabled ? `${base} ${COMPLETION_BELL_GLYPH}` : base;
41
42
  }
42
43
 
43
- function errorMessage(error: unknown): string {
44
- return error instanceof Error ? error.message : String(error);
45
- }
46
-
47
44
  export function registerCompletionNotifications(
48
45
  pi: ExtensionAPI,
49
46
  dependencies?: CompletionNotificationDependencies,
@@ -45,6 +45,12 @@ const STARTUP_TIPS = [
45
45
  "Run /variants to tune the model's reasoning depth.",
46
46
  "Type / to browse every command available in this session.",
47
47
  "Run /notification to enable a terminal bell when work settles.",
48
+ "Run /goal <objective> to keep long-running work moving across turns.",
49
+ "Run /init to generate root AGENTS.md from bounded repository evidence.",
50
+ "Run /handoff [focus] to continue work in a fresh linked session.",
51
+ "Run /codex-fast to toggle priority requests for Codex models.",
52
+ "Run /clear to start a fresh session after confirmation.",
53
+ "Ask the agent to show a short choice list when you need to decide.",
48
54
  ] as const;
49
55
 
50
56
  const EDITOR_SUGGESTIONS = [
@@ -52,6 +58,12 @@ const EDITOR_SUGGESTIONS = [
52
58
  'Try "find edge cases in <filepath>"',
53
59
  'Try "simplify <filepath> without changing behavior"',
54
60
  'Try "write tests for <filepath>"',
61
+ 'Try "trace this failure to its first bad state"',
62
+ 'Try "review this diff against <spec>"',
63
+ 'Try "explain why this test fails"',
64
+ 'Try "find the smallest safe fix for <issue>"',
65
+ 'Try "map the data flow through <feature>"',
66
+ 'Try "draft an implementation plan for <feature>"',
55
67
  ] as const;
56
68
 
57
69
  export function resolveGitBranch(cwd: string): Promise<string | undefined> {
@@ -216,6 +228,7 @@ function extractTerminalSequence(text: string, position: number): { code: string
216
228
  return undefined;
217
229
  }
218
230
 
231
+ /** Styles valid slash-command tokens while preserving embedded terminal control sequences. */
219
232
  export function highlightSlashCommands(
220
233
  line: string,
221
234
  isValidCommand: (name: string) => boolean,
@@ -1,6 +1,8 @@
1
1
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import type { StopReason } from "@earendil-works/pi-ai";
3
3
  import { Text } from "@earendil-works/pi-tui";
4
+ import { formatTokens } from "./display.ts";
5
+ import { errorMessage } from "./errors.ts";
4
6
 
5
7
  const WORKED_FOR_ENTRY_TYPE = "killeros-worked-for";
6
8
 
@@ -17,7 +19,14 @@ interface WorkedForEntryDataV2 {
17
19
  outcome: WorkedForOutcome;
18
20
  }
19
21
 
20
- type WorkedForEntryData = WorkedForEntryDataV1 | WorkedForEntryDataV2;
22
+ interface WorkedForEntryDataV3 {
23
+ version: 3;
24
+ milliseconds: number;
25
+ outcome: WorkedForOutcome;
26
+ tokens: number;
27
+ }
28
+
29
+ type WorkedForEntryData = WorkedForEntryDataV1 | WorkedForEntryDataV2 | WorkedForEntryDataV3;
21
30
 
22
31
  const OUTCOMES = {
23
32
  done: { marker: "✓", label: "Done", color: "success" },
@@ -29,6 +38,23 @@ function isWorkedForOutcome(value: unknown): value is WorkedForOutcome {
29
38
  return value === "done" || value === "stopped" || value === "failed";
30
39
  }
31
40
 
41
+ function sessionTokenTotal(ctx: ExtensionContext): number | undefined {
42
+ try {
43
+ let total = 0;
44
+ for (const entry of ctx.sessionManager.getEntries()) {
45
+ const tokens = entry.type === "message" && (entry.message.role === "assistant" || entry.message.role === "toolResult")
46
+ ? entry.message.usage?.totalTokens
47
+ : (entry.type === "compaction" || entry.type === "branch_summary")
48
+ ? entry.usage?.totalTokens
49
+ : undefined;
50
+ if (typeof tokens === "number" && Number.isFinite(tokens) && tokens > 0) total += tokens;
51
+ }
52
+ return total;
53
+ } catch {
54
+ return undefined;
55
+ }
56
+ }
57
+
32
58
  export function formatWorkedForDuration(milliseconds: number): string {
33
59
  const boundedMilliseconds = Number.isFinite(milliseconds) ? Math.max(0, milliseconds) : 0;
34
60
  const totalSeconds = Math.max(1, Math.floor(boundedMilliseconds / 1_000));
@@ -49,10 +75,11 @@ function parseWorkedForEntryData(data: unknown): WorkedForEntryData | undefined
49
75
  return undefined;
50
76
  }
51
77
  if (data.version === 1) return { version: 1, milliseconds: data.milliseconds };
52
- if (data.version !== 2 || !("outcome" in data) || !isWorkedForOutcome(data.outcome)) {
53
- return undefined;
54
- }
55
- return { version: 2, milliseconds: data.milliseconds, outcome: data.outcome };
78
+ if (!("outcome" in data) || !isWorkedForOutcome(data.outcome)) return undefined;
79
+ if (data.version === 2) return { version: 2, milliseconds: data.milliseconds, outcome: data.outcome };
80
+ if (data.version !== 3 || !("tokens" in data)
81
+ || typeof data.tokens !== "number" || !Number.isFinite(data.tokens) || data.tokens < 0) return undefined;
82
+ return { version: 3, milliseconds: data.milliseconds, outcome: data.outcome, tokens: data.tokens };
56
83
  }
57
84
 
58
85
  export function workedForOutcome(stopReason: StopReason | undefined): WorkedForOutcome {
@@ -61,15 +88,12 @@ export function workedForOutcome(stopReason: StopReason | undefined): WorkedForO
61
88
  return "failed";
62
89
  }
63
90
 
64
- function errorMessage(error: unknown): string {
65
- return error instanceof Error ? error.message : String(error);
66
- }
67
-
68
91
  export function registerWorkedFor(
69
92
  pi: ExtensionAPI,
70
93
  now: () => number = Date.now,
71
94
  ): void {
72
95
  let startedAt: number | undefined;
96
+ let startedTokens: number | undefined;
73
97
  let stopReason: StopReason | undefined;
74
98
 
75
99
  pi.registerEntryRenderer<WorkedForEntryData>(WORKED_FOR_ENTRY_TYPE, (entry, _options, theme) => {
@@ -83,8 +107,9 @@ export function registerWorkedFor(
83
107
  );
84
108
  }
85
109
  const outcome = OUTCOMES[data.outcome];
110
+ const tokens = data.version === 3 ? ` · ↑ ${formatTokens(data.tokens)} tokens` : "";
86
111
  return new Text(
87
- `${theme.fg(outcome.color, `${outcome.marker} ${outcome.label}`)}${theme.fg("dim", ` · ${formatWorkedForDuration(data.milliseconds)}`)}`,
112
+ `${theme.fg(outcome.color, `${outcome.marker} ${outcome.label}`)}${theme.fg("dim", ` · ${formatWorkedForDuration(data.milliseconds)}${tokens}`)}`,
88
113
  0,
89
114
  0,
90
115
  );
@@ -92,12 +117,14 @@ export function registerWorkedFor(
92
117
 
93
118
  pi.on("session_start", () => {
94
119
  startedAt = undefined;
120
+ startedTokens = undefined;
95
121
  stopReason = undefined;
96
122
  });
97
123
 
98
124
  pi.on("agent_start", (_event, ctx) => {
99
125
  if (ctx.mode !== "tui" || startedAt !== undefined) return;
100
126
  startedAt = now();
127
+ startedTokens = sessionTokenTotal(ctx);
101
128
  });
102
129
 
103
130
  pi.on("agent_end", (event, ctx) => {
@@ -112,16 +139,21 @@ export function registerWorkedFor(
112
139
 
113
140
  pi.on("agent_settled", (_event, ctx) => {
114
141
  if (ctx.mode !== "tui" || startedAt === undefined) return;
115
- if (!ctx.isIdle() || ctx.hasPendingMessages()) return;
116
142
  const milliseconds = Math.max(0, now() - startedAt);
143
+ const settledTokens = sessionTokenTotal(ctx);
144
+ const tokens = startedTokens === undefined || settledTokens === undefined
145
+ ? 0
146
+ : Math.max(0, settledTokens - startedTokens);
117
147
  const outcome = workedForOutcome(stopReason);
118
148
  startedAt = undefined;
149
+ startedTokens = undefined;
119
150
  stopReason = undefined;
120
151
  try {
121
- pi.appendEntry<WorkedForEntryDataV2>(WORKED_FOR_ENTRY_TYPE, {
122
- version: 2,
152
+ pi.appendEntry<WorkedForEntryDataV3>(WORKED_FOR_ENTRY_TYPE, {
153
+ version: 3,
123
154
  milliseconds,
124
155
  outcome,
156
+ tokens,
125
157
  });
126
158
  } catch (error) {
127
159
  ctx.ui.notify(`Worked-for timing could not be saved: ${errorMessage(error)}`, "error");
@@ -130,6 +162,7 @@ export function registerWorkedFor(
130
162
 
131
163
  pi.on("session_shutdown", () => {
132
164
  startedAt = undefined;
165
+ startedTokens = undefined;
133
166
  stopReason = undefined;
134
167
  });
135
168
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "killeros",
3
- "version": "2.0.16",
3
+ "version": "2.0.17",
4
4
  "description": "TUI, goals, and workflow automation for the Pi coding agent",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -1 +0,0 @@
1
- export const MAX_NODE_TIMER_MS = 2_147_483_647;