ac-teams 0.0.17 → 0.0.19

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.17",
3
+ "version": "0.0.19",
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/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 })),
@@ -33,16 +34,16 @@ export const TaskSchema = Type.Object({
33
34
  offset: Type.Optional(Type.Integer({ minimum: 0 })), limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 100 })),
34
35
  action: Type.Union(["create", "list", "read", "edit", "claim", "start", "release", "complete", "reopen"].map(v => Type.Literal(v))),
35
36
  id: Type.Optional(id), ...taskFields,
36
- summary: Type.Optional(Type.String({ minLength: 1, maxLength: 16000, description: "Complete: the outcome, published atomically with done: what was done, how it was checked, and caveats. Start with the result itself (e.g. 37 lines); the first line is the one people see. Complete enough that the delegator can act without reading detail." })),
37
- detail: Type.Optional(Type.String({ minLength: 1, maxLength: 16000, description: "Complete: optional supporting detail, read on demand." })),
37
+ summary: Type.Optional(Type.String({ minLength: 1, maxLength: 16000, description: "Complete: brief outcome, required action, and critical caveats, published atomically with done and delivered in notices. Put supporting evidence, file references, and check-by-check results in detail." })),
38
+ detail: Type.Optional(Type.String({ minLength: 1, maxLength: 16000, description: "Complete: optional supporting evidence, checks run, file references, and fuller limitations, read on demand." })),
38
39
  tasks: Type.Optional(Type.Array(Type.Object({ ...taskFields, title: Type.String({ minLength: 1, maxLength: 500 }) }, { additionalProperties: false }), { minItems: 1, maxItems: 100, description: "Create several tasks atomically. Use instead of top-level task fields. Dependencies must refer to existing task IDs, not items in this batch." })),
39
40
  }, { additionalProperties: false });
40
41
  export const MessageSchema = Type.Object({
41
42
  action: Type.Union(["send", "history", "search", "wait"].map(v => Type.Literal(v))),
42
43
  tasks: Type.Optional(Type.Array(id, { maxItems: 100, uniqueItems: true, description: "Wait: also wake on posts linked to these tasks, e.g. ones you delegated." })),
43
44
  recipients: Type.Optional(Type.Array(id, { minItems: 1, maxItems: 100, uniqueItems: true, description: "Exact teammate names or member IDs." })),
44
- summary: Type.Optional(Type.String({ minLength: 1, maxLength: 16000, description: "Send: delivered directly to recipients. Start with the point (the first line is the one people see) and what it changes; complete enough to act on." })),
45
- detail: Type.Optional(Type.String({ minLength: 1, maxLength: 16000, description: "Send: optional supporting detail, read only when needed." })),
45
+ summary: Type.Optional(Type.String({ minLength: 1, maxLength: 16000, description: "Send: brief outcome, question, or update, with any required action and critical caveats, delivered directly to recipients. Put supporting evidence, file references, and check-by-check results in detail." })),
46
+ detail: Type.Optional(Type.String({ minLength: 1, maxLength: 16000, description: "Send: optional supporting evidence, checks run, file references, and fuller limitations, read only when needed." })),
46
47
  task: Type.Optional(Type.String({ minLength: 1, maxLength: 128, description: "Send: link an existing task; its owner and delegator receive the post." })),
47
48
  replyTo: Type.Optional(Type.String({ minLength: 1, maxLength: 128, description: "Send: the post ID this answers; its author receives the reply and it stays on that post's task." })),
48
49
  query: Type.Optional(Type.String({ minLength: 1, maxLength: 500 })),
@@ -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;