@agwab/pi-workflow 0.2.1 → 0.4.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 (119) hide show
  1. package/README.md +3 -1
  2. package/dist/artifact-graph-runtime.d.ts +1 -1
  3. package/dist/artifact-graph-runtime.js +10 -5
  4. package/dist/artifact-graph-schema.js +127 -5
  5. package/dist/compiler.js +52 -19
  6. package/dist/dynamic-generated-task-runtime.js +3 -1
  7. package/dist/dynamic-profiles.d.ts +1 -1
  8. package/dist/engine-run-graph.d.ts +3 -0
  9. package/dist/engine-run-graph.js +194 -4
  10. package/dist/engine.d.ts +5 -0
  11. package/dist/engine.js +389 -41
  12. package/dist/extension.d.ts +2 -1
  13. package/dist/extension.js +30 -8
  14. package/dist/index.d.ts +11 -3
  15. package/dist/index.js +6 -1
  16. package/dist/prompt-json.d.ts +7 -0
  17. package/dist/prompt-json.js +13 -0
  18. package/dist/roles.d.ts +1 -1
  19. package/dist/roles.js +5 -8
  20. package/dist/store.d.ts +20 -1
  21. package/dist/store.js +139 -35
  22. package/dist/strings.d.ts +11 -0
  23. package/dist/strings.js +24 -0
  24. package/dist/subagent-backend.js +710 -40
  25. package/dist/types.d.ts +107 -1
  26. package/dist/verification-ontology.d.ts +31 -0
  27. package/dist/verification-ontology.js +66 -0
  28. package/dist/workflow-artifact-tool.js +5 -6
  29. package/dist/workflow-artifacts.d.ts +7 -0
  30. package/dist/workflow-artifacts.js +55 -4
  31. package/dist/workflow-fetch-cache-extension.d.ts +1 -0
  32. package/dist/workflow-fetch-cache-extension.js +57 -9
  33. package/dist/workflow-metrics.d.ts +113 -0
  34. package/dist/workflow-metrics.js +272 -0
  35. package/dist/workflow-output-artifacts.js +5 -3
  36. package/dist/workflow-partial-output.d.ts +45 -0
  37. package/dist/workflow-partial-output.js +205 -0
  38. package/dist/workflow-progress-health.js +42 -10
  39. package/dist/workflow-runtime.js +10 -1
  40. package/dist/workflow-view.js +3 -1
  41. package/dist/workflow-web-source-extension.js +194 -52
  42. package/dist/workflow-web-source.d.ts +2 -1
  43. package/dist/workflow-web-source.js +109 -30
  44. package/docs/usage.md +76 -29
  45. package/node_modules/@agwab/pi-subagent/README.md +3 -3
  46. package/node_modules/@agwab/pi-subagent/api.mjs +1 -0
  47. package/node_modules/@agwab/pi-subagent/docs/usage.md +63 -12
  48. package/node_modules/@agwab/pi-subagent/package.json +2 -2
  49. package/node_modules/@agwab/pi-subagent/src/api.ts +54 -1
  50. package/node_modules/@agwab/pi-subagent/src/artifacts/registry.ts +9 -4
  51. package/node_modules/@agwab/pi-subagent/src/artifacts/result.ts +8 -0
  52. package/node_modules/@agwab/pi-subagent/src/core/constants.ts +9 -0
  53. package/node_modules/@agwab/pi-subagent/src/core/validation.ts +21 -0
  54. package/node_modules/@agwab/pi-subagent/src/index.ts +1046 -576
  55. package/node_modules/@agwab/pi-subagent/src/orchestrate/async.ts +279 -156
  56. package/node_modules/@agwab/pi-subagent/src/orchestrate/interrupt.ts +165 -89
  57. package/node_modules/@agwab/pi-subagent/src/orchestrate/reconcile.ts +111 -65
  58. package/node_modules/@agwab/pi-subagent/src/orchestrate/run-ref.ts +219 -0
  59. package/node_modules/@agwab/pi-subagent/src/orchestrate/run.ts +88 -8
  60. package/node_modules/@agwab/pi-subagent/src/orchestrate/status.ts +614 -298
  61. package/node_modules/@agwab/pi-subagent/src/panel.ts +1356 -560
  62. package/node_modules/@agwab/pi-subagent/src/runners/headless-model.ts +53 -5
  63. package/node_modules/@agwab/pi-subagent/src/runners/tmux.ts +13 -6
  64. package/package.json +2 -2
  65. package/skills/workflow-guide/SKILL.md +1 -0
  66. package/src/artifact-graph-runtime.ts +19 -13
  67. package/src/artifact-graph-schema.ts +143 -3
  68. package/src/cli.mjs +52 -0
  69. package/src/compiler.ts +63 -18
  70. package/src/dynamic-generated-task-runtime.ts +3 -1
  71. package/src/dynamic-profiles.ts +1 -1
  72. package/src/engine-run-graph.ts +246 -4
  73. package/src/engine.ts +545 -38
  74. package/src/extension.ts +36 -6
  75. package/src/index.ts +52 -1
  76. package/src/prompt-json.ts +13 -0
  77. package/src/roles.ts +6 -9
  78. package/src/store.ts +194 -42
  79. package/src/strings.ts +38 -0
  80. package/src/subagent-backend.ts +921 -62
  81. package/src/types.ts +116 -2
  82. package/src/verification-ontology.ts +88 -0
  83. package/src/workflow-artifact-tool.ts +5 -7
  84. package/src/workflow-artifacts.ts +83 -3
  85. package/src/workflow-fetch-cache-extension.ts +78 -13
  86. package/src/workflow-metrics.ts +478 -0
  87. package/src/workflow-output-artifacts.ts +5 -3
  88. package/src/workflow-partial-output.ts +299 -0
  89. package/src/workflow-progress-health.ts +47 -15
  90. package/src/workflow-runtime.ts +18 -2
  91. package/src/workflow-view.ts +2 -1
  92. package/src/workflow-web-source-extension.ts +654 -232
  93. package/src/workflow-web-source.ts +153 -39
  94. package/workflows/README.md +7 -25
  95. package/workflows/deep-research/batched-verification.spec.json +253 -0
  96. package/workflows/deep-research/helpers/batch-verification-candidates.mjs +136 -0
  97. package/workflows/deep-research/helpers/claim-evidence-gate.mjs +229 -36
  98. package/workflows/deep-research/helpers/final-audit-packet.mjs +1 -4
  99. package/workflows/deep-research/helpers/normalize-input-packet.mjs +81 -2
  100. package/workflows/deep-research/helpers/render-executive.mjs +40 -26
  101. package/workflows/deep-research/helpers/sanitize-verification-candidates.mjs +89 -15
  102. package/workflows/deep-research/helpers/shadow-select-verification.mjs +229 -0
  103. package/workflows/deep-research/helpers/verification-ontology.mjs +77 -0
  104. package/workflows/deep-research/schemas/deep-research-executive-render-control.schema.json +3 -3
  105. package/workflows/deep-research/schemas/deep-research-research-questions-control.schema.json +38 -0
  106. package/workflows/deep-research/schemas/deep-research-sanitize-claims-control.schema.json +63 -0
  107. package/workflows/deep-research/schemas/deep-research-verify-claims-batch-control.schema.json +47 -0
  108. package/workflows/deep-research/schemas/deep-research-verify-claims-control.schema.json +13 -3
  109. package/workflows/deep-research/spec.json +32 -12
  110. package/workflows/impact-review/spec.json +3 -3
  111. package/workflows/spec-review/helpers/spec-review-pipeline.mjs +1 -8
  112. package/dist/dynamic-loader.d.ts +0 -25
  113. package/dist/dynamic-loader.js +0 -13
  114. package/skills/workflow-guide/scaffolds/dag-required-reads/spec.json.validate.stderr +0 -0
  115. package/skills/workflow-guide/scaffolds/dag-required-reads/spec.json.validate.stdout +0 -13
  116. package/src/dynamic-loader.ts +0 -49
  117. package/workflows/impact-review/schemas/docs-release-impact-control.schema.json +0 -42
  118. package/workflows/impact-review/schemas/security-performance-impact-control.schema.json +0 -42
  119. package/workflows/impact-review/schemas/state-data-impact-control.schema.json +0 -42
