killeros 2.0.4 → 2.0.6
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 +35 -0
- package/Killeros.ts +2 -0
- package/README.md +20 -12
- package/killeros/commands.ts +1 -2
- package/killeros/goals.ts +179 -51
- package/killeros/hooks.ts +25 -10
- package/killeros/init-evidence.ts +49 -13
- package/killeros/notifications.ts +1 -1
- package/killeros/question.ts +381 -182
- package/killeros/runtime.ts +7 -0
- package/killeros/shell-ui.ts +41 -13
- package/killeros/worked-for.ts +80 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,41 @@ All notable changes to KillerOS are documented here.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [2.0.6] - 2026-08-11
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Added a durable `✻ Worked for …` transcript line below each settled TUI response, including stopped and failed runs.
|
|
12
|
+
- Added optional multi-select to the `question` tool with bounded checked options, one additive custom answer, a dedicated multi-word filter editor, and compact expandable results while preserving single-select defaults.
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- Automated GitHub releases after successful `main` CI version bumps, with package, lockfile, changelog, tag, and verified-commit checks plus a manual tag recovery path.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- Entered automatic `/goal` continuations into durable goal-turn state before dispatch so turn numbers advance and blocker audits work on Pi custom-message turns.
|
|
21
|
+
|
|
22
|
+
## [2.0.5] - 2026-08-11
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- Added shuffled session-stable suggestions to the empty three-line prompt editor.
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
- Preserved complete UTF-8 characters in truncated `/init` evidence and chunked lifecycle-hook output, and kept filesystem-root paths visible in notification titles.
|
|
31
|
+
- Kept the startup-tip shuffle deck across session rebinds so every tip appears before the bank repeats.
|
|
32
|
+
- Rejected matchers on `agent_settled` lifecycle hooks instead of silently bypassing their tool-name guard.
|
|
33
|
+
- Made the goal update status schema compatible with Google models while preserving strict completion and blocking values.
|
|
34
|
+
- Kept multiline custom-answer drafts within tiny terminal row limits by rendering a clipped single-line preview.
|
|
35
|
+
- Preserved existing spaces and tabs after the cursor when applying slash-command autocomplete.
|
|
36
|
+
- Made `/init` abort safely when Git ignore inspection fails or returns untrusted output, preventing ignored files from entering model evidence.
|
|
37
|
+
- Wrapped long interactive questions to the current terminal width while keeping the question UI within the terminal height.
|
|
38
|
+
- Made `/goal pause` and `/goal clear` save terminal state before immediately stopping active goal work.
|
|
39
|
+
- Kept failed goal edits and replacements visible and fail-closed without restarting a stale objective.
|
|
40
|
+
- Required one durable blocker key on three consecutive goal turns before a goal can be marked blocked.
|
|
41
|
+
|
|
7
42
|
## [2.0.4] - 2026-08-10
|
|
8
43
|
|
|
9
44
|
### Changed
|
package/Killeros.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { registerQuestionTool } from "./killeros/question.ts";
|
|
|
14
14
|
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
|
+
import { registerWorkedFor } from "./killeros/worked-for.ts";
|
|
17
18
|
|
|
18
19
|
export { CONCISE_SYSTEM_PROMPT, isConciseEnabled, isConcisedEnabled } from "./killeros/concise.ts";
|
|
19
20
|
export { contextPercentRemaining, formatCost, formatContextProgress } from "./killeros/footer.ts";
|
|
@@ -43,4 +44,5 @@ export default function Killeros(pi: ExtensionAPI, options: KillerosOptions = {}
|
|
|
43
44
|
registerGoalSettlement(pi, goalRuntime, initRuntime);
|
|
44
45
|
registerInitSettlement(pi, initRuntime);
|
|
45
46
|
registerCompletionNotifications(pi, options.completionNotifications);
|
|
47
|
+
registerWorkedFor(pi);
|
|
46
48
|
}
|
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.6
|
|
35
35
|
```
|
|
36
36
|
|
|
37
37
|
Add `-l` to either command for a project-only install. Restart Pi after installing.
|
|
@@ -41,14 +41,14 @@ 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
|
|
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
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
|
-
- `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
|
|
51
|
+
- `question` tool with single-select and opt-in bounded multi-select, 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
53
|
- Goal-aware `/clear` that confirms, aborts active work, waits for settlement, and starts a new session, plus `/exit` for graceful shutdown
|
|
54
54
|
- Concise system-prompt guidance and supported native concise defaults that preserve explicit provider settings
|
|
@@ -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,12 +70,20 @@ 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
|
+
|
|
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.
|
|
74
76
|
|
|
75
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.
|
|
76
78
|
|
|
77
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.
|
|
78
80
|
|
|
81
|
+
### Interactive questions
|
|
82
|
+
|
|
83
|
+
Single-select remains the default. An agent opts into multi-select with `mode: "multiple"` and may set `minSelections` and `maxSelections`; the custom answer counts as one selection.
|
|
84
|
+
|
|
85
|
+
In multi-select, use Space or a visible number to toggle an option, `/` to filter, and Enter to submit. The filter accepts spaces; Enter applies it and Escape returns to the choices. Checked options remain selected when the filter changes. Select **Type a custom answer** with Enter to add or edit one custom item alongside checked options.
|
|
86
|
+
|
|
79
87
|
Supported reasoning levels are `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, and `max`. KillerOS limits choices to levels supported by the current model.
|
|
80
88
|
|
|
81
89
|
## Configuration
|
|
@@ -92,7 +100,7 @@ Manual `/compact` aborts the current goal turn before summarization, so KillerOS
|
|
|
92
100
|
|
|
93
101
|
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.
|
|
94
102
|
|
|
95
|
-
Lifecycle hooks are loaded from `.pi/killeros-hooks.json` at session start. Supported event keys are `tool_call`, `tool_result`, and `agent_settled
|
|
103
|
+
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.
|
|
96
104
|
|
|
97
105
|
## Behavior by mode
|
|
98
106
|
|
|
@@ -118,17 +126,17 @@ The package manifest lists Pi’s built-in modules as peer dependencies, so npm
|
|
|
118
126
|
|
|
119
127
|
## Publish
|
|
120
128
|
|
|
121
|
-
|
|
129
|
+
To create a GitHub release, update the version in `package.json` and `package-lock.json`, add the matching `CHANGELOG.md` section, and push the release commit to `main`. After the full CI workflow passes, the release workflow creates the matching tag and GitHub release from that verified commit.
|
|
130
|
+
|
|
131
|
+
Do not manually tag a normal release. If automation must recover a missing GitHub release, push the matching version tag; the same workflow validates the tag against the package and changelog before creating the release.
|
|
122
132
|
|
|
123
|
-
|
|
133
|
+
The [`pi-package`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/packages.md) keyword makes a published npm release visible in Pi’s package catalog. GitHub release automation does not publish to npm. Publish there separately after validation:
|
|
124
134
|
|
|
125
135
|
```bash
|
|
126
136
|
npm login
|
|
127
137
|
npm publish
|
|
128
138
|
```
|
|
129
139
|
|
|
130
|
-
For later releases, choose `patch`, `minor`, or `major` with `npm version`, then publish and push the version commit and tag.
|
|
131
|
-
|
|
132
140
|
## Security
|
|
133
141
|
|
|
134
142
|
Pi extensions run with your user permissions. Review the source before installing KillerOS globally. KillerOS executes lifecycle hook commands only for projects Pi marks as trusted; review `.pi/killeros-hooks.json` before enabling project trust.
|
package/killeros/commands.ts
CHANGED
|
@@ -150,12 +150,11 @@ export function registerSlashAutocomplete(pi: ExtensionAPI): void {
|
|
|
150
150
|
usage.set(tagged.killerosCommand, (usage.get(tagged.killerosCommand) ?? 0) + 1);
|
|
151
151
|
const line = lines[cursorLine] ?? "";
|
|
152
152
|
const beforeCursor = line.slice(0, cursorCol);
|
|
153
|
-
|
|
153
|
+
const afterCursor = line.slice(cursorCol);
|
|
154
154
|
const match = beforeCursor.match(/(?:^|[ \t])\/([^\s/]*)$/);
|
|
155
155
|
if (!match || match.index === undefined) return current.applyCompletion(lines, cursorLine, cursorCol, item, prefix);
|
|
156
156
|
const slashIndex = match.index + (match[0].startsWith("/") ? 0 : 1);
|
|
157
157
|
const newBefore = beforeCursor.slice(0, slashIndex) + item.value;
|
|
158
|
-
if (item.value.endsWith(" ") && afterCursor.startsWith(" ")) afterCursor = afterCursor.trimStart();
|
|
159
158
|
const nextLines = [...lines];
|
|
160
159
|
nextLines[cursorLine] = newBefore + afterCursor;
|
|
161
160
|
return { lines: nextLines, cursorLine, cursorCol: newBefore.length };
|
package/killeros/goals.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
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";
|
|
@@ -6,14 +7,14 @@ import { BoundedText } from "./bounded-text.ts";
|
|
|
6
7
|
import { formatTime, formatTokens } from "./display.ts";
|
|
7
8
|
import { reportError } from "./errors.ts";
|
|
8
9
|
import { resolvePersonalInstructions } from "./personal-instructions.ts";
|
|
9
|
-
import type { GoalRuntime, GoalState, GoalStatus, InitRuntime } from "./runtime.ts";
|
|
10
|
+
import type { GoalBlockerAudit, GoalRuntime, GoalState, GoalStatus, InitRuntime } from "./runtime.ts";
|
|
10
11
|
|
|
11
12
|
const GOAL_ENTRY_TYPE = "killeros-goal";
|
|
12
13
|
const GOAL_CONTINUATION_TYPE = "killeros-goal-continuation";
|
|
13
14
|
const GOAL_OBJECTIVE_LIMIT = 4_000;
|
|
14
15
|
const GOAL_VERSION = 1;
|
|
15
16
|
|
|
16
|
-
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";
|
|
17
18
|
interface GoalEntryData {
|
|
18
19
|
version: 1;
|
|
19
20
|
event: GoalEntryEvent;
|
|
@@ -23,6 +24,7 @@ interface GoalEntryData {
|
|
|
23
24
|
interface GoalTransitionOptions {
|
|
24
25
|
resetBlockedAudit?: boolean;
|
|
25
26
|
resumeAfterManualCompaction?: true;
|
|
27
|
+
blockerAudit?: GoalBlockerAudit;
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
interface RestoredGoalState {
|
|
@@ -31,7 +33,7 @@ interface RestoredGoalState {
|
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
const GoalUpdateParams = Type.Object({
|
|
34
|
-
status:
|
|
36
|
+
status: StringEnum(["complete", "blocked"] as const, {
|
|
35
37
|
description: "Mark the active goal complete or blocked",
|
|
36
38
|
}),
|
|
37
39
|
evidence: Type.String({
|
|
@@ -39,11 +41,19 @@ const GoalUpdateParams = Type.Object({
|
|
|
39
41
|
maxLength: 2_000,
|
|
40
42
|
description: "Concise evidence that the objective is complete, or the repeated blocker and attempted workarounds",
|
|
41
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
|
+
})),
|
|
42
50
|
});
|
|
43
51
|
|
|
44
52
|
interface GoalUpdateDetails {
|
|
45
|
-
status: "complete" | "blocked";
|
|
53
|
+
status: "complete" | "blocked" | "blocker-audit";
|
|
46
54
|
evidence: string;
|
|
55
|
+
blockerKey?: string;
|
|
56
|
+
streak?: number;
|
|
47
57
|
}
|
|
48
58
|
|
|
49
59
|
function isGoalStatus(value: unknown): value is GoalStatus {
|
|
@@ -54,6 +64,17 @@ function finiteNonNegative(value: unknown): value is number {
|
|
|
54
64
|
return typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
55
65
|
}
|
|
56
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
|
+
|
|
57
78
|
function parseGoalState(value: unknown): GoalState | undefined {
|
|
58
79
|
if (!value || typeof value !== "object") return undefined;
|
|
59
80
|
const candidate = value as Partial<GoalState>;
|
|
@@ -71,6 +92,7 @@ function parseGoalState(value: unknown): GoalState | undefined {
|
|
|
71
92
|
|| !finiteNonNegative(candidate.baselineTokens)
|
|
72
93
|
|| candidate.activeStartedAt !== undefined && !finiteNonNegative(candidate.activeStartedAt)
|
|
73
94
|
|| candidate.result !== undefined && typeof candidate.result !== "string"
|
|
95
|
+
|| candidate.blockerAudit !== undefined && !isGoalBlockerAudit(candidate.blockerAudit, candidate.turns!, candidate.status)
|
|
74
96
|
|| candidate.resumeAfterManualCompaction !== undefined && candidate.resumeAfterManualCompaction !== true
|
|
75
97
|
|| candidate.resumeAfterManualCompaction === true && candidate.status !== "paused") {
|
|
76
98
|
return undefined;
|
|
@@ -89,6 +111,7 @@ function parseGoalState(value: unknown): GoalState | undefined {
|
|
|
89
111
|
baselineTokens: candidate.baselineTokens,
|
|
90
112
|
result: candidate.result,
|
|
91
113
|
resumeAfterManualCompaction: candidate.resumeAfterManualCompaction,
|
|
114
|
+
blockerAudit: candidate.blockerAudit,
|
|
92
115
|
};
|
|
93
116
|
}
|
|
94
117
|
|
|
@@ -182,6 +205,7 @@ function transitionGoal(
|
|
|
182
205
|
updatedAt: now,
|
|
183
206
|
activeStartedAt: status === "active" ? now : undefined,
|
|
184
207
|
blockedAuditStartTurn: options.resetBlockedAudit ? stopped.turns : stopped.blockedAuditStartTurn,
|
|
208
|
+
blockerAudit: options.resetBlockedAudit ? undefined : options.blockerAudit ?? stopped.blockerAudit,
|
|
185
209
|
result,
|
|
186
210
|
resumeAfterManualCompaction: options.resumeAfterManualCompaction,
|
|
187
211
|
};
|
|
@@ -190,6 +214,24 @@ function transitionGoal(
|
|
|
190
214
|
return next;
|
|
191
215
|
}
|
|
192
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
|
+
|
|
193
235
|
function goalStatusLabel(status: GoalStatus): string {
|
|
194
236
|
return `${status.charAt(0).toLocaleUpperCase()}${status.slice(1)}`;
|
|
195
237
|
}
|
|
@@ -222,6 +264,7 @@ export function pauseGoalAfterFailure(
|
|
|
222
264
|
ctx: ExtensionContext,
|
|
223
265
|
reason: string,
|
|
224
266
|
recoveryInstruction = "Run /goal resume after resolving the problem.",
|
|
267
|
+
notify = true,
|
|
225
268
|
): void {
|
|
226
269
|
if (runtime.state?.status !== "active") return;
|
|
227
270
|
try {
|
|
@@ -237,7 +280,7 @@ export function pauseGoalAfterFailure(
|
|
|
237
280
|
runtime.continuationScheduled = false;
|
|
238
281
|
runtime.requestRender?.();
|
|
239
282
|
}
|
|
240
|
-
ctx.ui.notify(`Goal paused: ${reason}\n${recoveryInstruction}`, "error");
|
|
283
|
+
if (notify) ctx.ui.notify(`Goal paused: ${reason}\n${recoveryInstruction}`, "error");
|
|
241
284
|
}
|
|
242
285
|
|
|
243
286
|
function pauseGoalForPossibleManualCompaction(
|
|
@@ -290,6 +333,34 @@ function recoverGoalAfterManualCompaction(
|
|
|
290
333
|
return true;
|
|
291
334
|
}
|
|
292
335
|
|
|
336
|
+
function beginGoalTurn(
|
|
337
|
+
pi: ExtensionAPI,
|
|
338
|
+
runtime: GoalRuntime,
|
|
339
|
+
ctx: ExtensionContext,
|
|
340
|
+
current: GoalState,
|
|
341
|
+
): GoalState | undefined {
|
|
342
|
+
const now = Date.now();
|
|
343
|
+
const next: GoalState = {
|
|
344
|
+
...current,
|
|
345
|
+
revision: current.revision + 1,
|
|
346
|
+
turns: current.turns + 1,
|
|
347
|
+
updatedAt: now,
|
|
348
|
+
activeStartedAt: current.activeStartedAt ?? now,
|
|
349
|
+
resumeAfterManualCompaction: undefined,
|
|
350
|
+
};
|
|
351
|
+
try {
|
|
352
|
+
persistGoalState(pi, runtime, "turn", next);
|
|
353
|
+
} catch (error) {
|
|
354
|
+
pauseGoalAfterFailure(pi, runtime, ctx, `turn state could not be saved: ${error instanceof Error ? error.message : String(error)}`);
|
|
355
|
+
return undefined;
|
|
356
|
+
}
|
|
357
|
+
runtime.goalTurnInFlight = true;
|
|
358
|
+
runtime.agentEndObserved = false;
|
|
359
|
+
runtime.lastStopReason = undefined;
|
|
360
|
+
runtime.lastError = undefined;
|
|
361
|
+
return next;
|
|
362
|
+
}
|
|
363
|
+
|
|
293
364
|
function scheduleGoalContinuation(
|
|
294
365
|
pi: ExtensionAPI,
|
|
295
366
|
runtime: GoalRuntime,
|
|
@@ -303,17 +374,15 @@ function scheduleGoalContinuation(
|
|
|
303
374
|
|| runtime.continuationHeld
|
|
304
375
|
|| runtime.goalTurnInFlight
|
|
305
376
|
|| initState.active
|
|
377
|
+
|| !ctx.isIdle()
|
|
306
378
|
|| ctx.hasPendingMessages()) return false;
|
|
307
|
-
const current = runtime.state;
|
|
308
379
|
runtime.continuationScheduled = true;
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
runtime.lastStopReason = undefined;
|
|
312
|
-
runtime.lastError = undefined;
|
|
380
|
+
const next = beginGoalTurn(pi, runtime, ctx, runtime.state);
|
|
381
|
+
if (!next) return false;
|
|
313
382
|
try {
|
|
314
383
|
pi.sendMessage({
|
|
315
384
|
customType: GOAL_CONTINUATION_TYPE,
|
|
316
|
-
content: goalContinuationMessage(
|
|
385
|
+
content: goalContinuationMessage(next, ctx),
|
|
317
386
|
display: false,
|
|
318
387
|
}, { triggerTurn: true, deliverAs: "followUp" });
|
|
319
388
|
return true;
|
|
@@ -337,7 +406,7 @@ function goalInstructions(state: GoalState, heading: string): string {
|
|
|
337
406
|
"Continue making concrete progress toward this unchanged objective. Re-check repository state and prior results instead of repeating work.",
|
|
338
407
|
"Do not stop merely because one response is complete: KillerOS will start another goal turn while the goal remains active.",
|
|
339
408
|
"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.",
|
|
340
|
-
"Call killeros_goal_update with status blocked
|
|
409
|
+
"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.",
|
|
341
410
|
"Never use the goal tool to pause, resume, edit, replace, or clear the objective. Those transitions belong to the user.",
|
|
342
411
|
].join("\n");
|
|
343
412
|
}
|
|
@@ -398,7 +467,7 @@ export function registerGoal(
|
|
|
398
467
|
pi.registerTool<typeof GoalUpdateParams, GoalUpdateDetails>({
|
|
399
468
|
name: "killeros_goal_update",
|
|
400
469
|
label: "Goal update",
|
|
401
|
-
description: "Mark the active KillerOS long-running goal complete after verification, or
|
|
470
|
+
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.",
|
|
402
471
|
parameters: GoalUpdateParams,
|
|
403
472
|
executionMode: "sequential",
|
|
404
473
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
@@ -408,13 +477,40 @@ export function registerGoal(
|
|
|
408
477
|
if (!state || state.status !== "active") throw new Error("There is no active KillerOS goal to update");
|
|
409
478
|
const evidence = params.evidence.trim();
|
|
410
479
|
if (!evidence) throw new Error("Goal evidence must not be empty");
|
|
411
|
-
if (params.status === "
|
|
412
|
-
|
|
480
|
+
if (params.status === "complete") {
|
|
481
|
+
transitionGoal(pi, runtime, "complete", "complete", evidence, { resetBlockedAudit: true });
|
|
482
|
+
return {
|
|
483
|
+
content: [{ type: "text", text: `Goal marked complete: ${evidence}` }],
|
|
484
|
+
details: { status: "complete", evidence },
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
if (!runtime.goalTurnInFlight) throw new Error("A blocker audit can only be recorded during an active KillerOS goal turn");
|
|
488
|
+
const blockerKey = params.blockerKey;
|
|
489
|
+
if (!blockerKey || !/^[a-z0-9][a-z0-9._-]{0,119}$/u.test(blockerKey)) {
|
|
490
|
+
throw new Error("A blocked goal update requires a stable lowercase blockerKey");
|
|
413
491
|
}
|
|
414
|
-
|
|
492
|
+
const previous = state.blockerAudit;
|
|
493
|
+
const sameTurn = previous?.key === blockerKey && previous.lastTurn === state.turns;
|
|
494
|
+
const consecutive = previous?.key === blockerKey && previous.lastTurn === state.turns - 1;
|
|
495
|
+
const streak = sameTurn ? previous.streak : consecutive ? previous.streak + 1 : 1;
|
|
496
|
+
const blockerAudit = { key: blockerKey, streak, lastTurn: state.turns };
|
|
497
|
+
if (streak < 3) {
|
|
498
|
+
const next: GoalState = {
|
|
499
|
+
...state,
|
|
500
|
+
revision: state.revision + 1,
|
|
501
|
+
updatedAt: Date.now(),
|
|
502
|
+
blockerAudit,
|
|
503
|
+
};
|
|
504
|
+
persistGoalState(pi, runtime, "blocker-audit", next);
|
|
505
|
+
return {
|
|
506
|
+
content: [{ type: "text", text: `Blocker audit ${streak}/3 recorded; the goal remains active: ${evidence}` }],
|
|
507
|
+
details: { status: "blocker-audit", evidence, blockerKey, streak },
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
transitionGoal(pi, runtime, "blocked", "blocked", evidence, { blockerAudit });
|
|
415
511
|
return {
|
|
416
|
-
content: [{ type: "text", text: `Goal marked
|
|
417
|
-
details: { status:
|
|
512
|
+
content: [{ type: "text", text: `Goal marked blocked: ${evidence}` }],
|
|
513
|
+
details: { status: "blocked", evidence, blockerKey, streak },
|
|
418
514
|
};
|
|
419
515
|
},
|
|
420
516
|
renderCall(args, theme) {
|
|
@@ -423,7 +519,8 @@ export function registerGoal(
|
|
|
423
519
|
renderResult(result, options, theme) {
|
|
424
520
|
const details = result.details;
|
|
425
521
|
if (!details) return new BoundedText(theme.fg("dim", "Goal updated"));
|
|
426
|
-
const
|
|
522
|
+
const label = details.status === "complete" ? "✓ Complete" : details.status === "blocked" ? "! Blocked" : `! Blocker audit ${details.streak}/3`;
|
|
523
|
+
const text = `${theme.fg(details.status === "complete" ? "success" : "warning", label)}${theme.fg("dim", ` · ${details.evidence}`)}`;
|
|
427
524
|
return new BoundedText(text, options.expanded ? undefined : 3);
|
|
428
525
|
},
|
|
429
526
|
});
|
|
@@ -478,25 +575,8 @@ export function registerGoal(
|
|
|
478
575
|
const current = runtime.state;
|
|
479
576
|
if (!isGoalModeSupported(ctx) || !isSavedSession(ctx) || !current || current.status !== "active" || initState.active) return;
|
|
480
577
|
if (runtime.goalTurnInFlight) return { systemPrompt: `${event.systemPrompt}\n\n${goalSystemPrompt(current)}` };
|
|
481
|
-
const
|
|
482
|
-
|
|
483
|
-
...current,
|
|
484
|
-
revision: current.revision + 1,
|
|
485
|
-
turns: current.turns + 1,
|
|
486
|
-
updatedAt: now,
|
|
487
|
-
activeStartedAt: current.activeStartedAt ?? now,
|
|
488
|
-
resumeAfterManualCompaction: undefined,
|
|
489
|
-
};
|
|
490
|
-
try {
|
|
491
|
-
persistGoalState(pi, runtime, "turn", next);
|
|
492
|
-
} catch (error) {
|
|
493
|
-
pauseGoalAfterFailure(pi, runtime, ctx, `turn state could not be saved: ${error instanceof Error ? error.message : String(error)}`);
|
|
494
|
-
return;
|
|
495
|
-
}
|
|
496
|
-
runtime.goalTurnInFlight = true;
|
|
497
|
-
runtime.agentEndObserved = false;
|
|
498
|
-
runtime.lastStopReason = undefined;
|
|
499
|
-
runtime.lastError = undefined;
|
|
578
|
+
const next = beginGoalTurn(pi, runtime, ctx, current);
|
|
579
|
+
if (!next) return;
|
|
500
580
|
return { systemPrompt: `${event.systemPrompt}\n\n${goalSystemPrompt(next)}` };
|
|
501
581
|
});
|
|
502
582
|
|
|
@@ -544,10 +624,11 @@ export function registerGoal(
|
|
|
544
624
|
ctx.ui.notify("No goal is set", "info");
|
|
545
625
|
return;
|
|
546
626
|
}
|
|
627
|
+
const shouldStopGoalRun = runtime.goalTurnInFlight || runtime.continuationScheduled;
|
|
628
|
+
let saved = false;
|
|
547
629
|
try {
|
|
548
630
|
persistGoalState(pi, runtime, "clear", undefined);
|
|
549
|
-
|
|
550
|
-
ctx.ui.notify("Goal cleared", "info");
|
|
631
|
+
saved = true;
|
|
551
632
|
} catch (error) {
|
|
552
633
|
if (runtime.state?.status === "active") {
|
|
553
634
|
pauseGoalAfterFailure(
|
|
@@ -556,11 +637,24 @@ export function registerGoal(
|
|
|
556
637
|
ctx,
|
|
557
638
|
`the requested clear could not be saved: ${error instanceof Error ? error.message : String(error)}`,
|
|
558
639
|
"Automatic continuation is stopped. Retry /goal clear to remove the goal.",
|
|
640
|
+
false,
|
|
559
641
|
);
|
|
560
642
|
} else {
|
|
561
643
|
reportError(ctx, "Goal could not be cleared", error);
|
|
644
|
+
return;
|
|
562
645
|
}
|
|
563
646
|
}
|
|
647
|
+
try {
|
|
648
|
+
await stopGoalRun(runtime, ctx, shouldStopGoalRun);
|
|
649
|
+
} catch (error) {
|
|
650
|
+
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);
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
if (saved) {
|
|
654
|
+
ctx.ui.notify("Goal cleared", "info");
|
|
655
|
+
} else {
|
|
656
|
+
ctx.ui.notify("Goal paused: the requested clear could not be saved\nAutomatic continuation is stopped. Retry /goal clear to remove the goal.", "error");
|
|
657
|
+
}
|
|
564
658
|
return;
|
|
565
659
|
}
|
|
566
660
|
|
|
@@ -597,18 +691,34 @@ export function registerGoal(
|
|
|
597
691
|
ctx.ui.notify(`Goal is ${runtime.state.status}; only an active goal can be paused`, "warning");
|
|
598
692
|
return;
|
|
599
693
|
}
|
|
694
|
+
const shouldStopGoalRun = runtime.goalTurnInFlight || runtime.continuationScheduled;
|
|
695
|
+
let saved = false;
|
|
696
|
+
let failureReason: string | undefined;
|
|
600
697
|
try {
|
|
601
698
|
transitionGoal(pi, runtime, "pause", "paused");
|
|
602
|
-
|
|
699
|
+
saved = true;
|
|
603
700
|
} catch (error) {
|
|
701
|
+
failureReason = `the requested pause could not be saved: ${error instanceof Error ? error.message : String(error)}`;
|
|
604
702
|
pauseGoalAfterFailure(
|
|
605
703
|
pi,
|
|
606
704
|
runtime,
|
|
607
705
|
ctx,
|
|
608
|
-
|
|
706
|
+
failureReason,
|
|
609
707
|
"Automatic continuation is stopped. If session storage is still unavailable, retry /goal pause after it recovers.",
|
|
708
|
+
false,
|
|
610
709
|
);
|
|
611
710
|
}
|
|
711
|
+
try {
|
|
712
|
+
await stopGoalRun(runtime, ctx, shouldStopGoalRun);
|
|
713
|
+
} catch (error) {
|
|
714
|
+
reportError(ctx, "Goal paused, but the active goal turn could not be confirmed stopped", error);
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
if (saved) {
|
|
718
|
+
ctx.ui.notify("Goal paused. Run /goal resume to continue.", "info");
|
|
719
|
+
} else {
|
|
720
|
+
ctx.ui.notify(`Goal paused: ${failureReason}\nAutomatic continuation is stopped. If session storage is still unavailable, retry /goal pause after it recovers.`, "error");
|
|
721
|
+
}
|
|
612
722
|
return;
|
|
613
723
|
}
|
|
614
724
|
|
|
@@ -687,6 +797,7 @@ export function registerGoal(
|
|
|
687
797
|
updatedAt: now,
|
|
688
798
|
activeStartedAt: now,
|
|
689
799
|
blockedAuditStartTurn: current.turns,
|
|
800
|
+
blockerAudit: undefined,
|
|
690
801
|
result: undefined,
|
|
691
802
|
resumeAfterManualCompaction: undefined,
|
|
692
803
|
};
|
|
@@ -695,13 +806,17 @@ export function registerGoal(
|
|
|
695
806
|
runtime.continuationScheduled = false;
|
|
696
807
|
if (scheduleGoalContinuation(pi, runtime, initState, ctx)) ctx.ui.notify("Goal updated and active", "info");
|
|
697
808
|
} catch (error) {
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
809
|
+
if (runtime.state?.status === "active") {
|
|
810
|
+
pauseGoalAfterFailure(
|
|
811
|
+
pi,
|
|
812
|
+
runtime,
|
|
813
|
+
ctx,
|
|
814
|
+
`Goal could not be edited: ${error instanceof Error ? error.message : String(error)}`,
|
|
815
|
+
"Automatic continuation is stopped. Retry /goal edit after session storage recovers.",
|
|
816
|
+
);
|
|
817
|
+
} else {
|
|
818
|
+
reportError(ctx, "Goal could not be edited", error);
|
|
819
|
+
}
|
|
705
820
|
}
|
|
706
821
|
return;
|
|
707
822
|
}
|
|
@@ -761,8 +876,19 @@ export function registerGoal(
|
|
|
761
876
|
ctx.ui.notify("Goal active. KillerOS will continue until completion, a repeated blocker, or pause.", "info");
|
|
762
877
|
}
|
|
763
878
|
} catch (error) {
|
|
764
|
-
|
|
765
|
-
|
|
879
|
+
if (!unfinished) {
|
|
880
|
+
reportError(ctx, "Goal could not be started", error);
|
|
881
|
+
} else if (runtime.state?.status === "active") {
|
|
882
|
+
pauseGoalAfterFailure(
|
|
883
|
+
pi,
|
|
884
|
+
runtime,
|
|
885
|
+
ctx,
|
|
886
|
+
`Goal could not be replaced: ${error instanceof Error ? error.message : String(error)}`,
|
|
887
|
+
"Automatic continuation is stopped. Retry replacement after session storage recovers.",
|
|
888
|
+
);
|
|
889
|
+
} else {
|
|
890
|
+
reportError(ctx, "Goal could not be replaced", error);
|
|
891
|
+
}
|
|
766
892
|
}
|
|
767
893
|
};
|
|
768
894
|
|
|
@@ -800,6 +926,8 @@ export function registerGoalSettlement(
|
|
|
800
926
|
if (!wasGoalTurn || runtime.state?.status !== "active" || initState.active) {
|
|
801
927
|
if (continuationWasScheduled && runtime.state?.status === "active" && !initState.active) {
|
|
802
928
|
pauseGoalAfterFailure(pi, runtime, ctx, "the goal continuation ended before an agent turn started");
|
|
929
|
+
} else if (runtime.state?.status === "active" && !initState.active) {
|
|
930
|
+
scheduleGoalContinuation(pi, runtime, initState, ctx);
|
|
803
931
|
}
|
|
804
932
|
return;
|
|
805
933
|
}
|