aiwcli 0.14.0 → 0.15.2

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.
Files changed (29) hide show
  1. package/dist/templates/_shared/.claude/skills/codex/SKILL.md +19 -26
  2. package/dist/templates/_shared/.codex/workflows/codex.md +11 -0
  3. package/dist/templates/_shared/lib-ts/agent-exec/index.ts +2 -0
  4. package/dist/templates/_shared/lib-ts/agent-exec/structured-output.ts +166 -0
  5. package/dist/templates/_shared/lib-ts/base/cli-args.ts +4 -0
  6. package/dist/templates/_shared/lib-ts/base/state-io.ts +1 -1
  7. package/dist/templates/_shared/lib-ts/base/subprocess-utils.ts +4 -3
  8. package/dist/templates/_shared/lib-ts/base/tmux-driver.ts +70 -3
  9. package/dist/templates/_shared/lib-ts/base/tmux-pane-placement.ts +70 -0
  10. package/dist/templates/_shared/lib-ts/context/context-store.ts +3 -0
  11. package/dist/templates/_shared/scripts/resolve-run.ts +1 -1
  12. package/dist/templates/_shared/scripts/status_line.ts +36 -19
  13. package/dist/templates/_shared/skills/codex/CLAUDE.md +77 -0
  14. package/dist/templates/_shared/skills/codex/SKILL.md +71 -0
  15. package/dist/templates/_shared/skills/codex/lib/codex-watcher.ts +226 -0
  16. package/dist/templates/_shared/skills/{prompt-codex → codex}/scripts/launch-codex.ts +175 -8
  17. package/dist/templates/_shared/skills/codex/scripts/watch-codex.ts +42 -0
  18. package/dist/templates/cc-native/_cc-native/lib-ts/cli-output-parser.ts +9 -133
  19. package/dist/templates/cc-native/_cc-native/lib-ts/settings.ts +118 -42
  20. package/dist/templates/cc-native/_cc-native/lib-ts/state.ts +1 -0
  21. package/dist/templates/cc-native/_cc-native/lib-ts/types.ts +61 -0
  22. package/dist/templates/cc-native/_cc-native/plan-review/lib/agent-selection.ts +5 -4
  23. package/dist/templates/cc-native/_cc-native/plan-review/lib/orchestrator.ts +4 -4
  24. package/dist/templates/cc-native/_cc-native/plan-review/lib/review-pipeline.ts +16 -13
  25. package/dist/templates/cc-native/_cc-native/plan-review/lib/reviewers/providers/orchestrator-claude-agent.ts +54 -23
  26. package/oclif.manifest.json +1 -1
  27. package/package.json +1 -1
  28. package/dist/templates/_shared/.claude/skills/codex/prompt.md +0 -10
  29. package/dist/templates/_shared/skills/prompt-codex/CLAUDE.md +0 -46
@@ -1,42 +1,35 @@
1
1
  ---
2
2
  name: codex
3
- description: Launch Codex CLI in a tmux pane. USE WHEN codex OR send to codex OR codex implement OR hand off to codex OR launch codex OR codex plan OR run codex.
3
+ description: Delegate implementation to Codex sub-agents. USE WHEN codex OR send to codex OR codex implement OR hand off to codex OR launch codex OR codex plan OR run codex.
4
4
  user-invocable: true
5
5
  ---
6
6
 
7
- # Codex CLI
7
+ Read `.aiwcli/_shared/skills/codex/SKILL.md` for delegation patterns and examples.
8
8
 
9
- Launch Codex in a tmux split pane and optionally inject a prompt.
9
+ ## Role
10
10
 
11
- ## Command
11
+ You are the orchestrator. Codex instances are your implementation sub-agents. Decide what to delegate, how to split work, and review results when summaries arrive.
12
12
 
13
- `bun .aiwcli/_shared/skills/prompt-codex/scripts/launch-codex.ts [flags] <mode>`
13
+ ## Command
14
14
 
15
- **Modes:** `plan` (inject active plan) | `--file <path>` (inject file) | `<text...>` (inject inline text)
15
+ ```
16
+ bun ~/.aiwcli/bin/resolve-run.ts .aiwcli/_shared/skills/codex/scripts/launch-codex.ts [flags] <mode>
17
+ ```
16
18
 
