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.
- package/dist/templates/_shared/.claude/skills/codex/SKILL.md +19 -26
- package/dist/templates/_shared/.codex/workflows/codex.md +11 -0
- package/dist/templates/_shared/lib-ts/agent-exec/index.ts +2 -0
- package/dist/templates/_shared/lib-ts/agent-exec/structured-output.ts +166 -0
- package/dist/templates/_shared/lib-ts/base/cli-args.ts +4 -0
- package/dist/templates/_shared/lib-ts/base/state-io.ts +1 -1
- package/dist/templates/_shared/lib-ts/base/subprocess-utils.ts +4 -3
- package/dist/templates/_shared/lib-ts/base/tmux-driver.ts +70 -3
- package/dist/templates/_shared/lib-ts/base/tmux-pane-placement.ts +70 -0
- package/dist/templates/_shared/lib-ts/context/context-store.ts +3 -0
- package/dist/templates/_shared/scripts/resolve-run.ts +1 -1
- package/dist/templates/_shared/scripts/status_line.ts +36 -19
- package/dist/templates/_shared/skills/codex/CLAUDE.md +77 -0
- package/dist/templates/_shared/skills/codex/SKILL.md +71 -0
- package/dist/templates/_shared/skills/codex/lib/codex-watcher.ts +226 -0
- package/dist/templates/_shared/skills/{prompt-codex → codex}/scripts/launch-codex.ts +175 -8
- package/dist/templates/_shared/skills/codex/scripts/watch-codex.ts +42 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/cli-output-parser.ts +9 -133
- package/dist/templates/cc-native/_cc-native/lib-ts/settings.ts +118 -42
- package/dist/templates/cc-native/_cc-native/lib-ts/state.ts +1 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/types.ts +61 -0
- package/dist/templates/cc-native/_cc-native/plan-review/lib/agent-selection.ts +5 -4
- package/dist/templates/cc-native/_cc-native/plan-review/lib/orchestrator.ts +4 -4
- package/dist/templates/cc-native/_cc-native/plan-review/lib/review-pipeline.ts +16 -13
- package/dist/templates/cc-native/_cc-native/plan-review/lib/reviewers/providers/orchestrator-claude-agent.ts +54 -23
- package/oclif.manifest.json +1 -1
- package/package.json +1 -1
- package/dist/templates/_shared/.claude/skills/codex/prompt.md +0 -10
- package/dist/templates/_shared/skills/prompt-codex/CLAUDE.md +0 -46
|
@@ -1,42 +1,35 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: codex
|
|
3
|
-
description:
|
|
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
|
-
|
|
7
|
+
Read `.aiwcli/_shared/skills/codex/SKILL.md` for delegation patterns and examples.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
## Role
|
|
10
10
|
|
|
11
|
-
|
|
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
|
-
|
|
13
|
+
## Command
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
```
|
|
16
|
+
bun ~/.aiwcli/bin/resolve-run.ts .aiwcli/_shared/skills/codex/scripts/launch-codex.ts [flags] <mode>
|
|
17
|
+
```
|
|
16
18
|
|
|
17
|
-
|
|
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
|
-
|
|
21
|
+
**Modes:** `plan` | `--file <path>` | `<inline text...>`
|
|
23
22
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
##
|
|
29
|
+
## Delegation Decision
|
|
34
30
|
|
|
35
|
-
-
|
|
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
|
-
|
|
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
|
-
-
|
|
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: "
|
|
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
|
-
|
|
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?:
|
|
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
|
|
291
|
-
const
|
|
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: [],
|
|
@@ -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) =>
|
|
20
|
+
let getContextBySessionId: (id: string, root?: string) => ContextState | null =
|
|
20
21
|
() => null;
|
|
21
|
-
let getContext: (id: string) =>
|
|
22
|
-
let loadState: (id: string) =>
|
|
23
|
-
let saveState: (id: string, state:
|
|
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:
|
|
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 === "
|
|
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 === "
|
|
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 =
|
|
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):
|
|
763
|
+
function loadContextState(contextId: string): ContextState | null {
|
|
763
764
|
try {
|
|
764
|
-
return loadState(contextId)
|
|
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)
|
|
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
|
|
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:
|
|
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
|
|
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;
|