killeros 2.0.3 → 2.0.5

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,43 @@ All notable changes to KillerOS are documented here.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [2.0.5] - 2026-08-11
8
+
9
+ ### Added
10
+
11
+ - Added shuffled session-stable suggestions to the empty three-line prompt editor.
12
+
13
+ ### Fixed
14
+
15
+ - Preserved complete UTF-8 characters in truncated `/init` evidence and chunked lifecycle-hook output, and kept filesystem-root paths visible in notification titles.
16
+ - Kept the startup-tip shuffle deck across session rebinds so every tip appears before the bank repeats.
17
+ - Rejected matchers on `agent_settled` lifecycle hooks instead of silently bypassing their tool-name guard.
18
+ - Made the goal update status schema compatible with Google models while preserving strict completion and blocking values.
19
+ - Kept multiline custom-answer drafts within tiny terminal row limits by rendering a clipped single-line preview.
20
+ - Preserved existing spaces and tabs after the cursor when applying slash-command autocomplete.
21
+ - Made `/init` abort safely when Git ignore inspection fails or returns untrusted output, preventing ignored files from entering model evidence.
22
+ - Wrapped long interactive questions to the current terminal width while keeping the question UI within the terminal height.
23
+ - Made `/goal pause` and `/goal clear` save terminal state before immediately stopping active goal work.
24
+ - Kept failed goal edits and replacements visible and fail-closed without restarting a stale objective.
25
+ - Required one durable blocker key on three consecutive goal turns before a goal can be marked blocked.
26
+
27
+ ## [2.0.4] - 2026-08-10
28
+
29
+ ### Changed
30
+
31
+ - Rebuilt `/init` around a packaged four-section guideline-synthesis workflow that preserves compatible root policy without requiring an external skill.
32
+ - Removed live blue slash-command coloring while retaining slash autocomplete, the framed multiline editor, and Shift+Enter.
33
+
34
+ ### Fixed
35
+
36
+ - Prevented `/init` from reading Git-ignored files, known secrets, private keys, linked or non-regular files, other guidance, dependencies, and files outside its frozen evidence map.
37
+ - Protected root `AGENTS.md` from concurrent replacement and reported incompatible policy conflicts without writing.
38
+ - Made `/clear` confirm before aborting active work, wait for settlement, and then start the new session.
39
+ - Preserved explicit Responses API verbosity and reasoning-summary settings while applying concise defaults only to absent fields.
40
+ - Cancelled lifecycle hook process trees with bounded cleanup when Pi aborts the parent request.
41
+ - Kept `/init` middleware from freezing shared tool input and preserved custom editors installed by other extensions.
42
+ - Removed private Pi editor-state access in favor of public rendering contracts.
43
+
7
44
  ## [2.0.3] - 2026-08-09
8
45
 
9
46
  ### Added
package/Killeros.ts CHANGED
@@ -15,10 +15,12 @@ import { createGoalRuntime, createInitRuntime } from "./killeros/runtime.ts";
15
15
  import { registerShellUi } from "./killeros/shell-ui.ts";
16
16
  import { registerVariants } from "./killeros/variants.ts";
17
17
 
18
- export { CONCISE_SYSTEM_PROMPT, isConcisedEnabled } from "./killeros/concise.ts";
18
+ export { CONCISE_SYSTEM_PROMPT, isConciseEnabled, isConcisedEnabled } from "./killeros/concise.ts";
19
19
  export { contextPercentRemaining, formatCost, formatContextProgress } from "./killeros/footer.ts";
20
20
  export { executeHook } from "./killeros/hooks.ts";
21
- export { INIT_WORKFLOW_PROMPT, writeInitAgentsFile } from "./killeros/init.ts";
21
+ export { INIT_WORKFLOW_PROMPT } from "./killeros/init.ts";
22
+ export { buildInitEvidence, listInitEvidence, readInitEvidence } from "./killeros/init-evidence.ts";
23
+ export { captureInitTargetBaseline, installInitAgentsFile, validateGeneratedGuidance, writeInitAgentsFile } from "./killeros/init-target.ts";
22
24
 
