killeros 2.1.26 → 2.1.28
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 +38 -0
- package/Killeros.ts +6 -13
- package/README.md +8 -10
- package/killeros/activity.ts +16 -7
- package/killeros/auto-compaction.ts +1 -2
- package/killeros/change-receipt.ts +109 -37
- package/killeros/codex-fast.ts +8 -3
- package/killeros/footer.ts +82 -31
- package/killeros/goal-interface.ts +198 -60
- package/killeros/goal-runtime.ts +192 -49
- package/killeros/goal-settlement.ts +140 -49
- package/killeros/goal-state.ts +250 -34
- package/killeros/handoff.ts +36 -4
- package/killeros/passive-git-status.ts +206 -0
- package/killeros/personal-instructions.ts +2 -3
- package/killeros/runtime.ts +51 -35
- package/killeros/shell-ui.ts +13 -4
- package/killeros/worked-for.ts +4 -3
- package/package.json +1 -1
- package/themes/killeros.json +3 -2
- package/killeros/init-evidence.ts +0 -291
- package/killeros/init-target.ts +0 -309
- package/killeros/init.ts +0 -285
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,44 @@ All notable changes to KillerOS are documented here.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [2.1.28] - 2026-09-15
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- Simplified goal presentation without changing goal behavior: transcript history shows one iconless event row per lifecycle change (`Goal started`, `Goal replaced`, `Goal paused`, `Goal resumed`, `Goal cleared`), progress and blocker outcomes render once through the goal tool result, the footer shows compact `/goal active · 1/20 · 10s` status, the `/goal` panel leads with status then objective then `Pause goal` / `Resume goal` / `Clear goal` actions, and duplicate TUI success notifications are suppressed while RPC confirmations remain. Active-goal labels use muted teal `#6FAEB2` through the packaged theme's extension-label role.
|
|
12
|
+
- Removed the legacy `✻` marker from version-1 `Worked for …` transcript rows; stored entries are unchanged and remain readable.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- Appended the selected model ID as a gray `· <model-id>` suffix to every TUI activity working message, retained for the full request cycle and falling back to `unknown model` when no usable ID exists.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- Kept resumed blocked goals and goals paused by truncated provider errors readable after reload.
|
|
21
|
+
- Made goal continuation explicit: each settled turn now needs one accepted `continue`, `complete`, or blocker decision. Missing decisions, repeated continuation reports, unavailable goal tools, and runtime failures pause without scheduling another turn; compaction recovery resumes the same logical turn.
|
|
22
|
+
- Preserved accepted goal decisions across automatic compaction: a `continue` or blocker audit recorded before compaction now authorizes exactly one next turn instead of being discarded and re-decided. Duplicate compaction and settlement callbacks cannot start a second turn.
|
|
23
|
+
- Scoped goal pause reasons to the current paused state: resume and manual pause no longer surface an earlier automatic reason, terminal states keep no pause reason, and contradictory persisted states fail closed instead of starting automatic work.
|
|
24
|
+
|
|
25
|
+
## [2.1.27] - 2026-09-12
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
|
|
29
|
+
- Added `/codex-fast status` to report whether Codex fast mode is enabled without changing it.
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
- Restricted automatic startup, footer, and change-receipt Git scans to trusted projects, preventing repository executables and configuration races from launching commands before trust is granted.
|
|
34
|
+
- Resolved passive Git executable discovery without starting a locator or repository executable. Footer and change-receipt scans now use an absolute Git path outside the inspected repository and report unavailable when none exists.
|
|
35
|
+
- Closed the filter-configuration race in automatic Git inspection. Footer and change-receipt scans now run Git without `PATH` resolution, re-check effective filters after status, and skip results the scan cannot produce without an extension process or remote fetch. Change receipts no longer fetch missing objects from partial-clone remotes.
|
|
36
|
+
- Stopped attaching single-file verification to goals that name several files. Multi-file goals now complete through the model-reported path.
|
|
37
|
+
- Shared one passive Git status policy between footer telemetry and change receipts. Both scans now disable filesystem monitors and clean/process filters, and skip status when filter discovery is unavailable or unsafe.
|
|
38
|
+
- Recorded focused Node test runs in response receipts without persisting their test paths.
|
|
39
|
+
- Kept passive footer Git status from running configured filters, bounded loose and packed receipt blobs, and stopped stale `/handoff` summaries from replacing sessions with new work.
|
|
40
|
+
|
|
41
|
+
### Removed
|
|
42
|
+
|
|
43
|
+
- Removed `/init` and its `killeros_init_*` tools, prompts, workflow APIs, runtime state, and lifecycle handlers. Upgrading leaves existing `AGENTS.md` and recovery files untouched, and commands from other providers named `/init` pass through KillerOS unhandled.
|
|
44
|
+
|
|
7
45
|
## [2.1.26] - 2026-09-08
|
|
8
46
|
|
|
9
47
|
### Fixed
|
package/Killeros.ts
CHANGED
|
@@ -12,7 +12,6 @@ import { registerGoalRuntime } from "./killeros/goal-runtime.ts";
|
|
|
12
12
|
import { registerGoalSettlement } from "./killeros/goal-settlement.ts";
|
|
13
13
|
import { registerHandoff } from "./killeros/handoff.ts";
|
|
14
14
|
import { registerLifecycleHooks } from "./killeros/hooks.ts";
|
|
15
|
-
import { registerInitCommand, registerInitSettlement } from "./killeros/init.ts";
|
|
16
15
|
import {
|
|
17
16
|
registerCompletionNotifications,
|
|
18
17
|
type CompletionNotificationDependencies,
|
|
@@ -20,15 +19,12 @@ import {
|
|
|
20
19
|
import { registerCodexFastMode } from "./killeros/codex-fast.ts";
|
|
21
20
|
import { registerPersonalInstructions } from "./killeros/personal-instructions.ts";
|
|
22
21
|
import { registerQuestionTool } from "./killeros/question.ts";
|
|
23
|
-
import { createGoalRuntime
|
|
22
|
+
import { createGoalRuntime } from "./killeros/runtime.ts";
|
|
24
23
|
import { registerShellUi } from "./killeros/shell-ui.ts";
|
|
25
24
|
import { registerWorkedFor } from "./killeros/worked-for.ts";
|
|
26
25
|
|
|
27
26
|
export { contextPercentRemaining, formatCost, formatContextProgress } from "./killeros/footer.ts";
|
|
28
27
|
export { executeHook } from "./killeros/hooks.ts";
|
|
29
|
-
export { INIT_WORKFLOW_PROMPT } from "./killeros/init.ts";
|
|
30
|
-
export { buildInitEvidence, listInitEvidence, readInitEvidence } from "./killeros/init-evidence.ts";
|
|
31
|
-
export { captureInitTargetBaseline, installInitAgentsFile, validateGeneratedGuidance, writeInitAgentsFile } from "./killeros/init-target.ts";
|
|
32
28
|
export interface KillerosOptions {
|
|
33
29
|
completionNotifications?: CompletionNotificationDependencies;
|
|
34
30
|
/** Output-token budget for /handoff summaries; invalid values fall back to killeros.json, then the default. */
|
|
@@ -36,25 +32,22 @@ export interface KillerosOptions {
|
|
|
36
32
|
}
|
|
37
33
|
|
|
38
34
|
export default function Killeros(pi: ExtensionAPI, options: KillerosOptions = {}): void {
|
|
39
|
-
const initRuntime = createInitRuntime();
|
|
40
35
|
const goalRuntime = createGoalRuntime();
|
|
41
36
|
const commandResolver = createSlashCommandResolver(pi);
|
|
42
37
|
registerShellUi(pi, commandResolver);
|
|
43
|
-
registerGoalInterface(pi, goalRuntime
|
|
44
|
-
registerGoalRuntime(pi, goalRuntime
|
|
45
|
-
registerPersonalInstructions(pi
|
|
38
|
+
registerGoalInterface(pi, goalRuntime);
|
|
39
|
+
registerGoalRuntime(pi, goalRuntime);
|
|
40
|
+
registerPersonalInstructions(pi);
|
|
46
41
|
registerQuestionTool(pi);
|
|
47
42
|
registerAliases(pi);
|
|
48
43
|
registerHandoff(pi, goalRuntime, options.handoffMaxTokens);
|
|
49
44
|
registerSlashAutocomplete(pi, commandResolver);
|
|
50
45
|
registerFooter(pi, goalRuntime);
|
|
51
46
|
registerCodexFastMode(pi);
|
|
52
|
-
registerInitCommand(pi, initRuntime, goalRuntime);
|
|
53
47
|
registerLifecycleHooks(pi);
|
|
54
48
|
registerWorkedFor(pi);
|
|
55
|
-
const goalCompaction = registerGoalSettlement(pi, goalRuntime
|
|
56
|
-
registerAutoCompaction(pi, { goal: goalCompaction
|
|
57
|
-
registerInitSettlement(pi, initRuntime);
|
|
49
|
+
const goalCompaction = registerGoalSettlement(pi, goalRuntime);
|
|
50
|
+
registerAutoCompaction(pi, { goal: goalCompaction });
|
|
58
51
|
registerRequestActivity(pi);
|
|
59
52
|
registerCompletionNotifications(pi, options.completionNotifications);
|
|
60
53
|
}
|
package/README.md
CHANGED
|
@@ -5,9 +5,8 @@ A TypeScript extension for the [Pi coding agent](https://github.com/earendil-wor
|
|
|
5
5
|
## What you get
|
|
6
6
|
|
|
7
7
|
- 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
|
-
- `/goal`: set an objective and Pi keeps working toward it across turns, compaction, reloads, and branch navigation. New goals pause after 20 turns; `/goal resume` grants another 20.
|
|
9
|
-
- `/
|
|
10
|
-
- `/codex-fast`: toggles the `priority` service tier on Codex requests.
|
|
8
|
+
- `/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
10
|
- `/handoff`: starts a fresh linked session carrying visible continuation context.
|
|
12
11
|
- Automatic context compaction when remaining tokens drop below 15% of the window (configurable).
|
|
13
12
|
- A `question` tool with single-select and multi-select modes.
|
|
@@ -19,7 +18,7 @@ A TypeScript extension for the [Pi coding agent](https://github.com/earendil-wor
|
|
|
19
18
|
|
|
20
19
|
- Node.js 22.19.0+
|
|
21
20
|
- Pi 0.85.0 or later within the 0.x release line
|
|
22
|
-
- An interactive TUI session for the custom header, editor, footer,
|
|
21
|
+
- An interactive TUI session for the custom header, editor, footer, and `question`
|
|
23
22
|
|
|
24
23
|
## Install
|
|
25
24
|
|
|
@@ -33,18 +32,17 @@ Or from GitHub:
|
|
|
33
32
|
pi install git:github.com/KyrosHendrix/pi-KillerOS
|
|
34
33
|
```
|
|
35
34
|
|
|
36
|
-
Pin a release by appending its tag, for example `@v2.1.
|
|
35
|
+
Pin a release by appending its tag, for example `@v2.1.28`. Add `-l` to install only for the current project. Restart Pi after installing.
|
|
37
36
|
|
|
38
37
|
## Commands
|
|
39
38
|
|
|
40
39
|
```text
|
|
41
|
-
/init Generate root AGENTS.md from repository evidence
|
|
42
40
|
/goal View the current goal
|
|
43
41
|
/goal <objective> Set an objective
|
|
44
42
|
/goal pause Stop automatic continuation
|
|
45
43
|
/goal resume Resume automatic continuation
|
|
46
44
|
/goal clear Remove the current goal
|
|
47
|
-
/codex-fast
|
|
45
|
+
/codex-fast [status] Toggle or report Codex fast mode
|
|
48
46
|
/notification Configure the completion sound
|
|
49
47
|
/handoff [focus] Fresh session with continuation context
|
|
50
48
|
/clear New session after confirmation
|
|
@@ -56,8 +54,8 @@ Pin a release by appending its tag, for example `@v2.1.26`. Add `-l` to install
|
|
|
56
54
|
| Mode | What works |
|
|
57
55
|
| --- | --- |
|
|
58
56
|
| TUI | Everything |
|
|
59
|
-
| RPC | Goals, proactive compaction; no TUI components,
|
|
60
|
-
| Print/JSON | No interactive questions, `/goal`,
|
|
57
|
+
| RPC | Goals, proactive compaction; no TUI components, sounds, title indicator |
|
|
58
|
+
| Print/JSON | No interactive questions, `/goal`, or proactive compaction |
|
|
61
59
|
|
|
62
60
|
## Configuration
|
|
63
61
|
|
|
@@ -87,7 +85,7 @@ A direct quoted file target binds silent file proof:
|
|
|
87
85
|
/goal Fix `killeros/footer.ts`, verified by npm test
|
|
88
86
|
```
|
|
89
87
|
|
|
90
|
-
KillerOS captures the file baseline at goal start and only completes when the file is created or changed. 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.
|
|
88
|
+
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.
|
|
91
89
|
|
|
92
90
|
Completion sounds are off by default; change with `/notification` in TUI mode. The tab-title indicator requires a Nerd Font.
|
|
93
91
|
|
package/killeros/activity.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
3
|
+
import { modelDisplayName } from "./display.ts";
|
|
3
4
|
|
|
4
5
|
export type ActivityMessage =
|
|
5
6
|
| { kind: "prompt" }
|
|
@@ -12,7 +13,7 @@ function safeToolName(toolName: string): string {
|
|
|
12
13
|
return truncateToWidth(normalized || "tool", 32, "…");
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
export function formatActivityMessage(message: ActivityMessage, theme: Theme): string {
|
|
16
|
+
export function formatActivityMessage(message: ActivityMessage, theme: Theme, modelId?: string): string {
|
|
16
17
|
let verb: string;
|
|
17
18
|
let detail: string;
|
|
18
19
|
|
|
@@ -58,38 +59,46 @@ export function formatActivityMessage(message: ActivityMessage, theme: Theme): s
|
|
|
58
59
|
}
|
|
59
60
|
}
|
|
60
61
|
|
|
61
|
-
|
|
62
|
+
const suffix = modelDisplayName({ id: modelId }) || "unknown model";
|
|
63
|
+
return `${theme.fg("accent", verb)} ${theme.fg("dim", `(${theme.bold("esc")} to interrupt · ${detail})`)}${theme.fg("dim", ` · ${suffix}`)}`;
|
|
62
64
|
}
|
|
63
65
|
|
|
64
66
|
export function registerRequestActivity(pi: ExtensionAPI): void {
|
|
65
67
|
let active = false;
|
|
68
|
+
let activeModel: string | undefined;
|
|
69
|
+
|
|
70
|
+
const render = (message: ActivityMessage, ctx: ExtensionContext): void => {
|
|
71
|
+
ctx.ui.setWorkingMessage(formatActivityMessage(message, ctx.ui.theme, activeModel));
|
|
72
|
+
};
|
|
66
73
|
|
|
67
74
|
const clear = (ctx?: ExtensionContext): void => {
|
|
75
|
+
active = false;
|
|
76
|
+
activeModel = undefined;
|
|
68
77
|
if (ctx?.mode === "tui") {
|
|
69
78
|
ctx.ui.setWorkingMessage();
|
|
70
79
|
}
|
|
71
|
-
active = false;
|
|
72
80
|
};
|
|
73
81
|
|
|
74
82
|
pi.on("agent_start", (_event, ctx) => {
|
|
75
83
|
if (ctx.mode !== "tui") return;
|
|
84
|
+
if (!active) activeModel = ctx.model?.id;
|
|
76
85
|
active = true;
|
|
77
|
-
|
|
86
|
+
render({ kind: "prompt" }, ctx);
|
|
78
87
|
});
|
|
79
88
|
|
|
80
89
|
pi.on("tool_execution_start", (event, ctx) => {
|
|
81
90
|
if (ctx.mode !== "tui" || !active) return;
|
|
82
|
-
|
|
91
|
+
render({ kind: "tool", toolName: event.toolName }, ctx);
|
|
83
92
|
});
|
|
84
93
|
|
|
85
94
|
pi.on("tool_execution_end", (event, ctx) => {
|
|
86
95
|
if (ctx.mode !== "tui" || !active) return;
|
|
87
|
-
|
|
96
|
+
render({ kind: "tool-result", failed: event.isError }, ctx);
|
|
88
97
|
});
|
|
89
98
|
|
|
90
99
|
pi.on("message_update", (event, ctx) => {
|
|
91
100
|
if (ctx.mode !== "tui" || !active || event.assistantMessageEvent.type !== "text_start") return;
|
|
92
|
-
|
|
101
|
+
render({ kind: "responding" }, ctx);
|
|
93
102
|
});
|
|
94
103
|
|
|
95
104
|
pi.on("agent_settled", (_event, ctx) => {
|
|
@@ -33,7 +33,6 @@ export interface AutoCompactionDependencies {
|
|
|
33
33
|
loadPreference?: (ctx: ExtensionContext) => AutoCompactionPreference;
|
|
34
34
|
getCompactionSettings?: (ctx: ExtensionContext) => CompactionSettings;
|
|
35
35
|
goal?: AutoCompactionGoalHandlers;
|
|
36
|
-
isInitActive?: () => boolean;
|
|
37
36
|
}
|
|
38
37
|
|
|
39
38
|
type AutoCompactionRequest = {
|
|
@@ -193,7 +192,7 @@ export function registerAutoCompaction(
|
|
|
193
192
|
};
|
|
194
193
|
|
|
195
194
|
pi.on("turn_end", (_event, ctx) => {
|
|
196
|
-
if (!supportedMode(ctx) ||
|
|
195
|
+
if (!supportedMode(ctx) || request) return;
|
|
197
196
|
|
|
198
197
|
let preference: AutoCompactionPreference;
|
|
199
198
|
let compactionSettings: CompactionSettings;
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import { watch } from "node:fs";
|
|
3
|
-
import {
|
|
2
|
+
import { createReadStream, watch } from "node:fs";
|
|
3
|
+
import { PASSIVE_GIT_CONFIG_ARGS, passiveGitCommand, passiveGitEnv, passiveStatusSafetyArgs, samePassiveFilters } from "./passive-git-status.ts";
|
|
4
|
+
import { lstat, open, readlink } from "node:fs/promises";
|
|
4
5
|
import path from "node:path";
|
|
5
|
-
import {
|
|
6
|
-
import { inflate } from "node:zlib";
|
|
6
|
+
import { createInflate } from "node:zlib";
|
|
7
7
|
|
|
8
8
|
const GIT_TIMEOUT_MS = 5_000;
|
|
9
9
|
const GIT_OUTPUT_LIMIT = 16 * 1024 * 1024;
|
|
10
10
|
const SNAPSHOT_CONTENT_LIMIT = 128 * 1024 * 1024;
|
|
11
11
|
const MAX_DIFF_OPERATIONS = 500_000;
|
|
12
12
|
const MAX_FILES = 20;
|
|
13
|
-
const
|
|
13
|
+
const GIT_OBJECT_HEADER_LIMIT = 1_024;
|
|
14
14
|
|
|
15
15
|
export type ChangeUnavailableReason = "not-git" | "timeout" | "too-large" | "error";
|
|
16
16
|
|
|
@@ -43,7 +43,12 @@ export const CHECK_LABELS = [
|
|
|
43
43
|
...GRADLE_CHECK_LABELS,
|
|
44
44
|
] as const;
|
|
45
45
|
|
|
46
|
-
|
|
46
|
+
const FOCUSED_CHECK_LABEL = "node --test (focused)";
|
|
47
|
+
const FOCUSED_TEST_UNSAFE_CHARACTERS = [
|
|
48
|
+
'"', "'", "`", "$", ";", "&", "|", "<", ">", "*", "?", "[", "]", "{", "}", "(", ")", "#", "!",
|
|
49
|
+
] as const;
|
|
50
|
+
|
|
51
|
+
export type CheckLabel = typeof CHECK_LABELS[number] | typeof FOCUSED_CHECK_LABEL;
|
|
47
52
|
|
|
48
53
|
export interface ChangeReceiptCollection {
|
|
49
54
|
finish(): Promise<ChangeSummary>;
|
|
@@ -61,11 +66,19 @@ class GitFailure extends Error {
|
|
|
61
66
|
}
|
|
62
67
|
}
|
|
63
68
|
|
|
64
|
-
function runGit(cwd: string, args: readonly string[], input?: Buffer): Promise<Buffer> {
|
|
69
|
+
function runGit(cwd: string, args: readonly string[], input?: Buffer, stdoutLimit = GIT_OUTPUT_LIMIT): Promise<Buffer> {
|
|
70
|
+
let gitCommand: string;
|
|
71
|
+
try {
|
|
72
|
+
const found = passiveGitCommand(cwd);
|
|
73
|
+
if (!found) return Promise.reject(new GitFailure("error"));
|
|
74
|
+
gitCommand = found;
|
|
75
|
+
} catch {
|
|
76
|
+
return Promise.reject(new GitFailure("error"));
|
|
77
|
+
}
|
|
65
78
|
return new Promise((resolve, reject) => {
|
|
66
|
-
const child = spawn(
|
|
79
|
+
const child = spawn(gitCommand, args, {
|
|
67
80
|
cwd,
|
|
68
|
-
env:
|
|
81
|
+
env: passiveGitEnv(),
|
|
69
82
|
stdio: [input ? "pipe" : "ignore", "pipe", "pipe"],
|
|
70
83
|
windowsHide: true,
|
|
71
84
|
});
|
|
@@ -82,7 +95,7 @@ function runGit(cwd: string, args: readonly string[], input?: Buffer): Promise<B
|
|
|
82
95
|
timer.unref();
|
|
83
96
|
const capture = (chunks: Buffer[], isStdout: boolean) => (chunk: Buffer): void => {
|
|
84
97
|
const nextBytes = (isStdout ? stdoutBytes : stderrBytes) + chunk.length;
|
|
85
|
-
if (nextBytes > GIT_OUTPUT_LIMIT) {
|
|
98
|
+
if (nextBytes > (isStdout ? stdoutLimit : GIT_OUTPUT_LIMIT)) {
|
|
86
99
|
failure = new GitFailure("too-large");
|
|
87
100
|
child.kill();
|
|
88
101
|
return;
|
|
@@ -129,15 +142,6 @@ type Repository = {
|
|
|
129
142
|
};
|
|
130
143
|
const repositoryCache = new Map<string, Promise<Repository>>();
|
|
131
144
|
|
|
132
|
-
async function loadFilterNames(root: string): Promise<readonly string[]> {
|
|
133
|
-
const records = decode(await runGit(root, ["config", "--null", "--name-only", "--list"])).split("\0");
|
|
134
|
-
const names = new Set<string>();
|
|
135
|
-
for (const key of records) {
|
|
136
|
-
if (key && /^filter\..*\.(clean|process)$/u.test(key)) names.add(key.slice("filter.".length, key.lastIndexOf(".")));
|
|
137
|
-
}
|
|
138
|
-
return [...names];
|
|
139
|
-
}
|
|
140
|
-
|
|
141
145
|
async function repository(cwd: string): Promise<Repository> {
|
|
142
146
|
const cached = repositoryCache.get(cwd);
|
|
143
147
|
if (cached) return cached;
|
|
@@ -253,13 +257,15 @@ function discardMonitor(monitor: RepositoryMonitor): void {
|
|
|
253
257
|
}
|
|
254
258
|
|
|
255
259
|
async function snapshot(repo: Repository, paths?: readonly string[]): Promise<Snapshot> {
|
|
256
|
-
const
|
|
260
|
+
const config = decode(await runGit(repo.root, PASSIVE_GIT_CONFIG_ARGS));
|
|
261
|
+
const safetyArgs = passiveStatusSafetyArgs(config);
|
|
262
|
+
if (!safetyArgs) throw new GitFailure("error");
|
|
257
263
|
const output = decode(await runGit(repo.root, [
|
|
258
|
-
|
|
259
|
-
...filterNames.flatMap((name) => ["-c", `filter.${name}.clean=`, "-c", `filter.${name}.process=`, "-c", `filter.${name}.required=false`]),
|
|
264
|
+
...safetyArgs,
|
|
260
265
|
"status", "--porcelain=v2", "--branch", "--no-ahead-behind", "-z", "--no-renames", "--untracked-files=all", "--ignore-submodules=all",
|
|
261
266
|
...(paths ? ["--", ...paths] : []),
|
|
262
267
|
]));
|
|
268
|
+
if (!samePassiveFilters(config, decode(await runGit(repo.root, PASSIVE_GIT_CONFIG_ARGS)))) throw new GitFailure("error");
|
|
263
269
|
const records = output.split("\0").filter(Boolean);
|
|
264
270
|
const headRecord = records.find((record) => record.startsWith("# branch.oid "));
|
|
265
271
|
if (!headRecord) throw new Error("missing HEAD state");
|
|
@@ -402,24 +408,60 @@ function createMonitor(repo: Repository, initialSnapshot: Snapshot): RepositoryM
|
|
|
402
408
|
return monitor;
|
|
403
409
|
}
|
|
404
410
|
|
|
405
|
-
|
|
411
|
+
function boundedBlobSize(value: string, limit: number): number {
|
|
412
|
+
const size = Number(value);
|
|
413
|
+
if (!Number.isSafeInteger(size) || size > limit) throw new GitFailure("too-large");
|
|
414
|
+
return size;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
async function looseBlob(objectDirectory: string, id: string, limit: number): Promise<Buffer | undefined> {
|
|
418
|
+
const source = createReadStream(path.join(objectDirectory, id.slice(0, 2), id.slice(2)));
|
|
419
|
+
const inflater = createInflate();
|
|
420
|
+
source.once("error", (error) => inflater.destroy(error));
|
|
421
|
+
source.pipe(inflater);
|
|
422
|
+
let header = Buffer.alloc(0);
|
|
423
|
+
let expectedSize: number | undefined;
|
|
424
|
+
let body: Buffer | undefined;
|
|
425
|
+
let bodyBytes = 0;
|
|
406
426
|
try {
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
427
|
+
for await (const chunk of inflater) {
|
|
428
|
+
let data = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
429
|
+
if (expectedSize === undefined) {
|
|
430
|
+
const separator = data.indexOf(0);
|
|
431
|
+
if (separator < 0) {
|
|
432
|
+
if (header.length + data.length > GIT_OBJECT_HEADER_LIMIT) throw new Error("invalid blob header");
|
|
433
|
+
header = header.length === 0 ? Buffer.from(data) : Buffer.concat([header, data]);
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
if (header.length + separator > GIT_OBJECT_HEADER_LIMIT) throw new Error("invalid blob header");
|
|
437
|
+
const headerBytes = header.length === 0 ? data.subarray(0, separator) : Buffer.concat([header, data.subarray(0, separator)]);
|
|
438
|
+
const match = /^blob ([0-9]+)$/u.exec(headerBytes.toString("ascii"));
|
|
439
|
+
if (!match) throw new Error("invalid blob");
|
|
440
|
+
expectedSize = boundedBlobSize(match[1], limit);
|
|
441
|
+
body = Buffer.alloc(expectedSize);
|
|
442
|
+
data = data.subarray(separator + 1);
|
|
443
|
+
}
|
|
444
|
+
if (!body || expectedSize === undefined || data.length > expectedSize - bodyBytes) throw new Error("invalid blob");
|
|
445
|
+
data.copy(body, bodyBytes);
|
|
446
|
+
bodyBytes += data.length;
|
|
447
|
+
}
|
|
448
|
+
if (expectedSize === undefined || !body || bodyBytes !== expectedSize) throw new Error("invalid blob");
|
|
449
|
+
return body;
|
|
411
450
|
} catch (error) {
|
|
412
|
-
if (error
|
|
451
|
+
if (missingFile(error)) return undefined;
|
|
413
452
|
throw error;
|
|
453
|
+
} finally {
|
|
454
|
+
source.destroy();
|
|
455
|
+
inflater.destroy();
|
|
414
456
|
}
|
|
415
457
|
}
|
|
416
458
|
|
|
417
459
|
function cacheBlob(repo: Repository, id: string, content: Buffer): void {
|
|
460
|
+
if (content.length > SNAPSHOT_CONTENT_LIMIT) return;
|
|
418
461
|
if (repo.blobCacheBytes + content.length > SNAPSHOT_CONTENT_LIMIT) {
|
|
419
462
|
repo.blobCache.clear();
|
|
420
463
|
repo.blobCacheBytes = 0;
|
|
421
464
|
}
|
|
422
|
-
if (content.length > SNAPSHOT_CONTENT_LIMIT) return;
|
|
423
465
|
repo.blobCache.set(id, content);
|
|
424
466
|
repo.blobCacheBytes += content.length;
|
|
425
467
|
}
|
|
@@ -427,34 +469,42 @@ function cacheBlob(repo: Repository, id: string, content: Buffer): void {
|
|
|
427
469
|
async function loadHeadBlobs(repo: Repository, ids: readonly string[]): Promise<Map<string, Buffer>> {
|
|
428
470
|
const blobs = new Map<string, Buffer>();
|
|
429
471
|
const missing: string[] = [];
|
|
472
|
+
let blobBytes = 0;
|
|
430
473
|
for (const id of new Set(ids)) {
|
|
431
474
|
const cached = repo.blobCache.get(id);
|
|
432
|
-
if (cached) {
|
|
475
|
+
if (cached !== undefined) {
|
|
476
|
+
if (cached.length > SNAPSHOT_CONTENT_LIMIT - blobBytes) throw new GitFailure("too-large");
|
|
477
|
+
blobBytes += cached.length;
|
|
433
478
|
blobs.set(id, cached);
|
|
434
479
|
continue;
|
|
435
480
|
}
|
|
436
|
-
const content = await looseBlob(repo.objectDirectory, id);
|
|
437
|
-
if (content) {
|
|
481
|
+
const content = await looseBlob(repo.objectDirectory, id, SNAPSHOT_CONTENT_LIMIT - blobBytes);
|
|
482
|
+
if (content !== undefined) {
|
|
483
|
+
blobBytes += content.length;
|
|
438
484
|
blobs.set(id, content);
|
|
439
485
|
cacheBlob(repo, id, content);
|
|
440
486
|
} else missing.push(id);
|
|
441
487
|
}
|
|
442
488
|
if (missing.length > 0) {
|
|
443
|
-
const
|
|
489
|
+
const batchOutputLimit = SNAPSHOT_CONTENT_LIMIT + missing.reduce((total, id) => total + id.length + 32, 0);
|
|
490
|
+
const output = await runGit(repo.root, ["cat-file", "--batch"], Buffer.from(`${missing.join("\n")}\n`), batchOutputLimit);
|
|
444
491
|
let offset = 0;
|
|
445
492
|
for (const id of missing) {
|
|
446
493
|
const headerEnd = output.indexOf(10, offset);
|
|
447
|
-
|
|
494
|
+
if (headerEnd < 0 || headerEnd - offset > GIT_OBJECT_HEADER_LIMIT) throw new Error("invalid batch blob");
|
|
495
|
+
const match = /^([0-9a-f]+) blob ([0-9]+)$/u.exec(output.subarray(offset, headerEnd).toString("ascii"));
|
|
448
496
|
if (!match || match[1] !== id) throw new Error("invalid batch blob");
|
|
449
|
-
const size =
|
|
497
|
+
const size = boundedBlobSize(match[2], SNAPSHOT_CONTENT_LIMIT - blobBytes);
|
|
450
498
|
const start = headerEnd + 1;
|
|
451
499
|
const end = start + size;
|
|
452
500
|
if (output[end] !== 10) throw new Error("invalid batch body");
|
|
453
501
|
const content = Buffer.from(output.subarray(start, end));
|
|
502
|
+
blobBytes += size;
|
|
454
503
|
blobs.set(id, content);
|
|
455
504
|
cacheBlob(repo, id, content);
|
|
456
505
|
offset = end + 1;
|
|
457
506
|
}
|
|
507
|
+
if (offset !== output.length) throw new Error("invalid batch blob");
|
|
458
508
|
}
|
|
459
509
|
return blobs;
|
|
460
510
|
}
|
|
@@ -633,14 +683,36 @@ async function compare(repo: Repository, baseline: Snapshot, settlement: Snapsho
|
|
|
633
683
|
return { state: "available", totalFiles: changes.length, additions, deletions, files: changes.slice(0, MAX_FILES), omittedFiles: Math.max(0, changes.length - MAX_FILES) };
|
|
634
684
|
}
|
|
635
685
|
|
|
686
|
+
function isFocusedTestFile(value: string): boolean {
|
|
687
|
+
if (value.startsWith("-") || value.startsWith("~") || path.posix.isAbsolute(value) || path.win32.isAbsolute(value)
|
|
688
|
+
|| /^[A-Za-z][A-Za-z\d+.-]*:/u.test(value) || /[\s\p{Cc}]/u.test(value)
|
|
689
|
+
|| FOCUSED_TEST_UNSAFE_CHARACTERS.some((character) => value.includes(character))) return false;
|
|
690
|
+
const segments = value.split(/[\\/]/u);
|
|
691
|
+
const filename = segments.at(-1);
|
|
692
|
+
return segments.every((segment) => segment.length > 0 && segment !== "." && segment !== "..")
|
|
693
|
+
&& filename !== undefined
|
|
694
|
+
&& filename.includes(".test.")
|
|
695
|
+
&& /\.(?:js|cjs|mjs|ts|cts|mts)$/u.test(filename);
|
|
696
|
+
}
|
|
697
|
+
|
|
636
698
|
export function recognizedCheck(command: unknown, failed: boolean): CheckAttempt | undefined {
|
|
637
699
|
if (typeof command !== "string") return undefined;
|
|
638
700
|
const normalized = command.replace(/^[\t ]+|[\t ]+$/gu, "");
|
|
701
|
+
const outcome = failed ? "failed" : "passed";
|
|
639
702
|
const label = CHECK_LABELS.find((candidate) => normalized === candidate);
|
|
640
|
-
|
|
703
|
+
if (label) return { label, outcome };
|
|
704
|
+
const focused = /^node --test(?: --experimental-strip-types)? ([^ \t]+)$/u.exec(normalized);
|
|
705
|
+
if (!focused?.[1] || !isFocusedTestFile(focused[1])) return undefined;
|
|
706
|
+
return { label: FOCUSED_CHECK_LABEL, outcome };
|
|
641
707
|
}
|
|
642
708
|
|
|
643
|
-
export async function beginChangeReceipt(cwd: string): Promise<ChangeReceiptCollection> {
|
|
709
|
+
export async function beginChangeReceipt(cwd: string, trusted = true): Promise<ChangeReceiptCollection> {
|
|
710
|
+
if (!trusted) {
|
|
711
|
+
return {
|
|
712
|
+
finish: async () => ({ state: "unavailable", reason: "error" }),
|
|
713
|
+
dispose: async () => undefined,
|
|
714
|
+
};
|
|
715
|
+
}
|
|
644
716
|
try {
|
|
645
717
|
const repo = await repository(cwd);
|
|
646
718
|
let monitor = repositoryMonitors.get(repo.root);
|
package/killeros/codex-fast.ts
CHANGED
|
@@ -10,10 +10,15 @@ function isRequestPayload(value: unknown): value is RequestPayload {
|
|
|
10
10
|
|
|
11
11
|
export function registerCodexFastMode(pi: ExtensionAPI): void {
|
|
12
12
|
pi.registerCommand("codex-fast", {
|
|
13
|
-
description: "Toggle Codex fast mode",
|
|
13
|
+
description: "Toggle or report Codex fast mode",
|
|
14
14
|
handler: async (args, ctx) => {
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
const trimmed = args.trim();
|
|
16
|
+
if (trimmed === "status") {
|
|
17
|
+
ctx.ui.notify(`Codex fast mode: ${isCodexFastEnabled() ? "enabled" : "disabled"}`, "info");
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
if (trimmed) {
|
|
21
|
+
ctx.ui.notify("Usage: /codex-fast [status]", "error");
|
|
17
22
|
return;
|
|
18
23
|
}
|
|
19
24
|
|