@schilderlabs/pitown-package 0.2.1 → 0.2.6

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/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
1
  //#region src/index.d.ts
2
- declare const bundledAgentNames: readonly ["leader", "supervisor", "scout", "planner", "worker", "reviewer", "docs-keeper"];
2
+ declare const bundledAgentNames: readonly ["mayor", "supervisor", "scout", "planner", "worker", "reviewer", "docs-keeper"];
3
3
  declare const townPackageName = "@schilderlabs/pitown-package";
4
4
  declare function resolvePiTownPackageRoot(): string;
5
5
  declare function resolvePiTownExtensionPath(): string;
package/dist/index.mjs CHANGED
@@ -4,7 +4,7 @@ import { fileURLToPath } from "node:url";
4
4
 
5
5
  //#region src/index.ts
6
6
  const bundledAgentNames = [
7
- "leader",
7
+ "mayor",
8
8
  "supervisor",
9
9
  "scout",
10
10
  "planner",
@@ -23,7 +23,7 @@ function resolvePiTownExtensionPath() {
23
23
  return join(resolvePackageRoot(), "pi", "extensions", "index.ts");
24
24
  }
25
25
  function resolvePiTownMayorPromptPath() {
26
- return join(resolvePackageRoot(), "pi", "agents", "leader.md");
26
+ return join(resolvePackageRoot(), "pi", "agents", "mayor.md");
27
27
  }
28
28
  function readPiTownMayorPrompt() {
29
29
  return readFileSync(resolvePiTownMayorPromptPath(), "utf-8");
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["import { readFileSync } from \"node:fs\"\nimport { dirname, join } from \"node:path\"\nimport { fileURLToPath } from \"node:url\"\n\nexport const bundledAgentNames = [\n\t\"leader\",\n\t\"supervisor\",\n\t\"scout\",\n\t\"planner\",\n\t\"worker\",\n\t\"reviewer\",\n\t\"docs-keeper\",\n] as const\n\nexport const townPackageName = \"@schilderlabs/pitown-package\"\n\nfunction resolvePackageRoot() {\n\treturn join(dirname(fileURLToPath(import.meta.url)), \"..\")\n}\n\nexport function resolvePiTownPackageRoot() {\n\treturn resolvePackageRoot()\n}\n\nexport function resolvePiTownExtensionPath() {\n\treturn join(resolvePackageRoot(), \"pi\", \"extensions\", \"index.ts\")\n}\n\nexport function resolvePiTownMayorPromptPath() {\n\treturn join(resolvePackageRoot(), \"pi\", \"agents\", \"leader.md\")\n}\n\nexport function readPiTownMayorPrompt() {\n\treturn readFileSync(resolvePiTownMayorPromptPath(), \"utf-8\")\n}\n"],"mappings":";;;;;AAIA,MAAa,oBAAoB;CAChC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AAED,MAAa,kBAAkB;AAE/B,SAAS,qBAAqB;AAC7B,QAAO,KAAK,QAAQ,cAAc,OAAO,KAAK,IAAI,CAAC,EAAE,KAAK;;AAG3D,SAAgB,2BAA2B;AAC1C,QAAO,oBAAoB;;AAG5B,SAAgB,6BAA6B;AAC5C,QAAO,KAAK,oBAAoB,EAAE,MAAM,cAAc,WAAW;;AAGlE,SAAgB,+BAA+B;AAC9C,QAAO,KAAK,oBAAoB,EAAE,MAAM,UAAU,YAAY;;AAG/D,SAAgB,wBAAwB;AACvC,QAAO,aAAa,8BAA8B,EAAE,QAAQ"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["import { readFileSync } from \"node:fs\"\nimport { dirname, join } from \"node:path\"\nimport { fileURLToPath } from \"node:url\"\n\nexport const bundledAgentNames = [\n\t\"mayor\",\n\t\"supervisor\",\n\t\"scout\",\n\t\"planner\",\n\t\"worker\",\n\t\"reviewer\",\n\t\"docs-keeper\",\n] as const\n\nexport const townPackageName = \"@schilderlabs/pitown-package\"\n\nfunction resolvePackageRoot() {\n\treturn join(dirname(fileURLToPath(import.meta.url)), \"..\")\n}\n\nexport function resolvePiTownPackageRoot() {\n\treturn resolvePackageRoot()\n}\n\nexport function resolvePiTownExtensionPath() {\n\treturn join(resolvePackageRoot(), \"pi\", \"extensions\", \"index.ts\")\n}\n\nexport function resolvePiTownMayorPromptPath() {\n\treturn join(resolvePackageRoot(), \"pi\", \"agents\", \"mayor.md\")\n}\n\nexport function readPiTownMayorPrompt() {\n\treturn readFileSync(resolvePiTownMayorPromptPath(), \"utf-8\")\n}\n"],"mappings":";;;;;AAIA,MAAa,oBAAoB;CAChC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AAED,MAAa,kBAAkB;AAE/B,SAAS,qBAAqB;AAC7B,QAAO,KAAK,QAAQ,cAAc,OAAO,KAAK,IAAI,CAAC,EAAE,KAAK;;AAG3D,SAAgB,2BAA2B;AAC1C,QAAO,oBAAoB;;AAG5B,SAAgB,6BAA6B;AAC5C,QAAO,KAAK,oBAAoB,EAAE,MAAM,cAAc,WAAW;;AAGlE,SAAgB,+BAA+B;AAC9C,QAAO,KAAK,oBAAoB,EAAE,MAAM,UAAU,WAAW;;AAG9D,SAAgB,wBAAwB;AACvC,QAAO,aAAa,8BAA8B,EAAE,QAAQ"}
package/package.json CHANGED
@@ -1,37 +1,54 @@
1
1
  {
2
2
  "name": "@schilderlabs/pitown-package",
3
- "version": "0.2.1",
4
- "description": "Pi package resources for Pi Town",
3
+ "version": "0.2.6",
5
4
  "type": "module",
6
- "imports": {
7
- "#pitown-extension": "./pi/extensions/index.ts",
8
- "#pitown-package-api": "./src/index.ts",
9
- "#pitown-mayor-plan": "./pi/extensions/mayor-plan.ts",
10
- "#pitown-town-tools": "./pi/extensions/town-tools.ts"
11
- },
5
+ "types": "./dist/index.d.mts",
12
6
  "exports": {
13
7
  ".": {
14
8
  "import": "./dist/index.mjs",
15
9
  "types": "./dist/index.d.mts"
16
10
  }
17
11
  },
18
- "types": "./dist/index.d.mts",
12
+ "engines": {
13
+ "node": ">=24"
14
+ },
15
+ "peerDependencies": {
16
+ "@mariozechner/pi-coding-agent": "*"
17
+ },
18
+ "dependencies": {
19
+ "@mariozechner/pi-ai": "0.58.3",
20
+ "@schilderlabs/pitown-core": "0.2.6"
21
+ },
22
+ "devDependencies": {
23
+ "@mariozechner/pi-coding-agent": "0.58.3",
24
+ "eslint": "9.39.3",
25
+ "tsdown": "0.20.3",
26
+ "typescript": "5.9.2",
27
+ "vitest": "4.0.18"
28
+ },
29
+ "bugs": "https://github.com/RobSchilderr/pitown/issues",
30
+ "description": "Pi package resources for Pi Town",
19
31
  "files": [
20
32
  "dist",
21
33
  "pi",
22
34
  "README.md"
23
35
  ],
24
- "engines": {
25
- "node": ">=24"
36
+ "homepage": "https://github.com/RobSchilderr/pitown#readme",
37
+ "imports": {
38
+ "#pitown-extension": "./pi/extensions/index.ts",
39
+ "#pitown-package-api": "./src/index.ts",
40
+ "#pitown-mayor-plan": "./pi/extensions/mayor-plan.ts",
41
+ "#pitown-town-tools": "./pi/extensions/town-tools.ts"
26
42
  },
27
43
  "keywords": [
28
- "pitown",
29
- "pi-package",
30
- "pi",
31
44
  "agents",
45
+ "night-shift",
32
46
  "orchestration",
33
- "night-shift"
47
+ "pi",
48
+ "pi-package",
49
+ "pitown"
34
50
  ],
51
+ "license": "MIT",
35
52
  "pi": {
36
53
  "extensions": [
37
54
  "./pi/extensions"
@@ -40,32 +57,10 @@
40
57
  "./pi/prompts"
41
58
  ]
42
59
  },
43
- "dependencies": {
44
- "@mariozechner/pi-ai": "0.58.3",
45
- "@schilderlabs/pitown-core": "0.2.1"
46
- },
47
- "peerDependencies": {
48
- "@mariozechner/pi-coding-agent": "*"
49
- },
50
- "devDependencies": {
51
- "@mariozechner/pi-coding-agent": "0.58.3",
52
- "eslint": "9.39.3",
53
- "tsdown": "0.20.3",
54
- "typescript": "5.9.2",
55
- "vitest": "4.0.18"
56
- },
57
- "repository": {
58
- "type": "git",
59
- "url": "git+https://github.com/RobSchilderr/pitown.git"
60
- },
61
- "homepage": "https://github.com/RobSchilderr/pitown#readme",
62
- "bugs": {
63
- "url": "https://github.com/RobSchilderr/pitown/issues"
64
- },
65
60
  "publishConfig": {
66
61
  "access": "public"
67
62
  },
68
- "license": "MIT",
63
+ "repository": "RobSchilderr/pitown.git",
69
64
  "scripts": {
70
65
  "build": "tsdown",
71
66
  "lint": "eslint .",
@@ -19,3 +19,4 @@ Rules:
19
19
  - prefer spawning focused workers over doing everything yourself
20
20
  - check blockers, open questions, and active agent state before creating more work
21
21
  - escalate clearly when the next step depends on a human product or policy decision
22
+ - when delegating multiple independent tasks, call pitown_delegate for ALL of them in the same response — do not wait between delegates
@@ -15,30 +15,30 @@ import { describe, expect, it, vi } from "vitest"
15
15
 
16
16
  import piTownPackage from "#pitown-extension"
17
17
 
18
- type RegisteredHandler = (event: unknown, ctx: unknown) => unknown | Promise<unknown>
18
+ type RegisteredHandler = (...args: unknown[]) => unknown | Promise<unknown>
19
19
 
20
20
  function createRepoArtifacts() {
21
21
  const repoRoot = mkdtempSync(join(tmpdir(), "pitown-repo-"))
22
22
  const artifactsDir = join(mkdtempSync(join(tmpdir(), "pitown-home-")), "repos", "demo-repo")
23
23
 
24
- const leaderSessionDir = join(artifactsDir, "agents", "leader", "sessions")
24
+ const mayorSessionDir = join(artifactsDir, "agents", "mayor", "sessions")
25
25
  const workerSessionDir = join(artifactsDir, "agents", "worker-001", "sessions")
26
26
  const reviewerSessionDir = join(artifactsDir, "agents", "reviewer-001", "sessions")
27
- const leaderSessionFile = join(leaderSessionDir, "session_leader.jsonl")
27
+ const mayorSessionFile = join(mayorSessionDir, "session_mayor.jsonl")
28
28
  const workerSessionFile = join(workerSessionDir, "session_worker.jsonl")
29
29
 
30
30
  writeAgentState(
31
31
  artifactsDir,
32
32
  createAgentState({
33
- agentId: "leader",
34
- role: "leader",
33
+ agentId: "mayor",
34
+ role: "mayor",
35
35
  status: "running",
36
36
  task: "coordinate town work",
37
37
  lastMessage: "checking board",
38
38
  session: createAgentSessionRecord({
39
- sessionDir: leaderSessionDir,
40
- sessionPath: leaderSessionFile,
41
- sessionId: "leader",
39
+ sessionDir: mayorSessionDir,
40
+ sessionPath: mayorSessionFile,
41
+ sessionId: "mayor",
42
42
  }),
43
43
  }),
44
44
  )
@@ -79,11 +79,11 @@ function createRepoArtifacts() {
79
79
  status: "queued",
80
80
  role: "worker",
81
81
  assignedAgentId: "worker-001",
82
- createdBy: "leader",
82
+ createdBy: "mayor",
83
83
  }),
84
84
  )
85
85
 
86
- return { repoRoot, artifactsDir, leaderSessionFile, workerSessionFile }
86
+ return { repoRoot, artifactsDir, mayorSessionFile, workerSessionFile }
87
87
  }
88
88
 
89
89
  function setup() {
@@ -161,13 +161,13 @@ function createToolContext(repoRoot: string, sessionFile: string) {
161
161
  }
162
162
 
163
163
  describe("pi town extension", () => {
164
- it("injects hidden town context for managed leader sessions", async () => {
165
- const { repoRoot, leaderSessionFile } = createRepoArtifacts()
164
+ it("injects hidden town context for managed mayor sessions", async () => {
165
+ const { repoRoot, mayorSessionFile } = createRepoArtifacts()
166
166
  const { handlers } = setup()
167
167
 
168
168
  const result = await handlers["before_agent_start"]?.[0]?.(
169
169
  { systemPrompt: "base", prompt: "coordinate", images: [] },
170
- createToolContext(repoRoot, leaderSessionFile),
170
+ createToolContext(repoRoot, mayorSessionFile),
171
171
  )
172
172
 
173
173
  expect(result).toEqual(
@@ -182,11 +182,11 @@ describe("pi town extension", () => {
182
182
  })
183
183
 
184
184
  it("renders the live board for town-managed sessions", async () => {
185
- const { repoRoot, leaderSessionFile } = createRepoArtifacts()
185
+ const { repoRoot, mayorSessionFile } = createRepoArtifacts()
186
186
  const { tools } = setup()
187
187
  const boardTool = tools.get("pitown_board")
188
188
 
189
- const result = await boardTool?.execute("tool-call", {}, undefined, () => {}, createToolContext(repoRoot, leaderSessionFile))
189
+ const result = await boardTool?.execute("tool-call", {}, undefined, () => {}, createToolContext(repoRoot, mayorSessionFile))
190
190
 
191
191
  expect(result).toEqual(
192
192
  expect.objectContaining({
@@ -195,7 +195,7 @@ describe("pi town extension", () => {
195
195
  )
196
196
  })
197
197
 
198
- it("blocks workers from messaging non-leader agents directly", async () => {
198
+ it("blocks workers from messaging non-mayor agents directly", async () => {
199
199
  const { repoRoot, workerSessionFile } = createRepoArtifacts()
200
200
  const { tools } = setup()
201
201
  const messageTool = tools.get("pitown_message_agent")
@@ -208,7 +208,7 @@ describe("pi town extension", () => {
208
208
  () => {},
209
209
  createToolContext(repoRoot, workerSessionFile),
210
210
  ),
211
- ).rejects.toThrow("Only the leader may message non-leader agents")
211
+ ).rejects.toThrow("Only the mayor may message non-mayor agents")
212
212
  })
213
213
 
214
214
  it("updates worker state and task status through the status tool", async () => {
@@ -238,10 +238,10 @@ describe("pi town extension", () => {
238
238
  })
239
239
 
240
240
  it("enables mayor plan mode with a read-only tool set", async () => {
241
- const { repoRoot, leaderSessionFile } = createRepoArtifacts()
241
+ const { repoRoot, mayorSessionFile } = createRepoArtifacts()
242
242
  const { commands, activeTools, appendedEntries } = setup()
243
243
  const planCommand = commands.get("plan") as { handler: RegisteredHandler } | undefined
244
- const ctx = createToolContext(repoRoot, leaderSessionFile)
244
+ const ctx = createToolContext(repoRoot, mayorSessionFile)
245
245
 
246
246
  await planCommand?.handler([], ctx)
247
247
 
@@ -254,10 +254,10 @@ describe("pi town extension", () => {
254
254
  })
255
255
 
256
256
  it("captures numbered mayor plan steps after a planning turn", async () => {
257
- const { repoRoot, leaderSessionFile } = createRepoArtifacts()
257
+ const { repoRoot, mayorSessionFile } = createRepoArtifacts()
258
258
  const { commands, handlers, pi } = setup()
259
259
  const planCommand = commands.get("plan") as { handler: RegisteredHandler } | undefined
260
- const ctx = createToolContext(repoRoot, leaderSessionFile)
260
+ const ctx = createToolContext(repoRoot, mayorSessionFile)
261
261
 
262
262
  await planCommand?.handler([], ctx)
263
263
  await handlers["agent_end"]?.[0]?.(
@@ -2,8 +2,9 @@ import { existsSync, readdirSync, readFileSync } from "node:fs"
2
2
  import { dirname, join } from "node:path"
3
3
  import { fileURLToPath } from "node:url"
4
4
  import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"
5
+ import { listAgentStates } from "@schilderlabs/pitown-core"
5
6
  import { registerMayorPlanMode } from "#pitown-mayor-plan"
6
- import { registerTownTools } from "#pitown-town-tools"
7
+ import { registerTownTools, resolveTownAgentContext } from "#pitown-town-tools"
7
8
 
8
9
  const extensionDir = dirname(fileURLToPath(import.meta.url))
9
10
  const agentsDir = join(extensionDir, "..", "agents")
@@ -61,4 +62,26 @@ export default function piTownPackage(pi: ExtensionAPI) {
61
62
  ctx.ui.notify(lines.join("\n"), "info")
62
63
  },
63
64
  })
65
+
66
+ pi.registerCommand("workers", {
67
+ description: "Show current worker status",
68
+ handler: async (_args, ctx) => {
69
+ const context = resolveTownAgentContext(ctx.sessionManager.getSessionFile())
70
+ if (!context) {
71
+ ctx.ui.notify("Not in a Pi Town session.", "warning")
72
+ return
73
+ }
74
+ const agents = listAgentStates(context.artifactsDir)
75
+ const workers = agents.filter((a) => a.agentId !== "mayor")
76
+ if (workers.length === 0) {
77
+ ctx.ui.notify("No workers spawned yet.", "info")
78
+ return
79
+ }
80
+ const lines = workers.map((w) => {
81
+ const task = w.task ? (w.task.length > 60 ? w.task.slice(0, 59) + "…" : w.task) : "—"
82
+ return `${w.agentId.padEnd(16)} ${w.status.padEnd(10)} ${task}`
83
+ })
84
+ ctx.ui.notify(["Workers:", ...lines].join("\n"), "info")
85
+ },
86
+ })
64
87
  }
@@ -1,8 +1,12 @@
1
- import type { AgentMessage } from "@mariozechner/pi-agent-core"
2
1
  import type { AssistantMessage, TextContent } from "@mariozechner/pi-ai"
3
2
  import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent"
4
3
  import { resolveTownAgentContext } from "#pitown-town-tools"
5
4
 
5
+ interface AgentMessage {
6
+ role: string
7
+ content?: unknown
8
+ }
9
+
6
10
  interface PlanTodo {
7
11
  step: number
8
12
  text: string
@@ -46,7 +50,7 @@ function resolvePlanTools(candidateTools: string[]): string[] {
46
50
  function isMayorSession(ctx: ExtensionContext): boolean {
47
51
  const context = resolveTownAgentContext(ctx.sessionManager.getSessionFile())
48
52
  if (context === null) return false
49
- return context.agentId === "leader" || context.role === "leader" || context.role === "mayor"
53
+ return context.agentId === "mayor" || context.role === "mayor"
50
54
  }
51
55
 
52
56
  function persistPlanState(pi: ExtensionAPI, enabled: boolean, savedTools: string[], todos: PlanTodo[]) {
@@ -163,7 +167,7 @@ export function registerMayorPlanMode(pi: ExtensionAPI) {
163
167
  return
164
168
  }
165
169
 
166
- ctx.ui.notify(renderTodos(todos), todos.length === 0 ? "info" : "success")
170
+ ctx.ui.notify(renderTodos(todos), "info")
167
171
  },
168
172
  })
169
173
 
@@ -6,6 +6,7 @@ import {
6
6
  queueAgentMessage,
7
7
  readAgentMessages,
8
8
  readAgentState,
9
+ stopManagedAgents,
9
10
  updateAgentStatus,
10
11
  } from "@schilderlabs/pitown-core"
11
12
  import { readPiTownMayorPrompt, resolvePiTownExtensionPath } from "#pitown-package-api"
@@ -15,6 +16,7 @@ type TownToolName =
15
16
  | "pitown_delegate"
16
17
  | "pitown_message_agent"
17
18
  | "pitown_peek_agent"
19
+ | "pitown_stop"
18
20
  | "pitown_update_status"
19
21
 
20
22
  interface TownAgentContext {
@@ -32,10 +34,11 @@ const ROLE_STATUS = Type.Union([
32
34
  Type.Literal("blocked"),
33
35
  Type.Literal("completed"),
34
36
  Type.Literal("failed"),
37
+ Type.Literal("stopped"),
35
38
  ])
36
39
 
37
40
  const toolPermissions: Record<string, TownToolName[]> = {
38
- leader: ["pitown_board", "pitown_delegate", "pitown_message_agent", "pitown_peek_agent", "pitown_update_status"],
41
+ mayor: ["pitown_board", "pitown_delegate", "pitown_message_agent", "pitown_peek_agent", "pitown_stop", "pitown_update_status"],
39
42
  worker: ["pitown_board", "pitown_message_agent", "pitown_peek_agent", "pitown_update_status"],
40
43
  reviewer: ["pitown_board", "pitown_message_agent", "pitown_peek_agent", "pitown_update_status"],
41
44
  "docs-keeper": ["pitown_board", "pitown_message_agent", "pitown_peek_agent", "pitown_update_status"],
@@ -74,18 +77,18 @@ function assertPermission(context: TownAgentContext, toolName: TownToolName, tar
74
77
  throw new Error(`${context.role} agents may not use ${toolName}`)
75
78
  }
76
79
 
77
- if (toolName === "pitown_delegate" && context.role !== "leader") {
78
- throw new Error("Only the leader may delegate work")
80
+ if (toolName === "pitown_delegate" && context.role !== "mayor") {
81
+ throw new Error("Only the mayor may delegate work")
79
82
  }
80
83
 
81
- if (toolName === "pitown_message_agent" && context.role !== "leader" && targetAgentId && targetAgentId !== "leader") {
82
- throw new Error("Only the leader may message non-leader agents")
84
+ if (toolName === "pitown_message_agent" && context.role !== "mayor" && targetAgentId && targetAgentId !== "mayor") {
85
+ throw new Error("Only the mayor may message non-mayor agents")
83
86
  }
84
87
 
85
- if (toolName === "pitown_peek_agent" && context.role !== "leader" && targetAgentId) {
86
- const allowedTargets = new Set([context.agentId, "leader"])
88
+ if (toolName === "pitown_peek_agent" && context.role !== "mayor" && targetAgentId) {
89
+ const allowedTargets = new Set([context.agentId, "mayor"])
87
90
  if (!allowedTargets.has(targetAgentId)) {
88
- throw new Error("Non-leader agents may only peek themselves or the leader")
91
+ throw new Error("Non-mayor agents may only peek themselves or the mayor")
89
92
  }
90
93
  }
91
94
  }
@@ -139,6 +142,8 @@ function requireTownContext(ctx: ExtensionContext): TownAgentContext {
139
142
  return context
140
143
  }
141
144
 
145
+ const notifiedCompletions = new Set<string>()
146
+
142
147
  export function registerTownTools(pi: ExtensionAPI) {
143
148
  pi.on("before_agent_start", async (_event, ctx) => {
144
149
  const context = resolveTownAgentContext(ctx.sessionManager.getSessionFile())
@@ -153,6 +158,50 @@ export function registerTownTools(pi: ExtensionAPI) {
153
158
  }
154
159
  })
155
160
 
161
+ pi.on("before_agent_start", async (_event, ctx) => {
162
+ const context = resolveTownAgentContext(ctx.sessionManager.getSessionFile())
163
+ if (!context || context.agentId !== "mayor") return
164
+ if (!ctx.hasUI) return
165
+
166
+ const agents = listAgentStates(context.artifactsDir)
167
+ const workers = agents.filter((a) => a.agentId !== "mayor")
168
+ if (workers.length === 0) {
169
+ ctx.ui.setStatus("pitown-workers", undefined)
170
+ ctx.ui.setWidget("pitown-workers", undefined)
171
+ return
172
+ }
173
+
174
+ for (const w of workers) {
175
+ if ((w.status === "idle" || w.status === "completed") && !notifiedCompletions.has(w.agentId)) {
176
+ notifiedCompletions.add(w.agentId)
177
+ ctx.ui.notify(`✓ ${w.agentId} finished: ${w.lastMessage ?? w.task ?? "done"}`, "info")
178
+ }
179
+ if ((w.status === "blocked" || w.status === "failed" || w.status === "stopped") && !notifiedCompletions.has(w.agentId)) {
180
+ notifiedCompletions.add(w.agentId)
181
+ ctx.ui.notify(`✗ ${w.agentId} blocked: ${w.lastMessage ?? "needs attention"}`, "warning")
182
+ }
183
+ }
184
+
185
+ const running = workers.filter((a) => a.status === "running" || a.status === "starting").length
186
+ const done = workers.filter((a) => a.status === "idle" || a.status === "completed").length
187
+ const blocked = workers.filter((a) => a.status === "blocked" || a.status === "failed" || a.status === "stopped").length
188
+
189
+ const parts: string[] = []
190
+ if (running > 0) parts.push(`${running} running`)
191
+ if (done > 0) parts.push(`${done} done`)
192
+ if (blocked > 0) parts.push(`${blocked} blocked`)
193
+
194
+ const label = `workers: ${parts.join(", ")}`
195
+ ctx.ui.setStatus("pitown-workers", label)
196
+ ctx.ui.setWidget(
197
+ "pitown-workers",
198
+ workers.map((w) => {
199
+ const task = w.task ? (w.task.length > 50 ? w.task.slice(0, 49) + "…" : w.task) : "—"
200
+ return `${w.agentId}: ${w.status} — ${task}`
201
+ }),
202
+ )
203
+ })
204
+
156
205
  pi.registerTool({
157
206
  name: "pitown_board",
158
207
  label: "Pi Town Board",
@@ -191,20 +240,21 @@ export function registerTownTools(pi: ExtensionAPI) {
191
240
  task: input.task,
192
241
  agentId: input.agentId ?? null,
193
242
  extensionPath: resolvePiTownExtensionPath(),
194
- appendedSystemPrompt: input.role === "leader" ? readPiTownMayorPrompt() : null,
243
+ appendedSystemPrompt: input.role === "mayor" ? readPiTownMayorPrompt() : null,
195
244
  })
196
245
 
197
246
  return {
198
247
  content: [
199
248
  {
200
249
  type: "text",
201
- text: `Delegated ${result.task.taskId} to ${result.agentId} (${input.role}). Exit code: ${result.piResult.exitCode}.`,
250
+ text: `Delegated ${result.task.taskId} to ${result.agentId} (${input.role}). Status: ${result.task.status}.`,
202
251
  },
203
252
  ],
204
253
  details: {
205
254
  taskId: result.task.taskId,
206
255
  agentId: result.agentId,
207
256
  status: result.task.status,
257
+ processId: result.launch.processId,
208
258
  sessionPath: result.latestSession.sessionPath,
209
259
  },
210
260
  }
@@ -271,6 +321,61 @@ export function registerTownTools(pi: ExtensionAPI) {
271
321
  },
272
322
  })
273
323
 
324
+ pi.registerTool({
325
+ name: "pitown_stop",
326
+ label: "Pi Town Stop",
327
+ description: "Stop a managed Pi Town agent, or stop the other agents in this repo",
328
+ parameters: Type.Object({
329
+ scope: Type.Optional(Type.Union([Type.Literal("repo"), Type.Literal("agent")])),
330
+ agentId: Type.Optional(Type.String({ description: "Required when scope is agent" })),
331
+ force: Type.Optional(Type.Boolean({ description: "Escalate to SIGKILL if SIGTERM does not stop the process" })),
332
+ reason: Type.Optional(Type.String({ description: "Optional stop reason recorded on the board" })),
333
+ }),
334
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
335
+ const context = requireTownContext(ctx)
336
+ assertPermission(context, "pitown_stop")
337
+ const input = params as {
338
+ scope?: "repo" | "agent"
339
+ agentId?: string
340
+ force?: boolean
341
+ reason?: string
342
+ }
343
+ const scope = input.scope ?? "repo"
344
+
345
+ if (scope === "agent" && !input.agentId) throw new Error("pitown_stop requires agentId when scope=agent")
346
+ if (scope === "agent" && input.agentId === context.agentId) {
347
+ throw new Error("pitown_stop may not stop the current live mayor session; use the CLI for self-stop")
348
+ }
349
+
350
+ const result = stopManagedAgents({
351
+ artifactsDir: context.artifactsDir,
352
+ agentId: scope === "agent" ? input.agentId! : null,
353
+ excludeAgentIds: scope === "repo" ? [context.agentId] : [],
354
+ actorId: context.agentId,
355
+ reason: input.reason ?? `Stopped by ${context.agentId} via pitown_stop`,
356
+ force: input.force ?? false,
357
+ })
358
+
359
+ return {
360
+ content: [
361
+ {
362
+ type: "text",
363
+ text:
364
+ scope === "agent"
365
+ ? `Stopped ${input.agentId}. Agents updated: ${result.stoppedAgents}. Signaled processes: ${result.signaledProcesses}.`
366
+ : `Stopped repo agents except ${context.agentId}. Agents updated: ${result.stoppedAgents}. Signaled processes: ${result.signaledProcesses}.`,
367
+ },
368
+ ],
369
+ details: {
370
+ scope,
371
+ agentId: input.agentId ?? null,
372
+ stoppedAgents: result.stoppedAgents,
373
+ signaledProcesses: result.signaledProcesses,
374
+ },
375
+ }
376
+ },
377
+ })
378
+
274
379
  pi.registerTool({
275
380
  name: "pitown_update_status",
276
381
  label: "Pi Town Update Status",
@@ -285,7 +390,7 @@ export function registerTownTools(pi: ExtensionAPI) {
285
390
  const context = requireTownContext(ctx)
286
391
  assertPermission(context, "pitown_update_status")
287
392
  const input = params as {
288
- status: "queued" | "running" | "idle" | "blocked" | "completed" | "failed"
393
+ status: "queued" | "running" | "idle" | "blocked" | "completed" | "failed" | "stopped"
289
394
  lastMessage?: string
290
395
  waitingOn?: string
291
396
  blocked?: boolean
@@ -297,7 +402,7 @@ export function registerTownTools(pi: ExtensionAPI) {
297
402
  status: input.status,
298
403
  lastMessage: input.lastMessage ?? null,
299
404
  waitingOn: input.waitingOn ?? null,
300
- blocked: input.blocked,
405
+ ...(input.blocked === undefined ? {} : { blocked: input.blocked }),
301
406
  })
302
407
 
303
408
  return {