@@ -79,6 +79,13 @@ export interface PiJsonParseResult {
79
79
  metadata: Partial<ResultMetadata>;
80
80
  }
81
81
 
82
+ export interface ContextLengthResolution {
83
+ rawContextLengthExceeded: boolean;
84
+ contextLengthExceeded: boolean;
85
+ contextOverflowRecovered: boolean;
86
+ recoveredStreamErrors: string[];
87
+ }
88
+
82
89
  const CONTEXT_LENGTH_ERROR_PATTERN =
83
90
  /\bcontext[_ -]?length[_ -]?exceeded\b|\bcontext[_ -]?window[_ -]?(?:exceeded|overflow|exhausted)\b|\b(?:maximum|max)[_ -]?context[_ -]?length\b|\btoo many tokens\b|\b(?:prompt|input|request)[^\n]{0,80}\btoo large\b|\bcontext_length_exceeded\b/i;
84
91
 
@@ -94,6 +101,32 @@ export function detectContextLengthExceeded(signals: {
94
101
  return CONTEXT_LENGTH_ERROR_PATTERN.test(text);
95
102
  }
96
103
 
104
+ export function resolveContextLengthState(
105
+ parsed: PiJsonParseResult,
106
+ rawContextLengthExceeded: boolean,
107
+ ): ContextLengthResolution {
108
+ const contextOverflowRecovered =
109
+ rawContextLengthExceeded && finalAssistantSucceeded(parsed);
110
+ return {
111
+ rawContextLengthExceeded,
112
+ contextLengthExceeded:
113
+ rawContextLengthExceeded && !contextOverflowRecovered,
114
+ contextOverflowRecovered,
115
+ recoveredStreamErrors: contextOverflowRecovered
116
+ ? parsed.errors.filter((error) =>
117
+ detectContextLengthExceeded({ errors: [error] }),
118
+ )
119
+ : [],
120
+ };
121
+ }
122
+
123
+ function finalAssistantSucceeded(parsed: PiJsonParseResult): boolean {
124
+ return (
125
+ parsed.finalAssistantText.length > 0 &&
126
+ parsed.metadata.stopReason !== "error"
127
+ );
128
+ }
129
+
97
130
  function normalizeTimeoutMs(timeoutMs: number | undefined): number | undefined {
98
131
  if (timeoutMs === undefined) return undefined;
99
132
  if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
@@ -390,18 +423,29 @@ export function resolvePiJsonOutcome(
390
423
 
391
424
  export function resultMetadataFromParse(
392
425
  parsed: PiJsonParseResult,
393
- contextLengthExceeded: boolean,
426
+ contextLength: ContextLengthResolution,
394
427
  outcome: ProcessOutcome,
395
428
  ): Partial<ResultMetadata> {
396
429
  return {
397
430
  ...parsed.metadata,
398
- contextLengthExceeded,
431
+ contextLengthExceeded: contextLength.contextLengthExceeded,
432
+ ...(contextLength.contextOverflowRecovered
433
+ ? { contextOverflowRecovered: true }
434
+ : {}),
399
435
  ...(parsed.errors.length === 0
400
436
  ? {}
401
437
  : { streamErrors: parsed.errors.slice(0, MAX_METADATA_ERRORS) }),
402
438
  ...(outcome.status === "completed" && parsed.errors.length > 0
403
439
  ? { nonFatalStreamErrors: parsed.errors.slice(0, MAX_METADATA_ERRORS) }
404
440
  : {}),
441
+ ...(contextLength.recoveredStreamErrors.length === 0
442
+ ? {}
443
+ : {
444
+ recoveredStreamErrors: contextLength.recoveredStreamErrors.slice(
445
+ 0,
446
+ MAX_METADATA_ERRORS,
447
+ ),
448
+ }),
405
449
  ...(parsed.parseErrors.length === 0
406
450
  ? {}
407
451
  : { parseErrors: parsed.parseErrors.slice(0, MAX_METADATA_ERRORS) }),
@@ -774,14 +818,18 @@ export async function runHeadlessModel(
774
818
  stderrText,
775
819
  stderrContextLengthExceeded,
776
820
  } = processResult;
777
- const contextLengthExceeded =
821
+ const rawContextLengthExceeded =
778
822
  stderrContextLengthExceeded ||
779
823
  detectContextLengthExceeded({ stderrText, errors: parsed.errors });
824
+ const contextLength = resolveContextLengthState(
825
+ parsed,
826
+ rawContextLengthExceeded,
827
+ );
780
828
 
781
829
  const outcome = resolvePiJsonOutcome(
782
830
  processOutcome,
783
831
  parsed,
784
- contextLengthExceeded,
832
+ contextLength.contextLengthExceeded,
785
833
  );
786
834
 
787
835
  const completedAt = new Date();
@@ -811,7 +859,7 @@ export async function runHeadlessModel(
811
859
  artifacts,
812
860
  correlationId: options.correlationId,
813
861
  metadata: {
814
- ...resultMetadataFromParse(parsed, contextLengthExceeded, outcome),
862
+ ...resultMetadataFromParse(parsed, contextLength, outcome),
815
863
  ...sessionMetadata,
816
864
  ...(options.parentSessionId === undefined
817
865
  ? {}
@@ -20,6 +20,7 @@ import {
20
20
  detectContextLengthExceeded,
21
21
  parsePiJsonFile,
22
22
  parsePiJsonLines,
23
+ resolveContextLengthState,
23
24
  resolvePiJsonOutcome,
24
25
  resultMetadataFromParse,
25
26
  resultSessionMetadata,
@@ -157,9 +158,7 @@ function workerScript(
157
158
  return `import { spawn } from "node:child_process";\nimport { appendFileSync, closeSync, openSync, writeFileSync } from "node:fs";\nconst argv = ${JSON.stringify(argv)};\nconst cwd = ${JSON.stringify(cwd)};\nconst eventPath = ${JSON.stringify(eventPath)};\nconst stderrPath = ${JSON.stringify(stderrPath)};\nconst metaPath = ${JSON.stringify(metaPath)};\nconst messageUpdatePattern = /"type"\\s*:\\s*"message_update"/;\nconst maxStdoutLogLineChars = 64 * 1024 * 1024;\ncloseSync(openSync(eventPath, "w"));\ncloseSync(openSync(stderrPath, "w"));\nlet settled = false;\nlet stdoutBuffer = "";\nlet discardingOversizedLine = false;\nlet omittedMessageUpdates = 0;\nlet omittedMessageUpdateBytes = 0;\nlet omittedOversizedLines = 0;\nlet omittedOversizedBytes = 0;\nfunction writeStdoutLine(line) {\n if (messageUpdatePattern.test(line)) {\n omittedMessageUpdates += 1;\n omittedMessageUpdateBytes += Buffer.byteLength(line, "utf8");\n return;\n }\n appendFileSync(eventPath, line);\n process.stdout.write(line);\n}\nfunction handleStdoutChunk(chunk) {\n let text = chunk.toString("utf8");\n while (text.length > 0) {\n if (discardingOversizedLine) {\n const newline = text.indexOf("\\n");\n omittedOversizedBytes += Buffer.byteLength(newline < 0 ? text : text.slice(0, newline + 1), "utf8");\n if (newline < 0) return;\n discardingOversizedLine = false;\n text = text.slice(newline + 1);\n continue;\n }\n const newline = text.indexOf("\\n");\n const segment = newline < 0 ? text : text.slice(0, newline + 1);\n stdoutBuffer += segment;\n text = newline < 0 ? "" : text.slice(newline + 1);\n if (stdoutBuffer.length > maxStdoutLogLineChars) {\n omittedOversizedLines += 1;\n omittedOversizedBytes += Buffer.byteLength(stdoutBuffer, "utf8");\n stdoutBuffer = "";\n discardingOversizedLine = newline < 0;\n continue;\n }\n if (newline >= 0) {\n writeStdoutLine(stdoutBuffer);\n stdoutBuffer = "";\n }\n }\n}\nfunction finishStdoutFilter() {\n if (!discardingOversizedLine && stdoutBuffer.length > 0) writeStdoutLine(stdoutBuffer);\n stdoutBuffer = "";\n if (omittedMessageUpdates > 0 || omittedOversizedLines > 0) {\n appendFileSync(eventPath, JSON.stringify({ type: "pi-subagent.stdout_filter", omitted: { messageUpdateEvents: omittedMessageUpdates, messageUpdateBytes: omittedMessageUpdateBytes, oversizedLines: omittedOversizedLines, oversizedBytes: omittedOversizedBytes }, reason: "cumulative message_update snapshots are omitted from durable stdout artifacts; final assistant text is stored in output.log" }) + "\\n");\n }\n}\nfunction writeMeta(meta) {\n if (settled) return;\n settled = true;\n finishStdoutFilter();\n writeFileSync(metaPath, JSON.stringify(meta, null, 2) + "\\n");\n}\nconst env = { ...process.env };\ndelete env.TMUX;\nconst child = spawn(argv[0], argv.slice(1), { cwd, shell: false, stdio: ["ignore", "pipe", "pipe"], env });\nchild.stdout?.on("data", handleStdoutChunk);\nchild.stderr?.on("data", (chunk) => { appendFileSync(stderrPath, chunk); process.stderr.write(chunk); });\nchild.on("error", () => { writeMeta({ status: "failed", failureKind: "spawn", exitCode: null, signal: null }); });\nchild.on("close", (exitCode, signal) => {\n const failureKind = exitCode === 0 ? null : "exit";\n writeMeta({ status: failureKind === null ? "completed" : "failed", failureKind, exitCode, signal });\n});\n`;
158
159
  }
159
160
 
160
- async function runTmuxProcess(
161
- options: RunTmuxProcessOptions,
162
- ): Promise<{
161
+ async function runTmuxProcess(options: RunTmuxProcessOptions): Promise<{
163
162
  result: TmuxRunResult | null;
164
163
  store: Awaited<ReturnType<typeof createAttemptArtifactStore>>;
165
164
  cwd: string;
@@ -439,11 +438,19 @@ export async function runTmuxModel(
439
438
  parsePiJsonLines(""),
440
439
  );
441
440
  await unlink(result.eventPath).catch(() => undefined);
442
- const contextLengthExceeded = detectContextLengthExceeded({
441
+ const rawContextLengthExceeded = detectContextLengthExceeded({
443
442
  stderrText,
444
443
  errors: parsed.errors,
445
444
  });
446
- const meta = resolvePiJsonOutcome(result.meta, parsed, contextLengthExceeded);
445
+ const contextLength = resolveContextLengthState(
446
+ parsed,
447
+ rawContextLengthExceeded,
448
+ );
449
+ const meta = resolvePiJsonOutcome(
450
+ result.meta,
451
+ parsed,
452
+ contextLength.contextLengthExceeded,
453
+ );
447
454
 
448
455
  const outputRef = await store.writeTextArtifact(
449
456
  "output",
@@ -464,7 +471,7 @@ export async function runTmuxModel(
464
471
  tmux: result.tmux,
465
472
  correlationId: options.correlationId,
466
473
  metadata: {
467
- ...resultMetadataFromParse(parsed, contextLengthExceeded, meta),
474
+ ...resultMetadataFromParse(parsed, contextLength, meta),
468
475
  ...sessionMetadata,
469
476
  ...(options.parentSessionId === undefined
470
477
  ? {}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agwab/pi-workflow",
3
- "version": "0.2.1",
3
+ "version": "0.4.0",
4
4
  "description": "Workflow orchestration for Pi subagents.",
5
5
  "private": false,
6
6
  "type": "module",
@@ -76,7 +76,7 @@
76
76
  "node": ">=22.19.0"
77
77
  },
78
78
  "dependencies": {
79
- "@agwab/pi-subagent": "^0.3.6",
79
+ "@agwab/pi-subagent": "^0.4.2",
80
80
  "pi-web-access": "^0.10.7",
81
81
  "typebox": "^1.1.39"
82
82
  },
@@ -29,6 +29,7 @@ Resolve paths relative to this skill directory. Treat those docs as the source o
29
29
  - For deterministic local post-processing, declare a `support` object with `support.uses` pointing to a bundle-local `./*.mjs` helper; support is trusted local code, not sandboxed subagent work and does not use a separate `type` value.
30
30
  - For bounded iteration, use `loop` with fixed child stages, `maxRounds`, and deterministic `until`.
31
31
  - Agent-declared tools are the authority ceiling; workflow `tools` can only narrow them.
32
+ - To reuse agent knowledge across stages, declare top-level `roles` (`fromAgent` extracts safe agent sections; `prompt` appends literal text). Compiled role text is injected as a `# Role Context` block; check the result with `/workflow roles <workflow>`. See "Roles" in `docs/usage.md`.
32
33
  - Keep review/research workflows read-only unless the workflow explicitly documents managed-worktree mutation.
33
34
  - Write-capable workflows need explicit worktree policy, validation/check stages, and protected-path awareness.
34
35
  - In non-git workspaces with `worktreePolicy: "off"`, writes mutate the live directory.
@@ -2,6 +2,8 @@ import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
2
2
  import { dirname, extname, join } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
 
5
+ import { stringifyPromptJson } from "./prompt-json.js";
6
+ import { compactStrings } from "./strings.js";
5
7
  import { loadWorkflowHelper } from "./workflow-helpers.js";
6
8
  import {
7
9
  WORKFLOW_ARTIFACT_TOOL_NAME,
@@ -13,7 +15,7 @@ import {
13
15
  type WorkflowSourceManifestSource,
14
16
  } from "./workflow-artifact-tool.js";
15
17
  import { writeWorkflowTaskArtifactBundle } from "./workflow-output-artifacts.js";
16
- import { type JsonSchema } from "./json-schema.js";
18
+ import type { JsonSchema } from "./json-schema.js";
17
19
  import {
18
20
  buildRunSourceContext,
19
21
  readOutputText,
@@ -29,11 +31,11 @@ import {
29
31
  writeJsonAtomic,
30
32
  writeRunRecord,
31
33
  } from "./store.js";
32
- import {
33
- type CompiledTask,
34
- type CompiledWorkflow,
35
- type WorkflowRunRecord,
36
- type WorkflowTaskRunRecord,
34
+ import type {
35
+ CompiledTask,
36
+ CompiledWorkflow,
37
+ WorkflowRunRecord,
38
+ WorkflowTaskRunRecord,
37
39
  } from "./types.js";
38
40
 
39
41
  export async function executeSupportTask(
@@ -372,7 +374,9 @@ export function normalizeDynamicControllerOutput(value: unknown): {
372
374
  refs: [],
373
375
  };
374
376
  }
375
- export function normalizeSupportControl(value: unknown): Record<string, unknown> {
377
+ export function normalizeSupportControl(
378
+ value: unknown,
379
+ ): Record<string, unknown> {
376
380
  if (value && typeof value === "object" && !Array.isArray(value)) {
377
381
  const record = value as Record<string, unknown>;
378
382
  return {
@@ -474,7 +478,7 @@ export async function prepareDagTask(
474
478
  compiledTask.compiledPrompt,
475
479
  "# Source Stage Context",
476
480
  "Use this deterministic source context packet. Prefer structuredOutput over outputPreview. Do not assume dependencies beyond this explicit packet.",
477
- JSON.stringify({ ...context, missingDependencies: missing }, null, 2),
481
+ stringifyPromptJson({ ...context, missingDependencies: missing }),
478
482
  ].join("\n\n"),
479
483
  };
480
484
  }
@@ -486,6 +490,10 @@ async function prepareArtifactGraphTask(
486
490
  task: WorkflowTaskRunRecord,
487
491
  contextDependsOn: readonly string[],
488
492
  ): Promise<CompiledTask> {
493
+ if (compiledTask.artifactGraph?.artifactAccess === "none") {
494
+ return { ...compiledTask, cwd: task.cwd };
495
+ }
496
+
489
497
  const taskDir = dirname(fromProjectPath(cwd, task.files.result));
490
498
  const manifestPath = join(taskDir, "source-manifest.json");
491
499
  const ledgerPath = join(taskDir, "read-ledger.jsonl");
@@ -880,7 +888,7 @@ export function formatArtifactGraphSourceContext(
880
888
  return [
881
889
  "# Workflow Artifact Inputs",
882
890
  "Use workflow_artifact to list/read upstream workflow artifacts. Inline controlProjection fields are authoritative for the projected data they contain; use artifact reads for declared requiredReads, missing fields, or debug detail.",
883
- "Projected reads must include a JSON path when using maxItems or maxChars, for example {\"action\":\"read\",\"source\":\"plan\",\"artifact\":\"control\",\"path\":\"$.factSlots\",\"maxItems\":8,\"maxChars\":2000}. For a whole artifact read, omit maxItems/maxChars.",
891
+ 'Projected reads must include a JSON path when using maxItems or maxChars, for example {"action":"read","source":"plan","artifact":"control","path":"$.factSlots","maxItems":8,"maxChars":2000}. For a whole artifact read, omit maxItems/maxChars.',
884
892
  requiredReads.length > 0
885
893
  ? [
886
894
  "Required reads before final output:",
@@ -888,7 +896,7 @@ export function formatArtifactGraphSourceContext(
888
896
  ].join("\n")
889
897
  : "No hard requiredReads are declared for this stage.",
890
898
  "Available sources:",
891
- JSON.stringify(
899
+ stringifyPromptJson(
892
900
  sources.map((source) => ({
893
901
  source: source.source,
894
902
  taskId: source.taskId,
@@ -904,13 +912,11 @@ export function formatArtifactGraphSourceContext(
904
912
  projectionTruncated: source.projectionTruncated,
905
913
  availableArtifacts: Object.keys(source.artifacts),
906
914
  })),
907
- null,
908
- 2,
909
915
  ),
910
916
  ].join("\n\n");
911
917
  }
912
918
  function uniqueStrings(values: readonly string[]): string[] {
913
- return [...new Set(values.filter((value) => value.trim().length > 0))];
919
+ return compactStrings(values, { trim: false, dropWhitespaceOnly: true });
914
920
  }
915
921
 
916
922
  export async function readArtifactGraphControl(
@@ -1,6 +1,7 @@
1
1
  import { isAbsolute } from "node:path";
2
2
 
3
3
  import { DYNAMIC_OUTPUT_PROFILES } from "./dynamic-profiles.js";
4
+ import { compactStrings } from "./strings.js";
4
5
  import {
5
6
  APPROVAL_MODES,
6
7
  FAST_MODES,
@@ -71,10 +72,16 @@ const OUTPUT_KEYS = new Set([
71
72
  "analysis",
72
73
  "refs",
73
74
  "maxDigestChars",
75
+ "partial",
74
76
  ]);
77
+ const OUTPUT_PARTIAL_KEYS = new Set(["paths"]);
75
78
  const REQUIRED_FLAG_KEYS = new Set(["required"]);
76
79
  const REFS_OUTPUT_KEYS = new Set(["required", "minItems"]);
77
- const INPUT_POLICY_KEYS = new Set(["requiredReads", "enforcement"]);
80
+ const INPUT_POLICY_KEYS = new Set([
81
+ "requiredReads",
82
+ "enforcement",
83
+ "artifactAccess",
84
+ ]);
78
85
  const SOURCE_PROJECTION_KEYS = new Set(["include", "maxChars"]);
79
86
  const SUPPORT_KEYS = new Set(["uses", "options"]);
80
87
  const DYNAMIC_STAGE_FORBIDDEN_KEYS = new Set([
@@ -185,7 +192,8 @@ const UNTIL_KEYS = new Set([
185
192
  "all",
186
193
  "any",
187
194
  ]);
188
- const FOREACH_FROM_KEYS = new Set(["source", "path"]);
195
+ const FOREACH_FROM_KEYS = new Set(["source", "path", "streaming"]);
196
+ const FOREACH_STREAMING_KEYS = new Set(["enabled", "minChunk"]);
189
197
  const NORMAL_ARTIFACT_KINDS = new Set<WorkflowArtifactKind>([
190
198
  "control",
191
199
  "analysis",
@@ -296,9 +304,54 @@ function validateStageArray(
296
304
  for (const [index, item] of value.entries()) {
297
305
  validateStage(item, `${path}[${index}]`, ids, sourceIds, issues);
298
306
  }
307
+ validateStreamingProducerDeclarations(value, path, issues);
299
308
  validateStageDependencyGraph(value, path, issues);
300
309
  }
301
310
 
311
+ function validateStreamingProducerDeclarations(
312
+ stages: readonly unknown[],
313
+ path: string,
314
+ issues: ValidationIssue[],
315
+ ): void {
316
+ const byId = new Map<string, Record<string, unknown>>();
317
+ for (const stage of stages) {
318
+ if (isRecord(stage) && typeof stage.id === "string") {
319
+ byId.set(stage.id, stage);
320
+ }
321
+ }
322
+ for (const [index, stage] of stages.entries()) {
323
+ if (!isRecord(stage) || !isRecord(stage.from)) continue;
324
+ const from = stage.from;
325
+ if (!isRecord(from.streaming) || from.streaming.enabled !== true) continue;
326
+ const source = typeof from.source === "string" ? from.source : undefined;
327
+ const controlPath = typeof from.path === "string" ? from.path : undefined;
328
+ if (!source || !controlPath) continue;
329
+ const sourceStage = byId.get(source);
330
+ const partialPaths = outputPartialPaths(sourceStage);
331
+ if (!partialPaths.includes(controlPath)) {
332
+ issues.push({
333
+ path: `${path}[${index}].from.streaming`,
334
+ message: `source stage "${source}" must declare output.partial.paths including "${controlPath}" to use streaming`,
335
+ });
336
+ }
337
+ }
338
+ }
339
+
340
+ function outputPartialPaths(
341
+ stage: Record<string, unknown> | undefined,
342
+ ): string[] {
343
+ const output = isRecord(stage?.output) ? stage.output : undefined;
344
+ const partial = isRecord(output?.partial) ? output.partial : undefined;
345
+ return Array.isArray(partial?.paths)
346
+ ? compactStrings(partial.paths, {
347
+ trim: false,
348
+ unique: false,
349
+ dropEmpty: false,
350
+ dropWhitespaceOnly: false,
351
+ })
352
+ : [];
353
+ }
354
+
302
355
  function validateStageDependencyGraph(
303
356
  stages: readonly unknown[],
304
357
  path: string,
@@ -372,7 +425,12 @@ function extractDependencyRefs(value: unknown): string[] {
372
425
  if (value === undefined) return [];
373
426
  if (typeof value === "string") return [value];
374
427
  if (Array.isArray(value))
375
- return value.filter((item): item is string => typeof item === "string");
428
+ return compactStrings(value, {
429
+ trim: false,
430
+ unique: false,
431
+ dropEmpty: false,
432
+ dropWhitespaceOnly: false,
433
+ });
376
434
  if (isRecord(value) && typeof value.source === "string")
377
435
  return [value.source];
378
436
  return [];
@@ -517,6 +575,7 @@ function validateStage(
517
575
  `${path}.inputPolicy`,
518
576
  sourceIds,
519
577
  issues,
578
+ stage.sourceProjection,
520
579
  );
521
580
  validateOutput(stage.output, `${path}.output`, issues);
522
581
  validateSupportStage(stage, type, path, issues);
@@ -611,6 +670,23 @@ function validateControlPathRef(
611
670
  message: "must be a control JSONPath starting with $.",
612
671
  });
613
672
  }
673
+ if (value.streaming !== undefined) {
674
+ validateForeachStreaming(value.streaming, `${path}.streaming`, issues);
675
+ }
676
+ }
677
+
678
+ function validateForeachStreaming(
679
+ value: unknown,
680
+ path: string,
681
+ issues: ValidationIssue[],
682
+ ): void {
683
+ const streaming = recordAt(value, path, issues);
684
+ if (!streaming) return;
685
+ rejectUnknownKeys(streaming, FOREACH_STREAMING_KEYS, path, issues);
686
+ if (streaming.enabled !== true) {
687
+ issues.push({ path: `${path}.enabled`, message: "must be true" });
688
+ }
689
+ optionalPositiveInteger(streaming.minChunk, `${path}.minChunk`, issues);
614
690
  }
615
691
 
616
692
  function validateKnownStageRef(
@@ -649,6 +725,42 @@ function validateOutput(
649
725
  );
650
726
  validateRequiredFlagObject(output.analysis, `${path}.analysis`, issues);
651
727
  validateRefsOutputObject(output.refs, `${path}.refs`, issues);
728
+ validatePartialOutput(output.partial, `${path}.partial`, issues);
729
+ }
730
+
731
+ function validatePartialOutput(
732
+ value: unknown,
733
+ path: string,
734
+ issues: ValidationIssue[],
735
+ ): void {
736
+ if (value === undefined) return;
737
+ const partial = recordAt(value, path, issues);
738
+ if (!partial) return;
739
+ rejectUnknownKeys(partial, OUTPUT_PARTIAL_KEYS, path, issues);
740
+ if (!Array.isArray(partial.paths)) {
741
+ issues.push({ path: `${path}.paths`, message: "must be an array" });
742
+ return;
743
+ }
744
+ if (partial.paths.length === 0) {
745
+ issues.push({ path: `${path}.paths`, message: "must not be empty" });
746
+ }
747
+ const seen = new Set<string>();
748
+ for (const [index, item] of partial.paths.entries()) {
749
+ const itemPath = `${path}.paths[${index}]`;
750
+ if (typeof item !== "string" || item.trim() === "") {
751
+ issues.push({ path: itemPath, message: "must be a non-empty string" });
752
+ continue;
753
+ }
754
+ if (!item.startsWith("$.")) {
755
+ issues.push({
756
+ path: itemPath,
757
+ message: "must be a control JSONPath starting with $.",
758
+ });
759
+ }
760
+ if (seen.has(item))
761
+ issues.push({ path: itemPath, message: `duplicate value "${item}"` });
762
+ seen.add(item);
763
+ }
652
764
  }
653
765
 
654
766
  function validateControlSchemaRef(
@@ -694,6 +806,7 @@ function validateInputPolicy(
694
806
  path: string,
695
807
  siblingIds: ReadonlySet<string>,
696
808
  issues: ValidationIssue[],
809
+ sourceProjection: unknown,
697
810
  ): void {
698
811
  if (value === undefined) return;
699
812
  const policy = recordAt(value, path, issues);
@@ -708,6 +821,33 @@ function validateInputPolicy(
708
821
  if (policy.enforcement !== undefined && policy.enforcement !== "fail") {
709
822
  issues.push({ path: `${path}.enforcement`, message: 'must be "fail"' });
710
823
  }
824
+ if (
825
+ policy.artifactAccess !== undefined &&
826
+ policy.artifactAccess !== "enabled" &&
827
+ policy.artifactAccess !== "none"
828
+ ) {
829
+ issues.push({
830
+ path: `${path}.artifactAccess`,
831
+ message: 'must be "enabled" or "none"',
832
+ });
833
+ }
834
+ if (policy.artifactAccess === "none") {
835
+ if (
836
+ Array.isArray(policy.requiredReads) &&
837
+ policy.requiredReads.length > 0
838
+ ) {
839
+ issues.push({
840
+ path: `${path}.requiredReads`,
841
+ message: 'must be empty when artifactAccess is "none"',
842
+ });
843
+ }
844
+ if (sourceProjection !== undefined) {
845
+ issues.push({
846
+ path: `${path}.artifactAccess`,
847
+ message: 'cannot be "none" when sourceProjection is declared',
848
+ });
849
+ }
850
+ }
711
851
  }
712
852
 
713
853
  function validateRequiredReads(
package/src/cli.mjs CHANGED
@@ -53,6 +53,7 @@ const tasks = Array.isArray(run.tasks) ? run.tasks : [];
53
53
  const selected = options.has("--failures")
54
54
  ? tasks.filter((task) => ["failed", "blocked", "interrupted"].includes(task.status))
55
55
  : tasks;
56
+ const reliability = summarizeReliability(tasks);
56
57
 
57
58
  const lines = [
58
59
  `runId: ${run.runId}`,
@@ -60,6 +61,8 @@ const lines = [
60
61
  `type: ${run.type}`,
61
62
  `status: ${run.status}`,
62
63
  `tasks: ${tasks.length}`,
64
+ `completion: ${reliability.health}`,
65
+ `retries: output=${reliability.outputRetries}, launch=${reliability.launchRetries}, resumes=${reliability.resumeEvents}, contextLimitFailures=${reliability.contextLimitFailures}`,
63
66
  ];
64
67
 
65
68
  for (const task of selected) {
@@ -184,3 +187,52 @@ async function readJson(path) {
184
187
  function indent(text, prefix) {
185
188
  return text.split(/\r?\n/).map((line) => `${prefix}${line}`).join("\n");
186
189
  }
190
+
191
+ function summarizeReliability(tasks) {
192
+ let outputRetries = 0;
193
+ let launchRetries = 0;
194
+ let resumeEvents = 0;
195
+ let contextLimitFailures = 0;
196
+ for (const task of tasks) {
197
+ outputRetries += positiveCount(task.outputRetry?.attempts);
198
+ launchRetries += positiveCount(task.launchRetry?.attempts);
199
+ if (hasContextLimitFailure(task)) contextLimitFailures += 1;
200
+ for (const event of Array.isArray(task.resumeEvents) ? task.resumeEvents : []) {
201
+ resumeEvents += 1;
202
+ outputRetries += positiveCount(event.outputRetryAttempts);
203
+ launchRetries += positiveCount(event.launchRetryAttempts);
204
+ if (hasContextLimitFailure(event)) contextLimitFailures += 1;
205
+ }
206
+ }
207
+ const allCompleted = tasks.length > 0 && tasks.every((task) => task.status === "completed");
208
+ const repairEvents = outputRetries + launchRetries + resumeEvents;
209
+ const health = !allCompleted
210
+ ? "incomplete"
211
+ : repairEvents === 0 && contextLimitFailures === 0
212
+ ? "clean"
213
+ : "repaired";
214
+ return { health, outputRetries, launchRetries, resumeEvents, contextLimitFailures };
215
+ }
216
+
217
+ function positiveCount(value) {
218
+ return Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0;
219
+ }
220
+
221
+ function hasContextLimitFailure(value) {
222
+ return [
223
+ value?.statusDetail,
224
+ value?.fromStatusDetail,
225
+ value?.lastMessage,
226
+ value?.outputRetry?.reason,
227
+ value?.outputRetry?.message,
228
+ value?.launchRetry?.reason,
229
+ value?.launchRetry?.message,
230
+ value?.outputRetryReason,
231
+ value?.launchRetryReason,
232
+ ].some(isContextLimitText);
233
+ }
234
+
235
+ function isContextLimitText(value) {
236
+ const text = String(value ?? "").toLowerCase();
237
+ return text.includes("context_or_request_too_large") || /context (window|length)|maximum context|request too large|token limit/.test(text);
238
+ }