@agwab/pi-workflow 0.2.0 → 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 (79) hide show
  1. package/README.md +2 -0
  2. package/dist/compiler.d.ts +4 -6
  3. package/dist/compiler.js +70 -39
  4. package/dist/dynamic-decision.d.ts +0 -1
  5. package/dist/dynamic-decision.js +0 -7
  6. package/dist/dynamic-generated-task-runtime.d.ts +2 -0
  7. package/dist/dynamic-generated-task-runtime.js +21 -8
  8. package/dist/dynamic-profiles.d.ts +0 -1
  9. package/dist/dynamic-profiles.js +0 -3
  10. package/dist/engine-run-graph.d.ts +1 -0
  11. package/dist/engine-run-graph.js +142 -2
  12. package/dist/engine.d.ts +10 -6
  13. package/dist/engine.js +146 -77
  14. package/dist/extension.d.ts +2 -1
  15. package/dist/extension.js +38 -15
  16. package/dist/index.d.ts +3 -3
  17. package/dist/index.js +2 -1
  18. package/dist/store.d.ts +3 -1
  19. package/dist/store.js +189 -49
  20. package/dist/subagent-backend.d.ts +4 -0
  21. package/dist/subagent-backend.js +281 -31
  22. package/dist/types.d.ts +9 -1
  23. package/dist/workflow-runtime.d.ts +2 -0
  24. package/dist/workflow-runtime.js +40 -1
  25. package/dist/workflow-view.js +3 -1
  26. package/dist/workflow-web-source-extension.js +167 -48
  27. package/dist/workflow-web-source.d.ts +2 -1
  28. package/dist/workflow-web-source.js +84 -19
  29. package/docs/usage.md +11 -0
  30. package/node_modules/@agwab/pi-subagent/README.md +3 -3
  31. package/node_modules/@agwab/pi-subagent/api.mjs +1 -0
  32. package/node_modules/@agwab/pi-subagent/docs/usage.md +63 -12
  33. package/node_modules/@agwab/pi-subagent/package.json +2 -2
  34. package/node_modules/@agwab/pi-subagent/src/api.ts +54 -1
  35. package/node_modules/@agwab/pi-subagent/src/artifacts/registry.ts +9 -4
  36. package/node_modules/@agwab/pi-subagent/src/artifacts/result.ts +8 -0
  37. package/node_modules/@agwab/pi-subagent/src/core/constants.ts +9 -0
  38. package/node_modules/@agwab/pi-subagent/src/core/validation.ts +21 -0
  39. package/node_modules/@agwab/pi-subagent/src/index.ts +995 -573
  40. package/node_modules/@agwab/pi-subagent/src/orchestrate/async.ts +279 -156
  41. package/node_modules/@agwab/pi-subagent/src/orchestrate/interrupt.ts +165 -89
  42. package/node_modules/@agwab/pi-subagent/src/orchestrate/reconcile.ts +111 -65
  43. package/node_modules/@agwab/pi-subagent/src/orchestrate/run-ref.ts +219 -0
  44. package/node_modules/@agwab/pi-subagent/src/orchestrate/run.ts +88 -8
  45. package/node_modules/@agwab/pi-subagent/src/orchestrate/status.ts +614 -298
  46. package/node_modules/@agwab/pi-subagent/src/panel.ts +1352 -560
  47. package/node_modules/@agwab/pi-subagent/src/runners/headless-model.ts +53 -5
  48. package/node_modules/@agwab/pi-subagent/src/runners/tmux.ts +13 -6
  49. package/package.json +2 -2
  50. package/src/compiler.ts +127 -66
  51. package/src/dynamic-decision.ts +0 -11
  52. package/src/dynamic-generated-task-runtime.ts +47 -12
  53. package/src/dynamic-profiles.ts +0 -4
  54. package/src/engine-run-graph.ts +185 -2
  55. package/src/engine.ts +192 -107
  56. package/src/extension.ts +50 -17
  57. package/src/index.ts +3 -1
  58. package/src/store.ts +253 -55
  59. package/src/subagent-backend.ts +369 -32
  60. package/src/types.ts +13 -1
  61. package/src/workflow-runtime.ts +53 -2
  62. package/src/workflow-view.ts +2 -1
  63. package/src/workflow-web-source-extension.ts +621 -228
  64. package/src/workflow-web-source.ts +118 -28
  65. package/workflows/deep-research/helpers/claim-evidence-gate.mjs +56 -16
  66. package/workflows/deep-research/helpers/final-audit-packet.mjs +1 -4
  67. package/workflows/deep-research/helpers/normalize-input-packet.mjs +1 -1
  68. package/workflows/deep-research/helpers/render-executive.mjs +8 -21
  69. package/workflows/deep-research/helpers/sanitize-verification-candidates.mjs +89 -15
  70. package/workflows/deep-research/schemas/deep-research-executive-render-control.schema.json +0 -1
  71. package/workflows/deep-research/schemas/deep-research-verify-claims-control.schema.json +4 -1
  72. package/workflows/impact-review/spec.json +3 -3
  73. package/workflows/spec-review/helpers/spec-review-pipeline.mjs +1 -8
  74. package/dist/dynamic-loader.d.ts +0 -25
  75. package/dist/dynamic-loader.js +0 -13
  76. package/src/dynamic-loader.ts +0 -49
  77. package/workflows/impact-review/schemas/docs-release-impact-control.schema.json +0 -42
  78. package/workflows/impact-review/schemas/security-performance-impact-control.schema.json +0 -42
  79. package/workflows/impact-review/schemas/state-data-impact-control.schema.json +0 -42
