ai-fob 1.11.2 → 1.11.3

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.
@@ -6,7 +6,7 @@ Project-local MVP extension for persistent, PTY-backed teammates.
6
6
 
7
7
  - Runtime: Pi by default; experimental terminal-level Claude Code support via `runtime: "claude-code"`
8
8
  - Tool: `inter_agent_team`
9
- - Actions: `create`, `list`, `show`, `send`, `attach`, `close_agent`, `close_team`, `kill_agent`
9
+ - Actions: `create`, `list`, `show`, `send`, `attach`, `close_agent`, `close_team`, `kill_agent`, `start_build_feature`, `status_build_feature`, `resume_build_feature`, `stop_build_feature`
10
10
  - Storage: `.pi/inter-agent-team/`
11
11
  - Nested team spawning: disabled by policy/environment guard
12
12
 
@@ -67,6 +67,59 @@ This launches roughly:
67
67
  claude --dangerously-skip-permissions "tell me about this repo"
68
68
  ```
69
69
 
70
+ ## Extension-backed build-feature monitor
71
+
72
+ The extension can run a deterministic multi-phase build-feature monitor that launches Claude Code teammates phase-by-phase and polls filesystem completion reports.
73
+
74
+ Start:
75
+
76
+ ```ts
77
+ inter_agent_team({
78
+ action: "start_build_feature",
79
+ plan: "specs/07_instagram-x-creator-feed/07_instagram-x-creator-feed_V1.md",
80
+ startPhase: 2,
81
+ endPhase: 5,
82
+ pollSeconds: 600,
83
+ closeTeams: true
84
+ })
85
+ ```
86
+
87
+ Status:
88
+
89
+ ```ts
90
+ inter_agent_team({ action: "status_build_feature", plan: "specs/.../plan.md" })
91
+ ```
92
+
93
+ Resume after interruption/restart:
94
+
95
+ ```ts
96
+ inter_agent_team({ action: "resume_build_feature", plan: "specs/.../plan.md" })
97
+ ```
98
+
99
+ Stop monitor:
100
+
101
+ ```ts
102
+ inter_agent_team({ action: "stop_build_feature", plan: "specs/.../plan.md", closeTeams: false })
103
+ ```
104
+
105
+ Workflow state is stored under:
106
+
107
+ ```txt
108
+ .pi/inter-agent-team/workflows/build-feature/<feature-slug>.json
109
+ ```
110
+
111
+ The monitor uses phase reports as the primary completion contract:
112
+
113
+ ```txt
114
+ {SPEC_DIR}/phase<N>_*/phase_completion_report.md
115
+ ```
116
+
117
+ and launches each phase with Claude Code:
118
+
119
+ ```bash
120
+ claude --dangerously-skip-permissions "/build-phase-V2 <plan> <phase>"
121
+ ```
122
+
70
123
  ## Attach behavior
71
124
 
72
125
  `attach` is modal and captures keyboard focus. Use it only when the user explicitly wants to take over or interact directly with the teammate terminal.
@@ -2,12 +2,14 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import { getSessionManager } from "./runtime/session-manager.js";
3
3
  import { registerInterAgentTeamTool } from "./tool.js";
4
4
  import { registerInterAgentTeamWidget } from "./widget.js";
5
+ import { getBuildFeatureWorkflowManager } from "./workflows/build-feature/manager.js";
5
6
 
6
7
  export default function interAgentTeamExtension(pi: ExtensionAPI): void {
7
8
  const manager = getSessionManager();
9
+ const buildFeatureManager = getBuildFeatureWorkflowManager(manager);
8
10
 
9
- registerInterAgentTeamTool(pi, manager);
10
- registerInterAgentTeamWidget(pi, manager);
11
+ registerInterAgentTeamTool(pi, manager, buildFeatureManager);
12
+ registerInterAgentTeamWidget(pi, manager, buildFeatureManager);
11
13
 
12
14
  pi.on("session_start", (_event, ctx) => {
13
15
  if (process.env.PI_INTER_AGENT_TEAM_CAN_SPAWN === "false") {
@@ -16,6 +18,7 @@ export default function interAgentTeamExtension(pi: ExtensionAPI): void {
16
18
  });
17
19
 
18
20
  pi.on("session_shutdown", async () => {
21
+ buildFeatureManager.stopAllTimers();
19
22
  await manager.closeAll({ force: true, reason: "pi_session_shutdown" });
20
23
  });
21
24
  }
@@ -11,6 +11,10 @@ export const InterAgentTeamParams = Type.Object({
11
11
  "close_agent",
12
12
  "close_team",
13
13
  "kill_agent",
14
+ "start_build_feature",
15
+ "status_build_feature",
16
+ "resume_build_feature",
17
+ "stop_build_feature",
14
18
  ] as const),
15
19
 
16
20
  team: Type.Optional(Type.String({ description: "Team id or name" })),
@@ -20,6 +24,13 @@ export const InterAgentTeamParams = Type.Object({
20
24
  message: Type.Optional(Type.String({ description: "Message to send to a teammate" })),
21
25
  task: Type.Optional(Type.String({ description: "Initial team task" })),
22
26
 
27
+ plan: Type.Optional(Type.String({ description: "High-level build plan path for build-feature workflows" })),
28
+ workflow: Type.Optional(Type.String({ description: "Build-feature workflow id, feature slug, or plan path" })),
29
+ startPhase: Type.Optional(Type.Number({ description: "First phase to run" })),
30
+ endPhase: Type.Optional(Type.Number({ description: "Last phase to run" })),
31
+ pollSeconds: Type.Optional(Type.Number({ description: "Polling interval in seconds for build-feature workflows" })),
32
+ closeTeams: Type.Optional(Type.Boolean({ description: "Whether completed phase teams should be closed automatically" })),
33
+
23
34
  agents: Type.Optional(
24
35
  Type.Array(
25
36
  Type.Object({
@@ -43,4 +54,8 @@ export type InterAgentTeamAction =
43
54
  | "attach"
44
55
  | "close_agent"
45
56
  | "close_team"
46
- | "kill_agent";
57
+ | "kill_agent"
58
+ | "start_build_feature"
59
+ | "status_build_feature"
60
+ | "resume_build_feature"
61
+ | "stop_build_feature";
@@ -6,6 +6,8 @@ import type { TeamAgentRecord, TeamRecord, ToolDetails } from "./types.js";
6
6
  import { getRuntimeAdapter } from "./runtime/runtime-adapter.js";
7
7
  import type { InterAgentSessionManager } from "./runtime/session-manager.js";
8
8
  import { TeammateOverlay } from "./ui/teammate-overlay.js";
9
+ import type { BuildFeatureWorkflowManager } from "./workflows/build-feature/manager.js";
10
+ import { formatBuildFeatureWorkflow } from "./workflows/build-feature/manager.js";
9
11
 
10
12
  function textResult(text: string, details: ToolDetails): AgentToolResult<ToolDetails> {
11
13
  return { content: [{ type: "text", text }], details };
@@ -47,7 +49,7 @@ function syncTeamStatuses(team: TeamRecord, manager: InterAgentSessionManager):
47
49
  return team;
48
50
  }
49
51
 
50
- export function registerInterAgentTeamTool(pi: ExtensionAPI, manager: InterAgentSessionManager): void {
52
+ export function registerInterAgentTeamTool(pi: ExtensionAPI, manager: InterAgentSessionManager, buildFeatureManager?: BuildFeatureWorkflowManager): void {
51
53
  pi.registerTool({
52
54
  name: "inter_agent_team",
53
55
  label: "Inter-Agent Team",
@@ -60,13 +62,49 @@ export function registerInterAgentTeamTool(pi: ExtensionAPI, manager: InterAgent
60
62
  "Do not use inter_agent_team attach automatically after create, list, show, or send.",
61
63
  "Do not use inter_agent_team for simple one-shot delegation; use subagent for disposable tasks instead.",
62
64
  "Do not create nested inter-agent teams from inside a teammate session.",
65
+ "Use start_build_feature/status_build_feature/resume_build_feature/stop_build_feature for extension-backed multi-phase build orchestration instead of manual long-running polling.",
63
66
  ],
64
67
  parameters: InterAgentTeamParams,
65
68
 
66
69
  async execute(_toolCallId, params: any, _signal, _onUpdate, ctx: any) {
67
70
  try {
68
71
  switch (params.action) {
69
- case "create": {
72
+ case "start_build_feature": {
73
+ if (!buildFeatureManager) throw new Error("Build-feature workflow manager is not available.");
74
+ const plan = requireString(params.plan, "plan");
75
+ const workflow = await buildFeatureManager.start({
76
+ cwd: ctx.cwd ?? process.cwd(),
77
+ plan,
78
+ startPhase: typeof params.startPhase === "number" ? params.startPhase : undefined,
79
+ endPhase: typeof params.endPhase === "number" ? params.endPhase : undefined,
80
+ pollSeconds: typeof params.pollSeconds === "number" ? params.pollSeconds : undefined,
81
+ closeTeams: typeof params.closeTeams === "boolean" ? params.closeTeams : undefined,
82
+ });
83
+ return textResult(`Started build-feature workflow.\n\n${formatBuildFeatureWorkflow(workflow)}`, { ok: true, workflow });
84
+ }
85
+
86
+ case "status_build_feature": {
87
+ if (!buildFeatureManager) throw new Error("Build-feature workflow manager is not available.");
88
+ const result = await buildFeatureManager.status({ cwd: ctx.cwd ?? process.cwd(), workflow: params.workflow, plan: params.plan });
89
+ if (result.workflow) return textResult(formatBuildFeatureWorkflow(result.workflow), { ok: true, workflow: result.workflow });
90
+ const workflows = result.workflows ?? [];
91
+ const text = workflows.length ? workflows.map(formatBuildFeatureWorkflow).join("\n\n") : "No build-feature workflows found.";
92
+ return textResult(text, { ok: true, workflows });
93
+ }
94
+
95
+ case "resume_build_feature": {
96
+ if (!buildFeatureManager) throw new Error("Build-feature workflow manager is not available.");
97
+ const workflow = await buildFeatureManager.resume({ cwd: ctx.cwd ?? process.cwd(), workflow: params.workflow, plan: params.plan });
98
+ return textResult(`Resumed build-feature workflow.\n\n${formatBuildFeatureWorkflow(workflow)}`, { ok: true, workflow });
99
+ }
100
+
101
+ case "stop_build_feature": {
102
+ if (!buildFeatureManager) throw new Error("Build-feature workflow manager is not available.");
103
+ const workflow = await buildFeatureManager.stop({ cwd: ctx.cwd ?? process.cwd(), workflow: params.workflow, plan: params.plan, closeTeams: params.closeTeams === true });
104
+ return textResult(`Stopped build-feature workflow.\n\n${formatBuildFeatureWorkflow(workflow)}`, { ok: true, workflow });
105
+ }
106
+
107
+ case "create": {
70
108
  if (process.env.PI_INTER_AGENT_TEAM_CAN_SPAWN === "false") {
71
109
  throw new Error("Nested inter-agent-team creation is disabled for teammate sessions.");
72
110
  }
@@ -57,11 +57,64 @@ export interface CreateTeamInput {
57
57
  cwd?: string;
58
58
  }
59
59
 
60
+ export type BuildFeatureWorkflowStatus = "running" | "completed" | "blocked" | "failed" | "stopped" | "interrupted";
61
+
62
+ export type BuildFeaturePhaseStatus = "pending" | "running" | "completed" | "blocked" | "failed" | "interrupted" | "skipped";
63
+
64
+ export interface BuildFeaturePhaseRecord {
65
+ phase: number;
66
+ status: BuildFeaturePhaseStatus;
67
+ teamId?: string;
68
+ teamName?: string;
69
+ agentId?: string;
70
+ agentName?: string;
71
+ launchPrompt?: string;
72
+ startedAt?: string;
73
+ completedAt?: string;
74
+ lastPollAt?: string;
75
+ completionReport?: string;
76
+ result?: "complete" | "blocked" | "failed" | "unknown";
77
+ validationResult?: "pass" | "pass-with-followups" | "blocked" | "fail" | "unknown" | string;
78
+ stateHashBefore?: string;
79
+ stateHashAfter?: string;
80
+ note?: string;
81
+ }
82
+
83
+ export interface BuildFeatureWorkflowRecord {
84
+ workflow: "build-feature-interagent";
85
+ id: string;
86
+ status: BuildFeatureWorkflowStatus;
87
+ planPath: string;
88
+ planPathAbs: string;
89
+ projectRoot: string;
90
+ specDir: string;
91
+ featureSlug: string;
92
+ taskName: string;
93
+ teamName: string;
94
+ createdAt: string;
95
+ updatedAt: string;
96
+ startedAt?: string;
97
+ completedAt?: string;
98
+ stoppedAt?: string;
99
+ startPhase: number;
100
+ endPhase: number;
101
+ totalPhases: number;
102
+ currentPhase: number;
103
+ pollSeconds: number;
104
+ closeTeams: boolean;
105
+ lastPollAt?: string;
106
+ nextPollAt?: string;
107
+ lastMessage?: string;
108
+ phases: BuildFeaturePhaseRecord[];
109
+ }
110
+
60
111
  export interface ToolDetails {
61
112
  ok: boolean;
62
113
  team?: TeamRecord;
63
114
  teams?: TeamRecord[];
64
115
  agent?: TeamAgentRecord;
116
+ workflow?: BuildFeatureWorkflowRecord;
117
+ workflows?: BuildFeatureWorkflowRecord[];
65
118
  output?: string;
66
119
  error?: string;
67
120
  }
@@ -1,7 +1,8 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import { getProjectRegistry } from "./registry.js";
3
3
  import type { InterAgentSessionManager, ManagedTeammateSession } from "./runtime/session-manager.js";
4
- import type { TeamAgentRecord, TeamRecord } from "./types.js";
4
+ import type { TeamAgentRecord, TeamRecord, BuildFeatureWorkflowRecord } from "./types.js";
5
+ import type { BuildFeatureWorkflowManager } from "./workflows/build-feature/manager.js";
5
6
 
6
7
  const WIDGET_KEY = "inter-agent-team";
7
8
 
@@ -28,8 +29,19 @@ function renderAgentLine(team: TeamRecord, agent: TeamAgentRecord, live: Managed
28
29
  return `${theme.fg(role(status), icon(status))} ${theme.fg("text", agent.name)} ${theme.fg("muted", `[${agent.runtime}]`)} ${theme.fg(role(status), `— ${suffix}`)}`;
29
30
  }
30
31
 
31
- function renderLines(activeTeams: TeamRecord[], liveSessions: ManagedTeammateSession[], theme: any): string[] {
32
+ function renderWorkflowLine(workflow: BuildFeatureWorkflowRecord, theme: any): string {
33
+ const phase = workflow.phases.find((item) => item.phase === workflow.currentPhase);
34
+ const next = workflow.nextPollAt ? ` next poll ${workflow.nextPollAt}` : "";
35
+ return `${theme.fg(role(workflow.status), icon(workflow.status))} ${theme.fg("text", workflow.featureSlug)} ${theme.fg("muted", `— ${workflow.status}; phase ${workflow.currentPhase}/${workflow.endPhase}${phase ? ` ${phase.status}` : ""}${next}`)}`;
36
+ }
37
+
38
+ function renderLines(activeTeams: TeamRecord[], liveSessions: ManagedTeammateSession[], workflows: BuildFeatureWorkflowRecord[], theme: any): string[] {
32
39
  const lines: string[] = [theme.fg("accent", "Inter-Agent Teams")];
40
+ const activeWorkflows = workflows.filter((workflow) => workflow.status === "running" || workflow.status === "blocked" || workflow.status === "failed" || workflow.status === "interrupted");
41
+ if (activeWorkflows.length > 0) {
42
+ lines.push(theme.fg("accent", "Build Feature Workflows"));
43
+ for (const workflow of activeWorkflows) lines.push(renderWorkflowLine(workflow, theme));
44
+ }
33
45
  for (const team of activeTeams) {
34
46
  lines.push(theme.fg("muted", `Team: ${team.name} (${team.id}) — ${team.status}`));
35
47
  if (team.agents.length === 0) {
@@ -44,7 +56,7 @@ function renderLines(activeTeams: TeamRecord[], liveSessions: ManagedTeammateSes
44
56
  return lines;
45
57
  }
46
58
 
47
- export function registerInterAgentTeamWidget(pi: ExtensionAPI, manager: InterAgentSessionManager): void {
59
+ export function registerInterAgentTeamWidget(pi: ExtensionAPI, manager: InterAgentSessionManager, buildFeatureManager?: BuildFeatureWorkflowManager): void {
48
60
  let lastCtx: any | null = null;
49
61
  let repainting = false;
50
62
  let repaintQueued = false;
@@ -59,11 +71,13 @@ export function registerInterAgentTeamWidget(pi: ExtensionAPI, manager: InterAge
59
71
  try {
60
72
  const registry = getProjectRegistry(ctx.cwd ?? process.cwd());
61
73
  const activeTeams = (await registry.listTeams()).filter((team) => team.status === "active");
62
- if (activeTeams.length === 0) {
74
+ const workflows = buildFeatureManager ? await buildFeatureManager.list(ctx.cwd ?? process.cwd()) : [];
75
+ const visibleWorkflows = workflows.filter((workflow) => workflow.status === "running" || workflow.status === "blocked" || workflow.status === "failed" || workflow.status === "interrupted");
76
+ if (activeTeams.length === 0 && visibleWorkflows.length === 0) {
63
77
  ctx.ui.setWidget(WIDGET_KEY, undefined);
64
78
  return;
65
79
  }
66
- ctx.ui.setWidget(WIDGET_KEY, renderLines(activeTeams, manager.list(), ctx.ui.theme), { placement: "belowEditor" });
80
+ ctx.ui.setWidget(WIDGET_KEY, renderLines(activeTeams, manager.list(), workflows, ctx.ui.theme), { placement: "belowEditor" });
67
81
  } finally {
68
82
  repainting = false;
69
83
  if (repaintQueued) {
@@ -77,6 +91,10 @@ export function registerInterAgentTeamWidget(pi: ExtensionAPI, manager: InterAge
77
91
  if (lastCtx) void repaint(lastCtx);
78
92
  });
79
93
 
94
+ buildFeatureManager?.onChange(() => {
95
+ if (lastCtx) void repaint(lastCtx);
96
+ });
97
+
80
98
  pi.on("session_start", (_event: any, ctx: any) => {
81
99
  lastCtx = ctx;
82
100
  void repaint(ctx);