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 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, and concise-response guidance.
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.82.1` or later
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.6
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, with orange shuffled Claude-adjacent verbs changing every 2.5 seconds, a gray `(esc to interrupt · thinking)` status with bold `esc`, and a quiet hidden-thinking label
44
- - Full-width framed multiline editor with a shuffled session-stable empty-state suggestion, Shift+Enter support, and slash-command autocomplete; KillerOS preserves an editor factory configured by another extension
45
- - Responsive footer with polished model/provider identity, plain-language context, and active goal state remaining; reasoning, Git branch, elapsed time, cost, and path cut down by available width
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.82.1` commands, extensions, prompts, and skills; paths, URLs, and invalid commands remain plain text
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 mark verified 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
+ `/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
- ### Interactive questions
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 and concise prompt guidance work; TUI components, `/goal edit`, `/init`, completion sounds, and the title indicator are disabled |
111
- | Print/JSON | Concise prompt guidance works; interactive questions, `/goal`, and `/init` fail explicitly; completion sounds and the title indicator are disabled |
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
+ }
@@ -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 === "paused") return theme.fg("warning", "Ⅱ goal paused");
149
- if (state.status === "blocked") return theme.fg("error", "! goal blocked");
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 rich = joinFooterParts([
217
+ const primary = joinFooterParts([
217
218
  signature,
218
219
  level,
219
220
  context,
220
- goal,
221
- branch ? theme.fg("dim", branch) : "",
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
- return [renderFooterRow(essentialModel, context, width)];
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
  });