package/dist/store.js CHANGED
@@ -1,13 +1,17 @@
1
1
  import { AsyncLocalStorage } from "node:async_hooks";
2
- import { cp, mkdir, open, readdir, readFile, realpath, rename, stat, unlink, utimes, writeFile, } from "node:fs/promises";
2
+ import { cp, link, mkdir, open, readdir, readFile, realpath, rename, stat, unlink, utimes, writeFile, } from "node:fs/promises";
3
3
  import { basename, dirname, isAbsolute, join, normalize, relative, resolve, sep, } from "node:path";
4
4
  import { randomBytes } from "node:crypto";
5
5
  import { parseWorkflow } from "./schema.js";
6
6
  import { WORKFLOW_RUN_TYPE, } from "./types.js";
7
7
  const TERMINAL_INDEX_LIMIT = 50;
8
8
  const LEASE_STALE_MS = 30_000;
9
+ const LEASE_ABSOLUTE_STALE_MS = 7 * 24 * 60 * 60 * 1000;
9
10
  const INDEX_LOCK_WAIT_MS = 5_000;
10
11
  const INDEX_LOCK_RETRY_MS = 50;
12
+ const DEFAULT_INDEX_UPDATE_DEBOUNCE_MS = 500;
13
+ let indexUpdateDebounceMs = DEFAULT_INDEX_UPDATE_DEBOUNCE_MS;
14
+ const pendingIndexUpdates = new Map();
11
15
  const runLeaseContext = new AsyncLocalStorage();
