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 +16 -0
- package/Killeros.ts +1 -1
- package/README.md +2 -2
- package/killeros/auto-compaction.ts +2 -4
- package/killeros/errors.ts +6 -2
- package/killeros/goals.ts +4 -5
- package/killeros/init-target.ts +1 -0
- package/killeros/init.ts +4 -6
- package/killeros/notifications.ts +1 -4
- package/killeros/shell-ui.ts +13 -0
- package/killeros/worked-for.ts +46 -13
- package/package.json +1 -1
- package/killeros/limits.ts +0 -1
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
|
|
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
|
}
|
package/killeros/errors.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
package/killeros/init-target.ts
CHANGED
|
@@ -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 ??=
|
|
54
|
-
|
|
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
|
-
|
|
54
|
+
pi.setActiveTools(initState.activeTools);
|
|
57
55
|
initState.activeTools = undefined;
|
|
58
56
|
} else {
|
|
59
|
-
|
|
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,
|
package/killeros/shell-ui.ts
CHANGED
|
@@ -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,
|
package/killeros/worked-for.ts
CHANGED
|
@@ -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
|
-
|
|
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 (
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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<
|
|
122
|
-
version:
|
|
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
package/killeros/limits.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export const MAX_NODE_TIMER_MS = 2_147_483_647;
|