killeros 2.0.9 → 2.0.11

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,24 @@ All notable changes to KillerOS are documented here.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [2.0.11] - 2026-08-17
8
+
9
+ ### Fixed
10
+
11
+ - Made automated npm publication and GitHub release recovery idempotent, aligned the documented and tested Pi floor, and made machine-identifier casing locale-independent.
12
+ - Extended strict type checking to release scripts and typed test suites, while correctly identifying JavaScript-only suites as `.js`.
13
+
14
+ ### Removed
15
+
16
+ - Removed the decision-gated workflow subsystem, its skill-specific policy, public extension interface, and YAML dependency. Skills now remain instruction-only.
17
+
18
+ ## [2.0.10] - 2026-08-16
19
+
20
+ ### Added
21
+
22
+ - Added proactive turn-boundary compaction with Pi-owned settings and safe continuation for ordinary prompts and active goals.
23
+ - Added reusable multi-activation decision-gated workflow registrations while preserving exact-match behavior.
24
+
7
25
  ## [2.0.9] - 2026-08-15
8
26
 
9
27
  ### Added
package/Killeros.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import { registerRequestActivity } from "./killeros/activity.ts";
3
+ import { registerAutoCompaction } from "./killeros/auto-compaction.ts";
3
4
  import {
4
5
  createSlashCommandResolver,
5
6
  registerAliases,
@@ -14,33 +15,19 @@ import {
14
15
  type CompletionNotificationDependencies,
15
16
  } from "./killeros/notifications.ts";
16
17
  import { registerPersonalInstructions } from "./killeros/personal-instructions.ts";
17
- import { registerQuestionTool, type QuestionRunner } from "./killeros/question.ts";
18
- import { createDecisionGatedWorkflowAdapter } from "./killeros/decision-gated-workflow.ts";
18
+ import { registerQuestionTool } from "./killeros/question.ts";
19
19
  import { createGoalRuntime, createInitRuntime } from "./killeros/runtime.ts";
20
20
  import { registerShellUi } from "./killeros/shell-ui.ts";
21
21
  import { registerVariants } from "./killeros/variants.ts";
22
22
  import { registerWorkedFor } from "./killeros/worked-for.ts";
23
- import { registerWorkflowGate, type WorkflowAdapter } from "./killeros/workflow-gate.ts";
24
23
 
25
24
  export { contextPercentRemaining, formatCost, formatContextProgress } from "./killeros/footer.ts";
26
25
  export { executeHook } from "./killeros/hooks.ts";
27
26
  export { INIT_WORKFLOW_PROMPT } from "./killeros/init.ts";
28
27
  export { buildInitEvidence, listInitEvidence, readInitEvidence } from "./killeros/init-evidence.ts";
29
28
  export { captureInitTargetBaseline, installInitAgentsFile, validateGeneratedGuidance, writeInitAgentsFile } from "./killeros/init-target.ts";
30
- export { createDecisionGatedWorkflowAdapter } from "./killeros/decision-gated-workflow.ts";
31
- export { explicitSkillActivation, registerWorkflowGate } from "./killeros/workflow-gate.ts";
32
- export type {
33
- WorkflowAdapter,
34
- WorkflowGateController,
35
- WorkflowGateState,
36
- WorkflowPolicy,
37
- WorkflowTerminalReason,
38
- WorkflowToolAuthorization,
39
- } from "./killeros/workflow-gate.ts";
40
-
41
29
  export interface KillerosOptions {
42
30
  completionNotifications?: CompletionNotificationDependencies;
43
- decisionGatedWorkflows?: readonly WorkflowAdapter[];
44
31
  }
45
32
 
46
33
  export default function Killeros(pi: ExtensionAPI, options: KillerosOptions = {}): void {
@@ -50,19 +37,15 @@ export default function Killeros(pi: ExtensionAPI, options: KillerosOptions = {}
50
37
  registerShellUi(pi, commandResolver);
51
38
  registerGoal(pi, goalRuntime, initRuntime);
52
39
  registerPersonalInstructions(pi, initRuntime);
53
- const questionRunner: QuestionRunner = registerQuestionTool(pi);
54
- registerWorkflowGate(
55
- pi,
56
- questionRunner,
57
- options.decisionGatedWorkflows ?? [createDecisionGatedWorkflowAdapter()],
58
- );
40
+ registerQuestionTool(pi);
59
41
  registerAliases(pi);
60
42
  registerSlashAutocomplete(pi, commandResolver);
61
43
  registerFooter(pi, goalRuntime);
62
44
  registerVariants(pi);
63
45
  registerInitCommand(pi, initRuntime, goalRuntime);
64
46
  registerLifecycleHooks(pi);
65
- registerGoalSettlement(pi, goalRuntime, initRuntime);
47
+ const goalCompaction = registerGoalSettlement(pi, goalRuntime, initRuntime);
48
+ registerAutoCompaction(pi, { goal: goalCompaction });
66
49
  registerInitSettlement(pi, initRuntime);
67
50
  registerRequestActivity(pi);
68
51
  registerCompletionNotifications(pi, options.completionNotifications);
package/README.md CHANGED
@@ -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.9
34
+ pi install git:github.com/KyrosHendrix/pi-KillerOS@v2.0.11
35
35
  ```
36
36
 
37
37
  Add `-l` to either command for a project-only install. Restart Pi after installing.
@@ -44,12 +44,11 @@ Add `-l` to either command for a project-only install. Restart Pi after installi
44
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
45
  - One compact TUI transcript line reporting truthful `Done`, `Stopped`, or `Failed` settlement with elapsed time while preserving older `✻ Worked for …` entries
46
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
47
- - Pi-owned context compaction with active goals continuing from Pi's settled boundary after manual, threshold, and overflow compaction
47
+ - Proactive turn-boundary context compaction in TUI and RPC modes, with ordinary prompts and active goals continuing safely after a successful summary
48
48
  - Optional completion sounds after successful or failed settled requests, excluding manual aborts
49
49
  - `/variants` selector and direct reasoning-level arguments
50
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
51
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`
52
- - Opt-in decision-gated workflows that ask a structured policy question before explicit skill expansion, preserve the selected allowlist, and fail closed across tool and session boundaries
53
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
54
53
  - Mid-prompt slash completion with current Pi `0.84.2` commands, extensions, prompts, and skills; paths, URLs, and invalid commands remain plain text
55
54
  - Goal-aware `/clear` that confirms, aborts active work, waits for settlement, and starts a new session, plus `/exit` for graceful shutdown
@@ -79,10 +78,6 @@ Add `-l` to either command for a project-only install. Restart Pi after installi
79
78
 
80
79
  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.
81
80
 
82
- ### Decision-gated workflows
83
-
84
- Explicit `/skill:decision-gated-workflow` activation opens the shared question UI before Pi expands the skill. `Normal` allows only interview and read-only tools; `With docs` additionally permits agreed glossary, context-map, and ADR paths. The selected policy remains active until the workflow is explicitly finished or cancelled, and lifecycle changes clear it safely. Extensions can supply additional adapters through `KillerosOptions.decisionGatedWorkflows`.
85
-
86
81
  ### Interactive questions
87
82
 
88
83
  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.
@@ -97,9 +92,22 @@ KillerOS activates its packaged `killeros` theme when a TUI session starts. Tool
97
92
 
98
93
  The completion sound is a global user preference stored in Pi's agent directory and is off by default. Run `/notification` in TUI mode to enable or disable it; enabling does not play a preview. Enabled TUI tabs append `󰂚`, which requires a Nerd Font in the terminal tab UI. An unsupported font may show a box without affecting sound. KillerOS uses the terminal's audible bell and cannot produce sound when the terminal disables it.
99
94
 
95
+ Automatic compaction is enabled by default when Pi's effective `compaction.enabled` setting is true. After each completed assistant turn, including its tool execution, KillerOS reads the active model's current context usage and triggers the public Pi compaction API when `remainingTokens <= max(contextWindow * percentRemaining / 100, reserveTokens)`. The default `percentRemaining` is `15`; `reserveTokens` and `keepRecentTokens` remain Pi-owned. KillerOS reads those effective settings with Pi's public `SettingsManager` using `getAgentDir()` and the current project trust state. The KillerOS-only preference lives in the same global `killeros.json` file:
96
+
97
+ ```json
98
+ {
99
+ "autoCompaction": {
100
+ "enabled": true,
101
+ "percentRemaining": 15
102
+ }
103
+ }
104
+ ```
105
+
106
+ Missing context readings skip the check. Successful ordinary-prompt compaction queues one hidden continuation; active `/goal` runs use the existing session-compaction and goal-continuation path. A failed compaction does not automatically retry. Manual `/compact` behavior is unchanged.
107
+
100
108
  KillerOS displays session costs in USD. The footer uses Pi's human-readable model name when available, keeps the provider visually secondary, and renders context as `percent left (tokens)` without a progress bar. An active goal replaces the right-side path with warning-yellow `/goal is active (...)` and keeps exact seconds in minute and hour formats. Paused and blocked goals retain their existing placement; completed goals remain in transcript history and `/goal` status rather than the footer. At narrow widths, context pressure and actionable goal state take priority.
101
109
 
102
- Pi decides when compaction runs and Pi writes the summary, applies manual focus instructions, tracks files, retries summarization, and handles overflow recovery. KillerOS does not add a second threshold or replace Pi's summary. Active `/goal` work continues from Pi's settled boundary, after Pi finishes retries, compaction, and queued work.
110
+ Pi writes the summary, applies manual focus instructions, tracks files, retries summarization, and handles overflow recovery. KillerOS owns only the proactive turn-boundary trigger and does not replace Pi's compaction implementation. Active `/goal` work continues from the settled compaction boundary, after Pi finishes retries, compaction, and queued work.
103
111
 
104
112
  Manual `/compact` aborts the current goal turn before summarization, so KillerOS records an honest temporary pause for that exact goal revision. After Pi saves the manual summary, KillerOS resumes that revision automatically. A failed or cancelled manual compaction stays paused; run `/goal pause` during the pause to cancel automatic recovery.
105
113
 
@@ -111,9 +119,9 @@ Lifecycle hooks are loaded from `.pi/killeros-hooks.json` at session start. Supp
111
119
 
112
120
  | Mode | Behavior |
113
121
  |---|---|
114
- | TUI | All features are available, including the completion sound and tab-title indicator |
115
- | RPC | Goal set/view/pause/resume/clear work; TUI components, `/goal edit`, `/init`, completion sounds, and the title indicator are disabled |
116
- | Print/JSON | Interactive questions, `/goal`, and `/init` fail explicitly; completion sounds and the title indicator are disabled |
122
+ | TUI | All features are available, including proactive compaction, the completion sound, and the tab-title indicator |
123
+ | RPC | Proactive compaction and goal set/view/pause/resume/clear work; TUI components, `/goal edit`, `/init`, completion sounds, and the title indicator are disabled |
124
+ | Print/JSON | Interactive questions, `/goal`, `/init`, and proactive compaction are disabled; completion sounds and the title indicator are disabled |
117
125
 
118
126
  ## Validation
119
127
 
@@ -131,16 +139,9 @@ The package manifest lists Pi’s built-in modules as peer dependencies, so npm
131
139
 
132
140
  ## Publish
133
141
 
134
- 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.
135
-
136
- 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.
137
-
138
- 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:
142
+ To release KillerOS, 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 publishes the package to npm through trusted publishing, then creates the matching tag and GitHub release from that verified commit. The [`pi-package`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/packages.md) keyword makes the npm release visible in Pi’s package catalog.
139
143
 
140
- ```bash
141
- npm login
142
- npm publish
143
- ```
144
+ Do not manually tag a normal release. If automation must recover a missing GitHub release, push the matching version tag. The workflow validates the tag against the package and changelog, skips npm publication when that version already exists, and creates only the missing release.
144
145
 
145
146
  ## Security
146
147
 
@@ -30,7 +30,7 @@ export function formatActivityMessage(message: ActivityMessage, theme: Theme): s
30
30
  detail = "assembling the answer";
31
31
  break;
32
32
  case "tool":
33
- switch (message.toolName.trim().toLocaleLowerCase()) {
33
+ switch (message.toolName.trim().toLowerCase()) {
34
34
  case "read":
35
35
  case "grep":
36
36
  case "find":
@@ -0,0 +1,224 @@
1
+ import {
2
+ getAgentDir,
3
+ SettingsManager,
4
+ type CompactionSettings,
5
+ type ContextUsage,
6
+ type ExtensionAPI,
7
+ type ExtensionContext,
8
+ } from "@earendil-works/pi-coding-agent";
9
+ import { createKillerosSettingsStore } from "./settings.ts";
10
+
11
+ export const DEFAULT_AUTO_COMPACTION_PERCENT_REMAINING = 15;
12
+ export const AUTO_COMPACTION_MESSAGE_TYPE = "killeros-auto-compaction";
13
+ export const AUTO_COMPACTION_MESSAGE = "Continue the interrupted task from the compacted context.";
14
+
15
+ export interface AutoCompactionPreference {
16
+ enabled: boolean;
17
+ percentRemaining: number;
18
+ }
19
+
20
+ export interface AutoCompactionGoalHandlers {
21
+ isActive(ctx: ExtensionContext): boolean;
22
+ onRequested(): void;
23
+ onCompleted(ctx: ExtensionContext): void;
24
+ onFailed(ctx: ExtensionContext, error: unknown): void;
25
+ }
26
+
27
+ export interface AutoCompactionDependencies {
28
+ loadPreference?: (ctx: ExtensionContext) => AutoCompactionPreference;
29
+ getCompactionSettings?: (ctx: ExtensionContext) => CompactionSettings;
30
+ goal?: AutoCompactionGoalHandlers;
31
+ }
32
+
33
+ interface AutoCompactionRequest {
34
+ goal: boolean;
35
+ token: symbol;
36
+ }
37
+
38
+ function isRecord(value: unknown): value is Record<string, unknown> {
39
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
40
+ }
41
+
42
+ export function readAutoCompactionPreference(
43
+ settings: Readonly<Record<string, unknown>>,
44
+ ): AutoCompactionPreference {
45
+ const raw = isRecord(settings.autoCompaction) ? settings.autoCompaction : {};
46
+ const percentRemaining = typeof raw.percentRemaining === "number"
47
+ && Number.isFinite(raw.percentRemaining)
48
+ && raw.percentRemaining >= 0
49
+ && raw.percentRemaining <= 100
50
+ ? raw.percentRemaining
51
+ : DEFAULT_AUTO_COMPACTION_PERCENT_REMAINING;
52
+ return {
53
+ enabled: typeof raw.enabled === "boolean" ? raw.enabled : true,
54
+ percentRemaining,
55
+ };
56
+ }
57
+
58
+ function defaultPreference(): AutoCompactionPreference {
59
+ return readAutoCompactionPreference(createKillerosSettingsStore().load());
60
+ }
61
+
62
+ function defaultCompactionSettings(ctx: ExtensionContext): CompactionSettings {
63
+ return SettingsManager.create(ctx.cwd, getAgentDir(), {
64
+ projectTrusted: ctx.isProjectTrusted(),
65
+ }).getCompactionSettings();
66
+ }
67
+
68
+ function reserveTokens(settings: Pick<CompactionSettings, "reserveTokens">): number {
69
+ return typeof settings.reserveTokens === "number" && Number.isFinite(settings.reserveTokens)
70
+ ? Math.max(0, settings.reserveTokens)
71
+ : 0;
72
+ }
73
+
74
+ export function shouldTriggerAutoCompaction(
75
+ usage: Pick<ContextUsage, "tokens" | "contextWindow"> | undefined,
76
+ preference: AutoCompactionPreference,
77
+ compactionSettings: Pick<CompactionSettings, "enabled" | "reserveTokens">,
78
+ ): boolean {
79
+ if (!preference.enabled || !compactionSettings.enabled || usage?.tokens === undefined || usage.tokens === null) {
80
+ return false;
81
+ }
82
+ if (!Number.isFinite(usage.tokens) || !Number.isFinite(usage.contextWindow) || usage.contextWindow <= 0) {
83
+ return false;
84
+ }
85
+ const remainingTokens = usage.contextWindow - usage.tokens;
86
+ const threshold = Math.max(
87
+ usage.contextWindow * preference.percentRemaining / 100,
88
+ reserveTokens(compactionSettings),
89
+ );
90
+ return remainingTokens <= threshold;
91
+ }
92
+
93
+ function errorMessage(error: unknown): string {
94
+ return error instanceof Error ? error.message : String(error);
95
+ }
96
+
97
+ function supportedMode(ctx: ExtensionContext): boolean {
98
+ return ctx.mode === "tui" || ctx.mode === "rpc";
99
+ }
100
+
101
+ export function registerAutoCompaction(
102
+ pi: ExtensionAPI,
103
+ dependencies: AutoCompactionDependencies = {},
104
+ ): void {
105
+ const loadPreference = dependencies.loadPreference ?? (() => defaultPreference());
106
+ const getCompactionSettings = dependencies.getCompactionSettings ?? defaultCompactionSettings;
107
+ let request: AutoCompactionRequest | undefined;
108
+ let armed = true;
109
+ let settingsErrorReported = false;
110
+
111
+ const resetForLifecycle = (): void => {
112
+ request = undefined;
113
+ armed = true;
114
+ settingsErrorReported = false;
115
+ };
116
+
117
+ const notifyFailure = (ctx: ExtensionContext, error: unknown): void => {
118
+ ctx.ui.notify(`Automatic compaction failed: ${errorMessage(error)}`, "error");
119
+ };
120
+
121
+ const finishFailure = (
122
+ ctx: ExtensionContext,
123
+ token: symbol,
124
+ goal: boolean,
125
+ error: unknown,
126
+ ): void => {
127
+ if (!request || request.token !== token) return;
128
+ request = undefined;
129
+ if (goal && dependencies.goal) {
130
+ try {
131
+ dependencies.goal.onFailed(ctx, error);
132
+ } catch (callbackError) {
133
+ notifyFailure(ctx, callbackError);
134
+ }
135
+ return;
136
+ }
137
+ notifyFailure(ctx, error);
138
+ };
139
+
140
+ pi.on("turn_end", (_event, ctx) => {
141
+ if (!supportedMode(ctx) || request) return;
142
+
143
+ let preference: AutoCompactionPreference;
144
+ let compactionSettings: CompactionSettings;
145
+ let usage: ContextUsage | undefined;
146
+ try {
147
+ preference = loadPreference(ctx);
148
+ compactionSettings = getCompactionSettings(ctx);
149
+ } catch (error) {
150
+ if (!settingsErrorReported) {
151
+ settingsErrorReported = true;
152
+ ctx.ui.notify(`Automatic compaction settings could not be read: ${errorMessage(error)}`, "error");
153
+ }
154
+ return;
155
+ }
156
+ try {
157
+ usage = ctx.getContextUsage();
158
+ } catch {
159
+ return;
160
+ }
161
+ if (!usage) return;
162
+ if (usage.tokens !== null && usage.tokens !== undefined && Number.isFinite(usage.tokens)
163
+ && Number.isFinite(usage.contextWindow) && usage.contextWindow > 0) {
164
+ const remainingTokens = usage.contextWindow - usage.tokens;
165
+ const threshold = Math.max(
166
+ usage.contextWindow * preference.percentRemaining / 100,
167
+ reserveTokens(compactionSettings),
168
+ );
169
+ if (remainingTokens > threshold) {
170
+ armed = true;
171
+ }
172
+ }
173
+ if (!armed) return;
174
+ if (!shouldTriggerAutoCompaction(usage, preference, compactionSettings)) return;
175
+
176
+ armed = false;
177
+ const token = Symbol();
178
+ const goal = dependencies.goal?.isActive(ctx) === true;
179
+ request = { goal, token };
180
+ if (goal && dependencies.goal) {
181
+ try {
182
+ dependencies.goal.onRequested();
183
+ } catch (error) {
184
+ finishFailure(ctx, token, true, error);
185
+ return;
186
+ }
187
+ }
188
+
189
+ try {
190
+ ctx.compact({
191
+ onComplete: () => {
192
+ if (!request || request.token !== token) return;
193
+ request = undefined;
194
+ if (goal && dependencies.goal) {
195
+ try {
196
+ dependencies.goal.onCompleted(ctx);
197
+ } catch (error) {
198
+ notifyFailure(ctx, error);
199
+ }
200
+ return;
201
+ }
202
+ try {
203
+ pi.sendMessage({
204
+ customType: AUTO_COMPACTION_MESSAGE_TYPE,
205
+ content: AUTO_COMPACTION_MESSAGE,
206
+ display: false,
207
+ }, { triggerTurn: true, deliverAs: "followUp" });
208
+ } catch (error) {
209
+ notifyFailure(ctx, error);
210
+ }
211
+ },
212
+ onError: (error) => finishFailure(ctx, token, goal, error),
213
+ });
214
+ } catch (error) {
215
+ finishFailure(ctx, token, goal, error);
216
+ }
217
+ });
218
+
219
+ pi.on("session_start", resetForLifecycle);
220
+ pi.on("session_shutdown", resetForLifecycle);
221
+ pi.on("session_tree", resetForLifecycle);
222
+ pi.on("session_before_switch", resetForLifecycle);
223
+ pi.on("session_before_fork", resetForLifecycle);
224
+ }
@@ -166,8 +166,8 @@ export function createSlashCommandResolver(
166
166
 
167
167
  function scoreCommandMatch(name: string, prefix: string): number {
168
168
  if (!prefix) return 1;
169
- const normalizedName = name.toLocaleLowerCase();
170
- const normalizedPrefix = prefix.toLocaleLowerCase();
169
+ const normalizedName = name.toLowerCase();
170
+ const normalizedPrefix = prefix.toLowerCase();
171
171
  if (normalizedName.startsWith(normalizedPrefix)) return 100;
172
172
  if (normalizedName.split(/[:\-_]/).some((token) => token.startsWith(normalizedPrefix))) return 80;
173
173
  if (normalizedName.includes(normalizedPrefix)) return 50;
@@ -190,7 +190,7 @@ export function registerSlashAutocomplete(
190
190
  const prefixMatch = getSlashCommandPrefix(beforeCursor);
191
191
  if (!prefixMatch) return current.getSuggestions(lines, cursorLine, cursorCol, options);
192
192
 
193
- const prefix = prefixMatch.prefix.toLocaleLowerCase();
193
+ const prefix = prefixMatch.prefix.toLowerCase();
194
194
  const baseSuggestions = await current.getSuggestions(lines, cursorLine, cursorCol, options);
195
195
  resolver.updateFallbackCommands(baseSuggestions?.items ?? []);
196
196
  const commands = resolver.getCommandCatalog(baseSuggestions?.items ?? []);
@@ -6,8 +6,8 @@ export function formatCwd(cwd: string): string {
6
6
  if (!home) return cwd;
7
7
  const normalizedHome = home.replace(/[\\/]+$/, "");
8
8
  const normalizedCwd = cwd.replace(/[\\/]+$/, "");
9
- const comparedHome = process.platform === "win32" ? normalizedHome.toLocaleLowerCase() : normalizedHome;
10
- const comparedCwd = process.platform === "win32" ? normalizedCwd.toLocaleLowerCase() : normalizedCwd;
9
+ const comparedHome = process.platform === "win32" ? normalizedHome.toLowerCase() : normalizedHome;
10
+ const comparedCwd = process.platform === "win32" ? normalizedCwd.toLowerCase() : normalizedCwd;
11
11
  if (comparedCwd === comparedHome) return "~";
12
12
  const separator = normalizedCwd.slice(normalizedHome.length, normalizedHome.length + 1);
13
13
  return comparedCwd.startsWith(comparedHome) && (separator === "/" || separator === "\\")
@@ -75,12 +75,12 @@ const PROVIDER_WORDS: Readonly<Record<string, string>> = {
75
75
 
76
76
  function formatProviderName(provider: string): string {
77
77
  const normalized = provider.trim();
78
- const known = PROVIDER_LABELS[normalized.toLocaleLowerCase()];
78
+ const known = PROVIDER_LABELS[normalized.toLowerCase()];
79
79
  if (known) return known;
80
80
  return normalized
81
81
  .split(/[-_]+/u)
82
82
  .filter(Boolean)
83
- .map((word) => PROVIDER_WORDS[word.toLocaleLowerCase()] ?? `${word.charAt(0).toLocaleUpperCase()}${word.slice(1)}`)
83
+ .map((word) => PROVIDER_WORDS[word.toLowerCase()] ?? `${word.charAt(0).toUpperCase()}${word.slice(1)}`)
84
84
  .join(" ") || "Unknown provider";
85
85
  }
86
86
 
package/killeros/goals.ts CHANGED
@@ -264,7 +264,10 @@ function transitionGoal(
264
264
  resumeAfterManualCompaction: options.resumeAfterManualCompaction,
265
265
  };
266
266
  persistGoalState(pi, runtime, event, next);
267
- if (status !== "active") runtime.continuationScheduled = false;
267
+ if (status !== "active") {
268
+ runtime.continuationScheduled = false;
269
+ runtime.automaticCompaction = undefined;
270
+ }
268
271
  return next;
269
272
  }
270
273
 
@@ -272,6 +275,7 @@ function clearGoalExecutionFlags(runtime: GoalRuntime): void {
272
275
  runtime.continuationScheduled = false;
273
276
  runtime.goalTurnInFlight = false;
274
277
  runtime.agentEndObserved = false;
278
+ runtime.automaticCompaction = undefined;
275
279
  runtime.lastStopReason = undefined;
276
280
  runtime.lastError = undefined;
277
281
  }
@@ -287,7 +291,7 @@ async function stopGoalRun(runtime: GoalRuntime, ctx: ExtensionCommandContext, s
287
291
  }
288
292
 
289
293
  function goalStatusLabel(status: GoalStatus): string {
290
- return `${status.charAt(0).toLocaleUpperCase()}${status.slice(1)}`;
294
+ return `${status.charAt(0).toUpperCase()}${status.slice(1)}`;
291
295
  }
292
296
 
293
297
  function goalPanelActions(status: GoalStatus): Array<{ label: string; control: "pause" | "resume" | "edit" | "clear" }> {
@@ -305,7 +309,7 @@ function goalPanelActions(status: GoalStatus): Array<{ label: string; control: "
305
309
  function goalStatusSummary(state: GoalState, ctx: ExtensionContext): string {
306
310
  const usedTokens = Math.max(0, sumGoalTokens(ctx) - state.baselineTokens);
307
311
  const lines = [
308
- `Goal ${goalStatusLabel(state.status).toLocaleLowerCase()} · ${state.turns} turn${state.turns === 1 ? "" : "s"} · ${formatTime(goalElapsedMilliseconds(state))} · ${formatTokens(usedTokens)} tokens`,
312
+ `Goal ${goalStatusLabel(state.status).toLowerCase()} · ${state.turns} turn${state.turns === 1 ? "" : "s"} · ${formatTime(goalElapsedMilliseconds(state))} · ${formatTokens(usedTokens)} tokens`,
309
313
  state.objective,
310
314
  ];
311
315
  if (state.result) lines.push(state.result);
@@ -333,6 +337,7 @@ export function pauseGoalAfterFailure(
333
337
  syncGoalUpdateTool(pi, runtime);
334
338
  runtime.persistenceRetryNeeded = true;
335
339
  runtime.continuationScheduled = false;
340
+ runtime.automaticCompaction = undefined;
336
341
  runtime.requestRender?.();
337
342
  }
338
343
  if (notify) ctx.ui.notify(`Goal paused: ${reason}\n${recoveryInstruction}`, "error");
@@ -359,6 +364,7 @@ function pauseGoalForPossibleManualCompaction(
359
364
  syncGoalUpdateTool(pi, runtime);
360
365
  runtime.persistenceRetryNeeded = true;
361
366
  runtime.continuationScheduled = false;
367
+ runtime.automaticCompaction = undefined;
362
368
  runtime.requestRender?.();
363
369
  }
364
370
  ctx.ui.notify(
@@ -384,6 +390,7 @@ function recoverGoalAfterManualCompaction(
384
390
  return false;
385
391
  }
386
392
  runtime.continuationScheduled = false;
393
+ runtime.automaticCompaction = undefined;
387
394
  ctx.ui.notify("Manual compaction complete. Goal resumed.", "info");
388
395
  setImmediate(() => scheduleGoalContinuation(pi, runtime, initState, ctx));
389
396
  return true;
@@ -450,6 +457,42 @@ function scheduleGoalContinuation(
450
457
  }
451
458
  }
452
459
 
460
+ function completeAutomaticCompaction(
461
+ pi: ExtensionAPI,
462
+ runtime: GoalRuntime,
463
+ initState: InitRuntime,
464
+ ctx: ExtensionContext,
465
+ ): void {
466
+ if (runtime.automaticCompaction === undefined) return;
467
+ if (runtime.state?.status !== "active" || initState.active) {
468
+ runtime.automaticCompaction = undefined;
469
+ return;
470
+ }
471
+ runtime.automaticCompaction = "completed";
472
+ if (runtime.goalTurnInFlight || !ctx.isIdle()) return;
473
+ runtime.automaticCompaction = undefined;
474
+ setImmediate(() => scheduleGoalContinuation(pi, runtime, initState, ctx));
475
+ }
476
+
477
+ function failAutomaticCompaction(
478
+ pi: ExtensionAPI,
479
+ runtime: GoalRuntime,
480
+ ctx: ExtensionContext,
481
+ error: unknown,
482
+ ): void {
483
+ if (runtime.automaticCompaction === undefined) return;
484
+ runtime.automaticCompaction = undefined;
485
+ if (runtime.state?.status !== "active") return;
486
+ const reason = error instanceof Error ? error.message : String(error);
487
+ pauseGoalAfterFailure(
488
+ pi,
489
+ runtime,
490
+ ctx,
491
+ `automatic compaction failed: ${reason}`,
492
+ "Automatic continuation is stopped. Run /goal resume after resolving the compaction problem.",
493
+ );
494
+ }
495
+
453
496
  function goalInstructions(state: GoalState, heading: string): string {
454
497
  return [
455
498
  `# ${heading}`,
@@ -598,6 +641,7 @@ export function registerGoal(
598
641
  runtime.continuationHeld = false;
599
642
  runtime.goalTurnInFlight = false;
600
643
  runtime.agentEndObserved = false;
644
+ runtime.automaticCompaction = undefined;
601
645
  runtime.persistenceRetryNeeded = false;
602
646
  runtime.lastStopReason = undefined;
603
647
  runtime.lastError = undefined;
@@ -630,6 +674,7 @@ export function registerGoal(
630
674
  runtime.continuationHeld = false;
631
675
  runtime.goalTurnInFlight = false;
632
676
  runtime.agentEndObserved = false;
677
+ runtime.automaticCompaction = undefined;
633
678
  runtime.persistenceRetryNeeded = false;
634
679
  runtime.lastStopReason = undefined;
635
680
  runtime.lastError = undefined;
@@ -663,7 +708,7 @@ export function registerGoal(
663
708
  return;
664
709
  }
665
710
  const input = args.trim();
666
- const control = input.toLocaleLowerCase();
711
+ const control = input.toLowerCase();
667
712
  const isControl = control === "clear" || control === "edit" || control === "pause" || control === "resume";
668
713
 
669
714
  if (!input) {
@@ -963,7 +1008,7 @@ export function registerGoal(
963
1008
  pi.registerCommand("goal", {
964
1009
  description: "Set or view the goal for a long-running task",
965
1010
  getArgumentCompletions: (prefix) => {
966
- const normalized = prefix.trimStart().toLocaleLowerCase();
1011
+ const normalized = prefix.trimStart().toLowerCase();
967
1012
  if (normalized.includes(" ")) return null;
968
1013
  const actions = [
969
1014
  { value: "clear", description: "Remove the current goal" },
@@ -983,7 +1028,12 @@ export function registerGoalSettlement(
983
1028
  pi: ExtensionAPI,
984
1029
  runtime: GoalRuntime,
985
1030
  initState: InitRuntime,
986
- ): void {
1031
+ ): {
1032
+ isActive(ctx: ExtensionContext): boolean;
1033
+ onRequested(): void;
1034
+ onCompleted(ctx: ExtensionContext): void;
1035
+ onFailed(ctx: ExtensionContext, error: unknown): void;
1036
+ } {
987
1037
  pi.on("agent_settled", (_event, ctx) => {
988
1038
  const wasGoalTurn = runtime.goalTurnInFlight;
989
1039
  const continuationWasScheduled = runtime.continuationScheduled;
@@ -1000,6 +1050,7 @@ export function registerGoalSettlement(
1000
1050
  return;
1001
1051
  }
1002
1052
  if (!agentEndObserved) {
1053
+ if (runtime.automaticCompaction !== undefined) return;
1003
1054
  pauseGoalAfterFailure(pi, runtime, ctx, "the goal turn ended without an agent result");
1004
1055
  return;
1005
1056
  }
@@ -1007,6 +1058,12 @@ export function registerGoalSettlement(
1007
1058
  const reason = runtime.lastError || "the agent turn was aborted";
1008
1059
  runtime.lastStopReason = undefined;
1009
1060
  runtime.lastError = undefined;
1061
+ if (runtime.automaticCompaction !== undefined) {
1062
+ if (runtime.automaticCompaction === "completed") {
1063
+ completeAutomaticCompaction(pi, runtime, initState, ctx);
1064
+ }
1065
+ return;
1066
+ }
1010
1067
  pauseGoalForPossibleManualCompaction(pi, runtime, ctx, reason);
1011
1068
  return;
1012
1069
  }
@@ -1023,7 +1080,23 @@ export function registerGoalSettlement(
1023
1080
  });
1024
1081
 
1025
1082
  pi.on("session_compact", (event, ctx) => {
1083
+ if (runtime.automaticCompaction !== undefined) {
1084
+ completeAutomaticCompaction(pi, runtime, initState, ctx);
1085
+ return;
1086
+ }
1026
1087
  if (event.reason !== "manual") return;
1027
1088
  recoverGoalAfterManualCompaction(pi, runtime, initState, ctx);
1028
1089
  });
1090
+
1091
+ return {
1092
+ isActive: (ctx: ExtensionContext): boolean => isGoalModeSupported(ctx)
1093
+ && isSavedSession(ctx)
1094
+ && runtime.state?.status === "active"
1095
+ && !initState.active,
1096
+ onRequested: (): void => {
1097
+ if (runtime.state?.status === "active") runtime.automaticCompaction = "pending";
1098
+ },
1099
+ onCompleted: (ctx: ExtensionContext): void => completeAutomaticCompaction(pi, runtime, initState, ctx),
1100
+ onFailed: (ctx: ExtensionContext, error: unknown): void => failAutomaticCompaction(pi, runtime, ctx, error),
1101
+ };
1029
1102
  }