pi-ca-leash 0.10.0

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 (138) hide show
  1. package/AGENTS.md +77 -0
  2. package/ARCHITECTURE.md +290 -0
  3. package/CHANGELOG.md +158 -0
  4. package/DEVELOPMENT.md +197 -0
  5. package/KNOWN_LIMITS.md +80 -0
  6. package/LICENSE +21 -0
  7. package/README.md +288 -0
  8. package/extensions/backend-tool-actions.test.ts +59 -0
  9. package/extensions/backend-tool-actions.ts +31 -0
  10. package/extensions/command-drivers.test.ts +37 -0
  11. package/extensions/command-drivers.ts +126 -0
  12. package/extensions/command-parity.test.ts +560 -0
  13. package/extensions/command-visibility.test.ts +21 -0
  14. package/extensions/command-visibility.ts +10 -0
  15. package/extensions/index.ts +3218 -0
  16. package/extensions/llm-tools.test.ts +537 -0
  17. package/extensions/model-catalog.test.ts +34 -0
  18. package/extensions/model-catalog.ts +173 -0
  19. package/extensions/peer-history.test.ts +141 -0
  20. package/extensions/peer-history.ts +90 -0
  21. package/extensions/peer-naming.test.ts +25 -0
  22. package/extensions/peer-naming.ts +129 -0
  23. package/extensions/peer-relay.test.ts +122 -0
  24. package/extensions/peer-relay.ts +83 -0
  25. package/extensions/peer-ux.test.ts +239 -0
  26. package/extensions/peer-ux.ts +327 -0
  27. package/extensions/persistence.test.ts +68 -0
  28. package/extensions/persistence.ts +67 -0
  29. package/extensions/prompts/extension-log-tool.md +5 -0
  30. package/extensions/prompts/peer-ask-tool.md +5 -0
  31. package/extensions/prompts/peer-bridge-system.md +4 -0
  32. package/extensions/prompts/peer-history-tool.md +3 -0
  33. package/extensions/prompts/peer-init-user-help.md +11 -0
  34. package/extensions/prompts/peer-init.md +17 -0
  35. package/extensions/prompts/peer-interrupt-tool.md +2 -0
  36. package/extensions/prompts/peer-list-tool.md +3 -0
  37. package/extensions/prompts/peer-no-babysitting.md +6 -0
  38. package/extensions/prompts/peer-send-tool.md +5 -0
  39. package/extensions/prompts/peer-start-tool.md +7 -0
  40. package/extensions/prompts/peer-stop-tool.md +3 -0
  41. package/extensions/prompts/runtime-models-tool.md +6 -0
  42. package/extensions/prompts/subagent-list-tool.md +3 -0
  43. package/extensions/prompts/subagent-run-tool.md +6 -0
  44. package/extensions/prompts/subagent-status-tool.md +2 -0
  45. package/extensions/prompts/team-list-tool.md +2 -0
  46. package/extensions/prompts/team-message-tool.md +2 -0
  47. package/extensions/prompts/team-spawn-tool.md +5 -0
  48. package/extensions/prompts/team-stop-tool.md +2 -0
  49. package/extensions/prompts/team-task-tool.md +3 -0
  50. package/extensions/prompts.ts +41 -0
  51. package/extensions/runtime-driver.test.ts +38 -0
  52. package/extensions/runtime-driver.ts +33 -0
  53. package/extensions/runtime-safety.test.ts +21 -0
  54. package/extensions/runtime-safety.ts +49 -0
  55. package/extensions/support.test.ts +144 -0
  56. package/extensions/support.ts +205 -0
  57. package/extensions/tool-inputs.test.ts +45 -0
  58. package/extensions/tool-inputs.ts +79 -0
  59. package/node_modules/@pi-claude-code-agent/intercom-bridge/dist/bridge.d.ts +48 -0
  60. package/node_modules/@pi-claude-code-agent/intercom-bridge/dist/bridge.js +406 -0
  61. package/node_modules/@pi-claude-code-agent/intercom-bridge/dist/bridge.js.map +1 -0
  62. package/node_modules/@pi-claude-code-agent/intercom-bridge/dist/cli.d.ts +2 -0
  63. package/node_modules/@pi-claude-code-agent/intercom-bridge/dist/cli.js +18 -0
  64. package/node_modules/@pi-claude-code-agent/intercom-bridge/dist/cli.js.map +1 -0
  65. package/node_modules/@pi-claude-code-agent/intercom-bridge/dist/index.d.ts +5 -0
  66. package/node_modules/@pi-claude-code-agent/intercom-bridge/dist/index.js +5 -0
  67. package/node_modules/@pi-claude-code-agent/intercom-bridge/dist/index.js.map +1 -0
  68. package/node_modules/@pi-claude-code-agent/intercom-bridge/dist/persistence.d.ts +12 -0
  69. package/node_modules/@pi-claude-code-agent/intercom-bridge/dist/persistence.js +31 -0
  70. package/node_modules/@pi-claude-code-agent/intercom-bridge/dist/persistence.js.map +1 -0
  71. package/node_modules/@pi-claude-code-agent/intercom-bridge/dist/pi-intercom-transport.d.ts +12 -0
  72. package/node_modules/@pi-claude-code-agent/intercom-bridge/dist/pi-intercom-transport.js +347 -0
  73. package/node_modules/@pi-claude-code-agent/intercom-bridge/dist/pi-intercom-transport.js.map +1 -0
  74. package/node_modules/@pi-claude-code-agent/intercom-bridge/dist/types.d.ts +103 -0
  75. package/node_modules/@pi-claude-code-agent/intercom-bridge/dist/types.js +2 -0
  76. package/node_modules/@pi-claude-code-agent/intercom-bridge/dist/types.js.map +1 -0
  77. package/node_modules/@pi-claude-code-agent/intercom-bridge/package.json +32 -0
  78. package/node_modules/@pi-claude-code-agent/runtime/dist/cli.d.ts +2 -0
  79. package/node_modules/@pi-claude-code-agent/runtime/dist/cli.js +26 -0
  80. package/node_modules/@pi-claude-code-agent/runtime/dist/cli.js.map +1 -0
  81. package/node_modules/@pi-claude-code-agent/runtime/dist/driver-config.d.ts +4 -0
  82. package/node_modules/@pi-claude-code-agent/runtime/dist/driver-config.js +12 -0
  83. package/node_modules/@pi-claude-code-agent/runtime/dist/driver-config.js.map +1 -0
  84. package/node_modules/@pi-claude-code-agent/runtime/dist/drivers/claude-sdk.d.ts +8 -0
  85. package/node_modules/@pi-claude-code-agent/runtime/dist/drivers/claude-sdk.js +320 -0
  86. package/node_modules/@pi-claude-code-agent/runtime/dist/drivers/claude-sdk.js.map +1 -0
  87. package/node_modules/@pi-claude-code-agent/runtime/dist/drivers/codex-cli.d.ts +24 -0
  88. package/node_modules/@pi-claude-code-agent/runtime/dist/drivers/codex-cli.js +266 -0
  89. package/node_modules/@pi-claude-code-agent/runtime/dist/drivers/codex-cli.js.map +1 -0
  90. package/node_modules/@pi-claude-code-agent/runtime/dist/drivers/messages.d.ts +72 -0
  91. package/node_modules/@pi-claude-code-agent/runtime/dist/drivers/messages.js +2 -0
  92. package/node_modules/@pi-claude-code-agent/runtime/dist/drivers/messages.js.map +1 -0
  93. package/node_modules/@pi-claude-code-agent/runtime/dist/index.d.ts +6 -0
  94. package/node_modules/@pi-claude-code-agent/runtime/dist/index.js +5 -0
  95. package/node_modules/@pi-claude-code-agent/runtime/dist/index.js.map +1 -0
  96. package/node_modules/@pi-claude-code-agent/runtime/dist/persistence.d.ts +16 -0
  97. package/node_modules/@pi-claude-code-agent/runtime/dist/persistence.js +94 -0
  98. package/node_modules/@pi-claude-code-agent/runtime/dist/persistence.js.map +1 -0
  99. package/node_modules/@pi-claude-code-agent/runtime/dist/runtime.d.ts +31 -0
  100. package/node_modules/@pi-claude-code-agent/runtime/dist/runtime.js +409 -0
  101. package/node_modules/@pi-claude-code-agent/runtime/dist/runtime.js.map +1 -0
  102. package/node_modules/@pi-claude-code-agent/runtime/dist/types.d.ts +185 -0
  103. package/node_modules/@pi-claude-code-agent/runtime/dist/types.js +2 -0
  104. package/node_modules/@pi-claude-code-agent/runtime/dist/types.js.map +1 -0
  105. package/node_modules/@pi-claude-code-agent/runtime/package.json +32 -0
  106. package/node_modules/@pi-claude-code-agent/subagents-backend/dist/backend.d.ts +34 -0
  107. package/node_modules/@pi-claude-code-agent/subagents-backend/dist/backend.js +327 -0
  108. package/node_modules/@pi-claude-code-agent/subagents-backend/dist/backend.js.map +1 -0
  109. package/node_modules/@pi-claude-code-agent/subagents-backend/dist/cli.d.ts +2 -0
  110. package/node_modules/@pi-claude-code-agent/subagents-backend/dist/cli.js +17 -0
  111. package/node_modules/@pi-claude-code-agent/subagents-backend/dist/cli.js.map +1 -0
  112. package/node_modules/@pi-claude-code-agent/subagents-backend/dist/index.d.ts +4 -0
  113. package/node_modules/@pi-claude-code-agent/subagents-backend/dist/index.js +4 -0
  114. package/node_modules/@pi-claude-code-agent/subagents-backend/dist/index.js.map +1 -0
  115. package/node_modules/@pi-claude-code-agent/subagents-backend/dist/persistence.d.ts +12 -0
  116. package/node_modules/@pi-claude-code-agent/subagents-backend/dist/persistence.js +81 -0
  117. package/node_modules/@pi-claude-code-agent/subagents-backend/dist/persistence.js.map +1 -0
  118. package/node_modules/@pi-claude-code-agent/subagents-backend/dist/types.d.ts +72 -0
  119. package/node_modules/@pi-claude-code-agent/subagents-backend/dist/types.js +2 -0
  120. package/node_modules/@pi-claude-code-agent/subagents-backend/dist/types.js.map +1 -0
  121. package/node_modules/@pi-claude-code-agent/subagents-backend/package.json +32 -0
  122. package/node_modules/@pi-claude-code-agent/teams-backend/dist/backend.d.ts +27 -0
  123. package/node_modules/@pi-claude-code-agent/teams-backend/dist/backend.js +194 -0
  124. package/node_modules/@pi-claude-code-agent/teams-backend/dist/backend.js.map +1 -0
  125. package/node_modules/@pi-claude-code-agent/teams-backend/dist/cli.d.ts +2 -0
  126. package/node_modules/@pi-claude-code-agent/teams-backend/dist/cli.js +21 -0
  127. package/node_modules/@pi-claude-code-agent/teams-backend/dist/cli.js.map +1 -0
  128. package/node_modules/@pi-claude-code-agent/teams-backend/dist/index.d.ts +4 -0
  129. package/node_modules/@pi-claude-code-agent/teams-backend/dist/index.js +4 -0
  130. package/node_modules/@pi-claude-code-agent/teams-backend/dist/index.js.map +1 -0
  131. package/node_modules/@pi-claude-code-agent/teams-backend/dist/persistence.d.ts +8 -0
  132. package/node_modules/@pi-claude-code-agent/teams-backend/dist/persistence.js +66 -0
  133. package/node_modules/@pi-claude-code-agent/teams-backend/dist/persistence.js.map +1 -0
  134. package/node_modules/@pi-claude-code-agent/teams-backend/dist/types.d.ts +41 -0
  135. package/node_modules/@pi-claude-code-agent/teams-backend/dist/types.js +2 -0
  136. package/node_modules/@pi-claude-code-agent/teams-backend/dist/types.js.map +1 -0
  137. package/node_modules/@pi-claude-code-agent/teams-backend/package.json +33 -0
  138. package/package.json +98 -0
