killeros 2.0.15 → 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 +24 -0
- package/Killeros.ts +1 -1
- package/README.md +38 -137
- package/killeros/auto-compaction.ts +2 -4
- package/killeros/errors.ts +6 -2
- package/killeros/goals.ts +4 -5
- package/killeros/handoff.ts +62 -24
- 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,30 @@ 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
|
+
|
|
23
|
+
## [2.0.16] - 2026-08-23
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- Kept `/handoff` input inside a cancellable TUI loader so buffered editor text cannot cross the session boundary.
|
|
28
|
+
- Prevented a cancelled handoff from starting provider completion when authentication resolves late.
|
|
29
|
+
- Made repository contract tests validate README facts without depending on discarded prose.
|
|
30
|
+
|
|
7
31
|
## [2.0.15] - 2026-08-23
|
|
8
32
|
|
|
9
33
|
### Added
|
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
|
@@ -1,127 +1,66 @@
|
|
|
1
1
|
# KillerOS
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A TypeScript extension for the [Pi coding agent](https://github.com/earendil-works/pi) that replaces the stock TUI and adds long-running goals, reasoning controls, and workflow commands.
|
|
4
|
+
|
|
5
|
+
## What you get
|
|
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; 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. Pause, resume, edit, or clear it anytime.
|
|
9
|
+
- `/init`: generates a root `AGENTS.md` from repository evidence, preserving compatible existing rules.
|
|
10
|
+
- `/variants`: pick a reasoning level supported by the active model.
|
|
11
|
+
- `/codex-fast`: toggles the `priority` service tier on Codex requests.
|
|
12
|
+
- `/handoff`: starts a fresh linked session carrying visible continuation context.
|
|
13
|
+
- Automatic context compaction when remaining tokens drop below 15% of the window (configurable).
|
|
14
|
+
- A `question` tool with single-select and multi-select modes.
|
|
15
|
+
- Lifecycle hooks (`tool_call`, `tool_result`, `agent_settled`) from `.pi/killeros-hooks.json`, plus `AGENTS.local.md` loading for trusted projects.
|
|
16
|
+
- Optional completion sounds for settled requests.
|
|
17
|
+
|
|
4
18
|
|
|
5
19
|
## Requirements
|
|
6
20
|
|
|
7
|
-
- Node.js
|
|
8
|
-
- Pi
|
|
21
|
+
- Node.js 22.19.0+
|
|
22
|
+
- Pi 0.84.2+
|
|
9
23
|
- An interactive TUI session for the custom header, editor, footer, `question`, and `/init`
|
|
10
24
|
|
|
11
|
-
KillerOS ships as TypeScript. Pi supplies the runtime modules listed as peer dependencies.
|
|
12
|
-
|
|
13
25
|
## Install
|
|
14
26
|
|
|
15
|
-
Install the current npm release:
|
|
16
|
-
|
|
17
27
|
```bash
|
|
18
28
|
pi install npm:killeros
|
|
19
29
|
```
|
|
20
30
|
|
|
21
|
-
|
|
31
|
+
Or from GitHub:
|
|
22
32
|
|
|
23
33
|
```bash
|
|
24
34
|
pi install git:github.com/KyrosHendrix/pi-KillerOS
|
|
25
35
|
```
|
|
26
36
|
|
|
27
|
-
Pin
|
|
28
|
-
|
|
29
|
-
```bash
|
|
30
|
-
pi install git:github.com/KyrosHendrix/pi-KillerOS@v2.0.15
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
Add `-l` to either command to install only for the current project. Restart Pi after installing.
|
|
34
|
-
|
|
35
|
-
## Features
|
|
36
|
-
|
|
37
|
-
- A compact startup card with the extension version, model, provider, `/model`, working directory, Git branch, and a session-stable tip.
|
|
38
|
-
- A dark theme with coral accents and neutral tool-call containers for pending, successful, and failed calls.
|
|
39
|
-
- A 12-frame orange activity glyph with event-based status text and a quiet hidden-thinking label.
|
|
40
|
-
- A multiline editor with a single focus-aware prompt arrow, a session-stable empty-state suggestion, overflow-only scroll indicators, Shift+Enter support, and slash-command completion.
|
|
41
|
-
- A settled transcript line that reports `Done`, `Stopped`, or `Failed` with elapsed time, while preserving older `✻ Worked for ...` entries.
|
|
42
|
-
- A responsive footer that keeps model, context, and goal state visible as the terminal gets narrower.
|
|
43
|
-
- Automatic turn-boundary context compaction in TUI and RPC modes.
|
|
44
|
-
- Optional completion sounds for successful and failed settled requests.
|
|
45
|
-
- `/variants` for selecting a model reasoning level.
|
|
46
|
-
- `/codex-fast` for toggling the `priority` service tier on Codex requests.
|
|
47
|
-
- `/goal` for durable objectives with pause, resume, edit, clear, completion, continuation, and blocker audits.
|
|
48
|
-
- `/init` for generating a root `AGENTS.md` from a bounded, safe set of repository files.
|
|
49
|
-
- A `question` tool with single-select and opt-in multi-select controls.
|
|
50
|
-
- Slash completion based on Pi's registered commands, extensions, prompts, and skills.
|
|
51
|
-
- Goal-aware `/clear` and graceful `/exit` handling.
|
|
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.
|
|
52
38
|
|
|
53
39
|
## Commands
|
|
54
40
|
|
|
55
41
|
```text
|
|
56
42
|
/init Generate root AGENTS.md from repository evidence
|
|
57
|
-
/goal Open
|
|
58
|
-
/goal
|
|
59
|
-
/
|
|
60
|
-
/
|
|
61
|
-
/goal resume Resume automatic continuation
|
|
62
|
-
/goal clear Stop current goal work and remove the goal
|
|
63
|
-
/variants Open the reasoning-level selector
|
|
64
|
-
/variants high Set a reasoning level directly
|
|
65
|
-
/codex-fast Toggle process-local Codex fast mode
|
|
43
|
+
/goal Open goal status, or set an objective with /goal <objective>
|
|
44
|
+
/goal edit|pause|resume|clear
|
|
45
|
+
/variants Reasoning-level selector (/variants high sets directly)
|
|
46
|
+
/codex-fast Toggle Codex fast mode
|
|
66
47
|
/notification Configure the completion sound
|
|
67
|
-
/
|
|
68
|
-
/
|
|
48
|
+
/handoff [focus] Fresh session with continuation context
|
|
49
|
+
/clear New session after confirmation
|
|
69
50
|
/exit Quit Pi gracefully
|
|
70
51
|
```
|
|
71
52
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
`/codex-fast` takes no arguments. It toggles a process-local setting. When it is enabled and the active model uses the `openai-codex` provider, KillerOS adds `service_tier: "priority"` to the provider request. The footer shows bold `Fast` between the model and provider.
|
|
75
|
-
|
|
76
|
-
The setting survives a session reload within the same process, does not change other providers, is not saved to KillerOS configuration, and starts disabled after Pi restarts. A provider failure leaves the setting enabled and follows Pi's normal error handling.
|
|
77
|
-
|
|
78
|
-
### Goals
|
|
79
|
-
|
|
80
|
-
`/goal` requires a saved session in TUI or RPC mode. Goal state is stored in versioned session entries on the active branch and restored after reload, resume, fork, and tree navigation.
|
|
81
|
-
|
|
82
|
-
An active goal injects its unchanged objective on each turn and continues one settled turn at a time. The model must use KillerOS's private goal tool to report completion. If the objective explicitly asks for a named file-like deliverable at one quoted absolute path, KillerOS records that path and checks that a regular file exists before accepting completion. Other objectives use the model's completion report.
|
|
83
|
-
|
|
84
|
-
KillerOS marks a goal blocked only after a stable lowercase blocker key recorded on three consecutive goal turns. A changed key, skipped turn, resume, or edit resets the streak. Final prose does not end the loop.
|
|
85
|
-
|
|
86
|
-
Active goals replace the footer path with warning-yellow `/goal is active (...)` and keep the exact elapsed time visible.
|
|
87
|
-
|
|
88
|
-
`/goal pause` and `/goal clear` save paused or cleared state before aborting current goal work, so settlement cannot restart it. Aborted turns, provider failures, and continuation failures pause safely. Failed edit and replacement writes dispatch no edited objective. Replacing unfinished work requires confirmation, and `/goal edit` works only in TUI mode.
|
|
89
|
-
|
|
90
|
-
### Handoff
|
|
91
|
-
|
|
92
|
-
`/handoff [focus]` requires a saved source session and is available only when Pi is idle, has no pending messages, and has no active `/goal`. It creates a fresh linked session with a visible handoff document that supplies context for the next turn. The source session stays unchanged, and handoff does not start an agent turn.
|
|
93
|
-
|
|
94
|
-
### Repository initialization
|
|
95
|
-
|
|
96
|
-
`/init` freezes a safe project-file map and exposes only dedicated read and list operations while it generates the root `AGENTS.md`. Git-ignored files, known secret paths, private-key formats, other guidance files, dependencies, links, non-regular files, and files outside the map are unavailable to the generation step.
|
|
97
|
-
|
|
98
|
-
An existing root `AGENTS.md` is protected policy. Compatible rules are preserved. A real policy conflict leaves the file unchanged with a reason, and a concurrent target change aborts installation instead of replacing the newer file.
|
|
99
|
-
|
|
100
|
-
The generated file uses four behavioral sections adapted from `writing-great-guidelines`. `/init` does not require another skill installation, ask setup questions, start a second model process, or write another file. Pi resources reload only after a successful write.
|
|
101
|
-
|
|
102
|
-
### Interactive questions
|
|
103
|
-
|
|
104
|
-
Single-select remains the default. Explicit `minSelections: 1` and `maxSelections: 1` are accepted; other single-select bounds are rejected. Use `mode: "multiple"` to opt into multi-select. The custom answer counts as one selection.
|
|
105
|
-
|
|
106
|
-
In multi-select mode, use Space or a visible number to toggle an option, `/` to filter, and Enter to submit. The filter accepts spaces. Enter applies the filter and Escape returns to the choices. Checked options remain selected when the filter changes. Select `Type a custom answer` to add or edit one custom item alongside the checked options.
|
|
53
|
+
## Behavior by mode
|
|
107
54
|
|
|
108
|
-
|
|
55
|
+
| Mode | What works |
|
|
56
|
+
| --- | --- |
|
|
57
|
+
| TUI | Everything |
|
|
58
|
+
| RPC | Goals, proactive compaction; no TUI components, `/goal edit`, `/init`, sounds, title indicator |
|
|
59
|
+
| Print/JSON | No interactive questions, `/goal`, `/init`, or proactive compaction |
|
|
109
60
|
|
|
110
61
|
## Configuration
|
|
111
62
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
The completion sound is a global user preference in Pi's agent directory and is off by default. Run `/notification` in TUI mode to change it. Enabled TUI tabs append ``, which requires a Nerd Font. KillerOS uses the terminal's audible bell, so a terminal that disables the bell cannot play the sound.
|
|
115
|
-
|
|
116
|
-
### Automatic compaction
|
|
117
|
-
|
|
118
|
-
Automatic compaction is enabled by default when Pi's effective `compaction.enabled` setting is true. After each completed assistant turn, including tool execution, KillerOS reads the active model's context usage and calls Pi's public compaction API when:
|
|
119
|
-
|
|
120
|
-
```text
|
|
121
|
-
remainingTokens <= max(contextWindow * percentRemaining / 100, reserveTokens)
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
The default `percentRemaining` is `15`. Pi owns `reserveTokens` and `keepRecentTokens`. KillerOS reads the effective settings through Pi's public `SettingsManager` with `getAgentDir()` and stores its own preference in the global `killeros.json` file:
|
|
63
|
+
The packaged `killeros` theme activates on TUI start. Compaction triggers by default at 15% tokens remaining, stored in global `killeros.json`:
|
|
125
64
|
|
|
126
65
|
```json
|
|
127
66
|
{
|
|
@@ -132,59 +71,21 @@ The default `percentRemaining` is `15`. Pi owns `reserveTokens` and `keepRecentT
|
|
|
132
71
|
}
|
|
133
72
|
```
|
|
134
73
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
Pi writes the summary, applies manual focus instructions, tracks files, retries summarization, and handles overflow recovery. KillerOS only decides when to request proactive compaction.
|
|
138
|
-
|
|
139
|
-
Manual `/compact` pauses the current goal turn before summarization. After Pi saves the summary, KillerOS resumes that goal revision automatically. A failed or cancelled manual compaction stays paused. Run `/goal pause` during the pause to cancel automatic recovery.
|
|
140
|
-
|
|
141
|
-
### Project instructions and hooks
|
|
142
|
-
|
|
143
|
-
For trusted projects, KillerOS loads `AGENTS.local.md` after Pi's shared repository context. A one-line `@path` or `@~/path` file imports personal guidance from another location.
|
|
74
|
+
Completion sounds are off by default; change with `/notification` in TUI mode. The tab-title indicator requires a Nerd Font.
|
|
144
75
|
|
|
145
|
-
|
|
76
|
+
## Development
|
|
146
77
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
## Behavior by mode
|
|
150
|
-
|
|
151
|
-
| Mode | Behavior |
|
|
152
|
-
| --- | --- |
|
|
153
|
-
| TUI | All features are available, including proactive compaction, completion sounds, and the tab-title indicator. |
|
|
154
|
-
| RPC | Proactive compaction and goal set/view/pause/resume/clear work. TUI components, `/goal edit`, `/init`, completion sounds, and the title indicator are disabled. |
|
|
155
|
-
| Print/JSON | Interactive questions, `/goal`, `/init`, and proactive compaction are disabled. Completion sounds and the title indicator are disabled. |
|
|
156
|
-
|
|
157
|
-
## Development and validation
|
|
158
|
-
|
|
159
|
-
Source and tests use strict TypeScript. Tests run with Node's built-in test runner and type stripping.
|
|
160
|
-
|
|
161
|
-
Before a release, run:
|
|
78
|
+
Strict TypeScript throughout. Tests run on Node's built-in test runner:
|
|
162
79
|
|
|
163
80
|
```bash
|
|
164
|
-
npm ci
|
|
165
|
-
npm run check
|
|
166
|
-
npm test
|
|
167
|
-
npm pack --dry-run
|
|
168
|
-
pi -ne -e . --mode rpc
|
|
81
|
+
npm ci && npm run check && npm test
|
|
169
82
|
```
|
|
170
83
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
## Releases
|
|
174
|
-
|
|
175
|
-
For a normal release:
|
|
176
|
-
|
|
177
|
-
1. Update the version in `package.json` and both matching version fields in `package-lock.json`.
|
|
178
|
-
2. Add a dated section with the same version to `CHANGELOG.md`.
|
|
179
|
-
3. Push the release commit to `main`.
|
|
180
|
-
|
|
181
|
-
After the full CI workflow passes on `main`, the release workflow validates the commit and changelog, publishes the package to npm through trusted publishing, and creates the matching tag and GitHub release. The [`pi-package` keyword](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/packages.md) makes the npm package visible in Pi's package catalog.
|
|
182
|
-
|
|
183
|
-
Do not push version tags manually. Tag pushes cannot publish; every published commit must pass the full `main` CI workflow.
|
|
84
|
+
Releases go through CI on `main`; do not push version tags manually.
|
|
184
85
|
|
|
185
86
|
## Security
|
|
186
87
|
|
|
187
|
-
Pi extensions run with your user permissions. Review the source before installing
|
|
88
|
+
Pi extensions run with your user permissions. Review the source before installing globally. Hook commands run only for projects Pi marks as trusted; check `.pi/killeros-hooks.json` before enabling project trust.
|
|
188
89
|
|
|
189
90
|
## License
|
|
190
91
|
|
|
@@ -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/handoff.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { contentText } from "@earendil-works/pi-ai";
|
|
2
|
-
import { convertToLlm, type ExtensionAPI, type ExtensionCommandContext, serializeConversation, sessionEntryToContextMessages } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { BorderedLoader, convertToLlm, type ExtensionAPI, type ExtensionCommandContext, serializeConversation, sessionEntryToContextMessages } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import type { GoalRuntime } from "./runtime.ts";
|
|
4
4
|
import { safeTerminalText } from "./safe-terminal-text.ts";
|
|
5
5
|
|
|
@@ -25,6 +25,10 @@ const HANDOFF_SYSTEM_PROMPT = [
|
|
|
25
25
|
"Keep active constraints and unfinished work even when the requested focus is narrower.",
|
|
26
26
|
"Use exactly these second-level Markdown headings: Objective, Current state, Decisions, Constraints, Completed work, Relevant artifacts, Verification, Blockers or open questions, Exact next action, and Suggested skills.",
|
|
27
27
|
].join("\n");
|
|
28
|
+
type HandoffGenerationResult =
|
|
29
|
+
| { kind: "summary"; summary: string }
|
|
30
|
+
| { kind: "cancelled" }
|
|
31
|
+
| { kind: "error"; error: unknown };
|
|
28
32
|
|
|
29
33
|
/** Builds the one-off summary request from Pi's active context projection. */
|
|
30
34
|
function createHandoffRequest(
|
|
@@ -83,6 +87,41 @@ function reportHandoffError(ctx: ExtensionCommandContext, error: unknown): void
|
|
|
83
87
|
ctx.ui.notify(`Handoff failed: ${message}`, "error");
|
|
84
88
|
}
|
|
85
89
|
|
|
90
|
+
/** Generates and validates a handoff summary with optional cancellation. */
|
|
91
|
+
async function generateHandoffSummary(
|
|
92
|
+
ctx: ExtensionCommandContext,
|
|
93
|
+
conversation: string,
|
|
94
|
+
focus: string,
|
|
95
|
+
signal?: AbortSignal,
|
|
96
|
+
): Promise<string> {
|
|
97
|
+
if (!ctx.model) throw new Error("No current model is available");
|
|
98
|
+
|
|
99
|
+
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(ctx.model);
|
|
100
|
+
signal?.throwIfAborted();
|
|
101
|
+
if (!auth.ok) throw new Error(auth.error);
|
|
102
|
+
|
|
103
|
+
const response = await ctx.modelRegistry.complete(ctx.model, {
|
|
104
|
+
systemPrompt: HANDOFF_SYSTEM_PROMPT,
|
|
105
|
+
messages: [{
|
|
106
|
+
role: "user",
|
|
107
|
+
content: createHandoffRequest(conversation, focus, ctx.getSystemPromptOptions().skills ?? []),
|
|
108
|
+
timestamp: Date.now(),
|
|
109
|
+
}],
|
|
110
|
+
}, {
|
|
111
|
+
apiKey: auth.apiKey,
|
|
112
|
+
headers: auth.headers,
|
|
113
|
+
env: auth.env,
|
|
114
|
+
maxTokens: 2_048,
|
|
115
|
+
signal,
|
|
116
|
+
});
|
|
117
|
+
if (response.stopReason === "error") throw new Error(response.errorMessage || "Handoff summary failed");
|
|
118
|
+
if (response.stopReason !== "stop") throw new Error("The handoff summary did not finish");
|
|
119
|
+
|
|
120
|
+
const summary = safeTerminalText(contentText(response.content)).trim();
|
|
121
|
+
if (!summary) throw new Error("The handoff summary was empty");
|
|
122
|
+
return summary;
|
|
123
|
+
}
|
|
124
|
+
|
|
86
125
|
/** Registers the idle-only command that summarizes context into a child session. */
|
|
87
126
|
export function registerHandoff(pi: ExtensionAPI, goalRuntime: GoalRuntime): void {
|
|
88
127
|
pi.registerCommand("handoff", {
|
|
@@ -106,31 +145,30 @@ export function registerHandoff(pi: ExtensionAPI, goalRuntime: GoalRuntime): voi
|
|
|
106
145
|
const messages = ctx.sessionManager.buildContextEntries().flatMap(sessionEntryToContextMessages);
|
|
107
146
|
const conversation = serializeConversation(convertToLlm(messages));
|
|
108
147
|
if (!conversation.trim()) throw new Error("No usable session context is available");
|
|
109
|
-
if (!ctx.model) throw new Error("No current model is available");
|
|
110
|
-
|
|
111
|
-
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(ctx.model);
|
|
112
|
-
if (!auth.ok) throw new Error(auth.error);
|
|
113
|
-
|
|
114
148
|
focus = safeTerminalText(args).trim();
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
if (
|
|
149
|
+
const generation = ctx.mode === "tui"
|
|
150
|
+
? await ctx.ui.custom<HandoffGenerationResult>((tui, theme, _keybindings, done) => {
|
|
151
|
+
const loader = new BorderedLoader(tui, theme, "Generating handoff...");
|
|
152
|
+
let settled = false;
|
|
153
|
+
const finish = (result: HandoffGenerationResult): void => {
|
|
154
|
+
if (settled) return;
|
|
155
|
+
settled = true;
|
|
156
|
+
done(result);
|
|
157
|
+
};
|
|
158
|
+
loader.onAbort = () => finish({ kind: "cancelled" });
|
|
159
|
+
generateHandoffSummary(ctx, conversation, focus, loader.signal)
|
|
160
|
+
.then((summary) => finish({ kind: "summary", summary }))
|
|
161
|
+
.catch((error: unknown) => finish({ kind: "error", error }));
|
|
162
|
+
return loader;
|
|
163
|
+
})
|
|
164
|
+
: { kind: "summary", summary: await generateHandoffSummary(ctx, conversation, focus) } as const;
|
|
165
|
+
if (generation.kind === "cancelled") {
|
|
166
|
+
ctx.ui.notify("Handoff cancelled", "info");
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (generation.kind === "error") throw generation.error;
|
|
132
170
|
|
|
133
|
-
document = handoffDocument(summary);
|
|
171
|
+
document = handoffDocument(generation.summary);
|
|
134
172
|
if (!hasRequiredHandoffContent(document, focus)) {
|
|
135
173
|
throw new Error("The handoff summary did not contain every required section");
|
|
136
174
|
}
|
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;
|