pi-extensible-workflows 1.0.1 → 3.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 +9 -2
- package/dist/src/agent-execution.d.ts +13 -14
- package/dist/src/agent-execution.js +110 -197
- package/dist/src/budget.d.ts +38 -0
- package/dist/src/budget.js +160 -0
- package/dist/src/cli.d.ts +9 -0
- package/dist/src/cli.js +536 -6
- package/dist/src/doctor.d.ts +4 -4
- package/dist/src/doctor.js +9 -29
- package/dist/src/execution.d.ts +17 -0
- package/dist/src/execution.js +630 -0
- package/dist/src/herdr.d.ts +12 -0
- package/dist/src/herdr.js +74 -0
- package/dist/src/host.d.ts +62 -0
- package/dist/src/host.js +2696 -0
- package/dist/src/index.d.ts +11 -557
- package/dist/src/index.js +8 -3974
- package/dist/src/persistence.d.ts +32 -19
- package/dist/src/persistence.js +310 -70
- package/dist/src/registry.d.ts +31 -0
- package/dist/src/registry.js +169 -0
- package/dist/src/session-inspector.d.ts +1 -0
- package/dist/src/session-inspector.js +4 -1
- package/dist/src/types.d.ts +565 -0
- package/dist/src/types.js +27 -0
- package/dist/src/utils.d.ts +26 -0
- package/dist/src/utils.js +128 -0
- package/dist/src/validation.d.ts +24 -0
- package/dist/src/validation.js +740 -0
- package/dist/src/workflow-evals.js +2 -1
- package/package.json +4 -3
- package/skills/pi-extensible-workflows/SKILL.md +52 -67
- package/src/agent-execution.ts +84 -147
- package/src/budget.ts +75 -0
- package/src/cli.ts +427 -6
- package/src/doctor.ts +13 -32
- package/src/execution.ts +540 -0
- package/src/herdr.ts +73 -0
- package/src/host.ts +2265 -0
- package/src/index.ts +11 -3453
- package/src/persistence.ts +255 -47
- package/src/registry.ts +157 -0
- package/src/session-inspector.ts +5 -1
- package/src/types.ts +113 -0
- package/src/utils.ts +108 -0
- package/src/validation.ts +616 -0
- package/src/workflow-evals.ts +2 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { BudgetApprovalRequest, JsonValue, LaunchSnapshot, RunRecord, WorkflowRunEvent } from "./
|
|
1
|
+
import type { BudgetApprovalRequest, JsonValue, LaunchSnapshot, RunRecord, WorkflowRunEvent } from "./types.js";
|
|
2
2
|
import type { OwnershipRecord } from "./agent-execution.js";
|
|
3
3
|
export interface NativeSessionReference {
|
|
4
4
|
sessionId: string;
|
|
@@ -11,20 +11,6 @@ export interface EffectiveSystemPrompt {
|
|
|
11
11
|
sha256: string;
|
|
12
12
|
prompt: string;
|
|
13
13
|
}
|
|
14
|
-
export interface ConversationHead {
|
|
15
|
-
turn: number;
|
|
16
|
-
sessionId: string;
|
|
17
|
-
sessionFile: string;
|
|
18
|
-
leafId: string;
|
|
19
|
-
systemPrompt: string;
|
|
20
|
-
systemPromptSha256: string;
|
|
21
|
-
toolDefinitionsSha256: string;
|
|
22
|
-
}
|
|
23
|
-
export interface PersistedConversation {
|
|
24
|
-
id: string;
|
|
25
|
-
policy: JsonValue;
|
|
26
|
-
head: ConversationHead;
|
|
27
|
-
}
|
|
28
14
|
export interface PersistedRun extends RunRecord {
|
|
29
15
|
nativeSessions: readonly NativeSessionReference[];
|
|
30
16
|
}
|
|
@@ -47,6 +33,11 @@ export interface WorktreeReference {
|
|
|
47
33
|
cwd: string;
|
|
48
34
|
base: string;
|
|
49
35
|
}
|
|
36
|
+
export interface BorrowedWorktreeBinding {
|
|
37
|
+
name: string;
|
|
38
|
+
sourceRunId: string;
|
|
39
|
+
owner: string;
|
|
40
|
+
}
|
|
50
41
|
export declare function projectStorageKey(cwd: string): string;
|
|
51
42
|
export declare function runsDirectory(cwd: string, sessionId: string, home?: string): string;
|
|
52
43
|
export declare class SessionLease {
|
|
@@ -66,14 +57,15 @@ export declare class RunStore {
|
|
|
66
57
|
readonly cwd: string;
|
|
67
58
|
readonly sessionId: string;
|
|
68
59
|
readonly runId: string;
|
|
60
|
+
readonly home: string;
|
|
69
61
|
readonly directory: string;
|
|
70
62
|
private journalWrite;
|
|
71
63
|
private stateWrite;
|
|
72
64
|
private worktreeWrite;
|
|
65
|
+
private borrowedWorktreeWrite;
|
|
73
66
|
private snapshotWrite;
|
|
74
67
|
private launchSnapshotWrite;
|
|
75
68
|
private systemPromptWrite;
|
|
76
|
-
private conversationWrite;
|
|
77
69
|
constructor(cwd: string, sessionId: string, runId: string, home?: string);
|
|
78
70
|
create(run: PersistedRun, snapshot: Readonly<LaunchSnapshot>): Promise<void>;
|
|
79
71
|
isComplete(): Promise<boolean>;
|
|
@@ -90,12 +82,11 @@ export declare class RunStore {
|
|
|
90
82
|
systemPromptPath(): string;
|
|
91
83
|
recordSystemPrompt(entry: Omit<EffectiveSystemPrompt, "sha256">): Promise<void>;
|
|
92
84
|
systemPrompts(): Promise<readonly EffectiveSystemPrompt[]>;
|
|
93
|
-
conversationPath(): string;
|
|
94
|
-
conversation(id: string): Promise<PersistedConversation | undefined>;
|
|
95
|
-
saveConversation(conversation: PersistedConversation): Promise<void>;
|
|
96
85
|
private updateJournal;
|
|
97
86
|
complete(path: string, value: JsonValue): Promise<void>;
|
|
98
87
|
replay(path: string): Promise<CompletedOperation | undefined>;
|
|
88
|
+
replayableOperations(): Promise<readonly CompletedOperation[]>;
|
|
89
|
+
private replayableOperationsFrom;
|
|
99
90
|
awaitCheckpoint(checkpoint: AwaitingCheckpoint): Promise<boolean | undefined>;
|
|
100
91
|
awaitingCheckpoints(): Promise<readonly AwaitingCheckpoint[]>;
|
|
101
92
|
requestWorkflowDecision(request: PendingWorkflowDecision): Promise<void>;
|
|
@@ -104,13 +95,35 @@ export declare class RunStore {
|
|
|
104
95
|
answerCheckpoint(name: string, approved: boolean): Promise<AwaitingCheckpoint | undefined>;
|
|
105
96
|
private expectedWorktree;
|
|
106
97
|
private markerPath;
|
|
98
|
+
private namedWorktreeOwner;
|
|
99
|
+
private worktreeName;
|
|
107
100
|
private structuralWorktree;
|
|
101
|
+
private borrowedWorktreeRecords;
|
|
102
|
+
borrowedWorktrees(): Promise<readonly BorrowedWorktreeBinding[]>;
|
|
103
|
+
private borrowedWorktree;
|
|
104
|
+
private sourceRun;
|
|
105
|
+
validateParentRun(parentRunId: string): Promise<void>;
|
|
106
|
+
validateRetrySource(): Promise<void>;
|
|
107
|
+
private ownedWorktree;
|
|
108
|
+
private resolveBorrowedWorktree;
|
|
109
|
+
private findNamedWorktree;
|
|
110
|
+
resolveNamedWorktree(name: string, seen?: Set<string>): Promise<{
|
|
111
|
+
reference: WorktreeReference;
|
|
112
|
+
sourceRunId: string;
|
|
113
|
+
owner: string;
|
|
114
|
+
}>;
|
|
115
|
+
validateBorrowedWorktrees(): Promise<void>;
|
|
116
|
+
validateNamedWorktrees(): Promise<void>;
|
|
117
|
+
ownsWorktree(owner: string): Promise<boolean>;
|
|
108
118
|
private cleanupMarker;
|
|
109
119
|
private cleanupOrphanWorktrees;
|
|
110
120
|
validateWorktree(owner: string, cwd?: string): Promise<WorktreeReference>;
|
|
111
121
|
worktree(owner: string): Promise<WorktreeReference>;
|
|
122
|
+
private resolveNamedWorktreeFromParent;
|
|
123
|
+
private bindBorrowedWorktree;
|
|
112
124
|
snapshotWorktree(owner: string): Promise<string>;
|
|
113
125
|
worktrees(): Promise<readonly WorktreeReference[]>;
|
|
126
|
+
validNamedWorktrees(): Promise<readonly string[]>;
|
|
114
127
|
changedWorktrees(): Promise<readonly WorktreeReference[]>;
|
|
115
128
|
saveResult(value: JsonValue): Promise<string>;
|
|
116
129
|
delete(confirmed: boolean): Promise<void>;
|
package/dist/src/persistence.js
CHANGED
|
@@ -5,13 +5,8 @@ import { access, link, mkdir, open, readFile, readdir, rename, rm, stat, writeFi
|
|
|
5
5
|
import { basename, dirname, join, relative, resolve, sep } from "node:path";
|
|
6
6
|
import { homedir } from "node:os";
|
|
7
7
|
import { promisify } from "node:util";
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
11
|
-
return false;
|
|
12
|
-
const artifact = value;
|
|
13
|
-
return artifact.version === 1 && Boolean(artifact.conversations) && typeof artifact.conversations === "object" && !Array.isArray(artifact.conversations);
|
|
14
|
-
}
|
|
8
|
+
import { WorkflowError } from "./types.js";
|
|
9
|
+
import { loadLaunchSnapshot } from "./utils.js";
|
|
15
10
|
const execute = promisify(execFile);
|
|
16
11
|
const gitIdentity = {
|
|
17
12
|
GIT_AUTHOR_NAME: "pi-extensible-workflows", GIT_AUTHOR_EMAIL: "pi-extensible-workflows@localhost", GIT_COMMITTER_NAME: "pi-extensible-workflows", GIT_COMMITTER_EMAIL: "pi-extensible-workflows@localhost",
|
|
@@ -215,20 +210,22 @@ export class RunStore {
|
|
|
215
210
|
cwd;
|
|
216
211
|
sessionId;
|
|
217
212
|
runId;
|
|
213
|
+
home;
|
|
218
214
|
directory;
|
|
219
215
|
journalWrite = Promise.resolve();
|
|
220
216
|
// ponytail: serializes one RunStore instance; cross-process run sharing remains unsupported.
|
|
221
217
|
stateWrite = Promise.resolve();
|
|
222
218
|
worktreeWrite = Promise.resolve();
|
|
219
|
+
borrowedWorktreeWrite = Promise.resolve();
|
|
223
220
|
snapshotWrite = Promise.resolve();
|
|
224
221
|
launchSnapshotWrite = Promise.resolve();
|
|
225
222
|
// ponytail: the session lease prevents concurrent RunStore writers for one run.
|
|
226
223
|
systemPromptWrite = Promise.resolve();
|
|
227
|
-
conversationWrite = Promise.resolve();
|
|
228
224
|
constructor(cwd, sessionId, runId, home = homedir()) {
|
|
229
225
|
this.cwd = cwd;
|
|
230
226
|
this.sessionId = sessionId;
|
|
231
227
|
this.runId = runId;
|
|
228
|
+
this.home = home;
|
|
232
229
|
this.cwd = resolve(cwd);
|
|
233
230
|
this.directory = join(runsDirectory(this.cwd, sessionId, home), safePart(runId));
|
|
234
231
|
}
|
|
@@ -244,9 +241,9 @@ export class RunStore {
|
|
|
244
241
|
await atomicJson(join(temporary, "journal.json"), { completed: {}, awaiting: {}, decisions: {} });
|
|
245
242
|
await atomicJson(join(temporary, "ownership.json"), []);
|
|
246
243
|
await atomicJson(join(temporary, "worktrees.json"), []);
|
|
244
|
+
await atomicJson(join(temporary, "borrowed-worktrees.json"), []);
|
|
247
245
|
await atomicJson(join(temporary, "state.json"), run);
|
|
248
246
|
await atomicJson(join(temporary, "system-prompts.json"), { version: 1, entries: [] });
|
|
249
|
-
await atomicJson(join(temporary, "conversations.json"), { version: 1, conversations: {} });
|
|
250
247
|
await rename(temporary, this.directory);
|
|
251
248
|
}
|
|
252
249
|
catch (error) {
|
|
@@ -325,54 +322,6 @@ export class RunStore {
|
|
|
325
322
|
return (await json(this.systemPromptPath()).catch((error) => { if (error.code === "ENOENT")
|
|
326
323
|
return { version: 1, entries: [] }; throw error; })).entries;
|
|
327
324
|
}
|
|
328
|
-
conversationPath() { return join(this.directory, "conversations.json"); }
|
|
329
|
-
async conversation(id) {
|
|
330
|
-
await this.conversationWrite;
|
|
331
|
-
let artifact;
|
|
332
|
-
try {
|
|
333
|
-
const raw = await json(this.conversationPath());
|
|
334
|
-
if (!isConversationArtifact(raw))
|
|
335
|
-
throw new WorkflowError("RESUME_INCOMPATIBLE", "Conversation state is corrupt");
|
|
336
|
-
artifact = raw;
|
|
337
|
-
}
|
|
338
|
-
catch (error) {
|
|
339
|
-
if (error.code === "ENOENT")
|
|
340
|
-
return undefined;
|
|
341
|
-
if (error instanceof WorkflowError)
|
|
342
|
-
throw error;
|
|
343
|
-
throw new WorkflowError("RESUME_INCOMPATIBLE", `Cannot load conversation state: ${error instanceof Error ? error.message : String(error)}`);
|
|
344
|
-
}
|
|
345
|
-
return artifact.conversations[id];
|
|
346
|
-
}
|
|
347
|
-
async saveConversation(conversation) {
|
|
348
|
-
const write = this.conversationWrite.then(async () => {
|
|
349
|
-
const path = this.conversationPath();
|
|
350
|
-
let artifact;
|
|
351
|
-
try {
|
|
352
|
-
const raw = await json(path);
|
|
353
|
-
if (!isConversationArtifact(raw))
|
|
354
|
-
throw new WorkflowError("RESUME_INCOMPATIBLE", "Conversation state is corrupt");
|
|
355
|
-
artifact = raw;
|
|
356
|
-
}
|
|
357
|
-
catch (error) {
|
|
358
|
-
if (error.code === "ENOENT")
|
|
359
|
-
artifact = { version: 1, conversations: {} };
|
|
360
|
-
else if (error instanceof WorkflowError)
|
|
361
|
-
throw error;
|
|
362
|
-
else
|
|
363
|
-
throw new WorkflowError("RESUME_INCOMPATIBLE", `Cannot load conversation state: ${error instanceof Error ? error.message : String(error)}`);
|
|
364
|
-
}
|
|
365
|
-
const previous = artifact.conversations[conversation.id];
|
|
366
|
-
if (previous && previous.head.turn + 1 !== conversation.head.turn)
|
|
367
|
-
throw new WorkflowError("RESUME_INCOMPATIBLE", `Conversation head is not the previous turn: ${conversation.id}`);
|
|
368
|
-
if (!previous && conversation.head.turn !== 1)
|
|
369
|
-
throw new WorkflowError("RESUME_INCOMPATIBLE", `Conversation must start at turn one: ${conversation.id}`);
|
|
370
|
-
artifact.conversations[conversation.id] = structuredClone(conversation);
|
|
371
|
-
await atomicJson(path, artifact);
|
|
372
|
-
});
|
|
373
|
-
this.conversationWrite = write.catch(() => undefined);
|
|
374
|
-
await write;
|
|
375
|
-
}
|
|
376
325
|
async updateJournal(update) {
|
|
377
326
|
let result;
|
|
378
327
|
const write = this.journalWrite.then(async () => {
|
|
@@ -394,10 +343,34 @@ export class RunStore {
|
|
|
394
343
|
});
|
|
395
344
|
}
|
|
396
345
|
async replay(path) {
|
|
346
|
+
const operations = await this.replayableOperations();
|
|
347
|
+
return operations.find((operation) => operation.path === path);
|
|
348
|
+
}
|
|
349
|
+
async replayableOperations() {
|
|
350
|
+
return this.replayableOperationsFrom(new Set());
|
|
351
|
+
}
|
|
352
|
+
async replayableOperationsFrom(seen) {
|
|
353
|
+
if (seen.has(this.runId))
|
|
354
|
+
throw new WorkflowError("RESUME_INCOMPATIBLE", "Retry provenance contains a cycle");
|
|
355
|
+
const nextSeen = new Set(seen);
|
|
356
|
+
nextSeen.add(this.runId);
|
|
397
357
|
await this.journalWrite;
|
|
398
|
-
|
|
358
|
+
const loaded = await this.load();
|
|
359
|
+
const operations = new Map();
|
|
360
|
+
if (loaded.run.retry?.sourceRunId) {
|
|
361
|
+
const source = await this.sourceRun(loaded.run.retry.sourceRunId);
|
|
362
|
+
for (const operation of await source.replayableOperationsFrom(nextSeen))
|
|
363
|
+
operations.set(operation.path, operation);
|
|
364
|
+
}
|
|
365
|
+
const journal = await json(join(this.directory, "journal.json"));
|
|
366
|
+
for (const operation of Object.values(journal.completed))
|
|
367
|
+
operations.set(operation.path, operation);
|
|
368
|
+
return [...operations.values()].map((operation) => structuredClone(operation));
|
|
399
369
|
}
|
|
400
370
|
async awaitCheckpoint(checkpoint) {
|
|
371
|
+
const replayed = await this.replay(checkpoint.path);
|
|
372
|
+
if (replayed)
|
|
373
|
+
return replayed.value;
|
|
401
374
|
return this.updateJournal((journal) => {
|
|
402
375
|
const completed = journal.completed[checkpoint.path];
|
|
403
376
|
if (completed)
|
|
@@ -447,6 +420,26 @@ export class RunStore {
|
|
|
447
420
|
const key = createHash("sha256").update(`${this.sessionId}\0${this.runId}\0${owner}`).digest("hex").slice(0, 16);
|
|
448
421
|
return join(this.directory, `worktree-${key}.creating`);
|
|
449
422
|
}
|
|
423
|
+
namedWorktreeOwner(name) {
|
|
424
|
+
if (!name.trim())
|
|
425
|
+
throw new WorkflowError("WORKTREE_FAILED", "Named worktree names must be non-empty");
|
|
426
|
+
return structuralPath("worktree", "named", name.trim());
|
|
427
|
+
}
|
|
428
|
+
worktreeName(owner) {
|
|
429
|
+
const prefix = `${structuralPath("worktree", "named")}/`;
|
|
430
|
+
if (!owner.startsWith(prefix))
|
|
431
|
+
return undefined;
|
|
432
|
+
const encoded = owner.slice(prefix.length);
|
|
433
|
+
if (!encoded || encoded.includes("/"))
|
|
434
|
+
return undefined;
|
|
435
|
+
try {
|
|
436
|
+
const name = decodeURIComponent(encoded);
|
|
437
|
+
return name.trim() ? name : undefined;
|
|
438
|
+
}
|
|
439
|
+
catch {
|
|
440
|
+
return undefined;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
450
443
|
structuralWorktree(owner, record) {
|
|
451
444
|
if (!record || typeof record !== "object")
|
|
452
445
|
throw new Error(`Invalid worktree record for ${owner}`);
|
|
@@ -458,6 +451,171 @@ export class RunStore {
|
|
|
458
451
|
throw new Error(`Invalid worktree record for ${owner}`);
|
|
459
452
|
return candidate;
|
|
460
453
|
}
|
|
454
|
+
async borrowedWorktreeRecords(wait = true) {
|
|
455
|
+
if (wait)
|
|
456
|
+
await this.borrowedWorktreeWrite;
|
|
457
|
+
const records = await json(join(this.directory, "borrowed-worktrees.json")).catch((error) => { if (error.code === "ENOENT")
|
|
458
|
+
return []; throw error; });
|
|
459
|
+
if (!Array.isArray(records))
|
|
460
|
+
throw new WorkflowError("WORKTREE_FAILED", "Borrowed worktree bindings are invalid");
|
|
461
|
+
const seen = new Set();
|
|
462
|
+
return records.map((record) => {
|
|
463
|
+
if (!record || typeof record !== "object")
|
|
464
|
+
throw new WorkflowError("WORKTREE_FAILED", "Borrowed worktree binding is invalid");
|
|
465
|
+
const candidate = record;
|
|
466
|
+
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))
|
|
467
|
+
throw new WorkflowError("WORKTREE_FAILED", "Borrowed worktree binding is invalid");
|
|
468
|
+
if (seen.has(candidate.name))
|
|
469
|
+
throw new WorkflowError("WORKTREE_FAILED", `Duplicate borrowed worktree binding for ${candidate.name}`);
|
|
470
|
+
seen.add(candidate.name);
|
|
471
|
+
return { name: candidate.name, sourceRunId: candidate.sourceRunId, owner: candidate.owner };
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
async borrowedWorktrees() { return this.borrowedWorktreeRecords(); }
|
|
475
|
+
async borrowedWorktree(name) {
|
|
476
|
+
return (await this.borrowedWorktreeRecords()).find((binding) => binding.name === name);
|
|
477
|
+
}
|
|
478
|
+
async sourceRun(sourceRunId) {
|
|
479
|
+
if (!sourceRunId || sourceRunId === this.runId)
|
|
480
|
+
throw new WorkflowError("WORKTREE_FAILED", "Borrowed worktree source run is invalid");
|
|
481
|
+
const source = new RunStore(this.cwd, this.sessionId, sourceRunId, this.home);
|
|
482
|
+
try {
|
|
483
|
+
const loaded = await source.load();
|
|
484
|
+
if (!["completed", "failed", "stopped"].includes(loaded.run.state))
|
|
485
|
+
throw new Error(`Source run ${sourceRunId} is not terminal`);
|
|
486
|
+
return source;
|
|
487
|
+
}
|
|
488
|
+
catch (error) {
|
|
489
|
+
if (error instanceof WorkflowError && error.code === "WORKTREE_FAILED")
|
|
490
|
+
throw error;
|
|
491
|
+
throw new WorkflowError("WORKTREE_FAILED", error instanceof Error ? error.message : String(error));
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
async validateParentRun(parentRunId) { await this.sourceRun(parentRunId); }
|
|
495
|
+
async validateRetrySource() {
|
|
496
|
+
const validate = async (current, seen) => {
|
|
497
|
+
if (seen.has(current.runId))
|
|
498
|
+
throw new WorkflowError("RESUME_INCOMPATIBLE", "Retry provenance contains a cycle");
|
|
499
|
+
const nextSeen = new Set(seen);
|
|
500
|
+
nextSeen.add(current.runId);
|
|
501
|
+
const loaded = await current.load();
|
|
502
|
+
const retry = loaded.run.retry;
|
|
503
|
+
if (!retry)
|
|
504
|
+
return;
|
|
505
|
+
if (typeof retry.sourceRunId !== "string" || !retry.sourceRunId || retry.sourceRunId === current.runId || typeof retry.lineageRootRunId !== "string" || !retry.lineageRootRunId || !Array.isArray(retry.completedPaths) || retry.completedPaths.some((path) => typeof path !== "string") || !Array.isArray(retry.incompletePaths) || retry.incompletePaths.some((path) => typeof path !== "string") || !Array.isArray(retry.namedWorktrees) || retry.namedWorktrees.some((name) => typeof name !== "string"))
|
|
506
|
+
throw new WorkflowError("RESUME_INCOMPATIBLE", "Retry provenance is incomplete");
|
|
507
|
+
const source = await current.sourceRun(retry.sourceRunId);
|
|
508
|
+
const sourceRun = (await source.load()).run;
|
|
509
|
+
if (loaded.run.parentRunId !== retry.sourceRunId)
|
|
510
|
+
throw new WorkflowError("RESUME_INCOMPATIBLE", "Retry parent run does not match its source run");
|
|
511
|
+
if (sourceRun.state !== "failed")
|
|
512
|
+
throw new WorkflowError("RESUME_INCOMPATIBLE", `Retry source run ${retry.sourceRunId} is not failed`);
|
|
513
|
+
const expectedLineageRoot = sourceRun.retry?.lineageRootRunId ?? sourceRun.id;
|
|
514
|
+
if (retry.lineageRootRunId !== expectedLineageRoot)
|
|
515
|
+
throw new WorkflowError("RESUME_INCOMPATIBLE", "Retry lineage root does not match its source run");
|
|
516
|
+
await validate(source, nextSeen);
|
|
517
|
+
};
|
|
518
|
+
try {
|
|
519
|
+
await validate(this, new Set());
|
|
520
|
+
}
|
|
521
|
+
catch (error) {
|
|
522
|
+
throw error instanceof WorkflowError && error.code === "RESUME_INCOMPATIBLE" ? error : new WorkflowError("RESUME_INCOMPATIBLE", error instanceof Error ? error.message : String(error));
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
async ownedWorktree(owner, cwd) {
|
|
526
|
+
const records = await json(join(this.directory, "worktrees.json"));
|
|
527
|
+
const matches = records.filter((candidate) => candidate && typeof candidate === "object" && candidate.owner === owner);
|
|
528
|
+
if (matches.length !== 1)
|
|
529
|
+
throw new Error(`Missing or duplicate worktree record for ${owner}`);
|
|
530
|
+
const record = this.structuralWorktree(owner, matches[0]);
|
|
531
|
+
if (cwd !== undefined && resolve(cwd) !== resolve(record.cwd))
|
|
532
|
+
throw new Error(`Invalid worktree record for ${owner}`);
|
|
533
|
+
await access(record.cwd);
|
|
534
|
+
return record;
|
|
535
|
+
}
|
|
536
|
+
async resolveBorrowedWorktree(binding, seen) {
|
|
537
|
+
try {
|
|
538
|
+
const source = await this.sourceRun(binding.sourceRunId);
|
|
539
|
+
const resolved = await source.findNamedWorktree(binding.name, seen);
|
|
540
|
+
if (!resolved)
|
|
541
|
+
throw new Error(`Missing named worktree ${binding.name} in source run ${binding.sourceRunId}`);
|
|
542
|
+
if (resolved.owner !== binding.owner)
|
|
543
|
+
throw new Error(`Borrowed worktree binding does not match source owner for ${binding.name}`);
|
|
544
|
+
return resolved;
|
|
545
|
+
}
|
|
546
|
+
catch (error) {
|
|
547
|
+
throw error instanceof WorkflowError && error.code === "WORKTREE_FAILED" ? error : new WorkflowError("WORKTREE_FAILED", error instanceof Error ? error.message : String(error));
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
async findNamedWorktree(name, seen = new Set()) {
|
|
551
|
+
const owner = this.namedWorktreeOwner(name);
|
|
552
|
+
if (seen.has(this.runId))
|
|
553
|
+
throw new WorkflowError("WORKTREE_FAILED", "Borrowed worktree bindings contain a cycle");
|
|
554
|
+
const nextSeen = new Set(seen);
|
|
555
|
+
nextSeen.add(this.runId);
|
|
556
|
+
const binding = await this.borrowedWorktree(name);
|
|
557
|
+
if (binding) {
|
|
558
|
+
const loaded = await this.load();
|
|
559
|
+
if (loaded.run.parentRunId === undefined)
|
|
560
|
+
throw new WorkflowError("WORKTREE_FAILED", `Borrowed worktree ${name} has no parent run`);
|
|
561
|
+
const parent = await this.sourceRun(loaded.run.parentRunId);
|
|
562
|
+
const resolved = await parent.findNamedWorktree(name, nextSeen);
|
|
563
|
+
if (!resolved || resolved.sourceRunId !== binding.sourceRunId || resolved.owner !== binding.owner)
|
|
564
|
+
throw new WorkflowError("WORKTREE_FAILED", `Borrowed worktree binding for ${name} is not inherited from its parent run`);
|
|
565
|
+
return resolved;
|
|
566
|
+
}
|
|
567
|
+
const records = await json(join(this.directory, "worktrees.json"));
|
|
568
|
+
const matches = records.filter((candidate) => candidate && typeof candidate === "object" && candidate.owner === owner);
|
|
569
|
+
if (matches.length === 0) {
|
|
570
|
+
const loaded = await this.load();
|
|
571
|
+
if (loaded.run.parentRunId === undefined)
|
|
572
|
+
return undefined;
|
|
573
|
+
const parent = await this.sourceRun(loaded.run.parentRunId);
|
|
574
|
+
return parent.findNamedWorktree(name, nextSeen);
|
|
575
|
+
}
|
|
576
|
+
try {
|
|
577
|
+
const reference = await this.ownedWorktree(owner);
|
|
578
|
+
return { reference, sourceRunId: this.runId, owner };
|
|
579
|
+
}
|
|
580
|
+
catch (error) {
|
|
581
|
+
throw new WorkflowError("WORKTREE_FAILED", error instanceof Error ? error.message : String(error));
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
async resolveNamedWorktree(name, seen = new Set()) {
|
|
585
|
+
const resolved = await this.findNamedWorktree(name, seen);
|
|
586
|
+
if (!resolved)
|
|
587
|
+
throw new WorkflowError("WORKTREE_FAILED", `Missing named worktree ${name}`);
|
|
588
|
+
return resolved;
|
|
589
|
+
}
|
|
590
|
+
async validateBorrowedWorktrees() {
|
|
591
|
+
try {
|
|
592
|
+
const loaded = await this.load();
|
|
593
|
+
if (loaded.run.parentRunId !== undefined)
|
|
594
|
+
await this.validateParentRun(loaded.run.parentRunId);
|
|
595
|
+
for (const binding of await this.borrowedWorktreeRecords())
|
|
596
|
+
await this.resolveBorrowedWorktree(binding, new Set([this.runId]));
|
|
597
|
+
}
|
|
598
|
+
catch (error) {
|
|
599
|
+
throw error instanceof WorkflowError && error.code === "WORKTREE_FAILED" ? error : new WorkflowError("WORKTREE_FAILED", error instanceof Error ? error.message : String(error));
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
async validateNamedWorktrees() {
|
|
603
|
+
try {
|
|
604
|
+
const records = await json(join(this.directory, "worktrees.json"));
|
|
605
|
+
for (const record of records) {
|
|
606
|
+
const owner = record && typeof record === "object" && typeof record.owner === "string" ? record.owner : undefined;
|
|
607
|
+
if (owner && this.worktreeName(owner))
|
|
608
|
+
await this.validateWorktree(owner);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
catch (error) {
|
|
612
|
+
throw error instanceof WorkflowError && error.code === "WORKTREE_FAILED" ? error : new WorkflowError("WORKTREE_FAILED", error instanceof Error ? error.message : String(error));
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
async ownsWorktree(owner) {
|
|
616
|
+
const records = await json(join(this.directory, "worktrees.json"));
|
|
617
|
+
return records.filter((candidate) => candidate && typeof candidate === "object" && candidate.owner === owner).length === 1;
|
|
618
|
+
}
|
|
461
619
|
async cleanupMarker(markerPath) {
|
|
462
620
|
let marker;
|
|
463
621
|
try {
|
|
@@ -490,15 +648,15 @@ export class RunStore {
|
|
|
490
648
|
async validateWorktree(owner, cwd) {
|
|
491
649
|
try {
|
|
492
650
|
await this.load();
|
|
493
|
-
const
|
|
494
|
-
const
|
|
495
|
-
if (
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
return
|
|
651
|
+
const name = this.worktreeName(owner);
|
|
652
|
+
const binding = name ? await this.borrowedWorktree(name) : undefined;
|
|
653
|
+
if (binding) {
|
|
654
|
+
const resolved = await this.resolveBorrowedWorktree(binding, new Set([this.runId]));
|
|
655
|
+
if (cwd !== undefined && resolve(cwd) !== resolve(resolved.reference.cwd))
|
|
656
|
+
throw new Error(`Invalid worktree record for ${owner}`);
|
|
657
|
+
return resolved.reference;
|
|
658
|
+
}
|
|
659
|
+
return await this.ownedWorktree(owner, cwd);
|
|
502
660
|
}
|
|
503
661
|
catch (error) {
|
|
504
662
|
throw error instanceof WorkflowError && error.code === "WORKTREE_FAILED" ? error : new WorkflowError("WORKTREE_FAILED", error instanceof Error ? error.message : String(error));
|
|
@@ -506,10 +664,23 @@ export class RunStore {
|
|
|
506
664
|
}
|
|
507
665
|
async worktree(owner) {
|
|
508
666
|
const write = this.worktreeWrite.then(async () => {
|
|
509
|
-
await this.load();
|
|
667
|
+
const loaded = await this.load();
|
|
510
668
|
const recordsPath = join(this.directory, "worktrees.json");
|
|
511
669
|
let records = await json(recordsPath).catch((error) => { if (error.code === "ENOENT")
|
|
512
670
|
return []; throw error; });
|
|
671
|
+
const name = this.worktreeName(owner);
|
|
672
|
+
const binding = name ? await this.borrowedWorktree(name) : undefined;
|
|
673
|
+
if (binding)
|
|
674
|
+
return (await this.resolveBorrowedWorktree(binding, new Set([this.runId]))).reference;
|
|
675
|
+
if (name && loaded.run.parentRunId !== undefined) {
|
|
676
|
+
const resolved = await this.resolveNamedWorktreeFromParent(name, loaded.run.parentRunId);
|
|
677
|
+
if (resolved) {
|
|
678
|
+
await this.bindBorrowedWorktree({ name, sourceRunId: resolved.sourceRunId, owner: resolved.owner });
|
|
679
|
+
return resolved.reference;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
if (name && Array.isArray(loaded.run.retry?.namedWorktrees) && loaded.run.retry.namedWorktrees.includes(name))
|
|
683
|
+
throw new WorkflowError("WORKTREE_FAILED", `Missing inherited named worktree ${name}`);
|
|
513
684
|
const existing = records.find((record) => record.owner === owner);
|
|
514
685
|
if (existing)
|
|
515
686
|
return this.validateWorktree(owner);
|
|
@@ -564,6 +735,25 @@ export class RunStore {
|
|
|
564
735
|
this.worktreeWrite = write.then(() => undefined, () => undefined);
|
|
565
736
|
return write;
|
|
566
737
|
}
|
|
738
|
+
async resolveNamedWorktreeFromParent(name, parentRunId) {
|
|
739
|
+
const source = await this.sourceRun(parentRunId);
|
|
740
|
+
return source.findNamedWorktree(name, new Set([this.runId]));
|
|
741
|
+
}
|
|
742
|
+
async bindBorrowedWorktree(binding) {
|
|
743
|
+
const write = this.borrowedWorktreeWrite.then(async () => {
|
|
744
|
+
const records = [...await this.borrowedWorktreeRecords(false)];
|
|
745
|
+
const existing = records.find((candidate) => candidate.name === binding.name);
|
|
746
|
+
if (existing) {
|
|
747
|
+
if (JSON.stringify(existing) !== JSON.stringify(binding))
|
|
748
|
+
throw new WorkflowError("WORKTREE_FAILED", `Borrowed worktree binding for ${binding.name} changed`);
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
records.push(binding);
|
|
752
|
+
await atomicJson(join(this.directory, "borrowed-worktrees.json"), records);
|
|
753
|
+
});
|
|
754
|
+
this.borrowedWorktreeWrite = write.then(() => undefined, () => undefined);
|
|
755
|
+
await write;
|
|
756
|
+
}
|
|
567
757
|
async snapshotWorktree(owner) {
|
|
568
758
|
try {
|
|
569
759
|
const write = this.snapshotWrite.then(async () => {
|
|
@@ -593,13 +783,63 @@ export class RunStore {
|
|
|
593
783
|
async worktrees() {
|
|
594
784
|
const records = await json(join(this.directory, "worktrees.json")).catch((error) => { if (error.code === "ENOENT")
|
|
595
785
|
return []; throw error; });
|
|
596
|
-
const
|
|
786
|
+
const bindings = await this.borrowedWorktreeRecords();
|
|
787
|
+
const boundOwners = new Set(bindings.map((binding) => binding.owner));
|
|
788
|
+
const owned = await Promise.all(records.filter((record) => !boundOwners.has(record.owner)).map(async (record) => { try {
|
|
597
789
|
return await this.validateWorktree(record.owner);
|
|
598
790
|
}
|
|
599
791
|
catch {
|
|
600
792
|
return undefined;
|
|
601
793
|
} }));
|
|
602
|
-
|
|
794
|
+
const borrowed = await Promise.all(bindings.map(async (binding) => (await this.resolveBorrowedWorktree(binding, new Set([this.runId]))).reference));
|
|
795
|
+
return [...owned.filter((record) => record !== undefined), ...borrowed];
|
|
796
|
+
}
|
|
797
|
+
async validNamedWorktrees() {
|
|
798
|
+
const load = async (path) => {
|
|
799
|
+
try {
|
|
800
|
+
return await json(path);
|
|
801
|
+
}
|
|
802
|
+
catch (error) {
|
|
803
|
+
if (error.code === "ENOENT")
|
|
804
|
+
return [];
|
|
805
|
+
throw error;
|
|
806
|
+
}
|
|
807
|
+
};
|
|
808
|
+
const names = new Set();
|
|
809
|
+
const rawRecords = await load(join(this.directory, "worktrees.json"));
|
|
810
|
+
let bindings;
|
|
811
|
+
try {
|
|
812
|
+
bindings = await this.borrowedWorktreeRecords();
|
|
813
|
+
}
|
|
814
|
+
catch (error) {
|
|
815
|
+
if (error instanceof WorkflowError && error.code === "WORKTREE_FAILED")
|
|
816
|
+
return [];
|
|
817
|
+
throw error;
|
|
818
|
+
}
|
|
819
|
+
const boundOwners = new Set(bindings.map((binding) => binding.owner));
|
|
820
|
+
for (const record of Array.isArray(rawRecords) ? rawRecords : []) {
|
|
821
|
+
if (!record || typeof record !== "object")
|
|
822
|
+
continue;
|
|
823
|
+
const owner = record.owner;
|
|
824
|
+
if (typeof owner !== "string")
|
|
825
|
+
continue;
|
|
826
|
+
const name = this.worktreeName(owner);
|
|
827
|
+
if (!name || owner !== this.namedWorktreeOwner(name) || boundOwners.has(owner))
|
|
828
|
+
continue;
|
|
829
|
+
try {
|
|
830
|
+
await this.ownedWorktree(owner);
|
|
831
|
+
names.add(name);
|
|
832
|
+
}
|
|
833
|
+
catch { /* Do not advertise stale or invalid records. */ }
|
|
834
|
+
}
|
|
835
|
+
for (const binding of bindings) {
|
|
836
|
+
try {
|
|
837
|
+
await this.resolveBorrowedWorktree(binding, new Set([this.runId]));
|
|
838
|
+
names.add(binding.name);
|
|
839
|
+
}
|
|
840
|
+
catch { /* Do not advertise stale inherited records. */ }
|
|
841
|
+
}
|
|
842
|
+
return [...names];
|
|
603
843
|
}
|
|
604
844
|
async changedWorktrees() {
|
|
605
845
|
const changed = [];
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { JsonValue, RegisteredAgentSetupHook, WorkflowCatalog, WorkflowCatalogError, WorkflowCatalogFunction, WorkflowCatalogIndex, WorkflowCatalogVariable, WorkflowExtension, WorkflowFunction, WorkflowFunctionContext, WorkflowJournal, WorkflowVariable } from "./types.js";
|
|
2
|
+
export declare class WorkflowRegistry {
|
|
3
|
+
#private;
|
|
4
|
+
get frozen(): boolean;
|
|
5
|
+
freeze(): void;
|
|
6
|
+
register(extension: WorkflowExtension): void;
|
|
7
|
+
function(name: string): WorkflowFunction;
|
|
8
|
+
functions(): Readonly<Record<string, WorkflowFunction>>;
|
|
9
|
+
catalog(): WorkflowCatalog;
|
|
10
|
+
catalogIndex(): WorkflowCatalogIndex;
|
|
11
|
+
catalogDetail(name: string): WorkflowCatalogFunction | WorkflowCatalogVariable | WorkflowCatalogError;
|
|
12
|
+
globals(): Readonly<Record<string, {
|
|
13
|
+
name: string;
|
|
14
|
+
}>>;
|
|
15
|
+
invokeFunction(name: string, input: unknown, context: Readonly<WorkflowFunctionContext>, path: string, journal: WorkflowJournal): Promise<JsonValue>;
|
|
16
|
+
variables(): readonly {
|
|
17
|
+
name: string;
|
|
18
|
+
variable: WorkflowVariable;
|
|
19
|
+
}[];
|
|
20
|
+
agentSetupHooks(): readonly RegisteredAgentSetupHook[];
|
|
21
|
+
}
|
|
22
|
+
export type WorkflowRegistryApi = Pick<WorkflowRegistry, "frozen" | "freeze" | "register" | "function" | "functions" | "catalog" | "catalogIndex" | "catalogDetail" | "globals" | "invokeFunction" | "variables" | "agentSetupHooks">;
|
|
23
|
+
export declare function resetWorkflowRegistry(): void;
|
|
24
|
+
export declare function beginWorkflowExtensionLoading(): void;
|
|
25
|
+
export declare function loadingRegistry(): WorkflowRegistryApi;
|
|
26
|
+
export declare function registerWorkflowExtension(extension: WorkflowExtension): void;
|
|
27
|
+
export declare function workflowCatalog(): WorkflowCatalog;
|
|
28
|
+
export declare function workflowCatalogIndex(): WorkflowCatalogIndex;
|
|
29
|
+
export declare function workflowCatalogDetail(name: string): WorkflowCatalogFunction | WorkflowCatalogVariable | WorkflowCatalogError;
|
|
30
|
+
export declare function registeredWorkflowFunctions(): Readonly<Record<string, WorkflowFunction>>;
|
|
31
|
+
export type { WorkflowCatalog, WorkflowCatalogError, WorkflowCatalogFunction, WorkflowCatalogIndex, WorkflowCatalogIndexFunction, WorkflowCatalogIndexVariable, WorkflowCatalogVariable } from "./types.js";
|