@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
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Schilder Labs
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
//#region src/index.d.ts
|
|
2
|
+
declare const bundledAgentNames: readonly ["leader", "supervisor", "scout", "planner", "worker", "reviewer", "docs-keeper"];
|
|
3
|
+
declare const townPackageName = "@schilderlabs/pitown-package";
|
|
4
|
+
declare function resolvePiTownPackageRoot(): string;
|
|
5
|
+
declare function resolvePiTownExtensionPath(): string;
|
|
6
|
+
declare function resolvePiTownMayorPromptPath(): string;
|
|
7
|
+
declare function readPiTownMayorPrompt(): string;
|
|
8
|
+
//#endregion
|
|
9
|
+
export { bundledAgentNames, readPiTownMayorPrompt, resolvePiTownExtensionPath, resolvePiTownMayorPromptPath, resolvePiTownPackageRoot, townPackageName };
|
|
10
|
+
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
//#region src/index.ts
|
|
6
|
+
const bundledAgentNames = [
|
|
7
|
+
"leader",
|
|
8
|
+
"supervisor",
|
|
9
|
+
"scout",
|
|
10
|
+
"planner",
|
|
11
|
+
"worker",
|
|
12
|
+
"reviewer",
|
|
13
|
+
"docs-keeper"
|
|
14
|
+
];
|
|
15
|
+
const townPackageName = "@schilderlabs/pitown-package";
|
|
16
|
+
function resolvePackageRoot() {
|
|
17
|
+
return join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
18
|
+
}
|
|
19
|
+
function resolvePiTownPackageRoot() {
|
|
20
|
+
return resolvePackageRoot();
|
|
21
|
+
}
|
|
22
|
+
function resolvePiTownExtensionPath() {
|
|
23
|
+
return join(resolvePackageRoot(), "pi", "extensions", "index.ts");
|
|
24
|
+
}
|
|
25
|
+
function resolvePiTownMayorPromptPath() {
|
|
26
|
+
return join(resolvePackageRoot(), "pi", "agents", "leader.md");
|
|
27
|
+
}
|
|
28
|
+
function readPiTownMayorPrompt() {
|
|
29
|
+
return readFileSync(resolvePiTownMayorPromptPath(), "utf-8");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
//#endregion
|
|
33
|
+
export { bundledAgentNames, readPiTownMayorPrompt, resolvePiTownExtensionPath, resolvePiTownMayorPromptPath, resolvePiTownPackageRoot, townPackageName };
|
|
34
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@schilderlabs/pitown-package",
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "Pi package resources for Pi Town",
|
|
5
|
+
"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
|
+
},
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"import": "./dist/index.mjs",
|
|
15
|
+
"types": "./dist/index.d.mts"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"types": "./dist/index.d.mts",
|
|
19
|
+
"files": [
|
|
20
|
+
"dist",
|
|
21
|
+
"pi",
|
|
22
|
+
"README.md"
|
|
23
|
+
],
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=24"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"pitown",
|
|
29
|
+
"pi-package",
|
|
30
|
+
"pi",
|
|
31
|
+
"agents",
|
|
32
|
+
"orchestration",
|
|
33
|
+
"night-shift"
|
|
34
|
+
],
|
|
35
|
+
"pi": {
|
|
36
|
+
"extensions": [
|
|
37
|
+
"./pi/extensions"
|
|
38
|
+
],
|
|
39
|
+
"prompts": [
|
|
40
|
+
"./pi/prompts"
|
|
41
|
+
]
|
|
42
|
+
},
|
|
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
|
+
"publishConfig": {
|
|
66
|
+
"access": "public"
|
|
67
|
+
},
|
|
68
|
+
"license": "MIT",
|
|
69
|
+
"scripts": {
|
|
70
|
+
"build": "tsdown",
|
|
71
|
+
"lint": "eslint .",
|
|
72
|
+
"test": "vitest run --passWithNoTests",
|
|
73
|
+
"typecheck": "tsc --noEmit"
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: docs-keeper
|
|
3
|
+
description: Summarizes overnight outcomes into concise human-readable updates
|
|
4
|
+
tools: read, grep, find, ls
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
You are the Pi Town docs keeper.
|
|
8
|
+
|
|
9
|
+
Your job is to summarize outcomes, blockers, and continuity for the next run.
|
|
10
|
+
|
|
11
|
+
Rules:
|
|
12
|
+
- keep summaries factual and compact
|
|
13
|
+
- capture blockers clearly
|
|
14
|
+
- preserve enough context for the next unattended cycle
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: mayor
|
|
3
|
+
description: Primary human-facing coordinator for a Pi Town repo
|
|
4
|
+
tools: read, grep, find, ls, pitown_board, pitown_delegate, pitown_message_agent, pitown_peek_agent, pitown_update_status
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
You are the Pi Town mayor.
|
|
8
|
+
|
|
9
|
+
Your job is to coordinate work across the repo and act as the primary interface for the human operator.
|
|
10
|
+
|
|
11
|
+
Rules:
|
|
12
|
+
- `/plan` puts you into read-only planning mode; use it before execution when the user wants to think through the work first
|
|
13
|
+
- use `pitown_board` before delegating or redirecting work
|
|
14
|
+
- use `pitown_delegate` for bounded implementation, review, or documentation tasks
|
|
15
|
+
- use `pitown_peek_agent` before assuming a worker is blocked or idle
|
|
16
|
+
- use `pitown_message_agent` to redirect or clarify work instead of restating the full task
|
|
17
|
+
- use `pitown_update_status` to keep your own mayor state current in short, high-signal updates
|
|
18
|
+
- break large goals into bounded tasks before delegating
|
|
19
|
+
- prefer spawning focused workers over doing everything yourself
|
|
20
|
+
- check blockers, open questions, and active agent state before creating more work
|
|
21
|
+
- escalate clearly when the next step depends on a human product or policy decision
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: planner
|
|
3
|
+
description: Turns a bounded task into a concrete implementation plan
|
|
4
|
+
tools: read, grep, find, ls
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
You are the Pi Town planner.
|
|
8
|
+
|
|
9
|
+
Your job is to turn the task and scout findings into a concrete implementation brief.
|
|
10
|
+
|
|
11
|
+
Rules:
|
|
12
|
+
- do not change code
|
|
13
|
+
- keep the plan small and specific
|
|
14
|
+
- include checks and risks
|
|
15
|
+
- keep the worker focused on one deliverable
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: reviewer
|
|
3
|
+
description: Reviews changes against task intent, repo rules, and code quality
|
|
4
|
+
tools: read, grep, find, ls, bash
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
You are the Pi Town reviewer.
|
|
8
|
+
|
|
9
|
+
Review the current changes for correctness, safety, and completeness.
|
|
10
|
+
|
|
11
|
+
Focus on:
|
|
12
|
+
- task alignment
|
|
13
|
+
- code quality
|
|
14
|
+
- validation confidence
|
|
15
|
+
- whether the work is good enough to reach a human in polished form
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: scout
|
|
3
|
+
description: Fast codebase recon with concise handoff for other agents
|
|
4
|
+
tools: read, grep, find, ls, bash
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
You are the Pi Town scout.
|
|
8
|
+
|
|
9
|
+
Your job is to gather the smallest useful body of context so another agent can act without rereading the entire codebase.
|
|
10
|
+
|
|
11
|
+
Output:
|
|
12
|
+
- relevant files
|
|
13
|
+
- architecture notes
|
|
14
|
+
- likely change points
|
|
15
|
+
- risks and unknowns
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: supervisor
|
|
3
|
+
description: Selects the next bounded unit of overnight work
|
|
4
|
+
tools: read, grep, find, ls
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
You are the Pi Town supervisor.
|
|
8
|
+
|
|
9
|
+
Your job is to choose the next best bounded task for an unattended run.
|
|
10
|
+
|
|
11
|
+
Rules:
|
|
12
|
+
- do not edit code
|
|
13
|
+
- prefer one bounded deliverable
|
|
14
|
+
- prefer tasks that unlock downstream work
|
|
15
|
+
- avoid tasks that require remote side effects or human credentials
|
|
16
|
+
- stop and record blockers clearly when the task is underspecified
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: worker
|
|
3
|
+
description: Implements one bounded task at a time
|
|
4
|
+
tools: read, bash, edit, write, grep, find, ls
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
You are the Pi Town worker.
|
|
8
|
+
|
|
9
|
+
You implement one bounded task at a time.
|
|
10
|
+
|
|
11
|
+
Rules:
|
|
12
|
+
- keep scope tight
|
|
13
|
+
- prefer explicit validations before finishing
|
|
14
|
+
- do not widen the task without saying so
|
|
15
|
+
- summarize what changed and what still needs follow-up
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { mkdtempSync } from "node:fs"
|
|
2
|
+
import { tmpdir } from "node:os"
|
|
3
|
+
import { join } from "node:path"
|
|
4
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"
|
|
5
|
+
import {
|
|
6
|
+
createAgentSessionRecord,
|
|
7
|
+
createAgentState,
|
|
8
|
+
createTaskRecord,
|
|
9
|
+
readAgentState,
|
|
10
|
+
readTaskRecord,
|
|
11
|
+
writeAgentState,
|
|
12
|
+
writeTaskRecord,
|
|
13
|
+
} from "@schilderlabs/pitown-core"
|
|
14
|
+
import { describe, expect, it, vi } from "vitest"
|
|
15
|
+
|
|
16
|
+
import piTownPackage from "#pitown-extension"
|
|
17
|
+
|
|
18
|
+
type RegisteredHandler = (event: unknown, ctx: unknown) => unknown | Promise<unknown>
|
|
19
|
+
|
|
20
|
+
function createRepoArtifacts() {
|
|
21
|
+
const repoRoot = mkdtempSync(join(tmpdir(), "pitown-repo-"))
|
|
22
|
+
const artifactsDir = join(mkdtempSync(join(tmpdir(), "pitown-home-")), "repos", "demo-repo")
|
|
23
|
+
|
|
24
|
+
const leaderSessionDir = join(artifactsDir, "agents", "leader", "sessions")
|
|
25
|
+
const workerSessionDir = join(artifactsDir, "agents", "worker-001", "sessions")
|
|
26
|
+
const reviewerSessionDir = join(artifactsDir, "agents", "reviewer-001", "sessions")
|
|
27
|
+
const leaderSessionFile = join(leaderSessionDir, "session_leader.jsonl")
|
|
28
|
+
const workerSessionFile = join(workerSessionDir, "session_worker.jsonl")
|
|
29
|
+
|
|
30
|
+
writeAgentState(
|
|
31
|
+
artifactsDir,
|
|
32
|
+
createAgentState({
|
|
33
|
+
agentId: "leader",
|
|
34
|
+
role: "leader",
|
|
35
|
+
status: "running",
|
|
36
|
+
task: "coordinate town work",
|
|
37
|
+
lastMessage: "checking board",
|
|
38
|
+
session: createAgentSessionRecord({
|
|
39
|
+
sessionDir: leaderSessionDir,
|
|
40
|
+
sessionPath: leaderSessionFile,
|
|
41
|
+
sessionId: "leader",
|
|
42
|
+
}),
|
|
43
|
+
}),
|
|
44
|
+
)
|
|
45
|
+
writeAgentState(
|
|
46
|
+
artifactsDir,
|
|
47
|
+
createAgentState({
|
|
48
|
+
agentId: "worker-001",
|
|
49
|
+
role: "worker",
|
|
50
|
+
status: "queued",
|
|
51
|
+
taskId: "task-123",
|
|
52
|
+
task: "fix the failing auth flow",
|
|
53
|
+
session: createAgentSessionRecord({
|
|
54
|
+
sessionDir: workerSessionDir,
|
|
55
|
+
sessionPath: workerSessionFile,
|
|
56
|
+
sessionId: "worker",
|
|
57
|
+
}),
|
|
58
|
+
}),
|
|
59
|
+
)
|
|
60
|
+
writeAgentState(
|
|
61
|
+
artifactsDir,
|
|
62
|
+
createAgentState({
|
|
63
|
+
agentId: "reviewer-001",
|
|
64
|
+
role: "reviewer",
|
|
65
|
+
status: "idle",
|
|
66
|
+
task: null,
|
|
67
|
+
session: createAgentSessionRecord({
|
|
68
|
+
sessionDir: reviewerSessionDir,
|
|
69
|
+
sessionPath: join(reviewerSessionDir, "session_reviewer.jsonl"),
|
|
70
|
+
sessionId: "reviewer",
|
|
71
|
+
}),
|
|
72
|
+
}),
|
|
73
|
+
)
|
|
74
|
+
writeTaskRecord(
|
|
75
|
+
artifactsDir,
|
|
76
|
+
createTaskRecord({
|
|
77
|
+
taskId: "task-123",
|
|
78
|
+
title: "fix the failing auth flow",
|
|
79
|
+
status: "queued",
|
|
80
|
+
role: "worker",
|
|
81
|
+
assignedAgentId: "worker-001",
|
|
82
|
+
createdBy: "leader",
|
|
83
|
+
}),
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
return { repoRoot, artifactsDir, leaderSessionFile, workerSessionFile }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function setup() {
|
|
90
|
+
const handlers: Record<string, RegisteredHandler[]> = {}
|
|
91
|
+
const tools = new Map<string, { execute: RegisteredHandler }>()
|
|
92
|
+
const commands = new Map<string, unknown>()
|
|
93
|
+
const flags = new Map<string, unknown>()
|
|
94
|
+
const activeTools: string[] = [
|
|
95
|
+
"read",
|
|
96
|
+
"grep",
|
|
97
|
+
"find",
|
|
98
|
+
"ls",
|
|
99
|
+
"pitown_board",
|
|
100
|
+
"pitown_delegate",
|
|
101
|
+
"pitown_message_agent",
|
|
102
|
+
"pitown_peek_agent",
|
|
103
|
+
"pitown_update_status",
|
|
104
|
+
]
|
|
105
|
+
const appendedEntries: Array<{ customType: string; data: unknown }> = []
|
|
106
|
+
const pi = {
|
|
107
|
+
on: vi.fn((event: string, handler: RegisteredHandler) => {
|
|
108
|
+
;(handlers[event] ??= []).push(handler)
|
|
109
|
+
}),
|
|
110
|
+
registerTool: vi.fn((definition: { name: string; execute: RegisteredHandler }) => {
|
|
111
|
+
tools.set(definition.name, definition)
|
|
112
|
+
}),
|
|
113
|
+
registerCommand: vi.fn((name: string, definition: unknown) => {
|
|
114
|
+
commands.set(name, definition)
|
|
115
|
+
}),
|
|
116
|
+
registerFlag: vi.fn((name: string, definition: unknown) => {
|
|
117
|
+
flags.set(name, definition)
|
|
118
|
+
}),
|
|
119
|
+
getFlag: vi.fn(() => undefined),
|
|
120
|
+
getActiveTools: vi.fn(() => [...activeTools]),
|
|
121
|
+
getAllTools: vi.fn(() => activeTools.map((name) => ({ name }))),
|
|
122
|
+
setActiveTools: vi.fn((toolNames: string[]) => {
|
|
123
|
+
activeTools.splice(0, activeTools.length, ...toolNames)
|
|
124
|
+
}),
|
|
125
|
+
appendEntry: vi.fn((customType: string, data?: unknown) => {
|
|
126
|
+
appendedEntries.push({ customType, data })
|
|
127
|
+
}),
|
|
128
|
+
sendMessage: vi.fn(),
|
|
129
|
+
} as unknown as ExtensionAPI & {
|
|
130
|
+
appendEntry: ReturnType<typeof vi.fn>
|
|
131
|
+
getActiveTools: ReturnType<typeof vi.fn>
|
|
132
|
+
getAllTools: ReturnType<typeof vi.fn>
|
|
133
|
+
getFlag: ReturnType<typeof vi.fn>
|
|
134
|
+
registerFlag: ReturnType<typeof vi.fn>
|
|
135
|
+
setActiveTools: ReturnType<typeof vi.fn>
|
|
136
|
+
sendMessage: ReturnType<typeof vi.fn>
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
piTownPackage(pi)
|
|
140
|
+
|
|
141
|
+
return { handlers, tools, commands, flags, appendedEntries, activeTools, pi }
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function createToolContext(repoRoot: string, sessionFile: string) {
|
|
145
|
+
return {
|
|
146
|
+
hasUI: true,
|
|
147
|
+
ui: {
|
|
148
|
+
notify: vi.fn(),
|
|
149
|
+
setStatus: vi.fn(),
|
|
150
|
+
setWidget: vi.fn(),
|
|
151
|
+
theme: {
|
|
152
|
+
fg: vi.fn((_token: string, value: string) => value),
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
cwd: repoRoot,
|
|
156
|
+
sessionManager: {
|
|
157
|
+
getSessionFile: () => sessionFile,
|
|
158
|
+
getEntries: () => [],
|
|
159
|
+
},
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
describe("pi town extension", () => {
|
|
164
|
+
it("injects hidden town context for managed leader sessions", async () => {
|
|
165
|
+
const { repoRoot, leaderSessionFile } = createRepoArtifacts()
|
|
166
|
+
const { handlers } = setup()
|
|
167
|
+
|
|
168
|
+
const result = await handlers["before_agent_start"]?.[0]?.(
|
|
169
|
+
{ systemPrompt: "base", prompt: "coordinate", images: [] },
|
|
170
|
+
createToolContext(repoRoot, leaderSessionFile),
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
expect(result).toEqual(
|
|
174
|
+
expect.objectContaining({
|
|
175
|
+
message: expect.objectContaining({
|
|
176
|
+
customType: "pitown-context",
|
|
177
|
+
display: false,
|
|
178
|
+
content: expect.stringContaining("Allowed Pi Town tools: pitown_board, pitown_delegate"),
|
|
179
|
+
}),
|
|
180
|
+
}),
|
|
181
|
+
)
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
it("renders the live board for town-managed sessions", async () => {
|
|
185
|
+
const { repoRoot, leaderSessionFile } = createRepoArtifacts()
|
|
186
|
+
const { tools } = setup()
|
|
187
|
+
const boardTool = tools.get("pitown_board")
|
|
188
|
+
|
|
189
|
+
const result = await boardTool?.execute("tool-call", {}, undefined, () => {}, createToolContext(repoRoot, leaderSessionFile))
|
|
190
|
+
|
|
191
|
+
expect(result).toEqual(
|
|
192
|
+
expect.objectContaining({
|
|
193
|
+
content: [expect.objectContaining({ text: expect.stringContaining("worker-001") })],
|
|
194
|
+
}),
|
|
195
|
+
)
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
it("blocks workers from messaging non-leader agents directly", async () => {
|
|
199
|
+
const { repoRoot, workerSessionFile } = createRepoArtifacts()
|
|
200
|
+
const { tools } = setup()
|
|
201
|
+
const messageTool = tools.get("pitown_message_agent")
|
|
202
|
+
|
|
203
|
+
await expect(
|
|
204
|
+
messageTool?.execute(
|
|
205
|
+
"tool-call",
|
|
206
|
+
{ agentId: "reviewer-001", body: "please review now" },
|
|
207
|
+
undefined,
|
|
208
|
+
() => {},
|
|
209
|
+
createToolContext(repoRoot, workerSessionFile),
|
|
210
|
+
),
|
|
211
|
+
).rejects.toThrow("Only the leader may message non-leader agents")
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
it("updates worker state and task status through the status tool", async () => {
|
|
215
|
+
const { artifactsDir, repoRoot, workerSessionFile } = createRepoArtifacts()
|
|
216
|
+
const { tools } = setup()
|
|
217
|
+
const statusTool = tools.get("pitown_update_status")
|
|
218
|
+
|
|
219
|
+
await statusTool?.execute(
|
|
220
|
+
"tool-call",
|
|
221
|
+
{ status: "completed", lastMessage: "auth flow fixed", blocked: false },
|
|
222
|
+
undefined,
|
|
223
|
+
() => {},
|
|
224
|
+
createToolContext(repoRoot, workerSessionFile),
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
expect(readAgentState(artifactsDir, "worker-001")).toEqual(
|
|
228
|
+
expect.objectContaining({
|
|
229
|
+
status: "completed",
|
|
230
|
+
lastMessage: "auth flow fixed",
|
|
231
|
+
}),
|
|
232
|
+
)
|
|
233
|
+
expect(readTaskRecord(artifactsDir, "task-123")).toEqual(
|
|
234
|
+
expect.objectContaining({
|
|
235
|
+
status: "completed",
|
|
236
|
+
}),
|
|
237
|
+
)
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
it("enables mayor plan mode with a read-only tool set", async () => {
|
|
241
|
+
const { repoRoot, leaderSessionFile } = createRepoArtifacts()
|
|
242
|
+
const { commands, activeTools, appendedEntries } = setup()
|
|
243
|
+
const planCommand = commands.get("plan") as { handler: RegisteredHandler } | undefined
|
|
244
|
+
const ctx = createToolContext(repoRoot, leaderSessionFile)
|
|
245
|
+
|
|
246
|
+
await planCommand?.handler([], ctx)
|
|
247
|
+
|
|
248
|
+
expect(activeTools).toEqual(["read", "grep", "find", "ls", "pitown_board", "pitown_peek_agent"])
|
|
249
|
+
expect(appendedEntries.at(-1)).toEqual(
|
|
250
|
+
expect.objectContaining({
|
|
251
|
+
customType: "pitown-mayor-plan",
|
|
252
|
+
}),
|
|
253
|
+
)
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
it("captures numbered mayor plan steps after a planning turn", async () => {
|
|
257
|
+
const { repoRoot, leaderSessionFile } = createRepoArtifacts()
|
|
258
|
+
const { commands, handlers, pi } = setup()
|
|
259
|
+
const planCommand = commands.get("plan") as { handler: RegisteredHandler } | undefined
|
|
260
|
+
const ctx = createToolContext(repoRoot, leaderSessionFile)
|
|
261
|
+
|
|
262
|
+
await planCommand?.handler([], ctx)
|
|
263
|
+
await handlers["agent_end"]?.[0]?.(
|
|
264
|
+
{
|
|
265
|
+
messages: [
|
|
266
|
+
{
|
|
267
|
+
role: "assistant",
|
|
268
|
+
content: [
|
|
269
|
+
{
|
|
270
|
+
type: "text",
|
|
271
|
+
text: "Plan:\n1. Inspect the current board and open tasks.\n2. Split the work into two bounded worker tasks.",
|
|
272
|
+
},
|
|
273
|
+
],
|
|
274
|
+
},
|
|
275
|
+
],
|
|
276
|
+
},
|
|
277
|
+
ctx,
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
expect(pi.sendMessage).toHaveBeenCalledWith(
|
|
281
|
+
expect.objectContaining({
|
|
282
|
+
customType: "pitown-mayor-plan-captured",
|
|
283
|
+
content: expect.stringContaining("1. ○ Inspect the current board and open tasks."),
|
|
284
|
+
}),
|
|
285
|
+
{ triggerTurn: false },
|
|
286
|
+
)
|
|
287
|
+
})
|
|
288
|
+
})
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs"
|
|
2
|
+
import { dirname, join } from "node:path"
|
|
3
|
+
import { fileURLToPath } from "node:url"
|
|
4
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"
|
|
5
|
+
import { registerMayorPlanMode } from "#pitown-mayor-plan"
|
|
6
|
+
import { registerTownTools } from "#pitown-town-tools"
|
|
7
|
+
|
|
8
|
+
const extensionDir = dirname(fileURLToPath(import.meta.url))
|
|
9
|
+
const agentsDir = join(extensionDir, "..", "agents")
|
|
10
|
+
|
|
11
|
+
function parseFrontmatter(content: string): { frontmatter: Record<string, string>; body: string } {
|
|
12
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/)
|
|
13
|
+
if (!match) return { frontmatter: {}, body: content.trim() }
|
|
14
|
+
|
|
15
|
+
const frontmatter: Record<string, string> = {}
|
|
16
|
+
for (const rawLine of match[1]!.split(/\r?\n/)) {
|
|
17
|
+
const index = rawLine.indexOf(":")
|
|
18
|
+
if (index === -1) continue
|
|
19
|
+
frontmatter[rawLine.slice(0, index).trim()] = rawLine.slice(index + 1).trim()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return { frontmatter, body: match[2]!.trim() }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function listBundledAgents(): Array<{ name: string; description: string }> {
|
|
26
|
+
if (!existsSync(agentsDir)) return []
|
|
27
|
+
|
|
28
|
+
return readdirSync(agentsDir)
|
|
29
|
+
.filter((file) => file.endsWith(".md"))
|
|
30
|
+
.map((file) => {
|
|
31
|
+
const content = readFileSync(join(agentsDir, file), "utf-8")
|
|
32
|
+
const { frontmatter } = parseFrontmatter(content)
|
|
33
|
+
return {
|
|
34
|
+
name: frontmatter["name"] ?? file.replace(/\.md$/, ""),
|
|
35
|
+
description: frontmatter["description"] ?? "",
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export default function piTownPackage(pi: ExtensionAPI) {
|
|
41
|
+
registerTownTools(pi)
|
|
42
|
+
registerMayorPlanMode(pi)
|
|
43
|
+
|
|
44
|
+
pi.registerCommand("town-status", {
|
|
45
|
+
description: "Show Pi Town package status",
|
|
46
|
+
handler: async (_args, ctx) => {
|
|
47
|
+
ctx.ui.notify("Pi Town package loaded. Use /town-agents to inspect bundled roles.", "info")
|
|
48
|
+
},
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
pi.registerCommand("town-agents", {
|
|
52
|
+
description: "List bundled Pi Town agents",
|
|
53
|
+
handler: async (_args, ctx) => {
|
|
54
|
+
const agents = listBundledAgents()
|
|
55
|
+
if (agents.length === 0) {
|
|
56
|
+
ctx.ui.notify("No bundled Pi Town agents were found.", "warning")
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const lines = ["Bundled Pi Town agents:", ...agents.map((agent) => `- ${agent.name}: ${agent.description}`)]
|
|
61
|
+
ctx.ui.notify(lines.join("\n"), "info")
|
|
62
|
+
},
|
|
63
|
+
})
|
|
64
|
+
}
|