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.
@@ -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.toLocaleLowerCase() : 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("\\", "/").toLocaleLowerCase();
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.toLocaleLowerCase()))
54
- || EXCLUDED_GUIDANCE.has(segment.toLocaleLowerCase()))
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.toLocaleLowerCase())) {
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("\\", "/").toLocaleLowerCase() === "agents.md";
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 { randomUUID } from "node:crypto";
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 = defaultSettingsPath(),
22
+ settingsPath?: string,
42
23
  ): NotificationPreferenceStore {
24
+ const settings = createKillerosSettingsStore(settingsPath);
43
25
  return {
44
- load: () => readStoredSettings(settingsPath).completionSound === true,
26
+ load: () => settings.load().completionSound === true,
45
27
  save: (enabled) => {
46
- const current = readStoredSettings(settingsPath);
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
  }
@@ -45,7 +45,7 @@ const QuestionParams = Type.Object({
45
45
  })),
46
46
  });
47
47
 
48
- export type QuestionParamsValue = Static<typeof QuestionParams>;
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
- export type QuestionDetails = SingleQuestionDetails | MultipleQuestionDetails;
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): QuestionRunner {
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.toLocaleLowerCase() === "alt" ? "option" : part).join("+"))
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().toLocaleLowerCase();
321
+ const query = filterQuery.trim().toLowerCase();
326
322
  return options.filter((option) => option.isOther
327
323
  || query.length === 0
328
- || option.label.toLocaleLowerCase().includes(query)
329
- || option.description?.toLocaleLowerCase().includes(query));
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
  }
@@ -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
+ }
@@ -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().toLocaleLowerCase();
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.toLocaleLowerCase() === "alt" ? "option" : part).join("+"))
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.9",
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.ts"
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.1",
46
- "@earendil-works/pi-coding-agent": ">=0.84.1",
47
- "@earendil-works/pi-tui": ">=0.84.1",
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
- }