killeros 2.0.5 → 2.0.7

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,44 @@ All notable changes to KillerOS are documented here.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [2.0.7] - 2026-08-13
8
+
9
+ ### Changed
10
+
11
+ - Raised the supported and locked Pi baseline from 0.82.1 to the current matched 0.84.1 AI, coding-agent, and TUI packages.
12
+ - Added a compact two-deck footer beneath the prompt and removed the work-trail widget.
13
+ - Replaced the framed prompt and shuffled activity verbs with a focus-aware single-arrow editor and event-derived working copy.
14
+ - Made settled timing entries report `Done`, `Stopped`, or `Failed` while keeping existing `Worked for` history readable.
15
+
16
+ ### Fixed
17
+
18
+ - Separated response output from the prompt editor while keeping footer telemetry close beneath a quiet muted divider.
19
+ - Kept `/variants` within the active terminal height, preserved the focused reasoning level across resizes, and initially focused the current level.
20
+ - Rendered `/variants` controls from the same Pi keybinding manager that handles input, including installations with separate package module instances.
21
+ - Rendered active, paused, and blocked `/goal` status text consistently in the footer's far-right slot.
22
+ - Kept the private goal-update tool inactive outside active `/goal` runs and rendered its real execution errors instead of malformed blocker-audit fields.
23
+ - Verified exact persisted paths for clearly declared file-deliverable goals before completion, while preserving model-reported completion for general objectives.
24
+ - Accepted explicit `1`/`1` bounds for single-select questions and applied the same bounds validation before rendering and execution.
25
+ - Prevented reload and branch navigation from preserving or reconstructing stale manual-compaction recovery eligibility; only Pi's live event in the current session can resume the interrupted goal safely.
26
+ - Restricted goal deliverable verification to explicit destination phrases so source and reference paths cannot be reported as completed output.
27
+ - Kept saved goals fully inactive in print and JSON modes, including the private update tool and shutdown checkpoints.
28
+ - Rejected concurrent `/init` starts during preflight, cancelled pending preflight work on shutdown, and settled active `/init` command handlers when their session closes.
29
+
30
+ ## [2.0.6] - 2026-08-11
31
+
32
+ ### Added
33
+
34
+ - Added a durable `✻ Worked for …` transcript line below each settled TUI response, including stopped and failed runs.
35
+ - Added optional multi-select to the `question` tool with bounded checked options, one additive custom answer, a dedicated multi-word filter editor, and compact expandable results while preserving single-select defaults.
36
+
37
+ ### Changed
38
+
39
+ - Automated GitHub releases after successful `main` CI version bumps, with package, lockfile, changelog, tag, and verified-commit checks plus a manual tag recovery path.
40
+
41
+ ### Fixed
42
+
43
+ - Entered automatic `/goal` continuations into durable goal-turn state before dispatch so turn numbers advance and blocker audits work on Pi custom-message turns.
44
+
7
45
  ## [2.0.5] - 2026-08-11
8
46
 
9
47
  ### Added
package/Killeros.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { registerRequestActivity } from "./killeros/activity.ts";
2
3
  import { registerAliases, registerSlashAutocomplete } from "./killeros/commands.ts";
3
4
  import { registerConcisePrompt } from "./killeros/concise.ts";
4
5
  import { registerFooter } from "./killeros/footer.ts";
@@ -14,6 +15,7 @@ import { registerQuestionTool } from "./killeros/question.ts";
14
15
  import { createGoalRuntime, createInitRuntime } from "./killeros/runtime.ts";
15
16
  import { registerShellUi } from "./killeros/shell-ui.ts";
16
17
  import { registerVariants } from "./killeros/variants.ts";
18
+ import { registerWorkedFor } from "./killeros/worked-for.ts";
17
19
 
18
20
  export { CONCISE_SYSTEM_PROMPT, isConciseEnabled, isConcisedEnabled } from "./killeros/concise.ts";
19
21
  export { contextPercentRemaining, formatCost, formatContextProgress } from "./killeros/footer.ts";
@@ -42,5 +44,7 @@ export default function Killeros(pi: ExtensionAPI, options: KillerosOptions = {}
42
44
  registerLifecycleHooks(pi);
43
45
  registerGoalSettlement(pi, goalRuntime, initRuntime);
44
46
  registerInitSettlement(pi, initRuntime);
47
+ registerRequestActivity(pi);
45
48
  registerCompletionNotifications(pi, options.completionNotifications);
49
+ registerWorkedFor(pi);
46
50
  }
