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.
- package/docs/operations.md +19 -1
- package/package.json +1 -1
- package/src/analysis.ts +10 -0
- package/src/coordinator.ts +11 -3
- package/src/index.ts +1 -1
- package/src/schemas.ts +6 -4
- package/src/store.ts +1 -0
- package/src/terminal-agent.ts +1 -1
- package/src/types.ts +3 -0
package/docs/operations.md
CHANGED
|
@@ -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
|
|
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
package/src/analysis.ts
ADDED
|
@@ -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
|
+
}
|
package/src/coordinator.ts
CHANGED
|
@@ -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))
|
|
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:
|
|
37
|
-
detail: Type.Optional(Type.String({ minLength: 1, maxLength: 16000, description: "Complete: optional supporting
|
|
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:
|
|
45
|
-
detail: Type.Optional(Type.String({ minLength: 1, maxLength: 16000, description: "Send: optional supporting
|
|
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");
|
package/src/terminal-agent.ts
CHANGED
|
@@ -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;
|