@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 +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 +62 -22
- package/pi/extensions/index.ts +24 -1
- package/pi/extensions/mayor-plan.ts +7 -3
- package/pi/extensions/town-tools.ts +132 -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.7",
|
|
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.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
|
-
"
|
|
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(
|
|
@@ -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("
|
|
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,
|
|
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 () => {
|
|
@@ -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,
|
|
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,
|
|
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,
|
|
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,
|
|
300
|
+
const ctx = createToolContext(repoRoot, mayorSessionFile)
|
|
261
301
|
|
|
262
302
|
await planCommand?.handler([], ctx)
|
|
263
303
|
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
|
}
|
|
@@ -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 === "
|
|
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}).
|
|
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 {
|