pi-roundtable-coding 0.7.4 → 0.7.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -0
- package/README.md +5 -2
- package/package.json +2 -2
- package/src/coding-desk.ts +28 -4
- package/src/coding-plugin.ts +24 -1
- package/src/index.ts +1 -0
- package/src/pi-coding-worker.ts +29 -3
- package/src/repo-shelf.ts +32 -22
- package/src/worker-entry.ts +18 -5
- package/src/worker-failure.ts +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.7.5] - 2026-10-03
|
|
6
|
+
|
|
7
|
+
- Add `workerBlockText`, `limits` and `diagnosticChars`; git, gh and worker error text and the report and held-action bounds can be raised or lifted by the host.
|
|
8
|
+
|
|
5
9
|
## [0.7.4] - 2026-10-03
|
|
6
10
|
|
|
7
11
|
- Add `toolText` and `presentation.list`, accept a 7–40 character sha prefix in `repo_push`, name git and gh stderr (scrubbed) in failures, carry a worker's own error text into the report, restore the earlier timeout, stop and refusal wording, stop listing declined calls as held, and report the channels the service works for.
|
package/README.md
CHANGED
|
@@ -121,8 +121,8 @@ Without `threads`, the resolved report channel's prompts are used.
|
|
|
121
121
|
`timeoutMs` must be positive and finite, and at most 2,147,483,647 ms to fit the host timer.
|
|
122
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.
|
|
123
123
|
Unapproved actions appear in the report rather than running later automatically.
|
|
124
|
-
|
|
125
|
-
|
|
124
|
+
By default worker answers are capped at 20,000 characters and the Held list keeps ten entries of at most 1,000 characters each, with explicit truncation and omission notices; `limits` changes each bound.
|
|
125
|
+
A worker failure carries its exit category and numeric code; git and gh stderr and the worker's own error text reach the report only after `scrubDiagnostic` masks credentials, and are cut at `diagnosticChars`.
|
|
126
126
|
Card expiration is determined by the host's surface implementation, not this package.
|
|
127
127
|
|
|
128
128
|
The worker runs in a fresh Bun child process with an unsaved Pi session and uses the public `runWorkerTask` helper.
|
|
@@ -172,6 +172,9 @@ The `CODING` service exposes `shelf: RepoShelf` and `desk: CodingDesk` for trust
|
|
|
172
172
|
| `threadText` | English package text | Thread introduction, held-action notice, approval title and final report. |
|
|
173
173
|
| `workerWorkspace` | individual clone | Trusted shell-policy write boundary; not an OS sandbox. |
|
|
174
174
|
| `workerPrompt` | generic worker instructions | Trusted standing prompt replacement; it cannot bypass approval or cleanup. |
|
|
175
|
+
| `workerBlockText` | "The owner declined / has not approved this call. Do not retry it or work around it; list it under Held in your report." | Trusted wording of what the worker reads when a call is declined or held, from `(answer, action)`; it never changes who is held, and the desk lists only unanswered calls as held. |
|
|
176
|
+
| `limits` | 20,000 report characters, 10 held entries of 1,000 characters | `{ reportChars?, heldEntries?, heldChars? }`: what a report keeps of a long run; each is a whole number of at least 1, or `Infinity` for no bound. |
|
|
177
|
+
| `diagnosticChars` | `600` | Longest git, gh and worker error text kept in a failure; a whole number of at least 1, or `Infinity`. |
|
|
175
178
|
| `skipUnavailableCarriedSkills` | `false` | Skip and disclose unavailable implicit skills; explicit skill requests still refuse them. |
|
|
176
179
|
| `workerPackages` | `[]` | Absolute installed extension paths. |
|
|
177
180
|
| `agentDir` | Pi host directory | Pi credentials and model configuration. |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-roundtable-coding",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.6",
|
|
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.6",
|
|
48
48
|
"typescript": "7.0.2"
|
|
49
49
|
}
|
|
50
50
|
}
|
package/src/coding-desk.ts
CHANGED
|
@@ -64,6 +64,26 @@ export interface CodingDeskOptions {
|
|
|
64
64
|
deliver(result: CodingResult): Promise<void>;
|
|
65
65
|
logger: Logger;
|
|
66
66
|
timeoutMs?: number;
|
|
67
|
+
limits?: CodingLimits;
|
|
68
|
+
}
|
|
69
|
+
/** What a coding report keeps of a long run; each is a count of characters or entries, or `Infinity` for no bound. */
|
|
70
|
+
export interface CodingLimits {
|
|
71
|
+
/** Longest worker report, and longest thread report; default 20,000 characters. */
|
|
72
|
+
reportChars?: number;
|
|
73
|
+
/** Held actions listed in the report, the rest counted; default 10. */
|
|
74
|
+
heldEntries?: number;
|
|
75
|
+
/** Longest held-action entry; default 1,000 characters. */
|
|
76
|
+
heldChars?: number;
|
|
77
|
+
}
|
|
78
|
+
export function checkLimit(name: string, value: number | undefined): void {
|
|
79
|
+
if (
|
|
80
|
+
value !== undefined &&
|
|
81
|
+
value !== Number.POSITIVE_INFINITY &&
|
|
82
|
+
(!Number.isSafeInteger(value) || value < 1)
|
|
83
|
+
)
|
|
84
|
+
throw new Error(
|
|
85
|
+
`${name} must be a whole number of at least 1, or Infinity; got ${value}.`,
|
|
86
|
+
);
|
|
67
87
|
}
|
|
68
88
|
export const MAX_CODING_TASK_CHARS = 8_000;
|
|
69
89
|
const MAX_REPORT_CHARS = 20_000;
|
|
@@ -182,6 +202,10 @@ export class CodingDesk {
|
|
|
182
202
|
const slot = promptSlot();
|
|
183
203
|
const held: string[] = [];
|
|
184
204
|
let omittedHeld = 0;
|
|
205
|
+
const maxReport = this.#options.limits?.reportChars ?? MAX_REPORT_CHARS;
|
|
206
|
+
const maxHeldEntries =
|
|
207
|
+
this.#options.limits?.heldEntries ?? MAX_HELD_ENTRIES;
|
|
208
|
+
const maxHeldChars = this.#options.limits?.heldChars ?? MAX_HELD_CHARS;
|
|
185
209
|
let cancel = () => {};
|
|
186
210
|
let timedOut = false;
|
|
187
211
|
let outcome: CodingResult["outcome"];
|
|
@@ -232,10 +256,10 @@ export class CodingDesk {
|
|
|
232
256
|
}
|
|
233
257
|
// A call the owner declined is settled, not held for the report.
|
|
234
258
|
if (answer === "held") {
|
|
235
|
-
if (held.length <
|
|
259
|
+
if (held.length < maxHeldEntries) {
|
|
236
260
|
const entry = bounded(
|
|
237
261
|
`${call.action}: ${call.tool} ${call.input}`,
|
|
238
|
-
|
|
262
|
+
maxHeldChars,
|
|
239
263
|
);
|
|
240
264
|
held.push(entry);
|
|
241
265
|
try {
|
|
@@ -256,7 +280,7 @@ export class CodingDesk {
|
|
|
256
280
|
);
|
|
257
281
|
if (controller.signal.aborted)
|
|
258
282
|
throw new AgentError("the worker was stopped");
|
|
259
|
-
outcome = { ok: true, report: bounded(report,
|
|
283
|
+
outcome = { ok: true, report: bounded(report, maxReport) };
|
|
260
284
|
} catch (error) {
|
|
261
285
|
// The host's own refusals and the worker's scrubbed, bounded error text reach the report.
|
|
262
286
|
let message =
|
|
@@ -302,7 +326,7 @@ export class CodingDesk {
|
|
|
302
326
|
bounded(
|
|
303
327
|
threadText?.report?.(result) ??
|
|
304
328
|
codingReport(result, job.startedAt.toISOString()),
|
|
305
|
-
|
|
329
|
+
maxReport,
|
|
306
330
|
),
|
|
307
331
|
);
|
|
308
332
|
} catch {
|
package/src/coding-plugin.ts
CHANGED
|
@@ -21,9 +21,11 @@ import { Type } from "typebox";
|
|
|
21
21
|
import {
|
|
22
22
|
CodingDesk,
|
|
23
23
|
type CodingJob,
|
|
24
|
+
type CodingLimits,
|
|
24
25
|
type CodingResult,
|
|
25
26
|
type CodingThreadText,
|
|
26
27
|
type CodingWorker,
|
|
28
|
+
checkLimit,
|
|
27
29
|
codingReport,
|
|
28
30
|
MAX_CODING_TASK_CHARS,
|
|
29
31
|
} from "./coding-desk.ts";
|
|
@@ -97,6 +99,12 @@ export interface CodingOptions {
|
|
|
97
99
|
threadText?: CodingThreadText;
|
|
98
100
|
workerWorkspace?: string;
|
|
99
101
|
workerPrompt?: (dir: string) => string;
|
|
102
|
+
/** How much of a long run a report keeps; see `CodingLimits`. */
|
|
103
|
+
limits?: CodingLimits;
|
|
104
|
+
/** Longest git, gh and worker error text kept in a failure, in characters; default 600. */
|
|
105
|
+
diagnosticChars?: number;
|
|
106
|
+
/** Trusted wording of what the worker reads when a call is declined or held; see `PiCodingWorkerOptions.blockText`. */
|
|
107
|
+
workerBlockText?: (answer: "declined" | "held", action: string) => string;
|
|
100
108
|
/** Opt in to skipping unavailable implicit skills; explicit requests always fail closed. */
|
|
101
109
|
skipUnavailableCarriedSkills?: boolean;
|
|
102
110
|
/** Absolute installed extension package paths. Default: none. */
|
|
@@ -126,6 +134,16 @@ export function coding(options: CodingOptions) {
|
|
|
126
134
|
if (!options.shelfDir.trim()) throw new PluginError("shelfDir is required.");
|
|
127
135
|
if (!/^[^/\s]+\/\S+$/.test(options.model))
|
|
128
136
|
throw new PluginError("model must be provider/model-id.");
|
|
137
|
+
try {
|
|
138
|
+
checkLimit("limits.reportChars", options.limits?.reportChars);
|
|
139
|
+
checkLimit("limits.heldEntries", options.limits?.heldEntries);
|
|
140
|
+
checkLimit("limits.heldChars", options.limits?.heldChars);
|
|
141
|
+
checkLimit("diagnosticChars", options.diagnosticChars);
|
|
142
|
+
} catch (error) {
|
|
143
|
+
throw new PluginError(
|
|
144
|
+
error instanceof Error ? error.message : String(error),
|
|
145
|
+
);
|
|
146
|
+
}
|
|
129
147
|
const ownerRepos = new Set(options.ownerRepos ?? []);
|
|
130
148
|
for (const repo of ownerRepos) checkRepoName(repo);
|
|
131
149
|
const ownerOwned = (repo: string): boolean => {
|
|
@@ -136,7 +154,9 @@ export function coding(options: CodingOptions) {
|
|
|
136
154
|
name: "coding",
|
|
137
155
|
provides: [CODING],
|
|
138
156
|
setup(context) {
|
|
139
|
-
const shelf = new RepoShelf(options.shelfDir, options.clone
|
|
157
|
+
const shelf = new RepoShelf(options.shelfDir, options.clone, {
|
|
158
|
+
diagnosticChars: options.diagnosticChars,
|
|
159
|
+
});
|
|
140
160
|
for (const adoption of options.adoptClones ?? [])
|
|
141
161
|
shelf.adopt(adoption.from, adoption.repo);
|
|
142
162
|
const skills = context.services.find(SKILLS);
|
|
@@ -147,6 +167,8 @@ export function coding(options: CodingOptions) {
|
|
|
147
167
|
agentDir: options.agentDir,
|
|
148
168
|
workspace: options.workerWorkspace,
|
|
149
169
|
prompt: options.workerPrompt,
|
|
170
|
+
blockText: options.workerBlockText,
|
|
171
|
+
diagnosticChars: options.diagnosticChars,
|
|
150
172
|
holds: (tool, input, scope) =>
|
|
151
173
|
options.holds?.(tool, input, scope) ??
|
|
152
174
|
context.sessions().holds(tool, input, scope),
|
|
@@ -155,6 +177,7 @@ export function coding(options: CodingOptions) {
|
|
|
155
177
|
shelf,
|
|
156
178
|
worker,
|
|
157
179
|
timeoutMs: options.timeoutMs,
|
|
180
|
+
limits: options.limits,
|
|
158
181
|
logger: context.logger,
|
|
159
182
|
threads: options.threads,
|
|
160
183
|
threadText: options.threadText,
|
package/src/index.ts
CHANGED
package/src/pi-coding-worker.ts
CHANGED
|
@@ -16,6 +16,13 @@ export interface PiCodingWorkerOptions {
|
|
|
16
16
|
workspace?: string;
|
|
17
17
|
/** Trusted host standing prompt, replacing the generic worker instructions. */
|
|
18
18
|
prompt?: (dir: string) => string;
|
|
19
|
+
/**
|
|
20
|
+
* Trusted wording of what the worker reads when a call is not approved, given what the call
|
|
21
|
+
* would do. Default: the owner declined or has not approved it; list it under Held in the report.
|
|
22
|
+
*/
|
|
23
|
+
blockText?: (answer: "declined" | "held", action: string) => string;
|
|
24
|
+
/** Longest worker error text kept for the report, in characters; default 600. */
|
|
25
|
+
diagnosticChars?: number;
|
|
19
26
|
}
|
|
20
27
|
interface WorkerMessage {
|
|
21
28
|
type: string;
|
|
@@ -82,7 +89,12 @@ export class PiCodingWorker implements CodingWorker {
|
|
|
82
89
|
value.type === "failure" &&
|
|
83
90
|
typeof value.message === "string"
|
|
84
91
|
) {
|
|
85
|
-
failure = value.message.slice(
|
|
92
|
+
failure = value.message.slice(
|
|
93
|
+
0,
|
|
94
|
+
this.#options.diagnosticChars === undefined
|
|
95
|
+
? 2_000
|
|
96
|
+
: this.#options.diagnosticChars * 4,
|
|
97
|
+
);
|
|
86
98
|
} else if (
|
|
87
99
|
value.type === "call" &&
|
|
88
100
|
Number.isSafeInteger(value.id) &&
|
|
@@ -93,6 +105,7 @@ export class PiCodingWorker implements CodingWorker {
|
|
|
93
105
|
const { id, tool, input } = value;
|
|
94
106
|
void (async () => {
|
|
95
107
|
let answer: HeldCallAnswer = "held";
|
|
108
|
+
let reason: string | undefined;
|
|
96
109
|
try {
|
|
97
110
|
const context = {
|
|
98
111
|
workspace: this.#options.workspace ?? job.dir,
|
|
@@ -108,12 +121,19 @@ export class PiCodingWorker implements CodingWorker {
|
|
|
108
121
|
action,
|
|
109
122
|
})
|
|
110
123
|
: "approved";
|
|
124
|
+
if (action && answer !== "approved")
|
|
125
|
+
reason = this.#options.blockText?.(answer, action);
|
|
111
126
|
}
|
|
112
127
|
} catch {
|
|
113
128
|
/* Fail closed. */
|
|
114
129
|
}
|
|
115
130
|
if (!signal.aborted && child.exitCode === null)
|
|
116
|
-
proc.send({
|
|
131
|
+
proc.send({
|
|
132
|
+
type: "answer",
|
|
133
|
+
id,
|
|
134
|
+
answer,
|
|
135
|
+
...(reason ? { reason } : {}),
|
|
136
|
+
});
|
|
117
137
|
})();
|
|
118
138
|
}
|
|
119
139
|
},
|
|
@@ -131,7 +151,13 @@ export class PiCodingWorker implements CodingWorker {
|
|
|
131
151
|
if (signal.aborted) kill();
|
|
132
152
|
const code = await child.exited;
|
|
133
153
|
if (signal.aborted) throw new CodingWorkerFailure("stopped");
|
|
134
|
-
if (code !== 0)
|
|
154
|
+
if (code !== 0)
|
|
155
|
+
throw new CodingWorkerFailure(
|
|
156
|
+
"exit",
|
|
157
|
+
code,
|
|
158
|
+
failure,
|
|
159
|
+
this.#options.diagnosticChars,
|
|
160
|
+
);
|
|
135
161
|
if (!report?.trim()) throw new CodingWorkerFailure("missing-report");
|
|
136
162
|
return report;
|
|
137
163
|
} finally {
|
package/src/repo-shelf.ts
CHANGED
|
@@ -87,25 +87,28 @@ async function run(
|
|
|
87
87
|
return { out: out.trimEnd(), err: err.trim(), code };
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
/** Clones with gh, which authenticates with the host's GH_TOKEN
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
}
|
|
90
|
+
/** Clones with gh, which authenticates with the host's GH_TOKEN; a failure's stderr is cut at `diagnosticChars`. */
|
|
91
|
+
const ghCloneWith =
|
|
92
|
+
(diagnosticChars?: number): CloneCommand =>
|
|
93
|
+
async (repo, dir) => {
|
|
94
|
+
checkRepoName(repo);
|
|
95
|
+
if (repo.startsWith("-"))
|
|
96
|
+
throw new AgentError("Repository owner cannot start with a dash.");
|
|
97
|
+
const { err, code } = await run([
|
|
98
|
+
"gh",
|
|
99
|
+
"repo",
|
|
100
|
+
"clone",
|
|
101
|
+
repo,
|
|
102
|
+
dir,
|
|
103
|
+
"--",
|
|
104
|
+
"--quiet",
|
|
105
|
+
]);
|
|
106
|
+
if (code !== 0)
|
|
107
|
+
throw new AgentError(
|
|
108
|
+
`Cloning ${repo} failed: ${scrubDiagnostic(err, diagnosticChars) || `exit ${code}`}`,
|
|
109
|
+
);
|
|
110
|
+
};
|
|
111
|
+
export const ghClone: CloneCommand = ghCloneWith();
|
|
109
112
|
|
|
110
113
|
const MAX_METADATA_BYTES = 65_536;
|
|
111
114
|
|
|
@@ -175,9 +178,16 @@ export class RepoShelf {
|
|
|
175
178
|
readonly #reports = new Map<string, ChangeReport>();
|
|
176
179
|
readonly #adding = new Set<string>();
|
|
177
180
|
|
|
178
|
-
|
|
181
|
+
readonly #diagnosticChars: number | undefined;
|
|
182
|
+
|
|
183
|
+
constructor(
|
|
184
|
+
dir: string,
|
|
185
|
+
clone?: CloneCommand,
|
|
186
|
+
options: { diagnosticChars?: number | undefined } = {},
|
|
187
|
+
) {
|
|
179
188
|
this.dir = resolve(dir);
|
|
180
|
-
this.#
|
|
189
|
+
this.#diagnosticChars = options.diagnosticChars;
|
|
190
|
+
this.#clone = clone ?? ghCloneWith(options.diagnosticChars);
|
|
181
191
|
}
|
|
182
192
|
|
|
183
193
|
/** Adopt an existing standalone clone at startup; an existing destination is never replaced. */
|
|
@@ -483,7 +493,7 @@ export class RepoShelf {
|
|
|
483
493
|
const { out, err, code } = await run(["git", "-C", dir, ...args]);
|
|
484
494
|
if (code !== 0)
|
|
485
495
|
throw new AgentError(
|
|
486
|
-
`git ${args[0]} failed in ${dir}: ${scrubDiagnostic(err) || `exit ${code}`}`,
|
|
496
|
+
`git ${args[0]} failed in ${dir}: ${scrubDiagnostic(err, this.#diagnosticChars) || `exit ${code}`}`,
|
|
487
497
|
);
|
|
488
498
|
return out;
|
|
489
499
|
}
|
package/src/worker-entry.ts
CHANGED
|
@@ -31,7 +31,10 @@ export function codingWorkerPrompt(dir: string): string {
|
|
|
31
31
|
"Report changes and reasons, commits, checks and outcomes, held actions, and remaining work.",
|
|
32
32
|
].join("\n\n");
|
|
33
33
|
}
|
|
34
|
-
const answers = new Map<
|
|
34
|
+
const answers = new Map<
|
|
35
|
+
number,
|
|
36
|
+
(answer: HeldCallAnswer, reason?: string) => void
|
|
37
|
+
>();
|
|
35
38
|
let nextId = 1;
|
|
36
39
|
let started = false;
|
|
37
40
|
async function run({
|
|
@@ -76,8 +79,11 @@ async function run({
|
|
|
76
79
|
factory: (pi) => {
|
|
77
80
|
pi.on("tool_call", async (event) => {
|
|
78
81
|
const id = nextId++;
|
|
79
|
-
const answer = await new Promise<
|
|
80
|
-
|
|
82
|
+
const { answer, reason } = await new Promise<{
|
|
83
|
+
answer: HeldCallAnswer;
|
|
84
|
+
reason?: string | undefined;
|
|
85
|
+
}>((resolve) => {
|
|
86
|
+
answers.set(id, (answer, reason) => resolve({ answer, reason }));
|
|
81
87
|
process.send?.({
|
|
82
88
|
type: "call",
|
|
83
89
|
id,
|
|
@@ -88,7 +94,9 @@ async function run({
|
|
|
88
94
|
if (answer !== "approved")
|
|
89
95
|
return {
|
|
90
96
|
block: true,
|
|
91
|
-
reason:
|
|
97
|
+
reason:
|
|
98
|
+
reason ??
|
|
99
|
+
`The owner ${answer === "declined" ? "declined" : "has not approved"} this call. Do not retry it or work around it; list it under Held in your report.`,
|
|
92
100
|
};
|
|
93
101
|
return undefined;
|
|
94
102
|
});
|
|
@@ -128,7 +136,12 @@ if (process.send) {
|
|
|
128
136
|
message.answer === "declined" ||
|
|
129
137
|
message.answer === "held")
|
|
130
138
|
) {
|
|
131
|
-
answers.get(message.id)?.(
|
|
139
|
+
answers.get(message.id)?.(
|
|
140
|
+
message.answer,
|
|
141
|
+
"reason" in message && typeof message.reason === "string"
|
|
142
|
+
? message.reason
|
|
143
|
+
: undefined,
|
|
144
|
+
);
|
|
132
145
|
answers.delete(message.id);
|
|
133
146
|
} else if (message.type === "start" && !started) {
|
|
134
147
|
started = true;
|
package/src/worker-failure.ts
CHANGED
|
@@ -6,8 +6,9 @@ export class CodingWorkerFailure extends AgentError {
|
|
|
6
6
|
readonly category: "stopped" | "exit" | "missing-report",
|
|
7
7
|
readonly exitCode?: number,
|
|
8
8
|
detail?: string,
|
|
9
|
+
diagnosticChars?: number,
|
|
9
10
|
) {
|
|
10
|
-
const reason = detail ? scrubDiagnostic(detail) : "";
|
|
11
|
+
const reason = detail ? scrubDiagnostic(detail, diagnosticChars) : "";
|
|
11
12
|
let message = "the worker was stopped";
|
|
12
13
|
if (category === "exit")
|
|
13
14
|
message =
|