killeros 3.0.2 → 3.0.4

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,39 @@ All notable changes to KillerOS are documented here. Releases use New Features f
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [3.0.4] - 2026-10-07
8
+
9
+ ### Changed
10
+
11
+ - Made the explicitly enabled `prepublishOnly` lifecycle the single release-validator invocation at npm publication. The release workflow now publishes the source directory with `npm publish . --ignore-scripts=false`, preserving the existing release checks and trusted publishing.
12
+ - Added current release-metadata validation and isolated publication-lifecycle tests with release-pinned npm to the Node-floor quality check. Extended the existing packed-package Pi test to check all required files and package identity without another pack operation. The package smoke job remains until replacement coverage lands and the required-check migration is approved and verified.
13
+
14
+ ### Fixed
15
+
16
+ - Allowed later verified ordinary work to complete the same saved blocked goal without resume, clear, or extra automatic turns. Completion retains the original objective, file baseline, and accounting, respects native tool selection, and prevents compaction from restarting completed work.
17
+ - Stopped false CRLF change counts in the footer and task receipts for text files with `eol=lf`. Added a differential Git-oracle matrix covering attributes, `core.autocrlf`, text and binary contents, plus mode and symlink comparisons. Fixes #47.
18
+ - Gave each real Pi decision-tool exposure scenario its own bounded timeout so slower Windows TUI scenarios do not cancel the remaining codemode and RPC coverage. All behavioral assertions remain unchanged.
19
+
20
+ ## [3.0.3] - 2026-10-07
21
+
22
+ ### Changed
23
+
24
+ - Raised the minimum supported Pi version and all four locked Pi development packages to 1.0.4. Compatibility CI now targets the exact 1.0.4 minimum and latest `>=1.0.4 <2`, with unchanged stable check names and package-file immutability guards.
25
+ - Documented Pi 1.0.4's native wildcard tool filters, MCP availability under explicit tool allowlists, and codemode image reads.
26
+ - Updated Azure footer naming and fast-mode exclusion tests for Pi's renamed `azure` provider. Documented the required Azure configuration migration, the unchanged Responses API identifier, native editor/transcript navigation, and saved codemode images.
27
+ - Gave Pi compatibility CI checks stable minimum/latest names so future Pi version changes do not require renaming GitHub's required checks. Documented the one-time protection migration without weakening existing safeguards.
28
+
29
+ ### Added
30
+
31
+ - Added real Pi regressions for wildcard decision-tool selection and exclusion, codemode-only provider prompt rules, and pending-stream cancellation retries that preserve one goal turn and defer receipts and sounds until settlement.
32
+ - Added real Pi contract coverage for Azure Responses and Foundry Chat Completions requests both before and after reload, preserving sampling settings and deployment names without applying OpenAI priority service.
33
+ - Added custom-editor coverage for Pi 1.0.3's separate line-navigation and fullscreen transcript Home/End shortcuts.
34
+
35
+ ### Fixed
36
+
37
+ - Fixed a process-tree test startup race by waiting for the complete descendant PID instead of readiness-file creation. Hook termination assertions remain unchanged.
38
+ - Reused bounded cleanup retries throughout real Pi integration tests so transient Windows directory locks do not fail CI after the assertions pass. Added a repository contract against unbounded fixture cleanup.
39
+
7
40
  ## [3.0.2] - 2026-10-05
8
41
 
9
42
  ### Changed