12
16
  const TASK_STATUSES = [
13
17
  "pending",
@@ -139,34 +143,74 @@ async function reclaimStaleLock(lockFile) {
139
143
  const snapshot = await readLockSnapshot(lockFile);
140
144
  if (!snapshot)
141
145
  return true;
142
- if (Date.now() - snapshot.mtimeMs <= LEASE_STALE_MS)
146
+ if (!isReclaimableLockSnapshot(snapshot))
143
147
  return false;
144
- if (snapshot.pid !== undefined && isProcessAlive(snapshot.pid))
148
+ const reclaimFile = `${lockFile}.reclaim-${process.pid}-${randomBytes(3).toString("hex")}`;
149
+ try {
150
+ await rename(lockFile, reclaimFile);
151
+ }
152
+ catch (error) {
153
+ if (error.code === "ENOENT")
154
+ return true;
145
155
  return false;
146
- const latest = await readLockSnapshot(lockFile);
147
- if (!latest)
156
+ }
157
+ const claimed = await readLockSnapshot(reclaimFile);
158
+ if (!claimed)
148
159
  return true;
149
- if (latest.ownerId !== snapshot.ownerId || latest.pid !== snapshot.pid)
160
+ if (!sameLockOwnerSnapshot(snapshot, claimed)) {
161
+ await restoreReclaimFile(reclaimFile, lockFile);
150
162
  return false;
151
- if (Date.now() - latest.mtimeMs <= LEASE_STALE_MS)
163
+ }
164
+ if (!isReclaimableLockSnapshot(claimed)) {
165
+ await restoreReclaimFile(reclaimFile, lockFile);
152
166
  return false;
153
- if (latest.pid !== undefined && isProcessAlive(latest.pid))
167
+ }
168
+ await unlink(reclaimFile).catch(() => undefined);
169
+ return true;
170
+ }
171
+ async function restoreReclaimFile(reclaimFile, lockFile) {
172
+ try {
173
+ await link(reclaimFile, lockFile);
174
+ }
175
+ catch (error) {
176
+ if (error.code !== "EEXIST")
177
+ throw error;
178
+ }
179
+ finally {
180
+ await unlink(reclaimFile).catch(() => undefined);
181
+ }
182
+ }
183
+ function isReclaimableLockSnapshot(snapshot) {
184
+ const now = Date.now();
185
+ const leaseStale = now - snapshot.mtimeMs > LEASE_STALE_MS;
186
+ const absoluteStale = now - (snapshot.createdAtMs ?? snapshot.mtimeMs) > LEASE_ABSOLUTE_STALE_MS;
187
+ if (!leaseStale && !absoluteStale)
188
+ return false;
189
+ if (snapshot.pid !== undefined &&
190
+ isProcessAlive(snapshot.pid) &&
191
+ !absoluteStale)
154
192
  return false;
155
- await unlink(lockFile).catch(() => undefined);
156
193
  return true;
157
194
  }
195
+ function sameLockOwnerSnapshot(left, right) {
196
+ return (left.ownerId === right.ownerId &&
197
+ left.pid === right.pid &&
198
+ left.createdAtMs === right.createdAtMs);
199
+ }
158
200
  async function readLockSnapshot(lockFile) {
159
201
  try {
160
202
  const [fileStat, text] = await Promise.all([
161
203
  stat(lockFile),
162
204
  readFile(lockFile, "utf8"),
163
205
  ]);
164
- const [ownerId = "", pidText] = text.split(/\r?\n/);
206
+ const [ownerId = "", pidText, createdAtText] = text.split(/\r?\n/);
165
207
  const pid = Number.parseInt(pidText ?? "", 10);
208
+ const createdAtMs = Date.parse(createdAtText ?? "");
166
209
  return {
167
210
  ownerId,
168
211
  pid: Number.isFinite(pid) ? pid : undefined,
169
212
  mtimeMs: fileStat.mtimeMs,
213
+ createdAtMs: Number.isFinite(createdAtMs) ? createdAtMs : undefined,
170
214
  };
171
215
  }
172
216
  catch (error) {
@@ -256,7 +300,46 @@ export async function writeRunRecord(cwd, run) {
256
300
  const derived = deriveRunStatus(run);
257
301
  Object.assign(run, derived);
258
302
  await writeJsonAtomic(workflowRunPath(cwd, run.runId), run);
259
- await updateIndex(cwd).catch(() => undefined);
303
+ scheduleIndexUpdate(cwd, run.runId, {
304
+ immediate: isTerminalWorkflowStatus(run.status),
305
+ });
306
+ }
307
+ function indexUpdateKey(cwd, runId) {
308
+ return `${cwd}\0${runId}`;
309
+ }
310
+ function scheduleIndexUpdate(cwd, runId, options) {
311
+ const key = indexUpdateKey(cwd, runId);
312
+ const existing = pendingIndexUpdates.get(key);
313
+ if (existing) {
314
+ clearTimeout(existing.timer);
315
+ pendingIndexUpdates.delete(key);
316
+ }
317
+ const runUpdate = () => {
318
+ pendingIndexUpdates.delete(key);
319
+ void updateIndex(cwd, runId).catch(() => undefined);
320
+ };
321
+ if (options.immediate) {
322
+ runUpdate();
323
+ return;
324
+ }
325
+ // Pending debounced index writes are intentionally not flushed on process exit:
326
+ // the next explicit index rebuild/read path self-heals from run.json records.
327
+ const timer = setTimeout(runUpdate, indexUpdateDebounceMs);
328
+ timer.unref?.();
329
+ pendingIndexUpdates.set(key, { cwd, runId, timer });
330
+ }
331
+ export async function flushPendingIndexUpdatesForTests() {
332
+ const pending = [...pendingIndexUpdates.values()];
333
+ pendingIndexUpdates.clear();
334
+ for (const item of pending)
335
+ clearTimeout(item.timer);
336
+ await Promise.all(pending.map((item) => updateIndex(item.cwd, item.runId)));
337
+ }
338
+ export function setIndexUpdateDebounceMsForTests(value) {
339
+ indexUpdateDebounceMs =
340
+ value === undefined
341
+ ? DEFAULT_INDEX_UPDATE_DEBOUNCE_MS
342
+ : Math.max(0, Math.floor(value));
260
343
  }
261
344
  export async function writeCompiledRunArtifact(cwd, runId, compiled) {
262
345
  const runDir = workflowRunDir(cwd, runId);
@@ -830,48 +913,15 @@ function isRunRecordLike(value) {
830
913
  typeof task.status === "string" &&
831
914
  TASK_STATUSES.includes(task.status)));
832
915
  }
833
- export async function updateIndex(cwd) {
916
+ export async function updateIndex(cwd, changedRunId) {
834
917
  const lockFile = join(workflowsRoot(cwd), "index.lock");
835
918
  const ownerId = `${process.pid}-${randomBytes(3).toString("hex")}`;
836
919
  await ensureDir(workflowsRoot(cwd));
837
920
  await acquireLockWithWait(lockFile, ownerId);
838
921
  try {
839
- const runs = (await listRunRecords(cwd)).sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
840
- const active = runs.filter((run) => !isTerminalWorkflowStatus(run.status));
841
- const terminal = runs
842
- .filter((run) => isTerminalWorkflowStatus(run.status))
843
- .slice(0, TERMINAL_INDEX_LIMIT);
844
- const selected = [...active, ...terminal].sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
845
- const index = {
846
- schemaVersion: 1,
847
- updatedAt: nowIso(),
848
- runs: selected.map((run) => ({
849
- runId: run.runId,
850
- name: run.name,
851
- type: run.type,
852
- artifactGraph: run.artifactGraph,
853
- status: run.status,
854
- taskSummary: run.taskSummary,
855
- createdAt: run.createdAt,
856
- updatedAt: run.updatedAt,
857
- parentRunId: run.parentRunId,
858
- rootRunId: run.rootRunId,
859
- round: run.round,
860
- fanout: run.fanout,
861
- runJson: toProjectPath(cwd, workflowRunPath(cwd, run.runId)),
862
- tasks: run.tasks.map((task) => ({
863
- taskId: task.taskId,
864
- displayName: task.displayName,
865
- agent: task.agent,
866
- kind: task.kind,
867
- stageId: task.stageId,
868
- backendHandle: task.backendHandle,
869
- status: task.status,
870
- statusDetail: task.statusDetail,
871
- lastMessage: task.lastMessage,
872
- })),
873
- })),
874
- };
922
+ const index = changedRunId
923
+ ? await updateIndexIncremental(cwd, changedRunId)
924
+ : await rebuildIndex(cwd);
875
925
  await writeJsonAtomic(workflowIndexPath(cwd), index);
876
926
  return index;
877
927
  }
@@ -879,6 +929,93 @@ export async function updateIndex(cwd) {
879
929
  await releaseLock(lockFile, ownerId);
880
930
  }
881
931
  }
932
+ async function updateIndexIncremental(cwd, changedRunId) {
933
+ const existing = await readIndexForIncremental(cwd);
934
+ if (!existing)
935
+ return rebuildIndex(cwd);
936
+ let changedRun;
937
+ try {
938
+ changedRun = await readRunRecord(cwd, changedRunId);
939
+ }
940
+ catch {
941
+ return rebuildIndex(cwd);
942
+ }
943
+ const changedEntry = buildIndexEntry(cwd, changedRun);
944
+ const entries = existing.runs
945
+ .filter((entry) => entry.runId !== changedRun.runId)
946
+ .concat(changedEntry);
947
+ return {
948
+ schemaVersion: 1,
949
+ updatedAt: nowIso(),
950
+ runs: selectIndexEntries(entries),
951
+ };
952
+ }
953
+ async function readIndexForIncremental(cwd) {
954
+ let index;
955
+ try {
956
+ index = await readIndex(cwd);
957
+ }
958
+ catch {
959
+ return undefined;
960
+ }
961
+ if (!isIndexRecordLike(index))
962
+ return undefined;
963
+ return index;
964
+ }
965
+ async function rebuildIndex(cwd) {
966
+ const runs = await listRunRecords(cwd);
967
+ return {
968
+ schemaVersion: 1,
969
+ updatedAt: nowIso(),
970
+ runs: selectIndexEntries(runs.map((run) => buildIndexEntry(cwd, run))),
971
+ };
972
+ }
973
+ function selectIndexEntries(entries) {
974
+ const sorted = [...entries].sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
975
+ const active = sorted.filter((entry) => !isTerminalWorkflowStatus(entry.status));
976
+ const terminal = sorted
977
+ .filter((entry) => isTerminalWorkflowStatus(entry.status))
978
+ .slice(0, TERMINAL_INDEX_LIMIT);
979
+ return [...active, ...terminal].sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
980
+ }
981
+ function buildIndexEntry(cwd, run) {
982
+ return {
983
+ runId: run.runId,
984
+ name: run.name,
985
+ type: run.type,
986
+ artifactGraph: run.artifactGraph,
987
+ status: run.status,
988
+ taskSummary: run.taskSummary,
989
+ createdAt: run.createdAt,
990
+ updatedAt: run.updatedAt,
991
+ parentRunId: run.parentRunId,
992
+ rootRunId: run.rootRunId,
993
+ round: run.round,
994
+ fanout: run.fanout,
995
+ runJson: toProjectPath(cwd, workflowRunPath(cwd, run.runId)),
996
+ tasks: run.tasks.map((task) => ({
997
+ taskId: task.taskId,
998
+ displayName: task.displayName,
999
+ agent: task.agent,
1000
+ kind: task.kind,
1001
+ stageId: task.stageId,
1002
+ backendHandle: task.backendHandle,
1003
+ status: task.status,
1004
+ statusDetail: task.statusDetail,
1005
+ lastMessage: task.lastMessage,
1006
+ })),
1007
+ };
1008
+ }
1009
+ function isIndexRecordLike(value) {
1010
+ return (value?.schemaVersion === 1 &&
1011
+ Array.isArray(value.runs) &&
1012
+ value.runs.every((entry) => entry &&
1013
+ typeof entry === "object" &&
1014
+ typeof entry.runId === "string" &&
1015
+ typeof entry.updatedAt === "string" &&
1016
+ typeof entry.status === "string" &&
1017
+ Array.isArray(entry.tasks)));
1018
+ }
882
1019
  export function deriveRunStatus(run) {
883
1020
  const next = { ...run, tasks: run.tasks };
884
1021
  next.taskSummary = summarizeTasks(next.tasks);
@@ -900,8 +1037,10 @@ export function deriveWorkflowStatus(summary) {
900
1037
  return "running";
901
1038
  if (summary.total > 0 && summary.completed === summary.total)
902
1039
  return "completed";
903
- if (summary.failed > 0 || summary.interrupted > 0)
1040
+ if (summary.failed > 0)
904
1041
  return "failed";
1042
+ if (summary.interrupted > 0)
1043
+ return "interrupted";
905
1044
  return "interrupted";
906
1045
  }
907
1046
  export function isTerminalWorkflowStatus(status) {
@@ -1080,6 +1219,7 @@ export function createTaskRunRecord(cwd, runId, task, index) {
1080
1219
  dependsOn: task.dependsOn,
1081
1220
  artifactGraph: taskArtifactGraph,
1082
1221
  dynamicGenerated: task.dynamicGenerated,
1222
+ foreachGenerated: task.foreachGenerated,
1083
1223
  files,
1084
1224
  lastMessage: blocked ? task.safety.permission.reason : undefined,
1085
1225
  };
@@ -1,6 +1,10 @@
1
1
  import type { CompiledTask, WorkflowRunRecord, WorkflowTaskRunRecord } from "./types.js";
2
2
  import type { BackendLaunchResult } from "./backend.js";
3
3
  export declare function setSubagentApiForTests(api: unknown | undefined): void;
4
+ export declare function setSubagentLaunchControlsForTests(options?: {
5
+ releaseDelayMs?: number;
6
+ retryJitterMs?: number | (() => number);
7
+ }): void;
4
8
  export declare function cleanupSubagentRun(_cwd: string, run: WorkflowRunRecord): Promise<void>;
5
9
  export declare function launchSubagentTask(cwd: string, run: WorkflowRunRecord, task: WorkflowTaskRunRecord, compiledTask: CompiledTask): Promise<BackendLaunchResult>;
6
10
  export declare function refreshRunFromSubagentArtifacts(cwd: string, run: WorkflowRunRecord): Promise<WorkflowRunRecord>;