ac-teams 0.0.16 → 0.0.18

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.
@@ -53,7 +53,25 @@ Choose concurrency using resource readings and observed progress. These advisori
53
53
 
54
54
  Idle members keep Pi and its extensions alive. Stop unneeded members to release their owned processes, then resume their saved sessions when needed.
55
55
 
56
- This preserves transcripts, tasks, messages, and worktrees. Extension-owned language servers and scanners can dominate memory; ac-teams does not disable them or share them across sessions.
56
+ This preserves transcripts, tasks, messages, and worktrees. Extension-owned language servers and scanners can dominate memory; ac-teams does not disable checks or manage their processes.
57
+
58
+ ### Choose pi-lens reuse or separate analysis
59
+
60
+ Teammate launches default to `analysis: "auto"`, which sets `PI_LENS_WARM_ATTACH=1` in the child. Active pi-lens can then try its existing same-workspace reuse, including providers outside this team. ac-teams does not install or enable pi-lens; without it, launch proceeds normally.
61
+
62
+ Choose `analysis: "separate"` for different environments or rules, uncertain compatibility, suspicious diagnostics, or independent analyzer state. This sets the flag to `0` without disabling checks. Different assignments alone do not require separate analyzers.
63
+
64
+ An inherited `0` remains an explicit opt-out even with `auto`. Other nonempty values besides `1` stay unchanged. Restricted members always receive `0`; the wrapper adds no sharing channels or access.
65
+
66
+ Resume keeps the member's saved preference unless overridden. Older member records default to `auto`. This choice applies to teammate startup, not live processes or main-session resume.
67
+
68
+ Launch, list, and status results include `analysis`. When this coordinator launched the member, `analysisPolicy` also reports the effective `warmAttach` value and reason. `reuse requested` does not mean attachment succeeded.
69
+
70
+ The coordinator does not persist effective startup details; after recovery they appear when the member next launches.
71
+
72
+ pi-lens owns peer selection, supported shared analysis, and fallback to local analysis. No eligible provider means ordinary local analysis, not a launch failure. An existing leader participates only if its session started with warm attach enabled.
73
+
74
+ The wrapper leaves plugin configuration, data directories, tool paths, and caches unchanged. It does not prove matching environments or rules, team-only routing, successful attachment, or memory savings. Choose `separate` when those existing pi-lens limitations matter.
57
75
 
58
76
  ### Avoid repeated work without dropping updates
59
77
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ac-teams",
3
- "version": "0.0.16",
3
+ "version": "0.0.18",
4
4
  "description": "Small, persistent teams for Pi",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -0,0 +1,10 @@
1
+ import type { Member } from "./types.ts";
2
+
3
+ /** Launch policy only: pi-lens owns attachment, compatibility, and fallback. */
4
+ export function analysisPolicy(member: Pick<Member, "analysis" | "access">, inherited = process.env.PI_LENS_WARM_ATTACH) {
5
+ if (member.access) return { warmAttach: "0", reason: "separate: restricted access" };
6
+ if (member.analysis === "separate") return { warmAttach: "0", reason: "separate requested" };
7
+ if (inherited === "0") return { warmAttach: "0", reason: "separate: explicit opt-out" };
8
+ if (inherited && inherited !== "1") return { warmAttach: inherited, reason: "inherited setting unchanged" };
9
+ return { warmAttach: "1", reason: "reuse requested" };
10
+ }
@@ -13,6 +13,7 @@ import { Store, processAlive, processIdentity } from "./store.ts";
13
13
  import { stateRoot } from "./config.ts";
14
14
  import { RpcAgent } from "./rpc-agent.ts";
15
15
  import { hostResources, type HostResources } from "./resources.ts";
16
+ import { analysisPolicy } from "./analysis.ts";
16
17
  import { TerminalAgent, tmux } from "./terminal-agent.ts";
17
18
  import { authentic, isRecord, readLines, send } from "./connection.ts";
18
19
  import { validate } from "./schemas.ts";
