killeros 2.1.32 → 3.0.0
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 +31 -0
- package/README.md +17 -5
- package/killeros/activity.ts +1 -1
- package/killeros/change-receipt.ts +17 -6
- package/killeros/codex-fast-state.ts +8 -0
- package/killeros/codex-fast.ts +4 -3
- package/killeros/footer.ts +2 -3
- package/killeros/goal-interface.ts +1 -0
- package/killeros/goal-state.ts +94 -34
- package/killeros/notifications.ts +1 -1
- package/killeros/passive-git-status.ts +67 -11
- package/killeros/question.ts +1 -0
- package/killeros/worked-for.ts +14 -8
- package/package.json +9 -10
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,37 @@ All notable changes to KillerOS are documented here. Releases use New Features f
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [3.0.0] - 2026-10-01
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- Raised the minimum supported Pi version and locked Pi development packages to 0.99.2, with matching CI and lockfile contracts. Users on older Pi versions must upgrade.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- Added a Windows CI job on Node.js 22.19.0 that runs the full quality suite, with a repository contract protecting the job's required checks.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- Canonicalized Windows short-path aliases in passive Git executable checks, preventing repository-local shims from being selected through paths such as `RUNNER~1` and fixing the Node.js 22.19.0 Windows CI failure. Removed temporary receipt debug logging.
|
|
20
|
+
- Applied `/codex-fast` priority requests and the footer's `fast` label to eligible native OpenAI Responses models at the official endpoint, for both API-key and ChatGPT subscription authentication. Legacy Codex behavior, command strings, and the process-local opt-in preference are unchanged; priority acceptance and billing remain OpenAI-controlled.
|
|
21
|
+
- Kept `question` and active `killeros_goal_update` directly declared in codemode-only sessions using Pi's native `model-only` exposure, which also prevents scripts and other tools from executing them. Goal activation and TUI-only question behavior are unchanged.
|
|
22
|
+
- Discarded pending change receipts after session replacement or reload and kept late activity and completion-sound handlers from using the old context, preventing stale-context extension errors during receipt finalization.
|
|
23
|
+
- Updated the `brace-expansion` override to patched 5.0.12, fixing recursion and quadratic-time denial of service in development and CI installs.
|
|
24
|
+
- Removed the stale `undici` override so development and CI installs use Pi's patched 8.10.2 dependency, resolving GHSA-3wwx-pv8p-q78v.
|
|
25
|
+
- Refreshed cached change-receipt baselines when `HEAD`, `core.filemode`, or `core.symlinks` changes between responses, while still rejecting `HEAD` changes made during a response.
|
|
26
|
+
- Made passive footer scans and change receipts honor `core.symlinks=false` placeholders and `core.filemode=false` without masking file-type changes.
|
|
27
|
+
- Preserved parent traversal in relative goal paths so directory links cannot redirect file proof to a lexically normalized target or collapse distinct targets.
|
|
28
|
+
- Made the Windows hook cleanup test wait for its child to start and verify that both processes terminate, avoiding false failures when `taskkill` cannot confirm an already-finished cleanup.
|
|
29
|
+
- Showed stable provider-reported model IDs on settled receipts when they differ from the selected alias, including GitHub Copilot Claude models.
|
|
30
|
+
- Reported CRLF worktree content as modified when indexed attributes require `eol=lf`, matching Git in footer counts and change receipts.
|
|
31
|
+
- Kept quoted prose goal destinations on model-reported completion; inferred file proof now requires a path-shaped destination.
|
|
32
|
+
- Isolated passive footer, change-receipt, and startup branch Git scans from inherited repository-selection environment variables.
|
|
33
|
+
- Rejected passive Git executables reached through repository-owned directory links while continuing to safe later `PATH` entries.
|
|
34
|
+
- Prevented single-file goal proof from masking unquoted `Makefile`, `Dockerfile`, or `.gitignore` list items.
|
|
35
|
+
- Prevented single-file goal proof from overlooking additional extensionless targets extracted by goal action and destination clauses. Equivalent path spellings still share one exact-path verification contract.
|
|
36
|
+
- Prevented single-file goal proof from masking additional filenames that extend version or Node.js prose. Goal parsing now keeps spaces and trailing punctuation in quoted paths and matches exact Node.js prose case-insensitively.
|
|
37
|
+
|
|
7
38
|
## [2.1.32] - 2026-09-24
|
|
8
39
|
|
|
9
40
|
### Fixed
|
package/README.md
CHANGED
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
A TypeScript extension for the [Pi coding agent](https://github.com/earendil-works/pi) that replaces the stock TUI and adds long-running goals, reasoning controls, and workflow commands.
|
|
4
4
|
|
|
5
|
+

|
|
6
|
+
|
|
5
7
|
## What you get
|
|
6
8
|
|
|
7
9
|
- A custom TUI: startup masthead with versions, model, working directory, and Git branch; a dark theme with coral accents; a multiline editor with slash-command completion; a footer that tracks model, context, and goal state; settled task receipts with duration and token usage.
|
|
8
10
|
- `/goal`: set an objective and Pi keeps working toward it across turns, compaction, reloads, and branch navigation. Each turn must record `continue` with evidence and one next action, `complete`, or the existing blocker decision; otherwise the goal pauses. New goals pause after 20 turns; `/goal resume` grants another 20.
|
|
9
|
-
- `/codex-fast`: toggles the `priority` service tier on Codex requests or reports its status.
|
|
11
|
+
- `/codex-fast`: toggles the `priority` service tier on legacy Codex and eligible native OpenAI Responses requests or reports its status.
|
|
10
12
|
- `/handoff`: starts a fresh linked session carrying visible continuation context.
|
|
11
13
|
- `/auto-compact`: reports, enables, disables, or tunes automatic context compaction.
|
|
12
14
|
- A `question` tool with single-select and multi-select modes.
|
|
@@ -17,7 +19,7 @@ A TypeScript extension for the [Pi coding agent](https://github.com/earendil-wor
|
|
|
17
19
|
## Requirements
|
|
18
20
|
|
|
19
21
|
- Node.js 22.19.0+
|
|
20
|
-
- Pi 0.
|
|
22
|
+
- Pi 0.99.2 or later within the 0.x release line
|
|
21
23
|
- An interactive TUI session for the custom header, editor, footer, and `question`
|
|
22
24
|
|
|
23
25
|
## Install
|
|
@@ -32,7 +34,7 @@ Or from GitHub:
|
|
|
32
34
|
pi install git:github.com/KyrosHendrix/pi-KillerOS
|
|
33
35
|
```
|
|
34
36
|
|
|
35
|
-
Pin a release by appending its tag, for example `@
|
|
37
|
+
Pin a release by appending its tag, for example `@v3.0.0`. Add `-l` to install only for the current project. Restart Pi after installing.
|
|
36
38
|
|
|
37
39
|
## Commands
|
|
38
40
|
|
|
@@ -51,6 +53,12 @@ Pin a release by appending its tag, for example `@v2.1.32`. Add `-l` to install
|
|
|
51
53
|
/exit Quit Pi gracefully
|
|
52
54
|
```
|
|
53
55
|
|
|
56
|
+
Fast mode is off by default. `/codex-fast` toggles it, and `/codex-fast status` reports the preference without changing it. The preference stays in the current Pi process across model switches and extension reloads, but is not saved between processes. The command name and notification strings retain their legacy Codex wording.
|
|
57
|
+
|
|
58
|
+
Fast mode applies to legacy `openai-codex` requests and selected native models with provider `openai`, API `openai-responses`, and base URL `https://api.openai.com/v1`, with an optional trailing slash. Native request payloads must also name the selected model's exact ID. Both API-key and ChatGPT subscription authentication use this rule. Custom endpoints, Azure, other APIs, and ambiguous native requests pass through unchanged. When enabled, fast mode overrides an eligible request's existing service tier with `priority`; when disabled, it leaves the request unchanged.
|
|
59
|
+
|
|
60
|
+
The TUI footer's `fast` label means the enabled preference applies to the selected model, not that OpenAI accepted priority service. OpenAI controls account and model eligibility, the actual service tier, and charges. Priority can affect billing; subscription login does not guarantee accepted or free priority service. KillerOS leaves provider errors to Pi and does not silently retry with a different tier.
|
|
61
|
+
|
|
54
62
|
## Behavior by mode
|
|
55
63
|
|
|
56
64
|
| Mode | What works |
|
|
@@ -59,6 +67,8 @@ Pin a release by appending its tag, for example `@v2.1.32`. Add `-l` to install
|
|
|
59
67
|
| RPC | Goals, proactive compaction; no TUI components, sounds, title indicator |
|
|
60
68
|
| Print/JSON | No interactive questions, `/goal`, or proactive compaction |
|
|
61
69
|
|
|
70
|
+
`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.
|
|
71
|
+
|
|
62
72
|
## Configuration
|
|
63
73
|
|
|
64
74
|
The packaged `killeros` theme activates on TUI start. Compaction triggers by default at 15% tokens remaining, stored in global `killeros.json`:
|
|
@@ -83,13 +93,15 @@ State proof in the objective so the agent can verify it with its normal tools:
|
|
|
83
93
|
/goal Reduce p95 checkout latency below 120 ms, verified by the checkout benchmark, while keeping the correctness suite green
|
|
84
94
|
```
|
|
85
95
|
|
|
86
|
-
A direct quoted file target binds silent file proof
|
|
96
|
+
A direct quoted path-shaped file target binds silent file proof. For an extensionless relative file, use an explicit path such as `./summary` or `.\summary`:
|
|
87
97
|
|
|
88
98
|
```text
|
|
89
99
|
/goal Fix `killeros/footer.ts`, verified by npm test
|
|
90
100
|
```
|
|
91
101
|
|
|
92
|
-
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. Restored goals keep their persisted limit.
|
|
102
|
+
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. Restored goals keep their persisted limit.
|
|
103
|
+
|
|
104
|
+
Session replacement and reload discard unfinished task receipts. Late receipt results do not write or notify through the old session context.
|
|
93
105
|
|
|
94
106
|
Completion sounds are off by default; change with `/notification` in TUI mode. The tab-title indicator requires a Nerd Font.
|
|
95
107
|
|
package/killeros/activity.ts
CHANGED
|
@@ -102,7 +102,7 @@ export function registerRequestActivity(pi: ExtensionAPI): void {
|
|
|
102
102
|
});
|
|
103
103
|
|
|
104
104
|
pi.on("agent_settled", (_event, ctx) => {
|
|
105
|
-
if (ctx.mode !== "tui" || !
|
|
105
|
+
if (!active || ctx.mode !== "tui" || !ctx.isIdle?.() || ctx.hasPendingMessages?.()) return;
|
|
106
106
|
clear(ctx);
|
|
107
107
|
});
|
|
108
108
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { createReadStream, watch } from "node:fs";
|
|
3
|
-
import { inspectWorktreeWithoutFilters, passiveGitCommand, passiveGitEnv, type PassiveWorktreeFile as DirtyFile } from "./passive-git-status.ts";
|
|
3
|
+
import { inspectWorktreeWithoutFilters, passiveGitCommand, passiveGitEnv, passiveWorktreeMode, passiveWorktreeSettings, type PassiveWorktreeFile as DirtyFile, type PassiveWorktreeSnapshot } from "./passive-git-status.ts";
|
|
4
4
|
import { lstat, open, readlink } from "node:fs/promises";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { createInflate } from "node:zlib";
|
|
@@ -176,7 +176,7 @@ async function readBoundedFile(filePath: string, limit: number): Promise<Buffer>
|
|
|
176
176
|
}
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
-
type Snapshot =
|
|
179
|
+
type Snapshot = PassiveWorktreeSnapshot;
|
|
180
180
|
type RepositoryMonitor = {
|
|
181
181
|
changedPaths: Set<string>;
|
|
182
182
|
inUse: boolean;
|
|
@@ -218,7 +218,11 @@ async function snapshotKnownPaths(repo: Repository, baseline: Snapshot, paths: r
|
|
|
218
218
|
try {
|
|
219
219
|
const absolutePath = path.join(repo.root, ...filePath.split("/"));
|
|
220
220
|
const stats = await lstat(absolutePath);
|
|
221
|
-
const mode =
|
|
221
|
+
const mode = passiveWorktreeMode(
|
|
222
|
+
stats.isSymbolicLink() ? "120000" : stats.mode & 0o111 ? "100755" : "100644",
|
|
223
|
+
previous.indexMode,
|
|
224
|
+
baseline,
|
|
225
|
+
);
|
|
222
226
|
const content = stats.isSymbolicLink()
|
|
223
227
|
? Buffer.from(await readlink(absolutePath))
|
|
224
228
|
: await readBoundedFile(absolutePath, SNAPSHOT_CONTENT_LIMIT - totalBytes);
|
|
@@ -230,7 +234,7 @@ async function snapshotKnownPaths(repo: Repository, baseline: Snapshot, paths: r
|
|
|
230
234
|
files.set(filePath, { ...previous, mode: undefined, content: undefined });
|
|
231
235
|
}
|
|
232
236
|
}
|
|
233
|
-
return {
|
|
237
|
+
return { ...baseline, files };
|
|
234
238
|
}
|
|
235
239
|
|
|
236
240
|
async function currentHead(repo: Repository): Promise<string> {
|
|
@@ -262,7 +266,7 @@ async function refreshSnapshot(monitor: RepositoryMonitor, observedPaths: readon
|
|
|
262
266
|
if (observes(filePath, observedPaths)) files.delete(filePath);
|
|
263
267
|
}
|
|
264
268
|
for (const [filePath, file] of scoped.files) files.set(filePath, file);
|
|
265
|
-
return {
|
|
269
|
+
return { ...scoped, files };
|
|
266
270
|
}
|
|
267
271
|
|
|
268
272
|
function createMonitor(repo: Repository, initialSnapshot: Snapshot): RepositoryMonitor {
|
|
@@ -626,7 +630,14 @@ export async function beginChangeReceipt(cwd: string, trusted = true): Promise<C
|
|
|
626
630
|
const idleChanges = [...monitor.changedPaths];
|
|
627
631
|
monitor.changedPaths.clear();
|
|
628
632
|
try {
|
|
629
|
-
|
|
633
|
+
const [head, settings] = await Promise.all([
|
|
634
|
+
currentHead(repo),
|
|
635
|
+
passiveWorktreeSettings((args) => runGit(repo.root, args)),
|
|
636
|
+
]);
|
|
637
|
+
monitor.snapshot = head !== monitor.snapshot.head
|
|
638
|
+
|| settings.fileMode !== monitor.snapshot.fileMode || settings.symlinks !== monitor.snapshot.symlinks
|
|
639
|
+
? await snapshot(repo)
|
|
640
|
+
: await refreshSnapshot(monitor, idleChanges);
|
|
630
641
|
} catch (error) {
|
|
631
642
|
discardMonitor(monitor);
|
|
632
643
|
throw error;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
1
3
|
type CodexFastStateListener = () => void;
|
|
2
4
|
|
|
3
5
|
interface CodexFastState {
|
|
@@ -25,6 +27,12 @@ const state: CodexFastState = isCodexFastState(savedState) ? savedState : {
|
|
|
25
27
|
};
|
|
26
28
|
globalThis.__killerosCodexFastState = state;
|
|
27
29
|
|
|
30
|
+
export function isCodexFastModel(model: ExtensionContext["model"]): boolean {
|
|
31
|
+
return model?.provider === "openai-codex"
|
|
32
|
+
|| (model?.provider === "openai" && model.api === "openai-responses"
|
|
33
|
+
&& (model.baseUrl === "https://api.openai.com/v1" || model.baseUrl === "https://api.openai.com/v1/"));
|
|
34
|
+
}
|
|
35
|
+
|
|
28
36
|
export function isCodexFastEnabled(): boolean {
|
|
29
37
|
return state.enabled;
|
|
30
38
|
}
|
package/killeros/codex-fast.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { isCodexFastEnabled, toggleCodexFast } from "./codex-fast-state.ts";
|
|
2
|
+
import { isCodexFastEnabled, isCodexFastModel, toggleCodexFast } from "./codex-fast-state.ts";
|
|
3
3
|
|
|
4
|
-
const CODEX_PROVIDER = "openai-codex";
|
|
5
4
|
type RequestPayload = Record<string, unknown>;
|
|
6
5
|
|
|
7
6
|
function isRequestPayload(value: unknown): value is RequestPayload {
|
|
@@ -28,9 +27,11 @@ export function registerCodexFastMode(pi: ExtensionAPI): void {
|
|
|
28
27
|
});
|
|
29
28
|
|
|
30
29
|
pi.on("before_provider_request", (event, ctx) => {
|
|
31
|
-
if (!isCodexFastEnabled() || ctx.model
|
|
30
|
+
if (!isCodexFastEnabled() || !isCodexFastModel(ctx.model) || !isRequestPayload(event.payload)) {
|
|
32
31
|
return event.payload;
|
|
33
32
|
}
|
|
33
|
+
// The hook exposes no dispatched provider/API. Native requests must match the selected model.
|
|
34
|
+
if (ctx.model?.provider === "openai" && event.payload.model !== ctx.model.id) return event.payload;
|
|
34
35
|
return { ...event.payload, service_tier: "priority" };
|
|
35
36
|
});
|
|
36
37
|
}
|
package/killeros/footer.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { watch } from "node:fs";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { type ExtensionAPI, type ExtensionContext, type Theme, type ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import { truncateToWidth, visibleWidth, type TUI } from "@earendil-works/pi-tui";
|
|
6
|
-
import { isCodexFastEnabled, subscribeCodexFast } from "./codex-fast-state.ts";
|
|
6
|
+
import { isCodexFastEnabled, isCodexFastModel, subscribeCodexFast } from "./codex-fast-state.ts";
|
|
7
7
|
import { formatCwd, formatTime, modelDisplayName, padRight } from "./display.ts";
|
|
8
8
|
import { inspectWorktreeWithoutFilters, passiveGitCommand, passiveGitEnv, type PassiveGitRunner } from "./passive-git-status.ts";
|
|
9
9
|
import { goalElapsedMilliseconds } from "./goal-state.ts";
|
|
@@ -15,7 +15,6 @@ const GIT_STATUS_REFRESH_INTERVAL_MS = 30_000;
|
|
|
15
15
|
const GIT_STATUS_TIMEOUT_MS = 5_000;
|
|
16
16
|
const GIT_STATUS_WATCH_DEBOUNCE_MS = 250;
|
|
17
17
|
const GIT_STATUS_WATCH_INTERVAL_MS = 5_000;
|
|
18
|
-
const CODEX_PROVIDER = "openai-codex";
|
|
19
18
|
export const colorDirectory = (text: string): string => `\x1B[38;2;240;248;154m${text}\x1B[39m`;
|
|
20
19
|
|
|
21
20
|
export interface GitFileChanges {
|
|
@@ -309,7 +308,7 @@ export function formatModel(
|
|
|
309
308
|
if (!model) return theme.fg("dim", "no model");
|
|
310
309
|
const displayName = modelDisplayName(model) || "unknown model";
|
|
311
310
|
const name = theme.fg("text", displayName);
|
|
312
|
-
const fast = showCodexFast && model
|
|
311
|
+
const fast = showCodexFast && isCodexFastModel(model) ? theme.fg("text", "fast") : "";
|
|
313
312
|
const provider = includeProvider ? theme.fg("dim", formatProviderName(model.provider)) : "";
|
|
314
313
|
return [name, fast, provider].filter(Boolean).join(" ");
|
|
315
314
|
}
|
|
@@ -150,6 +150,7 @@ export function registerGoalInterface(
|
|
|
150
150
|
label: "Goal update",
|
|
151
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.",
|
|
152
152
|
parameters: GoalUpdateParams,
|
|
153
|
+
exposure: "model-only",
|
|
153
154
|
executionMode: "sequential",
|
|
154
155
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
155
156
|
signal?.throwIfAborted();
|
package/killeros/goal-state.ts
CHANGED
|
@@ -144,11 +144,19 @@ function isAbsoluteFilePath(value: string): boolean {
|
|
|
144
144
|
}
|
|
145
145
|
|
|
146
146
|
function stripUnquotedPathPunctuation(value: string): string {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
147
|
+
let stripped = value;
|
|
148
|
+
while (true) {
|
|
149
|
+
const withoutMarks = stripped.replace(/[.,;:!?]+$/u, "");
|
|
150
|
+
if (withoutMarks !== stripped) {
|
|
151
|
+
stripped = withoutMarks;
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
const closer = stripped.at(-1);
|
|
155
|
+
const opener = closer === ")" ? "(" : closer === "]" ? "[" : closer === "}" ? "{" : undefined;
|
|
156
|
+
if (closer === undefined || opener === undefined
|
|
157
|
+
|| stripped.split(closer).length <= stripped.split(opener).length) return stripped;
|
|
158
|
+
stripped = stripped.slice(0, -1);
|
|
159
|
+
}
|
|
152
160
|
}
|
|
153
161
|
|
|
154
162
|
function isGoalFileVerification(value: unknown): value is GoalFileVerification {
|
|
@@ -348,41 +356,80 @@ export async function captureGoalFileBaseline(
|
|
|
348
356
|
const GOAL_URL_PATTERN = /(?:https?|file):\/\/[^\s"'`]+/giu;
|
|
349
357
|
const GOAL_QUOTED_PATTERN = /`([^`\r\n]+)`|"([^"\r\n]+)"|'([^'\r\n]+)'/gu;
|
|
350
358
|
const GOAL_ABSOLUTE_PATTERN = /(?:^|[\s"'`(\[{,;])([A-Za-z]:[\\/][^\s,;'"`]+|\/[^\s,;'"`]+)/gu;
|
|
351
|
-
const GOAL_LIST_FILE_PATTERN = /(?:\band\b|\bor\b|[,;&+])\s*(?:`([^`\r\n]+)`|"([^"\r\n]+)"|'([^'\r\n]+)'|([A-Za-z0-9_][A-Za-z0-9_.-]*(?:[\\/][A-Za-z0-9_.-]+)*\.[A-Za-z0-9]{1,12})\b)/giu;
|
|
359
|
+
const GOAL_LIST_FILE_PATTERN = /(?:\band\b|\bor\b|\bplus\b|\balong\s+with\b|[,;&+])\s*(?:`([^`\r\n]+)`|"([^"\r\n]+)"|'([^'\r\n]+)'|((?:\.{1,2}[\\/])*[A-Za-z0-9_][A-Za-z0-9_.-]*(?:[\\/][A-Za-z0-9_.-]+)*\.[A-Za-z0-9]{1,12})\b|(Makefile|Dockerfile|\.gitignore)\b)/giu;
|
|
352
360
|
const GOAL_RELATIVE_FILE_PATTERN = /(?:^|[\s"'`(\[{,;])((?:\.{1,2}[\\/])*[A-Za-z0-9_][A-Za-z0-9_.-]*(?:[\\/][A-Za-z0-9_.-]+)*\.[A-Za-z0-9]{1,12})\b/gu;
|
|
353
361
|
|
|
354
362
|
function isQuotedGoalFileMention(raw: string): boolean {
|
|
355
363
|
const value = raw.trim();
|
|
356
|
-
if (!value ||
|
|
364
|
+
if (!value || /\p{Cc}/u.test(value) || /^(?:https?|file):\/\//iu.test(value) || /[\\/]$/u.test(value)) return false;
|
|
357
365
|
return /[\/\\.:]/u.test(value);
|
|
358
366
|
}
|
|
359
367
|
|
|
360
|
-
function
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
368
|
+
function isPathShapedGoalCandidate(raw: string): boolean {
|
|
369
|
+
if (/\p{Cc}/u.test(raw)) return false;
|
|
370
|
+
const value = raw.trim();
|
|
371
|
+
if (!value || /[\\\/]$/u.test(value)) return false;
|
|
372
|
+
if (path.isAbsolute(value) || path.win32.isAbsolute(value)) return true;
|
|
373
|
+
if (URL.canParse(value)) return false;
|
|
374
|
+
return /[\\\/]/u.test(value)
|
|
375
|
+
|| value.startsWith(".") && value !== "." && value !== ".."
|
|
376
|
+
|| /\.[A-Za-z0-9]{1,12}$/u.test(value);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function goalPathIdentity(filePath: string): string {
|
|
380
|
+
const nativePath = process.platform === "win32" ? filePath.replaceAll("/", "\\") : filePath;
|
|
381
|
+
// Parent segments may traverse symlinks; keep them distinct without filesystem proof.
|
|
382
|
+
const normalized = nativePath.split(path.sep).includes("..") ? nativePath : path.normalize(nativePath);
|
|
383
|
+
return process.platform === "win32" ? normalized.toLowerCase() : normalized;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function absoluteGoalFilePath(value: string, cwd: string): string {
|
|
387
|
+
if (path.isAbsolute(value) || path.win32.isAbsolute(value)) return value;
|
|
388
|
+
const nativePath = process.platform === "win32" ? value.replaceAll("/", "\\") : value;
|
|
389
|
+
// Resolving parent segments lexically can select a different file across a directory symlink.
|
|
390
|
+
return nativePath.split(path.sep).includes("..")
|
|
391
|
+
? `${path.resolve(cwd)}${path.sep}${nativePath}`
|
|
392
|
+
: path.resolve(cwd, value);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function normalizeGoalFileMention(raw: string, cwd: string, quoted = false): string | undefined {
|
|
396
|
+
const trimmed = raw.trim();
|
|
397
|
+
const value = quoted ? trimmed : stripUnquotedPathPunctuation(trimmed);
|
|
398
|
+
if (!value || /\p{Cc}/u.test(value) || !quoted && /\s/u.test(value)) return undefined;
|
|
399
|
+
const absolute = absoluteGoalFilePath(value, cwd);
|
|
364
400
|
if (!isAbsoluteFilePath(absolute)) return undefined;
|
|
365
|
-
return
|
|
401
|
+
return goalPathIdentity(absolute);
|
|
366
402
|
}
|
|
367
403
|
|
|
368
404
|
/** Counts distinct path-shaped mentions so several files cannot bind proof to one. */
|
|
369
|
-
function countGoalTargetFiles(objective: string, cwd: string, target: string): number {
|
|
370
|
-
const
|
|
405
|
+
function countGoalTargetFiles(objective: string, cwd: string, target: string, targetQuoted: boolean): number {
|
|
406
|
+
const source = objective.replace(GOAL_URL_PATTERN, " ");
|
|
407
|
+
// ponytail: Only these exact prose forms are excluded; quote ambiguous bare filenames for exact proof.
|
|
408
|
+
const prose = source.replace(GOAL_QUOTED_PATTERN, " ")
|
|
409
|
+
.replace(/(?<![A-Za-z0-9_.\\/-])version\s+\d+(?:\.\d+)+(?=$|[\s,;:!?)\]}]|\.(?=$|[\s,;:!?)\]}]))/giu, " ")
|
|
410
|
+
.replace(/(?<![A-Za-z0-9_.\\/-])for\s+Node\.js(?!\s+compatibility\.[A-Za-z0-9])(?=$|[\s,;:!?)\]}]|\.(?=$|[\s,;:!?)\]}]))|(?<![A-Za-z0-9_.\\/-])Node\.js\s+compatibility(?=$|[\s,;:!?)\]}]|\.(?=$|[\s,;:!?)\]}]))/giu, " ");
|
|
371
411
|
const mentions = new Set<string>();
|
|
372
|
-
const add = (raw: string): void => {
|
|
373
|
-
const normalized = normalizeGoalFileMention(raw, cwd);
|
|
412
|
+
const add = (raw: string, quoted = false): void => {
|
|
413
|
+
const normalized = normalizeGoalFileMention(raw, cwd, quoted);
|
|
374
414
|
if (normalized) mentions.add(normalized);
|
|
375
415
|
};
|
|
376
|
-
add(target);
|
|
377
|
-
for (const match of
|
|
416
|
+
add(target, targetQuoted);
|
|
417
|
+
for (const match of source.matchAll(GOAL_QUOTED_PATTERN)) {
|
|
378
418
|
const raw = match[1] ?? match[2] ?? match[3] ?? "";
|
|
379
|
-
if (isQuotedGoalFileMention(raw)) add(raw);
|
|
419
|
+
if (isQuotedGoalFileMention(raw)) add(raw, true);
|
|
420
|
+
}
|
|
421
|
+
for (const match of source.matchAll(GOAL_LIST_FILE_PATTERN)) {
|
|
422
|
+
const quoted = match[1] ?? match[2] ?? match[3];
|
|
423
|
+
if (quoted !== undefined) add(quoted, true);
|
|
380
424
|
}
|
|
381
425
|
for (const match of prose.matchAll(GOAL_ABSOLUTE_PATTERN)) {
|
|
382
426
|
if (match[1]) add(stripUnquotedPathPunctuation(match[1].trim()));
|
|
383
427
|
}
|
|
384
|
-
for (const match of prose.matchAll(GOAL_LIST_FILE_PATTERN))
|
|
385
|
-
|
|
428
|
+
for (const match of prose.matchAll(GOAL_LIST_FILE_PATTERN)) {
|
|
429
|
+
if (match[4]) add(match[4]);
|
|
430
|
+
if (match[5]) add(match[5]);
|
|
431
|
+
}
|
|
432
|
+
for (const match of prose.matchAll(GOAL_RELATIVE_FILE_PATTERN)) {
|
|
386
433
|
if (match[1]) add(match[1]);
|
|
387
434
|
}
|
|
388
435
|
return mentions.size;
|
|
@@ -390,30 +437,43 @@ function countGoalTargetFiles(objective: string, cwd: string, target: string): n
|
|
|
390
437
|
|
|
391
438
|
/** Captures one explicit output path so goal completion can verify its creation or modification. */
|
|
392
439
|
export async function inferGoalVerification(objective: string, cwd: string): Promise<GoalFileVerification | undefined> {
|
|
393
|
-
const candidates: string
|
|
440
|
+
const candidates: Array<{ raw: string; quoted: boolean }> = [];
|
|
394
441
|
const destination = /\b(?:create|write|save|generate)\b[^\r\n]{0,160}?\b(?:file|document|markdown|report|spreadsheet|presentation|image)\b\s+(?:to|at|as|destination(?:\s+is)?|output(?:\s+(?:to|at))?)\b\s*(?:`([^`\r\n]+)`|"([^"\r\n]+)"|'([^'\r\n]+)'|([A-Za-z]:\\[^\s,;]+|\/[^\s,;]+))/giu;
|
|
395
442
|
for (const match of objective.matchAll(destination)) {
|
|
396
443
|
const quoted = match[1] ?? match[2] ?? match[3];
|
|
397
|
-
candidates.push(
|
|
444
|
+
candidates.push({
|
|
445
|
+
raw: quoted !== undefined ? quoted : stripUnquotedPathPunctuation((match[4] ?? "").trim()),
|
|
446
|
+
quoted: quoted !== undefined,
|
|
447
|
+
});
|
|
398
448
|
}
|
|
399
449
|
const direct = /\b(?:update|edit|fix|refactor|migrate)\s+(`([^`\r\n]+)`|"([^"\r\n]+)"|'([^'\r\n]+)')/giu;
|
|
400
450
|
for (const match of objective.matchAll(direct)) {
|
|
401
451
|
const quoted = match[2] ?? match[3] ?? match[4];
|
|
402
|
-
if (quoted !== undefined) candidates.push(quoted
|
|
452
|
+
if (quoted !== undefined) candidates.push({ raw: quoted, quoted: true });
|
|
403
453
|
}
|
|
404
|
-
const
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
const
|
|
408
|
-
if (
|
|
454
|
+
const operands = new Set<string>();
|
|
455
|
+
const resolved = new Map<string, { filePath: string; quoted: boolean }>();
|
|
456
|
+
for (const candidate of candidates) {
|
|
457
|
+
const value = candidate.raw.trim();
|
|
458
|
+
if (!value || /\p{Cc}/u.test(value)) continue;
|
|
459
|
+
const alreadyAbsolute = path.isAbsolute(value) || path.win32.isAbsolute(value);
|
|
460
|
+
if (!alreadyAbsolute && URL.canParse(value)) continue;
|
|
461
|
+
const absolute = absoluteGoalFilePath(value, cwd);
|
|
462
|
+
const identity = goalPathIdentity(absolute);
|
|
463
|
+
operands.add(identity);
|
|
464
|
+
if (!isPathShapedGoalCandidate(candidate.raw) || !isAbsoluteFilePath(absolute)) continue;
|
|
465
|
+
const existing = resolved.get(identity);
|
|
466
|
+
resolved.set(identity, {
|
|
467
|
+
filePath: existing?.filePath ?? absolute,
|
|
468
|
+
quoted: candidate.quoted || existing?.quoted === true,
|
|
469
|
+
});
|
|
409
470
|
}
|
|
410
|
-
|
|
411
|
-
if (unique.length !== 1) return undefined;
|
|
471
|
+
if (operands.size !== 1 || resolved.size !== 1) return undefined;
|
|
412
472
|
// A goal that names several files must not verify from one file alone.
|
|
413
473
|
// Repeated references to the same file count as one target.
|
|
414
|
-
const
|
|
415
|
-
if (countGoalTargetFiles(objective, cwd, filePath) > 1) return undefined;
|
|
416
|
-
return { kind: "file", path: filePath, baseline: await captureGoalFileBaseline(filePath) };
|
|
474
|
+
const target = resolved.values().next().value;
|
|
475
|
+
if (target === undefined || countGoalTargetFiles(objective, cwd, target.filePath, target.quoted) > 1) return undefined;
|
|
476
|
+
return { kind: "file", path: target.filePath, baseline: await captureGoalFileBaseline(target.filePath) };
|
|
417
477
|
}
|
|
418
478
|
|
|
419
479
|
export async function verifyGoalDeliverable(verification: GoalFileVerification): Promise<void> {
|
|
@@ -117,7 +117,7 @@ export function registerCompletionNotifications(
|
|
|
117
117
|
});
|
|
118
118
|
|
|
119
119
|
pi.on("agent_settled", (_event, ctx) => {
|
|
120
|
-
if (ctx.mode !== "tui"
|
|
120
|
+
if (!requestPending || ctx.mode !== "tui") return;
|
|
121
121
|
if (!ctx.isIdle() || ctx.hasPendingMessages()) return;
|
|
122
122
|
requestPending = false;
|
|
123
123
|
if (!enabled || lastStopReason === "aborted") return;
|
|
@@ -22,8 +22,30 @@ export type PassiveWorktreeFile = {
|
|
|
22
22
|
export type PassiveWorktreeSnapshot = {
|
|
23
23
|
head: string;
|
|
24
24
|
files: Map<string, PassiveWorktreeFile>;
|
|
25
|
+
fileMode: boolean;
|
|
26
|
+
symlinks: boolean;
|
|
25
27
|
};
|
|
26
28
|
|
|
29
|
+
export async function passiveWorktreeSettings(runGit: PassiveGitRunner): Promise<Pick<PassiveWorktreeSnapshot, "fileMode" | "symlinks">> {
|
|
30
|
+
const [fileMode, symlinks] = await Promise.all([
|
|
31
|
+
runGit(["config", "--type=bool", "--default", "true", "--get", "core.filemode"]),
|
|
32
|
+
runGit(["config", "--type=bool", "--default", "true", "--get", "core.symlinks"]),
|
|
33
|
+
]);
|
|
34
|
+
return { fileMode: decode(fileMode).trim() === "true", symlinks: decode(symlinks).trim() === "true" };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function passiveWorktreeMode(
|
|
38
|
+
actual: string,
|
|
39
|
+
indexed: string | undefined,
|
|
40
|
+
settings: Pick<PassiveWorktreeSnapshot, "fileMode" | "symlinks">,
|
|
41
|
+
): string {
|
|
42
|
+
if (actual === "100644" || actual === "100755") {
|
|
43
|
+
if (indexed === "120000" && !settings.symlinks) return indexed;
|
|
44
|
+
if ((indexed === "100644" || indexed === "100755") && !settings.fileMode) return indexed;
|
|
45
|
+
}
|
|
46
|
+
return actual;
|
|
47
|
+
}
|
|
48
|
+
|
|
27
49
|
type IndexEntry = {
|
|
28
50
|
mode: string;
|
|
29
51
|
objectId: string;
|
|
@@ -77,7 +99,7 @@ function inspectedRoot(cwd: string): string | undefined {
|
|
|
77
99
|
}
|
|
78
100
|
let base: string;
|
|
79
101
|
try {
|
|
80
|
-
base = realpathSync(start);
|
|
102
|
+
base = realpathSync.native(start);
|
|
81
103
|
} catch {
|
|
82
104
|
base = start;
|
|
83
105
|
}
|
|
@@ -86,7 +108,7 @@ function inspectedRoot(cwd: string): string | undefined {
|
|
|
86
108
|
try {
|
|
87
109
|
if (existsSync(path.join(current, ".git"))) {
|
|
88
110
|
try {
|
|
89
|
-
return realpathSync(current);
|
|
111
|
+
return realpathSync.native(current);
|
|
90
112
|
} catch {
|
|
91
113
|
return current;
|
|
92
114
|
}
|
|
@@ -120,6 +142,20 @@ function insideInspected(candidate: string, root: string): boolean {
|
|
|
120
142
|
return normalizedCandidate.startsWith(`${trimmed}${path.sep}`);
|
|
121
143
|
}
|
|
122
144
|
|
|
145
|
+
function traversesInspected(candidate: string, root: string): boolean {
|
|
146
|
+
let current = path.dirname(candidate);
|
|
147
|
+
for (;;) {
|
|
148
|
+
try {
|
|
149
|
+
if (insideInspected(realpathSync.native(current), root)) return true;
|
|
150
|
+
} catch {
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
const parent = path.dirname(current);
|
|
154
|
+
if (parent === current) return false;
|
|
155
|
+
current = parent;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
123
159
|
/** Returns an absolute Git binary outside the inspected repository without starting a locator process. */
|
|
124
160
|
export function passiveGitCommand(cwd: string, env: NodeJS.ProcessEnv = process.env): string | undefined {
|
|
125
161
|
const root = inspectedRoot(cwd);
|
|
@@ -134,6 +170,7 @@ export function passiveGitCommand(cwd: string, env: NodeJS.ProcessEnv = process.
|
|
|
134
170
|
if (directory === "" || directory.trim() === "" || !path.isAbsolute(directory)) continue;
|
|
135
171
|
for (const base of baseNames) {
|
|
136
172
|
const candidate = path.join(directory, base);
|
|
173
|
+
if (insideInspected(candidate, root)) continue;
|
|
137
174
|
try {
|
|
138
175
|
if (!statSync(candidate).isFile()) continue;
|
|
139
176
|
} catch {
|
|
@@ -148,27 +185,45 @@ export function passiveGitCommand(cwd: string, env: NodeJS.ProcessEnv = process.
|
|
|
148
185
|
}
|
|
149
186
|
let resolved: string;
|
|
150
187
|
try {
|
|
151
|
-
resolved = realpathSync(candidate);
|
|
188
|
+
resolved = realpathSync.native(candidate);
|
|
152
189
|
} catch {
|
|
153
190
|
continue;
|
|
154
191
|
}
|
|
155
|
-
if (path.isAbsolute(resolved) && !insideInspected(resolved, root)) return resolved;
|
|
192
|
+
if (path.isAbsolute(resolved) && !insideInspected(resolved, root) && !traversesInspected(candidate, root)) return resolved;
|
|
156
193
|
}
|
|
157
194
|
}
|
|
158
195
|
return undefined;
|
|
159
196
|
}
|
|
160
197
|
|
|
161
|
-
/** Environment for automatic Git children,
|
|
198
|
+
/** Environment for automatic Git children, without inherited repository selection or PATH command resolution. */
|
|
162
199
|
export function passiveGitEnv(base: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv {
|
|
163
|
-
const env: NodeJS.ProcessEnv = { ...base
|
|
200
|
+
const env: NodeJS.ProcessEnv = { ...base };
|
|
201
|
+
const selectors = [
|
|
202
|
+
"GIT_DIR",
|
|
203
|
+
"GIT_CEILING_DIRECTORIES",
|
|
204
|
+
"GIT_DISCOVERY_ACROSS_FILESYSTEM",
|
|
205
|
+
"GIT_WORK_TREE",
|
|
206
|
+
"GIT_INDEX_FILE",
|
|
207
|
+
"GIT_OBJECT_DIRECTORY",
|
|
208
|
+
"GIT_ALTERNATE_OBJECT_DIRECTORIES",
|
|
209
|
+
"GIT_COMMON_DIR",
|
|
210
|
+
"GIT_NAMESPACE",
|
|
211
|
+
];
|
|
164
212
|
let hasPath = false;
|
|
165
213
|
for (const key of Object.keys(env)) {
|
|
166
|
-
|
|
214
|
+
const normalized = key.toUpperCase();
|
|
215
|
+
if (selectors.includes(normalized)
|
|
216
|
+
|| normalized === "GIT_OPTIONAL_LOCKS"
|
|
217
|
+
|| normalized === "GIT_NO_LAZY_FETCH") {
|
|
218
|
+
delete env[key];
|
|
219
|
+
} else if (normalized === "PATH") {
|
|
167
220
|
env[key] = "";
|
|
168
221
|
hasPath = true;
|
|
169
222
|
}
|
|
170
223
|
}
|
|
171
224
|
if (!hasPath) env.PATH = "";
|
|
225
|
+
env.GIT_OPTIONAL_LOCKS = "0";
|
|
226
|
+
env.GIT_NO_LAZY_FETCH = "1";
|
|
172
227
|
return env;
|
|
173
228
|
}
|
|
174
229
|
|
|
@@ -263,7 +318,7 @@ function matchesIndex(content: Buffer, entry: IndexEntry, autoCrlf: string): boo
|
|
|
263
318
|
if (blobObjectId(content, entry.objectId.length) === entry.objectId) return true;
|
|
264
319
|
if (entry.mode === "120000" || !content.includes(Buffer.from("\r\n"))) return false;
|
|
265
320
|
const attributes = entry.eolAttributes;
|
|
266
|
-
if (attributes === "-text") return false;
|
|
321
|
+
if (attributes.endsWith("eol=lf") || attributes === "-text") return false;
|
|
267
322
|
const automatic = attributes.startsWith("text=auto") || !attributes;
|
|
268
323
|
if (!attributes && !["true", "input"].includes(autoCrlf)) return false;
|
|
269
324
|
if (automatic) {
|
|
@@ -288,12 +343,13 @@ function matchesIndex(content: Buffer, entry: IndexEntry, autoCrlf: string): boo
|
|
|
288
343
|
* No Git command in this path asks Git to convert worktree content, so clean and process filters cannot start.
|
|
289
344
|
*/
|
|
290
345
|
export async function inspectWorktreeWithoutFilters(root: string, runGit: PassiveGitRunner): Promise<PassiveWorktreeSnapshot> {
|
|
291
|
-
const [indexOutput, untrackedOutput, resolvedHead, autoCrlfOutput] = await Promise.all([
|
|
346
|
+
const [indexOutput, untrackedOutput, resolvedHead, autoCrlfOutput, settings] = await Promise.all([
|
|
292
347
|
runGit([...PASSIVE_METADATA_ARGS, "ls-files", "--cached", "--debug", "-z", "--full-name",
|
|
293
348
|
"--format=%(objectmode) %(objectname) %(stage)%x09%(eolattr)%x09%(path)"]),
|
|
294
349
|
runGit([...PASSIVE_METADATA_ARGS, "ls-files", "--others", "--exclude-standard", "-z", "--full-name"]),
|
|
295
350
|
runGit(["rev-parse", "--verify", "HEAD"]).then(decode, () => ""),
|
|
296
351
|
runGit(["config", "--type=bool-or-str", "--default", "false", "--get", "core.autocrlf"]),
|
|
352
|
+
passiveWorktreeSettings(runGit),
|
|
297
353
|
]);
|
|
298
354
|
const autoCrlf = decode(autoCrlfOutput).trim().toLowerCase();
|
|
299
355
|
const head = resolvedHead.trim();
|
|
@@ -314,7 +370,7 @@ export async function inspectWorktreeWithoutFilters(root: string, runGit: Passiv
|
|
|
314
370
|
if (indexEntry && !indexEntry.skipWorktree) {
|
|
315
371
|
try {
|
|
316
372
|
const state = await readFileState(root, indexEntry, filePath, WORKTREE_CONTENT_LIMIT - contentBytes);
|
|
317
|
-
mode =
|
|
373
|
+
mode = passiveWorktreeMode(state.mode, indexEntry.mode, settings);
|
|
318
374
|
content = state.content;
|
|
319
375
|
if (content) contentBytes += content.length;
|
|
320
376
|
worktreeChanged = !state.statsMatch && (mode !== indexEntry.mode || !content || !matchesIndex(content, indexEntry, autoCrlf));
|
|
@@ -342,5 +398,5 @@ export async function inspectWorktreeWithoutFilters(root: string, runGit: Passiv
|
|
|
342
398
|
const previous = files.get(filePath);
|
|
343
399
|
files.set(filePath, { ...previous, path: filePath, mode: state.mode, content: state.content, contentObjectId: undefined });
|
|
344
400
|
}
|
|
345
|
-
return { head: head || "(initial)", files };
|
|
401
|
+
return { head: head || "(initial)", files, ...settings };
|
|
346
402
|
}
|
package/killeros/question.ts
CHANGED
|
@@ -123,6 +123,7 @@ export function registerQuestionTool(pi: ExtensionAPI): void {
|
|
|
123
123
|
"Use multiple mode only when the user may need to choose more than one answer; ordinary either/or decisions remain single-select.",
|
|
124
124
|
],
|
|
125
125
|
parameters: QuestionParams,
|
|
126
|
+
exposure: "model-only",
|
|
126
127
|
executionMode: "sequential",
|
|
127
128
|
|
|
128
129
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
package/killeros/worked-for.ts
CHANGED
|
@@ -332,8 +332,8 @@ function receiptModelName(
|
|
|
332
332
|
model: ExtensionContext["model"],
|
|
333
333
|
): string | undefined {
|
|
334
334
|
if (settled.modelMismatch || settled.modelProvider === undefined || settled.modelId === undefined) return undefined;
|
|
335
|
-
if (!model || model.provider !== settled.modelProvider
|
|
336
|
-
const resolved = modelDisplayName(
|
|
335
|
+
if (!model || model.provider !== settled.modelProvider) return undefined;
|
|
336
|
+
const resolved = modelDisplayName({ id: settled.modelId });
|
|
337
337
|
if (!resolved || resolved.length > 200) return undefined;
|
|
338
338
|
return resolved;
|
|
339
339
|
}
|
|
@@ -355,6 +355,7 @@ export function registerWorkedFor(
|
|
|
355
355
|
collect: (cwd: string) => Promise<ChangeReceiptCollection> = beginChangeReceipt,
|
|
356
356
|
): void {
|
|
357
357
|
let active: ActiveReceipt | undefined;
|
|
358
|
+
let lifecycleGeneration = 0;
|
|
358
359
|
let collectionNoticeShown = false;
|
|
359
360
|
|
|
360
361
|
pi.registerEntryRenderer<WorkedForEntryData>(WORKED_FOR_ENTRY_TYPE, (entry, options, theme) => {
|
|
@@ -369,6 +370,7 @@ export function registerWorkedFor(
|
|
|
369
370
|
});
|
|
370
371
|
|
|
371
372
|
pi.on("session_start", async () => {
|
|
373
|
+
lifecycleGeneration += 1;
|
|
372
374
|
const stale = active;
|
|
373
375
|
active = undefined;
|
|
374
376
|
collectionNoticeShown = false;
|
|
@@ -407,18 +409,18 @@ export function registerWorkedFor(
|
|
|
407
409
|
if (ctx.mode !== "tui" || !active) return;
|
|
408
410
|
if (event.message.role !== "assistant") return;
|
|
409
411
|
const provider: unknown = event.message.provider;
|
|
410
|
-
const
|
|
411
|
-
|
|
412
|
+
const requestedModelId: unknown = event.message.model;
|
|
413
|
+
const responseModelId: unknown = event.message.responseModel;
|
|
414
|
+
if (typeof provider !== "string" || typeof requestedModelId !== "string"
|
|
415
|
+
|| responseModelId !== undefined && typeof responseModelId !== "string") active.modelMismatch = true;
|
|
412
416
|
else {
|
|
417
|
+
const modelId = responseModelId ?? requestedModelId;
|
|
413
418
|
if (active.modelProvider === undefined || active.modelId === undefined) {
|
|
414
419
|
active.modelProvider = provider;
|
|
415
420
|
active.modelId = modelId;
|
|
416
421
|
} else if (active.modelProvider !== provider || active.modelId !== modelId) {
|
|
417
422
|
active.modelMismatch = true;
|
|
418
423
|
}
|
|
419
|
-
if (event.message.responseModel !== undefined && event.message.responseModel !== modelId) {
|
|
420
|
-
active.modelMismatch = true;
|
|
421
|
-
}
|
|
422
424
|
}
|
|
423
425
|
finishAssistantStream(
|
|
424
426
|
active,
|
|
@@ -447,11 +449,14 @@ export function registerWorkedFor(
|
|
|
447
449
|
});
|
|
448
450
|
|
|
449
451
|
pi.on("agent_settled", async (_event, ctx) => {
|
|
450
|
-
if (ctx.mode !== "tui"
|
|
452
|
+
if (!active || ctx.mode !== "tui") return;
|
|
451
453
|
const settled = active;
|
|
454
|
+
const generation = lifecycleGeneration;
|
|
452
455
|
active = undefined;
|
|
453
456
|
const model = receiptModelName(settled, ctx.model);
|
|
454
457
|
const changes = await (await settled.collection).finish();
|
|
458
|
+
// Shutdown can invalidate ctx while the final Git scan is still running.
|
|
459
|
+
if (generation !== lifecycleGeneration) return;
|
|
455
460
|
if (changes.state === "unavailable" && changes.reason !== "not-git" && changes.reason !== "timeout" && !collectionNoticeShown) {
|
|
456
461
|
collectionNoticeShown = true;
|
|
457
462
|
ctx.ui.notify(`Change receipt unavailable: ${changes.reason}`, "warning");
|
|
@@ -481,6 +486,7 @@ export function registerWorkedFor(
|
|
|
481
486
|
});
|
|
482
487
|
|
|
483
488
|
pi.on("session_shutdown", async () => {
|
|
489
|
+
lifecycleGeneration += 1;
|
|
484
490
|
const stale = active;
|
|
485
491
|
active = undefined;
|
|
486
492
|
if (stale) await (await stale.collection).dispose();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "killeros",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
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": ">=0.
|
|
49
|
-
"@earendil-works/pi-coding-agent": ">=0.
|
|
50
|
-
"@earendil-works/pi-tui": ">=0.
|
|
48
|
+
"@earendil-works/pi-ai": ">=0.99.2 <1",
|
|
49
|
+
"@earendil-works/pi-coding-agent": ">=0.99.2 <1",
|
|
50
|
+
"@earendil-works/pi-tui": ">=0.99.2 <1",
|
|
51
51
|
"typebox": ">=1.1.38 <2"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
|
-
"@earendil-works/pi-ai": "0.
|
|
55
|
-
"@earendil-works/pi-coding-agent": "0.
|
|
56
|
-
"@earendil-works/pi-server": "0.
|
|
57
|
-
"@earendil-works/pi-tui": "0.
|
|
54
|
+
"@earendil-works/pi-ai": "0.99.2",
|
|
55
|
+
"@earendil-works/pi-coding-agent": "0.99.2",
|
|
56
|
+
"@earendil-works/pi-server": "0.99.2",
|
|
57
|
+
"@earendil-works/pi-tui": "0.99.2",
|
|
58
58
|
"@types/node": "24.12.4",
|
|
59
59
|
"eslint": "^10.9.1",
|
|
60
60
|
"typebox": "1.3.20",
|
|
@@ -62,7 +62,6 @@
|
|
|
62
62
|
"typescript-eslint": "^8.68.0"
|
|
63
63
|
},
|
|
64
64
|
"overrides": {
|
|
65
|
-
"brace-expansion": "5.0.
|
|
66
|
-
"undici": "8.9.0"
|
|
65
|
+
"brace-expansion": "5.0.12"
|
|
67
66
|
}
|
|
68
67
|
}
|