package/README.md CHANGED
@@ -5,7 +5,7 @@ A production-hardened Pi extension that combines a custom TUI, repository initia
5
5
  ## Requirements
6
6
 
7
7
  - Node.js `22.19.0` or later
8
- - Pi `0.82.1` or later
8
+ - Pi `0.84.1` or later
9
9
  - Interactive TUI mode for the custom header, editor, footer, `question` tool, and `/init`
10
10
 
11
11
  The extension is strict TypeScript. Pi provides the runtime modules.
@@ -31,7 +31,7 @@ pi install git:github.com/KyrosHendrix/pi-KillerOS
31
31
  Pin an install to a release:
32
32
 
33
33
  ```bash
34
- pi install git:github.com/KyrosHendrix/pi-KillerOS@v2.0.5
34
+ pi install git:github.com/KyrosHendrix/pi-KillerOS@v2.0.7
35
35
  ```
36
36
 
37
37
  Add `-l` to either command for a project-only install. Restart Pi after installing.
@@ -40,16 +40,17 @@ Add `-l` to either command for a project-only install. Restart Pi after installi
40
40
 
41
41
  - 52-column Compact startup card with inline version, polished model/provider identity, adjacent `/model`, directory, conditional Git branch, and a shuffled session-stable tip
42
42
  - Cohesive dark theme with coral accents and neutral tool-call containers across pending, success, and error states
43
- - Animated orange 12-frame activity glyph loop at 120 ms per frame, with orange shuffled Claude-adjacent verbs changing every 2.5 seconds, a gray `(esc to interrupt · thinking)` status with bold `esc`, and a quiet hidden-thinking label
44
- - Full-width framed multiline editor with a shuffled session-stable empty-state suggestion, Shift+Enter support, and slash-command autocomplete; KillerOS preserves an editor factory configured by another extension
45
- - Responsive footer with polished model/provider identity, plain-language context, and active goal state remaining; reasoning, Git branch, elapsed time, cost, and path cut down by available width
43
+ - Animated orange 12-frame activity glyph loop at 120 ms per frame with contextual copy derived from request, tool, result, and response events, plus a quiet hidden-thinking label
44
+ - Frameless multiline editor with one focus-aware `❯`, a shuffled session-stable empty-state suggestion, overflow-only scroll indicators, Shift+Enter support, and slash-command autocomplete; KillerOS preserves an editor factory configured by another extension
45
+ - One compact TUI transcript line reporting truthful `Done`, `Stopped`, or `Failed` settlement with elapsed time while preserving older `✻ Worked for …` entries
46
+ - Compact two-deck footer with session state above workspace state; model, context, and active goals stay prioritized as reasoning, time, cost, branch, and path reduce by available width
46
47
  - Pi-owned context compaction with active goals continuing from Pi's settled boundary after manual, threshold, and overflow compaction
47
48
  - Optional completion sounds after successful or failed settled requests, excluding manual aborts
48
49
  - `/variants` selector and direct reasoning-level arguments
49
50
  - Codex-style `/goal` with an interactive status/action panel, durable objectives, immediate pause and clear cancellation, automatic continuation, explicit completion, and durable blocker audits
50
51
  - Automatic `/init` guideline synthesis with a frozen safe evidence map, protected existing policy, and the four packaged behavioral sections adapted from `writing-great-guidelines`
51
- - `question` tool with height-bounded option windows, configured Pi keybindings, live option/input progress, proposal previews, custom answers, history, cancellation, and compact expandable transcript rendering
52
- - Mid-prompt slash completion with current Pi `0.82.1` commands, extensions, prompts, and skills; paths, URLs, and invalid commands remain plain text
52
+ - `question` tool with single-select and opt-in bounded multi-select, height-bounded option windows, configured Pi keybindings, live option/input progress, proposal previews, custom answers, history, cancellation, and compact expandable transcript rendering
53
+ - Mid-prompt slash completion with current Pi `0.84.1` commands, extensions, prompts, and skills; paths, URLs, and invalid commands remain plain text
53
54
  - Goal-aware `/clear` that confirms, aborts active work, waits for settlement, and starts a new session, plus `/exit` for graceful shutdown
54
55
  - Concise system-prompt guidance and supported native concise defaults that preserve explicit provider settings
55
56
 
@@ -70,7 +71,7 @@ Add `-l` to either command for a project-only install. Restart Pi after installi
70
71
  /exit Quit Pi gracefully