17
- **Flags:**
18
- - `--model <name>` — Model aliases: `spark`, `codex`, `gpt`. Tiers: `fast`, `standard`, `smart`. Or any full model ID.
19
- - `--sandbox <mode>` — `read-only`, `workspace-write`, `danger-full-access`. Default: Codex default.
20
- - `--context <id>` — Pass the active context ID so Codex receives project orientation (context folder, notes path). **Always pass this when an active context exists** (check the `Active Context:` system reminder for the ID).
19
+ The script blocks until Codex exits and prints a summary — run with Bash `run_in_background: true` so you stay unblocked.
21
20
 
22
- ## Model Reference
21
+ **Modes:** `plan` | `--file <path>` | `<inline text...>`
23
22
 
24
- | Name | Resolves To |
25
- |------|-------------|
26
- | `spark` | `gpt-5.3-codex-spark` (fastest) |
27
- | `codex` | `gpt-5.3-codex` (default) |
28
- | `gpt` | `gpt-5.2` (non-Codex GPT) |
29
- | `fast` | `gpt-5.3-codex-spark` (tier) |
30
- | `standard` | `gpt-5.3-codex` (tier) |
31
- | `smart` | `gpt-5.3-codex` (tier) |
23
+ **Key flags:**
24
+ - `--context <id>` — Project orientation. Pass when implementing a plan.
25
+ - `--prompt <text>` Scope the agent's work to a specific plan section or task.
26
+ - `--model <name>` — `spark`, `codex`, `gpt`, or tier: `fast`, `standard`, `smart`.
27
+ - `--no-watch` Fire-and-forget (skip waiting for summary).
32
28
 
33
- ## Examples
29
+ ## Delegation Decision
34
30
 
35
- - `/codex --model spark --context <context-id> plan` hand off active plan to Spark with context
36
- - `/codex --model codex --context <context-id> Refactor auth to use JWT` — inline prompt with context
37
- - `/codex --file ./spec.md` — inject file contents (no context)
31
+ **One-shot:** Plan is small or tightly coupled launch one Codex with `plan` mode. Wait for the summary, then review.
38
32
 
39
- ## Requirements
33
+ **Parallel:** Plan has independent sections → launch multiple Codex instances, each scoped with `--prompt` to its section. All share the same `--context`. Review when summaries arrive, check for conflicts.
40
34
 
41
- - Must be running inside tmux
42
- - `codex` CLI must be on PATH
35
+ **Ad-hoc:** No plan, just a task → pass inline text (e.g., `"Fix the failing test in auth.ts"`).
@@ -0,0 +1,11 @@
1
+ # Codex Workflow
2
+
3
+ Use Codex CLI handoff instructions from `.aiwcli/_shared/skills/codex/SKILL.md`.
4
+
5
+ ## Command
6
+
7
+ `bun ~/.aiwcli/bin/resolve-run.ts .aiwcli/_shared/skills/codex/scripts/launch-codex.ts [flags] <mode>`
8
+
9
+ **Modes:** `plan` | `--file <path>` | `<inline text...>`
10
+
11
+ **Common flags:** `--model <name>`, `--sandbox <mode>`, `--context <id>`, `--prompt <text>`, `--no-yolo`, `--capture`
@@ -2,3 +2,5 @@ export { BaseCliAgent, type AgentExecutionConfig } from "./base-agent.js";
2
2
  export type { ExecutionBackend, ExecutionRequest, ExecutionResult, AgentDebugLogger } from "./execution-backend.js";
3
3
  export { HeadlessBackend } from "./backends/headless.js";
4
4
  export { TmuxBackend } from "./backends/tmux.js";
