@rmrdeveloper/sideroom-pi 5.0.0

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.
@@ -0,0 +1,1175 @@
1
+ // src/core/catalog.ts
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ var agents = /* @__PURE__ */ new Map();
6
+ var guidelines = /* @__PURE__ */ new Map();
7
+ var skills = /* @__PURE__ */ new Map();
8
+ var sharedGuidelines;
9
+ function getPackageRoot() {
10
+ let directory = path.dirname(fileURLToPath(import.meta.url));
11
+ while (!existsSync(path.join(directory, "package.json"))) {
12
+ const parent = path.dirname(directory);
13
+ if (parent === directory) throw new Error("cannot locate Sideroom assets");
14
+ directory = parent;
15
+ }
16
+ return directory;
17
+ }
18
+ function assetsDir() {
19
+ return path.join(getPackageRoot(), "src", "assets");
20
+ }
21
+ function getAgentDefinition(id) {
22
+ const cached = agents.get(id);
23
+ if (cached !== void 0) return cached;
24
+ const file = path.join(assetsDir(), "agents", `${id}.md`);
25
+ const definition = parseAgentDefinition(id, readFileSync(file, "utf8"), file);
26
+ agents.set(id, definition);
27
+ return definition;
28
+ }
29
+ function getLanguageGuidelines(language) {
30
+ const cached = guidelines.get(language);
31
+ if (cached !== void 0) return cached;
32
+ const file = path.join(
33
+ assetsDir(),
34
+ "artifacts",
35
+ "guidelines",
36
+ `${language}.md`
37
+ );
38
+ const source = readFileSync(file, "utf8").trim();
39
+ if (source.length === 0)
40
+ throw new Error(`coding guidelines are empty: ${language}`);
41
+ guidelines.set(language, source);
42
+ return source;
43
+ }
44
+ function getSharedGuidelines() {
45
+ if (sharedGuidelines !== void 0) return sharedGuidelines;
46
+ const file = path.join(assetsDir(), "artifacts", "GUIDELINES_TEMPLATE.md");
47
+ const source = readFileSync(file, "utf8").trim();
48
+ if (source.length === 0) throw new Error("guidelines template is empty");
49
+ sharedGuidelines = source;
50
+ return source;
51
+ }
52
+ function getPackagedSkill(skill) {
53
+ const cached = skills.get(skill);
54
+ if (cached !== void 0) return cached;
55
+ const file = path.join(getPackageRoot(), "skills", skill, "SKILL.md");
56
+ const source = readFileSync(file, "utf8").trim();
57
+ if (source.length === 0) throw new Error(`packaged skill is empty: ${skill}`);
58
+ skills.set(skill, source);
59
+ return source;
60
+ }
61
+ function buildSystemPrompt(id, language) {
62
+ return [
63
+ getAgentDefinition(id).instructions,
64
+ "## Shared engineering policy",
65
+ getSharedGuidelines(),
66
+ "## Language policy",
67
+ getLanguageGuidelines(language),
68
+ "The language policy overrides conflicting language-specific examples or commands above."
69
+ ].join("\n\n");
70
+ }
71
+ function parseAgentDefinition(id, source, file) {
72
+ const match = /^---\n([\s\S]*?)\n---\n?([\s\S]*)$/.exec(
73
+ source.replaceAll("\r\n", "\n")
74
+ );
75
+ if (match === null)
76
+ throw new Error(`agent definition must start with frontmatter: ${file}`);
77
+ const [, header, body] = match;
78
+ if (header === void 0 || body === void 0)
79
+ throw new Error(`invalid agent definition: ${file}`);
80
+ const metadata = /* @__PURE__ */ new Map();
81
+ for (const line of header.split("\n")) {
82
+ const separator = line.indexOf(":");
83
+ if (separator > 0)
84
+ metadata.set(
85
+ line.slice(0, separator).trim(),
86
+ line.slice(separator + 1).trim()
87
+ );
88
+ }
89
+ if (metadata.get("name") !== id)
90
+ throw new Error(`agent name mismatch in ${file}`);
91
+ const description = metadata.get("description");
92
+ if (description === void 0 || description.length === 0) {
93
+ throw new Error(`agent description is missing in ${file}`);
94
+ }
95
+ const instructions = body.trim();
96
+ if (instructions.length === 0)
97
+ throw new Error(`agent instructions are empty in ${file}`);
98
+ return {
99
+ id,
100
+ description,
101
+ readonly: metadata.get("readonly") === "true",
102
+ instructions
103
+ };
104
+ }
105
+
106
+ // src/core/findings.ts
107
+ var FINDING = /^FINDING (\d+) \| (Critical|High|Medium|Low) \| ([^|]+) \| ([^|]+) \| (.+)$/;
108
+ var FINDING_SEVERITIES = ["Critical", "High", "Medium", "Low"];
109
+ function parseFindings(source, stage = "finding report") {
110
+ const trimmed = source.trim();
111
+ if (trimmed.length === 0 || trimmed === "No findings") return [];
112
+ const parsed = trimmed.split("\n").map((line, index) => {
113
+ const match = FINDING.exec(line.trim());
114
+ if (match === null) {
115
+ throw new Error(`${stage} finding ${String(index + 1)} is malformed`);
116
+ }
117
+ const [, number, severity, fileLine, rule, description] = match;
118
+ if (number === void 0 || severity === void 0 || fileLine === void 0 || rule === void 0 || description === void 0) {
119
+ throw new Error(`${stage} finding ${String(index + 1)} is malformed`);
120
+ }
121
+ return {
122
+ number: Number(number),
123
+ severity,
124
+ fileLine: fileLine.trim(),
125
+ rule: rule.trim(),
126
+ description: description.trim()
127
+ };
128
+ });
129
+ return validateFindings(parsed, stage);
130
+ }
131
+ function validateFindings(value, stage) {
132
+ if (!Array.isArray(value)) {
133
+ throw new Error(`${stage} findings must be an array`);
134
+ }
135
+ return value.map((finding, index) => validateFinding(finding, index, stage));
136
+ }
137
+ function validateFinding(value, index, stage) {
138
+ const number = index + 1;
139
+ const record = recordOf(value, `${stage} finding ${String(number)}`);
140
+ if (record.number !== number) {
141
+ throw new Error(
142
+ `${stage} finding ${String(number)} field "number" must be ${String(number)}`
143
+ );
144
+ }
145
+ if (!isFindingSeverity(record.severity)) {
146
+ throw new Error(
147
+ `${stage} finding ${String(number)} field "severity" is invalid`
148
+ );
149
+ }
150
+ const fileLine = textField(record.fileLine, "fileLine", number, stage);
151
+ const rule = textField(record.rule, "rule", number, stage);
152
+ const description = textField(
153
+ record.description,
154
+ "description",
155
+ number,
156
+ stage
157
+ );
158
+ return { number, severity: record.severity, fileLine, rule, description };
159
+ }
160
+ function recordOf(value, label) {
161
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
162
+ throw new Error(`${label} must be an object`);
163
+ }
164
+ return value;
165
+ }
166
+ function textField(value, field, number, stage) {
167
+ if (typeof value !== "string" || value.trim().length === 0) {
168
+ throw new Error(
169
+ `${stage} finding ${String(number)} field "${field}" must be non-empty text`
170
+ );
171
+ }
172
+ return value.trim();
173
+ }
174
+ function isFindingSeverity(value) {
175
+ return typeof value === "string" && FINDING_SEVERITIES.includes(value);
176
+ }
177
+
178
+ // src/core/types.ts
179
+ var LANGUAGES = [
180
+ "typescript",
181
+ "javascript",
182
+ "php-laravel",
183
+ "python",
184
+ "java"
185
+ ];
186
+ function hasBlockingFindings(findings) {
187
+ return findings.some((finding) => finding.severity !== "Low");
188
+ }
189
+
190
+ // src/core/agents.ts
191
+ function createPlanner(model) {
192
+ return {
193
+ id: "sideroom-planner",
194
+ async run(input) {
195
+ requireText(input.request, "planner needs a request");
196
+ const value = await model.generate({
197
+ agent: "sideroom-planner",
198
+ input,
199
+ schema: "{ summary: string, tasks: [{ id: string, title: string }], verification: string[] }"
200
+ });
201
+ return parsePlan(value);
202
+ }
203
+ };
204
+ }
205
+ function createImplementer(model) {
206
+ return {
207
+ id: "sideroom-implementer",
208
+ async run(input) {
209
+ requireText(input.task.id, "implementer needs a task id");
210
+ const value = await model.generate({
211
+ agent: "sideroom-implementer",
212
+ input,
213
+ schema: "{ filesChanged: string[], summary: string }"
214
+ });
215
+ const record = recordOf2(
216
+ value,
217
+ "implementer returned a non-object response"
218
+ );
219
+ if (!Array.isArray(record.filesChanged) || !record.filesChanged.every(isText)) {
220
+ throw new Error("implementer returned malformed filesChanged");
221
+ }
222
+ if (!isText(record.summary))
223
+ throw new Error("implementer returned no summary");
224
+ return {
225
+ taskId: input.task.id,
226
+ filesChanged: record.filesChanged,
227
+ summary: record.summary
228
+ };
229
+ }
230
+ };
231
+ }
232
+ function createReviewer(model) {
233
+ return {
234
+ id: "sideroom-code-reviewer",
235
+ async run(input) {
236
+ const value = await model.generate({
237
+ agent: "sideroom-code-reviewer",
238
+ input,
239
+ schema: 'a JSON string containing FINDING lines, or the JSON string "No findings"'
240
+ });
241
+ if (!isText(value))
242
+ throw new Error("reviewer returned a non-string response");
243
+ return parseFindings(value, "reviewer");
244
+ }
245
+ };
246
+ }
247
+ function createVerifier(model) {
248
+ return {
249
+ id: "sideroom-verifier",
250
+ async run(input) {
251
+ const value = await model.generate({
252
+ agent: "sideroom-verifier",
253
+ input,
254
+ schema: "{ findings: string, summary: string }"
255
+ });
256
+ const record = recordOf2(value, "verifier output must be an object");
257
+ const findings = findingReport(record.findings, "verifier");
258
+ requireText(
259
+ record.summary,
260
+ "verifier output.summary must be non-empty text"
261
+ );
262
+ return findings;
263
+ }
264
+ };
265
+ }
266
+ function createFixer(model) {
267
+ return {
268
+ id: "sideroom-fixer",
269
+ async run(input) {
270
+ if (!hasBlockingFindings(input.findings))
271
+ return "No blocking findings to fix.";
272
+ const value = await model.generate({
273
+ agent: "sideroom-fixer",
274
+ input,
275
+ schema: "{ summary: string }"
276
+ });
277
+ const record = recordOf2(value, "fixer returned a non-object response");
278
+ if (!isText(record.summary)) throw new Error("fixer returned no summary");
279
+ return record.summary;
280
+ }
281
+ };
282
+ }
283
+ function parsePlan(value) {
284
+ const record = recordOf2(value, "planner returned a non-object response");
285
+ if (!isText(record.summary)) throw new Error("planner returned no summary");
286
+ if (!Array.isArray(record.tasks) || !record.tasks.every(isPlanTask)) {
287
+ throw new Error("planner returned malformed tasks");
288
+ }
289
+ if (!Array.isArray(record.verification) || !record.verification.every(isText)) {
290
+ throw new Error("planner returned malformed verification commands");
291
+ }
292
+ if (record.tasks.length === 0) throw new Error("planner returned no tasks");
293
+ return {
294
+ summary: record.summary,
295
+ tasks: record.tasks,
296
+ verification: record.verification
297
+ };
298
+ }
299
+ function isPlanTask(value) {
300
+ if (typeof value !== "object" || value === null) return false;
301
+ const record = value;
302
+ return isText(record.id) && isText(record.title) && (record.description === void 0 || isText(record.description)) && (record.files === void 0 || Array.isArray(record.files) && record.files.every(isText)) && (record.acceptance === void 0 || Array.isArray(record.acceptance) && record.acceptance.every(isText));
303
+ }
304
+ function recordOf2(value, message) {
305
+ if (typeof value !== "object" || value === null) throw new Error(message);
306
+ return value;
307
+ }
308
+ function isText(value) {
309
+ return typeof value === "string" && value.trim().length > 0;
310
+ }
311
+ function requireText(value, message) {
312
+ if (!isText(value)) throw new Error(message);
313
+ }
314
+ function findingReport(value, stage) {
315
+ if (!isText(value)) {
316
+ throw new Error(`${stage} output.findings must be non-empty text`);
317
+ }
318
+ return parseFindings(value, stage);
319
+ }
320
+
321
+ // src/core/grilling.ts
322
+ function createGriller(model) {
323
+ return {
324
+ async run(input) {
325
+ const value = await model.generate({
326
+ agent: "sideroom-planner",
327
+ skill: "sideroom-grilling",
328
+ input,
329
+ schema: '{ status: "questions", questions: [{ id: string, title: string, question: string, recommendation: string }] } | { status: "settled", summary: string }'
330
+ });
331
+ return parseGrillingResult(value);
332
+ }
333
+ };
334
+ }
335
+ function parseGrillingResult(value) {
336
+ const record = recordOf3(value);
337
+ if (record.status === "settled") {
338
+ if (!isText2(record.summary))
339
+ throw new Error("grilling returned no settled summary");
340
+ return { status: "settled", summary: record.summary };
341
+ }
342
+ if (record.status !== "questions" || !Array.isArray(record.questions)) {
343
+ throw new Error("grilling returned an unknown status");
344
+ }
345
+ if (record.questions.length === 0 || record.questions.length > 3) {
346
+ throw new Error("grilling must ask between one and three questions");
347
+ }
348
+ const questions = record.questions.map(parseQuestion);
349
+ if (new Set(questions.map((question) => question.id)).size !== questions.length) {
350
+ throw new Error("grilling returned duplicate question ids");
351
+ }
352
+ return { status: "questions", questions };
353
+ }
354
+ function parseQuestion(value) {
355
+ const record = recordOf3(value);
356
+ if (!isText2(record.id) || !isText2(record.title) || !isText2(record.question) || !isText2(record.recommendation)) {
357
+ throw new Error("grilling returned a malformed question");
358
+ }
359
+ return {
360
+ id: record.id,
361
+ title: record.title,
362
+ question: record.question,
363
+ recommendation: record.recommendation
364
+ };
365
+ }
366
+ function recordOf3(value) {
367
+ if (typeof value !== "object" || value === null) {
368
+ throw new Error("grilling returned a non-object response");
369
+ }
370
+ return value;
371
+ }
372
+ function isText2(value) {
373
+ return typeof value === "string" && value.trim().length > 0;
374
+ }
375
+
376
+ // src/core/orchestrator.ts
377
+ var SideroomOrchestrator = class {
378
+ options;
379
+ constructor(options) {
380
+ const maxFixPasses = options.maxFixPasses ?? 2;
381
+ if (!Number.isInteger(maxFixPasses) || maxFixPasses < 1) {
382
+ throw new Error("maxFixPasses must be an integer of at least 1");
383
+ }
384
+ const maxGrillingRounds = options.maxGrillingRounds ?? 8;
385
+ if (!Number.isInteger(maxGrillingRounds) || maxGrillingRounds < 1) {
386
+ throw new Error("maxGrillingRounds must be an integer of at least 1");
387
+ }
388
+ this.options = { ...options, maxFixPasses, maxGrillingRounds };
389
+ }
390
+ /** Plan, implement, review, verify, and repair in the caller's project directory. */
391
+ async run(request, answerQuestions2) {
392
+ let implementations = [];
393
+ let plan;
394
+ let findings = [];
395
+ try {
396
+ const settledRequest = await this.settleRequest(request, answerQuestions2);
397
+ this.report({ phase: "planner", status: "started" });
398
+ plan = await this.options.planner.run({ request: settledRequest });
399
+ this.report({ phase: "planner", status: "completed" });
400
+ for (const task of plan.tasks) {
401
+ this.report({
402
+ phase: "implementer",
403
+ status: "started",
404
+ taskId: task.id
405
+ });
406
+ const implementation = await this.options.implementer.run({
407
+ task,
408
+ plan: plan.summary
409
+ });
410
+ this.report({
411
+ phase: "implementer",
412
+ status: "completed",
413
+ taskId: task.id
414
+ });
415
+ implementations = [...implementations, implementation];
416
+ }
417
+ const combined = combineImplementation(implementations);
418
+ for (let pass = 0; pass <= this.options.maxFixPasses; pass += 1) {
419
+ const qualityInput = { implementation: combined, plan: plan.summary };
420
+ this.report({ phase: "reviewer", status: "started" });
421
+ this.report({ phase: "verifier", status: "started" });
422
+ const [review, verification] = await Promise.all([
423
+ this.options.reviewer.run(qualityInput),
424
+ this.options.verifier.run(qualityInput)
425
+ ]);
426
+ this.report({ phase: "reviewer", status: "completed" });
427
+ this.report({ phase: "verifier", status: "completed" });
428
+ findings = validateFindings(
429
+ mergeFindings(
430
+ validateFindings(review, "reviewer stage"),
431
+ validateFindings(verification, "verifier stage")
432
+ ),
433
+ "quality gate"
434
+ );
435
+ if (!hasBlockingFindings(findings)) {
436
+ return {
437
+ status: "completed",
438
+ plan,
439
+ implementations,
440
+ findings,
441
+ summary: "Completed with no blocking findings."
442
+ };
443
+ }
444
+ if (pass === this.options.maxFixPasses) break;
445
+ this.report({ phase: "fixer", status: "started" });
446
+ await this.options.fixer.run({
447
+ implementation: combined,
448
+ findings: validateFindings(findings, "fixer input")
449
+ });
450
+ this.report({ phase: "fixer", status: "completed" });
451
+ }
452
+ return {
453
+ status: "failed",
454
+ plan,
455
+ implementations,
456
+ findings,
457
+ summary: "Blocking findings remain after the configured fixer passes."
458
+ };
459
+ } catch (error) {
460
+ this.report({ phase: "pipeline", status: "failed" });
461
+ return {
462
+ status: "failed",
463
+ ...plan === void 0 ? {} : { plan },
464
+ implementations,
465
+ findings,
466
+ summary: error instanceof Error ? error.message : String(error)
467
+ };
468
+ }
469
+ }
470
+ /** Collect user-owned design decisions without writing a project-local run file. */
471
+ async settleRequest(request, answerQuestions2) {
472
+ const griller = this.options.griller;
473
+ if (griller === void 0) return request;
474
+ let answers = [];
475
+ for (let round = 0; round < this.options.maxGrillingRounds; round += 1) {
476
+ const roundNumber = round + 1;
477
+ this.report({
478
+ phase: "grilling",
479
+ status: "started",
480
+ round: roundNumber
481
+ });
482
+ const result = await griller.run({ request, answers });
483
+ if (result.status === "settled") {
484
+ this.report({
485
+ phase: "grilling",
486
+ status: "completed",
487
+ round: roundNumber
488
+ });
489
+ return `${request}
490
+
491
+ ## Settled understanding
492
+ ${result.summary}`;
493
+ }
494
+ this.report({
495
+ phase: "grilling",
496
+ status: "awaiting-input",
497
+ round: roundNumber
498
+ });
499
+ if (answerQuestions2 === void 0) {
500
+ throw new Error(
501
+ "Grilling needs user decisions; run sideroom in an interactive terminal."
502
+ );
503
+ }
504
+ const roundAnswers = await answerQuestions2(result.questions);
505
+ if (roundAnswers === void 0) {
506
+ throw new Error(
507
+ "Grilling was cancelled before the design was settled."
508
+ );
509
+ }
510
+ assertAnswersCoverQuestions(result.questions, roundAnswers);
511
+ answers = [...answers, ...roundAnswers];
512
+ }
513
+ throw new Error(
514
+ "Grilling exceeded the configured maximum number of rounds."
515
+ );
516
+ }
517
+ report(event) {
518
+ this.options.onStage?.(event);
519
+ }
520
+ };
521
+ function assertAnswersCoverQuestions(questions, answers) {
522
+ const expected = new Set(questions.map((question) => question.id));
523
+ if (answers.length !== questions.length || answers.some(
524
+ (answer) => !expected.delete(answer.id) || answer.answer.trim().length === 0
525
+ ) || expected.size !== 0) {
526
+ throw new Error(
527
+ "Grilling answers must cover each current question exactly once."
528
+ );
529
+ }
530
+ }
531
+ function combineImplementation(implementations) {
532
+ return {
533
+ taskId: "all",
534
+ filesChanged: [
535
+ ...new Set(implementations.flatMap((result) => result.filesChanged))
536
+ ],
537
+ summary: implementations.map((result) => `${result.taskId}: ${result.summary}`).join("\n")
538
+ };
539
+ }
540
+ function mergeFindings(review, verification) {
541
+ const unique = /* @__PURE__ */ new Map();
542
+ for (const finding of [...review, ...verification]) {
543
+ unique.set(
544
+ `${finding.fileLine}|${finding.rule}|${finding.description}`,
545
+ finding
546
+ );
547
+ }
548
+ return [...unique.values()].map((finding, index) => ({
549
+ ...finding,
550
+ number: index + 1
551
+ }));
552
+ }
553
+
554
+ // src/runtimes/pi.ts
555
+ import { existsSync as existsSync2, readFileSync as readFileSync2, statSync } from "node:fs";
556
+ import path2 from "node:path";
557
+ import {
558
+ createAgentSession,
559
+ DefaultResourceLoader,
560
+ ModelRuntime,
561
+ resolveCliModel,
562
+ SessionManager,
563
+ SettingsManager
564
+ } from "@earendil-works/pi-coding-agent";
565
+ var READONLY_TOOLS = ["read", "grep", "find", "ls"];
566
+ var DEFAULT_TIMEOUT_MS = 10 * 60 * 1e3;
567
+ function piToolsFor(role, allowWrite, platform = process.platform) {
568
+ if (role.readonly || !allowWrite) return READONLY_TOOLS;
569
+ return [
570
+ "read",
571
+ platform === "win32" ? "powershell" : "bash",
572
+ "edit",
573
+ "write",
574
+ "grep",
575
+ "find",
576
+ "ls"
577
+ ];
578
+ }
579
+ function createPiModelProvider(options) {
580
+ assertTargetDirectory(options.dir);
581
+ let runtimePromise;
582
+ const runtime = () => {
583
+ if (options.modelRuntime !== void 0)
584
+ return Promise.resolve(options.modelRuntime);
585
+ runtimePromise ??= ModelRuntime.create();
586
+ return runtimePromise;
587
+ };
588
+ const createSession = options.createSession ?? ((sessionOptions) => createSdkSession(sessionOptions, runtime()));
589
+ return {
590
+ async generate(request) {
591
+ const role = getAgentDefinition(request.agent);
592
+ const session = await createSession({
593
+ cwd: options.dir,
594
+ ...options.model === void 0 ? {} : { model: options.model },
595
+ tools: piToolsFor(role, options.allowWrite !== false),
596
+ systemPrompt: systemPromptFor(
597
+ request.agent,
598
+ role,
599
+ options.language,
600
+ options.allowWrite !== false,
601
+ options.dir,
602
+ request.skill
603
+ )
604
+ });
605
+ const timeout = setTimeout(() => {
606
+ void session.abort().catch(reportAbortFailure);
607
+ }, options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
608
+ try {
609
+ await session.prompt(buildTaskPrompt(request));
610
+ return parseJsonResponse(finalPiText(session.messages));
611
+ } finally {
612
+ clearTimeout(timeout);
613
+ session.dispose();
614
+ }
615
+ }
616
+ };
617
+ }
618
+ function assertTargetDirectory(directory) {
619
+ if (directory.trim().length === 0) {
620
+ throw new Error("Pi needs a target directory");
621
+ }
622
+ try {
623
+ if (!statSync(directory).isDirectory()) {
624
+ throw new Error("path is not a directory");
625
+ }
626
+ } catch (error) {
627
+ throw new Error(`Pi target directory is unavailable: ${directory}`, {
628
+ cause: error
629
+ });
630
+ }
631
+ }
632
+ function systemPromptFor(agent, role, language, allowWrite, directory, skill) {
633
+ const sections = [buildSystemPrompt(agent, language)];
634
+ if (skill !== void 0) {
635
+ sections.push(`## Packaged skill: ${skill}`, getPackagedSkill(skill));
636
+ }
637
+ const projectInstructions = loadTargetProjectInstructions(directory);
638
+ if (projectInstructions.length > 0) {
639
+ sections.push("## Target repository instructions", projectInstructions);
640
+ }
641
+ if (!role.readonly && allowWrite) {
642
+ sections.push(
643
+ "## Mandatory write-gate enforcement",
644
+ "The shared template and selected language guidelines above are mandatory. Before every code-writing tool call (edit, write, or a shell command that changes source), read the relevant guideline sections first. Do not write code and review its guidelines afterwards."
645
+ );
646
+ }
647
+ sections.push(
648
+ "## Sideroom execution boundary",
649
+ "Run only this Sideroom role through the Pi SDK. Do not delegate work to another agent, harness, task runner, or skill with a similar name. Never use or claim a fallback agent. The only valid pipeline roles are sideroom-planner, sideroom-implementer, sideroom-code-reviewer, sideroom-verifier, and sideroom-fixer."
650
+ );
651
+ return sections.join("\n\n");
652
+ }
653
+ async function createSdkSession(options, modelRuntime) {
654
+ const resolvedRuntime = await modelRuntime;
655
+ const resolved = resolveModel(options.model, resolvedRuntime);
656
+ const settingsManager = createIsolatedSettings(options.cwd);
657
+ const loader = createIsolatedResourceLoader({ ...options, settingsManager });
658
+ await loader.reload();
659
+ const { session } = await createAgentSession({
660
+ cwd: options.cwd,
661
+ modelRuntime: resolvedRuntime,
662
+ ...resolved.model === void 0 ? {} : { model: resolved.model },
663
+ ...resolved.thinkingLevel === void 0 ? {} : { thinkingLevel: resolved.thinkingLevel },
664
+ tools: [...options.tools],
665
+ resourceLoader: loader,
666
+ settingsManager,
667
+ sessionManager: SessionManager.inMemory(options.cwd)
668
+ });
669
+ return session;
670
+ }
671
+ function createIsolatedResourceLoader(options) {
672
+ return new DefaultResourceLoader({
673
+ cwd: options.cwd,
674
+ // This avoids Pi's global agent directory; noContextFiles prevents target
675
+ // context from being loaded implicitly, too.
676
+ agentDir: options.cwd,
677
+ settingsManager: options.settingsManager ?? SettingsManager.inMemory(),
678
+ noExtensions: true,
679
+ noSkills: true,
680
+ noPromptTemplates: true,
681
+ noContextFiles: true,
682
+ additionalSkillPaths: [path2.join(getPackageRoot(), "skills")],
683
+ systemPromptOverride: () => options.systemPrompt,
684
+ appendSystemPromptOverride: () => []
685
+ });
686
+ }
687
+ function createIsolatedSettings(directory) {
688
+ const global = SettingsManager.create(directory);
689
+ return SettingsManager.inMemory({
690
+ ...global.getDefaultProvider() === void 0 ? {} : { defaultProvider: global.getDefaultProvider() },
691
+ ...global.getDefaultModel() === void 0 ? {} : { defaultModel: global.getDefaultModel() },
692
+ ...global.getDefaultThinkingLevel() === void 0 ? {} : { defaultThinkingLevel: global.getDefaultThinkingLevel() },
693
+ modelThinkingLevels: global.getAllModelThinkingLevels()
694
+ });
695
+ }
696
+ function resolveModel(model, runtime) {
697
+ if (model === void 0)
698
+ return { model: void 0, thinkingLevel: void 0 };
699
+ const resolved = resolveCliModel({ cliModel: model, modelRuntime: runtime });
700
+ if (resolved.error !== void 0)
701
+ throw new Error(`Pi model ${model}: ${resolved.error}`);
702
+ return resolved;
703
+ }
704
+ function finalPiText(messages) {
705
+ let lastError;
706
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
707
+ const message = messages[index];
708
+ if (!isRecord(message) || message.role !== "assistant" || !Array.isArray(message.content))
709
+ continue;
710
+ if (typeof message.errorMessage === "string")
711
+ lastError = message.errorMessage;
712
+ const text = message.content.flatMap(
713
+ (part) => isRecord(part) && part.type === "text" && typeof part.text === "string" ? [part.text] : []
714
+ );
715
+ if (text.length > 0) return text.join("");
716
+ }
717
+ if (lastError !== void 0)
718
+ throw new Error(`Pi assistant failed: ${lastError}`);
719
+ throw new Error("Pi returned no final assistant text");
720
+ }
721
+ function buildTaskPrompt(request) {
722
+ const task = [
723
+ "Task context (JSON):",
724
+ JSON.stringify(request.input),
725
+ "",
726
+ "Return only one JSON value matching this schema:",
727
+ request.schema
728
+ ].join("\n");
729
+ return task;
730
+ }
731
+ function loadTargetProjectInstructions(directory) {
732
+ const root = findProjectRoot(directory);
733
+ const paths = directoriesFrom(root, path2.resolve(directory));
734
+ const files = paths.flatMap(readInstructionFile);
735
+ return files.map(({ file, content }) => `### ${file}
736
+ ${content}`).join("\n\n");
737
+ }
738
+ function findProjectRoot(directory) {
739
+ let current = path2.resolve(directory);
740
+ while (true) {
741
+ if (existsSync2(path2.join(current, ".git"))) return current;
742
+ const parent = path2.dirname(current);
743
+ if (parent === current) return path2.resolve(directory);
744
+ current = parent;
745
+ }
746
+ }
747
+ function directoriesFrom(root, directory) {
748
+ const result = [];
749
+ let current = directory;
750
+ while (true) {
751
+ result.unshift(current);
752
+ if (current === root) return result;
753
+ const parent = path2.dirname(current);
754
+ if (parent === current) return [directory];
755
+ current = parent;
756
+ }
757
+ }
758
+ function readInstructionFile(directory) {
759
+ for (const name of [
760
+ "AGENTS.override.md",
761
+ "AGENTS.md",
762
+ "AGENTS.MD",
763
+ "CLAUDE.md",
764
+ "CLAUDE.MD"
765
+ ]) {
766
+ const file = path2.join(directory, name);
767
+ if (!existsSync2(file)) continue;
768
+ try {
769
+ if (!statSync(file).isFile()) continue;
770
+ return [{ file, content: readFileSync2(file, "utf8") }];
771
+ } catch {
772
+ return [];
773
+ }
774
+ }
775
+ return [];
776
+ }
777
+ function parseJsonResponse(text) {
778
+ const trimmed = text.trim();
779
+ try {
780
+ return JSON.parse(trimmed);
781
+ } catch (initialError) {
782
+ const fenced = /```(?:json)?\s*([\s\S]*?)```/g;
783
+ let match = fenced.exec(trimmed);
784
+ let candidate;
785
+ while (match !== null) {
786
+ candidate = match[1]?.trim();
787
+ match = fenced.exec(trimmed);
788
+ }
789
+ if (candidate !== void 0) {
790
+ try {
791
+ return JSON.parse(candidate);
792
+ } catch (fencedError) {
793
+ throw new Error("Pi response was not valid JSON", {
794
+ cause: fencedError
795
+ });
796
+ }
797
+ }
798
+ throw new Error("Pi response was not valid JSON", { cause: initialError });
799
+ }
800
+ }
801
+ function reportAbortFailure(error) {
802
+ const message = error instanceof Error ? error.message : String(error);
803
+ process.stderr.write(
804
+ `Sideroom could not abort a timed-out Pi session: ${message}
805
+ `
806
+ );
807
+ }
808
+ function isRecord(value) {
809
+ return typeof value === "object" && value !== null;
810
+ }
811
+
812
+ // src/app.ts
813
+ function createPipeline(options) {
814
+ const model = createPiModelProvider({
815
+ dir: options.dir,
816
+ language: options.language,
817
+ ...options.model === void 0 ? {} : { model: options.model },
818
+ allowWrite: options.allowWrite
819
+ });
820
+ return new SideroomOrchestrator({
821
+ planner: createPlanner(model),
822
+ implementer: createImplementer(model),
823
+ reviewer: createReviewer(model),
824
+ verifier: createVerifier(model),
825
+ fixer: createFixer(model),
826
+ griller: createGriller(model),
827
+ maxFixPasses: options.maxFixPasses,
828
+ onStage: options.onStage
829
+ });
830
+ }
831
+
832
+ // src/pi-command.ts
833
+ function parsePiCommand(args) {
834
+ const tokenResult = splitArguments(args);
835
+ if (typeof tokenResult === "string")
836
+ return { kind: "error", message: tokenResult };
837
+ let language = "typescript";
838
+ let maxFixPasses;
839
+ let allowWrite = true;
840
+ const requestParts = [];
841
+ for (let index = 0; index < tokenResult.length; index += 1) {
842
+ const token = tokenResult[index];
843
+ if (token === void 0) continue;
844
+ if (token === "--help" || token === "-h") return { kind: "help" };
845
+ if (token === "--read-only") {
846
+ allowWrite = false;
847
+ continue;
848
+ }
849
+ if (token === "--language" || token === "--max-fix-passes") {
850
+ const value = tokenResult[index + 1];
851
+ if (value === void 0 || value.startsWith("-") || value.length === 0) {
852
+ return { kind: "error", message: `${token} requires a value` };
853
+ }
854
+ index += 1;
855
+ if (token === "--language") {
856
+ if (!LANGUAGES.includes(value)) {
857
+ return {
858
+ kind: "error",
859
+ message: `--language must be one of: ${LANGUAGES.join(", ")}`
860
+ };
861
+ }
862
+ language = value;
863
+ } else {
864
+ const count = Number(value);
865
+ if (!Number.isInteger(count) || count < 1) {
866
+ return {
867
+ kind: "error",
868
+ message: "--max-fix-passes must be an integer of at least 1"
869
+ };
870
+ }
871
+ maxFixPasses = count;
872
+ }
873
+ continue;
874
+ }
875
+ if (token.startsWith("-")) {
876
+ return { kind: "error", message: `unknown option: ${token}` };
877
+ }
878
+ requestParts.push(token);
879
+ }
880
+ return {
881
+ kind: "run",
882
+ language,
883
+ ...requestParts.length === 0 ? {} : { request: requestParts.join(" ") },
884
+ ...maxFixPasses === void 0 ? {} : { maxFixPasses },
885
+ allowWrite
886
+ };
887
+ }
888
+ function formatPiCommandHelp() {
889
+ return `Usage: /sideroom [request] [options]
890
+
891
+ Options:
892
+ --language <name> typescript | javascript | php-laravel | python | java
893
+ --max-fix-passes <number> Maximum repair passes (default: 2)
894
+ --read-only Do not grant write-capable tools
895
+ -h, --help Show this help
896
+
897
+ Run Pi from the repository you want to change. Sideroom creates no project-local state.`;
898
+ }
899
+ function splitArguments(source) {
900
+ const tokens = [];
901
+ let current = "";
902
+ let quote;
903
+ let escaping = false;
904
+ for (const character of source.trim()) {
905
+ if (escaping) {
906
+ current += character;
907
+ escaping = false;
908
+ continue;
909
+ }
910
+ if (character === "\\") {
911
+ escaping = true;
912
+ continue;
913
+ }
914
+ if (quote !== void 0) {
915
+ if (character === quote) quote = void 0;
916
+ else current += character;
917
+ continue;
918
+ }
919
+ if (character === '"' || character === "'") {
920
+ quote = character;
921
+ continue;
922
+ }
923
+ if (/\s/.test(character)) {
924
+ if (current.length > 0) {
925
+ tokens.push(current);
926
+ current = "";
927
+ }
928
+ continue;
929
+ }
930
+ current += character;
931
+ }
932
+ if (escaping) return "command arguments cannot end with an escape character";
933
+ if (quote !== void 0)
934
+ return "command arguments contain an unterminated quote";
935
+ if (current.length > 0) tokens.push(current);
936
+ return tokens;
937
+ }
938
+
939
+ // src/pi-extension.ts
940
+ var VERSION = "5.0.0";
941
+ var TRACE_TYPE = "sideroom:run";
942
+ var STATUS_KEY = "sideroom";
943
+ var PROGRESS_WIDGET_KEY = "sideroom-progress";
944
+ var RECOMMENDATION_PREFIX = "Use recommendation: ";
945
+ var CUSTOM_ANSWER = "Write a different answer\u2026";
946
+ var PIPELINE_PHASES = [
947
+ "grilling",
948
+ "planner",
949
+ "implementer",
950
+ "reviewer",
951
+ "verifier",
952
+ "fixer"
953
+ ];
954
+ var PHASE_LABELS = {
955
+ grilling: "Grilling",
956
+ planner: "Planner",
957
+ implementer: "Implementer",
958
+ reviewer: "Reviewer",
959
+ verifier: "Verifier",
960
+ fixer: "Fixer",
961
+ pipeline: "Pipeline"
962
+ };
963
+ function registerSideroom(pi) {
964
+ pi.registerCommand("sideroom", {
965
+ description: "Run the isolated Sideroom coding pipeline in this Pi project",
966
+ handler: async (args, context) => {
967
+ const command = parsePiCommand(args);
968
+ if (command.kind === "help") {
969
+ context.ui.notify(formatPiCommandHelp(), "info");
970
+ return;
971
+ }
972
+ if (command.kind === "error") {
973
+ context.ui.notify(`Sideroom: ${command.message}`, "error");
974
+ return;
975
+ }
976
+ const request = await requestFor(command.request, context);
977
+ if (request === void 0) return;
978
+ const trace = [];
979
+ const progress = new PipelineProgress(context);
980
+ const observe = createObserver(trace, progress);
981
+ progress.start();
982
+ try {
983
+ const result = await createPipeline({
984
+ dir: context.cwd,
985
+ language: command.language,
986
+ ...modelReference(context) === void 0 ? {} : { model: modelReference(context) },
987
+ ...command.maxFixPasses === void 0 ? {} : { maxFixPasses: command.maxFixPasses },
988
+ allowWrite: command.allowWrite,
989
+ onStage: observe
990
+ }).run(
991
+ request,
992
+ (questions) => answerQuestions(questions, context, progress)
993
+ );
994
+ recordRun(pi, context, command.language, result, trace);
995
+ } catch (error) {
996
+ const message = error instanceof Error ? error.message : String(error);
997
+ const result = {
998
+ status: "failed",
999
+ implementations: [],
1000
+ findings: [],
1001
+ summary: message
1002
+ };
1003
+ recordRun(pi, context, command.language, result, [
1004
+ ...trace,
1005
+ { phase: "pipeline", status: "failed" }
1006
+ ]);
1007
+ } finally {
1008
+ progress.clear();
1009
+ context.ui.setStatus(STATUS_KEY, void 0);
1010
+ }
1011
+ }
1012
+ });
1013
+ }
1014
+ async function requestFor(request, context) {
1015
+ if (request !== void 0 && request.trim().length > 0) return request;
1016
+ if (!context.hasUI) {
1017
+ context.ui.notify("Sideroom needs a request after /sideroom.", "error");
1018
+ return void 0;
1019
+ }
1020
+ const response = await context.ui.input(
1021
+ "What should Sideroom build?",
1022
+ "Describe the change"
1023
+ );
1024
+ return response === void 0 || response.trim().length === 0 ? void 0 : response;
1025
+ }
1026
+ async function answerQuestions(questions, context, progress) {
1027
+ if (!context.hasUI) return void 0;
1028
+ const answers = [];
1029
+ for (const [index, question] of questions.entries()) {
1030
+ const questionNumber = index + 1;
1031
+ progress.awaitDecision(questionNumber, questions.length);
1032
+ const answer = await answerQuestion(
1033
+ question,
1034
+ questionNumber,
1035
+ questions.length,
1036
+ context
1037
+ );
1038
+ if (answer === void 0) return void 0;
1039
+ answers.push({
1040
+ id: question.id,
1041
+ answer
1042
+ });
1043
+ }
1044
+ return answers;
1045
+ }
1046
+ async function answerQuestion(question, questionNumber, totalQuestions, context) {
1047
+ const recommendationChoice = `${RECOMMENDATION_PREFIX}${question.recommendation}`;
1048
+ const choice = await context.ui.select(
1049
+ formatQuestionPrompt(question, questionNumber, totalQuestions),
1050
+ [recommendationChoice, CUSTOM_ANSWER]
1051
+ );
1052
+ if (choice === void 0) return void 0;
1053
+ if (choice === recommendationChoice) return question.recommendation;
1054
+ const answer = await context.ui.input(
1055
+ `${formatQuestionPrompt(question, questionNumber, totalQuestions)}
1056
+
1057
+ Custom answer`,
1058
+ "Type your answer"
1059
+ );
1060
+ return answer === void 0 || answer.trim().length === 0 ? void 0 : answer;
1061
+ }
1062
+ function formatQuestionPrompt(question, questionNumber, totalQuestions) {
1063
+ return [
1064
+ `Sideroom \xB7 Grilling question ${questionNumber}/${totalQuestions}`,
1065
+ question.title,
1066
+ question.question,
1067
+ `Recommended answer: ${question.recommendation}`
1068
+ ].join("\n\n");
1069
+ }
1070
+ function createObserver(trace, progress) {
1071
+ return (event) => {
1072
+ trace.push(event);
1073
+ progress.report(event);
1074
+ };
1075
+ }
1076
+ function modelReference(context) {
1077
+ const model = context.model;
1078
+ return model === void 0 ? void 0 : `${model.provider}/${model.id}`;
1079
+ }
1080
+ function recordRun(pi, context, language, result, trace) {
1081
+ const details = {
1082
+ version: VERSION,
1083
+ source: "sideroom-pi-extension",
1084
+ cwd: context.cwd,
1085
+ language,
1086
+ model: modelReference(context),
1087
+ status: result.status,
1088
+ trace
1089
+ };
1090
+ pi.appendEntry(TRACE_TYPE, details);
1091
+ pi.sendMessage(
1092
+ {
1093
+ customType: TRACE_TYPE,
1094
+ display: true,
1095
+ content: formatRun(result, trace),
1096
+ details
1097
+ },
1098
+ { triggerTurn: false }
1099
+ );
1100
+ }
1101
+ function formatRun(result, trace) {
1102
+ const phases = trace.filter((event) => event.status === "completed").map((event) => event.phase).filter((phase, index, values) => values.indexOf(phase) === index);
1103
+ const files = [
1104
+ ...new Set(result.implementations.flatMap((item) => item.filesChanged))
1105
+ ];
1106
+ return [
1107
+ `Sideroom ${result.status === "completed" ? "completed" : "failed"}`,
1108
+ "Provenance: sideroom Pi extension \u2192 isolated Pi SDK role sessions.",
1109
+ `Roles completed: ${phases.length === 0 ? "-" : phases.join(", ")}`,
1110
+ `Files changed: ${files.length === 0 ? "-" : files.join(", ")}`,
1111
+ `Findings: ${String(result.findings.length)}`,
1112
+ result.summary
1113
+ ].join("\n");
1114
+ }
1115
+ var PipelineProgress = class {
1116
+ constructor(context) {
1117
+ this.context = context;
1118
+ }
1119
+ activePhase;
1120
+ awaitingDecision = false;
1121
+ message = "starting isolated Pi SDK role sessions";
1122
+ completed = /* @__PURE__ */ new Set();
1123
+ start() {
1124
+ this.render();
1125
+ }
1126
+ report(event) {
1127
+ const label = PHASE_LABELS[event.phase];
1128
+ const task = event.taskId === void 0 ? "" : ` ${event.taskId}`;
1129
+ const round = event.round === void 0 ? "" : ` (round ${event.round})`;
1130
+ this.activePhase = event.phase;
1131
+ this.awaitingDecision = event.status === "awaiting-input";
1132
+ if (event.status === "completed") this.completed.add(event.phase);
1133
+ this.message = stageMessage(event.status, `${label}${task}${round}`);
1134
+ this.render();
1135
+ }
1136
+ awaitDecision(questionNumber, totalQuestions) {
1137
+ this.activePhase = "grilling";
1138
+ this.awaitingDecision = true;
1139
+ this.message = `awaiting your decision (question ${questionNumber}/${totalQuestions})`;
1140
+ this.render();
1141
+ }
1142
+ clear() {
1143
+ this.context.ui.setWidget(PROGRESS_WIDGET_KEY, void 0);
1144
+ }
1145
+ render() {
1146
+ this.context.ui.setStatus(STATUS_KEY, `Sideroom: ${this.message}`);
1147
+ this.context.ui.setWidget(
1148
+ PROGRESS_WIDGET_KEY,
1149
+ [
1150
+ "Sideroom pipeline",
1151
+ `Status: ${this.message}`,
1152
+ "Runtime: direct Pi SDK sessions \xB7 isolated roles \xB7 no fallback agents",
1153
+ "",
1154
+ ...PIPELINE_PHASES.map((phase) => this.renderPhase(phase))
1155
+ ],
1156
+ { placement: "aboveEditor" }
1157
+ );
1158
+ }
1159
+ renderPhase(phase) {
1160
+ if (this.completed.has(phase)) return `\u2713 ${PHASE_LABELS[phase]}`;
1161
+ if (this.activePhase === phase) {
1162
+ return this.awaitingDecision && phase === "grilling" ? `\u25CF ${PHASE_LABELS[phase]} \u2014 awaiting your decision` : `\u25CF ${PHASE_LABELS[phase]} \u2014 running`;
1163
+ }
1164
+ return `\u25CB ${PHASE_LABELS[phase]}`;
1165
+ }
1166
+ };
1167
+ function stageMessage(status, subject) {
1168
+ if (status === "awaiting-input") return `${subject}: awaiting your decisions`;
1169
+ if (status === "completed") return `${subject}: completed`;
1170
+ if (status === "failed") return `${subject}: failed`;
1171
+ return `${subject}: running`;
1172
+ }
1173
+ export {
1174
+ registerSideroom as default
1175
+ };