@agwab/pi-workflow 0.1.2 → 0.2.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 (34) hide show
  1. package/README.md +7 -13
  2. package/dist/compiler.d.ts +2 -0
  3. package/dist/compiler.js +27 -2
  4. package/dist/engine.d.ts +2 -0
  5. package/dist/engine.js +3 -2
  6. package/dist/extension.js +201 -16
  7. package/dist/store.js +1 -0
  8. package/dist/types.d.ts +3 -0
  9. package/dist/workflow-progress-health.d.ts +37 -0
  10. package/dist/workflow-progress-health.js +296 -0
  11. package/dist/workflow-runtime.d.ts +6 -0
  12. package/dist/workflow-runtime.js +33 -10
  13. package/dist/workflow-view.d.ts +2 -0
  14. package/dist/workflow-view.js +97 -18
  15. package/dist/workflow-web-source.js +32 -14
  16. package/docs/usage.md +1 -1
  17. package/package.json +6 -6
  18. package/src/compiler.ts +41 -2
  19. package/src/engine.ts +7 -16
  20. package/src/extension.ts +254 -22
  21. package/src/store.ts +1 -0
  22. package/src/types.ts +4 -0
  23. package/src/workflow-progress-health.ts +461 -0
  24. package/src/workflow-runtime.ts +50 -13
  25. package/src/workflow-view.ts +186 -41
  26. package/src/workflow-web-source.ts +192 -69
  27. package/workflows/deep-research/helpers/claim-evidence-gate.mjs +111 -37
  28. package/workflows/deep-research/helpers/final-audit-packet.mjs +191 -14
  29. package/workflows/deep-research/helpers/normalize-input-packet.mjs +159 -50
  30. package/workflows/deep-research/helpers/render-executive.mjs +671 -37
  31. package/workflows/deep-research/helpers/sanitize-verification-candidates.mjs +624 -0
  32. package/workflows/deep-research/schemas/deep-research-executive-render-control.schema.json +2 -0
  33. package/workflows/deep-research/schemas/deep-research-final-synthesis-control.schema.json +110 -0
  34. package/workflows/deep-research/spec.json +41 -11
@@ -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
@@ -211,7 +211,7 @@ Benchmark note: cache-enabled runs are a distinct cohort from older uncached run
211
211
 
212
212
  | Workflow | Required agents | Mode | Use when |
213
213
  |---|---|---|---|
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. |
214
+ | `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
215
  | `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
216
  | `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
217
  | `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.0",
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"
package/src/compiler.ts CHANGED
@@ -31,6 +31,10 @@ import {
31
31
  type WorkflowToolSpec,
32
32
  type WorktreePolicy,
33
33
  } from "./types.js";
34
+ import {
35
+ resolveWorkflowRuntime,
36
+ type WorkflowModelInfo,
37
+ } from "./workflow-runtime.js";
34
38
 