5
+ export { parseJsonObjectMaybe, parseStructuredOutput } from "./structured-output.js";
6
+ export type { StructuredOutputParseOptions } from "./structured-output.js";
@@ -0,0 +1,166 @@
1
+ /**
2
+ * Shared structured output parsing utilities for CLI-based agents.
3
+ * Supports Claude/Codex-style envelopes and heuristic JSON extraction.
4
+ */
5
+
6
+ import { logDebug, logError, logWarn } from "../base/logger.js";
7
+
8
+ export interface StructuredOutputParseOptions {
9
+ requireFields?: string[];
10
+ loggerTag?: string;
11
+ }
12
+
13
+ const DEFAULT_LOG_TAG = "structured_output";
14
+
15
+ function getTag(options?: StructuredOutputParseOptions): string {
16
+ return options?.loggerTag ?? DEFAULT_LOG_TAG;
17
+ }
18
+
19
+ function validateRequiredFields(
20
+ obj: Record<string, unknown>,
21
+ parseMethod: "strict" | "heuristic",
22
+ options?: StructuredOutputParseOptions,
23
+ ): Record<string, unknown> | null {
24
+ const required = options?.requireFields;
25
+ if (!required || required.length === 0) return obj;
26
+
27
+ const missing = required.filter((field) => !(field in obj) || obj[field] === undefined || obj[field] === null);
28
+ if (missing.length === 0) return obj;
29
+
30
+ const tag = getTag(options);
31
+ logWarn(tag, `Parsed JSON (${parseMethod}) missing required fields: ${JSON.stringify(missing)}`);
32
+ logDebug(tag, `Parsed keys: ${JSON.stringify(Object.keys(obj))}`);
33
+
34
+ // Heuristic extraction often grabs the wrong JSON blob. Reject in that case.
35
+ if (parseMethod === "heuristic") {
36
+ return null;
37
+ }
38
+ return obj;
39
+ }
40
+
41
+ /**
42
+ * Parse a JSON object from text using strict parse first, then heuristic
43
+ * extraction of the first object-like block.
44
+ */
45
+ export function parseJsonObjectMaybe(
46
+ text: string,
47
+ options?: StructuredOutputParseOptions,
48
+ ): Record<string, unknown> | null {
49
+ const tag = getTag(options);
50
+ const trimmed = text.trim();
51
+ if (!trimmed) return null;
52
+
53
+ // Strict parse first.
54
+ try {
55
+ const parsed: unknown = JSON.parse(trimmed);
56
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
57
+ return validateRequiredFields(parsed as Record<string, unknown>, "strict", options);
58
+ }
59
+ } catch {
60
+ // Fall through to heuristic extraction.
61
+ }
62
+
63
+ // Heuristic parse: extract the first object-like block.
64
+ const start = trimmed.indexOf("{");
65
+ const end = trimmed.lastIndexOf("}");
66
+ if (start === -1 || end === -1 || end <= start) return null;
67
+
68
+ const candidate = trimmed.slice(start, end + 1);
69
+ try {
70
+ const parsed: unknown = JSON.parse(candidate);
71
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
72
+ logDebug(tag, `Used heuristic JSON extraction (chars ${start}-${end})`);
73
+ return validateRequiredFields(parsed as Record<string, unknown>, "heuristic", options);
74
+ }
75
+ } catch {
76
+ logDebug(tag, `Heuristic JSON extraction failed (chars ${start}-${end})`);
77
+ }
78
+
79
+ return null;
80
+ }
81
+
82
+ function parseAssistantEnvelope(
83
+ envelope: Record<string, unknown>,
84
+ options?: StructuredOutputParseOptions,
85
+ ): Record<string, unknown> | null {
86
+ const tag = getTag(options);
87
+ const message = envelope.message;
88
+ if (!message || typeof message !== "object") return null;
89
+
90
+ const content = (message as Record<string, unknown>).content;
91
+ if (!Array.isArray(content)) return null;
92
+
93
+ for (const item of content) {
94
+ if (!item || typeof item !== "object") continue;
95
+ const toolUse = item as Record<string, unknown>;
96
+ if (toolUse.name !== "StructuredOutput") continue;
97
+ if (toolUse.input && typeof toolUse.input === "object" && !Array.isArray(toolUse.input)) {
98
+ logDebug(tag, "Found StructuredOutput in assistant envelope");
99
+ return toolUse.input as Record<string, unknown>;
100
+ }
101
+ }
102
+ return null;
103
+ }
104
+
105
+ /**
106
+ * Parse structured output across known CLI envelope formats.
107
+ * Falls back to generic JSON extraction when no recognized envelope exists.
108
+ */
109
+ export function parseStructuredOutput(
110
+ raw: string,
111
+ options?: StructuredOutputParseOptions,
112
+ ): Record<string, unknown> | null {
113
+ const tag = getTag(options);
114
+
115
+ try {
116
+ const parsed: unknown = JSON.parse(raw);
117
+
118
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
119
+ const obj = parsed as Record<string, unknown>;
120
+
121
+ if (obj.structured_output && typeof obj.structured_output === "object" && !Array.isArray(obj.structured_output)) {
122
+ logDebug(tag, "Found structured_output in root object");
123
+ return validateRequiredFields(obj.structured_output as Record<string, unknown>, "strict", options);
124
+ }
125
+
126
+ const assistantResult = parseAssistantEnvelope(obj, options);
127
+ if (assistantResult) return assistantResult;
128
+
129
+ // Session result envelope (no structured output tool call).
130
+ if (obj.type === "result" || ("duration_ms" in obj && "session_id" in obj)) {
131
+ if (obj.is_error === true || (Array.isArray(obj.errors) && obj.errors.length > 0)) {
132
+ logWarn(tag, `CLI returned error envelope: ${JSON.stringify(obj.errors ?? "is_error=true")}`);
133
+ return null;
134
+ }
135
+
136
+ if (typeof obj.result === "string" && obj.result.trim().length > 0) {
137
+ logDebug(tag, "Found text result in session envelope, attempting JSON extraction");
138
+ const extracted = parseJsonObjectMaybe(obj.result, options);
139
+ if (extracted) return extracted;
140
+ logWarn(tag, "Session envelope result contained no extractable JSON object");
141
+ }
142
+ return null;
143
+ }
144
+ } else if (Array.isArray(parsed)) {
145
+ for (let i = 0; i < parsed.length; i++) {
146
+ const event = parsed[i];
147
+ if (!event || typeof event !== "object") continue;
148
+ const eventObj = event as Record<string, unknown>;
149
+ const assistantResult = parseAssistantEnvelope(eventObj, options);
150
+ if (assistantResult) {
151
+ logDebug(tag, `Found StructuredOutput in event[${i}]`);
152
+ return assistantResult;
153
+ }
154
+ }
155
+ }
156
+ } catch (error: unknown) {
157
+ if (error instanceof SyntaxError) {
158
+ logWarn(tag, `JSON decode error: ${error.message}`);
159
+ } else {
160
+ logError(tag, `Unexpected parse error: ${error}`);
161
+ }
162
+ }
163
+
164
+ logDebug(tag, "No structured envelope found, falling back to generic JSON extraction");
165
+ return parseJsonObjectMaybe(raw, options);
166
+ }
@@ -44,6 +44,7 @@ export interface CodexReplSpec {
44
44
  mode: "repl";
45
45
  model?: string | ModelTier;
46
46
  sandbox?: CodexSandbox;
47
+ yolo?: boolean;
47
48
  extraArgs?: string[];
48
49
  }
