@schilderlabs/pitown-package 0.2.1
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/LICENSE +21 -0
- package/README.md +5 -0
- package/dist/index.d.mts +10 -0
- package/dist/index.mjs +34 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +75 -0
- package/pi/agents/docs-keeper.md +14 -0
- package/pi/agents/leader.md +21 -0
- package/pi/agents/planner.md +15 -0
- package/pi/agents/reviewer.md +15 -0
- package/pi/agents/scout.md +15 -0
- package/pi/agents/supervisor.md +16 -0
- package/pi/agents/worker.md +15 -0
- package/pi/extensions/index.test.ts +288 -0
- package/pi/extensions/index.ts +64 -0
- package/pi/extensions/mayor-plan.ts +265 -0
- package/pi/extensions/require-tests/index.test.ts +119 -0
- package/pi/extensions/require-tests/index.ts +106 -0
- package/pi/extensions/town-tools.ts +309 -0
- package/pi/prompts/overnight-start.md +8 -0
- package/pi/prompts/retry-task.md +8 -0
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import { Type } from "@mariozechner/pi-ai"
|
|
2
|
+
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent"
|
|
3
|
+
import {
|
|
4
|
+
delegateTask,
|
|
5
|
+
listAgentStates,
|
|
6
|
+
queueAgentMessage,
|
|
7
|
+
readAgentMessages,
|
|
8
|
+
readAgentState,
|
|
9
|
+
updateAgentStatus,
|
|
10
|
+
} from "@schilderlabs/pitown-core"
|
|
11
|
+
import { readPiTownMayorPrompt, resolvePiTownExtensionPath } from "#pitown-package-api"
|
|
12
|
+
|
|
13
|
+
type TownToolName =
|
|
14
|
+
| "pitown_board"
|
|
15
|
+
| "pitown_delegate"
|
|
16
|
+
| "pitown_message_agent"
|
|
17
|
+
| "pitown_peek_agent"
|
|
18
|
+
| "pitown_update_status"
|
|
19
|
+
|
|
20
|
+
interface TownAgentContext {
|
|
21
|
+
artifactsDir: string
|
|
22
|
+
repoSlug: string
|
|
23
|
+
agentId: string
|
|
24
|
+
role: string
|
|
25
|
+
sessionFile: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const ROLE_STATUS = Type.Union([
|
|
29
|
+
Type.Literal("queued"),
|
|
30
|
+
Type.Literal("running"),
|
|
31
|
+
Type.Literal("idle"),
|
|
32
|
+
Type.Literal("blocked"),
|
|
33
|
+
Type.Literal("completed"),
|
|
34
|
+
Type.Literal("failed"),
|
|
35
|
+
])
|
|
36
|
+
|
|
37
|
+
const toolPermissions: Record<string, TownToolName[]> = {
|
|
38
|
+
leader: ["pitown_board", "pitown_delegate", "pitown_message_agent", "pitown_peek_agent", "pitown_update_status"],
|
|
39
|
+
worker: ["pitown_board", "pitown_message_agent", "pitown_peek_agent", "pitown_update_status"],
|
|
40
|
+
reviewer: ["pitown_board", "pitown_message_agent", "pitown_peek_agent", "pitown_update_status"],
|
|
41
|
+
"docs-keeper": ["pitown_board", "pitown_message_agent", "pitown_peek_agent", "pitown_update_status"],
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function resolveTownAgentContext(sessionFile: string | null | undefined): TownAgentContext | null {
|
|
45
|
+
if (!sessionFile) return null
|
|
46
|
+
|
|
47
|
+
const normalizedSessionFile = sessionFile.replace(/\\/g, "/")
|
|
48
|
+
const match = normalizedSessionFile.match(/^(.*\/repos\/([^/]+))\/agents\/([^/]+)\/sessions\/[^/]+\.jsonl$/)
|
|
49
|
+
if (!match) return null
|
|
50
|
+
|
|
51
|
+
const artifactsDir = match[1]
|
|
52
|
+
const repoSlug = match[2]
|
|
53
|
+
const agentId = match[3]
|
|
54
|
+
if (!artifactsDir || !repoSlug || !agentId) return null
|
|
55
|
+
|
|
56
|
+
const state = readAgentState(artifactsDir, agentId)
|
|
57
|
+
if (state === null) return null
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
artifactsDir,
|
|
61
|
+
repoSlug,
|
|
62
|
+
agentId,
|
|
63
|
+
role: state.role,
|
|
64
|
+
sessionFile: normalizedSessionFile,
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function getAllowedTools(role: string): TownToolName[] {
|
|
69
|
+
return toolPermissions[role] ?? ["pitown_board"]
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function assertPermission(context: TownAgentContext, toolName: TownToolName, targetAgentId?: string) {
|
|
73
|
+
if (!getAllowedTools(context.role).includes(toolName)) {
|
|
74
|
+
throw new Error(`${context.role} agents may not use ${toolName}`)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (toolName === "pitown_delegate" && context.role !== "leader") {
|
|
78
|
+
throw new Error("Only the leader may delegate work")
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (toolName === "pitown_message_agent" && context.role !== "leader" && targetAgentId && targetAgentId !== "leader") {
|
|
82
|
+
throw new Error("Only the leader may message non-leader agents")
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (toolName === "pitown_peek_agent" && context.role !== "leader" && targetAgentId) {
|
|
86
|
+
const allowedTargets = new Set([context.agentId, "leader"])
|
|
87
|
+
if (!allowedTargets.has(targetAgentId)) {
|
|
88
|
+
throw new Error("Non-leader agents may only peek themselves or the leader")
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function formatBoard(artifactsDir: string): string {
|
|
94
|
+
const agents = listAgentStates(artifactsDir)
|
|
95
|
+
if (agents.length === 0) return "No agents found."
|
|
96
|
+
|
|
97
|
+
return agents
|
|
98
|
+
.map((agent) => {
|
|
99
|
+
const task = agent.task ?? "no active task"
|
|
100
|
+
const taskId = agent.taskId ? ` [${agent.taskId}]` : ""
|
|
101
|
+
const note = agent.lastMessage ? ` | ${agent.lastMessage}` : ""
|
|
102
|
+
const waitingOn = agent.waitingOn ? ` | waiting on: ${agent.waitingOn}` : ""
|
|
103
|
+
return `${agent.agentId.padEnd(12)} ${agent.status.padEnd(9)} ${task}${taskId}${note}${waitingOn}`
|
|
104
|
+
})
|
|
105
|
+
.join("\n")
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function formatMailbox(artifactsDir: string, agentId: string, box: "inbox" | "outbox"): string {
|
|
109
|
+
const records = readAgentMessages(artifactsDir, agentId, box).slice(-3)
|
|
110
|
+
if (records.length === 0) return `${box}: empty`
|
|
111
|
+
|
|
112
|
+
return `${box}:\n${records.map((record) => `- ${record.from}: ${record.body}`).join("\n")}`
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function buildStartupContext(context: TownAgentContext): string {
|
|
116
|
+
const state = readAgentState(context.artifactsDir, context.agentId)
|
|
117
|
+
if (state === null) throw new Error(`Unknown agent: ${context.agentId}`)
|
|
118
|
+
|
|
119
|
+
const currentTask = state.task ?? "no active task"
|
|
120
|
+
const board = formatBoard(context.artifactsDir)
|
|
121
|
+
const tools = getAllowedTools(context.role).join(", ")
|
|
122
|
+
|
|
123
|
+
return [
|
|
124
|
+
`Pi Town agent context: ${context.agentId} (${context.role})`,
|
|
125
|
+
`Repo slug: ${context.repoSlug}`,
|
|
126
|
+
`Current task: ${currentTask}`,
|
|
127
|
+
`Allowed Pi Town tools: ${tools}`,
|
|
128
|
+
"",
|
|
129
|
+
"Current board:",
|
|
130
|
+
board,
|
|
131
|
+
].join("\n")
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function requireTownContext(ctx: ExtensionContext): TownAgentContext {
|
|
135
|
+
const context = resolveTownAgentContext(ctx.sessionManager.getSessionFile())
|
|
136
|
+
if (context === null) {
|
|
137
|
+
throw new Error("This Pi session is not managed by Pi Town")
|
|
138
|
+
}
|
|
139
|
+
return context
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function registerTownTools(pi: ExtensionAPI) {
|
|
143
|
+
pi.on("before_agent_start", async (_event, ctx) => {
|
|
144
|
+
const context = resolveTownAgentContext(ctx.sessionManager.getSessionFile())
|
|
145
|
+
if (context === null) return undefined
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
message: {
|
|
149
|
+
customType: "pitown-context",
|
|
150
|
+
content: buildStartupContext(context),
|
|
151
|
+
display: false,
|
|
152
|
+
},
|
|
153
|
+
}
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
pi.registerTool({
|
|
157
|
+
name: "pitown_board",
|
|
158
|
+
label: "Pi Town Board",
|
|
159
|
+
description: "Show the current Pi Town board with all known agents and their statuses",
|
|
160
|
+
parameters: Type.Object({}),
|
|
161
|
+
async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
|
|
162
|
+
const context = requireTownContext(ctx)
|
|
163
|
+
assertPermission(context, "pitown_board")
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
content: [{ type: "text", text: formatBoard(context.artifactsDir) }],
|
|
167
|
+
details: { agentId: context.agentId, role: context.role },
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
pi.registerTool({
|
|
173
|
+
name: "pitown_delegate",
|
|
174
|
+
label: "Pi Town Delegate",
|
|
175
|
+
description: "Delegate a bounded task to a new Pi Town agent",
|
|
176
|
+
parameters: Type.Object({
|
|
177
|
+
role: Type.String({ description: "Agent role to spawn, such as worker or reviewer" }),
|
|
178
|
+
task: Type.String({ description: "Bounded task description" }),
|
|
179
|
+
agentId: Type.Optional(Type.String({ description: "Optional explicit agent id" })),
|
|
180
|
+
}),
|
|
181
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
182
|
+
const context = requireTownContext(ctx)
|
|
183
|
+
assertPermission(context, "pitown_delegate")
|
|
184
|
+
const input = params as { role: string; task: string; agentId?: string }
|
|
185
|
+
|
|
186
|
+
const result = delegateTask({
|
|
187
|
+
repoRoot: ctx.cwd,
|
|
188
|
+
artifactsDir: context.artifactsDir,
|
|
189
|
+
fromAgentId: context.agentId,
|
|
190
|
+
role: input.role,
|
|
191
|
+
task: input.task,
|
|
192
|
+
agentId: input.agentId ?? null,
|
|
193
|
+
extensionPath: resolvePiTownExtensionPath(),
|
|
194
|
+
appendedSystemPrompt: input.role === "leader" ? readPiTownMayorPrompt() : null,
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
content: [
|
|
199
|
+
{
|
|
200
|
+
type: "text",
|
|
201
|
+
text: `Delegated ${result.task.taskId} to ${result.agentId} (${input.role}). Exit code: ${result.piResult.exitCode}.`,
|
|
202
|
+
},
|
|
203
|
+
],
|
|
204
|
+
details: {
|
|
205
|
+
taskId: result.task.taskId,
|
|
206
|
+
agentId: result.agentId,
|
|
207
|
+
status: result.task.status,
|
|
208
|
+
sessionPath: result.latestSession.sessionPath,
|
|
209
|
+
},
|
|
210
|
+
}
|
|
211
|
+
},
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
pi.registerTool({
|
|
215
|
+
name: "pitown_message_agent",
|
|
216
|
+
label: "Pi Town Message Agent",
|
|
217
|
+
description: "Send a durable message to another Pi Town agent",
|
|
218
|
+
parameters: Type.Object({
|
|
219
|
+
agentId: Type.String({ description: "Target Pi Town agent id" }),
|
|
220
|
+
body: Type.String({ description: "Message body" }),
|
|
221
|
+
}),
|
|
222
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
223
|
+
const context = requireTownContext(ctx)
|
|
224
|
+
const input = params as { agentId: string; body: string }
|
|
225
|
+
assertPermission(context, "pitown_message_agent", input.agentId)
|
|
226
|
+
|
|
227
|
+
queueAgentMessage({
|
|
228
|
+
artifactsDir: context.artifactsDir,
|
|
229
|
+
agentId: input.agentId,
|
|
230
|
+
from: context.agentId,
|
|
231
|
+
body: input.body,
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
content: [{ type: "text", text: `Queued message for ${input.agentId}.` }],
|
|
236
|
+
details: { agentId: input.agentId },
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
pi.registerTool({
|
|
242
|
+
name: "pitown_peek_agent",
|
|
243
|
+
label: "Pi Town Peek Agent",
|
|
244
|
+
description: "Inspect another Pi Town agent's current state and recent mailbox activity",
|
|
245
|
+
parameters: Type.Object({
|
|
246
|
+
agentId: Type.String({ description: "Agent id to inspect" }),
|
|
247
|
+
}),
|
|
248
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
249
|
+
const context = requireTownContext(ctx)
|
|
250
|
+
const input = params as { agentId: string }
|
|
251
|
+
assertPermission(context, "pitown_peek_agent", input.agentId)
|
|
252
|
+
|
|
253
|
+
const state = readAgentState(context.artifactsDir, input.agentId)
|
|
254
|
+
if (state === null) throw new Error(`Unknown agent: ${input.agentId}`)
|
|
255
|
+
|
|
256
|
+
const summary = [
|
|
257
|
+
`${state.agentId} (${state.role})`,
|
|
258
|
+
`status: ${state.status}`,
|
|
259
|
+
`task: ${state.task ?? "none"}`,
|
|
260
|
+
`taskId: ${state.taskId ?? "none"}`,
|
|
261
|
+
`last message: ${state.lastMessage ?? "none"}`,
|
|
262
|
+
`waiting on: ${state.waitingOn ?? "none"}`,
|
|
263
|
+
formatMailbox(context.artifactsDir, input.agentId, "inbox"),
|
|
264
|
+
formatMailbox(context.artifactsDir, input.agentId, "outbox"),
|
|
265
|
+
].join("\n")
|
|
266
|
+
|
|
267
|
+
return {
|
|
268
|
+
content: [{ type: "text", text: summary }],
|
|
269
|
+
details: { agentId: state.agentId, status: state.status, taskId: state.taskId },
|
|
270
|
+
}
|
|
271
|
+
},
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
pi.registerTool({
|
|
275
|
+
name: "pitown_update_status",
|
|
276
|
+
label: "Pi Town Update Status",
|
|
277
|
+
description: "Update this Pi Town agent's durable status on the board",
|
|
278
|
+
parameters: Type.Object({
|
|
279
|
+
status: ROLE_STATUS,
|
|
280
|
+
lastMessage: Type.Optional(Type.String({ description: "Short progress update" })),
|
|
281
|
+
waitingOn: Type.Optional(Type.String({ description: "What this agent is waiting on, if anything" })),
|
|
282
|
+
blocked: Type.Optional(Type.Boolean({ description: "Whether the agent is currently blocked" })),
|
|
283
|
+
}),
|
|
284
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
285
|
+
const context = requireTownContext(ctx)
|
|
286
|
+
assertPermission(context, "pitown_update_status")
|
|
287
|
+
const input = params as {
|
|
288
|
+
status: "queued" | "running" | "idle" | "blocked" | "completed" | "failed"
|
|
289
|
+
lastMessage?: string
|
|
290
|
+
waitingOn?: string
|
|
291
|
+
blocked?: boolean
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
updateAgentStatus({
|
|
295
|
+
artifactsDir: context.artifactsDir,
|
|
296
|
+
agentId: context.agentId,
|
|
297
|
+
status: input.status,
|
|
298
|
+
lastMessage: input.lastMessage ?? null,
|
|
299
|
+
waitingOn: input.waitingOn ?? null,
|
|
300
|
+
blocked: input.blocked,
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
return {
|
|
304
|
+
content: [{ type: "text", text: `Updated ${context.agentId} to ${input.status}.` }],
|
|
305
|
+
details: { agentId: context.agentId, status: input.status },
|
|
306
|
+
}
|
|
307
|
+
},
|
|
308
|
+
})
|
|
309
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
Retry the current task with the newest available context.
|
|
2
|
+
|
|
3
|
+
Checklist:
|
|
4
|
+
1. restate the task briefly
|
|
5
|
+
2. identify what blocked the prior attempt
|
|
6
|
+
3. narrow the scope if needed
|
|
7
|
+
4. prefer structural fixes over repeated blind retries
|
|
8
|
+
5. stop and record a blocker if human input is truly required
|