23
25
  export interface KillerosOptions {
24
26
  completionNotifications?: CompletionNotificationDependencies;
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.3
34
+ pi install git:github.com/KyrosHendrix/pi-KillerOS@v2.0.5
35
35
  ```
36
36
 
37
37
  Add `-l` to either command for a project-only install. Restart Pi after installing.
@@ -41,17 +41,17 @@ Add `-l` to either command for a project-only install. Restart Pi after installi
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
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
- - Framed multiline editor with Shift+Enter support and live command-blue highlighting for recognized slash command prefixes
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
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
46
46
  - Pi-owned context compaction with active goals continuing from Pi's settled boundary after manual, threshold, and overflow compaction
47
47
  - Optional completion sounds after successful or failed settled requests, excluding manual aborts
48
48
  - `/variants` selector and direct reasoning-level arguments
49
- - Codex-style `/goal` with an interactive status/action panel, durable objectives, pause, resume, edit, confirmed panel clearing, automatic continuation, and explicit completion
50
- - Claude Code-style `/init` that scans the repository and generates a concise root `AGENTS.md` without setup questions
49
+ - 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
+ - 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
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
52
  - Mid-prompt slash completion with current Pi `0.82.1` commands, extensions, prompts, and skills; paths, URLs, and invalid commands remain plain text
53
- - `/clear` for a confirmed new session, plus `/exit` for graceful shutdown
54
- - Concise system-prompt guidance without modifying completed assistant messages
53
+ - Goal-aware `/clear` that confirms, aborts active work, waits for settlement, and starts a new session, plus `/exit` for graceful shutdown
54
+ - Concise system-prompt guidance and supported native concise defaults that preserve explicit provider settings
55
55
 
56
56
  ## Commands
57
57
 
@@ -60,9 +60,9 @@ Add `-l` to either command for a project-only install. Restart Pi after installi
60
60
  /goal Open current goal status and valid actions
61
61
  /goal <objective> Set an objective and start working
62
62
  /goal edit Edit and reactivate the current goal
63
- /goal pause Stop automatic continuation
63
+ /goal pause Stop the current goal turn and automatic continuation
64
64
  /goal resume Resume automatic continuation
65
- /goal clear Remove the current goal
65
+ /goal clear Stop current goal work and remove the goal
66
66
  /variants Open the reasoning-level selector
67
67
  /variants high Set a reasoning level directly
68
68
  /notification Configure the completion sound
@@ -70,9 +70,13 @@ Add `-l` to either command for a project-only install. Restart Pi after installi
70
70
  /exit Quit Pi gracefully
71
71
  ```
72
72
 
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 or a blocker repeated across at least three goal turns; final prose alone does not end the loop. Aborted turns, provider failures, and continuation failures pause safely. Replacing unfinished work requires confirmation, and `/goal edit` requires TUI mode.
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
74
 
75
- `/init` builds a bounded project map, reads high-value manifests, documentation, and CI configuration, and lets the active model inspect additional implementation files before generating root `AGENTS.md`. Existing `AGENTS.md` and `CLAUDE.md` content is intentionally excluded so stale guidance is not inherited. The command asks no setup questions, starts no second model process, writes only `AGENTS.md`, and reloads Pi resources when finished.
75
+ `/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
+ `/init` freezes a safe project-file map and exposes only dedicated read and list operations while it generates root `AGENTS.md`. Git-ignored files, known secret paths, private-key formats, other guidance, dependencies, links, non-regular files, and files outside that map are unavailable. Existing root `AGENTS.md` is separate protected policy: compatible rules are preserved, a real policy conflict leaves it unchanged with a reason, and any concurrent target change aborts installation without replacing the newer file.
78
+
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.
76
80
 
77
81
  Supported reasoning levels are `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, and `max`. KillerOS limits choices to levels supported by the current model.
78
82
 
@@ -90,7 +94,7 @@ Manual `/compact` aborts the current goal turn before summarization, so KillerOS
90
94
 
91
95
  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.
92
96
 
93
- Lifecycle hooks are loaded from `.pi/killeros-hooks.json` at session start. Supported event keys are `tool_call`, `tool_result`, and `agent_settled`; matchers are JavaScript regular expressions over Pi tool names. Hook commands run from the repository root with `KILLEROS_EVENT`, `KILLEROS_TOOL`, and `KILLEROS_PAYLOAD` environment variables. Failed `tool_call` hooks block the tool, while later-event failures notify the user.
97
+ Lifecycle hooks are loaded from `.pi/killeros-hooks.json` at session start. Supported event keys are `tool_call`, `tool_result`, and `agent_settled`. Optional matchers are JavaScript regular expressions over Pi tool names, so they are valid only for `tool_call` and `tool_result`; KillerOS rejects an `agent_settled` hook that defines a matcher. Hook commands run from the repository root with `KILLEROS_EVENT`, `KILLEROS_TOOL`, and `KILLEROS_PAYLOAD` environment variables. Failed `tool_call` hooks block the tool, while later-event failures notify the user. Aborting the parent request stops the hook process tree with bounded graceful and forced cleanup without reporting cancellation as a hook failure.
94
98
 
95
99
  ## Behavior by mode
96
100
 
@@ -8,8 +8,9 @@ async function confirmNewSession(ctx: ExtensionCommandContext): Promise<boolean>
8
8
 
9
9
  export function registerAliases(pi: ExtensionAPI): void {
10
10
  const startNewSession = async (_args: string, ctx: ExtensionCommandContext): Promise<void> => {
11
- await ctx.waitForIdle();
12
11
  if (!await confirmNewSession(ctx)) return;
12
+ if (!ctx.isIdle()) ctx.abort();
13
+ await ctx.waitForIdle();
13
14
  await ctx.newSession();
14
15
  };
15
16
  pi.registerCommand("clear", { description: "Start a new session after confirmation", handler: startNewSession });
@@ -54,13 +55,6 @@ const BUILTIN_COMMANDS: ReadonlyArray<{ name: string; description: string }> = [
54
55
  { name: "quit", description: "Quit Pi" },
55
56
  ];
56
57
 
57
- export function availableCommandNames(pi: ExtensionAPI): ReadonlySet<string> {
58
- return new Set([
59
- ...BUILTIN_COMMANDS.map((command) => command.name),
60
- ...pi.getCommands().map((command) => command.name),
61
- ]);
62
- }
63
-
64
58
  const COMMAND_SYNTAX_HINTS: Readonly<Record<string, string>> = {
65
59
  goal: "/goal [objective|clear|edit|pause|resume]",
66
60
  variants: "/variants [level]",
@@ -156,12 +150,11 @@ export function registerSlashAutocomplete(pi: ExtensionAPI): void {
156
150
  usage.set(tagged.killerosCommand, (usage.get(tagged.killerosCommand) ?? 0) + 1);
157
151
  const line = lines[cursorLine] ?? "";
158
152
  const beforeCursor = line.slice(0, cursorCol);
159
- let afterCursor = line.slice(cursorCol);
153
+ const afterCursor = line.slice(cursorCol);
160
154
  const match = beforeCursor.match(/(?:^|[ \t])\/([^\s/]*)$/);
161
155
  if (!match || match.index === undefined) return current.applyCompletion(lines, cursorLine, cursorCol, item, prefix);
162
156
  const slashIndex = match.index + (match[0].startsWith("/") ? 0 : 1);
163
157
  const newBefore = beforeCursor.slice(0, slashIndex) + item.value;
164
- if (item.value.endsWith(" ") && afterCursor.startsWith(" ")) afterCursor = afterCursor.trimStart();
165
158
  const nextLines = [...lines];
166
159
  nextLines[cursorLine] = newBefore + afterCursor;
167
160
  return { lines: nextLines, cursorLine, cursorCol: newBefore.length };
@@ -41,22 +41,26 @@ function applyConciseModelSettings(payload: unknown, api: unknown, modelId: unkn
41
41
  const supportsVerbosity = api === "openai-codex-responses"
42
42
  || api === "openai-responses" && typeof modelId === "string" && /^gpt-5(?:[.-]|$)/u.test(modelId);
43
43
  let updated = payload;
44
- if (supportsVerbosity) {
45
- updated = {
46
- ...updated,
47
- text: { ...(isRecord(payload.text) ? payload.text : {}), verbosity: "low" },
48
- };
44
+ const text = isRecord(payload.text) ? payload.text : undefined;
45
+ if (supportsVerbosity && !Object.hasOwn(text ?? {}, "verbosity")) {
46
+ updated = { ...updated, text: { ...(text ?? {}), verbosity: "low" } };
49
47
  }
50
- if (supportsSummary && isRecord(payload.reasoning)) {
51
- updated = { ...updated, reasoning: { ...payload.reasoning, summary: "concise" } };
48
+ const reasoning = isRecord(payload.reasoning) ? payload.reasoning : undefined;
49
+ if (supportsSummary && reasoning && !Object.hasOwn(reasoning, "summary")) {
50
+ updated = { ...updated, reasoning: { ...reasoning, summary: "concise" } };
52
51
  }
53
52
  return updated;
54
53
  }
55
54
 
56
- export function isConcisedEnabled(): boolean {
55
+ export function isConciseEnabled(): boolean {
57
56
  return true;
58
57
  }
59
58
 
59
+ /** @deprecated Use isConciseEnabled instead. */
60
+ export function isConcisedEnabled(): boolean {
61
+ return isConciseEnabled();
62
+ }
63
+
60
64
  export function registerConcisePrompt(pi: ExtensionAPI): void {
61
65
  pi.on("before_agent_start", (event) => ({
62
66
  systemPrompt: `${event.systemPrompt}\n\n${CONCISE_SYSTEM_PROMPT}`,
package/killeros/goals.ts CHANGED
@@ -1,20 +1,20 @@
1
+ import { StringEnum } from "@earendil-works/pi-ai";
1
2
  import { type ExtensionAPI, type ExtensionCommandContext, type ExtensionContext, type ThemeColor } from "@earendil-works/pi-coding-agent";
2
3
  import { Text } from "@earendil-works/pi-tui";
3
4
  import { Type } from "typebox";
4
5
  import { MAX_NODE_TIMER_MS } from "./limits.ts";
5
6
  import { BoundedText } from "./bounded-text.ts";
6
- import { CONCISE_SYSTEM_PROMPT } from "./concise.ts";
7
7
  import { formatTime, formatTokens } from "./display.ts";
8
8
  import { reportError } from "./errors.ts";
9
9
  import { resolvePersonalInstructions } from "./personal-instructions.ts";
10
- import type { GoalRuntime, GoalState, GoalStatus, InitRuntime } from "./runtime.ts";
10
+ import type { GoalBlockerAudit, GoalRuntime, GoalState, GoalStatus, InitRuntime } from "./runtime.ts";
11
11
 
12
12
  const GOAL_ENTRY_TYPE = "killeros-goal";
13
13
  const GOAL_CONTINUATION_TYPE = "killeros-goal-continuation";
14
14
  const GOAL_OBJECTIVE_LIMIT = 4_000;
15
15
  const GOAL_VERSION = 1;
16
16
 
17
- type GoalEntryEvent = "set" | "replace" | "edit" | "turn" | "pause" | "resume" | "blocked" | "complete" | "error" | "clear" | "checkpoint";
17
+ type GoalEntryEvent = "set" | "replace" | "edit" | "turn" | "pause" | "resume" | "blocked" | "complete" | "error" | "clear" | "checkpoint" | "blocker-audit";
18
18
  interface GoalEntryData {
19
19
  version: 1;
20
20
  event: GoalEntryEvent;
@@ -24,6 +24,7 @@ interface GoalEntryData {
24
24
  interface GoalTransitionOptions {
25
25
  resetBlockedAudit?: boolean;
26
26
  resumeAfterManualCompaction?: true;
27
+ blockerAudit?: GoalBlockerAudit;
27
28
  }
28
29
 
29
30
  interface RestoredGoalState {
@@ -32,7 +33,7 @@ interface RestoredGoalState {
32
33
  }
33
34
 
34
35
  const GoalUpdateParams = Type.Object({
35
- status: Type.Union([Type.Literal("complete"), Type.Literal("blocked")], {
36
+ status: StringEnum(["complete", "blocked"] as const, {
36
37
  description: "Mark the active goal complete or blocked",
37
38
  }),
38
39
  evidence: Type.String({
@@ -40,11 +41,19 @@ const GoalUpdateParams = Type.Object({
40
41
  maxLength: 2_000,
41
42
  description: "Concise evidence that the objective is complete, or the repeated blocker and attempted workarounds",
42
43
  }),
44
+ blockerKey: Type.Optional(Type.String({
45
+ minLength: 1,
46
+ maxLength: 120,
47
+ pattern: "^[a-z0-9][a-z0-9._-]{0,119}$",
48
+ description: "Stable lowercase key identifying the repeated blocker",
49
+ })),
43
50
  });
44
51
 
45
52
  interface GoalUpdateDetails {
46
- status: "complete" | "blocked";
53
+ status: "complete" | "blocked" | "blocker-audit";
47
54
  evidence: string;
55
+ blockerKey?: string;
56
+ streak?: number;
48
57
  }
49
58
 
50
59
  function isGoalStatus(value: unknown): value is GoalStatus {
@@ -55,6 +64,17 @@ function finiteNonNegative(value: unknown): value is number {
55
64
  return typeof value === "number" && Number.isFinite(value) && value >= 0;
56
65
  }
57
66
 
67
+ function isGoalBlockerAudit(value: unknown, turns: number, status: GoalStatus): value is GoalBlockerAudit {
68
+ if (!value || typeof value !== "object") return false;
69
+ const candidate = value as Partial<GoalBlockerAudit>;
70
+ if (typeof candidate.key !== "string"
71
+ || !/^[a-z0-9][a-z0-9._-]{0,119}$/u.test(candidate.key)
72
+ || !Number.isInteger(candidate.streak) || (candidate.streak ?? 0) < 1 || (candidate.streak ?? 0) > 3
73
+ || !Number.isInteger(candidate.lastTurn) || (candidate.lastTurn ?? 0) < 1 || (candidate.lastTurn ?? 0) > turns) return false;
74
+ if (status === "complete") return false;
75
+ return status === "blocked" ? candidate.streak === 3 : candidate.streak! < 3;
76
+ }
77
+
58
78
  function parseGoalState(value: unknown): GoalState | undefined {
59
79
  if (!value || typeof value !== "object") return undefined;
60
80
  const candidate = value as Partial<GoalState>;
@@ -72,6 +92,7 @@ function parseGoalState(value: unknown): GoalState | undefined {
72
92
  || !finiteNonNegative(candidate.baselineTokens)
73
93
  || candidate.activeStartedAt !== undefined && !finiteNonNegative(candidate.activeStartedAt)
74
94
  || candidate.result !== undefined && typeof candidate.result !== "string"
95
+ || candidate.blockerAudit !== undefined && !isGoalBlockerAudit(candidate.blockerAudit, candidate.turns!, candidate.status)
75
96
  || candidate.resumeAfterManualCompaction !== undefined && candidate.resumeAfterManualCompaction !== true
76
97
  || candidate.resumeAfterManualCompaction === true && candidate.status !== "paused") {
77
98
  return undefined;
@@ -90,6 +111,7 @@ function parseGoalState(value: unknown): GoalState | undefined {
90
111
  baselineTokens: candidate.baselineTokens,
91
112
  result: candidate.result,
92
113
  resumeAfterManualCompaction: candidate.resumeAfterManualCompaction,
114
+ blockerAudit: candidate.blockerAudit,
93
115
  };
94
116
  }
95
117
 
@@ -183,6 +205,7 @@ function transitionGoal(
183
205
  updatedAt: now,
184
206
  activeStartedAt: status === "active" ? now : undefined,
185
207
  blockedAuditStartTurn: options.resetBlockedAudit ? stopped.turns : stopped.blockedAuditStartTurn,
208
+ blockerAudit: options.resetBlockedAudit ? undefined : options.blockerAudit ?? stopped.blockerAudit,
186
209
  result,
187
210
  resumeAfterManualCompaction: options.resumeAfterManualCompaction,
188
211
  };
@@ -191,6 +214,24 @@ function transitionGoal(
191
214
  return next;
192
215
  }
193
216
 
217
+ function clearGoalExecutionFlags(runtime: GoalRuntime): void {
218
+ runtime.continuationScheduled = false;
219
+ runtime.goalTurnInFlight = false;
220
+ runtime.agentEndObserved = false;
221
+ runtime.lastStopReason = undefined;
222
+ runtime.lastError = undefined;
223
+ }
224
+
225
+ async function stopGoalRun(runtime: GoalRuntime, ctx: ExtensionCommandContext, shouldStop: boolean): Promise<void> {
226
+ if (!shouldStop) return;
227
+ try {
228
+ ctx.abort();
229
+ await ctx.waitForIdle();
230
+ } finally {
231
+ clearGoalExecutionFlags(runtime);
232
+ }
233
+ }
234
+
194
235
  function goalStatusLabel(status: GoalStatus): string {
195
236
  return `${status.charAt(0).toLocaleUpperCase()}${status.slice(1)}`;
196
237
  }
@@ -223,6 +264,7 @@ export function pauseGoalAfterFailure(
223
264
  ctx: ExtensionContext,
224
265
  reason: string,
225
266
  recoveryInstruction = "Run /goal resume after resolving the problem.",
267
+ notify = true,
226
268
  ): void {
227
269
  if (runtime.state?.status !== "active") return;
228
270
  try {
@@ -238,7 +280,7 @@ export function pauseGoalAfterFailure(
238
280
  runtime.continuationScheduled = false;
239
281
  runtime.requestRender?.();
240
282
  }
241
- ctx.ui.notify(`Goal paused: ${reason}\n${recoveryInstruction}`, "error");
283
+ if (notify) ctx.ui.notify(`Goal paused: ${reason}\n${recoveryInstruction}`, "error");
242
284
  }
243
285
 
244
286
  function pauseGoalForPossibleManualCompaction(
@@ -304,6 +346,7 @@ function scheduleGoalContinuation(
304
346
  || runtime.continuationHeld
305
347
  || runtime.goalTurnInFlight
306
348
  || initState.active
349
+ || !ctx.isIdle()
307
350
  || ctx.hasPendingMessages()) return false;
308
351
  const current = runtime.state;
309
352
  runtime.continuationScheduled = true;
@@ -338,7 +381,7 @@ function goalInstructions(state: GoalState, heading: string): string {
338
381
  "Continue making concrete progress toward this unchanged objective. Re-check repository state and prior results instead of repeating work.",
339
382
  "Do not stop merely because one response is complete: KillerOS will start another goal turn while the goal remains active.",
340
383
  "Before declaring completion, audit every part of the objective and verify the relevant results. Then call killeros_goal_update with status complete and concise evidence.",
341
- "Call killeros_goal_update with status blocked only when the same external impasse has prevented progress for three consecutive goal turns; name the blocker and attempted workarounds.",
384
+ "Call killeros_goal_update with status blocked and the same lowercase blockerKey on each turn where one external impasse persists; attempts one and two record the audit, and attempt three marks the goal blocked.",
342
385
  "Never use the goal tool to pause, resume, edit, replace, or clear the objective. Those transitions belong to the user.",
343
386
  ].join("\n");
344
387
  }
@@ -355,7 +398,6 @@ function goalContinuationMessage(state: GoalState, ctx: ExtensionContext): strin
355
398
  sections.push(`<personal_instructions source=${JSON.stringify(personal.source)}>\n${personal.content}\n</personal_instructions>`);
356
399
  }
357
400
  }
358
- sections.push(CONCISE_SYSTEM_PROMPT);
359
401
  return sections.join("\n\n");
360
402
  }
361
403
 
@@ -400,7 +442,7 @@ export function registerGoal(
400
442
  pi.registerTool<typeof GoalUpdateParams, GoalUpdateDetails>({
401
443
  name: "killeros_goal_update",
402
444
  label: "Goal update",
403
- description: "Mark the active KillerOS long-running goal complete after verification, or blocked after the same impasse persists for three consecutive goal turns.",
445
+ description: "Mark the active KillerOS long-running goal complete after verification, or record the same blocker key on three consecutive goal turns before blocking it.",
404
446
  parameters: GoalUpdateParams,
405
447
  executionMode: "sequential",
406
448
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
@@ -410,13 +452,40 @@ export function registerGoal(
410
452
  if (!state || state.status !== "active") throw new Error("There is no active KillerOS goal to update");
411
453
  const evidence = params.evidence.trim();
412
454
  if (!evidence) throw new Error("Goal evidence must not be empty");
413
- if (params.status === "blocked" && state.turns - state.blockedAuditStartTurn < 3) {
414
- throw new Error("A goal cannot be marked blocked before three goal turns in the current audit; keep working and audit the same blocker again");
455
+ if (params.status === "complete") {
456
+ transitionGoal(pi, runtime, "complete", "complete", evidence, { resetBlockedAudit: true });
457
+ return {
458
+ content: [{ type: "text", text: `Goal marked complete: ${evidence}` }],
459
+ details: { status: "complete", evidence },
460
+ };
415
461
  }
416
- transitionGoal(pi, runtime, params.status, params.status, evidence);
462
+ if (!runtime.goalTurnInFlight) throw new Error("A blocker audit can only be recorded during an active KillerOS goal turn");
463
+ const blockerKey = params.blockerKey;
464
+ if (!blockerKey || !/^[a-z0-9][a-z0-9._-]{0,119}$/u.test(blockerKey)) {
465
+ throw new Error("A blocked goal update requires a stable lowercase blockerKey");
466
+ }
467
+ const previous = state.blockerAudit;
468
+ const sameTurn = previous?.key === blockerKey && previous.lastTurn === state.turns;
469
+ const consecutive = previous?.key === blockerKey && previous.lastTurn === state.turns - 1;
470
+ const streak = sameTurn ? previous.streak : consecutive ? previous.streak + 1 : 1;
471
+ const blockerAudit = { key: blockerKey, streak, lastTurn: state.turns };
472
+ if (streak < 3) {
473
+ const next: GoalState = {
474
+ ...state,
475
+ revision: state.revision + 1,
476
+ updatedAt: Date.now(),
477
+ blockerAudit,
478
+ };
479
+ persistGoalState(pi, runtime, "blocker-audit", next);
480
+ return {
481
+ content: [{ type: "text", text: `Blocker audit ${streak}/3 recorded; the goal remains active: ${evidence}` }],
482
+ details: { status: "blocker-audit", evidence, blockerKey, streak },
483
+ };
484
+ }
485
+ transitionGoal(pi, runtime, "blocked", "blocked", evidence, { blockerAudit });
417
486
  return {
418
- content: [{ type: "text", text: `Goal marked ${params.status}: ${evidence}` }],
419
- details: { status: params.status, evidence },
487
+ content: [{ type: "text", text: `Goal marked blocked: ${evidence}` }],
488
+ details: { status: "blocked", evidence, blockerKey, streak },
420
489
  };
421
490
  },
422
491
  renderCall(args, theme) {
@@ -425,7 +494,8 @@ export function registerGoal(
425
494
  renderResult(result, options, theme) {
426
495
  const details = result.details;
427
496
  if (!details) return new BoundedText(theme.fg("dim", "Goal updated"));
428
- const text = `${theme.fg(details.status === "complete" ? "success" : "warning", details.status === "complete" ? " Complete" : "! Blocked")}${theme.fg("dim", ` · ${details.evidence}`)}`;
497
+ const label = details.status === "complete" ? "✓ Complete" : details.status === "blocked" ? "! Blocked" : `! Blocker audit ${details.streak}/3`;
498
+ const text = `${theme.fg(details.status === "complete" ? "success" : "warning", label)}${theme.fg("dim", ` · ${details.evidence}`)}`;
429
499
  return new BoundedText(text, options.expanded ? undefined : 3);
430
500
  },
431
501
  });
@@ -546,10 +616,11 @@ export function registerGoal(
546
616
  ctx.ui.notify("No goal is set", "info");
547
617
  return;
548
618
  }
619
+ const shouldStopGoalRun = runtime.goalTurnInFlight || runtime.continuationScheduled;
620
+ let saved = false;
549
621
  try {
550
622
  persistGoalState(pi, runtime, "clear", undefined);
551
- runtime.continuationScheduled = false;
552
- ctx.ui.notify("Goal cleared", "info");
623
+ saved = true;
553
624
  } catch (error) {
554
625
  if (runtime.state?.status === "active") {
555
626
  pauseGoalAfterFailure(
@@ -558,11 +629,24 @@ export function registerGoal(
558
629
  ctx,
559
630
  `the requested clear could not be saved: ${error instanceof Error ? error.message : String(error)}`,
560
631
  "Automatic continuation is stopped. Retry /goal clear to remove the goal.",
632
+ false,
561
633
  );
562
634
  } else {
563
635
  reportError(ctx, "Goal could not be cleared", error);
636
+ return;
564
637
  }
565
638
  }
639
+ try {
640
+ await stopGoalRun(runtime, ctx, shouldStopGoalRun);
641
+ } catch (error) {
642
+ reportError(ctx, saved ? "Goal cleared, but the active goal turn could not be confirmed stopped" : "Goal paused, but the active goal turn could not be confirmed stopped", error);
643
+ return;
644
+ }
645
+ if (saved) {
646
+ ctx.ui.notify("Goal cleared", "info");
647
+ } else {
648
+ ctx.ui.notify("Goal paused: the requested clear could not be saved\nAutomatic continuation is stopped. Retry /goal clear to remove the goal.", "error");
649
+ }
566
650
  return;
567
651
  }
568
652
 
@@ -599,18 +683,34 @@ export function registerGoal(
599
683
  ctx.ui.notify(`Goal is ${runtime.state.status}; only an active goal can be paused`, "warning");
600
684
  return;
601
685
  }
686
+ const shouldStopGoalRun = runtime.goalTurnInFlight || runtime.continuationScheduled;
687
+ let saved = false;
688
+ let failureReason: string | undefined;
602
689
  try {
603
690
  transitionGoal(pi, runtime, "pause", "paused");
604
- ctx.ui.notify("Goal paused. Run /goal resume to continue.", "info");
691
+ saved = true;
605
692
  } catch (error) {
693
+ failureReason = `the requested pause could not be saved: ${error instanceof Error ? error.message : String(error)}`;
606
694
  pauseGoalAfterFailure(
607
695
  pi,
608
696
  runtime,
609
697
  ctx,
610
- `the requested pause could not be saved: ${error instanceof Error ? error.message : String(error)}`,
698
+ failureReason,
611
699
  "Automatic continuation is stopped. If session storage is still unavailable, retry /goal pause after it recovers.",
700
+ false,
612
701
  );
613
702
  }
703
+ try {
704
+ await stopGoalRun(runtime, ctx, shouldStopGoalRun);
705
+ } catch (error) {
706
+ reportError(ctx, "Goal paused, but the active goal turn could not be confirmed stopped", error);
707
+ return;
708
+ }
709
+ if (saved) {
710
+ ctx.ui.notify("Goal paused. Run /goal resume to continue.", "info");
711
+ } else {
712
+ ctx.ui.notify(`Goal paused: ${failureReason}\nAutomatic continuation is stopped. If session storage is still unavailable, retry /goal pause after it recovers.`, "error");
713
+ }
614
714
  return;
615
715
  }
616
716
 
@@ -689,6 +789,7 @@ export function registerGoal(
689
789
  updatedAt: now,
690
790
  activeStartedAt: now,
691
791
  blockedAuditStartTurn: current.turns,
792
+ blockerAudit: undefined,
692
793
  result: undefined,
693
794
  resumeAfterManualCompaction: undefined,
694
795
  };
@@ -697,13 +798,17 @@ export function registerGoal(
697
798
  runtime.continuationScheduled = false;
698
799
  if (scheduleGoalContinuation(pi, runtime, initState, ctx)) ctx.ui.notify("Goal updated and active", "info");
699
800
  } catch (error) {
700
- pauseGoalAfterFailure(
701
- pi,
702
- runtime,
703
- ctx,
704
- `Goal could not be edited: ${error instanceof Error ? error.message : String(error)}`,
705
- "Automatic continuation is stopped. Retry /goal edit after session storage recovers.",
706
- );
801
+ if (runtime.state?.status === "active") {
802
+ pauseGoalAfterFailure(
803
+ pi,
804
+ runtime,
805
+ ctx,
806
+ `Goal could not be edited: ${error instanceof Error ? error.message : String(error)}`,
807
+ "Automatic continuation is stopped. Retry /goal edit after session storage recovers.",
808
+ );
809
+ } else {
810
+ reportError(ctx, "Goal could not be edited", error);
811
+ }
707
812
  }
708
813
  return;
709
814
  }
@@ -763,8 +868,19 @@ export function registerGoal(
763
868
  ctx.ui.notify("Goal active. KillerOS will continue until completion, a repeated blocker, or pause.", "info");
764
869
  }
765
870
  } catch (error) {
766
- reportError(ctx, "Goal could not be started", error);
767
- scheduleGoalContinuation(pi, runtime, initState, ctx);
871
+ if (!unfinished) {
872
+ reportError(ctx, "Goal could not be started", error);
873
+ } else if (runtime.state?.status === "active") {
874
+ pauseGoalAfterFailure(
875
+ pi,
876
+ runtime,
877
+ ctx,
878
+ `Goal could not be replaced: ${error instanceof Error ? error.message : String(error)}`,
879
+ "Automatic continuation is stopped. Retry replacement after session storage recovers.",
880
+ );
881
+ } else {
882
+ reportError(ctx, "Goal could not be replaced", error);
883
+ }
768
884
  }
769
885
  };
770
886
 
@@ -802,6 +918,8 @@ export function registerGoalSettlement(
802
918
  if (!wasGoalTurn || runtime.state?.status !== "active" || initState.active) {
803
919
  if (continuationWasScheduled && runtime.state?.status === "active" && !initState.active) {
804
920
  pauseGoalAfterFailure(pi, runtime, ctx, "the goal continuation ended before an agent turn started");
921
+ } else if (runtime.state?.status === "active" && !initState.active) {
922
+ scheduleGoalContinuation(pi, runtime, initState, ctx);
805
923
  }
806
924
  return;
807
925
  }