49
50
 
@@ -203,6 +204,7 @@ function buildCodexReplInvocation(
203
204
  env: Record<string, string | undefined>,
204
205
  ): CliInvocation {
205
206
  const args: string[] = [];
207
+ if (spec.yolo) args.push("--dangerously-bypass-approvals-and-sandbox");
206
208
  if (spec.sandbox) args.push("--sandbox", spec.sandbox);
207
209
  if (model) args.push("--model", model);
208
210
  if (spec.extraArgs) args.push(...spec.extraArgs);
@@ -263,12 +265,14 @@ export function reviewSpec(
263
265
  export function codexReplSpec(
264
266
  model?: string,
265
267
  sandbox?: CodexSandbox,
268
+ yolo?: boolean,
266
269
  ): CodexReplSpec {
267
270
  return {
268
271
  provider: "codex",
269
272
  mode: "repl",
270
273
  model,
271
274
  sandbox,
275
+ yolo,
272
276
  };
273
277
  }
274
278
 
@@ -16,7 +16,7 @@ import type { ContextState, Mode } from "../types.js";
16
16
  const MODE_MIGRATION: Record<string, Mode> = {
17
17
  none: "idle",
18
18
  planning: "idle",
19
- pending_implementation: "has_plan",
19
+ pending_implementation: "has_staged_work",
20
20
  implementing: "active",
21
21
  };
22
22
 
@@ -4,7 +4,7 @@
4
4
  */
5
5
 
6
6
  import { execSync, execFile } from "node:child_process";
7
- import type { ChildProcess } from "node:child_process";
7
+ import type { ChildProcess, ExecSyncOptionsWithStringEncoding } from "node:child_process";
8
8
 
9
9
  // ─── Child Process Cleanup ─────────────────────────────────────────────────
10
10
  //
@@ -68,7 +68,7 @@ export function isInternalCall(): boolean {
68
68
  * claude instances can run without being blocked.
69
69
  */
70
70
  export function getInternalSubprocessEnv(): Record<string, string | undefined> {
71
- const env = {
71
+ const env: Record<string, string | undefined> = {
72
72
  ...process.env,
73
73
  AIWCLI_INTERNAL_CALL: "true",
74
74
  };
@@ -87,7 +87,8 @@ export function getInternalSubprocessEnv(): Record<string, string | undefined> {
87
87
  export function findExecutable(name: string): string | null {
88
88
  try {
89
89
  const cmd = process.platform === "win32" ? `where ${name}` : `which ${name}`;
90
- const lines = execSync(cmd, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], shell: true })
90
+ // shell: true is valid at runtime but Node's TS types restrict it to string for this overload
91
+ const lines = execSync(cmd, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], shell: true } as unknown as ExecSyncOptionsWithStringEncoding)
91
92
  .trim()
92
93
  .split(/\r?\n/)
93
94
  .map((l) => l.trim())
@@ -12,9 +12,11 @@
12
12
  import * as fs from "node:fs";
13
13
 
14
14
  import { execFileAsync, findExecutable } from "./subprocess-utils.js";
15
+ import { findBestSplit, listPanes } from "./tmux-pane-placement.js";
15
16
 
16
17
  export type DriverMode = "exec" | "repl";
17
18
  export type TmuxSplitFlag = "-h" | "-v";
19
+ export type TmuxSplitOption = TmuxSplitFlag | "auto";
18
20
 
19
21
  export interface DriverPreflightResult {
20
22
  available: boolean;
@@ -53,7 +55,7 @@ export interface LaunchDriverOptions {
53
55
  env?: Record<string, string>;
54
56
  promptPath?: string;
55
57
  sendPromptInRepl?: boolean;
56
- splitFlag?: string;
58
+ splitFlag?: TmuxSplitOption;
57
59
  splitTarget?: string;
58
60
  autoClose?: boolean;
59
61
  holdPane?: boolean;
@@ -165,6 +167,54 @@ export function normalizeSplitFlag(value: string | undefined): TmuxSplitFlag {
165
167
  return value?.trim() === "-v" ? "-v" : "-h";
166
168
  }
167
169
 
170
+ function splitFlagFromDimensions(width: number, height: number): TmuxSplitFlag {
171
+ return width >= height ? "-h" : "-v";
172
+ }
173
+
174
+ async function resolveSplitFlagForTargetPane(
175
+ tmuxPath: string,
176
+ splitTarget: string,
177
+ ): Promise<TmuxSplitFlag | null> {
178
+ const size = await execFileAsync(
179
+ tmuxPath,
180
+ ["display-message", "-p", "-t", splitTarget, "#{pane_width} #{pane_height}"],
181
+ { timeout: 3000 },
182
+ );
183
+ if (size.exitCode !== 0) return null;
184
+
185
+ const parts = size.stdout.trim().split(/\s+/);
186
+ if (parts.length < 2) return null;
187
+
188
+ const width = Number.parseInt(parts[0] ?? "", 10);
189
+ const height = Number.parseInt(parts[1] ?? "", 10);
190
+ if (!Number.isFinite(width) || !Number.isFinite(height)) return null;
191
+
192
+ return splitFlagFromDimensions(width, height);
193
+ }
194
+
195
+ async function resolveAutoSplit(
196
+ tmuxPath: string,
197
+ splitTarget?: string,
198
+ ): Promise<{ splitFlag: TmuxSplitFlag; splitTarget?: string }> {
199
+ const explicitTarget = splitTarget?.trim();
200
+ if (explicitTarget) {
201
+ const splitFlag = await resolveSplitFlagForTargetPane(tmuxPath, explicitTarget);
202
+ return {
203
+ splitFlag: splitFlag ?? "-h",
204
+ splitTarget: explicitTarget,
205
+ };
206
+ }
207
+
208
+ const panes = await listPanes(tmuxPath);
209
+ const placement = findBestSplit(panes);
210
+ if (!placement) return { splitFlag: "-h" };
211
+
212
+ return {
213
+ splitFlag: placement.splitFlag,
214
+ splitTarget: placement.targetPane,
215
+ };
216
+ }
217
+
168
218
  export function isTruthy(value: string | undefined): boolean {
169
219
  if (!value) return false;
170
220
  const normalized = value.trim().toLowerCase();
@@ -287,8 +337,25 @@ export async function launchDriverInTmuxOrFallback(
287
337
 
288
338
  const tmux = getTmuxAvailability();
289
339
  if (tmux.available && tmux.tmuxPath) {
290
- const splitFlag = normalizeSplitFlag(options.splitFlag);
291
- const splitTarget = options.splitTarget?.trim();
340
+ const requestedSplitFlag = options.splitFlag;
341
+ const explicitSplitTarget = options.splitTarget?.trim();
342
+ let splitFlag: TmuxSplitFlag;
343
+ let splitTarget: string | undefined;
344
+
345
+ if (requestedSplitFlag === "auto") {
346
+ try {
347
+ const resolved = await resolveAutoSplit(tmux.tmuxPath, explicitSplitTarget);
348
+ splitFlag = resolved.splitFlag;
349
+ splitTarget = resolved.splitTarget;
350
+ } catch {
351
+ splitFlag = "-h";
352
+ splitTarget = explicitSplitTarget;
353
+ }
354
+ } else {
355
+ splitFlag = normalizeSplitFlag(requestedSplitFlag);
356
+ splitTarget = explicitSplitTarget;
357
+ }
358
+
292
359
  const baseCmd = buildToolCommand(toolPath, args, envVars, mode, options.promptPath);
293
360
  const holdMessage = options.holdMessage ?? "[aiwcli] Driver exited. Pane held open.";
294
361
  const paneBody = wrapPaneCommand(
@@ -0,0 +1,70 @@
1
+ import { execFileAsync } from "./subprocess-utils.js";
2
+
3
+ export type TmuxSplitFlag = "-h" | "-v";
4
+
5
+ export interface TmuxPaneInfo {
6
+ paneId: string;
7
+ width: number;
8
+ height: number;
9
+ active: boolean;
10
+ }
11
+
12
+ export interface PlacementResult {
13
+ targetPane: string;
14
+ splitFlag: TmuxSplitFlag;
15
+ }
16
+
17
+ const LIST_PANES_FORMAT = "#{pane_id} #{pane_width} #{pane_height} #{pane_active}";
18
+
19
+ export async function listPanes(tmuxPath: string): Promise<TmuxPaneInfo[]> {
20
+ const result = await execFileAsync(tmuxPath, ["list-panes", "-F", LIST_PANES_FORMAT], {
21
+ timeout: 3000,
22
+ });
23
+ if (result.exitCode !== 0) return [];
24
+
25
+ const panes: TmuxPaneInfo[] = [];
26
+ for (const rawLine of result.stdout.split(/\r?\n/)) {
27
+ const line = rawLine.trim();
28
+ if (!line) continue;
29
+
30
+ const parts = line.split(/\s+/);
31
+ if (parts.length < 4) continue;
32
+
33
+ const paneId = parts[0] ?? "";
34
+ const width = Number.parseInt(parts[1] ?? "", 10);
35
+ const height = Number.parseInt(parts[2] ?? "", 10);
36
+ const activeRaw = parts[3] ?? "";
37
+
38
+ if (!paneId || !Number.isFinite(width) || !Number.isFinite(height)) continue;
39
+ panes.push({
40
+ paneId,
41
+ width,
42
+ height,
43
+ active: activeRaw === "1",
44
+ });
45
+ }
46
+
47
+ return panes;
48
+ }
49
+
50
+ export function findBestSplit(panes: TmuxPaneInfo[]): PlacementResult | null {
51
+ if (panes.length === 0) return null;
52
+
53
+ let best = panes[0];
54
+ let bestArea = best.width * best.height;
55
+
56
+ for (let i = 1; i < panes.length; i++) {
57
+ const pane = panes[i];
58
+ if (!pane) continue;
59
+ const area = pane.width * pane.height;
60
+ if (area > bestArea) {
61
+ best = pane;
62
+ bestArea = area;
63
+ }
64
+ }
65
+
66
+ return {
67
+ targetPane: best.paneId,
68
+ splitFlag: best.width >= best.height ? "-h" : "-v",
69
+ };
70
+ }
@@ -139,9 +139,12 @@ function migrateContextJson(contextId: string, projectRoot?: string): ContextSta
139
139
  plan_signature: null,
140
140
  plan_id: null,
141
141
  plan_anchors: [],
142
+ plan_hash_consumed: null,
142
143
  plan_consumed: false,
143
144
  handoff_path: inFlight.handoff_path ?? null,
144
145
  handoff_consumed: false,
146
+ work_consumed: false,
147
+ next_artifact_type: null,
145
148
  session_ids: sessionIds,
146
149
  last_session: null,
147
150
  tasks: [],
@@ -52,7 +52,7 @@ if (!fs.existsSync(fullPath)) {
52
52
  process.exit(1);
53
53
  }
54
54
 
55
- const result = Bun.spawnSync(["bun", fullPath], {
55
+ const result = Bun.spawnSync(["bun", fullPath, ...process.argv.slice(3)], {
56
56
  stdin: "inherit",
57
57
  stdout: "inherit",
58
58
  stderr: "inherit",
@@ -13,14 +13,15 @@ import { execFileSync } from "node:child_process";
13
13
  import * as fs from "node:fs";
14
14
  import { homedir } from "node:os";
15
15
  import * as path from "node:path";
16
+ import type { ContextState } from "../lib-ts/types.js";
16
17
 
17
18
  // PAI infrastructure imports — graceful fallback when libs aren't available
18
19
  let CONTEXT_BASELINE_TOKENS = 22_600;
19
- let getContextBySessionId: (id: string) => Record<string, unknown> | null =
20
+ let getContextBySessionId: (id: string, root?: string) => ContextState | null =
20
21
  () => null;
21
- let getContext: (id: string) => Record<string, unknown> | null = () => null;
22
- let loadState: (id: string) => Record<string, unknown> | null = () => null;
23
- let saveState: (id: string, state: unknown) => void = () => {};
22
+ let getContext: (id: string, root?: string) => ContextState | null = () => null;
23
+ let loadState: (id: string, root?: string) => ContextState | null = () => null;
24
+ let saveState: (id: string, state: ContextState) => void = () => {};
24
25
  let findLatestPlan: (contextId: string) => string | null = () => null;
25
26
 
26
27
  try {
@@ -619,7 +620,7 @@ function findActivePlanFile(): string | null {
619
620
  function renderContextManager(
620
621
  mode: string,
621
622
  contextId: string,
622
- contextState: Record<string, unknown> | null,
623
+ contextState: ContextState | null,
623
624
  ): string {
624
625
  // Strip YYMMDD-HHMM- timestamp prefix from context ID for display
625
626
  let displayId = contextId.replace(/^\d{6}-\d{4}-/, "");
@@ -649,7 +650,7 @@ function renderContextManager(
649
650
  if (isPlanning) {
650
651
  const label = mode === "nano" ? "Plan" : "Planning";
651
652
  modeBadge = ` ${SLATE_600}\u2502${RESET} ${CTX_SECONDARY}Mode:${RESET} ${AMBER}${label}${RESET}`;
652
- } else if (stateMode === "has_plan") {
653
+ } else if (stateMode === "has_staged_work") {
653
654
  const label = mode === "nano" ? "Ready" : "Plan Ready";
654
655
  modeBadge = ` ${SLATE_600}\u2502${RESET} ${CTX_SECONDARY}Mode:${RESET} ${EMERALD}${label}${RESET}`;
655
656
  } else if (stateMode === "active") {
@@ -663,7 +664,7 @@ function renderContextManager(
663
664
  planFilePath = activePlanFile;
664
665
  } else if (statePlanPath) {
665
666
  planFilePath = statePlanPath;
666
- } else if (stateMode === "has_plan" || stateMode === "active") {
667
+ } else if (stateMode === "has_staged_work" || stateMode === "active") {
667
668
  try {
668
669
  planFilePath = findLatestPlan(contextId) ?? null;
669
670
  } catch {
@@ -746,7 +747,7 @@ function resolveContextId(sessionId: string): string | null {
746
747
  const context = getContextBySessionId(sessionId);
747
748
  if (context) {
748
749
  if (!cache.sessions) cache.sessions = {};
749
- const ctxId = (context as Record<string, unknown>).id as string;
750
+ const ctxId = context.id;
750
751
  cache.sessions[sessionId] = { context_id: ctxId };
751
752
  saveCache(cache);
752
753
  return ctxId;
@@ -759,9 +760,9 @@ function resolveContextId(sessionId: string): string | null {
759
760
  return null;
760
761
  }
761
762
 
762
- function loadContextState(contextId: string): Record<string, unknown> | null {
763
+ function loadContextState(contextId: string): ContextState | null {
763
764
  try {
764
- return loadState(contextId) as Record<string, unknown> | null;
765
+ return loadState(contextId);
765
766
  } catch {
766
767
  return null;
767
768
  }
@@ -772,12 +773,12 @@ function writeContextWindow(
772
773
  contextWindowData: Record<string, unknown>,
773
774
  ): void {
774
775
  try {
775
- const state = getContext(contextId) as Record<string, unknown> | null;
776
+ const state = getContext(contextId);
776
777
  if (state) {
777
- if (!state.last_session) state.last_session = {};
778
- state.last_session.context_remaining_pct =
778
+ if (!state.last_session) state.last_session = { session_id: undefined, saved_at: undefined, save_reason: undefined, transcript_path: undefined };
779
+ (state.last_session as Record<string, unknown>).context_remaining_pct =
779
780
  contextWindowData.remaining_percentage;
780
- saveState(contextId, state as unknown);
781
+ saveState(contextId, state);
781
782
  }
782
783
  } catch {
783
784
  /* ignore */
@@ -788,11 +789,28 @@ function writeContextWindow(
788
789
  // Main
789
790
  // ---------------------------------------------------------------------------
790
791
 
792
+ /** Shape of the JSON payload piped to status_line.ts via stdin */
793
+ interface StatusLineInput {
794
+ session_id?: string;
795
+ model?: { display_name?: string };
796
+ workspace?: { project_dir?: string };
797
+ context_window?: {
798
+ current_usage?: {
799
+ cache_read_input_tokens?: number;
800
+ input_tokens?: number;
801
+ cache_creation_input_tokens?: number;
802
+ output_tokens?: number;
803
+ };
804
+ context_window_size?: number;
805
+ used_percentage?: number;
806
+ };
807
+ }
808
+
791
809
  function main(): void {
792
810
  // Read JSON from stdin
793
- let inputData: Record<string, unknown>;
811
+ let inputData: StatusLineInput;
794
812
  try {
795
- inputData = JSON.parse(fs.readFileSync(0, "utf-8"));
813
+ inputData = JSON.parse(fs.readFileSync(0, "utf-8")) as StatusLineInput;
796
814
  } catch {
797
815
  inputData = {};
798
816
  }
@@ -804,8 +822,7 @@ function main(): void {
804
822
  // Extract input fields
805
823
  const sessionId = inputData.session_id ?? "";
806
824
  const modelName = inputData.model?.display_name ?? "unknown";
807
- const workspace = inputData.workspace ?? {};
808
- const currentDir: string = workspace.project_dir ?? process.cwd();
825
+ const currentDir: string = inputData.workspace?.project_dir ?? process.cwd();
809
826
  const dirName = path.basename(currentDir);
810
827
 
811
828
  // Context window data
@@ -824,7 +841,7 @@ function main(): void {
824
841
  const contextUsed = totalInput + outputTokens + CONTEXT_BASELINE_TOKENS;
825
842
 
826
843
  if (usedPct !== undefined && usedPct !== null) {
827
- contextPct = Math.floor(usedPct);
844
+ contextPct = Math.floor(usedPct as number);
828
845
  } else {
829
846
  contextPct =
830
847
  contextMax > 0 ? Math.floor((contextUsed * 100) / contextMax) : 0;