killeros 2.0.6 → 2.0.8
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 +38 -0
- package/Killeros.ts +22 -5
- package/README.md +21 -16
- package/killeros/activity.ts +97 -0
- package/killeros/decision-gated-workflow.ts +76 -0
- package/killeros/footer.ts +31 -45
- package/killeros/goals.ts +85 -17
- package/killeros/init.ts +23 -2
- package/killeros/question.ts +56 -26
- package/killeros/runtime.ts +9 -0
- package/killeros/shell-ui.ts +14 -68
- package/killeros/variants.ts +74 -32
- package/killeros/worked-for.ts +69 -14
- package/killeros/workflow-gate.ts +347 -0
- package/package.json +7 -6
- package/killeros/concise.ts +0 -69
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,44 @@ All notable changes to KillerOS are documented here.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [2.0.8] - 2026-08-15
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Added a reusable, opt-in pre-turn gate for explicitly activated decision-gated workflows. It opens the shared structured question UI before skill expansion, keeps the selected policy active, blocks unknown and disallowed tools, and clears safely across lifecycle boundaries.
|
|
12
|
+
- Added a disposable decision-gated workflow fixture and focused coverage for activation ordering, pending safety, policy allowlists, lifecycle cleanup, adapter reuse, and Pi 0.84.2 compatibility.
|
|
13
|
+
|
|
14
|
+
### Removed
|
|
15
|
+
|
|
16
|
+
- Removed KillerOS's always-on concise response policy and provider-specific concise defaults; Pi now owns response-style guidance.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- Raised the locked direct Pi development packages to 0.84.2 while keeping the peer dependency floor at 0.84.1.
|
|
21
|
+
|
|
22
|
+
## [2.0.7] - 2026-08-13
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
|
|
26
|
+
- Raised the supported and locked Pi baseline from 0.82.1 to the current matched 0.84.1 AI, coding-agent, and TUI packages.
|
|
27
|
+
- Added a compact two-deck footer beneath the prompt and removed the work-trail widget.
|
|
28
|
+
- Replaced the framed prompt and shuffled activity verbs with a focus-aware single-arrow editor and event-derived working copy.
|
|
29
|
+
- Made settled timing entries report `Done`, `Stopped`, or `Failed` while keeping existing `Worked for` history readable.
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
- Separated response output from the prompt editor while keeping footer telemetry close beneath a quiet muted divider.
|
|
34
|
+
- Kept `/variants` within the active terminal height, preserved the focused reasoning level across resizes, and initially focused the current level.
|
|
35
|
+
- Rendered `/variants` controls from the same Pi keybinding manager that handles input, including installations with separate package module instances.
|
|
36
|
+
- Rendered active, paused, and blocked `/goal` status text consistently in the footer's far-right slot.
|
|
37
|
+
- Kept the private goal-update tool inactive outside active `/goal` runs and rendered its real execution errors instead of malformed blocker-audit fields.
|
|
38
|
+
- Verified exact persisted paths for clearly declared file-deliverable goals before completion, while preserving model-reported completion for general objectives.
|
|
39
|
+
- Accepted explicit `1`/`1` bounds for single-select questions and applied the same bounds validation before rendering and execution.
|
|
40
|
+
- Prevented reload and branch navigation from preserving or reconstructing stale manual-compaction recovery eligibility; only Pi's live event in the current session can resume the interrupted goal safely.
|
|
41
|
+
- Restricted goal deliverable verification to explicit destination phrases so source and reference paths cannot be reported as completed output.
|
|
42
|
+
- Kept saved goals fully inactive in print and JSON modes, including the private update tool and shutdown checkpoints.
|
|
43
|
+
- Rejected concurrent `/init` starts during preflight, cancelled pending preflight work on shutdown, and settled active `/init` command handlers when their session closes.
|
|
44
|
+
|
|
7
45
|
## [2.0.6] - 2026-08-11
|
|
8
46
|
|
|
9
47
|
### Added
|
package/Killeros.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { registerRequestActivity } from "./killeros/activity.ts";
|
|
2
3
|
import { registerAliases, registerSlashAutocomplete } from "./killeros/commands.ts";
|
|
3
|
-
import { registerConcisePrompt } from "./killeros/concise.ts";
|
|
4
4
|
import { registerFooter } from "./killeros/footer.ts";
|
|
5
5
|
import { registerGoal, registerGoalSettlement } from "./killeros/goals.ts";
|
|
6
6
|
import { registerLifecycleHooks } from "./killeros/hooks.ts";
|
|
@@ -10,31 +10,47 @@ import {
|
|
|
10
10
|
type CompletionNotificationDependencies,
|
|
11
11
|
} from "./killeros/notifications.ts";
|
|
12
12
|
import { registerPersonalInstructions } from "./killeros/personal-instructions.ts";
|
|
13
|
-
import { registerQuestionTool } from "./killeros/question.ts";
|
|
13
|
+
import { registerQuestionTool, type QuestionRunner } from "./killeros/question.ts";
|
|
14
|
+
import { createDecisionGatedWorkflowAdapter } from "./killeros/decision-gated-workflow.ts";
|
|
14
15
|
import { createGoalRuntime, createInitRuntime } from "./killeros/runtime.ts";
|
|
15
16
|
import { registerShellUi } from "./killeros/shell-ui.ts";
|
|
16
17
|
import { registerVariants } from "./killeros/variants.ts";
|
|
17
18
|
import { registerWorkedFor } from "./killeros/worked-for.ts";
|
|
19
|
+
import { registerWorkflowGate, type WorkflowAdapter } from "./killeros/workflow-gate.ts";
|
|
18
20
|
|
|
19
|
-
export { CONCISE_SYSTEM_PROMPT, isConciseEnabled, isConcisedEnabled } from "./killeros/concise.ts";
|
|
20
21
|
export { contextPercentRemaining, formatCost, formatContextProgress } from "./killeros/footer.ts";
|
|
21
22
|
export { executeHook } from "./killeros/hooks.ts";
|
|
22
23
|
export { INIT_WORKFLOW_PROMPT } from "./killeros/init.ts";
|
|
23
24
|
export { buildInitEvidence, listInitEvidence, readInitEvidence } from "./killeros/init-evidence.ts";
|
|
24
25
|
export { captureInitTargetBaseline, installInitAgentsFile, validateGeneratedGuidance, writeInitAgentsFile } from "./killeros/init-target.ts";
|
|
26
|
+
export { createDecisionGatedWorkflowAdapter } from "./killeros/decision-gated-workflow.ts";
|
|
27
|
+
export { explicitSkillActivation, registerWorkflowGate } from "./killeros/workflow-gate.ts";
|
|
28
|
+
export type {
|
|
29
|
+
WorkflowAdapter,
|
|
30
|
+
WorkflowGateController,
|
|
31
|
+
WorkflowGateState,
|
|
32
|
+
WorkflowPolicy,
|
|
33
|
+
WorkflowTerminalReason,
|
|
34
|
+
WorkflowToolAuthorization,
|
|
35
|
+
} from "./killeros/workflow-gate.ts";
|
|
25
36
|
|
|
26
37
|
export interface KillerosOptions {
|
|
27
38
|
completionNotifications?: CompletionNotificationDependencies;
|
|
39
|
+
decisionGatedWorkflows?: readonly WorkflowAdapter[];
|
|
28
40
|
}
|
|
29
41
|
|
|
30
42
|
export default function Killeros(pi: ExtensionAPI, options: KillerosOptions = {}): void {
|
|
31
43
|
const initRuntime = createInitRuntime();
|
|
32
44
|
const goalRuntime = createGoalRuntime();
|
|
33
45
|
registerShellUi(pi);
|
|
34
|
-
registerConcisePrompt(pi);
|
|
35
46
|
registerGoal(pi, goalRuntime, initRuntime);
|
|
36
47
|
registerPersonalInstructions(pi, initRuntime);
|
|
37
|
-
registerQuestionTool(pi);
|
|
48
|
+
const questionRunner: QuestionRunner = registerQuestionTool(pi);
|
|
49
|
+
registerWorkflowGate(
|
|
50
|
+
pi,
|
|
51
|
+
questionRunner,
|
|
52
|
+
options.decisionGatedWorkflows ?? [createDecisionGatedWorkflowAdapter()],
|
|
53
|
+
);
|
|
38
54
|
registerAliases(pi);
|
|
39
55
|
registerSlashAutocomplete(pi);
|
|
40
56
|
registerFooter(pi, goalRuntime);
|
|
@@ -43,6 +59,7 @@ export default function Killeros(pi: ExtensionAPI, options: KillerosOptions = {}
|
|
|
43
59
|
registerLifecycleHooks(pi);
|
|
44
60
|
registerGoalSettlement(pi, goalRuntime, initRuntime);
|
|
45
61
|
registerInitSettlement(pi, initRuntime);
|
|
62
|
+
registerRequestActivity(pi);
|
|
46
63
|
registerCompletionNotifications(pi, options.completionNotifications);
|
|
47
64
|
registerWorkedFor(pi);
|
|
48
65
|
}
|
package/README.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# KillerOS
|
|
2
2
|
|
|
3
|
-
A production-hardened Pi extension that combines a custom TUI, repository initialization, long-running goals, reasoning controls, interactive questions, command aliases
|
|
3
|
+
A production-hardened Pi extension that combines a custom TUI, repository initialization, long-running goals, reasoning controls, interactive questions, and command aliases.
|
|
4
4
|
|
|
5
5
|
## Requirements
|
|
6
6
|
|
|
7
7
|
- Node.js `22.19.0` or later
|
|
8
|
-
- Pi `0.
|
|
8
|
+
- Pi `0.84.2` or later
|
|
9
9
|
- Interactive TUI mode for the custom header, editor, footer, `question` tool, and `/init`
|
|
10
10
|
|
|
11
11
|
The extension is strict TypeScript. Pi provides the runtime modules.
|
|
@@ -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.8
|
|
35
35
|
```
|
|
36
36
|
|
|
37
37
|
Add `-l` to either command for a project-only install. Restart Pi after installing.
|
|
@@ -40,18 +40,19 @@ Add `-l` to either command for a project-only install. Restart Pi after installi
|
|
|
40
40
|
|
|
41
41
|
- 52-column Compact startup card with inline version, polished model/provider identity, adjacent `/model`, directory, conditional Git branch, and a shuffled session-stable tip
|
|
42
42
|
- Cohesive dark theme with coral accents and neutral tool-call containers across pending, success, and error states
|
|
43
|
-
- Animated orange 12-frame activity glyph loop at 120 ms per frame
|
|
44
|
-
-
|
|
45
|
-
-
|
|
43
|
+
- Animated orange 12-frame activity glyph loop at 120 ms per frame with contextual copy derived from request, tool, result, and response events, plus a quiet hidden-thinking label
|
|
44
|
+
- Frameless multiline editor with one focus-aware `❯`, a shuffled session-stable empty-state suggestion, overflow-only scroll indicators, Shift+Enter support, and slash-command autocomplete; KillerOS preserves an editor factory configured by another extension
|
|
45
|
+
- One compact TUI transcript line reporting truthful `Done`, `Stopped`, or `Failed` settlement with elapsed time while preserving older `✻ Worked for …` entries
|
|
46
|
+
- Compact two-deck footer with session state above workspace state; model, context, and active goals stay prioritized as reasoning, time, cost, branch, and path reduce by available width
|
|
46
47
|
- Pi-owned context compaction with active goals continuing from Pi's settled boundary after manual, threshold, and overflow compaction
|
|
47
48
|
- Optional completion sounds after successful or failed settled requests, excluding manual aborts
|
|
48
49
|
- `/variants` selector and direct reasoning-level arguments
|
|
49
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
|
|
50
|
-
- Automatic `/init` guideline synthesis with a frozen safe evidence map, protected existing policy, and the four packaged behavioral sections adapted from `writing-great-guidelines`
|
|
51
|
+
- 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
|
|
51
53
|
- `question` tool with single-select and opt-in bounded multi-select, height-bounded option windows, configured Pi keybindings, live option/input progress, proposal previews, custom answers, history, cancellation, and compact expandable transcript rendering
|
|
52
|
-
- Mid-prompt slash completion with current Pi `0.
|
|
54
|
+
- Mid-prompt slash completion with current Pi `0.84.2` commands, extensions, prompts, and skills; paths, URLs, and invalid commands remain plain text
|
|
53
55
|
- Goal-aware `/clear` that confirms, aborts active work, waits for settlement, and starts a new session, plus `/exit` for graceful shutdown
|
|
54
|
-
- Concise system-prompt guidance and supported native concise defaults that preserve explicit provider settings
|
|
55
56
|
|
|
56
57
|
## Commands
|
|
57
58
|
|
|
@@ -70,17 +71,21 @@ Add `-l` to either command for a project-only install. Restart Pi after installi
|
|
|
70
71
|
/exit Quit Pi gracefully
|
|
71
72
|
```
|
|
72
73
|
|
|
73
|
-
`/goal` requires a saved session in TUI or RPC mode. Goal state is stored in versioned session entries on the active branch and restored after reload, resume, fork, or tree navigation. Active goals inject their unchanged objective every turn and continue one settled turn at a time. The model must use KillerOS’s private goal tool to
|
|
74
|
+
`/goal` requires a saved session in TUI or RPC mode. Goal state is stored in versioned session entries on the active branch and restored after reload, resume, fork, or tree navigation. Active goals inject their unchanged objective every turn and continue one settled turn at a time. The model must use KillerOS’s private goal tool to report completion. For an objective that clearly asks to create, write, save, or generate a named file-like deliverable at one quoted absolute path, KillerOS saves that exact path with the goal and verifies that a regular file exists there before accepting completion. Other objectives retain model-reported completion. Blocking requires one stable lowercase blocker key recorded on three consecutive goal turns; a changed key, skipped turn, resume, or edit resets the streak. Final prose alone does not end the loop.
|
|
74
75
|
|
|
75
76
|
`/goal pause` and `/goal clear` save paused or cleared state before aborting current goal work, so settlement cannot restart it. Aborted turns, provider failures, and continuation failures otherwise pause safely. Failed edit and replacement writes dispatch no edited objective; an active prior objective pauses fail-closed, while inactive durable state remains unchanged. Replacing unfinished work requires confirmation, and `/goal edit` requires TUI mode.
|
|
76
77
|
|
|
77
78
|
`/init` freezes a safe project-file map and exposes only dedicated read and list operations while it generates root `AGENTS.md`. Git-ignored files, known secret paths, private-key formats, other guidance, dependencies, links, non-regular files, and files outside that map are unavailable. Existing root `AGENTS.md` is separate protected policy: compatible rules are preserved, a real policy conflict leaves it unchanged with a reason, and any concurrent target change aborts installation without replacing the newer file.
|
|
78
79
|
|
|
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.
|
|
80
|
+
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
|
+
|
|
82
|
+
### Decision-gated workflows
|
|
83
|
+
|
|
84
|
+
Explicit `/skill:decision-gated-workflow` activation opens the shared question UI before Pi expands the skill. `Normal` allows only interview and read-only tools; `With docs` additionally permits agreed glossary, context-map, and ADR paths. The selected policy remains active until the workflow is explicitly finished or cancelled, and lifecycle changes clear it safely. Extensions can supply additional adapters through `KillerosOptions.decisionGatedWorkflows`.
|
|
85
|
+
|
|
86
|
+
### Interactive questions
|
|
80
87
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
Single-select remains the default. An agent opts into multi-select with `mode: "multiple"` and may set `minSelections` and `maxSelections`; the custom answer counts as one selection.
|
|
88
|
+
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.
|
|
84
89
|
|
|
85
90
|
In multi-select, use Space or a visible number to toggle an option, `/` to filter, and Enter to submit. The filter accepts spaces; Enter applies it and Escape returns to the choices. Checked options remain selected when the filter changes. Select **Type a custom answer** with Enter to add or edit one custom item alongside checked options.
|
|
86
91
|
|
|
@@ -107,8 +112,8 @@ Lifecycle hooks are loaded from `.pi/killeros-hooks.json` at session start. Supp
|
|
|
107
112
|
| Mode | Behavior |
|
|
108
113
|
|---|---|
|
|
109
114
|
| TUI | All features are available, including the completion sound and tab-title indicator |
|
|
110
|
-
| RPC | Goal set/view/pause/resume/clear
|
|
111
|
-
| Print/JSON |
|
|
115
|
+
| RPC | Goal set/view/pause/resume/clear work; TUI components, `/goal edit`, `/init`, completion sounds, and the title indicator are disabled |
|
|
116
|
+
| Print/JSON | Interactive questions, `/goal`, and `/init` fail explicitly; completion sounds and the title indicator are disabled |
|
|
112
117
|
|
|
113
118
|
## Validation
|
|
114
119
|
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
3
|
+
|
|
4
|
+
export type ActivityMessage =
|
|
5
|
+
| { kind: "prompt" }
|
|
6
|
+
| { kind: "tool"; toolName: string }
|
|
7
|
+
| { kind: "tool-result"; failed: boolean }
|
|
8
|
+
| { kind: "responding" };
|
|
9
|
+
|
|
10
|
+
function safeToolName(toolName: string): string {
|
|
11
|
+
const normalized = toolName.replace(/[\u0000-\u001F\u007F]+/gu, " ").replace(/\s+/gu, " ").trim();
|
|
12
|
+
return truncateToWidth(normalized || "tool", 32, "…");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function formatActivityMessage(message: ActivityMessage, theme: Theme): string {
|
|
16
|
+
let verb: string;
|
|
17
|
+
let detail: string;
|
|
18
|
+
|
|
19
|
+
switch (message.kind) {
|
|
20
|
+
case "prompt":
|
|
21
|
+
verb = "Mapping…";
|
|
22
|
+
detail = "understanding request";
|
|
23
|
+
break;
|
|
24
|
+
case "tool-result":
|
|
25
|
+
verb = message.failed ? "Recovering…" : "Reviewing…";
|
|
26
|
+
detail = message.failed ? "tool failed" : "reading the result";
|
|
27
|
+
break;
|
|
28
|
+
case "responding":
|
|
29
|
+
verb = "Responding…";
|
|
30
|
+
detail = "assembling the answer";
|
|
31
|
+
break;
|
|
32
|
+
case "tool":
|
|
33
|
+
switch (message.toolName.trim().toLocaleLowerCase()) {
|
|
34
|
+
case "read":
|
|
35
|
+
case "grep":
|
|
36
|
+
case "find":
|
|
37
|
+
case "ls":
|
|
38
|
+
verb = "Inspecting…";
|
|
39
|
+
detail = "reading relevant code";
|
|
40
|
+
break;
|
|
41
|
+
case "edit":
|
|
42
|
+
case "write":
|
|
43
|
+
verb = "Changing…";
|
|
44
|
+
detail = "editing";
|
|
45
|
+
break;
|
|
46
|
+
case "bash":
|
|
47
|
+
verb = "Running…";
|
|
48
|
+
detail = "command";
|
|
49
|
+
break;
|
|
50
|
+
default:
|
|
51
|
+
verb = "Working…";
|
|
52
|
+
detail = `using ${safeToolName(message.toolName)}`;
|
|
53
|
+
}
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return `${theme.fg("accent", verb)} ${theme.fg("dim", `(${theme.bold("esc")} to interrupt · ${detail})`)}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function registerRequestActivity(pi: ExtensionAPI): void {
|
|
61
|
+
let active = false;
|
|
62
|
+
|
|
63
|
+
const clear = (ctx?: ExtensionContext): void => {
|
|
64
|
+
if (ctx?.mode === "tui") {
|
|
65
|
+
ctx.ui.setWorkingMessage();
|
|
66
|
+
}
|
|
67
|
+
active = false;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
pi.on("agent_start", (_event, ctx) => {
|
|
71
|
+
if (ctx.mode !== "tui") return;
|
|
72
|
+
active = true;
|
|
73
|
+
ctx.ui.setWorkingMessage(formatActivityMessage({ kind: "prompt" }, ctx.ui.theme));
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
pi.on("tool_execution_start", (event, ctx) => {
|
|
77
|
+
if (ctx.mode !== "tui" || !active) return;
|
|
78
|
+
ctx.ui.setWorkingMessage(formatActivityMessage({ kind: "tool", toolName: event.toolName }, ctx.ui.theme));
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
pi.on("tool_execution_end", (event, ctx) => {
|
|
82
|
+
if (ctx.mode !== "tui" || !active) return;
|
|
83
|
+
ctx.ui.setWorkingMessage(formatActivityMessage({ kind: "tool-result", failed: event.isError }, ctx.ui.theme));
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
pi.on("message_update", (event, ctx) => {
|
|
87
|
+
if (ctx.mode !== "tui" || !active || event.assistantMessageEvent.type !== "text_start") return;
|
|
88
|
+
ctx.ui.setWorkingMessage(formatActivityMessage({ kind: "responding" }, ctx.ui.theme));
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
pi.on("agent_settled", (_event, ctx) => {
|
|
92
|
+
if (ctx.mode !== "tui" || !active || !ctx.isIdle?.() || ctx.hasPendingMessages?.()) return;
|
|
93
|
+
clear(ctx);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
pi.on("session_shutdown", (_event, ctx) => clear(ctx));
|
|
97
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
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
|
+
}
|
package/killeros/footer.ts
CHANGED
|
@@ -125,6 +125,10 @@ function renderFooterRow(left: string, right: string, width: number): string {
|
|
|
125
125
|
return ` ${clippedLeft}${gap}${clippedRight} `;
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
+
function renderFooter(rows: string[], width: number, theme: Theme): string[] {
|
|
129
|
+
return [theme.fg("borderMuted", "─".repeat(width)), ...rows];
|
|
130
|
+
}
|
|
131
|
+
|
|
128
132
|
function formatGoalElapsed(milliseconds: number): string {
|
|
129
133
|
const totalSeconds = Number.isFinite(milliseconds) ? Math.max(0, Math.floor(milliseconds / 1_000)) : 0;
|
|
130
134
|
if (totalSeconds < 60) return `${totalSeconds}s`;
|
|
@@ -138,15 +142,13 @@ function formatGoalElapsed(milliseconds: number): string {
|
|
|
138
142
|
return `${hours}h ${minutes.toString().padStart(2, "0")}m ${seconds.toString().padStart(2, "0")}s`;
|
|
139
143
|
}
|
|
140
144
|
|
|
141
|
-
function formatActiveGoalFooter(state: GoalState | undefined, theme: Theme): string {
|
|
142
|
-
if (state?.status !== "active") return "";
|
|
143
|
-
return theme.fg("warning", `/goal is active (${formatGoalElapsed(goalElapsedMilliseconds(state))})`);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
145
|
function formatGoalFooter(state: GoalState | undefined, theme: Theme): string {
|
|
147
146
|
if (!state) return "";
|
|
148
|
-
if (state.status === "
|
|
149
|
-
|
|
147
|
+
if (state.status === "active") {
|
|
148
|
+
return theme.fg("warning", `/goal is active (${formatGoalElapsed(goalElapsedMilliseconds(state))})`);
|
|
149
|
+
}
|
|
150
|
+
if (state.status === "paused") return theme.fg("warning", "/goal is paused");
|
|
151
|
+
if (state.status === "blocked") return theme.fg("error", "/goal is blocked");
|
|
150
152
|
return "";
|
|
151
153
|
}
|
|
152
154
|
|
|
@@ -211,52 +213,36 @@ export function registerFooter(pi: ExtensionAPI, goalRuntime: GoalRuntime): void
|
|
|
211
213
|
const signature = formatModel(model, theme);
|
|
212
214
|
const fullDirectory = theme.fg("dim", cwd);
|
|
213
215
|
const focusedDirectory = theme.fg("dim", compactDirectory(cwd));
|
|
214
|
-
const activeGoal = formatActiveGoalFooter(goalRuntime.state, theme);
|
|
215
216
|
const goal = formatGoalFooter(goalRuntime.state, theme);
|
|
216
|
-
const
|
|
217
|
+
const primary = joinFooterParts([
|
|
217
218
|
signature,
|
|
218
219
|
level,
|
|
219
220
|
context,
|
|
220
|
-
|
|
221
|
-
|
|
221
|
+
], theme);
|
|
222
|
+
const primaryFocused = joinFooterParts([signature, context], theme);
|
|
223
|
+
const session = joinFooterParts([
|
|
222
224
|
theme.fg("dim", formatTime(Date.now() - sessionStart)),
|
|
223
225
|
theme.fg("dim", formatCost(getSessionCost(ctx))),
|
|
224
226
|
], theme);
|
|
225
|
-
const focused = joinFooterParts([signature, context, goal], theme);
|
|
226
|
-
|
|
227
|
-
if (activeGoal) {
|
|
228
|
-
if (footerRowFits(rich, activeGoal, width)) {
|
|
229
|
-
return [renderFooterRow(rich, activeGoal, width)];
|
|
230
|
-
}
|
|
231
|
-
if (footerRowFits(focused, activeGoal, width)) {
|
|
232
|
-
return [renderFooterRow(focused, activeGoal, width)];
|
|
233
|
-
}
|
|
234
|
-
if (footerRowFits(context, activeGoal, width)) {
|
|
235
|
-
return [renderFooterRow(context, activeGoal, width)];
|
|
236
|
-
}
|
|
237
|
-
return [renderFooterRow("", activeGoal, width)];
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
if (footerRowFits(rich, fullDirectory, width)) {
|
|
241
|
-
return [renderFooterRow(rich, fullDirectory, width)];
|
|
242
|
-
}
|
|
243
|
-
if (footerRowFits(rich, focusedDirectory, width)) {
|
|
244
|
-
return [renderFooterRow(rich, focusedDirectory, width)];
|
|
245
|
-
}
|
|
246
|
-
if (footerRowFits(focused, focusedDirectory, width)) {
|
|
247
|
-
return [renderFooterRow(focused, focusedDirectory, width)];
|
|
248
|
-
}
|
|
249
|
-
if (footerRowFits(focused, "", width)) {
|
|
250
|
-
return [renderFooterRow(focused, "", width)];
|
|
251
|
-
}
|
|
252
|
-
if (goal) {
|
|
253
|
-
const essentialGoal = joinFooterParts([context, goal], theme);
|
|
254
|
-
if (footerRowFits(essentialGoal, "", width)) return [renderFooterRow(essentialGoal, "", width)];
|
|
255
|
-
return [renderFooterRow(goal, context, width)];
|
|
256
|
-
}
|
|
257
|
-
|
|
258
227
|
const essentialModel = formatModel(model, theme, false);
|
|
259
|
-
|
|
228
|
+
const primaryRow = footerRowFits(primary, session, width)
|
|
229
|
+
? renderFooterRow(primary, session, width)
|
|
230
|
+
: footerRowFits(primary, "", width)
|
|
231
|
+
? renderFooterRow(primary, "", width)
|
|
232
|
+
: footerRowFits(primaryFocused, "", width)
|
|
233
|
+
? renderFooterRow(primaryFocused, "", width)
|
|
234
|
+
: renderFooterRow(essentialModel, context, width);
|
|
235
|
+
const branchLabel = branch ? theme.fg("dim", branch) : "";
|
|
236
|
+
const workspaceRight = goal || fullDirectory;
|
|
237
|
+
const secondaryRow = footerRowFits(branchLabel, workspaceRight, width)
|
|
238
|
+
? renderFooterRow(branchLabel, workspaceRight, width)
|
|
239
|
+
: goal
|
|
240
|
+
? renderFooterRow("", goal, width)
|
|
241
|
+
: footerRowFits(branchLabel, focusedDirectory, width)
|
|
242
|
+
? renderFooterRow(branchLabel, focusedDirectory, width)
|
|
243
|
+
: renderFooterRow(branchLabel, "", width);
|
|
244
|
+
|
|
245
|
+
return renderFooter([primaryRow, secondaryRow], width, theme);
|
|
260
246
|
},
|
|
261
247
|
};
|
|
262
248
|
});
|