package/README.md CHANGED
@@ -19,9 +19,11 @@ A TypeScript extension for the [Pi coding agent](https://github.com/earendil-wor
19
19
  ## Requirements
20
20
 
21
21
  - Node.js 22.19.0+
22
- - Pi 1.0.2 or later below 2.0.0
22
+ - Pi 1.0.4 or later below 2.0.0
23
23
  - An interactive TUI session for the custom header, editor, footer, and `question`
24
24
 
25
+ Users on older Pi versions must upgrade.
26
+
25
27
  ## Install
26
28
 
27
29
  ```bash
@@ -34,7 +36,7 @@ Or from GitHub:
34
36
  pi install git:github.com/KyrosHendrix/pi-KillerOS
35
37
  ```
36
38
 
37
- Pin a release by appending its tag, for example `@v3.0.2`. Add `-l` to install only for the current project. Restart Pi after installing.
39
+ Pin a release by appending its tag, for example `@v3.0.4`. Add `-l` to install only for the current project. Restart Pi after installing.
38
40
 
39
41
  ## Commands
40
42
 
@@ -69,7 +71,11 @@ Pi 1.0 defaults to fullscreen. Use `--tui-mode regular` for one invocation or se
69
71
  | RPC | Goals, proactive compaction; no TUI components, sounds, title indicator |
70
72
  | Print/JSON | No interactive questions, `/goal`, or proactive compaction |
71
73
 
72
- `question` and the active `killeros_goal_update` tool use Pi's `model-only` exposure. They stay directly declared to the model when codemode is disabled or enabled in `on` or `only` mode, but scripts and other tools cannot call them through `ctx.executeTool()`. The goal tool remains inactive without an active goal. Questions still require TUI mode; a direct RPC call fails with `The question tool requires interactive TUI mode`. KillerOS does not enable or configure codemode.
74
+ With Pi 1.0.3's default [keybindings](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/keybindings.md#fullscreen), `Home` and `End` move within the current editor line. In fullscreen mode, `Ctrl+Home` and `Ctrl+End` scroll to the transcript's beginning or end without moving the editor cursor. KillerOS inherits these bindings; user overrides still apply.
75
+
76
+ `question` and the active `killeros_goal_update` tool use Pi's `model-only` exposure. They stay directly declared to the model when codemode is disabled or enabled in `on` or `only` mode, but scripts and other tools cannot call them through `ctx.executeTool()`. The goal tool is active for active goals and, during a later ordinary request, for completion only of a saved blocked goal. It is inactive outside those cases. Questions still require TUI mode; a direct RPC call fails with `The question tool requires interactive TUI mode`. KillerOS does not enable or configure codemode.
77
+
78
+ Pi 1.0.4's native `--tools` and `--exclude-tools` support `*` patterns. Quote patterns such as `"killeros_*"` so the shell does not expand them. An explicit allowlist must include `question` for interactive questions and permit `killeros_goal_update` for goals. Selection does not make questions usable outside TUI mode. Excluding the goal tool pauses goals that are active before any model request. A later ordinary request with a blocked goal still runs, but cannot record its completion when the tool is excluded. See Pi's [tool selection](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/cli.md#tools).
73
79
 
74
80
  ## Configuration
75
81
 
@@ -103,7 +109,9 @@ A direct quoted path-shaped file target binds silent file proof. For an extensio
103
109
  /goal Fix `killeros/footer.ts`, verified by npm test
104
110
  ```
105
111
 
106
- Bare quoted prose remains model-reported. KillerOS captures the file baseline at goal start and only completes when the file is created or changed. A normal response never continues a goal by itself: the agent must record `continue`, `complete`, or a blocker decision through `killeros_goal_update`. Repeated continuation reports and unavailable goal tools pause the goal. New goals pause after 20 turns without warning. An explicit `/goal resume` on an exhausted goal grants another 20 turns; compaction recovery never grants turns. Same-turn recovery requires an actual interruption; successful or skipped compaction cannot restart a normally stopped goal without a decision. A request that completes or blocks a goal remains goal-owned through settlement, so automatic compaction cannot restart it as ordinary work. Later ordinary requests still support compaction continuation. Restored goals keep their persisted limit.
112
+ Bare quoted prose remains model-reported. KillerOS captures the file baseline at goal start and only completes when the file is created or changed. File proof verifies that deliverable, not every natural-language acceptance criterion. The assistant must audit the whole objective before reporting completion; KillerOS cannot independently prove arbitrary objectives from prose. A normal response never continues a goal by itself: the agent must record `continue`, `complete`, or a blocker decision through `killeros_goal_update`. Repeated continuation reports and unavailable goal tools pause the goal. New goals pause after 20 turns without warning. An explicit `/goal resume` on an exhausted goal grants another 20 turns; compaction recovery never grants turns. Same-turn recovery requires an actual interruption; successful or skipped compaction cannot restart a normally stopped goal without a decision. A request that completes or blocks a goal remains goal-owned through settlement, so automatic compaction cannot restart it as ordinary work. Later ordinary requests still support compaction continuation. Restored goals keep their persisted limit.
113
+
114
+ A blocked goal stays stopped. In a later ordinary TUI or RPC request in a saved session, the assistant can complete the same unchanged objective through `killeros_goal_update` after verification and current evidence. Only `complete` is accepted on this path. Completion does not resume automatic work, grant turns, restart the active clock, or require `/goal clear`. It retains the original file baseline and records either `file` proof or `model-reported` completion. Failed proof and ordinary responses without a completion decision leave the goal blocked. Paused goals still require explicit user controls. A completed goal leaves the footer but remains inspectable through `/goal` and session history.
107
115
 
108
116
  Session replacement, reload, and committed tree navigation discard unfinished task receipts. Late receipt results do not write or notify through the old session context or attach to a different branch. Cancelled navigation preserves the pending receipt.
109
117
 
@@ -111,6 +119,8 @@ Pending `/goal` commands discard their mutation if committed navigation changes
111
119
 
112
120
  Failed Git metadata reads keep the footer's last successful file counts and mark task changes unavailable. They are not treated as an empty repository.
113
121
 
122
+ Passive Git scans compare content without running repository filters. CRLF text that normalizes to the indexed LF content stays unchanged, including with `text=auto eol=lf` or bare `eol=lf`. Git's `status` can still report a modification after a size-changing rewrite because of its index stat cache, even when `git diff` is empty.
123
+
114
124
  Completion sounds are off by default; change with `/notification` in TUI mode. The tab-title indicator requires a Nerd Font.
115
125
 
116
126
  ### Pi model and MCP settings
@@ -129,8 +139,16 @@ Use Pi's native `/thinking` selector for reasoning levels. Pi 1.0.2 can also app
129
139
 
130
140
  Only configure parameters the endpoint accepts. These settings apply to `openai-completions`, `openai-responses`, and `azure-openai-responses`, not legacy `openai-codex`. Missing levels inherit model defaults; request-level sampling parameters take precedence. `/codex-fast` preserves sampling parameters when it adds the priority service tier. See Pi's [sampling configuration](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/models.md#configure-sampling-by-thinking-level) for the full file format and merge rules.
131
141
 
142
+ Pi 1.0.3 renames the Azure **provider** from `azure-openai-responses` to `azure` and adds Azure Foundry Chat Completions, including `azure/deepseek-v4-pro`. Rename the provider key in `auth.json` or run `/login azure` again, rename it in `models.json`, and update `defaultProvider`, `enabledModels` patterns, and `modelThinkingLevels` keys in `settings.json`. The Responses **API** identifier remains `azure-openai-responses`; Foundry Chat Completions uses `openai-completions`. The `AZURE_OPENAI_*` environment variables are unchanged. Old Azure sessions fall back to another model on resume and lose prompt-cache reuse. KillerOS does not migrate credentials or Pi settings. See Pi's [Azure configuration](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/providers.md#azure-openai).
143
+
144
+ In Pi 1.0.4 codemode, `tools.read()` on an image returns an image block that you can pass to native `image()`. See Pi's [codemode tool calls](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/codemode.md#call-tools).
145
+
146
+ When codemode is enabled, Pi 1.0.3's `image()` also saves images to temporary files and includes their paths in the result, so later turns can copy or move them. Pi creates these output files with user-only permissions on POSIX systems; Windows access follows inherited filesystem ACLs. KillerOS uses Pi's native codemode behavior; see [image generation](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/codemode.md#generate-images).
147
+
132
148
  Use Pi's `/mcp` command and `.pi/mcp.json` to enable, disable, or change the exposure of a user-level MCP server for a trusted project without copying its credentials. See [project MCP overrides](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/mcp.md#configure-servers). KillerOS does not manage MCP configuration.
133
149
 
150
+ In Pi 1.0.4, a nonempty `--tools` list with no `mcp__` entry preserves MCP tool availability according to each server's exposure. For example, `--tools codemode` does not disable MCP connections or remove their tools. Use `--no-mcp` to disable MCP connections through Pi's built-in MCP support for one run. Explicit MCP selection and exclusion remain Pi's responsibility; see [MCP tool exposure](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/mcp.md#control-tool-exposure).
151
+
134
152
  ## Development
135
153
 
136
154
  Strict TypeScript throughout. Tests run on Node's built-in test runner:
@@ -147,9 +165,17 @@ npm ci && npm run check && npm test
147
165
  4. Merge using **Create a merge commit**. Squash and rebase merging are disabled because the automated `main`-to-`dev` sync requires shared ancestry. Avoid advancing `dev` until that sync finishes.
148
166
  5. Check the `main` CI run and the following Release run. Successful CI on the current `main` commit triggers npm publication with provenance, creates the GitHub release, and fast-forwards `dev` to the released commit. A green PR alone never publishes.
149
167
 
150
- `main` requires pull requests and the CI workflow's required checks, including for administrators. Force pushes and branch deletion are blocked. If CI job names change, update GitHub's required status checks to match.
168
+ `main` requires pull requests and the CI workflow's required checks, including for administrators. Force pushes and branch deletion are blocked. Pi compatibility checks use the stable names `Pi compatibility (minimum)` and `Pi compatibility (latest)`, independent of the tested versions.
169
+
170
+ When adopting these names, first confirm both checks passed on the release PR's exact head commit. Replace only the two old versioned Pi required checks with their corresponding stable names, preserving their GitHub Actions app bindings and every other protection.
171
+
172
+ `Quality (Node 22.19.0)` validates current release metadata before the full test suite. It also tests the publication lifecycle with the release workflow's pinned npm CLI in an isolated installation. The packed-package Pi test checks the installed archive's entry point, README, changelog, theme, package name, and current version before activation and reload. These archive assertions run throughout the Node, Windows, and Pi compatibility matrix.
173
+
174
+ `Package smoke test` remains during the required-check migration. Before removing it, land the replacement coverage and confirm the Node-floor quality check passed on the replacement PR's exact head commit and that the coverage is present on `main`. Inspect live branch protection and rulesets, obtain approval, and remove only the smoke requirement while preserving the required quality check's GitHub Actions binding and every other protection. Remove the job in a follow-up PR and recheck that PR's final head. Do not remove the job if these prerequisites are unmet.
175
+
176
+ Releases go through CI on `main`; do not push version tags manually. The release workflow publishes the source directory with `npm publish . --ignore-scripts=false`. The package's `prepublishOnly` lifecycle invokes the existing release validator once at that boundary, even if an inherited npm setting disables scripts. A validator failure stops publication and release creation. The earlier verified-commit, provenance, version, tag, and release-note checks remain.
151
177
 
152
- Releases go through CI on `main`; do not push version tags manually. The package smoke job validates release metadata before packing, and the release workflow checks it again before publication. The prepublish check rejects ordinary direct `npm publish`, but `--ignore-scripts` can bypass it. Configure npm's trusted publisher for `release.yml`, set package publishing access to "Require two-factor authentication and disallow tokens", and revoke unused publish tokens. npm maintainers can still publish interactively with 2FA, so workflow-only publishing also depends on maintainer policy.
178
+ The prepublish guard rejects ordinary direct publication, but it does not prove GitHub authorization. Disabling scripts or publishing a tarball bypasses `prepublishOnly`. Configure npm's trusted publisher for `release.yml`, set package publishing access to "Require two-factor authentication and disallow tokens", and revoke unused publish tokens. npm maintainers can still publish interactively with 2FA, so workflow-only publishing also depends on maintainer policy.
153
179
 
154
180
  ## Security
155
181
 
@@ -23,6 +23,7 @@ export interface AutoCompactionPreference {
23
23
 
24
24
  export interface AutoCompactionGoalHandlers {
25
25
  isActive(ctx: ExtensionContext): boolean;
26
+ hasAcceptedTerminalDecision(): boolean;
26
27
  onRequested(): void;
27
28
  onCompleted(ctx: ExtensionContext): void;
28
29
  onFailed(ctx: ExtensionContext, error: unknown): void;
@@ -246,7 +247,20 @@ export function registerAutoCompaction(
246
247
  goalRequestOwned = dependencies.goal?.isActive(ctx) === true;
247
248
  });
248
249
 
250
+ // Capture later blocked completion before goal settlement discards its permission.
251
+ // A compaction already pending as ordinary work must also stop after that decision.
252
+ const observeTerminalDecision = (): void => {
253
+ if (!dependencies.goal?.hasAcceptedTerminalDecision()) return;
254
+ goalRequestOwned = true;
255
+ if (request?.phase === "compacting") {
256
+ request.goal = true;
257
+ if (request.compactionCompleted) request = undefined;
258
+ }
259
+ };
260
+ pi.on("agent_end", observeTerminalDecision);
261
+
249
262
  pi.on("turn_end", (_event, ctx) => {
263
+ observeTerminalDecision();
250
264
  if (!supportedMode(ctx) || request) return;
251
265
 
252
266
  let preference: AutoCompactionPreference;
@@ -305,9 +319,10 @@ export function registerAutoCompaction(
305
319
  try {
306
320
  ctx.compact({
307
321
  onComplete: () => {
322
+ observeTerminalDecision();
308
323
  if (request?.phase !== "compacting" || request.token !== token) return;
309
324
  rearmOnNextLogicalRequest = preference.percentRemaining === 100;
310
- if (goal) {
325
+ if (goal || request.goal) {
311
326
  request = undefined;
312
327
  if (activeGoal && dependencies.goal) {
313
328
  try {
@@ -280,7 +280,7 @@ function sumSessionCost(ctx: ExtensionContext): number {
280
280
 
281
281
  const PROVIDER_LABELS: Readonly<Record<string, string>> = {
282
282
  "amazon-bedrock": "Amazon Bedrock",
283
- "azure-openai-responses": "Azure OpenAI",
283
+ azure: "Azure",
284
284
  "github-copilot": "GitHub Copilot",
285
285
  "google-vertex": "Google Vertex",
286
286
  "openai-codex": "OpenAI",
@@ -13,7 +13,7 @@ import { safeTerminalText } from "./safe-terminal-text.ts";
13
13
 
14
14
  const GoalUpdateParams = Type.Object({
15
15
  status: StringEnum(["complete", "continue", "blocked"] as const, {
16
- description: "Record exactly one active-goal decision: complete, continue, or blocked",
16
+ description: "Record an active-goal decision, or complete a blocked goal during an eligible later ordinary request",
17
17
  }),
18
18
  evidence: Type.String({
19
19
  minLength: 1,
@@ -148,7 +148,7 @@ export function registerGoalInterface(
148
148
  pi.registerTool<typeof GoalUpdateParams, GoalUpdateDetails>({
149
149
  name: GOAL_UPDATE_TOOL,
150
150
  label: "Goal update",
151
- description: "Record exactly one active-goal decision: complete after verification, continue with evidence and one next action, or audit the same blocker before blocking it.",
151
+ description: "Record exactly one active-goal decision: complete after verification, continue with evidence and one next action, or audit the same blocker before blocking it. During an eligible later ordinary request for a blocked goal, only verified completion of the whole unchanged objective is permitted; it does not resume work or grant turns.",
152
152
  parameters: GoalUpdateParams,
153
153
  exposure: "model-only",
154
154
  executionMode: "sequential",
@@ -157,14 +157,27 @@ export function registerGoalInterface(
157
157
  if (!isGoalModeSupported(ctx)) throw new Error("KillerOS goals require TUI or RPC mode");
158
158
  if (!isSavedSession(ctx)) throw new Error("KillerOS goals require a saved session");
159
159
  const state = runtime.state;
160
- if (!state || state.status !== "active") throw new Error("There is no active KillerOS goal to update");
161
- if (!runtime.goalTurnInFlight
162
- || runtime.goalTurn?.turn !== state.turns
163
- || runtime.goalTurn.revision !== state.revision) {
164
- throw new Error("A goal decision can only be recorded during an active KillerOS goal turn");
165
- }
166
- if (state.turnDecision !== undefined) {
167
- throw new Error("Only one goal decision may be accepted per logical goal turn");
160
+ if (!state || state.status !== "active" && state.status !== "blocked") throw new Error("There is no active KillerOS goal to update");
161
+ const generation = runtime.lifecycleGeneration;
162
+ const permission = runtime.blockedCompletion;
163
+ const ownsDecision = (): boolean => runtime.state === state
164
+ && runtime.lifecycleGeneration === generation
165
+ && (state.status === "blocked"
166
+ ? permission?.kind === "eligible"
167
+ && runtime.blockedCompletion === permission
168
+ && permission.state === state
169
+ && permission.generation === generation
170
+ : runtime.goalTurnInFlight
171
+ && runtime.goalTurn?.turn === state.turns
172
+ && runtime.goalTurn.revision === state.revision
173
+ && state.turnDecision === undefined);
174
+ if (state.status === "blocked") {
175
+ if (!ownsDecision()) throw new Error("A blocked goal can only be completed during an eligible later ordinary request");
176
+ if (params.status !== "complete") throw new Error("A blocked goal accepts only complete; automatic continuation remains stopped");
177
+ if (state.turns === 0) throw new Error("A zero-turn blocked goal cannot record completion; no accounted goal turn exists");
178
+ } else {
179
+ if (state.turnDecision !== undefined) throw new Error("Only one goal decision may be accepted per logical goal turn");
180
+ if (!ownsDecision()) throw new Error("A goal decision can only be recorded during an active KillerOS goal turn");
168
181
  }
169
182
  if (params.status !== "complete" && params.status !== "continue" && params.status !== "blocked") {
170
183
  throw new Error("Goal update status is invalid");
@@ -175,11 +188,7 @@ export function registerGoalInterface(
175
188
  if (params.status === "complete") {
176
189
  if (state.verification) await verifyGoalDeliverable(state.verification);
177
190
  signal?.throwIfAborted();
178
- if (runtime.state !== state
179
- || runtime.goalTurn?.turn !== state.turns
180
- || runtime.goalTurn.revision !== state.revision
181
- || state.turnDecision !== undefined
182
- || !runtime.goalTurnInFlight) throw new Error("Goal changed while completion was being verified");
191
+ if (!ownsDecision()) throw new Error("Goal changed while completion was being verified");
183
192
  const verification: "file" | "model-reported" = state.verification ? "file" : "model-reported";
184
193
  const decision = { kind: "complete" as const, turn: state.turns, evidence, verification };
185
194
  try {
@@ -188,6 +197,7 @@ export function registerGoalInterface(
188
197
  pauseGoalAfterFailure(pi, runtime, ctx, `goal completion could not be saved: ${error instanceof Error ? error.message : String(error)}`);
189
198
  throw error;
190
199
  }
200
+ if (state.status === "blocked") runtime.blockedCompletion = { kind: "accepted", generation };
191
201
  const text = state.verification
192
202
  ? `Goal verified complete at ${safeTerminalText(state.verification.path)}: ${evidence}`
193
203
  : `Goal marked complete (model-reported): ${evidence}`;
@@ -197,6 +207,7 @@ export function registerGoalInterface(
197
207
  };
198
208
  }
199
209
 
210
+ if (state.status !== "active") throw new Error("A blocked goal accepts only complete");
200
211
  if (params.status === "continue") {
201
212
  if (typeof params.nextAction !== "string") throw new Error("A continue decision requires a nextAction");
202
213
  const nextAction = normalizeGoalText(params.nextAction, GOAL_EVIDENCE_LIMIT, "Goal nextAction");
@@ -97,7 +97,11 @@ function setGoalUpdateToolActive(pi: ExtensionAPI, active: boolean): void {
97
97
  }
98
98
 
99
99
  export function syncGoalUpdateTool(pi: ExtensionAPI, runtime: GoalRuntime): void {
100
- setGoalUpdateToolActive(pi, runtime.state?.status === "active");
100
+ const permission = runtime.blockedCompletion;
101
+ setGoalUpdateToolActive(pi, runtime.state?.status === "active"
102
+ || permission?.kind === "eligible"
103
+ && permission.state === runtime.state
104
+ && permission.generation === runtime.lifecycleGeneration);
101
105
  }
102
106
 
103
107
  function goalUpdateToolIsAvailable(pi: ExtensionAPI): boolean {
@@ -119,6 +123,7 @@ export function persistGoalState(
119
123
  const data: GoalEntryData = { version: GOAL_VERSION, event, state: state ?? null };
120
124
  pi.appendEntry(GOAL_ENTRY_TYPE, data);
121
125
  runtime.state = state;
126
+ runtime.blockedCompletion = undefined;
122
127
  if (event === "clear") {
123
128
  runtime.lifecycleGeneration += 1;
124
129
  runtime.continuationHeld = undefined;
@@ -372,6 +377,21 @@ function goalInstructions(state: GoalState, heading: string): string {
372
377
  ].join("\n");
373
378
  }
374
379
 
380
+ function blockedGoalSystemPrompt(state: Extract<GoalState, { status: "blocked" }>): string {
381
+ return [
382
+ "# Blocked KillerOS goal",
383
+ "Status: blocked",
384
+ "Objective:",
385
+ state.objective,
386
+ "",
387
+ "Treat the exact saved objective above as authoritative. Automatic goal continuation remains stopped; this ordinary request grants no goal turns.",
388
+ "If killeros_goal_update is available, you may call it only with status complete and current evidence after auditing and verifying the whole unchanged objective.",
389
+ "File proof checks only the persisted deliverable against its original baseline, not every acceptance criterion. General-objective completion is model-reported.",
390
+ "The disappearance of a blocker alone does not prove completion. If useful work remains or this request is unrelated, finish normally without a goal update. No decision is mandatory.",
391
+ "Never use the goal tool to continue, audit a blocker, pause, resume, edit, replace, or clear this goal. User controls remain unchanged.",
392
+ ].join("\n");
393
+ }
394
+
375
395
  function goalSystemPrompt(state: GoalState): string {
376
396
  return goalInstructions(state, "Active KillerOS goal");
377
397
  }
@@ -406,6 +426,7 @@ export function registerGoalRuntime(
406
426
  const generation = runtime.lifecycleGeneration;
407
427
  const restored = restoreGoalState(ctx);
408
428
  runtime.state = isGoalModeSupported(ctx) ? restored.state : undefined;
429
+ runtime.blockedCompletion = undefined;
409
430
  syncGoalUpdateTool(pi, runtime);
410
431
  runtime.continuationScheduled = false;
411
432
  runtime.continuationHeld = undefined;
@@ -434,6 +455,7 @@ export function registerGoalRuntime(
434
455
 
435
456
  pi.on("session_shutdown", (_event, ctx) => {
436
457
  runtime.lifecycleGeneration += 1;
458
+ runtime.blockedCompletion = undefined;
437
459
  if (runtime.state?.status === "active") {
438
460
  const checkpoint = checkpointActiveGoalState(runtime.state, Date.now());
439
461
  try {
@@ -453,7 +475,13 @@ export function registerGoalRuntime(
453
475
  runtime.continuationScheduled = false;
454
476
  if (!runtime.goalTurnInFlight && isGoalModeSupported(ctx) && isSavedSession(ctx) && pauseGoalAtTurnLimit(pi, runtime, ctx)) return;
455
477
  const current = runtime.state;
456
- if (!isGoalModeSupported(ctx) || !isSavedSession(ctx) || !current || current.status !== "active") return;
478
+ if (!isGoalModeSupported(ctx) || !isSavedSession(ctx) || !current) return;
479
+ if (current.status === "blocked") {
480
+ runtime.blockedCompletion ??= { kind: "eligible", state: current, generation: runtime.lifecycleGeneration };
481
+ syncGoalUpdateTool(pi, runtime);
482
+ return { systemPrompt: `${event.systemPrompt}\n\n${blockedGoalSystemPrompt(current)}` };
483
+ }
484
+ if (current.status !== "active") return;
457
485
  if (runtime.goalTurnInFlight) {
458
486
  if (!goalUpdateToolIsAvailable(pi)) {
459
487
  pauseGoalBeforeTurn(pi, runtime, ctx);
@@ -220,6 +220,8 @@ export function registerGoalSettlement(
220
220
  runtime: GoalRuntime,
221
221
  ): AutoCompactionGoalHandlers {
222
222
  pi.on("agent_settled", (_event, ctx) => {
223
+ runtime.blockedCompletion = undefined;
224
+ syncGoalUpdateTool(pi, runtime);
223
225
  const wasGoalTurn = runtime.goalTurnInFlight;
224
226
  const continuationWasScheduled = runtime.continuationScheduled;
225
227
  const agentEndObserved = runtime.agentEndObserved;
@@ -311,6 +313,8 @@ export function registerGoalSettlement(
311
313
  });
312
314
 
313
315
  return {
316
+ hasAcceptedTerminalDecision: (): boolean => runtime.blockedCompletion?.kind === "accepted"
317
+ && runtime.blockedCompletion.generation === runtime.lifecycleGeneration,
314
318
  isActive: (ctx: ExtensionContext): boolean => isGoalModeSupported(ctx)
315
319
  && isSavedSession(ctx)
316
320
  && runtime.state?.status === "active",
@@ -325,7 +325,8 @@ function matchesIndex(content: Buffer, entry: IndexEntry, autoCrlf: string): boo
325
325
  if (blobObjectId(content, entry.objectId.length) === entry.objectId) return true;
326
326
  if (entry.mode === "120000" || !content.includes(Buffer.from("\r\n"))) return false;
327
327
  const attributes = entry.eolAttributes;
328
- if (attributes.endsWith("eol=lf") || attributes === "-text") return false;
328
+ // eol controls checkout bytes; text files still normalize CRLF when compared with the index.
329
+ if (attributes === "-text") return false;
329
330
  const automatic = attributes.startsWith("text=auto") || !attributes;
330
331
  if (!attributes && !["true", "input"].includes(autoCrlf)) return false;
331
332
  if (automatic) {
@@ -121,6 +121,10 @@ export interface GoalTurnExecution {
121
121
  revision: number;
122
122
  }
123
123
 
124
+ export type BlockedGoalCompletion =
125
+ | { kind: "eligible"; state: Extract<GoalState, { status: "blocked" }>; generation: number }
126
+ | { kind: "accepted"; generation: number };
127
+
124
128
  export interface GoalRuntime {
125
129
  state?: GoalState;
126
130
  continuationScheduled: boolean;
@@ -128,6 +132,7 @@ export interface GoalRuntime {
128
132
  goalTurnInFlight: boolean;
129
133
  agentEndObserved: boolean;
130
134
  goalTurn?: GoalTurnExecution;
135
+ blockedCompletion?: BlockedGoalCompletion;
131
136
  automaticCompaction?: AutomaticGoalCompaction;
132
137
  persistenceRetryNeeded: boolean;
133
138
  lastStopReason?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "killeros",
3
- "version": "3.0.2",
3
+ "version": "3.0.4",
4
4
  "description": "TUI, goals, and workflow automation for the Pi coding agent",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -45,16 +45,16 @@
45
45
  ]
46
46
  },
47
47
  "peerDependencies": {
48
- "@earendil-works/pi-ai": ">=1.0.2 <2",
49
- "@earendil-works/pi-coding-agent": ">=1.0.2 <2",
50
- "@earendil-works/pi-tui": ">=1.0.2 <2",
48
+ "@earendil-works/pi-ai": ">=1.0.4 <2",
49
+ "@earendil-works/pi-coding-agent": ">=1.0.4 <2",
50
+ "@earendil-works/pi-tui": ">=1.0.4 <2",
51
51
  "typebox": ">=1.1.38 <2"
52
52
  },
53
53
  "devDependencies": {
54
- "@earendil-works/pi-ai": "1.0.2",
55
- "@earendil-works/pi-coding-agent": "1.0.2",
56
- "@earendil-works/pi-server": "1.0.2",
57
- "@earendil-works/pi-tui": "1.0.2",
54
+ "@earendil-works/pi-ai": "1.0.4",
55
+ "@earendil-works/pi-coding-agent": "1.0.4",
56
+ "@earendil-works/pi-server": "1.0.4",
57
+ "@earendil-works/pi-tui": "1.0.4",
58
58
  "@types/node": "24.12.4",
59
59
  "eslint": "^10.9.1",
60
60
  "typebox": "1.3.20",