71
72
  ```
72
73
 
73
- `/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, or tree navigation. Active goals inject their unchanged objective every turn and continue one settled turn at a time. The model must use KillerOS’s private goal tool to mark verified completion. Blocking requires one stable lowercase blocker key recorded on three consecutive goal turns; a changed key, skipped turn, resume, or edit resets the streak. Final prose alone does not end the loop.
74
+ `/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, or tree navigation. Active goals inject their unchanged objective every turn and continue one settled turn at a time. The model must use KillerOS’s private goal tool to report completion. For an objective that clearly asks to create, write, save, or generate a named file-like deliverable at one quoted absolute path, KillerOS saves that exact path with the goal and verifies that a regular file exists there before accepting completion. Other objectives retain model-reported completion. Blocking requires one stable lowercase blocker key recorded on three consecutive goal turns; a changed key, skipped turn, resume, or edit resets the streak. Final prose alone does not end the loop.
74
75
 
75
76
  `/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 otherwise pause safely. Failed edit and replacement writes dispatch no edited objective; an active prior objective pauses fail-closed, while inactive durable state remains unchanged. Replacing unfinished work requires confirmation, and `/goal edit` requires TUI mode.
76
77
 
@@ -78,6 +79,12 @@ Add `-l` to either command for a project-only install. Restart Pi after installi
78
79
 
79
80
  The generated file uses the four behavioral sections adapted from `writing-great-guidelines`; no external skill installation is required. `/init` asks no setup questions, starts no second model process, writes no other file, and reloads Pi resources only after a successful write.
80
81
 
82
+ ### Interactive questions
83
+
84
+ Single-select remains the default. Explicit `minSelections: 1` and `maxSelections: 1` are equivalent to omitting both bounds; other single-select bounds are rejected. An agent opts into multi-select with `mode: "multiple"` and may set `minSelections` and `maxSelections`; the custom answer counts as one selection.
85
+
86
+ In multi-select, use Space or a visible number to toggle an option, `/` to filter, and Enter to submit. The filter accepts spaces; Enter applies it and Escape returns to the choices. Checked options remain selected when the filter changes. Select **Type a custom answer** with Enter to add or edit one custom item alongside checked options.
87
+
81
88
  Supported reasoning levels are `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, and `max`. KillerOS limits choices to levels supported by the current model.
82
89
 
83
90
  ## Configuration
@@ -120,17 +127,17 @@ The package manifest lists Pi’s built-in modules as peer dependencies, so npm
120
127
 
121
128
  ## Publish
122
129
 
123
- The [`pi-package`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/packages.md) keyword makes a published npm release visible in Pi’s package catalog.
130
+ To create a GitHub release, update the version in `package.json` and `package-lock.json`, add the matching `CHANGELOG.md` section, and push the release commit to `main`. After the full CI workflow passes, the release workflow creates the matching tag and GitHub release from that verified commit.
124
131
 
125
- For a release, publish after the validation checks pass:
132
+ Do not manually tag a normal release. If automation must recover a missing GitHub release, push the matching version tag; the same workflow validates the tag against the package and changelog before creating the release.
133
+
134
+ The [`pi-package`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/packages.md) keyword makes a published npm release visible in Pi’s package catalog. GitHub release automation does not publish to npm. Publish there separately after validation:
126
135
 
127
136
  ```bash
128
137
  npm login
129
138
  npm publish
