pi-roundtable-coding 0.7.0 → 0.7.3
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/CHANGELOG.md +10 -1
- package/README.md +26 -3
- package/package.json +2 -2
- package/src/coding-desk.ts +81 -12
- package/src/coding-plugin.ts +97 -16
- package/src/index.ts +7 -1
- package/src/pi-coding-worker.ts +11 -2
- package/src/repo-shelf.ts +24 -0
- package/src/worker-entry.ts +8 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,7 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
-
## [0.7.
|
|
5
|
+
## [0.7.3] - 2026-10-03
|
|
6
|
+
|
|
7
|
+
- Hook caller model/thinking/origin/channel, owner policy, clone adoption, threaded approvals and report formatting through options, and add `pushHoldText` for the held `repo_push` card.
|
|
8
|
+
|
|
9
|
+
## [0.7.2] - 2026-10-02
|
|
10
|
+
|
|
11
|
+
First release published by the lockstep workflow; no changes to the package.
|
|
12
|
+
The `v0.7.1` tag published nothing.
|
|
13
|
+
|
|
14
|
+
## [0.7.0] - 2026-10-02
|
|
6
15
|
|
|
7
16
|
Prepared for the first npm publication.
|
|
8
17
|
The earlier `0.1.0` was local-only and was never published on npm.
|
package/README.md
CHANGED
|
@@ -78,7 +78,9 @@ All five tools have `minTier: "owner"` and are contributed through `defineTool`
|
|
|
78
78
|
The operator remains responsible for any tool-tier overrides.
|
|
79
79
|
A custom owner claim with a restricted `ToolSelection` must include the exported `REPO_TOOLS` names; include `skill_list` separately when the host skills addon is enabled.
|
|
80
80
|
The host's skills addon resolves explicit skills, or the calling agent's carried skills when omitted; an owner call carries none unless it names them.
|
|
81
|
-
Missing or unknown skills refuse the task; requesting skills while the addon is off also refuses it.
|
|
81
|
+
Missing or unknown skills refuse the task by default; requesting skills while the addon is off also refuses it.
|
|
82
|
+
`skipUnavailableCarriedSkills: true` opts into skipping unavailable implicit skills and disclosing them in the task-start text or through `presentation.taskStarted`.
|
|
83
|
+
Explicit skill requests still refuse missing or unknown skills.
|
|
82
84
|
Sessions with kind `owner` get the public `skillListExtension`; agent sessions keep the core's existing `skill_list` instead of registering it twice.
|
|
83
85
|
|
|
84
86
|
A worker reads the repository's instructions, edits and checks the work, and commits using repository conventions.
|
|
@@ -99,7 +101,10 @@ Shipping is separate from coding:
|
|
|
99
101
|
|
|
100
102
|
`ownerRepos` is an exact list of `owner/repo` names, not a prefix or wildcard; its default is empty.
|
|
101
103
|
Listed repositories skip the plugin's `repo_push` hold, though other host hold rules can still hold the call.
|
|
102
|
-
|
|
104
|
+
A trusted `isOwnerRepo(repo)` host policy can grant additional exemptions, including repositories created after startup; only literal `true` grants one.
|
|
105
|
+
The package validates the repository name before consulting it.
|
|
106
|
+
Never derive this policy from model input, clone files, or remote repository instructions.
|
|
107
|
+
Neither exemption bypasses shell pushes, risky worker actions, or other linked host rules.
|
|
103
108
|
The report and approval card name the full SHA, current default branch and credential-free push destination.
|
|
104
109
|
The clone must have exactly one push destination; HTTP remote URLs with user information, query strings or fragments are refused before fetching.
|
|
105
110
|
A changed HEAD, dirty clone, changed remote default branch, changed push destination, missing report or non-fast-forward remote causes refusal.
|
|
@@ -109,6 +114,9 @@ Report receipts live in memory, so request a fresh report after restarting the h
|
|
|
109
114
|
|
|
110
115
|
Every worker tool call crosses a private Bun IPC channel to the parent process before execution.
|
|
111
116
|
The parent consults `shellHoldRule`, additional `holds`, and the host's linked hold rules; a recognized risky action asks the owner's surface prompts using `approvalCard`.
|
|
117
|
+
When `threads` is configured, progress and cards stay in a thread opened under the resolved run's `origin`; if no thread can open, calls remain held rather than falling back to another channel's approval cards.
|
|
118
|
+
Without `threads`, the resolved report channel's prompts are used.
|
|
119
|
+
`threadText` controls the initial post, held-action post, approval title and final report; the thread is archived before `onResult` is called, and a failed thread post/close does not discard result delivery.
|
|
112
120
|
`promptSlot` tracks these cards and `workTimeout` excludes their waiting time from the worker's budget (one hour by default).
|
|
113
121
|
`timeoutMs` must be positive and finite, and at most 2,147,483,647 ms to fit the host timer.
|
|
114
122
|
An approved call runs; a declined, expired, missing or failed card blocks it and instructs the worker not to retry or work around the refusal.
|
|
@@ -129,7 +137,7 @@ This is a host coding desk, **not a sandbox**.
|
|
|
129
137
|
The worker can run local checks, read the host's accessible files, use the network and commit changes.
|
|
130
138
|
Its standing instructions delegate shipping to the calling agent; repository tools and extra package tools are not activated inside the worker.
|
|
131
139
|
`repo_push` is the supported shipping route and always uses a previously reported full SHA and a non-force push.
|
|
132
|
-
The public shell rule holds recognized shell pushes, destructive commands, privileged commands and recognized writes outside the clone, even for
|
|
140
|
+
The public shell rule holds recognized shell pushes, destructive commands, privileged commands and recognized writes outside the configured worker workspace (the clone by default), even for owner-owned repositories.
|
|
133
141
|
It is a heuristic guard: scripts, interpreters, symlinks, Git hooks, package extensions and detached grandchildren are not an OS isolation boundary.
|
|
134
142
|
A trusted package or arbitrary shell program can bypass heuristic detection; only run code and repositories you trust, under a dedicated low-privilege host user.
|
|
135
143
|
Use an isolated container or VM when host access is unacceptable.
|
|
@@ -153,6 +161,17 @@ The `CODING` service exposes `shelf: RepoShelf` and `desk: CodingDesk` for trust
|
|
|
153
161
|
| `model` | required | Worker provider/model ID. |
|
|
154
162
|
| `thinking` | `medium` | Pi thinking level. |
|
|
155
163
|
| `ownerRepos` | `[]` | Exact push-hold exemptions. |
|
|
164
|
+
| `isOwnerRepo` | none | Trusted host policy for additional push-hold exemptions; must return literal true. |
|
|
165
|
+
| `pushHoldText` | repository, sha, target and branch | Trusted wording for the approval card of a held `repo_push`; it never changes who is held. |
|
|
166
|
+
| `adoptClones` | `[]` | Startup `{ from, repo }` moves of standalone clones; existing shelf destinations are never replaced. |
|
|
167
|
+
| `resolveRun` | configured model/thinking, caller channel | Per-caller model, thinking, report channel and optional thread origin. |
|
|
168
|
+
| `postChangeReport` | caller surface reply | Post the record using the calling identity and channel. |
|
|
169
|
+
| `presentation` | English package text | Separate change-report post/result text and task-start/omitted-skill wording. |
|
|
170
|
+
| `threads` | none | Public `DispatchThreads`-compatible progress and approval thread port. |
|
|
171
|
+
| `threadText` | English package text | Thread introduction, held-action notice, approval title and final report. |
|
|
172
|
+
| `workerWorkspace` | individual clone | Trusted shell-policy write boundary; not an OS sandbox. |
|
|
173
|
+
| `workerPrompt` | generic worker instructions | Trusted standing prompt replacement; it cannot bypass approval or cleanup. |
|
|
174
|
+
| `skipUnavailableCarriedSkills` | `false` | Skip and disclose unavailable implicit skills; explicit skill requests still refuse them. |
|
|
156
175
|
| `workerPackages` | `[]` | Absolute installed extension paths. |
|
|
157
176
|
| `agentDir` | Pi host directory | Pi credentials and model configuration. |
|
|
158
177
|
| `timeoutMs` | `3600000` | Work-time limit in ms, excluding owner wait; 0 < value <= 2147483647. |
|
|
@@ -162,6 +181,10 @@ The `CODING` service exposes `shelf: RepoShelf` and `desk: CodingDesk` for trust
|
|
|
162
181
|
| `onResult` | surface reply | Report delivery integration. |
|
|
163
182
|
|
|
164
183
|
`RepoShelf`, `CodingDesk`, `PiCodingWorker`, `CodingWorkerFailure`, `REPO_TOOLS`, `codingReport`, `reportPost` and their option/result types are exported for testing and host integrations.
|
|
184
|
+
`resolveRun(turn)` receives the public `ToolTurn`, including the calling agent scope and channel.
|
|
185
|
+
It returns `{ model, thinking, channel, origin? }`; skill selection still follows the caller, not the resolved report destination.
|
|
186
|
+
`onResult` receives those fields plus loaded `skillNames` and the optional progress `thread` in `result.job`, so a host can enqueue its own localized conversation turn.
|
|
187
|
+
`adoptClones` and `RepoShelf.adopt` keep existing clone contents and path layout without database migrations.
|
|
165
188
|
No database or Discord-specific types are required.
|
|
166
189
|
|
|
167
190
|
## Development and publishing
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-roundtable-coding",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.3",
|
|
4
4
|
"description": "Repository shelves and owner-approved Pi coding workers for pi-roundtable",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"@earendil-works/pi-ai": ">=1.0.0 <2",
|
|
45
45
|
"@biomejs/biome": "2.5.15",
|
|
46
46
|
"@types/bun": "1.4.2",
|
|
47
|
-
"pi-roundtable": "0.7.
|
|
47
|
+
"pi-roundtable": "0.7.3",
|
|
48
48
|
"typescript": "7.0.2"
|
|
49
49
|
}
|
|
50
50
|
}
|
package/src/coding-desk.ts
CHANGED
|
@@ -8,6 +8,8 @@ import type {
|
|
|
8
8
|
import {
|
|
9
9
|
AgentError,
|
|
10
10
|
approvalCard,
|
|
11
|
+
type DispatchThread,
|
|
12
|
+
type DispatchThreads,
|
|
11
13
|
promptSlot,
|
|
12
14
|
workTimeout,
|
|
13
15
|
} from "pi-roundtable/kit";
|
|
@@ -20,6 +22,12 @@ export interface CodingJob {
|
|
|
20
22
|
repo: string;
|
|
21
23
|
task: string;
|
|
22
24
|
channel: ChannelKey;
|
|
25
|
+
/** The calling turn's channel, where a progress thread opens. */
|
|
26
|
+
origin?: ChannelKey;
|
|
27
|
+
/** Host-resolved names of the skills carried to this run. */
|
|
28
|
+
skillNames?: string[];
|
|
29
|
+
/** Progress thread, when the surface supports one. */
|
|
30
|
+
thread?: DispatchThread;
|
|
23
31
|
model: string;
|
|
24
32
|
thinking: ThinkingLevel;
|
|
25
33
|
skillFiles: string[];
|
|
@@ -40,10 +48,19 @@ export interface CodingResult {
|
|
|
40
48
|
state?: RepoState;
|
|
41
49
|
commits: string[];
|
|
42
50
|
}
|
|
51
|
+
export interface CodingThreadText {
|
|
52
|
+
initial?(job: CodingJob): string;
|
|
53
|
+
held?(entry: string, job: CodingJob): string;
|
|
54
|
+
approvalTitle?(job: CodingJob): string;
|
|
55
|
+
report?(result: CodingResult): string;
|
|
56
|
+
}
|
|
43
57
|
export interface CodingDeskOptions {
|
|
44
58
|
shelf: Pick<RepoShelf, "dirOf" | "state" | "commitsSince">;
|
|
45
59
|
worker: CodingWorker;
|
|
46
|
-
prompts(channel: ChannelKey): OwnerPrompts | undefined;
|
|
60
|
+
prompts?(channel: ChannelKey): OwnerPrompts | undefined;
|
|
61
|
+
/** When configured, approval cards stay in the job's thread; no thread means held. */
|
|
62
|
+
threads?: Pick<DispatchThreads, "open">;
|
|
63
|
+
threadText?: CodingThreadText;
|
|
47
64
|
deliver(result: CodingResult): Promise<void>;
|
|
48
65
|
logger: Logger;
|
|
49
66
|
timeoutMs?: number;
|
|
@@ -79,6 +96,9 @@ export class CodingDesk {
|
|
|
79
96
|
);
|
|
80
97
|
this.#options = options;
|
|
81
98
|
}
|
|
99
|
+
runningChannels(): ChannelKey[] {
|
|
100
|
+
return [...this.#running.values()].map(({ job }) => job.channel);
|
|
101
|
+
}
|
|
82
102
|
busy(): string[] {
|
|
83
103
|
return [...this.#running.values()].map(
|
|
84
104
|
({ job }) => `Coding #${job.id} in ${job.repo}`,
|
|
@@ -97,7 +117,7 @@ export class CodingDesk {
|
|
|
97
117
|
await this.idle();
|
|
98
118
|
}
|
|
99
119
|
async start(
|
|
100
|
-
request: Omit<CodingJob, "id" | "startedAt" | "startHead">,
|
|
120
|
+
request: Omit<CodingJob, "id" | "startedAt" | "startHead" | "thread">,
|
|
101
121
|
): Promise<CodingJob> {
|
|
102
122
|
if (this.#stopped) throw new AgentError("The coding desk is stopped.");
|
|
103
123
|
const task = request.task.trim();
|
|
@@ -146,6 +166,8 @@ export class CodingDesk {
|
|
|
146
166
|
shelf,
|
|
147
167
|
worker,
|
|
148
168
|
prompts,
|
|
169
|
+
threads,
|
|
170
|
+
threadText,
|
|
149
171
|
deliver,
|
|
150
172
|
timeoutMs = 3_600_000,
|
|
151
173
|
} = this.#options;
|
|
@@ -159,7 +181,29 @@ export class CodingDesk {
|
|
|
159
181
|
job.startHead = (await shelf.state(job.repo)).head;
|
|
160
182
|
if (controller.signal.aborted)
|
|
161
183
|
throw new AgentError("The worker was stopped.");
|
|
162
|
-
|
|
184
|
+
if (threads) {
|
|
185
|
+
try {
|
|
186
|
+
const thread = await threads.open(
|
|
187
|
+
job.origin,
|
|
188
|
+
`${job.repo} #${job.id}`,
|
|
189
|
+
threadText?.initial?.(job) ?? `Task:\n${job.task}`,
|
|
190
|
+
);
|
|
191
|
+
if (thread) job.thread = thread;
|
|
192
|
+
} catch {
|
|
193
|
+
this.#options.logger.warn(
|
|
194
|
+
{ job: job.id },
|
|
195
|
+
"Coding thread unavailable.",
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
if (controller.signal.aborted)
|
|
200
|
+
throw new AgentError("The worker was stopped.");
|
|
201
|
+
slot.bind(
|
|
202
|
+
threads
|
|
203
|
+
? job.thread && prompts?.(job.thread.channel)
|
|
204
|
+
: prompts?.(job.channel),
|
|
205
|
+
`Coding worker #${job.id}`,
|
|
206
|
+
);
|
|
163
207
|
cancel = workTimeout(timeoutMs, slot, () => {
|
|
164
208
|
timedOut = true;
|
|
165
209
|
controller.abort();
|
|
@@ -168,7 +212,8 @@ export class CodingDesk {
|
|
|
168
212
|
let answer: HeldCallAnswer = "held";
|
|
169
213
|
try {
|
|
170
214
|
const decision = await slot.prompts?.confirm(
|
|
171
|
-
|
|
215
|
+
threadText?.approvalTitle?.(job) ??
|
|
216
|
+
`${slot.asker} requests approval`,
|
|
172
217
|
approvalCard(call),
|
|
173
218
|
controller.signal,
|
|
174
219
|
);
|
|
@@ -178,14 +223,20 @@ export class CodingDesk {
|
|
|
178
223
|
/* A failed card never authorizes the call. */
|
|
179
224
|
}
|
|
180
225
|
if (answer !== "approved") {
|
|
181
|
-
if (held.length < MAX_HELD_ENTRIES)
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
MAX_HELD_CHARS,
|
|
186
|
-
),
|
|
226
|
+
if (held.length < MAX_HELD_ENTRIES) {
|
|
227
|
+
const entry = bounded(
|
|
228
|
+
`${answer}: ${call.action}: ${call.tool} ${call.input}`,
|
|
229
|
+
MAX_HELD_CHARS,
|
|
187
230
|
);
|
|
188
|
-
|
|
231
|
+
held.push(entry);
|
|
232
|
+
try {
|
|
233
|
+
await job.thread?.post(
|
|
234
|
+
threadText?.held?.(entry, job) ?? `Held: ${entry}`,
|
|
235
|
+
);
|
|
236
|
+
} catch {
|
|
237
|
+
/* A failed progress post does not discard the held action. */
|
|
238
|
+
}
|
|
239
|
+
} else omittedHeld++;
|
|
189
240
|
}
|
|
190
241
|
return answer;
|
|
191
242
|
};
|
|
@@ -230,7 +281,25 @@ export class CodingDesk {
|
|
|
230
281
|
}
|
|
231
282
|
if (omittedHeld)
|
|
232
283
|
held.push(`[${omittedHeld} more unapproved actions omitted]`);
|
|
233
|
-
|
|
284
|
+
const result: CodingResult = {
|
|
285
|
+
job,
|
|
286
|
+
outcome,
|
|
287
|
+
held,
|
|
288
|
+
...(state ? { state } : {}),
|
|
289
|
+
commits,
|
|
290
|
+
};
|
|
291
|
+
try {
|
|
292
|
+
await job.thread?.close(
|
|
293
|
+
bounded(
|
|
294
|
+
threadText?.report?.(result) ??
|
|
295
|
+
codingReport(result, job.startedAt.toISOString()),
|
|
296
|
+
MAX_REPORT_CHARS,
|
|
297
|
+
),
|
|
298
|
+
);
|
|
299
|
+
} catch {
|
|
300
|
+
this.#options.logger.warn({ job: job.id }, "Coding thread close failed.");
|
|
301
|
+
}
|
|
302
|
+
await deliver(result);
|
|
234
303
|
}
|
|
235
304
|
}
|
|
236
305
|
|
package/src/coding-plugin.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
type ChannelKey,
|
|
2
3
|
definePlugin,
|
|
3
4
|
defineTool,
|
|
4
5
|
type HoldCheck,
|
|
@@ -7,29 +8,52 @@ import {
|
|
|
7
8
|
serviceKey,
|
|
8
9
|
type ThinkingLevel,
|
|
9
10
|
ToolRefusal,
|
|
11
|
+
type ToolTurn,
|
|
10
12
|
} from "pi-roundtable";
|
|
11
13
|
import {
|
|
12
14
|
AgentError,
|
|
13
15
|
checkRepoName,
|
|
16
|
+
type DispatchThreads,
|
|
14
17
|
skillListExtension,
|
|
15
18
|
zonedStamp,
|
|
16
19
|
} from "pi-roundtable/kit";
|
|
17
20
|
import { Type } from "typebox";
|
|
18
21
|
import {
|
|
19
22
|
CodingDesk,
|
|
23
|
+
type CodingJob,
|
|
20
24
|
type CodingResult,
|
|
25
|
+
type CodingThreadText,
|
|
21
26
|
type CodingWorker,
|
|
22
27
|
codingReport,
|
|
23
28
|
MAX_CODING_TASK_CHARS,
|
|
24
29
|
} from "./coding-desk.ts";
|
|
25
30
|
import { PiCodingWorker } from "./pi-coding-worker.ts";
|
|
26
|
-
import {
|
|
31
|
+
import {
|
|
32
|
+
type ChangeReport,
|
|
33
|
+
type CloneCommand,
|
|
34
|
+
RepoShelf,
|
|
35
|
+
reportPost,
|
|
36
|
+
} from "./repo-shelf.ts";
|
|
27
37
|
|
|
28
38
|
export interface CodingService {
|
|
29
39
|
readonly shelf: RepoShelf;
|
|
30
40
|
readonly desk: CodingDesk;
|
|
31
41
|
}
|
|
32
42
|
export const CODING = serviceKey<CodingService>("coding.workbench");
|
|
43
|
+
export interface CodingRun {
|
|
44
|
+
model: string;
|
|
45
|
+
thinking: ThinkingLevel;
|
|
46
|
+
channel: ChannelKey;
|
|
47
|
+
origin?: ChannelKey;
|
|
48
|
+
}
|
|
49
|
+
export interface CodingPresentation {
|
|
50
|
+
/** Separate the owner's posted record from the model's shipping instructions. */
|
|
51
|
+
changeReport?(
|
|
52
|
+
report: ChangeReport,
|
|
53
|
+
direct: boolean,
|
|
54
|
+
): { post: string; result: string };
|
|
55
|
+
taskStarted?(job: CodingJob, skippedSkills: string[]): string;
|
|
56
|
+
}
|
|
33
57
|
export interface CodingOptions {
|
|
34
58
|
shelfDir: string;
|
|
35
59
|
/** Provider/model-id, resolved with the worker's Pi host login. */
|
|
@@ -37,6 +61,24 @@ export interface CodingOptions {
|
|
|
37
61
|
thinking?: ThinkingLevel;
|
|
38
62
|
/** Exact owner/repo names allowed to use repo_push without a hold. Default: none. */
|
|
39
63
|
ownerRepos?: string[];
|
|
64
|
+
/** Trusted host policy for additional owner-owned repositories, including future clones.
|
|
65
|
+
* Only literal true grants an exemption. This never bypasses linked host hold rules. */
|
|
66
|
+
isOwnerRepo?: (repo: string) => boolean;
|
|
67
|
+
/** Trusted wording for the approval card of a held `repo_push`. Default: names the repository, sha, target and branch. */
|
|
68
|
+
pushHoldText?: (repo: string, sha: string) => string;
|
|
69
|
+
/** Moves legacy standalone clones before the host reads shelf-linked skills. */
|
|
70
|
+
adoptClones?: { from: string; repo: string }[];
|
|
71
|
+
/** Resolve the calling agent/owner's run identity, independently of report posting. */
|
|
72
|
+
resolveRun?: (turn: ToolTurn) => CodingRun | Promise<CodingRun>;
|
|
73
|
+
/** Post change reports in the caller's identity/channel rather than the default surface. */
|
|
74
|
+
postChangeReport?: (turn: ToolTurn, text: string) => Promise<void>;
|
|
75
|
+
presentation?: CodingPresentation;
|
|
76
|
+
threads?: Pick<DispatchThreads, "open">;
|
|
77
|
+
threadText?: CodingThreadText;
|
|
78
|
+
workerWorkspace?: string;
|
|
79
|
+
workerPrompt?: (dir: string) => string;
|
|
80
|
+
/** Opt in to skipping unavailable implicit skills; explicit requests always fail closed. */
|
|
81
|
+
skipUnavailableCarriedSkills?: boolean;
|
|
40
82
|
/** Absolute installed extension package paths. Default: none. */
|
|
41
83
|
workerPackages?: string[];
|
|
42
84
|
agentDir?: string;
|
|
@@ -66,17 +108,25 @@ export function coding(options: CodingOptions) {
|
|
|
66
108
|
throw new PluginError("model must be provider/model-id.");
|
|
67
109
|
const ownerRepos = new Set(options.ownerRepos ?? []);
|
|
68
110
|
for (const repo of ownerRepos) checkRepoName(repo);
|
|
111
|
+
const ownerOwned = (repo: string): boolean => {
|
|
112
|
+
checkRepoName(repo);
|
|
113
|
+
return ownerRepos.has(repo) || options.isOwnerRepo?.(repo) === true;
|
|
114
|
+
};
|
|
69
115
|
return definePlugin({
|
|
70
116
|
name: "coding",
|
|
71
117
|
provides: [CODING],
|
|
72
118
|
setup(context) {
|
|
73
119
|
const shelf = new RepoShelf(options.shelfDir, options.clone);
|
|
120
|
+
for (const adoption of options.adoptClones ?? [])
|
|
121
|
+
shelf.adopt(adoption.from, adoption.repo);
|
|
74
122
|
const skills = context.services.find(SKILLS);
|
|
75
123
|
const worker =
|
|
76
124
|
options.worker ??
|
|
77
125
|
new PiCodingWorker({
|
|
78
126
|
packages: options.workerPackages,
|
|
79
127
|
agentDir: options.agentDir,
|
|
128
|
+
workspace: options.workerWorkspace,
|
|
129
|
+
prompt: options.workerPrompt,
|
|
80
130
|
holds: (tool, input, scope) =>
|
|
81
131
|
options.holds?.(tool, input, scope) ??
|
|
82
132
|
context.sessions().holds(tool, input, scope),
|
|
@@ -86,6 +136,8 @@ export function coding(options: CodingOptions) {
|
|
|
86
136
|
worker,
|
|
87
137
|
timeoutMs: options.timeoutMs,
|
|
88
138
|
logger: context.logger,
|
|
139
|
+
threads: options.threads,
|
|
140
|
+
threadText: options.threadText,
|
|
89
141
|
prompts: (channel) => context.surfaces.prompts(channel),
|
|
90
142
|
deliver:
|
|
91
143
|
options.onResult ??
|
|
@@ -187,11 +239,21 @@ export function coding(options: CodingOptions) {
|
|
|
187
239
|
refusal(() =>
|
|
188
240
|
ship(repo, async () => {
|
|
189
241
|
const report = await shelf.report(repo);
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
242
|
+
const direct = ownerOwned(repo);
|
|
243
|
+
const text = options.presentation?.changeReport?.(
|
|
244
|
+
report,
|
|
245
|
+
direct,
|
|
246
|
+
) ?? {
|
|
247
|
+
post: reportPost(report, direct),
|
|
248
|
+
result: reportPost(report, direct),
|
|
249
|
+
};
|
|
250
|
+
if (options.postChangeReport)
|
|
251
|
+
await options.postChangeReport(turn, text.post);
|
|
252
|
+
else
|
|
253
|
+
await context.surfaces.sendReply(turn.channel, {
|
|
254
|
+
chunks: [text.post],
|
|
255
|
+
});
|
|
256
|
+
return text.result;
|
|
195
257
|
}),
|
|
196
258
|
),
|
|
197
259
|
}),
|
|
@@ -199,15 +261,16 @@ export function coding(options: CodingOptions) {
|
|
|
199
261
|
name: "repo_push",
|
|
200
262
|
minTier: "owner",
|
|
201
263
|
description:
|
|
202
|
-
"Push the exact full SHA from the latest change report to its default branch, without force. Held for owner approval unless
|
|
264
|
+
"Push the exact full SHA from the latest change report to its default branch, without force. Held for owner approval unless the trusted host policy explicitly marks the clone owner-owned.",
|
|
203
265
|
parameters: Type.Object({
|
|
204
266
|
repo: repoSchema,
|
|
205
267
|
sha: Type.String({ pattern: "^[0-9a-f]{40}$" }),
|
|
206
268
|
}),
|
|
207
269
|
hold: ({ repo, sha }) =>
|
|
208
|
-
|
|
270
|
+
ownerOwned(repo)
|
|
209
271
|
? undefined
|
|
210
|
-
:
|
|
272
|
+
: (options.pushHoldText?.(repo, sha) ??
|
|
273
|
+
shelf.pushDescription(repo, sha)),
|
|
211
274
|
run: ({ repo, sha }) =>
|
|
212
275
|
refusal(() =>
|
|
213
276
|
ship(
|
|
@@ -242,21 +305,39 @@ export function coding(options: CodingOptions) {
|
|
|
242
305
|
names ??
|
|
243
306
|
(turn.agent ? skills?.carriedNames(turn.agent.name) : []) ??
|
|
244
307
|
[];
|
|
245
|
-
|
|
308
|
+
if (names || !options.skipUnavailableCarriedSkills)
|
|
309
|
+
skills?.checkRegistered(carried);
|
|
246
310
|
const set = skills?.resolve(carried);
|
|
247
|
-
if (
|
|
311
|
+
if (
|
|
312
|
+
set?.missing.length &&
|
|
313
|
+
(names || !options.skipUnavailableCarriedSkills)
|
|
314
|
+
)
|
|
248
315
|
throw new AgentError(
|
|
249
|
-
|
|
316
|
+
`These skills cannot load: ${set.missing.map((item) => `${item.name} (${item.reason})`).join(", ")}`,
|
|
250
317
|
);
|
|
251
|
-
const
|
|
252
|
-
repo,
|
|
253
|
-
task,
|
|
318
|
+
const run = (await options.resolveRun?.(turn)) ?? {
|
|
254
319
|
channel: turn.channel,
|
|
255
320
|
model: options.model,
|
|
256
321
|
thinking: options.thinking ?? "medium",
|
|
322
|
+
};
|
|
323
|
+
if (shipping.has(repo))
|
|
324
|
+
throw new AgentError(
|
|
325
|
+
"A report or push is in progress for this repository.",
|
|
326
|
+
);
|
|
327
|
+
const job = await desk.start({
|
|
328
|
+
...run,
|
|
329
|
+
repo,
|
|
330
|
+
task,
|
|
257
331
|
skillFiles: set?.skills.map((skill) => skill.file) ?? [],
|
|
332
|
+
skillNames: set?.skills.map((skill) => skill.name) ?? [],
|
|
258
333
|
});
|
|
259
|
-
return
|
|
334
|
+
return (
|
|
335
|
+
options.presentation?.taskStarted?.(
|
|
336
|
+
job,
|
|
337
|
+
set?.missing.map((item) => item.name) ?? [],
|
|
338
|
+
) ??
|
|
339
|
+
`Started coding task #${job.id} in ${repo} on ${job.model}; its report returns to ${job.channel}.${set?.missing.length ? ` Unavailable implicit skills skipped: ${set.missing.map((item) => item.name).join(", ")}.` : ""}`
|
|
340
|
+
);
|
|
260
341
|
}),
|
|
261
342
|
}),
|
|
262
343
|
],
|
package/src/index.ts
CHANGED
|
@@ -2,11 +2,17 @@ export type {
|
|
|
2
2
|
CodingDeskOptions,
|
|
3
3
|
CodingJob,
|
|
4
4
|
CodingResult,
|
|
5
|
+
CodingThreadText,
|
|
5
6
|
CodingWorker,
|
|
6
7
|
HeldCallAnswer,
|
|
7
8
|
} from "./coding-desk.ts";
|
|
8
9
|
export { CodingDesk, codingReport } from "./coding-desk.ts";
|
|
9
|
-
export type {
|
|
10
|
+
export type {
|
|
11
|
+
CodingOptions,
|
|
12
|
+
CodingPresentation,
|
|
13
|
+
CodingRun,
|
|
14
|
+
CodingService,
|
|
15
|
+
} from "./coding-plugin.ts";
|
|
10
16
|
export { CODING, coding } from "./coding-plugin.ts";
|
|
11
17
|
export type { PiCodingWorkerOptions } from "./pi-coding-worker.ts";
|
|
12
18
|
export { PiCodingWorker } from "./pi-coding-worker.ts";
|
package/src/pi-coding-worker.ts
CHANGED
|
@@ -12,6 +12,10 @@ export interface PiCodingWorkerOptions {
|
|
|
12
12
|
agentDir?: string;
|
|
13
13
|
/** Additional host rules, consulted in the parent process. */
|
|
14
14
|
holds?: HoldCheck;
|
|
15
|
+
/** Trusted host policy boundary for writes; defaults to the individual clone. */
|
|
16
|
+
workspace?: string;
|
|
17
|
+
/** Trusted host standing prompt, replacing the generic worker instructions. */
|
|
18
|
+
prompt?: (dir: string) => string;
|
|
15
19
|
}
|
|
16
20
|
interface WorkerMessage {
|
|
17
21
|
type: string;
|
|
@@ -43,6 +47,7 @@ export class PiCodingWorker implements CodingWorker {
|
|
|
43
47
|
if (process.platform === "win32")
|
|
44
48
|
throw new AgentError("Coding workers require a POSIX host.");
|
|
45
49
|
if (signal.aborted) throw new AgentError("The worker was stopped.");
|
|
50
|
+
const prompt = this.#options.prompt?.(job.dir);
|
|
46
51
|
let report: string | undefined;
|
|
47
52
|
const child = Bun.spawn(
|
|
48
53
|
[
|
|
@@ -60,9 +65,11 @@ export class PiCodingWorker implements CodingWorker {
|
|
|
60
65
|
if (value.type === "ready") {
|
|
61
66
|
proc.send({
|
|
62
67
|
type: "start",
|
|
63
|
-
|
|
68
|
+
// Thread handles and their host callbacks never cross IPC.
|
|
69
|
+
job: { ...job, thread: undefined },
|
|
64
70
|
packages: this.#options.packages ?? [],
|
|
65
71
|
agentDir: this.#options.agentDir ?? getAgentDir(),
|
|
72
|
+
prompt,
|
|
66
73
|
});
|
|
67
74
|
} else if (
|
|
68
75
|
value.type === "report" &&
|
|
@@ -80,7 +87,9 @@ export class PiCodingWorker implements CodingWorker {
|
|
|
80
87
|
void (async () => {
|
|
81
88
|
let answer: HeldCallAnswer = "held";
|
|
82
89
|
try {
|
|
83
|
-
const context = {
|
|
90
|
+
const context = {
|
|
91
|
+
workspace: this.#options.workspace ?? job.dir,
|
|
92
|
+
};
|
|
84
93
|
const action =
|
|
85
94
|
shellHoldRule.describe(tool, input, context) ??
|
|
86
95
|
this.#options.holds?.(tool, input, context);
|
package/src/repo-shelf.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
readdirSync,
|
|
10
10
|
readSync,
|
|
11
11
|
realpathSync,
|
|
12
|
+
renameSync,
|
|
12
13
|
} from "node:fs";
|
|
13
14
|
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
14
15
|
import { AgentError, checkRepoName } from "pi-roundtable/kit";
|
|
@@ -179,6 +180,29 @@ export class RepoShelf {
|
|
|
179
180
|
this.#clone = clone;
|
|
180
181
|
}
|
|
181
182
|
|
|
183
|
+
/** Adopt an existing standalone clone at startup; an existing destination is never replaced. */
|
|
184
|
+
adopt(from: string, repo: string): boolean {
|
|
185
|
+
checkRepoName(repo);
|
|
186
|
+
const to = join(this.dir, repo);
|
|
187
|
+
if (
|
|
188
|
+
!existsSync(join(from, ".git")) ||
|
|
189
|
+
lstatSync(to, { throwIfNoEntry: false })
|
|
190
|
+
)
|
|
191
|
+
return false;
|
|
192
|
+
if (
|
|
193
|
+
!lstatSync(from).isDirectory() ||
|
|
194
|
+
!lstatSync(join(from, ".git")).isDirectory()
|
|
195
|
+
)
|
|
196
|
+
throw new AgentError("Only standalone local clones can be adopted.");
|
|
197
|
+
mkdirSync(dirname(to), { recursive: true });
|
|
198
|
+
const rel = relative(realpathSync(this.dir), realpathSync(dirname(to)));
|
|
199
|
+
if (rel.startsWith("..") || isAbsolute(rel))
|
|
200
|
+
throw new AgentError("Repository owner directory escapes the shelf.");
|
|
201
|
+
renameSync(from, to);
|
|
202
|
+
this.dirOf(repo);
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
|
|
182
206
|
/** Every clone as `<owner>/<repo>`, sorted. */
|
|
183
207
|
repos(): string[] {
|
|
184
208
|
if (!existsSync(this.dir)) return [];
|
package/src/worker-entry.ts
CHANGED
|
@@ -19,6 +19,7 @@ interface StartMessage {
|
|
|
19
19
|
job: CodingJob & { dir: string };
|
|
20
20
|
packages: string[];
|
|
21
21
|
agentDir: string;
|
|
22
|
+
prompt?: string;
|
|
22
23
|
}
|
|
23
24
|
export function codingWorkerPrompt(dir: string): string {
|
|
24
25
|
return [
|
|
@@ -33,7 +34,12 @@ export function codingWorkerPrompt(dir: string): string {
|
|
|
33
34
|
const answers = new Map<number, (answer: HeldCallAnswer) => void>();
|
|
34
35
|
let nextId = 1;
|
|
35
36
|
let started = false;
|
|
36
|
-
async function run({
|
|
37
|
+
async function run({
|
|
38
|
+
job,
|
|
39
|
+
agentDir,
|
|
40
|
+
packages,
|
|
41
|
+
prompt,
|
|
42
|
+
}: StartMessage): Promise<string> {
|
|
37
43
|
const modelRuntime = await ModelRuntime.create({
|
|
38
44
|
authPath: join(agentDir, "auth.json"),
|
|
39
45
|
modelsPath: join(agentDir, "models.json"),
|
|
@@ -89,7 +95,7 @@ async function run({ job, agentDir, packages }: StartMessage): Promise<string> {
|
|
|
89
95
|
},
|
|
90
96
|
},
|
|
91
97
|
],
|
|
92
|
-
appendSystemPrompt: [codingWorkerPrompt(job.dir)],
|
|
98
|
+
appendSystemPrompt: [prompt ?? codingWorkerPrompt(job.dir)],
|
|
93
99
|
});
|
|
94
100
|
await loader.reload();
|
|
95
101
|
const { session } = await createAgentSession({
|