pi-extensible-workflows 1.0.0 → 2.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.
- package/README.md +7 -2
- package/dist/src/agent-execution.d.ts +13 -0
- package/dist/src/agent-execution.js +177 -40
- package/dist/src/ambient-workflow-evals.js +14 -33
- package/dist/src/cli.d.ts +9 -0
- package/dist/src/cli.js +530 -6
- package/dist/src/doctor.d.ts +4 -4
- package/dist/src/doctor.js +9 -31
- package/dist/src/herdr.d.ts +12 -0
- package/dist/src/herdr.js +74 -0
- package/dist/src/index.d.ts +107 -27
- package/dist/src/index.js +838 -322
- package/dist/src/persistence.d.ts +28 -0
- package/dist/src/persistence.js +213 -16
- package/dist/src/session-inspector.d.ts +1 -0
- package/dist/src/session-inspector.js +3 -0
- package/dist/src/workflow-evals.d.ts +1 -0
- package/dist/src/workflow-evals.js +25 -26
- package/package.json +8 -4
- package/skills/pi-extensible-workflows/SKILL.md +48 -60
- package/src/agent-execution.ts +140 -27
- package/src/ambient-workflow-evals.ts +18 -28
- package/src/cli.ts +418 -6
- package/src/doctor.ts +13 -35
- package/src/herdr.ts +73 -0
- package/src/index.ts +734 -266
- package/src/persistence.ts +192 -16
- package/src/session-inspector.ts +4 -0
- package/src/workflow-evals.ts +15 -17
|
@@ -47,6 +47,11 @@ export interface WorktreeReference {
|
|
|
47
47
|
cwd: string;
|
|
48
48
|
base: string;
|
|
49
49
|
}
|
|
50
|
+
export interface BorrowedWorktreeBinding {
|
|
51
|
+
name: string;
|
|
52
|
+
sourceRunId: string;
|
|
53
|
+
owner: string;
|
|
54
|
+
}
|
|
50
55
|
export declare function projectStorageKey(cwd: string): string;
|
|
51
56
|
export declare function runsDirectory(cwd: string, sessionId: string, home?: string): string;
|
|
52
57
|
export declare class SessionLease {
|
|
@@ -60,14 +65,18 @@ export declare class SessionLease {
|
|
|
60
65
|
export declare function acquireSessionLease(cwd: string, sessionId: string, home?: string): Promise<SessionLease>;
|
|
61
66
|
export declare function listRunIds(cwd: string, sessionId: string, home?: string): Promise<string[]>;
|
|
62
67
|
export declare function structuralPath(...names: string[]): string;
|
|
68
|
+
export declare function atomicWriteFile(path: string, content: string): Promise<void>;
|
|
69
|
+
export declare function atomicWriteFile(path: string, content: string, sync: true): void;
|
|
63
70
|
export declare class RunStore {
|
|
64
71
|
readonly cwd: string;
|
|
65
72
|
readonly sessionId: string;
|
|
66
73
|
readonly runId: string;
|
|
74
|
+
readonly home: string;
|
|
67
75
|
readonly directory: string;
|
|
68
76
|
private journalWrite;
|
|
69
77
|
private stateWrite;
|
|
70
78
|
private worktreeWrite;
|
|
79
|
+
private borrowedWorktreeWrite;
|
|
71
80
|
private snapshotWrite;
|
|
72
81
|
private launchSnapshotWrite;
|
|
73
82
|
private systemPromptWrite;
|
|
@@ -102,11 +111,30 @@ export declare class RunStore {
|
|
|
102
111
|
answerCheckpoint(name: string, approved: boolean): Promise<AwaitingCheckpoint | undefined>;
|
|
103
112
|
private expectedWorktree;
|
|
104
113
|
private markerPath;
|
|
114
|
+
private namedWorktreeOwner;
|
|
115
|
+
private worktreeName;
|
|
105
116
|
private structuralWorktree;
|
|
117
|
+
private borrowedWorktreeRecords;
|
|
118
|
+
borrowedWorktrees(): Promise<readonly BorrowedWorktreeBinding[]>;
|
|
119
|
+
private borrowedWorktree;
|
|
120
|
+
private sourceRun;
|
|
121
|
+
validateParentRun(parentRunId: string): Promise<void>;
|
|
122
|
+
private ownedWorktree;
|
|
123
|
+
private resolveBorrowedWorktree;
|
|
124
|
+
private findNamedWorktree;
|
|
125
|
+
resolveNamedWorktree(name: string, seen?: Set<string>): Promise<{
|
|
126
|
+
reference: WorktreeReference;
|
|
127
|
+
sourceRunId: string;
|
|
128
|
+
owner: string;
|
|
129
|
+
}>;
|
|
130
|
+
validateBorrowedWorktrees(): Promise<void>;
|
|
131
|
+
ownsWorktree(owner: string): Promise<boolean>;
|
|
106
132
|
private cleanupMarker;
|
|
107
133
|
private cleanupOrphanWorktrees;
|
|
108
134
|
validateWorktree(owner: string, cwd?: string): Promise<WorktreeReference>;
|
|
109
135
|
worktree(owner: string): Promise<WorktreeReference>;
|
|
136
|
+
private resolveNamedWorktreeFromParent;
|
|
137
|
+
private bindBorrowedWorktree;
|
|
110
138
|
snapshotWorktree(owner: string): Promise<string>;
|
|
111
139
|
worktrees(): Promise<readonly WorktreeReference[]>;
|
|
112
140
|
changedWorktrees(): Promise<readonly WorktreeReference[]>;
|
package/dist/src/persistence.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { execFile } from "node:child_process";
|
|
3
|
+
import { renameSync, rmSync, writeFileSync } from "node:fs";
|
|
3
4
|
import { access, link, mkdir, open, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
4
5
|
import { basename, dirname, join, relative, resolve, sep } from "node:path";
|
|
5
6
|
import { homedir } from "node:os";
|
|
@@ -182,21 +183,45 @@ export function structuralPath(...names) {
|
|
|
182
183
|
throw new WorkflowError("INVALID_METADATA", "Structural paths require non-empty explicit names");
|
|
183
184
|
return names.map((name) => encodeURIComponent(name)).join("/");
|
|
184
185
|
}
|
|
185
|
-
|
|
186
|
+
export function atomicWriteFile(path, content, sync = false) {
|
|
186
187
|
const temporary = `${path}.${String(process.pid)}.${randomUUID()}.tmp`;
|
|
187
|
-
|
|
188
|
-
|
|
188
|
+
if (sync) {
|
|
189
|
+
try {
|
|
190
|
+
writeFileSync(temporary, content, { encoding: "utf8", mode: 0o600 });
|
|
191
|
+
renameSync(temporary, path);
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
try {
|
|
195
|
+
rmSync(temporary, { force: true });
|
|
196
|
+
}
|
|
197
|
+
catch { /* Preserve the original write error. */ }
|
|
198
|
+
throw error;
|
|
199
|
+
}
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
return writeFile(temporary, content, { encoding: "utf8", mode: 0o600 }).then(() => rename(temporary, path)).catch(async (error) => {
|
|
203
|
+
try {
|
|
204
|
+
await rm(temporary, { force: true });
|
|
205
|
+
}
|
|
206
|
+
catch { /* Preserve the original write error. */ }
|
|
207
|
+
throw error;
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
async function atomicJson(path, value) {
|
|
211
|
+
await atomicWriteFile(path, `${JSON.stringify(value)}\n`);
|
|
189
212
|
}
|
|
190
213
|
async function json(path) { return JSON.parse(await readFile(path, "utf8")); }
|
|
191
214
|
export class RunStore {
|
|
192
215
|
cwd;
|
|
193
216
|
sessionId;
|
|
194
217
|
runId;
|
|
218
|
+
home;
|
|
195
219
|
directory;
|
|
196
220
|
journalWrite = Promise.resolve();
|
|
197
221
|
// ponytail: serializes one RunStore instance; cross-process run sharing remains unsupported.
|
|
198
222
|
stateWrite = Promise.resolve();
|
|
199
223
|
worktreeWrite = Promise.resolve();
|
|
224
|
+
borrowedWorktreeWrite = Promise.resolve();
|
|
200
225
|
snapshotWrite = Promise.resolve();
|
|
201
226
|
launchSnapshotWrite = Promise.resolve();
|
|
202
227
|
// ponytail: the session lease prevents concurrent RunStore writers for one run.
|
|
@@ -206,6 +231,7 @@ export class RunStore {
|
|
|
206
231
|
this.cwd = cwd;
|
|
207
232
|
this.sessionId = sessionId;
|
|
208
233
|
this.runId = runId;
|
|
234
|
+
this.home = home;
|
|
209
235
|
this.cwd = resolve(cwd);
|
|
210
236
|
this.directory = join(runsDirectory(this.cwd, sessionId, home), safePart(runId));
|
|
211
237
|
}
|
|
@@ -221,6 +247,7 @@ export class RunStore {
|
|
|
221
247
|
await atomicJson(join(temporary, "journal.json"), { completed: {}, awaiting: {}, decisions: {} });
|
|
222
248
|
await atomicJson(join(temporary, "ownership.json"), []);
|
|
223
249
|
await atomicJson(join(temporary, "worktrees.json"), []);
|
|
250
|
+
await atomicJson(join(temporary, "borrowed-worktrees.json"), []);
|
|
224
251
|
await atomicJson(join(temporary, "state.json"), run);
|
|
225
252
|
await atomicJson(join(temporary, "system-prompts.json"), { version: 1, entries: [] });
|
|
226
253
|
await atomicJson(join(temporary, "conversations.json"), { version: 1, conversations: {} });
|
|
@@ -277,7 +304,7 @@ export class RunStore {
|
|
|
277
304
|
await write;
|
|
278
305
|
}
|
|
279
306
|
async appendEvent(event) {
|
|
280
|
-
await this.updateState((run) => ({ ...run, events: [...(run.events ?? []), ...(run.events?.some((current) => current.message === event.message) ? [] : [event])] }));
|
|
307
|
+
await this.updateState((run) => ({ ...run, events: [...(run.events ?? []), ...(run.events?.some((current) => current.type === event.type && current.message === event.message) ? [] : [event])] }));
|
|
281
308
|
}
|
|
282
309
|
async saveOwnership(nodes) {
|
|
283
310
|
await atomicJson(join(this.directory, "ownership.json"), nodes);
|
|
@@ -424,6 +451,26 @@ export class RunStore {
|
|
|
424
451
|
const key = createHash("sha256").update(`${this.sessionId}\0${this.runId}\0${owner}`).digest("hex").slice(0, 16);
|
|
425
452
|
return join(this.directory, `worktree-${key}.creating`);
|
|
426
453
|
}
|
|
454
|
+
namedWorktreeOwner(name) {
|
|
455
|
+
if (!name.trim())
|
|
456
|
+
throw new WorkflowError("WORKTREE_FAILED", "Named worktree names must be non-empty");
|
|
457
|
+
return structuralPath("worktree", "named", name.trim());
|
|
458
|
+
}
|
|
459
|
+
worktreeName(owner) {
|
|
460
|
+
const prefix = `${structuralPath("worktree", "named")}/`;
|
|
461
|
+
if (!owner.startsWith(prefix))
|
|
462
|
+
return undefined;
|
|
463
|
+
const encoded = owner.slice(prefix.length);
|
|
464
|
+
if (!encoded || encoded.includes("/"))
|
|
465
|
+
return undefined;
|
|
466
|
+
try {
|
|
467
|
+
const name = decodeURIComponent(encoded);
|
|
468
|
+
return name.trim() ? name : undefined;
|
|
469
|
+
}
|
|
470
|
+
catch {
|
|
471
|
+
return undefined;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
427
474
|
structuralWorktree(owner, record) {
|
|
428
475
|
if (!record || typeof record !== "object")
|
|
429
476
|
throw new Error(`Invalid worktree record for ${owner}`);
|
|
@@ -435,6 +482,123 @@ export class RunStore {
|
|
|
435
482
|
throw new Error(`Invalid worktree record for ${owner}`);
|
|
436
483
|
return candidate;
|
|
437
484
|
}
|
|
485
|
+
async borrowedWorktreeRecords(wait = true) {
|
|
486
|
+
if (wait)
|
|
487
|
+
await this.borrowedWorktreeWrite;
|
|
488
|
+
const records = await json(join(this.directory, "borrowed-worktrees.json")).catch((error) => { if (error.code === "ENOENT")
|
|
489
|
+
return []; throw error; });
|
|
490
|
+
if (!Array.isArray(records))
|
|
491
|
+
throw new WorkflowError("WORKTREE_FAILED", "Borrowed worktree bindings are invalid");
|
|
492
|
+
const seen = new Set();
|
|
493
|
+
return records.map((record) => {
|
|
494
|
+
if (!record || typeof record !== "object")
|
|
495
|
+
throw new WorkflowError("WORKTREE_FAILED", "Borrowed worktree binding is invalid");
|
|
496
|
+
const candidate = record;
|
|
497
|
+
if (typeof candidate.name !== "string" || !candidate.name.trim() || candidate.name !== candidate.name.trim() || typeof candidate.sourceRunId !== "string" || !candidate.sourceRunId || typeof candidate.owner !== "string" || candidate.owner !== this.namedWorktreeOwner(candidate.name))
|
|
498
|
+
throw new WorkflowError("WORKTREE_FAILED", "Borrowed worktree binding is invalid");
|
|
499
|
+
if (seen.has(candidate.name))
|
|
500
|
+
throw new WorkflowError("WORKTREE_FAILED", `Duplicate borrowed worktree binding for ${candidate.name}`);
|
|
501
|
+
seen.add(candidate.name);
|
|
502
|
+
return { name: candidate.name, sourceRunId: candidate.sourceRunId, owner: candidate.owner };
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
async borrowedWorktrees() { return this.borrowedWorktreeRecords(); }
|
|
506
|
+
async borrowedWorktree(name) {
|
|
507
|
+
return (await this.borrowedWorktreeRecords()).find((binding) => binding.name === name);
|
|
508
|
+
}
|
|
509
|
+
async sourceRun(sourceRunId) {
|
|
510
|
+
if (!sourceRunId || sourceRunId === this.runId)
|
|
511
|
+
throw new WorkflowError("WORKTREE_FAILED", "Borrowed worktree source run is invalid");
|
|
512
|
+
const source = new RunStore(this.cwd, this.sessionId, sourceRunId, this.home);
|
|
513
|
+
try {
|
|
514
|
+
const loaded = await source.load();
|
|
515
|
+
if (!["completed", "failed", "stopped"].includes(loaded.run.state))
|
|
516
|
+
throw new Error(`Source run ${sourceRunId} is not terminal`);
|
|
517
|
+
return source;
|
|
518
|
+
}
|
|
519
|
+
catch (error) {
|
|
520
|
+
if (error instanceof WorkflowError && error.code === "WORKTREE_FAILED")
|
|
521
|
+
throw error;
|
|
522
|
+
throw new WorkflowError("WORKTREE_FAILED", error instanceof Error ? error.message : String(error));
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
async validateParentRun(parentRunId) { await this.sourceRun(parentRunId); }
|
|
526
|
+
async ownedWorktree(owner, cwd) {
|
|
527
|
+
const records = await json(join(this.directory, "worktrees.json"));
|
|
528
|
+
const matches = records.filter((candidate) => candidate && typeof candidate === "object" && candidate.owner === owner);
|
|
529
|
+
if (matches.length !== 1)
|
|
530
|
+
throw new Error(`Missing or duplicate worktree record for ${owner}`);
|
|
531
|
+
const record = this.structuralWorktree(owner, matches[0]);
|
|
532
|
+
if (cwd !== undefined && resolve(cwd) !== resolve(record.cwd))
|
|
533
|
+
throw new Error(`Invalid worktree record for ${owner}`);
|
|
534
|
+
await access(record.cwd);
|
|
535
|
+
return record;
|
|
536
|
+
}
|
|
537
|
+
async resolveBorrowedWorktree(binding, seen) {
|
|
538
|
+
try {
|
|
539
|
+
const source = await this.sourceRun(binding.sourceRunId);
|
|
540
|
+
const resolved = await source.findNamedWorktree(binding.name, seen);
|
|
541
|
+
if (!resolved)
|
|
542
|
+
throw new Error(`Missing named worktree ${binding.name} in source run ${binding.sourceRunId}`);
|
|
543
|
+
if (resolved.owner !== binding.owner)
|
|
544
|
+
throw new Error(`Borrowed worktree binding does not match source owner for ${binding.name}`);
|
|
545
|
+
return resolved;
|
|
546
|
+
}
|
|
547
|
+
catch (error) {
|
|
548
|
+
throw error instanceof WorkflowError && error.code === "WORKTREE_FAILED" ? error : new WorkflowError("WORKTREE_FAILED", error instanceof Error ? error.message : String(error));
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
async findNamedWorktree(name, seen = new Set()) {
|
|
552
|
+
const owner = this.namedWorktreeOwner(name);
|
|
553
|
+
if (seen.has(this.runId))
|
|
554
|
+
throw new WorkflowError("WORKTREE_FAILED", "Borrowed worktree bindings contain a cycle");
|
|
555
|
+
const nextSeen = new Set(seen);
|
|
556
|
+
nextSeen.add(this.runId);
|
|
557
|
+
const binding = await this.borrowedWorktree(name);
|
|
558
|
+
if (binding) {
|
|
559
|
+
const loaded = await this.load();
|
|
560
|
+
if (loaded.run.parentRunId === undefined)
|
|
561
|
+
throw new WorkflowError("WORKTREE_FAILED", `Borrowed worktree ${name} has no parent run`);
|
|
562
|
+
const parent = await this.sourceRun(loaded.run.parentRunId);
|
|
563
|
+
const resolved = await parent.findNamedWorktree(name, nextSeen);
|
|
564
|
+
if (!resolved || resolved.sourceRunId !== binding.sourceRunId || resolved.owner !== binding.owner)
|
|
565
|
+
throw new WorkflowError("WORKTREE_FAILED", `Borrowed worktree binding for ${name} is not inherited from its parent run`);
|
|
566
|
+
return resolved;
|
|
567
|
+
}
|
|
568
|
+
const records = await json(join(this.directory, "worktrees.json"));
|
|
569
|
+
const matches = records.filter((candidate) => candidate && typeof candidate === "object" && candidate.owner === owner);
|
|
570
|
+
if (matches.length === 0)
|
|
571
|
+
return undefined;
|
|
572
|
+
try {
|
|
573
|
+
const reference = await this.ownedWorktree(owner);
|
|
574
|
+
return { reference, sourceRunId: this.runId, owner };
|
|
575
|
+
}
|
|
576
|
+
catch (error) {
|
|
577
|
+
throw new WorkflowError("WORKTREE_FAILED", error instanceof Error ? error.message : String(error));
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
async resolveNamedWorktree(name, seen = new Set()) {
|
|
581
|
+
const resolved = await this.findNamedWorktree(name, seen);
|
|
582
|
+
if (!resolved)
|
|
583
|
+
throw new WorkflowError("WORKTREE_FAILED", `Missing named worktree ${name}`);
|
|
584
|
+
return resolved;
|
|
585
|
+
}
|
|
586
|
+
async validateBorrowedWorktrees() {
|
|
587
|
+
try {
|
|
588
|
+
const loaded = await this.load();
|
|
589
|
+
if (loaded.run.parentRunId !== undefined)
|
|
590
|
+
await this.validateParentRun(loaded.run.parentRunId);
|
|
591
|
+
for (const binding of await this.borrowedWorktreeRecords())
|
|
592
|
+
await this.resolveBorrowedWorktree(binding, new Set([this.runId]));
|
|
593
|
+
}
|
|
594
|
+
catch (error) {
|
|
595
|
+
throw error instanceof WorkflowError && error.code === "WORKTREE_FAILED" ? error : new WorkflowError("WORKTREE_FAILED", error instanceof Error ? error.message : String(error));
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
async ownsWorktree(owner) {
|
|
599
|
+
const records = await json(join(this.directory, "worktrees.json"));
|
|
600
|
+
return records.filter((candidate) => candidate && typeof candidate === "object" && candidate.owner === owner).length === 1;
|
|
601
|
+
}
|
|
438
602
|
async cleanupMarker(markerPath) {
|
|
439
603
|
let marker;
|
|
440
604
|
try {
|
|
@@ -467,15 +631,15 @@ export class RunStore {
|
|
|
467
631
|
async validateWorktree(owner, cwd) {
|
|
468
632
|
try {
|
|
469
633
|
await this.load();
|
|
470
|
-
const
|
|
471
|
-
const
|
|
472
|
-
if (
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
return
|
|
634
|
+
const name = this.worktreeName(owner);
|
|
635
|
+
const binding = name ? await this.borrowedWorktree(name) : undefined;
|
|
636
|
+
if (binding) {
|
|
637
|
+
const resolved = await this.resolveBorrowedWorktree(binding, new Set([this.runId]));
|
|
638
|
+
if (cwd !== undefined && resolve(cwd) !== resolve(resolved.reference.cwd))
|
|
639
|
+
throw new Error(`Invalid worktree record for ${owner}`);
|
|
640
|
+
return resolved.reference;
|
|
641
|
+
}
|
|
642
|
+
return await this.ownedWorktree(owner, cwd);
|
|
479
643
|
}
|
|
480
644
|
catch (error) {
|
|
481
645
|
throw error instanceof WorkflowError && error.code === "WORKTREE_FAILED" ? error : new WorkflowError("WORKTREE_FAILED", error instanceof Error ? error.message : String(error));
|
|
@@ -483,10 +647,21 @@ export class RunStore {
|
|
|
483
647
|
}
|
|
484
648
|
async worktree(owner) {
|
|
485
649
|
const write = this.worktreeWrite.then(async () => {
|
|
486
|
-
await this.load();
|
|
650
|
+
const loaded = await this.load();
|
|
487
651
|
const recordsPath = join(this.directory, "worktrees.json");
|
|
488
652
|
let records = await json(recordsPath).catch((error) => { if (error.code === "ENOENT")
|
|
489
653
|
return []; throw error; });
|
|
654
|
+
const name = this.worktreeName(owner);
|
|
655
|
+
const binding = name ? await this.borrowedWorktree(name) : undefined;
|
|
656
|
+
if (binding)
|
|
657
|
+
return (await this.resolveBorrowedWorktree(binding, new Set([this.runId]))).reference;
|
|
658
|
+
if (name && loaded.run.parentRunId !== undefined) {
|
|
659
|
+
const resolved = await this.resolveNamedWorktreeFromParent(name, loaded.run.parentRunId);
|
|
660
|
+
if (resolved) {
|
|
661
|
+
await this.bindBorrowedWorktree({ name, sourceRunId: resolved.sourceRunId, owner: resolved.owner });
|
|
662
|
+
return resolved.reference;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
490
665
|
const existing = records.find((record) => record.owner === owner);
|
|
491
666
|
if (existing)
|
|
492
667
|
return this.validateWorktree(owner);
|
|
@@ -541,6 +716,25 @@ export class RunStore {
|
|
|
541
716
|
this.worktreeWrite = write.then(() => undefined, () => undefined);
|
|
542
717
|
return write;
|
|
543
718
|
}
|
|
719
|
+
async resolveNamedWorktreeFromParent(name, parentRunId) {
|
|
720
|
+
const source = await this.sourceRun(parentRunId);
|
|
721
|
+
return source.findNamedWorktree(name, new Set([this.runId]));
|
|
722
|
+
}
|
|
723
|
+
async bindBorrowedWorktree(binding) {
|
|
724
|
+
const write = this.borrowedWorktreeWrite.then(async () => {
|
|
725
|
+
const records = [...await this.borrowedWorktreeRecords(false)];
|
|
726
|
+
const existing = records.find((candidate) => candidate.name === binding.name);
|
|
727
|
+
if (existing) {
|
|
728
|
+
if (JSON.stringify(existing) !== JSON.stringify(binding))
|
|
729
|
+
throw new WorkflowError("WORKTREE_FAILED", `Borrowed worktree binding for ${binding.name} changed`);
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
records.push(binding);
|
|
733
|
+
await atomicJson(join(this.directory, "borrowed-worktrees.json"), records);
|
|
734
|
+
});
|
|
735
|
+
this.borrowedWorktreeWrite = write.then(() => undefined, () => undefined);
|
|
736
|
+
await write;
|
|
737
|
+
}
|
|
544
738
|
async snapshotWorktree(owner) {
|
|
545
739
|
try {
|
|
546
740
|
const write = this.snapshotWrite.then(async () => {
|
|
@@ -570,13 +764,16 @@ export class RunStore {
|
|
|
570
764
|
async worktrees() {
|
|
571
765
|
const records = await json(join(this.directory, "worktrees.json")).catch((error) => { if (error.code === "ENOENT")
|
|
572
766
|
return []; throw error; });
|
|
573
|
-
const
|
|
767
|
+
const bindings = await this.borrowedWorktreeRecords();
|
|
768
|
+
const boundOwners = new Set(bindings.map((binding) => binding.owner));
|
|
769
|
+
const owned = await Promise.all(records.filter((record) => !boundOwners.has(record.owner)).map(async (record) => { try {
|
|
574
770
|
return await this.validateWorktree(record.owner);
|
|
575
771
|
}
|
|
576
772
|
catch {
|
|
577
773
|
return undefined;
|
|
578
774
|
} }));
|
|
579
|
-
|
|
775
|
+
const borrowed = await Promise.all(bindings.map(async (binding) => (await this.resolveBorrowedWorktree(binding, new Set([this.runId]))).reference));
|
|
776
|
+
return [...owned.filter((record) => record !== undefined), ...borrowed];
|
|
580
777
|
}
|
|
581
778
|
async changedWorktrees() {
|
|
582
779
|
const changed = [];
|
|
@@ -63,6 +63,7 @@ export interface InspectorViewState {
|
|
|
63
63
|
scroll: number;
|
|
64
64
|
}
|
|
65
65
|
export declare function transcriptLines(entries: readonly SessionEntry[]): string[];
|
|
66
|
+
export declare function transcriptFileLines(path: string): string[];
|
|
66
67
|
export declare function matchSession(query: string, sessions: readonly SessionInfo[]): SessionInfo;
|
|
67
68
|
export declare function loadSessionReport(path: string, home?: string): Promise<SessionReport>;
|
|
68
69
|
export declare function renderInspector(report: SessionReport, state: InspectorViewState, width?: number, height?: number, highlighter?: (script: string) => string[]): string[];
|
|
@@ -45,6 +45,9 @@ export function transcriptLines(entries) {
|
|
|
45
45
|
return index ? ["", ...lines] : lines;
|
|
46
46
|
});
|
|
47
47
|
}
|
|
48
|
+
export function transcriptFileLines(path) {
|
|
49
|
+
return transcriptLines(SessionManager.open(path).buildContextEntries());
|
|
50
|
+
}
|
|
48
51
|
function mergedModels(groups) {
|
|
49
52
|
const totals = new Map();
|
|
50
53
|
for (const group of groups)
|
|
@@ -252,6 +252,7 @@ export interface CaptureCaseInput {
|
|
|
252
252
|
maxCost: number;
|
|
253
253
|
}
|
|
254
254
|
export declare function parseSemanticJudge(raw: string, criteria: readonly SemanticCriterion[]): CriterionResult[];
|
|
255
|
+
export declare function findSessionFile(directory: string, sessionId: string): string | undefined;
|
|
255
256
|
export declare function captureEvalCase(input: CaptureCaseInput): Promise<EvalCaseResult>;
|
|
256
257
|
export interface IsolatedProcessOptions {
|
|
257
258
|
childPath: string;
|
|
@@ -8,13 +8,12 @@ import { Value } from "typebox/value";
|
|
|
8
8
|
import { getAgentDir, parseFrontmatter, SessionManager } from "@earendil-works/pi-coding-agent";
|
|
9
9
|
import { CAPTURE_ERROR_PREFIX, CAPTURE_IDENTITY, resolveWorkflowSkillPath } from "./eval-capture-extension.js";
|
|
10
10
|
export { resolveWorkflowSkillPath } from "./eval-capture-extension.js";
|
|
11
|
-
import { ERROR_CODES, inspectWorkflowScript, loadAgentDefinitions, runWorkflow, WorkflowError } from "./index.js";
|
|
11
|
+
import { ERROR_CODES, inspectWorkflowScript, isObject, loadAgentDefinitions, runWorkflow, WORKFLOW_CALL_KINDS, WorkflowError } from "./index.js";
|
|
12
12
|
const CASE_PROCESS_GRACE_MS = 1_000;
|
|
13
13
|
export const SAFE_PARENT_EVAL_TOOLS = Object.freeze(["read", "grep", "find", "bash", "workflow"]);
|
|
14
14
|
const EVAL_MODEL_TOKEN = "$EVAL_MODEL";
|
|
15
15
|
const semantic = (description) => [{ id: "intent", description }];
|
|
16
16
|
const JSON_RESULT_TYPES = ["null", "boolean", "number", "integer", "string", "array", "object"];
|
|
17
|
-
const WORKFLOW_CALL_KINDS = ["agent", "conversation", "parallel", "pipeline", "checkpoint", "phase", "withWorktree"];
|
|
18
17
|
const AGENT_OPTION_NAMES = ["role", "model", "thinking", "tools", "retries"];
|
|
19
18
|
const expectationKeys = ["firstSignificantAction", "firstTool", "firstBatchToolSequence", "parentToolSequence", "workflowCallCount", "requiredOperations", "forbiddenOperations", "requiredRoles", "minimumAgentCalls", "requireOutputSchema", "expectedResults", "agentPolicies", "requiredAgentOrder", "requiredAgentStructures", "requiredDataFlow"];
|
|
20
19
|
const caseKeys = ["id", "prompt", "timeoutMs", "maxCost", "expectations", "expectedWorkflowCalls", "semanticCriteria"];
|
|
@@ -234,7 +233,6 @@ catch (error) {
|
|
|
234
233
|
cases.push(candidate);
|
|
235
234
|
} return Object.freeze(cases); }
|
|
236
235
|
export const INITIAL_WORKFLOW_EVAL_CASES = loadWorkflowEvalCases();
|
|
237
|
-
function isObject(value) { return typeof value === "object" && value !== null && !Array.isArray(value); }
|
|
238
236
|
function isJson(value) { if (value === null || typeof value === "string" || typeof value === "boolean")
|
|
239
237
|
return true; if (typeof value === "number")
|
|
240
238
|
return Number.isFinite(value); if (Array.isArray(value))
|
|
@@ -709,6 +707,7 @@ export async function replayWorkflowScript(script, args = null, signal) {
|
|
|
709
707
|
active -= 1;
|
|
710
708
|
}
|
|
711
709
|
},
|
|
710
|
+
worktree: async () => ({ path: "/worktrees/eval", branch: "eval-branch" }),
|
|
712
711
|
phase: (name) => { phases.push(name); },
|
|
713
712
|
log: (message) => { logs.push(message); },
|
|
714
713
|
}, signal);
|
|
@@ -862,7 +861,7 @@ function semanticJudgePrompt(evalCase, calls, cwd, home) {
|
|
|
862
861
|
return [];
|
|
863
862
|
} }));
|
|
864
863
|
const roleText = [...usedRoles].map((role) => `${role}: ${roles[role]?.description ?? "no description"}`).join("\n") || "none";
|
|
865
|
-
const docs = "agent(prompt, options) delegates; parallel(name, tasks) runs independent tasks concurrently; pipeline(name, items, stages) applies ordered stages; prompt(template, data) carries values into prompts. A role owns model/thinking/tools policy.";
|
|
864
|
+
const docs = "agent(prompt, options) delegates; shell(command, options) runs a deterministic host command and returns exitCode/stdout/stderr; parallel(name, tasks) runs independent tasks concurrently; pipeline(name, items, stages) applies ordered stages; prompt(template, data) carries values into prompts. A role owns model/thinking/tools policy.";
|
|
866
865
|
return `Judge whether the captured workflow design satisfies each criterion. Do not execute it. Return only JSON: {"criteria":[{"id":"criterion id","pass":true,"evidence":"specific script evidence"}]}.\n\nOriginal request:\n${evalCase.prompt}\n\nCriteria:\n${JSON.stringify(evalCase.semanticCriteria ?? [])}\n\nDSL:\n${docs}\n\nRelevant roles:\n${roleText}\n\nCaptured workflow script(s):\n${calls.map((call, index) => `--- ${String(index)} ---\n${call.script ?? "<missing>"}`).join("\n")}`;
|
|
867
866
|
}
|
|
868
867
|
async function runSemanticJudge(input, calls, cwd, home, sessionDir, maxCost) {
|
|
@@ -945,6 +944,27 @@ function seedEvalProject(cwd, home, model) {
|
|
|
945
944
|
writeFileSync(path, `${content.slice(0, frontmatterEnd).replace(/^model:.*$/m, `model: ${model}`)}${content.slice(frontmatterEnd)}`);
|
|
946
945
|
}
|
|
947
946
|
}
|
|
947
|
+
export function findSessionFile(directory, sessionId) {
|
|
948
|
+
if (!existsSync(directory))
|
|
949
|
+
return undefined;
|
|
950
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
951
|
+
const path = join(directory, entry.name);
|
|
952
|
+
if (entry.isDirectory()) {
|
|
953
|
+
const found = findSessionFile(path, sessionId);
|
|
954
|
+
if (found)
|
|
955
|
+
return found;
|
|
956
|
+
}
|
|
957
|
+
else if (entry.name.endsWith(".jsonl")) {
|
|
958
|
+
try {
|
|
959
|
+
const header = JSON.parse(readFileSync(path, "utf8").split("\n")[0] ?? "{}");
|
|
960
|
+
if (isObject(header) && header.id === sessionId)
|
|
961
|
+
return path;
|
|
962
|
+
}
|
|
963
|
+
catch { /* Ignore incomplete sessions. */ }
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
return undefined;
|
|
967
|
+
}
|
|
948
968
|
async function findParentSession(cwd, sessionDir, sessionId) {
|
|
949
969
|
try {
|
|
950
970
|
const sessions = await SessionManager.list(cwd, sessionDir);
|
|
@@ -953,28 +973,7 @@ async function findParentSession(cwd, sessionDir, sessionId) {
|
|
|
953
973
|
return found.path;
|
|
954
974
|
}
|
|
955
975
|
catch { /* Fall through to the JSONL scan. */ }
|
|
956
|
-
|
|
957
|
-
if (!existsSync(directory))
|
|
958
|
-
return undefined;
|
|
959
|
-
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
960
|
-
const path = join(directory, entry.name);
|
|
961
|
-
if (entry.isDirectory()) {
|
|
962
|
-
const found = visit(path);
|
|
963
|
-
if (found)
|
|
964
|
-
return found;
|
|
965
|
-
}
|
|
966
|
-
else if (entry.name.endsWith(".jsonl")) {
|
|
967
|
-
try {
|
|
968
|
-
const header = JSON.parse(readFileSync(path, "utf8").split("\n")[0] ?? "{}");
|
|
969
|
-
if (isObject(header) && header.id === sessionId)
|
|
970
|
-
return path;
|
|
971
|
-
}
|
|
972
|
-
catch { /* Ignore incomplete files. */ }
|
|
973
|
-
}
|
|
974
|
-
}
|
|
975
|
-
return undefined;
|
|
976
|
-
};
|
|
977
|
-
return visit(sessionDir);
|
|
976
|
+
return findSessionFile(sessionDir, sessionId);
|
|
978
977
|
}
|
|
979
978
|
function emptyAccounting(cost = 0) { return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost, models: [] }; }
|
|
980
979
|
function emptyMetrics(requiredWorkflowCallCount = 1) { return { parentUsageThroughCandidate: null, parentOutputTokensThroughCandidate: null, nonWorkflowToolSequenceBeforeCandidate: [], nonWorkflowToolCallCountBeforeCandidate: 0, workflowCallCountBeforeCandidate: 0, invalidWorkflowCallCount: 0, productionValidationErrorCodes: [], candidateCallIndices: [], staticCandidates: [], semanticCriteria: [], anyValidCandidate: false, requiredWorkflowCallCount, surplusWorkflowCallCount: 0 }; }
|
package/package.json
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-extensible-workflows",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Deterministic multi-agent workflow orchestration for Pi",
|
|
5
5
|
"homepage": "https://vekexasia.github.io/pi-extensible-workflows/",
|
|
6
|
-
"repository": {
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/vekexasia/pi-extensible-workflows.git"
|
|
9
|
+
},
|
|
7
10
|
"type": "module",
|
|
8
11
|
"keywords": [
|
|
9
12
|
"pi-package",
|
|
@@ -28,8 +31,9 @@
|
|
|
28
31
|
"build": "rm -rf dist && tsc -p tsconfig.json && chmod +x dist/src/cli.js",
|
|
29
32
|
"inspect": "node dist/src/cli.js inspect",
|
|
30
33
|
"lint": "eslint .",
|
|
31
|
-
"test": "npm run build &&
|
|
32
|
-
"
|
|
34
|
+
"test": "npm run build && TEST_FILES='dist/test/*.test.js' npm run test:run",
|
|
35
|
+
"test:run": "tmp=$(mktemp -d); trap 'rm -rf \"$tmp\"' EXIT; TMPDIR=\"$tmp\" node --test $TEST_FILES",
|
|
36
|
+
"acceptance": "npm run build && TEST_FILES='dist/test/runtime-acceptance.test.js' npm run test:run",
|
|
33
37
|
"docs:check": "node scripts/check-docs.mjs",
|
|
34
38
|
"check": "npm run lint && npm test && npm run docs:check",
|
|
35
39
|
"evals": "npm run build && node dist/src/workflow-evals.js",
|