@@ -0,0 +1,537 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
4
+ import { mkdtemp } from "node:fs/promises";
5
+ import { tmpdir } from "node:os";
6
+ import { dirname, join, resolve } from "node:path";
7
+ import { fileURLToPath, pathToFileURL } from "node:url";
8
+ import { ADVANCED_COMMANDS_ENV } from "./command-visibility.ts";
9
+
10
+ const __dirname = dirname(fileURLToPath(import.meta.url));
11
+ const repoRoot = resolve(__dirname, "..");
12
+ const extensionModuleUrl = pathToFileURL(join(__dirname, "index.ts")).href;
13
+
14
+ const TOOL_NAMES = [
15
+ "runtime_models",
16
+ "extension_log",
17
+ "peer_start",
18
+ "peer_list",
19
+ "peer_history",
20
+ "peer_ask",
21
+ "peer_send",
22
+ "peer_interrupt",
23
+ "peer_stop",
24
+ ];
25
+
26
+ const ADVANCED_TOOL_NAMES = [
27
+ "subagent_run",
28
+ "subagent_list",
29
+ "subagent_status",
30
+ "team_spawn",
31
+ "team_task",
32
+ "team_message",
33
+ "team_list",
34
+ "team_stop",
35
+ ];
36
+
37
+ async function ensurePiTuiStub(): Promise<void> {
38
+ const dir = join(repoRoot, "node_modules", "@mariozechner", "pi-tui");
39
+ await mkdir(dir, { recursive: true });
40
+ await writeFile(join(dir, "package.json"), `${JSON.stringify({
41
+ name: "@mariozechner/pi-tui",
42
+ type: "module",
43
+ exports: "./index.js",
44
+ }, null, 2)}\n`, "utf8");
45
+ await writeFile(join(dir, "index.js"), [
46
+ "export class Box { constructor(...args) { this.args = args; this.children = []; } addChild(child) { this.children.push(child); } }",
47
+ "export class Text { constructor(text) { this.text = text; } }",
48
+ "export function truncateToWidth(text, width, suffix = '…') {",
49
+ " const value = String(text);",
50
+ " return value.length <= width ? value : value.slice(0, Math.max(0, width - suffix.length)) + suffix;",
51
+ "}",
52
+ "export function visibleWidth(text) { return String(text).length; }",
53
+ "",
54
+ ].join("\n"), "utf8");
55
+ }
56
+
57
+ async function createCodexStub(delayMs = 0): Promise<string> {
58
+ const dir = await mkdtemp(join(tmpdir(), "pi-ca-leash-codex-stub-"));
59
+ const executable = join(dir, "codex");
60
+ await writeFile(executable, [
61
+ "#!/usr/bin/env node",
62
+ "const args = process.argv.slice(2);",
63
+ "const resume = args[1] === 'resume';",
64
+ "const prompt = args.at(-1) ?? '';",
65
+ "const sessionId = resume ? args.at(-2) : `thread-${Math.random().toString(16).slice(2)}`;",
66
+ "",
67
+ "function buildReply(text) {",
68
+ " if (text.includes('[intercom kind=ask from=team-board]') && text.includes('Ship it')) {",
69
+ " return 'DONE: shipped';",
70
+ " }",
71
+ " if (text.includes('[intercom kind=ask from=team-board]')) {",
72
+ " return `teammate:${text}`;",
73
+ " }",
74
+ " if (text.includes('[intercom kind=ask from=team-chat]')) {",
75
+ " return `teammate:${text}`;",
76
+ " }",
77
+ " if (text.includes('[intercom kind=ask from=pi-main-agent]')) {",
78
+ " return `peer:${text}`;",
79
+ " }",
80
+ " return `assistant:${text}`;",
81
+ "}",
82
+ "",
83
+ `const delayMs = ${delayMs};`,
84
+ "const reply = buildReply(prompt);",
85
+ "if (delayMs > 0) {",
86
+ " await new Promise((resolve) => setTimeout(resolve, delayMs));",
87
+ "}",
88
+ "console.log(JSON.stringify({ type: 'thread.started', thread_id: sessionId }));",
89
+ "console.log(JSON.stringify({ type: 'item.completed', item: { type: 'assistant_message', text: reply } }));",
90
+ "console.log(JSON.stringify({ type: 'turn.completed', summary: `done:${reply}`, usage: { input_tokens: 1, output_tokens: 1 } }));",
91
+ "",
92
+ ].join("\n"), "utf8");
93
+ await chmod(executable, 0o755);
94
+ return executable;
95
+ }
96
+
97
+ interface FakeTool {
98
+ name: string;
99
+ promptSnippet?: string;
100
+ promptGuidelines?: string[];
101
+ execute: (toolCallId: string, params: unknown, signal: AbortSignal | undefined, onUpdate: unknown, ctx: unknown) => Promise<any>;
102
+ }
103
+
104
+ async function loadExtensionHarness(defaultDriver: "claude-sdk" | "codex-cli", options: { codexDelayMs?: number; advanced?: boolean } = {}) {
105
+ await ensurePiTuiStub();
106
+ const codexExecutable = await createCodexStub(options.codexDelayMs ?? 0);
107
+ const tempCwd = await mkdtemp(join(tmpdir(), "pi-ca-leash-extension-tools-"));
108
+ const previousCwd = process.cwd();
109
+ const previousDefaultDriver = process.env.PI_CLAUDE_RUNTIME_DRIVER;
110
+ const previousCodexExecutable = process.env.CODEX_CLI_EXECUTABLE;
111
+ const previousAdvanced = process.env[ADVANCED_COMMANDS_ENV];
112
+
113
+ process.env.PI_CLAUDE_RUNTIME_DRIVER = defaultDriver;
114
+ process.env.CODEX_CLI_EXECUTABLE = codexExecutable;
115
+ process.env[ADVANCED_COMMANDS_ENV] = options.advanced ? "1" : "0";
116
+ process.chdir(tempCwd);
117
+
118
+ const tools = new Map<string, FakeTool>();
119
+ const lifecycle = new Map<string, (...args: any[]) => any>();
120
+ const sentMessages: Array<{ message: unknown; options: unknown }> = [];
121
+ const userMessages: Array<{ message: unknown; options: unknown }> = [];
122
+ const notifications: Array<{ message: string; type?: "info" | "warning" | "error" }> = [];
123
+ const ctx = {
124
+ ui: {
125
+ setStatus() {},
126
+ setWidget() {},
127
+ notify(message: string, type?: "info" | "warning" | "error") { notifications.push({ message, type }); },
128
+ },
129
+ };
130
+
131
+ const pi = {
132
+ registerTool(tool: FakeTool) {
133
+ tools.set(tool.name, tool);
134
+ },
135
+ registerCommand() {},
136
+ registerMessageRenderer() {},
137
+ on(event: string, handler: (...args: any[]) => any) {
138
+ lifecycle.set(event, handler);
139
+ },
140
+ sendMessage(message: unknown, options: unknown) {
141
+ sentMessages.push({ message, options });
142
+ },
143
+ sendUserMessage(message: unknown, options: unknown) {
144
+ userMessages.push({ message, options });
145
+ },
146
+ };
147
+
148
+ const module = await import(extensionModuleUrl);
149
+ await module.default(pi as never);
150
+
151
+ return {
152
+ tools,
153
+ cwd: tempCwd,
154
+ sentMessages,
155
+ userMessages,
156
+ notifications,
157
+ async execute(name: string, params: unknown) {
158
+ const tool = tools.get(name);
159
+ assert.ok(tool, `Missing tool ${name}`);
160
+ return tool.execute(`tool-${name}`, params, undefined, undefined, ctx);
161
+ },
162
+ async close() {
163
+ await lifecycle.get("session_shutdown")?.();
164
+ process.chdir(previousCwd);
165
+ if (previousDefaultDriver == null) {
166
+ delete process.env.PI_CLAUDE_RUNTIME_DRIVER;
167
+ } else {
168
+ process.env.PI_CLAUDE_RUNTIME_DRIVER = previousDefaultDriver;
169
+ }
170
+ if (previousCodexExecutable == null) {
171
+ delete process.env.CODEX_CLI_EXECUTABLE;
172
+ } else {
173
+ process.env.CODEX_CLI_EXECUTABLE = previousCodexExecutable;
174
+ }
175
+ if (previousAdvanced == null) {
176
+ delete process.env[ADVANCED_COMMANDS_ENV];
177
+ } else {
178
+ process.env[ADVANCED_COMMANDS_ENV] = previousAdvanced;
179
+ }
180
+ },
181
+ };
182
+ }
183
+
184
+ test("extension registers expected LLM-callable tools", async () => {
185
+ const harness = await loadExtensionHarness("codex-cli");
186
+ try {
187
+ assert.deepEqual([...harness.tools.keys()].sort(), [...TOOL_NAMES].sort());
188
+ } finally {
189
+ await harness.close();
190
+ }
191
+ });
192
+
193
+ test("peer tools keep long one-time init guidance out of repeated tool prompts", async () => {
194
+ const harness = await loadExtensionHarness("codex-cli");
195
+ try {
196
+ const peerStart = harness.tools.get("peer_start");
197
+ assert.ok(peerStart, "Expected peer_start tool");
198
+ const guidelines = peerStart.promptGuidelines?.join("\n") ?? "";
199
+ assert.match(guidelines, /Use `peer_start` when you want a reusable long-lived peer/);
200
+ assert.doesNotMatch(guidelines, /orchestrator in the driver's seat/);
201
+ assert.doesNotMatch(guidelines, /multiple specialized peers at the same time/);
202
+
203
+ const extensionLog = harness.tools.get("extension_log");
204
+ assert.ok(extensionLog, "Expected extension_log tool");
205
+ assert.doesNotMatch(extensionLog.promptGuidelines?.join("\n") ?? "", /orchestrator in the driver's seat/);
206
+ } finally {
207
+ await harness.close();
208
+ }
209
+ });
210
+
211
+ test("advanced backend LLM tools are hidden unless advanced commands are enabled", async () => {
212
+ const defaultHarness = await loadExtensionHarness("codex-cli");
213
+ try {
214
+ for (const name of ADVANCED_TOOL_NAMES) {
215
+ assert.equal(defaultHarness.tools.has(name), false);
216
+ }
217
+ } finally {
218
+ await defaultHarness.close();
219
+ }
220
+
221
+ const advancedHarness = await loadExtensionHarness("codex-cli", { advanced: true });
222
+ try {
223
+ assert.deepEqual([...advancedHarness.tools.keys()].sort(), [...TOOL_NAMES, ...ADVANCED_TOOL_NAMES].sort());
224
+ } finally {
225
+ await advancedHarness.close();
226
+ }
227
+ });
228
+
229
+ test("runtime_models exposes driver-specific Lanista catalog", async () => {
230
+ const harness = await loadExtensionHarness("codex-cli");
231
+ try {
232
+ const listed = await harness.execute("runtime_models", { driver: "codex-cli" });
233
+ const text = String(listed.content?.[0]?.text ?? "");
234
+ assert.match(text, /codex-cli models/);
235
+ assert.match(text, /default gpt-5\.5/);
236
+ assert.match(text, /best for/);
237
+ assert.match(text, /context window=input token capacity/);
238
+ assert.match(text, /gpt-5\.4-mini/);
239
+ assert.doesNotMatch(text, /gpt-5\.1-codex-max/);
240
+
241
+ const verbose = await harness.execute("runtime_models", { driver: "codex-cli", verbose: true });
242
+ const verboseText = String(verbose.content?.[0]?.text ?? "");
243
+ assert.match(verboseText, /max output/);
244
+ assert.match(verboseText, /gpt-5\.1-codex-max/);
245
+ assert.equal(listed.details.catalogs[0].driver, "codex-cli");
246
+ } finally {
247
+ await harness.close();
248
+ }
249
+ });
250
+
251
+ test("extension_log appends structured local feedback", async () => {
252
+ const harness = await loadExtensionHarness("codex-cli");
253
+ try {
254
+ const result = await harness.execute("extension_log", {
255
+ category: "ux",
256
+ severity: "high",
257
+ summary: "Agent guide mixed operator controls into delegation policy.",
258
+ observed: "The guide mentioned dashboard and list surfaces.",
259
+ expected: "Agent-facing guidance should focus on delegation behavior.",
260
+ suggestedFix: "Keep operator commands in help and agent policy in the init guide.",
261
+ relatedTool: "peer_init",
262
+ files: ["extensions/prompts/peer-init.md"],
263
+ });
264
+
265
+ const text = String(result.content?.[0]?.text ?? "");
266
+ assert.match(text, /\.pi-ca-leash\/log\.md/);
267
+ const log = await readFile(join(harness.cwd, ".pi-ca-leash", "log.md"), "utf8");
268
+ assert.match(log, /## .* - ux - high/);
269
+ assert.match(log, /Summary: Agent guide mixed operator controls/);
270
+ assert.match(log, /Observed:\nThe guide mentioned dashboard and list surfaces\./);
271
+ assert.match(log, /- tool: peer_init/);
272
+ assert.match(log, /- file: extensions\/prompts\/peer-init\.md/);
273
+ } finally {
274
+ await harness.close();
275
+ }
276
+ });
277
+
278
+ test("peer_start returns and displays no-babysitting guidance", async () => {
279
+ const harness = await loadExtensionHarness("codex-cli", { codexDelayMs: 150 });
280
+ try {
281
+ const started = await harness.execute("peer_start", { prompt: "Wait briefly, then reply done." });
282
+ const toolText = String(started.content?.[0]?.text ?? "");
283
+ assert.match(toolText, /How to work with this peer:/);
284
+ assert.match(toolText, /Do not poll it with peer_list, peer_history, or repeated peer_ask status checks\./);
285
+ assert.equal(started.details.guidance.includes("Do not poll"), true);
286
+
287
+ const visibleStart = harness.notifications.find((entry) => entry.message.includes("Peer started:"));
288
+ assert.ok(visibleStart, "peer_start should emit operator guidance notification");
289
+ assert.match(visibleStart.message, /Do not poll it with peer_list/);
290
+ } finally {
291
+ await harness.close();
292
+ }
293
+ });
294
+
295
+ test("peer_start resolves model aliases before runtime launch", async () => {
296
+ const harness = await loadExtensionHarness("codex-cli", { codexDelayMs: 150 });
297
+ try {
298
+ const started = await harness.execute("peer_start", { prompt: "Use a fast model.", model: "mini" });
299
+ assert.equal(started.details.requestedModel, "mini");
300
+ assert.equal(started.details.model, "gpt-5.4-mini");
301
+ assert.match(started.details.modelNote, /model alias mini -> model gpt-5\.4-mini/);
302
+ } finally {
303
+ await harness.close();
304
+ }
305
+ });
306
+
307
+ test("peer_ask returns and displays the outgoing prompt", async () => {
308
+ const harness = await loadExtensionHarness("codex-cli");
309
+ try {
310
+ const started = await harness.execute("peer_start", { prompt: "You are a brief worker." });
311
+ const peerName = started.details.peerName;
312
+ let listed = await harness.execute("peer_list", {});
313
+ for (let i = 0; i < 60 && ["busy", "starting"].includes(listed.details.peers[0]?.state); i += 1) {
314
+ await new Promise((resolve) => setTimeout(resolve, 50));
315
+ listed = await harness.execute("peer_list", {});
316
+ }
317
+ assert.equal(["busy", "starting"].includes(listed.details.peers[0]?.state), false, "peer should become idle before peer_ask");
318
+
319
+ const outgoing = "Reply with exactly: peer-visible-prompt";
320
+
321
+ const before = harness.sentMessages.length;
322
+ const noticeBefore = harness.notifications.length;
323
+ const asked = await harness.execute("peer_ask", { name: peerName, message: outgoing });
324
+ const toolText = String(asked.content?.[0]?.text ?? "");
325
+ assert.match(toolText, /sent prompt shown in an operator notification/);
326
+ assert.doesNotMatch(toolText, /sent to peer\n```text/);
327
+ assert.equal(asked.details.message, outgoing);
328
+
329
+ const messages = harness.sentMessages.slice(before);
330
+ assert.equal(messages.length, 0);
331
+ const visibleAsk = harness.notifications.slice(noticeBefore).find((entry) => entry.message.includes(`Sent to peer: ${peerName}`));
332
+ assert.ok(visibleAsk, "peer_ask should emit an outgoing prompt notification");
333
+ assert.match(visibleAsk.message, /Reply with exactly: peer-visible-prompt/);
334
+ } finally {
335
+ await harness.close();
336
+ }
337
+ });
338
+
339
+ test("peer_interrupt returns explicit delivery status", async () => {
340
+ const harness = await loadExtensionHarness("codex-cli", { codexDelayMs: 500 });
341
+ try {
342
+ const started = await harness.execute("peer_start", { prompt: "Wait briefly, then reply done." });
343
+ const interrupted = await harness.execute("peer_interrupt", { name: started.details.peerName });
344
+ const toolText = String(interrupted.content?.[0]?.text ?? "");
345
+
346
+ assert.match(toolText, /Peer interrupt requested:/);
347
+ assert.match(toolText, /signal delivered yes/);
348
+ assert.match(toolText, /reason signalled/);
349
+ assert.match(toolText, /resulting state/);
350
+ assert.match(toolText, /follow-up send/);
351
+ assert.equal(interrupted.details.interruptDelivered, true);
352
+ assert.equal(interrupted.details.interruptReason, "signalled");
353
+ assert.equal(interrupted.details.signal, "SIGINT");
354
+ assert.equal(["busy", "interrupted"].includes(interrupted.details.peerState), true);
355
+ assert.equal(typeof interrupted.details.canSendImmediately, "boolean");
356
+ } finally {
357
+ await harness.close();
358
+ }
359
+ });
360
+
361
+ test("peer tools honor codex default driver through extension execute handlers", async () => {
362
+ const harness = await loadExtensionHarness("codex-cli", { codexDelayMs: 150 });
363
+ try {
364
+ const began = Date.now();
365
+ const started = await harness.execute("peer_start", { prompt: "Review auth flow and reply briefly." });
366
+ assert.ok(Date.now() - began < 140, "peer_start tool should not wait for peer idle");
367
+ assert.equal(started.details.driver, "codex-cli");
368
+ assert.equal(started.details.cwd, process.cwd());
369
+
370
+ let listed = await harness.execute("peer_list", {});
371
+ assert.equal(listed.details.peers.length, 1);
372
+ assert.equal(listed.details.peers[0].driver, "codex-cli");
373
+
374
+ for (let i = 0; i < 60 && ["busy", "starting"].includes(listed.details.peers[0]?.state); i += 1) {
375
+ await new Promise((resolve) => setTimeout(resolve, 50));
376
+ listed = await harness.execute("peer_list", {});
377
+ }
378
+ assert.equal(["busy", "starting"].includes(listed.details.peers[0]?.state), false, "peer should become idle before peer_ask");
379
+
380
+ const history = await harness.execute("peer_history", { name: started.details.peerName });
381
+ assert.equal(history.details.driver, "codex-cli");
382
+
383
+ const asked = await harness.execute("peer_ask", {
384
+ name: started.details.peerName,
385
+ message: "Reply with exactly: peer-ok",
386
+ });
387
+ assert.equal(asked.details.driver, "codex-cli");
388
+ assert.match(asked.details.reply, /^peer:/);
389
+
390
+ const stopped = await harness.execute("peer_stop", { name: started.details.peerName });
391
+ assert.equal(stopped.details.driver, "codex-cli");
392
+ assert.equal(stopped.details.state, "stopped");
393
+ } finally {
394
+ await harness.close();
395
+ }
396
+ });
397
+
398
+ test("peer_stop can bulk-stop all peers only with explicit confirmation", async () => {
399
+ const harness = await loadExtensionHarness("codex-cli");
400
+ try {
401
+ await harness.execute("peer_start", { prompt: "Review auth flow and reply briefly." });
402
+ await harness.execute("peer_start", { name: "tester", prompt: "Verify login flow and reply briefly." });
403
+
404
+ await assert.rejects(
405
+ harness.execute("peer_stop", { all: true }),
406
+ /confirmAll=true/,
407
+ );
408
+
409
+ const stopped = await harness.execute("peer_stop", { all: true, confirmAll: true });
410
+ assert.equal(stopped.details.count, 2);
411
+ assert.equal(stopped.details.stoppedPeers.length, 2);
412
+
413
+ const listed = await harness.execute("peer_list", {});
414
+ assert.equal(listed.details.peers.length, 0);
415
+ } finally {
416
+ await harness.close();
417
+ }
418
+ });
419
+
420
+ test("subagent tools honor codex extension default when driver omitted", async () => {
421
+ const harness = await loadExtensionHarness("codex-cli", { advanced: true });
422
+ try {
423
+ const started = await harness.execute("subagent_run", {
424
+ task: "Reply with exactly: subagent-ok",
425
+ model: "o4-mini",
426
+ });
427
+ assert.equal(started.details.driver, "codex-cli");
428
+ assert.equal(started.details.state, "completed");
429
+
430
+ const listed = await harness.execute("subagent_list", {});
431
+ assert.equal(listed.details.runs.length, 1);
432
+ assert.equal(listed.details.runs[0].driver, "codex-cli");
433
+
434
+ const status = await harness.execute("subagent_status", { runId: started.details.runId.slice(0, 8) });
435
+ assert.equal(status.details.driver, "codex-cli");
436
+ assert.equal(status.details.state, "completed");
437
+ assert.equal(status.details.runId, started.details.runId);
438
+ } finally {
439
+ await harness.close();
440
+ }
441
+ });
442
+
443
+ test("subagent tools thread explicit codex driver even when extension default stays claude", async () => {
444
+ const harness = await loadExtensionHarness("claude-sdk", { advanced: true });
445
+ try {
446
+ const started = await harness.execute("subagent_run", {
447
+ task: "Reply with exactly: subagent-ok",
448
+ driver: "codex-cli",
449
+ model: "o4-mini",
450
+ });
451
+ assert.equal(started.details.driver, "codex-cli");
452
+ assert.equal(started.details.state, "completed");
453
+
454
+ const listed = await harness.execute("subagent_list", {});
455
+ assert.equal(listed.details.runs.length, 1);
456
+ assert.equal(listed.details.runs[0].driver, "codex-cli");
457
+
458
+ const status = await harness.execute("subagent_status", { runId: started.details.runId });
459
+ assert.equal(status.details.driver, "codex-cli");
460
+ assert.equal(status.details.state, "completed");
461
+ } finally {
462
+ await harness.close();
463
+ }
464
+ });
465
+
466
+ test("team tools honor codex extension default when driver omitted", async () => {
467
+ const harness = await loadExtensionHarness("codex-cli", { advanced: true });
468
+ try {
469
+ const spawned = await harness.execute("team_spawn", {
470
+ name: "worker",
471
+ prompt: "You are teammate. Reply briefly.",
472
+ });
473
+ assert.equal(spawned.details.driver, "codex-cli");
474
+
475
+ const task = await harness.execute("team_task", {
476
+ name: "worker",
477
+ title: "Investigate",
478
+ details: "Look at logs",
479
+ });
480
+ assert.equal(task.details.teammate.driver, "codex-cli");
481
+ assert.equal(task.details.task.state, "in_progress");
482
+
483
+ const message = await harness.execute("team_message", {
484
+ name: "worker",
485
+ message: "Need update",
486
+ });
487
+ assert.equal(message.details.teammate.driver, "codex-cli");
488
+ assert.match(message.details.reply, /^teammate:/);
489
+
490
+ const listed = await harness.execute("team_list", {});
491
+ assert.equal(listed.details.teammates.length, 1);
492
+ assert.equal(listed.details.teammates[0].driver, "codex-cli");
493
+
494
+ const stopped = await harness.execute("team_stop", { name: "worker" });
495
+ assert.equal(stopped.details.driver, "codex-cli");
496
+ assert.equal(stopped.details.state, "stopped");
497
+ } finally {
498
+ await harness.close();
499
+ }
500
+ });
501
+
502
+ test("team tools thread and preserve explicit codex driver through extension execute handlers", async () => {
503
+ const harness = await loadExtensionHarness("claude-sdk", { advanced: true });
504
+ try {
505
+ const spawned = await harness.execute("team_spawn", {
506
+ name: "worker",
507
+ prompt: "You are teammate. Reply briefly.",
508
+ driver: "codex-cli",
509
+ });
510
+ assert.equal(spawned.details.driver, "codex-cli");
511
+
512
+ const task = await harness.execute("team_task", {
513
+ name: "worker",
514
+ title: "Investigate",
515
+ details: "Look at logs",
516
+ });
517
+ assert.equal(task.details.teammate.driver, "codex-cli");
518
+ assert.equal(task.details.task.state, "in_progress");
519
+
520
+ const message = await harness.execute("team_message", {
521
+ name: "worker",
522
+ message: "Need update",
523
+ });
524
+ assert.equal(message.details.teammate.driver, "codex-cli");
525
+ assert.match(message.details.reply, /^teammate:/);
526
+
527
+ const listed = await harness.execute("team_list", {});
528
+ assert.equal(listed.details.teammates.length, 1);
529
+ assert.equal(listed.details.teammates[0].driver, "codex-cli");
530
+
531
+ const stopped = await harness.execute("team_stop", { name: "worker" });
532
+ assert.equal(stopped.details.driver, "codex-cli");
533
+ assert.equal(stopped.details.state, "stopped");
534
+ } finally {
535
+ await harness.close();
536
+ }
537
+ });
@@ -0,0 +1,34 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { describeModelSelection, findRuntimeModel, modelCatalogsForDriver, resolveRuntimeModelSelection } from "./model-catalog.ts";
4
+
5
+ test("model catalog maps runtime drivers to Lanista provider snapshots", () => {
6
+ const claude = modelCatalogsForDriver("claude-sdk")[0]!;
7
+ const codex = modelCatalogsForDriver("codex-cli")[0]!;
8
+
9
+ assert.equal(claude.provider, "anthropic");
10
+ assert.equal(claude.defaultModel, "claude-opus-4-7");
11
+ assert.equal(claude.aliases.sonnet, "claude-sonnet-4-6");
12
+ assert.deepEqual(claude.recommendations.map((entry) => entry.alias), ["opus", "sonnet", "haiku"]);
13
+ assert.ok(findRuntimeModel("claude-sdk", "claude-sonnet-4-6"));
14
+
15
+ assert.equal(codex.provider, "openai-codex");
16
+ assert.equal(codex.defaultModel, "gpt-5.5");
17
+ assert.equal(codex.aliases.mini, "gpt-5.4-mini");
18
+ assert.deepEqual(codex.recommendations.map((entry) => entry.alias), ["default", "codex", "mini", "spark"]);
19
+ assert.ok(findRuntimeModel("codex-cli", "gpt-5.4-mini"));
20
+ });
21
+
22
+ test("model aliases resolve to concrete runtime model ids", () => {
23
+ const sonnet = resolveRuntimeModelSelection("claude-sdk", "sonnet");
24
+ assert.equal(sonnet.runtimeModel, "claude-sonnet-4-6");
25
+ assert.match(sonnet.note, /model alias sonnet -> model claude-sonnet-4-6/);
26
+
27
+ const mini = resolveRuntimeModelSelection("codex-cli", "mini");
28
+ assert.equal(mini.runtimeModel, "gpt-5.4-mini");
29
+ });
30
+
31
+ test("model selection notes are advisory for unknown models", () => {
32
+ assert.match(describeModelSelection("codex-cli", "gpt-future") ?? "", /not in bundled openai-codex catalog/);
33
+ assert.equal(describeModelSelection("claude-sdk", undefined), "model default claude-opus-4-7");
34
+ });