@agwab/pi-workflow 0.1.2 → 0.2.1

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 (41) hide show
  1. package/README.md +9 -13
  2. package/dist/compiler.d.ts +5 -5
  3. package/dist/compiler.js +82 -24
  4. package/dist/dynamic-generated-task-runtime.d.ts +2 -0
  5. package/dist/dynamic-generated-task-runtime.js +21 -8
  6. package/dist/engine.d.ts +6 -5
  7. package/dist/engine.js +39 -54
  8. package/dist/extension.js +211 -24
  9. package/dist/store.d.ts +3 -1
  10. package/dist/store.js +135 -38
  11. package/dist/subagent-backend.d.ts +4 -0
  12. package/dist/subagent-backend.js +128 -4
  13. package/dist/types.d.ts +5 -0
  14. package/dist/workflow-progress-health.d.ts +37 -0
  15. package/dist/workflow-progress-health.js +296 -0
  16. package/dist/workflow-runtime.d.ts +8 -0
  17. package/dist/workflow-runtime.js +63 -10
  18. package/dist/workflow-view.d.ts +2 -0
  19. package/dist/workflow-view.js +97 -18
  20. package/dist/workflow-web-source.js +32 -14
  21. package/docs/usage.md +12 -1
  22. package/package.json +6 -6
  23. package/src/compiler.ts +136 -41
  24. package/src/dynamic-generated-task-runtime.ts +47 -12
  25. package/src/engine.ts +55 -100
  26. package/src/extension.ts +270 -34
  27. package/src/store.ts +180 -44
  28. package/src/subagent-backend.ts +170 -6
  29. package/src/types.ts +10 -0
  30. package/src/workflow-progress-health.ts +461 -0
  31. package/src/workflow-runtime.ts +85 -13
  32. package/src/workflow-view.ts +186 -41
  33. package/src/workflow-web-source.ts +192 -69
  34. package/workflows/deep-research/helpers/claim-evidence-gate.mjs +111 -37
  35. package/workflows/deep-research/helpers/final-audit-packet.mjs +191 -14
  36. package/workflows/deep-research/helpers/normalize-input-packet.mjs +159 -50
  37. package/workflows/deep-research/helpers/render-executive.mjs +671 -37
  38. package/workflows/deep-research/helpers/sanitize-verification-candidates.mjs +624 -0
  39. package/workflows/deep-research/schemas/deep-research-executive-render-control.schema.json +2 -0
  40. package/workflows/deep-research/schemas/deep-research-final-synthesis-control.schema.json +110 -0
  41. package/workflows/deep-research/spec.json +41 -11
