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