130
139
  ```
131
140
 
132
- For later releases, choose `patch`, `minor`, or `major` with `npm version`, then publish and push the version commit and tag.
133
-
134
141
  ## Security
135
142
 
136
143
  Pi extensions run with your user permissions. Review the source before installing KillerOS globally. KillerOS executes lifecycle hook commands only for projects Pi marks as trusted; review `.pi/killeros-hooks.json` before enabling project trust.
@@ -0,0 +1,97 @@
1
+ import type { ExtensionAPI, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
2
+ import { truncateToWidth } from "@earendil-works/pi-tui";
3
+
4
+ export type ActivityMessage =
5
+ | { kind: "prompt" }
6
+ | { kind: "tool"; toolName: string }
7
+ | { kind: "tool-result"; failed: boolean }
8
+ | { kind: "responding" };
9
+
10
+ function safeToolName(toolName: string): string {
11
+ const normalized = toolName.replace(/[\u0000-\u001F\u007F]+/gu, " ").replace(/\s+/gu, " ").trim();
12
+ return truncateToWidth(normalized || "tool", 32, "…");
13
+ }
14
+
15
+ export function formatActivityMessage(message: ActivityMessage, theme: Theme): string {
16
+ let verb: string;
17
+ let detail: string;
18
+
19
+ switch (message.kind) {
20
+ case "prompt":
21
+ verb = "Mapping…";
22
+ detail = "understanding request";
23
+ break;
24
+ case "tool-result":
25
+ verb = message.failed ? "Recovering…" : "Reviewing…";
26
+ detail = message.failed ? "tool failed" : "reading the result";
27
+ break;
28
+ case "responding":
29
+ verb = "Responding…";
30
+ detail = "assembling the answer";
31
+ break;
32
+ case "tool":
33
+ switch (message.toolName.trim().toLocaleLowerCase()) {
34
+ case "read":
35
+ case "grep":
36
+ case "find":
37
+ case "ls":
38
+ verb = "Inspecting…";
39
+ detail = "reading relevant code";
40
+ break;
41
+ case "edit":
42
+ case "write":
43
+ verb = "Changing…";
44
+ detail = "editing";
45
+ break;
46
+ case "bash":
47
+ verb = "Running…";
48
+ detail = "command";
49
+ break;
50
+ default:
51
+ verb = "Working…";
52
+ detail = `using ${safeToolName(message.toolName)}`;
53
+ }
54
+ break;
55
+ }
56
+
57
+ return `${theme.fg("accent", verb)} ${theme.fg("dim", `(${theme.bold("esc")} to interrupt · ${detail})`)}`;
58
+ }
59
+
60
+ export function registerRequestActivity(pi: ExtensionAPI): void {
61
+ let active = false;
62
+
63
+ const clear = (ctx?: ExtensionContext): void => {
64
+ if (ctx?.mode === "tui") {
65
+ ctx.ui.setWorkingMessage();
66
+ }
67
+ active = false;
68
+ };
69
+
70
+ pi.on("agent_start", (_event, ctx) => {
71
+ if (ctx.mode !== "tui") return;
72
+ active = true;
73
+ ctx.ui.setWorkingMessage(formatActivityMessage({ kind: "prompt" }, ctx.ui.theme));
74
+ });
75
+
76
+ pi.on("tool_execution_start", (event, ctx) => {
77
+ if (ctx.mode !== "tui" || !active) return;
78
+ ctx.ui.setWorkingMessage(formatActivityMessage({ kind: "tool", toolName: event.toolName }, ctx.ui.theme));
79
+ });
80
+
81
+ pi.on("tool_execution_end", (event, ctx) => {
82
+ if (ctx.mode !== "tui" || !active) return;
83
+ ctx.ui.setWorkingMessage(formatActivityMessage({ kind: "tool-result", failed: event.isError }, ctx.ui.theme));
84
+ });
85
+
86
+ pi.on("message_update", (event, ctx) => {
87
+ if (ctx.mode !== "tui" || !active || event.assistantMessageEvent.type !== "text_start") return;
88
+ ctx.ui.setWorkingMessage(formatActivityMessage({ kind: "responding" }, ctx.ui.theme));
89
+ });
90
+
91
+ pi.on("agent_settled", (_event, ctx) => {
92
+ if (ctx.mode !== "tui" || !active || !ctx.isIdle?.() || ctx.hasPendingMessages?.()) return;
93
+ clear(ctx);
94
+ });
95
+
96
+ pi.on("session_shutdown", (_event, ctx) => clear(ctx));
97
+ }
@@ -125,6 +125,10 @@ function renderFooterRow(left: string, right: string, width: number): string {
125
125
  return ` ${clippedLeft}${gap}${clippedRight} `;
126
126
  }
127
127
 
128
+ function renderFooter(rows: string[], width: number, theme: Theme): string[] {
129
+ return [theme.fg("borderMuted", "─".repeat(width)), ...rows];
130
+ }
131
+
128
132
  function formatGoalElapsed(milliseconds: number): string {
129
133
  const totalSeconds = Number.isFinite(milliseconds) ? Math.max(0, Math.floor(milliseconds / 1_000)) : 0;
130
134
  if (totalSeconds < 60) return `${totalSeconds}s`;
@@ -138,15 +142,13 @@ function formatGoalElapsed(milliseconds: number): string {
138
142
  return `${hours}h ${minutes.toString().padStart(2, "0")}m ${seconds.toString().padStart(2, "0")}s`;
139
143
  }
140
144
 
141
- function formatActiveGoalFooter(state: GoalState | undefined, theme: Theme): string {
142
- if (state?.status !== "active") return "";
143
- return theme.fg("warning", `/goal is active (${formatGoalElapsed(goalElapsedMilliseconds(state))})`);
144
- }
145
-
146
145
  function formatGoalFooter(state: GoalState | undefined, theme: Theme): string {
147
146
  if (!state) return "";
148
- if (state.status === "paused") return theme.fg("warning", "Ⅱ goal paused");
149
- if (state.status === "blocked") return theme.fg("error", "! goal blocked");
147
+ if (state.status === "active") {
148
+ return theme.fg("warning", `/goal is active (${formatGoalElapsed(goalElapsedMilliseconds(state))})`);
149
+ }
150
+ if (state.status === "paused") return theme.fg("warning", "/goal is paused");
151
+ if (state.status === "blocked") return theme.fg("error", "/goal is blocked");
150
152
  return "";
151
153
  }
152
154
 
@@ -211,52 +213,36 @@ export function registerFooter(pi: ExtensionAPI, goalRuntime: GoalRuntime): void
211
213
  const signature = formatModel(model, theme);
212
214
  const fullDirectory = theme.fg("dim", cwd);
213
215
  const focusedDirectory = theme.fg("dim", compactDirectory(cwd));
214
- const activeGoal = formatActiveGoalFooter(goalRuntime.state, theme);
215
216
  const goal = formatGoalFooter(goalRuntime.state, theme);
216
- const rich = joinFooterParts([
217
+ const primary = joinFooterParts([
217
218
  signature,
218
219
  level,
219
220
  context,
220
- goal,
221
- branch ? theme.fg("dim", branch) : "",
221
+ ], theme);
222
+ const primaryFocused = joinFooterParts([signature, context], theme);
223
+ const session = joinFooterParts([
222
224
  theme.fg("dim", formatTime(Date.now() - sessionStart)),
223
225
  theme.fg("dim", formatCost(getSessionCost(ctx))),
224
226
  ], theme);
225
- const focused = joinFooterParts([signature, context, goal], theme);
226
-
227
- if (activeGoal) {
228
- if (footerRowFits(rich, activeGoal, width)) {
229
- return [renderFooterRow(rich, activeGoal, width)];
230
- }
231
- if (footerRowFits(focused, activeGoal, width)) {
232
- return [renderFooterRow(focused, activeGoal, width)];
233
- }
234
- if (footerRowFits(context, activeGoal, width)) {
235
- return [renderFooterRow(context, activeGoal, width)];
236
- }
237
- return [renderFooterRow("", activeGoal, width)];
238
- }
239
-
240
- if (footerRowFits(rich, fullDirectory, width)) {
241
- return [renderFooterRow(rich, fullDirectory, width)];
242
- }
243
- if (footerRowFits(rich, focusedDirectory, width)) {
244
- return [renderFooterRow(rich, focusedDirectory, width)];
245
- }
246
- if (footerRowFits(focused, focusedDirectory, width)) {
247
- return [renderFooterRow(focused, focusedDirectory, width)];
248
- }
249
- if (footerRowFits(focused, "", width)) {
250
- return [renderFooterRow(focused, "", width)];
251
- }
252
- if (goal) {
253
- const essentialGoal = joinFooterParts([context, goal], theme);
254
- if (footerRowFits(essentialGoal, "", width)) return [renderFooterRow(essentialGoal, "", width)];
255
- return [renderFooterRow(goal, context, width)];
256
- }
257
-
258
227
  const essentialModel = formatModel(model, theme, false);
259
- return [renderFooterRow(essentialModel, context, width)];
228
+ const primaryRow = footerRowFits(primary, session, width)
229
+ ? renderFooterRow(primary, session, width)
230
+ : footerRowFits(primary, "", width)
231
+ ? renderFooterRow(primary, "", width)
232
+ : footerRowFits(primaryFocused, "", width)
233
+ ? renderFooterRow(primaryFocused, "", width)
234
+ : renderFooterRow(essentialModel, context, width);
235
+ const branchLabel = branch ? theme.fg("dim", branch) : "";
236
+ const workspaceRight = goal || fullDirectory;
237
+ const secondaryRow = footerRowFits(branchLabel, workspaceRight, width)
238
+ ? renderFooterRow(branchLabel, workspaceRight, width)
239
+ : goal
240
+ ? renderFooterRow("", goal, width)
241
+ : footerRowFits(branchLabel, focusedDirectory, width)
242
+ ? renderFooterRow(branchLabel, focusedDirectory, width)
243
+ : renderFooterRow(branchLabel, "", width);
244
+
245
+ return renderFooter([primaryRow, secondaryRow], width, theme);
260
246
  },
261
247
  };
262
248
  });