killeros 2.0.22 → 2.1.23
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 +5 -2
- package/README.md +22 -5
- package/killeros/auto-compaction.ts +56 -15
- package/killeros/change-receipt.ts +727 -0
- package/killeros/commands.ts +1 -1
- package/killeros/display.ts +9 -7
- package/killeros/footer.ts +112 -21
- package/killeros/goal-command.ts +23 -0
- package/killeros/goal-interface.ts +420 -0
- package/killeros/goal-runtime.ts +347 -0
- package/killeros/goal-settlement.ts +261 -0
- package/killeros/goal-state.ts +66 -38
- package/killeros/hooks.ts +23 -8
- package/killeros/runtime.ts +2 -0
- package/killeros/worked-for.ts +271 -61
- package/package.json +3 -2
- package/killeros/goals.ts +0 -1022
package/killeros/goal-state.ts
CHANGED
|
@@ -4,7 +4,9 @@ import { lstat, open, type FileHandle } from "node:fs/promises";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import type { GoalBlockerAudit, GoalFileBaseline, GoalFileVerification, GoalState, GoalStateCommon, GoalStatus } from "./runtime.ts";
|
|
6
6
|
|
|
7
|
+
export const DEFAULT_GOAL_MAX_TURNS = 20;
|
|
7
8
|
export const GOAL_OBJECTIVE_LIMIT = 4_000;
|
|
9
|
+
export const GOAL_MAX_TURNS = 10_000;
|
|
8
10
|
export const GOAL_VERSION = 1;
|
|
9
11
|
const FILE_HASH_CHUNK_SIZE = 64 * 1024;
|
|
10
12
|
export const FILE_HASH_LIMIT = 64 * 1024 * 1024;
|
|
@@ -25,6 +27,20 @@ function finiteNonNegative(value: unknown): value is number {
|
|
|
25
27
|
return typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
26
28
|
}
|
|
27
29
|
|
|
30
|
+
function safeNonNegativeInteger(value: unknown): value is number {
|
|
31
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function incrementableNonNegativeInteger(value: unknown): value is number {
|
|
35
|
+
return safeNonNegativeInteger(value) && value < Number.MAX_SAFE_INTEGER;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function addGoalMilliseconds(accumulated: number, interval: number): number {
|
|
39
|
+
const total = accumulated + interval;
|
|
40
|
+
if (!safeNonNegativeInteger(total)) throw new Error("Goal active duration exceeds the safe integer range");
|
|
41
|
+
return total;
|
|
42
|
+
}
|
|
43
|
+
|
|
28
44
|
function isUnknownRecord(value: unknown): value is Record<string, unknown> {
|
|
29
45
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
30
46
|
}
|
|
@@ -47,6 +63,14 @@ function isAbsoluteFilePath(value: string): boolean {
|
|
|
47
63
|
return path.isAbsolute(value) || path.win32.isAbsolute(value);
|
|
48
64
|
}
|
|
49
65
|
|
|
66
|
+
function stripUnquotedPathPunctuation(value: string): string {
|
|
67
|
+
const pathWithoutMarks = value.replace(/[.!?]+$/u, "");
|
|
68
|
+
const trailingClosers = pathWithoutMarks.match(/\)+$/u)?.[0].length ?? 0;
|
|
69
|
+
const unmatchedClosers = Math.max(0, pathWithoutMarks.split(")").length - pathWithoutMarks.split("(").length);
|
|
70
|
+
const punctuationLength = Math.min(trailingClosers, unmatchedClosers);
|
|
71
|
+
return pathWithoutMarks.slice(0, punctuationLength ? -punctuationLength : undefined);
|
|
72
|
+
}
|
|
73
|
+
|
|
50
74
|
function isGoalFileVerification(value: unknown): value is GoalFileVerification {
|
|
51
75
|
return isUnknownRecord(value)
|
|
52
76
|
&& value.kind === "file"
|
|
@@ -56,12 +80,18 @@ function isGoalFileVerification(value: unknown): value is GoalFileVerification {
|
|
|
56
80
|
&& isGoalFileBaseline(value.baseline);
|
|
57
81
|
}
|
|
58
82
|
|
|
83
|
+
function isMaxTurns(value: unknown): value is number {
|
|
84
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 1 && value <= GOAL_MAX_TURNS;
|
|
85
|
+
}
|
|
86
|
+
|
|
59
87
|
function isGoalBlockerAudit(value: unknown, turns: number, status: GoalStatus): value is GoalBlockerAudit {
|
|
60
88
|
if (!isUnknownRecord(value)
|
|
61
89
|
|| typeof value.key !== "string"
|
|
62
90
|
|| !/^[a-z0-9][a-z0-9._-]{0,119}$/u.test(value.key)
|
|
63
91
|
|| typeof value.streak !== "number" || !Number.isInteger(value.streak) || value.streak < 1 || value.streak > 3
|
|
64
|
-
|| typeof value.lastTurn !== "number" || !Number.isInteger(value.lastTurn) || value.lastTurn < 1 || value.lastTurn > turns
|
|
92
|
+
|| typeof value.lastTurn !== "number" || !Number.isInteger(value.lastTurn) || value.lastTurn < 1 || value.lastTurn > turns
|
|
93
|
+
|| value.evidence !== undefined && (typeof value.evidence !== "string"
|
|
94
|
+
|| value.evidence !== value.evidence.trim() || !value.evidence || value.evidence.length > 2_000)) {
|
|
65
95
|
return false;
|
|
66
96
|
}
|
|
67
97
|
if (status === "complete") return false;
|
|
@@ -86,21 +116,22 @@ export function parseGoalState(value: unknown): GoalState | undefined {
|
|
|
86
116
|
resumeAfterManualCompaction,
|
|
87
117
|
blockerAudit,
|
|
88
118
|
verification,
|
|
119
|
+
maxTurns,
|
|
89
120
|
} = value;
|
|
90
121
|
if (version !== GOAL_VERSION
|
|
91
|
-
||
|
|
122
|
+
|| !incrementableNonNegativeInteger(revision) || revision < 1
|
|
92
123
|
|| typeof objective !== "string" || !objective.trim() || [...objective].length > GOAL_OBJECTIVE_LIMIT
|
|
93
124
|
|| !isGoalStatus(status)
|
|
94
|
-
|| !
|
|
95
|
-
|| !
|
|
96
|
-
|| !
|
|
97
|
-
||
|
|
125
|
+
|| !safeNonNegativeInteger(createdAt)
|
|
126
|
+
|| !safeNonNegativeInteger(updatedAt)
|
|
127
|
+
|| !incrementableNonNegativeInteger(activeMilliseconds)
|
|
128
|
+
|| !incrementableNonNegativeInteger(turns)
|
|
98
129
|
|| blockedAuditStartTurn !== undefined
|
|
99
|
-
&& (
|
|
100
|
-
|
|
101
|
-
|| !finiteNonNegative(baselineTokens)
|
|
130
|
+
&& (!safeNonNegativeInteger(blockedAuditStartTurn) || blockedAuditStartTurn > turns)
|
|
131
|
+
|| !safeNonNegativeInteger(baselineTokens)
|
|
102
132
|
|| result !== undefined && typeof result !== "string"
|
|
103
133
|
|| verification !== undefined && !isGoalFileVerification(verification)
|
|
134
|
+
|| maxTurns !== undefined && !isMaxTurns(maxTurns)
|
|
104
135
|
|| resumeAfterManualCompaction !== undefined && resumeAfterManualCompaction !== true
|
|
105
136
|
|| blockerAudit !== undefined && !isGoalBlockerAudit(blockerAudit, turns, status)) {
|
|
106
137
|
return undefined;
|
|
@@ -117,10 +148,11 @@ export function parseGoalState(value: unknown): GoalState | undefined {
|
|
|
117
148
|
blockedAuditStartTurn: blockedAuditStartTurn ?? 0,
|
|
118
149
|
baselineTokens,
|
|
119
150
|
...(verification === undefined ? {} : { verification }),
|
|
151
|
+
...(maxTurns === undefined ? {} : { maxTurns }),
|
|
120
152
|
};
|
|
121
153
|
switch (status) {
|
|
122
154
|
case "active":
|
|
123
|
-
if (!
|
|
155
|
+
if (!safeNonNegativeInteger(activeStartedAt) || resumeAfterManualCompaction !== undefined) return undefined;
|
|
124
156
|
return {
|
|
125
157
|
...common,
|
|
126
158
|
status,
|
|
@@ -201,13 +233,26 @@ export async function captureGoalFileBaseline(
|
|
|
201
233
|
}
|
|
202
234
|
}
|
|
203
235
|
|
|
204
|
-
/** Captures one explicit
|
|
205
|
-
export async function inferGoalVerification(objective: string): Promise<GoalFileVerification | undefined> {
|
|
236
|
+
/** Captures one explicit output path so goal completion can verify its creation or modification. */
|
|
237
|
+
export async function inferGoalVerification(objective: string, cwd: string): Promise<GoalFileVerification | undefined> {
|
|
238
|
+
const candidates: string[] = [];
|
|
206
239
|
const destination = /\b(?:create|write|save|generate)\b[^\r\n]{0,160}?\b(?:file|document|markdown|report|spreadsheet|presentation|image)\b\s+(?:to|at|as|destination(?:\s+is)?|output(?:\s+(?:to|at))?)\b\s*(?:`([^`\r\n]+)`|"([^"\r\n]+)"|'([^'\r\n]+)'|([A-Za-z]:\\[^\s,;]+|\/[^\s,;]+))/giu;
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
.
|
|
210
|
-
|
|
240
|
+
for (const match of objective.matchAll(destination)) {
|
|
241
|
+
const quoted = match[1] ?? match[2] ?? match[3];
|
|
242
|
+
candidates.push(quoted !== undefined ? quoted.trim() : stripUnquotedPathPunctuation((match[4] ?? "").trim()));
|
|
243
|
+
}
|
|
244
|
+
const direct = /\b(?:update|edit|fix|refactor|migrate)\s+(`([^`\r\n]+)`|"([^"\r\n]+)"|'([^'\r\n]+)')/giu;
|
|
245
|
+
for (const match of objective.matchAll(direct)) {
|
|
246
|
+
const quoted = match[2] ?? match[3] ?? match[4];
|
|
247
|
+
if (quoted !== undefined) candidates.push(quoted.trim());
|
|
248
|
+
}
|
|
249
|
+
const resolved: string[] = [];
|
|
250
|
+
for (const raw of candidates) {
|
|
251
|
+
if (!raw || /^(?:https?|file):\/\//iu.test(raw) || /[\\\/]$/u.test(raw)) continue;
|
|
252
|
+
const absolute = path.isAbsolute(raw) || path.win32.isAbsolute(raw) ? raw : path.resolve(cwd, raw);
|
|
253
|
+
if (isAbsoluteFilePath(absolute)) resolved.push(absolute);
|
|
254
|
+
}
|
|
255
|
+
const unique = [...new Set(resolved)];
|
|
211
256
|
const filePath = unique.length === 1 ? unique[0] : undefined;
|
|
212
257
|
return filePath ? { kind: "file", path: filePath, baseline: await captureGoalFileBaseline(filePath) } : undefined;
|
|
213
258
|
}
|
|
@@ -250,7 +295,7 @@ export function validateGoalObjective(input: string): string | undefined {
|
|
|
250
295
|
|
|
251
296
|
export function goalElapsedMilliseconds(state: GoalState, now: number): number {
|
|
252
297
|
const activeInterval = state.status === "active" ? Math.max(0, now - state.activeStartedAt) : 0;
|
|
253
|
-
return state.activeMilliseconds
|
|
298
|
+
return addGoalMilliseconds(state.activeMilliseconds, activeInterval);
|
|
254
299
|
}
|
|
255
300
|
|
|
256
301
|
export function commonGoalState(state: GoalState): GoalStateCommon {
|
|
@@ -265,13 +310,14 @@ export function commonGoalState(state: GoalState): GoalStateCommon {
|
|
|
265
310
|
blockedAuditStartTurn: state.blockedAuditStartTurn,
|
|
266
311
|
baselineTokens: state.baselineTokens,
|
|
267
312
|
...(state.verification === undefined ? {} : { verification: state.verification }),
|
|
313
|
+
...(state.maxTurns === undefined ? {} : { maxTurns: state.maxTurns }),
|
|
268
314
|
};
|
|
269
315
|
}
|
|
270
316
|
|
|
271
317
|
export function stopGoalClock(state: GoalState, now: number): GoalStateCommon {
|
|
272
318
|
const common = commonGoalState(state);
|
|
273
319
|
return state.status === "active"
|
|
274
|
-
? { ...common, activeMilliseconds: common.activeMilliseconds
|
|
320
|
+
? { ...common, activeMilliseconds: addGoalMilliseconds(common.activeMilliseconds, Math.max(0, now - state.activeStartedAt)) }
|
|
275
321
|
: common;
|
|
276
322
|
}
|
|
277
323
|
|
|
@@ -280,6 +326,7 @@ export function createNewGoalState(
|
|
|
280
326
|
baselineTokens: number,
|
|
281
327
|
verification: GoalFileVerification | undefined,
|
|
282
328
|
now: number,
|
|
329
|
+
controls: { maxTurns?: number } = {},
|
|
283
330
|
): GoalState {
|
|
284
331
|
return {
|
|
285
332
|
version: GOAL_VERSION,
|
|
@@ -294,26 +341,7 @@ export function createNewGoalState(
|
|
|
294
341
|
blockedAuditStartTurn: 0,
|
|
295
342
|
baselineTokens,
|
|
296
343
|
...(verification === undefined ? {} : { verification }),
|
|
297
|
-
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
export function editGoalState(
|
|
301
|
-
state: GoalState,
|
|
302
|
-
objective: string,
|
|
303
|
-
verification: GoalFileVerification | undefined,
|
|
304
|
-
now: number,
|
|
305
|
-
): GoalState {
|
|
306
|
-
const current = stopGoalClock(state, now);
|
|
307
|
-
const { verification: _previousVerification, ...common } = current;
|
|
308
|
-
return {
|
|
309
|
-
...common,
|
|
310
|
-
revision: current.revision + 1,
|
|
311
|
-
objective,
|
|
312
|
-
status: "active",
|
|
313
|
-
updatedAt: now,
|
|
314
|
-
activeStartedAt: now,
|
|
315
|
-
blockedAuditStartTurn: current.turns,
|
|
316
|
-
...(verification === undefined ? {} : { verification }),
|
|
344
|
+
...(controls.maxTurns === undefined ? {} : { maxTurns: controls.maxTurns }),
|
|
317
345
|
};
|
|
318
346
|
}
|
|
319
347
|
|
package/killeros/hooks.ts
CHANGED
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
spawn,
|
|
3
3
|
type SpawnOptionsWithStdioTuple,
|
|
4
4
|
} from "node:child_process";
|
|
5
|
+
import { EventEmitter } from "node:events";
|
|
5
6
|
import { closeSync, existsSync, fstatSync, lstatSync, openSync, readSync, realpathSync } from "node:fs";
|
|
6
7
|
import path from "node:path";
|
|
7
8
|
import { StringDecoder } from "node:string_decoder";
|
|
@@ -58,6 +59,7 @@ const HOOK_EVENTS: readonly KillerosHookEvent[] = ["tool_call", "tool_result", "
|
|
|
58
59
|
const HOOK_CONFIG_LIMIT = 64 * 1024;
|
|
59
60
|
const HOOK_OUTPUT_LIMIT = 16 * 1024;
|
|
60
61
|
const HOOK_PAYLOAD_LIMIT = 8_000;
|
|
62
|
+
const HOOK_TIMEOUT_DEFAULT_MS = 30_000;
|
|
61
63
|
const HOOK_TIMEOUT_MAX_MS = 300_000;
|
|
62
64
|
|
|
63
65
|
// Reads executable project configuration through a bounded, project-local file descriptor.
|
|
@@ -109,7 +111,7 @@ function isUnknownRecord(value: unknown): value is Record<string, unknown> {
|
|
|
109
111
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
110
112
|
}
|
|
111
113
|
|
|
112
|
-
function
|
|
114
|
+
function loadKillerosConfig(ctx: ExtensionContext): KillerosHookConfig {
|
|
113
115
|
const configPath = path.join(ctx.cwd, CONFIG_DIR_NAME, "killeros-hooks.json");
|
|
114
116
|
if (!existsSync(configPath)) return {};
|
|
115
117
|
const displayPath = safeTerminalText(configPath).replaceAll("\n", "");
|
|
@@ -166,6 +168,7 @@ function loadKillerosHooks(ctx: ExtensionContext): KillerosHookConfig {
|
|
|
166
168
|
}
|
|
167
169
|
hooks[event] = accepted;
|
|
168
170
|
}
|
|
171
|
+
|
|
169
172
|
return { hooks };
|
|
170
173
|
} catch (error) {
|
|
171
174
|
reportError(ctx, `Invalid ${CONFIG_DIR_NAME}/killeros-hooks.json`, error);
|
|
@@ -265,7 +268,7 @@ export function executeHook(
|
|
|
265
268
|
command,
|
|
266
269
|
cwd,
|
|
267
270
|
environment,
|
|
268
|
-
timeoutMs =
|
|
271
|
+
timeoutMs = HOOK_TIMEOUT_DEFAULT_MS,
|
|
269
272
|
spawnProcess = spawn,
|
|
270
273
|
signal,
|
|
271
274
|
} = options;
|
|
@@ -331,18 +334,19 @@ export function executeHook(
|
|
|
331
334
|
}, 2_000);
|
|
332
335
|
return;
|
|
333
336
|
}
|
|
334
|
-
terminateHookProcess(child, false);
|
|
335
337
|
forceTimer = setTimeout(() => {
|
|
336
338
|
if (completed) return;
|
|
337
339
|
terminateHookProcess(child, true);
|
|
338
340
|
settleTimer = setTimeout(() => finish(terminationCode(), true), 1_000);
|
|
339
341
|
}, 1_000);
|
|
342
|
+
terminateHookProcess(child, false);
|
|
340
343
|
};
|
|
341
344
|
const abort = (): void => beginTermination("cancelled");
|
|
342
|
-
signal?.addEventListener("abort", abort, { once: true });
|
|
343
|
-
if (signal?.aborted) beginTermination("cancelled");
|
|
344
345
|
child.stdout.on("data", (chunk) => appendBounded(stdout, chunk));
|
|
345
346
|
child.stderr.on("data", (chunk) => appendBounded(stderr, chunk));
|
|
347
|
+
const captureStreamError = (error: Error): void => appendBounded(stderr, error.message);
|
|
348
|
+
if (child.stdout instanceof EventEmitter) child.stdout.on("error", captureStreamError);
|
|
349
|
+
if (child.stderr instanceof EventEmitter) child.stderr.on("error", captureStreamError);
|
|
346
350
|
child.on("error", (error) => {
|
|
347
351
|
appendBounded(stderr, error.message);
|
|
348
352
|
if (!windowsCleanupPending) finish(termination ? terminationCode() : 1);
|
|
@@ -350,12 +354,23 @@ export function executeHook(
|
|
|
350
354
|
child.once("close", (code) => {
|
|
351
355
|
if (!windowsCleanupPending) finish(termination ? terminationCode() : code ?? 1);
|
|
352
356
|
});
|
|
353
|
-
|
|
357
|
+
if (completed) return;
|
|
358
|
+
const boundedTimeoutMs = Number.isNaN(timeoutMs)
|
|
359
|
+
? HOOK_TIMEOUT_DEFAULT_MS
|
|
360
|
+
: Math.max(1, Math.min(timeoutMs, HOOK_TIMEOUT_MAX_MS));
|
|
361
|
+
timer = setTimeout(() => beginTermination("timeout"), boundedTimeoutMs);
|
|
362
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
363
|
+
if (signal?.aborted) beginTermination("cancelled");
|
|
354
364
|
});
|
|
355
365
|
}
|
|
356
366
|
|
|
357
367
|
function serializeHookPayload(payload: unknown): string {
|
|
358
|
-
|
|
368
|
+
let serialized: string;
|
|
369
|
+
try {
|
|
370
|
+
serialized = JSON.stringify(payload) ?? "null";
|
|
371
|
+
} catch {
|
|
372
|
+
return JSON.stringify({ serializationError: true });
|
|
373
|
+
}
|
|
359
374
|
if (serialized.length <= HOOK_PAYLOAD_LIMIT) return serialized;
|
|
360
375
|
const previewLength = Math.floor((HOOK_PAYLOAD_LIMIT - 64) / 2);
|
|
361
376
|
return JSON.stringify({ truncated: true, preview: serialized.slice(0, previewLength) });
|
|
@@ -376,7 +391,7 @@ function hookFailureMessage(result: HookExecutionResult): string {
|
|
|
376
391
|
|
|
377
392
|
export function registerLifecycleHooks(pi: ExtensionAPI): void {
|
|
378
393
|
let config: KillerosHookConfig = {};
|
|
379
|
-
pi.on("session_start", (_event, ctx) => { config =
|
|
394
|
+
pi.on("session_start", (_event, ctx) => { config = loadKillerosConfig(ctx); });
|
|
380
395
|
|
|
381
396
|
pi.on("tool_call", async (event, ctx) => {
|
|
382
397
|
for (const hook of config.hooks?.tool_call ?? []) {
|
package/killeros/runtime.ts
CHANGED
|
@@ -26,6 +26,7 @@ export interface GoalBlockerAudit {
|
|
|
26
26
|
key: string;
|
|
27
27
|
streak: number;
|
|
28
28
|
lastTurn: number;
|
|
29
|
+
evidence?: string;
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
export type GoalFileBaseline =
|
|
@@ -49,6 +50,7 @@ export interface GoalStateCommon {
|
|
|
49
50
|
blockedAuditStartTurn: number;
|
|
50
51
|
baselineTokens: number;
|
|
51
52
|
verification?: GoalFileVerification;
|
|
53
|
+
maxTurns?: number;
|
|
52
54
|
}
|
|
53
55
|
|
|
54
56
|
export type GoalState = GoalStateCommon & (
|