pi-landstrip 0.17.40 → 0.17.42

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/README.md CHANGED
@@ -111,8 +111,11 @@ global or trusted-project concurrency limit.
111
111
 
112
112
  The `/agents` selector provides OpenCode-compatible `build` and `plan` roles by
113
113
  default. Build has normal development access; plan asks before shell commands
114
- and file changes. The selection controls the root system prompt and permissions
115
- and is restored with the session.
114
+ and file changes. Built-in colors match OpenCode's TUI agent palette
115
+ (`secondary` for build, `accent` for plan) and appear in the status line and
116
+ agent selectors.
117
+ The selection controls the root system prompt and permissions and is restored
118
+ with the session.
116
119
 
117
120
  ## Subagents
118
121
 
@@ -216,14 +219,15 @@ Sandbox policy remains in `~/.pi/agent/sandbox.json` and `.pi/sandbox.json`. Pi'
216
219
  }
217
220
  ```
218
221
 
219
- Optional OpenCode import flags live in `subagents.json` as top-level `opencode`:
222
+ OpenCode Markdown agent import flags live in `subagents.json` as top-level
223
+ `opencode` (both default to `true`):
220
224
 
221
225
  ```json
222
226
  {
223
227
  "maxSubagents": 0,
224
228
  "opencode": {
225
- "showGlobalAgents": false,
226
- "showLocalAgents": false
229
+ "showGlobalAgents": true,
230
+ "showLocalAgents": true
227
231
  },
228
232
  "subagents": {
229
233
  "agent": {}
@@ -231,18 +235,17 @@ Optional OpenCode import flags live in `subagents.json` as top-level `opencode`:
231
235
  }
232
236
  ```
233
237
 
234
- Both flags default to `false`. When enabled:
235
-
236
238
  - `showGlobalAgents` imports Markdown agents from the OpenCode global config
237
239
  directory (`$OPENCODE_CONFIG_DIR`, or `$XDG_CONFIG_HOME/opencode`, or
238
240
  `~/.config/opencode`) under `agent/` and `agents/`.
239
241
  - `showLocalAgents` imports Markdown agents from the trusted project's
240
242
  `.opencode/agent/` and `.opencode/agents/` directories.
241
243
 
242
- OpenCode local agents override OpenCode global agents. Pi agent definitions
243
- from `subagents.json` (built-in, global, and local) take precedence over OpenCode
244
- imports with the same name; conflicts are silent. Local OpenCode agents are
245
- skipped when the project is untrusted, regardless of the flag.
244
+ Set a flag to `false` to disable that source. OpenCode local agents override
245
+ OpenCode global agents. Pi agent definitions from `subagents.json` (built-in,
246
+ global, and local) take precedence over OpenCode imports with the same name;
247
+ conflicts are silent. Local OpenCode agents are skipped when the project is
248
+ untrusted, regardless of the flag.
246
249
 
247
250
  `subagents.json` accepts top-level `maxSubagents`, `opencode`, and `subagents`;
248
251
  sandbox fields belong in `sandbox.json`.
@@ -320,13 +323,18 @@ The packaged default is 0, which removes the `task` tool while retaining primary
320
323
  roles. There is no separate subagent enable switch. The Settings tab in
321
324
  `/agents` edits this limit for global and trusted-project configuration.
322
325
 
323
- Agent modes, hidden/disabled agents, prompts, and ordered `allow`/`ask`/`deny`
324
- permissions apply to primary agents and subagents. Subagent workers also honor
325
- model selection, supported Pi thinking-level variants, and step limits. Primary
326
- agent activation currently changes the prompt and permissions only. Later
327
- matching permission rules win. Put provider-specific values under `options`;
328
- unknown agent fields are rejected. Agent permissions cannot weaken an enabled
329
- OS sandbox, grant filesystem access, or grant network access.
326
+ Agent modes, hidden/disabled agents, prompts, colors, and ordered
327
+ `allow`/`ask`/`deny` permissions apply to primary agents and subagents. Prompt
328
+ strings may include OpenCode-style `{file:path}` tokens; relative paths resolve
329
+ against the `subagents.json` file that defines them. `color` accepts `#RRGGBB`
330
+ or OpenCode theme names (`primary`, `secondary`, `accent`, `success`, `warning`,
331
+ `error`, `info`). `hidden` only hides an agent from user-facing primary pickers;
332
+ the model can still invoke a hidden subagent via `task`. Subagent workers also
333
+ honor model selection, supported Pi thinking-level variants, and step limits.
334
+ Primary agent activation currently changes the prompt and permissions only.
335
+ Later matching permission rules win. Put provider-specific values under
336
+ `options`; unknown agent fields are rejected. Agent permissions cannot weaken an
337
+ enabled OS sandbox, grant filesystem access, or grant network access.
330
338
 
331
339
  ## Configuration migration
332
340
 
package/dist/index.ts CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  existsSync as existsSync5,
7
7
  mkdirSync as mkdirSync3,
8
8
  mkdtempSync as mkdtempSync2,
9
- readFileSync as readFileSync5,
9
+ readFileSync as readFileSync6,
10
10
  realpathSync as realpathSync2,
11
11
  rmSync as rmSync2,
12
12
  writeFileSync as writeFileSync3
@@ -19,7 +19,7 @@ import {
19
19
  Socket as NetSocket
20
20
  } from "node:net";
21
21
  import { homedir as homedir4, tmpdir as tmpdir2 } from "node:os";
22
- import { basename as basename3, dirname as dirname3, isAbsolute as isAbsolute2, join as join6, parse, resolve as resolve2 } from "node:path";
22
+ import { basename as basename3, dirname as dirname3, isAbsolute as isAbsolute3, join as join6, parse, resolve as resolve3 } from "node:path";
23
23
  import { StringDecoder as StringDecoder2 } from "node:string_decoder";
24
24
  import { fileURLToPath as fileURLToPath3, URL } from "node:url";
25
25
  import {
@@ -74,14 +74,14 @@ import {
74
74
  lstatSync,
75
75
  mkdirSync as mkdirSync2,
76
76
  mkdtempSync,
77
- readFileSync as readFileSync4,
77
+ readFileSync as readFileSync5,
78
78
  readlinkSync,
79
79
  realpathSync,
80
80
  rmSync,
81
81
  writeFileSync as writeFileSync2
82
82
  } from "node:fs";
83
83
  import { homedir as homedir3, tmpdir } from "node:os";
84
- import { basename as basename2, dirname as dirname2, isAbsolute, join as join5, relative as relative2, resolve, sep as sep2 } from "node:path";
84
+ import { basename as basename2, dirname as dirname2, isAbsolute as isAbsolute2, join as join5, relative as relative2, resolve as resolve2, sep as sep2 } from "node:path";
85
85
  import { fileURLToPath as fileURLToPath2 } from "node:url";
86
86
  import {
87
87
  getAgentDir as getAgentDir3,
@@ -92,20 +92,21 @@ import { matchesKey, Text, truncateToWidth as truncateToWidth2 } from "@earendil
92
92
  import { Type } from "typebox";
93
93
 
94
94
  // agents.ts
95
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
95
+ import { existsSync as existsSync3, readFileSync as readFileSync4 } from "node:fs";
96
96
  import { join as join4 } from "node:path";
97
97
  import { minimatch } from "minimatch";
98
98
  import { getAgentDir as getAgentDir2 } from "@earendil-works/pi-coding-agent";
99
99
 
100
100
  // config.ts
101
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
101
+ import { existsSync, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "node:fs";
102
102
  import { dirname, join as join2 } from "node:path";
103
103
  import { fileURLToPath } from "node:url";
104
104
  import { getAgentDir, withFileMutationQueue } from "@earendil-works/pi-coding-agent";
105
105
 
106
106
  // util.ts
107
+ import { readFileSync } from "node:fs";
107
108
  import { homedir } from "node:os";
108
- import { join } from "node:path";
109
+ import { isAbsolute, join, resolve } from "node:path";
109
110
  function expandHomePath(path) {
110
111
  if (path === "~" || path === "$HOME")
111
112
  return homedir();
@@ -115,20 +116,95 @@ function expandHomePath(path) {
115
116
  return join(homedir(), path.slice(6));
116
117
  return path;
117
118
  }
119
+ function expandFileReferences(text, baseDir) {
120
+ if (!text.includes("{file:"))
121
+ return text;
122
+ const matches = Array.from(text.matchAll(/\{file:[^}]+\}/g));
123
+ if (matches.length === 0)
124
+ return text;
125
+ let out = "";
126
+ let cursor = 0;
127
+ for (const match of matches) {
128
+ const token = match[0];
129
+ const index = match.index ?? 0;
130
+ out += text.slice(cursor, index);
131
+ let filePath = token.slice("{file:".length, -1);
132
+ if (filePath.startsWith("~/"))
133
+ filePath = join(homedir(), filePath.slice(2));
134
+ const resolved = isAbsolute(filePath) ? filePath : resolve(baseDir, filePath);
135
+ try {
136
+ out += readFileSync(resolved, "utf8").trim();
137
+ } catch (error) {
138
+ const detail = error instanceof Error && "code" in error && error.code === "ENOENT" ? `${resolved} does not exist` : error instanceof Error ? error.message : String(error);
139
+ throw new Error(`bad file reference: "${token}" ${detail}`);
140
+ }
141
+ cursor = index + token.length;
142
+ }
143
+ out += text.slice(cursor);
144
+ return out;
145
+ }
118
146
  function formatError(error) {
119
147
  return error instanceof Error ? error.message : String(error);
120
148
  }
121
149
  function isRecord2(value) {
122
150
  return typeof value === "object" && value !== null && !Array.isArray(value);
123
151
  }
152
+ var OPENCODE_THEME_COLORS = new Set([
153
+ "primary",
154
+ "secondary",
155
+ "accent",
156
+ "success",
157
+ "warning",
158
+ "error",
159
+ "info"
160
+ ]);
161
+ var THEME_COLOR_ALIASES = {
162
+ primary: "accent",
163
+ secondary: "customMessageLabel",
164
+ accent: "accent",
165
+ success: "success",
166
+ warning: "warning",
167
+ error: "error",
168
+ info: "accent"
169
+ };
170
+ var NAMED_COLOR_ANSI = {
171
+ primary: "\x1B[36m",
172
+ secondary: "\x1B[35m",
173
+ accent: "\x1B[36m",
174
+ success: "\x1B[32m",
175
+ warning: "\x1B[33m",
176
+ error: "\x1B[31m",
177
+ info: "\x1B[36m"
178
+ };
179
+ function isAgentColor(value) {
180
+ if (/^#[0-9a-fA-F]{6}$/.test(value))
181
+ return true;
182
+ return OPENCODE_THEME_COLORS.has(value);
183
+ }
184
+ function hexFg(hex, text) {
185
+ const r = Number.parseInt(hex.slice(1, 3), 16);
186
+ const g = Number.parseInt(hex.slice(3, 5), 16);
187
+ const b = Number.parseInt(hex.slice(5, 7), 16);
188
+ return `\x1B[38;2;${r};${g};${b}m${text}\x1B[39m`;
189
+ }
190
+ function colorizeAgentText(color, text, themeFg, fallback = "accent") {
191
+ if (!color)
192
+ return themeFg ? themeFg(fallback, text) : text;
193
+ if (color.startsWith("#"))
194
+ return hexFg(color, text);
195
+ if (themeFg)
196
+ return themeFg(THEME_COLOR_ALIASES[color] ?? fallback, text);
197
+ const ansi = NAMED_COLOR_ANSI[color];
198
+ return ansi ? `${ansi}${text}\x1B[39m` : text;
199
+ }
124
200
 
125
201
  class AsyncQueue {
126
202
  tail = Promise.resolve();
127
203
  async acquire(signal, cancellationMessage = "Request cancelled") {
128
204
  let release;
129
205
  const previous = this.tail;
130
- this.tail = new Promise((resolve) => {
131
- release = resolve;
206
+ this.tail = new Promise((resolve2) => {
207
+ release = resolve2;
132
208
  });
133
209
  if (!signal) {
134
210
  await previous;
@@ -161,8 +237,8 @@ class AsyncQueue {
161
237
  var MAX_SUBAGENTS = 16;
162
238
  var packageDir = dirname(fileURLToPath(import.meta.url));
163
239
  var DEFAULT_OPENCODE = {
164
- showGlobalAgents: false,
165
- showLocalAgents: false
240
+ showGlobalAgents: true,
241
+ showLocalAgents: true
166
242
  };
167
243
  var OPENCODE_KEYS = new Set(["showGlobalAgents", "showLocalAgents"]);
168
244
  function readBooleanField(section, key, path) {
@@ -192,12 +268,28 @@ function normalizeOpenCode(value, path) {
192
268
  result.showLocalAgents = showLocalAgents;
193
269
  return result;
194
270
  }
271
+ function expandAgentPromptReferences(config, path) {
272
+ if (!isRecord2(config.subagents) || !isRecord2(config.subagents.agent))
273
+ return;
274
+ const baseDir = dirname(path);
275
+ for (const [name, value] of Object.entries(config.subagents.agent)) {
276
+ if (!isRecord2(value) || typeof value.prompt !== "string")
277
+ continue;
278
+ if (!value.prompt.includes("{file:"))
279
+ continue;
280
+ try {
281
+ value.prompt = expandFileReferences(value.prompt, baseDir);
282
+ } catch (error) {
283
+ throw new Error(`${path}: agent ${name}: ${formatError(error)}`);
284
+ }
285
+ }
286
+ }
195
287
  function readConfig(path, isGlobal = false) {
196
288
  if (!existsSync(path))
197
289
  return {};
198
290
  let value;
199
291
  try {
200
- value = JSON.parse(readFileSync(path, "utf8"));
292
+ value = JSON.parse(readFileSync2(path, "utf8"));
201
293
  } catch (error) {
202
294
  const message = formatError(error);
203
295
  throw new Error(`${path}: ${message}`);
@@ -233,6 +325,7 @@ function readConfig(path, isGlobal = false) {
233
325
  if ("opencode" in value) {
234
326
  config.opencode = normalizeOpenCode(value.opencode, `${path}: opencode`);
235
327
  }
328
+ expandAgentPromptReferences(config, path);
236
329
  return config;
237
330
  }
238
331
  function mergeValue(base, override) {
@@ -306,7 +399,7 @@ function loadLandstripConfig(cwd, includeProject = true, agentDir = getAgentDir(
306
399
  }
307
400
 
308
401
  // opencode-agents.ts
309
- import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync2, statSync } from "node:fs";
402
+ import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync3, statSync } from "node:fs";
310
403
  import { homedir as homedir2 } from "node:os";
311
404
  import { basename, extname, join as join3, relative, sep } from "node:path";
312
405
  import { parse as parseYaml } from "yaml";
@@ -449,7 +542,7 @@ function loadAgentsFromDir(dir, source, agents, diagnostics) {
449
542
  const root = join3(dir, subdir);
450
543
  for (const filePath of listMarkdownFiles(root)) {
451
544
  try {
452
- const content = readFileSync2(filePath, "utf8");
545
+ const content = readFileSync3(filePath, "utf8");
453
546
  const { data, body } = parseFrontmatter(content);
454
547
  const name = agentNameFromPath(dir, filePath);
455
548
  if (!name) {
@@ -528,7 +621,7 @@ function legacyConfigWarnings(piAgentDir) {
528
621
  if (!existsSync3(path))
529
622
  return [];
530
623
  try {
531
- const value = JSON.parse(readFileSync3(path, "utf8"));
624
+ const value = JSON.parse(readFileSync4(path, "utf8"));
532
625
  if (!isRecord2(value))
533
626
  return [];
534
627
  const fields = ["agent", "permission", "subagents", "maxSubagents", "landstrip"].filter((field) => value[field] !== undefined);
@@ -542,11 +635,14 @@ function legacyConfigWarnings(piAgentDir) {
542
635
  }
543
636
  }
544
637
  function normalizeAgent(name, raw, source) {
545
- for (const field of ["description", "prompt", "model", "variant"]) {
638
+ for (const field of ["description", "prompt", "model", "variant", "color"]) {
546
639
  if (raw[field] !== undefined && typeof raw[field] !== "string") {
547
640
  throw new Error(`agent ${name} ${field} must be a string`);
548
641
  }
549
642
  }
643
+ if (typeof raw.color === "string" && !isAgentColor(raw.color)) {
644
+ throw new Error(`agent ${name} color must be #RRGGBB or one of primary, secondary, accent, success, warning, error, info`);
645
+ }
550
646
  for (const field of ["hidden", "disable"]) {
551
647
  if (raw[field] !== undefined && typeof raw[field] !== "boolean") {
552
648
  throw new Error(`agent ${name} ${field} must be a boolean`);
@@ -586,6 +682,7 @@ function normalizeAgent(name, raw, source) {
586
682
  model: typeof raw.model === "string" ? raw.model : undefined,
587
683
  variant: typeof raw.variant === "string" ? raw.variant : undefined,
588
684
  hidden: raw.hidden === true,
685
+ color: typeof raw.color === "string" ? raw.color : undefined,
589
686
  steps: typeof raw.steps === "number" ? raw.steps : undefined,
590
687
  permissions: normalizePermissions(raw.permission),
591
688
  providerOptions
@@ -721,7 +818,7 @@ function defaultSpawn(command, args, options) {
721
818
  return spawn(command, [...args], options);
722
819
  }
723
820
  function delay(milliseconds) {
724
- return new Promise((resolve) => setTimeout(resolve, milliseconds));
821
+ return new Promise((resolve2) => setTimeout(resolve2, milliseconds));
725
822
  }
726
823
 
727
824
  class RpcProcess {
@@ -774,19 +871,19 @@ class RpcProcess {
774
871
  });
775
872
  child.stdin.on("error", (error) => this.handleChildError(child, error, "RPC stdin error"));
776
873
  child.on("error", (error) => this.handleChildError(child, error, "RPC process error"));
777
- this.exitPromise = new Promise((resolve) => {
874
+ this.exitPromise = new Promise((resolve2) => {
778
875
  child.once("exit", (code, signal) => {
779
876
  if (this.child === child) {
780
877
  this.child = null;
781
878
  this.failAll(new Error(this.exitMessage(code, signal)));
782
879
  }
783
- resolve();
880
+ resolve2();
784
881
  });
785
882
  });
786
- await new Promise((resolve, reject) => {
883
+ await new Promise((resolve2, reject) => {
787
884
  const onSpawn = () => {
788
885
  cleanup();
789
- resolve();
886
+ resolve2();
790
887
  };
791
888
  const onError = (error) => {
792
889
  cleanup();
@@ -830,12 +927,12 @@ class RpcProcess {
830
927
  if (this.fatalError)
831
928
  throw this.fatalError;
832
929
  const id = `rpc-${++this.nextRequestId}`;
833
- const response = await new Promise((resolve, reject) => {
930
+ const response = await new Promise((resolve2, reject) => {
834
931
  const timer = setTimeout(() => {
835
932
  this.pending.delete(id);
836
933
  reject(this.withStderr(`Timed out waiting for ${type} response`));
837
934
  }, this.options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS);
838
- this.pending.set(id, { command: type, resolve, reject, timer });
935
+ this.pending.set(id, { command: type, resolve: resolve2, reject, timer });
839
936
  this.write({ ...fields, id, type }, (error) => {
840
937
  if (!error)
841
938
  return;
@@ -1035,12 +1132,12 @@ class RpcProcess {
1035
1132
  }
1036
1133
  waitForAgentSettled() {
1037
1134
  let waiter;
1038
- const promise = new Promise((resolve, reject) => {
1135
+ const promise = new Promise((resolve2, reject) => {
1039
1136
  const timer = setTimeout(() => {
1040
1137
  this.settledWaiters.delete(waiter);
1041
1138
  reject(this.withStderr("Timed out waiting for agent_settled"));
1042
1139
  }, this.options.settleTimeoutMs ?? DEFAULT_SETTLE_TIMEOUT_MS);
1043
- waiter = { resolve, reject, timer };
1140
+ waiter = { resolve: resolve2, reject, timer };
1044
1141
  this.settledWaiters.add(waiter);
1045
1142
  });
1046
1143
  return {
@@ -1186,7 +1283,7 @@ function resolvePiPackage() {
1186
1283
  }
1187
1284
  function readPiPackage(pkgPath) {
1188
1285
  try {
1189
- const value = JSON.parse(readFileSync4(pkgPath, "utf8"));
1286
+ const value = JSON.parse(readFileSync5(pkgPath, "utf8"));
1190
1287
  if (!isRecord2(value) || value.name !== "@earendil-works/pi-coding-agent" || typeof value.version !== "string") {
1191
1288
  return;
1192
1289
  }
@@ -1350,7 +1447,7 @@ class Semaphore {
1350
1447
  this.active += 1;
1351
1448
  return () => this.release();
1352
1449
  }
1353
- await new Promise((resolve2, reject) => {
1450
+ await new Promise((resolve3, reject) => {
1354
1451
  const abort = () => {
1355
1452
  const index = this.waiters.indexOf(start);
1356
1453
  if (index >= 0)
@@ -1360,7 +1457,7 @@ class Semaphore {
1360
1457
  const start = () => {
1361
1458
  signal?.removeEventListener("abort", abort);
1362
1459
  this.active += 1;
1363
- resolve2();
1460
+ resolve3();
1364
1461
  };
1365
1462
  this.waiters.push(start);
1366
1463
  signal?.addEventListener("abort", abort, { once: true });
@@ -1508,7 +1605,7 @@ function canonicalPermissionPath(path, seen = new Set) {
1508
1605
  if (seen.has(existing))
1509
1606
  return path;
1510
1607
  seen.add(existing);
1511
- const target = resolve(dirname2(existing), readlinkSync(existing), ...missing);
1608
+ const target = resolve2(dirname2(existing), readlinkSync(existing), ...missing);
1512
1609
  return canonicalPermissionPath(target, seen);
1513
1610
  }
1514
1611
  break;
@@ -1521,7 +1618,7 @@ function canonicalPermissionPath(path, seen = new Set) {
1521
1618
  }
1522
1619
  }
1523
1620
  try {
1524
- return resolve(realpathSync(existing), ...missing);
1621
+ return resolve2(realpathSync(existing), ...missing);
1525
1622
  } catch {
1526
1623
  return path;
1527
1624
  }
@@ -1536,10 +1633,10 @@ function permissionResource(tool, input, cwd) {
1536
1633
  }
1537
1634
  if (typeof input.path !== "string")
1538
1635
  return "*";
1539
- const projectRoot = canonicalPermissionPath(resolve(cwd));
1540
- const absolutePath = canonicalPermissionPath(resolve(cwd, input.path));
1636
+ const projectRoot = canonicalPermissionPath(resolve2(cwd));
1637
+ const absolutePath = canonicalPermissionPath(resolve2(cwd, input.path));
1541
1638
  const projectPath = relative2(projectRoot, absolutePath);
1542
- if (projectPath && !isAbsolute(projectPath) && !projectPath.startsWith(`..${sep2}`) && projectPath !== "..") {
1639
+ if (projectPath && !isAbsolute2(projectPath) && !projectPath.startsWith(`..${sep2}`) && projectPath !== "..") {
1543
1640
  return projectPath.split(sep2).join("/");
1544
1641
  }
1545
1642
  return absolutePath.split(sep2).join("/");
@@ -1801,7 +1898,7 @@ ${this.primaryAgent.prompt}` };
1801
1898
  const cursor = index === selectedAgent ? theme.fg("accent", "›") : " ";
1802
1899
  const name = `@${agent.name}`.padEnd(nameWidth);
1803
1900
  const description = `${agent.description ?? "No description"}${agent.hidden ? " [hidden]" : ""}`;
1804
- lines2.push(`${cursor} ${theme.fg("accent", name)} ${theme.fg("dim", agent.source.padEnd(8))} ${theme.fg("text", description)}`);
1901
+ lines2.push(`${cursor} ${colorizeAgentText(agent.color, name, (c, t) => theme.fg(c, t))} ${theme.fg("dim", agent.source.padEnd(8))} ${theme.fg("text", description)}`);
1805
1902
  }
1806
1903
  if (agents.length === 0)
1807
1904
  lines2.push(" No subagent types are configured.");
@@ -1950,7 +2047,7 @@ ${this.primaryAgent.prompt}` };
1950
2047
  createTaskTool(parentTask, callerRules, boundCtx) {
1951
2048
  const cwd = boundCtx?.cwd ?? process.cwd();
1952
2049
  const catalog = this.loadCatalog(cwd, getAgentDir3(), boundCtx ? isProjectTrusted(boundCtx) : false);
1953
- const agents = availableSubagents(catalog).filter((agent) => !agent.hidden && permissionDecision(callerRules ?? catalog.permissions, "task", agent.name) !== "deny");
2050
+ const agents = availableSubagents(catalog).filter((agent) => permissionDecision(callerRules ?? catalog.permissions, "task", agent.name) !== "deny");
1954
2051
  const descriptions = agents.map((agent) => `${agent.name}: ${agent.description ?? "No description"}`).join(`
1955
2052
  `);
1956
2053
  return {
@@ -2772,7 +2869,7 @@ ${renderTaskResult(task.id, failed ? "error" : "completed", value)}`;
2772
2869
  if (persist)
2773
2870
  this.pi.appendEntry(PRIMARY_AGENT_ENTRY, { name: agent.name });
2774
2871
  if (ctx.hasUI) {
2775
- ctx.ui.setStatus("landstrip-agent", `@${agent.name}`);
2872
+ ctx.ui.setStatus("landstrip-agent", colorizeAgentText(agent.color, `@${agent.name}`));
2776
2873
  if (persist)
2777
2874
  ctx.ui.notify(`Primary agent: ${agent.name}`, "info");
2778
2875
  }
@@ -2896,18 +2993,18 @@ function loadSandboxConfig(cwd, includeProject) {
2896
2993
  if (!existsSync5(globalConfigPath)) {
2897
2994
  const templatePath = join6(packageDir3, "sandbox.json");
2898
2995
  mkdirSync3(dirname3(globalConfigPath), { recursive: true });
2899
- writeFileSync3(globalConfigPath, readFileSync5(templatePath, "utf-8"), "utf-8");
2996
+ writeFileSync3(globalConfigPath, readFileSync6(templatePath, "utf-8"), "utf-8");
2900
2997
  }
2901
- let globalConfig = JSON.parse(readFileSync5(join6(packageDir3, "sandbox.json"), "utf-8"));
2998
+ let globalConfig = JSON.parse(readFileSync6(join6(packageDir3, "sandbox.json"), "utf-8"));
2902
2999
  try {
2903
- const override = JSON.parse(readFileSync5(globalConfigPath, "utf-8"));
3000
+ const override = JSON.parse(readFileSync6(globalConfigPath, "utf-8"));
2904
3001
  globalConfig = deepMerge(globalConfig, override);
2905
3002
  } catch (error) {
2906
3003
  console.error(`Warning: Could not parse ${globalConfigPath}: ${error}`);
2907
3004
  }
2908
3005
  if (includeProject && existsSync5(projectConfigPath)) {
2909
3006
  try {
2910
- const projectConfig = JSON.parse(readFileSync5(projectConfigPath, "utf-8"));
3007
+ const projectConfig = JSON.parse(readFileSync6(projectConfigPath, "utf-8"));
2911
3008
  const merged = deepMerge(globalConfig, projectConfig);
2912
3009
  merged.windows.backend = globalConfig.windows.backend;
2913
3010
  return merged;
@@ -2985,7 +3082,7 @@ function readOrEmptyConfig(configPath) {
2985
3082
  if (!existsSync5(configPath))
2986
3083
  return {};
2987
3084
  try {
2988
- return JSON.parse(readFileSync5(configPath, "utf-8"));
3085
+ return JSON.parse(readFileSync6(configPath, "utf-8"));
2989
3086
  } catch {
2990
3087
  return {};
2991
3088
  }
@@ -3064,7 +3161,7 @@ function shouldPromptForWrite(path, allowWrite, cwd) {
3064
3161
  return allowWrite.length === 0 || !matchesPattern(path, allowWrite, cwd);
3065
3162
  }
3066
3163
  function expandPath(filePath, cwd) {
3067
- return resolve2(cwd, expandHomePath(filePath));
3164
+ return resolve3(cwd, expandHomePath(filePath));
3068
3165
  }
3069
3166
  function canonicalizePath(filePath, cwd) {
3070
3167
  const abs = expandPath(filePath, cwd);
@@ -3081,7 +3178,7 @@ function canonicalizePath(filePath, cwd) {
3081
3178
  probe = parent;
3082
3179
  }
3083
3180
  try {
3084
- return resolve2(realpathSync2.native(probe), ...tail);
3181
+ return resolve3(realpathSync2.native(probe), ...tail);
3085
3182
  } catch {
3086
3183
  return abs;
3087
3184
  }
@@ -3104,7 +3201,7 @@ function matchesPattern(filePath, patterns, cwd) {
3104
3201
  }
3105
3202
  function normalizeBlockedPath(path, cwd) {
3106
3203
  const nativePath = process.platform === "win32" && /^\/[a-zA-Z](?:\/|$)/.test(path) ? `${path[1]}:${path.slice(2)}` : path;
3107
- return canonicalizePath(isAbsolute2(nativePath) ? nativePath : join6(cwd, nativePath), cwd);
3204
+ return canonicalizePath(isAbsolute3(nativePath) ? nativePath : join6(cwd, nativePath), cwd);
3108
3205
  }
3109
3206
  function pathUnderDirectory(filePath, dir) {
3110
3207
  const normalizedFilePath = normalizePathSeparators(filePath);
@@ -3326,13 +3423,13 @@ async function stopPreparedChild(child) {
3326
3423
  child.kill("SIGKILL");
3327
3424
  if (child.exitCode !== null || child.signalCode !== null)
3328
3425
  return;
3329
- await new Promise((resolve3) => {
3426
+ await new Promise((resolve4) => {
3330
3427
  let settleTimer;
3331
3428
  const done = () => {
3332
3429
  if (settleTimer)
3333
3430
  clearTimeout(settleTimer);
3334
3431
  child.removeListener("exit", done);
3335
- resolve3();
3432
+ resolve4();
3336
3433
  };
3337
3434
  child.once("exit", done);
3338
3435
  settleTimer = setTimeout(done, 1000);
@@ -3545,7 +3642,7 @@ function createLandstripIntegration(options = {}) {
3545
3642
  if (!id)
3546
3643
  throw new Error("Worker extension id must not be empty");
3547
3644
  const requestedEntry = extension.entry.startsWith("file:") ? fileURLToPath3(extension.entry) : extension.entry;
3548
- if (!isAbsolute2(requestedEntry)) {
3645
+ if (!isAbsolute3(requestedEntry)) {
3549
3646
  throw new Error("Worker extension entry must be an absolute path or file URL");
3550
3647
  }
3551
3648
  if (!existsSync5(requestedEntry)) {
@@ -3599,11 +3696,11 @@ function createLandstripIntegration(options = {}) {
3599
3696
  function getEffectiveDenyRead(config, cwd) {
3600
3697
  if (process.platform !== "win32")
3601
3698
  return config.filesystem.denyRead;
3602
- const volumeRoot = parse(resolve2(cwd)).root;
3699
+ const volumeRoot = parse(resolve3(cwd)).root;
3603
3700
  return [...new Set([...config.filesystem.denyRead, volumeRoot])];
3604
3701
  }
3605
3702
  function hasBaselineWindowsApplicationAccess(filePath) {
3606
- const normalizedPath = normalizePathSeparators(resolve2(filePath)).toLowerCase();
3703
+ const normalizedPath = normalizePathSeparators(resolve3(filePath)).toLowerCase();
3607
3704
  const systemRoots = [
3608
3705
  process.env.ProgramFiles,
3609
3706
  process.env["ProgramFiles(x86)"],
@@ -3613,13 +3710,13 @@ function createLandstripIntegration(options = {}) {
3613
3710
  return systemRoots.some((root) => {
3614
3711
  if (!root)
3615
3712
  return false;
3616
- return pathUnderDirectory(normalizedPath, normalizePathSeparators(resolve2(root)).toLowerCase());
3713
+ return pathUnderDirectory(normalizedPath, normalizePathSeparators(resolve3(root)).toLowerCase());
3617
3714
  });
3618
3715
  }
3619
3716
  function withWindowsProcessReadAccess(allowances, command, cwd, additionalPaths = []) {
3620
3717
  if (process.platform !== "win32")
3621
3718
  return allowances;
3622
- const executable = resolve2(cwd, command);
3719
+ const executable = resolve3(cwd, command);
3623
3720
  const executablePaths = hasBaselineWindowsApplicationAccess(executable) ? [] : [executable];
3624
3721
  return {
3625
3722
  ...allowances,
@@ -3833,7 +3930,7 @@ function createLandstripIntegration(options = {}) {
3833
3930
  }
3834
3931
  const server = createServer(handleClient);
3835
3932
  let stopped = false;
3836
- return new Promise((resolve3, reject) => {
3933
+ return new Promise((resolve4, reject) => {
3837
3934
  const listen = (port) => {
3838
3935
  server.once("error", (error) => {
3839
3936
  if (error.code === "EADDRINUSE" && portRange && port < portRange.high) {
@@ -3845,7 +3942,7 @@ function createLandstripIntegration(options = {}) {
3845
3942
  server.listen(port, "127.0.0.1", () => {
3846
3943
  server.removeAllListeners("error");
3847
3944
  const address = server.address();
3848
- resolve3({
3945
+ resolve4({
3849
3946
  port: address.port,
3850
3947
  stop: () => new Promise((done) => {
3851
3948
  if (stopped) {
@@ -3864,7 +3961,7 @@ function createLandstripIntegration(options = {}) {
3864
3961
  });
3865
3962
  }
3866
3963
  function createSocketPair() {
3867
- return new Promise((resolve3, reject) => {
3964
+ return new Promise((resolve4, reject) => {
3868
3965
  const server = createServer();
3869
3966
  server.on("error", reject);
3870
3967
  let client = null;
@@ -3873,7 +3970,7 @@ function createLandstripIntegration(options = {}) {
3873
3970
  server.close();
3874
3971
  if (client) {
3875
3972
  client.removeListener("error", reject);
3876
- resolve3([client, serverEnd]);
3973
+ resolve4([client, serverEnd]);
3877
3974
  }
3878
3975
  });
3879
3976
  server.listen(0, "127.0.0.1", () => {
@@ -4147,7 +4244,7 @@ function createLandstripIntegration(options = {}) {
4147
4244
  };
4148
4245
  let landstripArgs;
4149
4246
  try {
4150
- envFile = writeEnvFile({ ...process.env, ...env, PWD: resolve2(cwd) }, proxy?.port ?? null, proxy ? proxyToken : undefined);
4247
+ envFile = writeEnvFile({ ...process.env, ...env, PWD: resolve3(cwd) }, proxy?.port ?? null, proxy ? proxyToken : undefined);
4151
4248
  processAllowances = withWindowsProcessReadAccess(allowances, shell, cwd, [envFile.path]);
4152
4249
  policy = writePolicyFile(cwd, proxy?.port ?? null, processAllowances);
4153
4250
  const wrappedCommand = `source '${envFile.path}' && ${command}`;
@@ -4771,7 +4868,7 @@ ${message}
4771
4868
  const selected = index === selectedAgent;
4772
4869
  const cursor = selected ? accent("›") : " ";
4773
4870
  const active = agent.name === activeAgent ? theme.fg("success", "●") : dim("○");
4774
- const name = selected ? accent(agent.name) : text(agent.name);
4871
+ const name = colorizeAgentText(agent.color, agent.name, (c, t) => theme.fg(c, t), selected ? "accent" : "text");
4775
4872
  const model = dim(` ${agent.model ?? "current model"}`);
4776
4873
  const description = agent.description ? dim(` ${agent.description}`) : "";
4777
4874
  lines.push(row(`${cursor} ${active} ${name}${model}${description}`));
@@ -4787,7 +4884,7 @@ ${message}
4787
4884
  const index = start + offset;
4788
4885
  const selected = index === selectedSubagent;
4789
4886
  const cursor = selected ? accent("›") : " ";
4790
- const name = selected ? accent(`@${agent2.name}`) : text(`@${agent2.name}`);
4887
+ const name = colorizeAgentText(agent2.color, `@${agent2.name}`, (c, t) => theme.fg(c, t), selected ? "accent" : "text");
4791
4888
  const flags = [agent2.model ?? "current model", agent2.mode];
4792
4889
  if (agent2.hidden)
4793
4890
  flags.push("hidden");
@@ -4,11 +4,13 @@
4
4
  "build": {
5
5
  "description": "Default primary agent with full development access",
6
6
  "mode": "primary",
7
+ "color": "secondary",
7
8
  "prompt": "Work in build mode. Implement, test, and complete the requested changes."
8
9
  },
9
10
  "plan": {
10
11
  "description": "Planning agent that asks before commands or file changes",
11
12
  "mode": "primary",
13
+ "color": "accent",
12
14
  "prompt": "Work in plan mode. Analyze the problem and produce a clear plan before making changes.",
13
15
  "permission": { "edit": "ask", "bash": "ask", "task": "ask" }
14
16
  },
@@ -36,9 +38,9 @@
36
38
  }
37
39
  },
38
40
  "explore": {
39
- "description": "Fast read-focused agent for exploring codebases",
41
+ "description": "Fast agent specialized for exploring codebases",
40
42
  "mode": "subagent",
41
- "prompt": "Explore the codebase without modifying it. Report findings with file references.",
43
+ "prompt": "Explore the codebase without modifying it. Report findings with file references. Use bash and network fetch tools when needed for git, PR metadata, or remote docs; do not edit files.",
42
44
  "permission": {
43
45
  "*": "deny",
44
46
  "read": {
@@ -49,7 +51,10 @@
49
51
  },
50
52
  "glob": "allow",
51
53
  "grep": "allow",
52
- "list": "allow"
54
+ "list": "allow",
55
+ "bash": "allow",
56
+ "webfetch": "allow",
57
+ "websearch": "allow"
53
58
  }
54
59
  }
55
60
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-landstrip",
3
- "version": "0.17.40",
3
+ "version": "0.17.42",
4
4
  "description": "Sandbox-aware Pi subagents and interactive command permissions",
5
5
  "keywords": [
6
6
  "landstrip",
@@ -40,7 +40,7 @@
40
40
  "ci:test": "npm run test"
41
41
  },
42
42
  "dependencies": {
43
- "@landstrip/landstrip": "^0.17.40",
43
+ "@landstrip/landstrip": "^0.17.42",
44
44
  "minimatch": "^10.2.5",
45
45
  "yaml": "^2.9.0"
46
46
  },
package/subagents.json CHANGED
@@ -4,11 +4,13 @@
4
4
  "build": {
5
5
  "description": "Default primary agent with full development access",
6
6
  "mode": "primary",
7
+ "color": "secondary",
7
8
  "prompt": "Work in build mode. Implement, test, and complete the requested changes."
8
9
  },
9
10
  "plan": {
10
11
  "description": "Planning agent that asks before commands or file changes",
11
12
  "mode": "primary",
13
+ "color": "accent",
12
14
  "prompt": "Work in plan mode. Analyze the problem and produce a clear plan before making changes.",
13
15
  "permission": { "edit": "ask", "bash": "ask", "task": "ask" }
14
16
  },
@@ -36,9 +38,9 @@
36
38
  }
37
39
  },
38
40
  "explore": {
39
- "description": "Fast read-focused agent for exploring codebases",
41
+ "description": "Fast agent specialized for exploring codebases",
40
42
  "mode": "subagent",
41
- "prompt": "Explore the codebase without modifying it. Report findings with file references.",
43
+ "prompt": "Explore the codebase without modifying it. Report findings with file references. Use bash and network fetch tools when needed for git, PR metadata, or remote docs; do not edit files.",
42
44
  "permission": {
43
45
  "*": "deny",
44
46
  "read": {
@@ -49,7 +51,10 @@
49
51
  },
50
52
  "glob": "allow",
51
53
  "grep": "allow",
52
- "list": "allow"
54
+ "list": "allow",
55
+ "bash": "allow",
56
+ "webfetch": "allow",
57
+ "websearch": "allow"
53
58
  }
54
59
  }
55
60
  },