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,560 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { chmod, mkdir, 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, LEGACY_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
+ async function ensurePiTuiStub(): Promise<void> {
15
+ const dir = join(repoRoot, "node_modules", "@mariozechner", "pi-tui");
16
+ await mkdir(dir, { recursive: true });
17
+ await writeFile(join(dir, "package.json"), `${JSON.stringify({
18
+ name: "@mariozechner/pi-tui",
19
+ type: "module",
20
+ exports: "./index.js",
21
+ }, null, 2)}\n`, "utf8");
22
+ await writeFile(join(dir, "index.js"), [
23
+ "export class Box { constructor(...args) { this.args = args; this.children = []; } addChild(child) { this.children.push(child); } }",
24
+ "export class Text { constructor(text) { this.text = text; } }",
25
+ "export function truncateToWidth(text, width, suffix = '…') {",
26
+ " const value = String(text);",
27
+ " return value.length <= width ? value : value.slice(0, Math.max(0, width - suffix.length)) + suffix;",
28
+ "}",
29
+ "export function visibleWidth(text) { return String(text).length; }",
30
+ "",
31
+ ].join("\n"), "utf8");
32
+ }
33
+
34
+ async function createCodexStub(delayMs = 0): Promise<string> {
35
+ const dir = await mkdtemp(join(tmpdir(), "pi-ca-leash-codex-command-stub-"));
36
+ const executable = join(dir, "codex");
37
+ await writeFile(executable, [
38
+ "#!/usr/bin/env node",
39
+ "const args = process.argv.slice(2);",
40
+ "const resume = args[1] === 'resume';",
41
+ "const prompt = args.at(-1) ?? '';",
42
+ "const sessionId = resume ? args.at(-2) : `thread-${Math.random().toString(16).slice(2)}`;",
43
+ "",
44
+ "function buildReply(text) {",
45
+ " if (text.includes('[intercom kind=ask from=team-board]') && text.includes('Ship it')) {",
46
+ " return 'DONE: shipped';",
47
+ " }",
48
+ " if (text.includes('[intercom kind=ask from=team-board]')) {",
49
+ " return `teammate:${text}`;",
50
+ " }",
51
+ " if (text.includes('[intercom kind=ask from=team-chat]')) {",
52
+ " return `teammate:${text}`;",
53
+ " }",
54
+ " if (text.includes('[intercom kind=ask from=pi-user]')) {",
55
+ " return `peer:${text}`;",
56
+ " }",
57
+ " return `assistant:${text}`;",
58
+ "}",
59
+ "",
60
+ `const delayMs = ${delayMs};`,
61
+ "const reply = buildReply(prompt);",
62
+ "if (delayMs > 0) {",
63
+ " await new Promise((resolve) => setTimeout(resolve, delayMs));",
64
+ "}",
65
+ "console.log(JSON.stringify({ type: 'thread.started', thread_id: sessionId }));",
66
+ "console.log(JSON.stringify({ type: 'item.completed', item: { type: 'assistant_message', text: reply } }));",
67
+ "console.log(JSON.stringify({ type: 'turn.completed', summary: `done:${reply}`, usage: { input_tokens: 1, output_tokens: 1 } }));",
68
+ "",
69
+ ].join("\n"), "utf8");
70
+ await chmod(executable, 0o755);
71
+ return executable;
72
+ }
73
+
74
+ interface FakeCommand {
75
+ description: string;
76
+ handler: (args: string, ctx: unknown) => Promise<void>;
77
+ }
78
+
79
+ async function loadCommandHarness(options: { defaultDriver: "claude-sdk" | "codex-cli"; advanced?: boolean; legacy?: boolean; codexDelayMs?: number; noSession?: boolean }) {
80
+ await ensurePiTuiStub();
81
+ const codexExecutable = await createCodexStub(options.codexDelayMs ?? 0);
82
+ const tempCwd = await mkdtemp(join(tmpdir(), "pi-ca-leash-extension-commands-"));
83
+ const previousCwd = process.cwd();
84
+ const previousDefaultDriver = process.env.PI_CLAUDE_RUNTIME_DRIVER;
85
+ const previousCodexExecutable = process.env.CODEX_CLI_EXECUTABLE;
86
+ const previousAdvanced = process.env[ADVANCED_COMMANDS_ENV];
87
+ const previousLegacy = process.env[LEGACY_COMMANDS_ENV];
88
+ const previousArgv = [...process.argv];
89
+
90
+ process.env.PI_CLAUDE_RUNTIME_DRIVER = options.defaultDriver;
91
+ process.env.CODEX_CLI_EXECUTABLE = codexExecutable;
92
+ process.env[ADVANCED_COMMANDS_ENV] = options.advanced ? "1" : "0";
93
+ process.env[LEGACY_COMMANDS_ENV] = options.legacy ? "1" : "0";
94
+ if (options.noSession && !process.argv.includes("--no-session")) {
95
+ process.argv.push("--no-session");
96
+ }
97
+ process.chdir(tempCwd);
98
+
99
+ const commands = new Map<string, FakeCommand>();
100
+ const renderers = new Map<string, (...args: any[]) => any>();
101
+ const lifecycle = new Map<string, (...args: any[]) => any>();
102
+ const sentMessages: Array<{ message: any; options: unknown }> = [];
103
+ const notifications: Array<{ message: string; type?: "info" | "warning" | "error" }> = [];
104
+ const statusUpdates: Array<{ name: string; value: unknown }> = [];
105
+ const widgets = new Map<string, unknown>();
106
+ const ctx = {
107
+ ui: {
108
+ setStatus(name: string, value: unknown) { statusUpdates.push({ name, value }); },
109
+ setWidget(name: string, widget: unknown) { widgets.set(name, widget); },
110
+ notify(message: string, type?: "info" | "warning" | "error") { notifications.push({ message, type }); },
111
+ },
112
+ };
113
+
114
+ const pi = {
115
+ registerTool() {},
116
+ registerCommand(name: string, config: FakeCommand) {
117
+ commands.set(name, config);
118
+ },
119
+ registerMessageRenderer(name: string, renderer: (...args: any[]) => any) {
120
+ renderers.set(name, renderer);
121
+ },
122
+ on(event: string, handler: (...args: any[]) => any) {
123
+ lifecycle.set(event, handler);
124
+ },
125
+ sendMessage(message: any, options: unknown) {
126
+ sentMessages.push({ message, options });
127
+ },
128
+ sendUserMessage() {},
129
+ };
130
+
131
+ const module = await import(extensionModuleUrl);
132
+ await module.default(pi as never);
133
+
134
+ return {
135
+ commands,
136
+ renderers,
137
+ sentMessages,
138
+ notifications,
139
+ statusUpdates,
140
+ widgets,
141
+ async run(name: string, args: string) {
142
+ const command = commands.get(name);
143
+ assert.ok(command, `Missing command ${name}`);
144
+ const before = sentMessages.length;
145
+ await command.handler(args, ctx);
146
+ return sentMessages.slice(before);
147
+ },
148
+ async close() {
149
+ await lifecycle.get("session_shutdown")?.();
150
+ process.chdir(previousCwd);
151
+ if (previousDefaultDriver == null) delete process.env.PI_CLAUDE_RUNTIME_DRIVER;
152
+ else process.env.PI_CLAUDE_RUNTIME_DRIVER = previousDefaultDriver;
153
+ if (previousCodexExecutable == null) delete process.env.CODEX_CLI_EXECUTABLE;
154
+ else process.env.CODEX_CLI_EXECUTABLE = previousCodexExecutable;
155
+ if (previousAdvanced == null) delete process.env[ADVANCED_COMMANDS_ENV];
156
+ else process.env[ADVANCED_COMMANDS_ENV] = previousAdvanced;
157
+ if (previousLegacy == null) delete process.env[LEGACY_COMMANDS_ENV];
158
+ else process.env[LEGACY_COMMANDS_ENV] = previousLegacy;
159
+ process.argv.splice(0, process.argv.length, ...previousArgv);
160
+ },
161
+ };
162
+ }
163
+
164
+ function latestBody(entries: Array<{ message: any }>): string {
165
+ assert.ok(entries.length > 0, "Expected command messages");
166
+ return String(entries.at(-1)?.message?.content ?? "");
167
+ }
168
+
169
+ function messageBody(entries: Array<{ message: any }>, title: RegExp): string {
170
+ const entry = entries.find((item) => title.test(String(item.message?.details?.title ?? "")));
171
+ assert.ok(entry, `Expected message titled ${title}`);
172
+ return String(entry.message?.content ?? "");
173
+ }
174
+
175
+ function latestNotification(entries: Array<{ message: string }>): string {
176
+ assert.ok(entries.length > 0, "Expected notifications");
177
+ return entries.at(-1)?.message ?? "";
178
+ }
179
+
180
+ function notificationMatching(entries: Array<{ message: string }>, pattern: RegExp): string {
181
+ const entry = entries.find((item) => pattern.test(item.message));
182
+ assert.ok(entry, `Expected notification matching ${pattern}`);
183
+ return entry.message;
184
+ }
185
+
186
+ test("/peer is the only public slash command by default", async () => {
187
+ const harness = await loadCommandHarness({ defaultDriver: "codex-cli" });
188
+ try {
189
+ assert.equal(harness.commands.has("peer"), true);
190
+ assert.equal([...harness.commands.keys()].some((name) => name.startsWith("claude-")), false);
191
+ assert.equal(harness.widgets.has("peer-dashboard"), false);
192
+ assert.equal(harness.statusUpdates.length, 0);
193
+ assert.equal(harness.notifications.length, 0);
194
+ } finally {
195
+ await harness.close();
196
+ }
197
+ });
198
+
199
+ test("/peer help stays passive while /peer init activates and shows guide", async () => {
200
+ const harness = await loadCommandHarness({ defaultDriver: "codex-cli" });
201
+ try {
202
+ const helpBefore = harness.notifications.length;
203
+ const helpMessages = await harness.run("peer", "help");
204
+ assert.equal(helpMessages.length, 0);
205
+ const helpNotice = latestNotification(harness.notifications.slice(helpBefore));
206
+ assert.match(helpNotice, /\/peer init/);
207
+ assert.match(helpNotice, /pi-ca-leash v\d+\.\d+\.\d+/);
208
+ assert.match(helpNotice, /\/peer about/);
209
+ assert.equal(harness.widgets.has("peer-dashboard"), false);
210
+
211
+ const aboutBefore = harness.notifications.length;
212
+ const aboutMessages = await harness.run("peer", "about");
213
+ assert.equal(aboutMessages.length, 0);
214
+ const aboutNotice = latestNotification(harness.notifications.slice(aboutBefore));
215
+ assert.match(aboutNotice, /version \d+\.\d+\.\d+/);
216
+ assert.match(aboutNotice, /default driver codex-cli/);
217
+ assert.equal(harness.widgets.has("peer-dashboard"), false);
218
+
219
+ const initMessages = await harness.run("peer", "init");
220
+ assert.match(String(initMessages.at(0)?.message?.details?.title ?? ""), /Agent orchestration guide/);
221
+ assert.equal(initMessages.at(0)?.message?.details?.surface, "agent");
222
+ assert.equal(initMessages.length, 1);
223
+
224
+ const userHelp = notificationMatching(harness.notifications, /Peer mode is active/);
225
+ assert.match(userHelp, /Common commands:/);
226
+ assert.match(userHelp, /\/peer start <prompt>/);
227
+ assert.match(userHelp, /\/peer help/);
228
+
229
+ const agentGuide = messageBody(initMessages, /Agent orchestration guide/);
230
+ assert.match(agentGuide, /How to work with pi-ca-leash:/);
231
+ assert.match(agentGuide, /orchestrator in the driver's seat/);
232
+ assert.match(agentGuide, /multiple specialized peers at the same time/);
233
+ assert.match(agentGuide, /Use `peer_history` like a human scrolling back/);
234
+ assert.match(agentGuide, /extension_log/);
235
+ assert.equal(harness.widgets.has("peer-dashboard"), true);
236
+ assert.equal(harness.widgets.has("peer-init-help"), false);
237
+ } finally {
238
+ await harness.close();
239
+ }
240
+ });
241
+
242
+ test("first actionable /peer command activates and shows guide once", async () => {
243
+ const harness = await loadCommandHarness({ defaultDriver: "codex-cli" });
244
+ try {
245
+ const noticeBefore = harness.notifications.length;
246
+ const firstMessages = await harness.run("peer", "models codex-cli");
247
+ assert.match(String(firstMessages.at(0)?.message?.details?.title ?? ""), /Agent orchestration guide/);
248
+ assert.equal(firstMessages.at(0)?.message?.details?.surface, "agent");
249
+ assert.equal(firstMessages.length, 1);
250
+ const firstNotices = harness.notifications.slice(noticeBefore);
251
+ assert.match(notificationMatching(firstNotices, /Peer mode is active/), /\/peer help/);
252
+ assert.match(messageBody(firstMessages, /Agent orchestration guide/), /How to work with pi-ca-leash:/);
253
+ assert.match(notificationMatching(firstNotices, /Runtime models/), /codex-cli models/);
254
+ assert.equal(harness.widgets.has("peer-dashboard"), true);
255
+
256
+ const nextNoticeBefore = harness.notifications.length;
257
+ const nextMessages = await harness.run("peer", "list");
258
+ assert.equal(nextMessages.length, 0);
259
+ const nextText = harness.notifications.slice(nextNoticeBefore).map((entry) => entry.message).join("\n");
260
+ assert.doesNotMatch(nextText, /How to work with pi-ca-leash:/);
261
+ assert.doesNotMatch(nextText, /Common commands:/);
262
+ } finally {
263
+ await harness.close();
264
+ }
265
+ });
266
+
267
+ test("compact peer widget renders summary and column labels", async () => {
268
+ const harness = await loadCommandHarness({ defaultDriver: "codex-cli", codexDelayMs: 150 });
269
+ try {
270
+ await harness.run("peer", "start reviewer | Review auth flow and reply briefly.");
271
+ const widget = harness.widgets.get("peer-dashboard") as ((tui: unknown, theme: { fg: (color: string, text: string) => string }) => { render(width: number): string[] }) | undefined;
272
+ assert.ok(widget, "Expected peer dashboard widget");
273
+
274
+ const rendered = widget(undefined, { fg: (_color: string, text: string) => text });
275
+ const lines = rendered.render(88);
276
+ assert.match(lines[0] ?? "", /Peers 1 peer/);
277
+ assert.match(lines[0] ?? "", /● (active|idle|warning)/);
278
+ assert.match(lines.join("\n"), /peer\s+state\s+(driver\s+)?(model\s+)?updated\s+activity/);
279
+ assert.match(lines.join("\n"), /reviewer/);
280
+
281
+ const narrowLines = rendered.render(40);
282
+ assert.match(narrowLines.join("\n"), /peer\s+state\s+(updated\s+)?activity/);
283
+ assert.doesNotMatch(narrowLines.join("\n"), /\b(driver|model)\b/);
284
+ } finally {
285
+ await harness.close();
286
+ }
287
+ });
288
+
289
+ test("compact peer widget adapts widths and hides redundant driver column", async () => {
290
+ const harness = await loadCommandHarness({ defaultDriver: "codex-cli", codexDelayMs: 150 });
291
+ try {
292
+ await harness.run("peer", "start opsx-sonnet-reviewer | Review auth flow briefly. | codex-cli | claude-sonnet-4-6");
293
+ await harness.run("peer", "start opsx-haiku-reviewer | Summarize docs briefly. | codex-cli | claude-haiku-4-5");
294
+
295
+ const widget = harness.widgets.get("peer-dashboard") as ((tui: unknown, theme: { fg: (color: string, text: string) => string }) => { render(width: number): string[] }) | undefined;
296
+ assert.ok(widget, "Expected peer dashboard widget");
297
+
298
+ const rendered = widget(undefined, { fg: (_color: string, text: string) => text });
299
+ const lines = rendered.render(78).join("\n");
300
+ assert.doesNotMatch(lines, /\bdriver\b/);
301
+ assert.match(lines, /peer\s+state\s+model\s+updated\s+activity/);
302
+ assert.match(lines, /opsx-sonnet-reviewer/);
303
+ assert.match(lines, /opsx-haiku-reviewer/);
304
+ assert.match(lines, /\d{2}:\d{2}/);
305
+ } finally {
306
+ await harness.close();
307
+ }
308
+ });
309
+
310
+ test("/peer commands stay renderless in --no-session smoke mode", async () => {
311
+ const harness = await loadCommandHarness({ defaultDriver: "codex-cli", noSession: true });
312
+ try {
313
+ const noticeBefore = harness.notifications.length;
314
+ const messages = await harness.run("peer", "models codex-cli");
315
+ assert.equal(messages.length, 1);
316
+ assert.equal(messages.at(0)?.message?.details?.surface, "agent");
317
+ assert.match(notificationMatching(harness.notifications.slice(noticeBefore), /Runtime models/), /codex-cli models/);
318
+ assert.equal(harness.widgets.has("peer-dashboard"), false);
319
+ assert.equal(harness.statusUpdates.length, 0);
320
+ } finally {
321
+ await harness.close();
322
+ }
323
+ });
324
+
325
+ test("legacy env flag restores old claude slash commands", async () => {
326
+ const harness = await loadCommandHarness({ defaultDriver: "codex-cli", legacy: true });
327
+ try {
328
+ assert.equal(harness.commands.has("peer"), true);
329
+ assert.equal(harness.commands.has("claude-dashboard"), true);
330
+ assert.equal(harness.commands.has("claude-peer-start"), true);
331
+ assert.equal(harness.commands.has("claude-peer-stop-all"), true);
332
+ assert.equal(harness.commands.has("claude-subagent-run"), false);
333
+ } finally {
334
+ await harness.close();
335
+ }
336
+ });
337
+
338
+ test("legacy plus advanced env restores old internal claude commands", async () => {
339
+ const harness = await loadCommandHarness({ defaultDriver: "codex-cli", legacy: true, advanced: true });
340
+ try {
341
+ assert.equal(harness.commands.has("claude-subagent-run"), true);
342
+ assert.equal(harness.commands.has("claude-team-spawn"), true);
343
+ assert.equal(harness.commands.has("claude-runtime-list"), true);
344
+ } finally {
345
+ await harness.close();
346
+ }
347
+ });
348
+
349
+ test("renderer, status, and widget use peer/pi-ca-leash branding", async () => {
350
+ const harness = await loadCommandHarness({ defaultDriver: "codex-cli" });
351
+ try {
352
+ assert.equal(harness.renderers.has("peer-command-result"), true);
353
+ assert.equal(harness.renderers.has("claude-command-result"), false);
354
+
355
+ const renderer = harness.renderers.get("peer-command-result")!;
356
+ const operatorBox = renderer({
357
+ customType: "peer-command-result",
358
+ content: "operator only",
359
+ details: { level: "info", title: "Peer dashboard", surface: "custom", timestamp: Date.now() },
360
+ }, { expanded: false }, {
361
+ fg: (_color: string, text: string) => text,
362
+ bg: (color: string, text: string) => `${color}:${text}`,
363
+ });
364
+ const renderedText = String(operatorBox.children?.[0]?.text ?? "");
365
+ assert.match(renderedText, /^\[peer\]/);
366
+ assert.doesNotMatch(renderedText, /\[cca\]|pi-claude-code-agent/);
367
+ assert.equal(operatorBox.args?.[2]?.("body"), "toolPendingBg:body");
368
+
369
+ const initMessages = await harness.run("peer", "init");
370
+ const guideMessage = initMessages.find((entry) => String(entry.message?.details?.title ?? "") === "Agent orchestration guide")?.message;
371
+ assert.equal(guideMessage?.details?.surface, "agent");
372
+ const guideBox = renderer(guideMessage, { expanded: false }, {
373
+ fg: (_color: string, text: string) => text,
374
+ bg: (color: string, text: string) => `${color}:${text}`,
375
+ });
376
+ assert.match(String(guideBox.children?.[0]?.text ?? ""), /^\[peer\/agent\]/);
377
+ assert.equal(guideBox.args?.[2]?.("body"), "toolPendingBg:body");
378
+
379
+ const startMessages = await harness.run("peer", "start reviewer | Review auth flow and reply briefly.");
380
+ assert.equal(startMessages.length, 0);
381
+ assert.match(latestNotification(harness.notifications), /Peer started: reviewer/);
382
+
383
+ assert.equal(harness.statusUpdates.at(-1)?.name, "pi-ca-leash");
384
+ assert.equal(harness.widgets.has("peer-dashboard"), true);
385
+ assert.equal(harness.widgets.has("claude-dashboard"), false);
386
+ } finally {
387
+ await harness.close();
388
+ }
389
+ });
390
+
391
+ test("/peer start honors codex default and explicit driver forms", async () => {
392
+ const defaultHarness = await loadCommandHarness({ defaultDriver: "codex-cli", codexDelayMs: 150 });
393
+ try {
394
+ const started = Date.now();
395
+ const startMessages = await defaultHarness.run("peer", "start" + " " + "Review auth flow and reply briefly.");
396
+ assert.ok(Date.now() - started < 140, "/peer start should not wait for peer idle");
397
+ assert.equal(startMessages.length, 1);
398
+ assert.equal(startMessages.at(0)?.message?.details?.surface, "agent");
399
+ assert.match(latestNotification(defaultHarness.notifications), /driver codex-cli/);
400
+
401
+ const listMessages = await defaultHarness.run("peer", "list");
402
+ assert.equal(listMessages.length, 0);
403
+ assert.match(latestNotification(defaultHarness.notifications), /codex-cli/);
404
+ } finally {
405
+ await defaultHarness.close();
406
+ }
407
+
408
+ const overrideHarness = await loadCommandHarness({ defaultDriver: "claude-sdk" });
409
+ try {
410
+ const startMessages = await overrideHarness.run("peer", "start" + " " + "reviewer | Review auth flow and reply briefly. | codex-cli | gpt-5.4-mini");
411
+ assert.equal(startMessages.length, 1);
412
+ assert.equal(startMessages.at(0)?.message?.details?.surface, "agent");
413
+ assert.match(latestNotification(overrideHarness.notifications), /driver codex-cli/);
414
+ assert.match(latestNotification(overrideHarness.notifications), /gpt-5\.4-mini/);
415
+
416
+ const listMessages = await overrideHarness.run("peer", "list");
417
+ assert.equal(listMessages.length, 0);
418
+ assert.match(latestNotification(overrideHarness.notifications), /codex-cli/);
419
+ } finally {
420
+ await overrideHarness.close();
421
+ }
422
+ });
423
+
424
+ test("/peer models lists bundled runtime model catalog", async () => {
425
+ const harness = await loadCommandHarness({ defaultDriver: "codex-cli" });
426
+ try {
427
+ const allNoticeBefore = harness.notifications.length;
428
+ const allMessages = await harness.run("peer", "models");
429
+ assert.equal(allMessages.length, 1);
430
+ const allText = notificationMatching(harness.notifications.slice(allNoticeBefore), /Runtime models/);
431
+ assert.match(allText, /claude-sdk models/);
432
+ assert.match(allText, /codex-cli models/);
433
+
434
+ const codexNoticeBefore = harness.notifications.length;
435
+ const codexMessages = await harness.run("peer", "models codex-cli");
436
+ assert.equal(codexMessages.length, 0);
437
+ const codexText = notificationMatching(harness.notifications.slice(codexNoticeBefore), /Runtime models/);
438
+ assert.match(codexText, /default gpt-5\.5/);
439
+ assert.match(codexText, /gpt-5\.4-mini/);
440
+ assert.doesNotMatch(codexText, /claude-opus-4-7/);
441
+ } finally {
442
+ await harness.close();
443
+ }
444
+ });
445
+
446
+ test("legacy advanced subagent commands honor codex default and explicit driver forms", async () => {
447
+ const defaultHarness = await loadCommandHarness({ defaultDriver: "codex-cli", advanced: true, legacy: true });
448
+ try {
449
+ const runMessages = await defaultHarness.run("claude-subagent-run", "Reply with exactly: subagent-ok");
450
+ assert.equal(runMessages.length, 0);
451
+ assert.match(latestNotification(defaultHarness.notifications), /driver codex-cli/);
452
+
453
+ const listMessages = await defaultHarness.run("claude-subagent-list", "");
454
+ assert.equal(listMessages.length, 0);
455
+ assert.match(latestNotification(defaultHarness.notifications), /codex-cli/);
456
+ } finally {
457
+ await defaultHarness.close();
458
+ }
459
+
460
+ const overrideHarness = await loadCommandHarness({ defaultDriver: "claude-sdk", advanced: true, legacy: true });
461
+ try {
462
+ const runMessages = await overrideHarness.run("claude-subagent-run", "codex-cli | Reply with exactly: subagent-ok");
463
+ assert.equal(runMessages.length, 0);
464
+ assert.match(latestNotification(overrideHarness.notifications), /driver codex-cli/);
465
+
466
+ const listMessages = await overrideHarness.run("claude-subagent-list", "");
467
+ assert.equal(listMessages.length, 0);
468
+ assert.match(latestNotification(overrideHarness.notifications), /codex-cli/);
469
+ } finally {
470
+ await overrideHarness.close();
471
+ }
472
+ });
473
+
474
+ test("legacy advanced team commands honor codex default and explicit driver forms", async () => {
475
+ const defaultHarness = await loadCommandHarness({ defaultDriver: "codex-cli", advanced: true, legacy: true });
476
+ try {
477
+ const spawnMessages = await defaultHarness.run("claude-team-spawn", "worker | You are teammate. Reply briefly.");
478
+ assert.equal(spawnMessages.length, 0);
479
+ assert.match(latestNotification(defaultHarness.notifications), /driver codex-cli/);
480
+
481
+ const listMessages = await defaultHarness.run("claude-team-list", "");
482
+ assert.equal(listMessages.length, 0);
483
+ assert.match(latestNotification(defaultHarness.notifications), /codex-cli/);
484
+ } finally {
485
+ await defaultHarness.close();
486
+ }
487
+
488
+ const overrideHarness = await loadCommandHarness({ defaultDriver: "claude-sdk", advanced: true, legacy: true });
489
+ try {
490
+ const spawnMessages = await overrideHarness.run("claude-team-spawn", "worker | You are teammate. Reply briefly. | codex-cli");
491
+ assert.equal(spawnMessages.length, 0);
492
+ assert.match(latestNotification(overrideHarness.notifications), /driver codex-cli/);
493
+
494
+ const listMessages = await overrideHarness.run("claude-team-list", "");
495
+ assert.equal(listMessages.length, 0);
496
+ assert.match(latestNotification(overrideHarness.notifications), /codex-cli/);
497
+ } finally {
498
+ await overrideHarness.close();
499
+ }
500
+ });
501
+
502
+ test("/peer dispatcher covers ask, send, history, interrupt, and stop", async () => {
503
+ const harness = await loadCommandHarness({ defaultDriver: "codex-cli" });
504
+ try {
505
+ await harness.run("peer", "start worker | You are a brief worker.");
506
+
507
+ let listMessages = await harness.run("peer", "list");
508
+ let listText = latestNotification(harness.notifications);
509
+ for (let i = 0; i < 60 && /\b(busy|starting)\b/.test(listText); i += 1) {
510
+ await new Promise((resolve) => setTimeout(resolve, 50));
511
+ listMessages = await harness.run("peer", "list");
512
+ assert.equal(listMessages.length, 0);
513
+ listText = latestNotification(harness.notifications);
514
+ }
515
+
516
+ const askMessages = await harness.run("peer", "ask worker | Reply with exactly: peer-ok");
517
+ assert.equal(askMessages.length, 0);
518
+ assert.match(latestNotification(harness.notifications), /Peer reply: worker/);
519
+
520
+ const historyMessages = await harness.run("peer", "history worker 0 20");
521
+ assert.equal(historyMessages.length, 0);
522
+ assert.match(latestNotification(harness.notifications), /cursor .* of /);
523
+
524
+ const sendMessages = await harness.run("peer", "send worker | Keep working and report back.");
525
+ assert.equal(sendMessages.length, 0);
526
+ assert.match(latestNotification(harness.notifications), /delivery delivered_and_running/);
527
+
528
+ const interruptMessages = await harness.run("peer", "interrupt worker");
529
+ assert.equal(interruptMessages.length, 0);
530
+ assert.match(latestNotification(harness.notifications), /Peer interrupted: worker/);
531
+
532
+ const stopMessages = await harness.run("peer", "stop worker");
533
+ assert.equal(stopMessages.length, 0);
534
+ assert.match(latestNotification(harness.notifications), /Peer stopped: worker/);
535
+ } finally {
536
+ await harness.close();
537
+ }
538
+ });
539
+
540
+ test("/peer stop --all requires confirmation and clears peer list", async () => {
541
+ const harness = await loadCommandHarness({ defaultDriver: "codex-cli" });
542
+ try {
543
+ await harness.run("peer", "start" + " " + "Review auth flow and reply briefly.");
544
+ await harness.run("peer", "start" + " " + "tester | Verify login flow | codex-cli");
545
+
546
+ const usageMessages = await harness.run("peer", "stop --all");
547
+ assert.equal(usageMessages.length, 0);
548
+ assert.match(latestNotification(harness.notifications), /\/peer stop --all --confirm/);
549
+
550
+ const stopMessages = await harness.run("peer", "stop --all --confirm");
551
+ assert.equal(stopMessages.length, 0);
552
+ assert.match(latestNotification(harness.notifications), /reviewer|tester/);
553
+
554
+ const listMessages = await harness.run("peer", "list");
555
+ assert.equal(listMessages.length, 0);
556
+ assert.match(latestNotification(harness.notifications), /No peers yet\./);
557
+ } finally {
558
+ await harness.close();
559
+ }
560
+ });
@@ -0,0 +1,21 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { ADVANCED_COMMANDS_ENV, LEGACY_COMMANDS_ENV, advancedCommandsEnabled, legacyCommandsEnabled } from "./command-visibility.ts";
4
+
5
+ test("advanced commands stay hidden by default", () => {
6
+ assert.equal(advancedCommandsEnabled({}), false);
7
+ assert.equal(advancedCommandsEnabled({ [ADVANCED_COMMANDS_ENV]: "0" }), false);
8
+ });
9
+
10
+ test("advanced commands can be re-enabled explicitly", () => {
11
+ assert.equal(advancedCommandsEnabled({ [ADVANCED_COMMANDS_ENV]: "1" }), true);
12
+ });
13
+
14
+ test("legacy claude commands stay hidden by default", () => {
15
+ assert.equal(legacyCommandsEnabled({}), false);
16
+ assert.equal(legacyCommandsEnabled({ [LEGACY_COMMANDS_ENV]: "0" }), false);
17
+ });
18
+
19
+ test("legacy claude commands can be re-enabled explicitly", () => {
20
+ assert.equal(legacyCommandsEnabled({ [LEGACY_COMMANDS_ENV]: "1" }), true);
21
+ });
@@ -0,0 +1,10 @@
1
+ export const ADVANCED_COMMANDS_ENV = "PI_CLAUDE_ENABLE_ADVANCED_COMMANDS";
2
+ export const LEGACY_COMMANDS_ENV = "PI_CA_LEASH_ENABLE_LEGACY_COMMANDS";
3
+
4
+ export function advancedCommandsEnabled(env: Record<string, string | undefined> = process.env): boolean {
5
+ return env[ADVANCED_COMMANDS_ENV] === "1";
6
+ }
7
+
8
+ export function legacyCommandsEnabled(env: Record<string, string | undefined> = process.env): boolean {
9
+ return env[LEGACY_COMMANDS_ENV] === "1";
10
+ }