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.
@@ -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[]>;
@@ -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
- async function atomicJson(path, value) {
186
+ export function atomicWriteFile(path, content, sync = false) {
186
187
  const temporary = `${path}.${String(process.pid)}.${randomUUID()}.tmp`;
187
- await writeFile(temporary, `${JSON.stringify(value)}\n`, { encoding: "utf8", mode: 0o600 });
188
- await rename(temporary, path);
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 records = await json(join(this.directory, "worktrees.json"));
471
- const matches = records.filter((candidate) => candidate && typeof candidate === "object" && candidate.owner === owner);
472
- if (matches.length !== 1)
473
- throw new Error(`Missing or duplicate worktree record for ${owner}`);
474
- const record = this.structuralWorktree(owner, matches[0]);
475
- if (cwd !== undefined && resolve(cwd) !== resolve(record.cwd))
476
- throw new Error(`Invalid worktree record for ${owner}`);
477
- await access(record.cwd);
478
- return record;
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 validated = await Promise.all(records.map(async (record) => { try {
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
- return validated.filter((record) => record !== undefined);
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
- const visit = (directory) => {
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": "1.0.0",
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": { "type": "git", "url": "git+https://github.com/vekexasia/pi-extensible-workflows.git" },
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 && node --test 'dist/test/*.test.js'",
32
- "acceptance": "npm run build && node --test dist/test/runtime-acceptance.test.js",
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",