@@ -62,6 +62,7 @@ export declare class WorkflowView implements Component {
62
62
  private taskIdentityLines;
63
63
  private taskOverviewLines;
64
64
  private taskTimelineLines;
65
+ private taskHealthLines;
65
66
  private taskValidationStripLines;
66
67
  private taskArtifactViewerLines;
67
68
  private currentArtifactSourceLines;
@@ -83,6 +84,7 @@ export declare class WorkflowView implements Component {
83
84
  private syncSelectedTaskId;
84
85
  private breadcrumbText;
85
86
  private runSummaryLines;
87
+ private runHealthLines;
86
88
  private runDetailSummaryLines;
87
89
  private stageContextLines;
88
90
  private footerText;
@@ -1,6 +1,7 @@
1
1
  // @ts-nocheck
2
2
  import { readFile } from "node:fs/promises";
3
3
  import { workflowRunPath, fromProjectPath, listRunRecords, readIndex, readRunRecord, } from "./store.js";
4
+ import { diagnoseWorkflowRunHealth, diagnoseWorkflowTaskHealth, } from "./workflow-progress-health.js";
4
5
  import { WORKFLOW_RUN_TYPE, } from "./types.js";
5
6
  const REFRESH_INTERVAL_MS = 1_000;
6
7
  const MAX_LIST_ROWS = 18;
@@ -277,14 +278,18 @@ export class WorkflowView {
277
278
  }
278
279
  renderRunsScreen(width) {
279
280
  const selected = this.flows[this.selectedFlow];
281
+ const selectedDetail = selected && this.detailRun?.runId === selected.runId
282
+ ? this.detailRun
283
+ : undefined;
280
284
  const sideLines = [
281
285
  accent(this.theme, "All runs"),
282
286
  kvRow(this.theme, "total", String(this.flows.length)),
283
287
  kvRow(this.theme, "running", String(this.flows.filter((flow) => flow.status === "running").length)),
288
+ kvRow(this.theme, "needs action", String(this.flows.filter((flow) => ["failed", "blocked", "interrupted"].includes(flow.status)).length)),
284
289
  "",
285
290
  accent(this.theme, "Selected"),
286
291
  ...(selected
287
- ? this.runSummaryLines(selected)
292
+ ? this.runSummaryLines(selected, selectedDetail)
288
293
  : [placeholder(this.theme, "none")]),
289
294
  ];
290
295
  return this.renderTwoPane(width, "Filters / Summary", sideLines, "Runs", this.runLines(Math.max(1, this.mainPaneBodyWidth(width))), 32);
@@ -331,9 +336,10 @@ export class WorkflowView {
331
336
  return width - leftWidth - 5;
332
337
  }
333
338
  renderTaskDetail(width, run, task) {
339
+ const taskHealth = diagnoseWorkflowTaskHealth(task, run);
334
340
  const lines = [
335
341
  ...boxed(this.theme, "Task Detail", width, [
336
- `${statusGlyph(this.theme, task.status)} ${strong(this.theme, task.displayName)} ${statusBadge(this.theme, task.status)} ${muted(this.theme, this.breadcrumbText())}`,
342
+ `${statusGlyph(this.theme, task.status)} ${strong(this.theme, task.displayName)} ${statusBadge(this.theme, task.status)} ${healthInline(this.theme, taskHealth)} ${muted(this.theme, this.breadcrumbText())}`,
337
343
  taskMetaLine(this.theme, [
338
344
  ["agent", task.agent],
339
345
  ["stage", task.stageId ?? "(none)"],
@@ -343,6 +349,10 @@ export class WorkflowView {
343
349
  ], statusColor(task.status)),
344
350
  "",
345
351
  ];
352
+ const healthLines = this.taskHealthLines(taskHealth, width - 4);
353
+ if (healthLines.length > 0) {
354
+ lines.push(...boxed(this.theme, "Health", width, healthLines, healthColor(taskHealth)), "");
355
+ }
346
356
  const validationLines = this.taskValidationStripLines(task, width - 4);
347
357
  if (validationLines.length > 0) {
348
358
  lines.push(...boxed(this.theme, "Validation", width, validationLines, task.outputValidation?.status === "invalid" ||
@@ -388,13 +398,15 @@ export class WorkflowView {
388
398
  const name = flow.name ?? flow.type;
389
399
  const left = `${prefix}${marker} ${selected ? strong(this.theme, name) : name}`;
390
400
  const runIdText = shortId(flow.runId).slice(0, 16).padEnd(16, " ");
391
- const runningText = flow.taskSummary.running > 0
392
- ? ` ${muted(this.theme, "·")} ${metaLabel(this.theme, "running")} ${metaValue(this.theme, String(flow.taskSummary.running))}`
393
- : "";
401
+ const detailRun = this.detailRun?.runId === flow.runId ? this.detailRun : undefined;
402
+ const health = diagnoseWorkflowRunHealth(detailRun ?? flow);
403
+ const healthText = health.state === "completed"
404
+ ? ""
405
+ : ` ${muted(this.theme, "·")} ${healthLabel(this.theme, health)}`;
394
406
  const baseRight = `${statusColumn(this.theme, flow.status, runStatusLabel(flow), statusWidth)} ${progressBar(this.theme, flow.taskSummary, 5)} ${metaValue(this.theme, runIdText)}`;
395
407
  const right = width >= 90
396
- ? `${baseRight} ${muted(this.theme, "·")} ${metaLabel(this.theme, "start")} ${metaValue(this.theme, timestampText(flow.createdAt))}${runningText}`
397
- : `${baseRight}${runningText}`;
408
+ ? `${baseRight} ${muted(this.theme, "·")} ${metaLabel(this.theme, "start")} ${metaValue(this.theme, timestampText(flow.createdAt))}${healthText}`
409
+ : `${baseRight}${healthText}`;
398
410
  const line = joinColumns(left, right, width, 17);
399
411
  lines.push(selectedLine(this.theme, line, width, selected, true));
400
412
  }
@@ -437,7 +449,7 @@ export class WorkflowView {
437
449
  const selected = index === this.selectedTask;
438
450
  const prefix = selected ? accent(this.theme, "› ") : " ";
439
451
  const left = `${prefix}${statusGlyph(this.theme, task.status)} ${selected ? strong(this.theme, task.displayName) : task.displayName}`;
440
- const right = taskListStatusLabel(this.theme, task);
452
+ const right = taskListStatusLabel(this.theme, task, diagnoseWorkflowTaskHealth(task, run));
441
453
  const line = joinColumns(left, metaByStatus(this.theme, task.status, right), width, Math.max(22, Math.floor(width * 0.45)));
442
454
  lines.push(selectedLine(this.theme, line, width, selected, true));
443
455
  }
@@ -487,6 +499,24 @@ export class WorkflowView {
487
499
  : "warning"));
488
500
  return lines.map((line) => fit(line, width));
489
501
  }
502
+ taskHealthLines(health, width) {
503
+ if (health.state === "completed" || health.state === "pending")
504
+ return [];
505
+ const lines = [
506
+ `${healthGlyph(this.theme, health)} ${healthLabel(this.theme, health)} ${muted(this.theme, health.summary)}`,
507
+ kvRow(this.theme, "suggested", health.suggestion, healthColor(health)),
508
+ kvRow(this.theme, "why", health.reason),
509
+ ];
510
+ if (health.currentTask?.elapsedMs !== undefined)
511
+ lines.splice(1, 0, kvRow(this.theme, "elapsed", formatDuration(health.currentTask.elapsedMs)));
512
+ if (health.durationClass)
513
+ lines.push(kvRow(this.theme, "duration", `${health.durationClass} expected`));
514
+ if (health.heartbeatAgeMs !== undefined)
515
+ lines.push(kvRow(this.theme, "heartbeat", `${formatDuration(health.heartbeatAgeMs)} ago`));
516
+ if (health.lastActivityAgeMs !== undefined)
517
+ lines.push(kvRow(this.theme, "activity", `${formatDuration(health.lastActivityAgeMs)} ago`));
518
+ return lines.map((line) => fit(line, width));
519
+ }
490
520
  taskValidationStripLines(task, width) {
491
521
  const summary = taskValidationSummary(task);
492
522
  if (!summary)
@@ -502,9 +532,7 @@ export class WorkflowView {
502
532
  const total = sourceLines.length;
503
533
  const maxStart = Math.max(0, total - TASK_ARTIFACT_VIEW_LINES);
504
534
  const start = Math.min(this.artifactScrollLine, maxStart);
505
- const end = total === 0
506
- ? 0
507
- : Math.min(total, start + TASK_ARTIFACT_VIEW_LINES);
535
+ const end = total === 0 ? 0 : Math.min(total, start + TASK_ARTIFACT_VIEW_LINES);
508
536
  const visible = total === 0
509
537
  ? [
510
538
  this.taskArtifactView === "output"
@@ -686,7 +714,8 @@ export class WorkflowView {
686
714
  return this.tasksForSelectedStage(this.detailRun)[this.selectedTask];
687
715
  }
688
716
  syncSelectedTaskId(tasks) {
689
- const stageTasks = tasks ?? (this.detailRun ? this.tasksForSelectedStage(this.detailRun) : []);
717
+ const stageTasks = tasks ??
718
+ (this.detailRun ? this.tasksForSelectedStage(this.detailRun) : []);
690
719
  this.selectedTaskId = stageTasks[this.selectedTask]?.taskId ?? "";
691
720
  }
692
721
  breadcrumbText() {
@@ -704,7 +733,8 @@ export class WorkflowView {
704
733
  parts.push(task.displayName);
705
734
  return parts.join(" › ");
706
735
  }
707
- runSummaryLines(flow) {
736
+ runSummaryLines(flow, detailRun) {
737
+ const health = diagnoseWorkflowRunHealth(detailRun ?? flow);
708
738
  return [
709
739
  `${statusGlyph(this.theme, flow.status)} ${strong(this.theme, flow.name ?? flow.type)} ${statusBadge(this.theme, flow.status, runStatusLabel(flow))}`,
710
740
  progressBar(this.theme, flow.taskSummary, 8),
@@ -723,9 +753,28 @@ export class WorkflowView {
723
753
  elapsedText(flow.createdAt, flow.updatedAt, flow.status === "running"),
724
754
  ],
725
755
  ]),
756
+ ...this.runHealthLines(health),
757
+ ];
758
+ }
759
+ runHealthLines(health) {
760
+ if (health.state === "completed")
761
+ return [];
762
+ const lines = [
763
+ "",
764
+ accent(this.theme, "Health"),
765
+ `${healthGlyph(this.theme, health)} ${healthLabel(this.theme, health)} ${muted(this.theme, health.summary)}`,
726
766
  ];
767
+ if (health.currentTask?.displayName)
768
+ lines.push(kvRow(this.theme, "current", health.currentTask.displayName));
769
+ if (health.lastActivityAgeMs !== undefined)
770
+ lines.push(kvRow(this.theme, "activity", `${formatDuration(health.lastActivityAgeMs)} ago`));
771
+ if (health.heartbeatAgeMs !== undefined)
772
+ lines.push(kvRow(this.theme, "heartbeat", `${formatDuration(health.heartbeatAgeMs)} ago`));
773
+ lines.push(kvRow(this.theme, "suggested", health.suggestion, healthColor(health)));
774
+ return lines;
727
775
  }
728
776
  runDetailSummaryLines(run) {
777
+ const health = diagnoseWorkflowRunHealth(run);
729
778
  const lines = [
730
779
  `${statusGlyph(this.theme, run.status)} ${strong(this.theme, run.name ?? run.type)} ${statusBadge(this.theme, run.status)}`,
731
780
  progressBar(this.theme, run.taskSummary, 10),
@@ -745,6 +794,7 @@ export class WorkflowView {
745
794
  ["updated", timestampText(run.updatedAt)],
746
795
  ]),
747
796
  kvRow(this.theme, "run", shortId(run.runId)),
797
+ ...this.runHealthLines(health),
748
798
  ];
749
799
  if (run.fanout && run.fanout.length > 0) {
750
800
  lines.push("", accent(this.theme, "Fanout"));
@@ -1034,7 +1084,29 @@ function statusColor(status) {
1034
1084
  function statusText(status) {
1035
1085
  return status;
1036
1086
  }
1037
- function taskListStatusLabel(theme, task) {
1087
+ function healthColor(health) {
1088
+ return health.tone;
1089
+ }
1090
+ function healthGlyph(theme, health) {
1091
+ if (health.tone === "success")
1092
+ return success(theme, "✓");
1093
+ if (health.tone === "warning")
1094
+ return warning(theme, "●");
1095
+ if (health.tone === "error")
1096
+ return errorText(theme, "●");
1097
+ if (health.tone === "dim")
1098
+ return muted(theme, "•");
1099
+ return accent(theme, "●");
1100
+ }
1101
+ function healthLabel(theme, health) {
1102
+ return fg(theme, healthColor(health), strong(theme, health.label));
1103
+ }
1104
+ function healthInline(theme, health) {
1105
+ if (health.state === "completed" || health.state === "pending")
1106
+ return "";
1107
+ return `${healthGlyph(theme, health)} ${healthLabel(theme, health)}`;
1108
+ }
1109
+ function taskListStatusLabel(theme, task, health) {
1038
1110
  const validation = taskValidationSummary(task);
1039
1111
  const label = validation?.status === "invalid"
1040
1112
  ? "invalid output"
@@ -1042,8 +1114,13 @@ function taskListStatusLabel(theme, task) {
1042
1114
  ? "valid"
1043
1115
  : task.status === "completed"
1044
1116
  ? "done"
1045
- : statusText(task.status);
1046
- return fg(theme, statusColor(task.status), strong(theme, label));
1117
+ : task.status === "running"
1118
+ ? health.label
1119
+ : statusText(task.status);
1120
+ const suffix = task.status === "running" && health.currentTask?.elapsedMs !== undefined
1121
+ ? ` ${muted(theme, "·")} ${metaValue(theme, formatDuration(health.currentTask.elapsedMs))}`
1122
+ : "";
1123
+ return `${fg(theme, task.status === "running" ? healthColor(health) : statusColor(task.status), strong(theme, label))}${suffix}`;
1047
1124
  }
1048
1125
  function compactStatusLabel(theme, status) {
1049
1126
  const label = status === "completed" ? "done" : statusText(status);
@@ -1107,7 +1184,7 @@ function taskValidationSummary(task) {
1107
1184
  : undefined;
1108
1185
  const issueMessage = typeof issue === "string"
1109
1186
  ? issue
1110
- : issue?.message ?? issue?.path ?? issue?.code ?? "";
1187
+ : (issue?.message ?? issue?.path ?? issue?.code ?? "");
1111
1188
  const message = validation.message ?? validation.reason ?? issueMessage;
1112
1189
  if (status === "valid" && !message)
1113
1190
  return undefined;
@@ -1263,7 +1340,9 @@ function truncateToWidth(text, width) {
1263
1340
  return "";
1264
1341
  if (visibleWidth(text) <= safeWidth)
1265
1342
  return text;
1266
- const hasAnsi = text.includes("\u001b[") || text.includes("\u001b]") || text.includes("\u001b_");
1343
+ const hasAnsi = text.includes("\u001b[") ||
1344
+ text.includes("\u001b]") ||
1345
+ text.includes("\u001b_");
1267
1346
  const ellipsis = "…";
1268
1347
  const ellipsisWidth = visibleWidth(ellipsis);
1269
1348
  const limit = Math.max(0, safeWidth - ellipsisWidth);
@@ -1,5 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
- import { appendFile, mkdir, readFile, readdir, rename, writeFile } from "node:fs/promises";
2
+ import { appendFile, mkdir, readFile, readdir, rename, writeFile, } from "node:fs/promises";
3
3
  import { isIP } from "node:net";
4
4
  import { dirname, resolve } from "node:path";
5
5
  export const WORKFLOW_WEB_SOURCE_CACHE_SCHEMA = "workflow-web-source-cache-v1";
@@ -238,7 +238,9 @@ export async function findWorkflowWebSourceByUrl(config, url) {
238
238
  function sourceIndexEntryMatchesUrl(entry, url, redactedUrl, targetKey, targetDisplayKey) {
239
239
  if (entry.urlKey)
240
240
  return entry.urlKey === targetKey;
241
- if (redactedUrlIdentityUnsafe(redactedUrl) || redactedUrlIdentityUnsafe(entry.redactedUrl) || redactedUrlIdentityUnsafe(entry.url)) {
241
+ if (redactedUrlIdentityUnsafe(redactedUrl) ||
242
+ redactedUrlIdentityUnsafe(entry.redactedUrl) ||
243
+ redactedUrlIdentityUnsafe(entry.url)) {
242
244
  return false;
243
245
  }
244
246
  return (entry.redactedUrl === redactedUrl ||
@@ -247,7 +249,8 @@ function sourceIndexEntryMatchesUrl(entry, url, redactedUrl, targetKey, targetDi
247
249
  sourceUrlDisplayCacheKey(entry.url) === targetDisplayKey);
248
250
  }
249
251
  function redactedUrlIdentityUnsafe(url) {
250
- return /REDACTED/.test(url) || /[?&#][^=]*(?:token|secret|password|signature|sig|key|auth|session|credential)[^=]*=/i.test(url);
252
+ return (/REDACTED/.test(url) ||
253
+ /[?&#][^=]*(?:token|secret|password|signature|sig|key|auth|session|credential)[^=]*=/i.test(url));
251
254
  }
252
255
  async function findWorkflowWebSourceByUrlFromSources(config, url, redactedUrl, targetKey, targetDisplayKey) {
253
256
  let entries;
@@ -269,7 +272,9 @@ async function findWorkflowWebSourceByUrlFromSources(config, url, redactedUrl, t
269
272
  return source;
270
273
  continue;
271
274
  }
272
- if (redactedUrlIdentityUnsafe(redactedUrl) || redactedUrlIdentityUnsafe(source.redactedUrl) || redactedUrlIdentityUnsafe(source.url)) {
275
+ if (redactedUrlIdentityUnsafe(redactedUrl) ||
276
+ redactedUrlIdentityUnsafe(source.redactedUrl) ||
277
+ redactedUrlIdentityUnsafe(source.url)) {
273
278
  continue;
274
279
  }
275
280
  if (source.redactedUrl === redactedUrl ||
@@ -316,7 +321,7 @@ export function buildWorkflowWebSourceCard(options) {
316
321
  };
317
322
  }
318
323
  export function readWorkflowWebSourceSnippet(options) {
319
- return readWorkflowWebSourceSnippets({
324
+ return (readWorkflowWebSourceSnippets({
320
325
  source: options.source,
321
326
  requests: [
322
327
  {
@@ -328,7 +333,7 @@ export function readWorkflowWebSourceSnippet(options) {
328
333
  ],
329
334
  maxChars: options.maxChars,
330
335
  budget: options.budget,
331
- })[0] ?? { status: "not_found", visibleChars: 0 };
336
+ })[0] ?? { status: "not_found", visibleChars: 0 });
332
337
  }
333
338
  export function readWorkflowWebSourceSnippets(options) {
334
339
  let normalizedSource;
@@ -491,7 +496,10 @@ function readWorkflowWebSourceSnippetWithCache(options) {
491
496
  }
492
497
  function snippetForTerms(options) {
493
498
  const needles = options.terms
494
- .map((term) => ({ raw: term, normalized: normalizeForSearch(term).normalized }))
499
+ .map((term) => ({
500
+ raw: term,
501
+ normalized: normalizeForSearch(term).normalized,
502
+ }))
495
503
  .filter((term) => term.normalized.length > 0);
496
504
  if (needles.length === 0)
497
505
  return { status: "not_found", visibleChars: 0 };
@@ -545,7 +553,8 @@ function scoreTermWindow(text, matchStart, matchEnd, maxChars, terms) {
545
553
  .filter((term) => !windowNorm.includes(term.normalized))
546
554
  .map((term) => term.raw);
547
555
  const occurrenceScore = terms.reduce((score, term) => {
548
- return score + (windowNorm.includes(term.normalized) ? term.normalized.length : 0);
556
+ return (score +
557
+ (windowNorm.includes(term.normalized) ? term.normalized.length : 0));
549
558
  }, 0);
550
559
  return {
551
560
  start,
@@ -675,7 +684,8 @@ function nearbySnippet(text, needle, maxChars) {
675
684
  async function readWorkflowWebSourceIndexFile(config) {
676
685
  try {
677
686
  const parsed = JSON.parse(await readFile(indexPath(config), "utf8"));
678
- if (!isRecord(parsed) || parsed.schema !== WORKFLOW_WEB_SOURCE_INDEX_SCHEMA) {
687
+ if (!isRecord(parsed) ||
688
+ parsed.schema !== WORKFLOW_WEB_SOURCE_INDEX_SCHEMA) {
679
689
  throw new Error("invalid index");
680
690
  }
681
691
  const sources = Array.isArray(parsed.sources)
@@ -686,7 +696,9 @@ async function readWorkflowWebSourceIndexFile(config) {
686
696
  : [];
687
697
  return {
688
698
  schema: WORKFLOW_WEB_SOURCE_INDEX_SCHEMA,
689
- updatedAt: typeof parsed.updatedAt === "string" ? parsed.updatedAt : new Date().toISOString(),
699
+ updatedAt: typeof parsed.updatedAt === "string"
700
+ ? parsed.updatedAt
701
+ : new Date().toISOString(),
690
702
  runId: typeof parsed.runId === "string" ? parsed.runId : config.runId,
691
703
  sources: mergeSourceIndexEntries(sources),
692
704
  };
@@ -719,7 +731,8 @@ async function readWorkflowWebSourceIndexLedger(config) {
719
731
  continue;
720
732
  try {
721
733
  const parsed = JSON.parse(line);
722
- if (!isRecord(parsed) || parsed.schema !== WORKFLOW_WEB_SOURCE_INDEX_EVENT_SCHEMA)
734
+ if (!isRecord(parsed) ||
735
+ parsed.schema !== WORKFLOW_WEB_SOURCE_INDEX_EVENT_SCHEMA)
723
736
  continue;
724
737
  const entry = sourceIndexEntryFromUnknown(parsed.entry);
725
738
  if (entry)
@@ -734,7 +747,8 @@ async function readWorkflowWebSourceIndexLedger(config) {
734
747
  function sourceIndexEntryFromUnknown(value) {
735
748
  if (!isRecord(value))
736
749
  return undefined;
737
- if (typeof value.sourceRef !== "string" || !isWorkflowWebSourceRef(value.sourceRef))
750
+ if (typeof value.sourceRef !== "string" ||
751
+ !isWorkflowWebSourceRef(value.sourceRef))
738
752
  return undefined;
739
753
  if (typeof value.createdAt !== "string")
740
754
  return undefined;
@@ -822,7 +836,8 @@ function nonPublicIpReason(address) {
822
836
  }
823
837
  if (isIP(lower) === 4) {
824
838
  const parts = lower.split(".").map((part) => Number(part));
825
- if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255))
839
+ if (parts.length !== 4 ||
840
+ parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255))
826
841
  return "non_public_ip_blocked";
827
842
  const [a, b, c, d] = parts;
828
843
  if (a === 0 || a === 10 || a === 127 || a >= 224)
@@ -859,7 +874,10 @@ function nonPublicIpReason(address) {
859
874
  return undefined;
860
875
  }
861
876
  function redactRecordForModel(value) {
862
- return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, redactValueForModel(item)]));
877
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [
878
+ key,
879
+ redactValueForModel(item),
880
+ ]));
863
881
  }
864
882
  function redactValueForModel(value) {
865
883
  if (typeof value === "string")
package/docs/usage.md CHANGED
@@ -187,6 +187,17 @@ A run prints a `workflow_*` id. Use that id for follow-up commands:
187
187
 
188
188
  The runtime task is not optional. `/workflow run <workflow>` and `/workflow dynamic` without task text fail before launch.
189
189
 
190
+ ### Opt-in fast mode
191
+
192
+ For lower-latency runs, pass `--thinking low` explicitly:
193
+
194
+ ```text
195
+ /workflow run --thinking low deep-research "Research this repository and summarize the architecture tradeoffs."
196
+ /workflow dynamic --thinking low "Research this repository and summarize the architecture tradeoffs."
197
+ ```
198
+
199
+ This is an opt-in fast mode. Package defaults remain conservative until a separate holdout evaluation provides enough evidence to change them. Current evidence is limited but encouraging for explicit fast runs: the 2026-07-02 `deep-research` combined gate on P1/P2/P3-style prompts resolved non-support tasks to `low`, completed selected valid runs in about 15-17 minutes, passed the strict gate 9/9, and had zero source-ref join failures across those 9 runs. Treat this as a speed option, not proof that every workflow should default to `low`.
200
+
190
201
  ### Run-scoped web-source cache
191
202
 
192
203
  Prefer normalized workflow web tools in new workflows:
@@ -211,7 +222,7 @@ Benchmark note: cache-enabled runs are a distinct cohort from older uncached run
211
222
 
212
223
  | Workflow | Required agents | Mode | Use when |
213
224
  |---|---|---|---|
214
- | `deep-research` | `researcher` | plan + foreach questions + normalize-input packet support + normalize + foreach verifier + audit support + final-audit packet support + full audit reduce + deterministic executive render | Use when you need a grounded answer or summary based on source material. |
225
+ | `deep-research` | `researcher` | plan + foreach questions + normalize-input packet support + normalize + foreach verifier + audit support + final-audit packet support + compact final synthesis reduce + deterministic ledger-backed executive render | Use when you need a grounded answer or summary based on source material. |
215
226
  | `deep-review` | `scout` | triage + foreach review lenses + dedup support + foreach devil's advocate + verdict-partition support + reduce | Use when you want code or design reviewed carefully from multiple angles. |
216
227
  | `spec-review` | `scout` | extract spec + map implementation + inspect tests -> reduce candidates -> foreach verifier -> reduce report | Use when you want to check whether requirements, an API spec, or a contract are reflected in the implementation and tests. |
217
228
  | `impact-review` | `scout` | scope/implementation/validation maps -> impact lenses -> consistency/regression/ship-readiness joins -> final synthesis | Use before merging or releasing a change to check affected areas, risks, missing tests, and missing docs. |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agwab/pi-workflow",
3
- "version": "0.1.2",
3
+ "version": "0.2.1",
4
4
  "description": "Workflow orchestration for Pi subagents.",
5
5
  "private": false,
6
6
  "type": "module",
@@ -43,7 +43,7 @@
43
43
  "scripts": {
44
44
  "build": "rm -rf dist && tsc -p tsconfig.json --outDir dist --noEmit false",
45
45
  "typecheck": "tsc --noEmit",
46
- "check:scripts": "node scripts/check-scripts.mjs",
46
+ "check:scripts": "node tools/release/check-scripts.mjs",
47
47
  "validate": "npm run check:scripts && npm run typecheck && npm run test:unit",
48
48
  "test": "npm run test:unit",
49
49
  "test:build": "rm -rf .tmp/unit && tsc -p tsconfig.json --outDir .tmp/unit --noEmit false",
@@ -51,8 +51,8 @@
51
51
  "e2e": "node test/e2e/run.mjs",
52
52
  "pack:dry": "npm pack --dry-run --json",
53
53
  "prepack": "npm run build",
54
- "release:check": "node scripts/release-check.mjs",
55
- "release:dispatch": "node scripts/dispatch-release.mjs"
54
+ "release:check": "node tools/release/release-check.mjs",
55
+ "release:dispatch": "node tools/release/dispatch-release.mjs"
56
56
  },
57
57
  "pi": {
58
58
  "extensions": [
@@ -70,7 +70,6 @@
70
70
  "@earendil-works/pi-ai": "^0.78.0",
71
71
  "@earendil-works/pi-coding-agent": "*",
72
72
  "@types/node": "^24.0.0",
73
- "typebox": "^1.1.39",
74
73
  "typescript": "^5.0.0"
75
74
  },
76
75
  "engines": {
@@ -78,7 +77,8 @@
78
77
  },
79
78
  "dependencies": {
80
79
  "@agwab/pi-subagent": "^0.3.6",
81
- "pi-web-access": "^0.10.7"
80
+ "pi-web-access": "^0.10.7",
81
+ "typebox": "^1.1.39"
82
82
  },
83
83
  "publishConfig": {
84
84
  "access": "public"