pi-landstrip 0.17.39 → 0.17.41

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,10 @@ 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 agent palette (`#034cff` for
115
+ build, `#a753ae` for plan) and appear in the status line and agent selectors.
116
+ The selection controls the root system prompt and permissions and is restored
117
+ with the session.
116
118
 
117
119
  ## Subagents
118
120
 
@@ -216,14 +218,15 @@ Sandbox policy remains in `~/.pi/agent/sandbox.json` and `.pi/sandbox.json`. Pi'
216
218
  }
217
219
  ```
218
220
 
219
- Optional OpenCode import flags live in `subagents.json` as top-level `opencode`:
221
+ OpenCode Markdown agent import flags live in `subagents.json` as top-level
222
+ `opencode` (both default to `true`):
220
223
 
221
224
  ```json
222
225
  {
223
226
  "maxSubagents": 0,
224
227
  "opencode": {
225
- "showGlobalAgents": false,
226
- "showLocalAgents": false
228
+ "showGlobalAgents": true,
229
+ "showLocalAgents": true
227
230
  },
228
231
  "subagents": {
229
232
  "agent": {}
@@ -231,18 +234,17 @@ Optional OpenCode import flags live in `subagents.json` as top-level `opencode`:
231
234
  }
232
235
  ```
233
236
 
234
- Both flags default to `false`. When enabled:
235
-
236
237
  - `showGlobalAgents` imports Markdown agents from the OpenCode global config
237
238
  directory (`$OPENCODE_CONFIG_DIR`, or `$XDG_CONFIG_HOME/opencode`, or
238
239
  `~/.config/opencode`) under `agent/` and `agents/`.
239
240
  - `showLocalAgents` imports Markdown agents from the trusted project's
240
241
  `.opencode/agent/` and `.opencode/agents/` directories.
241
242
 
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.
243
+ Set a flag to `false` to disable that source. OpenCode local agents override
244
+ OpenCode global agents. Pi agent definitions from `subagents.json` (built-in,
245
+ global, and local) take precedence over OpenCode imports with the same name;
246
+ conflicts are silent. Local OpenCode agents are skipped when the project is
247
+ untrusted, regardless of the flag.
246
248
 
247
249
  `subagents.json` accepts top-level `maxSubagents`, `opencode`, and `subagents`;
248
250
  sandbox fields belong in `sandbox.json`.
@@ -320,13 +322,18 @@ The packaged default is 0, which removes the `task` tool while retaining primary
320
322
  roles. There is no separate subagent enable switch. The Settings tab in
321
323
  `/agents` edits this limit for global and trusted-project configuration.
322
324
 
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.
325
+ Agent modes, hidden/disabled agents, prompts, colors, and ordered
326
+ `allow`/`ask`/`deny` permissions apply to primary agents and subagents. Prompt
327
+ strings may include OpenCode-style `{file:path}` tokens; relative paths resolve
328
+ against the `subagents.json` file that defines them. `color` accepts `#RRGGBB`
329
+ or OpenCode theme names (`primary`, `secondary`, `accent`, `success`, `warning`,
330
+ `error`, `info`). `hidden` only hides an agent from user-facing primary pickers;
331
+ the model can still invoke a hidden subagent via `task`. Subagent workers also
332
+ honor model selection, supported Pi thinking-level variants, and step limits.
333
+ Primary agent activation currently changes the prompt and permissions only.
334
+ Later matching permission rules win. Put provider-specific values under
335
+ `options`; unknown agent fields are rejected. Agent permissions cannot weaken an
336
+ enabled OS sandbox, grant filesystem access, or grant network access.
330
337
 
331
338
  ## Configuration migration
332
339
 
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,85 @@ 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: "muted",
164
+ accent: "accent",
165
+ success: "success",
166
+ warning: "warning",
167
+ error: "error",
168
+ info: "accent"
169
+ };
170
+ function isAgentColor(value) {
171
+ if (/^#[0-9a-fA-F]{6}$/.test(value))
172
+ return true;
173
+ return OPENCODE_THEME_COLORS.has(value);
174
+ }
175
+ function hexFg(hex, text) {
176
+ const r = Number.parseInt(hex.slice(1, 3), 16);
177
+ const g = Number.parseInt(hex.slice(3, 5), 16);
178
+ const b = Number.parseInt(hex.slice(5, 7), 16);
179
+ return `\x1B[38;2;${r};${g};${b}m${text}\x1B[39m`;
180
+ }
181
+ function colorizeAgentText(color, text, themeFg, fallback = "accent") {
182
+ if (!color)
183
+ return themeFg ? themeFg(fallback, text) : text;
184
+ if (color.startsWith("#"))
185
+ return hexFg(color, text);
186
+ if (!themeFg)
187
+ return text;
188
+ return themeFg(THEME_COLOR_ALIASES[color] ?? fallback, text);
189
+ }
124
190
 
125
191
  class AsyncQueue {
126
192
  tail = Promise.resolve();
127
193
  async acquire(signal, cancellationMessage = "Request cancelled") {
128
194
  let release;
129
195
  const previous = this.tail;
130
- this.tail = new Promise((resolve) => {
131
- release = resolve;
196
+ this.tail = new Promise((resolve2) => {
197
+ release = resolve2;
132
198
  });
133
199
  if (!signal) {
134
200
  await previous;
@@ -161,8 +227,8 @@ class AsyncQueue {
161
227
  var MAX_SUBAGENTS = 16;
162
228
  var packageDir = dirname(fileURLToPath(import.meta.url));
163
229
  var DEFAULT_OPENCODE = {
164
- showGlobalAgents: false,
165
- showLocalAgents: false
230
+ showGlobalAgents: true,
231
+ showLocalAgents: true
166
232
  };
167
233
  var OPENCODE_KEYS = new Set(["showGlobalAgents", "showLocalAgents"]);
168
234
  function readBooleanField(section, key, path) {
@@ -192,12 +258,28 @@ function normalizeOpenCode(value, path) {
192
258
  result.showLocalAgents = showLocalAgents;
193
259
  return result;
194
260
  }
261
+ function expandAgentPromptReferences(config, path) {
262
+ if (!isRecord2(config.subagents) || !isRecord2(config.subagents.agent))
263
+ return;
264
+ const baseDir = dirname(path);
265
+ for (const [name, value] of Object.entries(config.subagents.agent)) {
266
+ if (!isRecord2(value) || typeof value.prompt !== "string")
267
+ continue;
268
+ if (!value.prompt.includes("{file:"))
269
+ continue;
270
+ try {
271
+ value.prompt = expandFileReferences(value.prompt, baseDir);
272
+ } catch (error) {
273
+ throw new Error(`${path}: agent ${name}: ${formatError(error)}`);
274
+ }
275
+ }
276
+ }
195
277
  function readConfig(path, isGlobal = false) {
196
278
  if (!existsSync(path))
197
279
  return {};
198
280
  let value;
199
281
  try {
200
- value = JSON.parse(readFileSync(path, "utf8"));
282
+ value = JSON.parse(readFileSync2(path, "utf8"));
201
283
  } catch (error) {
202
284
  const message = formatError(error);
203
285
  throw new Error(`${path}: ${message}`);
@@ -233,6 +315,7 @@ function readConfig(path, isGlobal = false) {
233
315
  if ("opencode" in value) {
234
316
  config.opencode = normalizeOpenCode(value.opencode, `${path}: opencode`);
235
317
  }
318
+ expandAgentPromptReferences(config, path);
236
319
  return config;
237
320
  }
238
321
  function mergeValue(base, override) {
@@ -306,7 +389,7 @@ function loadLandstripConfig(cwd, includeProject = true, agentDir = getAgentDir(
306
389
  }
307
390
 
308
391
  // opencode-agents.ts
309
- import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync2, statSync } from "node:fs";
392
+ import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync3, statSync } from "node:fs";
310
393
  import { homedir as homedir2 } from "node:os";
311
394
  import { basename, extname, join as join3, relative, sep } from "node:path";
312
395
  import { parse as parseYaml } from "yaml";
@@ -449,7 +532,7 @@ function loadAgentsFromDir(dir, source, agents, diagnostics) {
449
532
  const root = join3(dir, subdir);
450
533
  for (const filePath of listMarkdownFiles(root)) {
451
534
  try {
452
- const content = readFileSync2(filePath, "utf8");
535
+ const content = readFileSync3(filePath, "utf8");
453
536
  const { data, body } = parseFrontmatter(content);
454
537
  const name = agentNameFromPath(dir, filePath);
455
538
  if (!name) {
@@ -528,7 +611,7 @@ function legacyConfigWarnings(piAgentDir) {
528
611
  if (!existsSync3(path))
529
612
  return [];
530
613
  try {
531
- const value = JSON.parse(readFileSync3(path, "utf8"));
614
+ const value = JSON.parse(readFileSync4(path, "utf8"));
532
615
  if (!isRecord2(value))
533
616
  return [];
534
617
  const fields = ["agent", "permission", "subagents", "maxSubagents", "landstrip"].filter((field) => value[field] !== undefined);
@@ -542,11 +625,14 @@ function legacyConfigWarnings(piAgentDir) {
542
625
  }
543
626
  }
544
627
  function normalizeAgent(name, raw, source) {
545
- for (const field of ["description", "prompt", "model", "variant"]) {
628
+ for (const field of ["description", "prompt", "model", "variant", "color"]) {
546
629
  if (raw[field] !== undefined && typeof raw[field] !== "string") {
547
630
  throw new Error(`agent ${name} ${field} must be a string`);
548
631
  }
549
632
  }
633
+ if (typeof raw.color === "string" && !isAgentColor(raw.color)) {
634
+ throw new Error(`agent ${name} color must be #RRGGBB or one of primary, secondary, accent, success, warning, error, info`);
635
+ }
550
636
  for (const field of ["hidden", "disable"]) {
551
637
  if (raw[field] !== undefined && typeof raw[field] !== "boolean") {
552
638
  throw new Error(`agent ${name} ${field} must be a boolean`);
@@ -586,6 +672,7 @@ function normalizeAgent(name, raw, source) {
586
672
  model: typeof raw.model === "string" ? raw.model : undefined,
587
673
  variant: typeof raw.variant === "string" ? raw.variant : undefined,
588
674
  hidden: raw.hidden === true,
675
+ color: typeof raw.color === "string" ? raw.color : undefined,
589
676
  steps: typeof raw.steps === "number" ? raw.steps : undefined,
590
677
  permissions: normalizePermissions(raw.permission),
591
678
  providerOptions
@@ -721,7 +808,7 @@ function defaultSpawn(command, args, options) {
721
808
  return spawn(command, [...args], options);
722
809
  }
723
810
  function delay(milliseconds) {
724
- return new Promise((resolve) => setTimeout(resolve, milliseconds));
811
+ return new Promise((resolve2) => setTimeout(resolve2, milliseconds));
725
812
  }
726
813
 
727
814
  class RpcProcess {
@@ -774,19 +861,19 @@ class RpcProcess {
774
861
  });
775
862
  child.stdin.on("error", (error) => this.handleChildError(child, error, "RPC stdin error"));
776
863
  child.on("error", (error) => this.handleChildError(child, error, "RPC process error"));
777
- this.exitPromise = new Promise((resolve) => {
864
+ this.exitPromise = new Promise((resolve2) => {
778
865
  child.once("exit", (code, signal) => {
779
866
  if (this.child === child) {
780
867
  this.child = null;
781
868
  this.failAll(new Error(this.exitMessage(code, signal)));
782
869
  }
783
- resolve();
870
+ resolve2();
784
871
  });
785
872
  });
786
- await new Promise((resolve, reject) => {
873
+ await new Promise((resolve2, reject) => {
787
874
  const onSpawn = () => {
788
875
  cleanup();
789
- resolve();
876
+ resolve2();
790
877
  };
791
878
  const onError = (error) => {
792
879
  cleanup();
@@ -830,12 +917,12 @@ class RpcProcess {
830
917
  if (this.fatalError)
831
918
  throw this.fatalError;
832
919
  const id = `rpc-${++this.nextRequestId}`;
833
- const response = await new Promise((resolve, reject) => {
920
+ const response = await new Promise((resolve2, reject) => {
834
921
  const timer = setTimeout(() => {
835
922
  this.pending.delete(id);
836
923
  reject(this.withStderr(`Timed out waiting for ${type} response`));
837
924
  }, this.options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS);
838
- this.pending.set(id, { command: type, resolve, reject, timer });
925
+ this.pending.set(id, { command: type, resolve: resolve2, reject, timer });
839
926
  this.write({ ...fields, id, type }, (error) => {
840
927
  if (!error)
841
928
  return;
@@ -1035,12 +1122,12 @@ class RpcProcess {
1035
1122
  }
1036
1123
  waitForAgentSettled() {
1037
1124
  let waiter;
1038
- const promise = new Promise((resolve, reject) => {
1125
+ const promise = new Promise((resolve2, reject) => {
1039
1126
  const timer = setTimeout(() => {
1040
1127
  this.settledWaiters.delete(waiter);
1041
1128
  reject(this.withStderr("Timed out waiting for agent_settled"));
1042
1129
  }, this.options.settleTimeoutMs ?? DEFAULT_SETTLE_TIMEOUT_MS);
1043
- waiter = { resolve, reject, timer };
1130
+ waiter = { resolve: resolve2, reject, timer };
1044
1131
  this.settledWaiters.add(waiter);
1045
1132
  });
1046
1133
  return {
@@ -1186,7 +1273,7 @@ function resolvePiPackage() {
1186
1273
  }
1187
1274
  function readPiPackage(pkgPath) {
1188
1275
  try {
1189
- const value = JSON.parse(readFileSync4(pkgPath, "utf8"));
1276
+ const value = JSON.parse(readFileSync5(pkgPath, "utf8"));
1190
1277
  if (!isRecord2(value) || value.name !== "@earendil-works/pi-coding-agent" || typeof value.version !== "string") {
1191
1278
  return;
1192
1279
  }
@@ -1350,7 +1437,7 @@ class Semaphore {
1350
1437
  this.active += 1;
1351
1438
  return () => this.release();
1352
1439
  }
1353
- await new Promise((resolve2, reject) => {
1440
+ await new Promise((resolve3, reject) => {
1354
1441
  const abort = () => {
1355
1442
  const index = this.waiters.indexOf(start);
1356
1443
  if (index >= 0)
@@ -1360,7 +1447,7 @@ class Semaphore {
1360
1447
  const start = () => {
1361
1448
  signal?.removeEventListener("abort", abort);
1362
1449
  this.active += 1;
1363
- resolve2();
1450
+ resolve3();
1364
1451
  };
1365
1452
  this.waiters.push(start);
1366
1453
  signal?.addEventListener("abort", abort, { once: true });
@@ -1508,7 +1595,7 @@ function canonicalPermissionPath(path, seen = new Set) {
1508
1595
  if (seen.has(existing))
1509
1596
  return path;
1510
1597
  seen.add(existing);
1511
- const target = resolve(dirname2(existing), readlinkSync(existing), ...missing);
1598
+ const target = resolve2(dirname2(existing), readlinkSync(existing), ...missing);
1512
1599
  return canonicalPermissionPath(target, seen);
1513
1600
  }
1514
1601
  break;
@@ -1521,7 +1608,7 @@ function canonicalPermissionPath(path, seen = new Set) {
1521
1608
  }
1522
1609
  }
1523
1610
  try {
1524
- return resolve(realpathSync(existing), ...missing);
1611
+ return resolve2(realpathSync(existing), ...missing);
1525
1612
  } catch {
1526
1613
  return path;
1527
1614
  }
@@ -1536,10 +1623,10 @@ function permissionResource(tool, input, cwd) {
1536
1623
  }
1537
1624
  if (typeof input.path !== "string")
1538
1625
  return "*";
1539
- const projectRoot = canonicalPermissionPath(resolve(cwd));
1540
- const absolutePath = canonicalPermissionPath(resolve(cwd, input.path));
1626
+ const projectRoot = canonicalPermissionPath(resolve2(cwd));
1627
+ const absolutePath = canonicalPermissionPath(resolve2(cwd, input.path));
1541
1628
  const projectPath = relative2(projectRoot, absolutePath);
1542
- if (projectPath && !isAbsolute(projectPath) && !projectPath.startsWith(`..${sep2}`) && projectPath !== "..") {
1629
+ if (projectPath && !isAbsolute2(projectPath) && !projectPath.startsWith(`..${sep2}`) && projectPath !== "..") {
1543
1630
  return projectPath.split(sep2).join("/");
1544
1631
  }
1545
1632
  return absolutePath.split(sep2).join("/");
@@ -1801,7 +1888,7 @@ ${this.primaryAgent.prompt}` };
1801
1888
  const cursor = index === selectedAgent ? theme.fg("accent", "›") : " ";
1802
1889
  const name = `@${agent.name}`.padEnd(nameWidth);
1803
1890
  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)}`);
1891
+ 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
1892
  }
1806
1893
  if (agents.length === 0)
1807
1894
  lines2.push(" No subagent types are configured.");
@@ -1950,7 +2037,7 @@ ${this.primaryAgent.prompt}` };
1950
2037
  createTaskTool(parentTask, callerRules, boundCtx) {
1951
2038
  const cwd = boundCtx?.cwd ?? process.cwd();
1952
2039
  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");
2040
+ const agents = availableSubagents(catalog).filter((agent) => permissionDecision(callerRules ?? catalog.permissions, "task", agent.name) !== "deny");
1954
2041
  const descriptions = agents.map((agent) => `${agent.name}: ${agent.description ?? "No description"}`).join(`
1955
2042
  `);
1956
2043
  return {
@@ -2772,7 +2859,7 @@ ${renderTaskResult(task.id, failed ? "error" : "completed", value)}`;
2772
2859
  if (persist)
2773
2860
  this.pi.appendEntry(PRIMARY_AGENT_ENTRY, { name: agent.name });
2774
2861
  if (ctx.hasUI) {
2775
- ctx.ui.setStatus("landstrip-agent", `@${agent.name}`);
2862
+ ctx.ui.setStatus("landstrip-agent", colorizeAgentText(agent.color, `@${agent.name}`));
2776
2863
  if (persist)
2777
2864
  ctx.ui.notify(`Primary agent: ${agent.name}`, "info");
2778
2865
  }
@@ -2896,18 +2983,18 @@ function loadSandboxConfig(cwd, includeProject) {
2896
2983
  if (!existsSync5(globalConfigPath)) {
2897
2984
  const templatePath = join6(packageDir3, "sandbox.json");
2898
2985
  mkdirSync3(dirname3(globalConfigPath), { recursive: true });
2899
- writeFileSync3(globalConfigPath, readFileSync5(templatePath, "utf-8"), "utf-8");
2986
+ writeFileSync3(globalConfigPath, readFileSync6(templatePath, "utf-8"), "utf-8");
2900
2987
  }
2901
- let globalConfig = JSON.parse(readFileSync5(join6(packageDir3, "sandbox.json"), "utf-8"));
2988
+ let globalConfig = JSON.parse(readFileSync6(join6(packageDir3, "sandbox.json"), "utf-8"));
2902
2989
  try {
2903
- const override = JSON.parse(readFileSync5(globalConfigPath, "utf-8"));
2990
+ const override = JSON.parse(readFileSync6(globalConfigPath, "utf-8"));
2904
2991
  globalConfig = deepMerge(globalConfig, override);
2905
2992
  } catch (error) {
2906
2993
  console.error(`Warning: Could not parse ${globalConfigPath}: ${error}`);
2907
2994
  }
2908
2995
  if (includeProject && existsSync5(projectConfigPath)) {
2909
2996
  try {
2910
- const projectConfig = JSON.parse(readFileSync5(projectConfigPath, "utf-8"));
2997
+ const projectConfig = JSON.parse(readFileSync6(projectConfigPath, "utf-8"));
2911
2998
  const merged = deepMerge(globalConfig, projectConfig);
2912
2999
  merged.windows.backend = globalConfig.windows.backend;
2913
3000
  return merged;
@@ -2985,7 +3072,7 @@ function readOrEmptyConfig(configPath) {
2985
3072
  if (!existsSync5(configPath))
2986
3073
  return {};
2987
3074
  try {
2988
- return JSON.parse(readFileSync5(configPath, "utf-8"));
3075
+ return JSON.parse(readFileSync6(configPath, "utf-8"));
2989
3076
  } catch {
2990
3077
  return {};
2991
3078
  }
@@ -3064,7 +3151,7 @@ function shouldPromptForWrite(path, allowWrite, cwd) {
3064
3151
  return allowWrite.length === 0 || !matchesPattern(path, allowWrite, cwd);
3065
3152
  }
3066
3153
  function expandPath(filePath, cwd) {
3067
- return resolve2(cwd, expandHomePath(filePath));
3154
+ return resolve3(cwd, expandHomePath(filePath));
3068
3155
  }
3069
3156
  function canonicalizePath(filePath, cwd) {
3070
3157
  const abs = expandPath(filePath, cwd);
@@ -3081,7 +3168,7 @@ function canonicalizePath(filePath, cwd) {
3081
3168
  probe = parent;
3082
3169
  }
3083
3170
  try {
3084
- return resolve2(realpathSync2.native(probe), ...tail);
3171
+ return resolve3(realpathSync2.native(probe), ...tail);
3085
3172
  } catch {
3086
3173
  return abs;
3087
3174
  }
@@ -3104,7 +3191,7 @@ function matchesPattern(filePath, patterns, cwd) {
3104
3191
  }
3105
3192
  function normalizeBlockedPath(path, cwd) {
3106
3193
  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);
3194
+ return canonicalizePath(isAbsolute3(nativePath) ? nativePath : join6(cwd, nativePath), cwd);
3108
3195
  }
3109
3196
  function pathUnderDirectory(filePath, dir) {
3110
3197
  const normalizedFilePath = normalizePathSeparators(filePath);
@@ -3326,13 +3413,13 @@ async function stopPreparedChild(child) {
3326
3413
  child.kill("SIGKILL");
3327
3414
  if (child.exitCode !== null || child.signalCode !== null)
3328
3415
  return;
3329
- await new Promise((resolve3) => {
3416
+ await new Promise((resolve4) => {
3330
3417
  let settleTimer;
3331
3418
  const done = () => {
3332
3419
  if (settleTimer)
3333
3420
  clearTimeout(settleTimer);
3334
3421
  child.removeListener("exit", done);
3335
- resolve3();
3422
+ resolve4();
3336
3423
  };
3337
3424
  child.once("exit", done);
3338
3425
  settleTimer = setTimeout(done, 1000);
@@ -3545,7 +3632,7 @@ function createLandstripIntegration(options = {}) {
3545
3632
  if (!id)
3546
3633
  throw new Error("Worker extension id must not be empty");
3547
3634
  const requestedEntry = extension.entry.startsWith("file:") ? fileURLToPath3(extension.entry) : extension.entry;
3548
- if (!isAbsolute2(requestedEntry)) {
3635
+ if (!isAbsolute3(requestedEntry)) {
3549
3636
  throw new Error("Worker extension entry must be an absolute path or file URL");
3550
3637
  }
3551
3638
  if (!existsSync5(requestedEntry)) {
@@ -3599,11 +3686,11 @@ function createLandstripIntegration(options = {}) {
3599
3686
  function getEffectiveDenyRead(config, cwd) {
3600
3687
  if (process.platform !== "win32")
3601
3688
  return config.filesystem.denyRead;
3602
- const volumeRoot = parse(resolve2(cwd)).root;
3689
+ const volumeRoot = parse(resolve3(cwd)).root;
3603
3690
  return [...new Set([...config.filesystem.denyRead, volumeRoot])];
3604
3691
  }
3605
3692
  function hasBaselineWindowsApplicationAccess(filePath) {
3606
- const normalizedPath = normalizePathSeparators(resolve2(filePath)).toLowerCase();
3693
+ const normalizedPath = normalizePathSeparators(resolve3(filePath)).toLowerCase();
3607
3694
  const systemRoots = [
3608
3695
  process.env.ProgramFiles,
3609
3696
  process.env["ProgramFiles(x86)"],
@@ -3613,13 +3700,13 @@ function createLandstripIntegration(options = {}) {
3613
3700
  return systemRoots.some((root) => {
3614
3701
  if (!root)
3615
3702
  return false;
3616
- return pathUnderDirectory(normalizedPath, normalizePathSeparators(resolve2(root)).toLowerCase());
3703
+ return pathUnderDirectory(normalizedPath, normalizePathSeparators(resolve3(root)).toLowerCase());
3617
3704
  });
3618
3705
  }
3619
3706
  function withWindowsProcessReadAccess(allowances, command, cwd, additionalPaths = []) {
3620
3707
  if (process.platform !== "win32")
3621
3708
  return allowances;
3622
- const executable = resolve2(cwd, command);
3709
+ const executable = resolve3(cwd, command);
3623
3710
  const executablePaths = hasBaselineWindowsApplicationAccess(executable) ? [] : [executable];
3624
3711
  return {
3625
3712
  ...allowances,
@@ -3833,7 +3920,7 @@ function createLandstripIntegration(options = {}) {
3833
3920
  }
3834
3921
  const server = createServer(handleClient);
3835
3922
  let stopped = false;
3836
- return new Promise((resolve3, reject) => {
3923
+ return new Promise((resolve4, reject) => {
3837
3924
  const listen = (port) => {
3838
3925
  server.once("error", (error) => {
3839
3926
  if (error.code === "EADDRINUSE" && portRange && port < portRange.high) {
@@ -3845,7 +3932,7 @@ function createLandstripIntegration(options = {}) {
3845
3932
  server.listen(port, "127.0.0.1", () => {
3846
3933
  server.removeAllListeners("error");
3847
3934
  const address = server.address();
3848
- resolve3({
3935
+ resolve4({
3849
3936
  port: address.port,
3850
3937
  stop: () => new Promise((done) => {
3851
3938
  if (stopped) {
@@ -3864,7 +3951,7 @@ function createLandstripIntegration(options = {}) {
3864
3951
  });
3865
3952
  }
3866
3953
  function createSocketPair() {
3867
- return new Promise((resolve3, reject) => {
3954
+ return new Promise((resolve4, reject) => {
3868
3955
  const server = createServer();
3869
3956
  server.on("error", reject);
3870
3957
  let client = null;
@@ -3873,7 +3960,7 @@ function createLandstripIntegration(options = {}) {
3873
3960
  server.close();
3874
3961
  if (client) {
3875
3962
  client.removeListener("error", reject);
3876
- resolve3([client, serverEnd]);
3963
+ resolve4([client, serverEnd]);
3877
3964
  }
3878
3965
  });
3879
3966
  server.listen(0, "127.0.0.1", () => {
@@ -4147,7 +4234,7 @@ function createLandstripIntegration(options = {}) {
4147
4234
  };
4148
4235
  let landstripArgs;
4149
4236
  try {
4150
- envFile = writeEnvFile({ ...process.env, ...env, PWD: resolve2(cwd) }, proxy?.port ?? null, proxy ? proxyToken : undefined);
4237
+ envFile = writeEnvFile({ ...process.env, ...env, PWD: resolve3(cwd) }, proxy?.port ?? null, proxy ? proxyToken : undefined);
4151
4238
  processAllowances = withWindowsProcessReadAccess(allowances, shell, cwd, [envFile.path]);
4152
4239
  policy = writePolicyFile(cwd, proxy?.port ?? null, processAllowances);
4153
4240
  const wrappedCommand = `source '${envFile.path}' && ${command}`;
@@ -4771,7 +4858,7 @@ ${message}
4771
4858
  const selected = index === selectedAgent;
4772
4859
  const cursor = selected ? accent("›") : " ";
4773
4860
  const active = agent.name === activeAgent ? theme.fg("success", "●") : dim("○");
4774
- const name = selected ? accent(agent.name) : text(agent.name);
4861
+ const name = colorizeAgentText(agent.color, agent.name, (c, t) => theme.fg(c, t), selected ? "accent" : "text");
4775
4862
  const model = dim(` ${agent.model ?? "current model"}`);
4776
4863
  const description = agent.description ? dim(` ${agent.description}`) : "";
4777
4864
  lines.push(row(`${cursor} ${active} ${name}${model}${description}`));
@@ -4787,7 +4874,7 @@ ${message}
4787
4874
  const index = start + offset;
4788
4875
  const selected = index === selectedSubagent;
4789
4876
  const cursor = selected ? accent("›") : " ";
4790
- const name = selected ? accent(`@${agent2.name}`) : text(`@${agent2.name}`);
4877
+ const name = colorizeAgentText(agent2.color, `@${agent2.name}`, (c, t) => theme.fg(c, t), selected ? "accent" : "text");
4791
4878
  const flags = [agent2.model ?? "current model", agent2.mode];
4792
4879
  if (agent2.hidden)
4793
4880
  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": "#034cff",
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": "#a753ae",
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.39",
3
+ "version": "0.17.41",
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.39",
43
+ "@landstrip/landstrip": "^0.17.41",
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": "#034cff",
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": "#a753ae",
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
  },