@schilderlabs/pitown-package 0.2.1 → 0.2.7

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.7",
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.7"
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(
@@ -175,18 +175,18 @@ describe("pi town extension", () => {
175
175
  message: expect.objectContaining({
176
176
  customType: "pitown-context",
177
177
  display: false,
178
- content: expect.stringContaining("Allowed Pi Town tools: pitown_board, pitown_delegate"),
178
+ content: expect.stringContaining("Recent inbox:\ninbox: empty"),
179
179
  }),
180
180
  }),
181
181
  )
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 () => {
@@ -237,11 +237,51 @@ describe("pi town extension", () => {
237
237
  )
238
238
  })
239
239
 
240
+ it("shows queued mayor inbox updates as mayor UI notifications on the next turn", async () => {
241
+ const { artifactsDir, repoRoot, mayorSessionFile, workerSessionFile } = createRepoArtifacts()
242
+ const { handlers, tools } = setup()
243
+ const ctx = createToolContext(repoRoot, mayorSessionFile)
244
+
245
+ writeAgentState(
246
+ artifactsDir,
247
+ createAgentState({
248
+ agentId: "mayor",
249
+ role: "mayor",
250
+ status: "queued",
251
+ task: "coordinate town work",
252
+ lastMessage: "worker-001 completed task-123",
253
+ session: createAgentSessionRecord({
254
+ sessionDir: join(artifactsDir, "agents", "mayor", "sessions"),
255
+ sessionPath: mayorSessionFile,
256
+ sessionId: "mayor",
257
+ }),
258
+ }),
259
+ )
260
+
261
+ await tools.get("pitown_message_agent")?.execute(
262
+ "tool-call",
263
+ { agentId: "mayor", body: "worker-001 completed task-123 (fix the failing auth flow): auth flow fixed" },
264
+ undefined,
265
+ () => {},
266
+ createToolContext(repoRoot, workerSessionFile),
267
+ )
268
+
269
+ await handlers["before_agent_start"]?.[1]?.(
270
+ { systemPrompt: "base", prompt: "follow up", images: [] },
271
+ ctx,
272
+ )
273
+
274
+ expect(ctx.ui.notify).toHaveBeenCalledWith(
275
+ "worker-001 completed task-123 (fix the failing auth flow): auth flow fixed",
276
+ "info",
277
+ )
278
+ })
279
+
240
280
  it("enables mayor plan mode with a read-only tool set", async () => {
241
- const { repoRoot, leaderSessionFile } = createRepoArtifacts()
281
+ const { repoRoot, mayorSessionFile } = createRepoArtifacts()
242
282
  const { commands, activeTools, appendedEntries } = setup()
243
283
  const planCommand = commands.get("plan") as { handler: RegisteredHandler } | undefined
244
- const ctx = createToolContext(repoRoot, leaderSessionFile)
284
+ const ctx = createToolContext(repoRoot, mayorSessionFile)
245
285
 
246
286
  await planCommand?.handler([], ctx)
247
287
 
@@ -254,10 +294,10 @@ describe("pi town extension", () => {
254
294
  })
255
295
 
256
296
  it("captures numbered mayor plan steps after a planning turn", async () => {
257
- const { repoRoot, leaderSessionFile } = createRepoArtifacts()
297
+ const { repoRoot, mayorSessionFile } = createRepoArtifacts()
258
298
  const { commands, handlers, pi } = setup()
259
299
  const planCommand = commands.get("plan") as { handler: RegisteredHandler } | undefined
260
- const ctx = createToolContext(repoRoot, leaderSessionFile)
300
+ const ctx = createToolContext(repoRoot, mayorSessionFile)
261
301
 
262
302
  await planCommand?.handler([], ctx)
263
303
  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
  }
@@ -118,6 +121,7 @@ function buildStartupContext(context: TownAgentContext): string {
118
121
 
119
122
  const currentTask = state.task ?? "no active task"
120
123
  const board = formatBoard(context.artifactsDir)
124
+ const inbox = formatMailbox(context.artifactsDir, context.agentId, "inbox")
121
125
  const tools = getAllowedTools(context.role).join(", ")
122
126
 
123
127
  return [
@@ -128,6 +132,9 @@ function buildStartupContext(context: TownAgentContext): string {
128
132
  "",
129
133
  "Current board:",
130
134
  board,
135
+ "",
136
+ "Recent inbox:",
137
+ inbox,
131
138
  ].join("\n")
132
139
  }
133
140
 
@@ -139,6 +146,8 @@ function requireTownContext(ctx: ExtensionContext): TownAgentContext {
139
146
  return context
140
147
  }
141
148
 
149
+ const notifiedMayorInboxMessages = new Set<string>()
150
+
142
151
  export function registerTownTools(pi: ExtensionAPI) {
143
152
  pi.on("before_agent_start", async (_event, ctx) => {
144
153
  const context = resolveTownAgentContext(ctx.sessionManager.getSessionFile())
@@ -153,6 +162,52 @@ export function registerTownTools(pi: ExtensionAPI) {
153
162
  }
154
163
  })
155
164
 
165
+ pi.on("before_agent_start", async (_event, ctx) => {
166
+ const context = resolveTownAgentContext(ctx.sessionManager.getSessionFile())
167
+ if (!context || context.agentId !== "mayor") return
168
+ if (!ctx.hasUI) return
169
+
170
+ const agents = listAgentStates(context.artifactsDir)
171
+ const workers = agents.filter((a) => a.agentId !== "mayor")
172
+ const recentMayorMessages = readAgentMessages(context.artifactsDir, context.agentId, "inbox").slice(-5)
173
+
174
+ for (const record of recentMayorMessages) {
175
+ if (record.from === "human" || record.from === "system") continue
176
+
177
+ const key = `${record.createdAt}:${record.from}:${record.body}`
178
+ if (notifiedMayorInboxMessages.has(key)) continue
179
+
180
+ notifiedMayorInboxMessages.add(key)
181
+ const level = /\bblocked\b|\bfailed\b|\bstopped\b/i.test(record.body) ? "warning" : "info"
182
+ ctx.ui.notify(record.body, level)
183
+ }
184
+
185
+ if (workers.length === 0) {
186
+ ctx.ui.setStatus("pitown-workers", undefined)
187
+ ctx.ui.setWidget("pitown-workers", undefined)
188
+ return
189
+ }
190
+
191
+ const running = workers.filter((a) => a.status === "running" || a.status === "starting").length
192
+ const done = workers.filter((a) => a.status === "idle" || a.status === "completed").length
193
+ const blocked = workers.filter((a) => a.status === "blocked" || a.status === "failed" || a.status === "stopped").length
194
+
195
+ const parts: string[] = []
196
+ if (running > 0) parts.push(`${running} running`)
197
+ if (done > 0) parts.push(`${done} done`)
198
+ if (blocked > 0) parts.push(`${blocked} blocked`)
199
+
200
+ const label = `workers: ${parts.join(", ")}`
201
+ ctx.ui.setStatus("pitown-workers", label)
202
+ ctx.ui.setWidget(
203
+ "pitown-workers",
204
+ workers.map((w) => {
205
+ const task = w.task ? (w.task.length > 50 ? w.task.slice(0, 49) + "…" : w.task) : "—"
206
+ return `${w.agentId}: ${w.status} — ${task}`
207
+ }),
208
+ )
209
+ })
210
+
156
211
  pi.registerTool({
157
212
  name: "pitown_board",
158
213
  label: "Pi Town Board",
@@ -191,20 +246,30 @@ export function registerTownTools(pi: ExtensionAPI) {
191
246
  task: input.task,
192
247
  agentId: input.agentId ?? null,
193
248
  extensionPath: resolvePiTownExtensionPath(),
194
- appendedSystemPrompt: input.role === "leader" ? readPiTownMayorPrompt() : null,
249
+ appendedSystemPrompt: input.role === "mayor" ? readPiTownMayorPrompt() : null,
250
+ completionAutoResumeTarget:
251
+ context.agentId === "mayor"
252
+ ? {
253
+ agentId: "mayor",
254
+ message: "New agent check-ins arrived. Review the latest board and inbox updates, then decide the next bounded action.",
255
+ extensionPath: resolvePiTownExtensionPath(),
256
+ appendedSystemPrompt: readPiTownMayorPrompt(),
257
+ }
258
+ : null,
195
259
  })
196
260
 
197
261
  return {
198
262
  content: [
199
263
  {
200
264
  type: "text",
201
- text: `Delegated ${result.task.taskId} to ${result.agentId} (${input.role}). Exit code: ${result.piResult.exitCode}.`,
265
+ text: `Delegated ${result.task.taskId} to ${result.agentId} (${input.role}). Status: ${result.task.status}.`,
202
266
  },
203
267
  ],
204
268
  details: {
205
269
  taskId: result.task.taskId,
206
270
  agentId: result.agentId,
207
271
  status: result.task.status,
272
+ processId: result.launch.processId,
208
273
  sessionPath: result.latestSession.sessionPath,
209
274
  },
210
275
  }
@@ -271,6 +336,61 @@ export function registerTownTools(pi: ExtensionAPI) {
271
336
  },
272
337
  })
273
338
 
339
+ pi.registerTool({
340
+ name: "pitown_stop",
341
+ label: "Pi Town Stop",
342
+ description: "Stop a managed Pi Town agent, or stop the other agents in this repo",
343
+ parameters: Type.Object({
344
+ scope: Type.Optional(Type.Union([Type.Literal("repo"), Type.Literal("agent")])),
345
+ agentId: Type.Optional(Type.String({ description: "Required when scope is agent" })),
346
+ force: Type.Optional(Type.Boolean({ description: "Escalate to SIGKILL if SIGTERM does not stop the process" })),
347
+ reason: Type.Optional(Type.String({ description: "Optional stop reason recorded on the board" })),
348
+ }),
349
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
350
+ const context = requireTownContext(ctx)
351
+ assertPermission(context, "pitown_stop")
352
+ const input = params as {
353
+ scope?: "repo" | "agent"
354
+ agentId?: string
355
+ force?: boolean
356
+ reason?: string
357
+ }
358
+ const scope = input.scope ?? "repo"
359
+
360
+ if (scope === "agent" && !input.agentId) throw new Error("pitown_stop requires agentId when scope=agent")
361
+ if (scope === "agent" && input.agentId === context.agentId) {
362
+ throw new Error("pitown_stop may not stop the current live mayor session; use the CLI for self-stop")
363
+ }
364
+
365
+ const result = stopManagedAgents({
366
+ artifactsDir: context.artifactsDir,
367
+ agentId: scope === "agent" ? input.agentId! : null,
368
+ excludeAgentIds: scope === "repo" ? [context.agentId] : [],
369
+ actorId: context.agentId,
370
+ reason: input.reason ?? `Stopped by ${context.agentId} via pitown_stop`,
371
+ force: input.force ?? false,
372
+ })
373
+
374
+ return {
375
+ content: [
376
+ {
377
+ type: "text",
378
+ text:
379
+ scope === "agent"
380
+ ? `Stopped ${input.agentId}. Agents updated: ${result.stoppedAgents}. Signaled processes: ${result.signaledProcesses}.`
381
+ : `Stopped repo agents except ${context.agentId}. Agents updated: ${result.stoppedAgents}. Signaled processes: ${result.signaledProcesses}.`,
382
+ },
383
+ ],
384
+ details: {
385
+ scope,
386
+ agentId: input.agentId ?? null,
387
+ stoppedAgents: result.stoppedAgents,
388
+ signaledProcesses: result.signaledProcesses,
389
+ },
390
+ }
391
+ },
392
+ })
393
+
274
394
  pi.registerTool({
275
395
  name: "pitown_update_status",
276
396
  label: "Pi Town Update Status",
@@ -285,7 +405,7 @@ export function registerTownTools(pi: ExtensionAPI) {
285
405
  const context = requireTownContext(ctx)
286
406
  assertPermission(context, "pitown_update_status")
287
407
  const input = params as {
288
- status: "queued" | "running" | "idle" | "blocked" | "completed" | "failed"
408
+ status: "queued" | "running" | "idle" | "blocked" | "completed" | "failed" | "stopped"
289
409
  lastMessage?: string
290
410
  waitingOn?: string
291
411
  blocked?: boolean
@@ -297,7 +417,7 @@ export function registerTownTools(pi: ExtensionAPI) {
297
417
  status: input.status,
298
418
  lastMessage: input.lastMessage ?? null,
299
419
  waitingOn: input.waitingOn ?? null,
300
- blocked: input.blocked,
420
+ ...(input.blocked === undefined ? {} : { blocked: input.blocked }),
301
421
  })
302
422
 
303
423
  return {