@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.
Files changed (70) hide show
  1. package/dist/compiler.js +6 -8
  2. package/dist/dynamic-decision.d.ts +0 -1
  3. package/dist/dynamic-decision.js +0 -7
  4. package/dist/dynamic-profiles.d.ts +0 -1
  5. package/dist/dynamic-profiles.js +0 -3
  6. package/dist/engine-run-graph.d.ts +1 -0
  7. package/dist/engine-run-graph.js +142 -2
  8. package/dist/engine.d.ts +5 -0
  9. package/dist/engine.js +112 -27
  10. package/dist/extension.d.ts +2 -1
  11. package/dist/extension.js +27 -6
  12. package/dist/index.d.ts +3 -3
  13. package/dist/index.js +2 -1
  14. package/dist/store.js +55 -11
  15. package/dist/subagent-backend.js +155 -29
  16. package/dist/types.d.ts +6 -0
  17. package/dist/workflow-runtime.js +10 -1
  18. package/dist/workflow-view.js +3 -1
  19. package/dist/workflow-web-source-extension.js +167 -48
  20. package/dist/workflow-web-source.d.ts +2 -1
  21. package/dist/workflow-web-source.js +84 -19
  22. package/node_modules/@agwab/pi-subagent/README.md +3 -3
  23. package/node_modules/@agwab/pi-subagent/api.mjs +1 -0
  24. package/node_modules/@agwab/pi-subagent/docs/usage.md +63 -12
  25. package/node_modules/@agwab/pi-subagent/package.json +2 -2
  26. package/node_modules/@agwab/pi-subagent/src/api.ts +54 -1
  27. package/node_modules/@agwab/pi-subagent/src/artifacts/registry.ts +9 -4
  28. package/node_modules/@agwab/pi-subagent/src/artifacts/result.ts +8 -0
  29. package/node_modules/@agwab/pi-subagent/src/core/constants.ts +9 -0
  30. package/node_modules/@agwab/pi-subagent/src/core/validation.ts +21 -0
  31. package/node_modules/@agwab/pi-subagent/src/index.ts +995 -573
  32. package/node_modules/@agwab/pi-subagent/src/orchestrate/async.ts +279 -156
  33. package/node_modules/@agwab/pi-subagent/src/orchestrate/interrupt.ts +165 -89
  34. package/node_modules/@agwab/pi-subagent/src/orchestrate/reconcile.ts +111 -65
  35. package/node_modules/@agwab/pi-subagent/src/orchestrate/run-ref.ts +219 -0
  36. package/node_modules/@agwab/pi-subagent/src/orchestrate/run.ts +88 -8
  37. package/node_modules/@agwab/pi-subagent/src/orchestrate/status.ts +614 -298
  38. package/node_modules/@agwab/pi-subagent/src/panel.ts +1352 -560
  39. package/node_modules/@agwab/pi-subagent/src/runners/headless-model.ts +53 -5
  40. package/node_modules/@agwab/pi-subagent/src/runners/tmux.ts +13 -6
  41. package/package.json +2 -2
  42. package/src/compiler.ts +14 -9
  43. package/src/dynamic-decision.ts +0 -11
  44. package/src/dynamic-profiles.ts +0 -4
  45. package/src/engine-run-graph.ts +185 -2
  46. package/src/engine.ts +145 -24
  47. package/src/extension.ts +33 -4
  48. package/src/index.ts +3 -1
  49. package/src/store.ts +74 -11
  50. package/src/subagent-backend.ts +201 -28
  51. package/src/types.ts +6 -0
  52. package/src/workflow-runtime.ts +18 -2
  53. package/src/workflow-view.ts +2 -1
  54. package/src/workflow-web-source-extension.ts +621 -228
  55. package/src/workflow-web-source.ts +118 -28
  56. package/workflows/deep-research/helpers/claim-evidence-gate.mjs +56 -16
  57. package/workflows/deep-research/helpers/final-audit-packet.mjs +1 -4
  58. package/workflows/deep-research/helpers/normalize-input-packet.mjs +1 -1
  59. package/workflows/deep-research/helpers/render-executive.mjs +8 -21
  60. package/workflows/deep-research/helpers/sanitize-verification-candidates.mjs +89 -15
  61. package/workflows/deep-research/schemas/deep-research-executive-render-control.schema.json +0 -1
  62. package/workflows/deep-research/schemas/deep-research-verify-claims-control.schema.json +4 -1
  63. package/workflows/impact-review/spec.json +3 -3
  64. package/workflows/spec-review/helpers/spec-review-pipeline.mjs +1 -8
  65. package/dist/dynamic-loader.d.ts +0 -25
  66. package/dist/dynamic-loader.js +0 -13
  67. package/src/dynamic-loader.ts +0 -49
  68. package/workflows/impact-review/schemas/docs-release-impact-control.schema.json +0 -42
  69. package/workflows/impact-review/schemas/security-performance-impact-control.schema.json +0 -42
  70. 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 results: ResultEnvelope[] = new Array(tasks.length);
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
- results[index] = await runSubagentTask({
332
- input: taskInput,
333
- cwd: runCwd,
334
- signal,
335
- taskIndex: index,
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
- await Promise.all(Array.from({ length: concurrency }, () => worker()));
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
  }