@agwab/pi-workflow 0.2.1 → 0.3.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/dist/compiler.js +6 -8
- package/dist/dynamic-decision.d.ts +0 -1
- package/dist/dynamic-decision.js +0 -7
- package/dist/dynamic-profiles.d.ts +0 -1
- package/dist/dynamic-profiles.js +0 -3
- package/dist/engine-run-graph.d.ts +1 -0
- package/dist/engine-run-graph.js +142 -2
- package/dist/engine.d.ts +5 -0
- package/dist/engine.js +112 -27
- package/dist/extension.d.ts +2 -1
- package/dist/extension.js +27 -6
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -1
- package/dist/store.js +55 -11
- package/dist/subagent-backend.js +155 -29
- package/dist/types.d.ts +6 -0
- package/dist/workflow-runtime.js +10 -1
- package/dist/workflow-view.js +3 -1
- package/dist/workflow-web-source-extension.js +167 -48
- package/dist/workflow-web-source.d.ts +2 -1
- package/dist/workflow-web-source.js +84 -19
- package/node_modules/@agwab/pi-subagent/README.md +3 -3
- package/node_modules/@agwab/pi-subagent/api.mjs +1 -0
- package/node_modules/@agwab/pi-subagent/docs/usage.md +63 -12
- package/node_modules/@agwab/pi-subagent/package.json +2 -2
- package/node_modules/@agwab/pi-subagent/src/api.ts +54 -1
- package/node_modules/@agwab/pi-subagent/src/artifacts/registry.ts +9 -4
- package/node_modules/@agwab/pi-subagent/src/artifacts/result.ts +8 -0
- package/node_modules/@agwab/pi-subagent/src/core/constants.ts +9 -0
- package/node_modules/@agwab/pi-subagent/src/core/validation.ts +21 -0
- package/node_modules/@agwab/pi-subagent/src/index.ts +995 -573
- package/node_modules/@agwab/pi-subagent/src/orchestrate/async.ts +279 -156
- package/node_modules/@agwab/pi-subagent/src/orchestrate/interrupt.ts +165 -89
- package/node_modules/@agwab/pi-subagent/src/orchestrate/reconcile.ts +111 -65
- package/node_modules/@agwab/pi-subagent/src/orchestrate/run-ref.ts +219 -0
- package/node_modules/@agwab/pi-subagent/src/orchestrate/run.ts +88 -8
- package/node_modules/@agwab/pi-subagent/src/orchestrate/status.ts +614 -298
- package/node_modules/@agwab/pi-subagent/src/panel.ts +1352 -560
- package/node_modules/@agwab/pi-subagent/src/runners/headless-model.ts +53 -5
- package/node_modules/@agwab/pi-subagent/src/runners/tmux.ts +13 -6
- package/package.json +2 -2
- package/src/compiler.ts +14 -9
- package/src/dynamic-decision.ts +0 -11
- package/src/dynamic-profiles.ts +0 -4
- package/src/engine-run-graph.ts +185 -2
- package/src/engine.ts +145 -24
- package/src/extension.ts +33 -4
- package/src/index.ts +3 -1
- package/src/store.ts +74 -11
- package/src/subagent-backend.ts +201 -28
- package/src/types.ts +6 -0
- package/src/workflow-runtime.ts +18 -2
- package/src/workflow-view.ts +2 -1
- package/src/workflow-web-source-extension.ts +621 -228
- package/src/workflow-web-source.ts +118 -28
- package/workflows/deep-research/helpers/claim-evidence-gate.mjs +56 -16
- package/workflows/deep-research/helpers/final-audit-packet.mjs +1 -4
- package/workflows/deep-research/helpers/normalize-input-packet.mjs +1 -1
- package/workflows/deep-research/helpers/render-executive.mjs +8 -21
- package/workflows/deep-research/helpers/sanitize-verification-candidates.mjs +89 -15
- package/workflows/deep-research/schemas/deep-research-executive-render-control.schema.json +0 -1
- package/workflows/deep-research/schemas/deep-research-verify-claims-control.schema.json +4 -1
- package/workflows/impact-review/spec.json +3 -3
- package/workflows/spec-review/helpers/spec-review-pipeline.mjs +1 -8
- package/dist/dynamic-loader.d.ts +0 -25
- package/dist/dynamic-loader.js +0 -13
- package/src/dynamic-loader.ts +0 -49
- package/workflows/impact-review/schemas/docs-release-impact-control.schema.json +0 -42
- package/workflows/impact-review/schemas/security-performance-impact-control.schema.json +0 -42
- package/workflows/impact-review/schemas/state-data-impact-control.schema.json +0 -42
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import {
|
|
2
|
+
mkdir,
|
|
3
|
+
readdir,
|
|
4
|
+
readFile,
|
|
5
|
+
rename,
|
|
6
|
+
stat,
|
|
7
|
+
writeFile,
|
|
8
|
+
} from "node:fs/promises";
|
|
9
|
+
import { homedir } from "node:os";
|
|
10
|
+
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
11
|
+
|
|
12
|
+
const DEFAULT_RUNS_DIR = ".pi/agent/runs";
|
|
13
|
+
const SAFE_ID_PATTERN = /^[A-Za-z0-9._-]+$/;
|
|
14
|
+
const RUN_LOCATOR_SCHEMA_VERSION = 1 as const;
|
|
15
|
+
|
|
16
|
+
export interface RunRefLocator {
|
|
17
|
+
schemaVersion: typeof RUN_LOCATOR_SCHEMA_VERSION;
|
|
18
|
+
runId: string;
|
|
19
|
+
cwd: string;
|
|
20
|
+
runsDir?: string;
|
|
21
|
+
parentSessionId?: string;
|
|
22
|
+
correlationId?: string;
|
|
23
|
+
updatedAt: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface LocatableRunRef {
|
|
27
|
+
cwd?: string;
|
|
28
|
+
runId: string;
|
|
29
|
+
runsDir?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface WriteRunLocatorOptions extends LocatableRunRef {
|
|
33
|
+
cwd: string;
|
|
34
|
+
parentSessionId?: string;
|
|
35
|
+
correlationId?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface RunLocatorListResult {
|
|
39
|
+
locators: RunRefLocator[];
|
|
40
|
+
invalidCount: number;
|
|
41
|
+
skippedCount: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function assertSafeId(name: string, value: string): void {
|
|
45
|
+
if (!SAFE_ID_PATTERN.test(value))
|
|
46
|
+
throw new Error(
|
|
47
|
+
`${name} must contain only letters, numbers, dots, underscores, or dashes.`,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function isInsideOrEqual(parent: string, child: string): boolean {
|
|
52
|
+
const childRelative = relative(parent, child);
|
|
53
|
+
return (
|
|
54
|
+
childRelative === "" ||
|
|
55
|
+
(!childRelative.startsWith("..") && !isAbsolute(childRelative))
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function normalizeCwd(cwd: string): string {
|
|
60
|
+
return resolve(cwd);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function assertSafeRunsDir(cwd: string, runsDir: string | undefined): void {
|
|
64
|
+
const absolute = resolve(cwd, runsDir ?? DEFAULT_RUNS_DIR);
|
|
65
|
+
if (!isInsideOrEqual(cwd, absolute))
|
|
66
|
+
throw new Error(
|
|
67
|
+
"runsDir must be inside cwd so lifecycle refs remain relative and safe.",
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function runIndexDir(): string {
|
|
72
|
+
const override = process.env.PI_SUBAGENT_RUN_INDEX_DIR;
|
|
73
|
+
return resolve(
|
|
74
|
+
override && override.length > 0
|
|
75
|
+
? override
|
|
76
|
+
: join(homedir(), ".pi", "agent", "subagent-runs"),
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function runLocatorPath(runId: string): string {
|
|
81
|
+
assertSafeId("runId", runId);
|
|
82
|
+
return join(runIndexDir(), `${runId}.json`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function isRunRefLocator(value: unknown): value is RunRefLocator {
|
|
86
|
+
return (
|
|
87
|
+
typeof value === "object" &&
|
|
88
|
+
value !== null &&
|
|
89
|
+
(value as { schemaVersion?: unknown }).schemaVersion ===
|
|
90
|
+
RUN_LOCATOR_SCHEMA_VERSION &&
|
|
91
|
+
typeof (value as { runId?: unknown }).runId === "string" &&
|
|
92
|
+
typeof (value as { cwd?: unknown }).cwd === "string"
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function localRunDirExists(
|
|
97
|
+
ref: LocatableRunRef,
|
|
98
|
+
defaultCwd: string,
|
|
99
|
+
): Promise<boolean> {
|
|
100
|
+
const cwd = normalizeCwd(ref.cwd ?? defaultCwd);
|
|
101
|
+
assertSafeRunsDir(cwd, ref.runsDir);
|
|
102
|
+
const runsDir = resolve(cwd, ref.runsDir ?? DEFAULT_RUNS_DIR);
|
|
103
|
+
const info = await stat(join(runsDir, ref.runId)).catch(() => null);
|
|
104
|
+
return info?.isDirectory() === true;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export async function writeRunLocator(
|
|
108
|
+
options: WriteRunLocatorOptions,
|
|
109
|
+
): Promise<void> {
|
|
110
|
+
assertSafeId("runId", options.runId);
|
|
111
|
+
const cwd = normalizeCwd(options.cwd);
|
|
112
|
+
assertSafeRunsDir(cwd, options.runsDir);
|
|
113
|
+
const locator: RunRefLocator = {
|
|
114
|
+
schemaVersion: RUN_LOCATOR_SCHEMA_VERSION,
|
|
115
|
+
runId: options.runId,
|
|
116
|
+
cwd,
|
|
117
|
+
...(options.runsDir === undefined ? {} : { runsDir: options.runsDir }),
|
|
118
|
+
...(options.parentSessionId === undefined
|
|
119
|
+
? {}
|
|
120
|
+
: { parentSessionId: options.parentSessionId }),
|
|
121
|
+
...(options.correlationId === undefined
|
|
122
|
+
? {}
|
|
123
|
+
: { correlationId: options.correlationId }),
|
|
124
|
+
updatedAt: new Date().toISOString(),
|
|
125
|
+
};
|
|
126
|
+
const path = runLocatorPath(options.runId);
|
|
127
|
+
await mkdir(dirname(path), { recursive: true });
|
|
128
|
+
const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
129
|
+
await writeFile(tempPath, `${JSON.stringify(locator, null, 2)}\n`);
|
|
130
|
+
await rename(tempPath, path);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export async function readRunLocator(
|
|
134
|
+
runId: string,
|
|
135
|
+
): Promise<RunRefLocator | null> {
|
|
136
|
+
const path = runLocatorPath(runId);
|
|
137
|
+
try {
|
|
138
|
+
const parsed = JSON.parse(await readFile(path, "utf8"));
|
|
139
|
+
if (!isRunRefLocator(parsed) || parsed.runId !== runId) return null;
|
|
140
|
+
const cwd = normalizeCwd(parsed.cwd);
|
|
141
|
+
assertSafeRunsDir(cwd, parsed.runsDir);
|
|
142
|
+
return { ...parsed, cwd };
|
|
143
|
+
} catch (error) {
|
|
144
|
+
if (
|
|
145
|
+
error &&
|
|
146
|
+
typeof error === "object" &&
|
|
147
|
+
"code" in error &&
|
|
148
|
+
error.code === "ENOENT"
|
|
149
|
+
)
|
|
150
|
+
return null;
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export async function listRunLocators(): Promise<RunLocatorListResult> {
|
|
156
|
+
const indexDir = runIndexDir();
|
|
157
|
+
const entries = await readdir(indexDir, { withFileTypes: true }).catch(
|
|
158
|
+
() => [],
|
|
159
|
+
);
|
|
160
|
+
const locators: RunRefLocator[] = [];
|
|
161
|
+
let invalidCount = 0;
|
|
162
|
+
let skippedCount = 0;
|
|
163
|
+
|
|
164
|
+
for (const entry of entries) {
|
|
165
|
+
if (!entry.isFile()) {
|
|
166
|
+
skippedCount += 1;
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if (!entry.name.endsWith(".json") || entry.name.endsWith(".tmp")) {
|
|
170
|
+
skippedCount += 1;
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
const runId = entry.name.slice(0, -".json".length);
|
|
174
|
+
try {
|
|
175
|
+
assertSafeId("runId", runId);
|
|
176
|
+
const path = join(indexDir, entry.name);
|
|
177
|
+
const info = await stat(path);
|
|
178
|
+
if (!info.isFile() || info.size > 64 * 1024) {
|
|
179
|
+
invalidCount += 1;
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
const parsed = JSON.parse(await readFile(path, "utf8"));
|
|
183
|
+
if (!isRunRefLocator(parsed) || parsed.runId !== runId) {
|
|
184
|
+
invalidCount += 1;
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
const cwd = normalizeCwd(parsed.cwd);
|
|
188
|
+
assertSafeRunsDir(cwd, parsed.runsDir);
|
|
189
|
+
locators.push({ ...parsed, cwd });
|
|
190
|
+
} catch {
|
|
191
|
+
invalidCount += 1;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return { locators, invalidCount, skippedCount };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export async function resolveRunRef<T extends LocatableRunRef>(
|
|
199
|
+
ref: T,
|
|
200
|
+
defaultCwd = process.cwd(),
|
|
201
|
+
): Promise<T & { cwd: string }> {
|
|
202
|
+
assertSafeId("runId", ref.runId);
|
|
203
|
+
const fallbackCwd = normalizeCwd(ref.cwd ?? defaultCwd);
|
|
204
|
+
if (ref.cwd !== undefined || ref.runsDir !== undefined)
|
|
205
|
+
return { ...ref, cwd: fallbackCwd };
|
|
206
|
+
if (await localRunDirExists(ref, fallbackCwd))
|
|
207
|
+
return { ...ref, cwd: fallbackCwd };
|
|
208
|
+
|
|
209
|
+
const locator = await readRunLocator(ref.runId);
|
|
210
|
+
if (locator !== null) {
|
|
211
|
+
return {
|
|
212
|
+
...ref,
|
|
213
|
+
cwd: locator.cwd,
|
|
214
|
+
...(locator.runsDir === undefined ? {} : { runsDir: locator.runsDir }),
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return { ...ref, cwd: fallbackCwd };
|
|
219
|
+
}
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
resolveWorkspace,
|
|
22
22
|
type ResolvedWorkspace,
|
|
23
23
|
} from "../workspace/worktree.ts";
|
|
24
|
+
import { writeRunLocator } from "./run-ref.ts";
|
|
24
25
|
|
|
25
26
|
export const DEFAULT_PARALLEL_CONCURRENCY = 4;
|
|
26
27
|
export const MAX_PARALLEL_TASKS = 12;
|
|
@@ -44,6 +45,10 @@ export interface ParallelRunResult {
|
|
|
44
45
|
runIds: string[];
|
|
45
46
|
results: ResultEnvelope[];
|
|
46
47
|
concurrency: number;
|
|
48
|
+
totalTasks: number;
|
|
49
|
+
startedCount: number;
|
|
50
|
+
skippedCount: number;
|
|
51
|
+
failFastTriggered: boolean;
|
|
47
52
|
}
|
|
48
53
|
|
|
49
54
|
export class SubagentToolAuthorityError extends Error {
|
|
@@ -63,6 +68,8 @@ function mergeTaskInput(
|
|
|
63
68
|
worktree: parent.worktree,
|
|
64
69
|
worktreePolicy: parent.worktreePolicy,
|
|
65
70
|
concurrency: undefined,
|
|
71
|
+
failFast: undefined,
|
|
72
|
+
cancelSiblingsOnFailure: undefined,
|
|
66
73
|
asyncDependency: undefined,
|
|
67
74
|
runsDir: parent.runsDir,
|
|
68
75
|
correlationId: parent.correlationId,
|
|
@@ -150,6 +157,11 @@ export async function runSubagentTask(
|
|
|
150
157
|
},
|
|
151
158
|
],
|
|
152
159
|
});
|
|
160
|
+
await writeRunLocator({
|
|
161
|
+
...runRef,
|
|
162
|
+
parentSessionId: input.parentSessionId,
|
|
163
|
+
correlationId: input.correlationId,
|
|
164
|
+
}).catch(() => undefined);
|
|
153
165
|
|
|
154
166
|
try {
|
|
155
167
|
const workspace = await resolveWorkspace({
|
|
@@ -319,29 +331,97 @@ export async function runParallelSubagentTasks(
|
|
|
319
331
|
const tasks = input.tasks;
|
|
320
332
|
const runCwd = resolve(input.cwd ?? cwd);
|
|
321
333
|
const concurrency = Math.min(parallelConcurrency(input), tasks.length);
|
|
322
|
-
const
|
|
334
|
+
const resultSlots: Array<ResultEnvelope | undefined> = new Array(
|
|
335
|
+
tasks.length,
|
|
336
|
+
);
|
|
337
|
+
const failFast =
|
|
338
|
+
input.failFast === true || input.cancelSiblingsOnFailure === true;
|
|
339
|
+
const cancelSiblings = input.cancelSiblingsOnFailure === true;
|
|
340
|
+
const controller = cancelSiblings ? new AbortController() : undefined;
|
|
341
|
+
const childSignal = controller?.signal ?? signal;
|
|
323
342
|
let nextIndex = 0;
|
|
343
|
+
let startedCount = 0;
|
|
344
|
+
let stopScheduling = false;
|
|
345
|
+
let failFastTriggered = false;
|
|
346
|
+
let parentAbortTriggered = false;
|
|
347
|
+
let siblingCancelTriggered = false;
|
|
348
|
+
const workerErrors: unknown[] = [];
|
|
349
|
+
|
|
350
|
+
function triggerFailFast(): void {
|
|
351
|
+
if (!failFast) return;
|
|
352
|
+
failFastTriggered = true;
|
|
353
|
+
stopScheduling = true;
|
|
354
|
+
if (cancelSiblings && !controller?.signal.aborted) {
|
|
355
|
+
siblingCancelTriggered = true;
|
|
356
|
+
controller?.abort();
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function onParentAbort(): void {
|
|
361
|
+
parentAbortTriggered = true;
|
|
362
|
+
controller?.abort();
|
|
363
|
+
}
|
|
364
|
+
if (controller !== undefined && signal !== undefined) {
|
|
365
|
+
if (signal.aborted) controller.abort();
|
|
366
|
+
else signal.addEventListener("abort", onParentAbort, { once: true });
|
|
367
|
+
}
|
|
324
368
|
|
|
325
369
|
async function worker(): Promise<void> {
|
|
326
370
|
while (true) {
|
|
371
|
+
if (stopScheduling) return;
|
|
327
372
|
const index = nextIndex;
|
|
328
373
|
nextIndex += 1;
|
|
329
374
|
if (index >= tasks.length) return;
|
|
375
|
+
startedCount += 1;
|
|
330
376
|
const taskInput = mergeTaskInput(input, tasks[index]!);
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
377
|
+
try {
|
|
378
|
+
const result = await runSubagentTask({
|
|
379
|
+
input: taskInput,
|
|
380
|
+
cwd: runCwd,
|
|
381
|
+
signal: childSignal,
|
|
382
|
+
taskIndex: index,
|
|
383
|
+
});
|
|
384
|
+
resultSlots[index] = result;
|
|
385
|
+
if (result.status !== "completed") triggerFailFast();
|
|
386
|
+
} catch (error) {
|
|
387
|
+
const siblingAbort =
|
|
388
|
+
controller?.signal.aborted === true &&
|
|
389
|
+
siblingCancelTriggered &&
|
|
390
|
+
!parentAbortTriggered;
|
|
391
|
+
triggerFailFast();
|
|
392
|
+
if (!failFast || !siblingAbort) throw error;
|
|
393
|
+
workerErrors.push(error);
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
337
396
|
}
|
|
338
397
|
}
|
|
339
398
|
|
|
340
|
-
|
|
399
|
+
try {
|
|
400
|
+
const workers = await Promise.allSettled(
|
|
401
|
+
Array.from({ length: concurrency }, () => worker()),
|
|
402
|
+
);
|
|
403
|
+
const rejected = workers.find(
|
|
404
|
+
(workerResult): workerResult is PromiseRejectedResult =>
|
|
405
|
+
workerResult.status === "rejected",
|
|
406
|
+
);
|
|
407
|
+
if (rejected !== undefined) throw rejected.reason;
|
|
408
|
+
if (workerErrors.length > 0) failFastTriggered = true;
|
|
409
|
+
} finally {
|
|
410
|
+
if (controller !== undefined && signal !== undefined)
|
|
411
|
+
signal.removeEventListener("abort", onParentAbort);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const results = resultSlots.filter(
|
|
415
|
+
(result): result is ResultEnvelope => result !== undefined,
|
|
416
|
+
);
|
|
341
417
|
return {
|
|
342
418
|
mode: "parallel",
|
|
343
419
|
runIds: results.map((result) => result.runId),
|
|
344
420
|
results,
|
|
345
421
|
concurrency,
|
|
422
|
+
totalTasks: tasks.length,
|
|
423
|
+
startedCount,
|
|
424
|
+
skippedCount: Math.max(0, tasks.length - startedCount),
|
|
425
|
+
failFastTriggered,
|
|
346
426
|
};
|
|
347
427
|
}
|