35
39
  const DELEGATION_TOOLS = new Set([
36
40
  "skill_test_subagent",
@@ -57,6 +61,7 @@ const DEFAULT_DYNAMIC_DECISION_LOOP_MAX_STALLS = 3;
57
61
  interface CompileOptions {
58
62
  cwd: string;
59
63
  specPath?: string;
64
+ availableModels?: WorkflowModelInfo[];
60
65
  }
61
66
 
62
67
  interface ArtifactGraphCompilePlanBuildResult {
@@ -728,6 +733,20 @@ async function compileArtifactGraphPlan(
728
733
  defaultThinking,
729
734
  overrides,
730
735
  );
736
+ const resolvedDynamicRuntime = await resolveWorkflowRuntime(
737
+ { model: defaultModel, thinking: defaultThinking },
738
+ {
739
+ taskKey: key,
740
+ stageId: stage.id,
741
+ taskId,
742
+ agent: "dynamic",
743
+ },
744
+ { availableModels: options.availableModels },
745
+ );
746
+ dynamicTask.runtime = {
747
+ ...dynamicTask.runtime,
748
+ ...resolvedDynamicRuntime,
749
+ };
731
750
  if (dynamicToolSelection.tools || dynamicToolSelection.toolProviders) {
732
751
  dynamicTask.runtime = {
733
752
  ...dynamicTask.runtime,
@@ -804,11 +823,31 @@ async function compileArtifactGraphPlan(
804
823
  validateToolSubset(toolSelection.tools, stageAgent, issues, toolPath);
805
824
  validateDelegationBoundary(toolSelection.tools, issues, toolPath);
806
825
  const filteredToolSelection = filterToolSelection(toolSelection);
826
+ // Explicit runtime overrides outrank stage pins; spec defaults fill last.
827
+ const requestedRuntime = {
828
+ model:
829
+ options.runtimeDefaults?.model ?? stage.model ?? spec.defaults?.model,
830
+ thinking:
831
+ options.runtimeDefaults?.thinking ??
832
+ stage.thinking ??
833
+ spec.defaults?.thinking,
834
+ };
835
+ const resolvedRuntime = await resolveWorkflowRuntime(
836
+ requestedRuntime,
837
+ {
838
+ taskKey: key,
839
+ stageId: stage.id,
840
+ taskId,
841
+ agent: stageAgentName,
842
+ },
843
+ {
844
+ availableModels: options.availableModels,
845
+ },
846
+ );
807
847
  const runtime = {
808
848
  approvalMode:
809
849
  stage.approvalMode ?? spec.defaults?.approvalMode ?? "non-interactive",
810
- model: stage.model ?? defaultModel,
811
- thinking: stage.thinking ?? defaultThinking,
850
+ ...resolvedRuntime,
812
851
  tools: filteredToolSelection.tools,
813
852
  ...(filteredToolSelection.toolProviders
814
853
  ? { toolProviders: filteredToolSelection.toolProviders }
package/src/engine.ts CHANGED
@@ -1,11 +1,5 @@
1
1
  import { appendFile, mkdir, readFile, writeFile } from "node:fs/promises";
2
- import {
3
- dirname,
4
- extname,
5
- join,
6
- relative,
7
- resolve,
8
- } from "node:path";
2
+ import { dirname, extname, join, relative, resolve } from "node:path";
9
3
  import { fileURLToPath, pathToFileURL } from "node:url";
10
4
  import { Worker } from "node:worker_threads";
11
5
 
@@ -43,7 +37,10 @@ import {
43
37
  workflowBundleFingerprint,
44
38
  workflowBundleSpecPath,
45
39
  } from "./workflow-source-context-runtime.js";
46
- import { readSimpleJsonPath } from "./workflow-runtime.js";
40
+ import {
41
+ readSimpleJsonPath,
42
+ type WorkflowModelInfo,
43
+ } from "./workflow-runtime.js";
47
44
  import {
48
45
  dynamicRunDir,
49
46
  hashDynamicRequest,
@@ -158,6 +155,7 @@ const supervisorTimers = new Map<string, ReturnType<typeof setInterval>>();
158
155
  export interface WorkflowRunOptions {
159
156
  task?: string;
160
157
  runtimeDefaults?: { model?: string; thinking?: ThinkingLevel };
158
+ availableModels?: WorkflowModelInfo[];
161
159
  dynamicUi?: DynamicWorkflowUi;
162
160
  runId?: string;
163
161
  parentRunId?: string;
@@ -210,6 +208,7 @@ async function runLoadedWorkflowSpec(
210
208
  specPath,
211
209
  task: options.task,
212
210
  runtimeDefaults: options.runtimeDefaults,
211
+ availableModels: options.availableModels,
213
212
  });
214
213
 
215
214
  const { run } = await createRunRecord(cwd, compiled, specPath, {
@@ -861,7 +860,6 @@ async function extractArtifactGraphForeachItems(
861
860
  return { items };
862
861
  }
863
862
 
864
-
865
863
  async function launchPendingTaskAt(
866
864
  cwd: string,
867
865
  run: WorkflowRunRecord,
@@ -1801,7 +1799,6 @@ function isDynamicReplayInvariantError(error: unknown): boolean {
1801
1799
  );
1802
1800
  }
1803
1801
 
1804
-
1805
1802
  function requiredDynamicString(
1806
1803
  value: unknown,
1807
1804
  field: string,
@@ -2534,9 +2531,6 @@ async function runDynamicAgentRequest(input: {
2534
2531
  );
2535
2532
  }
2536
2533
 
2537
-
2538
-
2539
-
2540
2534
  interface DynamicControllerOutcome {
2541
2535
  taskStatus: "completed" | "blocked" | "failed";
2542
2536
  statusDetail: string;
@@ -2676,7 +2670,6 @@ function dynamicControllerIssueMessage(
2676
2670
  return `${prefix}: ${first}${suffix}`;
2677
2671
  }
2678
2672
 
2679
-
2680
2673
  function applyExistingLoopWorktree(
2681
2674
  run: WorkflowRunRecord,
2682
2675
  task: WorkflowTaskRunRecord,
@@ -2724,12 +2717,10 @@ function recordCreatedLoopWorktree(
2724
2717
  else run.loopWorktrees[index] = record;
2725
2718
  }
2726
2719
 
2727
-
2728
2720
  function uniqueStrings(values: readonly string[]): string[] {
2729
2721
  return [...new Set(values.filter((value) => value.trim().length > 0))];
2730
2722
  }
2731
2723
 
2732
-
2733
2724
  async function readCompiledWorkflow(
2734
2725
  cwd: string,
2735
2726
  runId: string,