killeros 2.0.10 → 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 +11 -0
- package/Killeros.ts +2 -21
- package/README.md +3 -15
- package/killeros/activity.ts +1 -1
- package/killeros/commands.ts +3 -3
- package/killeros/display.ts +2 -2
- package/killeros/footer.ts +2 -2
- package/killeros/goals.ts +4 -4
- package/killeros/init-evidence.ts +5 -5
- package/killeros/init.ts +1 -1
- package/killeros/question.ts +7 -19
- 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 -365
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,17 @@ All notable changes to KillerOS are documented here.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [2.0.11] - 2026-08-17
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Made automated npm publication and GitHub release recovery idempotent, aligned the documented and tested Pi floor, and made machine-identifier casing locale-independent.
|
|
12
|
+
- Extended strict type checking to release scripts and typed test suites, while correctly identifying JavaScript-only suites as `.js`.
|
|
13
|
+
|
|
14
|
+
### Removed
|
|
15
|
+
|
|
16
|
+
- Removed the decision-gated workflow subsystem, its skill-specific policy, public extension interface, and YAML dependency. Skills now remain instruction-only.
|
|
17
|
+
|
|
7
18
|
## [2.0.10] - 2026-08-16
|
|
8
19
|
|
|
9
20
|
### Added
|
package/Killeros.ts
CHANGED
|
@@ -15,33 +15,19 @@ import {
|
|
|
15
15
|
type CompletionNotificationDependencies,
|
|
16
16
|
} from "./killeros/notifications.ts";
|
|
17
17
|
import { registerPersonalInstructions } from "./killeros/personal-instructions.ts";
|
|
18
|
-
import { registerQuestionTool
|
|
19
|
-
import { createDecisionGatedWorkflowAdapter } from "./killeros/decision-gated-workflow.ts";
|
|
18
|
+
import { registerQuestionTool } from "./killeros/question.ts";
|
|
20
19
|
import { createGoalRuntime, createInitRuntime } from "./killeros/runtime.ts";
|
|
21
20
|
import { registerShellUi } from "./killeros/shell-ui.ts";
|
|
22
21
|
import { registerVariants } from "./killeros/variants.ts";
|
|
23
22
|
import { registerWorkedFor } from "./killeros/worked-for.ts";
|
|
24
|
-
import { registerWorkflowGate, type WorkflowAdapter } from "./killeros/workflow-gate.ts";
|
|
25
23
|
|
|
26
24
|
export { contextPercentRemaining, formatCost, formatContextProgress } from "./killeros/footer.ts";
|
|
27
25
|
export { executeHook } from "./killeros/hooks.ts";
|
|
28
26
|
export { INIT_WORKFLOW_PROMPT } from "./killeros/init.ts";
|
|
29
27
|
export { buildInitEvidence, listInitEvidence, readInitEvidence } from "./killeros/init-evidence.ts";
|
|
30
28
|
export { captureInitTargetBaseline, installInitAgentsFile, validateGeneratedGuidance, writeInitAgentsFile } from "./killeros/init-target.ts";
|
|
31
|
-
export { createDecisionGatedWorkflowAdapter } from "./killeros/decision-gated-workflow.ts";
|
|
32
|
-
export { explicitSkillActivation, registerWorkflowGate } from "./killeros/workflow-gate.ts";
|
|
33
|
-
export type {
|
|
34
|
-
WorkflowAdapter,
|
|
35
|
-
WorkflowGateController,
|
|
36
|
-
WorkflowGateState,
|
|
37
|
-
WorkflowPolicy,
|
|
38
|
-
WorkflowTerminalReason,
|
|
39
|
-
WorkflowToolAuthorization,
|
|
40
|
-
} from "./killeros/workflow-gate.ts";
|
|
41
|
-
|
|
42
29
|
export interface KillerosOptions {
|
|
43
30
|
completionNotifications?: CompletionNotificationDependencies;
|
|
44
|
-
decisionGatedWorkflows?: readonly WorkflowAdapter[];
|
|
45
31
|
}
|
|
46
32
|
|
|
47
33
|
export default function Killeros(pi: ExtensionAPI, options: KillerosOptions = {}): void {
|
|
@@ -51,12 +37,7 @@ export default function Killeros(pi: ExtensionAPI, options: KillerosOptions = {}
|
|
|
51
37
|
registerShellUi(pi, commandResolver);
|
|
52
38
|
registerGoal(pi, goalRuntime, initRuntime);
|
|
53
39
|
registerPersonalInstructions(pi, initRuntime);
|
|
54
|
-
|
|
55
|
-
registerWorkflowGate(
|
|
56
|
-
pi,
|
|
57
|
-
questionRunner,
|
|
58
|
-
options.decisionGatedWorkflows ?? [createDecisionGatedWorkflowAdapter()],
|
|
59
|
-
);
|
|
40
|
+
registerQuestionTool(pi);
|
|
60
41
|
registerAliases(pi);
|
|
61
42
|
registerSlashAutocomplete(pi, commandResolver);
|
|
62
43
|
registerFooter(pi, goalRuntime);
|
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.11
|
|
35
35
|
```
|
|
36
36
|
|
|
37
37
|
Add `-l` to either command for a project-only install. Restart Pi after installing.
|
|
@@ -49,7 +49,6 @@ Add `-l` to either command for a project-only install. Restart Pi after installi
|
|
|
49
49
|
- `/variants` selector and direct reasoning-level arguments
|
|
50
50
|
- Codex-style `/goal` with an interactive status/action panel, durable objectives, immediate pause and clear cancellation, automatic continuation, explicit completion, and durable blocker audits
|
|
51
51
|
- Automatic `/init` guideline synthesis with a frozen safe evidence map, protected existing policy, and the four packaged behavioral sections adapted from `writing-great-guidelines`
|
|
52
|
-
- Opt-in decision-gated workflows that ask a structured policy question before explicit skill expansion, preserve the selected allowlist, and fail closed across tool and session boundaries
|
|
53
52
|
- `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
|
|
54
53
|
- Mid-prompt slash completion with current Pi `0.84.2` commands, extensions, prompts, and skills; paths, URLs, and invalid commands remain plain text
|
|
55
54
|
- Goal-aware `/clear` that confirms, aborts active work, waits for settlement, and starts a new session, plus `/exit` for graceful shutdown
|
|
@@ -79,10 +78,6 @@ Add `-l` to either command for a project-only install. Restart Pi after installi
|
|
|
79
78
|
|
|
80
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.
|
|
81
80
|
|
|
82
|
-
### Decision-gated workflows
|
|
83
|
-
|
|
84
|
-
Explicit `/skill:<name>` activation opens the shared question UI before Pi expands a registered skill. `WorkflowAdapter.activation` keeps the existing exact-match API, while `WorkflowAdapter.activations` lets one policy register multiple explicit skill names without duplicating the gate. `Normal` allows only interview and read-only tools; `With docs` additionally permits agreed glossary, context-map, and ADR paths. Unregistered names pass through unchanged, duplicate or ambiguous registrations fail at startup, and the selected policy remains active until the workflow is explicitly finished or cancelled. Extensions can supply additional adapters through `KillerosOptions.decisionGatedWorkflows`.
|
|
85
|
-
|
|
86
81
|
### Interactive questions
|
|
87
82
|
|
|
88
83
|
Single-select remains the default. Explicit `minSelections: 1` and `maxSelections: 1` are equivalent to omitting both bounds; other single-select bounds are rejected. An agent opts into multi-select with `mode: "multiple"` and may set `minSelections` and `maxSelections`; the custom answer counts as one selection.
|
|
@@ -144,16 +139,9 @@ The package manifest lists Pi’s built-in modules as peer dependencies, so npm
|
|
|
144
139
|
|
|
145
140
|
## Publish
|
|
146
141
|
|
|
147
|
-
To
|
|
148
|
-
|
|
149
|
-
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.
|
|
142
|
+
To release KillerOS, 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 publishes the package to npm through trusted publishing, then creates the matching tag and GitHub release from that verified commit. The [`pi-package`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/packages.md) keyword makes the npm release visible in Pi’s package catalog.
|
|
150
143
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
```bash
|
|
154
|
-
npm login
|
|
155
|
-
npm publish
|
|
156
|
-
```
|
|
144
|
+
Do not manually tag a normal release. If automation must recover a missing GitHub release, push the matching version tag. The workflow validates the tag against the package and changelog, skips npm publication when that version already exists, and creates only the missing release.
|
|
157
145
|
|
|
158
146
|
## Security
|
|
159
147
|
|
package/killeros/activity.ts
CHANGED
|
@@ -30,7 +30,7 @@ export function formatActivityMessage(message: ActivityMessage, theme: Theme): s
|
|
|
30
30
|
detail = "assembling the answer";
|
|
31
31
|
break;
|
|
32
32
|
case "tool":
|
|
33
|
-
switch (message.toolName.trim().
|
|
33
|
+
switch (message.toolName.trim().toLowerCase()) {
|
|
34
34
|
case "read":
|
|
35
35
|
case "grep":
|
|
36
36
|
case "find":
|
package/killeros/commands.ts
CHANGED
|
@@ -166,8 +166,8 @@ export function createSlashCommandResolver(
|
|
|
166
166
|
|
|
167
167
|
function scoreCommandMatch(name: string, prefix: string): number {
|
|
168
168
|
if (!prefix) return 1;
|
|
169
|
-
const normalizedName = name.
|
|
170
|
-
const normalizedPrefix = prefix.
|
|
169
|
+
const normalizedName = name.toLowerCase();
|
|
170
|
+
const normalizedPrefix = prefix.toLowerCase();
|
|
171
171
|
if (normalizedName.startsWith(normalizedPrefix)) return 100;
|
|
172
172
|
if (normalizedName.split(/[:\-_]/).some((token) => token.startsWith(normalizedPrefix))) return 80;
|
|
173
173
|
if (normalizedName.includes(normalizedPrefix)) return 50;
|
|
@@ -190,7 +190,7 @@ export function registerSlashAutocomplete(
|
|
|
190
190
|
const prefixMatch = getSlashCommandPrefix(beforeCursor);
|
|
191
191
|
if (!prefixMatch) return current.getSuggestions(lines, cursorLine, cursorCol, options);
|
|
192
192
|
|
|
193
|
-
const prefix = prefixMatch.prefix.
|
|
193
|
+
const prefix = prefixMatch.prefix.toLowerCase();
|
|
194
194
|
const baseSuggestions = await current.getSuggestions(lines, cursorLine, cursorCol, options);
|
|
195
195
|
resolver.updateFallbackCommands(baseSuggestions?.items ?? []);
|
|
196
196
|
const commands = resolver.getCommandCatalog(baseSuggestions?.items ?? []);
|
package/killeros/display.ts
CHANGED
|
@@ -6,8 +6,8 @@ export function formatCwd(cwd: string): string {
|
|
|
6
6
|
if (!home) return cwd;
|
|
7
7
|
const normalizedHome = home.replace(/[\\/]+$/, "");
|
|
8
8
|
const normalizedCwd = cwd.replace(/[\\/]+$/, "");
|
|
9
|
-
const comparedHome = process.platform === "win32" ? normalizedHome.
|
|
10
|
-
const comparedCwd = process.platform === "win32" ? normalizedCwd.
|
|
9
|
+
const comparedHome = process.platform === "win32" ? normalizedHome.toLowerCase() : normalizedHome;
|
|
10
|
+
const comparedCwd = process.platform === "win32" ? normalizedCwd.toLowerCase() : normalizedCwd;
|
|
11
11
|
if (comparedCwd === comparedHome) return "~";
|
|
12
12
|
const separator = normalizedCwd.slice(normalizedHome.length, normalizedHome.length + 1);
|
|
13
13
|
return comparedCwd.startsWith(comparedHome) && (separator === "/" || separator === "\\")
|
package/killeros/footer.ts
CHANGED
|
@@ -75,12 +75,12 @@ const PROVIDER_WORDS: Readonly<Record<string, string>> = {
|
|
|
75
75
|
|
|
76
76
|
function formatProviderName(provider: string): string {
|
|
77
77
|
const normalized = provider.trim();
|
|
78
|
-
const known = PROVIDER_LABELS[normalized.
|
|
78
|
+
const known = PROVIDER_LABELS[normalized.toLowerCase()];
|
|
79
79
|
if (known) return known;
|
|
80
80
|
return normalized
|
|
81
81
|
.split(/[-_]+/u)
|
|
82
82
|
.filter(Boolean)
|
|
83
|
-
.map((word) => PROVIDER_WORDS[word.
|
|
83
|
+
.map((word) => PROVIDER_WORDS[word.toLowerCase()] ?? `${word.charAt(0).toUpperCase()}${word.slice(1)}`)
|
|
84
84
|
.join(" ") || "Unknown provider";
|
|
85
85
|
}
|
|
86
86
|
|
package/killeros/goals.ts
CHANGED
|
@@ -291,7 +291,7 @@ async function stopGoalRun(runtime: GoalRuntime, ctx: ExtensionCommandContext, s
|
|
|
291
291
|
}
|
|
292
292
|
|
|
293
293
|
function goalStatusLabel(status: GoalStatus): string {
|
|
294
|
-
return `${status.charAt(0).
|
|
294
|
+
return `${status.charAt(0).toUpperCase()}${status.slice(1)}`;
|
|
295
295
|
}
|
|
296
296
|
|
|
297
297
|
function goalPanelActions(status: GoalStatus): Array<{ label: string; control: "pause" | "resume" | "edit" | "clear" }> {
|
|
@@ -309,7 +309,7 @@ function goalPanelActions(status: GoalStatus): Array<{ label: string; control: "
|
|
|
309
309
|
function goalStatusSummary(state: GoalState, ctx: ExtensionContext): string {
|
|
310
310
|
const usedTokens = Math.max(0, sumGoalTokens(ctx) - state.baselineTokens);
|
|
311
311
|
const lines = [
|
|
312
|
-
`Goal ${goalStatusLabel(state.status).
|
|
312
|
+
`Goal ${goalStatusLabel(state.status).toLowerCase()} · ${state.turns} turn${state.turns === 1 ? "" : "s"} · ${formatTime(goalElapsedMilliseconds(state))} · ${formatTokens(usedTokens)} tokens`,
|
|
313
313
|
state.objective,
|
|
314
314
|
];
|
|
315
315
|
if (state.result) lines.push(state.result);
|
|
@@ -708,7 +708,7 @@ export function registerGoal(
|
|
|
708
708
|
return;
|
|
709
709
|
}
|
|
710
710
|
const input = args.trim();
|
|
711
|
-
const control = input.
|
|
711
|
+
const control = input.toLowerCase();
|
|
712
712
|
const isControl = control === "clear" || control === "edit" || control === "pause" || control === "resume";
|
|
713
713
|
|
|
714
714
|
if (!input) {
|
|
@@ -1008,7 +1008,7 @@ export function registerGoal(
|
|
|
1008
1008
|
pi.registerCommand("goal", {
|
|
1009
1009
|
description: "Set or view the goal for a long-running task",
|
|
1010
1010
|
getArgumentCompletions: (prefix) => {
|
|
1011
|
-
const normalized = prefix.trimStart().
|
|
1011
|
+
const normalized = prefix.trimStart().toLowerCase();
|
|
1012
1012
|
if (normalized.includes(" ")) return null;
|
|
1013
1013
|
const actions = [
|
|
1014
1014
|
{ value: "clear", description: "Remove the current goal" },
|
|
@@ -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);
|
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/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
|
-
}
|
|
@@ -1,365 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
ExtensionAPI,
|
|
3
|
-
ExtensionContext,
|
|
4
|
-
ToolCallEvent,
|
|
5
|
-
} from "@earendil-works/pi-coding-agent";
|
|
6
|
-
import type { QuestionDetails, QuestionParamsValue, QuestionRunner } from "./question.ts";
|
|
7
|
-
|
|
8
|
-
export type WorkflowToolAuthorization = true | false | string;
|
|
9
|
-
|
|
10
|
-
export interface WorkflowPolicy {
|
|
11
|
-
id: string;
|
|
12
|
-
allowedTools: readonly string[];
|
|
13
|
-
authorizeTool?: (
|
|
14
|
-
toolName: string,
|
|
15
|
-
input: Readonly<Record<string, unknown>>,
|
|
16
|
-
ctx: ExtensionContext,
|
|
17
|
-
) => WorkflowToolAuthorization;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface WorkflowAdapter {
|
|
21
|
-
id: string;
|
|
22
|
-
/** The existing single explicit skill activation API. */
|
|
23
|
-
activation?: string;
|
|
24
|
-
/** Additional explicit skill activations that share this adapter policy. */
|
|
25
|
-
activations?: readonly string[];
|
|
26
|
-
question: QuestionParamsValue;
|
|
27
|
-
policies: readonly WorkflowPolicy[];
|
|
28
|
-
selectPolicy: (details: QuestionDetails) => WorkflowPolicy | undefined;
|
|
29
|
-
onActivated?: (policy: WorkflowPolicy, details: QuestionDetails) => void | Promise<void>;
|
|
30
|
-
onFinish?: () => void | Promise<void>;
|
|
31
|
-
onCancel?: (reason: string) => void | Promise<void>;
|
|
32
|
-
onFailure?: (error: unknown) => void | Promise<void>;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export type WorkflowGateState =
|
|
36
|
-
| { kind: "inactive" }
|
|
37
|
-
| { kind: "pending_decision"; adapterId: string; activation: string }
|
|
38
|
-
| { kind: "active"; adapterId: string; activation: string; policyId: string }
|
|
39
|
-
| { kind: "terminal_cleanup"; adapterId: string; activation: string; reason: WorkflowTerminalReason };
|
|
40
|
-
|
|
41
|
-
export interface WorkflowGateController {
|
|
42
|
-
getState(): WorkflowGateState;
|
|
43
|
-
finish(): Promise<boolean>;
|
|
44
|
-
cancel(reason?: string): Promise<boolean>;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export type WorkflowTerminalReason = "finish" | "cancel" | "fail" | "session-reset";
|
|
48
|
-
|
|
49
|
-
type InternalState =
|
|
50
|
-
| { kind: "inactive" }
|
|
51
|
-
| {
|
|
52
|
-
kind: "pending_decision";
|
|
53
|
-
adapter: WorkflowAdapter;
|
|
54
|
-
activation: string;
|
|
55
|
-
abortController: AbortController;
|
|
56
|
-
token: symbol;
|
|
57
|
-
}
|
|
58
|
-
| { kind: "active"; adapter: WorkflowAdapter; activation: string; policy: WorkflowPolicy; token: symbol }
|
|
59
|
-
| { kind: "terminal_cleanup"; adapter: WorkflowAdapter; activation: string; reason: WorkflowTerminalReason; token: symbol };
|
|
60
|
-
|
|
61
|
-
const ACTIVATION_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/u;
|
|
62
|
-
|
|
63
|
-
function explicitSkillActivation(text: string): string | undefined {
|
|
64
|
-
if (!text.startsWith("/skill:")) return;
|
|
65
|
-
const spaceIndex = text.indexOf(" ");
|
|
66
|
-
const activation = spaceIndex === -1 ? text.slice(7) : text.slice(7, spaceIndex);
|
|
67
|
-
return /^[A-Za-z0-9][A-Za-z0-9._-]*$/u.test(activation) ? activation : undefined;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function isCancelled(details: QuestionDetails): boolean {
|
|
71
|
-
if (details.cancelled) return true;
|
|
72
|
-
return "answer" in details && details.answer === null;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function errorMessage(error: unknown): string {
|
|
76
|
-
return error instanceof Error ? error.message : String(error);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function publicState(state: InternalState): WorkflowGateState {
|
|
80
|
-
if (state.kind === "inactive") return state;
|
|
81
|
-
if (state.kind === "pending_decision") {
|
|
82
|
-
return {
|
|
83
|
-
kind: state.kind,
|
|
84
|
-
adapterId: state.adapter.id,
|
|
85
|
-
activation: state.activation,
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
if (state.kind === "terminal_cleanup") {
|
|
89
|
-
return {
|
|
90
|
-
kind: state.kind,
|
|
91
|
-
adapterId: state.adapter.id,
|
|
92
|
-
activation: state.activation,
|
|
93
|
-
reason: state.reason,
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
return {
|
|
97
|
-
kind: state.kind,
|
|
98
|
-
adapterId: state.adapter.id,
|
|
99
|
-
activation: state.activation,
|
|
100
|
-
policyId: state.policy.id,
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function notify(ctx: ExtensionContext, message: string, type: "info" | "warning" | "error" = "error"): void {
|
|
105
|
-
ctx.ui.notify(message, type);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function isKnownPolicy(adapter: WorkflowAdapter, policy: WorkflowPolicy): boolean {
|
|
109
|
-
return adapter.policies.includes(policy);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
async function invokeCleanup(callback: (() => void | Promise<void>) | undefined): Promise<void> {
|
|
113
|
-
try {
|
|
114
|
-
await callback?.();
|
|
115
|
-
} catch {
|
|
116
|
-
// Cleanup callbacks are best effort; the gate must still reach a terminal state.
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
async function invokeCancel(
|
|
121
|
-
callback: ((reason: string) => void | Promise<void>) | undefined,
|
|
122
|
-
reason: string,
|
|
123
|
-
): Promise<void> {
|
|
124
|
-
try {
|
|
125
|
-
await callback?.(reason);
|
|
126
|
-
} catch {
|
|
127
|
-
// Cleanup callbacks are best effort; the gate must still reach a terminal state.
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
async function invokeFailure(
|
|
132
|
-
callback: ((error: unknown) => void | Promise<void>) | undefined,
|
|
133
|
-
error: unknown,
|
|
134
|
-
): Promise<void> {
|
|
135
|
-
try {
|
|
136
|
-
await callback?.(error);
|
|
137
|
-
} catch {
|
|
138
|
-
// Cleanup callbacks are best effort; the gate must still reach a terminal state.
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
export function registerWorkflowGate(
|
|
143
|
-
pi: ExtensionAPI,
|
|
144
|
-
questionRunner: QuestionRunner,
|
|
145
|
-
adapters: readonly WorkflowAdapter[],
|
|
146
|
-
): WorkflowGateController {
|
|
147
|
-
const adaptersByActivation = new Map<string, WorkflowAdapter>();
|
|
148
|
-
for (const adapter of adapters) {
|
|
149
|
-
if (!adapter.id.trim()) throw new Error("Decision-gated workflow adapters require an id");
|
|
150
|
-
if (adapter.activations !== undefined && !Array.isArray(adapter.activations)) {
|
|
151
|
-
throw new Error(`Workflow adapter ${adapter.id} activations must be an array`);
|
|
152
|
-
}
|
|
153
|
-
const activations = [
|
|
154
|
-
...(adapter.activation === undefined ? [] : [adapter.activation]),
|
|
155
|
-
...(adapter.activations ?? []),
|
|
156
|
-
];
|
|
157
|
-
if (activations.length === 0) {
|
|
158
|
-
throw new Error(`Workflow adapter ${adapter.id} must register at least one explicit skill activation`);
|
|
159
|
-
}
|
|
160
|
-
for (const activation of activations) {
|
|
161
|
-
if (typeof activation !== "string" || !ACTIVATION_PATTERN.test(activation)) {
|
|
162
|
-
throw new Error(`Invalid decision-gated workflow activation: ${activation}`);
|
|
163
|
-
}
|
|
164
|
-
if (adaptersByActivation.has(activation)) {
|
|
165
|
-
throw new Error(`Duplicate decision-gated workflow activation: ${activation}`);
|
|
166
|
-
}
|
|
167
|
-
adaptersByActivation.set(activation, adapter);
|
|
168
|
-
}
|
|
169
|
-
if (adapter.policies.length === 0) throw new Error(`Workflow adapter ${adapter.id} has no policies`);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
let state: InternalState = { kind: "inactive" };
|
|
173
|
-
|
|
174
|
-
const transitionToCleanup = (
|
|
175
|
-
current: Exclude<InternalState, { kind: "inactive" } | { kind: "terminal_cleanup" }>,
|
|
176
|
-
reason: WorkflowTerminalReason,
|
|
177
|
-
): symbol => {
|
|
178
|
-
state = {
|
|
179
|
-
kind: "terminal_cleanup",
|
|
180
|
-
adapter: current.adapter,
|
|
181
|
-
activation: current.activation,
|
|
182
|
-
reason,
|
|
183
|
-
token: current.token,
|
|
184
|
-
};
|
|
185
|
-
if (current.kind === "pending_decision") current.abortController.abort();
|
|
186
|
-
return current.token;
|
|
187
|
-
};
|
|
188
|
-
|
|
189
|
-
const finishCleanup = (token: symbol): void => {
|
|
190
|
-
if (state.kind === "terminal_cleanup" && state.token === token) state = { kind: "inactive" };
|
|
191
|
-
};
|
|
192
|
-
|
|
193
|
-
const finish = async (): Promise<boolean> => {
|
|
194
|
-
if (state.kind !== "active") return false;
|
|
195
|
-
const active = state;
|
|
196
|
-
const token = transitionToCleanup(active, "finish");
|
|
197
|
-
await invokeCleanup(active.adapter.onFinish);
|
|
198
|
-
finishCleanup(token);
|
|
199
|
-
return true;
|
|
200
|
-
};
|
|
201
|
-
|
|
202
|
-
const cancel = async (reason = "Workflow cancelled"): Promise<boolean> => {
|
|
203
|
-
if (state.kind === "inactive" || state.kind === "terminal_cleanup") return false;
|
|
204
|
-
const current = state;
|
|
205
|
-
const token = transitionToCleanup(current, "cancel");
|
|
206
|
-
await invokeCancel(current.adapter.onCancel, reason);
|
|
207
|
-
finishCleanup(token);
|
|
208
|
-
return true;
|
|
209
|
-
};
|
|
210
|
-
|
|
211
|
-
const fail = async (
|
|
212
|
-
adapter: WorkflowAdapter,
|
|
213
|
-
token: symbol,
|
|
214
|
-
error: unknown,
|
|
215
|
-
ctx: ExtensionContext,
|
|
216
|
-
): Promise<void> => {
|
|
217
|
-
if (state.kind === "inactive" || state.kind === "terminal_cleanup") return;
|
|
218
|
-
if (state.adapter !== adapter || state.token !== token) return;
|
|
219
|
-
const cleanupToken = transitionToCleanup(state, "fail");
|
|
220
|
-
await invokeFailure(adapter.onFailure, error);
|
|
221
|
-
finishCleanup(cleanupToken);
|
|
222
|
-
notify(ctx, `Decision-gated workflow was not activated: ${errorMessage(error)}`);
|
|
223
|
-
};
|
|
224
|
-
|
|
225
|
-
pi.on("input", async (event, ctx) => {
|
|
226
|
-
if ((state.kind === "pending_decision" || state.kind === "terminal_cleanup") && event.text.startsWith("/skill:")) {
|
|
227
|
-
notify(ctx, "A decision-gated workflow is waiting for its structured question; skill routing is blocked", "warning");
|
|
228
|
-
return { action: "handled" };
|
|
229
|
-
}
|
|
230
|
-
const activation = explicitSkillActivation(event.text);
|
|
231
|
-
if (!activation) return;
|
|
232
|
-
|
|
233
|
-
const adapter = adaptersByActivation.get(activation);
|
|
234
|
-
if (!adapter) return;
|
|
235
|
-
|
|
236
|
-
if (state.kind === "active") {
|
|
237
|
-
notify(ctx, `Cannot start /skill:${activation} while a decision-gated workflow is active`, "warning");
|
|
238
|
-
return { action: "handled" };
|
|
239
|
-
}
|
|
240
|
-
if (ctx.mode !== "tui" || !ctx.hasUI) {
|
|
241
|
-
notify(ctx, `The /skill:${activation} workflow requires interactive TUI mode`);
|
|
242
|
-
return { action: "handled" };
|
|
243
|
-
}
|
|
244
|
-
if (!ctx.isIdle() || ctx.hasPendingMessages()) {
|
|
245
|
-
notify(ctx, `The /skill:${activation} workflow can start only when Pi is idle`, "warning");
|
|
246
|
-
return { action: "handled" };
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
const abortController = new AbortController();
|
|
250
|
-
const token = Symbol();
|
|
251
|
-
state = { kind: "pending_decision", adapter, activation, abortController, token };
|
|
252
|
-
let details: QuestionDetails;
|
|
253
|
-
try {
|
|
254
|
-
details = await questionRunner.ask(adapter.question, abortController.signal, ctx);
|
|
255
|
-
} catch (error) {
|
|
256
|
-
if (state.kind !== "pending_decision" || state.adapter !== adapter || state.token !== token) {
|
|
257
|
-
return { action: "handled" };
|
|
258
|
-
}
|
|
259
|
-
await fail(adapter, token, error, ctx);
|
|
260
|
-
return { action: "handled" };
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
if (state.kind !== "pending_decision" || state.adapter !== adapter || state.token !== token) {
|
|
264
|
-
return { action: "handled" };
|
|
265
|
-
}
|
|
266
|
-
if (isCancelled(details)) {
|
|
267
|
-
await cancel("Decision question cancelled");
|
|
268
|
-
notify(ctx, `The /skill:${activation} workflow was cancelled`, "warning");
|
|
269
|
-
return { action: "handled" };
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
let policy: WorkflowPolicy | undefined;
|
|
273
|
-
try {
|
|
274
|
-
policy = adapter.selectPolicy(details);
|
|
275
|
-
} catch (error) {
|
|
276
|
-
await fail(adapter, token, error, ctx);
|
|
277
|
-
return { action: "handled" };
|
|
278
|
-
}
|
|
279
|
-
if (!policy || !isKnownPolicy(adapter, policy)) {
|
|
280
|
-
await fail(adapter, token, new Error("The structured answer did not select a registered policy"), ctx);
|
|
281
|
-
return { action: "handled" };
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
try {
|
|
285
|
-
await adapter.onActivated?.(policy, details);
|
|
286
|
-
} catch (error) {
|
|
287
|
-
if (state.kind !== "pending_decision" || state.adapter !== adapter || state.token !== token) {
|
|
288
|
-
return { action: "handled" };
|
|
289
|
-
}
|
|
290
|
-
await fail(adapter, token, error, ctx);
|
|
291
|
-
return { action: "handled" };
|
|
292
|
-
}
|
|
293
|
-
if (state.kind !== "pending_decision" || state.adapter !== adapter || state.token !== token) {
|
|
294
|
-
return { action: "handled" };
|
|
295
|
-
}
|
|
296
|
-
state = { kind: "active", adapter, activation, policy, token };
|
|
297
|
-
return { action: "continue" };
|
|
298
|
-
});
|
|
299
|
-
|
|
300
|
-
pi.on("tool_call", (event: ToolCallEvent, ctx) => {
|
|
301
|
-
if (state.kind === "inactive") return;
|
|
302
|
-
if (state.kind === "pending_decision" || state.kind === "terminal_cleanup") {
|
|
303
|
-
return {
|
|
304
|
-
block: true,
|
|
305
|
-
reason: "A decision-gated workflow is waiting for its structured question; no model tool calls are allowed yet",
|
|
306
|
-
};
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
const { policy } = state;
|
|
310
|
-
if (!policy.allowedTools.includes(event.toolName)) {
|
|
311
|
-
return {
|
|
312
|
-
block: true,
|
|
313
|
-
reason: `Tool ${event.toolName} is not allowed by decision-gated policy ${policy.id}`,
|
|
314
|
-
};
|
|
315
|
-
}
|
|
316
|
-
const authorization = policy.authorizeTool?.(event.toolName, event.input, ctx) ?? true;
|
|
317
|
-
if (authorization === true) return;
|
|
318
|
-
return {
|
|
319
|
-
block: true,
|
|
320
|
-
reason: typeof authorization === "string"
|
|
321
|
-
? authorization
|
|
322
|
-
: `Tool ${event.toolName} is denied by decision-gated policy ${policy.id}`,
|
|
323
|
-
};
|
|
324
|
-
});
|
|
325
|
-
|
|
326
|
-
const resetForLifecycle = async (reason: string): Promise<void> => {
|
|
327
|
-
if (state.kind === "inactive" || state.kind === "terminal_cleanup") return;
|
|
328
|
-
const current = state;
|
|
329
|
-
const token = transitionToCleanup(current, "session-reset");
|
|
330
|
-
await invokeCancel(current.adapter.onCancel, reason);
|
|
331
|
-
finishCleanup(token);
|
|
332
|
-
};
|
|
333
|
-
|
|
334
|
-
pi.on("session_start", () => resetForLifecycle("Session lifecycle reset"));
|
|
335
|
-
pi.on("session_shutdown", () => resetForLifecycle("Session lifecycle reset"));
|
|
336
|
-
pi.on("session_tree", () => resetForLifecycle("Session tree reset"));
|
|
337
|
-
pi.on("session_before_switch", () => resetForLifecycle("Session switch reset"));
|
|
338
|
-
pi.on("session_before_fork", () => resetForLifecycle("Session fork reset"));
|
|
339
|
-
pi.on("session_before_tree", () => resetForLifecycle("Session tree reset"));
|
|
340
|
-
|
|
341
|
-
pi.on("agent_end", async (event, ctx) => {
|
|
342
|
-
if (state.kind !== "active") return;
|
|
343
|
-
|
|
344
|
-
let failureMessage: string | undefined;
|
|
345
|
-
for (let index = event.messages.length - 1; index >= 0; index -= 1) {
|
|
346
|
-
const message = event.messages[index];
|
|
347
|
-
if (message?.role === "assistant" && message.errorMessage) {
|
|
348
|
-
failureMessage = message.errorMessage;
|
|
349
|
-
break;
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
if (failureMessage === undefined) return;
|
|
353
|
-
|
|
354
|
-
const active = state;
|
|
355
|
-
await fail(active.adapter, active.token, new Error(failureMessage), ctx);
|
|
356
|
-
});
|
|
357
|
-
|
|
358
|
-
return {
|
|
359
|
-
getState: () => publicState(state),
|
|
360
|
-
finish,
|
|
361
|
-
cancel,
|
|
362
|
-
};
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
export { explicitSkillActivation };
|