@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 +1 -1
- package/dist/index.mjs +2 -2
- package/dist/index.mjs.map +1 -1
- package/package.json +33 -38
- package/pi/agents/{leader.md → mayor.md} +1 -0
- package/pi/extensions/index.test.ts +21 -21
- package/pi/extensions/index.ts +24 -1
- package/pi/extensions/mayor-plan.ts +7 -3
- package/pi/extensions/town-tools.ts +117 -12
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
//#region src/index.d.ts
|
|
2
|
-
declare const bundledAgentNames: readonly ["
|
|
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
|
-
"
|
|
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", "
|
|
26
|
+
return join(resolvePackageRoot(), "pi", "agents", "mayor.md");
|
|
27
27
|
}
|
|
28
28
|
function readPiTownMayorPrompt() {
|
|
29
29
|
return readFileSync(resolvePiTownMayorPromptPath(), "utf-8");
|
package/dist/index.mjs.map
CHANGED
|
@@ -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\"
|
|
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.
|
|
4
|
-
"description": "Pi package resources for Pi Town",
|
|
3
|
+
"version": "0.2.6",
|
|
5
4
|
"type": "module",
|
|
6
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
25
|
-
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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 = (
|
|
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
|
|
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
|
|
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: "
|
|
34
|
-
role: "
|
|
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:
|
|
40
|
-
sessionPath:
|
|
41
|
-
sessionId: "
|
|
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: "
|
|
82
|
+
createdBy: "mayor",
|
|
83
83
|
}),
|
|
84
84
|
)
|
|
85
85
|
|
|
86
|
-
return { repoRoot, artifactsDir,
|
|
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
|
|
165
|
-
const { repoRoot,
|
|
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,
|
|
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,
|
|
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,
|
|
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-
|
|
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
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
260
|
+
const ctx = createToolContext(repoRoot, mayorSessionFile)
|
|
261
261
|
|
|
262
262
|
await planCommand?.handler([], ctx)
|
|
263
263
|
await handlers["agent_end"]?.[0]?.(
|
package/pi/extensions/index.ts
CHANGED
|
@@ -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 === "
|
|
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),
|
|
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
|
-
|
|
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 !== "
|
|
78
|
-
throw new Error("Only the
|
|
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 !== "
|
|
82
|
-
throw new Error("Only the
|
|
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 !== "
|
|
86
|
-
const allowedTargets = new Set([context.agentId, "
|
|
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-
|
|
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 === "
|
|
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}).
|
|
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 {
|