pi-extensible-workflows 2.0.0 → 3.1.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 +18 -11
- package/dist/src/agent-execution.d.ts +4 -94
- package/dist/src/agent-execution.js +34 -208
- package/dist/src/budget.d.ts +38 -0
- package/dist/src/budget.js +160 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +60 -8
- package/dist/src/doctor-cleanup.d.ts +41 -0
- package/dist/src/doctor-cleanup.js +597 -0
- package/dist/src/doctor.d.ts +7 -2
- package/dist/src/doctor.js +127 -22
- package/dist/src/execution.d.ts +17 -0
- package/dist/src/execution.js +630 -0
- package/dist/src/host.d.ts +101 -0
- package/dist/src/host.js +3084 -0
- package/dist/src/index.d.ts +13 -634
- package/dist/src/index.js +10 -4432
- package/dist/src/persistence.d.ts +9 -19
- package/dist/src/persistence.js +175 -61
- package/dist/src/registry.d.ts +43 -0
- package/dist/src/registry.js +279 -0
- package/dist/src/session-inspector.js +5 -3
- package/dist/src/types.d.ts +692 -0
- package/dist/src/types.js +27 -0
- package/dist/src/utils.d.ts +32 -0
- package/dist/src/utils.js +168 -0
- package/dist/src/validation.d.ts +28 -0
- package/dist/src/validation.js +832 -0
- package/package.json +3 -2
- package/skills/pi-extensible-workflows/SKILL.md +69 -34
- package/src/agent-execution.ts +37 -189
- package/src/budget.ts +75 -0
- package/src/cli.ts +47 -7
- package/src/doctor-cleanup.ts +337 -0
- package/src/doctor.ts +117 -24
- package/src/execution.ts +540 -0
- package/src/host.ts +2598 -0
- package/src/index.ts +14 -3856
- package/src/persistence.ts +122 -37
- package/src/registry.ts +240 -0
- package/src/session-inspector.ts +5 -3
- package/src/types.ts +158 -0
- package/src/utils.ts +142 -0
- package/src/validation.ts +688 -0
|
@@ -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
|
}
|
|
@@ -53,7 +39,9 @@ export interface BorrowedWorktreeBinding {
|
|
|
53
39
|
owner: string;
|
|
54
40
|
}
|
|
55
41
|
export declare function projectStorageKey(cwd: string): string;
|
|
42
|
+
export declare function projectSessionsDirectory(cwd: string, home?: string): string;
|
|
56
43
|
export declare function runsDirectory(cwd: string, sessionId: string, home?: string): string;
|
|
44
|
+
export declare function hasLiveSessionLease(cwd: string, sessionId: string, home?: string): Promise<boolean>;
|
|
57
45
|
export declare class SessionLease {
|
|
58
46
|
#private;
|
|
59
47
|
readonly path: string;
|
|
@@ -80,7 +68,6 @@ export declare class RunStore {
|
|
|
80
68
|
private snapshotWrite;
|
|
81
69
|
private launchSnapshotWrite;
|
|
82
70
|
private systemPromptWrite;
|
|
83
|
-
private conversationWrite;
|
|
84
71
|
constructor(cwd: string, sessionId: string, runId: string, home?: string);
|
|
85
72
|
create(run: PersistedRun, snapshot: Readonly<LaunchSnapshot>): Promise<void>;
|
|
86
73
|
isComplete(): Promise<boolean>;
|
|
@@ -97,12 +84,11 @@ export declare class RunStore {
|
|
|
97
84
|
systemPromptPath(): string;
|
|
98
85
|
recordSystemPrompt(entry: Omit<EffectiveSystemPrompt, "sha256">): Promise<void>;
|
|
99
86
|
systemPrompts(): Promise<readonly EffectiveSystemPrompt[]>;
|
|
100
|
-
conversationPath(): string;
|
|
101
|
-
conversation(id: string): Promise<PersistedConversation | undefined>;
|
|
102
|
-
saveConversation(conversation: PersistedConversation): Promise<void>;
|
|
103
87
|
private updateJournal;
|
|
104
88
|
complete(path: string, value: JsonValue): Promise<void>;
|
|
105
89
|
replay(path: string): Promise<CompletedOperation | undefined>;
|
|
90
|
+
replayableOperations(): Promise<readonly CompletedOperation[]>;
|
|
91
|
+
private replayableOperationsFrom;
|
|
106
92
|
awaitCheckpoint(checkpoint: AwaitingCheckpoint): Promise<boolean | undefined>;
|
|
107
93
|
awaitingCheckpoints(): Promise<readonly AwaitingCheckpoint[]>;
|
|
108
94
|
requestWorkflowDecision(request: PendingWorkflowDecision): Promise<void>;
|
|
@@ -119,6 +105,7 @@ export declare class RunStore {
|
|
|
119
105
|
private borrowedWorktree;
|
|
120
106
|
private sourceRun;
|
|
121
107
|
validateParentRun(parentRunId: string): Promise<void>;
|
|
108
|
+
validateRetrySource(): Promise<void>;
|
|
122
109
|
private ownedWorktree;
|
|
123
110
|
private resolveBorrowedWorktree;
|
|
124
111
|
private findNamedWorktree;
|
|
@@ -127,7 +114,9 @@ export declare class RunStore {
|
|
|
127
114
|
sourceRunId: string;
|
|
128
115
|
owner: string;
|
|
129
116
|
}>;
|
|
117
|
+
validateDeletionWorktrees(): Promise<void>;
|
|
130
118
|
validateBorrowedWorktrees(): Promise<void>;
|
|
119
|
+
validateNamedWorktrees(): Promise<void>;
|
|
131
120
|
ownsWorktree(owner: string): Promise<boolean>;
|
|
132
121
|
private cleanupMarker;
|
|
133
122
|
private cleanupOrphanWorktrees;
|
|
@@ -137,6 +126,7 @@ export declare class RunStore {
|
|
|
137
126
|
private bindBorrowedWorktree;
|
|
138
127
|
snapshotWorktree(owner: string): Promise<string>;
|
|
139
128
|
worktrees(): Promise<readonly WorktreeReference[]>;
|
|
129
|
+
validNamedWorktrees(): Promise<readonly string[]>;
|
|
140
130
|
changedWorktrees(): Promise<readonly WorktreeReference[]>;
|
|
141
131
|
saveResult(value: JsonValue): Promise<string>;
|
|
142
132
|
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",
|
|
@@ -23,8 +18,11 @@ export function projectStorageKey(cwd) {
|
|
|
23
18
|
const slug = safePart(basename(exact)) || "root";
|
|
24
19
|
return `${slug}-${createHash("sha256").update(exact).digest("hex").slice(0, 12)}`;
|
|
25
20
|
}
|
|
21
|
+
export function projectSessionsDirectory(cwd, home = homedir()) {
|
|
22
|
+
return join(home, ".pi", "workflows", "projects", projectStorageKey(cwd), "sessions");
|
|
23
|
+
}
|
|
26
24
|
export function runsDirectory(cwd, sessionId, home = homedir()) {
|
|
27
|
-
return join(
|
|
25
|
+
return join(projectSessionsDirectory(cwd, home), safePart(sessionId), "runs");
|
|
28
26
|
}
|
|
29
27
|
const SESSION_OWNER_FILE = "owner.json";
|
|
30
28
|
const SESSION_OWNER_WRITE_GRACE_MS = 30_000;
|
|
@@ -48,6 +46,24 @@ async function processAlive(pid, startedAt) {
|
|
|
48
46
|
}
|
|
49
47
|
return true;
|
|
50
48
|
}
|
|
49
|
+
export async function hasLiveSessionLease(cwd, sessionId, home = homedir()) {
|
|
50
|
+
const path = join(runsDirectory(cwd, sessionId, home), SESSION_OWNER_FILE);
|
|
51
|
+
let owner;
|
|
52
|
+
try {
|
|
53
|
+
owner = JSON.parse(await readFile(path, "utf8"));
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
if (error.code === "ENOENT")
|
|
57
|
+
return false;
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
if (!owner || typeof owner !== "object" || Array.isArray(owner))
|
|
61
|
+
throw new WorkflowError("RUN_OWNED", `Pi session ${sessionId} has an invalid ownership lease`);
|
|
62
|
+
const candidate = owner;
|
|
63
|
+
if (typeof candidate.pid !== "number" || !Number.isInteger(candidate.pid) || candidate.pid < 1 || typeof candidate.token !== "string" || !candidate.token || typeof candidate.startedAt !== "number" || !Number.isFinite(candidate.startedAt))
|
|
64
|
+
throw new WorkflowError("RUN_OWNED", `Pi session ${sessionId} has an invalid ownership lease`);
|
|
65
|
+
return processAlive(candidate.pid, candidate.startedAt);
|
|
66
|
+
}
|
|
51
67
|
function sameOwner(left, right) {
|
|
52
68
|
if (!left || typeof left !== "object" || !right || typeof right !== "object")
|
|
53
69
|
return false;
|
|
@@ -226,7 +242,6 @@ export class RunStore {
|
|
|
226
242
|
launchSnapshotWrite = Promise.resolve();
|
|
227
243
|
// ponytail: the session lease prevents concurrent RunStore writers for one run.
|
|
228
244
|
systemPromptWrite = Promise.resolve();
|
|
229
|
-
conversationWrite = Promise.resolve();
|
|
230
245
|
constructor(cwd, sessionId, runId, home = homedir()) {
|
|
231
246
|
this.cwd = cwd;
|
|
232
247
|
this.sessionId = sessionId;
|
|
@@ -250,7 +265,6 @@ export class RunStore {
|
|
|
250
265
|
await atomicJson(join(temporary, "borrowed-worktrees.json"), []);
|
|
251
266
|
await atomicJson(join(temporary, "state.json"), run);
|
|
252
267
|
await atomicJson(join(temporary, "system-prompts.json"), { version: 1, entries: [] });
|
|
253
|
-
await atomicJson(join(temporary, "conversations.json"), { version: 1, conversations: {} });
|
|
254
268
|
await rename(temporary, this.directory);
|
|
255
269
|
}
|
|
256
270
|
catch (error) {
|
|
@@ -329,54 +343,6 @@ export class RunStore {
|
|
|
329
343
|
return (await json(this.systemPromptPath()).catch((error) => { if (error.code === "ENOENT")
|
|
330
344
|
return { version: 1, entries: [] }; throw error; })).entries;
|
|
331
345
|
}
|
|
332
|
-
conversationPath() { return join(this.directory, "conversations.json"); }
|
|
333
|
-
async conversation(id) {
|
|
334
|
-
await this.conversationWrite;
|
|
335
|
-
let artifact;
|
|
336
|
-
try {
|
|
337
|
-
const raw = await json(this.conversationPath());
|
|
338
|
-
if (!isConversationArtifact(raw))
|
|
339
|
-
throw new WorkflowError("RESUME_INCOMPATIBLE", "Conversation state is corrupt");
|
|
340
|
-
artifact = raw;
|
|
341
|
-
}
|
|
342
|
-
catch (error) {
|
|
343
|
-
if (error.code === "ENOENT")
|
|
344
|
-
return undefined;
|
|
345
|
-
if (error instanceof WorkflowError)
|
|
346
|
-
throw error;
|
|
347
|
-
throw new WorkflowError("RESUME_INCOMPATIBLE", `Cannot load conversation state: ${error instanceof Error ? error.message : String(error)}`);
|
|
348
|
-
}
|
|
349
|
-
return artifact.conversations[id];
|
|
350
|
-
}
|
|
351
|
-
async saveConversation(conversation) {
|
|
352
|
-
const write = this.conversationWrite.then(async () => {
|
|
353
|
-
const path = this.conversationPath();
|
|
354
|
-
let artifact;
|
|
355
|
-
try {
|
|
356
|
-
const raw = await json(path);
|
|
357
|
-
if (!isConversationArtifact(raw))
|
|
358
|
-
throw new WorkflowError("RESUME_INCOMPATIBLE", "Conversation state is corrupt");
|
|
359
|
-
artifact = raw;
|
|
360
|
-
}
|
|
361
|
-
catch (error) {
|
|
362
|
-
if (error.code === "ENOENT")
|
|
363
|
-
artifact = { version: 1, conversations: {} };
|
|
364
|
-
else if (error instanceof WorkflowError)
|
|
365
|
-
throw error;
|
|
366
|
-
else
|
|
367
|
-
throw new WorkflowError("RESUME_INCOMPATIBLE", `Cannot load conversation state: ${error instanceof Error ? error.message : String(error)}`);
|
|
368
|
-
}
|
|
369
|
-
const previous = artifact.conversations[conversation.id];
|
|
370
|
-
if (previous && previous.head.turn + 1 !== conversation.head.turn)
|
|
371
|
-
throw new WorkflowError("RESUME_INCOMPATIBLE", `Conversation head is not the previous turn: ${conversation.id}`);
|
|
372
|
-
if (!previous && conversation.head.turn !== 1)
|
|
373
|
-
throw new WorkflowError("RESUME_INCOMPATIBLE", `Conversation must start at turn one: ${conversation.id}`);
|
|
374
|
-
artifact.conversations[conversation.id] = structuredClone(conversation);
|
|
375
|
-
await atomicJson(path, artifact);
|
|
376
|
-
});
|
|
377
|
-
this.conversationWrite = write.catch(() => undefined);
|
|
378
|
-
await write;
|
|
379
|
-
}
|
|
380
346
|
async updateJournal(update) {
|
|
381
347
|
let result;
|
|
382
348
|
const write = this.journalWrite.then(async () => {
|
|
@@ -398,10 +364,34 @@ export class RunStore {
|
|
|
398
364
|
});
|
|
399
365
|
}
|
|
400
366
|
async replay(path) {
|
|
367
|
+
const operations = await this.replayableOperations();
|
|
368
|
+
return operations.find((operation) => operation.path === path);
|
|
369
|
+
}
|
|
370
|
+
async replayableOperations() {
|
|
371
|
+
return this.replayableOperationsFrom(new Set());
|
|
372
|
+
}
|
|
373
|
+
async replayableOperationsFrom(seen) {
|
|
374
|
+
if (seen.has(this.runId))
|
|
375
|
+
throw new WorkflowError("RESUME_INCOMPATIBLE", "Retry provenance contains a cycle");
|
|
376
|
+
const nextSeen = new Set(seen);
|
|
377
|
+
nextSeen.add(this.runId);
|
|
401
378
|
await this.journalWrite;
|
|
402
|
-
|
|
379
|
+
const loaded = await this.load();
|
|
380
|
+
const operations = new Map();
|
|
381
|
+
if (loaded.run.retry?.sourceRunId) {
|
|
382
|
+
const source = await this.sourceRun(loaded.run.retry.sourceRunId);
|
|
383
|
+
for (const operation of await source.replayableOperationsFrom(nextSeen))
|
|
384
|
+
operations.set(operation.path, operation);
|
|
385
|
+
}
|
|
386
|
+
const journal = await json(join(this.directory, "journal.json"));
|
|
387
|
+
for (const operation of Object.values(journal.completed))
|
|
388
|
+
operations.set(operation.path, operation);
|
|
389
|
+
return [...operations.values()].map((operation) => structuredClone(operation));
|
|
403
390
|
}
|
|
404
391
|
async awaitCheckpoint(checkpoint) {
|
|
392
|
+
const replayed = await this.replay(checkpoint.path);
|
|
393
|
+
if (replayed)
|
|
394
|
+
return replayed.value;
|
|
405
395
|
return this.updateJournal((journal) => {
|
|
406
396
|
const completed = journal.completed[checkpoint.path];
|
|
407
397
|
if (completed)
|
|
@@ -523,6 +513,36 @@ export class RunStore {
|
|
|
523
513
|
}
|
|
524
514
|
}
|
|
525
515
|
async validateParentRun(parentRunId) { await this.sourceRun(parentRunId); }
|
|
516
|
+
async validateRetrySource() {
|
|
517
|
+
const validate = async (current, seen) => {
|
|
518
|
+
if (seen.has(current.runId))
|
|
519
|
+
throw new WorkflowError("RESUME_INCOMPATIBLE", "Retry provenance contains a cycle");
|
|
520
|
+
const nextSeen = new Set(seen);
|
|
521
|
+
nextSeen.add(current.runId);
|
|
522
|
+
const loaded = await current.load();
|
|
523
|
+
const retry = loaded.run.retry;
|
|
524
|
+
if (!retry)
|
|
525
|
+
return;
|
|
526
|
+
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"))
|
|
527
|
+
throw new WorkflowError("RESUME_INCOMPATIBLE", "Retry provenance is incomplete");
|
|
528
|
+
const source = await current.sourceRun(retry.sourceRunId);
|
|
529
|
+
const sourceRun = (await source.load()).run;
|
|
530
|
+
if (loaded.run.parentRunId !== retry.sourceRunId)
|
|
531
|
+
throw new WorkflowError("RESUME_INCOMPATIBLE", "Retry parent run does not match its source run");
|
|
532
|
+
if (sourceRun.state !== "failed")
|
|
533
|
+
throw new WorkflowError("RESUME_INCOMPATIBLE", `Retry source run ${retry.sourceRunId} is not failed`);
|
|
534
|
+
const expectedLineageRoot = sourceRun.retry?.lineageRootRunId ?? sourceRun.id;
|
|
535
|
+
if (retry.lineageRootRunId !== expectedLineageRoot)
|
|
536
|
+
throw new WorkflowError("RESUME_INCOMPATIBLE", "Retry lineage root does not match its source run");
|
|
537
|
+
await validate(source, nextSeen);
|
|
538
|
+
};
|
|
539
|
+
try {
|
|
540
|
+
await validate(this, new Set());
|
|
541
|
+
}
|
|
542
|
+
catch (error) {
|
|
543
|
+
throw error instanceof WorkflowError && error.code === "RESUME_INCOMPATIBLE" ? error : new WorkflowError("RESUME_INCOMPATIBLE", error instanceof Error ? error.message : String(error));
|
|
544
|
+
}
|
|
545
|
+
}
|
|
526
546
|
async ownedWorktree(owner, cwd) {
|
|
527
547
|
const records = await json(join(this.directory, "worktrees.json"));
|
|
528
548
|
const matches = records.filter((candidate) => candidate && typeof candidate === "object" && candidate.owner === owner);
|
|
@@ -567,8 +587,13 @@ export class RunStore {
|
|
|
567
587
|
}
|
|
568
588
|
const records = await json(join(this.directory, "worktrees.json"));
|
|
569
589
|
const matches = records.filter((candidate) => candidate && typeof candidate === "object" && candidate.owner === owner);
|
|
570
|
-
if (matches.length === 0)
|
|
571
|
-
|
|
590
|
+
if (matches.length === 0) {
|
|
591
|
+
const loaded = await this.load();
|
|
592
|
+
if (loaded.run.parentRunId === undefined)
|
|
593
|
+
return undefined;
|
|
594
|
+
const parent = await this.sourceRun(loaded.run.parentRunId);
|
|
595
|
+
return parent.findNamedWorktree(name, nextSeen);
|
|
596
|
+
}
|
|
572
597
|
try {
|
|
573
598
|
const reference = await this.ownedWorktree(owner);
|
|
574
599
|
return { reference, sourceRunId: this.runId, owner };
|
|
@@ -583,6 +608,33 @@ export class RunStore {
|
|
|
583
608
|
throw new WorkflowError("WORKTREE_FAILED", `Missing named worktree ${name}`);
|
|
584
609
|
return resolved;
|
|
585
610
|
}
|
|
611
|
+
async validateDeletionWorktrees() {
|
|
612
|
+
try {
|
|
613
|
+
const records = await json(join(this.directory, "worktrees.json"));
|
|
614
|
+
if (!Array.isArray(records))
|
|
615
|
+
throw new Error("Worktree records are invalid");
|
|
616
|
+
const owners = new Set();
|
|
617
|
+
const paths = new Set();
|
|
618
|
+
records.forEach((record) => {
|
|
619
|
+
if (!record || typeof record !== "object" || typeof record.owner !== "string")
|
|
620
|
+
throw new Error("Invalid worktree record");
|
|
621
|
+
const owner = record.owner;
|
|
622
|
+
if (owners.has(owner))
|
|
623
|
+
throw new Error(`Duplicate worktree record for ${owner}`);
|
|
624
|
+
owners.add(owner);
|
|
625
|
+
const reference = this.structuralWorktree(owner, record);
|
|
626
|
+
paths.add(resolve(reference.path));
|
|
627
|
+
});
|
|
628
|
+
const entries = await readdir(join(this.directory, "worktrees"), { withFileTypes: true }).catch((error) => { if (error.code === "ENOENT")
|
|
629
|
+
return []; throw error; });
|
|
630
|
+
for (const entry of entries)
|
|
631
|
+
if (!entry.isDirectory() || entry.isSymbolicLink() || !paths.has(resolve(join(this.directory, "worktrees", entry.name))))
|
|
632
|
+
throw new Error(`Unrecorded worktree artifact: ${join(this.directory, "worktrees", entry.name)}`);
|
|
633
|
+
}
|
|
634
|
+
catch (error) {
|
|
635
|
+
throw new WorkflowError("WORKTREE_FAILED", error instanceof Error ? error.message : String(error));
|
|
636
|
+
}
|
|
637
|
+
}
|
|
586
638
|
async validateBorrowedWorktrees() {
|
|
587
639
|
try {
|
|
588
640
|
const loaded = await this.load();
|
|
@@ -595,6 +647,19 @@ export class RunStore {
|
|
|
595
647
|
throw error instanceof WorkflowError && error.code === "WORKTREE_FAILED" ? error : new WorkflowError("WORKTREE_FAILED", error instanceof Error ? error.message : String(error));
|
|
596
648
|
}
|
|
597
649
|
}
|
|
650
|
+
async validateNamedWorktrees() {
|
|
651
|
+
try {
|
|
652
|
+
const records = await json(join(this.directory, "worktrees.json"));
|
|
653
|
+
for (const record of records) {
|
|
654
|
+
const owner = record && typeof record === "object" && typeof record.owner === "string" ? record.owner : undefined;
|
|
655
|
+
if (owner && this.worktreeName(owner))
|
|
656
|
+
await this.validateWorktree(owner);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
catch (error) {
|
|
660
|
+
throw error instanceof WorkflowError && error.code === "WORKTREE_FAILED" ? error : new WorkflowError("WORKTREE_FAILED", error instanceof Error ? error.message : String(error));
|
|
661
|
+
}
|
|
662
|
+
}
|
|
598
663
|
async ownsWorktree(owner) {
|
|
599
664
|
const records = await json(join(this.directory, "worktrees.json"));
|
|
600
665
|
return records.filter((candidate) => candidate && typeof candidate === "object" && candidate.owner === owner).length === 1;
|
|
@@ -662,6 +727,8 @@ export class RunStore {
|
|
|
662
727
|
return resolved.reference;
|
|
663
728
|
}
|
|
664
729
|
}
|
|
730
|
+
if (name && Array.isArray(loaded.run.retry?.namedWorktrees) && loaded.run.retry.namedWorktrees.includes(name))
|
|
731
|
+
throw new WorkflowError("WORKTREE_FAILED", `Missing inherited named worktree ${name}`);
|
|
665
732
|
const existing = records.find((record) => record.owner === owner);
|
|
666
733
|
if (existing)
|
|
667
734
|
return this.validateWorktree(owner);
|
|
@@ -775,6 +842,53 @@ export class RunStore {
|
|
|
775
842
|
const borrowed = await Promise.all(bindings.map(async (binding) => (await this.resolveBorrowedWorktree(binding, new Set([this.runId]))).reference));
|
|
776
843
|
return [...owned.filter((record) => record !== undefined), ...borrowed];
|
|
777
844
|
}
|
|
845
|
+
async validNamedWorktrees() {
|
|
846
|
+
const load = async (path) => {
|
|
847
|
+
try {
|
|
848
|
+
return await json(path);
|
|
849
|
+
}
|
|
850
|
+
catch (error) {
|
|
851
|
+
if (error.code === "ENOENT")
|
|
852
|
+
return [];
|
|
853
|
+
throw error;
|
|
854
|
+
}
|
|
855
|
+
};
|
|
856
|
+
const names = new Set();
|
|
857
|
+
const rawRecords = await load(join(this.directory, "worktrees.json"));
|
|
858
|
+
let bindings;
|
|
859
|
+
try {
|
|
860
|
+
bindings = await this.borrowedWorktreeRecords();
|
|
861
|
+
}
|
|
862
|
+
catch (error) {
|
|
863
|
+
if (error instanceof WorkflowError && error.code === "WORKTREE_FAILED")
|
|
864
|
+
return [];
|
|
865
|
+
throw error;
|
|
866
|
+
}
|
|
867
|
+
const boundOwners = new Set(bindings.map((binding) => binding.owner));
|
|
868
|
+
for (const record of Array.isArray(rawRecords) ? rawRecords : []) {
|
|
869
|
+
if (!record || typeof record !== "object")
|
|
870
|
+
continue;
|
|
871
|
+
const owner = record.owner;
|
|
872
|
+
if (typeof owner !== "string")
|
|
873
|
+
continue;
|
|
874
|
+
const name = this.worktreeName(owner);
|
|
875
|
+
if (!name || owner !== this.namedWorktreeOwner(name) || boundOwners.has(owner))
|
|
876
|
+
continue;
|
|
877
|
+
try {
|
|
878
|
+
await this.ownedWorktree(owner);
|
|
879
|
+
names.add(name);
|
|
880
|
+
}
|
|
881
|
+
catch { /* Do not advertise stale or invalid records. */ }
|
|
882
|
+
}
|
|
883
|
+
for (const binding of bindings) {
|
|
884
|
+
try {
|
|
885
|
+
await this.resolveBorrowedWorktree(binding, new Set([this.runId]));
|
|
886
|
+
names.add(binding.name);
|
|
887
|
+
}
|
|
888
|
+
catch { /* Do not advertise stale inherited records. */ }
|
|
889
|
+
}
|
|
890
|
+
return [...names];
|
|
891
|
+
}
|
|
778
892
|
async changedWorktrees() {
|
|
779
893
|
const changed = [];
|
|
780
894
|
for (const valid of await this.worktrees()) {
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { JsonValue, RegisteredAgentSetupHook, WorkflowCatalog, WorkflowCatalogContext, WorkflowCatalogError, WorkflowCatalogFunction, WorkflowCatalogIndex, WorkflowCatalogModelAlias, WorkflowCatalogVariable, WorkflowExtension, WorkflowFunction, WorkflowFunctionContext, WorkflowJournal, WorkflowModelAlias, WorkflowModelAliasResolverContext, WorkflowRoleDirectoryRegistration, 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(context?: WorkflowCatalogContext): WorkflowCatalog;
|
|
10
|
+
catalogIndex(context?: WorkflowCatalogContext): WorkflowCatalogIndex;
|
|
11
|
+
catalogDetail(name: string, context?: WorkflowCatalogContext): WorkflowCatalogFunction | WorkflowCatalogVariable | WorkflowCatalogModelAlias | 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
|
+
roleDirectories(): readonly string[];
|
|
22
|
+
roleDirectoryRegistrations(): readonly WorkflowRoleDirectoryRegistration[];
|
|
23
|
+
modelAliases(): readonly {
|
|
24
|
+
name: string;
|
|
25
|
+
version: string;
|
|
26
|
+
headline: string;
|
|
27
|
+
extensionDescription: string;
|
|
28
|
+
resolve: WorkflowModelAlias["resolve"];
|
|
29
|
+
}[];
|
|
30
|
+
resolveModelAliases(context: Readonly<WorkflowModelAliasResolverContext>, shadowed?: ReadonlySet<string>): Promise<Readonly<Record<string, string>>>;
|
|
31
|
+
}
|
|
32
|
+
export type WorkflowRegistryApi = Pick<WorkflowRegistry, "frozen" | "freeze" | "register" | "function" | "functions" | "catalog" | "catalogIndex" | "catalogDetail" | "globals" | "invokeFunction" | "variables" | "modelAliases" | "resolveModelAliases" | "agentSetupHooks" | "roleDirectories" | "roleDirectoryRegistrations">;
|
|
33
|
+
export declare function resetWorkflowRegistry(): void;
|
|
34
|
+
export declare function beginWorkflowExtensionLoading(): void;
|
|
35
|
+
export declare function loadingRegistry(): WorkflowRegistryApi;
|
|
36
|
+
export declare function registerWorkflowExtension(extension: WorkflowExtension): void;
|
|
37
|
+
export declare function workflowCatalog(context?: WorkflowCatalogContext): WorkflowCatalog;
|
|
38
|
+
export declare function workflowCatalogIndex(context?: WorkflowCatalogContext): WorkflowCatalogIndex;
|
|
39
|
+
export declare function workflowCatalogDetail(name: string, context?: WorkflowCatalogContext): WorkflowCatalogFunction | WorkflowCatalogVariable | WorkflowCatalogModelAlias | WorkflowCatalogError;
|
|
40
|
+
export declare function registeredWorkflowFunctions(): Readonly<Record<string, WorkflowFunction>>;
|
|
41
|
+
export declare function registeredWorkflowRoleDirectories(): readonly string[];
|
|
42
|
+
export declare function registeredWorkflowRoleDirectoryRegistrations(): readonly WorkflowRoleDirectoryRegistration[];
|
|
43
|
+
export type { WorkflowCatalog, WorkflowCatalogContext, WorkflowCatalogError, WorkflowCatalogFunction, WorkflowCatalogIndex, WorkflowCatalogIndexFunction, WorkflowCatalogIndexVariable, WorkflowCatalogModelAlias, WorkflowCatalogSettings, WorkflowCatalogVariable, WorkflowRoleDirectoryRegistration } from "./types.js";
|