killeros 2.0.9 → 2.0.11
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 +18 -0
- package/Killeros.ts +5 -22
- package/README.md +21 -20
- package/killeros/activity.ts +1 -1
- package/killeros/auto-compaction.ts +224 -0
- package/killeros/commands.ts +3 -3
- package/killeros/display.ts +2 -2
- package/killeros/footer.ts +2 -2
- package/killeros/goals.ts +79 -6
- package/killeros/init-evidence.ts +5 -5
- package/killeros/init.ts +1 -1
- package/killeros/notifications.ts +6 -36
- package/killeros/question.ts +7 -19
- package/killeros/runtime.ts +2 -0
- package/killeros/settings.ts +47 -0
- package/killeros/variants.ts +2 -2
- package/package.json +5 -5
- package/killeros/decision-gated-workflow.ts +0 -76
- package/killeros/workflow-gate.ts +0 -347
|
@@ -35,11 +35,11 @@ export interface InitEvidenceBuildResult {
|
|
|
35
35
|
|
|
36
36
|
function evidenceKey(relativePath: string): string {
|
|
37
37
|
const normalized = relativePath.replaceAll("\\", "/");
|
|
38
|
-
return process.platform === "win32" ? normalized.
|
|
38
|
+
return process.platform === "win32" ? normalized.toLowerCase() : normalized;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
function sensitiveEvidencePath(relativePath: string): boolean {
|
|
42
|
-
const normalized = relativePath.replaceAll("\\", "/").
|
|
42
|
+
const normalized = relativePath.replaceAll("\\", "/").toLowerCase();
|
|
43
43
|
const name = path.posix.basename(normalized);
|
|
44
44
|
return /^\.env(?:\.|$)/u.test(name)
|
|
45
45
|
|| [".npmrc", ".pypirc", ".netrc", "id_rsa", "id_ed25519", "credentials.json"].includes(name)
|
|
@@ -50,8 +50,8 @@ function sensitiveEvidencePath(relativePath: string): boolean {
|
|
|
50
50
|
function excludedPath(relativePath: string): boolean {
|
|
51
51
|
const segments = relativePath.replaceAll("\\", "/").split("/");
|
|
52
52
|
return segments.some((segment, index) =>
|
|
53
|
-
(index < segments.length - 1 && EXCLUDED_DIRS.has(segment.
|
|
54
|
-
|| EXCLUDED_GUIDANCE.has(segment.
|
|
53
|
+
(index < segments.length - 1 && EXCLUDED_DIRS.has(segment.toLowerCase()))
|
|
54
|
+
|| EXCLUDED_GUIDANCE.has(segment.toLowerCase()))
|
|
55
55
|
|| sensitiveEvidencePath(relativePath);
|
|
56
56
|
}
|
|
57
57
|
|
|
@@ -74,7 +74,7 @@ async function collectCandidates(projectRoot: string): Promise<string[]> {
|
|
|
74
74
|
if (files.length >= PATH_LIMIT) break;
|
|
75
75
|
const relativePath = path.posix.join(current.relativePath.replaceAll("\\", "/"), entry.name);
|
|
76
76
|
if (entry.isDirectory()) {
|
|
77
|
-
if (current.depth < DEPTH_LIMIT && !EXCLUDED_DIRS.has(entry.name.
|
|
77
|
+
if (current.depth < DEPTH_LIMIT && !EXCLUDED_DIRS.has(entry.name.toLowerCase())) {
|
|
78
78
|
queue.push({ relativePath, depth: current.depth + 1 });
|
|
79
79
|
}
|
|
80
80
|
} else if (entry.isFile() && !excludedPath(relativePath)) {
|
package/killeros/init.ts
CHANGED
|
@@ -76,7 +76,7 @@ export function registerInitCommand(pi: ExtensionAPI, initState: InitRuntime, go
|
|
|
76
76
|
if (!initState.active || !initState.evidence || !initState.targetPath || !initState.projectRoot) {
|
|
77
77
|
throw new Error("killeros_init_read is available only during /init");
|
|
78
78
|
}
|
|
79
|
-
const generatedTarget = initState.outcome.kind === "written" && requestedPath.replaceAll("\\", "/").
|
|
79
|
+
const generatedTarget = initState.outcome.kind === "written" && requestedPath.replaceAll("\\", "/").toLowerCase() === "agents.md";
|
|
80
80
|
const text = generatedTarget
|
|
81
81
|
? await readGeneratedInitTarget(initState.projectRoot, initState.targetPath)
|
|
82
82
|
: await readInitEvidence(initState.evidence, requestedPath);
|
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { basename, dirname, join } from "node:path";
|
|
1
|
+
import { basename } from "node:path";
|
|
4
2
|
import type { StopReason } from "@earendil-works/pi-ai";
|
|
5
3
|
import {
|
|
6
|
-
getAgentDir,
|
|
7
4
|
type ExtensionAPI,
|
|
8
5
|
type ExtensionContext,
|
|
9
6
|
} from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import { createKillerosSettingsStore } from "./settings.ts";
|
|
10
8
|
|
|
11
9
|
export interface NotificationPreferenceStore {
|
|
12
10
|
load(): boolean;
|
|
@@ -20,42 +18,14 @@ export interface CompletionNotificationDependencies {
|
|
|
20
18
|
|
|
21
19
|
export const COMPLETION_BELL_GLYPH = "";
|
|
22
20
|
|
|
23
|
-
const defaultSettingsPath = (): string => join(getAgentDir(), "killeros.json");
|
|
24
|
-
|
|
25
|
-
type StoredSettings = Record<string, unknown>;
|
|
26
|
-
|
|
27
|
-
function readStoredSettings(settingsPath: string): StoredSettings {
|
|
28
|
-
try {
|
|
29
|
-
const parsed: unknown = JSON.parse(readFileSync(settingsPath, "utf8"));
|
|
30
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
31
|
-
throw new Error("KillerOS settings must contain a JSON object");
|
|
32
|
-
}
|
|
33
|
-
return parsed as StoredSettings;
|
|
34
|
-
} catch (error) {
|
|
35
|
-
if ((error as NodeJS.ErrnoException).code === "ENOENT") return {};
|
|
36
|
-
throw error;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
21
|
export function createNotificationPreferenceStore(
|
|
41
|
-
settingsPath
|
|
22
|
+
settingsPath?: string,
|
|
42
23
|
): NotificationPreferenceStore {
|
|
24
|
+
const settings = createKillerosSettingsStore(settingsPath);
|
|
43
25
|
return {
|
|
44
|
-
load: () =>
|
|
26
|
+
load: () => settings.load().completionSound === true,
|
|
45
27
|
save: (enabled) => {
|
|
46
|
-
|
|
47
|
-
mkdirSync(dirname(settingsPath), { recursive: true });
|
|
48
|
-
const temporaryPath = `${settingsPath}.${process.pid}.${randomUUID()}.tmp`;
|
|
49
|
-
try {
|
|
50
|
-
writeFileSync(
|
|
51
|
-
temporaryPath,
|
|
52
|
-
`${JSON.stringify({ ...current, completionSound: enabled }, null, 2)}\n`,
|
|
53
|
-
{ encoding: "utf8", mode: 0o600 },
|
|
54
|
-
);
|
|
55
|
-
renameSync(temporaryPath, settingsPath);
|
|
56
|
-
} finally {
|
|
57
|
-
rmSync(temporaryPath, { force: true });
|
|
58
|
-
}
|
|
28
|
+
settings.update({ completionSound: enabled });
|
|
59
29
|
},
|
|
60
30
|
};
|
|
61
31
|
}
|
package/killeros/question.ts
CHANGED
|
@@ -45,7 +45,7 @@ const QuestionParams = Type.Object({
|
|
|
45
45
|
})),
|
|
46
46
|
});
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
type QuestionParamsValue = Static<typeof QuestionParams>;
|
|
49
49
|
|
|
50
50
|
type NormalizedQuestionSelection =
|
|
51
51
|
| { mode: "single"; minSelections: 1; maxSelections: 1 }
|
|
@@ -100,11 +100,7 @@ interface MultipleQuestionDetails {
|
|
|
100
100
|
cancelled?: boolean;
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
export interface QuestionRunner {
|
|
106
|
-
ask(params: QuestionParamsValue, signal: AbortSignal | undefined, ctx: ExtensionContext): Promise<QuestionDetails>;
|
|
107
|
-
}
|
|
103
|
+
type QuestionDetails = SingleQuestionDetails | MultipleQuestionDetails;
|
|
108
104
|
|
|
109
105
|
type QuestionSelection =
|
|
110
106
|
| { kind: "selected"; answer: string; originalIndex: number }
|
|
@@ -219,7 +215,7 @@ class MultipleResultText {
|
|
|
219
215
|
invalidate(): void {}
|
|
220
216
|
}
|
|
221
217
|
|
|
222
|
-
export function registerQuestionTool(pi: ExtensionAPI):
|
|
218
|
+
export function registerQuestionTool(pi: ExtensionAPI): void {
|
|
223
219
|
const customInputHistory: string[] = [];
|
|
224
220
|
let customInputHistoryBytes = 0;
|
|
225
221
|
const clearCustomInputHistory = (): void => {
|
|
@@ -303,7 +299,7 @@ export function registerQuestionTool(pi: ExtensionAPI): QuestionRunner {
|
|
|
303
299
|
const keyText = keybindings.getKeys(keybinding)
|
|
304
300
|
.join("/")
|
|
305
301
|
.split("/")
|
|
306
|
-
.map((key) => key.split("+").map((part) => process.platform === "darwin" && part.
|
|
302
|
+
.map((key) => key.split("+").map((part) => process.platform === "darwin" && part.toLowerCase() === "alt" ? "option" : part).join("+"))
|
|
307
303
|
.join("/");
|
|
308
304
|
return theme.fg("dim", keyText) + theme.fg("muted", ` ${description}`);
|
|
309
305
|
};
|
|
@@ -322,11 +318,11 @@ export function registerQuestionTool(pi: ExtensionAPI): QuestionRunner {
|
|
|
322
318
|
customInputHistory.forEach((value) => editor.addToHistory(value));
|
|
323
319
|
|
|
324
320
|
const filteredOptions = (): DisplayOption[] => {
|
|
325
|
-
const query = filterQuery.trim().
|
|
321
|
+
const query = filterQuery.trim().toLowerCase();
|
|
326
322
|
return options.filter((option) => option.isOther
|
|
327
323
|
|| query.length === 0
|
|
328
|
-
|| option.label.
|
|
329
|
-
|| option.description?.
|
|
324
|
+
|| option.label.toLowerCase().includes(query)
|
|
325
|
+
|| option.description?.toLowerCase().includes(query));
|
|
330
326
|
};
|
|
331
327
|
const selectedCount = (): number => selectedOriginalIndices.size + (customAnswer === undefined ? 0 : 1);
|
|
332
328
|
const orderedMultipleSelection = () => {
|
|
@@ -763,12 +759,4 @@ export function registerQuestionTool(pi: ExtensionAPI): QuestionRunner {
|
|
|
763
759
|
},
|
|
764
760
|
};
|
|
765
761
|
pi.registerTool(questionTool);
|
|
766
|
-
|
|
767
|
-
return {
|
|
768
|
-
async ask(params, signal, ctx): Promise<QuestionDetails> {
|
|
769
|
-
const result = await questionTool.execute("killeros-workflow-gate", params, signal, undefined, ctx);
|
|
770
|
-
if (!result.details) throw new Error("Question did not return a structured result");
|
|
771
|
-
return result.details;
|
|
772
|
-
},
|
|
773
|
-
};
|
|
774
762
|
}
|
package/killeros/runtime.ts
CHANGED
|
@@ -57,6 +57,7 @@ export interface GoalRuntime {
|
|
|
57
57
|
continuationHeld: boolean;
|
|
58
58
|
goalTurnInFlight: boolean;
|
|
59
59
|
agentEndObserved: boolean;
|
|
60
|
+
automaticCompaction?: "pending" | "completed";
|
|
60
61
|
persistenceRetryNeeded: boolean;
|
|
61
62
|
lastStopReason?: string;
|
|
62
63
|
lastError?: string;
|
|
@@ -73,6 +74,7 @@ export function createGoalRuntime(): GoalRuntime {
|
|
|
73
74
|
continuationHeld: false,
|
|
74
75
|
goalTurnInFlight: false,
|
|
75
76
|
agentEndObserved: false,
|
|
77
|
+
automaticCompaction: undefined,
|
|
76
78
|
persistenceRetryNeeded: false,
|
|
77
79
|
};
|
|
78
80
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
|
|
6
|
+
export type KillerosSettings = Record<string, unknown>;
|
|
7
|
+
|
|
8
|
+
export interface KillerosSettingsStore {
|
|
9
|
+
load(): KillerosSettings;
|
|
10
|
+
update(patch: Readonly<Record<string, unknown>>): void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function readStoredSettings(settingsPath: string): KillerosSettings {
|
|
14
|
+
try {
|
|
15
|
+
const parsed: unknown = JSON.parse(readFileSync(settingsPath, "utf8"));
|
|
16
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
17
|
+
throw new Error("KillerOS settings must contain a JSON object");
|
|
18
|
+
}
|
|
19
|
+
return parsed as KillerosSettings;
|
|
20
|
+
} catch (error) {
|
|
21
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return {};
|
|
22
|
+
throw error;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function createKillerosSettingsStore(
|
|
27
|
+
settingsPath = join(getAgentDir(), "killeros.json"),
|
|
28
|
+
): KillerosSettingsStore {
|
|
29
|
+
return {
|
|
30
|
+
load: () => readStoredSettings(settingsPath),
|
|
31
|
+
update: (patch) => {
|
|
32
|
+
const current = readStoredSettings(settingsPath);
|
|
33
|
+
mkdirSync(dirname(settingsPath), { recursive: true });
|
|
34
|
+
const temporaryPath = `${settingsPath}.${process.pid}.${randomUUID()}.tmp`;
|
|
35
|
+
try {
|
|
36
|
+
writeFileSync(
|
|
37
|
+
temporaryPath,
|
|
38
|
+
`${JSON.stringify({ ...current, ...patch }, null, 2)}\n`,
|
|
39
|
+
{ encoding: "utf8", mode: 0o600 },
|
|
40
|
+
);
|
|
41
|
+
renameSync(temporaryPath, settingsPath);
|
|
42
|
+
} finally {
|
|
43
|
+
rmSync(temporaryPath, { force: true });
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
package/killeros/variants.ts
CHANGED
|
@@ -46,7 +46,7 @@ function isThinkingLevel(value: string): value is ThinkingLevel {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
function resolveThinkingLevel(input: string): ThinkingLevel | undefined {
|
|
49
|
-
const normalized = input.trim().
|
|
49
|
+
const normalized = input.trim().toLowerCase();
|
|
50
50
|
return isThinkingLevel(normalized) ? normalized : LEVEL_ALIASES[normalized];
|
|
51
51
|
}
|
|
52
52
|
|
|
@@ -144,7 +144,7 @@ export function registerVariants(pi: ExtensionAPI): void {
|
|
|
144
144
|
const keyText = keybindings.getKeys(keybinding)
|
|
145
145
|
.join("/")
|
|
146
146
|
.split("/")
|
|
147
|
-
.map((key) => key.split("+").map((part) => process.platform === "darwin" && part.
|
|
147
|
+
.map((key) => key.split("+").map((part) => process.platform === "darwin" && part.toLowerCase() === "alt" ? "option" : part).join("+"))
|
|
148
148
|
.join("/");
|
|
149
149
|
return theme.fg("dim", keyText) + theme.fg("muted", ` ${description}`);
|
|
150
150
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "killeros",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.11",
|
|
4
4
|
"description": "TUI, goals, and workflow automation for the Pi coding agent",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
},
|
|
32
32
|
"scripts": {
|
|
33
33
|
"check": "tsc --noEmit",
|
|
34
|
-
"test": "node --test --experimental-strip-types test/*.test
|
|
34
|
+
"test": "node --test --experimental-strip-types test/*.test.*"
|
|
35
35
|
},
|
|
36
36
|
"pi": {
|
|
37
37
|
"extensions": [
|
|
@@ -42,9 +42,9 @@
|
|
|
42
42
|
]
|
|
43
43
|
},
|
|
44
44
|
"peerDependencies": {
|
|
45
|
-
"@earendil-works/pi-ai": ">=0.84.
|
|
46
|
-
"@earendil-works/pi-coding-agent": ">=0.84.
|
|
47
|
-
"@earendil-works/pi-tui": ">=0.84.
|
|
45
|
+
"@earendil-works/pi-ai": ">=0.84.2",
|
|
46
|
+
"@earendil-works/pi-coding-agent": ">=0.84.2",
|
|
47
|
+
"@earendil-works/pi-tui": ">=0.84.2",
|
|
48
48
|
"typebox": ">=1.1.38 <2"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
|
-
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
|
-
import type { QuestionDetails } from "./question.ts";
|
|
4
|
-
import type { WorkflowAdapter, WorkflowPolicy, WorkflowToolAuthorization } from "./workflow-gate.ts";
|
|
5
|
-
|
|
6
|
-
const READ_ONLY_TOOLS = ["read", "grep", "find", "ls", "question"] as const;
|
|
7
|
-
|
|
8
|
-
const DOCUMENTATION_PATHS: readonly RegExp[] = [
|
|
9
|
-
/^(?:docs\/)?(?:glossary|context-map)(?:\.md|\/|$)/u,
|
|
10
|
-
/^docs\/adr\/[^/]+\.md$/u,
|
|
11
|
-
];
|
|
12
|
-
|
|
13
|
-
function relativePath(input: Readonly<Record<string, unknown>>, ctx: ExtensionContext): string | undefined {
|
|
14
|
-
if (typeof input.path !== "string" || input.path.trim().length === 0) return;
|
|
15
|
-
const absolute = path.resolve(ctx.cwd, input.path);
|
|
16
|
-
const relative = path.relative(ctx.cwd, absolute);
|
|
17
|
-
if (!relative || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) return;
|
|
18
|
-
return relative.replaceAll(path.sep, "/").toLocaleLowerCase();
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function authorizeDocumentationTool(
|
|
22
|
-
toolName: string,
|
|
23
|
-
input: Readonly<Record<string, unknown>>,
|
|
24
|
-
ctx: ExtensionContext,
|
|
25
|
-
): WorkflowToolAuthorization {
|
|
26
|
-
if (toolName !== "edit" && toolName !== "write") return true;
|
|
27
|
-
const target = relativePath(input, ctx);
|
|
28
|
-
if (target && DOCUMENTATION_PATHS.some((pattern) => pattern.test(target))) return true;
|
|
29
|
-
return "With docs policy permits writes only to the agreed glossary, context-map, and ADR paths";
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const NORMAL_POLICY: WorkflowPolicy = {
|
|
33
|
-
id: "normal",
|
|
34
|
-
allowedTools: READ_ONLY_TOOLS,
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
const WITH_DOCS_POLICY: WorkflowPolicy = {
|
|
38
|
-
id: "with-docs",
|
|
39
|
-
allowedTools: [...READ_ONLY_TOOLS, "edit", "write"],
|
|
40
|
-
authorizeTool: authorizeDocumentationTool,
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
const POLICIES = [NORMAL_POLICY, WITH_DOCS_POLICY] as const;
|
|
44
|
-
|
|
45
|
-
function selectedAnswer(details: QuestionDetails): string | undefined {
|
|
46
|
-
if (!("answer" in details) || details.answer === null) return;
|
|
47
|
-
return details.answer;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export function createDecisionGatedWorkflowAdapter(): WorkflowAdapter {
|
|
51
|
-
return {
|
|
52
|
-
id: "decision-gated-workflow",
|
|
53
|
-
activation: "decision-gated-workflow",
|
|
54
|
-
question: {
|
|
55
|
-
question: "Choose the policy for this workflow before the model starts",
|
|
56
|
-
options: [
|
|
57
|
-
{
|
|
58
|
-
label: "Normal",
|
|
59
|
-
description: "Interview and read-only work; implementation files stay protected.",
|
|
60
|
-
},
|
|
61
|
-
{
|
|
62
|
-
label: "With docs",
|
|
63
|
-
description: "Allow only agreed glossary, context-map, or ADR documentation writes.",
|
|
64
|
-
},
|
|
65
|
-
],
|
|
66
|
-
},
|
|
67
|
-
policies: POLICIES,
|
|
68
|
-
selectPolicy(details) {
|
|
69
|
-
switch (selectedAnswer(details)) {
|
|
70
|
-
case "Normal": return NORMAL_POLICY;
|
|
71
|
-
case "With docs": return WITH_DOCS_POLICY;
|
|
72
|
-
default: return undefined;
|
|
73
|
-
}
|
|
74
|
-
},
|
|
75
|
-
};
|
|
76
|
-
}
|