@@ -89,6 +90,7 @@ export class Coordinator {
89
90
  private plans = new Map<string, SandboxPlan>();
90
91
  private releasing = new Map<string, Promise<void>>();
91
92
  private sessionDirs = new Map<string, string>();
93
+ private analysisPolicies = new Map<string, ReturnType<typeof analysisPolicy>>();
92
94
  private sandbox?: { status: SandboxStatus; at: number };
93
95
 
94
96
  private options: CoordinatorOptions;
@@ -340,7 +342,7 @@ export class Coordinator {
340
342
  }
341
343
  private publicMember(member: Member) {
342
344
  const { token: _token, identity: _identity, ...view } = member;
343
- return view;
345
+ return member.role === "member" ? { ...view, analysis: member.analysis ?? "auto", analysisPolicy: this.analysisPolicies.get(member.id) } : view;
344
346
  }
345
347
  sandboxStatus(): SandboxStatus {
346
348
  if (!this.options.credentials) return { available: false, backend: "docker", reason: "This Pi root cannot broker provider credentials" };
@@ -449,7 +451,10 @@ export class Coordinator {
449
451
  if (!args.model || !args.model.includes("/")) throw new Error("A provider/model is required (normally inherited from the requesting member)");
450
452
  const resumed = args.action === "resume" ? state.members[args.id ?? ""] : undefined;
451
453
  if (args.action === "resume" && (!resumed || !resumed.sessionFile || !canControl(state, actor, resumed.id))) throw new Error("Resume requires a stopped member you control with a native session file");
452
- if (resumed && isMain(resumed)) return this.resumeMain(resumed.id);
454
+ if (resumed && isMain(resumed)) {
455
+ if (args.analysis !== undefined) throw new Error("Analysis can only be set for teammate launches; a main session keeps its own environment");
456
+ return this.resumeMain(resumed.id);
457
+ }
453
458
  if (resumed && this.cleaning.has(resumed.id)) throw new Error("Worktree cleanup is running; cannot resume this member");
454
459
  if (resumed && (isLive(resumed) || (resumed.pid && processAlive(resumed.pid, resumed.identity)))) throw new Error("Cannot resume a live or uncertain member");
455
460
  const depth = resumed ? resumed.depth : parent.depth + 1;
@@ -474,6 +479,8 @@ export class Coordinator {
474
479
  const locked = resumed ? resumed.provider : parent.access?.network.allow ? parent.provider : undefined;
475
480
  if (access?.network.allow && locked && provider !== locked) throw new Error(`A member with restricted network access can only use ${locked} models`);
476
481
  const member: Member = resumed ? { ...resumed, token: randomUUID(), status: "starting", error: undefined, contextUsage: undefined, modelName: undefined, handover: undefined } : { id, name: args.name, role: "member", parent: actor, depth, token: randomUUID(), cwd, status: "starting", notified: 0, access, provider: access ? provider : undefined };
482
+ member.analysis = args.analysis ?? resumed?.analysis ?? "auto";
483
+ const policy = analysisPolicy(member);
477
484
  const resources = this.resources();
478
485
  for (const warning of resources.warnings) this.options.warning(warning);
479
486
  const assigned = args.task === undefined ? undefined : state.tasks[args.task];
@@ -483,6 +490,7 @@ export class Coordinator {
483
490
  next.members[id] = member;
484
491
  if (assigned) Object.assign(next.tasks[assigned.id], { owner: id, delegator: actor, revision: next.tasks[assigned.id].revision + 1 });
485
492
  });
493
+ this.analysisPolicies.set(id, policy);
486
494
  const brief = assigned ? `${args.brief}\n\nYou own team task ${taskName(assigned)} [${assigned.id}], delegated by ${memberName(parent)}. Start it, and finish with team_task complete and an outcome summary; that one call reports back.` : args.brief;
487
495
  this.publishStatus();
488
496
  member.model = args.model; member.thinking = args.thinking ?? "off";
@@ -513,7 +521,7 @@ export class Coordinator {
513
521
  const rpcArgs = [...(this.options.interactive ? [] : ["--mode", "rpc"]), "--session-dir", sessions, "-e", this.options.extension, "--provider", provider, "--model", modelId, "--thinking", args.thinking ?? "off"];
514
522
  if (resumed?.sessionFile) rpcArgs.push("--session", resumed.sessionFile);
515
523
  // ACSTACK_AGENT_ROLE tells ACStack hooks this is not a main session; children inherit it.
516
- const identity = { HERDR_ENV: "0", ACSTACK_AGENT_ROLE: "member", AC_TEAMS_SOCKET: this.socketPath, AC_TEAMS_MEMBER: id, AC_TEAMS_TOKEN: member.token, AC_TEAMS_ACCESS: JSON.stringify(access ? { enforced: true, ...access } : { enforced: false }) };
524
+ const identity = { PI_LENS_WARM_ATTACH: policy.warmAttach, HERDR_ENV: "0", ACSTACK_AGENT_ROLE: "member", AC_TEAMS_SOCKET: this.socketPath, AC_TEAMS_MEMBER: id, AC_TEAMS_TOKEN: member.token, AC_TEAMS_ACCESS: JSON.stringify(access ? { enforced: true, ...access } : { enforced: false }) };
517
525
  const spawnOptions = access
518
526
  ? await this.sandboxed(id, access, member.cwd, provider, modelId, rpcArgs, identity, sessions)
519
527
  : { cwd: member.cwd, args: rpcArgs, command: this.options.command, env: { ...process.env, ...identity, AC_TEAMS_TERMINAL_SOCKET: join(this.store.dir, "t.sock") } };
package/src/index.ts CHANGED
@@ -303,7 +303,7 @@ export default function teams(pi: ExtensionAPI) {
303
303
  const taskTrackingGuidance = "Before delegating a deliverable through team_agent, create or reuse a team_task and pass it as the launch task; its owner's completion outcome reports back to you. Use one shared task list for your own work and delegated work; do not create one task per teammate by default. Quick lookups and brief second opinions need no separate task unless their results need coordination or follow-up.";
304
304
  pi.registerTool({
305
305
  name: "team_agent", label: "Team agent", description: "Launch/resume Pi members, list/status, stop owned subtrees, change root limits, and explicitly clean stopped worktrees. Create or reuse team_task entries for delegated deliverables; quick lookups need no task unless results need coordination or follow-up.",
306
- promptGuidelines: [guidance, taskTrackingGuidance, "Delegation needs the user's authorization. Give each new member a unique descriptive name (one or two lowercase words, hyphenated if two), plus a bounded brief. Names remain stable on resume. Avoid concurrent writers in the same cwd. Worktrees are opt-in."],
306
+ promptGuidelines: [guidance, taskTrackingGuidance, "Analysis defaults to auto: request existing pi-lens same-workspace reuse, possibly from sessions outside this team. If pi-lens is absent or disabled, launch proceeds without enabling it. Explicit environment opt-outs win; restricted members stay separate. Choose analysis: separate for different environments or rules, uncertain compatibility, suspicious diagnostics, or independent analyzer state. Different assignments alone do not require separation. This is a launch/resume preference, not proof of attachment or compatibility; an existing leader participates only if warm attach was already enabled there.", "Delegation needs the user's authorization. Give each new member a unique descriptive name (one or two lowercase words, hyphenated if two), plus a bounded brief. Names remain stable on resume. Avoid concurrent writers in the same cwd. Worktrees are opt-in."],
307
307
  parameters: AgentSchema,
308
308
  ...teamToolRenderers("team_agent", memberLabels),
309
309
  async execute(_id, args, _signal, _update, ctx) { return output(await handle("agent", args, ctx)); },
package/src/panel.ts CHANGED
@@ -65,6 +65,7 @@ export async function showTeamsPanel(ctx: ExtensionContext, host: PanelHost): Pr
65
65
  return clipped + " ".repeat(Math.max(0, width - visibleWidth(clipped)));
66
66
  };
67
67
  const heading = (text: string) => theme.bold(fg("text", text));
68
+ const tableHeader = (text: string) => theme.bold(theme.underline(fg("accent", text)));
68
69
  const label = (key: string, value: string) => fg("muted", key.padEnd(9) + " ") + value;
69
70
  const wrap = (lines: string[], width: number) => lines.flatMap(line => wrapTextWithAnsi(line, Math.max(1, width)));
70
71
  const editor = new Editor(tui, {
@@ -159,7 +160,8 @@ export async function showTeamsPanel(ctx: ExtensionContext, host: PanelHost): Pr
159
160
  else if (selected.right >= columnScroll + width) columnScroll = selected.right - width + 1;
160
161
  columnScroll = Math.max(0, Math.min(columnScroll, Math.max(0, art.width - width)));
161
162
  const rows = art.styled.map((spans, y) => {
162
- let row = spans.map(span => fg(span.cls === "border" || span.cls === "edge" ? "dim" : "text", span.text)).join("");
163
+ const row = spans.map(span => fg(span.cls === "border" || span.cls === "edge" ? "dim" : "text", span.text)).join("");
164
+ const replacements: { left: number; right: number; text: string }[] = [];
163
165
  for (const task of tasks) {
164
166
  const box = bounds.get(task.id);
165
167
  if (!box || y < box.top || y > box.bottom) continue;
@@ -174,10 +176,18 @@ export async function showTeamsPanel(ctx: ExtensionContext, host: PanelHost): Pr
174
176
  // The selected box keeps its status colour but gets a heavy border and bold text, so selection never competes with status.
175
177
  const edge = (part: string) => statusText(task, active ? part.replace(/[┌┐└┘─│┬┴]/g, ch => ({ "┌": "┏", "┐": "┓", "└": "┗", "┘": "┛", "─": "━", "│": "┃", "┬": "┯", "┴": "┷" })[ch]!) : part);
176
178
  const styled = border ? edge(text) : edge("│") + (active ? theme.bold(interior) : interior) + edge("│");
177
- row = sliceByColumn(row, 0, box.left) + styled + sliceByColumn(row, box.right + 1, art.width);
179
+ replacements.push({ left: box.left, right: box.right, text: styled });
178
180
  }
181
+ // Slice the original row once per gap; re-slicing styled replacements multiplies ANSI codes.
182
+ let column = 0;
183
+ const parts = replacements.sort((a, b) => a.left - b.left).flatMap(box => {
184
+ const gap = sliceByColumn(row, column, box.left - column);
185
+ column = box.right + 1;
186
+ return [gap, box.text];
187
+ });
188
+ parts.push(sliceByColumn(row, column, art.width - column));
179
189
  const indent = " ".repeat(Math.max(0, Math.floor((width - art.width) / 2)));
180
- return indent + sliceByColumn(row, columnScroll, width);
190
+ return indent + sliceByColumn(parts.join(""), columnScroll, width);
181
191
  });
182
192
  return { rows, selectedRow: selected.top, selectedHeight: selected.bottom - selected.top + 1, width: art.width };
183
193
  }
@@ -204,11 +214,13 @@ export async function showTeamsPanel(ctx: ExtensionContext, host: PanelHost): Pr
204
214
  let selectedHeight = 1, graphWidth = 0;
205
215
  if (tab === 0) {
206
216
  const graphData = graph ? graphLines(leftWidth) : undefined;
207
- left = graphData ? graphData.rows : tasks.length ? [fg("muted", " Task"), "",
217
+ const titleWidth = Math.max(1, leftWidth - 12);
218
+ left = graphData ? graphData.rows : tasks.length ? [tableHeader(` ${pad("Task", titleWidth)} State`), "",
208
219
  ...tasks.map((task, index) => {
209
- const text = `${index === taskIndex ? "●" : " "} ${taskTitle(task.id)}`;
210
- const body = fg(taskStatus(task) === "Done" ? "dim" : "text", truncateToWidth(text, Math.max(1, leftWidth - 10)));
211
- return (index === taskIndex ? theme.bold(body) : body) + " · " + stateLabel(task);
220
+ const selected = index === taskIndex;
221
+ const body = fg(taskStatus(task) === "Done" ? "dim" : "text", pad(taskTitle(task.id), titleWidth));
222
+ return fg(selected ? "accent" : "dim", `${selected ? "●" : " "} `)
223
+ + (selected ? theme.bold(body) : body) + " " + stateLabel(task);
212
224
  })] : [heading("No tasks yet"), "N Request a task from Main"];
213
225
  selectedRow = graphData ? graphData.selectedRow : taskIndex + 2;
214
226
  selectedHeight = graphData?.selectedHeight ?? 1;
@@ -231,7 +243,7 @@ export async function showTeamsPanel(ctx: ExtensionContext, host: PanelHost): Pr
231
243
  const nameExtra = Math.min(spare, Math.max(0, ...ids.map(id => visibleWidth(name(id)) - nameWidth)));
232
244
  const gap = " ".repeat(4 + Math.floor((spare - nameExtra) / gaps));
233
245
  nameWidth += nameExtra + (spare - nameExtra) % gaps;
234
- left = [fg("muted", ` ${pad("Member", nameWidth)}${gap}${pad("State", sessionWidth)}`
246
+ left = [tableHeader(` ${pad("Member", nameWidth)}${gap}${pad("State", sessionWidth)}`
235
247
  + (showTasks ? `${gap}${pad("Done", tasksWidth)}` : "") + (showDirectory ? `${gap}Directory` : "")), ...ids.map((id, index) => {
236
248
  const member = snapshot.members.find(member => member.id === id);
237
249
  const selected = index === memberIndex;
@@ -254,7 +266,7 @@ export async function showTeamsPanel(ctx: ExtensionContext, host: PanelHost): Pr
254
266
  const fromWidth = Math.min(16, Math.max(4, ...events.map(event => visibleWidth(name(event.from)))));
255
267
  const taskWidth = Math.min(5, Math.max(4, ...events.map(event => event.task ? visibleWidth(taskRef(event.task)) : 0)));
256
268
  const summaryWidth = Math.max(1, leftWidth - (2 + fromWidth + 2 + 7 + 2 + 2 + taskWidth + 2 + 5));
257
- left = events.length ? [fg("muted", ` ${pad("From", fromWidth)} ${pad("Kind", 7)} ${pad("Summary", summaryWidth)} ${pad("Task", taskWidth)} Time`),
269
+ left = events.length ? [tableHeader(` ${pad("From", fromWidth)} ${pad("Kind", 7)} ${pad("Summary", summaryWidth)} ${pad("Task", taskWidth)} Time`),
258
270
  ...events.map((event, index) => {
259
271
  const selected = index === eventIndex;
260
272
  const from = pad(name(event.from), fromWidth), summary = pad(headline(event), summaryWidth);
package/src/render.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { stripVTControlCharacters } from "node:util";
2
2
  import { keyHint, type MessageRenderer, type Theme, type ToolRenderers } from "@earendil-works/pi-coding-agent";
3
- import { Container, Text, TruncatedText, truncateToWidth, visibleWidth, wrapTextWithAnsi, type Component } from "@earendil-works/pi-tui";
3
+ import { Box, Container, Text, TruncatedText, truncateToWidth, visibleWidth, wrapTextWithAnsi, type Component } from "@earendil-works/pi-tui";
4
4
 
5
5
  export const cleanText = (text: string): string => stripVTControlCharacters(text).replaceAll("\t", " ").replace(/[\x00-\x08\x0b-\x1f\x7f-\x9f]/g, "");
6
6
  const inline = (value: unknown) => cleanText(String(value)).replace(/[\n\r\t]/g, " ");
@@ -125,7 +125,7 @@ export function teamToolRenderers(name: string, getLabels: MemberLabels = defaul
125
125
  }
126
126
 
127
127
  export function teamMessageRenderer(getLabels: MemberLabels = defaultLabels): MessageRenderer {
128
- return (message, { expanded }, theme) => {
128
+ return (message, { expanded, outputPad }, theme) => {
129
129
  const details = message.details as { through?: number; messages?: { from: string; summary: string; recipients: string[]; task?: string; kind?: string }[] } | undefined;
130
130
  const posts = details?.messages ?? [], recent = posts.at(-1);
131
131
  const more = posts.length > 1 ? ` (+${posts.length - 1} more)` : "";
@@ -134,7 +134,9 @@ export function teamMessageRenderer(getLabels: MemberLabels = defaultLabels): Me
134
134
  // Lead with what happened: "count-a completed #1 Count lines · 37 lines", or "count-a → Main · #1 … · question".
135
135
  const what = recent && (recent.kind === "outcome" && recent.task ? `${name(recent.from)} completed ${name(recent.task)}` : `${name(recent.from)} → ${recent.recipients.map(name).join(", ")}${recent.task ? ` · ${name(recent.task)}` : ""}`);
136
136
  const preview = recent ? `${what} · ${recent.summary.split("\n")[0]}${more}` : `Updates through ${details?.through ?? "unknown"}`;
137
- return entry("ac-teams", details ?? { content: message.content }, expanded, theme, false, preview);
137
+ const box = new Box(outputPad, 0);
138
+ box.addChild(entry("ac-teams", details ?? { content: message.content }, expanded, theme, false, preview));
139
+ return box;
138
140
  };
139
141
  }
140
142
 
package/src/schemas.ts CHANGED
@@ -10,6 +10,7 @@ export const AgentSchema = Type.Object({
10
10
  id: Type.Optional(id), brief: Type.Optional(text), cwd: Type.Optional(Type.String({ minLength: 1, maxLength: 4096 })),
11
11
  model: Type.Optional(Type.String({ minLength: 1, maxLength: 256 })),
12
12
  thinking: Type.Optional(Type.Union(["off", "minimal", "low", "medium", "high", "xhigh"].map(v => Type.Literal(v)))),
13
+ analysis: Type.Optional(Type.Union([Type.Literal("auto"), Type.Literal("separate")], { description: "Launch/resume only: auto (default) requests existing pi-lens same-workspace reuse, possibly outside this team; explicit environment opt-outs win. Choose separate for different environments or rules, uncertain compatibility, or independent analyzer state. Does not disable checks or guarantee attachment. Resume keeps the saved preference unless overridden; restricted members always use separate analysis." })),
13
14
  worktree: Type.Optional(Type.Boolean()), base: Type.Optional(id), target: Type.Optional(id),
14
15
  limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 100 })),
15
16
  depth: Type.Optional(Type.Integer({ minimum: 0, maximum: 10 })),
@@ -75,6 +76,7 @@ export function validate(operation: string, value: unknown): void {
75
76
  const args = value as { action: string; name?: string };
76
77
  if (args.action === "launch" && !args.name) throw new Error("A descriptive teammate name is required on launch");
77
78
  if (args.action !== "launch" && args.name !== undefined) throw new Error("A teammate name can only be set on launch");
79
+ if (args.action !== "launch" && args.action !== "resume" && (value as { analysis?: unknown }).analysis !== undefined) throw new Error("Analysis can only be set on launch or resume");
78
80
  if (args.action !== "launch" && (value as { task?: unknown }).task !== undefined) throw new Error("A task can only be assigned on launch");
79
81
  if (args.action !== "launch" && (value as { access?: unknown }).access !== undefined) throw new Error("Access can only be set on launch; resume keeps the member's access");
80
82
  }
package/src/store.ts CHANGED
@@ -116,6 +116,7 @@ function validate(state: unknown): asserts state is TeamState {
116
116
  requireValue(member.handover === undefined || ["busy", "idle", "input"].includes(String(member.handover)), "Invalid member handover state");
117
117
  requireValue(member.promotion === undefined || (object(member.promotion) && ["stopped", "moved", "role", "entry"].includes(String(member.promotion.step)) && text(member.promotion.sessionFile)), "Invalid member promotion");
118
118
  requireValue(member.waiting === undefined || (object(member.waiting) && Array.isArray(member.waiting.tasks) && member.waiting.tasks.every(task => typeof task === "string" && Object.hasOwn(state.tasks as object, task))), "Invalid member wait");
119
+ requireValue(member.analysis === undefined || member.analysis === "auto" || member.analysis === "separate", "Invalid member analysis preference");
119
120
  requireValue(member.terminal === undefined || typeof member.terminal === "boolean", "Invalid member terminal flag");
120
121
  requireValue(member.needsInput === undefined || typeof member.needsInput === "boolean", "Invalid member input flag");
121
122
  requireValue(member.pid === undefined || integer(member.pid, 1), "Invalid member PID");
@@ -44,7 +44,7 @@ export class TerminalAgent extends EventEmitter {
44
44
  writeFileSync(config, "set -g status off\nset -g prefix None\nset -s extended-keys always\nbind-key -n C-S-Left detach-client\n", { flag: "wx", mode: 0o600 });
45
45
  this.files.push(config);
46
46
  const exports = Object.entries(this.options.env)
47
- .filter(([key, value]) => (key.startsWith("AC_TEAMS_") || key === "HERDR_ENV" || key === "ACSTACK_AGENT_ROLE") && value !== undefined)
47
+ .filter(([key, value]) => (key.startsWith("AC_TEAMS_") || key === "HERDR_ENV" || key === "ACSTACK_AGENT_ROLE" || key === "PI_LENS_WARM_ATTACH") && value !== undefined)
48
48
  .map(([key, value]) => `export ${key}=${quote(value!)}`);
49
49
  const command = this.options.command ?? process.env.AC_TEAMS_PI ?? "pi";
50
50
  writeFileSync(script, [...exports,
package/src/types.ts CHANGED
@@ -36,6 +36,8 @@ export interface Member {
36
36
  modelName?: string;
37
37
  contextUsage?: ContextUsage;
38
38
  thinking?: string;
39
+ /** Launch-time pi-lens preference; absent older records default to auto. */
40
+ analysis?: "auto" | "separate";
39
41
  activity?: string;
40
42
  summary?: string;
41
43
  terminal?: boolean;
@@ -153,6 +155,7 @@ export interface AgentArgs {
153
155
  cwd?: string;
154
156
  model?: string;
155
157
  thinking?: string;
158
+ analysis?: "auto" | "separate";
156
159
  worktree?: boolean;
157
160
  base?: string;
158
161
  target?: string;